From 0ba54fa60283b34f03ddac061911c84bd342ef89 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> 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 <joegruffins@gmail.com> * 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 <ukanephilemon@gmail.com> * multi: hide decred rescan page if it's not ready - move hasRescan method to WalletBase and implement for decred Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> * cw_decred: fix bug where decred wallets are not loaded after app restart Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> * add buy and sell for decred via onramp Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> * bug-fix: account for other send outputs that are part of the same tx Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> * 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 <ukanephilemon@gmail.com> Co-authored-by: Wisdom Arerosuoghene <wisdom.arerosuoghene@gmail.com> Co-authored-by: Philemon Ukane <ukanephilemon@gmail.com> * 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 <wisdom.arerosuoghene@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * 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 <ukanephilemon@gmail.com> Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Co-authored-by: Wisdom Arerosuoghene <wisdom.arerosuoghene@gmail.com> Co-authored-by: Philemon Ukane <ukanephilemon@gmail.com> Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> --- .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<<EOF" >> $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 @@ <data android:scheme="zano" /> <data android:scheme="zano-wallet" /> <data android:scheme="zano_wallet" /> + <data android:scheme="decred" /> + <data android:scheme="decred-wallet" /> + <data android:scheme="decred_wallet" /> </intent-filter> <!-- nano-gpt link scheme --> <intent-filter android:autoVerify="true"> 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 zcmV-G2*3A<P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0004nX+uL$Nkc;* zaB^>EX>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&<O_x)MDYSv<aPb7{q!>kZ*5YKE@ z49@$+Ay$x8;&b8&lP*a7$aTf#H_my71)do)Q^|Sa5V4qVW2KE*!PJPSiNmU<Q@)UP zS>?RNSu0gp^Pc>L!K}Wr%yn8Lh+`2;kRU=q6-AU#L4<al6bnf@k9qiq9lt~_g<M52 zax9<>4YK10|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#<wa@L}p62|10EPr|uIeLrk^lez24YJ`L;(K){{a7>y{D4^000SaNLh0L z01m?d01m?e$8V@)00007bV*G`2k8M83oZvNyRvrx000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000ITNkl<ZXx`nLeN0tl9LGQBc;%w<A|eXndn$vhOh+>l zbonx8S}dF9sHtgER_>-t{lgY$Yg(;tE-Q5@wy4c4NjFUllQ1H6N~^ev%r_84AeHM2 zh;aJjJP_P-?m6dP-m>rZ{PUdWcb?zxJiqtnNVEZB9ZDdO0K@}vzz`q;2o_7+Km~9X zC<KlIxh|(v)7rfye+lCpid}&uAQ_kf1p4@j8sGqsAwg!LQ>yI{0b(6W5b!GSI?#u< z!4=>`;A59ly3sBIj9R8hwGGSz)&YHKACv>D>@wR6K9H0)6Cl>1L;>l*R5}J(z<ife zy3}d`Vjapj;9DS)j>TnQmdh#S87-1cm`?(-JArw`p4oy2-%mgg?f|;dxu^qXyPQ&{ zWdVfRWc%S7+Y2?oBt3PKycq29PEs2)n;7kB*s`aq6yD~)#(bkqqzfMP1rUB-*HzXl z+R<RJo8J1yq4+PvRKcTGg+zoc)(cWS17Q0raNmHI^Vi+5ViWvQrrl}H40!r+FIIUM zs;)wDDeV0Xb{F{^?1nW)>@JP75o1%A0Z-{#P(2(hfP@$b4+Tk5M>jhpJ_sj^pv2vh z=BFZv>I(M{{daxiuyujp9xx~hCdb2sVQ}U=T)JsVfFQtKnV)vJ=>$Y#cEK#SiLk>~ zos+mGPU7%IElt{VcxH-u#B|?+cfNq{Pg%>IBp6uelxk%FkyV-|W)-#Y!bT`A@ldf~ zcsB*cM{8-`+6z0fKrvDL;O_9|d`ReTNq|0r=O!agwyJ;?wXkp_6kpH^@m`8P18Mu< z>-|u6MIDvb^abi32&<DVXPC(Vk|;*5`Wm!)+u=(G42;wo_NpzAQ({d2NF=;A6UM}8 zpKbaMKFYNe+`?p0N%_QlLVx&V5e(2tszZZe-HVWDq6$YY!}D8U|F7C-lj1G05eOv6 zq71YQM<Q%k3L}T99z_qK!LU9BCJZot-|}yuysG8gkZ1@Eu*6QhENWOwSUMgSC4t>S zdKVf5>ledB6CoNDIB~M&To)M<2V1LbZK41-#mHVTZwAOZU;S1MoGDQQlqZrNg+nIx zaMi8jm8>cC5S!>8nBwJWYT#<|^%~%ZJXpIMuGjgWA9UAYw!YpHX@sp=>%$Na$7<(x zW<%=U)@NxCjnK~hdD#+auxyF>KF~9$<y=`s+c3XuA`I@QRjI$KtkK&<)5$P>j?szH zzZbQR@c<cCPlu$5+GhnPEx*QX6CJbRhIOZo;oi7u-Ysy>f$$I=2Og*<ay`1~M5jCO z*?!9cRM<qjZ=9i+It{?yVa9J(fL(d8ZZU)eS>r}gAbk(~QDs?xv$E(_8Y1T$-1YDZ zqhg?k&6w^`39Q@<)fRefcj{ovF4%C`CrvDrQ5&tsidF~bJc^dE5P1Jt^Br<Z)KvTW z4ewvQ@C+>92+mxeP3fo%<ch?|5C@KE=O&DSuU>-3qK#>PE`^m|geWP8l+BQR-mgwY zt|ZFI-M~yUihXaupnhJWX|<{biUK*u)f8Hj1V*cML#RjOIei`$ehQVo^{4l^oYIqx zaUjDA#o7$Gd0YQkSP1kDhsfSAXDY0DLc1pSBCOb~R&dQRd=PxT1j6k;36RlL&`6Mp z+Tu1uUKylrg=!16>#WIMgeX-FTO-6k*tSHiGc1;rnN3A~MSfa+#Mo3DFxiaq^xu$Q z05MVO7dMInaYG?001kQty$jWFst6JufWT%9_X>rC5%A*)xMiFRz3p;JIi6jK0$`!2 z`i;fE4LgrP={e|TQ<FTKO-+M6>%rK;(ANfsPJ0O9hLflCg&02q_8fz{2Cr?E=PL8l z?r2(zu?}TEu+87}K$5(4TNQ(%Ueb^BiubPB0+&-t*X_*NWwxVUF8BpSQ8(5M#W-Z^ z(Re|Y;PHRj+yxK|W?2bt9f%s#+ZxT4lWsjA`tWm6FBdu+b--M2S500E8gU~syOU3% z27}uf_`hHBHB+b2t?^ENAWIl*p4mc6HL=SnU9!te0~Uxt(q1SB7T9H`S-B&&q(V9> qb#MM^plONSt3P{qIh9WP-R2*c-6NCdBcsUx0000<MNUMnLSTY?^w6LH literal 0 HcmV?d00001 diff --git a/assets/images/2.0x/decred_menu.png b/assets/images/2.0x/decred_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..4a41efef1199f489b2668e9ce5b9130203ecb460 GIT binary patch literal 1871 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 JPDHLkV1jmSUwr@o literal 0 HcmV?d00001 diff --git a/assets/images/3.0x/decred.png b/assets/images/3.0x/decred.png new file mode 100644 index 0000000000000000000000000000000000000000..b2c9ac81805678519a20ab0bd936860a29e2e481 GIT binary patch literal 2918 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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)<h;3K|Lk000e1NJLTq004LZ004Lh1^@s6Ib=4{0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iTeYQBL@X%ckfA!+MMWHI6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RR<SmT^(EnLGS~_)x}BCMN0f%QfLw5!Ery{-Fw`<1N@B&Q_ZdfplX(p zNhO6`enkkrB7`t{(2qfhnfk0Krr|lh?&0I>U7TlmpZjz4Dg~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`}^3on<qfv8MxBg{%Ql5`6RvG z)?!CM-!^b@-PYti;Bp5TdeS9Ba-;xFf3XO>pV2qvfq`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^+=3S6<l@2UaicHl@lBAe~mLLgE$#^ z8oLzukwP1J4*D-KMn^%7jDwsEfn9~#9u&RZUjlmB?6Vc*OrCk!ev2=ef@00&&1{Z| zN^vIlzi>armW%;0XY%xmmQxjB<eel+ghHI!C$IFuC=+y#u50Qx6@zjn{{TE0u#so# zHMWCAX|A_3t!r8u2yZhBGE14i1xEO8%Z6NC?hfSjB|BZ8@TNw{=zVyq!5SF=cyj*9 zgxt1$l(D~3?USqf;Z)38#Q!hdM59#hL^;s<<2)^M&@;KZe7Uuh#K<$_j&!DIH`94? zhMbVe$8SDUN4~k~(|&H~1vDxUFzPQWCpY`)to0zvxWw_70ar9j3b<&aa-Y%RCfZFi z^PJilEZ#+>vsW{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~<s(J9#>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><X}v zfp#_+nj3Z<_!Q{v)uPn<g$zpstOAX8_9yH?q-@Zrhq_XnG>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 zcmYIw2|U#K|Nm!Ll*D#YLS>MMiQLz;a*Q+Nh%ibiISRRtskU?|HRHY`q>JRpJ!Q*I z<cKKa+y*0Oh{pMUeP(=r|MpS4>-~AZ->=v6^?aS5&+hHx7Dk)-ck?3%vYB9f%o;)X zTG8Jdc;P20>i>iw2<Gn#`ufKS`uck>`T3r^;B^*34m^x_sAXJ#eEZEc-IDz~_%_{9 zcYk6L_-g<CQhtLce|p}q6EpK!ye7SwaOZB$fa9g<R}NP$UGDty*7naoB=$~Tqh#NW z3-+--zvcVY#+Bbwtf)yH;c2yJW-8Y=;=jmCQcU<OmY@C^dPAswL$>hc+sTV*2l56_ zx-N8>UlIO(vM(a4lVpwCHL&g0HZwK(|2F@ZY;HPIZMnzoEMcFnOvVxBwSQtyB|1lb zKjrteMOo5NRoQaSzMUq{@<E5?L=XNva`}Xn%EyI~Q+DdUm+hWi>8zEBa(k2YLf${U zQ^eZgpRLC>Jg_q2v30oB_VVHB{rV#Wp|vO9g=tchXIzH&c{;NG@>?E=nhq;FcUX5w z`UVBxa@RX5hWn9o)|aA*sWWt+bmXeXfXUeJi{`|6%ABFmU2)8HI`#b(Bgz7)QH;nS zBnr?Ta+NH<-#;B`*dNDt)931{N|4Wlf0{0GGR1b0S6%%NX_L5`$)A(8lNBrU_Kbd^ z05`vU%hGLeU>4rfrbfq*HT18nntTfUWaA~{lm1}AF7!9%uLG&U@I$@;g1G_TC`R}X zsXy1kn<e2#y8;Xx0`z^oz0djtAo_l1-2%?K?+v;TaDJ~5!Th-WRY73{*^3a49X%1; zJ>Gk<<HgydQ)~SvG&b<xIxv7SqYa(?XJ-2WAMHmX4o)<iIOzml&+c65UzRot7E8i; z%}SOR3Qj)EZ#6N~ZnT({_wv6ph|xywY}mgc<K@bKp$TJd6{L}qEf<<YqIA!k`X0Fy z)9;|znbd!|ci{j?hl@7xu9E&1(e}+d)xX}rqiZ6~d1rdMuuhQUT#uhm(Wi>l?2%tP zbqW*ROPCstzx|EwuFjP)zmb=N+h!XGktIx4y+O<S5x%?43&J{u=%$xF&8)s`|M#jR zGlx8s#GOZ7cf&=q?(SUhw>*P_30}gyqk5Q+|NPhCp5Rpf9Pj2|hbo67;}})+w8k=x z{%^ylDnC_V!S+9y(Zhcr2r*|P6ZSr@w5D&S^c<n-ZfdC!)>*LdqZqUt@+h49vl-`{ zAl_bA!#q{Z47p79JrwE^$8dt}VR_uJy>tV*{Z#&qy|upNR7FiSdh<7r!JdCTpHrst z8s_ej!~L~JMDI4=xTXcqbs6IM5JaTNnjT4*cU52V9vQE?G+(mQ|G&>zVTH6!W&X{U zeyi@<mDCgPbiHI2T+#H%ntqSc<EJ&U^KSDh!H0P0>q`kZx=>Nml|FCwA*a!d2f)_* zBLG22yKFq^8Z->SrzYM1*^SrbxkTqvX^dx^M&#%kBjz7+rcyV8(5h^qZSp$G!pPi| zo^yj0LwkxY`J9zXl65ur>wBjW)W-57`^$W98<GZOlLWam5roQnptM(-=1o~{%Jxsa zF|xYz+EEQXTH@#Hi-|ka6)isGD5ipP#1;187m0%^V|-E-z3(a}vPZlr<6ct4jkTGK zyPHNf-OX?{_PgIz56V4Pz=**fzW8oKLS$f@#`(62^c$or(>Jgq@|tTkL!jO{anNgM zODhIJUg{T23=>qPDppiSJSgMt`-uE2dvzX1MW*`CyK67rgy@EpS)vd-9QUm9CTq!S zrtWO6?ZgcKdxOyIqMk|jlm6Z&Yz`L-q>}{UPm6+yENqaOb(w}*nIMjsD3$ExudB5y zw)ya8z;rvRH?G{on-9yXm~*Hd3TfCdgfw10Np4X{TkD=li!mBWhRB+v#(r?t#5)YD z5v(<Gt`p;++;29M?A-!l-M;_|oRmUsseE=&YbYAaVi~lUCt#9xd&scr2+iFya}ien z1v?Zu>WPD;_sCu@0fHxkC(nqHlKrj7%I!Ie$xhAg+MaoULf&mubgwto^p}Lml26~# z-0jDZrWbPU`iuF(nY7@5T=c?w`_T)>o_3U(lB=*4v#KMZofwg72^m(0>Ien+d$1kQ z8as-j)!Fp!;@7DZ@h0KSF(*Ef)S?yH(#KzS@YfW`<M<~)PqMY~tkMs~)zRP0z9a;` z7C@ej-@#@&s7qK1{m4|_gdj!MC<aCPCf;hMGn+Pz_-ty4IK)lmt-#S5fFcd#a4#kC zh<156(T}YsIj(y$h0!{9Jn}p_^EE$WGg(ItKce#_Dgd5)hlS@#QKOSGi!W6z7v1n@ zzWdgwgfzV|h0}~E;aHkck2T1?a~t|lT(?Q6eBIK!Z(chP-v64gYZaGtm3GBSOT*gW zR(OI)GVu<w^ozaWzkn?-<43TBTC#Y%UK*nwpwKf1me=nCz^G|*rM)=X;N4~&^7w^a zhy&TQ{mf!?SsE>PPUkhq6wW@ZIj(!&#IFUVL<Qw6LbP258m=`YjDYL@11jPqP;X4S zoLg4YuFqO?P>*)vK{~%glb17Z%u2wGry<N!z1U}cfV0f}{&}~h2}4};+W5l*t8oTG zo99+IU^9xTS;Y<Cq<%S_B;OD&<n{L$tVGYu+-4w5k_&qo;zNQ%<kXE31v?C>;*xEs z*&067Os^gw#Obi*;L0ugM3|(AH%+Cx`#wXcdrzf-?hhC%Ac+|&$p;i^B6nQ3PdYr5 zD&FLKaR;)OCJeeS0o?XxfX|o_P=$2mxF=moC&5>vj<^FMZq$X?*q_LVK>36o)XhHV zo_uw}O62I<<w!zcIX_awl_(S98)_C+*i{1%|3EE9jEBp}jDK(V*I~=iXKw0dig)x= z7&>qi9W`4#`zRhTTVv<>NO$tsNgm`YqSySFv1RP2gc`&#`ApOi8bKmu+KO=c_2*oO z*&}~)by#C^j8S3=ME94e=o4L!I_CWe;zi7HcaZt!@?KgM+n}!KH4|)Uv`oB(jQu*^ zY5cPvq3+R;qm2?QGGmOUlz?GgsO#d%*7Q#lZJbjM>(dJ^M1ZPR{AW9*+8?-z0xggX z7g34v-IGZS;nyl@+=xK$D4{Qz(GNk?;}Zs<wxXmLFe#XbdTcc7YUXEu7t%(KC6=`? zm$Vwy1W){lKG9TaP5(vN17?V|@N;?wO45ZX)si|->VOn0|H4&Gk`OXU6%go^X@>!0 zCU(hz2O)kr6$hqx$S8pTP(T232&3fif7DSH6?&xKFR+v!p{7(R(Jr8IM6(|~NC`bC z+19hloXmG9E$0vyQZ(52kTF5%)7=$Y>jlW2yN9Y}CR@7VMRqlb9X4|O10j0W$mry) z(IK+=`UAx_p%lw=DVPu(ZCg$x_t5u4oEzaLtRB+Ii)!8j{FOuRDJc2#Q<R2IN`E7) z`+vVadU(b4J-U;9MF->-U7eIQzn&v}h{!b|EUOD(34!yA+R*b=PZUgeke6F-`0D{~ z0-a%$Wm9q#nAsJzkSH3(h`sUMJ`cs4B+?kdpbAxWRfk6Zd}gr=ML!fJlY=n$BUu}( zlcM)^%>e%WT7vlJ{shJgpxjnqZ&AYX`Hg^*Y-#UaofKb<DsWnoSU?E5+(!c*i3Yk5 zOVM%xtroantE5bMY(WsaZ#>_2f9}5kaM|5O;d*+YR4jq<b?$sE4OD$jc@17v0Q)6} zA;wFg+5|eFX{U}X-aO?4C#ZgnlGae6=~;fr@kfc^hu$G?THswccYallAnp#&<aN*~ z%l@Heb^idO@;A_XAb^U;F&>Q$hFbzAI7tO_p;2Fe#8(X<VOJUhyMd5ohTG`i^88K^ z{zHT=&52wc0;K&6$9S0nYy+Mq-b;B#v(Zu&u?9dJn<>kg;kl&9M;ZuXj0(Fmu6xyk z9GxFM>x{yRKa~ui9oZGT`W}v`mj>Sua47Kh4?PkUT%8TcTsM<p4#qVD+X=*ioH$%= zna=0yEJ^a7v!KY=m*6&sff^0y_Pyx#V+9ksz@04wA5VC|f$W<bJ*Nis#6l`JLaar> z@PFPB*f>XA{TCa365QI3AxY?a09DyJL4?FYt0c5YIt*t#Bm}<N0H8m8_E~>X6q5Y~ z06j?pT!ET)pmfD76u0&Tn+kd$V`!EDm#K4yGrpl}G(kpH%m?Q*_tODDsW*RL4mDoO zhsa-u9%7CjB5)9DAe|(MM8==!BPSqI$bzzs=mTQx2V_gd(#1c;n)-3GA6Rk=HP$<; zBwkYw>b9e`Ns?b~*$R2tG4x1XDgaKDj?e~o`i>TT=pzL55$oKtFprDsb+6C|6iy|) z*tf!sUZH?qv0b)wB0>C9lnyOI9u)DPSRt(L)5&vn14Zu1fFfpBt?B!|?QZr(@fL|I zJa^GpuMy4`MI-oEd~R7ZgrMk?P#zf13}e@DyJGu1`4Q4Dbno-m5a-C+o`XH_fxOhf za3xy53~LEMFb9J^7Xnu%rpI+h?sKYV^#P}lgn|FOHjhdu^Fi=O^I0cw`w>Aro$<C4 z>I=I8Z-R>+TyPpLC`B{ua}ds)vTPWtUiTWbBiYUsU<)m0g1|wXKt%@~RK<e_OT8^( zhwC0P&Y^d7wyIH<oflusDT6N;`LUk>Cj*?LzU&4md4K26iH=U;co6D+G@J(>hfV<4 zaF{TOLO|pX$I-?h6bPZ)wbAWL5G#_!KS@v+sAC{`ts_u9jR9Y}zyXVnppR@tedh(o zxe(=n&!A-{1Se`KdIMUCUWFUfFM^WF<8gBjRxND6qtJ|WX|suUT?XSfcA=aX{L-i| zoly$fNtj`fOIOg7_ve<00?!(xE7%BV%mV_1*wMsfNHbs|V{mLi<JjR?bH6MUA&iEa z&f~gsO5*7rf(Y?n@JV8;iFdU7MfLu6)J7!d6yS$Em`5KA0AD}hskPs!9^4nPFPD0p z>Bf%`QAZ{oH}P)3vOZyjN<l2@l~7Z1U3!dBeliy#!Ubj(`P-T<*<R1GLha^I)((J( zg2+wEu$FW&w+!l8CxRfV67^2R0a`zl=5;DO2rd>qdlIsF5|~4dmX30==!b4P!xs== z063dAp(lvumZf4tPKB!Xqv4FC5)>F}9Q6y5NSOvy$0+rb3n5pP-JIn{-Lq)2`BUeE z$Xav}&A-aeIs%EI>QSf2E9?sf6^v(o%sq&Fi9Ucv0OkCSz~c~v!pSx0e%uuWi<zhJ zz<u-qlz7TH9f8phP=909${<8gJF4UXR!@k9kff+3U{z9qy=T=Ka=(SpNYzOQGsNLj z55+&Pg~B79=p#UHQh}|fmO60%B}$v55PR^gwHUbK0xE-zhn4!;1h&cn-8^|Jo>3_I z7=DU2eR(%8L4v@k|D}oGY*EDO4~2hU{(J7yr0oRYUGIMu;K=ob$3@WyplVj&Z4;`l zw~yw7UL;a2N3K@Vd7SAD45)#KNUelARRQD>N0*7B{j|;pt2{t~F$37$X<jfvnmZqP znu7)P${Tf>90bG=aInr4py=}oYkDmq<AHd~!fU_+X$&~JcyBHd63`}ak>@+WU=GTV zEI<3EW+pN45Xq3Dzee)<jicV96)u3`iQ;8z55?fh2i9$ZDvB4$HV`!`3m*B1J_0$H zWNYh5pSd@yg+?2a%KYdYomqH<2PxW(VqgmyEIx8<`4Z|yMa{Z#k)kV8<{-e|sEo!1 z6JO^pts?Bjw|(*ApDlrJMUv>wXlNcX8Mg`dQ7$7k!yQ<~R4CU}VSNv{Sdn~gnLcp& z%yU488W-jaf&7UqLdu0+h9o6h>TNRfcrG|H0#ytr3FCpY1Q$){`c(OlOWtt)fMb03 zyt&YbrAcu)xFC{bB`gziA=J3874({iI*l3_-yLW%qcy75?28rw$nZbHpM4*%eE~wF zoPCcTk{{Q7-?Hv<q#CLLE?+kT)GN({78R8u6Vj32;)~fFwutyNbkP-bhr{tE-V4S; zBX#GX2Ss>)NPs6s!%98)0hJWrEocI36viUk@&h1zA(&QU7#bY_9PM+$upxfnXD$0- z`z00O$I)|%;H*<g+{jIRJRj+*Y^jcdPMU8S3f-w^%I&^6b=}($oB#la=HkT9zUdNY z(YqZI+F@}mRhE;6aU)QS11hnv(4Ei!9fNyiAVkapV@VodXg!+E=kKgZR4|h5<q&cr zmemO5C;&pN1-;OLP#s0QC8`sGHc}+Ls-=^?77vF<qKigQf)j*B7T6Yjes=+ifbtYB z<Qy7O{fUEGBXy~aHZ(SoHve42Mhz~(9y-9B<OY!?zh8Z=7pqo`lG*FYlr-xuB!qq( zAJQZaTs{ZYV>!@4>;*`M*bKcMEpJ;xRdz!6AI>ey`CQLB%s#sy&=C@LFRY28Yg=;5 z=ur8{I0Ai<a~~T);}I}LyE$kq3(?{zIYqpMooA6Gf~)zO?qV>M>A(_PI=cOIZrPdk z@2ncs#)$vm_0cUWCu|Vv9BL?juq5ak%htCkw+qbl8o+EvO(bjWNk62M)`eALyJu6W zuk^0`=sCG23?d3O1WDG)Gb|XwLk|ih?tp|zEYnws2l0{zWmVB?jGQkW5~Bi(#8#I? z#(PLVW^nHZARh60Tz4Q!?R#CNs8YRZlxlUI$F5MnI=BJ@yhzscS5Po+KuL?xFB^s) z)BCa=DT)Mr9E@_yplFrwMae<j=mIV&lLvVnqBm6&yeh<b=>;2N`Fi`$l8bi%sf$o! zl2q(GwGPtS$=a=`yj1>bW!hcHwWH`>2+h>pP;o2ir0IQPFCPrG0(VUTUR;4lSHx@L z9SidEIib=zaAy)?nT3aWkmsmj?W_tWd_Y)s8b-zj&^&w@6#a;9<Tvrwk_ib7)$VUV zJ%OrOd0?e<QW1tW{ZJu~tE2m$bOf4(YS(3=P?0vz_08RiL@#atLKa;&@h<<j(`f{j zkoMD!-gAYiTu1`cP$aSAo?!uSt`{2z{`6$=HdkMM#6c5|OnB50cnM6?$JRw4*c)_- zkwpe7QIU@2mdVJlYOs6`s4_|WMx<D?P+DyTk<qeZKoc^*rO-$Nn{G*N`FYWELXe-) znHK3HyYo(4b0OED84zi@YD!)XftUobmR(nP85K-C`Cq_9{F%<cwg5QU4y-C~?O8>D z^l3<D=Vzqpl>|2AD2N8x7itbP8tJI(r1iJ6r6w&Kk%b{-lqYkM9)VmU)iAIGm@l9% zg7A(TjP6_EvO#bs;N<IeC>6EWc-VYloPkTt^AO@KpbKw7N8lI;<j!7Nj!OiJ7bbC$ z_5uAx(yZy1M}ICivyXgZm<bflhqXsQQIQyH`c=4&z+Q7GP6QbI%`wz!RFuDOIE~0i zRZviiAqf-h1dBjXF;WOn5Z48Ry2sJ}<;|$K*-hqxl-eLATA&Qu6-+#Y>l8Rgs$`mh z$KZI7S;)=o;^m=ub5s|qJaTbZ5xl$$y$@tblFKb~CCn@l1lfK{O<6im)`L{D=EaW; zUnL1rPaP<opg<1W#W^`h(YWqj<U?5`=`4T)CCz-+oYROA+up?bw1*JoCjcHhU_;Y$ zYx*Rp`Iu8MKuO>n&tO2wQlWt)si3<7zy7)4RXiI%v-F@mqqzkz$w?UP;rva!WpW#4 zYuNsVD^;+FWy++&8u$}V8RE35(1_4$G)_~Sg8jkJZ$P9#^ibgEX|kTNUltp|bIlcm zRhYQdyxWeLL8(V70AD?zleU(?S?=oxqxqX*s7IjsNpj3BGs<h=WOIb*6RxR~UH29? zilQ4~BB2FNT+c?)e~bs<KMn970{HV_?y(B+8?!ZOs#$n!G#3QYUV#1&r()vWVmz}Z zRdJhrCGU4SVRa3>0vN__{#8#EOgsP(8_*g8Ar@H>0O15#<m&-YNAzjOyy$3o^Ar*F zMbg`)Q;Xhnb;>;KnE+8A%F+b!=1fm?5h?rmAH)Z9I5q=h>U1ucz<^A(Y{L!gu2uwP z8n+|Qg+ZAsww_@SIqC}7BE1pp2YM<(-7*G~2BI?*9}gHp6zy)ds`Bk)lw}$u{xn&{ z!4zb=__AaE76=?vjN*@IG20>E^q=QPI`05sV7vhBTk{%@_KWSuRQ?rpS|*?j(VbmZ zIc(z;FW1?or5+4(T9oV6gAUD(0^6U;m9Crv$<o=9IY_!1$1<ZX!XnHbs88W+%Kz!x zNxKWBg8|Sj7xcA**CTzP?*X<AikdAfW0?}*0&>8ZB1lv+j~iy4*btL8&wQD?wdw~z z<-kUmR(P+08*8zFBAP7QLWb<$j>sp%je-!m6=}+po)$J1X2}cEA@Bktv=Fcm1R&4K zmFmc@46_cgQy?{NF?7*<E-{6RlnZ=vP%89-Sp`Dm4awC)OrZ%c!seDO0mGKq(Vnzx zb`k{9wc%7{FKr4Y{+XLf@BhfYtLaH$UX<|4jyz-_1>*Io9BaDn=wLNFc9Jv>TL_H` zgR^*pr+L{IOiV+)$IggIl5&2o5gJ$7Razua^DrSJp0SsW3ogHzFjEce(**5d%tEzf zcQ(yy?9_z_{DWF*3Su(oh9#iHv@g?p$CgK+NwnQ{6lLa3C=d3caicCI(t~sC@l2Dt z+p8^khzPv#AP%6;+ppcv@fCrdpwGV9nO^)PHvonADH=7MM(VuTq<xc8XHj==b%zZi z02anU_r3&%l4NrOq5sMNd?H~j8rERY(r*?!qIZU~(ase}3)CRFK^)`F?kKwm2_TP6 z8{)}~O!3c9h*QHMrt)TW*g@S^$TmI__^%$sncK3+FxVF@`5NH9Fm?cg!Am{F_ain) zlMHBaKfXI730erWCxm+N&TPBnVxt)b7xhN~N#lfP)n!P7G&VFv(iUKj#AGgZspp>u zDUz&bGQ+7cs41am$a)zuMwy2}9*}hyD*vOSYAsVfY(*S|*ODW*p<y#Af=d&f<LK;L z8LpU0N5cViQB!Y4da=<QgBF9NUC@z0;k0&@qo)1}TC_uGdkrNjS}2->19Wk)vl*0t z8s!_D3L^(<3=DSkbTaxI+4ACSb(6%~`n~x{-yqgDWm(hDP_*S}-s?L@?-DZ9cE!4! zfpzenrX*|n>(9O!Q|x*GN%HKs1Y1MuY2pQx41qw&;?0&tY?PYf>>-;K0upFJpq$ka zcpj3c6q_Vev&C{Sr<Dz2rU)B9#&?U8#apIMvr$O8z|L7JJk$UnNghmDZjb&9*W+wF z^=2;wucqxtJCr9$f8~~?L$&*w?Q)3!Xb42&CD2nG^o)b~6xb|kUC*vnX`Sp$;K(RM zVzY<VbT>jqIzxnW>fW>jMilgfu}NIS?;x#BcE|jKxzv#$cKweCe3WVT&8~yuP!x*1 zB(B`DZ2&?)#~E<Ot|$m5Hi*|A(1jab@IjQ$Oy{&M2<hmO6?rW^QnV(So45jJi$3ls z^96opvkgZwa7lnn`Iimy97O_NObCK6t>V~nA@rZabE!-Zzz}`cI0myp_r*`MVI2Ct z*(qTL7mLGI7*-L*;=Avg#*PGW&@cKU)0ACj5GUZyvBw?raS#@4IL9_lzX0?T^AHCo z0BSch*n5FV4><zu`28Jnhd!LT8BW~{?IE1HZqp)umkjYXMH~<N)kY_<cL1_ByQxKx zXH0W&8D%)t89f!|+*&|0PPC-%4)zT_wnmag>|jz_^bvebI>dtfi5+NR>Xl4b?Sf%F zAp8uv0tbWKvIdA6b)_7Uu1v=R427yZ>~i<n<BmXUs7ZU-1cqW)8w?8}1$~WT_e7-R z^Vt8s1?UK@hiJ~nxkR-!6Zi-Lja~D~L5-=m5;h{karY#yLbEl6MHUFJ3tGcl+DYJ! zgd3ZX#5*v?&5r8ajx@sTV;tIs!T-xHhz)ZNu-s>Z$isWqA~R@%K0_=_;s`D~|8NfC z-8p_zE(nJf%%{M9<{W{}QNN9<wOK+axB{Um0Sfffpw&8F0u?CF!JoS<0FP_dbOQJQ zyS;`f9P<uT@5QpnOL!M^=@1nE`B78L$(&P%sk7~McUL29kmVy7v4P#-lPOTN(biZ0 zHT~?H69viN9_mQ6j#dR#f;qJCXO@Bi8}pE_CV0NDh6NKN(9!90B0<xua;Rk1z<*GA zzyh)VSFR!4)6D>Zn+@90>p{PUhoqg)0RY2_g(U?WL?5ohf=8x`x2Ul*9wL&Z3{kWo z54mZE=i`O)@d*(0!TL5W8EBYmWJ!;<wq89dSDKE_Z#cmh-wfFt;yLaZmnP03w=5rW zs18TQ>1~M+&xKRDM0R7uL>^nyl|g<Bj^e*%^`LxcQRBg3Vc!9W8_)<VbM{?IM{`6J zk)OEXj{qVL#tM)J@;OdMq`Ab2x7D5DC-!Y*Quo3eO!NQq25NB)P|x)|KQRN=dgLJ9 zFe(W5IdW13!@S8OFx!WEz6st7p@T(p2=%e-QWYkP>0}7?UN*?AF`f^Kc6hI#t;Zf0 zfF})ifPwR5Ng=>Df2fTia_W6x(;ay>SptYc<eWVQ`{1qqEr9JY8#Z{m9R`7ZR~|A9 z`MfFKnvOOCoYVt@FlAT^Gs84-gD^0o%mHxRIN&&_NkFd&Q)S#30H6S~sx*kkoET5K zvQPjQs@fp0!A|Gk?Ux%IO5`xcE2#xop_a;(WDBUagI7bLzE1D2mpt-~=4{}?{czz) zsHgKAX6fswdWP=<diQOoKHt)MHTi^Rm=}3OT7$|_(PpF?+X4eyXnt{~AkZ)flo>g@ zoWPMb$<fsm;@&}!=r526s+xZwR4=isA%ytDISFuuQt2G@EKSf~Q6S;#uJ7C>4D}Gy zWN3eY+h*czNr>`Z>RVsb5YDe_UQsm1pxr@IXl@w{JQg^GEw!l<tCJlIOQ<D1fpws# z4kzp2W@{<X6}$5jcLNJL4wmZZtfY>pt#9o4?X#%XhH|8P0~7t?Va4VDLKoscG6T?# z<gq_=FiQu*es-lRWT4p|X)0`NcO6-U)}S4<%=`G4c9ajfcm2wRm&SFs6|%W)ph(ZV z84UAQ7>*e4*Tks+TnBHl8+qiqG(0dEDvhW*7ECNaR9Zh-pxuX%@p?M!F!knTZn3Y& zK^qyCE4;sEr)}zha|Q!_33ww9QN2bI^n#S%4iMhsTur?;h5A@BmxHg%prF?+YbWz4 z?<$T2)YEDvQR!hSJVkvV2E3%qfTX^_$wvtNl~L%is5`QxUog6dk2U6DELoe#;q9B9 zVPF>-I8?Frs^IffYx-@%syc1mYQ)<LCNoAL5f~Mn={~<}P5%z$<+L*hF<Ajx+(IRu zqJ-*qVh;Y0I`ZnuoVC$%8zCBvr><%)G^B7jnEGUKUxa&^*cMYi66a!vN3&$@i_jAS z6CE#ci=m_7?~vN>au9relN-ua2@ihaAuwJFlw<Oc(V96deV$DRp`$@)Z!XQEA28Fi zh#g5?M``z0h#C@J{6rPt#M6W58=Mv)_81b{Zsq`ZcZn?N3shXsph9$qc=>u=Aif}I zZ;e1)_#mR_pozB;grfTOLp3$!b+RSg_|YbZ_#R%2<Td!OX`mwjgebY^p?I4RK))6C z?T4=*Mt}B<T}GFXuf|4TvQK$PU>r$s9KOiP`@aSON#be%2Zk|5APl0%AE48SpUVy$ z=f)Y_H;-lVfh;c{ZX^vIES*SYSW@<|M`Gw}+KZIsw=e*2-A=WxxGIRg#f=3&Y~Vz9 zl5Ai&^glXg82NaTp!~@K?|euYf2^O(4FmbMATSwxcT}{eg)nnOnJp?#KO<pvNtJp9 z!afYIb(Cq!huHd21wx14P4ysMp6*-&m2uq}DU23!@OyT8MqdBmBSc1l6~At$ssN?m z*r5fQtG6V_lW>iJCTrr08}pEt@I4XQJmmnNGdNyP6dZ?{mc&_pVi<s01rZGL$qFYg zQ1^V(1Hdox6QN9+wn0Cfh#leML@<&@z;WO+NfvFb&^OBqu$KD2ee^W(wka?aybt<) z+nU}2kuQuBpGcTD9#9B{*dQQ_Jo=rPPKF+*<3a@f<%W#tZHC#vgu%EPBpUV$EI@{P zt`#U_V?!<4$i!Vji{9_7pX&-x??Xb=O69r<r72Xjv~z4EN!l7KLH@)nLuu0B!P4|N zMvFWfNW^}230%otnL53Ji38u?MybZpd^l&3o>lCpwZnHDa%PzN|8`8GJ=pipbz$p) zgK9}Xv=YW_Nang}fRBdZx)_YO7dZJ0n$i`>FNgC;gMYTVf%wrfA%W!eR6rUlk)^#B zj&$YY#luF>`Y<%g>&b^Cr6mu2jhH!R))F6$fgw|-cuOoNe8M*&R-;=YqHNIa&8-<; zW&Z!F4EgF73n^Xg2u~AK&cpq$DQQG@1VbENZTlP*#5dgW?;p52=s%@c%BE~ks81JH zU`k)Si?|BP=fk+wRLP>aisO1jTjz|&kgx4ixrXoBzp38?UYokgzw0NwPljK1{gZ@@ zn&~iYoY5ZvNf0N(WT1d`;;<bu?+c2y$6!zh(E?6}0Y6_pG`*Y*ia7YELi+&G;CIw` zZ^#>Go`|Wn7K^{6H#```b8Xy6AbH*VbN>J)?P$q)IiPNz9Zzy;P<eTo;Ma`y{=$i0 zLG*qd+vMka?F#Mank83=mkc%RK5W?IE`GM#45K77w)$%87~(FWUf*}I&ZVV3M<}XR z!hBvMbH(}DZUN@;Ex!y8s|^LO9$eS6srum1EnRzlW;k!a6H;hSQk<W5)Ll+c!wvV5 z{f=c~c>+|qHBZ!M%4j%qFcneD&tHzvX?^Rt74Pv(?T((=%z_CU7`)T(DShtJ*ng39 z8jof`QE1NZ-ruUC?6ne#UjO1!SQhuL)<^nZMBlf`Hq3tW<ZPoSrTZPzbL3}g^9C^S zzYJe#4+YnnaXk9GR-^TOW@9d?`7}OS(8hOrl?>}RIkA94VehfC1l^Kf3yb(@T$Vmx zf!ZC%kBzfx&(>F`KFKP!U9`q~z@i6iQ+8)xNV<R7@MHfJw-~kDIZMAo^nC%L*+R*R zb0PCUDkV+h$Z;)K{`0;Ht&g~2{im!%uMa1UkeZbb59Rd4CUSMBV#K;n)mT-l59K(| zA7`g(M6*+zEzl}HRVhnf6yUibtJq@ylp~Os@7a&0b2qH<VX&C`RP7E87GrHVTMvGJ z6PL)O<dWvk;G^e%J;KV+*wP~L{?;GGkn_qa8?#nGm|cWsJ9HL~AO|O!9$oF$P{J== zx+>_M*LUE4tZzE|CcN73)=p>S{5IZz^OC;|F9(188UB)E*W)4nHuss)rkIJhBth@& zz60}S8X1KrI5Jdz_!uX-m|;!*t(>L5^!qrutC1sv^T@BCq4wuD!Fk+po*mgHp1nYN zdF4lE4I*NsvXK}LX4RskCAt@LZfgCaiq-HwpEXaAC5FGjM_)eSyS)LcA@>1Z*}|`^ zo=wf&l@iEzDoODBW01p?rAv9wp#<V|$YSQv3oS0(2LUEd@W;K=p&x%PpIu+9`|qr* z(cGvt^{Y}={cn)6d>tw6yXCn(+en8`P>>IHHCAaEj6G5uF?XgN29w(Jiij$-x(R@w zWB<`lvzPE=x+Pa!kq;z{*azbpt364K=h)c}_7dWFM3HvVDTtRCw+nUw_AzmZBQ@(% z)OV!LY>l|N9ajk=sy3VWT@zsMqRs~V^|1ycVgQf?z)k<(ULqI|!Ob1b4>5+(n*u}( z0LN3|qMS`48Xn%g5+o}kKHAFKcl)on?<{SOlajV5DGuFoN#rujz=#<ht+6sQ*T|4~ z#PLPkyZgo(*_QEKh6%9PaFP&d$yp%AExS}3?le4Q&LwgZE;>!#6vxr&1C;<iy6G(G z;UJfo;c?*a{@Mv1_F7E(>$1sh)D5_-`VHVpJm8`=U)Fd1+Xs;ISW`E7;G-Y<+mUn5 zt}lFT`}y}NkuAIdBA8!>Kl2F|RvdDQ$mDzV%8`V#q%qYvSfLYilifYmb-G-lNz;nv z^>mHn%8(FdPICdaGjXxU4s-G}^4)tRZc5)(fk#`9Hy}6z?C(exlGZrJanUUEJ!Hmp zryv1hk^pmhIX#~7$1b)sBvt>vK8kURNKfITM;`SZ=*wX2UnhX-{C)71e0Nf31wPsh zTu<`5lM{Oreo5=%*h@^wza+u><G$Pdt!o#;3pfnMr#hv~L@@mylR8CO-GX(K80za6 z30^++F@pR2)u1f>yFc#zSArcm$Z_$2tai|Agod<yA_*RPqISn#n$>`vU58P#wT7@t z1efrZ9rzaVx0aVtja8HB%vF&V_HI(MiXM%xY*L*18f-8hv6psOi379PWKvue-?Y5; zJFyIXV0JR&j0tCD$IK)s_DzXesGL>*8of}86+qTmXYfIW2NXnb;UU?9M7U7<`irTx z-^3VBRg3>M{*ZCKFIup!nIzaU&P{NU;>3aDR)u#K9@Q8A+z|A82hb;!?7vQ*O6o44 zkN;T`86y@6Y^>I%d61jzIop_A3(_oKSf4V%hz|<t*`sjkKK+N&6d(b2Sp?mpD>7GK z0*TgUu3{TLa&!ntKJz_-`QRVPo_z#hzl?MR9zJ!2Us-q4j~qE(b)2^{c~d~83%I9v z|5FaXep5Sp|ENsBq`$QD-QI3<f5dcM<;os1#IOKarX6oUW$zND`Y6XGNZ28tnaEvC zyS}RyT|j_s<Zy*rW6ojIcgwJ>UcgcKaTZ-faMT2nko-`2{&EyYz=1?_<C)bLVf+ET zYrh|ff?@bML_jjy?!>{5cW`Su=l300H6r)hbB-L)SXu3K=Bd0Hl+|tv6!{|CAExWX zAuwqo6g#xksu+LGXMijHrIqh?9avq@Fv$5&<3RqYhu3_F`25srJ(}FQuZbks=k=V- zn$doj{t}oP^?+fsP6BOTUy0pMR&oSR08R%B`wk>DlxK59cX_hl+v}WFEWix7z94l{ znqw*IregV_Nv`;_`25cxTn!=8o`X1P%g-vO1>q;yDSUo0tR!Jwjg@TH#ojXOK;Cwf z*E5lD^vfH3J{kPd?xWGIb@n2LolV#&dSwGXSERMz>4KDo<2v$ImjkMW()njL1RVsB zB4ucLvK(*0{piy?a(t;;;KU7(I~U~sP@c|_`@_ejvPtFR8+ii`E<^{AcdR=?%<UbQ zdyH?u1u>yH>W65Abp&DxC}opJT7On^r<;Hi=%e4t-tgoc6;n|KAAp|E3e4Repd`#Z z@jmnYIwNr|JM(%jX65W^9SvqjufFH-O%ME{z6qV}?B&%=FnZJ81~9}!1)yuX4#VYz z^B;{WqMZ0OA8rcJghVr*G-I`QJ*|=EadNaw@7~K<-$hzS-J=p1D(j+`#CQF()H-@x ze9*!O_}3-H>cZNrllHvU7^V6G_^GWd6RXfVst%Q!%usp4x}zc!7fj&FmWS6Yln7j! zt(npx1p+apXM`-~XS%dovr{Kc73v6lS+f)BQW}H*x@Q-<a_=Y&^(Oz9xYpZ!k9O0X zmhy|1_lZUsBWD}i6%tf^2^rmsQ(e!ygb<>ZONw6?%SM*D9Tg2Cf4>Eb87SiKe-100 zo-E(zyotC38+4~D82yDET+Hx7*W~3-^cW`6;=Yo{pq185w^T9;N@?5brl&F-?FD~X zzAzP!(hOk~Jg(3@S#?wo$-2na&R`ew58ePxLxAid!;~OsfBom_V|+Ky%qB3dXPT~l zx+QpmdN~>^vFd9WKzdl>b7qFwd3dEQpWKjPA>OyxFA;I@f^M?*T=2~62FjHWfk@Wt z6%Vo+WYtJ%o{46XMFZIWDt66z;|F@$E}Be>aYEA<uCKb1n#ZLY#M721rerA6o)}zb zr>}{4=>Cd@hkovBxofqW6=Q=l@5GR-dn+rg_!s-Q(X));tbW3brAe#8xfWvurEKm) z-PKYWW0~Z_Wrs~!v%A_b-CaLaXgaZR^^3|YG6-HYc&rsaF{{+tEWj+O3C;N`H<UxS ze|l8wa5i_n+cCbUjHK)2y0Ea~>s!RK&S+PtR#cr791T*VXi1p(MBpmEzh#X6T11dD z!$0C;{py_fyW=tGWe`OrO#Sk<mPkr!;`XGLV<i^z8I6VkSzIo5mb!kYl|slnk#wJ5 zDSl>kzpgUM_&%;xvsM`oWlAxK;rnk8j)NNURZ3ayoj~LuO1Ry|?r-BSrjEIK9^-Q? z$~A6YOxq&%<fgs&gXO6pkv7cJr!04kC;j13e(e#o)`+1uwHIEiBGgUuVQ^Fn4X*`9 zm4WNKkric5KH{Bdz9lFS4JVKj<xO=%t(D&25^UX}>MJX;7Fl0I+q@8WeX%uy_TBO# zeD>T}O1KY^s#l6<;seQoKEn*u{PaAXe7E^XHusBf)^V}^YoDV$F#)xSJ`rhEfRfe@ z1D4CL)JeJMU$#L~UHz2jKYgws0~H~0THZ90g%f(hm*?qFR!0n9z<9Y@VWU`zw2`PM zJguzz-G$Z1GApm|M3xtG7t7pwzxQKEu7g&SC1Tu~-Vlj)Vp%3+{f?|{yV|Y};<CAI z{r;(?eVYPHPd8<_xZWS_>a~SG`jWM09xEWNLoW$4Hz7&rShp&GfzzSCF(xk@I4YwI z=C#^RTbnJU>8;>_8e)}LjVXrv;`T$x*V-^~!pjQDip6=x53P?uPq`82ksLr$FYaof z8$SO9ls-=6@S_`_5<4wuH$1RQyW6hr6luG9Fr@82MzsxD>>d*lXs>^^cG0YE@jl~2 zSYyja1V5CHgTU#r>pUNCKsWePllhD<LD1&<Q&58~%Tgh2B!-FSd2@`9h}B*^;Wy+> zi}fMAe4|k@>t>E*Wk;ql$|06#7}dW?J%E|W&Mz<2qou}YnRNOXD^w_i5Ok7t^o0g0 zzIf6af6&fYN_B;eZEGEjoOjR&u0Ce~tP@AH_1dLnOoycv`}IQTrj&k-gC}e4iyP-2 zv)oxOSCFi&^JYqqG`>dN-X5bS%q+PPniH%~R+KwxTLE5QF9lv1J)WKxWm4RJ;t(Pd zvY>74lIMEa-;ed+s2Fl^;5UG6?@rX-)w(MP0-`dlPDaCI^HVPXiYH%YY2p!2QG~(6 z<uAgN7MGV`a65UEW<8Z;@HFD~7yWUk{KIBe9x>=@!97|?z!M4h5Eu?TV~ulx40^e% zoxbo0>uYoEsZ-TH)?z7b%aU}}L^j`ak7p?(_ZQ=5J|(l~1z+no@!;xef$&dnX&qv7 z2r<Ac<v~+s`kvC~;XO;Df^rgTE~YHcho78|aKT$2d-A7GZ&!T@Z-XyiZGHn${tRW= zXltEe&c$(Uor-}H28qWJq1$h#xkkyd^!mFXm@c(g>Rxs%9=q>|;L19^jd^_uwf_1u zy`mJp%+^1U?yn!Ph2SzF5}M!Nk(%E?c$S4^wCbtCRMoOnj0y?Q+9G!TD(ZjSJL6oX zdqOIAA~CUt4E*YKhR*aUI>q-alBAK=n+oQv0}H>bah<60?Vx-uRz~iqnq%GJir<xw zyT12a-Pz$uGfkATeEw5CC~H=yTC-v~pZ1DRrd@grL;W3I+ZO?!)h%J`mdG<!Q=87o zSo$7t)#5@*Zb%V2Rrr?D<8bqt!cs(SVDv*mZHl|S3iF(Do|JIf<W{ls_e=w_Oy=p5 z71J^9c?}iZt%j3P@~mOYk7ILd)nCHeF~l^T48|333<;CB4t{_hb97>92^)RjXgR$| zmh}NO5RkddmoByVqm=fvY7Wt)OIk|ny`f2IA`@pnl(@6r%^X}gWP}a-BpVf2xAtmg z(+h-}l#=3Z=U=ooYmG}dm8CokK~RUFoBhY~j1W^eEn&}_#GTan9R@RMt`@qpI)_Nj z<-z3)iifn0)<q0{DfF~CYKp>T^<CBPlLV17<3dp1OT=G&bsFzs2JPKdbT%OUw=0nD zX3hcf8f%Erj>cxljsavoaZ3fK`0($Qj34x02<h;pAjsmbiaclDO7`rs(W!}$u$b|a z8tC%=dSZ3Dx5~1(3Xq{&J(E&AnV>y-c@oc~Js%MAW#PvV?Y_+~B&%lPNNIS}l5d@Q z$iyA@&KSlcnU8W;5aNk(vU@1#C})k^2|Atw9TzCeIa{ZD&Y774yZjyi`UY!DdgQ3z zd4l*6|J#E2{DXHGl~v?Im-OWZ+YbSVh*T6BQc473VBm8G9Sj)Hlw$dwVd!dSJZEmV z-(%v(AfVXFj}sI$+qZ=v%u}LjGJ0=hSs&wRYEfjb?&lkkm<MCzJ!)`F^7a_%*h?N} zb93{*q(XAGwx(=OCEqPHcd524UV0gd(1ZS|9a8-9az@ZFt6t|-UU=bn8=sVxWji-a zST6rne7=0<=cMnX`Fv8cb_rAx1aD-3Cnfc9=brJpWBi7cO#z0^&<-Rro|#amKP9Cs z*IyW_`hqwfp8mr4o;ZpG)asl|NMz1>fDu)CE?Lg_+DorR+RprL4~0}VT?v7p%bi#v z(z?qQB8vvCi&AZ}8NQZbQZGR=c<O+w3YS?FS1v`eG&{ZJGs|Xf$dToI+qsd9XkRJT zJ=o4GE3#R9u=&XkplLgH%9<;q&O6p$FFe!zB^dD#^A|YLe!D_*1?TwcA5{0m(NN_0 zW{R}*(&WSHpVN#hiY9sH7Cuo=_H<%M3FGPR-e5mJI8&i@mkP98=Rc0kYIssNq2T{) zCtr7l@?&wSP-?{WDR(Pt_>-A}rsT;1nN7$+PpHAPx)SY2?R%c{AthPKGyE$ivLYAI zgMeaDsf_m~)PFDC78J0b(xv4cQfRx{ywFj}D9*$d1^^KZ%u^o7r>9mA7BNcYKFa-a zk~okl&x2^^j1tZr;agg><u^<LxDBA$Q>NX3qf%8hG}hu6tx}H23)a6~lv#x~3^TlH zDkc0!le=Of5dUg^rxc3zw?+zGycIxue;XXz@*~w)DNFf3s2y6c{qQy3Zk#4y(5xk; z(R53&<X@=%-v9rqjWl`rml!R-F7Z^^!Ee<DLdb8PE}4)CD6n*8MPvc_TVtaWW2Q5_ zQp7p0?o+zW5kvpEx10oOE&LZjhWIz+?TDm%f4a#a+tQ5t_a6D*w*ZjMo~O?tSz1l0 zCw045)Li4t($jio@T{k5$lfX`f`3u}=myCTJerbEVWej6_i8In6%|uQLnFLkC;p=D z;xEQ2Jqu(9F3iwxaFnJyTY_XQX_qRnx>acTwUnNp55$oN3>AHDmAYT2kG;Wr9Q!pl zpPv3t9N!uD_F=|<V`TmLz}tc)!w^gj$P!h@_%wUEH`8*TkrUycvo@J$#O*2C8!~1^ zh=}}rDt&qUurtE+tb!1$C&?}OmehO;l7OBGl!Ul4l(1s89ROW@_vWFIti+DSy8#GO zb(<&czg~)(c*dTH<HbH_{KmKmR_D3#T_?NS>E)FgU-b^~D8^yZw?Oat3+o~C&0w=g z7~ODgN0`hkIQpitn#H%b1OqBdMaiZwP47D<G(id5`hIvkZIA!)nbhcnsflWq2au@} zX|<i70v9m!FRGK}QT!XuY=h*|MQ|zjBSr!$)>9A-4?;~*8|nSI%C|VRaE_~WC@?vr zx+J4|=$3nToS(64os}?B`MvNhmXdmXWle!c@t8T6%M+*;&X5%kVkX>vFc>`)im>7r zuJfP13Cnmy=|ivcDJkL9B}cyj7f1LHA1`*wxGr1yy)CEkNpx|OUQW}2aEdhQ>=0MF zFiaQoq%~w{f2m~YEH7`3uu_UD6Q|CL)gDPM*7tk=3FS$B9?Fv~K;?mvz@#{N*QDx+ z+@Wn_J`sCEPTx~NPIQZdGi6>BocASP8>uHXZ~6>s_F-i}KB#yFx<0-KMR#}ln$=!5 zp8*E;27Td1%97Rh6|`7JPUX^c{V7h9mf~TK!pOl3Ze$gx6B=^|SkoJVGRmQqRj1_= zLOV$FN3Qt!O;&w$-OF9_-!)&UsF6cfc%G)Dl<?&e2C?NM7s;a}*SI$5%*?|=8MWSg zJ|uqj64_p8bH_`{wLwUmvz;K;Al3-Px(J!_?Is8uFHTkjv4%twnfQE2r4@{BMo}*9 z6gcYiMbA?AHXoYPePh8>4|;wcpPBY_MxMloQOrQmsFVCR8%fQxFp}{l%-EaYm*^!L z6~XaPZ}fhwp<JL)B%oFmEO)b+DZL1swwO=L<wLY7u4133!oNNc#Gic*IlSr>6o5I~ z1)?$OZZHoYB{UO?@tc_fz_y`H6dnMLCmPCK{s_U!09!xif9`!|I>^Ac<#6LE^Mr|$ zwUW^P1z@tiPkaN#ECIE9)~yG*HS1A5`><2TxilN0v6bqAu-z$E9HbOsGE<)kEkxP9 zR7#<9>hrH!$x~a!kPMp_3G#h9tn?gm#IUsf0+cyzJf!=jdub`d#I=RoEL+}yo9F~0 zzgoB7YBN(iwG5;h1lq^5_JG>IE8XAkFD%87?4U~eTtRb+H3{rjsrcCkEgLw&!-R#o zdFP%z<59=?9o_IA4xyn-tI{+p6Z}ZGYPLj)eKGMqN;y=(r%<d>N3vc>Rw{1e!jDOt z84Ef*sQ&e@`+N-JW*DVZ6x7WbPjdHUXY^KnZp}jgoSHN%6HcR-NI@+Gk)imBkq~i7 z@TB`OzJoIw=U>BF)Y-!qPP3#+%EjZykEpO{z0mlBY=;F$M<<-VK;|Uymo<<qOd$YU zll>|3C%B)wLF@h5mR77oF{m2FluX-v$aZfipb0^ykEYKb%ivxxnTLLeS~QN6Jz1e1 zV&hvJX~Vidcj$%!67Y8j>Jk^gB_3WIIgQVkLeote!(A%N5sgbP96+|GZtV#S(XP16 z>+|y(8<3mc-3-RrUyL{U<Y5YJqVM1){E|}=g9bJb${nZ>Y5n#R`erqn1|iUn`>Drl z=cebCz_+$L1~iLyvPAH}j@H4l?X=u3sm6I|b*QtYWA|wn?XgfW_539It$j~HT07YR z2}N&<Y_KYCK-shW@+n2io)>?d=t{`m)8<UXya1hS7xx5=K6TfV)I_TH`Ujc!1qT<m z^gx3}`EpSOlXYk!&HXf-anX9(OMt1J4?e0&Gm0bqSYq)ctcw(Lfi?sL=FhxtC$-dU zx^wC!@ZYu6u{gkIrZb0pQcJq4FW&3x<4uV7NKr;#tz63Wm2$#c<-=LCX+RKNvb%&- zmR4|#EbbJa;z6T;DZp!5gQ)}}#oW5RjSLQ=wje#G8Yetz@CXh$8wdiuR+^5<Jc}Lp z26eV9Vbx(HQ-Q8f1O|I1=n~hI1YP~ppBhZ^82N_I&;voF8V5a^B3+MlPoG?sI;4lR zO3woys_MWk<F_w^E44i7I}m8<=R+1($y!25uNiFhSy_@b`CupBL+%d4sY@p86Jfqr zUkDM|s2pS#Iok?-46QUs+A>rRN^duUrEh;7lnR*%*IeE4BfB8o)j%&9N1814KGxp? zxt&Qu9esD%J?5r!iJ3PL(!*Eh%Nc)5QV829wn5UV<>$GmZ+;Mpp;=rzB>UTdHz3j+ z>Wu@m5Q6kGOfL+E1iCPr$QkLBsXl1!zuno-n%_(dM{#-rN7N@uH%Me?*GjWSTaI9e zr(TSMf{#JLy6s+P@FQJNg+{2;+)NN!)IH|9p^73vqJtn&Sd&S@L;)WX^Ef?Ex?xY; zo=R-RU<RWtA3e#;Lg@v_BqjUx1F2aN^6q!aa_Ls8Vz)Im5h9M8I`L?6D)=)g+ZejG z;`}XQFmqY{MoWz~&a<?5S)u%`<m5KwL(f}qjV{RGkUah&H5Wo$)RLj~nILr^r65H@ zES&01+U|#ldC|4nMw%cRXMsQ^K!NzXjSZD+<86$2J{1hHX`*k>_*$F*763x0?^?87 zCP>RU`mNenl#*o)6{0XX_}zD0>0&-3tEL1YwH38@Fw_KPqwc@?G0!Ntl7isQ%}<U_ zgaJHjnbt(S3_5b3CBu8WvfWgFbW(MNdd<Du`6K%*i-na(?!)&Dp+7Lc8)#~wUc4;! z&y}Ef%j+1@;Udfb8J?V7wX2mtMsdc`MB@<0SKh~5HPueSbkDJ94eWGWtiMk7+m+Db z@JBlloMT{8(w<koGf%lGqEqJLp?_^d41341qLBUj<Jd>BjbDKpzr+)?E6NX7)TGXj z>*;;#g<9ty@H#|fn}481Ux!oVzJ-r;^5#@9*9kF5%0ZXGNg`z{6EW!^&x$#@J`TD0 z>;T!<*%zeoK*w-1spRy0DL&CQ*z(15=-H$DdfF9L$xyjTBQYs7LLQL0PTpWaDJ!S~ zs*nh>kQ_$a6|7VPZgzJ*Gqb5zh1I+n2sFDgkWY&#puN3|VqNw<j`1dbNNy-H;9{{) z4jgu82N>QSHGD$0)~?p7Q-GKeMc#@lcpn-fzd*5d<5Z+gas6Y4!QE1X^r|Sp$8Xt< z77;65uJeQs$vJHS+9)dp$Z&@WLly{<Vo6Go9!}40iLfZ15)VZVh3^EL?NACfRl0E6 z?@6HyH#I3qR|@*WnYKy2+Cw3sH6X?lG&C=O7=;PhOCqgRCtw1b#^}NNe?biIY!MTg z_aL)q)yb%jMh2KFUA_i9{_r`wm)x(beX>TiV$ekg5wY471~H4iXM92Bg~A#9lr<Xe z$&7_K#IU<;OT^o8G?unrTg-rCG$R9^K^sJMNG;Z%xx-Cf5yskA81fMrFsKRujh3tf zgh3;ikmM4YO^YeJspk*efl&>FAGdJbb&bf5e!r~`Aw>C!8mF1OKKEa{yO>B!(h7)C z`x#R0i93^S_^H28F)|n;QlvJ>H#eWvda!=K_g#Iwto%n7W#xA#I-|Z7fns~)taZq} z+Lr1WQ|~cVPcC=~VnfabH`pv&8I7rFgm}6|wy;I!2_~k@TXGRt+nOCQ3|{`@&Eox; z#CfC1d>$mGRLY<7UC4AY`~VEnbdcbqg3yOlm1umOVo5u{BlVp{1A7{SC`EdO&bAwY zt~8Y(>_r&k;S};L2`=SICvh^Y!B4agpHh(NYBOVYgfWn<ypE~43o`ghvo6BsG6<di zcEw(nI;07@hYM@<FYqFsuAYa_zdhJ<_81@O@~eJv8cfTfmeC6{6GAc)ufx?ZeFW$G z<qduT*Np$iH4!0$hv5wnLt*axgnV(zEW;&pPpR1^L_|o-(lT<a)g6*D_|T!q4+!!1 zIW@G=&__==25u-xTRYArVhQBeVUs^O5z43vXxL7WP?B{0?O5!0d1hvXvR17U5`Iq_ zhP-4*bybk+x=gIRdFK@1T@?ks3;2f~ef2VozRC;Lpd@wD?1Cd2Ac=gEX>{%B@`S!T znE6cutuW3-6#q~F!X>c4w3(hzUnFG1TlU`Rt3~BgbuuCOTk%x~P;fz~6EI=Me)@vO z154<x6<;J;)+_?Ji}#=sAnTB|Az;@1b?1jtmOXr|F`hBA7@n{juk}qeyCpTb1MPJZ zn#~h{c6Z(k_A%Vhj8cVK*iO2uKfPz|jW|-`egS|Sf!JheYGuwlcL(gf^cveBhbhU~ zPb>E|9_GaB)_0aN!2G{_s?|c6w;4lxFiR(DLze=bwfnojVFa}D977HKWKvuPob#4_ zN+Y?r!HY&$@+`&7hWornzbI5C08IJv$Y<NgS3Ci=XmAYQA94c)`pe(o4WAhXrsTJ& zl>DxXTvUI}Cs5ePK}l@%_=z}o=ncrbD+Xly1DJqoQ=6q;2bl==n8Lf81TV2R#U(;9 zv?&2^_JM{a_gJsV{hCKA;B~>vJQ(9XyFj=D6<Aj;dW-=~wFYJTBXo~$&ay$v0f(7Q zvHoDQbRT9P<7G4@N9ds907Cky?Cm#G8Bc@%xrv<5&skoi<wq+R-WOu$6eZhHfVtxf zl<4*ZK`Soik$zE=a}#lUz(E~}Xar7J0)J>#fcAs+4kigSAs>^@;Ky1>ozGun8fc_w zYITTrjvt+%l9$uB;`0}4Ax!b2FLKJYAM~&;`NaSz-S0A9lhyVl2=pfQj2$XRmMhV4 zt9Kq;K*8XONUIzQkT0bp9??vO?Sg21#pvWCYl)lOKj{NL6o>;I7KNp@Nn`DM6MjGB zCb=HY0qcb9dRk9%ISlUvKVTzF5ZmeXESRNY8VE>yxje>A^A_@{IhKj$>GF-GjOkq3 z>MW(v*QC?8;}25iGiV210CW4X1_rBriCKOYiO7>X*~Y6s?$SIIyP%JKmM9~kb2wR> z0PhQ&{hViHGA&fSJGqeV_i}>9wR3%pfg6s<wCTA2j{mr7{(^~KofK=E6zM5z)f|O< zC{mrrvFM2mbeAw;^6JZmNwrT{QMF;zFF)w)S``9QQkFX((dsRs+4+INlikrO%^RS# zwGPk)vOsQ3Pj$b#6~Dt7khtk)Wyw1K-V|J%r)QPu_q_(4{BA>yv{Tx`*yRfG^J~+Y z>#5a-{3I-`2gq_m>BPWP$i}DeorT4{27P1)WY(K!9zle8P$h2R^Hnq366lPlLxqI4 z!%*Km|CYsvPjqSpR^TB89F)~e#nf;?Gh#xXB{$h}KhZ>AaBT|v=z8ASFMW#`GaO!x ziB1ykJHv356sY8T&adYqBSni0gK_Sx#!oPIsCuIDweIAd=Ck-h5^!$;8xp-0Uyi;5 zfdZrs9gCnrs=K{xAzQCRb62IM5J^E*jV1{&9F^5f#MIOS*<V7?A9;caK&=;;d=4mj z2Ia}*Ilq-&Cy9~C{7~e)Xx(p@xyBk{v8>dIUQi$z5YUv>q)a^vs%<Ba%W$`PLrt2h zD3i&`fEkgCqb2$(>@0aFeq`ybB_+u9!E^c(9;767+kImNb)Do=%1*@Dvo3B)Ozf%j z(GlQzFJ<|_?IqK!u(kcqq^7iVSu2Sb{vTEE9o1C!g$<t&0TqRDRBRLrMY>WAHKT(_ z2Mb8oF$hw}(4@!lr;ZIIp-7J)iqe$c!HO3If>NZbgx)kXL;d!3-fz9@{e!i1t=xOh z+4b4a-uGNW;SfNT1^fHi{g0r(A?QF)3ZN>7-WEnIFR?4yq=BuNso1C>>)w<R=NoX6 z*G~G1?HEa(oZZ=cRS`mMe~1kCjYbi&ouNAe?Kf7+DQ34uO1rna6VT+vsa(vZk5S1% z3aFg*Dq2fS^nLe7O&l7;aC(Qu)~&nGzMaUxv?P>lv!T-hHGp7`T-h5fDAr5YUO>WN zbY!29TB81j3S&g2B=a1iJbIGsLvM;9hl@5~Y<iGLQtzh~>||-3hX1BYaW_k%)duKS z{O8CEda?F?0G?4-3&b{Py?Iqc=S6p&bmd`5`u`Ut6=XE;5UPlvB)Y_~bo^t!TBSXe zWy}60fOg+H`p~ZlOT{_Hd4QpbPOa887?jrxox=lMI#F0cPFS+PdSB;rLm`?Mcc2C$ zffK~=NW&IVC9z5+Ie`+p2{n6s$-?TCJ`r3SEtg`Ze{~DV!xcjj>^DC2Hg~{u{=TrV z9jgaBgEc@*Z?dB+#6;il2Hk{GVCAG^s<|ccOXudkZ_wfB1@sO4-Lo$b+px$I;3IDv z`yyAZim3-8CCX(}O&m3Obh_^k2vANC81(k*yZn8%FVM_1A5IqR9{yc)XGHqEvF_0C zqM84WPZ{+E`g8t=-@q6uE=BDFfh$Jv{Mx3=*ZVB(nF05qO4b{kD{~s>DRliFOZoWb z7a!YQa?Xs(a>OJ-+Z(T3@dGL(7Ivj+BZOQvwn|IX$(uuWtFB~>U~!orLR4rO1iR<k zCYyPKT;Mn^-(Bd&@N`XVQutFrvU^0268Gm*O!dN63j%-)>6Aq6)j}}62)M<E4hiw) zNy<@EA=<`7G3$}!nT3LN$q)+!O*jJ*r`ph!u+{Gx^iL6OAljG`$tNqv@-J3_H`o=s zP+yJ?4*W%+&CfQbN=&c#0#X)6+lv&j^T64SXW(O(T6Hoo3s$*ToEt>#gZ54i$=8tD zhheiaz|~DZUZQVpHWUjr5<srj2IA2noDY<CGh1}JwKY0e+_Q-{i2rj?XqfZwHPyU3 z3DewrY+vBM?&Oy1kmd$JGHz!pgQ+d?Skf+AM6M`*5}y<jMh!S6K-*}W%C@i`nQR1c z9FctIL)$Xbk|#Y}4k2MS?JdTA0u88VU+4K50U`2%SBgLuxxya|b!Tp31c5@7$&>-@ z%-b_dNFBo{eYiKzwCTz{5Trej4fzXR_&kDtw?4jTcMXZ@vP0aj=0zxTr}YTF<Rza_ z!9zy6^Y_?Ecl^L(;dyny-EF^IcMQ{l-3A@SFN9itxKfTZe@+3(zS|y@n0WAyB?ulX zjHFZweK$%U(DODuX)NgU3><dmqSQ@AGsSAf&Qcp8W`WMCFDH2q{X(p?mMKt^T`g}m zV334_a?EZ_wGo*|1)(ZjIthXKyi8Z5!0f!g(eE|y&`xKU@B^f7Nx;G}cKx@i)1i~Z zGZk_qu&xs$M&fbA?c4v@O6Di}xL}~N6gT^aCDZ*?zZU{wayAyw7QY%g-XH003L_U0 zpRECoK<@$qnSDqO+-XMVg;d#uH1>abKd8$xCsw4$N!j@(gq}H+`R^d^E*^sO8X2xi zimdJqsLdMCA%EQ;jqJ5GAyHa;c8?Zm?KG%Yt!KfNGao6rpqmD9wje+>c^Mk(8`NM? z=Yr~W?yC>vb*jS%sxiYpnQ~({8kl)KB3|Rj&s4MSk4g#(<IDy@^G^ek3|PiLonRSS zOF-x^t(xa!TVEi}UWTC+44Qbwq=lA}z0Id@{}4IX6Y?z)>tCq=9a~K$hx9;4dkaLh zq$?4)=LXb?(nBWRQ0gTVkY>-_=!(6b5IGVda$JXu<MNbnsM;*(3`Z~Q6f6ROebV9& zDA2*cy&*C`X)+Q1&;oFvGt%4GZ0#oHp7dV?-+Y5pjt~1K^@AaFg!OG7Ad&4#zt~~@ zxP|(@?*j{!0OdkgSx7sja%F-&^%!ilNzm@(s>runXOXC<6{u(XjuB6w#D_8}byKtW z$V*3loWWATvTc1QlQEfLJcgZbx(hu}3FGDHjmUFyv4X*9HcvU5&P%=-w0Cg(;>yXp zdOJoY2OYpk3zG$EVwUs5@fdnre^9FhjP?vGL^-gI1Q5v;3}m9ViY{Pg?%!)EKoeBc z$Ypik5SUem&VvfWo<VuI8%>TT=b7{^*@2(q%*&%0dYdAdw(&vL-0z`J^FoMZyNc0j z_9GugT}nF-lh_->zW=L{e^&_>G4N{<Mdo2zTqma%SOoAy0%pekDY=j_#R6d+otT1{ z)OzIU*_nbp>1Rw7-w{TluIE7#ydViCGzUNu{pOo#^9xPZ64RmAK%HKt(t#a;efsN9 zUjtQ+Y%0-vkaWe1!`f<#d>^Dft~}}__x9)It9@c-U<b?#Asj7i4Pv-!3qhbhV9ONb zgG`!d+=$*vJgxxY>6DN$6M$u+t~JpW`v%_A`oxyS3C!2_v!YOCmahy0Z8LUd_9=VP zMY^Lu9XDWL?-&<`KCpP0uRm|D>-=-WJ8KyHe84^}i8PyW?k&gIG2aCIDahXpA~jA> zuN>E*H5G?l;11hN>ne65W^!ZCVUn8-^+yq=sdp3k-Yx@oS^$8a7xfp6WQ&Ja>+`T) z>dy7DUpuvf=m5bAG()HG^lO-FG8~3^n;d*8p~EqHHz&#=N{hY;I})*JWB?dL7~_jF zLcYc3p&Hy&sC(L)X~Bzw+bu-Ra}^9`Q=#}_=1i6tP#<;}5q#+fN~Zq3OC&D-)U@QG zx&5rmIS^#&3iCRl&79;BxO!uXePZQ!52eikcRah|x#9cC06vpnMI14M=YhFK7*n%< zM{PeM`%PClPIxJr%D*75M23HD*Dg~eaVyz?A5me^%CPisa?->ReXzE_C)#zQ4qqgT z>s71qSp1Q?cK#zy0u|!r>-*v@D_vT^3-}lRWl8Om!CEm;E(-p3B-8NFJOJ(t!|EsN z$bQLxQ#d=k&D>TW^V<FmBT=`8r6Tomz_|WzQxH@PgEgcTaiF0Jk*kl=p52+DPB&f* z@Pvj`o`MZ8y%J)qhT~1Yi_nw-OCa-R_gbtfuNsuN;U4mDD5GZO8YL|TF?&0=*L?<U z!(5@fIkg(L9Pqlz(8mu-Z1AjZ4VPiIWCPxnYxis=j(8-hOYd;9*4^*F>Y;;}4hnrb zofoV)Ipamh^}<b@k9!Ikp1JM9>wt!LDc-k`Z<LaK)GW;3*cJkPaNPyZXy<!xuxHIB zu4&-meB$S8`hs{Kk(^kd6tQo|$OX{#yb<RIp43OgH2M;?CvZDY+Jytk&v--VOn%7u z{94G;g*iZ2L)#_GtAmGW+V+3gqVS`)TZVZ;J*reNMP=lh8eW|Sy>(%bfxxfC=ryj@ zQWFfu#cJA}XT17l5N`C;;oHX04`8A2_QK%<gLB+mH{|=|7gxAHkV}9^PbR7y2pl8~ zN-idB4ubyO|I^1nlVh1H^d)g)uX1w%ntEXXz)#-Bmti$ByPXwTbxm4y`Dg;OVx{5S z#|2(kh&wo_z3)ZRW1??4%qi%VALOpy$FsXz(WABhv7P-CP!}NN-~;I&XuAH|aOSYY z;J>Roz;5;L1ehxZ0UXV|F_%aOm>)78qyTy_O?a@WQpM)Q33xokTZAa~RhJJ;%G0pv zG@Q?IW9q-U9v>8VP<Da4c+=Qg(!G5gx>gGM+~LmZnR=xSpbHm(b!Yei#_9$xU(!Kc z?<=9mu>%yV7xmXHNN3OVeMEDb%e#i9h67AuCA_VWOx0Ey?X-cS(z7RN8Ac}?mh1UZ zSNjK1#*CTa#o*n-<ch$X>(U!&y7jOiHNyySNEgmqalC&9f3ls0B<_HD3@~lvVB14l z|Nk6Y*ZaeE3kJt;5khCH9C%R59YFCB@VFgt&F#<XX=`<DIGECeYVXBXhG^w)!p!8) z;s#MQoa2^Nmf0p8OUW;5`2qcw)b_d6+Wvw>v7?$BHf;FK=&bEt-$!v4yie0;r!T#b zr+S`6Rkt&()7GEZ`kUOy+u*sb7c|Ge%Kq7?xYu;F>KKpV&_bO@bSjTl;=~7Sm6@RA z*SEfrlxI?I_QnV5r8e&vF>VgUh~^Snxzi@TOz#T@Qd-&8HlfQ&DlJ!~tQDp8BA)yP zN4Ea=jex4B%3sxeZfhgvzgD}PxDJy)x6@vIKKCv5N%(LJ*F}5p?W-<4RmD9k1ae#M zswwATULyEQ!J-kCAuTC)FP4|JEUaW`wqncauVL__ZEbyt65jm|f`~WmX7-k?BWfSp z9N-PbR}!xu5y(YeWlG%f16b1@2~nw*tGahZ<2`C}4Sktv(G6I$m)T?U6Yc7^1kv@v z(cRbqb7_r!IRP5aSr=H1(~ps5m6R=SYn87$Y8glz7FtKHS!teTEQFOQ-{NJ(wFNi- zsR7aDpSQ;Jg>W_Qytgv}>CZhR%4u+TSbwKq*jOd?p}37~9S~?iPh?~(Y(}wdDSM<{ zL}Ot{eskul-be`p!1v41^0c82OC#N&6Z!o^`G~eZ$o~)O=V5wxQl$XPwM5b;&&Bal z)Iq-QDo@wDeN(VZAwEy44!8!J2+A_eHi4QSRzZ^-WM6YKuU?h2LF6qthvZ2MpDD`+ zZIJ5cuBWr~O?!ERJkQ3VTH4RP%~){s2ssA+<YeL)kzsP&1ZdW7yfjwIUcSIMe~dz4 zoeRPU?K1ydZEJJrJAgf<qjOAPD4f-16!)BNkLRh3J$!&6+F=~VsbiO2=b(gsKOO9I z6vp=n7eT?tk@|a;dW-4uXt!(IExVM}M1$ef%M}OtZfuyn>SD@+h*L`d#;adRXj)7@ zkz&A<(?M9siVG)ZoB}r}>^_beR$v6?%UK5T{klaoHm2BY#0fM+76JeYdglJlZ@2;4 ztX>KIeZ?H}5nvNkYn=M%cg0dXN}tIirmMQ$fwF}c?Di#R0&DNMIUHo}t*5zm_vW83 zmQ9#K*{}5xSz+bNad`5aRq}%n?qp2C210^ESyB>rl6~79-^D3eN%6o7%E5jh!uu&y zW?0^>bLV{h;_9SF4V5i-qt-63J(8)q+M~X5A{N(|#LOeKOE}VGxsg6OlGhG6?yN<d zYft^&8FR@OjZH;KIOxn6g>6)lcB&9cD)}=|kes~y{T;jC1dv;TrefK$Cv2$w@N_s` zAbdYRI+y?wZWfK%v}eBnjqkxumxn^@c^7M62oq@Ki(l_yGEOIf5{p+ZKAD!M><H|~ zg9SZoN0<bDrQc&Iane~qv^gzL1SRi38|KhENTgSu`W>})iwd1t?Q*{M=(Xo%viR}M z=Kg(qxud4oC#Cm9*RY!EDvp?Iff}xZ{P}1$_d0ejpn-?kWR+4nII5uo5)`wP*K#Tn z+7#e}Scmi$k2xnVrM=BZi|WoE)J}U`lShV^urj@9BHGp{vn0{&2WlkD#x^{a_-F7F zSd%>FEkPisH^#?=EPkilUr{D+7rU~b^sR^8S4SZf-nbB8)>l*_f|w^#!gNVLC)nPR z^{@i<>3HkKoSm`vHp~tNJXnto^1!pgv$fUBLcEk4B{W(46`>RPAz%=}(!c%7t}KyC zzk$JpvvCbC?{-l~#uUv>sORZlKO*M(sVfJ|R$fv%HYeW_ob_z|PKn!0PbF;?b=^`^ zBWLsdWd589IFTj9itNKA?#p|IHX>*)zx|DZdCe>yp_Fc~&a^FZNb=Z`!^lEHZ}kGT zJ`NPwW>dCc;tLDQDrG;xI;8-PS@_%Pk-V%)sI{f^!BMA+8)pXxUJ;r1!<OsVlTId% z_7Vb2?{K9S>GwRYn|C-cLdI&?umw(WFT=Avibxh6JscOs6S~XKFRuSSrC0(TeCD)F z3LMTbf__i9-c|>2{eO+aQ<CO9!Q?D`qv__NDJHuhh-t?6cDsD+@3+~1_TtUkh?chW zHkA?Z079v1;9Woy3UO=uNdwOwP1K%22mjY{fZi+;wP`Avmz+GJ5_i?<R0A`sqVhc5 z2ehT~b37eOoi-i%V5Hfr$v=Dk`gtsOkCa{&DR40`5H(H@1=9^u4U3&LWMbF^bWi<~ ziL%>SYOg{*%Dm<$?G?D#7TKgXu+Q>zbH_--P~(2YjJNL|fK(A=EZy=1!n^I8m^Hij zKb`)<1JT##lk^U6S$>*ki~BqyIB)4`R)zL{EXU9)iVrnj?9OHT7)T%E$9P7%ubE3h z2wk4Ev_UyvnK|@Ng3pp(ZbYi>Gtk9vGM>6(A&k`gz3K7beEyrQuysLeRit_!l+fy@ z6Qk0yBLT#5DA0r&55e@hy<aG`D+brv`s15SbpuW-Q#0j}n@7xpqtiQhgVsc4c~C+p zM7nMzOHela<qX-)Fh0eMAdB@`dC^J!&qdG}xu#QS<7^lhoOddP-w?4T!UFF=Y0}3& zjK%dweRu#GR}1k8<5Vk2--&zAgLZpPcVbuV>Iuwu{v(s5r~mL&g=cFZrt`s;tE&$V zJyX6_{f>g~ucF4hFAG@Nqd#j2*asOZ$~>&&5@WPK(+pA!mEKLiOXov5lte$ePlVwO zK{_w1q%OrULR+b9;19U|eB?cbxmc|4?!$<7qkQEh`%ANgfFqrkd{3J}nM?_k>>rzb zS@(MEmw;^VgGBV+MH(q;Vc=bw)#1lDQ*vPA<n$a8DX_U3p8nAC@O8)Syg_q)5<JMZ zP08D^&;JGNGy<$kO|SGYR|D{=YR|qr<M|#4M{19zvte%=AsXq>J!Cf^+s-_%oahm& z3_{yk+qAr=xETm}o0xG==DWUtC>~{|bueDj46e+-<UNhKTQIHbdUwL?vs16F&2TQL z?r_*?>Qi{yK0Ffr4!(fyskmL?#2=vlkqoxCrOHLzX#W*Ycw!rUSrXfO2{`)Vl=zU< z2b9m}Y*1+Mawq$}52IT=nwPa3%$GZ1iT&2U4v}Bme;;5M^fQR$&GJV1R?U?kd(*Bs zOR<*xPec!+!uYi+wZKG>kkNqE3B|<3?4o>mTKo4qdeYP7+JPW2r24@wz_9qNF)aSj z!I)Ks^+WP>q8cA6%z#TtQ~$89?{Fr}diHFL@tXoyKC?XgxPJ3>fgKpw!^8CGkZ&x? z?0Os2N?V;N3gf@^r40grQ!3>c*i&v;fB|*+IyNH!cQ6J2PZ$rPm&k$Ho|n33ru)1` z1i77i-6s}p%_N6=w(mf!dHjl3`gq7NHl8htfjaKzphC>=y)Ej9wKLe8K2i1pE+Pvp z%7VC@ad9zv#58un*C;5AT^Mhj<bfOY;B5oVQU<AUJ@Yhtv*qy^sZQcGUjq*sn~dK< z57mp;{`p8A@v-O<?(-kydL?hKrBZ*kRaxqy??P(<PyD9HbMJEQhqQn_9S!qyr}psn z2@uE!?(R+rnJ_iPPVB8EkDbU68r=TR*BBdkQFQH32+uchW2&im9lQ^@M(4Df^;O;z zL3|H%XSW|@IsE~K-z-reK|pIp@Y}kNzlRQO15=-K93f3#N6e{J&s2(n#t6@*M>bVe z2bI#v;m+;bQLX#W8ZE{E4uW7o_S7_ZM$W=fBVUwR^Y9woTqFSxgth47oz<<pi+ZDw zJNzCZa(`IAUE0#Ootf`Sx6>PjrN&Jeye!S>>(Su<;SQ%5TZi5g&Y`LoCE^)zJS$G3 zNtDc;i0NH@gm3Y}+bx)+w8Xd^yk`=m<lDoG6$n6<o%8Kc^lIYWNj`RJ)L=A&yk`Bl znZOhe3wNO|^Wy4~gU*E&Od+#xRM69#j8noKg^`6)vM5#~{#T3^tT3|rF3*Z{d3l4D zMCZ}p5lgS8#u&H+@3&hZyn&*#O1UZQ2TBs=GGHHG7tNl63m@8BJThr`i)SU(#vIP% zK7mUv^J|slqAwn0AeLWfGEWIH!=EJm!LZ#yUl0-hH1Eoi_~&-+6z8_#l!>zAZuERY z-BG9P`%enn0{5h$>-DWerr{uw-Q|z1ynfrQauRMJRMHbC65{{MpJP8=FuXYXK~AT4 z>W>HHwVT({wE1a+{;wUd%R3~R74Jj1vV6@jHai3+9g(s`PY3!7{*C-zG_|rvL<i9! z^zO;WxMCep$yqp3f`|)cK<PA|cWu+WnRxh6SdcVGeF8Buh;*rhk`sepG@D_`lr{tr z70^FKtwWncaO40+XP*sMyw6#XzM0C<Uc3tdr~Sqzqt%uBk4_B`o>vad&Oi2S%wudE zUyqo4UX@y;u}b#nFz&Uj-`U@`f^f%6XCAU2%rNRwV|W1%@_k~IcqsAH2(0<4r#!va z5mNJ;(ZLQoS-=u^sb!x`^9FPZt`o;kFG*tHz#pF3R~@YESt){I89Y_JL`WOc42tKP zA|g-X@qee9eA&o4C^I#uLQ3;v3_u=5Y91#z4~c{5c~bk-q<MGj2HyZnM0Su@Xh{W7 zcm|%9GXyQ;1I?yl@PD$YXMtIM-8(8eYv$RT8r#Li0{r6OXj?7p(3%(`hh|q8QL$Ut z$QFQc^B?|Umnunw|IC7QJ|BC-&M(6rwLnc`8#ob9$us}_KmO+;!V)$)E{>xQ{X;mD zx>v)_K}58$4zJcA4TXSc{f=)oR@B}X?+i}0B|KDuG~ol2H4r7-oR>4%4#61V9BGqr zhJmN?MqltyaKdnT2RxeFz736(HVDxzTR05pD@UaC41jMEl5dXu=pXvc(~b$thN<b7 zMMCJ_kKtfOdh;n<^$$z_>~~eeEq7H~)+Mb!9kib?a!~S#c+xK~R_=OOV(wYfzt3|y zEm4=B#y4J;cQQ?nn<P)GhBB#){f2@g-a<Y|R;}#`3`V+tG_jWI+MrvMEHF#7pFylE zItypo%fNTtHi^?iz8=BVfrD0vIY`atRT8CZ9jr6Gc;ZxdK6x567(?;w&_>&iK~>xW zZfpcmx3L4yF;n4bRH?;&F|e0Sh3Bs^gt`p`o@av^&vYAAm@)D7{Zpn74*B}~K8)*q z22?rlQkwBvH?D_!UmmKkDzA(A13{q6<DMN^v0alJKqz$*pi9>dEwrs!^AtlV<-y%u zGJq<TDuF(=<Ro@G1FYVVPB%6;YJVUP11KjJv|}e`Zc)DOw?VU?P8nK_$6E*GJEZ)r zWyYKqA0(k|?H>(irSZz-tmdui>yYR4wHHmO>_BW%sGbM$eJuQeS!#hIcilD1aPr(U zY0D%cUjGKz?XV0mRA9JPGoMv3%VEjzKr+mp-H+&&{FTWY$XENGfh)R8bvCF8qPGF# zD<Ges*I)&Az=``GgYvE}^&C19xS$K0<O`mg&-Q%SE5$NRC6c!S;#&E!@O!8NuI(Q# za=$|Fp}zr^7TXvIq6VgCP`kezYr)5pNcO>heAwdzb@c^2!4I6nyUGccVxjvyT$yrT z1T8*=U!SbdZh@F4Y&cX=Okk24A4J=$Ys4Qi5+?Tx<Knx_8Lz>SIWQUq6Zd_iDVE-H zn~XIzWjiBxTXBPr@maScrIr4hf@bgTo_Kfn^n0Xw$9yG#I@-)eEOR?ZgDd52sT8g4 zBdI4ijvZ=9RSAqoT)@%9tXv{KJW(EHrfxYV=e+oNyR3%p-Ca?4#cX6c>fm0HMpj~p zCZfZk3giPiC5Jj!E?^g$-}5lVo5ny}PFZ@Gzbwn!Q2CSJX4L5U&Ioh%o^d{4AqrNk zuTHU0zT}%<|C7@7GklqJ*;o1UTT0AjLkwPfNhsqivS;N65lg59mY4Hs%sgMA?;kLu z0AmiZLlqU?*d-?!*OU|TbUiDMS4ql8$uQR%UPPcA7tHZEZ|mZgKVOg8%1Ns{kw`aq z&v$OGyd!;LZIrDfgs;vYuhY)U>Am!qVKtjm{~6zfY$1h@MDi+0`Y1svW9_yQiOMFC zY`kMARhO{c47!@N*$vHbQD%SGN*p_L1_USMG!k#E`64FuT*HnLKqG$`P8lk9&TT?D zxNE@eXjbZ_Dx<cNomYxrfSWSVCK|lXB4<F!BzGKYrWi33We8B0EVFc9`X6KonNVZg zVHuysU|(RKvI<vZ0=p%#H5i0Ow*5xwa{7x>S=Or)wvwZNKh~_Rfv8UDH@C7XCtAc@ z3mS^4?tEsRThQiI1yn|u8-24w9bzS<geY>MpmNYKIWEZH|K7#tCspAcJ=+tvJhvqf zvAR<19^k{#y$aZw4ry!q#N~cbY{mR^dzJaw!!k0h1ZDs0?5HP<#OWwra*Sh;@9M5K z{4p8*`~UN34eUh6VN_V$W^JUX^HkZH$co#sL$WtcQm@8F7!~HdIKXJ^Fe<jy>OAi- zopE)3{$~LuCT^T!vsZv7s~ty)NeRr5w)=q<&8}O7!c|WK`y=-}at#HRAuRN_vCQSl z(!o`>7h?p7UtXm%Qmx@(KAa3E3B3vuFemkE#heF^(vKY{FYVNoFqHXas?QEdU^#dp z_s%MWngfyFkFHUAgTM^UR)0a2Dj(g&+^wWwR<4w@jSto8-Q5*m3BhWm91|Rulq)sd z?n)HcAsL(+-?LCEkDh&piX&si_Q$OvkE?w^*wO13{iR6+aw6`x4<QV6fE8z$^n0dL zhBworjydxb=6bKGljgFZ@;FM0QJY#Q>DuZ(sBf|aWD4>eoid=~87~O;UA_RRnFu3) zdvjs*-N@1YbXm(psFPta_nh*=H+D^UoOX?cI(fX)<r8IS8ws6$7;PlpO|_lhG>el} zEfVhK{sg9u4mV^_zRp3T(ChciZ`UjhlwGW-<11LNPy-P;Hhz<g*<%&Xc~W0C8oum4 zIBlA0Q6{sQ*%@Pmz1hx~?#I=z30JGrrLunBoS#Mj;H}N6Puzvk*`k2tC5iE#tFo1a zM@FmVxt*qvVOMvOR68#Wu%U|G{J4SW>NQB^3^hyPsTe&wlnx0u*DB~}PWh`)@eWyZ zUn0DyNp$4q%Vew)r{dyx(QKWM0K>b<yzh#C{(>~T_-B}M#Y~axBO60M<p{7uw#Z&7 z3%=T7+xPMJm6y^ukhyfLngJDeES4-JnacIb9~i~3IB14h#Sg7>l}gNw@+gQ9LC;v! zDvySozag{bC9|#I>ED|lM`UIDq-vUhupMoal0s}ar^b5-=gM-_4|j&&sdpN%y3fxN z@v71zeffm_u0bHoI$wk;$}@*UvAxXG``32#Wqa?!73beqs_XET{Ov-Wj{=q!>fzXq z-pJ((vMW_ihF)dnQsf_vN%Rl#lCXio(|Y&@y6S%c1xLWSE`Pf~wFdzFkOeFwWH<m@ zO*IqLlIC(7+bf7~=bLmGO@bse`!Hj%GuHMi7@DKBVWj&uG|~|L0!pu}yKw+J_3y~J zlse8x+n`#qoe!1T8i6?$F!o(z6Sg(qtd0tCuZDENYE#McHI~zkva0Z|pCuuSn5ERr zY@Zq&t%UArjZX^wGyjeuj70to6lKU4cg)O1VtDmXAcqu>-cr9M)%(jL$5Rcng*jHt z>B+8j=vl^}{EHVXrZod_<na!QtKrqXo!kRh-D&|ROq99)@m<sTP7^^C>KdO?>}*wP z*Dr|VvtsI`;_QY6PhWyp)uWXp^;UsqWyV7IS@KRvDz#I-RH#jRSH5<O5*aC}q!7zn z>+X6>Z7N6gs@I|jIx8il9VK$viy8?@eIt-B3keDWC^JL<4nr=_d?-Yfv{DaY$(cvB zDf$IAt`S2di$iIC2J-u(lWF2mqCacxj0*W?fiL;Bah3j}m$-3LrDov7u6T6s@<*?x zqx>VspvF{q@u~{=BP)8hJp<KpYy*KF?4cw;4Y(c1`@Xdpc0~=?Y-_{653$4&!-!rP zo6G;xrY$ah>0qoa7l|Op*ImHbgkKtxX31_HfwG83__1I(YXHWQQ*eMZ!=iXY?6S~k zRx$zRuXC=Sp#*IA_cRmQA}Oq+{)HK;eD-+S(=LPD-%zw3ZHlf*y@gW--vK{t1$?Qa zGjo(vHZ2_-QknlP)X+wXJZZWobz(r?m)7xdw#xHU-Y<KPWh-{<M~7Hak%o$?i6)7A z0a)FMlbBv}<MggU_d)g5;wQ#J&A!enz!MkwHDACSU8L>^H}HHpliddf6HuW0)9L|P zBUBKzzPV<O^GzL{QAyk(_S|Dj#N6k#Xh;1?!k#*riiH`8kta>|Oz)JY>8kpL<CcD9 zGa$zrJZJg(ONfW#zPj7lkOZv;Mz3B<($Z_daPg?<@<fxNfuSJEOMAN1{dn~UZssIx zCHD^~FQn#jeNw%WwJfefo8nYWEcgfno8t-EQlouxfh$smhAq6Co|nZz;K%Rqml1d= zvGHeZVkl3jd50L3a6<85r{_l{7IkrI?4cNp8T4Dz2NX|Cg_?-f)&`9?DZ@fdy9Z?9 zgJsWVSq$uaDvWBMfY6|+MUTV_&)=Zj&u_ND-gcZ;e~UK#@1B*hy4^8+!Iy%29y{2# zwmFEwm)s;?+lPZ@On1iMDD1Eyj>41-b$`yWYr^CN(7}IjW;^skexIrhYM3sEZ=VSa zqVF4pCE|Nj@@7vL=zPDJLhe@qeve5I3zZ^qde{;C{}~K-;TSzs18*0-gc|YYhhIkf zRX-3lxOC@Ide5-oJ~uK9pRf|k$o;SV_>pRTdNZ_Vpq6}AQ21x~HJq3{|AP+A0ft71 zEaXFN+MrKs1Z;h~Y3H!|;KqgADXNR?c(-)v6v%hCo^08~Bg#DtimKvANg?0ev637g ze%h;pG4yN2CT5+|yPW$apKpTw&1<Wa4FiF>%rH1*Gf-U`FXmeSKzs`TaSIfQ3x9@? z9lgAih_i1|(6)-{kX}lOS)LaRL@QuSB1PB0U0(iA_3O5v0^yO0iasEFfl%wm&7J#O zyItCJNg2e56QDT|Nqy%Cgav+<-jcUv=SlwB8rO2il6Yy*-Nu0zF7$MO;zi<bjDSAq zFD=3^s~?0*ZicCsEdj7;+oODt<SIu(l{{NVirRtDm%=u|2Td$gBXv&2pM~Vz20IiI z&x8!VzkD=>vSu#nRBa_po)hdYKKY3FeJ#GFds_Y8#Uye`wJ186U6%?JAYLVq`(tBA zZdT&7gQ|d|k#3kDCqhZeTvu9%=?yHKfBy-v7v|GN2VPB7i-5VS#{;Luy^}Ir76GW6 zgU?czWdc`hFh3oTx&IqMPz~?KSzBOl@)9XqJ9Z)qU#JB1;3?jCS-ekY-h}#1A9JaR zlXu)N1Pq(0sk#Fg<r;<2mr^35+P$uP6fEfyUOGUfc?RE?EEx0i*l3187`&7lHdv)w zn??F7co(Yt=kaJ@Vh*{#u?S)U5CWzVfOXH{&-t-3G|#SGJFz;eVT1BK>gE`iwt}^p zCp;eZOyI$T<ViXy7y8$ocXuW%fAu0EoX!xhi~-qa;woECVkgcx6PB|gPEtP?Q_)Xp zpP;3_Z)+mnX%<ixA=A0@fmub2g2)rq;cnFX`!|i;JSjt(WU?E(A)DSFd#jJ#tcgkr zbLM>fD-f;KtdyKMzL`O+)w^`NI>2HpuN&lrgO#`Bq?Wv5ML6Y@`m^8_xu=bwP#SE` zvzHbE(L!?Ys>bP2T_RdLM0L%d9>U`CVt8vCJ{VH<Tf`|(Z9YznDe6U2Bs;c)0!1?@ z<MA;cLzes?|HdQY6MHk=#T@o{4IMnN^QR@|5^(||fH3(jAdft~EKZ6Tj5xoBh|jpa zx;HWHR~4PWHprC$1GL&1J5hF-6FbS+#b6U!&B~HOmfi5+?+ZlOO~Dg_eYS9;s9~v^ zGUx&;sXPI!RHq+CYs`PuyXcRYhpZCMlHz{S6c}HR!S#+$qm#;=2K8{pv=r|o*T|L) zJY<J@XD$o!cXjW}JH;d}Gf0NpMu^)afC~1FmL2aD>Q?NeDmeX*FjrFer&3H(`g)`v z@U{p>y`@u+sP57{A;xO*@7!y1$(Qr}R6ZI}lg29UEF*!nHgDi4t0XkaVj(K*R8c0m zHq?s_$VJBRv#zw45y>;3ugtM$>Le~0)bn(8f-%~EzX0C#VG(4Zv19I~pXwj9it-zX z7L=&_P<b@4@g{?@V3k6A`@;A4mG6^4BI*YKg_bxZ86NtlFsrGr)bC^GK7Lx9jPKg7 z6cXR5LI>Me9bSp?n`}D5e>?N^v!twN;Gf@?y3?^l-NfZpGaGURWQ5LK;H#BRjjWci z<a#Q~8`2rtbjow!zcOP0XPWJiFqnSeamYc8>_b|r!1O!b6VPq1&-dAkn`fGi@pUyr zYvjic=woyk8%jZKsdMHHzxC{fSqC8ckQ-(8U-th<nA(%zt{gZd?7vxnGmo2MhRG^p zkct}SsSvm9zW+t#IAbM44Ln^f>f@&)?hyAHW~}vQWD4c(0>X$!P&<EGIbeZpAa2z6 z!eFq5sG5~A-V-@9x&dwUhWPy2TFR-+Oqdlh-M!F%6yMHgZ)Ls>g2;7;drp!_xhLmF zBOQl!g|Smrvf++DaWD`S(*USTC2-^^5L6AsF049HayX=hs<IiqoxCa#%ohYo{3KE1 zHwk>HJG?UdIhj7$k<;^IGpl}RB@%K&!rK>GaxD*glm0$W^^6ojL7wf9k}A@4GWRz- zPEI5cQ0$qf(G$-doil!9pBW?+Mov}H<AkN-R!tz_`R;IHXjkmzejym%ObmUfFi~@z zNA!!czZp1!%Q=KrK3YCANk}cUgVYxYqkFwerz4(~eyM{@p0>1@M;{iJ5)VHGdKYr_ z)5=Y~Yqu|O*~SJv`pBPprjY;4`=#BWP587(!W+Beh$(wblTme1+PhaW{smnd(V@`+ z@r_*Y8LE%$V{)mUw08fYHf(!GJz+0Km)Hs=aLU1t*JR!PsPX}>kGvhqD~J^#!&O+k zH<)5&kJf^T*@FZ8$d#|ZW*>>mp|t#EL(b|B7ubbn$4(GR=sU`AG;`)PpY%=dxaFV6 zV!nm6-BKo8k(~Z~ATZ1PEMjfT<(`5ALnQE)m~H0=nTTZKm0i3+G5*vXgz_j*AAGmM z7WD2{hSyLsT9gSR4Op<WG~g-P<L~Kua6VU1{v&w-SLY>9MIz1d`uKH$fL~g(qVI8# zL$wW?r?Nvzwa=R=2j102v2RKJ=1pr+&}QC+o5a<XL6kEV&x_5An+aD+2+$<<%mH=t z@SxB|3j$FYz}xabqmfH~#{~OPFa6TMAmO=oU$wzWS;^rQ=rCINI+ZEzBB_q{84I&+ z1OEw4UvA&#d~_m1T+!sJ<D2bMTII(PttkSp?!-fw)OTeZk#pxNJs0ZLg%I<n;<DkO z5>sum=+-6_wEda5Vk-_OI4p@s$z7wGFtvSJR?_F9<aCdUjc1xRavSUsml*zU?^B^b zSl%ZVCIEz=G&hElPng^ttcd-D^7^{|l|%Z8<o?5eh+OULa+2-ma@i9HK!-)@0SlBV zFYvgF-$&QXaw{DLS#ErORyWq8_wTp4ZmjwjvMlSOx~)Q)hZoKcmuI~vG98De9JzqQ zim4>-hz28d*-)6}HedWs^qdTDP|Gy}a?5jfqkjE?S@*|#R6o{6yU$N~L27p8!y$9o z{<0rlHe|W@svu>*BoTCynNoVl1!r}iNOBY1iNM7?I*i@&p%aTwXlk(u7VE$(LWU~v zu&_r<nQIHL@&6;wJd?E2%T9A=qx+N~z^Xd;!hX|VtPkSvSO;tgaA~I^`t{UESL6ms z<S3B}A8P$LJ`=F;#s@RpA!)9sB;dMz$wL-wL~GmEKemm!yN`DhnEIQ?vbPQCEuM2e z^ZoftWx~O<3;uZWRw<qN%J%%PPw&d>ZerH%Xd{SZPB_^=;t37LANfTqWsW{wtwdV+ zLh0GaK?ln(_#3T4TMk`csL%q$oe<rSPqTX7A*GB?{IU4$=r)|uvN60^DutMS9;I%B z_SHU-e(K`n0#eWuc5-k%1l6fv4T;pXt0K(p6FOSZQ%8kWyQ-=#AKUKC6UyH~1dh%y zUGCG6Q>crw`<Xzbnl%8g(pN#)VUAQo<8rM3Jt3jD{z7OZ56k`LaXnhvwvqM{BAKxV zP6zfMW>-w-B?a_-&Ami>9MBqpW2bT|i8NWyE(v(glhB)6KtcC;X5T@$Jy#ZOgu$Fk zf1o!fw~B$&Ff5~puET)~&;X|Gw<|nC;^wn)GNI9<=|+1g@z1L%Fe6i9s@znBP@~Jr zXv~>O7x5^Qxo#1rvuM$=K`F@SrDEagz<UH<j*@5En2N?_m)sVi6>H>{=y^TZZ^HB* z)<m=+e2SxWg`P(m7GZjVA~|As%6A2_g>%chfshcdHiZm#_lWO!_zOuU{AnQ58Bo9u zo;DZkRvewUzk#epqVhwXx~;b(JZ5<~ZxA)QE!=~x@h#MC)-g?op#p7et6@^?M#Njv zSI9obFR>@#2)}DEJ{Zy9y9a1+)*sNax8#cOtER5##`FZ$z%q*uVV}XbcI^Hk<UQN- z*&oKS$Pe#LIdrVvW5f)j(;_cIIei*<YIOUW#!iltGS`+Ca6RsmeVKn}0$=WBZ{Mrc zIUVBY_R~UNhoz^Gltm@up+jomky>evn~mSvRv7Ow5>|@hRo&y>L!hfZH&A@FP7_%i za?eYGStW?NX73`R`BC14LX~6?W@?2jPRw{4>#h+=<N}+}3@AIOsLOH56n8FzI=FOO z{qvVtPcI|O%uwCYK@81KDs)Rv_gcgA^!-}QW%V^#_Wr#VeB|!#;u7=RisNQXIp{=^ z@t%0o61}WK@|xBlfjrlj4<iy+af?ZVc{BKsY>Ka#rfQ662ZKP~-6}1|A}||G?GMKX zP^hbRjHd%^LTx_j$~b!khdTRoW>0gzdV_6Zi=XH*#~TQ)!JgKD?$!8d@47tG*)@F( zAbJE)Z2$Rg$tR+Z@ET$fOQm}^#t~KToRsM-HWnmzyPrtyxsL;9ZAGlz5hB~&rvvmm z<|mqT_wQ0UDTX#BT`R$T*D`c@3|^fCwf}X~&ly}OCpdVp+^u!p;&^2rbZj$DW_<pc zw;*BH45BQ$4C!SQ-&oQPN0YvpL3YO5eA>n5a~~)pdr8PMZ>ox#zR?$t6-_WR2tEI# zu)mf6%G5@gIoE=WX9aOXt8ZKY%7-k-+H^z6P~8b%Y*uhyHkqF$jMr%;#+?3XQ-Hl< zsI~bIJ4x{)wGwbu<J%62nra8Q%Kxl6XaFv;U&uQQU6KoQ?XN|Av@W{~RO(P7eeOvE z5A7XyXc_o>hNLs#7E2g_;?7qvEMGel^yh20Vol;vMArN}9DLd2g`Yhj#Yzr`j^QeA ze{ehZj5O%fwUStpEwb;gWM~)F!N}&?4^a}iB}ZF5o}`bj*S)6aU!G9+yu+xqsqn|z zk@1kvzXc+TP5fT*NjNQgw9G9`8F*N7i_L(bMSoZ)SXbe{C#PGvCH)MKv1}i-aRlMQ zFans)PQ+g6=hOCg7aPOI+<Q07LRS0!00ctN2sGimWyqcyer=jev^dA_5g)?7hs*zV zg}7nU+CvfM`-?dVcXvziGjlw9yHz^$tE5>aX3C%4HzA@+$vt{jf3nAm$MVQkaLF5A z*#0v16T{|EDlKhn;>0iE+wDox6uhL72GE1AcH?W=t-}zB?uzXbLJ2UCgKl2{xL7DB z7tZsxaL!~G3ftB`F8&DCCRNzthLxB-O#)uI(QQqE9x0ZFKcIBf`xhv>J?6L3mlmtQ z3T+fZ4d;P^gwUu|b0A**4>6=#v!V`Dp-BG+kV^~Nvit|rMz`XS0jA{3=RWw!u>gwM zN$r{};-xvOz|<VKn|>j{N*S?i@%sL-{FTGp_ZovpzbHn=msxdhYBJG1H{}-tc5W9; z&DpO^)sOijSN$c=ln~DMM+L`VX8J@;xf-`coDHLsb6v{d`VNa1A%SoE3|{|qNpdE_ z<oM<|ew5?ce2-Beh=aZK&_ylK4M?lg&2u`2pto`ShtSjE$S#Gd$H<~eO<KDD2#MsO zWrJ9{K(Df{Ea=ACulj)6p>qy3fFRk<J$d^Sq_SLzPnB><^05-rTun(Ecd$!V*nH0s z?&n1O!V^1IBm&u&;6jf{5!jIF_++yCGe;)_kFRs<Xw~;b5|-=i=t2{zn?xmqP*;a; zqExCX$ZzvZ&nrsb+Izz7i!*At_a-bf%`M8lf`V@3^<6`(qk^HM9?Od)V)}2TYTCmU z_c%^9rch-=-v40R$l_(qyY*?|5edzAs+Ns8K(7hrI+aS6j)R%GMnnAikj0?d2wQAB z#xFUp#V_jW-ROizOlhh3!J>6E>s6M|SQsWPXnI2^iXT<}4pyz&bT2@<zv2%;3yDOq z|H2ZRsuiEc)`4;U;Z$O++yh-W)e^(bcf%OZV*b+cm_1FuhC@<U%{Z^_fUrON-q=Jd zw*Itga}~Du+`*bUh`}gVKBBozs%dcxfj`cyxS3*-TWTd}v29OY%BHbsBC9>2n%v*x zz8-DQI7yvo!UDZbtwg|#NAh~rjaCN~U_Y59#$gHCE0rS5^$>g7?ZB7qvM#ORu}2e- zDL&X#F>j`6#6dJ?H5aRlKQ;V_Xl~|%02XKR@YwkyuzR1zUoCbGL*;kw>&X>%p&9Hw z($W<kd!c2#-sj@44mg1srnnTy`B^}v?f+X)lQr~icAlkXIljU76O2VwkqYQpeKGim z8`?S2{~}fMgJuBAj2qq|6}M(DXtA!q<%;+wP~{w}X!S>Ve1{;-zi75{1||>h?vxcm znb$L1(qI~pw)m6d_n7r?Jyia_rZFG@xAtR+u$A4=j3O<qA5JEcb&hNmMPtL)cQEec zMG?2X%x<HAhYcm-%z?Q{ehPkX6{yBDO3qY&;74N@IyU2TdP+`9&K%Y$D(yH_`JP@? zeY!!lZB!ndC+KQAo+W9WOwZOZ1xW8*;q@TN#}`_~z{A7}D=J?X)Ia2YW*4Lasn9|m zQN;HidIU--(Iz_~W4T68c9`l0M?+X~n|wCT;7_?vRK?rmSNe|;CUZtN&YD(TM1|LY zm<gO_pSmdxb5lf^{9E#q8}H!<Iv=Ucd`n}0Ol=_SZrlcPb^B<5Z+P$!#e&*?9<vJo zQ{M*tk+bmAqWMNH#%RU}yDuH%)nxT0z_>inNM>|HM->>Vdk_VqMpD7e_!TYw<2Yp% z3(ai%z$y`Dl)9}cAK5OkwcA)I)7F&f{cVni8DpAyKV&W$lTyLigkn$Yj6cP3ejuUr zTNe<_ByzgPAss+epa;InxZJkLGhpnj{EK-hAvs!5kbr=8d348wn1K_j{eZszLYNZ2 zYxLJ+^dhU+|1MGob6<JBAcbZ4FkT!0H*BQ980W+RoD#JyWhXqck0q+s%MUY&lQ}=P z;t2WVHt0+;Uzfm9ny$*Fry$7P^*^h`T|NvX^f_iP_is*v9UI+Kc2@(HcryCJf@)j; z;t6F)9H_KHg||V_gLBx@g7GhtzgXO;iIKofcK#i=YR|_yIG($o$jrI-9tj0>q>)o6 z{dkyXHcgo{Y08r>LK}#j1w%BowTa<&oZ;X>bnp4QN0iUM0SxdYFI|GbT6ufRF@{g1 z>HK`+W@HQVj)<?%Suc*Ddep@rznua<G=&Wpsl#~U+ip3hTq4T31Y?=aqB$VetDM(+ z@R1>+>d`p0DRQu!7v2CB+YR?M^{W4D6xgSB2DUp^JZYpG?xF1Q9Oga(s|t^B^ZM%v zyU#oHUm#3+`g@z}?BxsM3H7I&<!ym91rq=;U7D?K370B5Y82!*;p31E(_oBL8$k<i zDb`i>j(CCyM{I;A^g^BxT+(}nu2iKHuYDXY@p9Bq;a0*%V!PqB1Q;TNOWe&3`F44) z>*}V>wQlr9C-UJH3DDnVIiMB0+S+k({$}l<M9eICcuh8OJE7|V?iTg)<5D}sQXzAL zTLjoOXICoi)csQ!J<krCy$J(UgJ|yJJuTMIZEHN%-qgP;NM>|~rn8gMO@Sp@dp^1w z*KnZx6&Y^R8b%n)aeoFOv-q;NQ&kTil_xZP)AFBMsPzN9Ns+4A!?i%QHC=9#^%9H! zK7=sp{G*YpObb%EDp*(+ae8r#z}4G*a8Dq7mqj?Z*$nhM>N~yzeV0{tI8gQl26>SD z9lL4sUs*#ZN6iLM3dDul{3EVe?5%hoVPoP4+!XS`{pZv!Qk27@p#*pgp1G<Cu(t3T zJ%TQeXfW|YIM*36z3?lg2HCnhgHh%j;RV{LESeJAv5vN1>#rqtArT4$@|vge0sQ5R zpTB{j6~UZ<{gMlbtdn!t5ckZsZaby8s|n=P7|H{<E=@C}f?ef(!Uz^~ZkOi^cvJQV zb^@=2io|;k5;LX2Wjwvk@|_zA-YWp;>Cv@fqrL?`67W>s{{OWWPY=6jNGnwy<j;&# z+dhIolUEdQG#7RsgL{7YkP>Bo4ix_^|I1tUj`q@Hy$4x@+ozb`BSr8c#eDyM(#kn# zMI8EmF%KFFG&e|la|Pf2rMx<b`$Ji<`&Mw&M=R>)x}0~9jsaw9{TIL6%$-Dr%Z~Si zG3Uk@NZ&>8%yD(QNW_1E+a83B%pKW>3wZuw-joTv?b+~3A8XFJe6#$;J4i)agO<jf z)qmTHm#r2%wQ^EnLKOdX7=9|ewyw?j2lD7k+wvE_UF0fQZfstS7@$^S9>lNbgI!p- zNR+QXSZQY7y<lk=gGAr!!vc*tch#mWc4V1G!+>6FRt>}?vhjl=Hx4(Q@$ediKGk>c zvheI<jfdi44Y0lQXLvPf8+=h$_XnAnTX|cW#CXwekMuT3>fhbMzKZx}-ifg?0eApW zZnp;R#*T+E6IjK)e3+d}+e+VV0IN871NvQxI@r=y^wXuLQq89FPeBX!jFb4-AFWuu zprp=GszK{&b@ejAHQMlktdp5Jo>Q1-+Y_xi@2k{9k+9tRsTq!*JiwoMb95^tx+>?X z7obbk49nMarAoD5K1@E`XsgEkW{A0hd3+D(n0t@L&X@bJ2zVrogkiIpep}KKa0Kt$ z6Jvi3y`tEY%wwS@C9g!uAdBFUi4(ekSCoTqfJF~IpOOU!KRX8Fpm?lm?RqBl-<_%< zw5~AWl?hy8JHUfZ%gacw1!svc+mo&evS`05Go4$(4^U<zaGb<qcol?|)b1czHu96w z`Zu7%zT66ZJ_~ooyVDVWtaH7vHF#+v%r0fa>>OK=TqRQK%xO;s%0{(h?um3<qv^Z5 z(Y$DKJWeMDTGsxWM%ZaEdHGYRRoc9Z<;wg_F^H39TF@LKyN@LNHy6%y(D`9MDDTTG zek=pV@w6FN^+V|IK#2%>j2E!>^yO-3Q8Dj1gePudwp)oEATlT8$^6VBlikTl!5&cA zfe_jYAhgre=feZ)k((#yEI4)E)YcL3Ah(Nb7F(cBlF=(8fC>+R6eh>sRU}^%EV{@e z%8bArKd@0Eogzq9fX;2Ijx1DH!iYR>_hi9jw&ef?PPx7^bRD{rgQ%!5e}ga0vZ~6z zMt=NP%gJr1b%l}R0z{CjIE)oH3`t!ug~6%gUZuu-7BDqQlW1VME4g3ogU>m;eQoTP z@D55AL9}@lA0sHKz%~mb`{h^<A4}cK^$1$$>a#Lc1h%s6-D7wBMQi+*FnPdCKFE4l z3=P^1a`jM$!=4g875<3vY32`r5kFUNP=43jsz^y89ZCMo3>lzyz|UNDVfw8}r#<cs zLdGk4^HZv3)8)?>Ru>ZDP3Cnd2MDjk)=|#<!TguUcE_Xb*=fRd-KU;*jXV`AY7;BE zGglll-!s{3oAu?Nv@3UbHP-2qpTCzi-aGNzdgI@J&zDy5N#?iyJ>P_Gle<<_-Jchi zx9NP~_rTv=hO}z>r|)-S9et@Ld-8Zu&SaB;8f|}L!XxnH+v2QYrGwL{Z{ohA)A}w3 zoa-|Tq6@s?`#fjG1<G4#Ec&u7VwKwu2)<1iU8h<5vvFyjws~;f;zLcMg@Sm{F;4w4 zUxY@Q_Z2YmG;ax#1yq>FUR*S5MSHLHlx!YBt&O7wQp2h66@l8R2|b;%kx<NWE1W+( z*Rw#$e`?KtuwFIw7>@)Wi16jh0(x^wT(prbLKE2PCpg6X=@vLK;nOF0&+o3FMf6iI zWu6mmhnN+vJ80+Q4@q%{^-dfQfOmioS-^2xwry!oRB35DSp@nfYQH*@XmLt3gY7dc zxn%N*2c3I9pPFJ1$2l?I%G;(9wmEkl*^ExIX}aNm1P0M}?Is>NJss>xtv|1c(5AM^ zQ2+$vs#{RbK$tY^;h_No>0Sp-<mvrxC^?wS7tGTO-OU>l(OgVC6#xnxX?#e9JGMk0 z-3kGkOS&rfRwjmynLQ**T>D(cuf-J4RzSxN`<&zi44=vUn;#A>8!|Porot)sw4sHG zdNx;0lWh68tZe<>U2mE^!Il1YNXGOk<>Ixi1faZ0DB+s=!n@*)O)|a*BM72<ZxKXk z$2m1kMiD7bi15ndd^a51MxF_-)f%yQ@RZM&_J$@%PQNl~i3LiJBV_SN;lSc1xPfW? z2Dkwv9mCr@am~lkMII?2qMCOQC%*p^0WpQ=yXCm~8gMMZ$px*eg4Zwei0(C{vWBXH zBl&!IR+th83VF}%g=5&8YvgumorJ~N#%t)5eTLdAf%UrC73s$xIZmXV_Mak%3X4zB zVEUai>}@hMode6vnH-0bCIOCC$7bApY5Prf<`8Nxg6hWx*xWuHew6tn=fENyKH&<- z(08EQhhBnnD?HO>if788VC4&caXM${Ck^4DL0tiP?D1*y`IIzX)c6&1-c8<;1Ydr3 zBL|m&Xr7H8lnh^(r*O_HAhi2s-ziz?7I-;dl?b}r3DU{g1cuozo|;nm?Hs&XyBq5} zMZePz=Rq-lD5x`!?X&+X`25Tq-z-<4zSmHYwT<Qwi1o1lQ2|SL{j)&Y`uHsA6e#*K zuIQa=`$wlfLbHdE7%<(tdt`&}lV-x_Iq9#x*l=HzuOB!QEAygxuKlnV+}|>gD0*9^ z59n9BiOUOOBeW@)hN_qaO%9oMfc#tFBQ35Jb4BxdXBcAI3J!$-4vhJbytHUSQt&_U z!Q95b=GpAOK%1LgkD2Ft6hUn`4PV4QQ$z?2S0H?DVZXrot94{z3SqTU1?rI_KKR}r zyv@CQ^JwWS(Q{ckQ+;M|M@njlF{Dxaj;`%P<30GV=uc992_8NTaUINL+d%dJLnZPf ztSr7U0r=B@DARDF4Qppgz;&%{f1QR8?<Ss^TvrY<&zVoSA2QJh?o;?r)e{M)P$%Vp z1Yy+Le_IWdbNn6)oh$e%G%~XzdcpXc3j=)~V|W`%-EDzepYaIY3a4a}lXbkwH;>*e z<{*!js=&3rjH^lHfJG5nPLrl?;M^Ac)(P>9yP&(&ts@q9qhJ#r?j=hLWWL$J-8}bC z=X$MWW_1&gEgW}C3-aL+y?wX~Hj>jj)elC-H?b8SSY8fa&B^fG)v|CnjcCDd#yocT zco_)A4t{ZCdUKsb-eo^6M7DcCk|}!+Ry|e=9&<y3MJQI8AcX2|CUkW_dVv`<x5K)V zk3hiF$v&G!*aqJwe4f1Gzo3y!=06vyyEsItKLVE3)fmfw(;{Gef8`xzMM{*}bsP-f zoZT_Kt9an?pxf-`Da?x#e^B}3EdKMh4?xc7-Jj4qCV<>tgENQXdvs|Z60LL!*1TGs z7ltO?PX};DOQbU3BRXi(qw8nA;Vki@8-e@-K9~sre*|fD4GLC2fyGV&CA8#}_`%OX z8lg_@G$%O8j(Nph!wt2mr=23wL?=qJYQ6#oxuQw&=QN!H^ZufL3e2Fj_OaMzW|yXx zD*F8LzBzLbi_m3ZAj54fGmJ|$;YH{8VNbV<pIQ?$ixr8QwQ#})+SF4fEoHEXe}K&Y ze8azCaB3Ef$HSLL^jYp}5Kj6&p^+t5ewL%uJyYH9ny}f=RZj;Pd2sB;L7H68!$sVE zF~xwtiJGXJ``!i)?M7&xGWL_5rl$s)asW-u`2{`Oa?aq|ucQ?q9iJ`=XTe-tV`mS* z%Flgyd^hXv2!G>89@^M<KF!lkPr!h+5()x^`mz6ysqc<!@@(6_V=B}l@c6hvq*jKN zBFK~->p(V&f^4h^C`*~LW3Ae%Kutsh1R{taTV#k#2hfN>kR{s@WrK#jzvHIw`~JTF z?{STD9OoWg`p0K(8>>$HuccRG{~q1i`r8+xR*3%iWl&QH#6sv~$+fZ3$T!8Y2r>KL zySGij8l#x6I=z0%tsOHGIuEz+OGnzZK}8mIBBNKU3o`cMO@lrA%*dylvDA+`MnZl- zJ*UoJ(Kv1&O%Y>Z93U}Q^nQ{((+KjF4>qetMo;CRLX{lIj+Vaf)M*uS-7ib{=H-B^ zzKq2$)YtkFF011LnHybqFQfUnWkES_UNnd?DJmnL)Bg9wgyhfCa+ytQch&#Zdf>wE z|C<YNO+r{Zej=)Xa5x`$k9IA6NFHmfp$k<`>8Ef%RWN;Sqs_<h2Bfvx+|is2&U2f* zL7lx`AQ!r@5X+ryiMoJMv9Y!mseGpHAt55f#Ayz7Zd)B;)j1TQAJJ`(nxGC2qU;Km zrN0FH);?9Cz0`Fh6>%Dtt&jg%qNUZ9#@}yJ5%d&WHVPj!Ug{I|MdCxt5EJbm`R_<f zYgx`tK6hqpZas&2Zk-|^y5sS9)}Fd@mysq4hZO0pzDtkTnQVY!YfpRLySI*wX2|+3 z+ZQWm_YjlJ*8YKOdefFp3_HGFSpE5%fZQ)x9aF0QlaK!C38ds_gPL?SjbLP*M2n_H z^dHt`bZ*xh4f)BkLT@ucY(eAb<MxphF$aq&W3S%Aic>UxmjlrG3VsiShy&lc1knA* zqMC<yHtjK<d;N@nfeCS$p3?yGl>vG9sX|TOu^{thr*)#bE{1sxy=03;O4VApVuaoy zFOYfz6qaqr+0CjfD(W0(tKnhhr`uW<?VTOtM;}wYZ0oz4WoRCZtWiv$OjAeZm>-)i z#Ho+y)=?WdFGl!HaGKZ+V|pG|x4U?z07&Gso1wPm0-*~^6y8%p3eAbE6l54JNQn^) z$HnT`13de#IGYN2%V+v{+GRrzNYwj^M&+K7{2L_azH*H?647PB&kAuLepGs9iK)TN z2xFqTAtvZ4{bcd<z$7UEqrt-?X-{?rEDr&_nt0lKjbCPmbu(<dE%L~gyacDeI-;4l zJ~Ow49x8Z|mYA}UHgxgRWA-IQU!tQgZk50Gods?Lm%m8z-vg<0e0vf?gw&-58Y3Sy z1WDQN?9}!WFvRH~nySU1eUfJ*gPUhBY0<^Vw8;P)DaqiLAl-?QW@QM-FE0cPOgtCE zLzi|yt6Kd75##-EkjQ^1sD*ak(!J$B7oWYg9cvpt>@U4jZ0z@R@aAx9Ch#O$Wu808 zORyLs@)NEl7?+0`@Z#g{sqhnYbA|Q+?_CEybNGmoy)x_(>!qX$O1DfgA^TA*RYFk; z-@kfdh$-G>wY6@uHX3ac`AL7BR-*qjaYvbW7d|p|fvQfsA)=@dFr5L|)0^BNx!K-Y z?Lv8IGP#u34{tUR*K_OJnuwCqpfUcwcy5OxtGIjoAa8PrDdRb5juFH8H}uM*DOXMw z?WfYa!L0eu8oSwJz{#B8xz}vpX(@Y*Gx~H?i$H^&EkG^M1H+h^%4#WFl(0yk)SVf* zN_N1#5o%YQ2TAE-!soNA`k9dkF<iZ!e`2c$MJcDTyGNTpwkGccyQ<>Ip0d4`1;)a7 z(w|ukm<9<|>ET`!2?DWt(|HdMR$f<Tt74Q^xr$|(9Uf%;mN#~mwt|JMOSY3}F%m<E zchbKo;ao?}d<l`ZbYCPgN!c$R%Bwc10Ap+1)v~1SdSL~XFlu<Of~6Vw;Yx}xyIz_T zug&lBTgCi!$H9wYbiRdOLk@Vc1=-P|cS>{3$$y1;KRky;trByHbdm^AY7d|#W((51 z2L}!7bx!cJ9%uI6wVBGmIKS*te*l(TndHL~X0{)MuHfi~8z%tzt!jrN^?(PdoGmJo zQoVC$c!yrXlOs=ik}8qH07k;1w!3$;!A7#I<N8ZqmBi@P=bMtaW98ABGiVb?R932O zA*w9N8_|0AnO+{PLe%xOT$ARnZ>FT>=q4CxPcL#ePXo{qYkRg))i*e0fl!0;e5FZt zr|X=b+m@ERxmC)3j%;8RMIsU6>I)KZ%XCquFs_xe+b-0>t2tDiC}``n2<Tf_Uc8X! zl?9@y{Q4#)9Y)EblpryM`NEvt^<hzbw2{SgN6DEqhYWz_i1gEikbrj!dY~!Sg9K&X zg#~)?g2m_^md|$24vk4!bU#`Rux|?i@R0<1k$!3{lO{e^K<QBPgX9NP(^4<B;*YJb zAB}HxoghExI{Xiq6hN~qgw1F1lse3AjfYm_Jb+s3@wq2N`<8I>&cc|dw5se|h`3$V zmlT!hNu{}a24seaTz+eRUWBap8++63@s9U!SRbbCVs47ur<&gDDd!HgNqR2b`v$$T ztM&O4y!KR2$bnYv-cwtoaY_5cX8IRS_ld3WmU%6kv8DGIjfUC?fDkV7H@qdT|3>5A zlKV(+lcxr&q+E=pSBj$Z4p7dZb;ZxK(c;pZZLOg{u~z}Wf(z!^ErbL8;hAfcZpdG$ zZ-q=HB7N+VBwM%KP7V1-gsA~PVAl^%UeJZ)P0g4iIp-6T*3dp_IN5p4*x;+^<l<?Q z7`$}d@pL3^gXWG}HbapN>V~R~qXbn&@*AHztx0XnQII}pS-5+4MwL0VnIwbenjJkl z*M23%XC?59i%O8u5o4WW{4DkCa+z6fN+_IpM4H@K;(^D(-NNz?8u2uciqg~Gpfhh< z9#Q2rhg2o)pDN6DpotzqO#0{Ies@79@VNoL<w+6Yu8@=UYt)IC-qWA3n*`0ddmvQW zX)wz5f|cDc*W>xvY8cr~)kDPEZt&ys#WQc4GC&EOEr)%i#vcw5hpYyE@TXsbhGNCs z3rgav<YadL9sKbvSGNe(Gof^6)_XTP6>t0Z*X#QsJ^5f0dM!}YiObVy;!L$S-Xz-y z5^&M1c?0QI2(qSMQovqrcpD$lmlUHvpRFp3LOZ%F*xH1Pp{^+BzA0dfFL1yXRy^N? zUM!)Mur8h*7tuu7@)S8x4T+MQmwp5xkqgL9p2AnT6Ka(#Lw%JME1`sOv+X<9w|ld1 zNhnRfWLc~-`?ZZBI=b=$H}6!*AR|yu{@eLJ;_}|mrQf~U?yT=OPNXWOdBgp%<?eif zq4w7($8QR-0qNELnM0lT&l^~rCFGAVq3Q(PD#{q2rrId)o+S1%7iKNWT=}pNVcgZX z1j|R1${*duIMU}RoQw6L0mpU^5W_wG9{SoLmTx=JRGr{xM(kxB_>iE?qONT{2xh!I zxJ%Mg)12R-?e%A_eUARp=Nj@(>(0Em>)5kajOBte-4ErYly0#-_RT~kU#@^?AS2SA zvC!{FF7S)@L}=7YD(x}WcG(HEbi86*L6E+bb&^>+rh}<eE2nP^a_hJ_wOWwiJ3B<z z-jDV+b4^GFNPp=|_)X!W!h-h(46Cht(D&mG5t^*#lV7Ry{jm=CA}vQpiZoT<_0$PV ziO=ZsURbR>sC>r8>)kdiG32x3z-~k#NJR2y&+wd|1HVp%#_anlcJ=m^C;i^u#{#TQ z7VaJxX`(p%E#>1jOB0}1=Z@L2KY05Rb`!~+Or4T{E`sOsuQI>55(Qu1=nE289fsCS ze6u2yuBwWqQobh1c<5;_8R_g@c?_A^AD^pZrOsgb>)%M)8}HP0C5{IVMPuw>5zh&u zi)k|Zf)ihh_S3$B&$j#*2fj04pganKwcN*u7ilqwv(CT6wCxpA9v>DEA=W;hk@Y0) z7LiKPh!#SgoYS2(#66&lVr-`?e8hp=$?u*ScoTz}Ks+Ny)oB_s(aeSro#L6z<h5lv zf4s|;^!CNm^`P|oTd+2l|447UXXq>beSQ|mcj62?MU|8R`45@GS=)YuxhC3A98mD6 zO(n6SdQf*Gy0*_>lL_y`>RmNH%L}sZB=|w=nnGEL?044n8Z-b%nQYL}FJDlEoS?TV z=haSYv+kH@>X~QZc)zdqC1q>W12lS_3uwQ@=03(c=LY2%VI}%NC9HyYO`WZ(`uu58 z(sw}D2jj)`3{?nUzqwoZ^)v2O?vrRZe~5f-leYctTII*7^!s4h=6T_d$9RJ~@pJFl z9JKLU_QgY1y=kM|qS7!S<;g#Qk^cc1`ua7OI_9jwKW7I#879Fp12Uwv$S?0z{{_iH z{ArR<8Eb8I85sKA+^{v<^ID<T%Mj$t-<g;?OFnq5O)Q1d9E(f3ih13px+%waB2@MK zaBFenp5?^<A*w0)XBuh&<#6f);;bc&f(SbW)fg>HewQ0^HqlaHQ&EnWNj2iKGe^5r zA&FY>B-D)8C7@2MjUpem=@l9|RTayPthoD&J-R;<96P(9e7MMOL`#0o1F~<q<(R*g znEW=lQ;BX~bYFV5Y1wdlkTI!j%5I8AmGVr*Sgi?Pm((hAaWjKl_)1F$pj|-_Cc%ou zi|a_zp8`or?=115TLpDN;Jk!_N@F&G++@ebEhH>@9)iBmOy28vK2u?GA11ntqd1iW zikMaBnF{@Ko;P{JQ$GI;bH5g=)3h`_K*pOu#$s>c?`%^t;CDIE)*Tl-sG*L#7#Z4@ z+Hl_^fy8JlYkGfsR)m&el`k>Nv0(OSuPlf}VOLN4&;$EN3=*uesg>u+)xUoL5;xC3 zXUn@JC+(_=Zl)-ARkVvW+>%UferZ<AG%CZvUBkgu!-@Av7`Vx(_})U5YMCO$-}^bg zv_$@dR>I+_I^!lf4*Est{J8a#iJgnua^MNCNw`3<I`^Il?%e=&TA60<K`Q+NNmB1* z$fM}1o@f3`n)IT3f8W+8AL)nFI2UUE9Gs;hG?@Ue0A8QHLEt<IXgWIotS60kdLT5D z!;Ct^r<O1kRg{*TZ@{{$t|o9B${CsH;tGJ|8fgq%O&Lrnk_(tn6C<wHf-gnXNwz{W zEDd%UdJq%D5yrSl3o$s*=*$e)!bYP3(3E``(+&5}1b>?22C0(#@VA8YcE+1tYyLFp zu|(=|U0udH1!6-^hGM|%e%$*-2R+{GGF)DSg8;5!37pUZdzBWxO;K*Ejy}JO9|X5> zSQbwP-(v57!m!w$L~ZV*g6q?vmsy8Fky#(c<V>J3$&cLduh{)_6gC-DIW0q^Lu;Gv zlx}DvV!H*WC8K-NKslA0nxv(2bPn^U6@_cDKN-Mw<^ydA8j2=v(*$ie)mli4S4jLE zU4UP{`zR(Yy;I#6ADoGKP-Fs&xBtjnNvICHfV7M^tQayMe~;dsY!b3L+<;l3Cf!ej zzu!-8HXJw47okI=x;XVd)h-Ub4-rgss`vfj_;HIEYaK`^9bN-dY{tb-SSx<vQxtjS zZW$#qXj&}BI=XV@=k~Iw8{FnzIgTocv9`Jo>n$y~vgeFQ!Ra>CwVoNn^r0WVF!q}u z$~1TCrLHBaF3fF<ZQGn<JQ_v{F1ma8@CpED9sR{NO7qDvYypmDmP5@}&i$XY7TkC# zuJGs(?;1NzjGYXdDI!(2k!J~0BXHJ+H*0)cv_n2~nZ4;SEpc`?4*P3_2!($cl~y9s z9!AtHQ&|MuEF|u!`@$F^RVR53x?`FtyhS0_g|5k`d_)TuH+m^T<45vlobtG{+{Dn8 zn}|U3fS$$OVywoll9N>WITAC`YDRJ>N^uoS9(hie$pg~}Y<t9hwHHbV{t(M@E36+} zu%Uk+(1Tequa(<3-H*p|lfNZ1{o|9UN+RUh+w1-}T7bIY+m+90T$ZIe?Fk$ev%{as z1D@y;Hi7esb{Z$UV*ajHo1#BxO3dXbX$KX{G~kgrMQ=t^A17@z&=AXlbCdOB4X|-+ z^+=?IMHk{L1T9S4%UmJdn%_kUn*0M3j+8iD_m5E7UuJ(Bhn;h~mb|0k{{8@Vj9d#v zq;Y41L`K^XVd}nTGvBVt{!wnOnmY2f7o5hf;LIjc(M)N0_yPC(O-d5I4aBZ~Z`Qs` zLZ-fQF++>qE_|*k63Y_@AlQgG@2Ohm%f3M({57a&?YIz)67jW4Qt3)q$?kyB)#M|7 zN`8*KOKk&0_;7ZOGU^erG(xUjm+InU@{2*aUp#rRi?d8cMwt}I^_-cf*D|)S;r>oN zOFsG_?m?utuRPIh_}|;~PL&$dJjCzf=&yCh?Q4i2{a12jXV%w9kPd?I;R^91*~JDV zu`Mm+sFKke+Rl>Crv!=XA93?hV%li81}bgoj1Hdi`gaSw1#bRJU;1JH+5;lQhAM32 zjNF9~4W;~A&;KP{G19li^T$JNozY!Oe<D?J;K*BPDs6Jx<48VE8Z6J{(1L$=@_tAL zw;t~P;iF?6A2KHY{jUe@+SSSUI7U_NzZAZ1UcysHkKcu_Vrof^b|2^_E$Qqh=GOK2 zM?REQ<iDJ76tTHS-ZB-d!W*D|sz7bW8`sUBBf?=3875fXCxuAF(u^|ueh|o>WkY_f zQJpdInlKf+*3d$TG(HPwN5jkqSFuBbB*TKA32}beB&%nhi4><JU@UwOgmQHve294_ zV`XjT@c6zNliXR7Pj~t-=SLR_j@SSTA^l6Png}h~vSdebztzgk`qZysKc2^bxp%JN zX03D?+tHWbv%Z-`?yN=|ANRcAl|$)_)l!?~i2Bl<j_r{yQpbhuBR|OHwBv&Tr`)~1 z$^GA-JBy@0>7J$3aU1&>DG6!RT}$36<P~p(hnEnklT?++XdM8_<Yp~j2;}8a@zHTt zIEl`Y0#*_>JHk5E3Ea4fW|I4@bw}e$r-8$_b`LIk(_`T_IR4lAe$L74r;`6epoLqy z<AbM4A7q#bxTt*g^7^!hGHsGde|V<O44HmQ5gE9l?}?g0!fi}0OtrmXT8{N|cga4> zFXRXNkL2m`(LW#<w)I15(fw2K#|3;nOCC?*zQupI!x-^fH#m1iglN7_eo~0Hq^5K` z$0h@dtK6h0C6%ad0XeVQl}e#yky7|QjjFD6b3ReuBh-|nVu^uWCwjCJmCOZP%rjag z{k0!bX?5C|M1B;1RXcwNd09*2&QS<dKnpH$Z(^Je{x}Sc+@p=Wx+-YP-M|w{SDnqJ zbczqGd{XV(-`q0iexhzPu&s#Nd>hL0C18bubGi`c`*J^9GCqr6x1*VQh2p?4XH~2v zu)<rMubG&h%0Gm>&wRtzs$ELILupMfk-l>JEnBwE@+JR<`x}3;I#zr^nBD=w?r?Wy zyMJVi8Yb8$lcXx$peGcUOwIp&Xwy03U*y+xi6p<T@o`ExvwdUSl_SggNb12l;D@U) zN+zdYy#<MpioT=s=)9!38g%1ENTDK~bSf?Df1mOnFw!f|#BVs|4<k&<&xa}`;Yv5E z%IXIOLbDb`NDQ4Jw!~toa5mGl+y{;JR9Is1tdQ`2Os>$0*?UyLMKaryKTQ}uBl%%1 z<$+xT#N@<y3G;Kt%FoRn-43410?oTpvL4tNXIDT82ZE&5zSM3~q6f~`!w*NoxZf3- zPa@hVR&`o8n34^Uv%&xgr|xQCbPUonEPAL|q>C=hvf}q3&5er_#)8Bk1dse$xHUpT zrYo4`K(GXDUTula_3SZ+qrpYXqBn&?EAx@C#N;&28yxT8LmT7rWmnJ|ed@{bP44o- z{<qyJtMilKd)G~9p|I=#VP3xfPQAH7EHY4FE&izk1-U$z;P_&TEbBxuJS^pS<iH{| z|MsxfA>=xVci#scaqJF<A6fdW70ME~>NW{WrqHx}B`jsAmGA5Ba?_R4qRaCVs{tE3 z3?Q9m{UznPEl@-jsbzt6(&wPc);4P6QlAx%eBj2c0Ev<N@VpJ)gPbj(oBKy0@70&I z`jXVL4K6>MeT)hLg08~*pepGZa7BnC)b*6#Gz>?!BX$U@KV+{~Jhx|Mn_r)+N{~qf z`Il2IB&L69rLcO`8JV{z4qvCp1Nqn{EY<J~CDMcobxdpLY%;vSBOj&xrAUKd64v#W zpWss04&@?qm0$^CMh$NXnrY<K<OmUtP@eojIOogY#2V}+^>}{a6Z<>C3vObpc;J<; zZ)wPvW}Sv>b5bubh1l}+uhWb*K;l;Olb6}N7E+&Q4c&~EN(^;9w)-q)gqvESHwRG2 z`94rXCG5R*BXL>YH4lbDTMx<poM(342YJF{`sy0_Fv+5D2qIPIKzaXpb2p9B{8CUr z8DF$&@)QRg8xw7~FQ!$ZQ2coZ+8MkW%$5~*p~#;nmn)6Y7&;jw*$39E?U>E*W(c(v z9<ZK!?yl#Vh=gg<U9=$x2<uBWf~q*0{hm5M&)(?$_C+oKiHt`J!Mfxo#<4hy&w=_z zz)f(Kk0$_m23|&lVtc4m@t7Wd`VRsrmH&ZHx{%a%i!72rhc5QIG6mo#?dT5cPNL8* zlb2uRLsf5XmKBzCRV7bRsRGsJ{c%ix#O(7+U_yTwK~DG<e9V;mn8*1L4Xk|?vT95! zl-1_XSP^XZNR%`V5FCd>p?*2+Q@-1l{Q$5_*494}bVu_Rg~(otXJ-httxB8esS_cT zSerq~mz6?(`}|CqrIKKGOLHQhT;X`EiuP=qj=oT0#%j~Y9;bOs4J5MDKwQsbwf9rg zQbfnvut*m>{(8fu3LSkiv*2q|Vpj~}Bvcv|3U}=tm=T2@At!OfEVZ%)7B{gs{-$`= z>fEwgSU;RJEK;Tbj;F1-jYmM8<ma?KwY6uPt(oiMt%%MbuK=&w1K4z06#ly(kmg+9 ztnv!pNAyki(QTXaPF~NqMnzKmCd%SgcwZ2BO8z817~i?w_;oq!J3RObR+C~ybm_o8 zS^Cms6mx7D0F;lU>R%Ad8F{)w_nY8~EdM&s-U}Hu+z~!)fwU9WDfqN3$a6HShHJ*F zFPRim;bV6L*qiHfZX)P1J|e12xSW?+Im3WVK2rhs@PZOZrQ+h&m+<#O)tEU1<GXmw z{&pRMXnCI1e}r*m^PNCO=3A=6w0?@?3l}lgrqR%!+Bu}8=iEK)CmA+g$PG($)#T^< zQc|%Zgk2mmHrbsV9O(F>YA$hM@7_v}o8s|4e*Hwj`I6}d5FVehs~68?%$U3`d3nJ7 zJbTc=dvghPA$4wCFZeZouITz27i<C$7mK5%zA@3Bj&S^aY#nj=YwbKk8m<q{`veoe z?HtNGK@ehnccd#xg%J_Qcs+`Y=>*H!Ee`VKAxu8P2p)I&lkf_fu7{!3P{tw`zQxH{ zUpvSS$fAJY^JI^+3xMf4-}#AfY`3q<<!4r(!(x}sR!;B($?fWGSw58<+rZQC5Tf7O z$poMk`CYiW@IwZqP8)=`o%_;RvM+(PTHXieS{s^zvO+TEk-Lzu)?5vlgq^Fa2Pblu z19bDo?5v2F<Ci;ko3v0k8~oHS!DaA{>d89bjt|54d;mgs)Iw7HqAh&M{PMxQ3n|!& zBRt13QbdVr<Hk|brx0QHQ%RL|&b5`x9cwU%(4Yyh&<^pJ;`s|Gy#6Qe6@bBh$js~0 z2)H6c0ZOC;4FLNR?gsY@vY#FeBwD=Lf?%wzq;t5IE>tzXgs_UK%)Ft2_GBwy#yQII zwkI-7@!o9grhVYMxno9zUjzQG3HXb+=nxshQ4WBB7~P|VX~c7r?1y&v_nWu>_iz6S zKo0_WlyOYx)hK()^<0dAZ_fdT{#wg;aJtrBwlp_?4;!<-=THoGb8?f&hS3ks<f*26 z9{2woKW?DoqRD@KvfEx#rWM!ZXztll`{dK=Lk7&7P5chd$tBDP#DE@@d*yKh$UP3K zp9$qz^hrFCa;0p@D3XH<9(;K}t08_`l{J<sTv?CAGU8B>-<iC}x+FGGpNhE?{B@EY z15Dant?Vypa#4OWYwL&?EzDSyz67y)k<3{_oVol$GDAmAE1=I)a;N$vajq}P6+Cs! z+TRPJjvyaIP5|Ms580Takp@c(;q2ybfoOlT5=<T>I1fnXJA)98tX~y8pYjI*U4s{f zU*`~$U#l|ksozMUrNBBsx<Uzp&$ar&YYk89r*i+B3()7Osq%Bw1hT4@Qb~U~h4mMN z`O4h|L7X6Z#X$;-c<JtdnxENe>jH_%$ttp#2gO^3wUk9mcVbSpnl5z3+Kv4Flmp1t zU0O11V$gBHMT<Tc^xIjlmUx_2ah66gT!r+*kq8SncSa+!jprRU+eiD815HZ%@_{e3 z=+)oE+m4s#P-tdwf3N*4;LF-)lJm7n3ef$@(QbC930wjBNY%oVyf?486&^51RuAJ` z4~x=mgP)ITDer1#^pQHyPAN$dih-S2uF~%M`*zytxwp#jB8m}Km96o?C6o{{+K3>t zg;Y%1y2`0)#K3+i2reKs#pm$xqSBvc7tP?Dm_Xzb1Y7Iz7C*|gxp?Yq_nnIV!9MYe zQaIW8n-`GUG&rja&ZcR^sb~ib$uN-&VmMK)#`&&%;)&yBz?LkB66+3sWhC~kubeuJ zOEo2BCc(KQ6cb-M@&H~f<}KSmqVCkd?v=nbVzQ~qtKPM{AZ)mD2P2>FE;6U@-C1Zo zrT6FoKgVY2)8GpKS-tX`M;e6#9o~&9uPe}r`Q@Y8Twh)3zC<gv03puJgrFU>5#$<> z<R`x&`~0=vvR3Sf{B)|5lxM&p-osA}_UTC?wlg{#ju%!H?1`&MPYzIbz2&u#yOP=3 z9(6D9qSxpewv3RMkp@R3!E#_ijV3^9xkE@>`Jn*u9Z1zR{Pmgllyp|SLt6A!o}7*! zzPEbh0?j^YU1hyGBxc%1gd++1L*-B(UZ<`__SrDD4IZ6%=gpq>04`hoZ;YAmAH+e8 zJ?)W700QI~0$3XF+ov|3#=W_dd9*tgX{pN#X*{j9gjM}0{{%EjLP7<ri@8+eOE3KV zo%#mn&m9kJkaR_ebQdPiBHWc6{)HBI5-!niVkn|yCE%5*i&uh<S+?62ewQ0=OvY#H z_yobaN8xQU)_kB4Tk+*j$2Uie=u_Swgc;g83&Nc1e$V1VEU0s4z$<QWd{wnKlaNe_ z@$)CR^)Qd}6#LP&t#y~C{5oI$s{oP`I=-ns<C_y+m9K$cIGaU+yiCV(0))FuytL*Y z$>~LO$iZG%Wx7Xt`Fx5HA^Qfmbleu<`J1GsI_HUOq8X}8^6c4b(oNN_t8$aYdvC3{ z5@g*fsdG##idzm6rmnQRkmvfsNF;7snZ4JWrKrZ>r5-)@&)!HdX^yj*f#1cd%edtK zNF{Xt4+L?%ZQ?I><2d7V--*z8otlDS;_?N=+1JCP=yCzX4gddv3nZb+YXVB0^0w1F z6*^J+dgBPOwhqDkisj?{bQQ#k$YhxfH5*6q;Q1%g()<m^LJ5-}bH*ReKikNPU0NJ; ztP;T(L^|j+EqLdu>rY^v!7a8`M_mmNhLg$tTlVs7=Ib$pf~D_B*;g3jvwL$_81w`f zG+H_)X)EmtMeO<~WS=4Jq(mlW@7@?i<rod$dHYlo!>L%-I-1tP;&ez32J`%44<2oa zsAw;U3p+(F-x>_JkEA=+3CtNV+#YR+cAp)*ioJ57n!Yv)u3mZ58$54Qm6GB%N$QkX z<L8f`R`hl?(L*12pbn71XL>t9TaegtFM2d?TNq1-wihU6r|xm>?K;nRlkfkmcQo|> zJY9X$5SIVjFBQGrlOJi?NoG29C-{pNx~HL+c3$4jN#v}3f9Nxi{Ovuenuse-?=fEL zS7x8RhA;`umkeKBHUSDbj0`UgGQ%rB!%o`qzP#}OA%7AXUcz?VKqF|dPge@q6$_}H zP(ixL79pH`YKZ|ApriKBrSYJAh}4Tqv%2i-%AoVD&>?X2-`}?r$FQ~$Gp3(gFkDCB z>ZRW=_P^jY5RQj-{O?0>>&S0g`IFm0BprW6W{LBj@iHB{igG_?@j@M8$7Q<gih$E# zOLjut&N*@_F~RpCFOH!h4z*!K=EHf>$kRW~sCq^@PW~9mEB$_|>g?Z0(4$kV;KEE5 zht_(y$+PFB|J%pc!Ww_}q1VkpG;Be5$W+$)@I>O$VF5S*$w)~<4y3!$h!;QN;A>5h zjrD^f>=K%?j%TN4((-#1gf{wnZ6l?ldo!stQBfX!^s_L%jueI9P}5g^9^#8v$saH2 zSS)0CN{5`mprdyyS%Z~7YCq<?{y`9liE|4h$-PIim6E7LZn&H+vuhd|UhW7%$=k#Q zB1QbB*dq#2g}8|}W~U-8wI;z}X)#ruke{F|9vvZew|XK4LE(bo+oNwO^xrUy*t2C7 zvOVEfPn8=F(IyzfPW=9WZS>&Z1gi#yk5q@ZQs^HirX<BnUcn{EsW12U_G<b`j5wmu zyuP2zQ`5^ks6%%g;S*#XA+v92j|*@1TXZ3J8MBQ-jd;Qq{baj%HksxrMx7DXOxi(5 zDnLH_v@$XxkepFd5%X`HxC0|nix32(P349OJJQ0AAE0B38u?k!L-P^j&E3k;K<yf5 zPiUw=?|=?%P34X4-QB&&!*rmxB4^?XtPZ+Whu41>D{ev#$hsqR#_YxVN7l}hdAvWu z_G<*NV6_I$Gb-e9g!%am!K#*paNHMyKGXPsK*=C=^UFKy7UwH40GQRXFg%KOQyu_X zFSm2VTmM7m%yvB3i_T`^SiF5GlG6!}eu6!7fy|2O@Aa4-(-I;NMl1G}+jsd|<R{60 zCg51Iz0nFsFl+>M+H*x0#+S+r#Hb6xYFC8v%L+u1gk`if6}{g{zwH8p>m?0uaBRm3 zf28$7E`AzC?)(;3%g{=JLnfYUvw*)g4rEPtntY>gQAEwPJ|W5xfBRn-e`HY-*Ok1F zk(n68WYL*}%RO~gko`=Urmu<^QLad)jGlTLgfw`0n%vAQ(H~mck0L#rwQeFx#xu!2 z5EH<Nx{n!clJMG5i4tfjS^=PWRcEL2D8}Bb1c_Jv{1rKUI*SljQww2RgXZ{gdQ{|* zejL#{fjZV&oTGI`GtTko<b;%Xi3IW$1&FD%bJV%ZR;xxlkU4xh64<pY6@>E%sVWV@ zm_&}G^75~jxueqPGeW~Eg<byT(|f=2|Fb(4cm#YZ(t=N~Z2l@4n5}9XBr55e`2o{C zSrPje_~U=&??`a%x_{Ce3sTC(dR>uJPJ4N>Tzz&%QqSH}fZ%gnNvL=D57;@go&4mz zbz&=Hu}*olT`8~8d4hph{XepH9r}6GcdvGCJ3i6>xGlMD=k6bV|8w#?nO3S_<aGnj zzwS><m91X&J>?ROF8{}W^}m1g;P3yaobewOf3i-%(VrO~)BC2>uEATpbh@}NcRr!_ z^lRseUUAyqiQ|m1_=GohtkuLKY2Ugv?0HL^JKHqLXt_2BE|L{*Y*@H~YY6A14l%8z z2e7}jbM&R$hqMwzNX}&>-*$TpV`+nZ3S)tXdGrKrXV<c$S2LVZ@(zm~B}fLN%8#iD zvRVtf4XH%zOWwN0d)5U=+KG1FEZM8BY}tX3GS!?(33-%F(+G09kHB)LC!Aw-$;b3U z{X)H}Nt$W?G;OD&tr0M|RhHP2o3eG7Qr2ae{t7(^>a?EFsA&ZwDs4#dIQB@Q;@LmA zJ5$$W&m-9-Cj5l?>z8Fg{)y^*GZiyC+%Ng5bYkM<G@|5$(aM9?>l2PdJO!>cx2o{> zfLtd)VFlB#!<nZthTzq()J{qa#b{>Y^4a<qj;~zCwD=vOfcMV9i&dcdU}wOj6M7Nd z+a1Mi+bjxR;WIFiRO8-stPYkXPD%n1SlwNOnJ?Uf8Ip?D+DRq#VuS-aLi_Tf`F#n8 zs1`R`;saR>gp@Pb<TpSR;1*czvK94<N+jo*cL+{P*N`sV%SpW&({#QGFXyvPjIDF4 zM&2long;!QQaL!OkfUC2w?Me_4L*)($><E)ZAIk1cA6ONT73KXzssvqIhP8ZXxES? zN#*D-UBv8j!r{jR>wCf8K6#Z!e-mpXC?lO%EhW~Sb}iOAY{ao?5)JTu7ry0jUkyQN z^&8-Z`!l(X16G8*%Q<b9PTF2hJ*ZaP^CHaR{5$ov$ZID;)TUy&u}G+rPvgt?M0yCH zOSPsTYr9{&K3XW5^nGUcKL#Kvy(=muRbz{qmCIz}f<Y5Lw7JABi$Y`#_=h3Gn|Pn7 zrLLvMop_K6A$~c#kQ!M0L`m&0ft!~KmEE&(pgElimZ#H%kRDQB%Uqw`LLq+C`=S*c z?0Y<qWdO$N{=Fxpq*vXXXMem^#Ki(`g2<L@=DI)@Cv<HvKAbq&8h>-2T8x4<Dgl10 zM2B?<a3}3JA9x`q-;4MYR^96?Bv#IzQyv<+pVx+WSx(^Og2GIVn2|%9uwoHZol$k| z&f;?&P5qDA0Z5c*J;XEQ7t$i*i;R)Fg}&Chb}{!ZA^=C5e|qXSubC9I%klPwnB<7K zIFl<AY&9<IAU|>dCv?FWO2Pg8rBudP!WZSgh~~cN_)AN|nay;&(O<L}uU1#nI%Bm> zsA-$W_M=j6o9`K~VbUp@<vLf&bxmd*x`EP}FPkPtJE(6T|FHZkdH2e7bRpmIhwe(q z$=DV`=Ki}F36wA{8yh;!lqjPCLCTaHh+iKOjOK>zoo7jx_*^=73pkqd>m6U|WkvM> z7qZ&KsGEP{uUP8H;3H=^;@jO8xh-)6<kQt)yZk@(C1(fJ*iWzJ6Rd3Wa}9pbijtaY zC3B)-EbX4cWDp7>>jdYAh-bTtXN1IvG!1w?20zbAvWHhAo1wHDSVdIhOFH*!Er>3* zNv%j~<q8Ck_O9>$T>qA-=QJRcpCRq+ikVkKD(ntdDE4`gmA@+`qF^cX0>qKhtIQs) zph|V~Z}!0zQ?!!67m@$j-<_!<F<aDz=YvGmq-I~@5|B|Dq0974WcW<GP-q7-CnBKU zPU?}df#PZ;e;;iFOVd9FxmfYSbtG?K{5tI=cz<wY4g6g#b~!Bs!5I8AlnZG`R4Mry z^HM&ctQb!nfInbxL=WCVTld1yygF=#E}^y*%bh0gcD17ODX;fy*qP3_s1GEye1Z49 zx6_Y95O?OACfQ?q!27$du5OU#-<GJUVkSWRaDOk!0TXTvy*cWLOXEk!DFmn!dcLU( z67R>t%b(-)m3m%eJ1&1>@~57o&~_LOc0abzulX^UGCjMN?)Mq=nA2Toq#}ZHx`nbv zRf&rECm_hvKC(vT+PjOF^<;@%H&1lr@Smntv^fJx*9i8TS{zVkEO+Vgqck|VMg(Uu zXWbgVE!YdpfEzY7KU+cG@J3B_Th!T4j{p#q-fcY1)UiRh+*G_<i|uc0;1Nv2`fK0@ zI~?#1R9Mtm$bLA4$fBH6^CWv&4PdJIq`wL8<r9Xh6s}>1^3-=L%+7X0BZr9XNCWKl zy^R7%LdJ{tj$5>O2>Rf#iR5gfB|F8R+-luucaDWs&5t2FLhILJJo*=DfoEhoNCo;+ zIb1O=Zp7iNEeC0~2e2{2o4D>8<v;Wsen#|284(weLNd&tdvq4FjtuUZb8m|>dr>t* z-w$=7qoJebkCf029lOU`f%Y?#bRV82Ce3$Cj$H`XASMQyd)Ad5L+aqkfGB<54FQI? zN6RYu3mGI)P}Mnw+kFvL{}@u@g2?-Y!Em<cGHEB68$*^Z{Lam^#v%T^;T&yxlLJdu zOTm`v@<+T~jN1oDpZ;hwPeUP%UUKPgc+5U$QFKQVvi<orDFjY@L>G>S+fhk(03FbD z0p*E_w&Ei4f|iCx^+J8_F#8Cls5NCJle-7DyprqRp16h+;q=B}lV%Q8>BVyA&aH0| z(I(%Kx)zVJJi<J)M0)lEdLaAb2F$kJE<;_H{P*{My;l9sZP77u1oV4ppFgk(aq(>J zGp1)e!zM6w6QLxA=;6`Zi3k{fW|qMHOZ6Rn%8lMkt|Tnq(UPZ=%!C_z75(Aye<)=& zeXH`5dB+7=M|&o(Ix>BI@lo}0IrjZNNiE>GAKBvujGS;okGpBB38P2SheSEG@hRC7 z{_}2hzX{-Vjy%XQau}&#nK#D+bG%gn(}S$P276V0W6LsemS?-WODuBoBAze32v(tK z1o{L{UYB9*)h<9YL=c?Jr_u}f85V~c>?c3YE;O>cBf+g^qWSSF`@Q2Q9ckD`5jj&E z&Q$e8s3$XX+*1*M(FhFm;1wq`^h{hSv?eIg4dK9bkF|@h@e#SZ>nmhu?IG6k!`X_= zhV3z$d!c}=em%uM@lEeph&D}QH<ESl_dGGnd)|Te!03%x$aU_M+(^EXk1X0LHHDbH zCgdyr=^B%)EzKmlvC_P2zm2B&Gt#X@&Q9KE*YrYiWTt!9Nb0E7pl;6P1e8npMK^_M zWTU!l{S@RsxWDn}4LOeUclpWuiN?=@z;GRXBkXgiAEux1{V>8dfrjRIEd}d(arD^U zq?SloJE%<}dI?(tMvXJlIwtZa8Pj9<(^@`qb$6OP^+PY3=e;pFNpaBvK1hn~*Xrk- zpD#Z^CAR#sxhG+P>xKuFo{T1N`}(9Sp6z-xql}W`<1>`SY>6Ae#>kVLUL(kA^t%|4 zSL27>I~4clKnT*F3|hqo58eKk3C_ss`zCHzx~$EL)-Z^AzVs6%whHwG8(xHtH3QQy znOJx*wEMR6!CoU}Rs`qm=wNp$zmaZMxoP4?2#I^KniU86UGj@Y|8}dmB1FjF-81LP zD{D$`t6W7?O>u`E$96KlF|p;(f^&OpGboG3<a^J882qLDVn2Ks4>}8oH*&p%IhKer zQu<G#%O~^voq4Z*!Ql#AE;k^Jlz*f&vr&n7t@n1RTL=JDE@`Yh%`VkwUw-E}>WyWo z#C{@W*MtLOtF|0Vtb6_epXql8u1N%q!31i4vC&^zN1e3Q4jp|YQDYMo3=i%%1ueow zR58$hV1LWemvS>giCZ}lqX9P5UDwN|4O?xIVv}~oVAQa@PXg!M_3;c_xZW_L_?h|x z*zb?n+ihzp-9gasO5ULgAqgnMc-dw+HMs{(mtthbjLU;!!LuS~(3Wjn_e5B(AkkrJ z6uKJLDvJ-?{c&c&j(v3qz^%Zmh2%k(KX}dLQO%bc|A#)+;v>xw?5B0ET;Z-(tDUnS zHNdo1q<y>?HnKQ;i=AJLZMVj{(&YT_I7!Czds90a!BiL$8B-)BDvXlba|7u?j4^Wh z_ru*$gC+G9A?Mx}97H#*j#oWefgIoF8G+@D1|gT)<<`*M0X^csvE;92B?J$2!<fjj z4Kd*@Hi2$U1q7Zkaw7F3iqpxR$7Qq&*$z><(W1uMs&z!G-&84aZkl^$2+E<rud2Jn z>;iVPMKiKNU)%ej*JTZRjDLt7;Xs5p4^|Y5w-DDI!|$`BVWQEWJ$3rk_n>_%43W%q z;L_!gi@bgxS?E=zzBK^c1KASe!J#1JPwNC(F51OGyzAj$sX)9oNscNfPO3%eD|i|5 z!jHBfwoI>xX)8I<c2HNWoI*k`pI)Y3+c7cLk*h*_LUSglj<vhlPvm-S@{{bHG4q-^ zqZc_HkA~M7151bpq}{?S@1R5^-hVm0e5cgI;D4T%m;d~TExq)&O;`oShWs}+lvdI0 z{C&R6*#2uDQH-rKbCL{&6O$j~!74h@T`|E^|2V#OlO-HhpL?bQAbzf&^w_@5?vz+A zX2L=;Y3p$Xp+~<hP?}|{v3EE_222oW?mtaxm^yuh=Y8MjC$EXL$noO8-lrjM+`0uy zy}7Ai+Pt@KaQYwO>+dyAQi+%@`rI9Jo^=fSyAxd>CL8zuX+L+Vu(?2~IPAKR>A{?L zyJlY?5=;*)`F4qmkV7NoN4-5IGP7aVq=k)8YB2ZcOUV;t>{NS%ouuwcr=oFQUN+X} zsj6*SRv+?csEgua2Jy74r8?U)z)q?PBr1vgH}p|0k}!SYYCP_mb!*!K!u70m)m_oS zr*A`A<R`L`=N5USUKGUynSaEN)%$KGwTah%qqk7q2NG(SSCFuFG{mQfE0}`>CXo*h z^ZZfTWVfeSahCgm{UEh~phwB4`1@x+ick+dkbQx_<CwpRSIthmg()h++IEZ5Xw#@V z9npUS<e8s)#TH=4NJuR?5MlP*V}p6BZQeBh9fF)f4<LmN9rb9b7zD({*`<nN80a9E zqtjY3!5^pD8_5m}8EJch$YT(KIJ!O-B>pc@Q+i$Znfsn;+b2XD>L1mMC{v$(vyOe+ z85gymV?`oDA8W>}wgpQ}OIs%3wfJoor`0+mFiL~q>^m(jWiS32LZi-e@SbDuX-0BR zx)2Qb-EqAos%<v9@fCYlEQ8S<f~<b+TmznX4!BihKu%by7j7xeEAlAbElc_v!3D*4 zwA=4Xpm2ZDoZBTo-wcXa><bf}n6={2#YzkmB0J^3pXHXMN?yBnOS9;tFtNqaw=+f% zDeelRJ1qS6E57q7<Hm^tUWwG94qb8Uv?=dauT*qo8z!$kNa3GI(YD7PN_GXj{~0M{ z!;7ts(OHl*hv#cAv)goWTD!8dCD|XMCGFL<WA@sk8{4gvvn`~a4Ae#!2>0QK{7Q5u z&Qbl)Zv$X#5M*+<!Xx!0|Je({#1CIaYZ#Hs+FRA61|IcOkg)7JR&TReOmK$J^V?SH z$hl1Hr@A#xioX;&R=a+;mO8NlE=<3(HONS<pH|x)@Y@{>n~2oe)H-h8`X^Zh#d6pB zdK~ku>^Mq7!Tx!Rn;@;$bt(Kj`wXYS7x(obZ%t}FvStRK$w76P>PB5R>*pAni!v5M z6CK~3!vfcJ4dQYH^DbKQt{GsOatwez_VuJy`x423H_ZBJq9qrS_&A1z-M`FEAu9_d zUT2PxhQqi4z>fB4XlCGGaPy>|KWeH>or|VMYkiv<S)LPe+O90y@St4-yyy_laqaO| zZhQ`UPh9_4ugjE6y!(q)=4zs#sepUMKi(waKBh?=>GnSZAXm2h6>K|l3PntSl?^)a zZki|=+jU(-+(<~BjeB$NLZ|#0(J^&?`YM;WfA!86;DKNj8|m2==k(LCYu7@#%fCyo zy}d2aW3ny!toI~AUzIkW-SszlTe0h5vv$0D_m^IkiQIvvfFYq4FgX3=Q)D9)9JOP9 zSD9WiQO~7T7}tw|b2q%oDl74_^;P;R>W8*mg--P*ug#E%I1wKy0pAD4IAiFWw7vRs z03!4H+wJUV<fDZhR&c(L6ZuiFohhou>K#_(?`RiXuM8rs=5SRLjm*W<@oBa#lSu_< zRT94pWtV?i*jZuZ5%Ykb*kF2K$`Pk=*~UMIkCR#0`_t@F4Fp)y)?SSy9kPdou)lU} z@WM1nj>w*&5UX44R@@UhhkaP#MuF-9ZV!bxnNvNAvpGdjt%npt(KQ|@N$%8A5|`u6 zl-tIH#e~TPxp6_$+-E~BUsXea8(o~+)UOB`jYuaN+7%(NWvcW^TH0P4rd_2#;BL00 zzm)ibuh;0)2ge$}BYwy}rQwy#4+XvA*!fb3t^K*k5NrjrUUynJ4YiamWvoXkzkjx} z?_CKbXiS%P5jLptUfLy)KPmqHp=L5w>4~h$k<jH?$0OI!RZ!`$4}D4{>?m*O24fk5 zeQZ76T5oyJPm82TE%COeRQJ(m^I){18b5YhFPyR)8fR)4R0u%#Gqb)U;xi|_I*eMD zA%2c~40hiVP5RzB#baVEF*4k-2E_^czIL;N$b|e2zzCA|+Az#GLZiV-v#-W7+6o#% zP(JH&Ot3rhhCB;QlV7mLM53t=S(cdh5L)aHGddq_okett1OFRxDtZ%<<pPHXzq6G6 zKB<_A{OtsXEAmWSHJF4V9>xka-Y{bIC5uPxkolKccKB_v=VT&5`_{F1cOA-nH7g|4 zz6^2~D+~dMC&w%iVpU1?M~~---~TudT5HXQ;aPie(uplTZFJQKg&v1|8b0BK%uOxL zL}5+*J0swcghekixL=jM*Mf8qtRiHSK9oF$ASbGkT3?Uc#42n5BcVJbcCpf+_v_pj zxLmU^Lm_R+7!?cbcQ4<B-DxcWn#K@5)s;ofXPAuK+V&M`c&xP10L}KXcii?vR+!vZ z<9tGQJS4(c%D54(wB~n*!zd|9x8Gej__<M{^P&_gojNQe`ofT%E(ud~QISzgMy=2e z)>>+(uzstxfdJ6^v<)v+lmXN7&eL?nH`}@JCw3kVTX!f>Bh|I*QsGpk89TYjmu`;k z@jA;sc|)y`R7Z{OF^0oz6ngc1=r0DW7&o^uOQ$!tgo&UD^XHAf$?bVd554TX%EccI zP+FgW5c|;~N(Dyo^}$Q2*z=*cW_sD>(z6^-#!I+4eQ-+dN&gADaEou^al#r_x2;Nf z;AcgSRja**+J{=;QSz`Sv64&D5&{Wrt5~<PD|DcWPW1XNP1tA=th$@BFE$^&yCP5+ zT%m^5ocf1vjHNX1Q}dH#RrMnlep7H$#?X?q(IAEP=`HZ2ikw<F&F+gdhN~msmCPss zY~EP1S=C1O`HQD$=@tY{g#Jd^*3!BoiJ98RA@$OE&D<L`QsW6tt61Ad!%#!|4<>s{ z`D>^16Zwx{48MXk^m{RH^g)R(YKwwA5*L=n*?$X>e|>G1F?JBfX>&a{rp2{mxkRz} zRMK_?^Fj`tTKKlX#npS0)TixlDOT+zy?-xwjiGcH^5{gsIbO<ohBHKxd>XbBdeMp3 z*eeC{I=(xWAOiOAO+?SUJnMY+0%J&PC#|G}Yb?lW32`jq>Lwo12fy#i{L73HLcYE$ zAt4Upmv_ltdUNlGDJ|l?xJ_@#`pv(tlN0s@ZBJ#Fs9Jt472*t%LTX7Ds^zDk^XGVn z+KQ=}`DeYE_V%}f-&?SYKdtRU{m1TRZJSpsN^ithHmh7f%q1kTS7vrFh<pz|?FRG@ zndqDdX!)?PSfOuR^0Q^0;d<h|<i>O2d$=k0H6qds_4DGLSGzcCl<igv`a_7bXX*S| zh1ZemR?+T{%<vqWEf|w+t*hRrXlkjd4Lsw%=wzmJ2f~{inAT<4QGUeURCoV@*`d>{ zk?#3}ynlSeShm4K2iRfDC6JPi0LGy;XpQytw}ikiS28*pid#@0Ibr?5??aMM|7lGb zf_<pSl^98a6q#c?R=~x9DU<^P+#BHLiU7wz-epDG79k=t|Ha6QL)$q)nsT&~`SEzR zg3e<>_vGXH<@1e$XMNBa9<<!Dyl3Hg47(;QdSN!=L1*umRRpzdJvDIowuXWCrw!_~ zFTWVV<Hhfy%3mr2t(|chI?)h{!8Sbc-Zr1458H?>8%_uMfB1D^mzR9QTAJ4fdu6&` z{&Lvy9G@}ZF4`$*koLYbIQV4gt>%v;fa4M|V4&+L+vtoDaNp5?v@WJO5LJFnRCo}X zMy)NYI_kofgYu?wAq@Ay5q?NH;BekpBb#k+qOv`kpJN5ThDA^OJte6CpJ&^NN|p?F zyG_5M5iUma9=9byjMW`7|FWeijTZE5Fo7GCDqZ`m01B_esmXDoZQ;EPM`V+%@t#tx zt-c2oPA6b2uaV}l(O=3Q{%L<w_xVfPzIMuyda>w<<7ByIc|0?@;_g~&Ba(;U51~ew zeEFq$m5UwRa@Xwdl#X+_7Mr(XVp{C(VIdeiF00I(q)Be*Xz#R-yXRw?*JDM>94$@R z*@dGm7V;D&CRh<_b9(V7AUI1-${Kjj`6f<$U(`HK{rg_X4L;6jPri}Y=ySGt9C^3O z0Zw^idvWG7P@C`mVlUN5CeDORPL-f0ghCAdcJ>ENR`hi{&zf~nwanm3@9*+Uc8IZC zbc_q%(F9!l#}f?7zol}s+2k_Z%g!pyWx!#1ma?HXsfvB(6Vt55Jggt75tQs*{&%!b zHp^uvuUTa`v~|@HxDy0CnkYvg$hJ;KaW9vke)Z7gyEmj)wp}2fxc=dhGo#m*Q97PG z=sN;WB<s=-Wy=U6#QqcTiE#!NFMIF+b9vr|S1=7@?$Bia1Xn;;TCvZSqErt>d)WlT z=`tKA1lhm$DFaJqbLKrCZT(?%Uy{NOwsC?a{egEcSIe`Ra`E}<BFSs<WBhIqM}-{{ zh&&?fJJ?rT#r}+bQy#|K6CUytrn{!^Xc%0vQ&W$X;40(w^d}yT#Efp@J1w<Q1Gf50 zQr!jyo8vG6>I$a0sI9DJ_%CJ#8kL}6bQkNzSUIGi)Oee2Y3kY;3kbn`Q{j%a;UJ!s z_j(_%__+Dn@<tI9!abbGIV-Lv72tOZ6QoaV44sx$HeieC9(R&xZ&qCW_we`>mtVY% zGGo!OM;|cG>k}1Mu&yk9T75dYxL)k!A;muF=q}5$M;;we1N1L5vW9TB<RyjO5;NM+ zjI;>5&5O#G?^38=b6Ra>P~pY37S~tT4<u-fnvFs6^J^jBWbUUjZU*gAo4GxxJaWz6 zNt2(5)i)b1BR7(tD^om^1E2=LA#~AVvaEk7I|YcaZv7L+;?4d_B3^Y*z?fXRMS*>Y z$=mMaH3Y|JZU5!ra}66R1&qts;&(1>4C;(vAL`Y!t4JXcRpfNS!9;Bn0ajQ~zI&cy z3_R{;uGh<o&i)(8Loj(JE;T{VgBNj{B^>AZ_r0k)3>rYlYvrFI5&9478!_8nB~B{2 z;tmXBv}rVZ_RxP!wlqRYU}%GSWv|-gVL{A_-m5fQmC6)fw8-oD4>aVFhxLyVNFB8J zxb7jF4eh{EQ?9G;bfeClNRXv(?piv+dw*PvzBD`B$r!JQcPlE!awW|AB<%Nf{3$fi z&|0>0jwE`xksr1P#GNBS)%GJ^(R~8%SEqKPwP9FJyQbRwd&k5g=@bUg_6C!NtcZRS ztz?X>%u+Qv9M(dHB*IGq{d*pA8vnQiPs@x(bfk%s9EZ|haVi5Yi;G?_F0rd$PD0dN zgt4IRzZ27k$)*0K)KZRyM=o9lcO?6|U{9R)?A}p46U0Wy1*qc1pF(T@Ak7A;#^duI zW_Df0pZ<KEtXKFglXBessj<SBFsAa=bCk6<DU5sm*1WhI%xp=}z&f|`{a&xz7#401 zutvbCuo<T>2-DJE#Mq4f;@)X!e_oe)wfyZ9zOTQl2`~*tIh2!XD5-v=#qbt3F6{jk zi2ZjcN5FT(3z!;wElUMw92Nt21MQnqcXnqc^_goIWr>~W0Z9OZq$fkwpk8~Q8c;81 z2NsZQG*niPK5P@$;FH^x_OR18ZuYx5w+7KlfrPtJFgJuM(=AWCD15xkp6xa6`KphL zYy{cSN?+qFH`EUAyJRQ+x&D|xf+1h!jGt!D?Kw?9QPBN`ZMl<iKqo4*;3`1ykucFl zF-c;M3<+)cC#tN(!)(Ci%lia1df3GKX2-4rXgM(<TGiv9Y`@U$*f4_QKRnl|f&P6o zodx%U_V#G*dOy~y!Tk$m@w$X<f(AWIi5DnHe?ECVoWT^*Uz)k%c)5UlD@MBko9WF^ z(KB8?%;O0ntS^!z)1p;0pfs}LMDj$caDh+f0d|PVAR;i+qxWnAkMN$`LOvWucR1=d zo?ymG<3JkKrm>_?B|gGQc&wc|W3dyN=grkuha@UGQ|F@3!_<S7`U1HFdLR*Dxn6m- z&qE}kjK=!aphHAHYiQJ9dwe$4RAYH+ko)_XM8&LIUgLS3Uk`bHZjbl-9t^8GY()^h zr)i#l0Q6eLPJA9G3XVULNUj+qnrBoQDcqeUk`@a;woi-Ux?(Zvu@35(np?%!688X_ zpC)G0Uh+vLC1TSIv1fYis)Y=AWfKAq^RuR|=EMY#C9_j{bIPR=(-hySF8?2LdB(AU z73A{L&UE)&5o&J3N#XRkCYbiyX#-n7dh0maZpRoV+y_Cz5;Ml35CtvG^X`u}YF~n2 z%r@ZdY51v9M?_tsn0Jj`tJ=IOEeEdlt_$onZBmMTL-6?Je*5Y;VZn4)ZGxc4FJ_n5 zNSJlm@e^ya_ab0g5;rT_?{Z7{1*GHV$C(;x1L`Op5`BAXxXjsW#DpXph!rVhOgz~v zF~2<yhAD=K#!cJxm8XEPE8m}+>n=ROH&Ls3gOD42Q4zRVowF}PP!BN`ugOS;c&KK( z3x)-m8Z>NayhI_>nUpybB_n^E&unn&^?P^E$WLb?Uayd6xN0FeL7#MXM6{iSHse5H z<>+fKjzP^Z{xX5>G`I@QERb>(8^kR0GgPtB6?Uk?!IsVovIW;8tO`RrlOYNp-pjbf zxPF)W!btz1N1sPrqghRz2rp_CVN=HRs~41JTdy%byi}sltxksicX6FGW=g*czRbOy zBWdjc&quDl)o(fEtz0_O&DJ}ObhgtYJv$cD#ijYnq%FhTjo{cvbf}%A?Ix!oG6H;} z^}j*xkD|11;YvIVd43I2l@<TrmF!A@&o*OJ&$v`2MiV}qErz1mW+64-GFGl+0>vK@ zdJo^gFyHcb-0S$1GOG`w11^t+tI74h-=J>lm?3PWooDUE-oaisFR9@PfV1`vBv44C z9`rOw<Fj<5{Q#Ur^767znhHDUp^u!`zfZBf7{q&!_#^-Ig}A0;yhII2=Oq)bo+3tM zxf;TI0&r%l31qYti#A?bLJ)7TZX-z2fv|E)l`ZIyegcvCP9iAaFvYIxnxD~<r#L#L zeuIX`WZf!%i3T|sndVjCz~qY9QYUX&dbv@eMcMNg!k?%_$kWI0yXj^GD`s6xsKG8a zK3y1npoun80i|A_xbR<i%vti{8l-~UQr94chUJTUkpi=>)x>z&X~m8w%+pe)Ea7H} zdvu(!b59$(_qygB4KbNZ%aMo<)lqwI%bs%`0=QOoKE*o(t~U`C&aop~d=loYl&SV( zGNHc$zuwg2i6@+(N2|=avaRj1sGKe&(p2{(-qYBFqPRifH56j;$p>|0w8y&jsr$3U zo#fJb9KMn6eR|mW5TJ)_^SP_)Ruh+>y?XLP?r*xb_bHr()-AE6{(YN`Q^HQ=3~oW4 zcAcJ&!;dQ46YedvICj?M6U5T~+L64hz*Qkb5=^ZeLdNe6g{@(qO66Dq1Hx?qtIbBc zTpc;Pa_%pE({*kZE^e|SLV}$3=jD)WB~bV?^r>r#XIgW5VspgK-4;>-iO!qer<6I3 zbCbrT#3cV8QEwg(_4hrFKM@M?sU(%9UIxh;A^W^uDaKyPzVBtr64^(+A`vx{eVfP< zS+Zu=OQlI!lkAnTWo@#rzw^ZV`+NT7;W_u-bI(2dInR5*_+MDIU9<P)_e$^Dhgd`% z;^y2BL8SJnJV;4w7A$EWywR=eOdMq6PNQ5*GX)#~ranH}<UihC4WB-Yoy55JpX-GL zAn+Na9<p0;aUxxeNL)ZL&T~|W%+t@GT$saHYy;8r+T};&&-Sv)(BOQ%X}X`arnI~M zg@JNt{Ut(<$!fqpP8JB`S4}^*TZG8!jP37|UzSUQD_Cm0r=)GY0NnR5RTvwkc*but zZf}oZ`0=4hK7~At-z7k@cYctqsFK*+g$&Otgc!eI8iVP6UY(_FHg5cq^k^dGeFVzm zH3AB$aML#=T<Oj^TJM<fh||SmkP_r|f9L+Pn{gqC7`3!P`|?|#GsbZ93`iah1X2Sv zyt?mov>^7cs07#f^|j4Q@A}`HFv}ult*Hy_SeX(_2$p*a2iq1XlZw)RNzXb5gaH^* z?%Kf{2e^@~UcN3UaJ#OXJ-lfo84xM}x2ybc7RY4Pu-MiB>PJ(31&Z;%7H9sL{7!aW zUA~8I#-GsUA5CN6qORuI;Y&1h&!%sD_{m@9xQg8IZKK8Qr|BC0XRDF+Bvc8Z88FwB zE3+a&h1VOZd^p=x5-Eyp=di;|{;r+_@sO>0;CSqMQD8%juU<ol#^Q-Ph!e1^)hX~O zA<s)i>w!&rw}pM<THU+d3%MB<p5d}F9@&RUygKq{dc6X;fnmWRjL^^PX;6{RTuT>h zyLA<~S~~Ev9p!KGNx1$%1qC8YTerl%s<iCo^@cZB6#^du8*#-`PPIv4hauQ0x82_@ zJV?W-7|9{XrmHKUl&ZMky=c-Bo+Ajc5C;4h+;PeP78oKrwE~1FT(EJlq6NP-gfa9> zqgfjt-%bs*@rC9m*1t>aAnOPYk5Jpjh)Q}&s>mG+J`lDm#dZ`!fMpwng}<(7z__|P z79gn!Bmg9e32(FBLN^X_H91yA>#NB{X8h&khBJxsf{D;|xsK#-6Ex&SuDwJaxZfSp z8c?{Fe$0E49y@c8*C1%IOQ)z4lwitz)YWjep+%C0o`bFrH`o^Fzk%J>Cd>sC0*rWH z{^7O9&_3yT&G`M$I_DcDw>&z#^fyC73VqE&f^U>OUp{!fO)PKAi(K++JLKkERUv5H z?zVlT#N!>Ip?7?)lx+VdzlD2xvBY&(AS-pa?s4WksezyDn&yn(GImw>{Jl&lkOgCv zp(_@yQB(k{@4U*MS>SY_21vTHa!t0kLoD54F8l`E?U|D`NER1|OcX=^x-TxcQdjc% zMP^c!=!8bSccA2t13-cWH3pKuNR&lW@G7-`RrqkW5oJ!a2bfcO873M`1^I2^OC)r- zk4y$#f%V^po^V6#M9%FVjmv1ogK_!i);_lo^AQpLMr89>#&c&Vypi|@2a$j4FgND| z^mjq;;9RwHU46b*tmP-c={~{42h`K7x%Um&eK{zPU`1=)$0^I;S<{P5b9c=7nSrg) z)B}H7!o_&!BU%D{zgDzIhiF3}q!D>^f~dF{1rZwNIS3Gm+E^QwY<s|co;PV0Tvw9n zc0y!WDX09Pa&yLo)if`tT`@`8SZafH3c#2!_zlJY=?IvmJCq{yDTb!5JXS&T4-~&N z;vZFsS53nA9wdVy=)M=;23l^cGEjAUiVs8(#H1Qf^Y_wi>$&<qnZCs$2eJ8=bSwi! z;yN7SfTf$GO72?0kWp`^O|v8fj$%N;s6gU;2UEFyZicQc4RIi$#dN8U$GNSnz&ua6 zQvh4hoSRaLQ1*Vnf%V!EQz&ZzF+=w|w#C^z*R0ew=?nK+iw|Fp#-a8o@})Ci$S6n< zl0r^CEouw>sFBUWYrx&%>mLga*Z;Xz<y8E^2iH6DSbc{m&31sr)fW)LD2IF0P40ul zfAFITY;H8){&(4ETHkINq%zs4O{uXF0!n7L&<L0_9LRNVAWhW%!k&(r8PxCBQA-Y) zhs@Z^=fj(FiHk4MpUwGYLXNZGhdbX|(;k-4(x^wB6$*JYN|V$Lz|h^oU+}LlLm{&$ z2p4+@6k$H}2v~UIQk)=P9JcXQiD6;s8f2&*=uaTRGbO*tkJuo~u{aMFh+5D&<DlF< z<op1O<^0>#I)$62zvfrR6%T+%Brc|&W(h{(mnMrM?dWe{W1Vjp<y&)Qy8%RzI&>&# zya0r?{Eey2vKdA+_fEiUy=EvTfcA>6xeTQc@pTr-8$-8evCI=DguvTRfyC2qhw*3D z6KefTA^7^9=hvWaKU`FI9o_t0sB>FZL2EBsAk@k=0~P?i2yaI-;40Y2YZ1N^LQ4y5 zT^o{5v@7jL@}UY!j`C^eN;{Ip&{iKs9U=yzBxfA8{0SgABraxRd+8=DbD3o#E%Z6$ zvmU>te~aW>QKKmTI^TvYHyl+#hBI8NKhYp<s2jZw$x-Cy_oSi0GPpj$@>bzKB!BGr z6k$CFMcKkqkANBkHk@dHHcSNp2ap{_q^-pt(QMA8##4?TM+_{WG>%vb@lcR9N_DQy zzaG@X2O3VE=9<K!Xf)IaEC{13L7G}gC?4sl6vTz8>q)OyG;z3>tUoj5_bWYt<j)IF z--RSkIs*471au3Krz*4xQ}s5V$-oykgG93c_mZX-h}n138NKQb!_4)|RgM3)`^BY% zSRGA)P@nl3xWak+Q~5sR-uD$}0%IE7-t=q6RY5>)W)7HWBThu~a!N{6YAzV7)}Z(s zt%`|DlBDfakiIU2{#e9F8j1m9+H~ToK7wwTEl>ak|606LAcj&G_?;n-pbO7ox4A)F z1kL8V6h4~9JP1ikdV&CH-w*Ep<bb#_Dn+ZZ2D=Y!LUmF*Ta$rqv;y4qj0nF`Uh`#{ z*#0$uiL^(IZa15y4s63GDee0M>SW~lpfv}*$c)ZqSQD<Qplig7Mo;x`hth1OL(o=# zBu4~cxCFWV4~bBx0cU&2Y@D_1VZ)<|WoIDD)d%(KK7f0bl&+eXPSA9R*&YHKY~jSw z8r2;~w!r3wujEg=9U%fR4Wd<hn^wm*1sN8Ofx<U`Vt4m2a&IImnV8%H(k&B}Zcu`} zqeg?0+G`@I0KNE(%K?Nz4N3zkDRx_b0U%d~sxNvj(2$F}%ivo>@P7T#UBh83fy9}& znSLhzAAYN|71zlJH%27LsjR+;v;yCKX`tT!Fp8=g&?#>OQTzXRg}>(~8nFLY1eH93 z)*~fJ4d4V%zFK*~jQ{)gOdW(Zfp{;Y#A~zqKzU3V)XhOtlo+=u(?d#6Lu{~fBX6F- z<=2}VwRU}GAa|cE4JgY&sHhANt5l+%>!y@TIpwuDe?T4dRCvI>0EVa`3AFopa-9ak zl16V%W@`g`2yn@_Swoz_MjG{{azQ=h8d~0VXdS(nKVW%jmQ=f+&VRU-0E-Q7-P$Dp zCp5G;KWg{6yBsi4GIVHDeh(GCwQ$i~U)-h_J<CLpf&y9cIKt3##ZX|R&K$bo%zo|{ z5aa^6l{8#3gx*Dt4SA`2esTc5@4yCZeU&S8w>(Yf&kIgj^GJ>a!e9=}ma0!&jRYZd zxxc_y)zbN(D1NLWP_^MJ^C~_VbtqE-jeRY~b?buSZ}3BHj5u2S8`&TWYL^#K!(ZQA z`zb5pAYV{z)DT$9fgOgVzc*U%A@0+r%<_cfUh1we{RH)kFXAfn=&WvOr#*40b)&9{ zqG6GKn<l%S0%E=(MzaRKFg45~n1zH;^Rx-@g#$1r#Hj;@610+%6qPmE5R<-CaPgPz zjLr$Ruea&iB40kZi)jcbL0vt2=+r={LB}AG%k#1)gs334GlN;1LTmnd71jap5Jv&y zQ$RPSwIZvnH*h_Nf#CKJl|2I4au9fx4Yd{vWH7t9YB72IYY4|{_G3sFpm+HpP{Uh& zogn*yMq*`5OlwSWJk=H=fTk{b+?|krI$tx|Q`O)(Vy7TJPPaB+IW>^3!;}7k5fL4I z3RLHEd~IysTI0$K7Pw!|J%xB00*~%}0uZj<JzrBegnLp%G<`YIPbE%lql%>9U2Cr{ z{{aRE2-=?{)LuM*??VN6%Do+{8*Y<YAw#-QAd&+@<&}V?q9q;W+GM~!XjR5^v$}U+ z3=VvQ)l(%GVvbDE`G)O7%Kw0cksPAI;e!U-UqLx}1fzg+fS`Ok1-v-kv{<ypb?5vx zJccRe?(I;=>Nw1W(5*G5CQ^is!>8xJjmf%gP^|sXuE&*d1aQWT#*AL|J*LcygG6?^ zsrznmhE7uTZfIgqzqO`p?`C^EzhC|dSo_0*U0pdL)glXmz3;$Vt8q;>3({TG(8oJm ze}R$J(z1AU1?o!t5n&nl%%n|+e6fqr?C=ZfPvr`MtFDG^dC>-qt_U5(%da$O=FOSW zgK-U{bEJM670KuhQe(}2!ryFAff%y!57iA4p-##^pJ^mXMJW3ZVaA1N^i0uu-t@Rn z#!z+39s$RH6eNtwFdCA2R)>ZUXq^5UU!F~3f&ArY>S-G6%;$vo!1XV`EM4FuKM-Rk zAUvrfr8`yvHvvTzT@6}AT<lo5Zw_mTL+SkuO7DC)8ACc~d(?_MiyNYz0D@DO89vqP znoRk1U?_t1e5D5eafNfezyaa$Q#3D))P($cP7(wi!4K)8!~b`}5_~7ku-KJyi@Vo7 ztM?&`9{-h-yWx8c<Webmc$PX}k3+a*0Nr|etQm?1HmDdL4wPB_UTA{o8TovQwZmF2 zGP*~zAmxTo-7Vj*>qCb<G*faLLeZe$R~N6Lo(X*Rqypv->CV_&e0?X&L_UbIq|qOA zx#`fy9JKD3GFqF0vdYmLhvVb^R|^1mi`>csyrJ|5h6Q`uWSxS5@Eg4Hem1yRWKjyf z5J=U}?-7QQ)RLC3kdZOD2zBhgd&zQ@U=+`QU6x#ikR+z#^Qq1?`|=%@T*H;>$g|<I z1Up9fNMi(?slqIBT0hi_T`KKeBPUy?nkk)zY%3ewett~{0u;5unxgolQ)l?Mf3Mkn zlp<fWP+`Q{ZdMH#%bSH=U%735_iv9R07ZI@!SF0C_R37{bu_(I?R*2+2Ca}^CHGc3 zf!b47qfWJ9c!wa3cjBX4)kBTLEYT4CdN0^}=>UEWqST&!vV@)nXi#$|1Jcf$F}i8y z5LO?6ZU}<u>dJK}<NW_jgnSts1W8pW`SOHdJ7Y}x7}PnvYazHt>!7(o<lzJqDP*!j zjL0b%Y8U=66G~G+FIWQ*`E{vq5T~bO?=|9;@91btYBJ#0l9Y?&GXk2=BJ5pY$ml3a z99Px?XX*M`vN$TX(Brd#QNa#b)rKW`4bhqs7tmnjpXBM_i59{}%?Ut&s~h$0G(}0_ zdXd3C<l(6OlwiQ@V-%aU*HCPl=|D}B-#tSdO%UAHF&7vld0R(v@`e(UTQvePNR<|e z{S9H761irPNc$;Bu5yK-?!mR9pi7d!gvtJJwn+mZyBxPJfqe!<XgQl2xB#EB>3R8l ze6zM_diRYKm}m|zV4^8dwo84-t^L2=uUeTC3^+wYtF>Qq|A9!B{WNBra6|UfimzGz zc}o6yM%Z?r>J?{QrOn&fv~Jgj&nAD!yDhZ%`dZmt{6nUDB4PZs*3ADFJASKt?AprP z_SWR612l{E8(RKTRcJ=$U26-PtesO__6lhrGiT9AY!y~385akY^yolq!n41=o%QQw z76G$tzRRARA}ZPu`b_&Q!Ei2lVEqBVJYqXG_4%^LL(a>SrKbBbTL)>8BX(A&R>O+~ zHD{mFBiCa2<=ZtHLQ+;fQ-oDw=_Sg@M8c7+3x1{O*o!K1N$LH6ahh)i)m7g1UaS=$ z)80U$8Wv9yG~Gq!E-qW~AbBrr^WP|R#2aPMEiRYZlVwaX>j8)Nu~?6!PAn^@h?J@_ zS}mA;bz$c{)O04;A<{}KMUWOB@#lj+8Lkvcy913P^{%HX2s-k{yOd^$;Qdc>s_U%9 z@ykmn5+73j^)-P!Qr-d~bJI`s?z!Jtn92t<RzF2TPOVOf$3{Z1K1tqJEXK<)gYI^Y zD&u7v+lZ~W_>Z*s&pw)5!^*$7eke1nt^3P*doQ}gQp!7#gbTila354+?M+MQlz9EN zORV=yqS>9)k%7>)_1+S@sl;e6#xMhRva>~k;(}_4ca|dZ%`W|h%)|5t*4{$x&FX4~ zZB&dJf8yzb_=r+(dr5mw>d!&wK6+i+cZ$+p*K#<h{4jTJRr=sdRmQNKGlZt4D;adf zWFAdV?-0wmBN?3LObh2ahkMplVWwytYaB!4+qHMDWF4=Mwl*~Qn|;Cglu>a#)Z>F5 zi>9BoID|VwPrHb8@CHj(U5Q|A{E*V&!ag2py--CL<dTXaQ&!^f#>ncZ$3ytMex}aL zvqGUGlw*~fGBT`FADAeDe|Gc|mCapqYh|<qu~J=lZ2Oirh-Gnkaj2)G`_Em>I*!iD z#+6e&_|C=7azjb?lYW31SF;RDy9lro(?vD2wY4qE)Hxde2tTC6rjq&M;2Zn4o}@=V z?aoCVWL|hRKEsB8XjCU4xAn>H-C4kEEDFVv2boI~Z1}*XyOhZI4>X)6S1{Y-u4Dzc z|CKXo%Y((36!U7UeyCre+MiYXr@1Yj>5hZ8r?HeAwb3qe61ocK^#%KQP7K9OKpVaq z{zUnA5b;O?!fd+bQF>(~lk<Hv{o2X}!+l8is}hyf#zcEa)L!dh_9+4FrZy5lK7Oo) zhO_TI|5^=eEIrcDRrMFADaXVJsi9~&EV?jFMXMweZp8obrkP+ms~K&SqD7TIj$8xU zYu8wFE8jgnLvMi?<=BB+@ruYwtfkD!cZ}~ODy}h%jB_1AuC>I#Ma<jXc1?H@g!B=( z(7+~wmBsK1hS$%TDp&+v0UI*hxfo&1=b8CAi4=2O#S;eX?Ez0q>O7Hbz5am~$#Yrn zCFd`T#!u+hgwf|#)teH9h{R)aV$9!H;n*J55u4L0R2feR6S%sDd!{OXp3BDa3Ianq z%UAnX=upqz05Bd26K|g+6e^))DC20b+L;V8ncnbhN2v8x;GbL|m}3#v(?Gsgfath! zL-BTDu=CNRQSzr7<WOdGTs9z$99t<s6chp(BGf_CH)Jc+GaeIRvoj88*OuE{NdYrS zV8AY#c{oe@=GzUgqB1C1N?7ib$1FDTHB|=X+mo6L&FQ#8%hPOl8883RD=Fe(zn@`w z9bxz~|0*7X<W1&$w)-()*T%gIa7&5m_K!t!SKCMU9!=O?Q$Sn80=lDz>9LcaX+U*~ zw-&5UXT!<MwBYd-I0p-xxQk)bpd<M+Fn}o1vbdmP+z!pFb0yKU3w{P;Z-6BD_b`+k zrF6NlJ3g@9&^r9)?FO4U5KFmRP-l>vpE#8GxJHf%I0l{S^@$U~udQ!{2;^9tVYG6( z^kicYZe(k?%7M6Ii!QW^v<qSZ1asKII)AU)38cHsEr@!LpB%}-uiz>S3zxV$@)!9( zgXK>IdzczW)!DHbBW*=z2tYs1U-^5602v&MA<2(^roWS@hxuB2|3Y#r9KT|kQztLL z4ZZg+c|rGIUHJ+Ey<Ujm_9;1(*s~ipHYsfu!i;#1EZr1@W8%njpxv)PyYc)UrAM*% z3ykB<USs6!itJ{;0>hk5F14rT1ra0N)zg3pZqDZQ6WGa`pcJexz;9;!dpc;!5+Z?f zdEZHF0TgkSmm$s((M`URBdBQzO&asuehg4jMrv0W%EI)4pbt%K)lnvT$TXz;6NdyJ z(68<LH8*A6*U{~Dhp^fSJCL3tL>7t9arC=YC;|_Rxg>H`#Gq&>tQPh2F|QJ%=hTSj zm&fM6uI)XurNMHfi8knGDnBjW{|@B^B`8Ora!{NZ$(0955u4(Nqb4#_5EhsKtJ%)0 zbBcN40D9mpuwz-lL_ugY5db3M$L-NP*M4Jk!~J&z*Vu|GRT+QH^(hd#or&>vSdPZ_ z>Lrb@uEna1xi9!Z{k`NAfcBZvcOv(ZF7pJJ)E)_1r31pv>o4GwTYvrBR@fMtOnl8E z=QId2!lfcVKTr~ST4+eomg}k#uh=KLS5^M|Y=h*+F!E{+d3k#NsY3^wo|l!Iy9e6B zq{%p5U>lY8lj6}@(Mo0z3-a`0pE%NbX!dCPa8FHPX1=4t;W;|mFCV0w3EO4_@0rO^ zMdaF`%)RC+%jpXc-P)i=Q;s)R(KUI`r;*n}DZl#dY4qsQU3S!tjWNlvIT2^@0Isa$ z38_|W-IP~(LqnqaOsTgjDfhmKbjLDZSns6hZ=XiNhnuq)MLKBm+%A2re|*3p)Z!3G zi+m8W4uXE==MiY0TTM|3N#rUvE%^gI<p75cEtGB$vXMrfj?{%D%4pm~qXbv2W62xf zpO4DGKgS+g{{$df5^=M~;2R>vO&7RFT_C6um0xb5aN7U0u3n+Om;)i7##VPuZg#&M za;<&ym2wda^8(Z4+*D8r;2;@O^ppX+`O-Aq>`qY0a3OhPW)uXE185lnzi*uQVtt^y zF>*0rl81~X<fB`74uVT;;>CV?|97ClMRciY7^y58J(a1ig2%*g^1ecI#`H+ulp+7x zL9%}SD9x9Cf<nu&yH6(?Z`FFoP~Cxy_@P!V0kllB>Xm~`3lC9PUIHxrm~apuX<yyd zgNre}9y*;>r6~DU^h)||Zb1tJ0bNBtHqsExqz-+U&yIL~zWh7nogO#S0@JUFUsY1% z0HRhr16JuE3r58x3Hlw23~56-jXmgVi<)$hz6a4V1G7{vXnG#@BP#&Yl_Ov{@E0@w z09Ezl;%ZYyE-Sr1J?LQNfsXFdAi~AxG<xnTrg4fjVsduks0wwJmhv0iBJ<0h=?Gn` zak~%Vp#B~Q$Yp_cApqv~xoeTMbs3-pW;Lkw7=1vCd(Y4l#WnBM`Mfg})E+Sf#6F%f z!05sZs2<RIkfUGNwtne~C?EpZcxG<mS7P#WO|Ywdo6N}{!wEgL5{b_tT>P=m2rlxl zrM^ZjnMeF<EtcQyEv%dvMKv6>^fTz9GW9`_S-#U#BC6`$aKspkGT^_?59Sk9R1}Qs z48WqZSNM!WY13W-SZg^e9h1JIBPnr;*~&?+PGEIP<uP631*6mw#^-lM+J#>>6!~5U zkq#eP#W7%=Uwd8moS{+{*9E(>KhPn0H<vz9%GZrigQ)r_Zu<!6O)xeuDmYn7dYO1u z@yj<F$rtpVsmFxfEWsAlOA!=>0UVF+8S?gqndf#UFzea=8#<n8K&OATHYiSRhM`(& zE<l?q3zm%SXs=PaVG@uSdKwq*9IHMmk7f3oAX9#xBwSq98T&+FM6PvDCz7W?9&)_$ zw(s12jZVNQrd5%UW)@s3f?V#s0b(YNGp~iVj^|}NjY+!_o8a7<ndjon&gvewB9w=L zC}_qlpDb+E>?|L28!mJu(^#<^r}yCL{SO*J8T5Q*RyldOP}^rleQDqd(9`vk@e?Yj zIn&r#L%bd%SOkBE{b;;T=8yvZxE!(QckoQRa|RH!Jz<$Y@vnpUuO-13sZCSF<NCyV z93KIF8Yp`uGiZ9F;>F{;8z|H(#q#7=AQadhX7sU7NL$05vTKE}vjRD{sdY+?wD`QW z>k;+m{Wfm+A&X!ZT>jZONDy_zsDPF2l7}9>rcJIgz5gTN$$?l(9WEPKN!q;Oud2m* zO1HRH_R3QRyhf6e)DA0YfC)I@?MwY`3kL@l$usU^uuiOwD5dW3<@JnQ<(M&N&}%z1 zu!Lg;3u($^3k$fjk5KkX0=9hU@LAoW#?tczSm_)j6d?Q{!B~ozE>5Ix+@CVw!oGbr z%s&Bhw{P_yLi0PMy|2(XZR58iv41Wee4K6_FM@cGL}Ei7s^l+QQe(yiPcqoiu*`&T z$8F3ck0KIUF*(!Axd)gRl28GWyCWdPfxja|^8E!?WAcf|rk_rKMJ<dBV<|2OG)IpO zlIddjch3Vq_%Yu#80475(>Cdo?|R<pfu(W+<b-h=B}5<*Vm)6ge0J;w11aL>F_fsC zz&y#!BHIFX#*%ymmsL<X;rQFlK4MnK8$Z5qi(tFH?$3MZQV^VPE-3vcpaKhswf~%e z=5XM^O_tbokHD?wf<!a;R5n)1`7%`=rSGI=VD=xTwWiQ=`!M%+xxfnGjMHKMzxUVI z*FS#y`8DE7*$^h!HY6zijsm0g;g~BiB_OaQA9We#KMNDLw}%J7hHVRA)!h0WQ|nDP ze{wdarp=j5TbB)<6z>$sj8w|?PUs^o%nM%`2UV#KJ7|K(&<#vk@Z3+=bC{2uZ<)(Z z7gjO1@!ZyjO3a2xeNZ*P!56mn3R_g!MR@st@Cmr_tFx=b5L{ucHVBMMpnrr{zd{d} zmFgY#fRdva_vOxm*pFMY=g)m10=c35+1=4C#YdikN=6HVB5kqP0*NyGa!yZxNNxg= z4BfJ#5|iO}X9A<1dkbp5eDe+Ne06<QX+P;vP)6?=G4_i}Lha6=k;mU-+;KMjNV@}= zkkEK%)-1NRL@M#8J~wjKK5~-$BQh4;4kI63&+&U?=VeU+u!>JlE2t#qt1|ZWe8xr3 zfE^_I(4U%HHwJhy^z~lU!ciG+AZ0nGbHa1S6ujr`@)J?@+6%Jus}_&nRUYi;kmc76 zwO4^9S(@J;-YiKzT!}?o$KE<*uDs&>)xted@S_Z)`4(>IjvFM_j!5B#ML~Y#^@1BT z>p`CVV@LC4csV3-W2Ql1O7;USXJ0>?(3~n@sr_=?+rja2$Iu~K0Hz0k!I0-P=}18Z z7RhF}3IZUL{1Ai{mo*532c~o4MFjs;Kr_pFtL~VaInTHo{693!A@lWBmc?wWJ8CiX z0C#hx+0~ziZmI7BI_82fa%F&@m&cLZDADYA`z~EK4W8SA@_JsCfSN<!;9?;8b`CLj znTI29^+I^^D>>Buff^I^Ons#Zv74wMOsu(0&0>>>)Y-x9)fP&VrB8~2gks2-*oE3A zO<AmFS6BId<^>M$jst=~A+!*u#SS8C>FV`C*GouE7yXf3!lUM@?XpBg)M^@Yt5K;M zc|*CqF8mJvY^GGR<Q{;K6#NZ>4ICGp^+=Toz%+uy4K^GA0#^7ji+IH?Z_f`FWQ=1Q zg5`HPrzB?vJKqneI1HS4Dd;tbp+ruTd$e`$7M2&qfY^;wp2KCfF5a6D(Y}J94=Pen zF>qHjTTqSbu^H+$HM%Yfw(x3M6afocr<)V}`Y|dNz|SWCRVF>kFk~rcVl9wPJxhxO zca^sgD+AB2u6{<3^_!1@=j?BCn!-og+y>@&Ovs3iw^hQB|1GNS_6|DQTgo{B9bdqP ztOb!|O`v;D(^zC$CsXx3iPBq6eiI;Y-|{Mr)qkH;WWWxV1RHwGtwE(~lSb3q>W(~^ z{c8$Ey=Rp9!oJ#s4pr-DmsqeM8`+_uluw!j+qU_~5WcbLHzxP;s{5o1GLyE%s>L9# z!at>$NE`N}fKiZl(^h;Cy2&el9Tx=wa0!hWbnVzH_p)dj=R6HVs5AK=`D~AQgG>|X z<LDyFa#;aF!}@v8Y}dLq*bxq}N&MTIO5`2~=}_*a0nVUM*w;*~yEV8{EyP-3vepe` zzDBWBV0EaRJfbUT9>7mj`bLXCQ2zVEGK3y@z;2O7BRd&h<5c=>2s`Pv(dE7lCvx?J zU-0;hNepe>eN-@f0l^?lw(dl}ubMUdwQ5{idn{;56zrfQH^#~+w@}%L8gtX-wk7=o z6vD};6WGyBuhJ`l7g-9(Jzu~2Y7qM0A&q)&W{I^k>BKn;QD!8;slQdDRk<+m0gImR zEUI@t&=GYvbaN;)u|5GGw;plWhYtM&hD!%opGctWhi+3(%coHHYc}|TU#|uEi;yG@ zJ_3RQWrR^SOyyvDZEy&jjZ*-GwYq(5O0SrSgnb>Ix*2dmE<))v(zKMCPpAx%hdcoX zBJl(bD4@EWEj9I}u^?(Z@gxLxf=!l|ufOD0Cc(TQgZdZsKpU<ESxsm<<Uc)0Nj4P7 z?-3C19n{!|2z-R-N1MA2kN-bB*t!Cv*z<EjA%#M~F4I-ECG9X(T+I2C@1$*<T<`Di zQ*&HyypLmz--qy(gL&S({d^<qD3ZV#qXO3zRUfedBkyA?#cNJ8yGn+MGmKXRr@S$0 zx5;3P9Ql>uka^4A^W=P*V}Qr=eMmATtcFmz>E$`bPiAlrq-*^5+s#PIOIu>lcneT_ zOA<L<p%ollS+2){zXo**5rLGn0__X`)ja~f8|nlaGbC&RwW}BOKQ{BwETObgz=$q& z_^=Oo-@ovJe3C=Za3JZWzZtr%3Eemd<^H(fYsI;g5HHeqy6T(9Y~$ONn{d2ZEHiHm zyOz8>35zy}mA-i}l%1rjw>JC(2tFhu)?w)chl5X-L6NKo#WgHVCt20EdcoQbnX(NU zG7xF+N23peK(GA*5c@!qZ-W2StO@;d)T@`Y9JGiBkf@wV{g!tQ!v*AfpvFEAcBg~E z<GJ$F?%`Dy&i{D+5C(x2e=n{&WM0r`nQ1cYT=MoLtOK=;Ro}u<vvo1Y%mtYdPIpt+ zCn<}-yCmt2vc$j%5tNIgFPvIk2mng(@CbR?)(?K{Nif}mUc;GRCiJM&;Xf~e%f_hr zQF={tS>3@Tldkb~qH?+7$FeyNR36O!(v2(BVpVB`9rexrfuFsT#hirsUj6wEYA2er zFS00OX6g@anx2MJ8oa9Ljf5H~N~ryuCihx(_&Nu6bJHae1Y}^ixY(R(y3N`BQIDps zF5M_Q&MWMi2bM`M2-(}XA<RMKa!aXmOHGISYhl0Nv*ee@f%bpT=kr5Lh<h<)x?EY< z55{M^T%iNdab=0^^JQzXFJVlF$|_rDLz#(OVh51R{nN+EmJLFF0ftvs-qRo>KO-Co znL4^|wVD73ceO8dHIp!@kJ@e-Q-p?9&k{E<>N-|mb-J*tJ$R<%7k)%d7m*N1GfG`Z zN$2vq>^a8=I{3FlfP?4PSL9&SU2klQ<L6cE0t%Vus=olp1$!>)#HR7dKUX?YRqj3s zQa^!$05fyU{t}<NWLbGy?v}M=?^FOHkuAeB9-M{Rp~ci^)N7BLj_v54`imuPVq~@= z-LLMC8H>rS&N2^mA3h86`&avF{iQ!oIT23b{2ols#wovMN5Yf=NXz$q;87%^UiZrC zXL5L}hCvY;es5aBZH?BE&-bVx$zv~!(kqqbTnXU5u7jiJASie31QL}Mo~B;!XYaXu zksNu9Un-jnEb$R#3Dz>uIZfckp~q|8SHL$3|B(6Y2;rU<8z*;;373T`Oyxrw{3{uW z$ARN{6zz2Nt&y94NTX4b5}({3I`S4XYGI7Z3i86pu*u>eLL2y9d|u8U>!W1GgR}`4 zFbs5kycbz8a#sPA@SGmZvEj#NK>;OqD(zb1;J{x=1ZjA_r6G&EY4Sh-=BC@|MY41Y z=!M5xi*`pi|GOJa)mexH0DnwEP+jvK_B3s<Imn$XoQ3EC<b7Av3-YUQ!MULFa4ST@ zAnlHT-}dC_p%2LI<)4GZu5FQCn`#JXZ=(D$ZTDFof^3s&;0~WS_xQqpsl#JVOvwAe zU*$N6{_+MYuKvD??k+(IP8&P*dTojFO<X`%MHobB(S(-rc0JXO2R}~Y9WfMXUlmOq zq_fnZy5SIsE3Cc3s~=2>mlPnvak>9UJ~2~Qx6t%7s7Q-4xEZ4ZA|&&`qZ_Ne<Wl%M zWO>vf^G*!sw-2<b!=si2MIi<dy+<oC?pxQWuiW`{jh`ZoxhRrHzS+G3T|^5G3YXW8 zABqAJ@|0hGepoTF<k&q+kCrnHS-@8Hz*eHfT0pbb(}Q&6MPb66S*(kKaIbUo^IJZw zNYukgeFC6$<7WjWw3!02kD!>Rm)1afWuJYAKRsO?ME%d47+I)^lH_Au@JkHeHWrm) z$EyOx$l4$aGn7`0sJ@oCtsWur=VfpeFSoorPCf>_$>H&tN^t^iO<*1DKg_Lub;)X0 zdA!ny^haUO1}_yO^MdR-`+E5a<zx^!uNmALj8qPgqD)WO8~(NHmEh8xg#gNI0H(^{ z4+vtkO%^|H8^MZMRRLEyk#j|x^`B9{+dgc8;^Zd;grwo1qq#*nN`6(OTah5fhI}(? zdx?uy$`t9O`87+r1jYC|6$>@A5-boT34|LBdvBNEW&Uh}2va&YUjt|VY$I|%lA8n( zRYwcL#!xfiPtkn#1;nK=v5U|ZqhnE01KwYakDuqKV}u-Fd6p*do&+19t<80gc83K^ zMM1UKe`-<w-wBIQ_&dR;Nb{Fj{oP3sl;vk06R2?!K7xF^rY{)Z3}uW`RpZv+ANvev zlBFwAU6zKLrt{TysqqWXxKsLE*wc^EwY{TF{jm~{@r|Ux=T%S6QtEOrYN>Gpp6QC9 zj6SEtr7Ss;HJ2Yl%(rI<jB}RZ7R$$hTW2)Ao#=d<*HTU!k))mj`-cPj3kb@Yoj|Uw z_0L@-H&{Xg57m;nz-5s4*$MJ;ew#lj5X}#Y`2ZpGbsp`)`Z`7HCXo3b3;PAvXOhey zms2pE37lenZa$EGf<+VTYjHyDSu~@kXHQ2*h8#lCX02J$bd9FuVE+gr@mE(i#$B-; zILG;QNU^Gl%_~%eY|KDhfgkk5*MfM)vV;}_R}Z8mjvh}lJWU&xUmEaT6jv{F0QqIt zGD%io(~b@{mvjz;&0^AwiCoL?z=%dg42XMiu9;<8t2>4DN^)aS)b;;smmC6|4Iiev z_zLUz4bCARGL9nZo+Jp4N0v{u)UY*l7#4Od4@xIW;=~XMF7zCDOu^~=zNJRYchil& z1<nMEf@^ZE(-kyCTuM(`Y6Tgi`ylkk{7S-;n;2t$2ISY+lN>^u+2h*ZZ&62hk@cp< z^swRoY5{2R)`{mt<!0Il_jV2if_Twe|4=D6(YPe&w8@Y-mviPEdS3ZgmrNx@I3}Xh z(bNPO2l~WX1L$JaLCh!eii-}P8g?@%aSDHa>%~Dtu?R9cO~>MMsv#$<2v_QCcTlLN z1X&TCY~5JK`3;edXZ$H&VDJA&*OY~Id<$n?h@<|({DFo>=1SCSt1WZ)dt!Z0&7Koa ze6G%jA3uLvgZiG1z%LAB{AC!JPuT5x23=EW7Z<<$mv5uE=sECBg_*)yY;9z4AY1#1 zDdGnZlXn|4giMR%+L`ZmC7N!?cew%!V&L~rl#izNa3uAAh*B}y6+{0(_<Tn?U8XLS zmf%nLsX?x-hLiIEy?pXZINt%xw&r1^?C>S%BdLuVwn09))-^oPMw)ZHpqg;UGci>C zsT`%_8+|2_JW`4?h@in6lu(~NAK#!n66A*%*vsKlN01e5scin7Iu*G)x@i2pYY>tU z{4q+RAe<r4*m^;}*RxuL^wS_~`jyq&fzVcgDX@D;VE1ScdNk;u2hWNW8S-z(%!r>7 zlBTON0ica)A!19-z4(Di9WZ?il+wU-L}kx%2!6bDB!%*BRy90N;T0MZVUW4cH_$5r zGGB^d_LOoIxINZYLD+Bn8M(&tiOZ;y8sz)Boe3kYkPCbQ{JE2(9tRuqPGKFN+!!-i zKiVriZ}kSDDfy92`I&F*Ox`lv$0BD#D>19t7{rOuR7V%}<%SFe|5Q9-L@@Rd!5hY- z{zpi&kg}dbqb5L9`WYAb>&gKjeg>IX+SHEKcNqcSqc9-?2avvZU&!PqjQn#U+9m6X zpvQidlYcux1nnyt0{bU|HJ17gEJ_m=7gkw6NfYH{yDUz`>6mkm;BeL1)h69I`#0iq zj}Jg&()V8|PnQM5r};sDB`ss-6h!?NgkW(9p?bbQH~Qjs<G&_}B}I2XOB(Ns3IlPo z1-b_#2eGCyVkpM9Py@`3s@MIV{w25EB*}6rNZB)>Xg*f;Ldz!un~#B{r$YV$r<Qyu zl6bs}LZjHgJ&qCYRjQh(1{LksxNp)`=HcAK(9feCVwrhBLyV9TZy%Gd&=`V?YIRv} zQ}~dX8(>z8x8j05i6E!@1I8&XXuxuzMS)=3s-9*h%FO9_4AQSB-!?iGV;!%7a8OL3 z41(rJRoZm%>~<wLSj4@Y(B*JRcab#}L&{>Hk^z`b?NpHwKPVzF_1P>_KB0C^F;D`3 z0OpuUM$;XIdQi1K=OoTHbUnmh*}R`ow7dkHFCcLJ1(&W3LSEtXvVi5bpXa-RXkSKB z9pvHT2Td(}Y4sd8bulWI1{V5gwh@x%{PIzyZQx7Yu(A3QoPLxhvj9jHsDt<^mH~M> z8}NMN-Rgh*qji(azUtY!PneMcSL$;JZ1c+w#lG}L<0x!s93`Prn_6=0X&Y^txiA~& zMcHh#bd}Rgmm@ci*k%Cc>Ts=b`;ZN{t{hB}zcn@bBH`i^#HR~fKUY6r9{|4uWo2d` z@LI(Z(QpDM82{1oN)XF6Zshc{1pk|l;WCjn^;}o4(tI~&?&&^6u_0(&&|L+yx_OTv zn%zQ&|61k)2~r&@A8M$ePP2%4V(s<<<s59Wq6iF$8aM-el9Ypd4WmU0MTO`h4ow64 zz<7AVC=jwiwVO*FrNK_Y85h`fu#&5q1^e6Lz_v}E)rpn|k&&6JLOuym<eOI{`hc&$ zZBl+s)m8qFv5~L}2lJAJS<0}Ou2@4>t*p=AYLeZuGKk4r3XVLrB+h$kxLX`fl*jMv zDp?x>b*UJ7mI7O0-;Aa+3eS4av>v0geFHn}?p2Zy9c9{-?u7n($>*fyT!yKI3x!I= z|0-n>6Fh8AgX6Py>m~_}H^xm!oE6iqQnut{dVg1~II0R<5<X&}?qQ_Z&nJbrG4!{6 zVttDb6+TV<TBuvyrXB2^33Q4k0moN7t(hdAjwzAXb*^VLIjq!yQBu#zDa_>k{t`&v z9Jt;cgIN86vK%@N0e627Dx)}9JK#4iLaxOm0wia|n9}Is&5j&98oq@@80~ycB946` zDiVN5B?PLFZEhfq6NyS_WkiX(%B^R7Lo~VRka=ymkW;J1Nt)eFQ(3<))bz9~y)q!o z^q!#hGAbE{`wFfSU~!k}Vr|crE%G5o45R2%Mr837#G~WK1>zwNA!-FFUo$|0R?94I zC`UE?hrcd3&c-ZJ()98pD7&U$Le3sQI87sc)Q9d^`8lq+aRVaOAcP`pd;t*f8nVwN zNLaRNxV2nm@0lTChWGbqQ$64S=KsSCYnJ4nN2~&;&lS~*Z?ak1(9>bdfzk~i!;ea_ zwy8o)Lyrr4u)dYqnfBwpAdz`BsB$=@aV1Gn$gd*<%>)$<iYHb+(n506AiOf`Mc@^U z1(B=){$$+2p@^*qhq;5h(q&J+8-Le$=L$*cG*D)JB1M#dK#fjwEwz4uk4=Pkng)CQ zrFr^BH{2%+(bL?9LHUmg9kGxs90+u$*6YVk%sbb2yRf?-r5nM8w*EUY&2z{2W9ESC zFhZi0iJL6y6YTY^S40x-K>ji01)Loe7;qlom*4o9DHP+@V8$LLNH2dR8!*O)(|NoP z3N!wz0*ckND0eT}zDu3(MH2Qy7|Dd}#!Z%M7F0m<a(WpT36yP+8xb~EwL<NQ60rX+ zVxTN}DvJin3_i!9QNE&~O0yjI^7~o{r{xqwj90@qG?~QA5I+GZxbSqMtcq7)^&Yq& z2JBI?00Mew^O$3p#Ipw>MGu0o&Q~mcGTNWfO7VY~w^^klr>7AXR8BMZeeA<D45K9? zcOytx$N#`6Pf4K6@xxcjk|8ye*mX?!46*jk6-nD906+l(rLh3{6TOVCFrAw+4`UvL zMr7;``-Jm*y50hSFP3?v27j;^O^=^W(@GRMhFtCeQ92yN4`IF%<d^&3YM!kg<XTs7 z<Xu<q0U$3`b3`{3M>}R>9^8f5at^}@Zb^_U0f+)?2*>}V-@cx!Fw2ea0J1S5xAw<r zE~HkX9`h)hz?>4l@#1R{ryK5Wpl1M_=hz%cm_Rv|QiOpC9OGvQ@iVct-fh&^%T>{N z`>doO3BB<-9^5119vuM^U!&sNGBPwGdw2L4|7r|I{0ZECT~TE!$S5Y#GbwJ80O2K} zKq-2weULleS~eswgD=Zb+?7J9Wdq&(<4Q(XMxh2Z8)E5}>{{nzgkC%aHmnqsvpfuq zBP-HVYn_%$v{Y_ccUEail4Y+WVW7=T(CRP#HL-p&#Pqq@1VZvubINNop<u$C<yvSn z;hGM}k#RzxI?PcIB^Dw>qP-zKcH8viU}i$E6ot0Y-L&Bu6bAgq57^a>V2yeUv&<<; z`^}#-)(c37puEa4y6exdIi7%)F~L<G0v()^%Xv6n@U7}=FzcUmp@=3?=8_n~BG%E6 zWOnNjh&Raf@;y_{q2G`s^MdZxxT1HI$@}aJpsrM0J&1(wRi6?J;wIO{&$KMm`RZ_Z zUs!{Rn}XrTfh#R!wjIQPu2n>3i-z~jS#xkdq|fQ`^N3$H1~L~El(RUDG~UuqHH({A zZ{04+T2oetw^x7+*Jc=Ht|2aP8SL)G%8~DfgX)3P!W0Z+eKUkRUL@*6NaLOj=_dQc zH3PuRUCgkKC;mKw*OvCX3N}iW-rRs(uUE6(GWSPD0#cR+1&a-vJ(Tr^r~mJ?zkDKK zP;xn%u`^9l<^aSlBwaO1rj{VlLQopx517{5s_P#p@n$5|!&Y|W)z8-J<N#e9k6wm; zP_$c42>_6Wj{JSz_GXpQa8S724^}8^AevVSZ>N=J1;Vv8=vL|pfpaE1TS=h|jC`DX zj$AZ8_gd93L*E)^>4aG<U>k+F`dG>-xY72=jkTvBQb(@_E&Gwh`eQXo<`tSgNw!t2 zdeT#}7~WMADlbHin88-?9Qq9+;Lg*DYv0OoLl6b?jkue4m88!M_}%6s_W=J~>jpbA z#TeIYVE;_nH=jEEr&3eCLTtP4PX!q>+(`327K8~uDobIOFT5Z{8zMr_{#l9c0|DrS zS!Kq-{R`4T8j|ih#)WNz)gl9xgOa^&J|&yecFC6O%N$*!^b#h!+(2Y5L~UhsGJ}^I zTMJ((p=$Pj^KWRDUh$3$&C@68p9H`ZvJ^jVW_3dM&cUb`_HXK8qA5|J;}#Xu&A#DM zhmH5c_qK<wuCmUXqSJ0UlYP;sgoJxCV2@}H5?f;6&#fxehH-0SiD$u2s1r*8m;UY+ zYJU-n@|wVE0|7zq1cU!7l^uI&V?hY5a{D4gemO~gj<Y|Y;&)Xt;Jy2{Zw~9$ct{ro z_t6jRLl(?9)V-p1@uDm&WFGzzidkybi0%vI&;yGVpp~;ZbLr^i%TddTjNt_{41KVp zL3zNZD(Cc$5mm7VgCxni?{;1wdN)+Gd#A9jXWCz+I%H1hQX?>J8qoO<9ugr<n;P-P zg7N0XP#saQsFr^|;@^1#xp#_HI$b%4z*glalz-0c{1>_Q@xD+1S1va!4+={6;MRPQ z(TUwxvGcb+X7;BaroRnR?<+8d+HZy<JFl?U18s^MLQT%9Vuj%-SC%m$m;<}39QgAH zv_%bPt`xJk$vZ(bA-nUU@n54uISgI2>=R}Gf&`N%Hv<8okHl$Z<!?RM0=fTE<2rkl zZaEM#L9*_=acc!7upAj`U=^y9zj}wjZ?p4%g!7o@&!<)<4`^Y83w!12X=pkIkAxe{ z)P~j6BXxNKw8Mlzjl;VGY=7zBt79o+p_ecCHG&g6v<FhcYm5$kHoZ{$4IIdi`R{Bq zYR~tU&f~%Pd9OERkh;&b-!O)_k_RCMN4qnd)wLj(;_%Qo{Reo1p8sd-qOTeq)=f&U z)M~Pnon`j!d(%u5IkLBsgE7Z)TGvn5d@XEVIocEY_QCVbjQ8ZUImsR%U<L@XFzK<o zXWs~Y>aB)pMYTh96jqH0yb-%#S*ZrI=WKQgz&wCuBk7f<&C|fu=)=--5iA8uex|-q z+SCd_i%OL2^W9frQ?~aunab|}0~dh<6^rxxRbqDEq;<uEwX9@AN;M*N73ye|s6ww| z^l9iKJPl$hJ4!F_`l9zt9B@_8z4N=TwvrE`3kfjwJ%WXRX2zS7^><%2Og?|ZL*Exo zo9eUt3NyTiW-OsZb~S<1$&ksr5748S>Ulr7W_L2YjBfzcEOlj&%mD=ffP%;i@I4^< ze1HCCXkT9(EjIyFXD?1!c$dCN+Ce_W;-MAU*zj%7tAxn6yDOI^M*b|bP|v`ITS8oB z>kV6Y4QL_sDT}Dl=NTLlXVN1zg+RfXN`&moofr5ek4`HUTGde2hOFK*I+~s(yGX%v z_)@LJX<4q4n&y125;XUCt}It`<eJ5bD3kXJHxO4nMo^&2aF<cAkyWzfh<ic%=bhOn z=DCs{{g(`3A9%3B@;8bZX@c+3a;HJ8BjxZi+)#=>1tXL<TXZYj>=P1YM1q+JbhLwq zG$t0^<@>`=sNH)1z2%UUrxdrn!<D}63!FF@z}j{(l(nfHcA?>!u?KFFyhrgIk`_+O z9R^3GquUAQA^PY^J$jQy$9b2Q`|{XZ%<!?@mGzDc$LRZ5e}k22KVR-pSvl>s)BhVf zbUv&R1uJPY1OMX+scAomj?o=cb7w9OL6)TxjS>La5jYM%oDN}ckt>1+myS2?KhtjA zmHL<xxF=MOdIhIsGbaOXlXH8|q~ZK{*TnbE<Jwzs{h@%Zs54d+_KD)-fXcO}?zfa6 z8;?F#ca_qpLw}|&(Ij7n!LXtFL{IoG9epI8=x|Mo;<c|mKrW;C#A5_5i0h+|6)jr0 z-u!?@CWc-<D(wit?5|ynTBVp1N^%exA|L@*dgUe8#;xj$yNA|zp^9&`Zd@2v(hUX2 zvG+_cm;0Svni(1Y_f0S@Cq0ulr#-N~58=q3tcrSdgfZZN)l=B6OUh$P{+>K~|G{wN zni=P9_&wd{%dh<BFHSM-om}Sh!>4MMMI7`|VgT9`hfJqzk&ZcNfr~y88nc?lX|k7S zG64iz0HL0@IcMh$k_jMe(Qwkil(qoEO@gk*uJrk)mCiNIjXwvZ{@ddp!k<0zbjkQL z^2L<X3x3ZCY@|wv5t#hnD*puGl;yw4BDU(GtRv5R&$wx=I(!QPC5b*^ll4!R(}vG5 zaR$IxG~rSARMUIs75>M{gjE9)1*$2k6yiam|4(MK{1~}cM=PNNRCG=`i6Xp*hqNZc zpVv$|eVM!y5SVD;d-#z3-ZH^W_8>JvOxPvrVbI@j>|;;)*F5*IbT2MPq|F_Wq(U#R zMFI(VfqVe>RehIa`-YM*Kz@vk`+$Y_K>*O}C+n^nBAKP7^D!h`xWAut0<2FRUC*dp zrXYz1fx8}kluk>8**iBnMxYA2E7;n7#2$3re0G?9Lhlfuwe5TONck>=_zDkK;IX+w z^jeDa^4HWMH2D|A)f#pM6=`%Zy+m>Q{u$B<7<vviH*+DIeg{i^Z`2qAXh?s-|Nq`O zsUv&pOLXerH_Axv|G{iMYQz+X5TlQioa4J2WqLR!@7!Dv;ZiF<R)co2fNfa`1_*i{ z(B`RyvL1yGTZ9Q4`UK}a#wqu8C4`&^++w@-n3nr&VegqjEwEGeM3A28a<*BTr4Umt zkl7b3IN1uLhlA?&fcN>DW0n?T%C6@Mt44sm@y|!-sDwVb@i1j8+qD$v;Xay)(>c0} z@-OdS2&K=_Nt3YQCqSNtJ{4YY-#d-7b0?v8pDqLMuSdH08H53y(D1bytzD+lGmht9 zo%rq*L;@A1cM;GL1kQ)#-u^~KG^-RFEZFtL0gM-L$`H8Rdb|gtB#(x1n&n?ixxBCe zBiMkb{T|<6Yf?F=+`xDfs8<@i`E*S`LHQlvC0;4ZuMF_y3en4#g@tH%Cc(`-@L2tq z!jbd-pBbU{XY>GH^&7U)ka&jq?Y^2n#pd~UhejF{9JT&dr4iJ^nT^qmhens$*knDu zc#WqVmm;sJsXP8f?%Ad0ds)6`nd~EOJkfXJXJBH*Fh!pvH@jNVo+aV&cbWR+LtgsU zSjwLK)})}=H11_SRlU8vxoFn5Y_hx~$zE3ZK5wYrT+hb$3Xqyo5~7;!PafK2hmI)l zG$zUME2UXFF(m}i%71!7zK{~j(;<ttIfut!Fl^yYaGa2p^8N`4M}d*~9~f~>&wA;d zmsZ^==@fg-?G`$(f3V?l=Z}YvbU|ml8@tle$ipyuSW#t&UBYsocY;3A<BqLA+>f*~ z>(`d}t#HP%C_Jl|izKU>fZzRHv(V~Yt<B`NuNCWo35jisGzzEG&hHy_6a<F0n7p)M z71`tAa~MBkLZ#_0gg)Q;iM1}1A-+~R2PTRUfstE(`>U#ucS)dlP)8$X{8$3*STdfK z*+!BzT{mVTXK!Rh@8sm~jX&A=RY6h{ZDLf@jmcq$UZ-8?U;llQSQ|LGecOs=|3tAB zO#9E8_5w5X07IXi$^RCT#|0B;Il^IHCrQ@kEVYGvXfU+%((}5%qDm`!vC%$nBR!(W z1$ia7Qv!WdegU>J>6kBL!|7#?hw~A+tPkwGSb4@5x23|!@fgoqt4MbKyz@`Q&vk8k ztnu#QcI!9V?|=MSFs$Nr-i40i+pEbUw}w9U;@A7UPZ2#{W0W!)T6Xs=Lh{XO{-dcy z6RC}5Bw&ahE1rOmj=h(Uk93W!#%sf-8rfh{FQ9|WuRF_G(FC4XN(*Ua$Noj4GX)xH z?&Moz*rgGPW|`;Be_|48zo}vw3+*LY=Kzn6yLiOQdHeV)hBOG_NB4PK<pZVy{Z$(p zch~#$gt>EM^>;Q>fg#cL+>m5-*N&Np|KGq{elRd!2w!lhJ#<R?c(^UBD7^<wm^qQB zE#@5kE%SZe-x&bZ?n$_;0(~61TjI9n7+&kp$`@<Ha;gMAksZu8vdG&^hPdIU_(^pA zBlb88`&}vL@FD-FXnv#2;r38hK)Z8XuwCt&J^1jlC}*6|NBaN4GFG(MaTTh^Of;M9 zoEKim$HiZ<;Xn4q3K%*ALn8@XX_O0lIJqCK>ABLm-HY^VVHtfs*m1RLH$(*2?(#;$ zU(Go{{XOiBOE8uj#y$d6@7o(nucIC_eo~uedOr>KHzA_Ox5qpkx)r<7PEAl2?$p0w zZ;Xb4sW4Cqu<~LLE8HnbJZ)_f^u<zu>q}bie|%YZ5L~^xq|wEkoMx~1T70SiiB8x- zl2tHAZJ}+Kk@3>=i3@jTr7{~I{$G3OQNZ;wP;BWg#WtRm7gkDZfNMfE;2@3yhb=!> zTX?j$<WU!`nDM!D^u+{N@;WS;3LD|t-AM7>Rvf%MPhZ>#FT3I8Yk((h4<4FvAjA<Z znwJbT+(y?y0BAy*AQ0_A4bSwyMFMhxlOc}y>!(R{`EEcO8i2khyJNr69LpM33?T~P zyU!;eM#5PYO@xgr1f@NIWvVZbx6`7UI<gUi<dYMPNycj)@THFCE_*i0ygt8KKJ!zq zSOB2X2HMGmW>S_8yS!7_Y(H7#s`8P&(FArY4ZH1u-3sr`Y+&|;r_Iuq$x8IVL}N8@ zK>|-lgX`|dxK$u@UTvC}<!|;i`kyV|)bUqu$l3*%V<;8}_d&3RI51f$y$)r4nV-e` zcewcGnH@vP%ELU7a_cvjkuK2eEQryZ$;$SklAQx@{HOXXKpS!s2KwDskdW4J6~{F^ zs*l{8`2F=s**41?OjdOO(X~VHIX^rc5AU#Q9i`z;kk>r4$wnOL+(oL_)W?OMipcTC zFL25)uH9T8Tu7eyOCBB3xKSm!IacMKMyG!X%b0EOV#XKfDrT1zt=s|?l+v~nh444{ zNObxIUzUCX#)kWLXO)_J2(vZ-#?AvtzA(I*KY1ufc9#M^y=(yncVe<K0qC^OlyCa@ zs}2wDjT`S=xU&Vk)ddXVaQVf|>g2HMaMRw)k)I3hX}tDKR(<G5p>pmxo{r_RU6g#O zIir-u>&j%s0dne-Q2D0A+SI_@u4u-;SnLLHwzGur#y~kzFTN~I!!CVa?G2R^-yB;_ z)}r}z7|>&!RpI!2ShWGV2%=B+dpSVnunm*dSD08!xSTr)7A@T!S>2xi5=V+&A`+-( zEVeI=z|%3ed-V9*UIrkla$rf2End&QEIkK0x7<a+%c?dIj>`$O3HG$|60#bux*$T> zcDWgptI{rOy*$z$T6OhifR?{%NWv~O27@m^0omyzf3R4_F`XAP*1%ckcG=EIS}>>i zkG?j|CuZ1&W>y6w@aw%@`Y!&7iVsIvP!JZZ)No}40=~FQz{NJvx*%F$`zK+Xb5_MB zobt2XIltX#28Mi;jf@?}{f_xxEx;2z>!-S_{mM92U8>0Lj;4!kiCoIZ!@Xd6Vc^~U zSrtm4*;RUWkFI~_AYf5kl3t<>FlGI;&n=aw!_sAkN${>qd0;Wz7sC4qlIWPTzW5Lq z9&D}e0`U58HNcd!7L5ok%&MMMF|5lrDZ6)Y<z_^l)))|N>gc4geHKr7I(*A_kA}@W z4Y(5Mp#zTXAK`K(L8XlU9gW%J9X(O}0EYdD?F*KMSG#J`Dl|2fr)?M*>&k%%1gGH| z4tS#5#gho>AmILvJ`xGEX$0qD=C8VaV|V+^luUTHnT>Fy;#rHeUd%j$bNRiy1&+oD zcxRi9{9ijwVb+%=OF*yquDVFDodaVdU5XwHJ9oMCVx|y=8Sf4oYkvj9TC$OEu-t3b ztcw0@wS{AQ<M@iSW5(kXXuqogm_3<ae7Z2(g5<ef)=Z#N!}zU&UXr12EQj~~q%TXI z3;h!{yMjI8Z^Nu%d6iCfqzBafx62?X0V0dtRUJOuhgF^Q5i=F&ANv2*bgcnRU0L^_ zrOc;On2wf?6+_iXL>}T3Ax5g!LLk5h6{(2Cs?hQl6blH6T836sXd(~<#7_x`pr#@q zqUKhsG#a4-gJ{Iq5kU&b6jChjS?9+3(|hkZ`|Q2;+H3C{68`bW=BU1-pRvVFVKN&^ zV0Q_*vVDpxld!02PQgU5!R*bZpO=<O`p4e#Cy{`W*bGmKK+ah#Zl$(+{6_8%Hj7MD zeQ)iN&iORFZZklA;?2c@AtZewp0vi3yyK6Dd@^4)8e{BadJN?Pa!fpmlkm%05;6=i zrzef|Lb}&*7>ppPpGo+iASQE_)ww0<h9IMp0@rUYNgtf=ZK8_Dp6`L$jv@o`L!0zp z#IrH%`H4U2U5!1{vLBlZCmlyjj7|u6@GnDup>@nOX*TGh;-H^yeiN{4<Thr}ss*5n zbgUbPbvOBi*X_eu&Kb}0p5Yj)en~(MfN<C|Dj7-G^~NBR7$o8QC#HyB;4I}8EeX#7 zbhR-<d%t!A6!gxf%&)*nt4CR1P4oJZDC5P*bg!mc!^i^FRcyK9({TQB!D)Rnn_JAv zKi0A0k(B$w2RoG2^}uYpF)CL$)^Hy$e~LKjFE2^Ysg`QVi1UM@&)LKi+da+e$b=j! zu_dj?V6G3$Ezy50dE?z(+lo>L$L9Hy3J3UTPWIz}fiwZLqxNitn%-w7+;%c`@LJwf zaUTxk5119e!)C09nRO#uM$QlJfX%6}*>+=ia@<Ii;SH25@0K>l6smPINsre@W{P<r zt5W^PZf)#|RDbjFP$#Bdg;hoP)y7?(QKKN_$-TyPw~m%WNO!@Dt~iqaSzsStG-D$t ztu@pGzTy5vWC4>8gOPJNO|OkrQ=2xu`POicw~12<HE1&og6b?{<0dXiaRuqZo@OEJ zF)@;7k&#QZ#S$NuJjF>k=Cac*3H8{~sAm|Fiwz|3zwscFNwGv(?dAc3GCnA(D#wo2 z@cv4ey6h~<4p)-Z5pA@!%%J2-R?kXz&YL1mpx4i}47I_-7Bwt`iLx_`^{XePwj?wI zUbqD>07@aov=F7KK>(10PRTgStj*!N1%f}bLL7X39=fw#)1;4}ptaxn>EeN;r^ZcJ z>|L9*N^%H`qP=l)N97R>m<c&XKm^XP_8q8)>bi*Q)b(MWQLlZ}-%czyy8c{JGP1Z* zWa4CvU0smHo6zCp8qpna`#Vs)8ru#dX|&(_=?0tDU!_022qkg%%7=mi_0yST&>dci zS5_Zto<FkQ2yXt19f%o-L(4<VOpZVH1h4Kg?r1(-XZUK(5>saE??0PZeq7eq34(lW zd`|b`8>DbBo^!@?KOBFY>L4hd{F;sDv!rqNq*ap<{=^<iQf!j_n3KnOV7-IIecL%X zcq|>rT!mwpZVIo9$Bquh9W(#PKmy;eC6@5I>KT-kH;CapyCrk5Zl2~{v}fSX4B+HL zWp#*$`skagt~#|wp4u~Jjdjst5g8Yil$OBzEU4`jHB{X<<4FMJS-NLbjR!odW@`&` z@DDjatzTkFrnr)as4byM9(rT#)Rt8OMETw(j18^@Z3^c*jc30yvaZx6e%*+j&Ucw9 zxd&)&E%noJy5GId21M$zq@;c@1JC8an&^L)^{EV5sXX?SPWXC3RL+&A<RO%uIRE%# zPIiZ6L1G2#JjD`gj{zRKz*Dy5${K8{t7xgF9CyHY`Q3|NEG0{4qwQ0*dqy>)Cht?R zsP+FfqWhPzh5kf!9=j%h#vXL*H%KlUpIlm6j2bZ$R|WK{V)Ips4yUwhm*z<4paoGi zV=-?mX3W^04m&LNP6-jyGFR~p<>C1wYgr_Vj}{*1G`dAjBMJYKnlp!OtNOl!q}a(u zxb>%UMEEuy45JTT@zY&YrW@*5PLytn^D#n0<p(1IP<7V=R6^sbY1eszlSSL7kpLN1 zJ%?3g9TL<O>&m~tQo`?claC2yll)of<ysTQa|XUrf3LE-6swQ2)m1vj@W>E5k$@v^ z!RoDm?8$C0?snF$>gJ%PkVg}yxO#$K{gg`Tm^1oT79vliUQIEDkklk_I=minp=AiY z(ZoVFWU+JBKx^t_rjgdeu<Yu>s&+@gY4uO+laXk3e;b>P^P&eK73F?9iI+vUQ5rKh zN7v!Jv#}?K?ipMbHOW=ycKnlF*1qDo>>8#VVJD_Ye@8g~>?KlX8!sP;Dbc%1lrSj< zCiz3Yv_VN^vMj_r;k%5t4x5QgnB}Nh3Rh)yJJQ;Vg|t)6wmeSb;uWT(8EvvS98`Rg z)6~yqptSZk>ZFza#3>3Um24@C(&L~#YzP#Il58}rz-F@t9<+gwz*&sm6?Yco8*xkf z192#Do7ExVCLU(t6?%s+A5&{q?MHfw_$G6tdH&(aUm}r=&4qtYlOZ;>*p?{X!%=7x za2A_>#hz1CNQWF|NEZ8daT4?ZK~P({Hnn*hXxLlZU(2j;oQuoNNF|g8tMwI}CTq4q zgwziQse&{Wo3JvpeziO0MHUd^``4FhzQeqwZ6?t!g;7xSqiOJ=1&eX!9DhE1=%$z= zP5cn<-{%>%50o;YX9-6YKCv2CAHu`esE309wNgNh)nnq@`&~h*_pvS%m$Wt@yv_?r zYMitsJl+UItcHg(;bF^lWqm&*`>ol-M%FY!7@^B%R#W``Ls^|7yT*+^Lvt}Q^Nj3T z+){JYS5}UzVsoO;sJWUH)}GkHHBj8`u4dxxC|LHze!4;g+&JQjb)cHGD}Rb~1EqzZ zQCg767HoU=(VFi#4SHmiC0YmOrd?GZ28KrYFFtU65j#sHmf2Es;)TaNExO&<CQ5t& z^Q8iH{Y+x-im%4av8`&i(exu5^8@MY8^w-?+{9fnx56X0obQ`Z=8W7_)_2N~HDR2$ z#J?N|eQ^EpUK7{0ru*scgFgCg9^IdZrx~j&_L?G4WPjdNl6o9_JD+Mv5M%K5Bh4x1 zulzNFf}+rAq>b)fy|Jt>t$XgcBa6|{pAxZhRSA*yK)5prNJhQ}GsYZ>L+Py8NSRyy zkz1eP`fRWoW4^FTFket?Ev6(Oo)+a{(G>%#j|4_Z0*Kih{BxUKxa6mD^u~xc(V<XE z3!(H@35t!%lW}!j0E@)1XbH|!A@$RFR}QZn;aJlCzV+wZkM{SBg~t=-7@{gRMajd6 z$bwZOs<)4M4V*J(hH4Oi2sA~P3nV48lo)6lrc57%4j}=Mfuy|HsBk`K(PH;<6OdGn zvt*-@Wfq|WMw?|F!=8m#px`w4F2zrhn&T`~wQdk@JKoX29;W)%uyC7TR{G|AES!sl zBhI!2AObOLeQJAKf{fx}-G5Ubx-P7mBl|O>#CAU37I-f^h1I2`2L=ryS5I?mSQ^gi zm@V9fK205AP6C6dFDm_XCU9aN-B+46)M<#tIE<*);HZkfBWY~Pgyt*~?T<IhjzHn0 z+Y)gKxYB8ZXH*^Z@&_09V$dv2e0`L=0|UIfRzx*HA6`d``23Md7XI<w-Uz!0fHEK( zsk}570^+P*)23w>BdNSiNHdKb<YnD%sEU5!tDWz{D1|UYIM4-0j&>4hH*^_u1fZEO zIPKO*(dOj8t*E1s_x!&bu=pMtzdPVPB!aQdmpuNzH%N}88-IeH)lN{XXUDfro7NO@ z8YXYfBu+up;_c`ZdBc6kg69+%(!Ia;8Tt#<8198Qn-MJsaq9f2tZxGDn#H<n?{7JV zpspeWQR_%AB8Zx9=O*i2G{TUCBK7WxXqeSY%`!)qoyBRAu+t;eq6q==vjJPX(XIWJ z)z>K>V7NpxOf~O<Xy7r_)BzVFz5&8;>dLv1IdPNQ$cCc#xHB*De1E}IR@{l#Lcpy= zlCTxL&5;L0N>Bu-xFeS6sGVikKGd)cnzW~CuBHbgF|=>Y=hPw4chQBZv&T(^Lzifs z9{Xmx{F)<}^F4U`HsmNX1)P%)&dKWdgnC8%!#EYB7$Jt`PbHR_0B3Q_(9giwJ?azj z+VNo2Oa(x)8z5mU;Bd!3ZL|DQh+q^UM{y*k>iFZ6*&V||4x8MaEj+mu9ZLfTk)T1U z*Xq7R-_y0AwvLt(#f&a)VhS3gaxj)XfKMKlw^WDGJ9v-Usn@h;z2sKe5R&ua2?@e% zjp&M7szbo++hnsp1Mf*^{f6JW>2mkJv;B+<7>K<Eh4~b$7txe;dhxWN<9Eo^TQdn$ z4gW~zKv6~}c^H{Zl=mEh5>=Z-qDdlcw#w@DvenL-=d^A!uE`RaGp4eba}Yi%Hwl1* z*^CC_0PC2eg*jmI@M%z1^z?@pVX&gvE_}t-^?*lVaw{Y}nm$tT7S;~jt*p*h>OXO_ z*QBy;Pm2=8?ZN3O=LXs$b5-HUKlC-XWW76Ro+LEY!nrRstG0%k6S0yy{~SV>mpXsk z<xOyP-8Z@Q#rjq1pR5QIj&dzs;m?*n?DC54^6c%cCsLn}Dezt@42#ea2y?=&s$Bpk z<!R14pnGL3FL<bTr)n{QUt&g70YI;~9fOB^omn?BXq+scZhB<Z;CupRMo=?fp%e1V ze5uav=%iZ&uF`uCybntUJ3+nZ6o}}kpN@h*W`R*NbUuBJmOTWXv_~9!p!rG3F?>fq zaeDoAJP|LVMzw6QY?3V7)<^%>;zoYQ;`s$riN7bk^=XrHlXIFOn6}zZ@fYrP#=e@3 zpDt`c8zjx!7+x0!Zn@8`p=f7XMgsuWd!Y9p;E~HnQ>uJ)&YGCwbVEBmuG3ye@M%oJ zkz0>oZ)h6q)mC7Dhl1uT>)i#%OZV>EE^~$~hzBmUA0T#$L}NBz<UOjVxQux9gbQWX zu-M6rIF(a}MPrO7zdtv{Ed!S;t{#QM)L+8kt@Lb9P^{+(PERJuMrpAY-%eR6D8LuO zS>4!yTeeZ~Y_+Qz-vU(rOc{{hciHg8#KRIoI-wDW{nviFX8G6_*&&Kb`ciJ-A!s|_ z795R|O6Zs{Hpydh0|opOyuV5xoB`C)DIuX_!t@1+L-<Z_^346ZttKBcj&pdD3KKH@ zJcb}E(+$DYG4`r^l`aKE)arL(HJ$kF(*OgRz<|^=E_!A9;Gh=ELbwreo}cbKzCs7Y z`w$LctVTsitK1Tnn(LRE5h-krE%c01%Q(&KD>w16{T~WWOrEzTMsX24)iWy7cR<Tc zr`1ofy?#OB;7%)_<)(z8{vtXIGSnb#Ew^q9{G~9-<t4@=te0H~xqx1rsIdAzZD&@h z1$DRo%M9x7aC0J!q6E-;%1?JxK30PjV%h7@<^!EEAjuTKj}F%G!|9-^EKak8zUgFK z{}_e8*C5M#SFsmS%sd8;7!;{|`h5Az*}@4MO_0rV6+lJ!7%j1)fes<VxV0Clf8I0X z;hRE`M&H6lRynBd!UNF7MU=$FylcGi>ft(sMdb>-NLPS`#IlfJ*Sa8_u<h+!o5ToN zjnAE5W&o6d!ALXa1Ei^A{`!$|iu=ghr3jAX7WR)=Pxp6MR`Ynn*<4@Req?nN3Mb(G zeiRAjF+fjBK4M#&0#0*3t+{~<QF3d96O`nU$Kr8dj`)L1y%2qO>g3MH>6cszLUzH4 zzf&p}mC+7!$H@N+qiIekrt3Z9wk9o~RN;IjG|8P_U_!Pl-a{hA`NcLVOY?6Hr@?py zUMxZ}+ExkXqDq|TxC4%mmJN*?zDRwt)9NW$NZL&|&>~8~@-aX3Bzi!|X;Xv9qD$g7 zD}6Dl5IY1S%h2`3cP|;wm5nLrCc$H2GPQ4J=j2W-LI*m8PIP>^iAl&D28*a{Rzi5x z;FXmdkm{tPp2lbfy0OGfpt(;SY4>Zg@M#X`zAsXj8ACxJoq=V2$Al+yAh;rN(l%SP z6@xXtcYZ0Sr*WgFISA3Z6l*wWqG+b*I`FAr*kKK@GyAE#X;E$!o;<;6vY<2+`EObl z=b+QMiGXFSr2!KwtEa-B{_-ccV5$9;#WsoVNz3(O9OX7eZ59_VYW632NIYfvz=YKF zw{~v72#5WG-a+&Hzw_x#(%n$XO)Q2x6#mwp7G3=yvJEiwLy$kY!ckU_qW=y#viaIn z;xu|g(GlGJV5}L}#a=iFEuTX_A}^*36RKe6vi}TlC$l<&>6i;~Wd|HYr);SX0;Q&L zeJ|r4|6i};EY_fnJlxV|NtfYc2oeLIb2aPXQ^s}lE7|krGsBcn#mO{->3rf43Z-1$ z8wQ`Y{&a`i?-MtMB!})Ie2QT4ZK(1h<VV5I#mF+%O-CHIA8#eUrK>0@Kr2olkt<z_ z-CQ&d=dq9Z)umn!RtpLwdM++5N2*F!Yz)^qKskQdjgRvY=AAEvPb>F4Mcut8!UN$0 zr3;WS7g`e1xo+4?atE%D{3WF6%&;4BErRZJp4|y!0^j0#Ly_;m7cj=Y!e2JF18BDZ zVX&hDQi2z79ARp7_v)KiBfkHZaKcvO2Bc-sMeuTw5RuYPC%~Z_Z4$2_KAm^mWn=MJ z<RJW}paU!7A7fiG+1s>x85n42|0-}|C8EnLrw~#=dlZQ*x-o?O2TTy1m&>m?<BmG| zv?+ahS$RvsLj><fuCMAZgt?j=lC)gXk17(0q8WM=Imv8YSk+h9@oZODXMhQQD?#ra zce7;Ktn}&B8TRZhd}VboO7o1I1`fSt$@``H<q|BUegdD;w`GLwo;LyjdCIQ#iYX{K zH;!|(lM6JXV9=>@q$+KriKS;*pSlDt6UIqk@77XZrP-hap94{YV5A$+z!Q~<Dg}$~ zOy9UHI0T>ZoB@<{p{g>cDO^7kmh}xivTCyMj7u&jWcsrnfVG&*Luh(zOo;e9>Q&Wy znBwn>7g3gAtX!6PJ=hPTqcDo3Nx{5>x2B)U>K*vb-%le>>+v_S6W(D<%_l?gd11C) zCDrT8q8@THgA<0Ta7S8$&%gnCM{Yi7FIoY&5k6i?O1YVzC8TN>(Q?qp<;A#S=vOR6 zZ0XAuqFRR`C92`XOYUpcG@?3;)H)t{tW&=nA3^E0g4;*{P9sHl&MLboT6@2?Yd$n= zCNEezEBzguhHV+a>S%TB1;sJ07MHQiI~WFN-4q>%u-Qqv+3ysMDsz-KpMjqYiivNy z`zoh_-@Sxu3UDVrBsAy^28uvmMWnVW!`0XBh=DuzQTrmI`Wuu@r?rno=eBz|R1Zc* zxaZUG<Um<^X}uwxSDUwqh#m2#^zKcEhTW+Upj#%<Eh(GV&=;>kLvNAL`Z5Hf1vPOF z!p-b~&?2-6OThat3$K9?xz!i=!k0y*3x*#W#HqjTv~qNY7qplz1BYA7qF!^|3OUWD zdr4q&{=s3j&G1XyvVz`XLvQI6l||JUPNhD()a7kImDv5~gh4#<z`KzOT}y!7f`VOJ zi{DSPzS6sTx#mKSi8LxMQI@H!A83{pbk0V`IjJcI13!Ygfv`)rN%WBMgG@1K?&1x9 zg`8{KX|*OA2ne*Ii)Hx*2?w6RompJ7KvSvd_j3gb$!blve78aY5LnWNoNV<f>r3cf zJN|=^b9qlf<zLCNSC_b9_Jz#|qq7!HXPX#Gp$U@+Go$SC{mfKq`#Prk`>`3q27K6| z6lq4bQhh<%=6cn%SYO%0Q5l{zFC`W7jHdO&yxfyTv2~~jZQH0-s?U)gqPR)8Vp3U2 zf81pI<^{toO~X57z95$Aq0npzcy(qZLJTnaYi`$mx0kei;^r~X?a)wa^ZaKK1Qg4J zBoG~EKDvj|XlQv?GQHnTzCSc$up3gi5ueLrw$NH9-HTp4Q8pU6+uS2AzK3YfV+54d zQF*TsG!pRv&~Tlif%$Yx()g&!y@m;|MnT0?r+7!7%okyf0%z`{XDfPf{W48oX&&D9 z1tp0l7vDQM`q|s!@JP`>ts!utmK%l)_M+y89-!uHuR$%!pCsFLCA69ZuF!kLwdNoJ zH!I#owCR|T(AV($gaiJ<S5>`<7op|NnU$jfvc|=Q5}g4RF0llQbyA8W_62^r8{KoS zNACW5ulTzZJI&6F!C|yFQPld4)OzBTbK_!a<E}8Zva)oq)Fp1?$iBPIHp4edp7USe zGTo4QXHixA8pzij8G~|R^yhQM(}Lw4dinm{8I`-~c)$Ypuws<(&$g_@RKz<>zpLw& z<&;*&McA+{VIrkD@5uUP7QExfrW0avSNQSTPw~hQbqonuTh><sV(UNI5q<CEKCd`` zI}_crU%4S$T)XU(Ps%%bf%PcV5otCp{iNa$v=AzXuRVWXaMG)&xq%Dc=i<-GLbfGE zyM~-Nax3>f?ouUC%u8+2;q8*8G#wgQbFU#dr=hUotz|FlgbhKiU5@+^pSV|e+r1n` zm)S!jPC~U!J-_tJIvRpp_HBXOoYwlIvirao9?;))X^d#4x8P%Vhyi^M;~7_fzJPDf zPv+HrUnMTTQ8W?$`$Y$SNc#Y~x1bW>TVc)mj}8&C@jlV#(!+p;lU`w)BbM+Sed8W9 z)yCR&9pt``+t>MHZF@Ro!R!w&$TlQ-sK+nG*PSmA<TPXk9bGZd$9;csVNB$U{V9)X z@i9RCl3553eGP+9-gYp3tpm5|N+j}>=J?m#n$L!w>`Cy7v+HrFKxb*Man3FD$`vg3 zj?-M~x{bdC`4QU%3aNF(A0GpFZpgY3G!pSVw<suPcXZY5;olq@j@n=SH8m@B>i*&3 wR?Du#4h=7Y3hv3a;$vd{`7>bV??aO&j_=j4?VA7J6f8r88+<;#uugpNf0uR|l>h($ literal 117789 zcmeFZi9giq`#)|;Dxp#-YrE4Tb%e5a;y5atlO+2oAz4Nt8OCU%m6M7PBa-Z8&oWa~ z%1(A631i>JHq6ZTzF)7|)cf}j{2rh8<8dD2ywA(^y6@|HUeD)sUH3h1|81xzyh36H z4-b#9{$DyLcz6VB;6KZjg8wJt;CpZIvE1!1Gj|>yAu;$5FHgkN4Lm%Xc=UA+pS;pK z)Q)qrx{2#Mv(Gx_zrRENuDjvVdSgeseum>3+j4~vU%S8;cA<_DI+-O^0ZWQ_b4DK9 zeGW;pDQ=Hawre%a%xZCNX}{c-WoKbmyuLitF0#?}Y|#d(n#cb*>xT+&e|Dp0$#2b# zheE3c%GO|bNfxtTCw(2F_aE61d2nG|ZO(0^=(jtucflIr|2Fk6>_C6w@%I;D{=}!s z{9zfH`NO6de)OmRd+7iC#Pk1q7mTRzGJ*ep*}?OFx&MDI07Ai0fd9)59-jYGMve<s zMfz)IZr@!p-`BqSPlCFYS9G{{g6)Srr@g0L4?dup{imtbOER}#f8Z%z=v3vkkCV<* zPLH388f3=IRm!WIHdb5<HO?eEXGJ@>IJk}{mQ`KLPI!?buYTfz@X@`$D=Oaoe1R%U zbv9v)lst@ekHZ|DN-V2>(b4qMd$(|*dG+!JVtl@letTl8&)#8G`P}M=(S@v1!*=Pu zhTQ7gYDT5s5@$;%&we=cda}ZIv_3`MR?Xgk*57P8CW^UD(WlYoG8y&fI_j@Gbl=q1 z(i9MsyKgv1$a0uy>IiR{`8NGoi(a5V9R5s@;oDGieMWf*KNhX;r849*MmjLm+1R=- ztLWlHdU7P?Lq$zaXGE5ICT6gZ@~P8?Ak)9_MnCM$%L?#6!@{N|l;ZX2D;e6_;xz&u zyjI}j;i?*;qnTZ=<DEXbUR?7e+|2T_1u^$|5`IRM`9!s&w(MH?IBq>3^-#l@M4OQ_ zSt;l^wd?tVfG)wAa{)OR!a_GjX{E+2{*<uRZ09Lq%zV1>X20`o;ydtBHb%kD3?1yb zQWCB1nRHmed$5!}4|t1SZSVg!?d=uN&6n@LH%5pIKCFj$aYvP({-qS?bw~(2wKrMf z$}GPL4L838T`g-#n_>v}#a|T9(}y*LOWI^Vt;8_C1UwVOJ2cRTlO?7x^VP!hIe1l# z`l;lp`P?EE@t)qiR`A}G^L*GdcHvvg`#Lu+<$5oL7iX?p-i$Nk2eB?6k(#Yia~;H= z4`ePZ*bVU)ZaqJ>f1}@6>HU(n=>sAzqo<X!XdV5*;iT+~v+s<Qf;@1~50R#hnns0( zhkw`+BYw)yhw@HfcX1zc-QYuGId9Rwj!rd%GF|tmW%l;#1LO7n+?zbmNi$`xrvnWR z?b=H6!&Aa8Y7cJf8yR4)G>qj4N(AQ6#*?0<wtUti#$SBtbofh6kdvjay}kYA!vd7z z6Unj3$4)I_Y(u>XovOc#EYOqM^)S)WBU|xZMa9{PlU^4a;tJdszf2>5m-bvDAV)fU z;i5-FP2;cv=>rjOVQ1gj=zZU4df}dJ4e*}5MvM`|&V7CQGM~j8ib;|8?Rw!%*_?0< zY}~GahBGmt74Rbf6Y#(=gL|Bpwyij<n@V@V|DG6s=E{8E3RzKtq%*1h=6SG%zWp(> zogt6AXp@{^Yw+^ocEviB{Oes_D;XsO+!c8%8h_p!BqCuML<|!+dt>Cb#SjjriDD$J zlnOn4y2o;SBm#3A4d}E-IYanw!)bpiDk`cu0Myk{$GiYbaDE$GD@fV?@Mz|{_Zgw% zYEM^!mxMJ;Yf&B~(c6O&SaM#X9;25W(0Lk|mra*w$#-fPYxI$0<UPBXH)wpnMAYx? zc1N%ihqX1qdX#TD-d;<80(aSu>NhFd7m_kTD86UpYxr~+H!y0vINpRQDL_%loU*&0 zekJGPmA0VIS~Cp|C2hnk?74Y4-?fDRTK;}z7KIak%`K>v>}UEId?--;HC-GBn{%Iv zz%Af56G#&|^ma+6@oE<x1jMxBdt{KuE6A8_&Qcx92j^_(2cD(A`S>7JMMbt5o9gtX zqT;wVzT|O}-l&4vYP!y1t~il;7{5!r((?~M6;h{KI{ZGn&<Yr*(TCb4VN6DZJ$PyN z_^FZJNf!s^8*)1`u`dT3KR!qc{t^3CO8jMbxLGkj_RRKva?;J+r6SAOam23S!yfjw zm_3=aGymhpGxqiu%{+<e&j1(YF$1jvc?iCRdPs<j`Ee7hy1+h`nuN3P`@H>vy?y9g ze@)t#3bT{4LkMM|?%cM7l6Wgx*ygyBo~-9l$Z@9{Nb|WnikOK47i^_Hvozn|uY->o zS(MnZe{y`(#mE^OSY8ikXf-cw7SJaB_##(?r3l5<WuG}s>?IpiEr-0}T0Rqca%$Ai zobw#wd_uNA9b5W>XpB4Grm_R?d|sl}Is)Q89xVCPa{$T-%9X)FwUXsr(VX_zl&+Nv z&AsRFbS)Cki=)wl$yBdVB#L0Ut4Eml$LMS0w<;J=FL(ddG@2sON|o$?P?c<%3XrIa zAn|o|hns^N*Xr+T#8kgLFH9Us*{7J9H&k_Xv@mWL$tQu^%g}WSxBTg<R;4?Hl|?Xb za>uL&gyY&e`>rX0&3bDqIM)t*6Pq->5y*irWVcNX>xVqd5icJ)c*W5Z-)Xag9bz<N z#Vxh*{1$&o2IdoM4-{snU3@;cRb03B!(MYm#mpyw$&O0IdMTXVsJ`_lCTnp!Z*Apq zQ>+9sna&l!WJM&dXAp<jka>%Co72p-PHWUnsU!#HpHd!aTNoAagtmbIo_Ir^(A|Cg zFvr(Me`phe$kn0gcm3XlBd`N@ZIVVtg*Z%vBB^-^rOC&BYOHa(SaKkF+GTFLcv^UP zi-1&(L(X%}LK{xtfvzTOr9Qah{P*{I=;RbtM$N<ko!IxY$<s?IruWu5Z;YafOg)f) zeE+)DsK;<bc(|26FYStsDE;q6HigPEfL6QM<{nf5z|>q#>k2;A=R;V<l5=J#x@Br? zY<=rMkzhnuCzMIIUVKyDR8cX+x14<QaYJzDevIxxHmeDdKgiT1qzKEMkngfG6oE)i zl4+Uh7y$f-&}8&0fBF*y8yAJh8-d@nrT{K1M2+Zqsg8Ncb`5_u$>H$%z!4TwHA1@$ zWQ)A%clL25V5}(l%P#YeBkq?WET(Vz(#wExl$l`K!KFaiySqPz@yJ0!FQ53&Y~D;` z?XcFMgtDR{uLjo1uPY;fY`cATaM5pzF_D-PqxJZ1uc{XChe)zBhV(p%H0s1H4S>TT z$D69<V9%AOeU`O|6mgE~+XW5dLUZ#D<zMb98zU?RaZI7S@9&TArd_^<06A*Z>E+0% zBs2BcA7ZO(Y)1d>9t-u6)XV7@D)>?fsFM(Ha_Y~py+;GR7bENrxn=BTWJYb^)6Iy) zP-gscUXVs#F_Vt<5uN8x-(?}9BHf%(1f2WqjK}=E<!5{Q9WjJH`9#rC(qRrD#6a;@ z))??^A~a4#RTn9ku;*rC2Np&g5SS^{u>yzqgK!{*)Uov;sUrZAab74~t(MVlgFvrN zH-}9ex9ZA*ny_i9Sh@MfQMVxA0Ou{#w<WSK%VvhfSOm<LUb?EFsciElqc7ypy1TLD zvtgOx;mgT=Zaw`m7wC7~EIF$MtYZ|u@Fq0-hK*+k;#Q63$*PEjk*J@dl@I9B;9-Pb zj=JJpw@QSbDkiNhv#&ER23fFR@?U|+`qEdHax$ri%umgVPqpp56bkKz%Jdheyui=c zAks3j^(!Z~v?*SiCp~`E7db{|S?@yQZmT-PwI4zobeqCbmJyH*vSs7iNei@6lLH}p znDF^-@+8h?^)Ei~yXH^7pm3bp)*z_Lh<oB{Z_h{Fn6^#YD*L>h+g}_Hz+$_stvKO? zL9LSET9{}TmQr1lt7Zg#@j!>iK+u8v9+y)zYrLIC5`%VIHXsY_==}cl!_+&&U$OP! zq&wvwhdZzUGvO~HBvs95$Z=xId_h9`pd3-Ly`o|XMX@=A`dW*PWdRD1$!jb@GhBzz zz@#WXOSN<U1_DkZKkAp<EwLb_sLo3$X_ISfY`R-sbji;s-*??9?`!wp?lx%qVT!Ze zlcF=a2RCz807+&GHQ(*5_ht2N%l(iOU8|#KPWH`DBP@feuy{?;)x9x3{4}ZAga*N! zuZrtHQUQx%I+S&`1OjQx!5xcChNWnw#VXC_Kl}hIsw7%LhjrBGAcuRHeFMe)yxRn+ zEkJ2&R8v77#1a1bWBY5~LZ<@$mwe?Q@C4P3z6rjw2>}{QZ|9eJ@wV&b5d?iAM4S_y zS{QHJtU6$5NEZm!>xvuzm*{b#8GXaw`tNArz}^O3(}OlhhE~le9hX4dj+RSsx3K!4 zdb>UMK6lJ#@~^(i$N$W4Ps1oG9`n^!*z@@AUK>kUKchuB0^*Wc-l*Gj=nP`CYzCLM zqtgqm!<Yp(Lh#hNFLShptx;a-8;56OguYf(tdT~<>X!nEsEo9`PxmA2^kaVgsh)X1 zlx$VF<b|d#KzZ5!cBSk%0`vN#ilrm+Iu|&2RG0=^&QVs+$#NN5pKuSwqbNQ`wKJDX zADJmxjnT%pJm{@8M9h5_CL6ED={9?m+;RJZvt39WX5xm=&5H}3D#F#Z=VN9Hk|uEl ziz0*6>Y75lv<8t>&6+o^4c%6w7avE4o5dnc-|J4xSHG|1Pys?3cXD3OKRp0##_Opn zfJ~%kv(<?8D8+0gSusdSwz&{j`6=`$CLWkP3ryZM)sa;&+erMW-AZ&uDMJ}jhFtAk zr-`CN9PcpmjnLySqeS}tx%WZkn_hm4bk7)I6o^#ftqv)xf>)ZpTs0)Z8kuMhH~A65 z9Mlm>1HLv~l+8pO=CC$lzw(q-f1DLz*TUWCG!I3^-L3xCr|s-K`oDV|<oXTJ83JFs z%oZE8kdGKjA!IZ?-F`Z1)W;BUeJ996nCC4zFoy{D{X0xjfNujL_0%jfw?=98olsE} ztGSCkHc&E;;S?a0C4fdUvd_FK>>=N;LhL5Vw9%54=7^(6gnr6%c@f~;w2l1>$x4C+ zWtM}0JF&HCd&KBOmLh`YZZiZxkb;vWsU*?X-v0G%a_|rqV?Z1&;G`O}p7G;d0tzbU z5sd1q_`NeHOV_1c20~v%&e<kbUtzN`2w4O2EG!E2;|H(3jI|ruT_p`YaBAhq6^kkD z&K*d=a5oB&HE0$i<+eB=jSof<qRzww&#Z3TUN30(Wsb+xgFACmU1))XEJxf&oA5fz zbI8I_1{&Sf)~1kHHAC}4j6!;YYk)b0iLv>Y7x(e?XPfjm@2o-NG{ofjoRq!!#OIp6 zXdIB=f*3f_f>t^>7LA<j^C?9@%BrD|w*!;32BO;3_(<So%aLm^NVcR9508de>Nl<i z0~Eaqzf}hq#Ye<CJ=KC4VA3<MY5H(fV-I_M$g9B{XnY_s8>Ps;`QFbbM?O9a8nhgT zar(*_tTwa!wbvpl`|fX$!qqQ7hs278i*K0gaA4&Vdz3@_LztX?$)k>t4#reBj&%T5 z4xT#t<(&;za}HX<{^uE<Xq&Z{2l{hh`m&A9!_hVcWSz{}3@S}R>M^9&wB83*kqY2| z(TV~WxhHp<25~yzaTls$><9&s#mY0gH;VfsBa7=AqbZ+fd@T^@a}y}`I3G2ua!yH5 z6;nCfyuZl=1Gv7_HD0)M7brr4xVqQZf|QNxTD}dUtE}4^71r>>hryM|Ob6Jmt#MHA zIFSG585|ac6@i9cCjD0Q^4`bwvTCUF5!itYjh8I`?XmFXd>#<;KaF0CH~sKoO>Q{A zm&x?k?0%y^lY%WA)Clr{^|G=><67GHV(M5a=P@WnuRL519!TGtU%c*L*j*bC6Y_U- zN^;!Itm?28eAIh!4~eyx+{)AQMk;~+o(3}1k@He>!#cJWz=EOw7JVqgv(_^WM#ey= zv(3wPSbKDm^Hye7%9QwP+7>#VbJv?3>s=#GgnJI`j6QVt%TJ|@!f!U>GKrs-$~1#5 zD<|W!=DU^VI%I!c;&FazdE-6jj>ZFpZ@xZ;L9rRL2w6_gT}&H#6x;GvFxuor6n*Mb z+DzjQA6cY?qwV3N+9E40m5+f1Gp>;z8|Lp9%}-4px&Ug(mUzVMx7E19xnJ><q51T$ z>>FNhU~KkU?9c0V$>!IHL=_3?X`rmba>jZIuZFq%Lg`52e=@q_1+&ndLYES$wgj`k zWdk^_vPPzmZOu3oKnQJop7BGF&CVJ4Z+ADxkeodx<DC9fHd)aRi%o$yA9lwEht&rv zaN!Y7Q+7#G5-!zRnj#?2o+AcC_>SiJsBM16x(mSZ7Lypob+z%jewA0NEZqNb4NC3= zNlA)1Rky0h=ZxylsuXHpB4_-Dd;v8N7`_?+RFuQ8EI6$epltLJ+FbPe1=9N-#Hag` zQ||4xdMYx*84V!QpSgUrN3!Gvj6}FaEJ)eWIj?~Dh){lu2`1!&DgZic7p!w7x!0ob zfh&yv-yr@!_eTj^v4|2aq7Labzm)LSzPz}B`C1_@KTPG}PSl<@XpQhcNoHeUce%y& zHY*;;m`ELRVM3#GJv%8_^=9l!a{85*bN}{LMgxF=fS<kl<?!$Vbx}j48Dq3Ow#UWR z28Acmw}2{+@M};m0!4AEd{z}7%?GXJ`W`ctO+5WK-kE!<qZK2Yo1(PvLg+0Rk5HEQ zUNj)8{MrKO%G5YG+bGtJ>x=S1>{#Saw8e?;O%Ww9YZLH+^a3Z9$wob~Z;;e3Pns-J z0*S~qjbO6bUG*68<0%`5oiT^<dQ8=qKLOnzrUj0o#Tx{TIC%@h`>rm#%Q*1`Spo$N z80?cysnyIE{<>dpP{&v=>vt)M)cof77MAPDeolRR3<cFVDguX1F}_x_XU&wUuOz{q z!9>+}R1VZzT1YuU^Vos{P~0AlJ}E8B$X0yVLViGP@`5UznbY6+N2hiEg{Av;$Q&mS z?q|zwS!3WC?`g6DiX3Dj0+MGdz#?}A0|ysIe`ynP`aks68hGWxVRQ5f7$Y+qGg`B0 zZ~97v5)j|QlG8(na5`GLI*&sez^#=W7GPpLI>z7c>s14-^M_s~rop^<colG_X7X;E zjlLKCaQnDY0W?+)C(m6^FF;I|1Ch!FNuvc`3;Go9WQ>04Pe1w>qvHpGAsW3BuCe7l zSy-=3{6(<zVp_j;Qn|UG56rC&rYd(f{rzxM8m^yP7W4g~wL8O3A};hiIet)NxJ+-s z;wW_8{xLbuZlLH;d-j><@0E%$6}ReIfy}~obk58HU#<qeUR1gWk(aL|3v@qhjTCXE z>lz7y(6pr>!t<N76sM>AMR>}Smgb8Cu_ZuW|7n$ABHSGhLzAGVnm%#kUwgj%%8wY@ z(+~asvgVxtexSk>f0Qm74E(r8g;f!dEWySRs^I2e%8jLzjauf4t+oHPy|ztDSOWGw zT$p}9Y$1_@V_2~+lAg<Y#QD~Us~y&?nu=}G$W5Cte!Bd8=_RA(aKqj<W;>k2ZDb9( zatD+aJ0**6pR~9V29x`H2Ia3ly9OU0Mmpz}%t*_r#~2Ng&-?jy&ui>_67KKX=b30y zs%ab|a)k40cvg@a>oTM%4);p#pZgx+i{UCaSxH9OX)7<NHu70Ghg4Z|4P^G^j-2zJ zW4A-7V_b79=)C8R0<=WSAM=@rY*)=R)~}@8;NXCT>M?(r?Bf@t+bJ|qP`S!42qi?X z2icuFnGq5|ZogaiPLcCF%z|b1+%9peVH7wyq`ogeWvjtnvHu8A&=F9ug?2UY6*WbW zvKMTTk?2p);E(iDmi7H+%d&oQZ@#a->W0M_x;Ek2C8S~(C;A4EDmZLxX`6W+aWz^F z?0#`;4rzqsT-c*ss*(XapWE*$mUyTs?5?~qQ;j4x5=_|nB;F!wt_;nUDykKL6;_Kh zO`{0_Q-+gXY$c~D`4!q{#XW<`ZG0X2wkmg<*upbTL?MV;Rs|_kFU>`BI~k0@9GsS7 zb6~8Qg)b+^NmcKH^vw0tj%p|&arf;Jem6Y%rHgfV0t}1ev<Yi;%G*8?zr*a#+mmZs z`nC0Hv=&$5u-Hw2LioMQw!IRGPDaIp9-)B}u7l&dP;K@|`h7t*YVJZzcy>9)0gn4v z9S&<F^pCQG(RVMos~<v^<=m62Cu{jc<Y&35&qvyud^8r4<ir$d@}VuD#%CUgvJ(b> zq|;HlWp=fZ2uznl7D;>a?a!z#IB?(%dz;hw$_t0#K@wW(4tGR<$o46~b2aNMq){bT zn2TKg`cTptADJKsv-xRC#_#za`a6cWf(xf(055m#TF50#Nfh+)dvh66&6Npkw#5v5 zi>M8XRM$Ya93MVLpJTlbP^Ui}k!)UBo~fsC5*&v)=UnO>@oE?urU$W{#>MY)b+xal zZ#!w%<d(t3S?W?cqinb;V%Wz*+&$sw$~-)2Z|W&v{K?TYc20|7FP?C+tM|K|fY_7L z0Y{Yr{fi0{cs}$=W<CNX8_TtoiIU{Gr2n<H?1bUBFTA6-z$DJJ_AYra5#qr;qJsy} ziO#Q>C>hrFav9b(f>R~&bJ}>Mc%kG8Q0@WK)mZIOK(<h(Zw7maT7ZyzS)erT*3gAM zbjY^rCsRW_W}k9a0eNSSmAFV{^4&By0Z`yOZ^7cL6PyIG*02Cz3IW73n$GkWpxM3} z${)E)ooN%c#oq(I{kO=bkuGKG9Wq?w;suZ-kQdk>WFkZBjJCp)^M~@rJ3?uqh3yY- zz&uuFWLB9yU4D6SZQEjXILqUR+w(0xdGCce<j{+fgKQTXahTQU+<~w!#;DEXeC4|! zo5^iwfNVuI994WrSXAIs$8X8VSTrhMzKcc*nSUzQWa@IIp$f|t)X#j>i>Yza!!5WO zQu!BTsULS^HejL%B63_sN;5z8k%FLDd24ZjB~s@0TpoI2p;j}=rR!{X5`f&y_bJ6m zYV|o5Lsp!;6)iJAbj@SYc_S!vEkdOWsuytu*MyJdSxV4DwdU2?GDGJvnr@8j=p7JN zimkB?@8piwb)Dh3A7=Q=(<+Tx-UAgE(a=*nMilBrDEBxHVzwXPSa-r-b65{-o$(pn z_Mf{QZ*>&?CZ@@Cc!(f%3LUfUMnra?6dP6#lJWru9_$2904J+z^{>5$!H{uW2MNzK zJXzcx?#5Lk!?8_hny7W&PD$J+NJL<AbzOsGS&-|%JTm776vcQYxs>**=kHPn^?wl1 zXVo3QLgKs~0{#(Z*)_=~8M6_Dz@2-D?W0MRGef_WDPU^z2(-wZ_dBNFzwo+}v`>+@ z-O#5;xXYB~Y~(Tut}LwdV_5DI8xN#Io{m4Fwl`gxJZHi2HJb#Dz#P`6uxH$0UGy#( zm@n35-B-F7Jh-R);Or>jo^E+#5G@Bn@+_s^@1hk^?bkMhkLoBuxpq7By5wl3M#IIM zf<lnovU0M<$(7r~xla$ji;_#_%L6)*6+S(sSYvFeRwKqqE>4)scyYa3+euBo=g%!Q zS_jp^F*Ri~^|XF(^Ib0Xg){lrt|{=?5LKH3jn2tIP**5$N-mC3b8w{Xys`a?m+LFq z38VGU>Q2l>caP!X<Fi?dVgrezOB5dNAOc~agC9~OnS;c^0cUFU@zT)S8r|z_V3nge z$Xj#_<Y_lJJXU|CQ9nMaZU%#%P*_$*uG=nr#+t+32##Er(X;nR;+hdbYmp9~rk@zr zu8sjqU`-leRw8Oi`65CV2$W;^(scjvc%6W__K|7C@=<~UGBiV$y&T>RvJol1GgKRi zCc4RR=hQT)faE#O;w1WI(1LnaKC9Fk?MD*5=W%@L&xaCQr~$8U_q2@PL+R*ikhJ^Y zQtSw<epm#@@|fSByhYyvQ-d)J!bakVQ`3D-{?6Vc#yz2hBen9Tr8e#mocE#!6w=DQ z`Ys4!cfjTbxopB-pAb1gx<+V6E2`wAdj0@N#YdCRuM7)4SIl)!2Bi)kj|60mSrNX~ zsoDJP=pcxl-iQMTV9Pn<T}pA%vJ?*_#p^=j;|9f*Z&Y(g2K2SmlR7ryE7>aKy9GOw z(R%gg8%l_6za6}|G9i~h<p%HkePT70hK}$W%=Yka7diQcRxg@YF1O^9Rnbm4SJZF5 z1gtWUL(^>H(2DcdjKeiNV|ZoDtprIvbdvrc+&JPV`2lO}TqTv*?8XwGJMRMhh?isL zH*;RhPW0-7F-EN(FQ?wV_z=Ahkb0)Ex%ZJVaSkd#X|B1hmLw%(k>fhF9%&-%Y(8O% zGJwI+|Fy>5XWGu|?FHBHu9P9$&W{h@SFlO!ryeKc>!aIuvb&a|1|JU(ht-RCFd+RB z+R)(-_2$M}z+X6JJ}{{RZQK5s>cacP)Va5`CM?tPN$k)ha46S2UTzaOKKNOF=HUas zCQjzQ{PB696!GZBAEw3be3H9QOolVzL1rU&rfg~AJzHek?Et5nwQE;9ToBB+fh3Gp z9Z+<c2=#lRy#9tuoapRdUNAa8<|(aYjk}V?vWqZ;J$wPj-RVrf9+?9HH-f0)GKLoC zTHXGd@x$Lf27Qz(=;+^Ba$H+QtqZEphj2Hp;So|FwXagg8!J=)LnSR@L5oOojhXl1 z#FCvs1Tb`&)+v8>Ur595$#Sy2+`QfeL&qJ>J2{jAH4;z;DnLtN2Z3H5+S^GJ1r`?_ zwL^eb?}`zZa?zk@Ur#`w`FwZz5{A=N76*U;g3!Eia`yFBo{dF0qwL+gV}ldzom>?h zXN4x(TCS9<^A>t-_{N6;fGjT;KTarlArQXkxKJ}jHke$Sk!?#Fh9bFvro|@?ieYz8 z{&q3AO60m18|(s<OmGw?v*Ci~?AucH^T^IE&AOX0d!3gpZkeOi=uLYq*e#EhUz7OY z(tOZ_E*?3+<vNJfBuIrXI5{z0`SrU=ZK=3@aTq5#(DO%tqeROCO(>@i&qeouMRKtV zM}|%TZF8Y8!ogg+0@?a>@9H9OcjccWKdBJdJC_KRUzqp8JqPjO>H^s;=O9@l)OnXU zUH||;I{Ey}j+n@z{$0D{jvX_-$<ILu+K3++OXu`Hr&elGCc#pF9rl81E0mX(=U^^_ zk11xwNcqN$!cRwp6AFQcb=l)J;(wi3yLRmvem-pR|D4JrPTk?Gmi4-UWi)jKp6fsv zNF=y)`TDj|_EJYK{s86MR<=}dr@I3<24Ar2emJbv?T94MVI9#6M<t56h!~t?YuZip znGuy|(M~9<wMawQ)29HOx^XoZtRWUg-G?aN4s)?3G)?owKXq=-+|yZ51M5-lWb1h$ zr#Xhnprbn8-Z26Y!DWjjY6LN+sqr#Qux1{pR24W`2x}}8=VbxCJ+ZryL3vpF{^raW zSIo>8PS7|>!!~uauGZw3#5Jy3AGwX(PyN<nHyAsJnrisL7^i_flH_9GRR^}rAo)Uj zI$lHutw_sq#>J;hjiE9eMy+E8lScVFC67o&)U4bbUUS27=l0JL&%PY@d2`#PC1iVf zz>UvY;T{LFiu5C2+_u|zPp0oLzrU2*L}m09`#nOuUqwtC9vO+cEm5w##6meMXB+<) zwdc0~1-;(h5^C_*Cc90K{_P)<ceIJJo&5MMaE}v#HkUSKvEyI0bEz@3`N0yrtiMPN zud22Ye~kEQoWViM?A~T)HUDipchdeDc~%}u(ae*KDY%L}BS_|vj^XzJzaZx{LX(x( z%k}%@c!ZB!r@tGsj3f-rAde*Q*4({CRF<%6j$l|xLR$?#E@c8!S5(jk5F6;zxJ&ju z&wQdSy~m=1)m;AKu~I!0+Z3Eie=lEqko~arfWP0_kIAR>oSb<=o-e_vbY0tCB)XKf z8rFOHDf+yW;+5$KuZ)f!a)!1X*X7>8kKLxatwh%io;2c(DeW07&P!5S2olx~dGwsP z(0twIblv>F%h(%WtxwBITcKjM?7GGEV$!RDtj)Y0{RYV?ve8-zejZCW7NP5k<VjwC zuBy%w_$wsj#BiLbi9$ba>=5c(j^B8xX8hD+lbw=;^Nd3Y!pmchVOl*^N=+X(=vDJV z$0A#C<e?OwOuHTZa^?49F1#VnpHq6iM%!4`&3{|Pb_pvWxDz}@`$W?<v!*Vg_RQ^8 z+5DcaslJ508yF6i+NOZ&&R4tWFXh3WAB7GL(*JcCJrc|VMvYimXjQ|}-%kkqU@St8 zf{vvm3Q%shP~Vq5OC@z7TR+F}iaJEv{FM{vFx42-r%%eVyFSIPE14Vyv3X9J)AK6y z(H}iB!{ecXF2NOeiuvP1Uh)U;GY!CGKVtHEC1xc_fW6W%sxT#NRneAD!SdEhRBseI zS-CcO&qtqOc-a1d%R^8oP$Q(6KQX4U`>FCa|GiOJ69F%K#Bz1ISr71HKl4*B>I93Y zRbjGtL1gN~d}NbsfFn2j;z(KwZ_z<9tr?5MX}HiY0ZVG)RZ=s~P!!!IVwbT)t<Yea zM_ON7l85!Am@4z=pNL&mH2SX78d%)ViXQvr)E0hfS;>dF!F7Udj0a4UDVyn2Z>DPR zvY*20cWUDsPG<MaoLV}rn*(f~Fb^3FFM94({e=e?!H-6Vxno{{-(@l4FEAh8`QSez zxfiKiph@&~JwiFa-cs~ZZG|n=2=eE$;|qQkF#mA<wQ0$rlo4;K|B=fAHFCWjvZ+$w z`m>?r8tt%-tcZv!jfK>gqMUuVfPK5}Y6?(zlzu1uH3IT>Ta8hON!MRDevXIwer5Y) z@ttQrnmx=wrw6NQ^0FhHz*8fXx#?DMlF3$;W}a>O`iVQjBD2As+j-h|e+Ap*&Y5jI zFo>t5Td8&k#^v-VAIVM}4@&UR4#xJCil;6MYXi2HQw`q>9K-tX`Hw7JWJpz;utZy- zs&V}Gaw#TYsAJ`Xm%$%-SkF;TEUcucRJ>15cNHQz^8dGOdtz&;%tFcbBA-8g-KQBp z_TniiVxK~&`lTi{1cXJ0VpW7qtJ8uxj~6|{#E>LvlUx7143~HN4~kwvau{V~ZYTFH zkrmK}z=e6|mf8&bU^T*q*e|;-bO%?Y21j!oa*Vo^n*8E|!If{VoBYDieAc9X)3T{6 z=v$^evk_rwO6FmhA_;soYKS*)^H5UZ7ho9fQ_oVvShgj<A1jwAj^lP^(Z0t{#SZIp zKAJZ3fw(w+CXR?UPLFsmVIjwhUz+u!mXEd;U3Dfk@RT%hhM<bjcMGGzp(;68W+_`N zj!mx{N!O!fceyo0*k&Ruk+mmxFhpTy<P8U6to`{-1FnyDJd`yjhM>`~b%FY_Ta2^B z+aO;y)?PoKg|(yq$)SiJuQfWCkgdBAVw_pw<v+4+1pX4D2@B32y5yTseQ{hjRVz(+ zDT=rSc_M^(m7cUQ-C8>JH6)3XE*K-6%V_j|7Jk}=Jy<_Nme@CUn-Ps4eCH+z{)zzl zcLAP5X@F$LaJziu;8Hfs!zP3&rbZ@Cv(j*(X19oanPToMS?*(PS)`^~0x+5CxO><^ zM^v_^O4hkc#;UUc!c6Ifj*W-azpUS+fAr|FOB$I@J~QgVHSO%Dn4;hyOuo61@Z_yN zqRj;(y|YQ)!nN)k7cM6i(;l12egu(@dwke85y8-a%;4PP)((wemijGPo>7GcUUh4b zf?Ml<Jo%ItmDVyO2kRsBee8Ez52(}#`CC6pvMA}poL+~oM^=e{<U`JgFA^PD$_fp~ zsvLjK-N~?bX;$KgP~>Q>Q0!B=QNRhz3DQ-1-N~g~@{;BIZ<`v}wy)r?PLu<3kg0UA zaPTb35~+on3qtIqEvg4us6IGhd)Gow3j;HoT(CiW&+zbacJi|Y<{=;XrVVNLhURIR zKq%63sTq#WABF<XOK}*nN0OZW2j$8c3%@v|j0l~c_I;h>W%NI)2n$HZTa%aO&}`*U zb4i;SEr+JMe&cwrPNl}?Ps+YD*Sqwxr69g*V`nrxAPVt3WKEE9{FE#{syqmx(V-YG zKDg3j>tU6Z-a~E_J;X$fK^F#4MG&Ww*e*(z15DwE0Y$+%A+YDAwX(P|LeuofAwCp0 z5ZfXw2pN@0xk~2u?UEA)CV-v}P-iDHsAF;*<a{O;)22Xeht2q1jMRw&aoQ9ADre@M z?EesGyPy=QuhcWpp1xe_@3$19ZDf1d{-&Vu?T{e;P25HNh*&GXRIoR37&m6k%wM0I zYQRa%v?e^78&BG5;fn!DQfFaWyTOz5&Uin5TD;k-y(^MW-T(S8jBk9z^t6zZY}=#( zGkakn!b)V^iQdF@g1HM34+D7epXa6ktWf0!IMAyDoUj5B6%j;K^pkRO6Noup)f==S z0WQiB!qmdK@A!F=TiGh@FiNONePDK`WGts3D5{601t~3Ge&GcZD;l$H*+o7eQq01L z?O|$5<((1>f`#Xma_5AVz-<^alP_*lPXm5i!XeZtgiw=m9<;+6BqYhlqxvbxT%$#+ zLjI*1>({E96<@LQ2=v&g==)mAxo0R2?AI}?kJL)pJmp)Rc1(z%L@>{rZ<@E*BqcM_ zBruom+*6|=6!rU(n(ak)b}h5~lM->VoCmk#_-Q7_Z<KxQ&p7$sM7V|i+f0oe*l<2i zz9f4asIzG)pnz3K)6DXGZH$KE#g1qt(*9N<Hj5(tgr5`aO`cQ}L$+;yp&-@ac@ZSX zD3S^6s6i-KuK-k9h$TlG9kN>%a}3+IOCm0)g|p!mJVm$2gR!*(kLM&^kZwZo$F=w( z4PJIGvPx@YWpgHiqjOnDkd^6CUXZI=gg3LFv<@L$rSDL*E>bnOMQR0L$0lVM%u3i) z!g_J_2C%G*3+Ky)4L9(SLppMj0><txLWzcezxC6koG~>tro-)-SI+vaU^{{3UoBhE zle~U>WM+>?F@)Rnt&R=yPuf=Xu)RPzfx9^`xe90CH+pYVv?GjZ{PtiCic@xxh8{cl z*ei5cNxtcQajK?qmmGq(w&I@g2KUHV0%sF2w|c5|ZNy|!SuKgjBwa%`X?S>!S6fS3 z2O)JMBlwBi#Zx4P=7-T8a<WKg=G)~Ed~M1;-V4#*_q)!pDOp3<-@5R&%nk0wr025L ziME2ic_dev#DsDbFLfjM?xSDMKuzc+WTgoc!7UYY_q#X+C^Yzb^S#~I-OknC>6*I? zi|Hgs4Cz%)Zl4xsJKQmu{L*+NQmgC6X8*nYk9J`y7^aJs_jGFR^@r3rLN2E;umKdn z5i!*3w0jU$yG1)DFD3|qq)n*57I3~{=p&b4;CE}M|0bZV6+|8{&RIYT7k&BRLHcm3 zWi^K?Sj<A*$csG_ZQ*@xGTTrb;KeYptY;3dn6O_;fSo_E4lKVV^HHctXBfpyTdqS@ zon^Jr2>~*WwMiB%j3i=8lLK?;v^&=={3`qZ03q3F@_2cARNDq={zW!nZOKuLcm=JF z$=dC>c7A3^SOx(g{iyzazB%gczt@jfKiv!HAnHU@dXUz^^!?Q3Y#+hWrJ^{y_Tl63 zSy~PZ2{rEP5%5LfDGwH=Iqs}pRwFc7lJm?b?NUBKx5rVFrQ;taCF3~T!9oc#$^7^1 zuIpnrCrrkb5R>MbuZE+taT*K-_IufB3}UnIdUBvLt_I!pV%-4E_LkW6;9|h0lIV0{ z2H}IN)u<&SIc<^vkl&qx6hjmmTE<{Qw(rD-!wy*jhEes@slL~tB+1qUFkz6@qb$tE zHgsKrWuJb-^vGOXD>1qT(OO<eflNrNYHPUwV~k~&hQkh;p~ljKf6tVRyU2o}CO;}4 zS|pZy?9;KaY-^h26o>4cX7S?ADl<IYt~7lxTn|z6X5!-lrqc1=7M3;Fm@xGif;Q!* z6WWaEU-)cmMLN~i<X+EgyP(t-6*F;Y1g^|xDm7;hZjnP^9Yk)KwFQN#I2#0%5J8Gq zrke}>ZT8`Xy-2g!UWl&_44odHQ~PxQz(dR8)v!L4H8rwH#9kHIaZg#sxL}z?Tmp-l zd~W(kz;rr1TT$FNy8^K(Up}1wTsgOI{%2V8Q@-$1zDu=!kE22SSt}`xr_34D+OnQ& zM<!b@b}b7`Zj_32fn?bGwXwsmtL|$w;|JOzu&EII5@AP-EWy)o9rRwoosTbVm05sN zsWIju>+0CZ-anPwi;fdur$DPCM(_Vw#X(%<wS!G%TU&>8AhTrcPOrwVV<H5^AAn9w z(Yk#-(r~P~NycfA3bN82-lG6vHKTA{M$$EM_P$eHl{Rj%Fd?Ymx;yDW!cGZR&W<d_ zuHd1~x=@(P5xa`^TCi_#<<!Feu$KF@>R#GG-)hXEe<0X~*JrIXt#rII!SWU_7I=Q2 zP}7f@s%789(aZuZeuF1YLy#37PSPeQDWpjZJQ+l{XpyH*#JU<&j9FN-?t-b`p<^S- zB^zJ5@$<xZoJKqQm8{)Tj0p`#Ea-j>?}CC=(dF4I1a~T??^News$jb?<p2+Mb?x=B z7D@B&-N<YV=?KE6rxsLL?`2!2_E$7sPZo74`g$AEUHHOQb$35oFwaD{fW`qNzmVSg zY?10*{=Y)jP@=+wKqbF~ZG*kKFA;jzEa$h2tOdxZdU@x?m;x{JzZF~s`?PuWdd$|( z`QjkC4^|c1Oq!Z#@nD~;S@2=6j<&Y1CR7mE+LN#xNQJ{m@9yS-L-#r3PL-NtGBv?l zv4yYXNwu&SI8lz3Ep&CXsh<3@re+?AgFZjS0{lR@q&<q(g>YQgeTyDdY!UTz2h4<j z9t#7($Xz5DGBqkPjOx3Me=_xAaG2f<;2Xy!wk(pT?yRcm6*gQ0cbk`V{ECa&7cyI> zth=lyK$pHgt#T9;1b3a`j1nmb%v*E&M*jy;_{bLD8}yVFyh0#`Kgw6UH?4BI6V1{x zfMF!#bN@6tW&eGq4H-O`xi)P-=2I{_33_?JaEoeDj3`-5G3UbhL5HYqaI+Ud7yEhP z<Jcy)w;}Y7_kcU`9Eq6tv~~1!#NeDe7cH@(f)OI$Jd|_crES{78=xP^oYV8Q_vS!_ zH6O>VJ(Uf*3qimliLD*cR1jcWmd98~@$jdIf<D;cl;-QH62sNdfk^-^7A#2Nn46WH z=)QX@P*eO^$fK77mo$`)&0_<8Y2|rn@_;Wjvg~Z!u1v<G*4EUPX1Ggm6oQ8-5V=^F zSp!sXG^f9PjeiqO;Iv1qw;PF&<?K!Wb0YMtg~XJSRB_Mm#U3$T)_*T5F8aYsTN0~a zshH7*g71<jE-x*;kB9ABw%*7Cy~*(_Uk<a&%(QV818ZhW*}{4b!Ld<AvBjidn(%k8 z78uq37p078oF}ebgY`mQmftA3e3atBuPP2tH^Gp4HcXB8Oh&}AWdTVu&`9Txh2Kma zU&oRCWAph8N!CkQ0R@h8aBk?TBwV}(A=lVY|30%<MYo9kT7>CL28+9rluGR(;+9j> zDg^-G_}C`c#h69aYoto#0WNbqk#zo!YA?sLJ7EEVx%FqV#iU9r9Aze@rOzJ&i;8n! zftF#E=e&zscV4Oo_)`rFFFoDL6;w3<6G!aPVc4?Y3(MFiWx`!oJT0<5A~68g(g{!c znDBtdF)XA|9#(Y<XgQMJ_a6p9cz)7a*3a8;2U{Kd=)6UIF$806{G2k@0v$u?H%wi| zJ3HFUaE5B^7^n~$pUKZOydM@v*YzmLA|Sa}`F<}eOlTB9JvUdRc<ssyq`M0p!IAH@ z64MwxOg&9^W4G*rWX#is53(u@H90}Rb`MYKx-2}%_(=9%wA;el;5)SD8{dbuA|Sat zKeMn<M@(};atoc}veJwy$qQ-&y&S)m&&ZCa5_`sddX)m$oeiga7!pn$e~jQ{#pU7r z^NyHmJTJS_Wo?MGx<+n?rKiWSHxI9a%>TSFu`jc5Ki44#dYZLY%B)^hy;B6zadwf_ z6o^PE%LGRe)4};u-!n#DPhcedwlTD)pIW%Hkfk^%jW`_P)`_seM6=c=jg`MNdU%>M z{cyL~uvTCFCI~x=nnxs@p)qT|)b0=yrWqqHCkryQ@Eubo3d%yKJsBFmCWEj_Eot~Y zv{MJ}^)UW5+#$tPK!8&cmr}fCmhuJI=YV?Tjg_o*Yg4=EuZwDzYB3*03{9-oP_+Cf zbfYxOr+~$Qq1$E~esGR5^83dHJ94wqeMEm<lM<MClDZ!nt^^dhqi8T=+ytb1A6q?u z)WflVbfM-gg-m$jP$w=jnxD6~iPrf)93ReTzw9yDz%*92iFnR$2m1^R94UO^A>T~2 z6z5EuYP@{+o^EYhS8t#iiv%!14t{E`^Y2)WZd+fFQvBoERhQ9=|3V7cOZ2VSgIVFl zinbR&%u^kT@IR-EF1=4Cg30~`Gn<*PL2CY1L}Ac55vw$$_4NM+F6|o%K|#=87d}?a z+69Z)K614Tpfe;A0guSC-N7zW6%6!}A6^5{a&NqLy#9Ak3bNWK5P(E*BC*5IaH@4I zQW+Ty4YyC?8>c)O%}99<=i*%Dy-=pNbHjrmBek8d8gwwR|5M92^ftR$;9*ID8dD06 z&1ib)TG=!)E+Hc@u3ZAFD&oCwa^fP>FQ0(ks!BS&k^ZNdF-H#4-4wjFwe|~PxMof9 z3L~A9lRysISk7R1SCD+O|H^^c+9G1^R-jW~O{`>XG!?`&Sci{jDQL%A+jO(qH(!ZZ zj7$&VFu7sMvz<jmY?6_V!30?ZD&xy{Lee;t3T8|u9c$U6NHhe;8>nu17b;)3K+Xs; zZ)`KMX=>8=<q*k8F@}(Lo31Oj1Q1$Z;hh&RZADn<W;O|(t2j+UeO*Ty-U1l<ImNO0 zytwkQfoBGcKrtkq*50<WQ!NHOOx<bMR=~l@@kh79e0g-hw4!!CzptSupCQBUHqqcy zD16w90jFrMx^affAi}&K8=M(_di((EECTrJgdipC1*j2_vj~0AQBmEj(h6jMwzQTX z+cjA;8nwDq{NQFD4}H}>6D4q><#LB}qJ;?5&BNUDCBe%@q`gf=f<If!0_zHLHtX`> zsoCy$G%0U8{VxCtcJG?*1}`4i^vm4P+kJiZ1k&r_y2jG(hV@{HoCzR`3hV`5-1$lJ zz*gzIQ6fC~+YIytbe=S=mHD}h94v9boWGwA&*Ilm+GB-6d-4%cr&5;wTs3EblgiQT zx&B7Ek)q-*o|t2Gk4_qkRY!EL9bs7&sR7&}w+i<oztPqXK}J&f)Skq0Pdy}1+({6v z3;-@F@MhY~a4C;f$cvOk*zk|$gW(KVX#U<<TMdr_^#@*9&R{@1ZIB`!xn{u}kW$uy zzmq`hJqg`*VRg~|d|SgEToYg!(3kJ$qJB8~h~3*Xbp#@Q(SX5db4+l$YW|c;)C%Y_ zN1==^*{aV0>;}1db2LAtpBHqtqTRZO>s`?~X7DJrC*RUb<mYfc0%Iw^x=S8|flpC$ z>%zP4ylMH3r4#^WU?5QZ6-=eBFLZ3WrI3M~6_+qtJsqvm1~EU=nbF1=aeFU=uYS*r zd9}ly?;Or|_H8IUPLV<r3VH-#Zjo5IbkXnSNT+e~VR>cJ;2`xFS9T)+F<wd*BQbiF z<@o&f>Kk_?gqDu(tiBP*rWV*do=Iv9R5&S!j*YGnBR$?_SepJFHpnJXR&lE#@2|Q4 zpTP$~b0SMXEUQYxUWE4;{4{KQdLNSikB65L!pKP<oEf}i#VKu=iEIpxlf2J1h^lq; z9RTs{FFYB&oWX~>76*=~jRa!Edvg|MJzbasBl${C`IK@`4Z)xE7kiXP!&SP~7t@t` z+>jrx>e%C!*X!IfInAR#?9ArJNjX>8%|LH{IzX#5=Rg*92uNZ-UscyjYk0^Z9vw*+ zKH+orhy|fjQrK)Elbu^cx8YB&NK*j`<7U2P6XTavsq)|^KMj;b;@kCM26-o+wVRc+ z3nw$@q%eq}qXnJ7CVUJy1Ij@lF<mP?Y8^4yiuCqcvbTWWsV8}`a!e+rm{xaH(&XoJ z>4gt@05T>|dE$KM7Mv+!?5LyiJJqE~aJ`@Y3NZ?R)Rke`gpRIZiOi}RVP!Ht=bpd= zkkOZ2Y*09H{1LMNg2gJB#}uD*0W+ee!r6<Eg!CJR2VGy5d)`OR(iT~|&5!+xy)rqf z$_vSH)Z~D!G|JdeD3EJZw4C!<`AT@?TyUQb<2V4m?k{<%V5d!ZLdcP6^@tZkHfU{U zOp$Z{ons5IKbRJCbphqn%XnY2w85Qh7cc>K3>+>H0z0f`4{nu1b|_n`XnU;BK91S3 z!%8GfHHLJcAveGaC7xcIl_?;eY0g<8`eN%0fQsL9@W4-BVg*_r;7!;C0ki^&VWG@@ zpMW~NS474yCudGq`wUKdj`ICfSr^^l#hn9p4P-NE{2uxd>mNIG2Uk|#qjTNFLav-y z9Xu`eJ`A~Q0Pld9gbmW!BRAGQx33_J;YFo!@46WCXWO&@931~KQ>MQKM6njd3p^%- zzSk1R;bz%&|23kR=l@rJ7uADFS8AXN{=;`sz_G26_IFkP8O%%oFr#IbcU_-6Q%lPr z^T46bl%VIEG`4)P7HCAuUtrw@hzBgBn1em<^f*-(WJ6X`?9PEFOeq6!q^yM#ES28U z^|?WQ9cXvwZY2bGgX;zuGZtq-A1HT9W{&RTLW6o@^0<Y}96~dLao62K(>l-o%5~&i z(Y0)Nt|8xY=9g<?+n18Z2TG*zj@^RfkORX>7gmrCsz{>XM&trkdehycnb=K83lLJD z@ioonG$1rQ;*aX3wY*^3w1PEl0;B(Y_j8k^>~R=dKlBJnit7jpnF<4(2w$7sf{41E zZ2R~~<RvQ$U<XZ0R$*N9oCm0SW-2(AsQrk9BHPW|*VD|MHA#d;D;(!+p;F182d~fm zju?bJYDZT3U?l1*7HtEduZk&6Jz|kleXCo@c>%i3ieL_IEBGl(F)<3>47>k{pnYkr ztB8L~zi&d|ccF!tMzwpB#vXIAEFXjS8U4;pu8GtH(Ceq#VeGw?Ssfw_DbSURu6K(E zavm79@*=X1D_~3kBH~>0Vs_YJd@)89le<Ma=8q}kV2<Sow6G?L@Jt|j1=uInZy`U& z?G1WV+?%iUdQa076X%@rcspolyx%5&<prXS(ZSp~Rp}D+5pgbZxBz9K^fo-R^D99> zrZPI3f59xx3AyaYnry&T0YhS6#&s>=-W;v$j?g<F*_HgmLN?L^Y}XNG9v38yypEn} z93Jf8T-sySW)r-)Nh1qzq8=-cz&>r4n6*Xdedc8ZwxDFXS<$t@Z3JiJSTe%WwK3!C z2<OTNv&%6$2xAyIn%M>MHEl&wrGEq5KZ8buO|-ZpvlR&z-Me9OF2RWpUIWqN`MG4d z!H+GZehTf({f5|D=lb2`9P2!ZW$DEQ+k=)xy&r;+(IarvyioZiE6zbL6J7cOl*Z!Z zuY|m&4@*JtZqH^M%v6^IcUzEkF=2I!oU0etkUA#`Z#r%BG)^nP0?tlgfe8(fbd22E z8X@=eN|~5rC(fGxx18a;7iBW2mc~5f($yML>DH3yjij4oA3smCG#_vg@t12Lz#hR; zTVbJSAsRK7bpGD&?h%W}gS7ZWqEaC`&pQV0Yq)_Gtp*`Rrbz6wD5EU^oQlZ6+#V$2 z&i#)aCP^lZQq18XyikR5q9@nzCnI>|$_#+|F9SWfRW;oWfoeIe0|27ilQIHkoBEXW z6c+QVi4Mi>)s9;V=#@f5zKQLO)gXpYmN|C;!R=J*r(30!a<IVfpZ}mZFI@GmtTkpk z6+O35xl!FwBVmIac_-S<=MRMSTn+{bhqiF+S|An>PI<5?!mAWO5Nk{f4Ahfj4`G?w z1dqSasn33?H)&I+!{L<cxod0gyp2Nnupe9<J3T-qQGW1X$J1$YZR#L{bWT0EuiA@l z5@r`N9qbI1wJ5wynS?EQ>?(~Jl}xZOEmkYbN=EDn9^BcFr=(jBR4YUGC*Go$F52M1 z+`ewzToKGG<SJ|Bd_kIdXd8`lO`3_)<-C;J{bZ|&PgbL^Dgow?p6jXX3hPzYW7hm5 zih%z<^8*zsxpHw;Hc}5DdA+yXbNH7kakIZnuFS}3$A>^U9yP;pU|EFTB)Tr>SEQ+L z7@Y9AzKQq=X~LD>d`+=?)6O20u*MV7%d}+&4mQFujj}gPpVaT6vs)*BujR6a)K54Z z<;T=vU&QS(qyAQIUx-#D<FG;Vi2(0~`rcl}Xwv@lNQ*TZqEDyybmH;t6Eu9$_bK;s z`n*cxDE?bj@$rRmJkgH9k5}G5Tt1`q%KLj$yA6qvK68AbPj81kLtaqk%7yQxT@Say z7#Z`TSDs;16PDI=H*S?y+cOS<(T?;G7?EJkxEGX)-M3`esuA)c4PHhWhkBVe_cjP` z-pNqO9F!$7s8oK@uLsAwE2o0BUZwdlsu*>z7<73tEzQ&TZ}h8F+6;*Y?bQ$fL~Cnn zX)?lw`xe0T+i`;UylPK%0EZaiG;F+DH<&FlAullZjnpab_i3UFM{$gxJ5-vQmMO6O zI9KY*w@CQsRHAQ#zkYw)TY?L(-~fpr5`BJV>D$R?8eoSza)JJ|aZt94hgQW0u05qY z2_e^>LP9LSS%Hc6^ldYbV+{Ud1M{+Z?q^%&dLK~vXJlC;L|D6fgVAU5S4PE)->1M> zL{(7!)4kN_HmUn)u292yumZr<`g|0%V8(c?QkM!iY~Q9IZ~DrqNw-{(P8|rK&ok_B z3|eI_rIXQ@E7d%gJ0&=R=bm)*N!65E1iL1^dT`tYEWYX=p$I*vGV@rbS|MdF_&a7d z$nTJzMgIi$g2dPHyHhmLJ+94o{1MvKsLrHLJVSIq^eaJ-9KV7q?lPdT#mv3U@oH6n zu@|7T?^RjF*n)Ru+*Xcv;CM69T6QEI9D0H3cbk5{^XIL>g>ilie%XSaxBU3OfpNkC z%IMbyw}1QGIj8)BWN^!7d>~FbHRoQU5U2_djlf11NE)-zW!0~%akgwp+oq3A{P?a) zRAsu8;d6i9i|z>4FRCKd4DRmnGV2L`iZMUsw)e#k@1|`s{yA;{yz(LjOh2EiOUf*+ zcJ>LW1e7#5Suf-PHru9;IR)d|NwU3sgh4Bp>s1pLyez}MKMPWp_8k0eHO;zlQ{`KU z;0k12J??XqS+p=StKo<N)dDqel`0z%yKVZak0aa*{GI`<M-<CXeWXVP9{{Ue%08?B zM`LAK8nxs4k|k@;ivYGvOx+^sCxqe?Y7v@TK?>$#*)3o`65^5HPMPH&P3|O3sz{0q zEqFOB8Q_HDHZS%4kOjhX+n=_9=|)uGuE^31yGm9&jC7vTH8ylhibY8QIF|)Wj5MFd z(B?kU1=p{0eMzX>%^4<B>i8+egzDB6RS39@8^UX=E1X#4P<Hoo-fDJ?d9O;p?p%E{ z<8AtZ>Cw}5HKfUOiFq$g&eRHAz7F<SIN}DTCC+?(2&i6{^#U2p)nUpYj3U_ccLVVC z=YI`W*_;Jc5ioj6EZ62rdAC}c%fVM$502;0Tx9D_kq)nht6E)`0lse^MCqG>25?Lk z8JB<}0$!enS~&ZLED5Gy%^MIg)USK9D|0kGea}}z%ur`b2Uw~A$KoR>JEj`7uiuI> zzC!YmfW46Uf48aVv<9h5k^0*O^g{1m!}?o4q{{P{pT}r{gy_&hlbE~L5k?i)=za|k zC2#6{X-Bc-Y4v_NEwIopF9lj&#vuZWsxc~z(QIK1*=ArOaQ>y18)M-kT^iq7L+VBc zY;d92v*6nGkx0z62VntDu`+i>#-+1ca|DS`)h5PcEdr5m6NDUS5V*K6b6nfO`v@aQ zEBuKZCtu$4QXd34n9}lSvOY;ht}tQF2t^btgk|fnXJW|rMxsjb>AJZAo!LNbxAps) zyrop!j_enk<oce)DsXp);2nZmaWs)HX_$<NTLs~aUOtC+ZJrE(YUwa~{Z_wjZqKLe z>I(y%<Ndei3~d=f;_QJK#ZiQ8>lEZnY!vgI02AB;_mjb}^wUBwdQ^sv1<|TR6FWiO zM63W}8}hurCPcP<J}rPAMQW?>Y-wpG*>XB;LI7wW_Zkg<hLm$49n4<js{`~9Um(R0 z+EoRN-5l72T)ze8PL|W7WG@VLnsqjlx|zHR*Z>#7i~XuxKW=6;_!+RliQkEq)hVAA z>l5gKxa8@8bot7Wyy^FS@ml<Bx-t?3Lyzm_f;mNBjInEUmI6zc%xk2$uJI<q(^T+X zj&#X_&!BGsD>CmE6Qx-iGpZConqAqz7Bj7CP?<;%pQm_e&I3$rPboX@p=WjsIUhs6 z1Od*+2v1-LVZpS`F~6w>&(Lxj>hyL1o_F9tVv?6EHu*J4_Htph@!Syb{iC<*&y{HH zaavx=2@mLK)1PWnJh!4ea`caPT~^{M(&PmifU6b{d-{>E0hGOZs`R3S@fHD4#XU1Q z7qGiG_g)ZELLfJ*kja{Hk>*#Mjqk-_#{s^PH8Ag0;X2eg&NzNtbB=uk#iPLsrjT7= zX}<NwvlaYcY2VVyKbk(y)j_8PNCA%$@qI7|>)2p(22u#P6Rbsf+hf2VwHr{<0`uY- zWSc%PD$4xuWd^Y|w(I(A$eqxI#aA+)+|0enr4;(RA!H?*&AtFcaGs<|fxlvU`P`Q+ zMjeU(UMC!V4-w32rC7}n$XYPC)zu+wl}NRYB-Mh0Q*cS7@!%`>!@&7Ia`v37^aN*Z zj7G-X=(U4S`fvLz{mxgjJ<-GbKVjC06a)$#t5v!A{<8`dhDSdn1l6>@f)yFb*>f;K zP}d&4)fRMl;hEfUzaX&l9-u4xQ8xOOFv<@;qzK`mJ>r4)Sw)MPvBXvej?UkF!LN4h zBb}SZKJ!pOD5EPhv%A})GZMssVy#Kp^^h@C*z2|yNps{2Mu5SK_n3=os&Bnj6nY$p z%<Xh=gmDb~v3#3`=FT||VQ4PTZ2U4s0SG%8Ym%}OZ)@%D!9|I{&SE7vu@kx7BsuT3 zC;W*lr>St*udw90wCvb9jVXZq$8HVD-Q|5Mfm*DSSj6`qs&z^7zS9Bsku&g+ZI6$F zZ?-jF{A<+r`vM<l&U%iI`W}xdZPNJ1NCWWnc5Bd_TRsqW=Ydrz(u4tc`SI22r3nQ+ zhIcqZP99yisi<GOZ&YQ<OC%q(t7s<C4#;&XdsLf5zerdQ9Q7zG=Fq@zoC*>2v=9wi zIadtm?w}C@3o_UJYqZn4t)A2!h`bFlJ#}YwZIIT?T8(V5v-p<+m0eAJIh^V{UX=Xd zjiRhVz9%yB0tMu0-a18^TIhi(FHO!&f>3-fO*NxO!l))!6kxL~sUjfTZZ~=*KOec+ z2?t)gOYy-USPY+dvRpI2yB(%X1XYc|S$HQ_8CU&qau_CGagupJa$mL%G8sVoW-!?n zN_Qtb`7k)iYJozYpGe)`<ZWh*?~4NVxU^m_swA@a$_1h)kew6e-pcgmg;25sd-F&a zdskV<$XfP&80zaXC7HaLJ0I!8UP{6q%;`ML<8(sKv!M3qP>eM2^;*-=`&xh3uAw2B zfNBr3f%Sc+dJESP2EulfSn)Q;38W2)byy=EL*lIsb~gW^?>zy%T?oJTcP;e10<h1l zT|o|ic(Zjya@(Jpu&dTTA#q`7)Uy6S%^Ptx5##NY8Pyvv`d&@=5eafm|HJwNdeU~t zrjQvQlDryP#4GL121wxJpGD-+iYv7H!XlQc;W=Ma6?Zs4iShY^=ncF>x%61RHp*@{ za_ESPAH41WZim@;IL!)6!V;wJ1Uy7=m1veZ0DQv&iIG~2m(;TFR^r~O#Qgx#SOb&F zF7fhHf1>h^ED1EN_<r}=ru#!VP#Sg`C`lKN#u?DmMp$u=J`0qp;fS;i<)p7ITu;Dy z6+NNe)LUG4B|$r28f-Kb;D%=owDeCzsE*H((JOku0SRK@^5$=VJ|}d1VNFS)>XlTK z9e=dbX0--K6cYsCc+q0E-t+JRZvnEg`U!CDpkn3F>6QaEbBe4|Es>v+=W>2h)?0#T zs1E{`SOf0I-T^7nK6&b%$oxv!zryn*P*W}eD%vR{Vw&zf-mT0Jk}W<p6!eGn2W#eB zSmyfqJaTI~H^5vqeNf4;)^*QHch=oNSpJkV--urVvsMx1@KDI%2O2@XegUvP!h)?a zKiTye36{4zhm6a+XZD;IA!(8hQ9IpDb;iq)eWLdcc)@)R#?ga*R)EAMpz3`LT_AS= zU^|OOELCMr0l-<2Y)$(gc<8SaCr!%TrmlsOA7q5OEhxFMs0I#QRHAeTOA4I-A75V{ z57qwvKSM~uEk$L!-EO&(R6<#&{btEcmW#%8uO!*Z-Y_UvZmTwgFcB3oWz8~NxuwN2 z2-!n1vNNHv{a$D0oH2cF-{0pkf85W#-rH+=zSj5q`~Yxk=-jlY#h?L5h4ue|g@v<` zRwBBQO<jM~vRzJZH02~`Jhm6xb?gfxoydK<{cDkPN&j0~0pV6`tZ*(8m?LDw(c!_A zKP5Jw{g~nFHwIzt#->X&C->EK=Rl+FGr8RTAb|l8Y%cN$eXRkfmazpnTf?@np~6h3 zW785Cz1NFD6(a~F>R*5oa}T5-VzE7}@U7)u`v*&V7xV>Mrh=qqbD_so^QBd@CNf!1 zjBsw_ap7E^D@AAF+8pmGBH(t>Ji0O{qiDUZUo-cBuy2GwM@lXR9@ybQJpH($hjYNj zfT?2A$u;jM^msdhO#HoSsQ=w&f+x@gk`#2OWBKgB;|~kgS%mrE9P{hbeY@r+U4BA{ z?SD0-B?vyxhg1YOODQQbDAL+M>b;f#Lr<tXi~Q~0o(weOPJ(2ckUxB&MS5L+`Fy8% z?s7yN>Fhm{N87hQp{Q^tfpR0B^{!(#l4YX4Q6JTKQLW`JTCTLRiZdF`-Bsf_Ijg|N z3qQdmm3L~kKO3A>7i=M6{#See!E{;`F0UOFeOTb|EETe|dP?7rB`XF)>X$Ib{p?2F zCxf3;&JFrwuAUMA`nZS_B_8-4^U~tPc!G+%gk+WL#jdUe7Y-VQ2BCg*+3hm7KZ%Z@ z$RJXaI3bdo{%Wa<B4&%@j}qK<YM-k89TM#n5Q)qgNQ!Z^bQsU8cE32+>9B)CLnKm# zgC;Zt2g=pJ1<J);;mr=f`TMKY*NA8bX{bL*#{d^%Aw4&}=26>6=4;@bbx9EJKTO<N z@<*U2t4kIoItK-ClxAoL8oI*G8M&a-!P5izDW!u!*wrFEcpP_YQ0&WI#Jw4B%`xio zRXwRdceeAqZJ^AB0*;pnDQmc}`FFe`smN_$gY7k#&a23HexmvW$R!r^>YYKH^7f<A zxxI)JFm4XW=pj&5mKW-Y2^|h55@D2(XD019NFL6aXr;*Td@^jS4gNC+ex!t%d*oZ{ zCMu9v=!FICni}akWL7l}Bw@<JaZX?|F$QTj$1gVk@vI#^Pb}R0X2RF40+_%<^B)KX zwOfnXBRwS3y747cquLeTdo~nj{v<VPV(qyju=yKk`G|)uwxYEI>7rfMxu7~f_xUxI zx{86E#bZ@Y$YrXv?w)sdA|FDsUF9p??phsBa`lW&8r|u*gsaTh{-hZ)b7xkm73c-@ z)X74zVn1h_HgFyE);Ds8gKWW-T)k0x`2A0-A#~`I`$FCk<b=s3P$ga;nwr1Jcb@(d zBV*uZ1oPayX62XcUq|STi#|8ljnVFb2Gw*Xe|WYxTQ>phgoy|3pR-eIxRQoBb!|xR z8f|Y<y)(3}-a%x;3Y`48SeU08^E4BRgWJKye+4~CipFK0NE+y83&hnxa@?9>_U^*m zlVX=Dp)r#rUQQEB)NHv0GqMb?hQujWvD||=&NAZNWkcU<YxD(2cxK>!)_{2@KhEH0 zHLe%y7qx6y4<x6XA-7nX&Q+CQT`&4XJiYtthmjs5quH~Nrs!Lm2A9#^WOcUf+u<JQ z|IO*ZvpKP0t~v3`x<bcAqsFljh>RufDE?9BiR-=-$ELB1w=5@LLHd?7!}-I=gxRT( z*ONZ}oEGJ9pzNo+CEGE2;yC~&$W&|F+i_j-v1pf(i#5VT6$g=At3ws07>i7G2ti~d zhZs4g4jpy1PT4<7<y1IdyFe1e2!&os&q6{5kO;1Ou8PQ5M2tu-F>dwyIqZ5F%Si|f z2Bogo=<8QO*xm{o#`4XvQ5WNbj$nu7>Xd;Ia?PSdT`qs~4$Bkch*;P;A?@?grGv4& zy(xl2qj-BPO9VD%Yu>bKuC&O7RnoDX3$VOW%Pmgr$sl*ftDq|B4G1SvyLuDdCASH- zlym3+L@^a3gd=&_U)su%ES{Em(~hy%Uz?b(kDN!<95ssi2cc|3yU>04{4Y;#z+4Au zsyfeRePOOJY_5bL0?E_s$bm=t?jWl4)JwKq;pWE^!+PT-P(Ot0{Ry&OFJeLFT}|To zaB~$RuZ@xN@Ab9RpUgU8(9$ad{?_9`W>1>Ys5OwxDY0L+`|ZZen%w-RlL1xHA9iw$ z4TiSx5wES6wLVS??+Uo#Y69CMClAcYTR8~lPGWr#1~f>6?#%2nH5}auGbR@R+Me9C zVTNq2d8uj@N;?#?s+WO-TJxngqZjK1aILx3C^G7xsl{QirCiaxB6vF;*gF`rGmybd z2-06QRpoCuxEh&1_f5}J7i&#zp9Tz)kPBHrEQ2tPX^=?`nOY-zm!n-sx1$3)uLFyF zW&>b4xxtg0a(cxGQMU_HGn3{$0yW8h)?)S?1@Z*ALs}Jg`1dx=v4bs~xtcQtTP!gT z+Q2k8@Oa&CP>uUTS<ebOP2ZWvU!+ARTvbT$`d0FwBLEUyw7={xH(Vg)HgPzd?sR%u zW?M)LuU!yclsVG^FLN68MMG#G5zx5#VuuBSASaSWJum!^VkkolY&;Wx`TU4%t}a5g z90!TEydCP#26=5&@Yd%6?E<5|v&d{-V%rYCEi&HjaR|c_^o*wKk$Y}0`VaWDOmL-T z^~_vObg8Q;#@Yl!LdOc=?bl|rvysrtv)+1N_bYFxM;-CZJbY9R6Dp8*L6#ZkZi3Q@ z8r0dJM)1w<(bo@j#n(&Sbg<S=%4Zy<tGAW7v3gnX=12Dn)=H7AZ(LLeT>Y(}ApJU7 zQxhZ6rtVj0Pect7#dx>n-Ph8J54qg*9HL|^sqSx(hkeG({4OBdI}Elon|hX&0m3{W z?#Gb|uCM16Z1)_wN*m~iR<V}=gx<HZIs0*XxN@mm5GN&x!0&Bncce|XzcKR*gc&c= zGL;{zzTI2iz5?B!-nn2qR5%}qGV4iV-TO!v6ZiFxl{o0!Moz3e&AjI8D4gewnFxbP zFcDsLH;{<#zJU%1%fGou`^MP~tvZwG|08z~7J&mXSKc}0-Jhfxo5&GjNJ?0qI6dvl z!vo}gGh@>2SGK1h)yVLu`*eZNH>|iVz&7%nEf{IZ-OtggC0akJhqTP`{76p_k78f< z+Jx0OqFG|9%bOrKyAA9n<uX+Hxi1Rf^g@H}raUh<h7ZIey-e4E2eog1^4R5bx<w1a z%mgDs$98GC_L;Y@1CIj1M48!kjVRqwj})E@RioEZ>;pMfY!if)YH6>8Dq%-@m}lz= z-8V7!O!C%GAI3A*n{-z(y$2Dg!V1&y&n3E-hZiT%|F928t~BEU(TCit+&_Xl?c&rt zaaZ<hBq}uitS$f-Msh8n{YJ|`ot>5SKbRZufhc#)$OD_QotnlR5`kq&$6TR62}t+b zgu4}NeN-(1_W_zMqAzwB7)a^q`fvLmb_ue&$0MLy^~d?0Eh!vkHHEaeV|}qmno={{ z$=57F99pW8Hm4<^(GP=Bg~r5_vK&(PL3hJtgvw-H;t(o>)yzxB1Xuqxc%NzEJ~QJ# z>&t-jI34C>MU4GM)_)5%kh;V%N_=K|)!>Rtqwq#J7`--s+8FB?QG?!vpkKSx@zBZM zLAr3la(j5YhLY5^5(QqPJ;$%avv<Pf%Pc?pK=&X1m1#PFXiB$(@KB*3n$Te17hs!a znNp_PF5Xgs*7E?1`s<MKGiFKh+@1In1U2pfSb$ex$W_^$NWr?&9L}e0H~t|v2+Ofh zYH@mY>e+op2mPf%g3zv1_a%yV<FJ4NBp)@Gfp*BfSKq&?)!eQb{Aq-EEtgJt?CSY2 z;g0EBf*iqJwwYycbMhC6+fts48BZl=rkwofpo>wITVBD<770Lwzhrr6z3aM!>~ush z_wCI8(SM}aod<yM8dB?SgE2bEq`@9KH)GCcxL;9=?O35V!*dof*N$QXJx;|jT<1SF z4R*HOe*X_5>WJzgFLWTH%E}U9!3}`o$or1a(dboWdtEfDvhqq##|j<`Fw3hDJeSj5 z(wOMIZoX7l1$bF`=jq2A2ikAQxB6fmO029!taY*Rc1VHm*EfyOlGt}Q=gHRMKLd|E z^R0S~2`i_64OzWU2x-$EW#4GawT2m;US4^r)Eu-~$B%?PJI69{JS#TqpK$O^V<0jc zctJT44v8lxS~^j1;ED(YVs^oEwcz$FowR}r=$f`LUostuQ4T;Pz{~arT4re|lP2+S z&BwKKZ0hq@lg?6}3xx4Q-xayAly|_CDD$u4lTW}1<-v4_!o#x!C-pEbnZ2T9qnF(m za&RgJT0cx|K3%m>>(vZRdJy`*>eP&|b$BVaRkbAp7*X5)rmQ7sr_Qs%13bVueWAE< zDYz4f@C4Z0bH}&@O<qzb2(GWA{BlCFJM*Wu7x=ZEyjp1mBH~0F{Xi*+ly99t3)6xu z_2_G}>RnUNQ~fA=v`G1JT<pc?>MwcW9EyVv^xzS^S!XV^%RWK2W%u^OWLjoO%YGi5 z&OKg@v_h_r*l<^5G<iHWF|2wC&oZK-VY-xRHDB26IeP&R{tph_2R=-6yq0j2<v`f{ z{6L$S7x4>)dG>VNZ4c!=PL`P`H&?#da?sjU?Ur4MgWfgQO{;&YGu|*17jchf+`qi# z<|f<TU&qo!X1ZK=dnnz@%QrDRar6DnBZ;1O%d<mTS~Oh^W!XRelXLB#<@bJGbd&Ip zw!cwx(z;E5+U2f~TQ^9l>Md+=5B<UVxI_BP<lJyIdl*`G*j^7NC4uvw%f?k-NH(~N znMMln#9W6P&YP~FM$M52<8RpaIwT1VT>-2yulw6>M>FQJQJQII=Kq*@G$)446g=+Y zwW~XKggNF>%9BSWZu;akBy>E#w0Hf(cJaDVcpT_b@6mTIiPvP4(Bs@{D)-eSc(d|o zdh<3iv8`d`g~O)>J{>~CYEkB`zl#n0a?iSZ<rg|2^PFn>rI4C+nq`DZ&x5U|EO-;* z=5k_pZjFDGyIA5S%ySm>Lick~h;?vpiN-3Ys$%or3?v%LX?_`|`0OD}#B0f{KKs&F z{|vJY3~|>f_&lR^iRw(=rT?7mq6q7Ce)^7FP1~YU`D_rKQ#ln$r0M*sb4g{`o~qB1 z!8+`%&t5-ZU97PxZAUz&s=fjoQwo_MwMzLHCkHF-weoL8LaI7FQhzRD{=M}M_Bzag z=-u7{>4fEnH_8&tN;LdVHZ^RC;W>IJh4Acr96L^JzO^|vMD1A`XQtfrkx!Cd5^}tk zG6E->^jrNQzqq1*jrOGy%RCL;uS@=SOVCXu$b4{Ou)S+flS?*3J{LLK)2i;T3fO)e z6NpM|HzDTDKW(z9XPo^$T3}v3u1yrRYzb?rQ9Yk$J8!8rG0i5Mfzqi5_o{sy<{9*c z)o=DU2?32CpJHCgNbO%-GI@dL9r90O`V;%yhcIW~{ljM(A>_PoqcS}uC<Diu6~+); zz53d{;{6~*ni@WTwM4p$zieLsZ&a#9m=L`J^HrBN96!*IKh2>~#95ZJLF3NrK}#?* zy1Mrk_yt8#Z}!V<_gYjQnIOtjb>2h{IN&^eN3l=!`whNb$kB3gB|d)E78&C-%p+m9 zyLNSsf&rT|mqo|wO=<m$gPsi>9l<gO1yX3~)lPy?GXAOK%$N8Z{;fA4Ir6BX*+(}I z>(*nhKJjoE?tk9wNlsCHG5c2M{HW)Dv;$Ah$IY%tq_wGgRK(>Y>`JdX*UGEB1=W3p z9>47mh;?g-M-ln4`7gWFw5X;O8479SWM9_qp?8SNt3=qDwpzcMSH-BTbyr3`ynVh7 z=yUh>DFbU#<Feu{>lQ5dA~dsVg<~i!n~nSs9Z9Ki7<$i{%T<2r^QSn$uZ|e1mIV6T zWloC&JdgPmuh)H6v`#Kz>wiU2Wf4<t9eFBAqlvlEcL*W%>b+_KXH9wufxPUDbI-+M zHRu?O2tkl&wqc~7=Uj>~G4&va;wj5(+$9e*!j~A^iA4yNl3AlkT9{iA)b>}?xpnT4 z$$ev2mwW6WAko*%{{RS%{ybR!BGY$KccoUCPFp3yFlj`P;cT!6u7(7)%|KhS`3YUc z{@+3CRb>BrF7OH;q?~m>O)DfLV76PnjFj)yLYU*V%+S~p>_)&2k6F^$U6ccU*^hZl z;{XQ|7s&~->!q@Ms$V@F&#V!coI=Pmp&dR+W6He}!61kfP3?WX4pN=#D^p)~gtupT zI8Rm78ZMj+(`rvqrY~K$a{7{Be^$-Dp$UX<gUMoFc@GsC#1l<ffvdpdI%8tHKgj-T z!<J3|>w$<c>jjv}*8WB#tvUWlzWM!7!!y&cVk?l@esjFXFG<meKqjrp6-u!6S+J#6 zGE^<hV+=a$CT?nXOFXi0nlnY?&XM{&Ux|<N;+UqsrJWhpzcIZUafxgj))0#_*9CYv zm8XqjI);j)AS+dnRoiMOXB(y3eoW~{fRIyeEu%+`K!&hCC6lt9pZVG)2;#hyccnK= zZ%S=hUcBYv|9CO-)J6e7wW>d}CwiX74lD*4GOtV)@$kh`5D9ng6+_+hpkw+y`+=7Z z@}=)p79fpN!UGqEZixd)40-5aIwGWl6uB&%WfcNORQt}&G~&@cPsUZ~dd|bkXa^nt z8UIl1@Bj+~d6o=EZ@|sI+dizOf4g$O*x+(xFIT;1CR^v<@4W7h=Mu?Tj*&!EA_vLC zi(*8zpkZA<mp<7AmZ3%wM;FG^w>wMzWtw|D$!KsDXR2he0|scrUXBD)C+u29i!BpZ zu%gbeLx)szrg`$eu!x&2|2xoV<oTB-rlF1qaxmgyufP*OqE%^DvoNbzCd>2-X)*YU z4bQw?>N2mL9=NhVA<TBr{vagmWwkv2^R0=JRnsmgQaoz@EI0MJrB3kcFqhV3aY1sn z{j=d9utHVN%wGb@O-(%48e=?^x5ioz%aIBlU2etw1;<KFulT2ClB=d<RYCT@q#$Xk zkwz=~OO0*6&U2cYp49@JExfWrimy!oYz-%da^|QyH-kP=L9P^F|JEU?sp~gviz_e> z$#Dta3`n04Fa2MQ$FtLZpde0_j|`%xDFsXBhv%;=yh`8}fmmyVSSNd>4p<6$J$h-A z^W|gAO8KO9b^pbBZ9>rOfS7`bLz39Qc7#=`4ogUOw-jE5qvtY#Jq;6a#MFCcarVTw zxqo%5Kj-<q7#zYZWr&s<zb$5N(~&@!WfWIFK99ck<1EFP7KRh8)9_j}S-Frf<e5O+ zY{lP!&Zj*pE7dkX9Y;#+8eX3*hET`HE+klCaAx8j6u8V*=AGQ*^#|p^RJwKd|GI~O zQ3+?=#Gd!W?$5{*yMfH|&iWNmgzG$Nh%)z3ph40+8KJ9f)3U(4P5%`ZXi_zR6w;RV z0MBQ2ZhL{Y^}hku%yV19S<Qm1b&@!*M@yVreKOCyh&+AY<$H7V>Q?%nH*JnU3Sxa< zbUi*gf~-B@sw4g7A!_)9?%9uy_yY5N^}dTrvL3HnrJ};)F%~mH%7d~YaK2e@U3t~p z{bB>Z0efzLtQ9;kFU7T?jjN~!3NKun83yj$T+@6xPqlw>V&(!_(Ao<yQh)XqbNaI2 z<;<E;pN()P!|jQR)jY|CglWh8CPGvy_2spZSDA+Ye21{rE)v2u&9)(qv^H7#+iUw^ zl7p@-$L5HAyXVk|%T7I5|J|gECi&P}Le%nRjuy0(1zFo1T+jl{s0*@I<Jmt$b^gkB zM^b8*Kf)|CBbhO@@@2ONIKJw$^CG61fNh-uJt<?|OjWJw%J0c<!d9wjmHdmR-dZy5 zPc9=9Eq|*&^3QXxweu!iV?<j?=<rS)whZ39{&C90vDcR-Mn3-h9ddVzGewl<+x^B( zIu+knBD}lkAUqwFBKCDFI*|}$dLeV5e9*6btH<o6Yg+;q!X31xgaqMJ@a3ex6><hd zi;r2Y0|W7M<mQhCXi8B-0Q&4+sNf222d`-k>9EaRzba@Y-)1nCB5&gd<kJZ6?-!VU z4BS9`6@*qnodp@;E&@!m3g^}}?PJ<}siHW{$@&*hc4|;qoZ#|$e^hfHa-@Ua5(=|u z@tY<@=+JmK{QyV2bVl6vdD+|crM&13^FBnF7vcF}h;sZoA?iw&M=F&!%{1R^{#9ly z2vseMAc5Jv9pkbJMU$#97qR9%Teb9Qvf~0!@G2`fy&H(flNVMRGw@2=UlE{UeX{Y| zk{!fX;auN|>|OGlPGfb@;8xJqjS*Kj|K7Ar*T<44Ls+%|ik{L8rQWKpsb)R<*n#s1 zg>Kh|&Y+KQl5+&v8Bzz_fIh}HAVT(^B1Z~)|N4&&7Gxb4WS1TZI#K;f&u0&!8@uBK zNfiHr>+||tDq5*SA+e$ZY`Ji{+trMR&XS}Zj(pppaC;vl7`G&u4*-;l0hAH_FkF-< z<AFR-g;?oSJmsN#_VnH%M_j@7a1A%{K!<(h%-2o}<}-K*Qw}Q;s%ho94>h$)aRY#b zH&^s#O6oq@LVRU_rr0f(GHa?j*D2zdwYoVT{jYLVw4a}D<^d7r(P<NCn?Yc6@`S6i z2QF{UIs%(AyW6iDKd*tx^9e%7)_2pQ!J8}hJeuedf2;HS>1zHRVMfc&o5M6ArN-8o zuH`yNMeIes*V;)(u3KnF4{o2_L5S*kmXdDo%CQj(nz|6g(F_qotW#Fwt#vgc&lA~= z3x0NVW*WGL*88kJskAEfUbez{IbA!&Tvg~~`5!+OPIU^Ir||q}Pi$G7&u=i#C1-G$ z(at~CXCYQQ+*NVfJW@nqNja4Atkr@LBJ4O7Gr1@s`xdebxqiRL^KpP=Xuw6=kLZQ( zBC1iu%W+lco5Bh-AbSg;mK1!OmhL=xA;^uBKiv_zdPf1OnQ1E+LD#P;{Jxlxyu_kp zy{-+v`0|vq8OM6GfXhhc>C%z(@^HJm3c74eLw6GrraJmgLxbB!#v9dA(rtM@4dr=U zDLBelAu~^?T66g7=DfGaneG}&_GqIPXAPdx$mkrz5jPIc3rRm7SUmUiCq8N6nPCkP zrd5ahUgzP(`(q}H%^gd?X}|yMHf83V7_bO^=Gsb;IvKc<YUJ?mIQ!a2g35Pkb1PVX zr>}H*L)+1!(qg1Wf98?rkMq;0xP%7JJ-;9_Oo0O3i6+EOSmnxyY`pXx-E0<O)yi$! zuj*g!mMIA3CNsG=D7K^3Ypd0W4fKz~dpSubXV&cV9_&2*g3o718=XLC;EhRo6{TyK z1ItPv!uNN8cMybr*C0)jF})2(jQQiOP15E5=CTKo+12>(z%tYzH7Z3eNjhZJ>@M5} zdQj;InWG;8l+pKI)5I8(y`bYEpS6?nXeT8TRQVKY0lTW#gVj<kz4)$_xx7%FGf(un zq+v8CnJU1#*RJ?DTw-<+Au8qYsXeURm4lt0NBEeAR&Wq=vr~jsdyr)tB9l?Q->Nwo zCUa`PlsK*)+8Tlqd|$AFLjrR;91bY--9sfva#ZvAF;}1aO>RgPc-$i8`ZW67nUxfE zSnl&6z|AIJ%3;APmSFUX0T5?&`Mr&QYU1MxgJYr+nKM<pht{GvBMmSFk&Kh{_YZIu zxNJNVFkXDaf1n)zZ@VJcT?6!yCyPbQ>Jv!=kb15B;XAS~Pp#IW&abKoZ+d|oyy|k| zcwOj>0cuMkaO7;~DMk=%UDmvDHGOu$U6ZHs<Q}Id9jyO_$g(|u;7AiwKt~(lheKS* zR}#vP&v;gQC5}iI)TKE~mIP_?p+8r}5H{=pTWjMc7XS4h#0l0x=CDX^lz$+We*iTC z+Bo_!ybE;V?*&Qcd(kgk7DQsO)2QDq@XST|c%Va1Dqt+yZBAQ4P}H9B>Kk7HpL7}i z<Q{J-@?C0_FJ~ChJWEvMh<UzV^f=BmiQ<y_Q;}Y44bV2z*CMR;UvvVPK?2P4y-m!- zf{BsBI)0qQLMIXC86no|ovW_UTC^JeC|3fd$^S*~PCxqG;wc=9SVq^vdDVMC!>&xm z`{nw2#Q)Hyqw$yv)(e2p>32c)J0}*URB3Pq&S>7Z4aoxa?H=066&6!ns^pizrPQ<& zwk7&xXQKb<KR0||Y~Yg^%^~aeD(>{L)3iAz^9$_GFjC<aG$|uu&$=(j9=g|FBEn21 z>UJ#G)jz_wGYo^FooF#3LHlChk2{KgktLqg;!IMHWCwt6QCz<1bt*AJ29C^TH5&+e zt$%WI7tw#}mCJWE;npI^EC<?-xQw?mJ^Xir3|JsMJcu3*K8!arX<#nNusGsZoT)d{ z*wn@IwXttCAX%SV!pvKq2ex>aX#Vcz&e>9S&h;Sog6rJ$zaZ;paBl9E#o>q=M`j~O zhTojylPD|~D8Y~tWhUpR?Z_SK=@R$(8}u@DZ(nI^6jx;RYJ&9xrnV5QOtLfereZz5 z3sc}?8EAi{b*oN4C;D8188+%rB|kQz+V$L&8YD|i`<&KcHlU)!2P|CJ0kk70;%6Q9 zhTBXCk)GH?@!ZTB(NDpAwERZ)z|*zf=r>Ic;|o%AUtX*6e|wehB<^T(2JSw<C8C%f z!53kfO(*ZQC4t<$u_(Acy_oBvDXnHG1y+l|98%RSWr7j^@R|GtQHE3!!kek%Mw$ih zHX{Y*Prpo?&xZm@CQ(K|f;w!TpqItEcz0WywcnK&I}?s8B%*dS;c7;9EOMruhB9M$ z+)NYM3{RPa3y^}$)YQ*9=R2&M6|SjaU62dt56*SqAx`G+LMg;|l|B&&Xu1+Z=Mqe1 zJd(KNi)TnNm9iui$tnaZi`&iH*mKFQyY_w0wn3U~F$m0)9qmP0?@#(FgCaCVDzg;1 zRchqxM%nfceYX;<x@_y0X{x%59Z=yX^Mb&n0lV2_)f&UH`Q@7tcQNbHxZhkOA9Wz3 zw!YxP1T*!MYf|aW+&2r+=7RqUDKcT!k-AKSq7FA?5_MOK7gMP=v{ec{*slb|A-W7z z<tZl;Z<j9K{@>gfC;oT~EbG`;vfl4!hy$LC1F>t34P`vg8^L-PgNywiBC@l*DN(#8 zRHeCWn0}s5e)wPRcYxlhTg*I>>VRV;uW0#)AVMiEkxIoL)&<wa&;y>MomOlb`9OEa zGYx;QIUg~&tJ8uHdKT;jJg~76Oy0GT<epg4(c_9xh`-<0VEqbi{UVx97QpK@YXmS2 za9-<E*%Kp*c3}&<;y0XcC7H>M`HD*qG!DuW-L*K=+*TNHN3#uk1=ttMN<<kEch)qQ z&B%TqEAZrfIEu%R#LLD(+5RUgP%p2|kGz@*9$Tv@3OjOCNqPOnjEKK2bFT%u^u>Nx z)dg8^@E9;30>{rQ!$H`?AH@nM4yDy_1%Hz4ou3UZWZC4Y3yuhM;G7|-&1Uq>q8STw zWSU&<gsIxXy3p1gsIvAiA`eSe0}kK|{<KY{`<2z^o(@=eDxFVDSYL$@4_V}VHz#xP z7-UJ?7A%Dtx~o5e^+&TvMFLcnNR8YCm!r+nSK?OkS;d6}2>2*Vfnm8H{7VY7$PeO> z+I^J(QxPRJ+=skaEGgjiLb5t<f0UZLtlrr}{|Do%Rgs+Sv5zq>Pg*4`VMp$k6P<fx zyrn(V&|Kn8r2Prr6nc@mPc~7^Dm$z?UY!pcu>c>D15e;T51=z)GIEx-h-U)2tK)cf zCsYAmJOqnlyJd?iA0BZD09FoiI|jjXHLrtS`n{clEV3hzoOK~s|Fo9|O710$F#i%k z#>&{wE$8j|GJp%w5MnI9LTME=cB+V&4y3WO<plU1rYNd;9HLGhnsM*&T}@~#%JVQf zU47-d<`2)F5n$`7nL;&~gVgd!Y5tHJ@l_jI?1iiEZDnCJJTs;-rulqBvib`?sOG%R zh<aSV<B;@_w0_ggtDAG`5TQ12SwgZwbI35w#e@!FhK75(i)_A+lkJY;g-_8fRB1SN z0}6ifrDBbaNh*VWK%DL1*ZQNkL!}lmAw}tIXOpEt9v;ixa)&<=!^J9&G={IF!Ysr; zp7>P%niWTbGZ}iX^gwGpoEIzRq^ZtTMy?k(Th}P)8#wcEiua5>uq+%J+N#=KjP}q= z_v;)3=irNOd(e86Pm39(9cq`VM3^ykpPSE@zu^b61%$?vb@6ns<&-W1_jWDFAwPl5 zYzvv4N1bJO_Trc6clY34e@vETZYW80p;*KvEC-)pC(tRhmOTfViZYNoNXh5RbI%*U zg3z%u;J7@Y@veD?^xQ6MK6}GA1GxwSXg5)XgGk;ODe0feir+m)N9R}tZ(OtpGb~19 zwD)I_nx(HTB$IG$Sm6T7AMo|oih`C9NBt*i1r-udKYygzzwsmb)hrgccmD<~p{6CK zH?5)jTDP41&4-V!3mzQ!If7vQ9H-32A$^>A1WK`~^lc&eu|RRyvfkR@xjY<kr3#pk zRPf$O%lKyD{s~8q;x|F<oVe?~M;+7Krvjmh1sie`sA+&no}WGJrBAThm+CC0khY3X zqtWJ64tbO$8AhJITZ-K}eh(vNfpg#*q5M%8odl1BVcM*59MOSROrMIl#E<A$_6KMc z4?VdWs?+C9-|Io_T>k@Aasq^%v)6+-R$SSE2E129PD<{R2VHz9#YbfD?b>xmbz&yN z<#g0!Ri7ZO0I73GEe|aZ?8QN)K{GgGtn=*jtLEhNP86IubMT9-!2>Xwi^^qy&w~$v zqjw~5!4KgOdN^$f}$2$MFcI)}GEEmD=UuGJR#{-)>~kxJF@;&|6$^@7$a8(?=q z|C)**VXl`ag<}SZF)S`53aK<d?_9_kg9h>iNg~X;IGwUvjmJYiO*8_NEX0{5sKpo+ zVHHE<eS%1Ip6aE2Q<3Bs7RPNOm+kxAHn$&(g8ILC92C~cXHu@Kpioy~N<GljxhG1% ztMXYyx_ygG!nN<)Zamw#H>s41y158tvW(YG&Z7=ME1pt5axWQ2R8M{CegEu1=N)`z zjeDb%BXklBaB-j{{f+~@c_1$<1=z>j(3iZ+<r%)>la$3&=VlIhvsLAkzi+$2^un{7 z8mf8Hif>LNh5CE|($p_vlIGAS4#IUHg4Zq*qdLdz6Z_o!{6We$_{}fDQL>C!1L?i0 zc|uAVulLKegn<ZJNG0|z;VQR6zZBF_5&*$1Ra&0Y;=iE|+J?e8D{T=1nsF%fzC<XL zv=9n~Yf&fXRuUR_HZrz0jM{u(B_$XtLZI1OmNpEncFI+O%B^8wbm<%zYL1sxgr6OO z_JQ6Dy@T*&TAL6wOV0;W8~~skf_kK)>NPFJ#hT4=aGLdnLsIz(uUBA>wnDcQ^YFx! zqna>N+a3vIH@iy)jqLwMiaWR{&Cw4P38tdXm&wWSR{N;UkaZq_<QUp6t5+3V+<&p1 zgol~D2TQd&zDcO_GQ2R2zEZG$C{twrwWdeNmhv*rwd!--wQzOnu>Ki8Jwjt_!+Qk< zT`4}O;qm~LC1|NV;%PhbD>U*PYeY6uR1iM-0L7Xr@QteBNmH`DuDiUGUF{lv5JqK> zgD6kImRkEB7^x7M*ca7AM>dl-P!`r6qx>-t%@;fsf^?LD0FzAgj;m>j$_-uTShird z<*C!$n^v4z^%arSU#OafqTiVtNMnRq=9{G-i&4Y<bXFLEXKQdqqafLTWGesv)Y4vY zz|k#>Z6~hUIV!2a*?uG_RzL;{ApmPifIJMnFtpU-oT>S{-&D?ctv(<H6g!0R;xBC{ zglz|yAb>XyTiq)`-_o&P%Iuae3kzIAb<TVjSu^=fI)%#!t{}0_im)CHCk{*MJ2}Ee zR?ORkE6%<)UhBJ@i(B?fkYs%OB+knua5CU*KNAf~`QVsCnE1VfgqbXa;hjL^7bO)0 z?2=2lG$Ub0<{D~$Q9<spV+H@G+}T4TRm!J^4^xRQUiadPfwWiflr6sMT+u9th-7ZY zU68egR;)Vp&iESz=XW~KaOyeh{BplmF+FqwA$0MB(1nP;#Pw9~oW7Tz41w0U#`Zu= zl#4_;M@qgCJPs31;h}9wMfWU{qv%BlQ<4eA_&drUFLD1?!H8f`GaZJb@8|eM4Vvs) z<8Pb^e$W9DfZ06b(pLFGRu>ONk4vQJ-9zYCH1y(Wr-x^k5NI`)*`eVT!h9b7H;jVB zQu&6UUJfMd3L%rM2Fr=vy8=E*0&yNc)zjT!`+wn$y(ABfA0TfVjXp}dK99a6S-ye+ z6=;oy)N4<i4)#}+`13<LKR!qafnFMo_9Oj{@k^oTRvFaSeE>7PcLd0gQNJ98W35p} zs$1;}`XmYd{qQObFh4mFN8g?o#%ZO{TMI{unco&VhjlG%!c(42ZVa|tm+m4qmwEIX z!QrEf5`3#V<x|Fouh5AT5x*SSkPa2A{>nkZLW<IeXRUd7&voFX2cJvAf4*Q4U-!AG ze_Q2+ohPqs_qTz@m+e8xB4avS@zA>#T<rO9avhHT2C9+7;%tqj-2cFl{ULZMiSrMf zDGf9xSCq#}E|S&LKu(mfE#zwsDl>1vxP(m#vNqB(^!$oizc~?B?LZJ<FR<Elc6xT* zq@M?}lS$XJK+@w&^3jeb$fXWz*Z3<EXkDN4uC&{V@)MXp0+*6x9{D@)*vC6ci-Fyq zq!U}LFp!BO49~*HJd@K;iN{9%pLzgYAsd4AHS&bQ?`PHs#cx2UON2y@9SY`cqJW*< zIQkM5S9VlhXti4V(bfE$DOgByi7+kTvRl?r)rV+FxhtE;&38dU+s#n&qc3XkmXbQ> zbP2TQmbt<~JHJVp@EcyZBmh}RPpgrn9v9Zz_)W-;@Kl+x67;g`R^4+7-b)qP%E5N~ zG_qyf{rGFxd}0Ft7C01a7ZbAUeWp80+Z5^K*=SpCKZ|{BXNLg$T4}K_p0!aqXDzv< zw&4l(T?T~L{J}S#y%g?{+YE_vuvpg#*q-VVy?fplHC8^5iae=IpxuO+WuCQ>pFaG| z!n0ZU*>{gy6a#AOgaNk<1;|5a7np(;p@Wh*Ne;FX(#XB)T`ci`fCLy`dxCrPlzP9! z6}j<uK;cKbgZ1ABVIG$_cwCBe#Nc=@-3}1r$NwKOR$B*U+Vs-VeND{WrauC4-q@0V zoJZf0L-w=t9o(l-0#K)pEcBuxq~Vif*pENN9dV^umA?C<{FlH@Tk~QF=!{Uc3RGhF z8!TZ+9WCqWn$zNomKW5CW2T}+06?w$#*nl`-D054*lo%bV_)=cH?%3u-bY||9H)0L zS@VF^<|er-Tt}5d-ETiOtuF#RFT8B8I(<oT%zQTxNY`OWKAO5yNCO3>OTrh^bBi=) z^3?w?v;e<W3{o3Pt4MhUt$nYdTgq%Tj<RLa0o97t)F3gc(i667(que;#)dx$4ir!` zphm%K7evn=r;3zTfP%tph~0DO>%ZYJi6m9&F_Mj3AA6qH>&S;R9MV)vA$;N{Bp$4U zNz_&J?#vT;I|+C98)?tOu@>bdYRpXR{KmoN4~0aS5%4v5aFwP$LMOx{C25$djyCQ$ zzl1l{fu?)YK@wD@`|RAB@C5#>&$pYRcX5)xLe(TlBC|dMk)6n#HX)SjZkQJ-da^Qw zNjI`B`#*eJd1^UQdmGyF>;$AHlvW{IQaSR9f_~7Tdl9qHRL)M2A;q}VaN5u28$9JV zS$H-)ndYQ#DM#}C@jX-d0_0n=akIP7g8>0&JfR+EZ)fl9_dksW-^y}cFl4adD%kuh zz4#LVJaOxwTu_X@B=M9SIz7_aA;jF8!TLP0KKB1|*rBrn`dNRNZMb0{e+rod-n1T@ zMoD8Ctnc+qidz0`WqRpF{$_=5tQF+@L>LcxCO+IS*DUy_P8<|M9mhU4G>=xUey0dh z9{Q>g!kxajcAi_!m&JzHL$H1h&WhuR9Iu<n1KZi6`w4)H++7<U1g`;d{5yMLrlJUQ ztBdymR#kFr$YCzQxQ-*5YT^7JrB#SKJ9XGolkH%t16W~4>Y1v+E=ZjL7d&Xl5l>!f z+q7n`|M54j8^4<*$nwLHorWZyM^cg+&vpS=uwtOnYP?np^RK@OvoE&m62h&ork93A zRY?DzFsPsiuv311G?HE_ZK{v#h`3<;-WNRw&w_?0Uomt@x*VB%Vclge`BBv2R}1Jd znXiVM0vaOcP5lJmISH>dz;mpb$K?=I^YoW8?9Gv=oWB`Eo@yme)p7_KedJUjY;V%E z#c%MwPncSIvwV?X!`G0VcU(eyQ~WAFM^{yPZt*{%>71hO-+OGLGB6{4xQP^P?a!9G z6O!*B(bKT;PK%vhWyGD9-eW@n<4($rtt%6(wPJVrWk}6EbeMl{5`P|~h<>!-dLjC$ z^=?6CScy2p-i*@nsvz?ljRz<DYT@N>lfvgWQi!FJW`*Y;9MK6Wwa^I(G8|m>)jiGk zFOp48(o|o4ONfr;m%pOZ@<bHvvLKD!9RH;6v+<LnvyQAGkh*`7dw%dsZP*q%6NqY3 zjVOG6qya)d+CDMM><b-yl0x2%qq7<Tcufk=%b4kuDIL+d)L}fhI6R|y*-Xvnp%F>L z#l@!@FJCrHa6FR$ZDlY!s|WIsiro4VYZadSjy%IOjYQ@Tug?=sQ2r(~qFC1L*lJQ( z_Qc6?<mjmbBd<~iU(g;8t^QilFd*0B<=yz2yal32nASWVB3c^iblk;Jra#OM|B`Ns zqr{&&8J=NNuv3n2-)vnR`|X2Q3B(+|sVIw<G6Rz@cUwKZ8=2mX+52S*ji4hBp+Z1h z$Rr8P0)xEAb?CFilD~*jtB-i6mr6O5^T(4V?7d$zj&09TpK4E)FEw~PBszE8d-DC& z%D9@swu`_^n6c7bFI?`cH4mYN*9;LYZ4z*7-FNTw8!Y*ge9o&Fvc93aPC1USQU}+S z8!Ekt&D&>H&=1NCQ(ba`w&I2S4+yNmnd4vHzLg>*)J5j(mr-*S=YPRG;p_IHrKXyL zixo}2EJ~mk=uDk9I^gxFa4Cm!_m9_GywXFzo%9sXk_rpT$=1ZPc4lO#&SYKUlQg@l z!Nn@8oST>F$9)#PmQ23nUv!?xIoTuaH%lkx*P&6J5W_cNM^Q0nlE+VHkK6NC0BHgQ z_tu@{Z8_^^df!cq45g{pc=#f(0l}}{`ox00m4s}IrIV`w)ARjU!wB!K032Mp6|V68 zYuzLH&OA$R5np196unVCgfoGe>pP7*^Gnfv^sF4(LswKzH^EV^oqB(?m2`&BOM~29 zvQ&==bbJ2l9TvpM)GMeUA`N+9_FbVO--crIDE^JZLwl?jfK}|8oC5-|>yda(OQf3w z=d&#whlDPTr~hHKqaj;Q?vS_0Wcp{($b5A!x6B?@5cJVd^$fi6+>w{XA<an#=aDZj zVta54fXBLnBiBug<eU3WC2Qy<4f`U^`mm|{S{v1`PphJmvy3{d0*9hM*T}uLU5!7z zX7?hv{fYW2_s^nXR<4Tl$!7VQ!dB#^ESROw1r!e}g9qSdM@nm(7co>?fw)aCfU4Z! zpS)6l+&yvn=X&`73L?M#jR)Ml>miK=bJThnO3D!zB&D<6-^f{tKszSx+BwLdy|fbo zU!g~@^l81${tVw_bhOK7?YU$U_azqAdmMoNv>`J5w@0cq>y5O+7M^X7KTdG}<=)q8 zBQFkaA69Vk3FXdUmj4-PdgJa@VI5&u6`{iJ`kV_v6Z{T1)JRNpp37K%Sut(a$E0L@ z=A0io3F1tHzI34J&Vb@`?f%&aBbK~-r?AO7Q~7VMXAhKM{fsAfU#nTBR0$avIQsS| za3`0c)EA*n%B!Rs;phVOIl`tzd|o?il>oWy+OcdW|0&-`LtQ~0Lg>klMk%v`&`&Jc zdkD<RCVPugqw+80K9%j@YY;-<@QCS-yPr1>Aw`O#baZtRtX__W9UFq@Xq?ec%{6VF zxr{Tz7k0e^mY*1TVead9|EVfDJPo<G2EVS~pfMD^DOSFSp~%_%mHHf^NxCv$o(^8q z)H(k&C4OYs()+4iuOafd2}s_vh}Eyq_jo`$*#sw|0MjA$j3;YoeJr1grza6?r|nvN zYMMz$(vby*kT8M?lrtQb%%V>t=)vCoIKxlvbDomCT*cSf3YU}U@ak3jJNZColM}4j z45XR?Kf~R8aj7U(GNNI9u-%22-1wsw0v=q30IpN?ar8g@r*@|*O(jKMW13kxaP|%# z`}A7ttB9h(B1XbSgl$XHPl}h&(H=T*5#Uh`_W{rs-|#l3;U?~(??<k#XhYvddWvIp zwh!Nih4tTlwh8Ho<!ce9*9P0c`z_9&B}$4@m45Hph>ed@{wRSOBjZEVAX@fjJj;5e z>hBF}^syV};7qJx^!xsW@ayLsRdQ@UQl*1mD$-nxdc*btOoLxw*A(gcysLpTUVH%d zR-7r`Qs!vo9lF|z$UU!xee}83&8VD;QcHfbiNdj$a&lP(I~S50EDzJ!Yp;i64Jh8e zBY#*OMF)FEp9?_W9EW9<jQW=o68`W`@3bOaUMMH9A9oh%{Bpl`UH?%hu6UY@d9P$+ zy(GpsJ<fK8Rh_Q}o*7L!jomMTz)0+_yJUH6!Fk#P?Ie;<1aeCrz*ZIHD-YF4V;to% zLaEi+i5dY@SNVL(ga#MM@*{1fdp;cH3Pz4i<FP;~9&3}v6PW?Q`jtomkUxRV^3*@m zE+0to)-HG+VSx@kn!*TLuvJ+#f?{~~cH5dYfeYVzOc1}T1@|p|_HlQ4vr0#eF8~v# z8qAf;;8;#jZ=9)$=G&)1nO4>OpI@#q<1$jXtJQ`ZNq&TuBd+f)$n4PbT1H6N|5huk zdLf?eUBdh#Rfh<F<pgu1)nP72`o1KKu(2jh=+H=W7x#@9=d%{dJKP5(Kyu?;yoAt= zwijjWp4)@Lxs>HbS=FNj$^oeF58`ehOVyfKwC54)m9~{By3PP7>AVQfdg~<2-GV;( z>U*4!P>hEWG$O&BDN6c1ZgpXe1=1x0KkdnFg83a7sNW?7rOxjT4vds`{d@D#0;lcV z#1s-%bS!4{`#(QsVRgFkZ0|m?qZ(Z4x!)JD`6D>nuruh~!?Uh+?lBNzRGU$3{g_92 zxmWTaj!ZG#690KgxE<W#95@C2Ldf!!k1Ggng4~S@vi{0`tVb^Isw`cF{u@YiExBuI ze8hL^s<ZFjVUJ|!#zDrNu(Db^iQ##rN~_|0w-}DSI<-YE&{dlkaMBC#^rt#gL!Vi{ zW~togjx>*lL<d@>g_w`}GQH;0Wi8=ujNo|ko=N7T;k#V)JGjvivKeAL!rDR_{vagZ z>Rlz~{#;0!(+hC)Vx_;$qCfl&b&;+&&^?E&7Ew~cE2{~z?&K9{6#U7jQRG1Z@*b<x ziB7gp{<S#F8F4W6X!Pz3^j&s5xgxDaZl-b}t4<!n(~JGLpEL}2_0>R0v5}O*NI;ve z-UkTIcJ6_PLTFA%2(PY3!2(phrr`E>D|O$suBNxuBi-xpi-`q~JDq!Q1zAFPCXmoM z)meN#Foj1#c%!bJAvJf*yYbbRjPu-awXKn>Us2F@1^}8Q#GEBq?(z4k?=sniy(&uA z#8Dhf!62+`)jG5D=!cCcQ7S3K&{nKq1O~+nF(?l#8rSG<m*s(DJUq7XSwf*CSBXqZ zOW<;Ddk|PrROQIVxY;nyvZr1&e6TvPfN$NBOqDzl`!wy#dHG{pHXIx$o%qq$0A->e zV>(_L0D0EjJN7x;z2lzWt}VFzz5LuHN6PAvH-_NXbr{-_*P!D*E@B@+Rx~xw+e+?8 zyqdKTJ&Z15vExr~j}V<K=t7qq-FW2H>`HW~0CdA7a8Na|pt4kpqbabgl@e9UJ4`YU zZiad0U`tv-rwD4=5Jv;dZR%NqY0kcqB0u{dFuZ~Am((P6CAr{|zsoM}UhhvuDt`=H z@G(wv2GX3xOSUc~ZPSDqvV~=x&i;v$&4FB}^5E#^Bd_#OleQJl3N!3CGZ7?r4<8t| zF51eAx$NYFi!*iP+zmx0n|!d5?nsN>HeQnzxF#isZDLP>QPdO)nq9b+Z@QhT<rP|; z0WLLf@au>6Qz}u#Yfvzx%;6}Lr-w;V4Jk1~EJP`3+6%(_G`Rf^#4DLKyKRwse(+P? znH;VNA6VvsvZnxK4yp~EkHBTdOO>61v;_+f7l|U;l3u*b=n8YuKuPrx+9PC8Z#lrO zGjM9Iw%Pnva4`pbOgb^}L0+x4xBBh(EIo3VD`cmTg>P4Ai{+<rIjE-(H&F69L+$eN zry?48kroSD9o`I`E_&+8vd=~CO2W@96Rw)gV<}$|$go-ft>BYGwuwg4?n_@{ULZ7c z$QC?TOg9NL$wjh#$k4I3D?DV;MK=c*PygHLj~o_Hrr3_;YyHa$W1kAMQZhztAHho% zuE<4C_?gRtX}}`ZN>T8H1e0?f!QG8nHZ$EwD-_R_Q9<NeI(-p?Cd5*`Tq$%}Z=e%) zg={M{QE>-4*q*MA<>g>m?>UJ1Dz&PM)?|oEMm@kY++^X9xEt=Cm3Z1Zxuz-;NV&=Z z*s}m|*NpyD<(KWg``z=da^oy+JA)Od28@JU(eUiAFl;r!{r!rGyUTcHvs@55Z#P6J z2R2VYDjhjx>JUib8Psh8b6_erR|$Mk4=xVyoZ`Kw;wMO^`0gNkYfn8%=SG`1&1V5q zdQr-1Dltr1lqcDm^^2%lZc76DAM;=ksp{U-nfYUnqgRCM!dS(i59b@PJYKaTbyz_^ zz{H7?DoP*~D+i#pT0A)bPk*E`1D2e4yoDPM{#?I5&NQH}4LvH&>e*!argT#aNv4Ud zX@<5E4+!<uf{QoG2fXybPMXe_2<#SO(o4*35>gcyRxT1~Um;L#4`Ep1rCD3)aPvl# zAvsnjw#>_-t0}<+tN><Q@|Q<&Bk9Wp=A{?1A(%H!F2cb<b@K~U9%q@LTsf7+I_8Ex zHB>Ij$m-DB{`FB4wA&Umj8s>x=JBOk6jAdIB7GhCfhso=ziD3Ru;i;L+N5{|M`0EH zqFs`$Z=p(kpt)a0wS5JzCJ-(OiMjJ(X3%Rrcn+1_7T^y0$-nZ&jD$Eisx-sOZOPo* zY@W+T-iOzV5Y>0IMJfBp96;L<m4#(g$wNR^F(yd58it<8!-q{Puh8CH$!S6glZXVx z0A6DWY{<ry(dT#M=v%C(Ho7m(Gm+&TF-o4%U-<mVRy~jOLFBz?_}zcRxl7=F1$D9Y zNlJ|AYbNQ3Ekv2%Bf<rE?p_i|;Cn4G&G;-K-6Vmos!bp2J`f~?qS(ZeNrO0>q*Mtw z;u`Bhzl9qJbeG`v>!Jx9%BExMHbyE`ps#tsH4t{yy>?SmSXG3H*fk<%hAHhh`c`#v zK+Of_?iL(p$Iw0sH0noPzB?4b*`y=R<`#JNXYrbq;0rCoAmJ;X_0sEf#b60~QuT<{ z%y~6TW~G>6zQ{3!V8ODvg>*K{8c(*@o_cH>gs$cYr4N7f>u~lH;w;$*4G&ZCEF}Cr zUUw3E@tNX=r;L3(Yvq^M0__2u6?@H6uFIl5U%$seWq-2tkw)q%96clFnWN=`a<cu} zSMGh}^{h@6+o|Ux`zeMgNY7SO5LQwp6%U!DAF&Vwri51WUONC)SyFx^`l)Q&80zt9 zHx^tA>WgZZ!z@!EPEW_{W7LC&N2xg0h1^@t($xzYZqx3Z!P57iSxj=?G<VZCQ;wW? zGLK$@LMn3le)BphoWWrH4K`yB*+MT)ai+@B#+0%Rd2DQ`ZNc>xG|{wGkTvSU`sev0 z?~S9u@y$QbU0=WC1!Lq-c=}(xSBXlpZ9V9|V-7}_&R|ex2bQU|uiv~2{6*ENqt-la zVO!g^C`em*i(GE)!Uj4)*8>Ul98M@P6m=+ZhPqU>mR0fC3$S+91lsT#`Gd(xllgRR zO62DDI?e^rBd@Na+6_5d#DXUjTdrTju*wy-eY0>&uVX4_Fd7=yU27uThPiRW&y^|h z<<m4>tYF11x=Wa^t7}Y!GW%F%CwlP@X%Ht<8kymidHb!N<zQ>K0N#tBTZXz7Gy#Mk zBBd=CW5P{3(x!XZhaXsPaAeVHTH7-ETUR<N#2&X-zZ%5C9r|<1!{HuS^j`t?o663b z%murqSdvS3W+lJOW$L;0p<9kf%h)}Hb!H5&917ElhtsZLBnk0b(lM|zR(>9p4NSUQ zFE$by^|Vk?AJsV*IMs;kSClCpTV~v}5G7@2s%+Bq+HaK~fu7X73Oax2JAlflztW21 zH2FQCSzTPj`V@N}z3nE#ba**ladK<Kwf0_1JFYmH<wK{VRy=ljlcIo|ZGl&E`}!@q za1?82*5&nAxXJ|xRgLa*eUjI`5;T=)znIzG6edak!uqoDOmMM>=N;N&G?XM?1$r58 ziD~x9Y*$4dqJiIi!-&-t6g2+;pF5>g7x~Z);kf{@NS8IL+*}GI5vt#Lhk|>4faW>P zP5l4-=qA{RAe1Gm;tBJ;jTu%K1z7B>ow>a1HM~2}=<s~h+;LPm4|zlf`E9iBA5Aid zR{qHO$T&0@ucZck%5k=Y&kVlk`LGCsv9XHJuDjA~eNj}20EbsiUkf#{tb8F>+#Y&D z;9yL|DuVmWEK~2Y3g$7}tqZo(+G-NlxjAnTU9%@KqjY8iiYlKt{uGUvwOo+-CH8sQ zWz31IUQ;bw_`I+!6VtIY5Lidl&qz^XXwiKt4VO~Y+Ex_vfZC_-wnPcjhntT-SbG%w zd8~&Q`dp^>4u3%w!ZWX?zmPP?(24#APv0s(mu~Bgp3bu=x18MpW6@8N>P6B{2aUa& zpK111Sr%-Ur#!U*qf2!6**zO}+<3EJtStlE=i36|X_|pAA8`h{Nkt!X`Yi|WdO2GQ zP67y_ksqF2F;b*2rSEIv?DceEed389^Uxc~_M%KBazap&x5@;F&ymsGeTvb7*wph0 zbpP<otPIIn662?hMx$n_IeH4=4HM@5Pyf1Jp&|JDsF_WRL!e$xy@k}3*YO88WT{EL zdNHJ6>#X+RK|0ZZbvORh(~{J+>Uj0WMQc{VBa6TOLpU+fe#=<kZ@=!Jt~XUz40QZ_ zj!^M)^xJmz>hXa_=k(zvOq-F8-sz_4%JG3!!Yua0hv>;<L-*_yK^**V{2ra*off>A zD#jEbuM%XxEVCVob#V*Tr>eP0C@f<LdEto!8O8%eMt_6Qt|KX|Ln_+HxGjp2k(wnZ ziTC=?aQ;AubzT2lyI#}45^sXLjWv0C)Q>Y~0ZeVGLT$_y={9jbpnr0o2r|0ot@qaq zp}Nbx0?2<CV|ub>nOer0g}db$&bH@W(SIwzW(kt*Pkg-9;E4=6rk}3jVzh+G`LAIU zD~HPD8%;T5WHy{nS{Nh1zRCJEtX>WobH_dUUPdf>h_W>dgEJpf4DP!4glbZ?vaih4 zyyn}bGF9vNdBFw`&RXpehP^qG-m?TaJPg+>qMa^z^vBrfN=Tm8$pzSQ|22`2Kqk1K zlq~MJtx7zp&OKX8!R|?#sX;tY6ggcrk<hW_-RR5LJyyR6vTKu*#WowQC||^-E8M#g zViin$U`EIqx<85Ju;dShes=mkblrd9!`4nt<CizYlI|^KUQH`+h+AHbo=1qKF3f74 zI5u->!Z9QBCU@}D3`UOi!r%|a<0`lD^pvwdt064rutSxqc2FetaW`^M!b!R5+40?t z>rncwm0;>QD*UlljA`O*Bty#GxQcUrL<Xwm8M5l7s<~&7aX%V2h`Ua^t1qw+uC#fk ztd=f-<cZu2t*$GdD^vx+fi?v9S1ZCEcX8Hx|5*03A-a<=ZK|a2W4*eiVnhO|RW85^ zqpE>Ff%bNhsaCQfE$0&VTv-{6HuMMzJ;8GqJ5?L+I$a3eNYx4~V!e%8zu_V#OakoJ z!FDZna%m;S&t9#OKvw<BWa2}WSc@3Z6kB;G)iF$KzB#(*_lE*x%G*~ZCTkYRdJ9iE zxZz~p1@6l4^xw_Ks)u9KdoE{`XGyyIH*v<Y6MH^v<!XQzezwB#pg+T2h$9EjQz<7~ zNTR5?>Qdnu#GTSH!NvD;o{d}?J&GMwtv*jA-1y4ic=`tpeSXi8DHuZaPSTSw|8J)% z$)H6}%!EL57Iot(q>_R6;I7l3KG(*kcW}o!Ncfaw&>0%2+9`}FF${uot&t^nEYnry zao=OZT$4%P4TH=;?(rJz-cQstz3@cUIDauyE8U=Flh{52&0Te~wzDoO0JJX1NGN<g zUoVTSx46(dG~*CD=7|XF>H#-4J*zVFw?!<^;$lQxKuGC^g5*6Zck+C=E9Gf7j^CWv z-6)01+8>h4rq0R|pWL(3mx7C#M``ZZQt-fZl;|a^+=%vLUxbn6p1ZY`W~4bPVmwk& zV1(GumsU6RG!==kl<B+`mjGgrKkjL8Lq`3$q(R(!HiRpP<V6DPfATqh-yBIb!su`C zWz#{CI7{x|Z)_GLo<$F}co8eMTBEv0k4kEno7s!~eNg9cyh!+=E4}0Ct;iaL6_4Li zzPNtkiC=^`Ghs*VXrE_g_E~YTT-DnTDPve*br85Hxu0ej*?#I1=U|D6bVkl0?&NyQ zq0!z%QLoWBwlUbQak&>NtYXaS1eIEQ=f^Fa1#f4|6cn%L@>AiW)iZ-<`8`L~UF`(f zw@Do+_w9thMq0Ew)N>d4IlAO)6r8OarTpQ=%trC~AewgL(b&9TyIY&)Uf%9T6_#8f zNG5LB(F};@(#9QrocM=rW&kF$bhv9=cqH0+T$<5ubBQF4*$C6SDLLyDvrloDeYnn! zS+EB^*Bbbkf-`r7n!7~ckC-fqB)>F1eOZGeP=Ci%lnyn5@V`W%aoyM=wqwe+mY(%g zQuIdpsNWNGCE8f9U5A2hu9s7vK$<2(k{>h%_q5o&k>yFpDn^)&rLjY9r4i73HNCUe z0zG&=7#Q`^sfgFqh8S^V=-$I89y&ijCFHmOJ0#YbyC#^wYm2$cYvoUHH<p=yeTh4w zCX42-xRQJ7aJXVy-81)P<&kK2PX&fd`eyo{rK!hI?WV_oTbwfWa3%MIsUZ8!W968p zT@qr29En4QIa`GXjQ!42H25ydK>gsCzOJ8CxW}^?49(C(Pq1tWlN9$kAa*>s_}ThZ zE4bUB^#m9HY%$~5ufZXQMfB0?SU@{2yET*R<*{A>F=mY@Q$9$)ezn#EJf*L;Rw!!y zc@)1`r^K0GqCZSaB~1ok3-|4wOfcWT6W@*04pzql?e3oK)W$q%scIf-oDySd43<t6 z-b{Yc;Ee3y$2Z$TGiK2}-&n#dvu)SpE_%l3OcYA*9AVZ4uhGkv=m<0L38F9Y=Df6s zm83GnZmmA#T~+BsAl+F-+0PXy`Wiv9>jpn472sj1;D^p?Uh}4?+WsQMx*C=@5L<OO zHY>P2J2iVMlqa}00_4hn^MBG~2c~fe<<JzTL%OT%ML{i(o|!3C(mlFC_I&RqYGcld zl1*GuwY!R^uO-gJ)UQF#_IJU=y^IchX-wRbRx)%N4mmbdS`$i&@~0kRrxRi|8~^6= zCUSGG4Ob_kl)c<B!LcE9oPL&cMmx*xkN~^hHQ7q-cZ*G^wXm`!xWBoQoL-NtMtGMz zJ_{|^#C_-q{}y76dWTlh?p77W-U_xWB!9eRAdl^(TY({4o3zuJdp?oA=eZv}vGeQg z##4?JjKhLt6VEdx=r%QkSTV+DW>Sy$r8k^H7W`3STj&r4WnGOJQ=SwvgsfEjWana3 zH`A2Cg^+R2HshS@%=L`{-koAR00A|+ac!cns_2pkt03}5N5UpdfXX!>qB;K{CNo*a zHjTr(9j{gtxc`7E6MP13s+aCupiYAzKKJZ>Q<U8Z-=22pSFfPuU@Nwjd`&c7a6T6c zvqlGQRMO&yTGRXpq}z)Fe?~c{il^%n<({D9YV6+Ujy>S*bguxLVccW455%d!>-Bl{ ze=*&B21J=SR<xE#4vXOO{PM&-S6>v64{QS|Y1(!x->L?nX!&t9l&Q3vpoS}3kH>Lf zv+Jk07E5(&Os&Ir6FU6--d>2VD83}jis|XXG`ju=M*p@_b(5yrP)jZYHrIZ2)j}QJ z1DfC?6*+Inq9x)fe|T>B2|brMBv>gfao*_e`W2Z0CMk?BS5U4(M!|35L&IgqZSxzY z!KuD11{oaGZ|q)<EV!=0iNozf+x8Iq;C}Fe*_(FMMh5lUUYh4yZLQvz4mxCWX7a$- z=-pO!D84_(&yK6dom(l!yc}~m3|j>xQN5IotpZ0X@z2HFZH03X9i^y0`H2fcVwNk* zv0-uy=h{-gBLys<{_V&He^3JSqhXZSrYbg%$s&u1GQX(42#;66Wb1$)RVyXLEe#zM z-I{Auy%Ia}9tB2%-I>PTK%W#(Sd^Z-^F8J#^#<F;N}Nw|>H8B~FM4;Q47!^nIS3h^ zbx$-^lmZ;>t1OjV*rse<QbLX)R|7I9e(UI056xJHQt_k+D|YULX4aDdJYD&4!O7Cp zKM$hB%D~e#)!D{J{vTgo9uIZ?KR!d~LM5qOyIWEuU2-$KT`NUNsa%z$a+Z4-w6<)g zPYJoF1M9AY5R+84T!V5mBxWQ>8P_o8_k6$S{T|xw`}?Q#$outrzMl8>dOdU5f(`p8 zG7V$);EW>aNm496+v{}tM3XR?`}wiPL4j{^033?mj_UMU1Tm!8Q;O}QPe2MLF=0I* z$&$US)2<ccdt?;C6tk&lj2o=Lr0a~oeZ|sa=%7aImC+<UHzMT?PW2&Cc8o%FzutO3 z5>0Qh-WooWDKr2-Z>c{W&gbXVh3?F2aLC|U+ex}kd~<R*c@*BXMReHRllUn@kCC#f z=Y$?hM8*8sS;f!OLmqfgKt7W|GJKL~pS$I8I<d&5@(9NaL)=|awo=M1!$*9)J~mT{ z1EtxfjKLEU6i;y*)^#vkR=%9>7=4X_*}qUbQkJQhWb;G*W=4|Eg{KPyYVRRDewLV6 zsUF>Lwce3r`(94(XKcMP<a`D1Rrojo`SA?FFs3KO*;mwk_nJNM^f6;Y!#Dxwh6mXs zNGLqn#V34r=A(>&HG-|+qF7g_n(VtxvgIH^7GknEyDX>2++Ia<#W1-hZt$}dru1O! z2N;6Ka@miah?D|aAkLRCG9+QXLN2yXUBDxUuX_cGYpyWo_t8#gb=5Ne^$gY4m|#Ur zK+2Hd?nXc(cBD$DcmvF-mb8Rm;+7g?eS`mcW0LKo`|KwoX;+mnylaWDIUYmSj(q9S z%%*TKA4cB;Od^9F*&Th$ejdxy@oIyKCN6Rz`c&u+MtJeB2NnA*vM}7G$CDJ>FYL=d zed?$X+1Z$m`>M&{)k_2%zrp9GBstRr4Cs*c)X>wCE=A9*+@HW*s{_A`XJSjm3Xu<< zUhj;fZOi*MRg84W5CfLV`@PdvE?-19X4*!~#T`dbZ;o{(b!Dex4)K*^7$h5-_uebS zOoBq1GQE6!l37-vL)A=Ipz!tz*zyQO4{OF{F&&~AbuT^+<Mpq8LX`3k-Lc`>5{TXj z!~(HUa4`3UE{oitW_=u&lJG&_!7BoGNei;sYj~lnyj)w|L7pz7yvrS1@%%iNaqa8C z$Fn(|jVBiLFB7>pzlByD6=h#a`{O=_4VZ$wFG*{({X?QfV-nwd8Zir}Cx-?6XF%ji zh3HQ2&UffZ2h<t&TBuuVXA6MIg__EI7lG1x`g8(ru8av|5ItVVlJv4VFp#eeK<AOa znBvU4gdS6ym}i;H-|}p=cZQY*&I4s@sEW?IPbLz(<72YtvvRI{?8gr{mo?Jc5rh+g zy^5<|y|h3P7g-0=VC&H<O|Vyh?=RyQXO39cd`t2ewwIAi0q+xA0DowZ!TD{6PI^`S zLXE7VIoWT-g=GX~tuq)29&(4&X#ZR1!Na09{Y)aIG+6$c>Lx=6IeK4oXz`$rKu>|Y zaC^n%!5QU0##9yr`;X_c438aap1QxMqI!iGyE&q!8uQV>+%zNwpPZ$}Wuk)H1}l~p z;JKZym2^${-bSmG6*^rIW6dpIg|Y4k1kw^Z8FiQvpWmiiC5Ia-yT$aZjQ0!U&ZBx5 zmz<HH05sI+e`58N>1W_JD!>XCw4%>&VCs9hxC10wK?eJCn1CVfgZm@D@CYBB&;A); z`2U(j*I$c4!VD_z{k=>$JT`I5e3rPHohokj6zGu;cNsb*@<kP5R0=m=pnP6|@~)lm z=6{u=R&p5AA#26h95%}|9fwlf$!~iBedHlV+9_d9#Mq$=@l|W74Xdw;%zX4_?^oWj z6u#;{qt5m}drkm;Es?T%lGSrfMcdS2DP1OW{!QF1(xQf<=^@Oqdz4AHOjzd;imO3= zs;gG5uVbRa=cmHtx~sL0rUE0ncmYei_oIy}<|1Pu07G{~fY9!s_8Obse4T!94b}$i z#SAy3o!4o9o1En&*Y^tCCYC6BBt(~ieHFPj0MEgt5{ouS>DgnLJl=n=xWQq5TPW`7 z?*vqyKeOR<-&0}E{T*(2x)G|>Mr#_KGWhtX>NB|Ey@J5;Z-a})-HObUxhA?)+xWTh zO9bw7R+N^7lCf*fVJzNw>rHbwEkI^8k;1)|F&KAkLMJ1r{mxqJrya9hVKSva3)}Jj zhRLDaNc(SXIF(f9vrKd3o2F{>?>?@BzCX29iUsBvUcfIkI_FgM0gH`1iYK!+Pech^ zi{C}rmOba*m_QPEYpDOP*hg2v3zE9N!TMOii*x+2x(P{}I(%!x!<=*TSuvM&4_ADM zjXO<}9KhqIlMstbdmqg}*=22c#1kf<voKVEoX_*1f+MCIa6a>N|5G7O_mvJjLA_R# zoo4nx@sSN5IT0H?GW1vAa+jt=RqTD{x4{AV=#TJ{XBgAI7Xr`8=M4tqQO*ks#+@o} zeJq`3bIFv-3(b$sgRyfX8LxMS2~Pb#G4>mCx?K@Uz+ckLb=1QKn|uX69ug^ZmfU>p zu6rSQC0(i{0ZTRP-bB?*j8Dls!G|&Yt*2&!z;91mMK!#wW?!999;Mq2DNZ>&L9|;9 z(M4tH4X8#jPrO{fxck=I5c6^%c%_tfs%ch?w8mW^*~YGwz*U$Uc+6LM=U(%H>}^bI zS<uFvIYX+C5I4!Yn#+niNuv258hVQ1<PH*!1r;Z;%X8tuK?oSH)=tES#%&Uq1VbuM zt1xg_#9iMnc_}^SN#X*TSV0af1ab$-!MigWQDB!QO8SrNbMW^p#1_|>!|tq7uPU30 zGb3f$QrOYPm`+-?j4pd{mF=U`eAI*iB3#uy0=ek|{vIH5e`gL>mMx5jPo3vY<7u^X zL~dJ3ZPu|IlxO}1lesN+ivU<%s$pEfr#F$+@ZB+z=~6ri848P-iWxq^_p2D0+O>wE zk4Hq^L`t83W#>8(_Rh3F@R;YpQh@Pw))D?rYu_rg`kv5XP#AXv9>#THPEu!8Xhp+% z0MhoE%5vP1*h9+FUPjMSoxbeJ&lcSVK^m+(cmvNne-&r_3NBw(T-H(ls-WWIJA3UN z1pf*7de4-O^H@4eDaEScL$CiB&cq+Y*sJ98Dw%o)4sa$pO6UwB?Npw<F5nB`P8~*| zkq(2~W`|?6s2QLwjZ<a1uSwWKZO{ny--C(o9+3IZEZ1ZCd@=k75yJ~@EeV2hz##?t z?(B&w;~w`7$fr-|;L+}Zd90k?<FDRfdnVB-Z8MpJa51+0uzqCq3UHME_!YAdf6rlG zHh)xxeer85y>MgP;7n;MfkK&dE4w-oU&&m^y0)YQ&o0A87MW_9xp?w9^HEngq_gOz z0#%_R!7A~m6?5;pi?T&jugyx1YNQ;0zRt3M{uzbxPiw$R_*(%DM?~3G(;F+8*`h#u zW13f$z;N6K|4K#SDYj+gTki~b4HHjlHwOj$ZRflEH2bJ9xit0TxtZg=$*rkAQiP&% zqp_rydh{1>fnL(1%47vT8rC>*x6T3bbMS5hZljmbv#hMi*&`^s%uUN(MaeShE|iQ{ z!&%$nEHp)xryDquvd&6O<H^|hV34Hs&T>(qf2m^mE<T}Zj2$P4qb=-643B4Bp{1L% zs#hX(2*Hv*4;Im7!(O`{DM0~!^zIXR+CxRaD)WUo|9F;KXU#8v4jUrs0#8M(79yWc zZ<^09#2f&JAk(AR4>KN6K>O4?49Y3w?CAszs``-?*YTJD{Huq5d=*jMqn>>k75Ni& z+Zsn~Yx4&bO$6R2WXcl8$JE5@T9NDKNZnB&Y8*YtKA8{wrGW-0b7Njj$H{<28#7TB z;KzbA=>NR`pPtCeXT9Kg`-Y4S0eI9i9~fNa*;^Gp)O%7qY!Dvk73lZW`7H3W*ZFr% z?j;uava>Ot7ZU6HoOd(qWceka)p3JOTf=bnu-S)r=Q^2lTc!ZoETg3~JkEw_Jx1l` zvDbVSFzO~}nK1o!VT2Z8XUB_0LbY|R)0Qa+FwfuD!Xuecgx@5NmhmZaM)5N@+@qNJ z_{u@dC4i!x|2A>&Df{Kj@fC=JG4v-U`i;Vz+vZ<)scIuyc#&v>xOb6C+@QAGFF0}* zupD&zLY;hfFL#)LPwmqLX=14Myvcnqm>YpoW!U+(wKvyk4a2=p5QbQj49h1>jxZ-4 zLNMpp%S;xyx6E}w)oXVABI{2JH&4XNnh2Db5GShYa_7y_M^(c{!sLVHEHRApN)_mR z2Va+BrP*TpZ~FboxD-RxZK1>Z$>U|}mpzkAp$~V)m_m4}x#pT}ZZsdzb=EAuKpn;w z6~6Neb?J%T;r<ixePK{Vef19wvx@MQZTL!V&<MYPtR7{>NbLQs+H{Q|C=lX6Mqwzz zaPF{E0vNNcGJ{wG6Y6zYM$%=<yBa4BdmxBCZji3(G-=xFU%lPoCa%bZIIEdu&bznK zwpI*NYN0xJKn^1p^1Z1TBl|ud;1kv-%2ewn;wcWZw(iYxWn9?a=r9E^eA_W}7o+E0 zsI;j(tBgkZwIb(BNt{g5a2rF*8&^Lvbf|sq&b3h()i#7=`ZE<gms=`Fk6azxm+=_^ z_=VLwQ02)B^fmVh)W`ysx*m99W4lk(CoH2Qq3*^EoTUvU!M66+#-#UQUw`tOcKM7e z1>l43-r?<Wy=yBtb&v|U|7<^IKPo^8X^`Ki^1)5qdvB~j;7bEVMU;;O{@pcl;O<y5 zl*d23#@+f5ZK~$O;9vW6kli*~0Q-bK9M;Dbw-9HqEu;PGhrcVP>Jmt<>!$I_A0Wpr zbv)~^7xLNh4XfRYXmM3zbLwWHbvVm)aJ=pB3T~Y++5OQz0hr+0ev(Y@M?NGaCJyUc zn8Z4q7I<VL>@Md1vIkAv81=v2eK7<#Y^xXpD^me6ktECJG+pH5{YL%UDUr#;`lc0n zGa^=9T+CH}u$8AgkM;TlW)B`glc~qh9i(PMApHH%qqwgh_($@z3~mzOafB_!U;l}) zy;&>ob_`8+V`U13dNgPBs2Js+@LE(~PQ-_t7Sv7Q+rava1BGHH3s?&*Zr~NJ5Kz5F zGPF~dP_?CUPrqUKT9m)(J-r!9GPXYN=3QyFMs|F(8Se^^bR@a{E}fP02Hml{UdN-n z8V)%Ysg57g!k1+D{wwRVRkiVbFU45M%~(Wii1NbOGNBYy-v;ZG;pooq*2N9d5T$9` zj&qnz8<A4y#<bn>)kI4P@-@CaDzgw}?)F|q5<%3Rx{>_&1Y@S(fNCJVcGl-0gLwTX z6KV&Z%b1|)VqE({o@aZi4_$}<X?b(7r8+3wV(Tcuu0`N8yOoXp-7cOCeJyfR>e!}` zD7|H%s*h~BUjoyb`Gps7JQbxTka)+<2SW}*+~7{f!UUj8``v}p701y1D(WBm#J}?# zOmPwb+8i|9ChbibaCku8ji5-XWW&rM)0z|VU!LQCx(o5qK-ao<3!?FGQTFz@TLC|P zy926ZQ#6)V1j5)BJoUIZqJV&UHw7uK0{f0t?h|bKE=b{&;+OH)YZ=nXklVh};fEQq z6AM{6DII6~&{J+a_|~7N4xa|Q3pg-sGnKJ)Wb)%3Bz5Csh6&4csF59#d-4%wg)O&M zeh#oxVT2{L2yjXPO5UncIzZb3)Bx^Vh8eh0xHlg5D@{WY1vT^3y~&Ie|LV^7vAB+B zr!HWPz3yxW2AJ(3Yyzge>HjVjMa|ybYZ|=IsxXF&1n#RfDa(h%^N}042@M@3S8K~p zFU4hu`4d$$N1hVu$=@^e{c&#qcDPYcD}!&(ka*`gw`6S<LMFRTL7MMQJ5J8#pm;Uw zQ{5=bCjD}hHJxEz%<PrHodalm_=Cu$N6>DR%U{?GHfUD>R>{%_9Bq@Ib2tDw>2a0X zO9PUL-}GO@(}x)9g~+dkIO8LdUd0+4#{PK=y)-)YxRdaPpFZtJx2;kNA3Umm{>>VJ z$1Oy@B*eLQvt6rANg2}p#$R1$q4#!)cltE4>5CQ6*rc?eC_(3-7Ur1B&{bvV4T_(A z0-tBzae#`o@mJ#+PPmcMk!FDS7X(7sTO@r{K<B=8$MYU=)G46qy_*-E*7*g*4uppx zwUuYM{seEu=)$Li2B@z|FjRXS$lTd$<vM7N`_ctLHWz~ep1Iq%4n)AGN!sJNm_dVH zv_mO1lH+U9t)K6oyx33s-@B2vuy?a9lf5L%5KXUkdC_9Q{f`MIVuG^~qWzXVS)*vT z62ZsBGIP-RtyQ&~N`s}qIbtePU3FT&zSU~qR4vBdG4wy&If6W^&hlaW({qn_ng{dS z%S`a{pa+o>FU0wWvt2$bv`gQ8GgW=h97EiOTavPj#Y`uDN8y$Ws~YoPAKs^CFiQnb z%!M+Iu7P4hVb7`W(tNq7`W}6Zewx}+TiYX}<W5O2(1^vH;svZ>X&_ZAr|Ma|wz4qi z<)H_7y5x}v5+ya1q0;xa%@wY$`krnwO~)6PcNseKXKLf<wV&`_fWg0pixAk4NMFZY zA0#30#|Fj42mn-_940wnQ+Qv|WCgC?z+E2>3OG|f`O$yu`9^9a?w^g#A%9i$--~<} z^f_z=%kg*E+FOvJgeEw97;fJB^+olaJ<Qh=)7lF3n8SB$bh`w)p#&0AD=m8;2q9~j zN1A79ZaROJIU^UGR#3wekDoDqErVk1eKy{D1Y}kX`!yqU0rK5``NK@I&Fyz1(+Me; zu+kRL>ZUMfRa0;RZR3D<%~yD9fXwD%{C&?~NZ+#HpE8s`|Dufww*>wi-q)s4gu?}p zAA<J1)*2h}G5FRdFUcoEz$BcH=I2}SZ}%1_ZwMcx_;6<7Dki9XV!YsGkHz8AIFFCg z18TqGS&l}O!@ky_g7r}?s?X7+o4k9rW1&qJ;?{k*pdRNVROI{VATE$-oL^>OfE<^k zr!+eFd~Nt3H!mt=)^UY6H6tN9HnwVGbpUeMaxNZ;jSG>#J_<#tyT~x*;<W9)Zt_43 zWfq70L=DW19{9Tpdgv3!)n_&xNjaVsx&+yDk$Q?%wPMnRBExm2GastT(RXVqG<_@o z;9(l)bIGs#cf61WC*7{n*x5A)`S?{S;s@NGXs4;$&Zr+y#({~=18K19OwnH-s$oW{ zJ`X~r(OoHTe;cy|AuKGPB}jWj+2SF2R)?Lv>#s0Kt{VcdzBBl?=&{=P0!20C+n<_m z2Nwm#z8f((DzNVxs1_Tjt{Lw%MGR%>!LBp>Z+lC@va3r3`EExY0IO=2-yWD@H3O{b z3M8v<Wp-n%;R+135G3{C?Of&Ek(|3IMQg%T*c<kClikB-TuidP5V=e<ZA8LvKzaag zG6Sha*%!^7E|PCD8=W%G94okTFg9j}1ufQ6X=%p>pL9X&=kA8rG(h!cutgQo0Pd%i zX4~6bVM88e;H9AP(&m=?Gj|IOhC`V!<Uv0^m?5Xss?Yw4s9*Zi)b0HC#1yN+&>jQa zWm~|q59Cb!&h$&2FwupkCZ#_VaRfo%hlxm+0qRJTo@G>xOj%ucl($7&y;IQ8nDt^} z6cO`7!)>Eqq*x`mcYb0%OQW~uekf{j7uh&Z6pw);E-63DfLRG~DjUovxuaI3K-{Y^ zC+bV<^{<+XP~^|*f|oZf*0>k(*s~}C^}f7z>weP&MR&NuVI{4nhE+F`kjlQo@Bx(d zUV_2&b=}<HH!jF|7X&GAgNncIXAb7~nuxM%9B_5El-_4nGIrw(vgh289I-r`)@_ZG z`EO@Xt8gu7Iu1-;vFu<|P(V}aahxguuDsr<9f%fwG38i3Vmthjp(8M~T{3h{32%S; z4eys%l<88xVx4dbnacFt5-V$v{Wb<}7Av%Eh#2hqy@#`hd3DA!cqq)#515?ttuB69 zq=f~s3QwT_?>K)uy|v+xZ<{m#{m1RtF(aE@@^shYH{D||9fw}2w&K;PQaEJYYrpI0 zA@5o`UmCDA?co$p_|LF9e3%zdUdK$<SxmPOokd6Sl#g+{&`NQ|joei+w$T+Mr^E7# zZ`I*WDPR^_2fhx~dl!nBdIDAO0NI)(>9XZMA|upqwZW7@)1FBQ6NM^4rsJ6?`$lP; z_cIaGF)NZf&UNdl-YZb#?APuU=DP0b;pE2a&)CGrFi$XVTfVrdw;t6NM@()|{6d#) z{H><q1Fijdbq8m$+CpJYhRdtB_zqwOjYKrDO>CU=ryZ-f1zvnt0=mB;v&<>g)O;qb zjCUfu1CM@vK+|7{<p}zAMxQ>Z`qs*JkX<T}K7~SXY_iKLeNG1mqk4(`ziCnOS6e<? z-Hm(yBDY};SobKcl8RY)dk|z$eAcH=Nb^$^4&1Qxz}t_s<$U(of&`nti;w2Ow=mjQ zD}lU2z+R<_bWMfNQ@1U@eZg6IV*Z%N*945p#a%S__&&8e>al)8xT;)3Eb?i)W!e5Q zNl%w{5v`|^e-9L<u(SCT-fP`LVsyVPDQ+p3okgcPNV$tw<Lv8#X_d}}tds-W(VR&V zRjpwx_tWt}hYOEfpKqQqh@rod0yq1)4fnN1Gcm7r0~HY!(wv-SUQ@}rCGFKpVw1a+ zO?t-#oz5^AsXC!RI~nW!(@!UIMOUHv(gbNJb_dW*O=BM@&tQH7HDSWvFs9G<lB~x$ z3=!K`&}DDG=GpN@WjI>#*@y;wNsHAojaw!-7n)4!8md2>$9j)re+xDFD$j}kU3RBG zRWnBJ%jDM;v$r!1kjShWdH%{-#}ibo5FRS(U!2~FLK<`~VrAoKvt8$N=SPE&-7gDl zHIn{JS8V6(c6r~c4MK+Rsmc$>(M|W?sGXQLZWf*7rNY<BQpxYPeVDNg3s}kVen>l( zOMS2J=sY&wDga&Wb~=rGu=nq47IpC}ZMI_YoMgZFea?5o%I3&l**=^pmApQ-{@DC6 zyi&Gbgnf#BmcP6B=KOfgJn>N;_myu?^6XPrfAR4DAg;Tm8g7)~HBv-dFaH7*CA?~e z{SuvpTU=pJ#W<}*sonfI5n(*&gb3aTEulS0aZwO)9Ka$-b0}7?aMDDD_OCdz-shiQ zjE(_A(5C&j26oQkh$ay}`(4tFNh@G)F?4Tw_YrTqgp;TG6fQ<G_av)yAj@iP`q-^c z$9a^nkYUJO8VIC%C6Rk&WP9G!^BC`*n_}!sSAN3P)FBe2;WqQ(D99NbD>BfgCpgP~ z;gvL%<}35dtsr&mCNY3RXNwD?38CIhI^~P>0AmyX^HlTVz>;j5t4-{&8O;h`in4Fs zvvbuuzo5D*o2W0)=b0AILO=0U#QoJpS-fu0Mcq<N%ereln4rO5HkU!Qbd9yW$wo9@ z{Bp`M41s$gt#(L{PxVD28DiwXL}$0j<QCD%K&|2Ls6H3c*WTQ{DIW2bzN29H4KeIW zT970a_@K{Cv&;I&I;M|TniEXPt2T%h1R+A4bcj+fLq|nhRrh&N?|bH$Vg<|)6>4&@ zB43gIx$gpXkd`Ap%58f!5#U?u+bB#<;Cs(tAliebMKnDo^d{$!XR?@(BvkRKMYZZr z3+thLcE05}?`p0UPd@xgPy8$flA|Y}DMFXNZ@vTb*s~mzhKIq6`BFE56iZ!!<1JcV zjLHHM5;A95Hxc%VIgmLyo9E@LFGCN+O>q)rQLN?oHtMcpMlko9$@BX~yw3dD1Tc6I z6z~&L;dB}NrUM?5@*i-`3Z+lTI;!F%gHOPom(3;eJPm|(%16X2Ot%UYwFtWxu1tjF zKCf1@34nlRpZixl_WWCj^U8PdbFJ!iv^`VhXN~vVH}xgcdjS#p!-|zqg_*>3nh4uX zgzfAy{LTkFN<prC89k={PP@xqM6d4OpeDv)yx;b$$kREKcQYrL+vT`jx=Np~=h4eR z+SE`h-f^a7+n4aKtx~KpoQ{yo%!35UCPa>2I6dv=wAV+s6uUUPOv*co<wcsrPKV7B z=m*PC$L5{5t+uid`TqDmOa~(V?d{k0Z%}8U?p9m=@awn26pM8EFTB2-;1;oj&>nTY z8OBzq3@Q$IHhk~PgBbM-T^3akzO=|WVQ7N}Hrv|^Gd0o9C#}f$FRB_c<r}9vuMCG@ z8Sc7rFj{ZMB@iON5{6`mWd4L|pAT{epcfZyXszCRWBLLAT&76>K$b5x@K@lF5h-Sj zqgZaJ!oOgm1qH;YQNP7=#o4#iXV9bh@Q)1sxynwB4*Fa7UF<h*koYp*+I+EJGHzJ~ z?$dz#&<>Zly6#o+&aL|Uxm1?htjsh-N0cYe%I_j>#NQ@q1O4PfEf?i!b=w|sw>XM< z$!R%koW(R+V!*J0F1mMRtEWXd|0rWyY?q-QED1tSv|k=I78ke;PsLq$>{t1Y{Vzrz zpWQvn;)I?+`jGqFIJc%FTbA{_frxlkk_l1}krKrYDEGXo``qg6>Z1RM8`><&;d%GH z7!^A$T`t1bp39=(MB_ZyzZfvAu`MJYyzhp*rG~fzRuB}wl6wV(Y7qrVN1T&yJizce z?&sLjF<J?T4$S&eVnrI8gRmi}7P6i&r*hksd`}3J9d`v_%!Pz<kTF9MO?oPKt=m!I zSw^mBNBt{Aw!GnVgB2XlFE})y8{>%F6!Goyu1~EiGNeqY%JC^P{9`!ETY+{bb>_f@ zuxp|EM~i!2jMnD}>$=2__i?2hJU-sJ8K)gOOV=u71qH-8+YQJ|vMe+r+h<M(Z*)bf zOsJ;;<p@1ZseXkvIXxGrMKXkS4Qh3K`jVBWTeqE`Uw!G@7J1y_pr%ibE{0?&PA-V@ z17`v0n#C)goA`yN1Bl4&_f=JZ=H7bYykfh`7o!Yk`PN%!ffCtLfdfwRjbFL$mtdyf zN}(BrYT!+z_?PbA{h<F;fSe?&4ChoNODY<39qB9&YMSf{)>XH1=Z)IhxOdd6-qbfT z_{vPG7uKEjt$cCQb?+3nbjyuTLItyI+!HZ2&)Gf75YkPvi-3@BI`#G1E~JU-MO2P_ z{%~UD7-RI!hq1vUt)nEKhNI+{Biqhzd#cblJth^{-U=wO2HYs5?D%PhqalX<LgaRS zDanv&aqFpm8&vFe2=8!LfM!K-u}l9(WaFa>k~&NqDB>PMHQLmR3|H&n7c})V+K-Ad zN2V<UvqxSy8#m~=^<DiS=3<%GbFpRmWaUM9O*m3yz1*{$ypfCYg_dGopN>hka!&PK zluyi#zc`^g!#Ge6+ybrk8*+ZG%z}JzO=`zHOo<J_sD|h*sYHUwIICTsmgB^TcU5aC zWM+PgZ7Zw(mJ&~K_w|aqkQrC)KDeMd&Ou-C+KY*C^J+@Bp7)>Z)&NeE;YjwA`%GDT z?X-6R#WuIX?pC|=-kX$PudZ^77rPl3m=KxGiDl-#ixWPm>saOE68E`D^Za~6&&$eH zp*C*u@mh&d@%0C9;<gi!m-BO3eUe`7A6r5<ia_d8e@5X#^=SKpm4ptF#T3SX<OTpV z3X~(ai88f50oILlXjsQ=p3oaB9%}WiAAans&xox(y6J<vt#-A1osMEcw)<o+Q}>{u zsw%noKt%SrR0p1Ub%W}KCHC63F-`e_=aPNjI51=5B6?$g1iN_UjH3p+&kLL^@rjtg zx0;GSLk8f=NxTCL1bdDA_2nN3gydshWAV}Ueq(x=bj@;4ar%&*gvi%i^?qAMm(i3N z!;S3*@XeKwvwD=%C``+U{<bZc9k+=Hn*L7iFkQ)1gR2{D*+}PWasOO+<Ke7?7rq*_ zP8*EsvY=IH1$0s+PyQ3HpLI0XyBcq3o#lVv%ANg>DEm0KBx8`RwtFsQ-DmP%%b|k( zWc+;BO2Y9A#)Hp?6U#@Z%a+a71o#^Ko4~79&<%TAZqOkQA&UoZNL}PXf<`8hP{nyW z@Z5gv%UcZA8CML`p5RcW1O)_+b}-f1uXp{9WbyxpwaiJvtnaYb=$V+KH59*0>s)VB z3{5}6G)tJnN-C9KK#5uW@w*1dSlUH{3-o8Agu6g2Z<=3IyFOl+{SiFvkO4T(Or~60 z3>Wqiv^WYFi$cnlV?Oloyf|T_XG445q<s14fHqda{1H5GYx*LRW%w+69lGj}j+<E( zkuZrj*BuE#y+^&q<Y|xm2jl%P<dw_Tpa_<9K;mOITx%7WEV8c<r}1)U6`5hR1L{Eb z#xghuY@#aa-jLlxJ9SE!u-Eh?uj*RFU`rjAPa2qqP>OG_X5wd<>(O;DBd|;M+Fx_n zcFWQizQO1URV}oK7Fo*Hi&-V34w5MPe=`76pDO9&>Gw)#c2<ACD7%8iyzWE@in1(r z_3nF8<cGJpf6qSXzY-#S402DfB6czM?^utthAQBu4`l)CUE2sR*018|`$ZsHVa5Lk zmT|dOj(2g?U&oPq*>ZSn04+AZi!n5w5DD)V)eLe&-+;_cVI_Ua>BU$|VfJ6_6|q)` zqcM5-mp_p5P9vT_fnrw7{Hp*EDe4za-k^UuoJ<rW-7NQ1z*pgco|7XXt}a8)JIsa2 zzck`EINrcrNvpQQ<ebOig#8MNP1#HhU?==K@N7`<3-^2CV|W#Liw;PzDp%R!ULjOV zZ_8Pqa}p-psYX)Nph>t&7=E(cW*aPYN%pq{@sQm1IJku-#31d^qBbc1A&8)$B<>{3 zy^a7hYI=!j?N^<dj%w~7=npV}=}cDADotL2y25=0y5%kdytfBx#{XO2egZ%;fAft} zm}y<??fw6eTJ3m)!M&W^pG85f9-R{B!_IyL%>>W`g}|Z>cnKK$R#d3?pW|Stfl|p~ z<|uMg=G(sQ3*q`euE#yhH-mz?v(9pjTp~fGuX)moW;fB^#&3EYlf?obU1}Ea30qro zM$kC#tHNX@T`AeF%TF$$haP>Dsx>3J0bjOetSf3`i4y)!xA$gzT~IzYP|WxGO!*1i z*#ZZQz}Z?|k`eeN=F(jecDd98XlFr<1KPVF+92CTAcW_A%q+C=?JsWA#X}^)(FClJ z@Nq>uIooyV$>m69dvInID19kBkR=><Ipsaz|24<YyN!=-i)ZNXK#T?Z@&82B6jLuW zU9q5>Nr_oh`mf5n>+dolH(Uro`8ocKf%<MgZ+^Qs|2#5uguev}1X4ik>!wqa45sX# zjiT(2vkL#w9ve@32oox{@1QwU*LF{O8S-xs%s6MWPG>mcr1gHHp;-H`^APVy=wRw$ zyByuvA}8XxkO<+p!umpQ)5>oYKLyk>v`O(1h+zQx%TT5|4`b|=uYj?$Bn<F;I&v@B z3hE<v{s2^H_asOyFeV;&{y-lwIPXWq@}^Q<7o3F4fX8-C|BmG;Dv%dkJY*U0{s?oG z<QHE6s*vAz3o}ywvk+XP=3MT~Q1v(7FGA#|_S9NpzXGjGeRU<<-)KUv{a9R|?^{@g z#+RQT;0RtL;2dU1m>ZD{PnV?PiLx`m*(%F5iiw2ZzFztE+;Ob`-T!4{$(|zYDlxPh zvv~R6_rY2Hz&bD^Xm$=psn6(h#h_#Ww6ZP)+GFkf0^SoyUfNI8&<w?Pa=5E->Ut!9 z!oW4PJH)4AfzE#l6aFkK8y%R)K<LhIJYev#M=|&XG5XazNrPCkhYUTGJ1}vt%5Y4L zBn@{<cevq~d+#D`VU2dkYUMd&{Q+fwp-?n3_#rdu#}m0d*65{cjwlu0QgC*EgaNM) zG}|VxcN$r$diQAi#AN`Cfn90&ui4%NDWz6;_GDMWR2uZiY0B_yF-&a~W7{!{(u}h< zz9AA`DkFY<`B*>y$qStNWAnX<_|GzQJ9u#;%h12vb@G*MC6Q8jBpcI8kWV^z&M9$| z1Ywa0a*e~kzt~#y|8qJHoR0R*i1rSygrwG<#7PJJjHl?X*Wq9-fF4WbDt=Sl57adq zE;Dpy_&u4_wC#`zfNtePV`CXS(1M0l=xe`mo)9#52itFKy>+qc*cMbp%@TG22S7V} zphsIt2CqrkkH~!nx4iJXAAo3Zj1?y(1Fl~n1Px6#QoQRoF!GoftId*mY%$~!0@`#( zz2x`~b!vhKwB+FF*a{+d+_AN8mEz`iM8f?O6VtkkC%~5c9*G4_HaqZMfTEZG<x!|6 z=<@27eTm$q*Q8$mJcBKzl;dd!0O#_mW0c|Bzy^b8@57JuiNeRA;+8?kFD9p)ohi?L zfjqCY-l;pngdOFmwH<5`SX=%!vkobFg2cPWfE1nbp$0w9(HStJ?=XH9BmBimt{Kj3 z%TvWJ2L6{7A?&d0lWh0Tk?e=g&6P3N%5nc%fo}Tz4TW9$Mw>v|s>aCcYlg#!p(L9W z=Y!xNg}9NF4T3<M{6&~E5|Zn}Hj3LK#*V0s!4`wO94~NmK;)myLW7~__Dgp!+B?^a zdPL3Ge&hOk%kQpeiumAawCk6j_crc*<#by3{Q8l;c9*voXi6>lf9l<hN?l#$L^Rpv zBosw7zvi6s!C6}`^H|%trYmA;lcV8EX_jicpRaitf28quiivkZf6MJhH>T(h6vtvG zCk}Jx@1Hno-o}_1+s$)N8(63~dLEt`Tgq@T9RH`_Mro)X`Y60Nf85Rh%#g%>l%UHj zpyw$DQkA9bGrjgSs0xwqNsE_YFUiU1QZ1fdt{&HdXW6cL$6N^YKi)-OrHnuKg}?F0 zpHRZyNnj^$X7m-e_gZDmW2wrPDq_#0KNjYsMy+$a(f)ZAJoB}#o${@t<J*nxp?c^u z%^>K=yJG0zS$vQgTVxJ<OAGHj^M*-x!CI1SxWq30D`G{BM9QciJ<r5>_!e?Np4r3~ zSxpK3+W-4}LiED;0$Tl9t4IQ?6Q3Olul|2nGF=qv0S~yHy7TsQq{~X3A#MKqX0rya zpd&IU21H6v^@L@zoAnZ^MN+9D_UyD4f$ZD&DU|XMUJ+ADyHP4O(5&Tg9D50l4=z5P z0$%HPBryV}F1otvKkgbaVHB)}=CbH<N$gcgF;W8MhM%!pSSUPb_VD4)oQ}&kwu_m3 zk6`RYO&DbkHxj@2e|F@(OtFky2oFE+a1}-$bY?#zkh|kbBK^}5kg}Rvk2fXV6IMoH z8J^2p#orBo99y0mxsV#ta&yqC|7k|K5c@aPmBH(<C*DM;d6yYuj{(JZDz^2Axi)U~ z=s<^UwuuYCz*ozfL(elD={TM2WWAWmh@H<{fjxmsU|l*&(;#09^+K>b`c_$?$fb*Z z5gX9+Z!~i+!1SB1Vl<XVhAP|_t1ZDFI$uDwV615Ge}M2#zJ_8yw+V1lj6J=M1A(3q z{$V?x>S{RMk(U?W_?4{eXuXM=b#A^N|9|y2AXJx_JOk{!XUfRyEg)ybk7>^Rg~%1_ zXG}EnK9e%EiR`OALtFS;Daer29p`OVL|(>Ata)=!IA)u;{9Jtd?<vN%AhM~BQ`6^? z&s46UMkZFJ&(P^aeeT|Xin2m@=lHC-pPL({f9Rsa|FKLY#dC7JN;KJ_avjyRagnt! z_Uyb6`(4CPj6plRVz?)HUukH1<5s<KedI(OX#xkOmp;4`3G`r#h_Ust0~nGN8NLYz zvwP&|3)<c<pI(lU3+7{}b%s)&1Mh?%78etXbX`gQgg!4^fX<XGyoiV?K3+0lS+!zo z<lDh#Z>}627GkBD;jh=1iO?NvyV?WDIiWs4_u9{+rNRv^ayoQuE(IS7oC%9Q5xGYi zYUF9Ghv^f-?4(E7BYH4SEnMwH9)Ahdu*wZ>z=7`Qx44oJFrpH?j^r{~Hpv+LGu>B& zeKl$041!=v%pLwB8D6YgYB853_M%#;4SNo7W)T>F46-ow*TKg<^7OQAA@&%$lu7D) zJrU9LMNZ?~HPmr=A1<5!StXIkYDplwW!F7gHizyo{V2_1>z{Rkg|g1gEB<T;?^LEd zO{)}-a9*k7hQ5}JDCF<N!E_tdTOqI`EB23^prC*chg1)C;*u%FSIKvQhil(Vb%?`> z<oHvm1V`cT7r&pR80coZhSs%G`MBCzD&w|90yb|G-px&oI@-1*edqv8lYMAv@Rj8$ z8zRB}o_n$NOy#JGeL4kQGI9Pn9YHqn5`&n&hgF&ef0b0^FY=qEn3yf?EbB$X56y$J zC_Q0d<zPUTRMs*vdQ8$WB6?(CPn-572FwS@<DKd{BI-?BJ$}Nd$3F(nDEgy83Qm5X zFXl)T9NP*xyT978*V|#n?n`rL{p_kT<gHVi*8i_amMx(t7siu2q)hUTnUmB#?CiAg zL0RzTDk4k+AWZDK4+?Ttwz~cu=o8|c#Gg=Li?kLaLv}6^a8a_^5mY?9A^DiJ2sO)T zAq?t9KxRK9l8-uSb#|dgYDj8brHE)x<Xuh|zNDdCgDFz76M?(HBwJ-A7`gey__QVD zNO^mwB0~M)eaj-N?}YJRtRlwR32&(F4Jgwvc>=4bo0<k-W0$+;vJPc*f&+lCimK;i z=S4?N33(P+ukWW&q8L^`Myhsrqv(?@m40<TT{XJee<`LG=Caa`1*b@R*2wyQ0oBkO z{_a$^v8K5EIP$_cb|OA8QEO)d!z;M`WyPZ=$`;b{)P_^{6IEBf#KvI3tXM|vvR#MN zq!LCSBdphu>NWnt8j(uiz5j>-oQ=q;_B0rIF+cf`v=UXOKwH`XQzPf+u&>q+6&j2J zy*xHK%PF)z0_H>Z`HxT%C<lo8jb2k1ZO`vHbU>J+*4yzDU*5`~WM5fP5Wy-k;GpY# z=n*XkeXfNKi~61gDF+A?^67wvUUNWscW;L}|JX2(*I;txSp86dA{V&0pb<5gcXfwW ze=pRS*nlQBNj7^e1do@jF%-o#xd<B+)zeVq325s*3H~2z`nL7_AM$9smc*Dintb53 zC|%R(ugs%0i?El7*At62eo&L5b$8hU7?EbfHSGy)00uS)^g9J3#)4ssEkJL#?N7Tr zP3~V6r^+0Qns17EY*xggRHcfEafEa1W2uo&E1NOy{+E(5u!U!cT~F&3RuIh}3$qXP zx?xYMcg+X3DeWC;xu0iAM6eROO+vo$=~<tR@Vc`8fgl5PiX!lXD?G+aDIroM`N*nP zrz!?+Y?ydIVC(kT9FCN(J+6v5C8|`Jq$e8Vx256D^M~abqa7Wb=FSqtX#U8tWfJJ2 zhCU}JS;2#D&E`(TqRkp#WB3GBa3HC>#!Wp&7g9Z2H?&H1o48~EHnEUTRor#sgcCSb zfu9l8buj6qI7=hy%nFQoR?rt5%+?r}cO=09EzC+wO1fh%i^7mffq=~(C~3QlYS=RI zURX7DgR~#09(R=~?8))66?D~O!K><#C(bjUwn$1^6`uD&4WTBVJ!VT;<Q$gaz)Bje zImPS>5kWX^z84~L%CfM$?|n=JL3r)vrZ2Vowg)HheU5(-F5p0rg2#<Eo})?Fo~2Za zhr!kO3_{={P#fIC$ZPaHsm=FF)N)FL5AqR*D`N~fwu`Hi^KKZr#YjC|BF0LKDLsc# zN|zd$bNSbOYmS|hhd1BPW~Rz2wC>l{^ZB83%qg!F_SVu?xfNt{W~bdoTpx?Fn=DzN zC~_qLkgbu3WS(=MG{s+3{*IjkPn@1UW^HnP&sIbc)5I{T5<{fwY#%}%_cp{>=H!>~ zz2Sc!r@E@h<SIP>9hq3FAUK0eQqRpRjHgSG{TnnE_6Iul4|V*<i3Y`G^Dd~ke>^zQ z_s~+c^Lvoz$4?HMV`mK`u%t6OPpBf342BONGMP~+Kfd_~sv+_hs;lmF$4&8?n+@9X zsjFdFMLkPRbtRu4^{0bu?&iR3r^m|>cubQAl{~jkB2KA{8Z{JE0bQajWW<lH8;OCL z&`2Un(q9d{C(pI!ZwcgkF$X*OK2%Wr91?gVysBd`?V#5Gu<mnOt`{nsm|a5rJlsUh zBXG7~(!8KOFZC0F8{y%$h;JF-WQb*m-pf9nhMw*!OH8bxK|&H&(cd9?o+z7Ee%&Eb zo0z&9q<Y;rHon7D2&B3|Y5e{NM5-rT4i4Osd@msx53UMsrVuH@#-PS;B{97G9Rm9# z6{fY74a4DyzOB3RFa>BvC`$L--1z9{f;VLJSPgD&Zod8W4rI&`>b>$c1@@z<1l3Th zF~EnJW8$xJ79{eZN-Q?Bb~S<X#rs~~WnL4y&~F}y@pKFk!vYChH}G8;V$gJdRk9cF zkFa<G=g=i^iSWq}0MK(a|0@0(;SgokONa+Z0QUal!fp~}2S-WiwtxInIVd3WNs^l~ zpFxyGK*#txxP?f=0NRI_(!I{O83Pk8h5wzc6c9|a?-DsB5v$r7bU(ldwZFermSFF7 zn?lt3^s;gWjH+I(=HXcyEPF0QOfaBfmK?!&A^G?PRGO9Q=lh!JXBH*{Gr@oBuL}wo zEu<??2Wy}}WNy<uayX_X5Fhk0+ab9oM^_AC3bC?k-+B08zYsPDkpS<EdC2~|WQT(_ z-}i_n;E(LjQYq|TMA&78#qK;$6IWQm?C8O9>^*$td4PttZTTgs2whL!2>=5L%3zWY z;CJLG`UgFP?NVy;AIJEjY`T(ak$YJ=eLe!f;k+9TpiCtEu_*PsC9c6NAMt7ty84so zy-&ZG(ZsN&ugTLFWEa?f)J$K1tnu+J#i)%wjm_))&TCfROf7XMvW=bXKi3xHpLY>h z;m#!o>yhC!%L<!2xjCRxXU0a(pg>)S8mQ}jvdl8JVh&yX@e}qdOdG)wvZAAM)WQ_A zH^U-N8=d8JhHAk2F~QqTa3}Ex)0T<TH36F9j-X<6YNHLjy!_La=@&6^|G;t&QhPeQ z1Q?M?y04+-Vh`#$WqI=*zfZyd5|Y{Bn^M&9U{<x55LB%0{&Z}g$w?trWK&{*Bt~(F z^cLjN9S35^$Lf(mL09ix4h}AN3_(rccSJ$hbE0(E8u3$Q<6g|6XFW2WP{2%5=mKhW zfDGAjoZ`40l;rE${t&Uu40(kwKk`&LvDJ^`3b%AM*_+tFh*f%`zW^G9N9tC-x0HuP zR3Z@T2l%P}kzbjRtzGQCyUlKldtC}94a#}0V+3PFfN0`s?odL81khR9GG}|R<++eC z%q^PD=n!F71*%T%vQAzPBWJ&u&Cptum7Pa#N!Cf1M3>;p+gO5N<^bO|p%Pmjb+b#C z_lp#y-U^nGLVKpw$;LWU${3AQsF8V>s;m9|$$tUYo7EhOkzJow*>CtG-!yWwb(gN8 zA7Xz}Z}-fjmq)+ErtHK+n6vkg<9I^GA_N{gfM*bptA}}>2pj^}K;{y&%sZgjXiqde z5^^TG6`R~(B+7nL|F**-`hn)rRS0t5)SBv%t({#0vFTznE5LHJcYD3%TRwcAqohWl z%GfwR|3#V~audmBL~g4Zdop7&0*NDE5Jv-%^hEwn6bSP9Ul1fOAhP_>DDtfk`)yU1 zuB-Qj+shWxvkJ}~(pipW-&R9XvEB{ZCfOU(b^uU2{Xg59X+AXs%lF@O1zV;vDzi&B zu=JwQvEy*G_$eA+?)BdW1q>|XFf5gjbBzM68>;rs{lOBET2w{G#Dl8GVy>QhZZ9L! z8<HN3rDO5*pGbo9Xj=d|!xM&bKfJ%29UR=}7&2xEvC4mdiChow1;L?sId!+5{vKq* z>M<Y3ru`@*9%ao+J^MHi%XJdY@ASW2Ea>F^AmN%Q%92zVTv^D|H`!VH3vBQ42%Eea zCc(OtQoQBw95ax}r6DUNBsOHpHuJOqt+TyRAeoIyP;no9v__24xplQPk!rC`q>|4L zHuE7WUuIw%b|vDNDj??S_B=&}XSpulIi)z~NJ+9npUU+Xx3BwMWeF+k?T+%pTQ6fd zk9ZG+`TAbjTB6GsM>%<&Z5w*Q<RgDJ>s=9Hi=jEYWW|)f2u1XF=HnE;9H@(sY(FTN zaL*aDP11G(H0M(hE7&$PjRlpnE!cemH)}raZ2gtpD!+k`Kff3o)}BaU<vk5KaL97# z*$0F~&kkwoJ-g@P!~4M+Ux*OpZ8p(1TP#d&il{q;;Y0=euV~#h0d-^;vu)Oz3JIJK z-J?ISV`a<e%7r)k(>)7rT}4uCccK@f%uFaUCluXR^M8XbAlGtN@%g<Cc|0TjF=wCR z3xS&tn!)g+`Ks4*=oT86N(C?&_$?^Xm^!Gs3qizs?yCn40K}Ludx<a`&6kp4Qpy%Y zgd{K#dJR>$FFNf-@6PVJx|nLQ^%9oxR9AvL<^tsgryCKLTM!!Cd4}qbUgosE;Np(o zEY>C02y?=mdE+k%V^2N=N8?=&YoF@MQVe_Q(m-Z=KOcHDf|~1rCm%iBkOz{8MdGs( zhv2J9ddc^5cZSHUyBuV2jg>Owhn;OtP;qJy%eiDt8hW-&uTz1})-Hi_XRm@SD6^QV zIN1Jp<kigv-N(O!@ASlDNCe{+IM!ZYKc9j%Qfk>QhzPjuP_(j*Yzdq{OVMFlE)5EB zUGvVW|KTsM<>(gMb`N6dSu-^Ma!K-tCW070r0_unO3fi*wTc--P<$rHVAF8*H=|^! zbwBgZgZO5!ek54zbCUO?BkH6N-vl^5+92_vXFfMn*aHje_)vi1;rt=Y*@7nG&3{o^ zNH4FI;;TPG(R*;Xn+AWAq`7t@Dq&t*fKNwr!L1UT|1b^b4<hHt<kvRZm#x1jEv9Rl zPGL|&SRs@2x#}(NWIwuu{gb9d&-{yE82{si4ok4aj_n={Ogs43o&BgojC1Z%BIS_f zM(etjMU2k~jm;pUdtSI9FK{L9SkLe;Q-NOi#+%zv;o}t*7IMc8Dc|t0L24*4_tkvf zp{u-HMA>fb@&g&<#1c6k%*LvULkFI|y0Tq`NXly6oFmPrps_UK3~v6Kkgf=JL@5Qz z97kc5|H=_ZT9^~66Y9-VG+ALbkI1<!UZVju4xDKZfdEJ*)o<NB_yif-cIP3IxB0ug z9~|3B3Dg{MPX6?rIRgCXntI$Eu<7*pIea;!qm=YFVSYm*<U39$J@GvGem?MwS?wM} z$f*LgzY``bpyDVAaeT(6oy)w6D^q?`IY3fxk(<Vn3Xz+ETB*(->*~Sg-@aY%44w`w zWBcC}uWhTxjX_RUR21_}^1PHkiAA5QSn}D&u@s`xLYBRA$$KQVbr=YCd&zj+WhGeX zzx57ITGc7$c&3EdTo@3ayIC3=f5kQvg(T$D%Z$9M2y)(kzAP5~`NHH6!e^NZ-WRRn zK8x6utAnP)-qn`KVCwEYnvg27J+>QAvgcgRs^Gr93&P0+@-1P0abm^**pf(wtpWaH z>bh#b6Szu;G<MQF>~M)CANA8J>Zy8Eu?In|S<9W4v5N&(ym|jk7&3Cw?3~J~b(LTd z`$8qkysYzlzGVL?#L2hFH!M7{-Qz|M9O&W0dL%fscIiMX95WIvK{sAXPi`smqO3vu zGM)bsII4$hmQIQD#afu{o^=7{86SmIsL`Gae7ksxDns?Vgt0i^86saHa_Z!}3LA76 z11uV8siEx{WeD8Ye<@65en5a&9f<^m-J&pD`(Ig0uzG`fxsHDir?l~28j*xpGHvfX z#<$~Q?~wdlZDGCZwRRVMkb!l*A9^@>@4H2lh_ep@|1gDTbv*;~N5mv~vB`n+sJ3R( zl_fIxZ_yyr6Sg};8I}nD?Z$R@L%bJYB%e#Oj+DL2_iqILAwmB{kb>8BPe$(*TL=X{ zG<&eI_(Y5)^ZXq*`!F({ch5b(M0Ttujmh&<;;O$QYsd)E$Iwt+-DP?=voSc2TnpZ} zbk1gZ0|9P!)-5x!$ca1Zun&MrS=uZxl2~#W5h4DMo5a|~&SkV7RUIXG8Z|sQn8x=E zA^%*Pawf(<bB$C{O)A1M>(F@d^95n9K;vR)DqBQP)(!11$QyV(8So~oXbzqEFuby6 zlFz{4%T|ot%e%xoeh2}^yuewnFnxpOkTlW)hAxF@P}Hj;Ung*zsvcYOs_LG@BF`;r zJCW23$_whT(e*=ymKFet%Dn{h^=JPXaUk$*D{2b(Kb8^bv^FpHMQ`7~04dMSYqox6 z;9%Hl**nlAxfU77nD%fp@l}(KeeS|P<7rPkek@u}EE;aiRChsY%<-)-_}#Ur$FY@9 z?V=qZjor?jp$)D#+D|$o2fCB0*vmUZap}<hw<M7l6FI~EAOkP!3g*yd9$K1p3e>B* zVR%N#Vn>Ak6A#<DFvLE)|JOrR7lJ2$HjQ@B*mqzqx=7mdv){19lLXz?H?`#t+LHoc z74vf%a&lHS39>$N{}v$5Ng}y3ygzEtlU7of4?sM9QqRiJ+=jcBuukVBy(Rd}zLrm- zeJC{bNZ{zad<B#^{*@e2-w3%^?cBaRy<0GozUElC#7%ytTZa^Qsdw&!Z#OquH7qG= zm=E)a%~H@z4Jwp;)ZuE1^OC^H3_PRTJh{R2;}Up9TqU}+5T_9hQQY_S{gV*K;|+4Q zs~?ykl+6DMWr8v(VL-2*;)>MfOc}5edmCi(^Ecsv5<q1SZj7hj_+5=PpeD>RT%HAP z2SRPdBq~znyS#ZKjJ?-=;}!TyDsK+`TI!RejuxExY~|?=i;L98Rnzt&1Ut4pla^$l zIkfOUaXuTW+>Zxbb9h}<yw_p5uukSx_9sE;LEA=^3CKuN8kg_mFQj7V0}%1T@QrX( z*%GNCadvG{NEXxgWrX=1B*O8~B4S}LSA#$~{Z9AgqbEw_gmCn5YN!{>6cr_)Js_BH z@%1SrFzu|%3N3t7Bh1-BHW3mu!9qE}&(i&!?iy+}BlXdU-pOfslU@GVT^NidN`Jnd zKz|H?S<1fYlh>lAZfcUFBbF{8Wi`u1;5<OnLi|@tpOZay+;eIB08vU)2X88g=R=A| z&?v({Ai&Q(RW_b!J4R4jcNj9(_A36%Fwc_nnn*s8+WgY>s52t#BP(}}Hq<jkfBUy| zamqU;8uAiYHUlX~&7j0_|LjMa02CjD@Qy=A`w%b|QQRm%cFd)m3A33!dzs?{H}~H8 zy3Ew*s|Z`ev!>7mi?Ve{w525Vsq-4(N~@Y6M7Mz@-b76p{L6if)PuV&*GHy|P5-%l zQj&Ga#VABTZd)O0_1>d?8*>n|8;wc@noNb2&j0H;#!yn#3H4;O_&*(U&|hH;1O6<w zhIk7Ov=t7tbMjM^MrBQ5x(=ei$f(skfmqF%BLN>pE(K#H*O&5b>-u%);nk8W5$&<_ z1tu?2l;yBkf9k+KV~-+FFj)`H)FH8*Q7-O})jM$xR8|1{4N5(WZ}pBF!N|zp?0czr zd54oIjF72x5NV~b>jsF7E)DJ+WSAw$-u+jW2EOcydr(;K^Evzu+#&ouF7p{a$eCl0 z5WD5&VpUUEK*zGpjfVtJm~)AH!!Xf(P^-S^0hn=G$5{(ABS^xB@9V_EV+nfJ+#2tr zey~7o(>eEG`;q+Ve+d+ny{Ch3ezm3z%(JMql9nZ@GvubUabKi7;mOe*lGCl9kvZyB z5%B861xhAwIdH%Z6apFL`+(u%MQA>deR2*Pu?<YMcqgSw`^Q9fv^o2YY+WbLUToFp zSK3#$J980~I2a=yEC58DU4%kJDpZ;pm_>O$X%~N5sd6_g^P7ET=dZYDV&NlIr2p5s z=A_!gQ~=5&moJo)cq9&VMu~n6q0;U)oye`U*YoLV(aHhBxENXeMAouy+xFG69$A4M zmheBm&HT={A=e0T9@jr^%OksmZ{?MhuC?L4P_646#@A;OYII+wL}xe{I7uUZqx$=e zPqK0Y9vc2D@w`)*RR@-?4O&@z9wZ+!T5#YCq3ERhOx4tVOSf~$rws31c}GGJLM_m^ z{>9SPur@IDA%TKT`{uJ=(OM$q>F^ov8_MQJ3y#7vh)1rOgpKKhzO75F{>~`0hO-2! z%@1W#tHoQ77}oMA{|X3YMCid$6AzF8O8KDC0&7@`p6am?PuWp6xZV#jv}st$y`8fb zXjQZy2`xufA)zHs1_TXk5fL`8UAw^VQt|<#YllG7To^VN#(2l$FKF&sUFpr2#fP+i z$zruGmFM1aWGn*J<MYFx-hI7<z93_72$E1m9gc*S6xQ6prva8QrR4x1hI|jEH$L6a z!1J9AgGUgQh}+vJOen(+eOi~Y{Hhu?QbPpXM?Rqp%IdVQv6AauuC<@uiP9;<pn;Yc z@DL(Pd?%EZs~5+H4onYJ7*^AUAFfNe3+ptz-`T*pGaoBc$WyD;s0rKI4loQi?^qZ@ zDr3ig#QmQKJmhV~56uUSc0cNzVeVw{XsK@2$e-&{8euUR703A!cyTcS3bcFecaN7w zN3h6gX2|Kzf;EVSdn^u1MEH$#(C5K94eQCsp<b<lMbPw-mYRXUt`sw~$IBd{6C+^) z8=4ErZz9`Wvxe8?eIM~uWFp>4w$nG(@*ZN{R6u{e7ii0#s?L2ppeB1?K5KX@9P^q6 zgqex!{Bdk>rU;yghWxB11+~1*2ops{AjOcCyEtOFC&EV?gmoU1XU1eKcPoyD-@1MB zz!zeXp8nSa{I_6kJ0OeMfP}x>(1pBOe7ug@1u!h0zr;NrRN}by%;PO*T=SE68J$-d zMfGyqo%w6{JP%k`1jpyO{?z#oJ*>9F$%xFCm|8?^{lwYwFE5ODYC3<YU=-Noed+y& z<MLnckMoT0Xk238B545ktQBe+a&`>n7$6`ma9rMG(Xz2tu>fg3!^IzoOm9AJ)6JD{ znb>9;a{J@4UkF7PjM*23_TwA_Q^yl_vTwTGzpr}a>?MS)Wa{rKj_2K2#jVc&ZmlKo z)_AU6p{oAkJy-p%5ZJ$^MgNg3z>y(>qerRz6*`XQut>opU;xQ&*V$Mj6($Mli%EfR z`kIxLQeF|r_6O;BYa-<Yp=hJMe2MwKmRGdAJ^T}CX+1+Y^1G;$Jx7?_7~nKUvEJ<$ zSW}nqaCP`{lKNAK#c_L7yfMf?SH9E3)9-&zOwff2xkE2<;5?GJp+%)cc@dpfv#EYq zUH_}mo|7<57<@rvzN7)CE}~br?+8t`EG^712jTC1IX`Q&^FJej-x*N-pM$1PwLET< z)$4`;7nT^Ye2~|AM?!!VbvbH6!UyLPhYTbv>g`Ue9&tUpy!ZPGN9ZbQ-p&5$1CvL* zlP1m#R+FEE-C=|1TzVJ%2Soso0=@inQmR^`V-70%w%ap#QsS$)Qak7UK*rd$bJ%Z3 zYuW;9bT~tn`jtj1;m~!U8uMTbM}e&zBDQ`$&k!H{B#I{EZq7Rp4pWSS5N{4iN7@ls zwqTgKznZ$;-jnf)NWOQdvQuDsWyI*!KXb+$kKOOfIEo_5Y@$RKmia#0t8PZBMh^K` z@;30Se}T(wXTRAA&L)ZE@)u8^u{LJgq#Z`Nhb<Lq)3CADuwz#E2)_^M%)n%dhj-iM zGi7~x8AdAIFe3ZX;cb^paelVdBN>LUq!iCBNXxK90dkS8-r!g<IC*tOgCO#8Z6T{V z2SC~jp~a1O`s#HhK{ho@rxkFA;k5|+<sI)ib!jch+br_|sM_-BQ>Tr#XBu{W*MlfS zq?dwN47PYVF<bQR7}Z`2J$v9J??Yy>w!W_@Zm2&BaVAn+5}sE#hlAML6mzDEdoKG6 zv>IR8+7d_!F7hqhoKZVT?qK{&V7ny?Sr*oZ;7L_A9IlT9cJ^Nia_Mh(7-!g`2L$Ii z!*zMDA=VDUN2>u$f1z(0-G510-H6=11iEp&ceobMiks9WfCCOWjK?90TlL8QEwc%B zq`;??FZXidb65A~7;gprZ!9Lr4_p(YmlwSJ`^hT*%p^6AMg1d0tDQjF@EStd`m=PA zM^M1)AcJQ2zxx{p#4UE#EHOQL+vB{F!mMmE6k4pHba1uG?Mc#>##7R$0q{Z^3S@VW z>f>{;QH>iA6*L{%|DdpM+YVhD=G#5DH)OmL2G;C-`v7OFYYXVrrk;L7B7OYBAvX8t zf7x8NF@YV{Z&#?h8Ip(bNvnImsmvo~ImJ)G(0bhaT7raytMtsnuC$*~LqR&6(AQGC z@rf(hhRAno6f{W;P-RGRS|P}$A}AnzETrE~KdR?Jk?S`$bUYnKz1iNONx`a8bO)MD z(H8&QCmNif4@kDc`pEXo!7j*HnEa~&!*(;X<GJ0JKEuH)&}Wo%8+xphI=qRPl9y3O zjLNFJR&@pw^ZJgWbi90v(!Is~yRHMuX_Fj<e(pl8$cOn^r@%{6+c%SUxh6xmW$ziA z%s7LI@E37_BcA=;eFOEQq=ZYux=81v@+za~ggwyw81oV<lZ&xlLtK>d`R;PthLuGh zQX>#9KgPM@YSjRUdV#jWqj|eFarSK?_ED`t|8ryNJ}aS*CF3CiP4AGvxBe&w=~R!) zQjRUkhNrC&jH_o1JT7bBpU^T!nB|^GNw9YN#k&|bhY^{#pSp$1Q7xS2AyrKjGx`c( z2jV(C4)uKseEbB}Z~>{=4}WfeKpCe3kqpi23G>|s4)x!Dx_oyAl!l<2f!*Wbg9|P+ zdT^)4?&pQ%M^(Fti6ZDpK36f;0G{mvoC2VF;^&eAQ|;3ojC=grT!-$gGP-u~EojQC zm0<#n8!AIh@JpPGLi!F*Q23`haf-o-(w-%^Y#$+V%bDsU#HyIk(dl$$S)*gEdzR2G z-tTmMrY(ONhr3IJWte}XeQ~?%CH_KSCW8^q;kWnj&u<XazO8vn>$c{}w}>B6cYU?= zZY9u`@9Ih{aL~)7K=n2)4)C^iWE&sO2cofm>)k%dXxjKh9cvo?f9<{bKh*30KTayA zgieae);TQ>9jPRhWwalu=9HueZL(9@GK^_CQi)DF5@Mu9_GByTiEv8xkaa>ah#|u; z7@x=WoaH%n&iniQ1K#h~FRxy&o7*+lbv+)B`{VxDt|$6UW{f!VQvJg`<vg_;cQ0JN z{iq}Uv}j&e5WO5eBB;tIu|qSNji%Y2e(k-Z%d8`@jbYtD-x)76MvNJE_n}^^)5E;a zQAbw9U08k8K;uyThE%K*10aLsBRVyCD8r8iAXk=%K3w19=-2Rf{VirIuV0P3DG}!N zu<?@<cXB9=#y3_hqO6ppJ7n%EbJ^o3h#EU3p~s+Fr${-;PA9Fx9=bGxEq@6C^WF5^ zg-;YQZ!m*inHe<Nk~8uf>pXE~@IvaIrBxCkkBeOR9`hD39BS|Avn=qK868NU(zHIY z_1iW*X9#m<xp@`0vVulD+-{Pmy7jLAc5qV7%Rr-LTLC~X-$xN=gULSEoIHV=<+Y%> zcSWSS)^kty&VTZ4k`<~k;S^s-TB<QKnjuP0(WB6|)DJ7qwR+sqvpu-O{0%_e^R7X8 zbPb4E-4UZ(Hy1w5)x4M7GjICcU0#f9<6^7O$l@|*er3^hi?JqUdZ0d4FN@Ul=cCim zdvEq~C8>E|&sjkc;XLU4A$LE*k=7-N&-&rY9wj70l_s!n>F!ZUAr+gt(M^@0xwM^6 z?2e3DyF&FSQ1=imKMG$+dR}0H7C^;z53X!R`IYS6cogF<9iGUCcWd~&7iC+Q-%&rM zXKv;Xt!l(YFi0c9etE{_fwslCx*$y3`2)QhU0Q6N#j8E$D3jp(^FnpQ&+PTW+*ytl zK3ZQLm68QmXM4h*^yi<C9w|eXn5gCu{y_i@FQE;&6`7Edh0`!xf82YAe@0xK-!UTi z{2H{7T~?v#bV`p>v$1~4nJ5{2Xp1OQPD?IJK(FPrqo+S@B$<vedq-;>oCvT`2r15I z+FyQnVn1JPfNM@o>N1a%(YL!>)~+#pyu@HS?ya`J9{==rr0LzU*~@hYSC>~uI2xcu z&O?Bz?D!8uho8ZNgwNV&s-0qAYxS1Q?!&~{!H1eenbi11EmB`d%QlS9f+8qspahcX z;zOQ)^k<@kz@C>^mtQG^?Fhm(ar(;H^oL2O@)Xr~XQ!|>EHhM_S!MO)dHD%#KEbFF zWeywH{TOuwrFmCDnc`raM`>rHwssebEF*|CHAD?)I>X(`l{>-`^hAFPwp1{ngxUAT z^ua)`G5>5v5&$o4i}p686DQqKkLz+EuW<{@pe<g0n9PO70C_<tmWmy`6Pl^w%?T;j zFl6^YIq6WF!T>-1wIp5i^;OF$>~`34nIw<m=*MQgztirPOy*;$ByL!!GPY<S_Ka`8 zsUsPvm>50f_j1MR2$wzL0?KLxuGi(EoMm6zlACquyBJwZz?cu+(E2o?-*%7YiJ<~F zA37)ZlCN*!-PEYr=2pRO`WHEBCL6>ld@agL7oGYt^T0lD1POW79@tIQLFy!TT>%4Y zXy;a7NZX*7huP9MQMJ<0PlTNSMB;91%R&KGYq%Q_bV4HA!|IYwgi5M{wH;g?QrT8@ zkH*5=;-iikP{UH&6wQfWF66=OJXSQI1ir16nVut%2xZPA&p(JMPGMvF%uyWv_z>oP z?=j@@+LR1@sKn-3pDjb9X)^dwwkR`zez!~Dd7&QE5%<A&V4}J?hGK1uWQA37ehpzv zs0kcz)Nuoicl5|pdW_WZm2n%y=yC^yA~~o8ozA3Em&FgvlTM_`=Czifd$~Tj!Ol>P zCggf$=F$DePc*0Mg~%4H2<f;j9&L3fYlA;tG-XP_^&WDTP%+ICx57GA>(NT8@9!}Z zBEoo$wW3hB`z2bgYj4?y?FH6-MhERsN8;P}uU*=pHHpWN3T6Ou-*9Sw4W<1F`}Nfa zn~mXm3^hV3eP67jdGcY8yVRNN5nEzOedwbaC;>u<dj$_ar|Lwe{N#J?#F~1}q@47I z>w#+GEhgpJPc+XN0x`gTsFA5r)3HEVd`r4QAAJ(9(s*#=ea{Vf{)WVu0U}^y8S1(w zwMs1~0?<ltv=qj>zLre3UEv##DCwjq`Ak#LD8~YNp|u+)ga7&>!d{ta@45-6!dB2C zHM1tCkP_cBZ)4*lIM*I~EKdbG_!Dx>LWTJ~O2A~v>7Fl6B~DP%##gGogzn44C#oby zV9#0H85Z1+YmwE@$#%6*2vh@YTyYmFuN%822C$Y7<VRW!h-%T)2u7x2>#&QXui>6E zB2()jDa7Vy1H_oC!)826>A7d0VFJeW*Xx7+?92^a6WpRJ!fWj+nYo|<2bzymrZlpH z|H>6%Tc>k^=>5WkLS$-BZ2W4U63<6KCRDF;EtH{yRY1AnDNJr@ygS`o9hIJ>Vfbcl zwAC5wh$|X6_PT0?G#7T8dhTz_Lgj4r)o`9iD7~2rg2(wWl37HQx8{K^`v|Mx;2#YN z-p&0x^|`7pzt{@78`Vdi`(|rJwohq=(aSTF09}KBSEJ@VSogh|?qsGop|aw}f)&Mt zi@vi=Hu$RxWXks0Tby}39>SZPWG(hec~t5=moo6%D7(amhLc;Kx$idHt?Zis<~}49 zZFK_7gMgcIxAnl>hd27cyPA%%9JUm$$I7PX1~P}fJU^7rND%xaSB-XgmztF*`=+49 zBw?|T2x3YeKSK|Vu_^`x!Xi!3$;nr}LC;!8Axwq#qy-r(LKtPZ%Qu|GbRFK?T-|0L zU=`d!aulVT9y{vFZ*vFzrV<S(Lx(*42tY*g(|sM7vgRoII+wVPW~!};H}YbF+tF9; z@7oNvHa4rOI~r9zocGQWP&ZNbHt^eLCK?%rR)~lMf+j7@f+=S3uVg`rMGX2CNGG0{ z-x@s)h=0RWgdC(sQ{Yd$!6j(Gd*Jku0nh`?Zl5DPK-J+#FAhOI`PEe)+8jQC714We zQD0L}lR}G{hB`Obd@n$e`YGyh7K#dBa=_Mr@}{pf%YW`|7x=!;G7-ZaTMyl-i)_Et zTk&yKGYZ4bJ#GI-I~2U85=JVl=HLYet5rY%o!nU31B)pi%Rx~VQAEg6s~w`oa4MO9 z575}W$Hd~5ae_|aDD2?<U5Kg<uJc3zvGF*Wo6bT(m2<SpB9yP9>D3@AhSu|hdoZt4 z;NUoE<!C^8Y2K5N6=~^U4E7Xv1i9hb@q9rEUyS((T0h2pCyrDGqBUG#jf6H*mms1R zC9M->2gKT&kNNDg8GMW;mYSR3q-rI|z8`%h<FW&%r)RI&=QU9AFlRamjI(%&0oDI) zL=mrl6WEI7&5M@jd^7!PwJc;FCNvi%OUlPMIJy7<T4dbgLn}2N&lg$`0<|xDE8d*} zPkck;qP}#JT~M35u`z!p5d;klb5v@MM9Kx}Ck~l@{)ath$A#^_lHhb6t+;*P@`<;w zqJRa6BDXx*lJis3&=#=y8!8v|i6PqJ6nylyRJ6)UH>CN>oABh1ACK-up@VGJA${-d zYTsD=af4e;yS$bD@kyDGM7OqEgd7zQ$r;m)`}FxlSTVYE4lHKUT%=hSxs?`P@kVkZ zj57zzCS)&f>;rWc!>#q8DA^-uRH23R$onN2Yk;x=wKQIv=-a`YQ2?^1_v9jH!QO(^ z^D1e3H6~ZBGe`525{;+<D$a){2=2s~@H|%IctvDsHo<n-sHS6fRrgB>8+elfh%kB@ zOsnpt9?Mf#Fss$7GeaqkX3!10OTIV0vwRpzDZFnl-#`AW7&hg_6rm3{g?G^{w6*+< zg*FczNwhFu)A=SY(v;t8m-yJZ+dM1mLc?)B4dA$qK2K~Dwl>RqXV3IHkqyo6i?C-L zO5H3p?Gv@bfHLO^b*hcoZJ%A}lGJ~1tUs*uW`f=<=*_d!-}5<5dH-i&o;_z4^QqIB zrrq8H>baMS!SVV@Mr#=OF>Y8iE)<dkbPik1ejgj3<)<<?tG2=I59_EOKolE7wZN6H z1*Rhf76m<O_<WGu;wl@PM9<x5b`LEp5v}pNNzX@5XFfB9MyMaEY`UZ5o_tMy6Me?} zf~~vFph`iu9w|0f47AU}PI7ntOQ3};G)#$wD+99umZ!c<x6#f&njnD!%C5SYe=$VM ze>d(!56)szqk3}+jkTH!ch|dJT@np-s7Gb}XiEIa*9j&3=kLsgf-jj>ty4;BI|Y)S z6t9wd7SE=)YHJDM=@y7QntNF9d@1a(M)}_6RSfgiMTyK$XqqShjXv$`X0?IJ3VI6w z?bB)}ih@%8)xgV#(?CY1jS4=q1Y8GtnJ)Eu`fVK<{rI8RsXZus^rPot$W;ukK;ti_ ztKFZv7^xLtlp3qsd5Y<UK}cw0w%c=>nwh_%+)9LesTrn_y6!$16fTBYb+qVt8%-T5 zAN|;zkaL+}mDA7c4ciT&h6PB!r}*(OS(*5mB}n>9VRRx4<$_X2R@(W_pa=Xua`%m- zKr^A#|1~aG=YDd&&IJ!xwJO}HeJUYKJPxJoFEl|1p;~|Vu6&cIw+uaeYCvUSRb{1> zY<|gWko!wer-Pr%+X=Wk1kj#8#kwr<gq{4cRI6EKm#K@i_hA<*`@Mg5VQ=`}PX<8e z4{dw=wNd`yc%INe9$3>;r^N1dlpuUQzS0hYoPa||9hmhI5rsHpLmgC_uAyzh+PkC9 zQ@pZaVZLeJYM%@H8eS`>4BQ(VLGxt9RbcyX693I05KAA7d?;N|g!VosTH|C$L{W|? zj}wFOL!pEd|H9XE?NL|HK?3YN=)I%Fv~zjp$I5z-y_2IU9y=LBub~BeAp@n*jXtw{ zd2OYz2ytz6<s_?((dL!TvTB|TcKa;K+?nj?DiljV6$R3Y6QiWH2gsg-%*KX7YL9uE z-dG1{N^4*CDnh2SIny`AAHYG4O}z-Ad>Y<zsh0|#IO$XlWrL=2>e$MjJ-4J`HaVeD zA#b|ti4a+ZyrTp?B+Qf=Ur1@wTxa4=yg-s|Vn~%ZWD<2P)AS}ua2}&9k6D!LOwBK5 zwTmVP!YefU)N*22b*E#rNekfi%swm~CcDQ?w&6EV{22)0o6*!(Rq!`!2p2793P*k@ zBQ^$KOWt3{*tesO^~W6()*r9lQ8+KyQ6I;{yt2EZKAtXbZNCmeJLNDiS1PGd|F#v` z^z27?mSXf7E4XW5<~zM&Ee8$H^>gldui~)d_qt`odO@o^7u8r%VA^^ELeEiP$;|#z z(m?r+B{mxkXV6op3fg<25Thxi;Tr0|Nql<!;=)tLr)J$F7JNNaSv>h_#ye%p$@j+w zCad9JR&9y-e;l2dXum!ZMpuvOUTJtP!RwQ`@?MZGO26^V6rRU)<`g8Uc;vPLdS6s3 zix02lJ;}i@YMY|Q338AqdBo<jZMpL7*n0P!jFBB0hBwTG;f}jO6f{n*gO+J7pB2%& z8=~!hRNxADc`SI!BJCPy_ts~lQJTtMv87NjvvK5gn?K+px2HU>5R}96-YLCP>Yc5= zooOpXnPVp4xh}XG3(~eo%emc#_eIDaoC_z}+dYX|7ve)#6emG4bVDS~TlqUE)@|L` zX6`g>1k*(pi#+!}7P1s>HUNgUEALNbxEIJx#>J8h4=NZb_BID^N#|w^uu2Cv7M(c4 zn1)ysEd+3Zy)D8!AGRi}<9dzrfas^ATW1v9abydPHT3>Lo}f0*W3<hsr1b!iqRb;E z^-J^It!q#7W{d<9C&BPTOXAe~*RS@jGVATJ1a=%QMM=<gw`CX#ZWd8moupHGR!_aZ zq*5VbpA%jB<ZY6%x$yaPP^^4wb#&#=Y=%E=$`<;DV!hGi2H`zx0QM$?T;LNjQ(OJU zNu?heC|uo$Skq^MC-Zm!BR3~n?o7GzOoCa&^|o$obG<>QQ(WG$gffVTRD3cE8p*B2 zlq}%8g}Cw#K)&@>;qiV88~rbHtl*Rw94R(UnBb>;bx|qfRzJtGj6SdkSQy-z+nbd0 z{KOb09QYr3A4JHq)hbU2%(%F86(7Tc^C+au6(N;paHoqXuvhZP5!P;XUu0)bb2tA3 zV98$r6Qq)Z!SuUEg3Z3JcU12}(5ciXD(v1G|4~7L9<j^3wRfBz=0pIy0g1_xLz!yW z>rK4-;r7i30B`?^ofM|&t@jz}f*N(|EW4^rh*1Gc{j$0hn#LlouQvst70JGleWUA{ zun)!vg_5@cLE8Zy3OfMlC9fK!=v}v6$ZN^A49{;)*VyB=Y&dT~`vZ5a08+>|-PmY* z>qMA^iBoQ8&rd1ua$!B<TeR-fYb-k<Cyhrk(1=!9@vu3l-zTD6V@`B}ZO&8Em;bsi z83StGb&mfl*S^f13|2Z?5VLDo5ZN`q)QoaMo9j_;JNKZO4L;v&jx3JtZU)$oQX8rA z({9~<xU|EoA}qoy7<v4rHOx85b%jCwJHg4<P{d+qb3*=GVGBeSgl2%hOjf^UX6IZ! zL!1bHw5i?=*a}=emQ2K?1h!*&?|NqS0I`XUF8+C8LW_&Dl#1X!p}`tUW5bgzmE|Bs z>d0$2+bI;i)cTKp%TCg2eG@0z5@KwxV;4n_Z&^Pi=sgAVLze&%7_{=Hcsq2xvm{Pp zdm1zU+_T;LTLV10L%DUT4)r{7mr`?)N<(K7R#TO^u%#xNbWq|yJ6s0eqbrY)9?8=I zaJVD*TWrj59yXHJ6*{xZ<Pi8o>nukpBA!9v{w)lXYrG4G54=xi5U$<z{)*HKg)R_2 ziVAE)k86bbO(<a=VYR@(^gu@T=EQteMd9bUT0PN5VL`4+^wbqhcZNhwf+OlfsSD<V zJS%>8I<UHV^aB&AtprE;e)X*Jm7;EoOrR^DCd9z7V?(T9HKlw7ypaDL2S(T!3$3?q z@=bH_6Q7Oze63=%ifmP7MNek6Gq_L@i{iTYu0izH4F5W?!FlG28SGzUD68(}Yss{I zegkk*8Xh%v*n<80M!YtFa#vx_%6%0dFuiz^(L>h*s*Rg1U!1Vm`1tRu#$sYA$YG$E zMfolA?mTJ1LN+#TEUVKnX#}u!F*)YxdDjm@1wl-tq+rp!L5D2g-gj*Y>#OVbi$L<C zPWNk)^b=|80yrMKWjWGU-Yh7^$$$_*_YS``n*Roq>cS8wDOdh4iej40y$jF}>SBxA z*V{hMT*mX-x>Gww`9TVB0hBxPAYC0$AP*{H``ej<RsCF~Durag6X|KxNksS|%9161 zTlt^gf=@P}F7tha+hpELC-B=uc#5-a$Fy^IbsGho?F_mZ69u<e`jj)B=E<n`s_VYQ zNIjSP!z0yaw%bhi$Q(93xF?$weDH5=3!mG2f4jFn?XF7gJiGPNe_mXf$5{O)*riG` z|E$FPI~D1BV`~lRJ$8$HZtpV)wl3>_q+a{C*(uwVc`1LW%gYpQ{=9zH&5;3hJLSBT z9kI11PnmzUb|L3G=&k<gqM@lw)KslXK_2&lxbsJ~QuN11Aq|+xOn%s)1A%Ao;hu?S ztG!u5jSaCd!`KyRBJR=Zh|aU6Tix-RFP@;~8YQVoH18&zwP<(ftm2f-XrUt(R>U*b zR+Wnd3K(_JKx5x~(#JR*NMwmvbhD>3(z<P8{`E){Q74*M@!@&S!@oBh1poE>j_KaF z`BA3?@+IvNC7+ZUXj<_4l_N63hRq4tYx28F*tWtQnE9x+i#<<-`Bm9UX5sWZLs;s! z9hD4=7td3=8GI!XW4?zEtW2);eANFC6Tj<WlQZd}LP&%89{uGa<nwpi%v+Ra4k^Tn z!c3PveWHMn^E|~yh*GbME>+j)CwY{AtSvZ>U6qZx;|~;m3m6~2*n35pUp=cCfkB7F zhaT@2gM_WS_k->mA$fWfJTk*>{qfx^m6P!Z1N-i$pWLbI=}Ts=65J=xWoyo=D!Z1i z>NC-`jB{V+(h5~Ovg_0Cm=N*=M<{8(E;_U;u>J}mJ{_S<<em8S?IR5hyYbW)uurUs z!DenW@n`nnbN5o&%|HGI!(zrcN8R)EMB!&u{E=N#B@Gymr6p&J=4~iS68B|l1z=LJ zJFuPUEszo)Ai`$SKfA=xOcjz|dVlKMKNHgJhZ_o5qakEH^yCdURW1rL&D(HFUEE_9 zFv5$;t~G^M&;DzmTysuU=wDr&leu45K~86X{&U63R2^?`XS2qJ;f>!erJE!k#qMmT zQaH;osirjrjtrG{Ywg%A5Io<rRsLe2v7EHHVbXF;ZvU*{M>~FI)B1CKL2)aE9Cz(p z`kia}IU|1$P%)H_tczcIOFchdf9dxx>X3JdvOm3V;i`&?7^n){gHma8c(lI{M}1gM zCaurKT>1;!8dKq;N$X|w<_Zcrym?IWsu?7^yH3|Ek1D+m)meh9bCkk7mi;`-MgtDa zo8cxR(bm;z=($H?nv$6L?5AG*x40)pOD(C^zRGx?d_Cpddbqo!=qx8^3tBz{pF!0t zzGxZlX)ZmG4cVocRqpj_Wa+7rt8@#22UO&Z=4k#{Wt7Iu|0QwGRfkA;lxNOlTw7vx zTh#G?8GQL~Wv?vUWfAnJ9d;+{GT3Ze-g9Eu&bam^BT9aA<uT>gpt{-Zk^99#Njz+5 zZ8VF<tQ5S=6}u3C=wq__R)u$&m=cGmEsc>C+8YK%HowACE<=?L19J-c+?GM-qkEqB z-NY|_UJ?SUCc-N@#J)=w&6iC*Ex4ezTHW>sK_?5}W9pw0ol^c$kJQuy;PJZGJ151W zu|X4uzSu=oD%~#iN{-Wyu$#YWs`%QqOMAyO3cdK2K*ng#5()YpV^^1Rl(ih6FMq+d z%sfX&j~%7ahGhf?z^?y_x_=Z~3sZ_EDbLsBv#(zL1$U>s*5Jz11{&qh1FI9VHWZs~ zS_z6#-(ShdevzMIBQE&qsFMa7WSOb*y6rjf7)`WMs@5z{;rBK3cc@<rre~z>&sZbb z;Sjk1>V^(GTRhs|a>(Wa8pvEIMzXu1F7D~uiYZ2k`G@%&RlzUufk?5c%++eKy3|z{ zk3Gmbv-6|gjolPsy3HV=R&)5wn+_~VpoxI3cNQofn_rp+X*UrIoAmK8TM>bH#OYk{ zzA4VEX|YmB$kR#m#MTSR^QPn^yU-5s#!f(Yn19V+PYly8w`IP>EM4=k`^J1X%k6Ie z*0D~}saMwtnO#wXtjL9M%r6xM&7Ncnt4asHs6<IOMD;AS(jAB%wi8^sqIq~{QR-s= zO5FeO_(kOf1L}Qi)(+xkRpjk53%WJk$rB_zxhSHS1m;vPMjT02YT_*6igX9lmOnj; zm=EtMBTm7Yo~vuE6kp42RtL!Sr%ylZt^t4GE%amrf}mkmf~CC`q`JWkpHhFfL(mk; z7p7Fz^qYOmeeHc^+QkL*bfjmG8AbIY{S*2EX(7ERPp!YAGo8fM+Kgo~QbK%~|J2g- zr&a8(!4EY#L6!`((nsP<o4OI!O!q18Qd~3s0*ERpnf~y12SSWp@f9VZ6%N<hgz$$q z-pHg}rn7c0*YdoTc=jaH4t9QCfq6qg*0%<3)7`n1SgC;BR=Re<y2*~*Zyh^VSRCT~ zsY_=akIp|6xfo=bxTe6y(88Btj*HSvG|&Cy7PGJY1RHc;;iGDr?OD!vd66e5xUu5* zGuxFntwT~IyRb4=Pq2ipLv=Ru!pJKNjr4i?Q%0DI*|{88HK`%^A)cdy)`H42lMUt` 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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;`1S<Q9P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0004nX+uL$Nkc;* zaB^>EX>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&<O_x)MDYSv<aPb7{q!>kZ*5YKE@ z49@$+Ay$x8;&b8&lP*a7$aTf#H_my71)do)Q^|Sa5V4qVW2KE*!PJPSiNmU<Q@)UP zS>?RNSu0gp^Pc>L!K}Wr%yn8Lh+`2;kRU=q6-AU#L4<al6bnf@k9qiq9lt~_g<M52 zax9<>4YK10|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#<wa@L}p62|10EPr|uIeLrk^lez24YJ`L;(K){{a7>y{D4^000SaNLh0L z01m?d01m?e$8V@)00007bV*G`2k8M83nwU6s5EB)000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}00084Nkl<ZNXNaFO-NKx6vuz>&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&#i0hc0vCAKDky<?>rcmNprJDUN#5x>%_b*gFLXMo^3z>(hsB}Qfja^*ob z#ttPiQ8YDwzs{BCDwi$dS7LhU=uChsz{Ar}?||iHxY-SXcOvLoGqmhUqQqz5%?MoY zgZN@bxjKPUij?Abx>_D-NuEGpNcgWFfWwWc%Z+}6wp%c<kfE+^I<wn%64;-fV^0`% zSAx|7E(iSj4#N`=8U^Mc`V~|KN=u-)2&#&qZzKgm2aM}w6J^*157=wOSg(h(M@0GV z4#DkL$+&xMu&WMSJF@PDN7Z!as-15D??EWs48K<(7Kf=BXl(>fWoqopx8hxBfZY`t z1hx8X1T78XJv|GT127QHEf>?4<alyElR!=PY&XKzVljT!pC8;^4y{dMJT(iSW;0{- z^3ZZ597TrY<kVbRK`Y_nF({DYHk-so0|W=Mu3=1-1gjQ~!B45_y{^>g&=+_b0+SIe zr2>3m5xQQ$jT{6zv)fk&?9+Pd%Anc`<z{HIfwL5xW*Ce@ctS|l<p5QMkx@AJJZBZ} z=*({4XP`ssR%G!JJWio+lMx&>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*Fq<ZsI#VCUP5#t>lh00000NkvXXu0mjfp@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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 literal 0 HcmV?d00001 diff --git a/assets/images/decred_menu.png b/assets/images/decred_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..5c67923c57c9b9f2fc269a3099a56cf105d008f0 GIT binary patch literal 1482 zcmV;*1vUDKP)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T70004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iTeYQBL@X%ckfA!+MMWHI6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RR<SmT^(EnLGS~_)x}BCMN0f%QfLw5!Ery{-Fw`<1N@B&Q_ZdfplX(p zNhO6`enkkrB7`t{(2qfhnfk0Krr|lh?&0I>U7TlmpZjz4Dg~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`}^3on<qfv8MxBg{%Ql5`6RvG z)?!CM-!^b@-PYti;Bp5TdeS9Ba-;xFf3XO>pV2qvfq`2fyz2JW+Q;bwkfpBDH^9Lm zFjk`MHIH|PI(z%~Osl^iI~sD3>NA5u00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=LiZ1A_P6izJmY&02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00W{)L_t(o!_}G3OB+EH$3MFma+F?+AmYJ;w^D)<Y=w~P zQt1y{idb(wGzGm$pv*-)6e_sw*^0CxrB(J;ieL$$;=P9+f(XHXp~#lBhrZBtnVFq6 zMC^lHlG&Z_zW06K_nnPL&UmgO|2GT+C13_914D-8ZJ-J40!JR4tgE1tXFo3|urja$ zOaO(P&lUrRK;46;D^MqZH5E+>urlD&TXZC=^-u+ko&eTVR0LLmXBWtrOGaM<%b^O+ zas#ZXXax8IjMCp|0o70i$Ibu++&(bE<-{>i(Sh`?05~g1Ps6$IQdSc$3YtcVh5V){ z<|y}gl9TC;Rs~!P+yDkOpE>zG87~ZL2Z3wAJ>Vhm!Sdk9-mBUSij?dpy4A>wnu;C) zn~AkCFdwR5+d;BMfem2F(%qG=1o>SN7M)8<$q2A7oIDo-WImC}0<bB-tr4oA1yq0~ zO(nYbS`kct0m_6ZYZmtlWE`qsO>|HfWx<3eBkaJE@{5%F3GlHyIYJf0hH@8#R=T|# z9!z_ZogWU9q6JKcDrh@^>?AUK3LF7%bfZnUe_&9CF<)GX3%CW@42N?2A}vLs$*fRC zSN=ob#K6UpFP6y`+oDa4(BeQeAaxsVr1>o6KQdf1;~SbI32xpt%yWV)0(Ugu@0yxr zz+yI!fdw7Gh1@0Jy5_oHKm^=1+;<XK(^xQF3=P;4qRU)OMYE#z!x!|1-!$JD?=xUg z^F2eyZS~*{&vf^-^?`43y{9Q~x@nUa+(KGQ6LiUyK~oo&EDyA6;Zj>Dqcf!wLYJNa zX2qva1@H4*AlX~7anP5ib3H>GS?=@JWe%snZDx$vwl60vT{&;2G_C{GGlba5EQsaW z2eQ^SeS=8)z<rGnH^i_W8~Bz9vZkU%aqUwMiQe_4Y`UyGHQ1~jU<|kgJdn?0;HM+V zJ=09rQ7X%B?ErHoyOhezZQyg_{9$z2jt8@b2h;xgKW^ztHVf251GDDvSD>0`I%6h9 zZYn|*lWf)3RnYO}VA3=+3&2ZYOHO=QARA)kdj;7POx8n_&OC&|p%a_+s;Q_9yh{jh zR=KwluIk*ODSa+}1uQ!1VHVLRQ%)%sm!o^IAz2Tw7OG&$G)b0$Ame>ZA;Ux3uDk2t z+?S1JLF`r4Lf(GA`lzNY-j#oO<QuxN+hMIs0M{~svpQM1U|j5Z+y`K>+<V*v?*D%U koQx~|5?urI(*B0{1E!mzip#o~umAu607*qoM6N<$g6@i%1ONa4 literal 0 HcmV?d00001 diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 908897845..7135d0a7a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -92,6 +92,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }); } + @override + bool get hasRescan => true; + static Future<BitcoinWallet> 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<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List<int> 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<bool> 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<BalanceType extends Balance, HistoryType extends Trans bool get isHardwareWallet => walletInfo.isHardwareWallet; + bool get hasRescan => false; + Future<void> 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<BalanceType extends Balance, HistoryType extends Trans Future<bool> 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 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.cakewallet.cw_decred"> + <uses-permission android:name="android.permission.INTERNET"/> +</manifest> 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<int, Completer<Object?>> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + static Future<Libwallet> 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<String, String> args) = message as (int, Map<String, String>); + 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<void> initLibdcrwallet(String logDir) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "initlibdcrwallet", + "logdir": logDir, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<void> createWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<void> createWatchOnlyWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwatchonlywallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<void> loadWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "loadwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<void> startSync(String walletName, String peers) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "startsync", + "name": walletName, + "peers": peers, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<void> closeWallet(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "closewallet", + "name": walletName, + }; + _commands.send((id, req)); + await completer.future; + } + + Future<String> changeWalletPassword( + String walletName, String currentPassword, String newPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String?> walletSeed(String walletName, String walletPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> syncStatus(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<Map> balance(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> estimateFee(String walletName, int numBlocks) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> createSignedTransaction( + String walletName, String createSignedTransactionReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> sendRawTransaction(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> listTransactions(String walletName, String from, String count) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> bestBlock(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> listUnspents(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> rescanFromHeight(String walletName, String height) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> signMessage( + String walletName, String message, String address, String walletPass) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> verifyMessage( + String walletName, String message, String address, String sig) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String?> newExternalAddress(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> defaultPubkey(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> addresses(String walletName, String nUsed, String nUnused) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<String> birthState(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<void> shutdown() async { + if (_closed) throw StateError('Closed'); + final completer = Completer<Object?>.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<Char> fn(), + required List<Pointer> 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<Pointer> ptrsToFree) { + for (final ptr in ptrsToFree) { + malloc.free(ptr); + } +} + +void checkErr(String err) { + if (err == "") return; + throw Exception(err); +} + +extension StringUtil on String { + Pointer<Char> toCString() => toNativeUtf8().cast<Char>(); +} + +extension CStringUtil on Pointer<Char> { + bool get isNull => address == nullptr.address; + + free() { + malloc.free(this); + } + + String? toDartString() { + if (isNull) return null; + + final str = cast<Utf8>().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 = <String>[ + "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<void> 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<void> commit() async { + return send(); + } + + @override + Future<String?> 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<OutputInfo> 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<TransactionInfo> { + DecredTransactionHistory() { + transactions = ObservableMap<String, TransactionInfo>(); + } + + @override + void addOne(TransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map<String, TransactionInfo> transactions) => this.transactions.addAll(transactions); + + @override + Future<void> save() async {} + + // update returns true if a known transaction that is not pending was found. + bool update(Map<String, TransactionInfo> 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<DecredTransactionPriority> 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<DecredBalance, DecredTransactionHistory, DecredTransactionInfo> with Store { + DecredWalletBase(WalletInfo walletInfo, String password, Box<UnspentCoinsInfo> 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 = <Unspent>[]; + + // 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> unspentCoinsInfo; + + @override + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap<CryptoCurrency, DecredBalance> 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<void> 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<void> 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<void> 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<void> 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<bool> 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<void> 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<void> startSync() async { + if (connecting) { + return; + } + connecting = true; + await this._startSync(); + connecting = false; + } + + Future<void> _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<PendingTransaction> 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<Map<String, DecredTransactionInfo>> fetchTransactions() async { + return this.fetchFiveTransactions(0); + } + + Future<Map<String, DecredTransactionInfo>> fetchFiveTransactions(int from) async { + final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); + final decoded = json.decode(res); + var txs = <String, DecredTransactionInfo>{}; + 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<void> save() async {} + + @override + bool get hasRescan => walletBirthdayBlockHeight() != -1; + + @override + Future<void> 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<void> 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<void> changePassword(String password) async { + if (watchingOnly) { + return; + } + return () async { + await _libwallet.changeWalletPassword(walletInfo.name, _password, password); + }(); + } + + @override + Future<void> 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<void> 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<String> 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<void> fetchUnspents() async { + final res = await _libwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = <Unspent>[]; + 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<Unspent> unspents() { + this.updateUnspents(_unspents); + return _unspents; + } + + void updateUnspents(List<Unspent> 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<dynamic> keys = <dynamic>[]; + 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<int> 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<bool> 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<void> 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<void> 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<AddressInfo> getAddressInfos() { + if (addressInfos.containsKey(0)) { + return addressInfos[0]!; + } + return <AddressInfo>[]; + } + + Future<void> 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> 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<String>.from(decoded["used"] ?? []); + final unusedAddrs = List<String>.from(decoded["unused"] ?? []); + // index is the index of the first unused address. + final index = decoded["index"] ?? 0; + return new LibAddresses(usedAddrs, unusedAddrs, index); + } + + Future<void> 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<String> 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<WalletInfo> walletInfoSource; + final Box<UnspentCoinsInfo> 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<void> 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<bool> isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future<DecredWallet> 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<DecredWallet> 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<void> 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<void> 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<DecredWallet> 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<DecredWallet> 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<DecredWallet> 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<MoneroBalance, @observable ObservableMap<CryptoCurrency, MoneroBalance> 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 @@ <string>wownero-wallet</string> </array> </dict> + <dict> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>CFBundleURLName</key> + <string>zano</string> + <key>CFBundleURLSchemes</key> + <array> + <string>zano</string> + </array> + </dict> + <dict> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>CFBundleURLName</key> + <string>zano-wallet</string> + <key>CFBundleURLSchemes</key> + <array> + <string>zano-wallet</string> + </array> + </dict> + <dict> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>CFBundleURLName</key> + <string>decred</string> + <key>CFBundleURLSchemes</key> + <array> + <string>decred</string> + </array> + </dict> + <dict> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>CFBundleURLName</key> + <string>decred-wallet</string> + <key>CFBundleURLSchemes</key> + <array> + <string>decred-wallet</string> + </array> + </dict> </array> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> 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<MnemonicItem> { 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<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { + return DecredWalletService(walletInfoSource, unspentCoinSource); + } + + @override + List<TransactionPriority> 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<Output> 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<AddressInfo> getAddressInfos(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.walletAddresses.getAddressInfos(); + } + + @override + Future<void> updateAddress(Object wallet, String address, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.updateAddress(address, label); + } + + @override + Future<void> 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<Unspent> 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<String> 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<void> setup({ (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param2: false), editingNode: editingNode, - isSelected: isSelected)); + isSelected: isSelected, + type: getIt.get<AppStore>().wallet!.type)); getIt.registerFactoryParam<PowNodeCreateOrEditPage, Node?, bool?>( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( @@ -1115,6 +1117,8 @@ Future<void> 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<void> defaultSettingsMigration( {required int version, @@ -96,30 +97,67 @@ Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> _changeDefaultNode({ required Box<Node> nodes, required SharedPreferences sharedPreferences, required WalletType type, - required String newDefaultUri, required String currentNodePreferenceKey, - required bool useSSL, - required List<String> - 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<String>? + 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<void> _changeDefaultNode({ uri: newDefaultUri, type: type, useSSL: useSSL, + trusted: trusted, ); await nodes.add(newNode); @@ -485,23 +598,38 @@ Future<void> _changeDefaultNode({ } } -/// Generic function for adding a new Node for a Wallet Type. -Future<void> updateWalletTypeNodesWithNewNode({ - required Box<Node> 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<void> _fixNodesUseSSLFlag(Box<Node> nodes) async { } Future<void> updateNanoNodeList({required Box<Node> nodes}) async { - final nodeList = await loadDefaultNanoNodes(); + final nodeList = await loadDefaultNodes(WalletType.nano); var listOfNewEndpoints = <String>[ "app.natrium.io", "rainstorm.city", @@ -682,87 +810,16 @@ Future<void> replaceNodesMigration({required Box<Node> nodes}) async { }); } -Future<void> changeMoneroCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> 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<Node> nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); -} - Node? getBitcoinTestnetDefaultElectrumServer({required Box<Node> nodes}) { return nodes.values .firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } -Node? getLitecoinDefaultElectrumServer({required Box<Node> nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletLitecoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.litecoin); -} - -Node? getHavenDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == havenDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); -} - -Node? getEthereumDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == ethereumDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); -} - -Node? getPolygonDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == polygonDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.polygon); -} - -Node? getNanoDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); -} - -Node? getNanoDefaultPowNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? - nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); -} - -Node? getBitcoinCashDefaultElectrumServer({required Box<Node> nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); -} - -Node? getZanoDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull( - (Node node) => node.uriRaw == zanoDefaultNodeUri) - ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.zano); -} - -Node getMoneroDefaultNode({required Box<Node> nodes}) { - var nodeUri = newCakeWalletMoneroUri; - - try { - return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); - } catch (_) { - return nodes.values.first; - } -} - -Node? getSolanaDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); -} - -Node? getTronDefaultNode({required Box<Node> nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +Node? getDefaultNode({required Box<Node> 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<Node> nodes}) { @@ -899,63 +956,6 @@ Future<void> rewriteSecureStoragePin({required SecureStorage secureStorage}) asy ); } -Future<void> changeBitcoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, - required Box<Node> 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<void> changeLitecoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final server = getLitecoinDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentLitecoinElectrumSererIdKey, serverId); -} - -Future<void> changeBitcoinCashCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); -} - -Future<void> changeHavenCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getHavenDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, nodeId); -} - -Future<void> replaceDefaultNode( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - const nodesForReplace = <String>[ - '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<void> updateNodeTypes({required Box<Node> nodes}) async { nodes.values.forEach((node) async { if (node.type == null) { @@ -965,42 +965,6 @@ Future<void> updateNodeTypes({required Box<Node> nodes}) async { }); } -Future<void> addBitcoinElectrumServerList({required Box<Node> 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<void> addLitecoinElectrumServerList({required Box<Node> 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<void> addBitcoinCashElectrumServerList({required Box<Node> 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<void> addHavenNodeList({required Box<Node> 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<void> addAddressesForMoneroWallets(Box<WalletInfo> walletInfoSource) async { final moneroWalletsInfo = walletInfoSource.values.where((info) => info.type == WalletType.monero); moneroWalletsInfo.forEach((info) async { @@ -1050,31 +1014,6 @@ Future<void> changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPre bitcoin!.getMediumTransactionPriority().serialize()); } -Future<void> changeDefaultMoneroNode( - Box<Node> 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<void> fixBtcDerivationPaths(Box<WalletInfo> walletsInfoSource) async { for (WalletInfo walletInfo in walletsInfoSource.values) { if (walletInfo.type == WalletType.bitcoin || @@ -1102,128 +1041,6 @@ Future<void> updateBtcNanoWalletInfos(Box<WalletInfo> walletsInfoSource) async { } } -Future<void> changeDefaultNanoNode( - Box<Node> 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<void> changeDefaultBitcoinNode( - Box<Node> 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<void> _addSethNode(Box<Node> nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: "fulcrum.sethforprivacy.com:50002", - useSSL: false, - ); -} - -Future<void> _addElectRsNode(Box<Node> nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: cakeWalletSilentPaymentsElectrsUri, - ); -} - -Future<void> _addBitcoinNode({ - required Box<Node> 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<void> _switchElectRsNode(Box<Node> 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<void> checkCurrentNodes( Box<Node> nodeSource, Box<Node> powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -1236,6 +1053,7 @@ Future<void> 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<void> 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<void> 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<void> 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<void> resetBitcoinElectrumServer( @@ -1385,15 +1212,6 @@ Future<void> resetBitcoinElectrumServer( await oldElectrumServer?.delete(); } -Future<void> changeDefaultHavenNode(Box<Node> 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<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async { final isExchangeDisabled = sharedPreferences.getBool(PreferencesKey.disableExchangeKey); if (isExchangeDisabled == null) { @@ -1406,65 +1224,14 @@ Future<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async { await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } -Future<void> addEthereumNodeList({required Box<Node> 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<void> changeEthereumCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getEthereumDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); -} - -Future<void> addWowneroNodeList({required Box<Node> 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<void> addZanoNodeList({required Box<Node> 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<void> changeWowneroCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box<Node> 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<void> changeZanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getZanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, nodeId); -} - -Future<void> addNanoNodeList({required Box<Node> 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<void> addNanoPowNodeList({required Box<Node> nodes}) async { final nodeList = await loadDefaultNanoPowNodes(); for (var node in nodeList) { @@ -1474,23 +1241,13 @@ Future<void> addNanoPowNodeList({required Box<Node> nodes}) async { } } -Future<void> changeNanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getNanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, nodeId); +Node? getNanoDefaultPowNode({required Box<Node> nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? + nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); } -Future<void> changeNanoCurrentPowNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getNanoDefaultPowNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, nodeId); -} - -Future<void> addPolygonNodeList({required Box<Node> nodes}) async { - final nodeList = await loadDefaultPolygonNodes(); +Future<void> addWalletNodeList({required Box<Node> nodes, required WalletType type}) async { + final List<Node> 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<void> addPolygonNodeList({required Box<Node> nodes}) async { } } -Future<void> changePolygonCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getPolygonDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); -} - -Future<void> addSolanaNodeList({required Box<Node> 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<void> changeSolanaCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getSolanaDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); -} - -Future<void> addTronNodeList({required Box<Node> 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<void> changeTronCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { - final node = getTronDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); -} - -Future<void> replaceTronDefaultNode({ - required SharedPreferences sharedPreferences, - required Box<Node> 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<void> removeMoneroWorld( {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; @@ -1582,19 +1271,13 @@ Future<void> 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<void> updateTronNodesWithNowNodes({ - required SharedPreferences sharedPreferences, - required Box<Node> 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<List<Node>> loadDefaultNodes() async { - final nodesRaw = await rootBundle.loadString('assets/node_list.yml'); +Future<List<Node>> 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 = <Node>[]; for (final raw in loadedNodes) { if (raw is Map) { final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.monero; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadBitcoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = <Node>[]; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.bitcoin; - serverList.add(node); - } - } - - return serverList; -} - -Future<List<Node>> loadLitecoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/litecoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = <Node>[]; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.litecoin; - serverList.add(node); - } - } - - return serverList; -} - -Future<List<Node>> loadDefaultHavenNodes() async { - final nodesRaw = await rootBundle.loadString('assets/haven_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.haven; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadDefaultEthereumNodes() async { - final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.ethereum; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadBitcoinCashElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_cash_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = <Node>[]; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.bitcoinCash; - serverList.add(node); - } - } - - return serverList; -} - -Future<List<Node>> loadDefaultNanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/nano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - node.type = WalletType.nano; + node.type = type; nodes.add(node); } } @@ -132,103 +83,19 @@ Future<List<Node>> loadDefaultNanoPowNodes() async { return nodes; } -Future<List<Node>> loadDefaultPolygonNodes() async { - final nodesRaw = await rootBundle.loadString('assets/polygon_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - - node.type = WalletType.polygon; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadDefaultSolanaNodes() async { - final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - - node.type = WalletType.solana; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadDefaultTronNodes() async { - final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - - node.type = WalletType.tron; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadDefaultWowneroNodes() async { - final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - - node.type = WalletType.wownero; - nodes.add(node); - } - } - - return nodes; -} - -Future<List<Node>> loadDefaultZanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/zano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = <Node>[]; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map<String, Object>.from(raw)); - - node.type = WalletType.zano; - nodes.add(node); - } - } - - return nodes; -} - Future<void> resetToDefault(Box<Node> 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<void> resetToDefault(Box<Node> 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<TransactionPriority> 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<void> 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<DesktopWalletSelectionD final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); final wowneroIcon = Image.asset('assets/images/wownero_icon.png', height: 24, width: 24); final zanoIcon = Image.asset('assets/images/zano_icon.png', height: 24, width: 24); + final decredIcon = Image.asset('assets/images/decred_icon.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); Image _newWalletImage(BuildContext context) => Image.asset( @@ -181,6 +182,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD return tronIcon; case WalletType.zano: return zanoIcon; + case WalletType.decred: + return decredIcon; default: return nonWalletTypeIcon; } diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index ae0605fff..cef313780 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_row_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/info_card.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; @@ -280,81 +281,50 @@ class CryptoBalanceWidget extends StatelessWidget { SizedBox(height: 16), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - marginV: 0, - marginH: 0, - customBorder: 30, + child: InfoCard( title: S.of(context).litecoin_mweb, - subTitle: S.of(context).litecoin_mweb_description, - hint: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse("https://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<BalancePageTheme>()!.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<BalancePageTheme>()!.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<MenuWidget> { 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<MenuWidget> { Image tronIcon; Image wowneroIcon; Image zanoIcon; + Image decredIcon; @override void initState() { @@ -250,6 +252,8 @@ class MenuWidgetState extends State<MenuWidget> { 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<FormState>(), _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<FormState> 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<BlockchainHeightState>(); @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<BlockchainHeightState> _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<void>( 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<WalletRestoreFromKeysForm> { 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<WalletRestoreFromKeysForm> { } 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<WalletKeysPageBody> 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<WalletKeysPageBody> 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<WalletKeysPageBody> ), ), 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<WalletKeysPageBody> ), ); } - + 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<WalletListBody> { 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<WalletListBody> { final double tileHeight = 60; Flushbar<void>? _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<WalletListBody> { } Future<void> _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<WalletListBody> { }, 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<WalletListBody> { .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<BlockchainHeightWidget> { 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<SeedWidget> { }); 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<WalletType, Node>.of(nodes), powNodes = ObservableMap<WalletType, Node>.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<TransactionListItem>(), 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<Trade> trades; @@ -316,8 +319,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isMoneroWallet => wallet.type == WalletType.monero; - - List<CryptoCurrency> receiveCurrencies; List<CryptoCurrency> 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<String, dynamic> 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<void> _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<void> _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<WalletRestoreMode> 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<void> main(List<String> 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<void> main(List<String> args) async { await generateWownero(hasWownero); await generateZano(hasZano); // await generateBanano(hasEthereum); + await generateDecred(hasDecred); await generatePubspec( hasMonero: hasMonero, @@ -59,6 +62,7 @@ Future<void> main(List<String> args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await generateWalletTypes( hasMonero: hasMonero, @@ -73,6 +77,7 @@ Future<void> main(List<String> 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<void> 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<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource); + + List<TransactionPriority> getTransactionPriorities(); + TransactionPriority getDecredTransactionPriorityMedium(); + TransactionPriority getDecredTransactionPrioritySlow(); + TransactionPriority deserializeDecredTransactionPriority(int raw); + + Object createDecredTransactionCredentials(List<Output> outputs, TransactionPriority priority); + + List<AddressInfo> getAddressInfos(Object wallet); + Future<void> updateAddress(Object wallet, String address, String label); + Future<void> generateNewAddress(Object wallet, String label); + + String formatterDecredAmountToString({required int amount}); + double formatterDecredAmountToDouble({required int amount}); + int formatterStringDoubleToDecredAmount(String amount); + + List<Unspent> getUnspents(Object wallet); + void updateUnspents(Object wallet); + + int heightByDate(DateTime date); + + List<String> 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<void> generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1496,6 +1584,7 @@ Future<void> generatePubspec({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { const cwCore = """ cw_core: @@ -1564,6 +1653,10 @@ Future<void> 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<void> generatePubspec({ output += '\n$cwSharedExternal\n$cwHaven'; } + if (hasDecred) { + output += '\n$cwDecred'; + } + if (hasFlutterSecureStorage) { output += '\n$flutterSecureStorage\n'; } @@ -1654,6 +1751,7 @@ Future<void> generateWalletTypes({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { final walletTypesFile = File(walletTypesPath); @@ -1709,6 +1807,10 @@ Future<void> generateWalletTypes({ outputContent += '\tWalletType.banano,\n'; } + if (hasDecred) { + outputContent += '\tWalletType.decred,\n'; + } + if (hasWownero) { outputContent += '\tWalletType.wownero,\n'; }