diff --git a/.github/ISSUE_TEMPLATE/bug-report-🪲-.md b/.github/ISSUE_TEMPLATE/bug-report-🪲-.md new file mode 100644 index 000000000..457cb84a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-🪲-.md @@ -0,0 +1,33 @@ +--- +name: "Bug Report \U0001FAB2 " +about: 'Report a bug ' +title: '' +labels: Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform:** + - OS: [e.g. iOS 15.1, Android 14] + - Device: [e.g. iPhone 14, Galaxy S21] + - Cake Wallet Version: [e.g. 4.12.1] + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..d7a1a3ed9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Not sure where to start? + url: https://guides.cakewallet.com + about: Start by reading checking out the guides! + - name: Need help? + url: https://cakewallet.com/#contact + about: Use our live chat or send a support email! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md new file mode 100644 index 000000000..20bf2d53f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md @@ -0,0 +1,20 @@ +--- +name: Feature or Enhancement Request ✨ +about: Suggest an idea for Cake Wallet +title: '' +labels: Enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/assets/Logo_CakeWallet.png b/.github/assets/Logo_CakeWallet.png new file mode 100644 index 000000000..459a6b37c Binary files /dev/null and b/.github/assets/Logo_CakeWallet.png differ diff --git a/.github/assets/NOTICE.txt b/.github/assets/NOTICE.txt new file mode 100644 index 000000000..9719639a1 --- /dev/null +++ b/.github/assets/NOTICE.txt @@ -0,0 +1,48 @@ +Notice for linux-badge.svg: + +1: +This is the Linux-penguin again... + +Originally drewn by Larry Ewing (http://www.isc.tamu.edu/~lewing/) +(with the GIMP) the Linux Logo has been vectorized by me (Simon Budig, +http://www.home.unix-ag.org/simon/). + +This happened quite some time ago with Corel Draw 4. But luckily +meanwhile there are tools available to handle vector graphics with +Linux. Bernhard Herzog (bernhard@users.sourceforge.net) deserves kudos +for creating Sketch (http://sketch.sourceforge.net), a powerful free +tool for creating vector graphics. He converted the Corel Draw file to +the Sketch native format. Since I am unable to maintain the Corel Draw +file any longer, the Sketch version now is the "official" one. + +Anja Gerwinski (anja@gerwinski.de) has created an alternate version of +the penguin (penguin-variant.sk) with a thinner mouth line and slightly +altered gradients. It also features a nifty drop shadow. + +The third bird (penguin-flat.sk) is a version reduced to three colors +(black/white/yellow) for e.g. silk screen printing. I made this version +for a mug, available at the friendly folks at +http://www.kernelconcepts.de/ - they do good stuff, mail Petra +(pinguin@kernelconcepts.de) if you need something special or don't +understand the german :-) + +These drawings are copyrighted by Larry Ewing and Simon Budig +(penguin-variant.sk also by Anja Gerwinski), redistribution is free but +has to include this README/Copyright notice. + +The use of these drawings is free. However I am happy about a sample of +your mug/t-shirt/whatever with this penguin on it... + +Have fun + Simon Budig + + +Simon.Budig@unix-ag.org +http://www.home.unix-ag.org/simon/ + +Simon Budig +Am Hardtkoeppel 2 +D-61279 Graevenwiesbach + +2: +Attribution: lewing@isc.tamu.edu Larry Ewing and The GIMP \ No newline at end of file diff --git a/.github/assets/app-store-badge.svg b/.github/assets/app-store-badge.svg new file mode 100755 index 000000000..072b425a1 --- /dev/null +++ b/.github/assets/app-store-badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/assets/devices.png b/.github/assets/devices.png new file mode 100644 index 000000000..7bdccc5b5 Binary files /dev/null and b/.github/assets/devices.png differ diff --git a/.github/assets/f-droid-badge.png b/.github/assets/f-droid-badge.png new file mode 100644 index 000000000..2c9521de1 Binary files /dev/null and b/.github/assets/f-droid-badge.png differ diff --git a/.github/assets/google-play-badge.png b/.github/assets/google-play-badge.png new file mode 100644 index 000000000..9667c568d Binary files /dev/null and b/.github/assets/google-play-badge.png differ diff --git a/.github/assets/linux-badge.svg b/.github/assets/linux-badge.svg new file mode 100755 index 000000000..8416e1bb1 --- /dev/null +++ b/.github/assets/linux-badge.svg @@ -0,0 +1,1071 @@ + +linux-badgeGET IT ONLinuxlinux-badge diff --git a/.github/assets/mac-store-badge.svg b/.github/assets/mac-store-badge.svg new file mode 100755 index 000000000..c36a76a5a --- /dev/null +++ b/.github/assets/mac-store-badge.svg @@ -0,0 +1,51 @@ + + Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 4d2dc136c..bf0d0f7bc 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -1,6 +1,7 @@ name: Cache Dependencies on: + workflow_dispatch: push: branches: [ main ] @@ -13,12 +14,12 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '8.x' + java-version: '11.x' - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: '3.3.x' + flutter-version: '3.10.x' channel: stable - name: Install package dependencies @@ -45,7 +46,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh') }} + key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 4ca762c12..dc231df42 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -2,26 +2,47 @@ name: PR Test Build on: pull_request: - branches: [ main ] + branches: [main] + workflow_dispatch: + inputs: + branch: + description: "Branch name to build" + required: true + default: "main" jobs: PR_test_build: - runs-on: ubuntu-20.04 env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet + PR_NUMBER: ${{ github.event.number }} steps: + - name: is pr + if: github.event_name == 'pull_request' + run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV + + - name: is not pr + if: github.event_name != 'pull_request' + run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV + + - name: Free Up GitHub Actions Ubuntu Runner Disk Space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '8.x' + java-version: "11.x" - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.x' + flutter-version: "3.10.x" channel: stable - name: Install package dependencies @@ -32,10 +53,13 @@ jobs: sudo mkdir -p /opt/android sudo chown $USER /opt/android cd /opt/android - git clone https://github.com/cake-tech/cake_wallet.git --branch $GITHUB_HEAD_REF + -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + cargo install cargo-ndk + git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }} cd cake_wallet/scripts/android/ ./install_ndk.sh source ./app_env.sh cakewallet + chmod +x pubspec_gen.sh ./app_config.sh - name: Cache Externals @@ -48,7 +72,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh') }} + key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals @@ -81,16 +105,14 @@ jobs: - name: Build generated code run: | cd /opt/android/cake_wallet - cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - flutter packages pub run build_runner build --delete-conflicting-outputs + ./model_generator.sh - name: Add secrets run: | cd /opt/android/cake_wallet touch lib/.secrets.g.dart + touch cw_evm/lib/.secrets.g.dart + touch cw_solana/lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart @@ -117,42 +139,58 @@ jobs: echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart + echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart + echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart - name: Rename app - run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties + run: | + echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties - name: Build run: | cd /opt/android/cake_wallet - flutter build apk --release + flutter build apk --release --split-per-abi -# - name: Push to App Center -# run: | -# echo 'Installing App Center CLI tools' -# npm install -g appcenter-cli -# echo "Publishing test to App Center" -# appcenter distribute release \ -# --group "Testers" \ -# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ -# --release-notes ${GITHUB_HEAD_REF} \ -# --app Cake-Labs/Cake-Wallet \ -# --token ${{ secrets.APP_CENTER_TOKEN }} \ -# --quiet + # - name: Push to App Center + # run: | + # echo 'Installing App Center CLI tools' + # npm install -g appcenter-cli + # echo "Publishing test to App Center" + # appcenter distribute release \ + # --group "Testers" \ + # --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ + # --release-notes ${{ env.BRANCH_NAME }} \ + # --app Cake-Labs/Cake-Wallet \ + # --token ${{ secrets.APP_CENTER_TOKEN }} \ + # --quiet - name: Rename apk file run: | - cd /opt/android/cake_wallet/build/app/outputs/apk/release + cd /opt/android/cake_wallet/build/app/outputs/flutter-apk mkdir test-apk - cp app-release.apk test-apk/$GITHUB_HEAD_REF.apk + cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk - name: Upload Artifact uses: kittaakos/upload-artifact-as-is@v0 with: - path: /opt/android/cake_wallet/build/app/outputs/apk/release/test-apk/ + path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ - name: Send Test APK continue-on-error: true - run: | - cd /opt/android/cake_wallet - var=$(curl --upload-file build/app/outputs/apk/release/app-release.apk https://transfer.sh/$GITHUB_HEAD_REF.apk -H "Max-Days: 10") - curl ${{ secrets.SLACK_WEB_HOOK }} -H "Content-Type: application/json" -d '{"apk_link": "'"$var"'","ticket": "'"$GITHUB_HEAD_REF"'"}' + uses: adrey/slack-file-upload-action@1.0.5 + with: + token: ${{ secrets.SLACK_APP_TOKEN }} + path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/${{env.BRANCH_NAME}}.apk + channel: ${{ secrets.SLACK_APK_CHANNEL }} + title: "${{ env.BRANCH_NAME }}.apk" + filename: ${{ env.BRANCH_NAME }}.apk + initial_comment: ${{ github.event.head_commit.message }} diff --git a/.gitignore b/.gitignore index 9fb7fd204..6f2d0a182 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml @@ -85,11 +86,17 @@ cw_monero/cw_monero/android/.cxx/ **/*.g.dart android/key.properties +android/app/key.jks **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.evm-secrets-config.json +**/tool/.ethereum-secrets-config.json +**/tool/.solana-secrets-config.json **/lib/.secrets.g.dart +**/cw_evm/lib/.secrets.g.dart +**/cw_solana/lib/.secrets.g.dart vendor/ @@ -120,6 +127,11 @@ cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart +lib/ethereum/ethereum.dart +lib/bitcoin_cash/bitcoin_cash.dart +lib/nano/nano.dart +lib/polygon/polygon.dart +lib/solana/solana.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -139,4 +151,13 @@ assets/images/app_logo.png macos/Runner/Info.plist macos/Runner/DebugProfile.entitlements -macos/Runner/Release.entitlements \ No newline at end of file +macos/Runner/Release.entitlements + +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +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 diff --git a/PRIVACY.md b/PRIVACY.md index d740dcba8..76cfcc4d3 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ Privacy Policy -Last modified: July 21, 2022 +Last modified: January 24, 2024 Introduction ============ @@ -13,7 +13,7 @@ Introduction - On this App. - In email, text, and other electronic messages between you and this App. It does not apply to information collected by: - - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries)]; or + - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or - Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App. Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates. @@ -25,7 +25,7 @@ Definitions - "Node" means a server on a supported cryptocurrency network which transmits data to your App for processing and synchronization, and to which your Device transmits transactions which you would like to submit to the supported cryptocurrency networks. This includes full nodes, Electrum servers, and lightning network nodes. - "Cake Labs Nodes" refers to the set of cryptocurrency nodes operated and maintained by Cake Labs LLC. - "Service" refers to the App. - - "Third-party Service" refers to any service integrated into the App. This includes but is not limited to ChangeNOW, Wyre, MoonPay, and BlockBuy. + - "Third-party Service" refers to any service integrated into the App. This includes but is not limited to ChangeNOW, Onramper, and MoonPay. - "Usage Data" refers to data collected automatically about your usage of an App. - "You" means the individual, group, corporation, or any other entity accessing or using the Service. @@ -40,26 +40,29 @@ Information We Never Receive Nor Collect Information We May Receive But Do Not Retain -------------------------------------------- - We receive but do NOT store information from and about users of our App, including: + We may receive but do NOT store information from and about users of our App, including: - The device IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to supported cryptocurrency networks. We receive this information: - - Automatically as you use the App. + - Automatically as you use the App, unless you turn certain features off in your App privacy settings. - This data is provided by connecting to the Nodes and price API maintained by Cake Labs. You have the right to choose not to provide synchronization data to Cake Labs by choosing a different Node. We provide a list of Nodes in the app that include our own and third party Nodes, or you can use your own Node (which we recommend). + This data is provided by connecting to the Nodes and price API maintained by Cake Labs. You have the right to choose not to provide synchronization data to Cake Labs by choosing a different Node. We provide a list of Nodes in the app that include our own and third party Nodes, or you can use your own Node (which we recommend). You have the right to choose not to connect to our Fiat API service by disabling this Fiat API in App privacy settings. - Personal Data sent through the Cake Labs Nodes is limited to your device's IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to the supported cryptocurrency networks. Personal Data received by Cake Labs in this manner is not stored for any length of time, and thus Cake Labs is both unwilling to and incapable of sharing this data, or using it for any purpose beyond ensuring your appropriate connection to our Nodes. + Personal Data that may be sent through the Cake Labs Nodes is limited to your device's IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to the supported cryptocurrency networks. Personal Data received by Cake Labs in this manner is not stored for any length of time, and thus Cake Labs is incapable of sharing this data and will not use it for any purpose beyond ensuring your appropriate connection to our Nodes. If you decide to use a Node offered by any third party, some of which we include in our Apps, said third party will receive this Personal Data instead of Cake Labs. We take no responsibility for the actions of any third-party Node offered within the Application. We recommend connecting to your own Node to limit third party sharing of your Personal Information. + If you use our Fiat API service, you will share your IP address and the cryptocurrency and fiat currency exchange pair for which your wallet requests a spot price quote. You can disable this Fiat API in App privacy settings. + Information We May Collect About You and How We Collect It ---------------------------------------------------------- We collect several types of information from and about users of our App, including information: - - By which you may be personally identified, such as name, e-mail address, or and a/any other identifier by which you may be contacted online or offline ("personal information" or "Personal Data”), ONLY when you provide it to us; + - By which you may be personally identified, such as name, e-mail address, or and a/any other identifier by which you may be contacted online or offline ("personal information" or "Personal Data”); + - Device data and error log data; We collect this information: - - Directly from you when you provide it to us. + - Directly from you ONLY when you provide it to us. - Personal information is received by Cake Labs ONLY in the event that you choose to provide it to us by voluntarily contacting Cake Labs regarding support, questions or suggestions. + Personal information is received by Cake Labs ONLY in the event that you choose to provide it to us by voluntarily contacting Cake Labs regarding support, questions or suggestions. You may optionally send us Error reports to help us improve the App. These Error reports contain error logs and basic device data. You can review and make modifications to these Error reports before sending them to us, or you may choose not to send them to us at all. How We Use Your Information --------------------------- @@ -109,10 +112,12 @@ Data Security In any situation, Cake Labs takes no responsibility for interception of personal data by any outside individual, group, corporation, or institution. You should understand this and take any and all appropriate actions to secure your own data. -Links to Other Websites ------------------------ +Other Websites and Third-Party Services +--------------------------------------- - The App may contain links to other websites that are not operated by us. If you click on a third-party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. + The App may contain links to other websites that are not operated by us. If you click on a Third-Party Service link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. + + The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. When using certain optional features in the app such as buying and selling, you may be asked to provide information to a Third-Party Service. You will need to read and accept the privacy policy for that third party. This Third-Party Service may ask for your name, your photo ID, your social security number or other similar number, mailing address, cryptocurrency address, or other information. They may ask you to take a selfie image. Information shared with a Third-Party Service is subject to their respective Privacy Policies. Changes to Our Privacy Policy ----------------------------- diff --git a/README.md b/README.md index 317ad91b7..7823734fb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,35 @@ -# Cake Wallet for Android and iOS +
-## Open Source Multi-Currency Wallet + -## Links +
-* Website: https://cakewallet.com -* App Store (iOS / MacOS): https://cakewallet.com/ios -* Google Play: https://cakewallet.com/gp -* APK: https://github.com/cake-tech/cake_wallet/releases -* Linux: https://github.com/cake-tech/cake_wallet/releases +![devices](.github/assets/devices.png) + +
+ +[](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=iphone) +[](https://play.google.com/store/apps/details?id=com.cakewallet.cake_wallet) +[](https://fdroid.cakelabs.com) +[](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=mac) +[](https://github.com/cake-tech/cake_wallet/releases) + +
+ +# Cake Wallet + +Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. + +Cake Wallet includes support for several cryptocurrencies, including: +* Monero (XMR) +* Bitcoin (BTC) +* Ethereum (ETH) +* Litecoin (LTC) +* Bitcoin Cash (BCH) +* Polygon (MATIC) +* Solana (SOL) +* Nano (XNO) +* Haven (XHV) ## Features @@ -17,9 +38,8 @@ * Completely noncustodial. *Your keys, your coins.* * Built-in exchange for dozens of pairs * Easily pay cryptocurrency invoices with fixed rate exchanges -* Buy cryptocurrency (BTC/LTC/XMR) with credit/debit/bank +* Buy cryptocurrency (BTC/LTC/XMR/ETH) with credit/debit/bank * Sell cryptocurrency by bank transfer -* Purchase gift cards at a discount using only an email with [Cake Pay](https://cakepay.com), available in-app * Scan QR codes for easy cryptocurrency transfers * Create several wallets * Select your own custom nodes/servers @@ -32,6 +52,7 @@ * Convenient exchange and sending templates for recurring payments * Create donation links and invoices in the receive screen * Robust privacy settings (eg: Tor-only connections) +* Robust security settings (eg: Cake 2FA) ### Monero Specific Features @@ -40,13 +61,19 @@ * Specify restore height for faster syncing * Specify multiple recipients for batch sending * Optionally set Monero nodes as trusted for faster syncing +* Specify a proxy for Monero nodes, compatible with Tor and i2p ### Bitcoin Specific Features * Bitcoin coin control (specify specific outputs to spend) * Automatically generate new addresses * Specify multiple recipients for batch sending -* Sell BTC for USD + +### Ethereum Specific Features + +* Store ETH and all ERc-20 tokens +* Add custom tokens by contract address +* Enable or disable Etherscan for transaction history ### Litecoin Specific Features @@ -69,6 +96,7 @@ * Website: https://monero.com * App Store (iOS): https://apps.apple.com/app/id1601990386 * Google Play: https://play.google.com/store/apps/details?id=com.monero.app +* F-Droid: https://fdroid.cakelabs.com * APK: https://github.com/cake-tech/cake_wallet/releases # Support @@ -123,7 +151,7 @@ Edit the applicable `strings_XX.arb` file in `res/values/` and open a pull reque 2. Edit the strings in this file, replacing XXX below with the translation for each string. -`"welcome" : "Welcome to",` -> `"welcome" : "XXX",` +`"welcome": "Welcome to",` -> `"welcome": "XXX",` 3. For strings where there is a variable, denoted by a $ symbol and braces, such as ${status}, the string in braces should not be translated. For example, when editing line 106: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..a1b489b76 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Reporting a Vulnerability + +If you need to report a vulnerability, please either: + +* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new +* Send an email to `dev@cakewallet.com` with details on the vulnerability + +## Supported Versions + +As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number. diff --git a/android/app/build.gradle b/android/app/build.gradle index 00cef6393..5e27aeb9e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (appPropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 lintOptions { disable 'InvalidPackage' @@ -45,8 +45,8 @@ android { defaultConfig { applicationId appProperties['id'] - minSdkVersion 21 - targetSdkVersion 31 + minSdkVersion 24 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -75,7 +75,6 @@ android { shrinkResources false minifyEnabled false - useProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 64adea1e7..eea9b5521 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -3,10 +3,12 @@ + + - @@ -45,6 +44,7 @@ + @@ -54,14 +54,27 @@ + + + + + + + + + + + + + result.success(null)); + break; + case "isBatteryOptimizationDisabled": + boolean isDisabled = isBatteryOptimizationDisabled(); + handler.post(() -> result.success(isDisabled)); + break; default: handler.post(() -> result.notImplemented()); } @@ -89,4 +101,22 @@ public class MainActivity extends FlutterFragmentActivity { } }); } + + private void disableBatteryOptimization() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(packageName)) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + packageName)); + startActivity(intent); + } + } + + private boolean isBatteryOptimizationDisabled() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + return pm.isIgnoringBatteryOptimizations(packageName); + } + } \ No newline at end of file diff --git a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java index 8c13d1f8d..d0a465d22 100644 --- a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java @@ -14,6 +14,10 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.WindowManager; +import android.content.Intent; +import android.net.Uri; +import android.os.PowerManager; +import android.provider.Settings; import com.unstoppabledomains.resolution.DomainResolution; import com.unstoppabledomains.resolution.Resolution; @@ -55,6 +59,14 @@ public class MainActivity extends FlutterFragmentActivity { handler.post(() -> result.success("")); } break; + case "disableBatteryOptimization": + disableBatteryOptimization(); + handler.post(() -> result.success(null)); + break; + case "isBatteryOptimizationDisabled": + boolean isDisabled = isBatteryOptimizationDisabled(); + handler.post(() -> result.success(isDisabled)); + break; default: handler.post(() -> result.notImplemented()); } @@ -79,4 +91,22 @@ public class MainActivity extends FlutterFragmentActivity { } }); } + + private void disableBatteryOptimization() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(packageName)) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + packageName)); + startActivity(intent); + } + } + + private boolean isBatteryOptimizationDisabled() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + return pm.isIgnoringBatteryOptimizations(packageName); + } + } \ No newline at end of file diff --git a/android/app/src/main/java/com/monero/app/MainActivity.java b/android/app/src/main/java/com/monero/app/MainActivity.java index 73914c43c..49c368ec7 100644 --- a/android/app/src/main/java/com/monero/app/MainActivity.java +++ b/android/app/src/main/java/com/monero/app/MainActivity.java @@ -14,6 +14,10 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.WindowManager; +import android.content.Intent; +import android.net.Uri; +import android.os.PowerManager; +import android.provider.Settings; import com.unstoppabledomains.resolution.DomainResolution; import com.unstoppabledomains.resolution.Resolution; @@ -64,6 +68,14 @@ public class MainActivity extends FlutterFragmentActivity { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); } break; + case "disableBatteryOptimization": + disableBatteryOptimization(); + handler.post(() -> result.success(null)); + break; + case "isBatteryOptimizationDisabled": + boolean isDisabled = isBatteryOptimizationDisabled(); + handler.post(() -> result.success(isDisabled)); + break; default: handler.post(() -> result.notImplemented()); } @@ -88,4 +100,22 @@ public class MainActivity extends FlutterFragmentActivity { } }); } + + private void disableBatteryOptimization() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(packageName)) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + packageName)); + startActivity(intent); + } + } + + private boolean isBatteryOptimizationDisabled() { + String packageName = getPackageName(); + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + return pm.isIgnoringBatteryOptimizations(packageName); + } + } \ No newline at end of file diff --git a/android/app/src/main/res/drawable-night/launch_background.xml b/android/app/src/main/res/drawable-night/launch_background.xml new file mode 100644 index 000000000..11d0cb8e6 --- /dev/null +++ b/android/app/src/main/res/drawable-night/launch_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml b/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml new file mode 100644 index 000000000..ca3826a46 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f88..50af1a418 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,12 +1,6 @@ - - - - - + + + diff --git a/android/build.gradle b/android/build.gradle index 692e8dfb1..8286d9cb9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.3.0' classpath 'com.google.gms:google-services:4.3.8' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/assets/bitcoin_cash_electrum_server_list.yml b/assets/bitcoin_cash_electrum_server_list.yml new file mode 100644 index 000000000..d76668169 --- /dev/null +++ b/assets/bitcoin_cash_electrum_server_list.yml @@ -0,0 +1,3 @@ +- + uri: bitcoincash.stackwallet.com:50002 + is_default: true \ No newline at end of file diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml new file mode 100644 index 000000000..125085d88 --- /dev/null +++ b/assets/ethereum_server_list.yml @@ -0,0 +1,10 @@ +- + uri: ethereum.publicnode.com +- + uri: eth.llamarpc.com +- + uri: rpc.flashbots.net +- + uri: eth-mainnet.public.blastapi.io +- + uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/faq/faq_de.json b/assets/faq/faq_de.json index be9b567b7..14d703e4f 100644 --- a/assets/faq/faq_de.json +++ b/assets/faq/faq_de.json @@ -1,7 +1,7 @@ [ { "question" : "Was ist der Unterschied zwischen verfügbarem Guthaben und vollständigem Guthaben?", - "answer" : "Nachdem Sie eine Transaktion getätigt oder Monero erhalten haben, muss die Transaktion noch bestätigt werden. In ungefähr 20 Minuten sollte Ihr \"verfügbares Guthaben\" aktualisiert werden!\nWenn Sie Monero senden, verringert sich manchmal Ihr verfügbares Guthaben um mehr als den Betrag, den Sie gesendet haben. Dies ist normal und zum Schutz Ihrer Privatsphäre erforderlich. Ihr \"vollständiges Gleichgewicht\" sollte in 20 Minuten wieder normal sein.\n" + "answer" : "Nachdem Sie eine Transaktion getätigt oder Monero erhalten haben, muss die Transaktion noch bestätigt werden. In ungefähr 20 Minuten sollte Ihr \"verfügbares Guthaben\" aktualisiert werden!\nWenn Sie Monero senden, verringert sich manchmal Ihr verfügbares Guthaben um mehr als den Betrag, den Sie gesendet haben. Dies ist normal und zum Schutz Ihrer Privatsphäre erforderlich. Ihr \"vollständiges Guthaben\" sollte in 20 Minuten wieder normal sein.\n" }, { "question" : "Wie sende ich Monero an eine Börse, für die eine Zahlungs-ID erforderlich ist?", @@ -12,24 +12,24 @@ "answer" : "Obwohl unser Support Sie bei diesem Problem nicht direkt unterstützen kann, ist es ein sehr häufiges Problem, mit dem die meisten Börsen vertraut sind. Wenden Sie sich einfach an den Support der Börse, erklären Sie, dass Sie vergessen haben, Ihre Zahlungs-ID anzugeben, und senden Sie ihnen dann Ihre Transaktions-ID als Nachweis. Sie finden die Transaktions-ID, indem Sie auf die Transaktion in Ihrem Wallet-Bildschirm tippen.\n" }, { - "question" : "Was bedeuten \"Samen\" und \"Schlüssel\"?", - "answer" : "Ihre Schlüssel verschlüsseln die privaten Informationen in Ihrer Brieftasche und ermöglichen es Ihnen, Münzen auszugeben und eingehende Transaktionen anzuzeigen.\nIhr Startwert ist nur eine Version Ihres privaten Schlüssels, die so geschrieben wurde, dass Sie sie leichter notieren können. Ihr Same und Schlüssel sind tatsächlich dasselbe, nur in verschiedenen Formen!\nGeben Sie niemals Ihren Samen oder Schlüssel an jemanden weiter. Ihr Geld wird gestohlen, wenn Sie Ihren Samen oder Schlüssel herausgeben. Bitte notieren Sie sich jedoch Ihren Samen und bewahren Sie ihn an einem sicheren Ort auf (so können Sie Ihre Brieftasche wiederherstellen, wenn Sie Ihr Telefon verlieren.)\n" + "question" : "Was bedeuten \"Seed\" und \"Schlüssel\"?", + "answer" : "Ihre Schlüssel verschlüsseln die privaten Informationen in Ihrer Brieftasche und ermöglichen es Ihnen, Münzen auszugeben und eingehende Transaktionen anzuzeigen.\nIhr Startwert ist nur eine Version Ihres privaten Schlüssels, die so geschrieben wurde, dass Sie sie leichter notieren können. Ihr Same und Schlüssel sind tatsächlich dasselbe, nur in verschiedenen Formen!\nGeben Sie niemals Ihren Seed oder Schlüssel an jemanden weiter. Ihr Geld wird gestohlen, wenn Sie Ihren Seed oder Schlüssel herausgeben. Bitte notieren Sie sich jedoch Ihren Seed und bewahren Sie ihn an einem sicheren Ort auf (so können Sie Ihr Wallet wiederherstellen, wenn Sie Ihr Telefon verlieren.)\n" }, { "question" : "Wie viele Geldbörsen kann ich erstellen?", "answer" : "Es gibt keine Grenzen! Sie können so viele Brieftaschen erstellen, wie Sie möchten.\n" }, { - "question" : "Wie kann ich meine Brieftasche wiederherstellen?", - "answer" : "Tippen Sie auf das Menü •••, wählen Sie „Brieftaschen“ und dann „Brieftasche wiederherstellen“. Geben Sie dann Ihren Startwert (oder Ihre Schlüssel) und optional ein Datum vor der ersten Transaktion in Ihrer Brieftasche ein (dies beschleunigt den Synchronisierungsvorgang) .) Möglicherweise müssen Sie die App 15 bis 30 Minuten geöffnet lassen, um Ihr Portemonnaie vollständig wiederherzustellen.\n" + "question" : "Wie kann ich mein Wallet wiederherstellen?", + "answer" : "Tippen Sie auf das Menü •••, wählen Sie „Wallest“ und dann „Wallet wiederherstellen“. Geben Sie dann Ihren Seed (oder Ihre Schlüssel) und optional ein Datum vor der ersten Transaktion in Ihrem Wallet ein (dies beschleunigt den Synchronisierungsvorgang) .) Möglicherweise müssen Sie die App 15 bis 30 Minuten geöffnet lassen, um Ihr Wallet vollständig wiederherzustellen.\n" }, { - "question" : "Was kann ich tun, wenn ich meinen Samen verliere?", - "answer" : "Wenn Sie Ihren Samen vergessen haben, haben Sie ihn wahrscheinlich irgendwo aufgeschrieben. Bitte überprüfen Sie Ihre Notizen und schauen Sie sich auf Ihrem Computer um. Wenn Sie es nirgendwo finden, haben Sie möglicherweise Cake Wallet gesichert (in diesem Fall können Sie es aus diesem Backup wiederherstellen.) Wenn keines dieser Probleme auftritt, können wir leider nichts tun.\n" + "question" : "Was kann ich tun, wenn ich meinen Seed verliere?", + "answer" : "Wenn Sie Ihren Seed vergessen haben, haben Sie ihn wahrscheinlich irgendwo aufgeschrieben. Bitte überprüfen Sie Ihre Notizen und schauen Sie sich auf Ihrem Computer um. Wenn Sie es nirgendwo finden, haben Sie möglicherweise Cake Wallet gesichert (in diesem Fall können Sie es aus diesem Backup wiederherstellen.) Wenn keines dieser Probleme auftritt, können wir leider nichts tun.\n" }, { - "question" : "Sammeln Sie Informationen zu meiner Brieftasche?", - "answer" : "Cake Wallet sammelt oder zeichnet keine Informationen über Ihre Brieftasche auf. Ihre Privatsphäre ist uns wichtig.\n" + "question" : "Sammeln Sie Informationen zu meinem Wallet?", + "answer" : "Cake Wallet sammelt oder zeichnet keine Informationen über Ihr Wallet auf. Ihre Privatsphäre ist uns wichtig.\n" }, { "question" : "Kann ich eine Transaktion stornieren?", @@ -37,7 +37,7 @@ }, { "question" : "Was sind Subadressen und wie verwende ich sie?", - "answer" : "Eine Unteradresse ist im Grunde eine eindeutige Adresse, die Sie jederzeit generieren können. An sie gesendete Münzen landen weiterhin in Ihrer Hauptbrieftasche, aber die Person, die die Münzen sendet, kann Ihre Hauptadresse nicht ermitteln. Unteradressen beginnen immer mit „8“.\nSie können eine neue Unteradresse im Empfangsbildschirm erstellen, indem Sie auf das „+“ neben der Schaltfläche Unteradressen tippen. Geben Sie einen Namen für die Unteradresse ein und tippen Sie auf \"Hinzufügen\". Dann tippen Sie einfach auf den Namen der Subadresse, wenn Sie ihn verwenden möchten!\nWenn Sie paranoid sind, sollten Sie wahrscheinlich jedes Mal, wenn Sie Monero erhalten, eine neue Unteradresse erstellen.\n" + "answer" : "Eine Unteradresse ist im Grunde eine eindeutige Adresse, die Sie jederzeit generieren können. An sie gesendete Münzen landen weiterhin in Ihrer Hauptwallet, aber die Person, die die Coins sendet, kann Ihre Hauptadresse nicht ermitteln. Unteradressen beginnen immer mit „8“.\nSie können eine neue Unteradresse im Empfangsbildschirm erstellen, indem Sie auf das „+“ neben der Schaltfläche Unteradressen tippen. Geben Sie einen Namen für die Unteradresse ein und tippen Sie auf \"Hinzufügen\". Dann tippen Sie einfach auf den Namen der Subadresse, wenn Sie ihn verwenden möchten!\nWenn Sie paranoid sind, sollten Sie wahrscheinlich jedes Mal, wenn Sie Monero erhalten, eine neue Unteradresse erstellen.\n" }, { "question" : "Was ist eine Transaktions-ID?", @@ -48,11 +48,11 @@ "answer" : "Wenn Sie Ihren Monero nicht erhalten haben, möchten Sie möglicherweise auf das Menü ••• tippen und auf Reconnect (Neu verbinden) klicken. Wenn dies nicht funktioniert, gehen Sie in das Einstellungsmenü, tippen Sie auf das Feld \"Aktueller Knoten\" und wählen Sie einen Knoten mit einem grünen Punkt daneben aus.\n" }, { - "question" : "Ich habe in der App keine Münzen aus dem Umtausch erhalten. Was kann ich tun?", - "answer" : "Wenn Sie Probleme mit einem Austausch haben, wenden Sie sich am besten an den Austausch. Wir sind eine Partnerschaft mit XMR.TO, Morph und ChangeNow eingegangen. Rufen Sie daher am besten http://xmr.to, http://changenow.io oder http://morphtoken.com auf und wenden Sie sich an deren Support.\n" + "question" : "Ich habe in der App keine Coins aus dem Umtausch erhalten. Was kann ich tun?", + "answer" : "Wenn Sie Probleme mit einem Austausch haben, besteht die beste Option, den Austausch selbst zu kontaktieren. Wir haben uns mit Chechenow, Simpleswap, Sideshift und Trocador zusammengetan. Am besten wechseln Sie zu https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ und kontaktieren Sie ihre Unterstützung.\n" }, { "question" : "Wie kontaktiere ich den Cake Wallet-Support?", "answer" : "Senden Sie eine E-Mail an support@cakewallet.com, schließen Sie sich dem Telegramm unter @cakewallet_bot an oder twittern Sie @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_en.json b/assets/faq/faq_en.json index aae40802c..741d31798 100644 --- a/assets/faq/faq_en.json +++ b/assets/faq/faq_en.json @@ -49,10 +49,10 @@ }, { "question" : "I didn't receive my coins from the exchange in the app. What can I do?", - "answer" : "If you're having issues with an exchange, the best option is to contact the exchange itself. We're partnered with XMR.TO, Morph and ChangeNow, so your best bet is to go to http://xmr.to, http://changenow.io, or http://morphtoken.com and contact their support.\n" + "answer" : "If you're having issues with an exchange, the best option is to contact the exchange itself. We're partnered with ChangeNow, SimpleSwap, SideShift and Trocador. So your best bet is to go to https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ and contact their support.\n" }, { "question" : "How do I contact Cake Wallet support?", "answer" : "Email support@cakewallet.com, join the Telegram at @cakewallet_bot, or tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_es.json b/assets/faq/faq_es.json index 28074662c..968853a0b 100644 --- a/assets/faq/faq_es.json +++ b/assets/faq/faq_es.json @@ -49,10 +49,10 @@ }, { "question" : "No recibí mis monedas del intercambio en la aplicación. ¿Que puedo hacer?", - "answer" : "Si tiene problemas con un intercambio, la mejor opción es ponerse en contacto con el intercambio en sí. Estamos asociados con XMR.TO, Morph y ChangeNow, por lo que su mejor opción es ir a http://xmr.to, http://changenow.io o http://morphtoken.com y contactar a su soporte.\n" + "answer" : "Si tiene problemas con un intercambio, la mejor opción es comunicarse con el intercambio en sí. Estamos asociados con ChangeNow, SimpleSwap, SideShift y Trocador. Entonces, su mejor opción es ir a https://changenow.io, https://simplewap.io/, https://sideshift.ai/, https://trocador.app/ y contactar su soporte.\n" }, { "question" : "¿Cómo contacto al soporte de Cake Wallet?", "answer" : "¡Envíe un correo electrónico a support@cakewallet.com, únase al Telegram en @cakewallet_bot o envíe un tweet a @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_fr.json b/assets/faq/faq_fr.json index cc4e52873..6b6ac5227 100644 --- a/assets/faq/faq_fr.json +++ b/assets/faq/faq_fr.json @@ -50,7 +50,7 @@ }, { "question" : "Je n'ai pas reçu mes fonds en provenance de la plateforme d'échange dans l'application. Que puis-je faire ?", - "answer" : "Si vous avez des soucis avec une plateforme d'échange, le mieux est de contacter la plateforme d'échange directement. Nous avons des partenariats avec XMR.TO, Morph et ChangeNow, donc essayez http://xmr.to, http://changenow.io, ou http://morphtoken.com et contactez leur support.\n" + "answer" : "Si vous rencontrez des problèmes avec un échange, la meilleure option est de contacter l'échange lui-même. Nous sommes en partenariat avec Changenow, Simpleswap, Sideshift et le Trocador. Donc, votre meilleur pari est d'aller sur https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ et contactez leur support.\n" }, { "question" : "Comment puis-je contacter le support de Cake Wallet ?", diff --git a/assets/faq/faq_hi.json b/assets/faq/faq_hi.json index cd9eb3fb9..f1ede3374 100644 --- a/assets/faq/faq_hi.json +++ b/assets/faq/faq_hi.json @@ -49,10 +49,10 @@ }, { "question" : "मुझे ऐप में एक्सचेंज से मेरे सिक्के नहीं मिले। मैं क्या कर सकता हूँ?", - "answer" : "यदि आप एक एक्सचेंज के साथ समस्या कर रहे हैं, तो सबसे अच्छा विकल्प एक्सचेंज से संपर्क करना है। हम XMR.TO, Morph और ChangeNow के साथ भागीदारी कर रहे हैं, इसलिए आपका सबसे अच्छा दांव http://xmr.to, http://changenow.io, या http://morphtoken.com पर जाना है और उनके समर्थन से संपर्क करना है।\n" + "answer" : "यदि आप एक एक्सचेंज के साथ समस्याएं कर रहे हैं, तो सबसे अच्छा विकल्प एक्सचेंज से संपर्क करना है। हम चंगेनो, सिम्प्लेवैप, सिडशिफ्ट और ट्रोकैडर के साथ भागीदारी कर रहे हैं। तो आपका सबसे अच्छा दांव https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ पर जाना और उनके समर्थन से संपर्क करना है।\n" }, { "question" : "मैं केक वॉलेट से कैसे संपर्क करूं?", "answer" : "ईमेल support@cakewallet.com, @cakewallet_bot पर टेलीग्राम में शामिल हों, या @CakeWalletXMR पर ट्वीट करें!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ja.json b/assets/faq/faq_ja.json index 8a5b48763..6d83e61d3 100644 --- a/assets/faq/faq_ja.json +++ b/assets/faq/faq_ja.json @@ -49,10 +49,10 @@ }, { "question" : "アプリの取引所からコインを受け取りませんでした。 私に何ができる?", - "answer" : "取引所に問題がある場合、最良の選択肢は取引所自体に連絡することです。 XMR.TO、Morph、ChangeNowと提携しているため、最善の策はhttp://xmr.to、http://changenow.io、またはhttp://morphtoken.comにアクセスしてサポートに連絡することです。\n" + "answer" : "交換に問題がある場合、最良の選択肢は、交換自体に連絡することです。 Changenow、SimpleSwap、Sideshift、Trocadorと提携しています。したがって、あなたの最善の策は、https://changenow.io、https://simpleswap.io/、https://sideshift.ai/、https://trocador.app/に行くことです。\n" }, { "question" : "Cake Walletサポートに連絡するにはどうすればよいですか?", "answer" : "support@cakewallet.comにメールを送信するか、@cakewallet_botで電報に参加するか、@CakeWalletXMRにツイートしてください。\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ko.json b/assets/faq/faq_ko.json index 7d6f36589..640567284 100644 --- a/assets/faq/faq_ko.json +++ b/assets/faq/faq_ko.json @@ -49,10 +49,10 @@ }, { "question" : "앱의 거래소에서 동전을받지 못했습니다. 내가 무엇을 할 수 있을지?", - "answer" : "교환에 문제가있는 경우 교환기에 연락하는 것이 가장 좋습니다. 우리는 XMR.TO, Morph 및 ChangeNow와 파트너 관계를 맺고 있으므로 가장 좋은 방법은 http://xmr.to, http://changenow.io 또는 http://morphtoken.com으로 이동하여 지원 부서에 문의하는 것입니다.\n" + "answer" : "교환에 문제가있는 경우 가장 좋은 선택은 Exchange 자체에 연락하는 것입니다. 우리는 Changenow, Simpleswap, Sideshift 및 Trocador와 파트너 관계를 맺고 있습니다. 따라서 가장 좋은 방법은 https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/로 이동하여 지원에 연락하는 것입니다.\n" }, { "question" : "Cake Wallet 지원팀에 연락하려면 어떻게해야합니까?", "answer" : "support@cakewallet.com로 이메일을 보내거나 @cakewallet_bot에서 전보에 가입하거나 @CakeWalletXMR을 트윗하십시오!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_nl.json b/assets/faq/faq_nl.json index bb4f70216..47cf8fdf0 100644 --- a/assets/faq/faq_nl.json +++ b/assets/faq/faq_nl.json @@ -49,10 +49,10 @@ }, { "question" : "Ik heb mijn munten niet ontvangen van de beurs in de app. Wat kan ik doen?", - "answer" : "Als u problemen ondervindt met een uitwisseling, kunt u het beste contact opnemen met de uitwisseling zelf. We werken samen met XMR.TO, Morph en ChangeNow, dus u kunt het beste naar http://xmr.to, http://changenow.io of http://morphtoken.com gaan en contact opnemen met hun ondersteuning.\n" + "answer" : "Als u problemen heeft met een uitwisseling, is de beste optie om contact op te nemen met de uitwisseling zelf. We werken samen met ChangeNow, SimpleSwap, SideShift en Trocador. Dus het beste is om naar https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ te gaan en contact op te nemen met hun ondersteuning.\n" }, { "question" : "Hoe neem ik contact op met Cake Wallet-ondersteuning?", "answer" : "E-mail support@cakewallet.com, word lid van het Telegram op @cakewallet_bot of tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_pl.json b/assets/faq/faq_pl.json index 1934f4d1a..a38d79068 100644 --- a/assets/faq/faq_pl.json +++ b/assets/faq/faq_pl.json @@ -49,10 +49,10 @@ }, { "question" : "Nie otrzymałem moich monet z wymiany w aplikacji. Co mogę zrobić?", - "answer" : "Jeśli masz problemy z wymianą, najlepszym rozwiązaniem jest skontaktowanie się z samą giełdą. Współpracujemy z XMR.TO, Morph i ChangeNow, więc najlepiej postawić się na stronie http://xmr.to, http://changenow.io lub http://morphtoken.com i skontaktować się z ich wsparciem.\n" + "answer" : "Jeśli masz problemy z wymianą, najlepszą opcją jest skontaktowanie się z samą wymianą. Współpracujemy z Changenow, Simpleswap, Sideshift i Trocador. Więc najlepszym rozwiązaniem jest przejście na https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ i skontaktować się z ich obsługą.\n" }, { "question" : "Jak skontaktować się z obsługą Cake Wallet?", "answer" : "Wyślij e-mail na adres support@cakewallet.com, dołącz do telegramu na @cakewallet_bot lub tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_pt.json b/assets/faq/faq_pt.json index 06ddab25e..66d3a6aaf 100644 --- a/assets/faq/faq_pt.json +++ b/assets/faq/faq_pt.json @@ -49,10 +49,10 @@ }, { "question" : "Não recebi minhas moedas da troca no aplicativo. O que eu posso fazer?", - "answer" : "Se você estiver tendo problemas com uma troca, a melhor opção é entrar em contato com a troca. Somos parceiros do XMR.TO, Morph e ChangeNow, portanto, sua melhor aposta é ir para http://xmr.to, http://changenow.io ou http://morphtoken.com e entrar em contato com o suporte deles.\n" + "answer" : "Se você estiver com problemas com uma troca, a melhor opção é entrar em contato com a própria troca. Estamos em parceria com ChangeNow, SimpleSwap, Sideshift e Trocador. Portanto, sua melhor aposta é ir para https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ e entre em contato com seu suporte.\n" }, { "question" : "Como entro em contato com o suporte da Cake Wallet?", "answer" : "Envie um e-mail para support@cakewallet.com, participe do Telegram em @cakewallet_bot ou envie um tweet para @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ru.json b/assets/faq/faq_ru.json index 4b8b18e32..af5ba32a6 100644 --- a/assets/faq/faq_ru.json +++ b/assets/faq/faq_ru.json @@ -49,10 +49,10 @@ }, { "question" : "Я не получил свои монеты после обмена в приложении. Что я могу сделать?", - "answer" : "Если у вас возникли проблемы с обменом, лучше всего связаться с провайдером обмена. Мы сотрудничаем с XMR.TO, Morph и ChangeNow, поэтому вам лучше всего зайти на http://xmr.to, http://changenow.io или http://morphtoken.com и связаться с их поддержкой.\n" + "answer" : "Если у вас есть проблемы с обменом, лучший вариант - связаться с самой биржей. Мы сотрудничаем с Changenow, Simpleswap, SideShift и Trocador. Так что лучше всего пойти по адресу https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ и свяжитесь с их поддержкой.\n" }, { "question" : "Как мне связаться со службой поддержки Cake Wallet?", "answer" : "По электронной почте support@cakewallet.com, присоединитесь к Telegram по адресу @cakewallet_bot или отправьте твит @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_uk.json b/assets/faq/faq_uk.json index c481e0538..3e209d2ad 100644 --- a/assets/faq/faq_uk.json +++ b/assets/faq/faq_uk.json @@ -49,10 +49,10 @@ }, { "question" : "Я не отримав свої монети після обміну в додатку. Що я можу зробити?", - "answer" : "Якщо у вас виникли проблеми з обміном, найкраще зв'язатися з провайдером обміну. Ми співпрацюємо з XMR.TO, Morph і ChangeNow, тому вам найкраще зайти на http://xmr.to, http://changenow.io або http://morphtoken.com і зв'язатися з їх підтримкою.\n" + "answer" : "Якщо у вас є проблеми з обміном, найкращим варіантом є зв’язок із самою біржею. Ми співпрацюємо з Changenow, Simplewap, Sideshift та Trocador. Тож найкраща ставка - перейти на https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ та звернутися до їх підтримки.\n" }, { "question" : "Як мені зв'язатися зі службою підтримки Cake Wallet?", "answer" : "По електронній пошті support@cakewallet.com, приєднайтеся до Telegram за адресою @cakewallet_bot або надішліть твіт @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_zh.json b/assets/faq/faq_zh.json index 22977f22c..8debe1873 100644 --- a/assets/faq/faq_zh.json +++ b/assets/faq/faq_zh.json @@ -49,10 +49,10 @@ }, { "question" : "我没有从应用程序中的交易所收到硬币。 我能做什么?", - "answer" : "如果您对交易所有疑问,最好的选择是与交易所本身联系。 我们与XMR.TO,Morph和ChangeNow合作,因此最好的选择是访问http://xmr.to、http://changenow.io或http://morphtoken.com,并与他们的支持部门联系。\n" + "answer" : "如果您对交易所有问题,最好的选择是与交易所本身联系。我们与ChangeNow,SimplesWap,SideShift和Trocador合作。因此,最好的选择是访问https://changenow.io,https://simpleswap.io/,https://sideshift.ai/,https://trocador.app/并联系他们的支持。\n" }, { "question" : "如何联系蛋糕钱包支持?", "answer" : "电子邮件support@cakewallet.com,通过@cakewallet_bot加入电报,或在@CakeWalletXMR上发布推文!\n" } -] \ No newline at end of file +] diff --git a/assets/images/aave_icon.png b/assets/images/aave_icon.png new file mode 100644 index 000000000..5f79393c2 Binary files /dev/null and b/assets/images/aave_icon.png differ diff --git a/assets/images/arb_icon.png b/assets/images/arb_icon.png new file mode 100644 index 000000000..a9cc4798f Binary files /dev/null and b/assets/images/arb_icon.png differ diff --git a/assets/images/avdo_icon.png b/assets/images/avdo_icon.png new file mode 100644 index 000000000..c02e2760d Binary files /dev/null and b/assets/images/avdo_icon.png differ diff --git a/assets/images/bat_icon.png b/assets/images/bat_icon.png new file mode 100644 index 000000000..d4f6ff492 Binary files /dev/null and b/assets/images/bat_icon.png differ diff --git a/assets/images/bonk_icon.png b/assets/images/bonk_icon.png new file mode 100644 index 000000000..c59537eab Binary files /dev/null and b/assets/images/bonk_icon.png differ diff --git a/assets/images/bttbsc_icon.png b/assets/images/bttbsc_icon.png deleted file mode 100644 index 253c01d1c..000000000 Binary files a/assets/images/bttbsc_icon.png and /dev/null differ diff --git a/assets/images/cake_icon.png b/assets/images/cake_icon.png new file mode 100644 index 000000000..cfa56cf0f Binary files /dev/null and b/assets/images/cake_icon.png differ diff --git a/assets/images/cakewallet_android_icon.png b/assets/images/cakewallet_android_icon.png index a96724f1c..59cc69414 100755 Binary files a/assets/images/cakewallet_android_icon.png and b/assets/images/cakewallet_android_icon.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 90f958096..00d924171 100644 --- a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png index 2bcdb4427..10d0a1a82 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png index a025d330c..5b0fde827 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png index 32891557d..9c16f0a27 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png index 4c4131065..8c59ec33e 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png index 9f0bf4907..5d25e42e7 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png index af6a1e312..021fe65de 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png index f857a604e..10c3acd7f 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png index 65a3f3a1c..c4b66dc58 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 5e783f26f..b440b154d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png index 7fe35095c..813a3678d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png index 66e3bf6b9..75dc0219d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index 2c0ecc492..90afb19e8 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png index 7f632717c..671422b96 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png index 9e40681b1..46b1e2cb1 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index a2104217c..0a2025220 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_app_logo.png b/assets/images/cakewallet_app_logo.png index 64682cd1d..59cc69414 100644 Binary files a/assets/images/cakewallet_app_logo.png and b/assets/images/cakewallet_app_logo.png differ diff --git a/assets/images/china.png b/assets/images/china.png deleted file mode 100644 index 0771ff1e0..000000000 Binary files a/assets/images/china.png and /dev/null differ diff --git a/assets/images/comp_icon.png b/assets/images/comp_icon.png new file mode 100644 index 000000000..620e4a4f7 Binary files /dev/null and b/assets/images/comp_icon.png differ diff --git a/assets/images/cro_icon.png b/assets/images/cro_icon.png new file mode 100644 index 000000000..db6367005 Binary files /dev/null and b/assets/images/cro_icon.png differ diff --git a/assets/images/dfx_dark.png b/assets/images/dfx_dark.png new file mode 100644 index 000000000..cbba87372 Binary files /dev/null and b/assets/images/dfx_dark.png differ diff --git a/assets/images/dfx_light.png b/assets/images/dfx_light.png new file mode 100644 index 000000000..e4836be3e Binary files /dev/null and b/assets/images/dfx_light.png differ diff --git a/assets/images/digibyte.png b/assets/images/digibyte.png new file mode 100644 index 000000000..0045c6852 Binary files /dev/null and b/assets/images/digibyte.png differ diff --git a/assets/images/dydx_icon.png b/assets/images/dydx_icon.png new file mode 100644 index 000000000..bc9b0e142 Binary files /dev/null and b/assets/images/dydx_icon.png differ diff --git a/assets/images/ens_icon.png b/assets/images/ens_icon.png new file mode 100644 index 000000000..fb9f8ba56 Binary files /dev/null and b/assets/images/ens_icon.png differ diff --git a/assets/images/exolix.png b/assets/images/exolix.png new file mode 100644 index 000000000..29e5f2db1 Binary files /dev/null and b/assets/images/exolix.png differ diff --git a/assets/images/france.png b/assets/images/france.png deleted file mode 100644 index 57b98bc32..000000000 Binary files a/assets/images/france.png and /dev/null differ diff --git a/assets/images/frax_icon.png b/assets/images/frax_icon.png new file mode 100644 index 000000000..81ca4269b Binary files /dev/null and b/assets/images/frax_icon.png differ diff --git a/assets/images/ftm_icon.png b/assets/images/ftm_icon.png new file mode 100644 index 000000000..6343be6ec Binary files /dev/null and b/assets/images/ftm_icon.png differ diff --git a/assets/images/germany.png b/assets/images/germany.png deleted file mode 100644 index 2dc720303..000000000 Binary files a/assets/images/germany.png and /dev/null differ diff --git a/assets/images/gmt_icon.png b/assets/images/gmt_icon.png new file mode 100644 index 000000000..25c8a00b6 Binary files /dev/null and b/assets/images/gmt_icon.png differ diff --git a/assets/images/grt_icon.png b/assets/images/grt_icon.png new file mode 100644 index 000000000..5631a4159 Binary files /dev/null and b/assets/images/grt_icon.png differ diff --git a/assets/images/gtc_icon.png b/assets/images/gtc_icon.png new file mode 100644 index 000000000..e6bdad8f1 Binary files /dev/null and b/assets/images/gtc_icon.png differ diff --git a/assets/images/gusd_icon.png b/assets/images/gusd_icon.png new file mode 100644 index 000000000..247bfb315 Binary files /dev/null and b/assets/images/gusd_icon.png differ diff --git a/assets/images/hnt_icon.png b/assets/images/hnt_icon.png new file mode 100644 index 000000000..8b64b76dd Binary files /dev/null and b/assets/images/hnt_icon.png differ diff --git a/assets/images/holland.png b/assets/images/holland.png deleted file mode 100644 index e753b98cf..000000000 Binary files a/assets/images/holland.png and /dev/null differ diff --git a/assets/images/home_screen_settings_icon.png b/assets/images/home_screen_settings_icon.png new file mode 100644 index 000000000..6c750f5f6 Binary files /dev/null and b/assets/images/home_screen_settings_icon.png differ diff --git a/assets/images/india.png b/assets/images/india.png deleted file mode 100644 index e6002f546..000000000 Binary files a/assets/images/india.png and /dev/null differ diff --git a/assets/images/italy.png b/assets/images/italy.png deleted file mode 100644 index 616fa97d8..000000000 Binary files a/assets/images/italy.png and /dev/null differ diff --git a/assets/images/japan.png b/assets/images/japan.png deleted file mode 100644 index 2295c0dfa..000000000 Binary files a/assets/images/japan.png and /dev/null differ diff --git a/assets/images/kaspa_icon.png b/assets/images/kaspa_icon.png new file mode 100644 index 000000000..5201174ef Binary files /dev/null and b/assets/images/kaspa_icon.png differ diff --git a/assets/images/ldo_icon.png b/assets/images/ldo_icon.png new file mode 100644 index 000000000..d614bd48b Binary files /dev/null and b/assets/images/ldo_icon.png differ diff --git a/assets/images/live_support.png b/assets/images/live_support.png new file mode 100644 index 000000000..89ad61f45 Binary files /dev/null and b/assets/images/live_support.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png new file mode 100644 index 000000000..f3f91af95 Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png new file mode 100644 index 000000000..48a1906e1 Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png new file mode 100644 index 000000000..5104ea7d3 Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png new file mode 100644 index 000000000..6ae8b769b Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png new file mode 100644 index 000000000..60c30c609 Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png new file mode 100644 index 000000000..7072c07e7 Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png new file mode 100644 index 000000000..74aa4e2ad Binary files /dev/null and b/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png differ diff --git a/assets/images/monero.com_android_icon.png b/assets/images/monero.com_android_icon.png index af47453c1..9e1fa0a65 100644 Binary files a/assets/images/monero.com_android_icon.png and b/assets/images/monero.com_android_icon.png differ diff --git a/assets/images/monero.com_logo.png b/assets/images/monero.com_logo.png index ecc703781..9e1fa0a65 100644 Binary files a/assets/images/monero.com_logo.png and b/assets/images/monero.com_logo.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 90f958096..00d924171 100644 --- a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png index 583765d8f..1785f4b05 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png index a025d330c..5b0fde827 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png index 646a13ff0..884745809 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png index 50e1973f5..12c31ef57 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png index 9f0bf4907..5d25e42e7 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png index 2230fe0ec..28bbd57b8 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png index 505971bbb..2d8878946 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png index 65a3f3a1c..c4b66dc58 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 3d414fcd3..811848c27 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png index 1ad794a7b..2ceba55ad 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png index 66e3bf6b9..75dc0219d 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index cab647d0c..e078655c9 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png index 43ab3ac30..beca2d29b 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png index 9e40681b1..46b1e2cb1 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index a790c3a74..255b1b71f 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/moonpay_dark.png b/assets/images/moonpay_dark.png new file mode 100644 index 000000000..872e322e2 Binary files /dev/null and b/assets/images/moonpay_dark.png differ diff --git a/assets/images/moonpay_light.png b/assets/images/moonpay_light.png new file mode 100644 index 000000000..c76ae6e74 Binary files /dev/null and b/assets/images/moonpay_light.png differ diff --git a/assets/images/more_links.png b/assets/images/more_links.png new file mode 100644 index 000000000..97891f3ad Binary files /dev/null and b/assets/images/more_links.png differ diff --git a/assets/images/nexo_icon.png b/assets/images/nexo_icon.png new file mode 100644 index 000000000..754a3f995 Binary files /dev/null and b/assets/images/nexo_icon.png differ diff --git a/assets/images/notification_icon.svg b/assets/images/notification_icon.svg new file mode 100644 index 000000000..099039e67 --- /dev/null +++ b/assets/images/notification_icon.svg @@ -0,0 +1,69 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/assets/images/onramper_dark.png b/assets/images/onramper_dark.png new file mode 100644 index 000000000..62f37cd29 Binary files /dev/null and b/assets/images/onramper_dark.png differ diff --git a/assets/images/onramper_light.png b/assets/images/onramper_light.png new file mode 100644 index 000000000..f07fc3709 Binary files /dev/null and b/assets/images/onramper_light.png differ diff --git a/assets/images/pepe_icon.png b/assets/images/pepe_icon.png new file mode 100644 index 000000000..24c19cc3d Binary files /dev/null and b/assets/images/pepe_icon.png differ diff --git a/assets/images/poland.png b/assets/images/poland.png deleted file mode 100644 index fc8f2e163..000000000 Binary files a/assets/images/poland.png and /dev/null differ diff --git a/assets/images/portugal.png b/assets/images/portugal.png deleted file mode 100644 index d0f1a4917..000000000 Binary files a/assets/images/portugal.png and /dev/null differ diff --git a/assets/images/ray_icon.png b/assets/images/ray_icon.png new file mode 100644 index 000000000..0d48e54a6 Binary files /dev/null and b/assets/images/ray_icon.png differ diff --git a/assets/images/robinhood_dark.png b/assets/images/robinhood_dark.png new file mode 100644 index 000000000..0d7273fc4 Binary files /dev/null and b/assets/images/robinhood_dark.png differ diff --git a/assets/images/robinhood_light.png b/assets/images/robinhood_light.png new file mode 100644 index 000000000..24aa345f1 Binary files /dev/null and b/assets/images/robinhood_light.png differ diff --git a/assets/images/russia.png b/assets/images/russia.png deleted file mode 100644 index 8b4752cb7..000000000 Binary files a/assets/images/russia.png and /dev/null differ diff --git a/assets/images/setup_2fa_img.png b/assets/images/setup_2fa_img.png new file mode 100644 index 000000000..ce6f0d733 Binary files /dev/null and b/assets/images/setup_2fa_img.png differ diff --git a/assets/images/shib_icon.png b/assets/images/shib_icon.png new file mode 100644 index 000000000..97bc94468 Binary files /dev/null and b/assets/images/shib_icon.png differ diff --git a/assets/images/south_korea.png b/assets/images/south_korea.png deleted file mode 100644 index 49e16e484..000000000 Binary files a/assets/images/south_korea.png and /dev/null differ diff --git a/assets/images/spain.png b/assets/images/spain.png deleted file mode 100644 index 6b806b016..000000000 Binary files a/assets/images/spain.png and /dev/null differ diff --git a/assets/images/status_website_image.png b/assets/images/status_website_image.png new file mode 100644 index 000000000..017bb64e1 Binary files /dev/null and b/assets/images/status_website_image.png differ diff --git a/assets/images/steth_icon.png b/assets/images/steth_icon.png new file mode 100644 index 000000000..6336e414b Binary files /dev/null and b/assets/images/steth_icon.png differ diff --git a/assets/images/storj_icon.png b/assets/images/storj_icon.png new file mode 100644 index 000000000..c67e10f98 Binary files /dev/null and b/assets/images/storj_icon.png differ diff --git a/assets/images/thorchain.png b/assets/images/thorchain.png new file mode 100644 index 000000000..674b60f82 Binary files /dev/null and b/assets/images/thorchain.png differ diff --git a/assets/images/tusd_icon.png b/assets/images/tusd_icon.png new file mode 100644 index 000000000..9a5189481 Binary files /dev/null and b/assets/images/tusd_icon.png differ diff --git a/assets/images/usa.png b/assets/images/usa.png deleted file mode 100644 index d67d3e2b4..000000000 Binary files a/assets/images/usa.png and /dev/null differ diff --git a/assets/images/usdcsol_icon.png b/assets/images/usdcsol_icon.png deleted file mode 100644 index 283533b42..000000000 Binary files a/assets/images/usdcsol_icon.png and /dev/null differ diff --git a/assets/images/wallet_guides.png b/assets/images/wallet_guides.png new file mode 100644 index 000000000..3f2d9f270 Binary files /dev/null and b/assets/images/wallet_guides.png differ diff --git a/assets/images/walletconnect_logo.png b/assets/images/walletconnect_logo.png new file mode 100644 index 000000000..9024b972c Binary files /dev/null and b/assets/images/walletconnect_logo.png differ diff --git a/assets/images/wbtc_icon.png b/assets/images/wbtc_icon.png new file mode 100644 index 000000000..d836eb391 Binary files /dev/null and b/assets/images/wbtc_icon.png differ diff --git a/assets/images/weth_icon.png b/assets/images/weth_icon.png new file mode 100644 index 000000000..e63222277 Binary files /dev/null and b/assets/images/weth_icon.png differ diff --git a/assets/images/zaddr_icon.png b/assets/images/zaddr_icon.png deleted file mode 100644 index 095ad2c56..000000000 Binary files a/assets/images/zaddr_icon.png and /dev/null differ diff --git a/assets/images/zrx_icon.png b/assets/images/zrx_icon.png new file mode 100644 index 000000000..fcf1a1515 Binary files /dev/null and b/assets/images/zrx_icon.png differ diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml new file mode 100644 index 000000000..2e4d1ec3c --- /dev/null +++ b/assets/nano_node_list.yml @@ -0,0 +1,28 @@ +- + uri: rpc.nano.to + useSSL: true + is_default: true +- + uri: node.nautilus.io + path: /api + useSSL: true +- + uri: app.natrium.io + path: /api + useSSL: true +- + uri: rainstorm.city + path: /api + useSSL: true +- + uri: node.somenano.com + path: /proxy + useSSL: true +- + uri: nanoslo.0x.no + path: /proxy + useSSL: true +- + uri: www.bitrequest.app + port: 8020 + useSSL: true \ No newline at end of file diff --git a/assets/nano_pow_node_list.yml b/assets/nano_pow_node_list.yml new file mode 100644 index 000000000..3bbc7c3fb --- /dev/null +++ b/assets/nano_pow_node_list.yml @@ -0,0 +1,9 @@ +- + uri: rpc.nano.to + useSSL: true + is_default: true +- + uri: workers.perish.co +- + uri: worker.nanoriver.cc + useSSL: true diff --git a/assets/node_list.yml b/assets/node_list.yml index 2cbd7f780..bc7a9dc4a 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -5,7 +5,8 @@ uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false - - uri: node.sethforprivacy.com:18089 + uri: node.sethforprivacy.com:443 + useSSL: true is_default: false - uri: nodes.hashvault.pro:18081 diff --git a/assets/polygon_node_list.yml b/assets/polygon_node_list.yml new file mode 100644 index 000000000..34504269d --- /dev/null +++ b/assets/polygon_node_list.yml @@ -0,0 +1,6 @@ +- + uri: polygon-rpc.com +- + uri: polygon-bor.publicnode.com +- + uri: polygon.llamarpc.com \ No newline at end of file diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml new file mode 100644 index 000000000..4a2e12161 --- /dev/null +++ b/assets/solana_node_list.yml @@ -0,0 +1,4 @@ +- + uri: rpc.ankr.com + is_default: true + useSSL: true \ No newline at end of file diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 178fa45ea..d5297ebe1 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,6 +1 @@ -Opt-in to Cake 2FA for security. More info: https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa -Auto generate restore height for Monero restore QR codes -Hausa and Yoruba languages -Additional privacy settings -Update Monero to 0.18.2.2 -Refactoring and bug fixes \ No newline at end of file +Generic bug fixes and enhancements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 178fa45ea..ac648921c 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,6 +1,3 @@ -Opt-in to Cake 2FA for security. More info: https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa -Auto generate restore height for Monero restore QR codes -Hausa and Yoruba languages -Additional privacy settings -Update Monero to 0.18.2.2 -Refactoring and bug fixes \ No newline at end of file +Support restoring Non-Electrum Bitcoin Wallets (check supported derivation paths https://github.com/cake-tech/cake_wallet/blob/main/cw_bitcoin/lib/bitcoin_derivations.dart) +Bitcoin enhancements and bug fixes +Generic bug fixes and enhancements \ No newline at end of file diff --git a/assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt b/assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt new file mode 100644 index 000000000..de54473f7 --- /dev/null +++ b/assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt @@ -0,0 +1,8717 @@ +000039088e7635bd3d088ff0376a2c12b3d14fc2714f3126f1b2f4c8cf919dcd +00010e2247ac2a27a3f266e1f7354b523e368e3790fab7529bb29d29b2264189 +000447f29779b8c3893421a796abd089a2fc9cc50297467be733ca64d5c3cb70 +00044d7e25a645f5da26bcb906df1aeb5ef35e1c86582579561240311bf8ccf5 +00090cb1f4794070340b8e6ba697711ff59545175b1acb69980a7f0c14f3e6a0 +000afe48e10afc15299e54e7ac3dec1cd6957903067eebc8424f68eb666f3790 +000f01e767f12f7e392693854b4e5e5cd79bc847fded720aaaefe885e00279d6 +001a911b00e9f1f201982ff86df75bbf718c46b0e441cda527a634cdf1186bcb +001d490eb469e0e93089485e7dbf477e872b24c680209f762d3501a8067f71be +0021a875205e02f95cf0f5fa2cb435c950f77a3c27e342afabc28c2aae2a7766 +00262b5e3444d083d2772b12b320f28c499f49a218d1e16af1a72f0221718554 +00289954798211fbdc5ae10303cc17ef5649068f12ced8ca73ba229896993fc2 +0028a1567f9a4a6c619db76dc9ff622d9e177848ea5c89413770426d0a705e7e +002cdc7c708744cffa707f5e161c64bcda06afeef254e730f28be35e1d866df0 +00472c6d9f5bcbedaff69723326965247d76dae59d0a8dfa3c888ff894ef96b9 +004ada6c6d9fbf968aaca13f9d3bc53c57dea45cf06b7a805c401375eac232ef +0051652606bbbefa01e6ff5abc69e785b7588263708690b06d85aca365000d70 +0053b9fc7ac7828051bf0d38d4e77f8920e80787e5eb316019ad7a700b0d8077 +0066a62953fe7aa63a0bd28796fffb1b6c888e337403f830747403be4a05792c +0066cb182adf7a9f3bf8ab07d6342c6f701f3e9cbd19fb09b67bd44f07b8fc61 +006f72695184e9f4eb694ea41049c0bcb43031ba396cc651f2e526b340e35e10 +0073dbc1905143eee9355dbb172930ff40bcaa07f4194128df88b415da0c9b63 +00748efa0448e91376bb58fff14eda5163e5d6f601b23a108a50a92261d8d8bc +00750a3efd0bb937a0a4abb99c4dad594c390ebc9cb09582b04be8b9539f1fdc +0077ff20158828edbac74a0c7ea8295201f8cf96a85bd39841e9afd49c884f5e +007a65ea4c15ef9a397fbbfa0bae4a4b5c58e53071102fb38f6b0f9f5f402032 +0084939fd209a3b90a1e09b69896de6cdddca9780cbf57b9379cad6ae3797636 +0090c2bdeedd129e587cc3dea4b744ffec9f5c2f7e7708e9df86ccc66e3adcf5 +0092e1c7c3dab725117d3f61b07be9d50b2f067434016680438dec20a079fdba +00c78b47ab7b699aefa6c78b0b045ffb7d99bbe7025da177b67803ad82259b8b +00cf7c07b20281c171a68dca3d517981ccb5823f5ea273a3e9bb218705e59f63 +00d71b0bb4ad85b33d62bf115b418a07b329dbe33dee1a9a93b016b67e4527b6 +00dcaa01b15b706918dd3a27dc0b1290cbe61d350d294ba8b5fb48b96fcd8a3f +00dda82b6b7ffb87052937555e6e5073ad5d3c492c4576de1a92c63948d7efc2 +00ecb86087cb1cc6ff59b4e6ea9394398ba12b10b48577dddc5e872940b11ab8 +00f16eb82cd88b72c0fe5a06661cd1f00e4f450eaa2a07fcda91d98b51b7de7f +00f57f9002890152916ab46b3d91f60f14d239e0b48b96f13374790fefb5f537 +00fdadc62e1139a21a0e0edf8bbb7d8e2c0c670831a98efd6a4d05ffae8664c2 +0103850d9fd1bd01510b026a795ee970619056ded7aac6df740d7625c5b41764 +0107338c9d85387be3e309dbb84569261d181b41f4aa693c3724981d6b99f494 +010986b900d45a5b2cf85e40ea783a35c90f0bb6cce682f4f2655d0975511077 +010aa6990557df1746703831acfeacb9e63db28109150cfb20d1b891da0839d9 +01198dbdca402406c23dcb946657a0743fd8966cb945bc4c0896931beb377900 +011a9c580fa0e7d69a177dfee0a7676e539ddc75e434617815c32282ac921866 +011ab37f89d19a791833ebf4c9c68fc1b2e02b0fae970d4502bb1c1de58cb816 +0121f8045225d48b8100e2fcc5c88f6b9e595cc8696450b8f24d92ee56b5b788 +012afda7d24835561f52770a3b7857db41f236878912fbeeb2cae712f5dd6a7b +013432372532de946523c91ebf29a358527f76c97d57f7dcd34ca1329993e0c1 +0135089b5f09fe3f68b796e9f3c5b7db1bf26de6f8f97e28824bc635f4d6b005 +0141fe7684497b1b5aa4d95776ba82f11614c357c99ba30c9b6117e54c50c623 +01436e653b2f03faaf845097e59174fca7fda63125fa68f44d2b40451a1ef340 +014391a1e04420975d3bbce0dd18797a7140433645ba892eb028e085d78d6b6f +015eba92a710c4e4b538f2d069c00da555bf57f8c38c4bd9afef9f37460383fd +01638e9d9c7dd0c49642514daa1076c9555b55c888b6dbb7efda2b54c927d36e +01649b21d9a781b778c80df37d59ef31de2bc5d45a2a80eb7bb8f788ef5a921d +016d30c1a01b24e780d28f6c552856936c643dca57d19dd20c00663894a8b2be +017b3056e5e7f90c72cab3d4c555fc3533217eb37b82cbdf4e796826c4653b05 +0184e29e10dc106cb2f5fa52da7d752a0c9bb8a4519576a8f6a7874c31878609 +0187025704e4b57dfeb0fc07948cc076c12c94635cb1d7c02c8d8f603bc2f325 +018a424338c2328f74de393628bd0d9e3306467d4cd6509bbdced15e6e1b05ee +0192a84c09abf17df69d7f07dcefdf658975b9dba98825ff3b64953c79823705 +0194437ef12f044f97106274940b681e816bfcdbd62345af13f8bfc1961add48 +019657da7cebdf8cd531a3874c924ba242b1bc2b7d863694de56a7f3b23240c3 +01ab967755e6f57d20eb7faafe6d28810b71788383b72d4a0ed6ad74c6cab51d +01abb622e29219767d909b038fa8ce7f4ba874ab0c14f71c111dd99aa714a572 +01b4e4ee78093925a73efc586c62db0a23eabdd0378aa2a109d2119a3437e7ab +01b6dbfac01fd182530f6e535e7f2f0da8f0dffa00006f5976b79f10ad6eb6a4 +01cdef7c54a1b8bcc91ea251cb89b0f88ab878c89dd08560485c8bf3438775f1 +01ce7b781636bed03a443549985215cb85f190dfb250872b443c436b61f14141 +01d91a3ac1c3f26e76bdff719b321ffe9fceb41a8fc5e1546a799632a1bae9f8 +01e65b9cf686a20207543a38a941fe6f593e0c7b21bfe4fdbfbd8722781c1284 +01e9b6850ef3b0f450652c7ef6c9a6939b78ac8c55b4b64274f7552af5e85f8a +01f01a1ae0983ae1d7d64858c6cdec0661fbcde54ebba7c05e88c4870a0f92fa +01f16ecd75c67e1b459aabed7c8dc3cbaf1e25d5fd3079b38dea7eba0ebc439b +01f5bb39e1a2efcd688db104f5d05b2b5f9f23a84fdc335545ee04e38e66f894 +02028d8ed82d2db379588bf14e48805adca0ce70104d4664230f333120105ea7 +02114ddefdcaf6d100e499fb3392967ab21da7e5cd1c01271822dd73dbb54e31 +021482b4e8ffc2906b2180d532213d1f197b02f6aaacc74580d2dbf07a4b6179 +0215e9c94001069afa2da537d68e09c5b5aba1bb36ac4fded93375702b45800f +0217ab4904ac9242722212bca9d460e05d99c5cba65ad096ac6dc552cecdd00e +02184ab24fcd25abce582bd6699b09e7ec7a2ce67188286b214596ee06bdb625 +021b58ed0ae7c1adc45f34aff3e338878641869fe241c115b562b8ef54bd33db +021ed0da8e92760cde50963654b23b53bf2496a89e08e93e61c101f040de11c0 +02256240ff566cfe79c676bbc773693c64672febcdd6aa6ea504b9bbd3b09996 +0225e5d9cdcc11c2c0837a44ef6a6bd7751d86503d9d301733ba2647c93af55b +02309f00b3243374a78972da1ff7df51f09e3cbe465ea3bc280e3e8db6d55e9a +02432a4134afd8c6be9844b446b3aaeecb98daef35beb89d0d0a5ab9739d4fb7 +02460db9708ac93ed17260bf81a7bfc12bcc24d52822ec277bae3d50b4c2ab0e +024b1aa80da00a026d776de396406b4e2bb7559ad312bf76fb8c144eaea7aca4 +02509d3516f8939ce59f3b6d29de933db4a21829d400f3c11240a1e60f01e376 +025b2edd014aa4b3ae16fdab8dc8b79a168eaf72af58bc71cbe65a9a6b0c0e13 +025b8747f8afc6e3cd281694025526684d6cf5091a22b89ce8e40ca8d60d3971 +0263fcbbcfc4f256579530fef1709f1a9c5ca9be886064d9d5dd57373631ad44 +027cdc52eff0c7114e82ac62d642300fc3143cdaa7e412f606248021bad81028 +028661ba0d98ebc3b8f3da7ed3da5d08bc58554dc231713f6f0fba366d4de056 +0293513ed8ceefc6529c51eac76fc297187f77abb1ad3b6dbc288e8b43ca7a4b +0298600936f3b6476d5900d3519ec197d16560af6f0108451dc9bd63298e8c70 +02999f15b404ad494ab5273e3d3eaef3dac0092f05849911e2808346c4cde5a7 +029a7b64570c3ceade3aa2e71c4e69059346789ca4953983dbf9d1b294c8ffbd +029b7b3640a35f2db6ce4de9b215a4c9c8f9d0bbbc0a7312fabc5012cf6cb0f0 +02a11a3d3aa640d840c7789761b3b06b7fbb9cc00534d79867f8914e59ef9654 +02a51316deb0764356c7d248084c305f8a10580035d2c56ec35c87213f1ef172 +02a9f55f904675179ababdfd0e72dee783cdc6fd22bb0b7ef7be9d8942155cf2 +02b65a80f14e88037656080865d38d39ca41194f1654adcbe36f41b8ef62bdd3 +02c3f217606a01eca39264f7a2da194737d42dec5b790617ffe8420a87552828 +02c92345c1095abb0d41233b694af05f39bff45782aa47b7879e7cb3cb0ff26c +02cb2261a4b8aa25ce341a0478c4158a0fc690cc09a4c35f7c8ba9b2d3624521 +02cea36dc91239b8748d82e7e2f4bb71b404275e3b7ec137c24461909b0fc95e +02d18db6ef9e055eae20d4882189b31deb664262f059fd21764eb80fb0bad196 +02d21b2e89f938b0ee84033210eff6800ef5da6968902406a5d5ca9f8c8f5162 +02d8c3a94c10358b2fd81936ff2cf890a6dfc6e57c8ae82a213e5258fc805420 +02db6d8c6530708392d3da5f1baf057ffae4ea907719e9e7b45e6fc2aafa7609 +02dbc3df064ba39a14fa74d9a8e435fc50807ef786ed3f5e458f1fce94dd78be +02e9c99342cab22c730b1d15dcad2afbcb5712ba6cf22e884206ed2c5f747735 +02f457d612ad9bc40e5a9294ef2f6779c2f3ca1d10f8e5750b29eea28b7a23a3 +02f9575e3836178517cebb3cccc16d39ceb3d7fe673a26fb6b8fb6a83126b2a2 +02fadb1ee79e91b40c715fa024a72c5dee1c1f872273d93621528d2f22134145 +0305a8d19dccd295173551437afd27cb15da9cdb4a3639307d1e13794435edce +030a73ff96fcb9ab7a2f1b02805992cebd2faf41caf6c5288cc1063b6ea96cd4 +031bd8149f49570af4ac6a9b2609005e5c2f83c1d760630ecd5da4ff1f3841ac +03269d089f5d08fd9c242cbfd173857c100ebbb595f3895da839f02337a6493e +0330a79af5742edd35389ca7280d215f7cb2fa366c1b99cba92d2cf2f9df3c7a +0335bff6c510add55c37cfeba308dbdf77afcc10bb664ed2c9b29117d005a832 +033f4f385331e34bab2d045790d3360d121078281a47fb5d348be79689cca7c5 +03462bbfd4a46e1db66c7ca1f1e78b1e9df52a79d520ff205a564c5a031bf8c3 +034dfa27d6db1c235b8aa4143f4d003000d6f698f4d58f85b91d5ae22fb69c7a +0350695c8149c4110a47f4f0a17e84008f2c27ca1c1ca57ca4d95965e2b89f26 +035249f0fd16678aecaa954abaec659e64235b474c625307073ac2e45a5ca4c6 +03543d282210b5bb24c92196450699b31425167253525dd52ac28f2bc73fab31 +0361ea4f9f70e051fdd7b1d90d1ef6a8e98e7bf7b841bd19bb4186bfcd7e54ef +03633b8089b373aca617e896f81045b73b0e0d6197bbf98656be1d7f19436671 +036ee09acb46e47b8ba441dd00b2c86d3049b7cc95d4145cb1da1fe8b4bd3b60 +0372da5987082416aeb0c194741c585ec7486f76c86443d2ef67dc05f54f7c74 +03746c1f7dc3bb267515846d752f4f02bd40f3f4c1ead4c8e88751c588e1a39b +0374aa439ef497473a09503ad3e82c944bb9cf7a72d75494d1d7be5a85b8d6b4 +0374bfc30a1ed92d6dd1cd8fccb0607bf395d594aa43ba89b5aebd7c6c4e9e3d +037f9c680266ca9d83b16fc700e55c3e50bdf37b1c2b162a825e8cdd67c898d0 +038105c0c9a2dffeb9866f40861c1c0769978f0404e9a223f9fd8edf8bd63731 +0386f1bb4d6598ddd5569123c4a605581083848e407f70c282f310e30c9b2262 +0391933ab24b2a2aa0b088d0b33656ae564f48acf4798e371ecdb88174076a6b +039c9635f12322c07dea5e392814c3c07bd9a505326c7f0a1225798aa66b728e +03b576f6e487153327f869bd4dc2873d6b4aefc272060ad859432e2173244da4 +03bb87bac9adc40de22deb878466b80f3def10b6158c2da4cb40a743dfe8be57 +03bdfbb8e8c0d35afe96db5b322f3ce5af43de023f6198482fde20835bfed396 +03c051f4fcf6fe77fb67ea99d098e80e4a713fe30eb80ef286db72434157dac3 +03c32e01f09fffb2ad0a0de0057d07b593da862e987b850505a0d6f19a42ce29 +03c4271444962ce30c98cfeb5d5878e18088a6d5f7df75af52a5295d8cb9eb0b +03c87428af54284e489e3d5878869b0b85ef3370c572203a77396b463d14b15e +03d6cc320c869e8f63d84a1fe744776f54642e2583c21d4bb8c4590be51a4417 +03de1ec76c94e716aa0588064886180c48f3519f5095e9981966d3acdc05d78c +03e49c2d24ecdd12a0c265965e0924cc694f3527eb678cb02fbc10bce6ea7563 +03f3b9885eb9dd3cbd93fd5f7e3c25887bd403baaf8a6a77791ec5d166b03cbf +03f66a39625e42189af0c0f5fe3f863629d50a33fd94912134ae4bc4a4a2c362 +03f849d69557bd5f60d42bcb4b72883599268ad9d16e94a43d1731f7eced4d16 +03f84d8dc0cf6a415f76c60308edb9584a1a6a3f12a769bc42a0358e27d445cf +040fbb780fe25fd30d58bc1428375a054cc88bb7c8bb66e5c1e1a119bd76ad97 +04100d4001f581ae1dd3528931892d3b38337eeaf9ee677529c33f0ce3406810 +0413ce625288ada0d0d6609285ff97c69689d6ecbafd1eaaca555e1b56cacdd2 +04182686a830e8719f09b4762c444e3329a111251ea160b7c6d7b335dec31d93 +04186834ba72c034b0885d812f239739885926f56e54831e22fc6d0dda345f6d +0426e87f5dece79cdf35750a06455685d06a5ae7aa7d86daa49a0bf726b53cfe +042809c65b6fee594fdb07040701c2c7e81919db9005fd1bf096233299f3aaef +044012f23e92bf935ce2118206851a00cf78f1e80b3062ca2a67a29415c838b0 +04451199b00988d48dfb745fe35ca369bfa1f7947c0c4ab3c9ee22e699344114 +045478ca6cb5485d8377ba2dcaf44706b9b13797f0ea42d23a24c5dabf860f19 +046ad86c5d3c8229c044024baf033d7a75c2bc9ca0b5e75f62f59a07df9da403 +046ba8f91579b9f0c91375308d5fc1474e38d6d23546d09dfa3c71110106c83b +046c9617113365a1e0edabe3eaf32dbebe21931617b589de93a3a17e3049bb35 +0478f06649ddddb07c677c3b99539a4fca0608198d0609a5891c924d2fb26a1c +048228cdcb498a1dafd8bf4293020f905518c902432cd4e778c56e1b6277a457 +0487dbf96b49409456d5769958e768816d5cc0d49bada3881ab6aa1a27ba1496 +04888123a859f8f1c99c0cee494ac186de9a12829b693d17a486ac029e114c9c +04941180d91544b64a41968bd4e44d52b656543b69980aa4342f735927d5af33 +049c2ee4914c5dda2f736ac9acb34dbcd5a29c3580982004bf28cc8fce5f927b +04a2cef1454824dc608489ed51cf4cad59ad2e0eed9e77cd3194bd518755ea9d +04b3a6e30db291bc6695caac4e329242625498b16135f6cdc9cbd6bb9763575d +04b76e701ad0980945d28360317df87dcfbd2eec46bf51acadc34ea15176b815 +04bb3a4a17c127e78aa4fd9ebdd2245c996d782daea248dc7a72533a66d2d83d +04c4b8d41617216af068faf852d956c95c26a9aebd6d7b4b67f9654faffcb417 +04d8a5a38bbe7ba6e619b4c9d27ce02e95e4a41dd1f55915ead929fb3b847c99 +05077947f347493d8c06b4dd9bd42f67c251bd3ecf9b04c696eff5087f08bd2e +05155913acb2e2c0cc6e21c47b2125883c79c2f6e097135da1567cc2f06933f4 +0516d529b7d6c29ca51f5e9e43db2c14192f29f3c6ce55dd14733095e7a3b108 +05173768fb647a471beb316312b0a7e67140de3835cef584c25ddcd196bd8d25 +05242124150e05aff1914d8731c77f6cc0e1080c5585c2b509e092ad8147eeb5 +05367f6d632c458312fb8e5782116372d1dc0f21c5acb2879e224ab86123dfdb +053968d4862c453243f3f163f591e9082450d2c58a96aad61bd95209612a7e5b +053ebd756f4acad5a18a3b5d638f9437c056dcb0be642c748e283d9467d3570f +05484895266fb2f54142d1fc85710a90950442b6fb144478b663c57c3121996d +0549d3cca5c7972a3a78172eb103e4d6a7dd9d11c46efc823fec5ae960afa6bd +054a734c424022c3ee898dd9afbd0d7854d84db91bfb2d60b1d2af16e879bd23 +055990de128d3c2184504e0dec3567f9b5b040d16520013a0de1d7c9a689dfd2 +056390158a4aba6bbc048ac7c3fe2792b025121e7ccf651cbf9a98315d47ada9 +0563cee4924684bfcf9f83ff091f640612e90a7df787ede7ad0cc30579e5ceb4 +0575e1d112baf91229bcf52d1ffa7dbca0c0ad3ac4553b04bae37f769aa0e39f +05863e650fc0deed0803bfd92746fec2b1c914f7ba3ace71165c6c1677e12e91 +058aca3f1bf367c1d3e73342e468f496da812ab28133c27a5b96a4761140a120 +0590cb4b1d49775e1f0c5ea557abfd89cce3c001b5a0a201754bdd501d2d0444 +059236edf2f7d9518b4fc3fa01ab1dd9ff1e25e8459b84495068cbf57d4fc356 +0594db5b902a25da02304afee9e3c7c1a0493785f992d868756fa72f3cba09a9 +0594fc24ec3209133ff90d72e3f969c33783f3fb8b2350898edd170ff9b27718 +059dfb1993a824317d583512f6072cac3fdd275352abc77da1c107c6395dcdcf +059f89c76ec40b05076e47b4b3e14b89d3e3272c60ee122f74e27537efa78c34 +05a39c963db9978b14cea9c85d868a2badd4958498a73a047232e8b6e70a15b8 +05a5f45c0aaa379c811f56d64d56de502f49c0086d946d57bb0c5184d11a86d5 +05a74b7cb3175732f8668541c29cf064f3f520f9b5d99a33cdf58ba3437a6f3b +05ad3f8cc4514ecaa3a9e5bc044657b3ec9c6033a1aecc9446aa3b2fd42f020d +05be62a10e2a4ce727a756b1b60fc09e4f82f63bcc7e115dc19325dfa6b4fde7 +05daed35611cf06308df9ed5383fc54863972c15542fd6fc834a277c8cbe8d8e +05de0102f7f07e40566d7ab346b95aa6a05b87c0aaaae42566350fad2ad116c3 +05e772545d3ac48cac9a4f2f71ac41e102c03df2eecb5ccfe0cd669964ec95c8 +05e7afad054a2e8b13891b35517fadb32a6556f8f48b40cebfe88d3caa844f9b +05e7b44dc734e8f54ce673caff49321c52e2a27a0fb276f3b7d12632394aee7c +05f8c4d4cec9b5e40b17371e4fd31a9578fdc52b8437c7008d23f316b23a57cc +05fdb1caad2e9496c936beee05ca4c8ca61eb47e09ee6d0903e2bb09ba50bc5e +060aa623da5b943ae2a44a68e1e6e1aa7423b02666ed63648027f3b36b457363 +060ddbdbc04867f65fb1728438bff362d1b89b27b89320ca9e6642a199cbbaea +0614fef14c50e542f4610c4b0c915ec0a9de58d0aaef86e2cb73406292c8867b +061fe3918274b66786fae5919ec038c11d8ec952bc12694ce11c4b8ee48c46ab +062b4b2ce5bbbbc861200b30529f98bc6aa13fe583428a1fe35e601e42374101 +063013ff0018d6005cb4485ad1c43c3c1b6e6ff939539ad84fe4287f954a8758 +06428b8d2aa9e4d7850e33085829b4910ea16c220de558927212ebabcf6fa38f +064ce20b230a936b7e083cca164172e61948312e2f73d01afb1c9feab451d9bb +064fa41fe471f009f003111f86f253991e6bd3a111e960f5ff2420f4c21d38aa +06522e5b5783e864a7270aa2cc822c449db73eba1ddeae9f941d0bb153411f79 +065350dcb7e189b5d9dd782d744d1643b52a5a4b591fced72aebc4f6de20c86c +0654954905b89a0ba48a2d0db1f6d6ef40425213ce169c09de86dfb4aeb341fd +065fb84f704f065ce6c497cf5b40c01590596d3052f6593a902187a74e3f6a85 +0664f30f1803cdaaa5c1a48586c7bf5a0ba24354ebd185a948631cbaf5e16b1a +06695db7e36ecfe4298993a0a5083b57ae4473a527f909f5e42200b751983543 +06702d364784a96040fe7faad17ed37d54b30a47153343be8f674228185d03f5 +067048717a9009cb98700b5fa72d2547fb1763ea19228ebf749213e7964676a9 +06732f214d050fd8507870649d077fb67e75d82de1cf2a3512f609ac047b8f33 +067d4db9dd62d21a379d1cc202af686a9914eda3d630b13c917c3c9de5d53b54 +068601df89aa1c212c681cb2524c0243613c6fca2c5927b17abd8c96bc9f28de +068a3d0ec7527621db5782acecc80e6f238a0df45889dea5f6fc4d808a632d4c +0693ab66b44fb6e20d5363e91779fa678fe58f8494a4855a0afe0b9be494231b +06982350d8be8af55458fb224ac141309ea41d2422b7ed105b734ed19d72ddd4 +06a30c9a6882ff0f8416d2ca26c1ee1c6b3bea7597eec44415376e69f1c099eb +06aa561fea0b5e5b0c88fb20f3a39c711ceeb326815b57f4d67240e8845132a1 +06b5c6811097aac10872af086c5ad55e018ff53d724a108e786f721d6bf4fbcb +06be301723e59bc4c24afc02cd1a4ed5457c073b0360af2e020bf31a6e52ad0d +06c9ce2b4f8a795970f30f06a1db140ed36a3e63a0d78769bd3d57fbe01f749b +06cd04027807920542bab041bda329cc273d1661c2773337c42627f88a088e6b +06dc6e02d2b8fd80af8ee0d56f60405420cb0e00d8d1cfdb9cbb86215bce0d75 +06e016aeeeaa94eb42ce1494c42aef820e51a76d1615003e4929d543064b4d9a +06e15fb4eb4bd6c66ad4cafa093f304b596aa680ff7b261dc5f0e3d5b4e8f3cb +06fc8a839a0d50846c34459644caebd9e1e489cd6e7619a25f4f68e44762073e +070025230c82b2aa3da0a02ebca48d20ee2f5c08493106f4cf1c204944be0c09 +0705305967a61138accf5938b5d32369771524c81317e645a6fd9ae6228e0075 +070e2cc58138031dbd5d92fd05aaf9c8a5b431ca4b1fca518d3524b9c20d0048 +070ec346f12a5526e350009daf540145324f536b26438281173af729f8f78f70 +07121c0b70bc4fa75568e15e6417005c6539af5c681ee4ae515404ecac762073 +0713065ae385ce486e38beafed7cb034a0549a9ea9fa020e2409d8a4aeb36ced +071c93bc1b3983d14d2365d8191149da91075416cf8083a4aa85b9426378c093 +0735ee5b86d4907900ac53b3be35e079ebff6f858136dafc71127ee2c9ce2799 +073df1a7231bc1d7bfbe4b9d7e69b544329cf375e710554d932d4e45990e6b51 +073fcddc204278088d02eefe51439f94c0f6edf7d4c61cfc373fbb6c59b93ff1 +0742f9b766d324756437345b49699527800bb32e82e662ca5a912bdef4b9836d +0747dee4936afac30fb17496b71ba5f73c3ff552040d56969e59c0816044e4d0 +074ef6aeb2af0ea564cf94f5d2190688f2fd42b3e95f616cf95ec77d0209f2fc +0751e5222c89f9f5ddc5f7b0b37051a3f2175df1c92a3ba985132db0592b14b0 +07698dacf7752fc46893a53eb523ae24fedf8799f2b047e867b9475783065aa2 +076e4b805d8b78282ec9436b857a580bd74218f335d59ba590c9415abe68b1ea +078059ec52d056617cc53d20f9a3d4ee19d366d776d40808df007740a6ebf5e8 +07809abd973e0f1812df8e229ed50ea85fea486b8416962b869cb2e0167de61d +07860f8fd4ab5817b2a8213e617d75c3ee6b57fee014547a5ecd3e1a071f6931 +078969b2010df6a2b1fa707989e33cafa1c35c5dd8da73ec70c8e586d90d2340 +078c1e10b62ae22d47cb9812e6f0b4201b0d194c3235aa41842919c5785d5d1b +07a1d2e8c3fd2ff8a00e75ae2aa7a18bab3b0a7fc74a63b2665b2736771c4f2e +07b1989d39d8b4e0d8ebbbc44a891ae999515a54207b4e2f50428697142d8179 +07b5cf3692c956fd209d1b439cb2471ca7ef9028bd93f736c04a0eeab98c231d +07b60f0b54daa140784b16ff7476027eeddfd78634445a690cb9a82b15a721d9 +07b654edc5c89aa0b652f126a35e14553a96485263eb2ea3ee204ba41bbd9642 +07c1df33db5cbdcbe27aa583632ee45368b894c69b50cd1b8e082610ce124ae2 +07dc302716b82790e8d84cc89caef581ff8764e9aba508a4e286c049606073cb +07e2a983256e5edc890b07015606de6250fe74bc88c4a07dce46b8ffa7d80582 +07ecc0f6f69e3c58dbb40a66ddda93f73423d1d338cf62a43d947bcad3028ed7 +07f22c12af7f38c4362f5bc11c1a0d6433f3cdb6e14c9bb90055ae3d7ab63138 +0805393a9ddbfd01e79d29697cc6464c9d5e60ef33ccc7d7123013a69c22c527 +080a27bb42b278ccd08b825c611f25463b2c0b7db0f81d69ae578d757ee4098c +0823481560e72eed90d56d069fdf65ebb2d1cd0192f04be3758baab80bc6cd8b +082447e59e1fc6b08338dbd56dc59b1058f2586899d7858982f2f701d57f2006 +0828706373ca8dd0bd88a8f41b08015ffb4bfe1ae725ebfe8e43eb95a9ee525a +0831efca1d4aad1cd08d883e2e70b2f3a7fd1f0d0f0eb85b780fbd3aa21409dd +0833543ef052c7138bc4e9621fa3685eccf5105b7ccbf7e704021d15ad0531a5 +08367b4a6da7ca5e0ac0811a445662c2efed6f1fe807b4ccd7e368fed759829e +083b2e07621b24e22a276fe58eea5548f0901f377241c36f60557182262d2827 +083e44bb9ad7ed4769177ce7d68c838bc679dd9096816ca363d0fd9d3ab75e69 +083e4892aa97360a2b13bb81138549b14d58b6c0c2ddeb008ec8e8b72e8c8ccf +083f09473dda97ee7e0949296d912be2bc681d3d5034e7d33c9386a14b3d3766 +084462e855e751966176bdd25e30ebe3f225410329816346823c30d6c6051a97 +084ae30994def7ec8baaecee5c16b336730bc90278224c266f84f48e3750fddd +08556a63e42d3fa5375a06d925c792cd91d0706a379dcb5138fa8ec761ceab5b +0856d8a51d004f58ce180b287b26214d1e5038d325e4d8c9f12e6edee491bfdf +08652ce9e29131f92b03b9b4937a75f2b1ef71771acde47eefac2af4cbfbe9cd +086c32b68eeda5ecc829f62a1aff368d4fbe5ce9e9665bfb000f9cc9119d7c2f +086d43560df06b20284dec5bfdc7664c79052b178d6eb70dd95c9dbe8f9efef5 +086e59db192ee7d333bc39ca05813dc6caeb55059cec22f21a908d9a06d08d6a +08708433e31ff0a97b94db2d5a954076214ac15d572f65c952eb22c60f564374 +0872c2d3dcea05aadafd76b88bdc9a8c3d5f568f6e6dc5bfa415d6c81a93ff52 +0874bb2548e83549b9fce1fd8af9ebb2c5d87acd7b01d163da8b8093858ea2a9 +08765fbe14dcd5a45a887249228c07fe6eadb356ce0a680acc4c55584e34bd77 +0879692755624cd839e288b473035e49c8f44bdd72e8c4601f42234ac94c3e6c +087b54432df80dbef2b48414bde08c327472a5a9d548e6f129c2d9cd96999447 +08896c1cb25b75a158875772ca2fee19bed77064bceb623c63f1c57d889621f0 +0897487b4fcfb2bd063412dfce99e2469686f2181bc07f12e4d1fb3451d7e50f +08976eec68469cca29621229a31060c547d5fa96f365c48b1b344f03b56a2bba +089fdad244246ab0b2d9dcabc5e419c58b544185e0f8d7a38cc238cd2e3fc132 +08a347414cbcf854e4f7c145e808ee733afdf9476524fbde69359f961887ab1b +08a498ee0f36b7b2edb5cbb27386d66a12b9bea0ca319f33e1eb7649c3ae0eb9 +08a7f132d91165ec5fda1832103f25a9cc9e04abc92a0e6d8833568009f47d5f +08aefadc3ec650da44bd24da0c32cf9c496eed87ea1f96b0b3fd71823f8fa32c +08b33384fd2bf69907ef15b7637475368aff808438ddf7d6fa01661405f45b66 +08b6fabb1622b258f7af8ed3bede949ec6249f3a1e2fdc0a1c601e7a11398424 +08c2587319fe93aaf1627245eb2709c076b20a6a98fc962cde8166c7f76ac011 +08c360060b6c87bd0d383cf42f579119495959d2099f49b8c27a388ad54789d1 +08cbac9612944b891e654d3bd233d5aa67b62e4571dc366182405653b6b49828 +08d139ad48bc0c21cd9f8ab721c09c63603cdad455147dacb17dc7960b354118 +08d4aa18e8a1fef29e741d95e9a66664db94ec1f068d27b3c635e0fbcd988e17 +08e7e84a9cbd313a1e03c66e3ef542e826cf4a5a995de3b964e2eaf26b4d3bac +08fed91262805f0a3fee90a66d66aea5696593d02bf33c8b5abc18d9e06ebb80 +09077a0e4caef161f9194e521f25151fd7b647824bc579e4dae0a652077a2d15 +090b23e5254eeb7928804b07d81989a22ec2742dc52a88714be8b577a85d2e7f +091b570f4868275dba0c65dbe45edce7c1c023ad170c5705a99b4bd3119f6464 +091d389ed5cb0af0fdcb83ee67019f385d79bf4974a721d3e3f1634ef0e9bb8c +0924557bd00814bb92076f8424216a4b27cf701511e8f95af7c538e69060f1c2 +093fad1a4f8ca4d1870a82262934b54626808e022389b9be7b2e2cff3d9f91f2 +09455c8cb0392bd0cb759c12ae0fbeb8ce5abd7898b29660350711e193699aae +095548dcc592c14e6443a0773b44d97886525c155d5b358543c34ccd42eff6aa +09579fe75acadb47714366e0bcac34727df7550930809f7ec362f097e5bf2543 +095d0e9083a68cfbb7ee1cc411d6a712ed682795cf5b5fc9c01bd365a369ac34 +095d49a5de862cf699b146db2172e149a22b36df95399b0c93b0d74c1fb7b6a5 +09623d68d894c17739845e70e1c6923554b461747734b2c4c1e68cff92e9b637 +096734c0ecc98b9af1e8af5a341316b81cc988e04634b694466a66ba36a85967 +096a882bb9250e0313a6887059f98e95dfb612e25c92e3e2b18af7f367bd6298 +0974cad608671463d46b37e85541a7181d8658adaab10addbf1f4a6397f8065c +0978ca20bed9e373d61b6b0f17aafa1b7820ad3868c590cf076d9cdc66963cb4 +0986fdd946ae9d7b9e2d48929bff694d9565cb7ba9fc7762a5c703499d54741d +09883f1dd8bae9a7baec4fa048c3db3e068445cee291781b7cce34b9d3793754 +099f1ce8a896970b159ac9396d413c673f54679c1e29eafedf8c563a40b6397b +09a6dce68fcfc37394e6f02aaefba88e96813ac93a7fc4ee6632acda580a7263 +09a8a4ffd987528c117cf38dfbba17898eba9ebce6574e17bcefebb520cd952e +09b13125db80a15ec9a6d1163e82053216c43215b3b25fb3712f990749a62950 +09b5a0276ff183937e5c3c08925b35774af7865017443f47fced5da95372aef8 +09b5b5e1a6d8447f0a5623c5e8218a07cace0810a0d4caa9509dbec3d0874169 +09bd9fb287a7779e230e11cfe30a90c6e90fe22524283160105feaf5cfe78c70 +09bf3954198dd762cf1c2903ae760c1dcf99b2506b0904725dfab71f998c7652 +09d01de4c403241c7166c21f5088745d23402cefdc2a5111713d288b6d837a72 +09d25c1a3bd507709bc1088206d5fd423851df0753f41b63f6bf3631f011d343 +09d44dac3108974d6b1fd8114705470fdfb95b5895ec821f12673c4a7deec47c +09d9b6f80623de453ca765ae3b6d5507a82feb486f3eb05c8a6418c5f073704a +09e504e676b6899f04743e2cd982e4c51a49152d850afadcdddb260f4e95028d +09f118c3377f1db098f43e12768eaa7ad1ad580431774f34134afec399929111 +09f145a6b0f3c6660159eb62140eac2d9693eeb4c7453d325a10359313bc3eba +09f688e259c8cccde2a5dabdfd3bb46a13c16e0a54e5d560633851db3d72a6b3 +09fac6a60677b29ef48997cc3a26792682cdea6eb177493adc8ced27da9e31ab +09faf60fb56d63cfdcd4f5f3797ef90cbb0ae3ed8cabf8059d4c018ab41f0696 +09fc931206ecbcbd54a209bbc3dd6d45e340187c709da0af893d17238b38083a +0a01847392e70812eac5005b064e3dea3259194ad24263b223e3eb230553ad02 +0a0457db9953078a0539538367dbf7b70d68fd867db8a49fccb71d2fdcd44941 +0a07aabde430c1ed43aed3432ca603fea749afebef03562bb7e04ab0a21fd4c8 +0a0d91be048549f88374e56f854dba1c7e8c7b1674ef6edf78aaafabd0d396de +0a15124036afbc1c37aff14fb3070bcebd2bdd71233db607c9e92c48d33aa112 +0a23f7c5418a96b00211599fad9dda41e5a7d3e1b93e89b6bdc01d3f74bb47c7 +0a280ff1aafa425d314f6a5694b1fef7c253e052ad8a57f57d778d09d898fe4c +0a288bfc64fce7789b64387f1b64e0cba23878ce8540492968e793ebc720472b +0a3d2cf39f0a113d7133f657ed9065e051038b95b8918d91ff8b681e2369152a +0a3d388797a893d3f58d53753fa201729f913d7af089dd4d98129c1044d7110d +0a3d6b6800b8c3e62e81260b26a9fd406bca20a6ac453614da03ed04e9e7ef67 +0a3e715105a2dbb42cd00566dba0a7b2cfb9171d73ed052bd3adf54a847c7f5f +0a57d8cf74a18360cfdc11f48c0c86d32fb4e6c19a35ab7d4f2e0c2eeff6dcd8 +0a5bde248f1b45a2185ca2221d5d0593c03cbcfd67f3e42337dac8e3544a2579 +0a5f386bc7afd1f7e71a5ef08c2def8f79b1c5078c61701e947db8b9c6dcfd98 +0a5fa6feef167a45be4d996431b0fff43d28112616d6b7ecf37e160584f3ee01 +0a654fe4ab795d6ca9c0476c6673da34c397f8cc8f599a04ef9d6960beabbce8 +0a6e5ea817c43b572b20110bce95963d2ee80c37eb6070812eef93e4626ef60a +0a6f31a77ae32557f041aad10944b9c1fb0ecf9cca8e9a8ea1dec556b584295d +0a6ff18a42774eef488f6ceed43ed9578499231b48a540680785beaebbe62fa1 +0a760af0d859cf77247c7ceb344ad983fb1ecc08548f902634c0b4f675cdd540 +0a9820eb81dede6bb238cf1771bac034dee63fc881bd52f338346a632fb816df +0a9f82e8436964cd9bc6f6c9c14d09a90ddb2e719f404545a28f246ce8763ba3 +0aabd8bb0d4798b3270dc04874fab0e0fd889eaa07f2d1c444f0135e3156e22f +0ab2519f453043e132e59df0edc33a77566938c3d47f2a9618436fb0950cb9c8 +0ab6662bbf060a7ff7eda4a33f20973ed758fca8c97dc86008f65d371c147c78 +0ab863f852e3f9b67a10ae7455db6ede2b96d42ad7d150009e03092234cfb6e7 +0aba79ff74b89bbe24b47db992a9285a9890775dd428d15880aa701dfd01a022 +0abc7d340258cb1110816d52533a17fdd395665539ca260e1b76289430510fac +0ac9dea425f14db08aa94345d1223b14cc635e3bddb16687fd39ae0d04e0f7ea +0acedd09af784d3f53851b87ee99a630fa0cee91512beddfe5ecc0c875111a4e +0ad114cfea0e4d55e9809b4338fd46568abc7a600c391ce965dbbdc97f0a1a3a +0ad3cbad64d2aa284d62cf32364468f1b72fc9314d16114287b2ed76882cba61 +0ad652cfe8c6d12f25c0c56ea1090a15b251367f1f77183ab3238d54afcd82f2 +0af1af70f75ddc074a6fc642085238f68d562341a9380db3848404e9e561a0e0 +0af2a1df2cb2aab17799b57c1c8f51f263d1cdd2bf85ba3ef01c87dd5b0b3079 +0af2ef1cf705048e77ebe0ecfbae87dbb7b169ffaeab4b492fa22ad21f87d1e1 +0b06bab63d4a90dfa901722ac17b5754d423d2c92beeb8141a662c34aae8d861 +0b07372c2ae2d9f92d2598e9cf1846a706d798ecc68880b9ed9f6818b4d13d34 +0b0747f9be3d0eed16b423dd10ead14a23cf8525798ce9e6c018a56c12b82d13 +0b10e557fe5a4350b3eeb3472b38ad2bfece1e48b0a9e6c056d1cb3e9fe3faed +0b1b7e8f2e05f6c35733fd71847940790efeaa3ace0d40ca07125d4ca18cd238 +0b1becc1034bcd3a0efca2f9b23a16c0611f3dd213602cde9d1b7eca39520718 +0b1d40ef48cef68faf858386edbedcac6d0b50dd077d147b324646d58873e1e2 +0b282fec163d7ca7b6108eea3fbe9190eb54e451be563112921d2ac9719e8334 +0b375cac56a9c075886e19e406adf9a2c55dda1c57ccdc622b3e942d581c65e0 +0b3f1e4c657dca8c1861814579ec1ce6c651ff43ae88e9589190810ae4833fed +0b4af5662df46c2133efc733b41fdd0a500726c2ac725b235bc451beb2498015 +0b564aaf56a69b42b78ac462eb622a8111ea00365adcb9affb7d05914996ba0d +0b5bf28c8038bbceca2c9e3d9839a6eb267ded39580ab7b099001b4d7b87d651 +0b5cbed090a2130f51bf232aead4578dc2e5b812ea4f395b95f64f4e0181b2bd +0b68893d34b95b7477a43fddddbae4093e57a952b0555de3eda8b4bd2f84f267 +0b6a0e9c269743241c1a595148df1f03d9ba5d7be29bdc59a314643fd6f6255a +0b6e458ed99acf85c6ba43e3acdf080697a2dc389b0caa7170aa8be77b29f12c +0b756c1b3c38cb8cf5583bac808a2df043d132c907f7185544860c9c5eff32b5 +0b825d8b2720184eb7d7221ae77621dde26ad4ca23605183bc5f8f79ca2cc740 +0b88eb2facb360efa418fc3eece855bd6f13638c31080ef48cb3cb6a10cabef7 +0b8bb30c443b849a36eb3dd3b929e7cce70aadcb17c2f121531111b3d5f42b91 +0b8f990e77c82f1c999934f3f93d57f9d55fcb3e423eb34503a1643c1e7bb521 +0b94ebe86979f7011b9e90f01f063129ee89bde3633231a294abb67f40537a42 +0b9519a86edab8ebf69bde672944be50d77f3367f1fb3082f74fa1a7fad30c91 +0b99720a903fb13b07d45a76e38800e33170bdd905c3c6dec49c734c86cbc5c9 +0b9bb8fcfef0128b9ad0f7140f79f7cf60c1c4e7dc039ecf97fbf7fdc03f9ca7 +0b9bcf1232091363ae9af829d88273f19b0880546e36b66d6b09f088d6ce8a17 +0b9d49f02dca925ccc8e7d88afa1fb4a7e065ee4570e4fcf906d6c327753fcc8 +0ba9ad68bb8ffb48fe0e74992d6b442828824bb63bc7a819ce18166f8243f8d2 +0bac19f6b76c3db0c0a82cc753350a027d999570b1fc2c56703efefaa9ff9170 +0bace2fcf574f0e0560106606c23444f07e6fc9e05b9f074258b1c7a36102588 +0bb5a743e6407accd192745855fd7035800b08324897dc0142bdb9a5893e0f34 +0bb73952f2168781a4acf74cdc65d710300ed0165f528b301a71378797570f67 +0bb78e7b46b09b55054dae53593997d2c1357c78ce7d817be045f9e7dacc5894 +0bc759de71afba50e06469dd382273bb99b7bbea0647c2130423613d33041105 +0bd02d1e65c231c904cc2d681a33cedd3b0b420433ad560bb5dbbc82393cfae0 +0bd41539680e07ed31e6f57d5ddccc822c4304f2644126f5ac4973ddab98a39a +0bd4270a4f55d6675dc87a9ad4c97d37593fa977e7027f45e43d0481d0b0096d +0bdd97d554c4bc7dcbc7b57464a0f0757d86b84735795065b707cf4e68e843e2 +0bde4a89b379f59a7c2daf4c0b01200f7a8f3aff85e9674a613b8cc0a8e6128a +0be0a0dbc961c3c6707c2b5248123d8f7f1c8c01b362ac08aac05a19e3480256 +0be14ae27059398300048e18570de0a31c3c7226fcd51e0c01422b206a8eb177 +0be1ff0ce1bdc60a1ac13fe9d40db374475946ad1485309914e9c3e4d774d2e7 +0be26c186254f95ff70984e445fe0c9444e82bd93dd26d3d43def4e0e503ba7b +0beeb08b22f9a01db4ee3f618fef87d5ce30e85384b8307ada340434c1cff6b0 +0bef61a470c418037457580392b17bd73f94d2d49203effce0d4db36fa173461 +0bf8856f5fbe0a709f78b60a53cf18b0bf8c6d2b0e109eebbc9fe5d97127aa94 +0c05afb02298c25ca7d8f70d48c03299bf62e73f886187f69a58495f638f1a91 +0c08669bf6bc38f1fe6a4c46a6f114f00871435390192b7b872bb11db8308170 +0c18bd3231508b98c7a54841fd1c7d74150dd0a4f596579844712efedab484e8 +0c1f2329815e733362ab988b3a6a6caf3f390d7cca520e1627c609a46b8adc30 +0c20a8a527d1dd12261610b7ab6325c7a9a655e160a78f342bba56a5c025f8d0 +0c330728b5f0aee6fcff5a9c68a835b541c51c0da73b22296049e59b9bf6a8be +0c3dacdb9ba6257c3c791a642a63b7733b8eb72346b2c5a24b51d0537648ebe8 +0c519bd01d25c926ee7b9c3ec5186c23da2a28f2a31e6905f5c85797be673345 +0c6cce02d69ae68fff8e781e50c7dfad992780697ebc3cc6f9a35e94999e9cf3 +0c6f736cc87b5291b8313852697136ab3bb02ea06d00ac364a282c0d229058f1 +0c7d33d09584b9590201f62dcffea61213c5399a37ea1a2e29c2e97a47427b82 +0c81a6324a233d2afdbf5f09362749345fe651da92733d72dc8b6c48abfe7ac3 +0c8f423e0d3ff395b068b24bb6f861fdb1cabfe94d3b15f5c7af90fa178b3216 +0c9b162aa86d5637f84aad5a96c8abbf8b05efd9e90af157417d2041d3ed98b9 +0c9d4f9c1c566d7b9fa9b21336f130d735dba340733f143c939d2dd19d4099c2 +0c9e7d2cb87bd076e0c6b8d69dfbddabca8b327206b6c4798753794128e7b589 +0ca4103621144439872755bb33dbcde00aafe42ae6127e17fca1ce3c3f32ee80 +0ca465d30967ba948a131dd776395598cbbbb75f855cd8d57ee5b231a079fb1f +0cb188897d51a641d2a798a275ec7254c539e9b26f5a6560f80ce4dc1f9c4c68 +0cc078afeff3b6a67ef24588f0871e4696ccacf165b28ee558dad85f8813e48f +0ce3fac3dacb4a1b3d04225e5f80b33cd02b6e8b26fe1af0a16f37513a4e26d5 +0cea8fbbd20984be9833861735ee1ab35d61cc3f68cc5e619319af55780317cd +0cec516d225cc4a560f232ad856d6141b5d755c140cae0e7f9d6875b1e238785 +0cf511e04442bda4c550aebafd0d29ad47c0f8062f1d6f73d2b95e07bc6a5a4a +0cf5d3a916489a13e379d3f6f1347d9ec1caabde3b5d30c77725c57efa115267 +0cfb40be305f7b6f935682e3f48956aa6658efabd4ee7db34ecbd6dad5760277 +0cfb4620b6fe64877465a3b6417b288e52f30773854d0cb1f3e9944783e8148a +0d05314c6dc310a2d8e124214c15d061f0922ba24c6a5c3ced86ab93e430a20f +0d079e387f9032fdbc4e82af9bd3ec24db538e34d1c1d36f17af407e306b818d +0d1730a8ae3098e05b98a2cc20fe935220c21288f57fd112d58f4edd29b8a110 +0d1e2b3bfae5d5e4c4611d539d84ae6f9177412336a6099af7344a120d8b6181 +0d2a3a963dd0c2e55ea12c58019c8e596ec7c3aef696ac6d35a25a91d92af46c +0d3d5d06a5f5c464321c873cf4a58076b160c97c68f5ceed3ea2687a8584658a +0d5118c40c97da53d3d3f3bfa9ab56e903b16eb2148622b79815094f23fdd5a8 +0d56259867909bfae7d1a93719dc19e9a460a9f2cee360f7212dd5027dc1f97e +0d56f79d52d4117a7e747f3fb158d3440bf2cde1bbe03a24b5c4409915e44962 +0d5c409f36475271b59abdcc0a545e1d22bfa240af5106cb65ba75516cce57d8 +0d626c04e8c70d719ad0f9ecf141f5bc86e51227fcfd536b59c6e938893dc574 +0d6c6ecfdde6d52ee352a7b00440baad6a3f3ac33690705d6e12130131b2d6bd +0d792a51a63b64696f7549198932dc1f3af5a7db261baf7bc042bf04ae906e46 +0d80c24e8805b2dffcffdfa42d7a8180a11cbf8c6b5256bcaef179a81198805c +0d956531d954da2f98e7b45b31202fea5b2466d3afe9356946884445a49f87e0 +0da5ec42ea152cd63d3704cb83054d636fa3707136b3ded5e5c74b767c9ac749 +0da7319729a692957b2320224aaad138670a41f0e99e7874d714cc9f9e63a50a +0da9e52cbe4cf59e2800cf7d0cff18d5f78a4d431fa579aae04dea1a23810bde +0daf04c44466e97578ff7b448b2006a022a13ecb4d7a0b484a975739f7ef2932 +0daf2e07f84d09ad526f1e12b29472fe66d41349f1b9e94eff83d17e167a3b8a +0dc7bdea69ce535ba61775fea77c2ca203468250f91403fcbc4449ffb06dd106 +0dd19c0df7ab7f5439142ce81c609fa6fd0c185e04b5ab8fd88491301c5071a9 +0de9ad69e765dd246c2f7714a423831049926087234ae52a1f38fbcf95e34060 +0deded4daacd402939394f668631bd9d675942ecd67d512035a2e2cdcb67123e +0df00e5070a4e6fa453b9bf30347834849cd8bf4acc3095844a1df2b67e093ae +0df0b96bb1cbd92244ccb841b040d9f3b2e4f5175ec83d56508ec0459a0b8e6b +0df6316a8173dce1754b026550ad371bb47395a8b6fe9fd8b9aec881ab68835e +0df6b83a39cbbf845805e13b733cfc88e8b65331f2fd1b4c48c328ad44e7bcc8 +0dfde2a1df7cd93d3d425111256546eca51db19c9d41bc6297c0430f39ad1ff4 +0e03e01fc61fe18829d28fb4b6bcc28034d684817ef36d747d477bc0dc51448f +0e11fe59c372fa50f5e97e02cb9dd9bd459a87454cdf1ba29536e59ea27a6be0 +0e136e88740d267a80d1f874ebd0bf5e99eb84fe4106455465e2010617076c41 +0e2c438e3ad54878715e2749f5bd98058ae49931c5e4eb691d3f372ec412ea5c +0e2f1fb4cfa3c8be9d97f309dfeecac77d3cff245fe7d6b36456d00b74b4340f +0e33aff9d2c594edc85d6ac2db6b8c3cbfa9711b1f973c2fa8d2ab5f745fc4e5 +0e3890a77ff679665810c12abda8dda1c4c55815eaa994afd2394cbb1b59c4ff +0e3c1c83c96ed8ebfffb4b560e1bc83bffce219a3a5e7ac98c4c78c90628cdd6 +0e41f0625e48da5c237d80c497253f225eccde02de6531cfb1b85a5d36e51eb0 +0e4cf82e5d6081344186dd64a1825a67c0ce5f149d23f690e9d3978786bb76b1 +0e4eec610f0ed8b9858b4a56fb44cf9ac68e29694eb680338cc5d0d3f1330749 +0e6f22a1e1bf262cb926b226a5633c8dba89fa626db350fe270dc9b379a85832 +0e737c14e22b4c6377d34f5b3b3fe6fb364f1ba7eb262431c9e33580c52303fa +0e88812a4b779eba99788a95c00b39169fd5a384f8b6721ff68fd27f5ec906f9 +0e889ca478fc44fbe43b7c5909f60a96c0a4dc33af89d9379565a541aa48b3f0 +0e93a4e57a6cd703a361db87a7eaea1cc95283f2f7eedeaf5b89abe783db1a8f +0e9f08e0a3e8d8a14ac2b6aa2b2f880d646036fa3589b1257ff9d733888e0f16 +0e9fd8ce7bd33c62078dcc4eba4cc5c8ca689a523fc6284f402b304becdb1c20 +0ea61d2b1b87584729a74b98dac1f60ecdaf6e0364c8c5034e7989d495f83429 +0ea87b3baec04821d5604d4a8e58115d9a6c345d0cdf0501a342306ff43296e4 +0eb7e2b30e7964e4000d0f6f805f4ef395214df6fb63c0186a575063b996ba2d +0ebe42c3ada4f4eed35280b6fae3e29f4ff127d1a4cad3b72714cc1ad714bdb2 +0ec6e82c28dd2119a29f77f8dc7e35a9d72f49de631ec882b3eb30135adb87ff +0edcaafd12570ce3e50d8de2699b478fda42660c61acf5651002678fd0a5fad9 +0ee843871a15b63e9d035103141fa179818666aaefc321409d9cde4d49318553 +0eebb57de9571a5674e35fdd04b67cb3c7dc9b816567815c703aedc5dbb56186 +0efc99c9360cfaf61476755c76875534322909455181d47dce9c84c398d2eac9 +0efdbc90e6631f8ce4cb2ff246fa7d17fc5a7f3864b77d456a50520d3f472396 +0f061ed23777f6e018023a9265b1940d282eee557c9b58f506b8ac26111031b6 +0f0723bbf43986d8285c8348df3f8df40a9340001dfd5a98e5b48b79324f26fe +0f0db27b5daadd9c0c6dd1378915fb01758191f036292ed809744b88e8c1e93e +0f1ad0c8ee31d0c4b5211a89a91fe97ae6cf1fea1f4cb85085df0ce51f230481 +0f1cd5ead4583b6e4a23971308ce5fb4aab8516f47a8787ac4e4f56dbc846eb5 +0f1f41f49178058a648234540522f9c9bd8a5e99592618214422a9c128ddee1f +0f243034f8bf1e57a44d973b074f4d80b1e8ae8bdda1e054396c375684e41b86 +0f26c8c69cd0ab675b57df945c9c0ffb6892d845097dbe895841f77a44375165 +0f423a8e11dae48436d10f742eb4cf5f4febf6a23d4d86d9d322ac1edd369399 +0f43e65dd4d1157ed77e10b6dbcbcb015718abdeebab6eb00ac69f7529b9e317 +0f52f2abb484e215fcc61263347036c143e961bdece6acbbbf0d840717f42bf5 +0f55a2e14b0bce6a8ba2c1ea5b20e25e53816b3cf33c80e6383f4b6f253aa09a +0f67000df73e3972a542a8b07e4d22746bfb747c5c10bde275a2f6fd8b2928f0 +0f6f9e3dac165cdbf753088e1b59942b82f8c6dc9f589631557984b668c571f4 +0f7b6d6d56765c611d9c3836b90fffd2befef3e65fadb41d1fb9e82b5ee0b7b5 +0f80e142aa78ea2601a5e810ef4e38b33a00a27c376e29785a4a7e73878fb29a +0f9486b6aa59639d199848eb0efa0596f017c28739112e6e5d39994fb4b0bd68 +0f957cd5614f5f2a2f4ee3520494b1c9d6fd5803f4f70e1aaa957ea9eebf3154 +0f99f3476bcdec6c983eb9376f92e28120b1b489f4fe6dd884a0b19060a998b6 +0f9c30a1e15cdce7f6bb59a16bdf148e3cb8fa9637460df1891257cc17e5c04d +0fa2b3c4db735115fe85d578d508cef2125ba835503b3393b0145667806887d7 +0fadfc9067b5d8d77caabcc6b4058475cafae02cb0c6648b77aac6a71dd11f93 +0fb3edbe411f25f90d71c6faf60a5e64190e20828bf9c8cb37bad3d4d24337a5 +0fba49b26bff2af41ed4a013ba3de244c10b52b76964b56f19ab03696e608e4a +0fbf1538d61732bc35e1c73d9cb428265e4564346e86b6beae19d9c666fa436c +0fc88e6fd738b150e115b07b11e27be66672e8f91d5a4d1c73406cb01c6ae551 +0fd99a412ea943e33a5fe0c80c0521686adda6b0ce8c107644e6cf2f90dd9b07 +0feca39673bb59523b72e354a6c6a663ea9c753eca83cf4cd8c62794484c8c4e +0ff9326ac4e7d1f254dac9e488f6a84d94df458e0ce88545c598870454c12ee5 +0ffbd3d17ed8f1461f46265c7d1a88374bddc45afafc39e7871a518666ded77e +0ffdfd01d440af1a78824aef783d179f411184888777668fed51a13e4680dcb2 +100329b164eb816b2a87d28845d49ffb0dee79728853397c6001f0dd5f073849 +10057842311aded999383514046cd9efd0ec9d9aa8a2fe36b902260b64e79ee3 +1007550eaaa43fe026aba79232c3ca3f829c50d17f099df07a25211067c1553d +1007b974a30442ec3f127c7dbaa9078937b1e12819596914ec49573e84f84475 +1007f91a19a63835e22a006652df052454fd7d5ed53367a3532340c26130b097 +1008aa1fc782be62141c00ed670e4ea94dc32df84279f59cba1d5a6e18126fa9 +100fbb6e4478014d6627517ffefa846d7cf657a662f17377745e2742237ff061 +10116cb15da829ac002d87f0e61f0065d9849ba10478a908851cec057d32f86d +101333e289b2dd64c611d6db446cd09dbc1516a68b6253ee3a53003c4f6f507c +101d8e63e560ed2cfe574084e7ba712c65e90b44649d06efb4032d026dd0b8a5 +1033e245d4a7545956fe9f723e215fabf2c53654b08207cc7bc7a3b0e1942631 +1034552a0b4863c942f7383786eafd7b2ea95361ff28a39b12cb3ad05adde9f1 +103b8763630fbd9963db2775647e2aef1c825182ea898e37a9b23c49f017b2cc +1042403fd010fc3670d50e6af305fb80249997922929506cf09a053c8013419a +1042e27a6f4d0b90daeb2fe46626097ac12efc83c17644071d3408ea2ef15d89 +1043dbb5648b0d7908cb6f2f20493acbc470725af2b814600733a44683352d93 +1047b8846a59b4514e1b6c9c6078ff864a45b0665d6b3f86e757e855c77dc40c +105c87bd967351afbc9dac3f753a72f20c0275543d42b966c226af78ba924db1 +1069705c5d645773d8b87e93cb07c2e03a3c3a177316d286cba066d6291224dc +107604c94b4259b2d3e09ade96fb94c0059f19bcceaf162bc40ecb962d7f560f +10784b4e3a22c8729d1eecb8e3278914aad4daba9607ae6740be9fe0a6601f58 +1078bf5c7cface7da013acffadb9944e21910ae188b8f7c2d0b8356c445fbde7 +1080b1014ee7c5c5eb9f8af4473e6a14c8e2fb2aab07e8621b5557e8baf9e67a +1084af5f0218e1c263987bd484092000946d9f5e71b306c99285fb7e7c73dfab +1085c6fe5789e09945a5bda156ad75f31d247b40b1b5a1e1e3b69780961bf350 +1094e5edddab567d026c07d82887125dfc72aa994718f94636ed99f92ba4567b +109e968e01d50eaa193e83e5413fe56a78e64671b002a4285c56df0f1db24d2d +10a06987479f4f9829f83ebd554f4dfec2455920957d8dd79c86f693672ade0f +10a175545d7697d0ae01f58ec68b152ac01c9cb81ee32abef9b448b999ad7298 +10b108127686e722c1f488fe9229cd24abe4ce2f9169ea2da3ff5f58a640564e +10b885a1b631798227d2c1add652f2a092f4dd721b5802b84fb5aad1716f7a01 +10bc557efadfa9d93a4a8810563a14aadb3309e039d3a2baf5e3c5163498da59 +10c951cb67b1dda551cd898e15bec5c822c05dd345d45e74e0f95683b8cc032a +10e3ac2ab668592f47e6b60f5d7b3783c02a7b3fa064ed2fec342b4086e10c2d +10fe7f2bfc1b7fa81ea1a3e1089dd42fbb7621b01a61f174bc29d3478c6bc8a3 +1101aa0d176b5e9129ee87c8b3ea2cb4c6beacdf29e22f722619d3d6e59130b8 +1109dd3457f983c38fb3d611eae0f9942f96e3d36908b5535562703ced22e4e3 +112471a583f0618aa6efc4b11b225a9383f03547d4f34ee8af7ef8629d2cdd5e +1128bdaef74d48f33fa3a15f6d6c758ae90cd47e591b05ee13733accc4d025bf +112c9415f3325d5afd49f41bc763a8cacc6e3f892407db2527081ae0cb173cd9 +11472ff513f1b8cf78fb593c0f50300d9c6112a483311017348bd03be1edb624 +114be39cc63af3e81720a077895a86b549cfca8ee4e0e214e17fc502b6246249 +1150c8ac2170035a42f7e8f11edc116ad2403848522a08d729b349681e565c0f +1158b5b7eac6b78c808b9cb3a6076545f3957df285dc278ac535d5466480c4fb +11613243cdc29efebc1bbd760bdec7960f0e3572ecfb4fdd5699908cc0bf531a +11713e1875b4070de327a89a5bb42d759af9e30614e121cd8935a47ba9138f00 +117799a5ddc6bf296f56755e8b8ec8663ccd41cb2f7c78e08bf055296cac8c8b +117bc2101558c0ee0d6a8abcf7217a52ed484bf493a23f1b9f066522a88fcccf +117d84749cccb6fbf81098eb04411f7b097891b065f4c727f181d1b62e142c81 +11885ec0bcf460247f4a1a6cb3562e351afd2746006583f0b1896077c6abdd0a +11892aba622e25e00532b4d6fb33539769f7cb311279ffcccef671b94d62b1cb +119bbd2684d2321fe65b6de36937ff1ad88a85bf28dac133e6e591f72d9164f1 +119ff0d03d40ed265576f72f89c00e8dcc76ccdd6849eaf70606ced2b473a372 +11a09baa49e2d5b68357a25951e333b529ce89e11449937fa3b380ff50446ec9 +11a17faa7543fa3719c4b3c3328d61048bf21ba1823aac512440f55b13c8e745 +11a266e18ea2570bac288cda1b644ab151f63a87ebe9bad2d4b0772668dd439a +11a673a882b0ae54522a6d982297c4f366889384536f22add3364b4b07dbc8ac +11a72d1f3d6aa8035f786cf7ef8807fb6a89540c49d7d5afec3e70b16252016e +11ac27ded8cddf49c9d38598de571c7ba5c29486a6db0fd6cd33f0a32994a302 +11ad81ca6b94dcd9d323479d0fdd355411fd53987df9922df148f609594a3b1d +11b6f4e6effb7474ab5c49b1db2adfac274e07b28d10de91e4bd06bb08864135 +11c26c5c0c7a915e5a6d7fef690a8f4baefddd513ae30867ff6b86e8a68cffda +11c5650a5e55e8d37431c3d5511d3b343ce1e1dbfad1e87ea941efdbf2f4560e +11c6c6a6cc29185e50e5d4aec93f74889e70876c41ad7f9edaf60dc56267c88f +11cd65e7985a6b9932e87e36e04fc8d1df281a690ae30ee851342d3ccb6cf826 +11dddb9470fd4a1daba1a08265b3334cca2b32655ad828b10186d2441a82346c +11de509c14fa04e01e7474744f07056cb0044bb0a0968ded372db91760d7ce5e +11debdb80129b0000530f30598cd8da13baf2c6a86e9effa1dcd71b3ca8ee06f +11e1425aa91b4b1e1d1b1d0805fdc02d5fd9b70f9619a21e9e5686b87ff61525 +11e4b3aab75b86e795d1ab769b1c448202dcde59dc3626f1c404cd926f98dc6a +11e5cc9860cde35223310e3475dbd59b566f556d70e997de25de34f204bc9a22 +11eb8e4e9588c6a15c3a81db07b9ff28935a25b7e880004dccc2e22d6df6b54d +11edd147595c65137a8cbb8ff5525a753c98a1ece95eba3fc88bf3a88cb2e846 +11f9d9d532c48327720bb369c5a4c6e94a171edb6307fecdbd167f3b78637d38 +11fdbb6bdb02a41915c7c6ad6acebe3bcd0d25fb58646cb7e75d96e2a1e44a3c +120959bc20b74c33e78872573162e0e94b06167f71321bd0e8f0c12f8cb5f659 +120da7c5f6224e334144390086f30423229eeedc5296cb8322da67217c80db51 +1211b7c81f1052052e2dc0cc5eebe0edf568fd895a4804c66b3548585a55cddd +121e2d9565ecf561f3e7e6d98e2ae6a2c49d11198d71e7d284a8659aee7d5096 +1221891650374406699eab079583c9747a856f496e9a9ccd0f01c7608624f4fa +122790f5f5827ee8232bd10fbc6477fe0269c90af96f587b16fb9696ead187e6 +122b3a571b98c5988b64554516d7818c6851bce897abda3a34c69abe3c6429aa +122e2d750364275019b0a06738b319c9bef6bcf036f10b02399ae585017d9278 +122ff4567df977b34c5ee817e09b28f45493192a92caea7678eec755db86f28a +123c626b0bc0254050097698e5252a15a824ef15c345a799468f905d985c40a7 +123cc5d86b4e7bb7736327d5a42c445fe66f721df91ca0435d60c73ebbff2715 +123dc12438f90aa2d12b6b7bb7980f32c7c5e025d1a7177a8c7fdb1a6acae40a +12401f1d3810dbc1acdc042c7bbc1783cdc52cafda9df943294c60770ce2fd94 +1241164a1b3e651f0dd03991bdb8400230da8b61f8d033ae3c6e1c0a4b24010c +1248c582c3b2053558d004dd06d6116256cd7b074cd5609cc02971e1b7e87737 +12496d90e17978788551c91993fe6d12a4d2f3e41bd06c395ffd797856f3e412 +1257176bbe6c09c15a1ae98d860b207b6e3d8ac614df776ce3138f685622f785 +1257afe79eb43476a80569de186dd31fdad3d2340f797a2f3ab3b965dd4928bb +126128e7306e48b9301f3a50a740b86f457a41118723186cbbcac0955448b29b +1268dceba3c0576fef38a332d6e7740305a9dc21b7cd577e0b688965ad001e77 +12759501e449878e2b668c48f15b36304333537cd407c7b98720c69373694c3c +127b7edb6a1cba95de69682293eabc3308c5ab040a7ac7f92367037769b38511 +128132e2ca4bf4a231f2e4ef27bbe1be040812fdc43a68389aa702306ae55940 +128b119b8fa294a54debad20f17f7635f088aaea313f28f3a54f1464c3281ac6 +128df13ed3ccc74933d8c00314e140acdf10a92d5bb18459396660fec8a43300 +12952acf91bcd21763ec39b06b45b018398248c2c598a67a7e7d71be696c4303 +12999f9f7cebcda08930c485ced2f0cb2dce58b04b8eebbbbec76c97e2696c43 +129b0faafd4c21f134885d191e1e50d9656ce2b8c6ff035c5fcf3f6ee3686927 +129b671dbcb91e69567af383ab1673eb79a879f86d92309240b84c2feac215c0 +129f87ae7d8de9b5785a420cbc656e94a480258a61d7e541e94d776e5c865929 +12a0792bfba1d9cc4b7ef1b626e3347896c299e092dc21c39ab5a5b80dcf95e3 +12a136d265ff710db4e84e87807d9a39dcfa211fb74050c909c8298ec565f88a +12a6ef639b434265f6ecab0a51c704dbca728ffcfa788b7ef4336969831a8e21 +12b266407642ef082643c30d528dbdeb78b462b3a3fca251b608215a62be0ec4 +12bbfe96ac2478921de24e525065c581f75fb64e225a9b6237b2fe76e574b183 +12c911cd260675df7044b2fb3aa48d68c6215cc2c6a5edf0119be5893b95e679 +12d1bab7f4258daad518cadfa5053e6b89c4224f1c6ac22778d2f244060cd36a +12d1faf5a380b4a99c5fb6ee16eb5c1de4aca66552fd1ed6e5ec77d2f29f1d88 +12e0644329773a327b9bfc3fe6b34fb3b3160ee27e029e68757d80d0d0a6edde +12e6814860a2a03f3b8764261872094824cab76d40f01f0c9f205d7320d9b14a +12eb488b09d99ba4f2398c90166efe09f6abc2e0351da6319acaf5538111bafc +12f41120eab79b7e81c1650a7be4359be107e85c9fb399d52e34780786864740 +12fb2af9ef04eb697551b49de9d9b50c82ec8a2051d3a524b21ee8618a4af3f7 +12fc21ca1cd18423280b41c93efbb58465e91c320a9e6514e8fbe1931e950470 +1301117848b275207601f757972c485ebf95d6d2b89ae7846d7b4c34b46e32b0 +130d36f306603904dd43c606397b369881ba3f1159fa93b4217ea055e24643f4 +130f193731ec0c63e1b9443b378d0b7c6020fe7221800ecc99f29c414af62ec3 +131117f92ccd5d936ec231974cbb1ea1dd059975fc4a5977bd2f44a6dd6a69b5 +13121c8f6876b131dfb68811c0e563f25978d085bb4ad2fa9640eafdd559b237 +131a2e95956ff195c4893e571455935b325e5d57db7bbf4c71ad34573a7a7db0 +1327b3a2d4a787016e5de7d9d9fdf8c4c326b23f4ab9aff1c607fcb16ef09f9e +132cf9aad939026c62838e0963863ded3aca18ba820145b296b77890e4aadc01 +132e31f101949bc0761e73e0a0b6ca61532808f8d984502692fd0908f38e0cd4 +1340d9062593b1ca3860bc51dc01e75f81ce680d2ed109c6a27c74dad3ef0a05 +1342075ebeca1ee22ea20dde347cb5c5544f4659c97fa9e4e12e6911d96b01c2 +1347237905ec82fb4cc980fe5b5ffef7ea81916fff7b466ea492983eb99db5b9 +1347e4cdd6ef61afd8cfbd3f941dc0acc6cb13252f27acb12904f41d83d43562 +1350cf01bbd4edfef75d1ccc3c06dda64f0478a6f2087108e4518b84c01104e6 +1352b4bc521ab556aaf8653c593e10c7123762a790193865785a57def0b328df +1360b657cae9c6eff52e413c99a82553d6be78d9e7e750d38d43668d3a0ad05d +136346cba103b1bafc2498514fb15b18747ea522470a1a7cb877dbe16a67b81b +136393dcf1875652b7ca733e4a80eb530c6a585a02660dbd645d7e662da215dc +13687dc848f33453ad7c939c7ae3f2c78513142dc32f51dd6dc38eed54618b0c +136b31b56a6f04c01a917fc53e2d7c75a310d32a5d07361ff836991fccd93175 +136de7180a62b71d9d959c75456ce5b9ea94d7712225b16cfd476788b27f243e +137e82f15530f53031a77fe083e09a9041958e797d056c20221a4b4577984902 +1387319a24a4a0ba8a21ff9ba53b153d6a6e741e1afeddb5d2f25fae29152f46 +138971e08ae33faa2fc46000354a8c6a3ff14d38a4e9a50192df788cf439c0c7 +138a6cb9edc1ab09615f9e2bd208d04bdbc78a7705bb212d11821b2d9ab72ca2 +1396ae4721a914dc346acb0d521a6cc58cad908bba54032a4981233bd3d578a4 +139ab768bef4debacc4885af7ba9dc1deaca8afe3aa73a72882abccd6df6c25f +13b1aaec8c9ce26e8b8191a0d2345f192104e2beec2c6d86602dd860fecae3f1 +13b3eb12e7124e7ab8f1538bc8a9546ced8d8a9b8dbf07e20fbf80da00799d97 +13b5ae4bc5b6ab32ad030e584afcd566ed0a22535306c7079e6027493906e649 +13b7c31e63ca06b5e39e5104d31d93f97d6915420eb0c844444e09b1d3778bdb +13bcfded3231c1317b9a4e71adc754b512bcb743a104ac11bae1cbeb2f8ece3a +13bd386f3fa37abd02df8699b413eebde8c6871ee48e34947df6813d55a93f08 +13d1ee274bcada854bfab75a61d74eb845eab7f1c4aaf001d2cf69992b8f4a12 +13d638d15d0e49f16d540f4dda92d0c74b34a75a909388d4ecdd3906205e2f58 +13e7904b0b0ce15cf5154cd57c2297671c50539227feebd45466e7555bed245c +13f4ed8d2ca98a87ac8b82e2f12cccba4aa2dff99731281d3e9f32996066498c +13f8bd068bed0c92fe90f857b0b4d8b4414d532c30589f5e691f0e12d4478307 +13f981a37573fadb062d36aaa4f1543ef9c0ae67f981e1be04aa354d1dbc3cc0 +13fb6f03d4a84678f2a46b312072c9dcb2ec43d914c67d789ecfbdf3d94d67fb +140a35b16a253d371350fb8e3a93eb51454bdb0572f2d88bb7c4c6c6df9da5bb +140d76fd63de47a84f4235a3c6a58719b3edec531fdf9f771e6833238958081c +141091296d8fd59449c9023a69c11ebc79fdc62866e3be652405c3dc751ff496 +1427a974068c34a753269221179c38c40df3b3cf3c8f94bc27b9f2550d4154f8 +1432220388c42d034b21ed84132c39fd06280dc49c186017a0ca04f9eb99ff21 +143fe88a62bb964f8e4acac354fb253c595d849866d14e0ba152e5d34403121d +14420a6f7b2bbbd7a90781b1b8d72148b754ba66d2b16e0f6dfcdc8db50922d0 +1446074c7e1398bb44139d41ac8acf2736e3df8feebcb84f8c1b88fdc6f03d69 +14521a15401ce994dd25690314638ed52dd087eea433da16737a94f2fabddd3f +1473e63297a1979475bdab13ee594e6e182b3abeaa1459e40c31ebe651880fe9 +14779418fae720a2c29f6f828c0358888a3d3a8fe3a1a356de6cceab35b9fac5 +1485da6ffde6976cf3fdf2274ae3108fc764c78f12b62a26fea63f9e93daeee2 +14884d32c976fd2b9804fba92726635cbc0f4a4390e44856961b72a1ed05382d +1494200891024b09770ec22a42648f95e687506a17eea24eb2fe38aa5af919cf +14942af7de9569ee1f6f238c6a0d0c4922297ebcd5b71deba188da338b67fb11 +1495b08d24eac30c03c85a9952c67125e43c48aac5fbdc1fcaa4cc773c767a10 +14a7a9f1eea7c685f35e0a0db68d8f05576a2263cb3b8a74a9f8db4daa3dcf49 +14ad813ef6d686db11d5e3da66b48ff85b916dfcba9fabcba6e6db5227d39f35 +14b80de84bfaf3aed715c94732069da7907f3433d79f5226569107f07bd307e6 +14bb1a814c2d5d0574bc85bf0ed46f7aaf255a78fe9917b49b2e3a2db8ab9360 +14c322a44e320d5585830594a6d43a9251b8917de026cdb1991ad0c949cfbaa0 +14c683051074951d70395df4ffffb780ba42adde48f2d28fe71da76abf9fb737 +14dea4cde51dd45bfb4637388548b4385f533288fac465112f8170f5faad1a35 +14e3fdc21410f462a6ca8a0646c1a2db64383eb3dd5833fbca5992a38d3cf99f +14e66944ff7bb446f4146a1b9c3a1d44e0db1d24768b1b6b004917887241aa18 +14f95232d1e97bd5cdf56b33693d8268adeab130516c53b01a6302d01f41ae8f +14fae5ca7b6f38adc35563ce46e8b7d756fb25df8cc4ef907e96c1cf31db8da7 +1507d1a4e8b996c566b76cf61fcec4cc58560a3c7c9a8ad61cb1b8aabf4c33ec +150ca147536d7e808ab7905fb5fefa4481e1b38f91eadedf6ac405d015249b8e +150f83596a817aaecd064133dada9b2fad255bc3715a21fbea26c6dc9fa184c9 +1514e48a4bee1a05597cb2e71d8c8844cad7a08fee1e775041321e3316ec2cb4 +151f481048ffe20b948713fec4de6704f0f7b3eb76523c805d9e83d53901eeb4 +152040ecf3366503e7277c13031fb56af7108d9535c30f295c855c48b32cba28 +152710481a31aa9252cb7d7a28b9e0f2260c9116c8faea1503777c28b8e976a9 +15345e016a0aee1214762a6b356255e0bce540bc16598a3e50710313f71c38d2 +1537137067a71511a41a88f1201e72b92b6b749f15c4e4f276ba43c37e0aba40 +154bbc6cb2ad505c5b6a530920d30d3aaf5576bd7d5a8c2d910841d2e34fe337 +155b0bcc6709069b6fdface89ecd23b0b32178b7c5d3d4066be14a62e5d56c4b +157068cd873fab755b180be602d130cc10fe5d774bd38e8b167a9a78f37a9e90 +157a65d42cc73357ae9a507cf7c72b51aaa8cb5148dfe6b1bd1f58e21f5cf438 +157b08855d2700895255dcba53f9ded5216fcaeed2bcebb15b1002531f992811 +1582e033d06550204365ddfa2202e653ba16f19b644484117fe84c833b5b7e58 +1587e39d2bfbeb5771fa87e303e9e422da2648a0a4828d06c24ac45adc127ee6 +1591d886aab8d9ba93f51a8ebcb73f9d94c46f12cdd0959560249d654878838b +15975241aa2b05a06bc9dcc307d7c450d7ce808ded71d97f07a438502bfa3943 +1598821f681a6ac9ef4bfde658aa99e18c46e70ab67f653b54f2d772297f4ba8 +159f30e6ff86d4e08e45131e53e0b3eef50981461bd0ee035f7e1a38fc436d33 +15a61f936f282ecacf51aabcf429b5894ff5ce2a0e358f301d7df95219d4bf51 +15bb24231f06fb115adecc03a6d0bb2d439ee09ac3d07ca78db8ff360529ee2f +15beaf57e79f83e778373953f68ffc90b664dbd28a2a1fa467bd76e99143d912 +15c0b9a148976ee19a48ac1a56dc4e805fddcb9df5fb2aefbb6d7627c1a34b79 +15c55318bd52ed5dc29edd598b85c71faf8b89c717bcc5718aa304d0bac0e43a +15c846e1980eb32bdf35087939b8c1014c6e11d8ebe975c2323a493b19a94068 +15d0fe2ac7fc4f5555ae167b0db9fcfd79d411a41530c362d273ca7ba0b9b52a +15d4316d06e146db379aa3747cd60d6a464ce95975f8e49cee5073186db26652 +15da69a97243b64768c9af9e5de0f003614b9ef76ca9eb5bbb3f177de3c88e4a +15dd8d059d92191b2ad91028ec79635b698cd54c6a8487bd999777422cfe86d3 +15e1a70c3fe122987bbefc4099d1935fbde1a3a14caee397112d720ea19bb40f +15e3250846be9ea154b1ed1ee8c6b742a388f40b962d073ae2f5b67ab3afa634 +15f1ef8a4500adfdfe0c4a1195ff0dd7bafa3a1acb802fe30e29f633865f7545 +1602e961be32584bb1a316163791e731733dc1f2f2d3aaffb7babead99658ab2 +160f3db92830eb745f8f7e1bc598d40ea6b7ca485c6bb92c817ae411598a3f71 +1626e20962eae650a0593d76ce37d70fa3188d0903da5ac9daa29bda6578f90f +162f5a31fef5b0b4368a8334bb9a136b949dbafc9333d3426dac6969b151c635 +1641590535913ef5d6d0e266f2d70f05f87ec46d7312b1f4148f2db0b1731721 +16446de9eaaf71b428c9b201d91795b60fe2d5830831c1b9e8bc5b3108722367 +1649a1e26cf4af53f9fd70432be6236d2e8e859c0528e65b898830d18b53ee9c +1657870e07bcfb625ac7709d819dbbd3c0b28a4f141694237d451227ec38250d +1660fc6abf80af80526261d2e547a30f343e2e38da36ba277ebde0c7764fbcca +1676473b0fb26985c630e360fa7bf9e82e2a234dd413664787c9e494315ee17e +167acaaf5c2eb3b3df31d1251ba148ef1e289d785d99f263516eb6e516d32284 +167b8e72ba05c652a4481748ca16528b4a4c94786b6ff7c2bf1fca67253a8167 +169238c60e75adbbc052fcdfff6c04c1fbfbe2512975bf76e2958aad5abc0a3a +169f2b87e9b698a6a3b50d88b66c737a0b1e7e0aa16ed5a01197eb3b87bf9836 +16aa373fbbaeed5b73366c4b9f8b40c4a43e69e8e802008b51ba2edb4cf53014 +16abe8ff6dc3e91d768b4788673225a5f633ddba5931284806b05c885759f3bc +16b5f74b9053c9050238f45b6a95de3100a563f32337d56d5922d48fc16df13c +16c0f32bf3bdf34a23cc0580bf9015b5b8e0c9a3c6c0a302294fcb706ce80821 +16d5039dc0241f8df751f120c6a9002dc80baaac197a77cf041f19ebf6febe88 +16db79e6ba1e6964787ee94a9f3ed19d81951b550144e70ae7d1261bb6b8f2cf +16dee607063777547c1ff926b71b1c7f712edfb1243f119f131a7ae109de2e16 +16e99365865f9519873848b753c653bb9d5f7bbef2679d511225d3c2652ff3c0 +16f2495e47fac7c73a75e6459d40a8dfc1156c1b327bc88a492ea581ef411eb4 +16f626eceba30a416c24efe50fa5d1a7f435c613f30d377d7ef50355f32d85e6 +170196686a3bfa30a885e7f6a54287f6503d9f3d01f69acede0791f993b2beca +171104b1b32ba885a6454dfd092609363838462dfaf25076765862106cb704f9 +1717c847bc2eaa6c118cbbe3ef437dcc5a1a0caa2561efa95eac47e0a56956d5 +173bb215758d9ec5839fdf2a3017268775d77f4ca280671b488ef37e9e6981a1 +173def979b6285e530088ac8fcf9fbd000f2c59bcc8eb18277bb57024005e9cc +1747b3dfecfed1f8e38dab1d8d88d889a017e5d7c663286c9460121a2ba3b59f +174f94f3e8bfd55a5813e50c49ce92b2e61f23b64c9a28b64137702d939cdc3a +175c18941876a29631cac3815b4fdbca921487ab9d42115f744b63be391de84e +176f414c6afb21d6e3ab780e32347fdb123fdd1526796c6007b3e71d9b6949ce +178072e20747f952d9ce8aedb33e7ca8fdca87a2ae5f214268e89be4da00c8bf +1785bb2f9c1bd1aa83184b73450bf7a042383f16b29651a9a286409aff4140fb +1794b8ff575cbb9b447cdcd59be0858a9870c76b7332bf69b91f6bda0b1a2f32 +17ad90bfa7499976d7984fb0b33886d9cfe1c34183182998195590fabbd6934d +17af650406218a21553eb402b8ca17518a9314f926e1262f7e916b29346f4890 +17b3b2f35ecf894392314f892b8eb7bfa24f1569d05d39cb88b83787a3250337 +17bbbd7306e0213558f6c6a716594469a057a0913bb516543d83ed27a3293b9e +17c4d4fd7ce97e53d9c5dbd946b8677eebd7b6952fd2751b29c1d70e558f1cf6 +17c62415f20dc81dbcb9df6693a021693c980ab36cb489f4d297755f2ed32d1f +17d0190df0f30d05b8b4a213b8267efeb668eef8ea3737039274fccd97b1e222 +17d67c9b742f0c683e4b831c888bcd68fbef8983549260432c2e3c57ba8d2893 +17e08317d99bb60719921cdcc66513583c6e9148de5ef801c6c8e1da46bdd218 +17eb6ac1fa06e9b15271796f35315541daee922ebff1524640d3f67fe0640e2b +17fe30256c51ddc9802b4090663b8e7156f883d991177165f290fb8f07c69f0c +1804697c632544ed70ffca2bf3b32ddf860dee65f65aabdd4d2eb51a735feaae +1807663c5a20f5d1c85ee4ea98a85760dc3614616fa12a623e7baf57480a7632 +180db89b7df89ad238594368b8fe3cf61494a741ad8bf5a56ec374044171f91b +180eb8818889fa76c2f6de4d2dafa3d03fabc5f79519c256b0670e462944d3e8 +18137729981cd85ff2d5ddfb9d26f71d0234ecf0c15069c36e503962514f8314 +181a01135b2e1f51ef2b196e287ed75249732074d1d01da3c0d77449021e4972 +18201f8ffe93d5a78011be5fe7ea4e56bce3a2c55a27a3b8d5e60c46aa99794a +1823c2aa2f07351268bafbd8bc9549c78cd4f512889f305e1fd6026eba157e01 +18282d09a64088acb4cfb669611a7ab7fd8bd19bae396529637106a06239cd54 +182a665b19c2d9335fe0dcc38af5a04ac8e3915bff3d2e2effcb0b1ac396e321 +184346a9205c0ef26779f06b8d42d242edce247149c69ab9fa20c62763e2fc6c +184655e1d8c264d67fa7b5c8d60d3c9d79dd7bad6d20c4a78fc2efaeb568a493 +1863967f135e54b9cbf83ff4cf6d883b76a518982b77f2ac410668ee5894928d +1867ee6ecf1cf174d6f664b5cf30d45ec6f199638eadacde0739710cc4e3a419 +186f9dea4c7b87755a6ec783729d6abcfaf126336ef98b4001248deb3c42459c +187d4b82fa397494d9aa76a3043a8969e23123eb60e9f027d13acba6c3424268 +187d8fbabeb65da2b2676447d2f3e3429e2554c08be15d1c2de988d12e8d92d6 +189e1ec623f96c6e4b3f5d3e9ca9f08ecb1fd8dfe41bc362f472f482d343c2d1 +18a18868b250f2e1fcd1ca3cc5946df1cd9c42069d8318053968d0a18556e570 +18a244758740e49169d8497e4afdaeb573e9cac0cebd8e27d1b77f428dfbfb91 +18ac914e3722a91ac9bacacd541986d21b981370b64cd1b2320cc76856deb5d0 +18c0400e34389023c74bd7c705a40c445231e7045847e1bd84da8a68b65047c0 +18cf8cccf09d8c3a26a76e26fd9e28cebf411f2269276b18636113048a94a05c +18da7d562a947797b5ed406e16cc1082d8184b63ce771292241b88de39a0cecb +18db51dda4102cc3ece2665c29aa1fcaa905a4e23eac4c32989c4919f8053cdf +18e0f9500eee2361473ab33cd223b575ca41bae28be63e9ee83ea01b22f414cf +18e75a561a9b7e332549c278dde5d268179705cdee040f38ef8eb8f9bb5fabd0 +18f19dc5864d51c6cf23f233c8d75a4015db938154a8ea88f18859d1d906607a +18f38566ca46a546c1eec0e55f27034d3ed9084ba3049a5ad36fb6782afa8a1b +18f42e57864529ec57bcd9e629e09f30a69f8adc2879a3d1db35ecec4ca3861d +18f4d3c827a4ce913d86784a211f7432e195649fe4795a844a5313fe66559282 +18fe249f716cce7a067934685003f2abbcfa821de294d0eb51e44696fd5ddb38 +190556bfde04273ff2b905129027f05a6cf01afa388472d5ff106fb1353ab375 +1912fe04d689dfa0bca10f7edba6d86908e23535da305f952f487a7d537b2013 +192c8cd0bfb81e2e5e919e2885fdd7c523dc7f52fbe764e022d9d1c07199f0be +192f56e4001c45e6657535c820c73ce2bcb682d95d62aa1d1b1545293493a24b +19450f329dcc0727e448cd4ff96224e5106b322d7f188f89d290cc286ab0997b +1947868bbf7bd877ba619e518f95b9c195dcf13fb81b4837e35e9f76360a79ef +1952fe302a711c2958954b9e0a75b8baa97831af5b8332b5e1e60e9824767dd6 +1965ef69231a5e57662bcb465dcf7579029e6ce9feaa897513018d8137398884 +19661ea0deb4f1eb65b147c46445dc7e6f3a28f61bb3822ad01f0044631e924d +1983fea9f10d8fd75da7ed12dcbe59a12cde092120de844e855ce15b67483c89 +199a9b838844113fbf643cf78961cef20918ca4af52eda76d855f06f4cfb878f +19a1710c7f11033bcea96395c06139253d901707924b48b84e37ad277ff4131c +19a612cdbc63bcf90f272787087fc56bdfe6336a5a2821060c2e12986e928c1f +19b4bf216ebaa4f7ccf55f93208d8f22c19f492a29775710fd453ed182ebfa5a +19c77a4ac5605749a41804e8f341a486b37739ec5459d4832f8477ca3b34c0fa +19d83d351a49c07ab307c4915048bb4674800b816fa2ae56e0fd3cf656396eab +19e40c4ca85c2dc6355ed93a049d062ed0f2c290e73b920fe72e6a2d0d7b0980 +19e68f0d75927008fdc14fc5b815963aac716e6e8380d4ce0d1b781ad98c376b +1a03370b7fd5efcf3ac342646379779b77d450cc41fb04c4bdd9f9141f2d9075 +1a0d162874baa6508c7953b3a091b87a6c78393b0c9e49f85f7ed80c8540e73f +1a12da96d247944de936803387ff8115a80363006c5dd992931e90ff2864d263 +1a194998c094db6c58e7a54ef200922f2e05bbf355d559c0109e67c50511bfd3 +1a254885d6559d1ef8f4b2fd7109c927fe58ecebc58ee353f155dfa6dbb68a85 +1a25deb3fd8a87527ca72ef10f605b4e79cbaa9033eca3a04697ee6af5667474 +1a318f30883295e862dab079e0625946076e43a3fe7f886742d4ab8c81c78a55 +1a35a1f2871f639db2fd4265e18cd0e66e50af30f292cd91a863bcfba3185e0a +1a396368dd71c1ab272c1f6e31fb2ed9bf6a5894e6109c40802e37af429095df +1a3f4fe09829ac498b00f6cd43119ae0c7318f5bf7af355b28c57b48564bfc90 +1a470cd78cdadd15bc81607ab15177543055cb3e63990565f6719796304fcdf0 +1a487f8d9f133079f5a05f87d72c8a2613b20925a5aff4ac9e998e34e4ea27da +1a4e9b6664f1f2aaf141fc5e18acc2f32abcf18a694bd9e3f829d0515cd102ad +1a5979b8188cf7d9d9e87726de68296a3168b9adfcc3c4c263a9369e2d7c44ab +1a5d10a1854a9f1d494b855fc12de0bf309c831455ca1b562dcac0450e42115b +1a5dce411ee2ce8609495627782519381330ea70e84c313f39dcbdb2b37e23c5 +1a623f7f7aace62d349c61183af10742c45d37675dd3cd4a9cfa2858e9f892d0 +1a695462323cb2ca296dd88bc8e04751075535794fb4b204f054605a7b6db1ea +1a6eddac71c4255f7a91580d77c7df05518df7296fc6d009e8cdd0a49cb38311 +1a6ffbabc647ff6bdffc4dd29a031861c509d086be26214547131a87772935d9 +1a733af2667d36a0948e9b3427ef0cb8d9a3254f98b46d0d2063dcba59350727 +1a81e70dd778315f04f94e6bd6306f4fb9f88f8a236218f8d86aa3e333838dc9 +1a971e5e1d0cb8b05618585e6986beb31b55d0a5336b7384cadc1c3e087a827b +1aa88d97e7595c98a2fc13134427f5986af2a0107158a23745077d20c2fa37d9 +1abbeac1b2628a7ecbbedb17ade6fb9b85c9535ef19ed5ec99b8e1d60ace9712 +1abd73784fa7951112c7f815bda9fa14ff66d8e3d2ee90a546fd4981cb4f1aa1 +1abef397c1759e88143f39f359f61c0950e5edd80601902af351432efda9a7f0 +1ac20aaea5262e24b84d190513f76c60ef04402b8227bebc44c974324eb203a7 +1ac23c1e1a2d13d5edc95f9bdcad9bed002bfdb02e456be34fc8a851bed21f38 +1ad977d9b8a621c97fd2844e27dd794b49cae1de2c0fe337c12a4b43d277bbc8 +1adba94af065e3037c60573f7541199997e8e88d6c5eee3873741bb43fab82c4 +1ae33ed55148822e525c134e57f6c29930d94297e9e6b5a5109fa4d5ea805f1d +1aefe3e139e9084f1ce4ac1c7f14ec085918a2007e1c214381aa699ef4057a21 +1af26dda38ff5d4fa64eb1705181e858c61b9675d7c73c83d3952578988668e7 +1af84f9143d1f91ef7208bf178b7d1933845086af759519f44558c363fc27567 +1afa38b9d45c30e6c7e82e9bcbf1195022a78bda46dc111d51e6d81319190487 +1b0595c7a62901bb9a40fdabe664d8ef7ac1a83cff5b5ed48a50afeff39e3d6b +1b0ad21d03389e62554a7b05ea0a19b2acae21cf1a3d5b10540a4d20c8e6f125 +1b0bddaeedfa227fc7fcbd726371798a5fbf3454dbb587d81b3498de410045b9 +1b0cc26f48bfc4aedc9360bfda24f6333522944e577436d6bbc3b45eb94ce53d +1b1326166fad2bed8f1d8b789a0a7b8c7e38b7f57b6da2fa11032320df2db2c8 +1b13f49d968973504d121f576c771d64a4249d82a03bd2a2851eaa7e9c4bbf5f +1b1ce4e0c9ae12b94c0a9570f2dcd35ed17ff5d9d1d6483dfab3f63242c41f96 +1b247759a8aad2792c3b2f469cafceaacbfa5e2f2abe846254fd9407067cd231 +1b2f16532fc7c0413d637f089c28deadcdef0f8648e9fca8a07bc9810029c741 +1b52d711dcab127884115c69348d3d09035e79860c841c859a485877dd6bf8d2 +1b578b7ac97188ffbc56d5b26072a3066d651792254d0710b609f39e880d0eff +1b61c23a5c1e452e7ff5545ae9d28b0c9fbd8254eab617b07b018bd7ead401a9 +1b6b1274bdc0c2973ebb1b46529731b3f254123fd811afa63b12c096040f1fd6 +1b7a83e3dd7ffa4a5462e77a4caef5d8b5738dab8ed79f899926a750aa5467f9 +1b869b3a083f3cce7cc60fe3be8f8f44d4c998562cef83bb0e65a9a0fb075d7e +1bb4f54fd7b860a1d539dd62c27611f7f2ee31b9a5d81cc90e447f7ffcb1ea66 +1bcc01ce0800b0ee13481668bbb9ffab55e68e78563815a720abd2ed67b455ab +1bce44baa6eded3627c604b6af68f5045003a01d3581620ff22eab422a3c5714 +1bce4b97361b3905a6dce201c0d90ff52bed6f2d161ceda8111462801ea6566b +1bce4e092462871cbd6cd5a38fc44f12fefa156cbddb4aa1825adfa3a1fd7a95 +1bd91ae94766730a6046d5c0309dfcd85b8d873e8ec3abf0fef0c55eb33162fb +1c08cad49a2b8084b158b1140435825b399fc8df167eb43cbfb9ec826bf4596c +1c0bad659c754ec55c3d1143351c4353279bdfa22e0f2d834ccff1cdfff7fb77 +1c10b1f157598a76eec298da287eef52b119211331087dcba2f3c8cc146b5655 +1c199e077fbe3c2961eec229714a6a48ae0cd8ad50b28b1c0d4cd8cfd790dbba +1c1a08dd6717668daa93f9ee1b90af1b05e4595830c552f733c3345ff7412626 +1c1cf0d32f8006ab7f0b81338a4d1ac272aaf76cb60e4f220afc4803ba694da7 +1c1d789cfec0d026db97a9a2119ba396c7a92d4f2efec7b8cf89701dd8957a51 +1c206f0343ba649b7fe19d86356aba6f28093520dd3b66c49b8b8388840328ae +1c23decee65039ad263d2e82f864f004da353377bd4ea8581d2b402f5dee0d63 +1c26fa3992facd4835ff0e82388b62a8c6b9c9b40bd912aa4174fc17e6da5a01 +1c2c209a284df2736ebb85da2361ec76c22343f8b9f97bb80ffd78c860bbbdf2 +1c2f40ee516ebd3f1e12bcf72573ef8d6cae7fcec6d4a2d1c8d471d0caa1a378 +1c3247cbf3b526215a4da5e95f53b77c85e4fb4a22f895104a8525e8d91867f1 +1c36a367fc9ef038aabf5714d61b861a0a786f23c601d3bdd296ae121ab50c58 +1c3cf009248e502f8a40ba3ea434fa65099f5aac9e5f0a54274d72beafc21def +1c4b5b05e08cdc4903b70423590e5ebbcd44c750216451e43f776288d53c17b3 +1c5238b3da64f427f686e008f7fc3635baae846c10b1a8cf90733436da275c34 +1c5bde5f0e2899a73fe05fbcd6f1ef3e680561773a962209166a7db5df0daaa9 +1c5dbcbd8266861db168b590c7b8590bdb125a7205552d1a91cf019d5f20cde6 +1c63b60c76a1764d481d6460189f0e6dcb34e24b4159f2acf613a6662b71855a +1c66e6cad878094d49c2a190604a18c361926a492b540d43a7cb39d0dfa6ab9d +1c690481dbe6d69d73654354635eb5d31a54312835e4d2b49fad788603f4ec92 +1c7098c368fbf6178edd3a8b2890d1e955fcd7467b000c4c98f7c838809a3a63 +1c70dbd14cbce5b547103952f2b542b734ee0db2cdf2442a6df71e52cbe9175d +1c72d2f93cfc0002d2e5bd6360b2405517d229f6cb499c6cefb691bb3717606b +1c9f5f7b6b8e6233269b126df931bbb9339adface1a5f083730ae869baa403f7 +1ca050a2846022e9c7b4d11437447a0054536075fb09990254a4d6098b0091c1 +1cb0594c50ddeab338449d7cf3d9ab73e033d981b9968ec47ac640002b50af7d +1cbe15acadfa518ab1a09d2e1ac60093501d494f142d021d3723145f76a13774 +1cbf4884809573fc8ba65990a3af1bff96e312b2f4dfd9b8b5b8bb26c3447408 +1cc4038d97c828c4962a5744253b7f3fe591aee819ad07fecf9352d926227c3f +1cc42fe259a96efeed9c7a3ce0aff65f7ef3b5d0875477da17d08fad262f338a +1cc57be625d0b78e5fde6c817898708d08a4221b43d51f8d934af97fd3ca6904 +1cccf88516c18547487d15a5d29d78bab3e9ead1f219ef5c2a951ca08ab414db +1cd7e9ab83e9ce637a8d655b1600b5943762725135b3c1fe4da13d636bfba0bf +1cda848ae864cf815d51a6735a3aa7698182a2afb002ef7a6bd4e1e8f5e46b86 +1cfbecd93bcdc6e36d519aba2d4625bf3a92b5805aa070f0dffea1dd2acdaa1b +1cfe27b80db0db58d3d6ed7651a59c430b89cab8ea1aa2bb6cef2572cf578e10 +1d0a8d65cd9f4f81b057df2b625e666e65642bf52ad3007c4885f1169b71ec41 +1d16cd711b7c74db8c06d54a3c7f8c7d7b5b8972c045c3723930e46966fd981f +1d1ef27eb7f3985a62909861c21375cdf318d91a3b5dd1f92794902fd9d1eb89 +1d29e28b127a53f0bbff4e0b977f9e927d2a3c9a21ae13a514e09cb817323d8e +1d3a66459a4dd800ab6b385512a9fb8971c69da7bc7c7e03ef7433459ad23f14 +1d4ae945094ec8156b539a246a289ce8da2d0690c2021ff9b6f7f41a1679647e +1d55979c44fcbef91ce968297447980a91c8df45a6e5f6d32461f84b3137e12a +1d5a2fd7eb56979b5eaab707ee44963dabb5d23e8aa39947b3ef6f46feec51ba +1d5b6fb1858d53656341e504242ea9eb9a701442e62f82fcb29033966c4645f9 +1d5db178f9bb454a5fa33086d1e993ae9c26c57f961dfd0350d4d573e3682cf0 +1d7145ee1bf52328861318d4f211491cd2fffa9204e3dbc5b4359c491bc361cd +1d73ae8ecea12e4af572d0d60e97a13b7c4dae4c17b619a49c2580ab0130f7d2 +1d74ded1b585659872758ffbcbc3880ec1bec4eb5c81cf716d58308dc4aeca75 +1d801271dc1a12e3550847ca4fae43af9a98f09bd612fd5e15eb5372e858767c +1d8d3ec43ba1551b9a900749609b9b9faf8761bf13efa39e74810b7c0eb0849a +1d8e9a0bf639190114b7010cced50be3b8f024df5417a1b1f2d715c92223f886 +1d8ef9df17ffbb6dd6c76c0c8a980a97ad885cd50632add51af78d35f18dd85f +1d8f19d20588796e738b2bb24ee814ac1dce67e28b8a2294e4ce443d7e01b65b +1d96174ad7007c94be22a7c1b61912688dcdb3ac9044bc32f8655ee8a63fba7a +1d97992d7fec418f739ec0cca3b42148917dc2aa7beb476898b7497832327091 +1dabb2630affb9959de6da18d9ffcdb99fc7f72b6ecc32541767ee588d09351e +1db22a95007eb0d6b6d0098e24d9c72a1c7168f1f642a8c60cdf214d9b24fee9 +1dbecbd87bd2c57ecf7c6ada23deafcd27f279269147a32edfdd5c5b9705d800 +1dd0b8d4a5cfecd3c19a0d448d1a4172d1104832e90f7cb7e84b2615a849281e +1dd127463c99d696529d7b494381b255ba6496876021aaecd3b5a9d431a895b7 +1ddd2eacae2c8d7d0076660a6e6c614b0cb40c2f00a518b953c738543263c7c2 +1de618af825d991ac6cf3db6160e24bf5245bcf0f838ace6db1abd69df8c085c +1deb0b39d8708e18c647b4bc8ae82a119ea176193eba1e02874364075ce603a0 +1def810717e69d6c66269989a1bedeafe814c45eb530c27a7eb1f06bc8ed8924 +1df3254c69abe1aaab64e9627688ded97f0943e5c1471f543627d8981464ed90 +1e0e832c5dee46a572fb263200380b6255cc746ba9a24fd14721e9d9ff00ceec +1e1277931faf2bf14ee6cb2a75588dc19dffb5fb76292a32670eeca94717f314 +1e12d1a794bb3df99a96514c54cc381b5f92bd7832beccceac6e29bd3148749a +1e19771d5138becfb8381c6c4e3017d258e30fab380d353d1b3c5da9af266065 +1e2449f86034813d65b890eba7506b38349dac93cf5ed568a28f120bc00167fe +1e26493e8c240e4da5bd636170702f883835aced1c929e345fe00e92a4d7abc9 +1e335f727d30a8a26f7f81d933dde15da9cf3cd1f9cf1571a3a1866686ba8a7a +1e38b2ebdca0ef879dba9944ae479cbf4fd36e524e41d5298fbfbc8cc0fd9c07 +1e3b481bff6ddda7dd0b0dfc90e80919b179f83eac7cf2a751f143670d8b2163 +1e4426307b701eacb4af8fa5077a8c0a10fb2267c90bf12044b8f44351c8b44c +1e4767c1a0236531b35fed35aa380889d17d67fa1ad797d22fccb5ca32f61f4e +1e4c7e2219178cb5d9065614c8faabdf3d36283b8e95dcd74b65ea16d6a85530 +1e4f5daa4495c99d74976a5b5dc9caa2343ab4f09141b59311aee6997dad9c81 +1e54205498d4cbe847cef89eca377a5aac6bb5b39b029c6306be4645160554cd +1e6287dabf363684a0c109a91b7c0eae2cb6dae826991b05110222d855481ae2 +1e62c41c8e7d11700ad91028ac467b88a73354af82c3920259892dcb1fa33b39 +1e6358c517612594029be7c643ef7ab47666251e1da5f6e71e25711f5d358840 +1e6c26f2a8c937c239d3f153244256196a51b785d842af4a1e3f0f3fa594ffdd +1e6c9ab63941654ff40a1fb3754d081dc9b2fc8135ab1f88792b33049667a5a3 +1e6e22dc23e34a24c15cb4f3739852ec93e416ee877e1c89d0e21a627d02a824 +1e70a10cb887ddf77150e94b8d1176af8a8b367103a369b6a77e3f0323c58657 +1e75eb94fea5a11924ae3f324e29eba5836ee5c9de38689944534abc41ef3533 +1e78428d80e5680290609ec9c7cfd3eab45c6d040a2f02b1fa193190dfab3b9e +1e8c1d50ef4ca034ec86ec3d1c9ee3eb0f21bc86c40752a7974bf1b17479c11c +1e9b22f63c6b6e8e29e5fabc2c32ad627ec5880480e18c47ec909dd63dafdbba +1e9d0e69528f8751a030ea00e75a89b5f9183edab80f3c7efff8281a3e29bde0 +1ebc343baaaf041d57a2226cd4663a7b0f8eda0ceba1c325532aa929c2701f11 +1ebffd6d8e77d25f30cbb7e66bbd860755a0cb5f49c27268673beef8703b2907 +1ec28940704833230db1469fb4c958cd3b890fa7832b3de5abe756f280c7e838 +1ec51a0d90fcabc52746f43ac93b4214b2e4afe24b7f1edbb260b53d3767dd3a +1ecc7dcaec866ee2782841af4f4f9bb6aa166f2c4e775b3929fd0bb4b902f066 +1ee05caadb2028c2b5de9b496d63b078c67013ff533ec175eacb837ac484171c +1ee2522c0d1840b0025d31e30905f1b13a98ec2b0d45399b79f2ee206e5be8e9 +1ee33ff95611a5f16fb094ba73bb92af50ea93d6ec4c35f987e918004de569e9 +1ee67ef9ba07d956e909c52b8fbe81b663ab1310c2fe93fceea1ea5b506e07b4 +1f0b73350113059d0017828120c33c102ad1bb240a7681943f395d349dae557a +1f19d6694cf986e4eade0393d1c65a51c42a11d6e2ade41d03fa84dba177d6f9 +1f1c4cd80dee4e81f9cac0492525e212aa73d83922a393019e69ec7481dd9fc3 +1f23ff05426f4f192b253c09c4b9ab59399cc135b29e52b7400474a2d9114677 +1f2e4a36b8c6c6d5dc676e7a40165c03b69692eafbb91e631a5875df97f40db8 +1f2f58a4601e273fe4b739edfa9dc468d6b2205e4fca085554430dcec735d457 +1f3a225bf18d7ff8f4610957527936aa2bab61068dbbf6488e6a9a3befa2be5c +1f476607d28f2dd96e619e9b297d38252e2e2ddac5e7f76fedef82f3e50ff451 +1f4d7ea16207f065eb5c168de835c5bd81c552d6691618d027c9f362e6b3f2f1 +1f54ace33d71094a698ae22ce5b5d09dc2776e3e075642be2fa2893e474a34c6 +1f57d0098ab8bfdd702d04b4ffec74dca2e7160d403c0021446b282b59d51a9d +1f582a512f479e3c9979a7dd0684a0d4ae0a65c94e16140872fd3f3ad8cee27c +1f5ebc1dc677dc128a8d9d1b94e6e3c5fcd2b3e3d06570211b4c5b320cd3dd85 +1f6948f9a59144a81dbaebff717ad81b1c9859eeafd82d74c01107837aab456b +1f6fa06d3d4a0c35838f2c6132e4df8ec8e05516e173dfa03148aa36fdc669ca +1f71dc3043e4131d878132bda49496d77f4273950ad45d4f53aca2a786095226 +1f75d6dc99c17c4ca2fb8d9e54ffb531a909ee3a87271ed8dd78809e3d84e496 +1f772cc98dc7edabf341d7b22ce1678d337dc5d5ee9030acd4c0103d1aee07b6 +1f78456d1dd292e51accf112b77bccbf070deaa3cfd7fd17d0c0e65c525b4c35 +1f80463c89dd53e08561c081e8a87b5f2437dc940cd5e1d67bca6e7c72315ced +1f8be3d87954adad1e79efea5e83fd9527beb8ca1ca7dfebd01787d84ffb30da +1f8ca800887ec9b7e69ac20ae518c2efcaf85e79c3c9a9bb20edc565e14f1460 +1f8fd2453428a8c20b3f21137d7cda099a91a4a6d35fd74fa3c2ee5cad14f12c +1f93662fb78019b3d606a00057479bd9736352781c3211e17f26aef97020cea4 +1fae8749456eec97bc8d71c3b4bff594b4a73f343839119bc22e66d051d3a59d +1fb2a6e0b878e2260c302c7f3f2859daba4ac440c2d27f00210347bf7cec0f33 +1fb5116f3fd3dd7821bf89e253caf65935ba3132f1a01b9b64b171b536ac2fe4 +1fbbec85016585e3e07bd51ae07c7d6c1c00fa502555385e5a4b78923cb0adff +1fbe7f70bfa7ffcf9e88e485b18e8720058ade7522c35cf01b51f7c8ea922dbd +1fccb8dc1ed96b3f04dd7ad7d7eceb2ea86a2eafa6223c37c6240bead72ae78a +1fd0f5cc4aa73320db5e209723a29dc30511b8cb90e861d8ea629603e5370111 +1fd9a1b1890d8f7f2b87df5b85cd404fbb6475329fd4cbfcaf02ad21df818b77 +1fde07c7e529ad71386c3bf089cebe5fab891e858aa1ac4e4c6280e676d32205 +1fe324a519207d195a57d5d789c246acc886b97e10022b5c3d53ac3c7e417997 +1feac8e438e7549d0a217fa3f1e74bc6660543840aa55834fca996b9fa501be7 +1fec27d0cc01385a88c4e92e1fc06b24a22dd264d4124a33f63c55e6e22eeb13 +1fecb74eef1e103a72ff97454b3d5614732a1ef23ef45e47dfdd926458f88297 +1ff11a6a96df8421cac1aa0c7476061556fc3cf68747a5aa5303772ecf957d94 +1ff3b4863d3ee7fc932461d44109d812ac88f5b114ba037442b57ff4501a937b +2000d83e77f3fa6e132583475a1f010afc9c3e4e2f1c87e67fc8abc73e12ecdf +2009526ca2b76418003a4ee3cb5b6f53fda08d3cac5a0f5495f513e51b65cb5d +200c00b3e0a3f76222b368942443ac6d3b7abab83a24bdac6f774f2bddeb7b33 +20102c0ace89c5786106d49cf81bccd8430268e4db95ac2cf5c6946e275c64df +20135702dc8325d2341f7efd8f27a82cc6c9c04d88a95758af4c64c825368d81 +20256f90466c71f9ab7da21c9ed66b7285eb32a903b2d50a66fd09d3b73627d7 +202f6c687967fc61fba64a82ff5c2a2991d339aaae138370cc1f32c1ed7aabdb +20343386786dd3e4887d6b96478c8521619d47d2bafcc5749be05f690e28c7bb +20386a22264420c8cd86f354cbb69fa48be437322f03a74f2c27629fc98e2035 +203d15a99f7ad4078198230bea61bfbdad963eb739ec1d78c85f534c0d29195d +2051ad97da43c12d0415ac8c3bf8e9aecfb5142688e16757ef4956a04b4069be +205a12242d08f12e71e4865c04f4afa1db9b58d7495d49f4c575d53508d2b09f +205de5d634d797385b2572880f0a73baa4cdae6e181c00b76b731bcb050c2e41 +205f8db71beaf8928d787e9ea7434c6878f5d6c666e1c0046bc28b963ada3f1e +206457b71fa828de047f4b208b81b065645b14518f5c398c10c0b56c11bf3454 +2064bdfaf6bcba7af933b20091e673df54baf6829bb5a46080823cf2dec82183 +206c25c6de92b1a888a983ae02ceb1935217ca37b5b500db4c9a094c4d995289 +20738a0210be45f413fd0639f4fb79edfa3bf63480cbc5d9f0c7e992c296064a +207dbf6668e038fcd08cefe13fec41fe7f89e0591d272c8ca114512b65c0aae0 +207fa19fdb133956967e2718ff599b7c0094cc7f2735b1b1d4d6e7a199ce39a0 +2081fe14eba4fbc5e5a059b184299fbbc6528b51a7427a8b8307beb329708891 +2084ad453b7afbea7f906c9f42614f9aa4e46628743dbcf5e8125870009ae966 +20854cc287f4ce5eb0e592571822512a5f93fae6ac67b3117a51e9cadea47bfd +208cbe8b70998b2470df63cb1c1559776a9ee63372d2fb1f04a771df4e1b0020 +209a4ab5e3eea503af350c7a384d2a1d30079476a976ba0d1e6e6041c46fec10 +209d83872495ab5eeb620b0edfa20a004d5a371c965b8e7efb3dd5a024adadc1 +20a23de07a115656ce39b717f47c4ad2195c608ffa83df82d02b46325f15bf4d +20a4afadfabc566ad89df085141f262a593a5818b0f40880fbaa8c14d28b219c +20a551bf81913e13819d2b30a0fb567ec935ac5a588aeae4ad4bd0ca1ed84a98 +20ac077a6cb72f575045f27773282cfeb172637021d870f047bd687b56b8f124 +20af335c511ce1ccabec4196320dd66b05a4ce5002b9edc6be7e57f4d4872cad +20afad0753edbcbaade5a621c7822331b8aaaadb78a31365e939585a6c8cdd26 +20bcf0838de62fbdaf76c7b4df3bf2807afacd46e43be4484a2dee3b70eaccf1 +20c4c554331e1232bf9c1a2b179a3c44aee1961bb5ae7814e23013155a615312 +20c8fef9c6bbb5390cfa52d28659ebbf2a7454c3477ec5a7c501ca7a53f5a23a +20c982b99798f42bd6bad4488810e96657a862d4800f0420b3a7999174af1dd1 +20cb1f8594b57cc83c086fdd6067381ecc1ad4959e665ff708a4fd8961dbe729 +20d02b6a232e1f280e32116f994f2cf2471cfec372381d0765169c4c6306ce7b +20d955ac4f1a7ef5b381da228671fca266dbe5b2d5eea8a6420f784419f9bb23 +20ddba51466fe6587088a58d8edde429f583af671fdd5c4ceee003861168ab36 +20dfb708f0cdbd062a74d22499f060da2acc80348a9c51db87cb68b797b51a31 +20e05ce25c3abd5f8ceb0cfddaddf2d2173ca5dc0d7d92b8586dd994270a83ad +20e981443a6081b3bf9cde7408860436ee8b8bf45d4476d64553a2aed1fbead3 +20ec34681aabd73a29ca01a5df05a46a6720094c8b8765c6c972d5952d297f17 +20f7a48dbf177efb83389fa074e06ad0756516fd4825a07cf599bea3e63bf8df +21017cd22bac9f9fe514c4fe192f901a7ac12d74b618dbff8495eb9f644ed0f7 +210504545e83256e16504d1d530f2cb1607bff1a47e37a4b64887171a8c2f328 +21068c482f8e99c4f143e3a555067a64a7345b9596c06fccb8e3c5a2f8e0ca6f +210beb283ea55a4944626894c89b4cf6c758ecbd3d7013fb2f0fc371dd2e86b2 +210c9098a6b6f1518bbbe034e63ce7eafd8344cb5fdb62f74fa80683b8ed0964 +211e3e13eae2ce240b9dab119fae72cfc9c354587942ea0985c11f8a04215cf1 +21206956e54fc29477a6578cc820e4388891edc2db865c3388bae77a65bf2cd3 +2141823dadaa7da76e3cbe334573d0e8fe55231fb05d57cd9fc94bd5cecbe089 +2144604a186c0c25c9ae294310bd833344e4b5605211e002616cca18ac65ce40 +2155d962113ab149a2efc3542cbb9f0df1f07924d4f7e45ab2c17075148f5b0a +215b6dcfb05c2fccd5d93bac65dd939f42154845279f0d0f5619498b1b5abfd5 +215bc19beb689b685e747dca91a999d6ffa6912776fd59d66626b666869611b0 +215bd97148502fe93d4256549821720f0f1a377794804403b2d17e266baecebe +215cc30ac6796c01efb12e1d6c57a64a69a56d8e1dc506ca4e4a676b984d1e47 +21658ac2f08d30f585f6522ab85431cbd04bc6d3f748a6c60e6ff447b23b1d79 +217182be44f9f7fe1e0ccfb8dadbfd11fc5b943c1df897d2168c5078b4d05904 +2174117d285f88dd5581196e56e4b9d0a4437ea079d1b9b1752e19cce251cbff +2179d57d8bfe50206480befc054ca387ebab63365a42462edba679721faf7891 +217dbb2db57ce33e20068f48aade28b697a8654fae2e8b822244289a38d26a1d +217dcd8ac138a52d15fdc34815b3a0e3822a4cc2756e81fa8e4babbca1f1277c +21852da3bdbc2e2e3b26c95205447fbfb0b58916403a9d77077b256bef3c2240 +2188a3aac1dc030e8458b2fa8fb288e0614f676c1287a023adb751e97e1ba6ae +2188e5a188b3e80fba033eff0a9f924c8453283c025fbb9718d61e64a1ec7fc2 +219617ccafacbd4e0c0d5e420627d725267fd8ae88fb2b4f357463de7f07a2d8 +219d744027e4475a0c5929c84ebaaef3a0b597e1d344293d12f4d23061ac0d0e +21a20da3c4501acb18d46583d3490565d48cac76ff9ef0354fe78ab01f1bdf7d +21b61581549c6d976520d9e63f7aa6b20679ede281360c52fa83cfd2389d3ef6 +21bc45c3d2d05e8fc5b81f6aa641916ba573d682faf27eb0a52db7b6528e27ad +21bee839c0356c0d82a1d49de3f17dbc5fc10e7912620adea107ea660326a28f +21c747c8baab2eb2d53275ea28d933b2a2cac875334ff6b80e4de9944fc0454a +21cad8adb19c5faf85a30330afd26edabf57e75beb39e0e8a22a4e6bf50a1a34 +21cf95436173c41216a9109f0fc3c86d57c3b739efd9f27f164ac5123a62d63c +21d1b34d2256cc32926f78eefa99451d33f4c0ae4df20c56475b4e4f97a07ebf +21d3b2c50d5341b169d286ad5b5f1e056637793e72bcac0b061c832d76866d08 +21df43ed3998412fc4f9ada7d5147b2f3ca0bfae2fe289f45f77fdf8377ec942 +21e30765ce3170f07e72b4b2f321d36e324fb04a206dfdeb0d9afb9546d3e5f6 +21f50f38c067338e187cd8d60f4787cf3eadd93aba13192d8fd562c26fbdc478 +2204741344c01a97459ea73016e308083b1d60ee5fecf9901d5c1ded14f88fa0 +220b98e4877545013f378ef822d8d75fdee2d979b82e22ebe0571afc539dd1d6 +221b042e2d3e8b5341e77f8ab8b83ca2b20691cd765a5aefb1c27a20bb1f88ec +221f802a981342a5ead76253070c58fb5cc46c6d5acec347856d5f9b92a91fc0 +222193a3a525ed239ebe4b72e7fec2cdb4e3acaf69fdecd15ea635ee3d8ab12e +222584ff4991475f505cba2b25a42a20f5ffcaf271faf1a5e2956f539a08b5ee +222f09faab8e5f34564fc8862e6d1ff05b2f388d2e0738effdf72657853a2367 +22313eb89a16e4d6e2d620835a1010d9b5da8e586036ba03ca783c4e295277e2 +2233353825ec6ec92c17c7ee58ba8da98b8f94838040cd373b7aca179734328e +224fcc371b2de1d47850671f3f3dfddb26d1c4c2972d9f3fa245f19220b091d9 +2264ba41e656db1374d19612f2fec52a9420624060af90973e5028da69a2f655 +2267feb5b0ada13eaa9ad742b257336d876f842ca4b3778b13896ae70fec21c4 +226a04d1edaa6408626e6257e02c16e7404cc6e451bdd55ea2aca6d65f4b97d8 +2271858a48c87f9299f38140f2ffccfbab027651560460ea9f3e38e96f2d1459 +2274f70e1671685eb0c2840a4bfd81cb98766b73e7baa620ec1fe950260e14b1 +227ca0bb0e2a4ee49a9fd530a472fd76eba76a8248fa43ac4d87ba3b28b31613 +227fdcc40260a610ff8ac1231989370d9180784f3233d17a650bb544e0dbc088 +2288990e3f5920c5485d5cbe764b814c51d86c0dc8747c3eff2a14c3dd2b305e +22892a85410b1166b9df969416bdbbd29e55bef722880871e42c341268e4539b +228efdfa4f00f339cd841b19e3c8a49a411a62f3efffd5c2719515dd7d7e852a +228fb306d9ee885a83669b65ac112885612e76bff85fbfb7a83fec96a443c349 +22904466ae1e501e4ff5a7b560b83ce598281cfacece54dd6ddde18850d60d46 +22921c9da32cca16b34ad23e017898c26923998f4defdc24b4a13a4ec76af5b6 +22a6bfafd292aa5c0ba63623c78816c7d9c7c9048b2dfbf1c3b8d3dafa286888 +22ab662128742f34fce508c3db0526cbd6ba4b18c6bdb5f52656ca871fda207d +22b6630b08e498f3c74200557254eea8653f5bdd9b78097d80e2ec76373189ba +22c4357d1f28de4dc6ffdc25a331fbb3975a9f51bbb93f7fb7080f9c0f35afb9 +22cc25786a7701092e55d39dd0a0148a8fb4f35db9170437bbe0e6a64be5ad99 +22dec70af1e04d4d3450a0d0125b90657a51c501196d9fa48a474ee4701f1e76 +22df2918a997f1c244327eca48f7176d324b277187c14c5db2ea1b53c22bcca6 +22dfe966670382dfd888aa918912dd358a6c0d6b401f894a61d1a8a27a19c771 +22e05722e6e1771cd1113fa79fc301e49c1a14a7f4d454bf95a30bcc20d3cc22 +22f1ce3581c30d7bf86706456692fdcb986271773b345f53e6ea0afbab95b15b +22fb1a4a941dab32374353cb4568c2d675bafcb1dfa476bf7afccf8f193b74d0 +23028d5736460e2fc7bb3acbec2b4c01b2f4546b45e0788ac81d34cd605d5839 +23078a26be7d0ad7dda4e7426f543bc3d5cef7799ee223d47da2c67d1a836011 +23164297d2bb83f7509911f785270e60a502a069618898073c17d80f5fac0af8 +2331e78e5c3fd387109d090df3dbcff1b3a5e3a950576a6db1281445180b5340 +2338adc87cc4928942615b848808e45d0bf1a146c6df822ed1af7ec4c14ea252 +2339d434617beee3de2b5ad5c86d633b94a06a67fcddb3690dcfb9f8043bfb2f +234e92461de3b933001000629139568fbdf841db7ef14a5522582191accfaabd +235dc178652480dc405f1ecea911ff5992a322eec6a23de0759593f23f32c711 +23602dc2055d26af923544eab5b400dd302187dd06f056857dbc461b232a2130 +2360618b92ec4e2718d841ece069cb1834916a69c612760d6cb9f8e21a054032 +2369430cdb64b6a3349860cb58b94d13bc505416a053eb11f2832440675c0e58 +236bdc080c486f467fc0a2c080be03e2ea0f063c5d20722b76db5a31bb21ce93 +237a0650351583281e52e2e473a896de12ea302294d97a8a056149328e8a3613 +237d4d78153ff456f9a89ea9189b655dc515471c40b59e47dab7dbbcd2d62cd2 +237df41b4bbff5da03de815035637052008ed7c8ce95049ee5c87ecc2466b0a4 +237fac8657d8911236dc79128d4fcba08c96417a6b7cfb095dddfab51ed2bddc +23963113da8e0fafaffe3945c5a26ee7736f6f690b9a4d4b826a9b05618d6fbe +23973df3ff21a5c29fbf965def9a00943ded118c0406073511ddc30da1cd91c3 +2398412c7cd1e7179e203abd866cd0fe01d4305db145ea529f3f09a010355477 +239f3d4d84f9600de30f22227e07306e21f3f23b677a41c36a32e58c467cc493 +23a102b52ff30edb0e7a495c4a68828b09c9882597d4bdfec985678ee9823f81 +23a5126a40fc8af0654c59c99c945841d6ea3dec6c306e22f1cde2e35fecac41 +23a7bd337ab26db9e1d88969883f4e11989010429a2637bb0f42f843534b8a9c +23b26c56e6c66c14582bc6513387c825f3e5331f34e726a7cab56825874f2e0f +23b6e978ee3b1f2ad71e9081892197bf34abefeaba24c182e855b3a8609d547d +23bb48687fb4b570f622d016297ca2d80bd404d5fac743971dd01d26619a43e3 +23c448fc946587727c7b1895b2fea7cec7616ad55dc7416b51cbf3799a4f2c89 +23c83678aa001f61bd699a09eba48fd16e906a2687559b875c9bc4f6c6a4d62a +23c95199086fda067da37d2fb4a9aa02cfad5803d51ab113bf5d80006b38859f +23cd2d2e7aaf33596f2c06ce8cbb5e45699aeaed9467c466243e0dba6f4d8400 +23d074e634393083e7f06929a1ebf87a50fd28a59ccecaa4c3d05a14195af5d9 +23d0a15352b26fafa37b055f972b9cc57b028ae85161169e234faec28c7e1749 +23d23c6eefd1a580eb8d3814b4a15c9724d3ad54a10c5308cb6801d127189a84 +23e508e5c5a29dd1dd44776b4102f9679e6ecf320e9b984592094e861ddc94c7 +23e9b49d19c8aba36c96c380201a5ec15092516ebe8ffbb1d1f258c760e20a31 +23fa55a9d945d6e3dc86c6bc610b349c0504f764cebfcf42ef71e9a7921a7019 +240705bea6d89ba5f655de8ed9dc224b1cdd78f32c1b2f05d3f8ac4e0221cd97 +240d00fba11a71c00e17b8cc8441a54aeb530932af61a7343d58b039b54ee8ab +240d7f22de6ff59c5b6a3fad0f86ecc8698af32b7cb21d37b4337769aa9675c2 +24188b49532383206662cc3284c10d8d1869b8cc074493262185eddb426201b3 +241d154dcf1c1d3768a94785f14e0eb38273cdb6b93c949c3f40008a4fd7f591 +24220ff3f2c79606ef14133ff5f72d3b82759feb59dc0eddc4c588308595feeb +24247f4cfe13c5915729277b0e55fad6995779a6ddb1c3bf5869d745ceec934c +243417abe5eb0b67a260238ee2e4642bd4fc82537f3e39df58de72cdfc07ed50 +24377f37c08109470e214f4b8250fcd5a2ba904f9e37654d44dee3bbf89f4ce8 +244425e821abb231a5f420320cbbf79e6be8bace69c77ede1b62aa0e7bf5088d +244cd1e5c9fa7360faa9c3792885f25a18ad02ca77311aca57567073654f9a04 +2456a96550c5d41a94381be6362a3bf68203ac3ce29cd2531f2333bcf293d1fd +2459354d2b3691e1acabab4c1fc8f28d7787003c2b24e93e66ef9171dff1191d +245a503e45152ad98896addbc6d3a4cf3eabc986a3d4361203557a85092d4ce9 +2460b82a74c0babe183a8ccd26bace38649c84baed9d0b55c742554f0fcf79fa +2462f64808bf900c967bf70b7597a1cf8803cbd2a755fd89ae95865491b7539d +24639ba041c13bf9ed40d1f5ed72f097ffda54ce3793f689e03fef8b32ed13c7 +246771d34da6912afebd64632742c7383d3f21fa63ae966217a16ed17bb806d4 +247baf6e032e5dbbd502ac168fb7b83b36d05d350fc0f4b7eeca2ce9c506eb54 +247eb6555611f9fc75ccab82fbc6ddfee955b1725d22469e6be8f7b8b5e91eb0 +248eb4d747141ed9fd4eda4a38fd19bdb3d01cf301166b1abb896f5250ba4b1a +249969c4a48a6190c83e04906175bf4a814391d76be6778440c0ad5f867113df +249b2d34a278ec42de3bdaab8c68f31ada74d82812c50f167ff968dec3ec5d9d +24a62c9ea0d95da6047bbbd8f0995564ecfe6b004f180b2d8ab6875abe4125ec +24accd4df72a95f4a422b66fbaeb46f22fa48da25fefdca3324062aae492f2e3 +24b111bfceba405690fcd9c57ba7125f27122991d2b4d1a25cd69dd0329020bb +24b378acd250f5fe17508f099e4aeed7d3c0d1eb2370e344c4ffbe4760dd888e +24c08bc465e8050c0acab05ea538097d9ff552d921879c831b62c39655d30852 +24d5c36338dfe4a7fc5929316a15a2ce41e1be2d73e068370184a22a17cea0e2 +24d71c7f5043bd31aa7de8e94c98d57049de86338d4c1b61f687ceffbdb8a9f9 +24d8d5478916842c99fca2ac0cbd666490fb5157135c36971ec3d1a6ab549289 +24dc9bbea77ddd9ce4b451605a91db25bd5430c4d535007d673bae1084aa1f6d +24ecc7c7b1112c4f138a9750f7197fe9e3c423cc27c684df3973f5edd5068596 +24f2f02433ccb3bd960ae4074eaffa2cfc670a28daf80e7b7bd592526f5ce330 +24fe4a7c9f63d74024102313784ac0b140f074ecf3a3dc29541731a5fdf999ed +25021dc44537795f1debfc9492add32e599de7df18c50d536850c81e43578837 +250c274092a863e35d4dca62af7c3001b7b2dea540dbc023751f6ed0833bbb5b +251ca2004d27240d404b6fcf373a72796e3c0c3e94cfb8066173490476481847 +2523e8feba869c621c69f3053080a4c5e33ef8777f4377af2345d587e97e7758 +2526af1cce3821b07e704772e649172d82b398325f9c24c5403400a024535e50 +25282efb31c32cff18d63f82b2b5ac86ca8acb38f67d103a8d62155f20073286 +252baa2355e59e046fe97b9ee9d0527debfce2471db6ec9d380f0d3ee426adce +25354ee377a1098a4f2c39d9a8f893e274189d4511596e544f78c87c0ccdace3 +2537d4b03c82265f007aa24abb997e576f4b92c05ffa3bb2068490629843b0b9 +254346eda1c5702cb9f44de407f0baea9a7ab00c7bf2a55e0fc3fcae208862c0 +254b9f64020cff1ec8d21b17de084ddc3964a6f21193d92581f2b25a1e07c2dd +254f902789ac2fcea0f03a1157e8800f09d607d7cf53a9c283887ffe4ae7e60c +25523b272efb5ffdbb9650ae0bb2278548c257d277d5d786a22a6e29427b6727 +255bc15e5b5cfffa12f8f28a146c94b33a69a24cd5abf1e36d0a7a052c7c852d +25705f1fe3afa030d4d80aa84c59c0826c7675e7626a848f7578679f1cbb5087 +2585629be7ef9c8a0028ee6eabb709f5dc585b5c4d2eadf5fca85c58e300cd2d +2587374b11fa1414b4f15f081712a3b167aa9c3ac7819c7e0f149e72c56de440 +2596da43e61df93ae45cc07ba135fcfa3c8f7dbdadfdfb858c4939f76ce82da0 +2596e283f1ebc78d1ad0ea3e32b48d9204dc54b848eeaddc6de2237bdd57c3da +259a8df1c416a8ae125a3c0615a32611798fa5407c3308e04ba710430a2e78f6 +259ef38770544a486782a3415eb14f9c961cf23ffb5a04e6f1db88b90bad86da +25a42b6777942ebaa59690850034a58c6b9b83e160dbc8750827926332a54c58 +25a9a062b7753e041fae8831d7e5cdf7f61499fdd3c4c113052bc966d60c1995 +25af1a6629c1b97e01ab5e3f8a562af38c24e985d5de5fe8c200cb36a5b5ff03 +25b0979ac11c91229b382be9587ee7440630f358846951bbd4770d6fa0ed64e1 +25b5429ce1cb6420c272b9e712c88b11ec942d15bdf9e33f2074f3e10eef50d8 +25b89cdc1b13b2aec52dfb2e805cea7fde4bd4241196e557246d6d507bc78817 +25b96e50d295b301251a08165eb214031286c91c496b93da6e57a0d38f3eda05 +25bd46bca72d9b9da13c2061f2e7c075fa682d68ea5843d22f9ae4bda7390342 +25c59d436526c4a0cc9c81793eaddfbd2b4f47e8dd6e8b5cfedf7be5da652fa6 +25c695ffa1d89b6ee69d86dd3ada201e8ce92bc66a1a79060aeeadfacc9070da +25d60733b20e1037136862478603b2315f00afabd1642650e72a94197dd981d9 +25db63bfab2e4d995487b8f4fe46bd2152d9d87074f364224cdc5943648a5f2f +25dee46057590a4994bd06a0a620a0d0360547bde5d6cfacae10ca120f4da32a +25e8709c324e126e864306074ba5ce730a688b279f54d7d0d258bc3ec03224ce +25eb1274a702d8e0575de8dcdac9bad18297d1ab5773e25f5c72f94b6665e48f +25fb948ca17e0cf4a9905906ef19142b5b67cc38f8b5484e92b0265a2da3bd0a +25fcf2a1204d24b384f5fe85c8d0792dad6c7037117066593476c0dcb6af0d5b +25fffed50712b4142641ab3876b1b9fb433b256045e946965231088cf44e5e55 +26045e6faa9036631adc56dc5265f53331569b57254dab9dd516eeff60ca7815 +260fc823ac76651544449aeb93b5ce582ec2925f0f3f80d8b4d3dd967d7c767b +2614d6650c0d28890dffb86bf4da37113e9b6d0334330a60b1cabab52ab367e0 +261bc05705af8d658ca928bf1d9afb8cd545b8be3e8ed279f1b82dbc8c24d0d5 +26244be784b9db9e98f61b2da8b1e8610582662044ae2274014c359075a075db +2628d1591c901b3741ca235058be30b93e753f5e2f319edbd70806413d527924 +262f0fbd4e81a834ef2223a5c7336c27efbbefa8858d72cbd6f0fc6e2f2f2c90 +263ccec136efd813da78d2899426e73d639cde467a5d77a84e56343b84d4e174 +263e71dcd7f57a19efa1752dd577191290ddd4c27193aa5b1820d76158f3a7cc +263f42b6522462854e38249854db6b80e69f9be8591ddf40c32cd233383af015 +264ce70a0dd42690625e2092daf9a5aa9627251c5e386e403b8a6b258679e92a +2652cda417bb4b874085543ad2b5f302c3fcf71a227d414ccabaabca86adcb20 +26546f1349e73c8a2b7c9cbb2f5f66e497c155f60104ee69f78047c4d233c03e +265a382c6c283f3dec057bb3c834239dd8c714ee8211b243a9c54b31563ba600 +265aa160e8b778f8b69206af405e69894bd5e961c74f9e80a3f5f1758015d179 +2682eb4a2dc4180ae52e94f4c4f60fe2aac15962889780c86284e4f6bf6e682f +26838c3aea405228cb24a18fa234f3c4ebb1876081671eb9db7a82f7ed0b6fcb +2689f9d8ca86a4cbdeca593071a2bad120d6dea4b247d52c167cb7cd4f34b922 +268c6c8916657bdbae96e2c7de4c49e0b256d028a13a3375b843b12533c853a2 +2691fa400ae4a1c4400b1e8ea354f693e261d2443bae07c477c91d68ba3b8e83 +269841dd8e1a9dfc3608eb7b316a759f16b55a02d091b94653b8dd83cf987d37 +26b049b15795401ca09bcbf50ffee0b98b52e6a235b584c1629bc39b12799fe3 +26b89bab4cbef9a8fe2d822bcc322dc051654ba27338c3ffa4de95c8cc5bfd45 +26d74ecbb77dd0ce4102e629997c3ea53a5c658e5376333ce311ccf68d889a06 +26db9581876cbf962c9d0136457cbca619e02780637e78fe73647c2a7bb3b9c7 +26ecfe2556c16f49a25cf1ad20372232e723dc7bd61950c7dbbe9ef24fb5a5a3 +26ee67a053a428d8472df61a0a3b945b5115a75f68fbc8f4b9a258b1f11e22b9 +26f1c6018b78166a7e7b4950b2b669b871f640a94e09aed48a33c58758b2e3ba +26f7446a57e0bb6f652974f76af87bb5a4f2098f0bd8d48ace5edc60696672a9 +2700d7f97415603971f21f63b1fae66e45936feff7c6b26d4df36984a22e96de +27029d24bcbfeb8c3f306cde90e8292feee9452097d4a65247cfb5b68539b2cc +270358841e350839e7be7f2485f5995463642dd20ff455db53d0c198b398fe8e +271cd503f547bba2b87c1216e1257adc5958e6232e74e103e6d0d612af3c4289 +271d0670934e92dc404d9587a2658a9626af162e3fc4ac3459d9f803dbdb1d25 +27225fb2d50ee8c4cae5452cbf38cc4ca956ab845eedc0a61901d7540ed17a6a +2723770b9c452e68ed1cd6c48f58bd8455a15dbce5ab8f77cf38a765e984f80d +272798432e8e3236148bb1ea06612555b86297b0594f8b2492699c20eb1997b1 +272d528e10511299bf7de108f274f217c74853b42f5343c6223af4a27c9e0cfd +272d843395f15c5225e49927b93bc273143a2b3b073c71c1253c547fb84fb9a0 +27347f1d54469b6c99bc871f1ba7ac4203135115b0ba1ec34355b690b8eab1ca +27364ac09e5b658460513b72a4f45cc1a277665e621376ba0e2071e8610cd89c +273d234a301924f1ec3287582694894949b94e5abac3bc2e6b82d53ff5d86f7f +274674ff0bdec8d30cdf34912b554ab736549c36299bee3261e1cfb676322398 +274a938469f99a090b62a3d59ab13cc2f21e0a9c141df033ac9f2a5c99ebadd6 +2755c30bf936f5735682766911accc60932355b87052fe8694cfc96358e5ea61 +27562a5c0a37090a16f648cfb9eee293ada1daec8382881999bc4ab9da9445d7 +2768605d526c0efb8f294471721edd9339bd830309f6214b35e56023fc61bc82 +27694d037176349812c51be4aa2f295faa9f9978f4fbd34aae1ed096933f3b16 +276edc16f25deed2c418892448333f070e1301c1031342c74ea152403da62bbc +27792dc5e65eccfb697f865abae9bd3ef09cf3fe5a3e46123767dbd3e7952365 +277961e62efd340c050d35b87ed56233b5679b74131eb06573836c3ae8ff764e +2780c140f9f5ac9d13e0b45fea6350fb1b36068c77fd52053056c463d8377350 +27868b2122d0ef90be78a2183a593aad0fb24518c455edd71b76c7105436e869 +27877ed76f58c2e8469bf88171f45591485158de1ae45723c2b65811e80c69d2 +27a01b7933fb41de0ddf29e4c4af203390b6962fccdd1320fb8a417c451893f3 +27a01da67dd60b619b4f93536c05364d13bfdf9450a1cd44dd52ae875a127d2a +27a5401049437e40d46ce57678bce28f8fb58c1dc112211227bd2faeec701bec +27b70c4c9554ac96fb0029824a1f5b9f22b8ae02dd46cd1815efe8c0ceb0d6b1 +27b79e200992a4d6bac2da2960ffa6029fe14405ececb559567eaf8f33bebeba +27bfb74105f5f6e579d09a8b2ebc4245081a1e3820182a9e2659afcee7ea471e +27c2578bd842b3dfcc13630417bb97574d0b9115f0107cf47c81942b16fd2445 +27cc10c2cd71c8cb6371a47adba8fa33b1cb49349757d90519edf7f01bbc40f6 +27d29cbf04b4ce3b7fa5233f507879dc9e22fb964d5e00ebe6172ba9fd43ff99 +27dc67503a67d508bff47f22b68e3c69840c988fe70ac042036371d96452b2fd +27e1d03600a0e3639abc43b881c9eb2040c29c3091603f23ca411ff1b02be5a7 +27e89b182fb31619c03d87409bb1341684b889b6e43010fa7ac69174c57fe57a +27f35b052ec0e0414d630a67cb6f126659802da1e5b3b8d18801fc5b17e9ce24 +28124e7c03c69e24c9833d2d5ce965658b054073503b224437a7d1563ce9b173 +281ca4df1e7d584fdeae4eba27add54f110a10fb73f16359a7412de03e02bddd +281fb0ed1906ee3983d084ff6dcc508baedcb23c406973da6450bcf2cb9d553d +28211d830da22358a44220b869fb73f36640c4ef6574bb4fb73905b09af41a3d +2821bde6806dc43bdb318bb3ed4c60fdecc9db9ef95f648fa2d014baba0f5364 +28274b04629ea1a8cde7973404cadf54356d1b7c919320f50eced06b17f742f3 +283f69f0e3a32ed52e0434972504157360ec8372172a0aa6b00c117dd7f53645 +2849c53845f2ca94179c2c06773c16032ba75741568613e7d194299568adec79 +2854b0e5de82f4d325bc3058e7dce18cfc6a6fd2a8136544356483357223e89e +2859f9e94d9b4e9b90b4c5b03afaa63ca41eea8680b591c13de6a7442f807c88 +28672ff60f2a35eacc5bf44ae6e01067c8e36ae5db59e813e6077fe2efb15a66 +286978865bd6b4db83e8240f0e8acb49e88495e7589249eae67ab91b69419301 +286e2f80ef88f254d7dbcf6744871247e32a37b708e1fed9bd166d8d6c8f3bc2 +287edeb8f5a5cb756b69692053dd7301d00afe00fa98d2242c8647ac026fdf85 +28924a7a4195e12294e5fae301d04f488c830d878f717cdfe2c52fa76bb4d388 +289515c98abefc291c356c333a0267eed8c4624e517d7c71c92b096eb353ba4f +28a2085e51c1fed289ec67fa14509e5270fbf017089e551ad8648d35336085f1 +28a4b5a8fa2d2634ca5eefce16cb1500038075c46500ef02f3f1987f11ca10b3 +28c0902c5fff77a83cef0481f4b16fc4be6e0c4027398840b9d89ed238f88c79 +28c33afc65867e5b33cb6889a49fa894a9b49576db52165e3925b8cdff1f44a6 +28c825f3802ccc27e56fec2fb74e07c26a3adb5c3b2a560a66d376c46e50a3a5 +28d1ada238ed177f5e7d7d9b36b4ec61b6855b86638dce42c349f5f621c1fdc5 +28d65a53ff00b6d304968545f51e7ca873e7e827e0323aa8843e543ca83306a5 +28e0859761374ce27241f99227353335130d7cb7af8f1c1d738b5e778e3bff98 +28e5577be6a216f52ae5241f7db02a8fd00211c2db83f4f29705bb71119c1b19 +28e626bb7d1a2639b33bd38e0d45f4e4ccf2653e9017537e7fb5f421132167e9 +28e725e18e889fa22d7c9bb6be215cff7ddec4902db7b771b75db912ad04b37f +28f6a8df5fd187ef40fad2e14640806cf60cfa7010659a52b7de27a96db2ca73 +28fe2588bd59f31917a7fd7d08d0422dd57579fd14ffceae82f83f7a7d506c4e +29013ec8802ac969e0deea93ab0a96bff2d3f425cf6080610c53ce57f4bdcf5e +290d4fa53860ec564fbf2f770406fb6ccb481fc2a48f6fbe21bb97e6bb73fa29 +29124fced18c09f1ff164ceff17d33b45a1d6d52bf80fb2166b78a19b8ba790f +291464a678f19f249571a7290add021a03488af3091f6285820cb71fb3a1bc33 +291a6ec2d182bf6edd47b12c371d1ba8a1aebc31242cc06890d56e93d8b9f221 +291d9e4e610cb9bc8dd0debbbb23cce11ff8a1cbdc998b32a1a63baedeac25f1 +2930cf8a003509aeaaac7ad763476cf58f6e6cf550cfbb34aa80de63ea62e280 +293c921236d9448e2c54fc3b0b42245e11b32a539e315df9914753a340b6632b +293db398c0909b31ba691dd153d2280740f2fd54d440a6f4bd32523ba0341537 +2946830a6aee78269979eb9e19a2705ec16fbebf7f692009bb9710759fa9e315 +29474be384b3b67e4831a2171c2d85e4101ffd7565e173abb38a8380ac843f97 +2950dc472a807a4d277131f46ff7f21d24415db42e27672316fc581c2fbc4a81 +2952a1e0a04776a5edbfcac38fbe5e4617e09340c6d00ca02bd6722e060e6609 +295c1349c18a2b439d4c44baf0c3eab68345a9593068d0ac1eb6f9ccee44b231 +2965f04d3279f465a6e6fc2329783aa2536a01bef0f44229ccb5b1d37ca97a15 +2969b0045a4330737f5a8ea1a0fb80320185e0ec35451b1464795b153c1d2011 +2976591677f24f0a351e442d9008e99d0e25b360c2cc50ef7da007b0ef9897e3 +297663e31e51c42eea7d8b423f19e270008de78748a64ceb72471fe1ab4947e2 +297a49d481c7fb7fa80f507a6f3f6685bc60a3367e258324c77829ddd0c7e3db +2984122c35bff472a286ddf3073985e8825289264bb0c331c0f4bf0315dc0742 +2987f15e5e68a7dc010839cd4b9a4740216795c7b0ff498037245cc83c5b5ff1 +298a4a3eb4dcf1f5fc78ab1d0557623c3861c252307956a6db7702f408780682 +29907dfa9aa21515b578540ca18eefcf3e83694d4f409865d36c5486725d01c9 +2993fde69e81dd210bd8a11293fb14fcb8eafedca636fb75da094e0a4ed44c41 +29987acfc03650897ed1f540eeccab493d927966e9595f2e539878866c5411d4 +29988a87448ce11ecad5f77f3f6166f71518007b27fc2d3cc95578369769669a +299b1c7160d37265009f82b87ca157de854e225f573f6aaad7deb7ec086d0bb5 +29a74ce1d9e28f29028f209d63b7eb3445a42df61ef490708521e20dd550cf8f +29bd519c7ad03d1171328546b38781d0145c8c04d3bc74adf3360d596e4de647 +29c13d30fc0406572a8d1f84b0ef00df659904fac5fea4eb5806c556204afaa3 +29caf0faf1047f2510c7bcad978a00079535017f4cb0397576f57c986858cbff +29cc6e5fa84d3d865ce98698adb3828c1a3d3a29534f93d005d20e720707ff34 +29cea3f0fe87b6f91cf1764723027332bf9ac6f4e4b6e38038f1c91ecb3edcea +29cfd3008ffae1c8fb94ddd40f9a83baa917db010092d3b0e674c5134d4f16c3 +29dba0231031a9531c5f57e6ba670a42f8a77960885d40005d4cbafe8c3a9df0 +29e07edf1233cf910cacff7d7766dea5ade602cf6d57581a7c519eff99eb5d1d +29ecb9af5cd5cf0bcfbb5904355e563d6aa538b8bcc3aaf6615fc356e3b15d2a +29f0acba45133898a82101a051a1f13ee00dd1af8b6316c9c465eb5dc7211ebf +29f3c689cf20ad6e04ccedacb2be0cf40768c9110fcfdec0c02e173ee7badecd +29f52fe0250d9dc78760584e3a1c0dfc35c85e7a875a4552da72aa1dc04d3ccc +29f5fec451ab2e3e89c6df826f5b29342e6dfa2293b66fe23a19ca8da50e6b1e +29f9771ec005bafe105dd2d6ef4c1e71046e0c5fbe622267c20f58bbccb499b1 +2a01e2291b6e5bc5127b44a51faef1974f044cbbb5f29ed0c1838e7bd9b93194 +2a05d3dd50713a4950d2b021304bea100af37f1c23d2f587fc6dd3bf80ad37d0 +2a08859dc977430e5c7f37a3384c254b9ef4330351522e6cb925f962b0f9fbb1 +2a164cb2e118b254b0e3e895002a0ffc2a1aa53127583ed62a817b954c120de5 +2a2742f602b167ecdb7474063926e4b7ecd7271bb4e7b33acbbbdb8a35416fd0 +2a2818076007b49771cab9ddf03d8b5eb2757bbb0fce6698c98a4e5139ee44d8 +2a2e9f6dcf276861ec0881749ab3c47335b878f5dad09b7e093b2c49c15bf41f +2a396a1475da44e6df9d6c249c169a98dc990b5479bcffea6f7f55f74de4c5d6 +2a3ff15ea937c5624f3b5cb520f34d9d23b2aabfd6f668c76f5c1c9d4fa08476 +2a40dcba4b7fdbdb9300e7f578e7acff60dd62d3d67ee54d5c289fc94d06c108 +2a45bb8484179fbcca449243f0857f7825a2cf1fd9654053de77a6245e76ca93 +2a48d797289aa92061656ba82c49a710c687fa7ae7c1052035c5993b605f5fa7 +2a53aafd87ad1565b05522498f373f26b87fcd41c53dbd1b5b93e46c8c1e3dfa +2a53c6b0f75ec25d7f97c4e5cf1bd905ff52e405b624ff749dafff6af70294fb +2a546dd80e54d8acd4af309357865dcd66a68458a22d52476a993a2c033a90e5 +2a683e7dcdb46bd1a1fa157851338e4562c5b3356fdb7c55654036149e89c9d1 +2a6affaa82db16edf2757961ff12034b43362db761f3df17ae1f4c9af6c4509d +2a711e81b8af9e33d4a03af1ada1d3696622a233e6369aea330ce3a26dc04a64 +2a724b83376c1259c2c3fc9d5488a9967d59a0834208590b5e05c5112459a4f0 +2a77b4d988b39945af6958089746640c3900f63566c74d9055bf21cfda5befa2 +2a7db7c3630a67e8d670be5943779b38404b8aeb293f3ec9688278cd77749e0a +2a8455443881c923450719531aa4da3d83b3592fcf91e09c03da7f0b2c6610b6 +2a85d43ebab017fb2b80d75790ab06683579601b9489b507c5bba58b02c79979 +2a8a8d5a61f3ca88ba47e7d4e308edcb97693924ea59733c494a9a2093c879bf +2a91706f75f95e423e4b8f69366b4fe7540093c7aaa2f01e592765d31112ad2a +2a98d8f123501049f69e154c114dcda31eee50f520466f103f0d1183da840c44 +2a993aaf5e0729f840e4a5f8bdc39e525d4b93862e70bde47f702eee4b9c859c +2a9dacc2b0e8f0664e29a77488d905d59a2066df390ff1a3cdabf99c7b72a234 +2a9dbcfdad69ed0bf36f834d48cd6c9f122aa4ec7185dd35ba6f7a2333fbc5c0 +2aa033b0d52f286fc23943c4bb29479918f2b0689f6f6eee2285908d486be600 +2aa250185068495950645e0f0b2506109bd532928e377999043589d19b640ad1 +2aad9d5a0f4149416831717e138e18a7dffd357bba63ea4a53c5d206aabed721 +2ab83a48d7aef9a364b45b44deefa6b75c8d7732e33677fa2bc40cc0f2b08e21 +2ac473c1fd218ee17cbf8c84a8f31e1ff74f6df8eea1888d1278809f327262e9 +2ac4c114d4f22b7ea151c692634e34cdf8a9e46ca5700ce7722b70d1a0a779d7 +2ac7f2af00c7d57c5308d8a35049673c067d3c7c0175b0f116a2edf1594b7490 +2aca0dd2fd2706aeaebcd18d4d655f4e36f4ee0b5229e624b5dc186d1be887fa +2accbff38146839c6e72c8207f46947e4a80c74137e55e3ad83c78e39ce08adf +2ad08244595b3f8d1a09c6bc26b8ab903a86abaf406c62d5f108f7fd3acb88b9 +2ad3a77fe37a00f06fbbb7eb53612497695213cef5c784c1347f559a57414d78 +2aeb71ca2099458ca69eb26a81b13f58b681ec6bd3a198dbe4a87f972cff4dd7 +2aee9c59405e0bba974d0ddc44a32da0ff7c21f3470210351d982c9ed4d327a3 +2aef3df7bb1b5191ccc321897f2683270f1d70e492a22a77341496c4dab656e7 +2af305961fee941f3c8a335d9560349952ba98da4aa608b45d604e5b0ca90a35 +2afdf8cef57b27193601d2e565a79e4ca2e3220b03b14c751f3f7acea3e84a35 +2b0212d4d1d2c494c64d8ecd4d874369ccdac45bcd6528396bcd53fbe25de3f7 +2b07a500c3209762d2ddda6c4ad600bba6660783bdf783b470db59d3ccf06591 +2b1221f17a3e1487df5378ecdae5d7067e6c8f772cd305994b6da9587358f0b1 +2b17101aa3c3c1d03999e229afca72aefc3d3db7934c90559a593a7aad5b3054 +2b17b2fdc91a8214ace6b226b160a72d15c963bee7e554787d2f67e37f5819da +2b194cb011579fcb10e757a4c85319e757a779371eb859aba981ee06d70f064e +2b1e02d4ba5eadb9527044a6494fe85bbfe7efa8704010b0561c0cf4155e8e09 +2b2414d1a45984e05c202bbb0a402c87a972142bc53889019bf1c448061bbb5d +2b2a10c8965da80f43231be2268d804054ed73d297b05fb969a0d4b303a59cbe +2b2c894d3b07d2310682fde06c2736dcd23790837d558e619ed7691836b2b5c7 +2b35b2f4f1dcdaae0151a090706d1a880d74490519fa801694fb7d0b54b723cd +2b3aac6c2f8f257fdaac11d7f333d86b40e65d254c524e2e9b9f0edd7be3dfe1 +2b4486309f22d833eb3622cced3976762f852ec9ce3007cd479c3fa6559a5ff2 +2b5ae202938b9642010caa22362eb37f7180f4a244f7395a000c720ab8d76f1e +2b5d792d7ef7fb07e2ffb8265e643d80197074848336909cbb2eb98c1b2bbac0 +2b6668404eefd7a1e0ad9553b699338eb677f6b875828f6c114070826c5c9a0e +2b66b02306936d4264239524cd0ed4abdececa8b84902332f8946e3fd9d95dcd +2b6c1575f1cd5bf7e5de6cedbabbd530361f69093e9d342e264ab59131b012ed +2b754d8e3303e5fd09774d37a2c4d55f837056ef9a9711224d54485fed99579d +2b822eb4568ec9b2164d9e031b6270e50997833fd07b4dc74880be3e6e2c6581 +2b8e35cc78df188365aefdf823738d866805a546930c242cfd315e89f7bd1fbc +2ba0b29c4423a4baff72cbd14055e8036969fa48e4d47019e8a8e543e0ec74e8 +2bb48a5a4b40446800eaf73f68afc5d6f0d3528e55ec63605e24c554ddea4ac6 +2bbb8ce667a8084fe386954af7e5571f4f6b0a12632b2472508f6b4f543127e9 +2bbe24092eb8a613d2949e9c0b6e9e9c5cc751893da23413b43ed46488cd7c6c +2bbf466f4ebf2dcf2b1e188ed050e29a771141c91e5c33d5864d6c4671906d6c +2bc0cd274d468bbab30d0e87d3e30631e7d97159a5873e64beb0ea4fac6132bd +2bc68ad477b3c3eb22868e5edb31977c3339aa323436169944eab5bcf50fda90 +2bccf9cd0087d29bb5d8559f75a343107514684e2415bb8a0761c8e5718f8f29 +2be0cc716e8b778035cbe0186768536bc9a246e397c205af4b2a8363c77f778a +2c01aac091fdbab045dec76ff3f0839f7dd4c3995418cf9503b2ac54379a3d7f +2c02a18c1be6a8f39da68258e4651192ca555fb03dedff2d2d170f64d4f38941 +2c03fc4be16f25dd1328fc40de47359673365c3ca47d6ae4d146c3fff569ac5c +2c068ae74adc7276ab32b552c9d6c9426c4b15feb69348ba40c04a47344aae92 +2c0899fffa7551d33ed37b2c6366c83707299c95e67d1823cf5338fb973c7969 +2c09c361bd9e49157696058973b93738726aa81c8a591d6269918883eabac69f +2c0ee28d2851cfc19016b00e3f0f2c19a3a43bad18545abfbf2a6fd290738c4f +2c1273aec6e02c642dfa543d5d105c2344b60888ef4d85ad5326db0c501cf51e +2c1ee34395ca87c1fb2264a14be0a134012719fc2e7ae941d3e9c8cd9f3513e1 +2c32a4bbd882ad7b6cab57c29524963ac03981c8e56a37d35d83163f1567866d +2c3d0e24d8be7b18947ee7be9cd5c428005a9c7d2ad30d788ab562b73f7d4dc7 +2c4f108a519d1c8cb103f7ab10aa1abd023d4fde7f3847b4c322758885a8988f +2c506aca96e0e83f6ec5a14a9acb6ce106dba38ae0a009605377e1499c632d79 +2c507418a82f5ea03aa000bc3d2efd0a0419b461bf564c8164781ba3034c87db +2c51916bec15fa5f6fdf40e0f05a7432e22aab6954994bbb389f8a8c75736ea8 +2c5ff476c1d8b08610578a8cab5511587258cec4ccc2cb2a7839ae55cfae6fef +2c61a0dbdbb43b365b2c9239689700dcf6518232d79b70848763aa4408ebf595 +2c6977f88667cd502e32ee7240c864ac9351e09fa9ba54bb83d8d2798189c809 +2c69c225b0453d4aba30f00e53a95e24bccec1a39cdfa39d0596ecfdeeb2a308 +2c7e56565d6e7581e9bb04703324c923b54c96724b65d9ae0b108627cacfa292 +2c7e589412a35acba5a2b9d1dc805f563b12e0d650d4f089cc4119cbd90d9ae9 +2c8181fec32d9af0e1a4d7386b069ffec9fb5ed8db222255ea36940c7d67f852 +2c819157e4e069471344835bc8098b6c4f8f294e014225ac6b48c38875893266 +2c819438a9035d0acf911c3834c3678c446e17d73394945fd8baed4f073bfef0 +2c8946dcf8c27e6cdf01369d2ef1277daac89f227847794ee49f1bde87671571 +2c91ab7650a4b579b12bf313f6ef6f5948ea1c23b6a7df5e641e965116fc9378 +2c99d5ae41be59d1f22f44640b883cedad8094cc405fd050176ee22591ceb45b +2cac6c262c0957f8d6d3e097b831b6a5a8bd4346d5bef5e284a226e9efe17d41 +2cb093d13ea7dda26ac913c37716a0e82f8bb2b36d48f2b9bdaf3327b0e45278 +2cb4d3c0c18c9f28f4fd558ab863e08c38231f925ba83f7afded185acd252529 +2cbba49825c2e37d5e7258d624f97c6dd292cd6092b10c297a59a5e574c410e9 +2cbf11c7c9c10ddaba64ac83a98fb423ac8f91680a5c0daee77bccdc0d8d05a1 +2cc833219dc324f40f61bc3538d26fef2e48cd5acf0ed7aa6c34db9e0191bb2f +2ccec602cb720eec4878955bdc9bb35c43d5c4fb299734675e723db1fde872ec +2cd38134ea798833bc7f61c28b62a3995a40d349e72d4486faa4e798dfed7a44 +2cd4648dcea46e3d1ca5dc12725195253996a65bd8e10206480c5ea27196dabf +2cd6f85d820c2c17d6281a944bfd229987d88c0a9e90b97b47d88aefa5ae4ddd +2cd9a8ab3fd1a793f6b997bd8e5e4d9272012b10d01a45df0cd471a233b65fcc +2cdced5cf0296704ff7d619a585044b1d2aa8faecb1dd3c80105a40d0165b05c +2cf158b1a5caecdefbdc1d11b9d9588cdac2e554c4ecf2e94c2e00de263978da +2cf20839f05da852691e3bf1ded2f180b96c89cf2e3514a0c3b41695dcece86a +2cf2998c55e9a13639b0c2f3557e7c29a3eff5c73a2b1ccd83ddc54493394d5f +2d09b4479b20d2f85c3d50262742b6ed1cee370cc9974bf7c063711dac36d853 +2d19997d2cf358d1748fe9b7c6719f9ce4cc833b3c8dce75e138eefa057af681 +2d1a8e8aa4797e5b64c5f29038d716b65099226cfd8c46cc9d90cff3a142c41f +2d1df705cb6f83fb569b94ec095a483593a8925a7a6c49ea4ad8df0820cd496c +2d2012362bf5143805fc2593d0521e00bf94e700513f3a7b707a636dbe467110 +2d380961feb30598b39380ba5d48decf1b4c7f89f971c7c6ecab54482628539d +2d405a5a7d207e672d45af0b2d43aefebc47afb6909c406a146642d35709409d +2d4251ed2d6d4c298c5b335664ff963603aac0bf2d0d9a44444a9f4bf953b326 +2d43759a522a42a4a0ed66e267bb4c08876d32881991a4d2311b8c60f6362820 +2d56a55300e3613deda0d47dbb67b2b3a1e1bd5a44485f86777b988a9fce4a0f +2d5936ae545197afb7da3a4dcc27717b97f028921c5d43679cb041a78624fa34 +2d5a27bf1c033ffe5e8933e66bf9be2ed45621c417e0b5017c5246ff134d9066 +2d850bd1bfbcf58fcf26bbbe97b3bab6e1327749e1b35e37b86e9d6af1f3512b +2d89439e8d0412d03885fefcdaa18bd7a4a429cfef01be0c183276827d288e17 +2d8c2bde4acf99ed7b420f92756188cadb79e1da4d00317f1111ca1402ef6da8 +2d9319ff1ef4287019c2153bf5936561809e82a6726d4e51d3248a10e506160b +2d9590a99f7949bd771f5c57a13844f4418fca47ddcec8959aba6b4ff8ec589b +2d9822472b73558ff2f1051b5d4171320803b4387c96b0eaa35bb15e5e90220a +2d98f5f5812162db4bfc9477a024c308816f00cd01d062dbde2297925eb17d40 +2daf155bfe3aeed5af62ef73bcfe729656ba6875bcf961b3bda1d6f813b7cb9d +2db1e456d7658cb4ca569ed4e15c7ec59448129cd77d3a04e9888a6c0f302852 +2db43f4c1e8fdbcf9d9f5563fe754e0769f31f35ca87021b214bb463ece10b32 +2db6fc10c1abd5781ee89efb27daf0e519714497cfbdf1f0cf4383bef3134311 +2dbddf6a1a685013bdd32f4dcfe6eb27269c7d39afb5922c2c8eb2026e2283a1 +2dc0e42591d35ed6ab7712858aa8be1ad3de3cb26e1e6fb8ad81fe0ea741bcb7 +2ddc251371cc78d9b224e5e4aca280f7860d0c6481c09bcdd0e2503fdba5c324 +2dde7676b5992d444ff69b0100345a008f6b7cbe806aa05758ed94a5265d457b +2de005b57e8a143889f7fd7662952577597b2e6581cb596f622aa160b880b825 +2de0cdebee6960004799b865a08357bc43c94eb0c3d89cb197f5046b7c82c348 +2de3aa3cc6ca26468f1b336da16b742b6554ed49e7b75cd4d901b39bad0ba6c0 +2de6b1c2c9d5611ff9cc878cf7c0cfb0b90c2a81536430c82af96f9ae98cee73 +2de7c6beb82e52717c01643a1eb4380bb4b51320b05f8946beb63513c1d5b5ad +2df98eacd0ee799d384cc0d7c849bbdbf61b26ed41048cee7179af5c6a34fbdb +2e0a93749fcac642200bc36b01366f138a1ee927ef30fd7f1a977a97a9e69a26 +2e0eb993291e4a856fbd63fece8d069f2c5e3e34e06d522eda4c07139281d32c +2e377762bd22a6f5935cc9a4f8732b3456070e8f63e6d7d33ec0f42990691d11 +2e428c1b4bc1f0b4774bb132b266a91dba6fe6db3fc9faaa56a31ec850409e24 +2e42c91250109732f6c5a83f5d52bde6a3c63cacfd973641d87ccccf6da11841 +2e48cfb10343776d11886003a921334c04c1ca620517bc743c91f9cf3c54cb14 +2e4de878ba8ee7e9173b8780febaa9f28338599fb6989dc42965a8adb00e04ce +2e53606c7febba004fa39d2d8f51183c3a8cc0c3018507009fac3f09dc710ca0 +2e59a8545be7e71f6d8f43459cb1a5ba341109dfb28143b144de6ae9bd03e6f3 +2e5f1b5f132eccbc065586b9764366383762ae64003fa211d787fb09223c4eae +2e5f8708e5796929428b75cf42606504766b8471c95f4b898637ca88474bbbbf +2e60f8a8aa583b0e89ca93950e4a52eea63dd216a929c872effb83d1898ac23e +2e61e1d3fc8838f4e34af6df2b85d7dcd47f6390599fb114953d4653eb263060 +2e643b88b3231825aa6f78fb6b8ed9f806aa20025ee15de8616f1fdfe2be815e +2e6a2c90a32293268c1e0e62c6c9cb71d028b57e0c319a41d2b5b78e38d76459 +2e744b0d7fa7a46da542040125b9acb33817370c21398ec44a4a7f44aad034c7 +2e7f7bb7e9daf62d4f22acc971fac8771553a5cd25be01b3f2b0a867aa2a4f32 +2e861d71928522c6e4adde861afee185192e7e929750546b969a81fd48315816 +2e970124abe1b8bcf9978d2e480edfaf85d8d6e54e6ece2d6c7ad3edf573ac72 +2eb4af6488edf1c3bc6de38b1fa13c783d6cd464b969697c75558f2770791098 +2ebb823ab4aad2fb63de965fe46904edbcb3476b6e0049531b63114235af29c8 +2ec7d0b4a42e009c5cc45fb763a2125437ebbb6f87e1a2efeb43db4883ba8656 +2ed89b4fac514c1f25da5ac2528b21ab483f2ac9fe79e8c3153c439ee1045130 +2ee5fa91a2dd54d95939b33ec2933f8d9d98f8245a2d7d937c06c1be3260ffad +2ee7311d9217d3c8d9c029a837a0e0681a362816afd262b9c113393a9fce795c +2ee7e751f577d1f3e60404e247747c97a9b14949bd427a791d85563001af62d6 +2eec01ee24786ede6e0bc13bc7011714dd68d7475a37746971c6f01014ef9408 +2ef690d5d4c35ea2c7e2c13153faffc4f03ca1c15f1b99f4b028c09de29119bf +2ef944eb124307efa55bf0d8e748524ee0110859faa4a8a1fff285e6493d0fb3 +2efaf8a48472d37c69b8f244890d56ec81d252a6091ba86afbd9ed3b23f19538 +2f1488035699ddfdb6a90fd654946a74c475c5bc3e813ab98330f2684f1e9226 +2f1569c703e68030f702771586f203d4c7181a30be604b1c9b7f2d4b78f72d91 +2f1957224b9a44d6b32fc3290326f968741497e0eb97f2a807f23d6adb74f41d +2f234c7acb6de76f21d001c7e447f862cf845fa280e7412748159dfcd572f8c9 +2f2651f6d733f435f1087c46a4b0c547facc09b40fb0de47e65613136a31e903 +2f3534e41515e5d71c0b52767efccc4e84463afc79f231f59acfe2e84f7c0c4d +2f416b950a9e57845fd1c51752c16c1917565b805cdcce6afa2588d9a712e1b9 +2f437b6c63ed6f8fbdf9986aa435802f28303f87b72cbeb458b19dd83cf66550 +2f556d353b1154a790ee9d36867c5ff3c4f52f6d2f3baba0a82c41ac79456d49 +2f5a6ba03ea394c6aa0991c1826ac76e38e0053a15328e949a2b4bd2170c16e1 +2f5b29dfdffdf24a64bcb539111a54b44b0c1052338e730ad002bc8d3a3a5a77 +2f639723a2dd995d0b0f9f5ca49602a60852de877930b0651da829e0e67e7629 +2f723d2ceb8e74543ad705413ef89bfe1a6459440cda429d96627709bb31a92d +2f78feb669f186cc5a3b21b96e2394f5e1e2d6a32c50cc88c63ebe00c36c929e +2f843e3df6b1bf344504a3a67b07b05068ce2ca947ed4d2d2885cede6b60b8ff +2f84725fc84bb96b75368624621be3e4cce25f41fc72c249d61998f10fd873d8 +2f8f5f5fde5467236a46484a92e43fc808a58e81f0327126669ec23e45be8dd0 +2f9046fe50600cbda7db91b6541c6a91288a9e49388abed8acd565a224bb0b89 +2f9772c11a79ed99a57381dc7949f18c08026c041572a17c777b7085cdd6a1a9 +2fa731941c92391f1dc795e1994b5db983d43bbcd48baf615e71a1089fa64884 +2faa623f3fbe3bcf2f4f12f4889065e496ca8d4715809c13f4adeb5c0108806b +2fae9fa23091b0a5ea6da2a5f930f9f822f3ff80458cbf1e2c2cfbb7d65e1632 +2fc571c0a7cdd170e28b3d3bf1ef8f0b5b6e1a45b09d01af2ed0872b59310064 +2fc6cb14ac25d9468c258a9777b2bbbf09b03c29e8920f295942f41e8d6dd413 +2fd86ff122d1982451eed76ecd2d9f68ae1a79ebef2bb2cf36851ee767ce3cc8 +2fe1c2bf93f5ec1fea63908cb6442bedf483a1c76eff02f15db2833cba45b01b +2fe97524a32f73c04260929b6347a28de5a21bcf3e07349ecca0f1c9a35e358f +2fef0639ddb96b1afb8cec72f823436d30ba697f527191de2d965b26c0c8ee92 +2fef728ff392c1648b9fc4593be23a29a5e4f8a4dd911664e4ed86abf394774f +2ffaad633a80380797d9cdb802e175254d175253b2ee1057a81c1082c32af524 +2ffc3137dfa42182b9be46609a7816f797d521be83d5d0d5a06ae68a42dd1dfa +2ffd81f8442e33a1849bc3f8d15c010c488f2cd8a405999fe79519232083276c +2ffddd69011ab7db595eb01b589384914434d24cb637907274b976036b58d804 +30095bcba4718bf6dcd91c4bc18c429b1f2ae16e12008032971c4df53568b7b7 +300b892ca6594e801638f113615cc62adffb5c9299d693bb46df8f66fc17a975 +3013ac265b82164374e7f8b5fde3ecefb13f2494fa6b923265714869f305eef8 +301eee33e6680e3900cb4d9b560f7a6b6652659f0d7ac79038e07b673c39667c +3027baeb5f7f7fb94f094a028951a3bd220f8fdb4e12504fd9580ad37bfdb0a6 +303a8e4776b7e9b9f17d47e393496b44741d4bc8ab7dd905924e8a3327035167 +303d251ced436ee1c493320c3b7fa6e4cc60efebe7770165f4fc82bee4e40ea4 +304494fceb42ac01dde908c256d333f7bcb95696c7cdd0826562c09e9b676ac5 +3049fbd41c8cf972ff4a5dbf4f9839f5bfa1c7de0c2092bad05f5bbddad91476 +305b0c31a585193aef56d8e5d3e028cf4cfcff2a3aad71a44c934d3c2677b92d +3061a3bdfb2d03052d86d4dff0db47c34d9708d28dbf46f355211ef4979bafab +307690d6e99a67cdd3712a16fe3231d571d56e20bf74de1add43e1241f949e22 +308115ba64e57c9f0a67bffb7b360a7669be7836dbefd2ebbb2c9a577a0c2e4a +3086d5d1cbbfd71e99f02ed238f3ec7955151a39ece89f513d29a3d3eee96529 +308f1fbb21cce241471cf66828fde7cf00467eb56e44f708ff06a7153f041cb5 +3090fc958c17d889ebb76a581b37d3370e2d6b0f272444d00f86e176e9f81a24 +309c0c2c8f3aea34417d251cfaa73581b6df3a6c20b79dc420b9e47ee762c3fd +309e16ec743844c59d36b1cc9c4e54e57f26521d8c4b8ba0e64d93bb47c361ee +309ecfa438fd307d2c348b5311467bd581134322f30f79332d1df10e57af9b55 +30a910ea02137cf888028f174aeee9188dd825439dcd9ada76e81c01f011c403 +30afdcca7789b285b0acb0248876edcf9d74ef7bb86c34752801e2e1fba7862f +30b129cb525fb2284d4e68ecb801570cda0b332ef7c810afc39e0a59608dbabb +30b153e549f745a4d3cfe2c23eadcf3f436256735637d7817d588589dfdd71d4 +30b99a90e7f200beee2ad37a9cd71dd12f1efddb57ba57e4c4245adbacc1cc12 +30bddfc80a6e25ad9df26a3e92e4b0f8777fe4beb6ee7ddfc2f5f89f7f5eb4b9 +30c6c2afb4b3226bbf8bb7660b9e6bf77a71cfb0a1dfd2eff3b5658c799224be +30d6c521f9fe72ba99424ae7de76f129f016ded180749b208f2fec732929f6aa +30d6db396d917530a9f51e8d5072758afca90bbec4dc46b9aa444f565b19886c +30dce52379390d134126cf0c6240f2a3f561787123cb8b02928c8c7413a81ea2 +30e573e93592661fdb7cf8175915ee887cb2eee24ef47aa94a6e53cf370fe96d +3101c2769f441e7afffe2221c19b1a5de6dbdc752b17e3ee8c6dd02cc2704e7e +3102d95a8b3309587c6d197946b03e6c6f3595434ac360fee040d5bbc6310996 +31048aecf6b49b4c353d2c5d6bbc69b831ae7980333adfb55a3116e8977b4610 +31078847b1a38cb04f71d8892c41e18c7ec18b75c9ada2e04d3eb68b4451808d +31082cc23ef0de72fbddaf7067f6a734b7a98ecbf01980188b599cb81d5a2dae +31094585cd38c8064f490580bebba23cb3e821bb6fbd2a59c57a53a8349af0ce +310a5534722c8ae86ad1e5887a4c871ea092a6ee7e836b83824aa2cbbffd400d +311005439e6225f38945b3a99214c5f445327966dfbaee597e591dfa4164f05f +3111417b397aef4e3a520c45785bb7f71d46d32b7d4e42d5b52bfee27035f264 +3115665350e896b8723d76a6d8565b72f957659515dd6221c30d760263b51fa0 +311aa4f37b858dfba2035432b4f82049b207dd535d8c31458c7aa72abcc76992 +3123a07c18e3dddf865deff0605f860183a4155eaccf94cadf574d50f04deb6f +3123e54ce315aed022777e0011b9d5363725a096adb492d83de2b138e72b0f8a +3125bd961e4f665ef62964a74fcf0c5d33705ff06cd41dd6bc2408b28debf084 +3125c8d26824c6668fe3b2ee755fb35bfaf0190cd935e377712b5408707791da +3130c48efb594a6e139d79cc8fc1438c0e8803c817213dc4da1a23c4f96bec0f +31321707eced2bd34c168a02b8102531eecc847630f58e06f98adf769cbec8d9 +3132735445d4f0f6864c8a3beb03ba5e26dc56dddf5d9ce9db94f2c7862f95a4 +313c10fe22c2eb5cf20e4a4418975e9212137fdbfdb0fec7d2599107cb11e124 +31421b48e4cdc496464ac1e0b61014d7dec31b6c6fc595a1e1619169a0b9a68c +31596f3f05e373b004f504ed07a834120bc6e1ac12cf110503860cc82d6e33b5 +315d60276b56ab5a04a7d1babb0c779784ca70bdbcb6a2b62753e003e7257b3e +3166927d2189d02a55b7e30b20536a0dd3980cd48ac808a0550b8000b5e488d2 +316740db2d3b9bff35340ddcc107a0e0d6bcd3bddfb93f0ab80aa20b8c9b3eed +31694c9d6b864d50341439c98c3701017024d3567f3e21e1cb987b26e66d9960 +316a7fbd7a2793238878fef6618d414f62fb86848236f4ae2d6848915200e820 +316a8a5faa19f86e011578412e66f93cef6cf60111dc9a273c5983f251487ddf +316bde79387ddfb467fd82ed775da01ff6201fdef689a7ca50410aa34c3a6fb1 +316c4359fc20005d72519763b7898318a6329d1505a44fbd49e362b3cf19e702 +317736101dbb9d4e39ccbd902e466288e0fbe6603a9234aa55ecbe6c55ba020f +317d9a33c50c63bf53053a20751ff633d07af3d8baf2640ea7fc6622b1a12fa0 +317f8d02d574aebdcc0dfb9e0801875aa8e84ce4b6bc0a8849cbc3a49a522f8f +31840f035afc6b0bc923fa8c322d78fc723486da58f94b47318cb67af7c1bfc6 +3187ad6c7831eb3d15b75619de9067943a8403a60391f7f4f112b78b2a123f45 +319440f70e90b285894f106def5388bfc4b38e634dda3ed518f572bb87b845ae +3197fffd404f3b9209f3a9e011c134e04424726a01d8125865470619305a03dc +319e591e2761be8d24da19eb0cb306b6517f36fa084084d0962b64ba117daf9f +31aa4cacaa723d5f62244e274684e590531b395b11fe5dbb6f988a834d85a23f +31ab4ed39b4c02850d4b14093e54abb591ff4fcc9d57bc6d973ca0fd4555af28 +31ad06b0833f2da9b9153b4eaff23e55609b851dbf476dcc9cdc3a2d330a9fd1 +31c6ca952bebdc492d77220e9b5f55f5c32ccda327311e13ed6ae109ceb383b8 +31c8601471d03ebe0f5bddf93f3601eb652bbd7554ac9f28bbfdafbc8b71e2d2 +31dfd2784ee818065232a48971d6bc3c8987140e708f2ee59b7b2bb5fd49f7cd +31dffbf273617547b98ac9d562fef877204451a3cda2e7d841fdbfbfb330f2a5 +31e3270b9d665f35b0f27b05814dba42f2b015c4249ddc239d85f03642e8b46b +31e608e6e0f3d513af537dcadaa65c6012f9c679d4926f0b3775043729d59b89 +31f3996879bd0224a4b3f794cf4d3aca609eb64ff0d6cb77da56a7a99aa49472 +3204415c7a09ee8e8d77171b25f92d4811e0e0a92c5b3af65b68788c39cffdb2 +320817dbed73d4d5c1a8dbf44ab33fd20ffedd242a66f68b6390357f68e16d5b +3208afaf5e0e9059ddf9ac8dff965d0b33bce587bdca455ce0abca5ed88468e8 +32094efad37386377fe800a65fdb85e227de838a71fe474fec47eee7e33fe249 +32279739c39a5465e32635036a2c2a9273f80037ea225e34964b83c7870b13e2 +322c6d081f740c8e638c01447fc819ac69a1871724fd965149aa51fd4a44786d +3232202d7000077d948526af659b1f43ea3f540b8ae35309789981ac8451ad92 +323462692b7e8d966da52afd95f862c5ed423bb8f3a59b4051b5fc3301cfa479 +323bd8cd4657c983aa3ba1d4061b0413045c99a9a2db8749c0909d8f6375fc44 +32594e707c84948cd9ed7355c182586d01c45a3e2553489e3ace42608a81e97c +32659fe3349460f114ec60595a73c3bb6b030a27a26a44f8111bb79375add41d +32738bd4ba0861c80cb6b005e3f9289386099e66feda8358229084238c02061a +32750f0b691889737be71b461ea1e065291f22ac6c615f66305dbddba808dc8d +327a3280799b65b5964c3199df296e24ac687215992167b8b5e26e1ba50c5cec +327b934913ce54cdc1b79814122dcfb59382bcbc02afc8540642d5a128eff8a3 +3281c98c1cc4d4bd2bb73de6e118a38d37363bfc315a3bdefa1c8491d25ef032 +329829e1809b86b835dd283c228cddba750e3d4588972dca6bd19530670b450b +329c592dba05766cede05ba9bdc4ee78774db5c67a6738fca141bec48037a8a9 +329e2a0bf57a11a238cc9d0bcd4351e4c46d6de9964fb821736f0b7b4264c8c2 +32a1d9efd89e5d2cee12e99f3b3ab50441c23a2c8d66f10cd1eb9d5c55be1a44 +32a1eeda05d8e74df58cb617a5cafa41905d5551cc5b9a459a0ed8290a79856c +32a6020d3a65258c356c9ca6edf34cc642100f09b96bfd43634cb5c124b953d4 +32a785d3453b0b86cb7e7d1ee72d43f1bdb77264320a1fbff21203ff4d35ca71 +32c1248471f427e6258d5bfcff66f83b0292a549a3dce11000e9281d92e00dba +32c1d6685bf2463e9fa589c6e997d2db25664b87bcdf79acb733999a919b707c +32c8e21c093886eb0c5f652aac14c0688adb25d8e85cd5819937d132474182cc +32cf87548099d8940f08576572a64fdec3e34103d27b14d3b6efc37a3090e724 +32d0651ef6e259f1f15783a5301ef39989973c4d8dd23271978d894d3fd12883 +32de1d360aaf7cd1ff58e3092fc1cd570f0c8e7c09f0f898812dc6732f1c9a58 +32e02ea13b6bb6241b864b28a1c17bbdb879d468210a10b0859166babd29ac8e +32e9e0d6f02df8159addbcdadc12736d2f4a8a08d88f9714c68f91677d619d10 +32f023ada5991920f19ba2cf0fd8b3674eaf6941b75b9a9a54072f840192c4f4 +32fac7cedb355da1d276fadbf67ceb05410aa42b74bfe253966cf3762d0784da +32fce7bf42671706b3e3e87ed2cf4bd5f11cec1599a5744ed10d8777b040e832 +33083a4f7b6601754fe2971a1c081510b356b8a2dddd729f44a825fa8753d544 +330e9415e753526941bdb75e9e66f4630dc3e287864633e7eb0b82aee3eea20c +33174831aa08688771b2ed1d17079856650515a6734b5764a6f8229f0bb135ca +331efb4822581bf072bc36b9893ccecf26dac869736a3b75aee22f18469cbb92 +3320156fbd6ad051b9ea2f18904ac4d6637f7edd75963cb9bfb75bc83e07ed4a +3339e95890b23168eaf77885edeb1e1db1e2c712a1c39345193d3667d8069951 +33433ddac23ff581b91880bf29135cfe642369dd0ed5c2c0ab1cf80068a5a8a8 +3345d6ce68f2ec67bfebfe6227b5ad46ddadf8ba4bad2785967398788db7acf7 +3348cfaf8b1d60533bc0a3467ecfc3e96f8e8d6af6e8ff0a60277cc9b1b8cf7a +334dbb3de54624bee2c425c8512e7602aa93baaf3859e8561eb34d4eea4972a6 +33557ffbf6040ca473e49fe30794a32b6e3081949bc956daf4e46577fbc73cfa +3359480bf63e6b630d2832d4f00d8ade2938c8b0393c5e506d0a1517bf56b5a8 +33599d9df52788c4e1eea1bd3a689d067351d76531ef71e0fecb3da11601cbf0 +33614e6f37c7f95ce48bfda0698c4f6d63eccdbf66b491de5f49fb67a1df1d78 +336c39c3b330bf1252d4e65e3284ad3171252ab5eab605275663d74d258a7372 +336ced33716595945deeaaec39ddd886b39789ec053c370f0f0ddecb192d1f7a +33a1305bbea03dec47f4cdf0170c9d31d175facb8c17a6f8ed6cc3ab96e0223b +33b38c18d28f31388ea1ef519674b035b2197fa029e4bf5b8a408047022dbf8a +33b50bda5d1ce32beeead2eb035b6d012d7b454e227746fe2e6f862ff8cdab87 +33c924e15d7520f5add776386a527f9f52ad02c5fcf6a352751e676516c15ec4 +33cf16a3b3b9dc7478cabf6257c7a9ff20717817ed1aeba45dd76d533fbd468d +33d14a54cfade9fdb661b594d9ec95ce4a93b056abccef79f7b90be77d0a7ac1 +33d2512e87dcce51686e473a26e81d8fa9b79c85c6e8ca667cfee152fb6e96bd +33da35f283ea69b00418cdd91b9f1dfa578fc46393a43ed29cdf66a080822828 +33e0413d42de13eb9029f62529eaf985a23f9146054ab6466d99125702071227 +33e0469779057455f14a0791ba981b0f33866ae9b3cc35398af4470c2b58193d +33ebdb6d7e3915266d11fef5097df642338f39f85e87d18ffa18d12417b85f26 +33f9264e229be2cd506faebe732be4d4275b97a3240a3ed343554142c2cf351b +33fd9f908e2606798a765994788c9defc981b2275f0335ea5a871257e0e87e01 +340bb5aa2994ab8eae435cb0d70be2110047fd18f928e1b80f7ad82de22f38c8 +3414751c22365ad1495e53b2cdbb4d17f00058a04f2d19d1bdb66dd829b4d3ad +341db76ab6c04b32f6779a6f2739ff4444ac2d47aae7540f4a86edea433f4738 +341dc6bbf7f96a19d637d650a2885ab474607c1aaa267a557b8d790eaeb637a3 +34236d70578ec08b5a4bf93a9188633fbf36fc808907f2d2abd90b8d9be410e7 +3424cff9c172314bf62df5fa8e3cc2cf91431081f46b04bf22114bfca2b9c2a5 +343250d178df628eb26ab957f9aa89970cafd918ddf75056ae2799d9a0556d93 +34460ca650af3355b6f5f918b3156f322eef85ba1cd597b0d8869181fdeb51f8 +3447464642d0c25bfab3962552b79d68a5649b2774c1f88005d027cea57f8216 +344a74b1182a2f503ad16762fb4f2d65182e06f473790a10d7a52ee37c2a31f4 +34542b78d7e5edcca1a9f014237997f8835219392f50c7a1f472b012cc60df56 +345afd2a6304f26c6600a10c8cd4b261f78ad6d613a7bce5b3c8e8bc0174ade6 +34615282e53b43f87660ff965c9492177738a93c281e7b5a3ea2ec91abe3a066 +34632b75292917c4211a76c43cf3388b956fa5265d45cb6fe19df4b5fcd8e27c +346352a65ffc84751b84caa5c1993708e6ebdf57057a3476757d53cad356c0cf +346d4fed9b1bbcf475ed62ee4afe320afcac61dccffe0bd3ba941dea600ff221 +3475b9ee6cca0b240217dd328f4f47023ab329e54260301922904dee40d32f44 +347dc8cfa686db00dc18af11958d89be4b12bb6d75dd0109388fed03ae5aaa5b +3495f2c9a90c6e291487bb81f1860b2fc21d1f156f49ef9781ffeb7c87fae82b +34b1b2348a403eee9ab4ffd00391f4e830c564acbcab0dd33a1708403c97519a +34b474304233418e64816af72cb3511f2cb74d3b3977eb321585412a41584cba +34b6c9af985d3210538cc474d0ac9563f342c98883bcd2dfd3e4b85c29d737ea +34b815779bb7be27a2c4f11f3eded9dd02a530d627bc6cb65cc393650b108d64 +34c3cca9ceac0e3aff1f778d5e6d8f3370f2ae0c77fe01341c1dda2308d6be80 +34caf9199d462010fbce5776ef831621e40b658975cfc4cc9d5e86d6c0c42ee4 +34cfd17a285681728360729db5177db6bdb3bc933a2655fcc8e66381977a8628 +34d187eee0fb3fb8f14926777752c7bf41661cc4820c82f3477df58b12c9e1c8 +34d3176146d8cddfb0e75ce5ee6bf5cf8def213a5af39ce8c5db9d7f28ff06f9 +34d3306b0f1cb65a19076b5604219e7b0c44bd57a0a0536abda8c4321e6eaab3 +34daa4ee43905ea0c84e12ff1906b97b6f5a0a6e487265c7e91cac6a0938f147 +34f4a51b187b1810e18b92580da908b8d055f24e28791d0cad6a753582bcccb5 +34fa91c3886597d94d884f1d06269e1f8918578701388e3cd67ed084a11a267f +35086616ece1cfe99d1eb750697cc1694fb1641949fd075d797ff5ea923728c4 +350f8ee8e0f5f652a71d4ed1af2ca8f7385ded2b9939de758723d9cb960f2d86 +35146e6fd7f2fd9779d41e23e0381ba7e3b83430160de565239cba0e54b2de33 +351b4de22af262d2c2392071c5e45d8008c1a14a02de2ee3b4c949a0bbd2423d +351fb6c48629dae76170e3e5621a7675c982574d6a6f93afde21074172f2ce1b +3523bbc7f9d16492f9020d75c52d33b9804bd4b9e0a7d3ea32179971b915def9 +352ef648d3c29a833c90f5770809a7b603a3d05b699822640ae9a844098f6e92 +354a0cf546cda85c1ec60e0bc5d4b39a1196128f0e61e11a3557c9d82c3c6c31 +354ac5083f6b4871faa16eaf6f04dccaab5ee83d839d9a3af87b50c5a7401ef0 +354dbba5ea4e5aae08e146f26c38a3bbbe8b6b838b3c3e57b6af06821b98e471 +3567f52323f8b5a357e7cdb049fd56580ee69d4467a932f6c2d0c63ba8a8d6b8 +35717c0d2ff83bfad00041e7b3bfab580539bf68d934a9ca7c96849f461219dc +3572e7c01763bc8d5b76978552a4dd426da24c2f74bd484e98971d18fa63ceef +3572fa364b4bf2f3e14404f30c85bfc065d275b878990778b929d80bef776416 +3574c34b8d3fddc098537f3786de337636891ebc1a63d85208d79fc5e481a87d +358375b8a070a8eed103d7b08213d919af4dbb3b9127edc02b5e614a8f00a9a1 +359d8c6af852c02637c1210c7eb1223bae841ea43f25cb4e8356cfdd19e04341 +35a05c5d1c3cc47bf5536d1880ccaea16e3d8d568ba4d1c2497bdd8baf5bb902 +35a116648def36024a61e34e0af6eaaba0b46688307697810c2b8d668ee41791 +35b31ec8b8f3fcbb3f6561337faae4ba681ee4c0f7570e29928021997987f095 +35c736488decfeaf9835935c903e61c0094014ff6a6a1e0dd1db37431977b1a3 +35e0ecd03515a9a2d47ab9f2d9e348a7667d3e25d436ae16ccc943e518ed760e +35e1d272741f38ede9baa0a33603125c694f924bdbf601bd6740aaf89547867d +35e742d5fdf7be040a589f0e344780677913870c820d667225e134b3b17e2fd7 +35f4ad5549d0cc5e388ca76d591844b2ad552ebfded4dc6f25804979e8886243 +360441846dc16cadc44e484c0a146b989e69e76c7e8db673df42e9a58f3304d9 +360647cba9e560cb9eae05493953b44db79121e870eb87f5df3f41af81b248f2 +360946a68947e2fd3b682a388b7b606b9f4190b386229ea8d929af6aefb1f8ff +360d1195c100a38546d83b8e2c410c5d31edd9a56ea1d633181a6f4a0ea0597a +360f53f9b67f8f2af932d16c5c99e6e0b2706287352cb81b70cad697113643f5 +36110ab2cdaa66ee12625af8c40a3fba99eb564b148485a52787906fa53b11f8 +3620f6202f9446baf2d0713859483ec4d8d6d3bfee4aa7281c44ce0ecbc126df +3623dcc88fe18ab45560f3edb5e0a9db058fab47336b3cefd606bebce497bc81 +3637bd5f7ee61fbc1d1e886effb381df066240731b3ed83d3a569974bef123ef +363bd49914ec1a80917a0199c76390ebb0560e13c38e17308157cd9d13629986 +3643a8c4325babdb7c19b99e0939661acc78c54f02aa39af9ece268897a8c3c5 +364686cf47e1b891d9f9fe99d9a35712788f11d15627f3b4a0d78a1e240e0a47 +3646e9a55e190a792b0578930d2f1a78488726a00f9ee385d41846bb4f3630ee +364892023bac3d3c206e173add5d7fff7cdda043cf7988b3990a98d7cc708618 +364ceee5166d1657c27e4b1970846dc19539eac53d7a16b542f1cf53171b92da +36615bb76619a27ff860cad4d489ba8321e56cf4c44e7e7797026d9d353f02a4 +36736255e4a255228539867bd2b6f4b451f8ed1c74feb6299b33a348468f6eeb +36736e3334572f6dbb4e005465ee77478ad1cd247562df8500da7e9113692054 +3674cfbb098cc650d07f2ba8668c324c097fb385af3890f11912cdb665c82fd0 +36773d2bcd489ee6c8ce34dbc0f225353cb6336ea0d585d76e75a45706906cd8 +368cfcc69e500ab4ba062635d487917e6e5f9ca8f183ff3edb54a41059b5fc11 +36933f3448fc9d867027f3ca08763d6762f49157ae287b76fa9dbe9aca6ebf7c +3693eb41fb7f9fae2cf7cf92a173ccd441dd9e27ec8a7c453697ceba7d76bb7c +3694f344e9c3a1e35950c25ccddd89b56913fcdbbece88deb162997ae0b4f55f +3696528c6946fb46537670570383289cffb3c34833c54ab7de37f358b71ae22e +3697f20ae0473d9b8ea22301f437aa82775c91692687ebd6678c8317207c0958 +369ad8fb68fa1bdb87916b27a7b5d940d03787607d604ed61ecf8be1fcd3a213 +369ca4621ca72d124a826a3165c8baaea6998f47d1c02e1bfc5a7a1bb1d24555 +36a3d15cfd010da0cb8632bcac0ff513448211d248c18729a5e7088631ec25c0 +36a85c99f2d02c2226819887b22c010d5eebf2b4875ebd94b9f68725845f6700 +36ab45745a19cb6f710a10cb4ad16edf5d32a1968a20a2ecb05fe66f2e883ec8 +36be42a7e7d0e9f59be2fe1ab5323bb066c9c28644213d907a9270ea880ce304 +36ceb801b8f44bb64878857296480436ffa3d67b76fb297b0fb8be34ee5fcce6 +36d6045b3828473e0dfa4dd20c9a3a275d7ddb275e3897bf7f7191c172cb7c69 +36db33e3822c436ddf2eae4cca48150ae363ada0b7e09850564ccc36e0ba558f +36dc236226c95759fb03ecf4322b7bda4e7baa842df58272830643ffe1ce553a +36de02c9c13d8730eae31ff292576fb3afea7f007dd3d00353ca8142896a159c +36eb9fcc2356d4802f41981e92af62861c2674c8fac47fdc2d01398bf706d4a5 +36ef7e15ac7488cf1b33963a56d6d6019b021123f6f04b1e2ff2f8e9bde3d1cf +37015c6fd717fac30e7faf027ecac401b2bf0afd212c4426d583c325d246f8e6 +3707eb1176b7180765833bcd491e489bb597439933d685325dfbb0f495a39a7e +37225967bc853da0d5d495bdde4f31da40a3678e354a6ad67fd64da4cfa811e9 +372cdf71505209a37ace33e30de99bddcb3fda25a5450a0baf25e9c401e84a5c +372cfaf3a545cb70afc4caff10ca80dbbcab3ecc280dc9d551de0996605b49a2 +373f2dbc946ae22ae5f8bd15d6b98731f5960249d3d44bc8e19d9d9f46eef9cb +374713152bbfe2a766c5462e712c0f00e72ef96b0f9573a12421543cf9cb002a +375db66fb7918f9bab7ecf18bc9093dabd4cc536d0a941efca78dd42eda0c851 +376aad0bd0309a7c7699fe0fc16c30e20ef70a98bc8ee6690f707675cdb76a43 +3774ce22b99a9648b5abd49229dacf13993ddac8056a38915ec463aadd23a812 +3777affaa188307d7c6aa8b81fa7088918c0552c0ae98237b586e6c1670da365 +378147dccf2f4f314184d103a5a3e56c047c0d52c5bd32ab72ef90dd472fc527 +378206145705eb7c2eefdf7d321614a8fa9ae9c909701f28f5cbb62890c4eaea +3790e8b316b26ea5e41e8cc8f904c753c766905c1c534f3e870bf3f2ec6b5b92 +3799601c7dfbcc4d5c013612f756218988a5a25e73167b04c4784352f47382aa +37a1b77a46c71b1e52b5f42852e2f1132eb124ab31d455709802dc52f6fe4d50 +37a3b1db4149dda12d7a2e8f65280aecdfd49c3ed368212b7fc15ad08050ccce +37ab83cee62fe6ef3602380cefb3d3d9ae65fa7e67c35cecbb1a61c05766c821 +37ba8d8158b34d6c759147eaae87667c65412dd3380195a811ea357a3aa56e39 +37c18e8b1c929974b07892f7714943380389264faa5279ddee4e912bc7cf2401 +37e36aad54b2b4b1dc7fd3166d006752341af693e7063ae4b6ea05170de4c7a9 +37e4ed2ce2842a5c6d6905fdd0f7a6c8f38f90907767bd97727bbe7cb3ef3cc8 +37eba7dc9ab410e1f9dc63c980df349f0400c15142ac3415ff836d3d30518bbe +37f4294701e5a13e8ac0623a6a647a4c092923644bad94a82d43bf42b4237128 +37f78733c4924a24b55ed9f57e597bc625cb51130088d5d78e3ecab0e828d7ae +37f88305f030403b40f7702054ed623587c4369d86a8c64e73681922f1d57660 +37fbf4021489f95371bb744445c7ba952c10e5806bd1512eef4d5887e7dce1b8 +38047caa5f5998f15ef17bf068407eb3c8447ebdde45c0d0baff9ca180b42882 +3804f5c58b05246d3ea0de9d442b8d3af10f37443096e6281c083a7668c6fdab +3806622605140e0d41d455f75d3acdb8d6f595a14af5749253df368aed6ca3aa +380b7dcdbda293ccf150e1ca27cf7f1eeca8b4b5bdcf972be66e7c7f03500c5e +380d4857a84b1bf787dadf5af108be2210921d90b5248b2659808544815a644d +380d8ac7a006053bc5d6bdf3663b901b378ec2553d5476c81435eb8b10c1d948 +3833ce8b8ce341952a2bcaab606b825d88d7c5213e176781f580c02cace557ac +383aef97207818a8cd8c2464c4dd6fc59d0bed398b449ebf60cbbeb2e1037cd3 +383d8981a0fa2013c49f3e48b160158499dea6cef69a572a97e4c51034b8c266 +3849821944ab2c111b6e0cc7fa057faaf56615cc5963db4695dd3bc8d7e7721b +384d0f225d54aa47eec8a0582d03c6126f76589f04ff88758e3a99bcdac44555 +385479e5a904d5e23a26ded8f422847b6c6c2b58cc05e4bc73625b1a7576bb4d +38555c9bfff744d7967bcd59f43a91aac78793a1771e2f42eb5be91a4a99f92e +38634d2fcd7d095933a0637bc3c403147685ac934dbf647f453281455ce9044e +38676e31042f9becf7b919960bbe7e0a10ea1d706b046f98efe7c6244b68d39b +386c00ebb54cd4d9f76af713b7799b6c93506cd919aa128aa7e035a55f7f1c73 +386f712df04604cb772af3c10209e0f7b141d645d16353c959edde1fd57c6b28 +3871aecae86fee829d4febc91ef397d7202e90d814a5dead13bc38293251363e +38726101f5c88fedce19895ff75abbf1f4c83dd6ee75e4934fb5d7412f3b96aa +3877c5fc887d52b571f95b4ed1da4fbbf80efa0a682037ee026d588e483a3c0e +389abcd746343586910ae84cbff203503a9ba8ebf47162415f24d0c76b776189 +389bc915a5b700a98f117976f3b0d4af9c8c54fdfd5027cda807d74775f4f11e +38a22a44569d444556c0aa44642229f9d65ffe0b579f7ca1331200d07c8dda3f +38a619b88f98ed5024d733bf52868f2cb374b3219f72599f6a18ff1279200a91 +38b61924cdee68f5d4529eda3cca5c0a63dc9ce850891caad95973b2107a878e +38ba270bac9881ade299e9108f41f7d105b20210f6de017e8a6f838c9546dc15 +38bf2e9b344b592826f207c5b704ace68fab41f00b7dc4f9d02b647aa3de2146 +38c1f68e908e418da4ab402afd17c66264128898d075853dd6503808ad858c96 +38e985a037be7681d7cf0cbc104ea8b7b68ff858784f1f8f7f765af1921ec771 +38f258bca3721b57a6ac3a9ab9a0b5ba0c0c47604bff2c13b2ec16956bba6819 +390ed798f7590be327a86aa2ae7c4eb65ffba462881fdb14e7761bc7e885fc82 +3914478d16d30a8e2acca31ea842a4f61d66fddf1417603f288bfe9df92e0dff +3922f4582b8813294846e7be79a09db3b24e04ac5400adfe230005ea5b5e1579 +392b4f73dd574ea1d898f1f9588ffc95d46f0da7e74658725136304f4a603190 +393d8dc6310dd2f69fa24d085c3f10da5d14758d4539daf51a64bd22141aff64 +394cc68f6beb96bf01e741f13adbe333e789fb340721eb31a962d45622a0f0aa +3951639861a049381a848c0532e1ce4300b86c74a11f5b133b661e6aba6f5879 +395342c7fe763a3a1d3b3a0a6933fd5c178f2765db0a45eb3a16be0fbf96c23d +3969c66a3526a9d50041be853b5b0b3038ccf0af9385bb478d701ff0c250d0ea +3970a0f7e53dab979ee18ab838cb060967d2757e2f10f79aa0fe1cf376ef23f2 +3977f0e4343fcab8814dd1eea9f45595cc6ce6a9da4862929b33ecda9628a6b9 +3979f860e24c39cfa7f59249a19b54716d5ca8ca966d4b0fb284c8f2f1ef14b8 +3980455af258dc95985b94ee5ba3ea1e44fe08db32d62fdb550ce88e95544af9 +3990226fe88c89ab1b0103e815e27207f9592105a40d04505459846fe9d50b71 +399a31f806433228e719c0824301e4e1f376178c54977b03d62d0e374c22104a +399a3c6b8a5f679bd5da1388a71bd086a315f64d2234011fa57bd7bd399fe780 +399abf9bb1684663b8bdede66c56fd1b7d035d88f9630aea3604624a3af03085 +39a3241a401d1c06edb831ad46d471fef75ed9cd49f19b49c74bc817202ed5e1 +39a3c6871d117555099e55024ac641913591bdc677b60589309b574cf33248a6 +39a7abddadc82f2b836ad8d4ea6a045204c0f84782c8bb470aa62b521f599128 +39aba87d328640a48cecbd13066cd3d2fc4b881a69ce937e156101a0b3c649c4 +39bbbb2b546615f0362e18fb68ab87d30001df548f9115bae5cc66a7ae1d1746 +39d1c60cd74de4e5f4fd55eedfddc04ebcc8043ba2f44fa793615b10efcd2069 +39d38f305ceb856097e6ac3b440f66bf157b23a4e0e3cbccff137a11ec983c1e +39d4361f3a1e949e02a1663d9bc1062627650edae325ec6887814262e59ac7ec +39d892542fecedac3e3944f087192e3dab12ba9dc7353bc401b2cf2e58338b0e +39d9ff3760a071602b8ad7d68a6ca21c753c55b4769c29e18b03add1a4bc9884 +39dc9689ef731d5224eed7e2299399b49a54dd47e7a023dc105e19b083f22c02 +39de0dea45b89c3fa9bf77b05c2a4a03a99d496d15b09671f124e87563450839 +39e255338b1ce0331f77145704cd0d32f0497773df81f0d2037557fae34027eb +39e909fc2b77fc1760b80f9592466869783f07649f47093ec10ed97b42e11f38 +3a0d3b559f89eb238075778de74e64bd5ced100283684124932e5250e47eeee1 +3a0dd2ac5ddb39d767a3c21e6d8e30f63f43bb7a52692a8d88c0646a1d022d51 +3a15e7a0e9b1b9e1c3858314af0c7dcbfeec3d75f36b7b321c5c0e27f8ec42eb +3a1bebd82092609133d6eebb5ba8f8710b5425077171dfc0f5c4eb3d6f7f2294 +3a2851b7e812cde96c7d3089829e42f095cde7055df35686bb0ad9295daac6c5 +3a2e17bc884c4738c01c111686e0c28f61a9b00cdb2a25dd5fc1315a40f57bbd +3a341af241d1022166f65b0f54f20e2d7d074e848409c596e791bab7ae96b8ec +3a3a46940f49baefc63a19da5b6365c81ab7c2cc8346e72a34d7c5de5c220a89 +3a453da021d15e3e200e5ab079283d52ab485c63d53b6ec86aa9a0d3ddd33353 +3a51bdf19bc3a40c631a3af15aca22377d88bd722079c179bd8123cd801d6178 +3a52df1ef71547edddaedc59c0990fa4d2a09817d97e5068d03d6cfc26f232ca +3a5be8d73cdbd96d6f0226d134cc33efa0f6d489d24970a1fc4c08c0c3fcf04c +3a5cca518448728bd878eb6fb38b86cc0563549a7707ef87d85f769bd23a8ad9 +3a5ccb2a4cb5d3f2e5bbb1f787457651a3b78744ed282495c673ac9c2f8aca27 +3a6b7d19dfa3e2169c0ff914c4ffc256125c341329adbab7a8fdfddec8eff652 +3a7b1403458ad65d2f786233bfa5fb11a632ad3321419eab57c3d28c2b91449b +3a7d762159a7d974a06e97d4e354edf75a2fc16d1fc6d286e636578cc9f2d936 +3a8c0580ff5caee0126094b6bae5503d5df0507ae17f61b776d01207fa6436a4 +3a8f2da8715cb66f83d38ca9b654eed02324ad0b024d721488bda4c56e82d1c2 +3a91a769c8a59dcb098b245127541d92fb19e786ac446c84f0af00dc06ec06e0 +3aa7a507d060e6aa04ec1717e29782b1d2288747af7d53c37b27e9d453762378 +3ab6e8ef84858a4d69890843c4b3f99c70bd8dbafcf216da718972d8c9dbfe14 +3ab888062c5e7009c86d693923a74b0b4a89a93fded14e6845fd84a16732f16c +3abb9c360bf4f926d184646b82cd4405944501ff3c4917c762bbd8440d66e0d5 +3aca4395334abebb408112d95adfbc51e7c0bd47e53570140f2089cbab0350c6 +3aca84a0ebda2b7a3f5000614c4e6bbbc0f9c53d7c586f951ba70b2be87544c2 +3adb951a441fdf5edeb69eb3b79cf1d840368ed190b123e214cb7e1a46ff7fc8 +3ade63a5b9cdb64a740dcc6a53773f1d562b19fd6a3678e3ee94fe022aae1e52 +3ae147967ab4b1d5f72dfb2268e783ace373f4d3f1854c9f4b306255e383d6e5 +3ae751ea3f6c344d461665f3e1aab6803b887810dc432870bf2d02861685b469 +3ae7bb4369f33b979c3d09afb7b5b59e829aa13c1446ac6585a57351befc40c1 +3ae864b06a78680fa577adcfabadeee9794ae830dbd14705377eec3c0f059160 +3aedef5d03aa547cea7644460db6fe3661a5a652e2e9658e3303f687cfe44a7c +3b005ce19f3130963c36c4a3e9b1a5a2a26f6f73d0d95cd10c77092a240dce59 +3b159d3ac09f37bdc31866a56100bd090d2a54894428e34a80b3cc4345261865 +3b1e16c08bf38e8b7e61864efab3b47804cb095c718a1bb0e767c73b84ef6dcb +3b249c2830778bf6db21d4f038e79f34d49a30373defdf2e150050d77f1c4621 +3b2708585e7f188059cedab0900277c09199c28881256a7bb053cec89ac4c41c +3b3a0f6b820b1070f54da319c9e2dfe36489b7659d462b5eb0be3388d2ca0589 +3b3fb1a6f55d36ad206521ef17150412eb28a170377b31490733ccc52f9ac35d +3b45bf1c827b9c953a47485485072dde6d60f80842c7cd4111fed94a76af6f36 +3b47a3d845a1fe513a3316b8fedc521dcaf0c0b244ee8c3084203e98a0ebdad1 +3b4de2f9c2686437a6c91725bc2db442bdb0f827e53ad871fa14aee2c3185824 +3b56cf57e6addb046f99cd1e61b9100a91df9c3bc127dcf2881a077ce8037c8c +3b61fb93179f5669c056a3194e847482d21502d761f4a818c742168158ad7095 +3b64e5df9e924512acf78b5bcdc8e36bddcd735cc939be15fa8b256c7acf0728 +3b661396aa47d8b4911a98e9f319c0447dbc8226e0878da6ab74724ba0bb23bd +3b6ef35a61cc52ab4e50f71c4320e0e5a592da4ecf77e7d0b58642fed948b7ef +3b77ba6a9a3494a5161ca7aca9b2801859acaf40076457f944d5eda963b0cdca +3b7c2de4ab07bb32a3da3c63666ef8bb0a075bf8c856f6a94c1aa521772e1123 +3b8094be9ad943e2b4fa6e4b098f16ede7a89c3d36101025dd07267581571679 +3b84e2e6ab8ab0809a40ecfbb86c819e42960d36b2fb7454d2d9481abcde008f +3b863a811f95268e7678de56b24c56f119a3f9c03ef52fb3b433a1bb6e83c074 +3b88b28efac4c125a50c1383b8c49c208698e67b4f89bc2479dedbc21cb1dc2c +3b8c3ce9fb7af3000243f8d3e25cadd3244b2f0e287b089d3774ce765b5aa249 +3b8e665139e8b58cfca248aa82485c35f46d6a443bca88efe205ab5b108587bf +3b91560c68b0d79692f82b292d8af1e3cbfddb15441ecb3e28a2246a3c554389 +3b9a68d10319ea7b8ed18df006fd2d55d522e71815aa399b34c59d73fa65409e +3ba59f37f928026651c95b47973f1e202a42676b7b779ecf31bfce04198b0cf4 +3bad902c846311447d6503d0d9385f8178f892dddbdc2844935ce0407e4b3f56 +3bb2dc89a9e6f1e334b7d0e4784014ef32f4a7161d55d980151c6f9aea1f8643 +3bbaa1488599b30ebcdc7ac4acd6292817249b6a3b96be696dd42cc4aed944c9 +3bc82ee917163f4e87145c4dd443d0f33e0c966eedabed3027392de2102ae06e +3bd7cba1bd488525541f0e604490375ddd3d8b1bd47222c2f5070769fd6da0e9 +3bda4a575b8b306a80fa0d4a7b9a3e1c91baa35405901f50e5da8fe8020538f1 +3bdde1104a064647da729fdecb24ad416b4971b865ad2a48f741d3427e5cf9c4 +3be6bf02d2418c1f1c626d6cd55c9d52b4a400f72136a662aefb3ac6ce2ae5ac +3bee09bf77d281662ac3b7175b9860c116c7a0709954d91c3e2760aca7c00e74 +3bef4da48f23560d9fef08e33d85bee280751b78ecc79f298fd88a5319478c43 +3bfd31e2cf246dbbc60621879b25dcb77c071df1498b0903827b6dccf4d50dac +3c00dbe225056d1028fa2119390f7b6c9cab1f4136f9ba11512a9a7c3ccdc7da +3c089cd05f8e60a4336446f90c1449496a1a70720dc0efde7f3085acdc2a796b +3c08d5b0af6a4f4bc42d0f3370cdc3279364f926730a4190b5e3e452ea1aa342 +3c19eb42ac4c491db219712e64caf90ec7c2aab003991ed1c018c6f9986ab1dc +3c1f68baba708d92b08b921b7008abf09deb59c4bf0884ac2a1a5c667b3077a1 +3c2bbb379a4b1ef283a6f11c45ff6cf78715296a36e848eb27f3564ee77fe383 +3c2f93317860f6bb0590d5fc4b229c0b4ab889fea5c03c93b0c3978fe0afc1e9 +3c34f192596177a45d1a7893fec798fe73823c268a3a4bbb56cf838831abb352 +3c39b6c05a3a2fedc0b010a4f4f76ef3866d57a599a078f0c4e32bf516b0283f +3c3b632bf15edab4e0d25689c48a2e57f464661b0b3565786836b44d3d7fb067 +3c3dd03254f9b801837c54b2197c571f52ceac90b78d90da3d861dace7260ed3 +3c468b89e6f401953c9e0ead1ed4e6c5891d3f99a3ee763839f8ed550cb83678 +3c572fb98ebee4800a0777d5a4a2cfc60fbf9200cc50a55717918ecbcecd13a5 +3c58b7e5b8cea79a0e8f5f6232da0627339272c3ba95272dce67d36799746afd +3c5982840de49e96d80eea559c15a71072b6ff96528e7be81fe78aa3f07a0770 +3c5f5ca2c7fbd50ee42c3a906bc87e8382c4f068c9c189beb39d230b17e10790 +3c6bb18799e9fe19668a0c63fb73ecda98a8eb82cb800fefd665d7655a4b7561 +3c6c9dc7a35eab9a00b847aa28e5f37122daf9c69400e881f14979bb1c9b7c3d +3c6ee8e06229bf5c9ffcb97ae7c412aadf86a98e62864e069238bc4079d7e4c4 +3c79325a4541a445691da6e93cbd654d2eba7106f75d3de15ad1a1924b35b7cc +3c7b7faae3fd69179b4650eab239d40435ddf86887fde416c8943690a4f67b7c +3c7cf9ec7bd3efa422a1d138656922224835560fca1a63bda78cc94b0ba58ee6 +3c8af236539abc018dab41b2466ee9e419fa204c2cda587f23450e681ff35947 +3c96a25b2fac520b5b14dc6813da497c63b5c528a8ff91f5d74b93c592f138b3 +3caa6c654e3fe0e2d80955ec6c7278d44b55af64725d4706108a882392a6f026 +3cb8e4c1216faf2e811295aae6084f8c94e9bf8a9d28b3cb5d56e64bdf482de0 +3cba52c43e33e862bf8f49aeb61f992bda90eb156767c91cbdcec97faeb22d85 +3cc431ac28b8a8184390f759dfb6622c1a55edf309adedb4c7f4ef9b3c157a90 +3ccac64abd8eeae582f42754ca2a63e58e234bde47f5779232cbccc8b499a40a +3cccd11c3c238acc3b60081c88bde53ecf93584d3a638951ee1f41586de0f2d3 +3cd4767348fb47855cad8fddbb19cf922afa2bea24ee2c85a503a748bdb329a8 +3cdf7013ddf16a17dffaf1023523c2a454c6b997099dddac26b900f63e59f6d3 +3cefc637af58a4e2970e84b072b3e1517f0e75166039fffb700f3658ea781503 +3cf5b949113eb9f8063d49393f095fcd80691dfa93a7196e5593e5ec59acbc7d +3cf8ab0c0666013ee5716dd3a53236df0f73416058b5d5accabbd81baed04ed8 +3cfbcd061e09fa7c8a9c93b0cf780edf87e338a6dcc8faa23378a152f99e6ffe +3cfe98898a17fe6de12b1468d9b10637e96857d7ed08cbb78fc2415f3c08068f +3d03ec89b8fade28b4ceb64b0593afef3a0afc6f6aa2a38cc8749e53e5070efe +3d0a1e576e9c9214b0cbfac6a779d499f26764adc08345028119ee4f8f5732a2 +3d1e3fe01e7abd559159999c494274cf7b0ad7872d94fa644b72b4329a8a7914 +3d220c796f07b1b74a1e95e4e0b8ba3b8d9955230ca9fdfe3521f2ff0d23d0bb +3d22c4e5e969687f2c5b61cff956a2e38fe2b9994e1818f557479591fdc79a65 +3d2a67dfdb0cdb594c2a52d14af6900ba459a4237522448691ad4e30d6ee1ac9 +3d2f000c87f4cae6bc9956dcf2c445dcb136bdba94dac60969634abddf62df24 +3d312415b0dea4db36dd78c06afc2eb1913d4403dfd5fc720329e36ca41dfacb +3d32fee70ffcb0268202f788a2a563a20919e871f3f13d460ecd3046b8280e54 +3d365db476e9dbcc111c09358a80b89a32e0b1498b1b0bd2586c549bf8fecc96 +3d372235181122033ea7a06016709b8547151e58415e640fee618df03063b4f0 +3d3b2ad36f21b8d3491192c8668691a458c5d7e4dfdf315efadd83c041df4273 +3d3b690ade2e51f114cfde14a87daec014e2ef1b12b20353ece7b6e253362c13 +3d46ca7e37f6544f4ebec2afa278536b641f9e2962bc92bd4b9987bc44f62027 +3d4ec8a351f4f12a81dbdf9bfa78a2b2635519ce4dd81c783dbf467883bf0c88 +3d511edffecaace24e6d18f96d9305aea0d36abba870a13825e12e8a88abf619 +3d608dec990f21634840ade12d89a1a3f716114aae80dbe6ef8dbdc5b9db0ee6 +3d627a285bec4ac1b4bf27d057a2db829e8d8b6bd0762485430b88c73fffe173 +3d691fa2f1493f2e2e39a81f5e10aee0abc4a011c0e042bd656e4007ee428f51 +3d6d03b438def602aaea4f8064b91c90335c15111c1eedfb548e9f504934ee5b +3d7682ea1cdb4b12065914115310c5932932cba0d7978e31ae2b5209710d4736 +3d7b9aafc61914af9d2d732c27d73a8dd2131ae6337ff6fb1e19734b03cd5c7c +3d8e997337437fa9c797f667130d91beadf5ce4168dc6cb4540ed403f512a303 +3d8f094d798175137252670e31b7c6a666af2805d0d96bcb06176a15b5f397d4 +3d9064c9bc54f07b043531fae3f65274419ce6232210880ed466090496fb2919 +3d9088a62c398c2633d284107e073f909393c837de19fe92729bf3b681bfb5bf +3d9c180960d1c900b8e946fcfde52b41659bffd0ae543633738b76e732b8af61 +3d9fa9ae6f4d9bb9d78504d03cac9588e83847c384b2fcb35d81fa46668e679c +3da5583623af81eed88c951019faa6cb482fbc170bc53bf311e088af43819d4d +3da5c0307220332236ae57dc6a8aac6316b306e4798057db81b4545525213842 +3da6d18ecd349dedb2552146cb68746af2397cfa26166d56bdb11ae37cde7928 +3da761dde3ecb27009fab650839fb3a7607e867519f7922f31ead7ec75f5cc8a +3dbbe3d8d5ec151272c66bcd7570b61347ee1ce31ce700ca5a4b32edaf3dfc70 +3dbff21096413d1b0e6ccc19cc37911a5073bde7f0db0c6583a160f875793577 +3dc3fb7cc8ef54b95d94348044467ba667cc323469e307411b54505beb20bed8 +3dc94bb7af6905e8562360a53b8106e5deca5a82b64c4b35e630f7fab428077a +3dcc982852e663741146e1988e09728280c447b526574424a55790ef7b2a060e +3dd967fc0f1b7b8c89ab13f86e118405cb3ca3528d0d3156f31c1cdcfe8f7d88 +3dd973aa6e7ad841d2a139bb702781534cd84d190caba368aa011ca4c1ea180d +3ddebc189d8aafa476295948c9b289e8fd94c94556ffe9ca4647c0b45f13e0c4 +3de118c1ea7b473c785acbde07db5e714982dbb397800d24dc01e499e3390a62 +3de2d6468c6fdc189240dc267bffef80307daa7863e967e569027ae2508ea4cb +3de810e32a93c3ddb343fb2ea43785d078a88cbe23ed6539f7d225f9ac985980 +3deafbd56fdfa6b73643847d125ec5fb325314c703b584aba6d98c2cfdcd94dd +3df89e3e38fd9818c1d6e048c5c9a0305954f582f54199f18e6b9f2feabdc9c0 +3df9c95d6f1eb84f0540f4246ffc3ab46ff0d6026891850982b2ee9dab616a3a +3dfaa9c78ccaf054fae84322dcc3ee11c42bd3e598f38845bc6e4ea880e9e676 +3e06171006f6cd25e50d4c648a5ba67c200aaea41cc23dbbc9566b776e379955 +3e06ecf7e6cd0846011d919b33212ef333d62cc61118e29330696b01fee9cb95 +3e15e282d71e30c8bc7c7f80a9a255e0d0727c72776758bd6b797ea125e629f5 +3e295e78fb6f8b68ed8590c20ed53d6e03ed0e6538aad51cb3c879a657989721 +3e31357f30861af661b38924301eb94066dd869aca7086edf8735ca41d81e588 +3e4825c120c46252e61fe750b6398650461c4780752d8af943571726d487c30a +3e5268038cc95964e0b0c2a26d7b7c17be4c79e4124c42f4682c37b6d192df7e +3e5353fdd7e01a2d0f972cce0ce8dac16bb10bddb30a479b1dd0dea3411dbf93 +3e676f534064519ec37f3f706a50cb8f530dbd0325b01e69330de6588948cd64 +3e79d0a463d34083769d0a11c8168c3f45fd97daf55ac15beb85f453bd7965a7 +3eb5dc749223a42a39dff803fbce823c3b7f385e468b10cc484dc363a2c4586b +3eb814e7b3bd0d3c2142a41290a1c76c372c72ebf49ba00b118306e1ada7b9d0 +3eba7c38c4e4e614bf481ea6e96ea0fd66517ce0d40e203fcc329bc416c83f0e +3ec17a72a05fc002b69532c3a37661570eb87df6611e521b7c74e47b8e25727b +3ec655157f07b630ded2f1399dfe3da69a5ca2f3ecdd3c7573782a64ca16e2d6 +3ec9644f00b972ee81d885982a4a6998967c404c448037c709cd95c022227c33 +3ed218bf4789fe3bb7b0b21237ddaf1ba10a093a2a822fa7923620aeeb369495 +3ed6405bfe5f02a6d65ef2446159ca296793cb958b9c5920cbdf8afd02a0d446 +3ed6b74943a97552a7ab6e5edd0f33727c04dbf6de276efceb0fb0c5444371e2 +3ed7ff0272139be091787dfe504e8971d454fd0ceb024bb772286f3f55db3f1e +3ed9d004b4a238af0ac25768c288c5c0dbe2629017e30cc969cbfe6f4704f58d +3edea2d2964e2b8ccc28951224a33eb61fcb0a18622fc9df5a30651d33fe3b1e +3ee027559ccc6581f475f399d6fa339c699a6d5eb8b99ec1c18a0ba2f42a2b89 +3ef2c014fcacdcc88b68dd6a8cfd3a1a58440129afa52cbeaf98192499d3b13f +3f05156a6b865575aa2975c38dc3d880c2fa3d557df48bd32a9f2508b93607c4 +3f0e8f7e16eea7478d8e122d9e29b98f093f2d2c288df06db83d22774cbb2a9a +3f1448f091a26db5e3c24d2ca2848792e6d23990e9a6a58d0acb67eec480e1fb +3f2ef5f4c546ed5031837cef72b11a312469bb9578db14b88a54c287ac3932ff +3f2f1d877d6f8a4a72d802ed9b57160d1f4d5075b5a9821c16d74f5958b69da5 +3f35f0e6ea1955cd2f5cae8dfe132f233b75f5ca85a76b6b0c31249d51ecc664 +3f39c5b76b6a0afb400fde34a6f25bccbea91ae0ce9f5e4fd251aaf5894509dc +3f3fd19320889b801411004912c7674c45178b8d95b23151284e66aa5149dd6c +3f4fe4eb1c887f409a0a3841b6d548d56feb8b64fe4891ba9213223ca9837841 +3f5e9add3e26d3ff0c52bde1a8a0937772b3190ffa1ee8b07d271b7470f6d157 +3f68bde0a326d5f5f28a2347722b5669abf818f1831acbddb420d838ad289e53 +3f6aee119abf40b59f9de6deb3740efe84316b137f4ad073cf9d9df22950df46 +3f73efd194648a47ebf8436654a3b0d7b6911c9ba347687026c6a11859960961 +3f74b08ea74668a1305afab8c3c6e6f0db682ea97f9097e7342e2072ecb6b74d +3f77558be78ad11289bcb90f87fe6d376f608218f51215ebf1f34c607d88613e +3f8cc385c5e63d20829d03ec6d5c1340661a6045e92b7b3937c62a292d31170a +3f8f4f78e8cc1b451bf3a59a91a3481b583893c33cee1fe816a30f9b6716e9a0 +3f909faeea1faa47316f68db698b713c56c387dafc2f0f716d3fd7c043aee79d +3f90bd329da9146f9126c7c8e28470e94254c7063959ac10a6d826d31e846151 +3f933b7724a11806e6838b7cfbb0063154a9592d4f6954805904802bcfdf6e7b +3f968f9dada3427192c558feac579ebe8ce0b9c07975b7b976b4a272d77234dc +3fa66d0effe721e12b1be2f023d5d2ab34fece19126cfea2dc5ac1ee9a7a6019 +3fb9ef40d059926c6ca03f9a237dd35f6b7b3922b0a5db169269115008cb4197 +3fcf63219a7ccbbcf7d987a193f6c84459ee6f82aea737a7832917c882d9103c +3fec87cd91b515ce88df7a40025b1122f5bc51d40ca4c9f8e971513cbeb451d0 +3ff098cc98f4eca49893798bc11bf9032b04df35586334e68222088e4311cffa +3ff1d6f7af86afa7a049dc843dbcfbc8ad7968f64e76e5a30fb1dafc0c138bc5 +3ff346f9017582170ede4003fe5a5e75d8ec4f22b44722c76eb3e1de3ff2e5e1 +3ff6c4b9afa67e88705f774e86c786ef126f874a032703e4f5cac8af4c1052c2 +4006ba85d2abd477b25f677a99daceb29c8046652fb50a2d6327e03d7cd4e9e6 +4007222ec8547246a1166e0b32d313877d83b1d7cdf301fa0b7e00da3a6c6ac3 +400acb4f0462d5040a34c399908cdb1411bb2d3e5d2db9da0e638004a5663c25 +4010c24ce668f558b69df38dcb7e31ae5cb9c6091b1333d17d6c3c56103417bf +4017effa6029694195835eec414e5d585e98aa2aa0fb63222d7de124bbec3c34 +4018c1beda54d5ddc64ba276299aa1511c47e6a60482e5cf8582b7c578ec441b +4019fca1b330ac0db5a608b25c301d56373dae35d63fc1c09eb924c1a6b3100e +401de293454f181cd606c645df346091b0eec4aca42ab2b5579a1f0e320db1bb +401ec85f0671084d3613bd84d2b0a9070aa41a2baa53a9a2d7ed5d43dc572bc2 +402d4e168ec957ac8e7c73315e5a2f83cff53baf3d2603c0b3fa360731ca1634 +40307ff1007410d8d3de26d38e199d5b72dadfa6b8f5ae2a522c90ac1268acf5 +4030f68ee324f29c0a5ce730c346f0d22d726ec5ff724dbe601429f105a03a36 +403538678bbd5e4a1ab1a7580d7594cfbe9e3be93c20eec85f1325f5275e6baa +40434773a641fb0991144db5318722443c416b17a6ffacba497fbc734a8c69ba +4047ba59f3ebd7838e50eeef989e1784562532b996ec934711e3d62a29f921a4 +404805218cec6cea8926d116f129bb4a5f30368689d19f1ee9ff1982552a00ea +404c3024913f5e99808d0053227e8e83823d4a1cb2bfa1c1d22451398ac8a3ec +4067d213a77b2c1b9ec67bfef24bb5ff92b47bb553b3893e3e1c6ede436231ba +406d43769024e5551aeb243f8bef8edbcf9b6dba030d452bfc2663c631d75fe6 +406de82bee677edaa2f0939a26ac357858f9208b2a9453d919e477451e3f111e +407372bb641468c7b85cddb6185d7fcd2650680e05a6e5a6471fd62e5ca70c4c +4080a12a93f2a69e88d19518365635cfb0c0ff81e54624d6376ce286ff830670 +4083b31599bb95497e57710c4883f09665c92312c545f45dd1826af570c4e0fd +4088bcc3c1790350eded8b52c4c729a8a6ea392af40244d3308f6d23ceaaab81 +40928ab178f8468f40c245696dd074a3db4fad13ccfecfd4bc252ff1e2b1b382 +4098e7710b4d18480963c8cfe33505754e5da1a25a3088f964305cb6ea04c007 +4098ed89ac9fd989651bfd715f7e9370a2c4caab9b5d35bfe24330832f3faee4 +409e3d7a2fb1f3c5c16286e1f4b1d1d8853827200fc6a46cd3d4fb587859fcde +40ab00a467dfb55614e8062781286c654aa905b011123c123bce649ae1dc881c +40b0ff3fe608f17954849effa0bac6ceb797f39d13b218e5f07f770b512c819f +40bdb859f0a9e71a98be3155b01293bb711b4d40d044b2867388c5211c77930f +40c3def1a241786663ea4d147c7db89e34f489d7966a6a07aa8669dc6977ece8 +40c72e2f6c8884993b8342c79bb6e037f601854e94caac3e22ab7f8034b47886 +40cb9855c1b516b62026b421f06ec1631c70d52ad4cfab2e030787e554615396 +40d403c23ca5f5875dd442f7ca45f66d1571fc6b70d949b90a2a9264c6c0f62d +40d5dd4b8977b133b6df59cbcdfb6534c7122249495b403bc10bca60c01717e6 +40d66ddb04223d2316e7efebc24e5b86cc7ed9f3833d6beb40c408d6941ae471 +40d876c11dd8472b534c34275e6313e9a0b5ebaa78ff19caa21f562047947f61 +40efd1d04afbbd9961ee53a7afdea571a2a11568549ed464a81b3350853b01c2 +40f4dc0128bdfefd8a85f8fc0965d4f81fb303312fdda8702ccebf58b921af57 +40fe7b270ba59e01c3e204f9b73e5d9e7932ae668eee8343a44e93cfa6407205 +4106184111b24c35fccdbc8d5bb729ea021711fef3d556997f0a55bd5c16131d +410678a7fc231a70eadeb756bc301434a04002f4173378bcd7e10a27a83a6104 +4117cbb29edbc97d96380b2519e4f693f77b3cdcee837d89a706672d7df7c82f +411896549c3b5b741dcbef2a22c17f27969c4fe4f3f7bd8b8c49d140fbe2f728 +41315261b963ee2b4a706bb5b7fbfbf5996e8ed4f1c62d309bb9a1f2a652d019 +4137824217f279831c08735ba23316ddd2d77a02faadc604d9fb44295ec5d696 +413dd0e02de3edb7755193078428708e5b3590a4950b2e02785b8d6af5d0f246 +4144468c64d7e4ae056dfa366430374039af1ac3fac9bbe1eaa5656cd7cb8e15 +41496631975555b0f20163b9700a80830de88b460e7e56ec025c61c97634d775 +414d5f2999f3eead375625fb00b0dc10f857a4ad24a9d32be07ab28b251a98ff +415635d6f9b1b71cde5d04c551a7a03ff551f56308dc7fe0253c3d02c48887d5 +415e0c948be5e554f3a7e3e73c364821d3d3acc7c8fc5993cba031815e4d9b23 +4168ed870f753287b6c87f1d7b3f4f85d56904cfd262e06304c4f0a6f411c4db +4172fd0d73d8b648cb1782de7c2581e67d00e6f76f67dbec29e1279c902ba131 +41794773c13cfc7433544fb011ac84f3b8ebd63f323f9ad10c3eb7d17352769d +41896bfbaadd43447c7d61f63c639c6e2343f4d03882ed221cb463b83f2d0ef3 +418f2a362d499d0e8352819c0d1d4bbe78717ec312e8d859c9324c4cc7244693 +4193c4580bd20d7aea9ccab09cc2686bba94c05a9efb932125bfec8ba81787c5 +4197fca7d1aa01cd75738df39f31ec4710e84208465fdb5a7544c8aa1f72715b +41a33a4c88c92a479e4e56b23fc011493e378daa230e8f9da4869d147a342972 +41a8ef820e87643a02d46c51d1b5f065d7558fe5026e5572e82cc940b040ad99 +41b23dd0e0699eda3dcfd6cd333704bf36976bbc20b90f709fad9d9e8cfd83c5 +41b39ae1ec2276708f7b50560d16f7b9ce64fa93ec848ecf4cd611bb9abedeaf +41bbd6afc7d6e3e6db1b51a5db737599125d030fabf05c7b29a82bc2f2567482 +41cabe0788ce798d7d4225c4563343c2eaf8529dbcdc0f2a28eff6206daa30da +41cefea372cfb5f795757bb1106f0eaf445fb42e36435dc7e3fc41f8c217c7a0 +41d4a0316cd0b11c6dd04c9dfb6e7fafccd134f4688a007e788ffeee9244786c +41ea13ef660e00ead0aaf43b2246da69f8864a113ed39868b0eb0231c40a0228 +41f04be0cafdc133683e9270764affcb77218e70d5f22af5ad49978fd29937fb +41fbeda89641921ab04a0ed211fbc7d5593c0ec20228c13d6e1e67a7e603977d +420ab7d61ce15d22ded14147f918f076b3fca3e187e03964302843d414d661d3 +421115631da4185d809a2c1074c0176dcce56bee853b77402d09d9f953be1156 +421896d86e89fd5e406fd7b1ca8c12bc591658436453bc4ad53eca74fd4f5599 +421f2131c2910aeb31af919b810011ed21b3b0b0ed312d3640707188cab9deaa +4226d33153a176978c7a54203f65f0dce06f20f8139391bbffdf50385c1828b0 +422b5f6eba96249d7e11f8e13dbc71a73b4a1345727016e1cd18f515e31b658e +422cecee7e1abc31041333fd3b37ab488745073355b5cc81b391ce7b71ef11a1 +422fb7d9dddd4ae5eab56fc3dd26b60553158ea740c50cab984d5f48d67222f3 +423f53f675e765a38535c1231b410bf83bd54986b68f9235ce97350b6d44c974 +42435e1738563619d3d5a3042e0cbf55e00953e92e086e377b5f9b03c09c1008 +4246f9bc83ff7263f800721957f34a8e34768813f41b269d23d569640faf1f46 +425cf5573ed2026043477c860edd00b3066496db2505144d122c233dae99c616 +425f764a53a614d269a608ae4d85f66596709f6b9f9e34806cc47027a8b3bc42 +426c9edb16bebeb8dce7bce2f18db8d19a2e566f064b0d17fb184210b4b163cc +42742ade2f99e48c82eb3cf4196908a6790eb23d3644bfd40b87a932dd1ab7da +427aa4caac035b7ed1f4eb975ee3d0d3748e0a66c658ac34a57b93309c08e040 +42811c9d00a1c41bd9ffc0d414d08abe1e3d36e06db282e7b0a39e6a9330cefc +4289c373ffae06c0a0d3993706deee29062275fe37a28537a406829d055fcc0e +42939ff3c92ab1b1f14a82d0c91255d070e490732ebaef9c85806aa489fa8dcb +429879cc721b1ec56b03952966f8c7a26428f1faa17e7c9f369a67073d53233d +42a52759754cb162471991f1f99a7d37fe90811230df9caf41e4d622c7533fbd +42aa8928f52abc63f40df682ca95cc961fc7d0d6ef09d9a18107cd4e25fbc2bf +42c8592ae6876393873ea927c1df065388b4b6a524c0cea6694f4f8f07022fd4 +42c9bbd1e6e0932d0e5f8f65c1e817ef34ed37c5845954b01d08193c53b0f3e7 +42d7b948365945999b91ecde5f90dcec7269142f01d211518c165ed9fa88a2f8 +42dd71da0c9a58188613b657267e3507206f5952176f9cc60840c0992763b074 +42e01125f35f234d1353a5dcb5e9efb7237881ad48409a8566b1685861b33cc3 +42e1181983e3a71352eb7fac05fe1d939a650a6df72ff288e2896eaa44d4d651 +42e78727138914797e6e86d217ebb183e3d7e3f594100af9fb40fd65852865e0 +42fe9af69c90a8e658124133284dbca09e8feab768dec8d77e371b2368019f9d +4308de77939f766deb666080f95cd95ba628c874da6806c078ab12b80111db71 +430af0ae6fa3d482ce2fa62329ccad408df40303d083251f176df943cba68c63 +4316c3d6f5596547edb638d8f3aba03a10eb508610b901d72d42896b2596b6bb +431717d40fa64293a085d5a0180b295edf42bba5ed134f22e59d93667cdc9d23 +431ba5d71dfc6e93a8f0e9c1ebf3faef9cd797e61753d07e96577a63210dfaf2 +431d83499ae4689c637e5c154b20d8f1df6021b5eae02eba45c78650d433cf57 +432ce92ff114320f9a1dc4644d9a156e9f1b48340a1aacd46f1161fb99064f40 +4333b5d4e8f1c7e9ab057e184620cf5d5f2f15fcd01d740b9d5764b6ac39bef7 +433ffbfea65ed432719dbcec05bc6d39e34280d7fc6f3ff591c33b23d48b020d +4340a5e8b37e0a687bb2e45944e79aafbbbc6ec6dde104234b95af9e9ec7f0f0 +4341b8eda2cea8b1b43d1f8d5128384a98eb18a98fed5c57a7adc3631c0e2c72 +4349ac5e6f6f9ff59636a84d20dc49788549f76a5b7dce67409c94d5bfa01db4 +434debc4ebfbc0e53c774347142dfd1b764d9e9c295e4be8791dc75ef0875ead +435228fbccf5d605a52dbdf1090364f25df12a4e069d7e70cfdcaf2ebd15b1db +435a56d60cb472d5154165a43a089ff7800e7f2958de56ce13a61a01b518e064 +435ff2f9dd8df45f46af786d388f71acf9526592cf11f03ac38649c75b4c02c7 +43626d0d6be19cf27512192884402661f47640cf90d43f4619453c375b235240 +4367fe2b561319658b412537922c30e897288b8fcae0494eb8c760e781e51a91 +437c87373b58cd15865e113a0b647a5f87f418342b6b21f84242a20542cca812 +438cadaabc7914e353e76fc14f5623b7aafb873fad4be7e50818045ec230af53 +4394e0b4c3cb9641b13c328806b3067b1d900da947f814508714129a4bcaac2c +43a9083ea650a8e10866bb0fb11e0b2553cfe385612ca1becf7ceabed70bafe5 +43b24e9c45666956174c2f8bf667b7f307c29f8796522672ff70e90712c61d24 +43b3fa1ff2e61da9f05ec08818c2983ca4f95d7b4c2c27b83bfc41e080c53e5f +43c28946fbb6a03c2e2c6c8e918e6beb408caea4975459322c8349514d71edd9 +43c369a90c4e7d941f27c568fc3fa35ce5531508a724d4bebfd562f9970025eb +43d5f277d4075910102d3930f3b37b1a968fd1521219b0ba62ebbdc6cf30300e +43d8812cbbc299451f6d2bc37161cdad4d3e4c6e700ea6f27079517463223516 +43e381863013ff289d3d77ea0b18eba874941d4f40338e0d05ce255b0600ed6b +43e3e0601dc4e3b060f5949faabe381be657f7d117c9cde8c5db02c5d1f855d8 +43f007f30ebdb67645f24458f3f1971be6b81397e9a9dea20f5feb26f2cdbc0e +43f7b70448c7f74a33c61f530111f121465879bb6f1493c34469b9e8f03a984f +44079501ac825d2e6497ec18eab50eb9247db394d32b608423c21320eaf5472e +44088cd4c049e44a8aaf5941a9100734a1065874feea3a6a1cd4b6a14a0e0728 +44091b4e4d2a8fda6313b66970976f6ec6503ea04c4999cca086221e21a8d7c8 +440b29077ce05bf4cc7608c993db115807d23d2694189915bcb09e4bd093cf8b +440fe7fdf9474de1cee5862adc3e46896435f887df9bf92af4abe4d032829eb1 +44129e263896e1efefd50df66cae0468fb2f59dfe3876022878d3b6781c20278 +44258de4b2bf1a80630c6f90833a3ee00727ea6c4d6aec59f4319e47b69faf70 +44340929feb0437352f8281f809de71159b1c242708714d3455f1ed65d53da4f +444f046674d549dd3d7b949ec4fa28fd40278c9ea4cabe0a06065bdb6b7a62ea +4459fd4b009c03ca62fdd01ed79f441889820937a269ff8c3228aa4cfffbaef5 +445e255e4e0e4d944636bef5864c5f0d433b97672dbf94c13a73b14bf05ea8b9 +4468f65393e92a5af04cb2f4d942e218aa14ac49eaabadf5f7d50c70218306a1 +446a81c90e6063ae5b18a67ada4057f94a3dfee026afcd04440f57c111fd86ae +446ce2664b6a495912d1daa5047562dcb03dec1ef9b1a6c65ba636adea1a52cd +446ec76126744d541b1efd270c71c076313f077872ae0275695d8b7d3647bf18 +446f07d7a4d815953b5c385235922e1ce4617a71f4d583bd380d0b4cecf0d5b6 +4475471f54bd9ba27264d3ae5dff6460567a20bafca5f495984f07012ad3c581 +4477d4b92769ab2cacd70ef1c3dd42eeed01210e199b9689f0956475871cf9d2 +4478a755f2c9e05a0a7e121957ccec7b3ebebf4e8c367f46194fe503a20fc36f +44859b3cdafa864a0a44418611e0f4ae44207badb48691bd73e0ea2ec09009dc +44878e0d438feb7f68ca41f16100dcb6438d113db0239f584a6d622b5f2439d2 +448857e75e302bb4124fe05f3e1b1df19bd944b68c1abad817a862a5b200049b +44940b9355c44bfa03ddd2da4e4b44e066cb7c1d062f75ebdb66c4fd4ff237b6 +4496c6e58edc742d2b8b0e7f6db9751a3beefdddaf3e10d68015ce43b87ee29e +449b20627a7a987ec5b2b4993b4f8256f1a693e8b9d04ecea2ca0a7bddfad4ac +44a0cdbf21785ad9c814a46376ba32bd851876e6c8fc7cadb8e0e9c3769ec3db +44ad57a8f5cf75c5a6b03b4fb0f54cb49d970a44d72cd4fbace940f8f4d63997 +44bc45aa67a1f1b6417f26db76d3161c7573b69f1f46f6a39a9289ac6f7114f1 +44d73575707579adea9845f3c9cdc2bbc34aa562341f0b2b30aea12b97a18504 +44d91ff33a75267a9f7972bda696709b573ee2c8aefa7a66193485a3ccdf914f +44db14af1411eb8d70cfbc12a444861a52f4c35a67a90c704ca48558be3bf48f +44dec7f1287991e2fb8dcd67691cf0d3246c949a941a5ec9ad9131833a6a9b61 +44e1031a5e735e3b9f0cb69b35bcb96f2d776747711b5a743f1065d609ecce0b +44e46e5a4494b43bd839da3615d42d1b9e416e0ac076963647babc7ebddf6bfb +44f924fc63c3200a451721bd19bbb83aa486f04ab265518c1f6313168510792e +44fb5e17e94388b4535b2fa276a2e7593e9b4491bab59d78690583cf214456ab +4502325fb49130ba4121b9c7d1bfe13abfdb631ebcc2c1019a0a80dc438e6db7 +451890f5a467be7e16aef8a39a6cf7a66abc21ce85483a14ffc9ecc32c0d787e +4521c673f08c6cff33f26c4158d7084d31e2321a3faae6b81cc43129ae6872a1 +4548b153d2ce6e738fa910883543598d294d9bd9e1869009d557b6ae66511e59 +454b3e9ed23289d50b25be75606de4eee164833989b044bfac999048b7add97a +454c8c503a1fdbeb0aee166923642858fe0f3d300af982afac36e3e107fe7ea3 +456e346805608952031f699b6ad0a3557bbeaa3a4329c9f0c10cc87bba5886c9 +4576abc865a0ef8a5c047c1014b32c270f006c3421a16d6ced9dea63d211f550 +457a0dc916d518d3031e2dd904136ea529db8fa4f344211d734082071dd2a676 +457d52ebe0a785c7733a65c7434de2c78d6411fb1f7d6c77e2235d85876dd10f +4582f0c2044ce40c8ea399f8ede3c938ad5cec3dbc209fc3e9b31c0edc2445fc +45868317058048fc0d26007168501f3c48723ad781b06a3c9e4ad29ed72f7d7e +4587fd348aba345d545d31a08238cb21a42faf48d79bae8fdf1258b9c0457f2a +45900822972c3147d1cdcc536d486f3d6c5e7f1166b185283f9f21ed53f92612 +4590483a40abf7fe7e3509b4e1a7ce48a0be18d2f97c0f2ea9ed9bf75f5e6973 +459509da8dae9a5d11feb7ee630db63b43c742da8eec83c73a5d4759d1507ff4 +459cd7aed2d2683bb2810564ede0b1a7031af4fa3ae10419bcbabadd1b0e82fe +459e8481b151a42354835491e984459619b0e3292f32d197bb3d349e4c171204 +45b36e9f69cc3e551d7d10e38d744a9ef45f97d2265b46928e823db951c0c9f9 +45b9b18ca58100f98ca0b66a6f7c82ae3c3203d7fd56c9d933361c2b1f628d66 +45bded0b5549bc1767961d85f12df6eafb253f4907b256fdb21861d999d55680 +45c3fdeb0e63a3f389566c22ea9f8711278e2f4412700755a18fa260923acaaf +45c5ad5c536d81a78f9c6c2ecbd2919f0a5c408853f7751502ee05ae2f35064c +45cf78f49b0c88bb21bab012d4a5e5bf7476d909534e32c159f30257970a7674 +45dd46bd5fc5830637d516519256d6681ac8cc44534a280062385c1088d6d5a5 +45dd4cae7a0f525a9f9b3cbd07820601d1f71fc783f60eb7239002b80d81b816 +45ddeed33568f19a3a65a40b24b050a7c6e415c72b09fd2a0322a742bda30937 +45e0367911850e9f59c73258a706ef3480430b47c9f80bd9f4444cc2eb687a1b +45e1b935cd22e726caac5e00b67edf423471b7e35a4dfa39d8d74d7a9eda9676 +45e3381db21103072301969dd661d7caff374aaa14258b0e8fac72aef20a6196 +45e9e722005494c314dbf1658535828cb2f0365786a1b0a0b15d854c37ce3cf3 +45f3d0fbe2b427e50365835002cbff21658eabd0b46b8084dff4e061a3589fc9 +45f99bf9e73346e2bfc3361f849b244d2728a1b3a943ab0762d1251858701070 +4600b3de699082b727f8bbe120342ba6cd3586a9a39bb5714d8da2fd46f09877 +4601a25f02ba0c1f2f341c814cbdfbca74205827f7371e574d4a4c3ded8350e2 +460b8825cf8266739ba011239a43f2842e65443eeddf56d83f3f479e0ba09430 +460bc40db740bfb5d8eb9e6d438d9e8ee347ede0672ab2ef69a5d03df0514cc2 +4618599c5c3ca548148bff086bd9f0e9998b47bed10306fd67a17b5ad2df6e5f +4619738d1b99c8c281222ada6970fff92b940b4496f18533585013a558c73c82 +461b68e13081cf16e7b5829ffe39c7698611620590f3f0c0af45394dd728834e +461e027cb14bf32b96e9c5d4f486a2fec4a5a2865b4ce8476a7df64c26ff9e57 +462b628aed5e762b54866a3a83e90597ca35d0142c08cc7af166b0c6cf7f2c0c +463244caea086a9dc4b8123917ce4f3ac2ec0713d188964f01352aaca38e7ab8 +463bac339824081532c28800a75f5c9c31be046af6fd4a434310a2288cb5ccec +463c29e8337c541ca878bbcd7b49ffb8b12b5b5545ab35d1886e01968feb19dc +4645a8182018113af190bc1c0be7d8f702697e31bc7a314b572f20256efbd2ed +464b72984e6844e2673bc8f769966c118086745236d9f68777d3147c4932c905 +465293d833696280da06e006577cd2e1848d8909e9e4782c1109096106e019b9 +46639d21d06e79334aab9854c66babb19d60b70396f9628948e48026e7586469 +4669b885e63ce577a3e7ef35176075e73213db48f4f244356f70cce1f644d4f5 +466b1d95d3850f4dc3e54d684a45b78682f093842f98e2bf4432ee48eea6ce77 +466ce1f044abf27180c5bdec980b64ffb757694cf6137e24a5e3336c2401d0e1 +466dd3c263d345c46b0e25d8e677314d5011534e489c036dffcffb6d24dadc3c +46752c2352810feae91fee9099875564ebbc6beeda0150cde3b3cc170fd35f9f +468639c908c2af96720fb79aea226d219a425ec0febbd928e0ba54396152ec69 +46884c91312e7231205a9dda5a2e8f2fbb8ed7e54334b9c41dda467bc22de568 +468eeeb59584c99fb9386d37ca46922dce4e9ce387de334d6857d98d861429c9 +469cc0bd943319862cbd7084a6b36deb5bc2e6d7637fb1e7e0ed69ef6d5d5ca1 +46a83fcbfd18dfee0a1e6fe9548f05e75578c5dd4caeda74125eaceb7cf9ec31 +46aadfb4291c9e88bd742eb9e0844b130856ede7fb7f73c64cc282a7406a0c02 +46ac5a1f7eae7b8a362a15ce0e45e99e4fbeb4afd4a270a3aea42be18af1e42b +46b7ffb1e1b073ee8f99d21ea074ab1a4ac82e683348ac3f27c96b15ceffd235 +46be36cc96edc16e8555853ad97d308ed84fd380a0da685cd7825f2d36afe9c6 +46c452b9634c79e923f40abb27bb7430ba9461d545133c6ec74cbcf2313e0932 +46c7d445bfbe7b69a09483f2dc2383e90dc79f85509114d77dd6d8c24905bb8b +46e44cc796bfe018ba8b80f68cfd2473d765fc46b473e9869c40be6392f0a369 +46fc3447217adeffe3c5983c496c060894a973d627328ba86c74c806864242fb +46fd20ff8ff60f5bb2661e9ee66565f213c34357fb278ff52636c83f979d549b +4700fab4b56391e604e8997859b2338f3cc9f1ae63f8a7aa8b65c52133be1686 +470dac805db24a2b4ddb7b0d86771270261dbcb4b6e7779b6133032ac78d09cb +470f494a4928f066f7ed030433d8b63fa03da6b7e870ed587811be0e6c2c2797 +470fcd8d5dd52392e2437253cf06c48238313346c4483c84b6f07d68686ddd37 +4714d9e55403f04bbb9736a38855fcfb6b3c8a0fd8f976c334a390efd26d2f7e +4717d585a84c338e6b4b2103361de1ef54648318db826058a0f9d9592b06515a +471af6b00acd237c97203a30f60a3873e7b78276c71835656df48bb2c76330eb +471eaef403502c26b81f69feae8eafb9ca708f723f56b40f52bef61df64c42d8 +47293201dcc933c02b984e73f951d4a71ca24d8c5769bd90dbfb2c0148ab601c +472d9236a1b383a03b3a1b2446f101eed4f43582afc1867fc265d53dca381171 +472e32e4f046a98acf48e7b89697a6b8d6a9c9b4a2e2d2a70cda480452882aa7 +472fca5910db544f665b479ba55d1663ae413664fc6fd3b54507e4eb26c981f7 +473c98551d88cf848765baa4a4f6ac8643bc5eefef4d33a97143726a1a9b585f +473ded38448b05677dcd9a142b39aea8732069ff69aaeab92f5df50b6a8f1df2 +47434d676d08e78bfd9c2701c3f8f9eae18016c4793ad18964c8dbd46ab1f1ee +4748bf8f350add1fb8c33d493f140d1616b0346d67eda2a1a14c9209c1167380 +4760414ca1f0c52f16fae16b820d8023f9c3c5016b82a520da578bb60b1bc1b5 +47698fede91053bda054742edb88c9a17cc1c9dbbd61da2e8154d5fd3cd3bc87 +47761e4d40520d5187342bfade93b4bd123dcc5e45357a808bc388f670767fb1 +47854e524dc16617f18821822eae8a44d46e5b4954651e0c50c64eebff299e7f +478930547a0614909af93716f30cd06f69153e25814fcf1fd9d80c5022b9552e +479116d88429b1086e1f90ecb65f5c4ad36b04cccf710d9d925347a7894b24d0 +479c21b9690ccca3ee92939e8545fd621b6a71d61a142d56bbd2e2edf528f971 +47ac0df367efaa8384c3de2e2b113970625c0a0b1fc62659763212cf098f515f +47b2d227fc43f6ae1850d9ef820805ee41639a51fbdb7182861172fa0f2e2fe6 +47c80107ea650493edcd2abb889674cca0bb21913300210bf706e2cb7c1df3b6 +47c825734377a018eb1452f266be6cfe7932716de689fb36886c3002ece889f7 +47d5850743fdfe67b87cbd35b278436a01ff8e77ecaa6d342ce3ffc901018435 +47e21cfb5e5a7e372e51ef4a4b6b8b43c6ab4e6253c410a50d8184c0212de3c1 +47f4b03d2b40c0419ad097f1fc9f66b88ed37b5bc80c164481735fd45c37aad6 +47f883e8686b368ef68cda1751c6a6d9daf4032bdb9d731eb062c4b288400388 +47f919b6d5b6ca03f8d8388cb65957b30ed0afa5aad04cd397e56470524dee78 +47fcb002583daf73087828c656349779196f3672ed8eb35a8e16b9c683397944 +48162890a2d95b5e7073a0f906a46f401421b33fc2f79b1747e9a4d23ce30f2a +4816429a69131f859a294d5c6f1810974b886c34d3cdd0104c1230fb9ea010b6 +48197dc3af1b0790596350371c42c3d2eb466a2aa0600395de33cfbac05a17f8 +4828c719f371ef552f91dc2985f8662bc94c2041df3f7427cd750b7805440ffd +4830bcb78712d10c3203ade630d826eedd9e8023cc16a7eedeb8af5730df0b47 +4832885c197dc6d2b5cbb5bb3a925caa40aa7d86aed93e5a6c2c7ee8222b0c2d +484362b3fef040e3ddbced8d77879e2b54c49d18ee8fdb9f80f1055b987556df +4858c35808e7230b740c1c2dfefeb1e38df57847731ab73c55a361eacbe7a268 +485903cbd59d20b3bf7dd74725f2655c0461e3ab2366e17e8d9036f83519097b +485d68aa49d9361ce459d76ac1cd2f0858f3f1493a0cff60502f0585b39292e0 +486058c664931b99b3e1b19fdb1b2eacca57ea877ced386ee29a321f463a8490 +48640d959d4044249e416cd7d1a58754199c55a4226374a7c362bc8b7cf120f6 +4868988daccd6e118d9f5d60dd80bfc1cddbad18b25811be5e284fd47684a6ef +4875b0b8203324705fb54882736dc4cc7074ab82f1f15bb468ebea458b693722 +4877485eccf0dc1e532ca1d44a5196d75c5f6f4e7bc9fa236680510909388042 +4877c02b407a389eaaa7ce02536196a05844ab76bf04928f90e63cb4ac99fcd1 +4877ed2d9733dd52874c988084bcfa62314541e3a7b2af049e4f7eaf89277fe1 +4879928b665133f33b9e7a87d39a317ad478de973d26ead531778a0125817235 +487b2c23e80d53acc3513645ed2f8590359aa9d966c928668e6b462499dc9383 +4882a4c226cd0f7d5e61ed6e6e4f85d62f81a2914180f020185ebe7991f2d020 +4883140d3103f7b24c8444737cbcfa890d35adf574e3cc4973a9ce6294c3b6ee +48838bd275895d92610632b4b90d7fc363ab00c29848a2efad97568d48bae125 +488805c16bfa04668c527fc1d72e3564f3b4bababb3ac31ab40cd8b72e076aa4 +4889f66c093e4052edfa9126ed98cc360a0d0f0867af4e0c29fa0b5e7e5cc0da +4892cfe6e1b7243955a4ad1702f1402f2aed3a50f6644acbd8d59afc65f2e836 +48957f3a451b67df14bd3b3362f11f731d39d740d6ea4503b8095e0bf84885f6 +489b32b3c2efa0710ef000d7ccdcacd35e79e2fc9f4d3079f0b2a4998f160190 +48a1e8026eef94490db40d988d3684c32a979ef8503275d8350e167374de7422 +48a4ca62495bb29f9bf4edb8444eac6f9644ad8b1864a3672536144bb5ecfd1e +48b07f79c7f3f49435b7f527de1fb581b76079fbe26c6f313ac8b4db804faab4 +48b2b480853676bfbe68813fad333439466ffebb03b85ff47d9a3325f36dd7db +48b2ef9c2913f71cd1dadbae9f0212b039cb808f540eb2b40de4d505a2c32341 +48c5194f93244807df553b733a5162087e0bacf89509919ac746941cff75d1b4 +48d3648f72b946ebe7b856c6eb9449239a5445ecdf01951529e678450b755129 +48d55fd33b073c428df3cf5039b7b4318896522098a30ab3a47d9c4a9b0d5a12 +48d67257f8bc6c584b9aaefd1fe33f13fb4ec0c179a6f389e75ac45454db17e9 +48eaec9df8ef440953cc0e14c4214195157cf5f8677aa79c525a2b1e86e387f6 +48f2a692457a9bfb81906288c87004a4f86b1082086351dc725e9bbac6b50fe3 +48f35eb4050024c99ab49467dac5a8718561648a4faf11abcb080825a9e73e6e +48fce1c2c22b03eec33a1e1dd619dd9040c26bb44b5fca597a29f64ce812436c +48fcf2ad0e63824b4f2f79ae7d75b081fd6cea89eb4022dcb69c750e2ba9f436 +490136ae73aaaa45f42f174533433f441e755f2da59c85f3d9c3bc7439fd5f71 +4901804b7de96449affef5ddad4125fb726801e6557fed6a7264399420b48b53 +49029546820946f907aa9f6d5a49469869e0bff30ce33b3a2d42cd9bbeb34a94 +4909a160aded877b723cede972d447e0b4c1cbdd691e6e78671480f7831bcb4c +490a030c28c513bbd13773a3fd4f3b148ddcc0cf9aea92ce6d355214495ed7a9 +490dfce57625a8f81c071f16f0172cd4c6e548201621afc782db76e42e59ab49 +490fda7e7f5aa43bf61e74d3524cabc93cac6eecb302b445966d4ec43589d634 +491caf394c9dd1e6692f0f3edf19d06782644c17ce837013e2b1c6a73a0d7e1c +491cd7b59d8d34b696b82f5653eb23f88e520060f7fc4b36a9b7cad4a2422735 +49249e1b5a9dfaa8c4d78f55b53a1788f5c8677ec03c23496a2a85ef509e08b4 +4928ebb9cdc882a31c2104b1c6bb478e31bc0787ce86f68f069fb3ab223b271e +492d1a8083d6e962059dfd6602ef29df03419de0fa902ff40e017b5a4cc213dd +493335fbbcd4c0cdd6767e42117adbddc05599001b0d281aa38a259d33f18dc2 +4935ca7b53ab417e8a2974b2bce6c0e66a8ff64a3a2fc29b5797fe9ec2c513d7 +4938938c1074502a896d0f635742272e4107c16c3d786e25094edc863b85e386 +494532b829a0074084cc58ecb04ad5625fe26eabd5df8f74e844a28be791b44c +494fa687fba340fd4a9bc663c0bc913d2b4c385ce3b81c9d887038287568f00b +495465b0c1ddfc7f5cb301c5319c598032fde5862d8287577618e8423973e318 +4959e6b6ec0a5f37895cf673f1126d65f78e7f2661b1f19c5c25bd8703dac79a +495e4d70b38018ac8ffdda4cfe1308931f257308672c58c392d341467d8e187c +496a7b86cc3b90a15489544ab94ad8399a731eb95bc4ee1691a70fbc68e7f254 +4970606230b5993ee5d38b841ac5fb15c9b79f5ff032eb00e49530b6b6b65fb9 +4979dc890ac7d635d93a289fd9ec8c060e2d133cbd061f2fd65c61c596f61c65 +49845547924097c80040c95996d0c04a5a6c0f9625ce4c008fc29b3d416660aa +498a8fb68f2dad29ff589913a8e590629513c46ba356201b3d1acc38e5ed3e91 +499355ba1619bfd75ea2bffeb56a6870b87a8dee419cc85e65b6d016eac925b7 +4998e965f74f87c975bbe98a0e81921d75b2fd2186876fa6c2cb120e12cfc657 +499e358f4b4763d1e804f49d5068995fe643f5d72a330152d30a90fe732ab635 +49b448185228021eb025d2778c1b19131c8306f955afe8f53afb5f1575607806 +49b4ef15a0e5a4e92f6a88b9c98a90329339a8c32caefda5175ba8731e6649b8 +49bcc2b7f5dfac0e10e5266b9d28922f4860e6474dfe3ca0295ebc363b6ad8f5 +49c022f5f3250ced0f29b08306d84ff894edca8e6060a63d4e3205ffc4190bd2 +49c07ce75639eeb3ae43c16f75f143bc526bb4a4269a52aaff3a20af90426f02 +49c5265b56422a0861d5b441500dcc1f41bd50591d8a05ee3e36e5ecda7a11d5 +49d279281eced61c4cba30b5c29ab48a68d7ba188f535d320016617413cf5b23 +49d46a79767a822d35420c012aa5e8ef2b0c0e35c5e2fe5dbcc7de32a89fb59e +49d5924a76d13a0ec6de6771da2346ccfbc3386ac65445db4b334e9befa17489 +49e293313d7f9d70f02b9ce1d269b60596bdc9414280c5eafccf0fd15c7c3bad +49eb96eec947916fe81dc5581dbd4ef93aee7a5c2ddef2d6de5b07d4dcb159e0 +49fa2d2ab6d1f2b9b72fff6094b9a5557c2277b6e7a8d10a5a33f02179a0d9e5 +49fbc2d2cc21b4489e132a4db6c3b8f70f6ef82b38962a49d3872da54b5f9ed9 +4a00ce4c091e004706d4088132f51c31a3ad9399757b8a59483812ca08904f60 +4a014be87d1399877ada3f300bd4e4684da3d54971853d64a9fa841ec6ec5203 +4a01ec75fadae73489f217c3177e549e99ba9f604bbc0483ed4ca8efa97031f3 +4a04ad76479afa26833ddea6b0d08935ed54cd6163225102f4f5ce3a88a76515 +4a0f237be11fb725cfff174b2f74bb0a95ddc92f3f32fa63aa9e56e9912c99cc +4a15152946ef31502b0d4740666e3c22dcc367f11865f07146e570d04495827f +4a17bede49066dd1c0374d6563b4914979d44d9940200bb1c0fecfc71a2e2263 +4a236b979007306f8a38e1bccc0f229d0b5697f77f5315ae3de248cfaf1674c4 +4a278b295230a108d9e787dbda06bde09ef89efb80ec4bd9606e1c95af09f1cd +4a28a8bb02cc641bee4a41a2e6c4cf9dbd17825d7e9c31bc6bcb55c1c47af963 +4a2f015e780918462ad3adaaf5dab69c516a98ff3978964d6da8d9a606d5b47a +4a37991514985fbb2242bcabdd85d81976ab945e0982df706ee9a19ef4a4e47c +4a3cdb022efe52fe9900f2d34d96aef536cbd3d700c4b536be84cecbd45e961f +4a43e1fd889378494e89e36fe1aaed4aeb3c0ffc077198bccf6639ed0cb2f4ab +4a537cd636a5ac235390fa2c23ae1bfaa8913092f8fa76f9dd24364135ed945c +4a546bb8d4f06ce27a2d156e035b776a01149854023332c698dea4b09ddce582 +4a5a64499ef8c004a527b0557cc8457fe5a8d16317e3d735ab6a2998cb5009c5 +4a67d5ddd0d916e02f99e759a79fc527c293c8c98eb1d01a60ed4f193d869788 +4a6a26647101346bd4a67efd273653ca6639286ddcd942dff08a4b09c8a57e9a +4a6e4f943d577eef0dbfd351551a5993e7c3c42560054a18f06b065d8a72e152 +4a7597bf34e57c375097b086c40b59e8e615334b8255b882e157d53e8c4efd09 +4a7dbbca75689c33f5ce643b743a1eb9d4c7c6741978ae1167fdd75bc9139ca9 +4a8410ac0153f94af52e382808b79844bbf63f6d613e0ae7b2c12594b8307e8a +4a84f9f1fa7c6949fdd88375a2129c0811b4f1cdabbef178d352e37cb8c12ab5 +4a9ac4f99cac0767edc268285e371c53d4874aab2a99fac2cdb85f7161d92e1e +4aa73278027554e61798aefc361ce10116d8e8bab6fdb7e987424dfa04fa8280 +4aabfce0733b78672d1fef1252392a17e036abe946ff2238612e55142aee5e4d +4ab35ecccac97bd877eda1d33b31a818144e9cd95cfaac622fe4b4d0e734b190 +4ab8e77c810dff753ac04875d8d26837fd1335eaf756962b403629a0dcb3ba8f +4aba47478cd67b29430eb01162ab71054e24766855fbc85a1cccef878259dd88 +4ada01e9ea879f02ab54f3f0529066c8bf779d97bd5a3bd3c255b1ea297db152 +4adf11e3deec3c75a826962c7c2bf83c86b42f5a18bfaf574cb7b96ec35b3fba +4aee1253a238ba42632f2e4b5d86adf42c2a2395551aa45036b902a54de5aa66 +4aef8e3fb3cd9750628add6d4f693875075bfe3d5585470f521487c8a42f9686 +4af796bcdc2bee8f3f45d19cadf069f4874d10864788ffead2ed1c8ad6185596 +4afff8313270a4f9e0a7759582b78ae193484c735a5dec262e55a3e5b83578b8 +4b05c98eec1b6a64401318cb60b8e4cf4967beab61bcda503246d1a989e9aa36 +4b08dc3f2e83b462247ae5abc25161b03131df551376369692e01c4c7834aa9d +4b0b104bc3783b6bc064b7ad034468c531fcbf44a486435eb03cc2425f4f8eb4 +4b0cf312346abc0f97eb8e68765d413dafcf35b9788a7899c6d89fc742208ca5 +4b10cc85a42d1a5574cea4edba73c352bf0925f3ce6dcf98030819664a3ee597 +4b12269ca81c54e45ba172cde16a5f98300a2ec03136cdd9cc87a7b64ab3d403 +4b12506cf01380c9a988be4ce1fa10ace829ef988a11ad0aacffc517e31fe65d +4b12fa031366116610ccadda864eb4806cb369ceb99eb964e809ad08c9db657c +4b1b2e98294f6bed9c4607da58dcde4d1fb3716bc50e9ffa59e4f2cd57c1b51e +4b1fc4e1160ec91b6fb0879501018185a01426a2fc897729a54c6c79f812e115 +4b277c5d03126ef3542056edbeb9625c9b71abf70d51ff1417565b3361b2e2e6 +4b28bc7cede1fe5f2a82806a88846f3036f7c9202b11df5f17b44b312299169c +4b2ca9d579aa0f19b07d2af2f5ae4f11baaec168b145af21227834f473fc403b +4b409e6e875ad21c0cfe900dbb92657c716a78192155ff589c6ab995b80b5531 +4b46b7931ab289943933fedb78edccce947e6a315a47fb7222999a1c1a49809f +4b4ce64cbc7bbcfb4256806a11a35462622b2b78aebc88ba09d406f214514aa5 +4b53ec1fb0fdfd7112a68e1f8c2e335f483fd97c432c61045977dfbfc94d6e68 +4b67b476b8f277c9a4f5f42a329463d8a089bc72b30a767944f6782517e73874 +4b791ca53c2a2beed2c2593fd32169151854f3619b70f57c9d3e1a9bded9223a +4b87ee9a2d42d6ed5e033f14031622010d8f090a82a47f9b42ca9a579fdb132f +4b8bad16c23ee95008fe66c3277779ac023bf174a90b048430f8bf0239aa10dd +4b8d38d95db7fe0659db8c5e9b8a304af0c59510b08c004b9058ad31c891142c +4b9afbd696c76fc0ca79ac7c91f202428a63373ab698ba8f3dbac606c1aa15f4 +4b9cba0854124098292cdc89fcbb235449502a3bbcacd281a3f6775735a84db1 +4ba66992d6cd030c9592a006b288a0f47f295d10dd7cd4f0d4c1851eef3b2d64 +4bbb5262acdb19ac8ffec198365dd52e5ff91374f4da14aeb663163da03fb6ea +4bc3bc23768d36825046218e3ce4638f2410e6f4a850e838aece84deb385c7b4 +4bc5baf7bb609869713aa38ac204128482fa3b9549db10129c80aafa85ea431b +4bca62cb5e5d91ff86c7ac7336b879b6eca31ae25daec951fcfbd55b2dd22693 +4bcefd48bae6b64c36e6c4970ff914215bed86fb4593fb767509f6980aecc10c +4bd7ae2a1f91fbaad93177617376e03c6e73ac0feed0b9312001ec565c336753 +4bdac644e5ffb0a3bd7e9874d406ee60717a9b7e6d799fd14f7fd1dd5ff5250f +4be66b1fa0d2f2eb3ace6a5842d11169c72ad1dc3704ca13033adb8f76fd28ee +4bf4157732a7848bfd7a646dc658fe3d86169d3a64052bb554c4e9f4b0f632c0 +4bf81a039d17bb192b0ee9ca2ebf2ecbcdce3bcbe4d0d34e4ed7959fce87d3a1 +4bfceb22ef5f40744316327e8cce314c76a1a0a49701625a5ad2f4e279a1349f +4bfd4e7884474bf6b775d42de6d78ab9a21ef5b09ccdd6485c44573c9fed5a8a +4c0147a24d040ce905f73bf3c198e4fdecd4eaba062e4b7bbf2ef1686bb90ad1 +4c046ad72de1cb398220f2352afdf8f3343b48c22762c711d509f13a9eb65f03 +4c17636072cb8a849cde8907af254894dab88cf49a1429911c8aec420c22ecb1 +4c200190145976363b19d82a4bbfbebe6a3928d469065c5e1502787409c23604 +4c213180d2848413f5a57ceb8339d5703a888d372ff489e004b1d62689900a6a +4c2bac35fb3a8aea681a879af141e44d3cfc22254ffe62d5ede0862ae327efa4 +4c2d0e1fd940f27e7e26eb027fe1f5a162dcfe5bb423b04251482f91ed2b20d9 +4c39d4407e3ead526fac40ea8eaf2c1881b40f44be2cf21b365057d7023c635a +4c3cea49bedc10d49952fed8e00f1fc0933515bf3ce841bae6703be1a14e03d5 +4c3eb182606e6460542c8ab6d34557e967aca67294283eaa6e0a0fefd1356927 +4c41289141bdfa1c38fa93e16c801b542192613e61a28a96059734118e215a0b +4c46cc36811a8bcaa3627dca544f9f19a390d4eb0227a7812a07957f4f52e02c +4c4be838dc60a8a97a753d9e9160554ff5550f6ab8d7759537dfb69c0c391779 +4c52ec0ae805a749b3615b1aa9b054fcfc47830a027cde104ea76df35528c612 +4c60b6f973805dbd5a66e8d78593c6751580b88f426f75815eea4ea83b5deaff +4c620a880fff5d6a223b0209a43164990b944bf2d09a865153390bce318baad1 +4c6d6c9c8e83d29816df4d50aa97dfe8224cd39095850591d20c84e879253b81 +4c709e19469cd5367755ba4dd5f074899c8d7bd9743355ee7c839dffe399e2dd +4c71b1a8f3d3ca6f4afefa729c4d862a456438e3c0ebaf94186284c56a718ebc +4c79a3adad16320bca755ba0da85df70c422dc2a13b11e2e2c3c54582a9cb924 +4c82ca9ba44a391f7ab5d8f689541357ec07d8acc7367843118a648f1cf9c9e5 +4c88a46939708f6b53cbc2ad07e98a3db2861ae04de9a984bf915174ebd02e42 +4c88acd0dc0d49a7323732b88906045812948e3691e42adb628ed37b76811879 +4c91a38fc60ad5ea32eae1ec9139eb05c8f8109c0bd1beb5b9dbd24e55be7fb2 +4c93fae636d0f13c612aa668db79c747ce36f020cfd5fea8b7abcb50ad767385 +4c9561bcd3884b3cdb52cf70e4d907df4399ef69db0631eed6c1b0e69ef75d08 +4c9ec5abffc458f60d697cf48faa440f4153acf29e679feae957665264091d29 +4ca4c0dbaa2f7832de12d42ca358e332904142c4e725f4962e3732432c7b2276 +4cac2ed879eea9896d4b3fbd9234186d5e15ba54aa064e80b138153f28d107e9 +4cadcb9d4638e6aae3e5f59bb4bfaef1e433a2f56edbf90dce1a05bb2575c317 +4cb25053f1e079a406aee1fa0e5fa41a037be305e529fc7c10aa76d35249c44c +4cb59bb67251ca88cb6aa554fc88748ece22ad200b7cd57b437d340e034e5811 +4cb66fe2d205a727b13da445a17e9134c35e77f3a6776b719e9f1d89f318e142 +4cbaae3e203c0fb0d820bd36a8a92f2c2becfa49909affebe32b04a07a755598 +4cbc05d39236fe07c9ef3cab562361d07b9f22177257fd242f544b386c860c0d +4cbce6c87c151b0cc4c1a4a04689e208260cf5e031e118ef74b2194529df0b76 +4ccc2cd2dc4c27aeb6b3d2a870180796ec85dbf14ddca949be09de62b7396d0e +4cd5ee40efa6bbaf7a70bcb7f897ae8887b791e171ab8712c74c1cbbf46eafd7 +4cd9341541367b042728b0b31c1027042f9d9280c349c65e672e8a7751b68cd7 +4cda1e13048c5b252223cb53ec05032fee89cbad59fda0529c89b98168650bdd +4cedff17fa5c0c3dfacc5946f841774ecba01420bf233a19d6bb8add4bdd7402 +4d10a4bef54175a13eaa2cb9c22d4a895417a5fc040ae0a52dc5783307bfc092 +4d1a1a326349070b9cebcf4be13a609aa94cbc69c403200ffe1e473005bb432e +4d1b853684678e963475f7f7cf04e36e9e201bb1514a6960a837b3d9a933f046 +4d1d7162e1daab06b519d6afd55908dc2bb5c0ff6cb4e02fac3ed33064cf219d +4d1fa9ecd2b2b171fa25f0af68f1703bd6f8307e0e9fcac8935f8076485df2a9 +4d21d0048c3cdfd2ae05bf3ee22c6b64d6aef3fdc046d14a9d99b20809387a76 +4d24490aae47efbd713c1df36cfb5ec2ea566be6ad164e77952bd4ba71a21f07 +4d259bde235f58609548fd985e50402f61dce535f371a44bdcb9f7a39189c45b +4d259d4db27211cd0ef730fdaaae39b50734b38702290af2e42bd38185ee2cd1 +4d2613e7bf593827281aa684f6ce4f7533487db17d5e31f045a10611b372940c +4d27f4a59049138dcb4f1ade0fa89887679f5ae8c3c295e492694e5079508e63 +4d2ab3944c02654cb796e2e7ccfbaf7b76cb5869306d493a81c8a6abd9b42446 +4d2ce683aa18410c56cb5dc052a8613eb7dbefa3e6fcfb6f32d72453d16064e4 +4d3ef3a9e8928b9b150897b58ee5ba7255738e9543ce8f2ae9056985b870b309 +4d4237e293d7e383df7418a7137391c8315f66ded6f83954f1e273cef7dfe6b6 +4d44d8af36ff213bfaacac5f0f9a33c7b4c027455f9ef503c473ab83acb38b2d +4d505d8169b3bd98dd99bd18a1375b91826561442c6c95f9a0d561d7b03c628a +4d52c8b7a92197ae08c02db5192a8a3afb6e9829a799dc4b9e607db0eabdb4d1 +4d54f8b8e6cbcd0346b5b5cfe02dd73cf4055764b40a49661e75a109737fc5ae +4d598c440d0e03508e6a3c97b33f36ae93dff9a1093e6c04daefefc49d07d80e +4d6653af781e057e2ff67d31637f9ae7cce18d0e42282e24bdfcdeb93c667226 +4d66807fc619a9191bf73339614636f3163915e2fc9bc5dbc65a52c4a74c0927 +4d670fae86e2f19c8ca79ca8dcd86f3511cc57521a442c1dd87f6d7522eb26c5 +4d71c2487b3e4f78d3c4b6cc22bbb84be2665aacd1d8190e861f265e10b02638 +4d7873cfb97c1f4042ad3f3ca87aecbe64d5426fae692c78d183cb2161ecb7d7 +4d84b278efabbfb20b17db491d80473560200bde3e6b546d87e3f6684b4c5aa0 +4d85ac1061d6552862e9740cee072368d1134ad43f8c1f4ee068ba569ece5387 +4d87b79b0d31035eed8cf4e95fd1b6625471de3a23b1b39d8b6b3b8f02cfb9bf +4d924b7b96ef28243d0dbe3f486375898016c9dea078af18e833927a7eaa00d3 +4d927227a2ecfa1c845cd25a4c13632d9d4e40f46a8958aa1ba480a239681d02 +4dae8f358dae36bb6614e20fd13558502b22be9fd56e530058792238bdf8c59c +4daece7352560bfc27ceeaea7d08b0183e9c7caf2f63afe41db530cf408e73e9 +4daf4e14553b627121c578ca5015e79b00f8796d158c55f1ce1e0ab5bfb3f6b6 +4dba046a6eeaaf3763d0a3f9d05683b3477cf03ecbdef0a1c4c0a66cbaf54935 +4dbca8c64061cc9bcdae9b6d91272bdfc25f61d206566b4969330a3afa6e38c7 +4dbec363ecaaff9d3683695e889f324233c45bd044a3469943dcd0a1c3cd46d0 +4dc46ca9eee893128212568d4349dfe7d17ec7e4b17ccd6dc4758956d326757d +4dc4bc6deb064afab0c9ae06890456b0d664e973e8ce4546acd3e32b12a0cfeb +4dcae2d692cbd30ba38df0a8057015765255c27b012309c6856a473c7d640387 +4dd30c16bd3203bc53945b9153d0259174a63ea2b740ff045a2db387e9c9496f +4dda357f120fbc9dadd85468c0ee122b24086616f3b81d945d0c0834bb3dad9a +4dea2e21c59409f8d500a295f4d57dfbbe222364f036533a312ef4b266a99325 +4df3257239a067b67d8fa3ef05ec60df757544dd457e4e88b3932c268e759f4b +4df7b592f81368b2427d0842221f18ec4e76d6557872f229ceb8d398ba6d4774 +4df8d5d16503ed1fae58c3e0ff46c85523a08184e93b191317ee66d62b43d688 +4e10a31c6790a254ba44627728af778053984bf40b340de64238d3f45163a662 +4e1957a1b830940900439586c14437ee5b56df068392f6a60ac4b59cfc38dffd +4e25573d06c8dcc615977699159dd2109bb80b87ff56a9852b5963eb56cc17bf +4e28f0b1b31daa00166deb7c7ef33f32d5dae1bc08cc2115d7bdcac9d6faf4de +4e346ffc03ce3e47109409d5c61ee6fbf98e560fe5753186beacf6d4d91fc350 +4e37e043ed0c15fe88c1285fd4404c71e32c198dfeffbc914806087566bdbda4 +4e3c7cc9a9333749dfd02a312915955b71d767da98592676de6bef907e79b766 +4e5a5a99b58cde7bd651b983f3357f107ccb5334e7ec0ebf67250c80a0878c5a +4e5cec1fea6ae2253feaa28532250ab47459dded6d710eb2deb43246bbd710c9 +4e60f8e6111c480f9a13ea8f7bee0a59de7539cffaeaae40e4614d5031f8de11 +4e6419939c76ecd6248704662929510429b011293ecd9cec6b2dd083e02773ad +4e6cdbf0b9798c05b9bfe09e343e5c67124c5f3f6fc3f043b89ef5dc51cd8749 +4e70bbb598d63848f189df54dc21d61e1960e5be1400dd441ec56bbcfd39d0d9 +4e79b10956a7e28a18e6592072ef1ed73934bacaf62616d24de06691c21212e6 +4e8368eabc578d2ad150f46c19c89b5866c31809a02511281a11f554bafc4344 +4e8886bcbb25b7687243ce07cf7e5c856c0c7335127b2db646ac409b7c40b843 +4e8c9a0bfdccc347da87318a6a91d05c6b16e45eb66754e71cf849d8a316e842 +4e9b057adb8a8b5344ecf26daa5724fc9d2cabdd08288aaa433fb8ea3804daf1 +4ea2fff28d8d132eb037291677ffed652eb729e197a9147b61e578031d4909de +4ea5a3b180343259f9279cc81dd371e65c734d0d4b8b98ec8c352e39c5b7fa40 +4ea8280e461d032f7b726dda8f3b7896702fb35f71dfb5011c5f2c99c8164a25 +4eae6412c7b3196940b6f9333572b10b567be1604cbe6f474f15a485dd548387 +4eb546424d04712cc72da26628b9bb6bc230518815bdf75424f41133e1da8aa5 +4eb7e3c802a2fccc8683b4f25d81fe18db04ca7ca964b64d05f1fa8962adaeea +4eb8b2cbe89f46e9f828603e59d18e7a17a355ebae2a4edb0cef6cd5f7a05afb +4eb948fddfbc38e87168caa168aad3274e3cf91aaea5b4ae708ffa1716058721 +4ecf8f6ccb7d8f8d45f795494cbce8d70ad54104a47a6a2aad7322c7bf021268 +4ed2ddf4c5b00013da07c8c757055a179b709bd2e841e3235fcc7fa4f3997763 +4ee407186e96c482ff2154cf4a91cff1dbdae741ba17b6de19def8cc089f3b93 +4ee894264e9e093b3cc22bad958e8c92f0df1c5378b469d23d042112247aad7d +4f0385dbd35f49e5862c02727591ddd179d0f706759ee8f0da20c6fab85d0ab1 +4f09be0e430fe5f4ca99d9dab6cd4d59aa0181b6894800dd1d417778b6995ab5 +4f0d1d878c5cd060db1dbfe72eef406ee41212339e1b0516a4792460f64a7c3d +4f360da5522f8a20f6baa955fd8a867f204bfa40844fedf301776db30935a092 +4f37e1f3891b5687a44956591c74c24357e51d0d5dda9aa2c496f43f825f18f3 +4f38814011f58be79f318e67149b5e878a6c1f48080a164503d3c8d5263e9f69 +4f39d34836f6cf682db9de1d2faacd47a5a8d707098077cc18b88abdd710ef92 +4f439845cb0bb5ac333f4d36deb516cc149727bf2e737af077d0c1df0aa1712b +4f4bd9e1560dc047eb6bda318008a94422b1f39656b1eeafee56742a0d8ca18b +4f5355c6237f14aa4912f083ed99d4b2d3019f1241f94d23bcdd4fe01479945b +4f5d2b9ed6f7c18869299d12bf8e4c9a084fc9630fadee44d2d95caa688bf47a +4f62c6e7b485606aa7aebae11e4d5ebb3120a7713418a6544cc76e80ba112fb0 +4f68292c9f904f674595f835f58e15feabe19f241942a3d7a4076c5e21e19f96 +4f6a809428033ad3570b679b41e93d3ca25238b47c663d1761ad6003a51c3a0e +4f7a6ed551a0884af3a262eb21179a096f2051710dda4165da4d85b16edf6dda +4f7e8563e8314ba6bcf365ad4eea34ee48d036fb7530a33ba36430ce2de871c8 +4f8cd89716487a37d78c8203714b2b3e3519f3e75c9c6fb84b066300835aaeff +4f8e1b483857d6121981775c72c55a1f9f34ba7408dd1c57603f879f0d891a75 +4fa4a689a05e80baa54184d807d65c77f6a4481e71604c9ef6b2629ffe5a8648 +4fa5def82fc5f39de878e0150452c37727bb0629bd1741f804d5471f5d6558fd +4fa67b7d0b3c5160e6ad61e2a4fedaff349dfe65288a83071636e2d35790f454 +4fa9f1e697a13b19d2c3f373fe22336c5a504fa16d84a8dc26e52dd8ff646fdf +4fbf9bb841755bec933a3ac1a93af5da31f38c76b7d372dc139f5345fd8ffc5f +4fc4ea85a29c715d2d580a1e791f375b144893435d2025415f6ab95bc8afb8b1 +4fd258eb072c51da78a80fb5c41b3d25ce5c35e25a32ad64a8503688bb12397a +4fd89794b17ff32250b1917dd9a48cf3767dce250fddc0d1f7e166de4197a77d +4fda3ce292f4edcc8ec3198f839c28ca024ed5d05e14113091caa1eaefdeab8f +4fdfb562ad816975c0f9b7c2da4b6fa5223281b0f9c71ef164221f83a4b01fcb +4ff8394cbfefb8522496e1ef0b5e4dc3bdae5e4694a16fe52eb8f85f173919e0 +4ff91216466616754d19b69a049563b3cebd747bc6b6d9ad0a1bc8df8227d5fe +4ffb894a1e9ea1b63ee51695474520bccd2ebfbb957e404f26283e4da29d846a +4ffc9c29cc26a3cd3e9cc8050a0242165468acd9d91fd3165d3132a9a979f59f +4ffd479f629ac648818411ef62a0f09c7954c644bf7ebcbcf817a6bd00112153 +4ffd73c264706f17f6d37e9a5c6910c24a90c75a783044e6a9ce19597f7999e8 +50108df3ed32b14166f6d48e1ea8a7780f372c33a13ca609a8187eb01543df6c +501a49e2ca50412bc1950e40387c5d3204cc635505aa3a235df40ba623e911a8 +5028b3ebd25da6897fddc48039c035131ca43aad9dee1221c4159506d7a37a96 +50339a1027fc5a2ed68fcb75b99aefb374293b7a872ea2847034a5851691a8f2 +503600e8f553e5c863154c1b7f76e65a2482c3f7deddec4edd9b4a4111e53937 +50362d3ac50954a051a00d628440f8d5f7f2c5acf4c9bbcd5264d6324afc8442 +503b61708bf22ae8504f0a9defb4b5a47c4961097a385c1876a7ef5187204be8 +503f61537e7fc2cc3079e0ec8b79340b1579786d226e7c5a9c8023d82f2fb9e4 +504444ee59b42f6ca5cdd3a3077c2ae48e4ecfccc4240044ddd8aae8fb81433d +5047c628be3cdcb2681fb18c56a781156a3271bf22c86fc9063d6ac7fd665cb5 +5055a3b77f5c21de450523bf745c7e812a817b52fc747cbca29fbc1f1c218d41 +505ab3e98a0c37c33a1cb65249276e68daa0066a823d3fdfa03355a0517fbf8e +505ddbaad42a77f5444ab0fd67813e5dbfa8f2e8c97c57db3268892705641523 +5068b3ddbe1900f62ba172b162de0b8e062ef030bab762bf36bfcb720c07dfb2 +506c5e37c81d21904b72528f810085e3f07e41692183abcd80ba064139feb225 +506f7cc54b0050b75e85945fd0b1a9e10937a5245a0c90e80f7f5f761ca0dc18 +507a4fbe9595b4dc235cee4defaca7d4f9a8da72a6b5ddca3e50e7254ad1208c +508c2596e6dffd9d5c364f7cfcae01a277620f0a3ff219a7e049bc8d36d01f41 +50ad8f622d73f13cc175811d6555f3d8d464bf0f64bb7796b20b695d3a01c0f0 +50b5e8d3c3e7e7d1da1b3aefc1d38567cb79b2c16440e80a5b95b35178a7b9ef +50c2ce22d16c9593f2d2981c20d82d46610d11c9a37dcd73cef65b3a5ab056a9 +50cae0b5b828aca9a3aa2f4bcff3cd2fc17fb09a24a4087ccd00d2433ce9a8ed +50cd0cbbccf09935a22232d0f145f689fc08faaf81d73c4daffe122aaa2609fb +50d002b05287dd63a0dc1e40d8b07dc51c6aeacf4d9088df23340863b0edc255 +50da1bc9a57218a664594c395cf7d75b7307f3e218bececb92320c2a8f32fbad +50e2bbce7c47534f28c0bd5bbad371b404518084f872af61de2019d6eedccfa0 +50e64d6bccbdbe6c732ad35ce6bfddce5f68d763d96361a97e91f3ab96f1919f +50eed73f3d17f6a55ff70758d7d06b981d988f97dc6ba539bd8fc35fb67a48f0 +50f119b45e2d3ef82a055e0695952a16dc63e56d0c11ce1b024c2d12d4cade1d +50f22bb4c9be992bf8855f5a8ec38b2c2b1caa6d15f22981bfe41bcc2ca364fb +50fc9ca3470b56f0250e5292d23570da178bee49e91c75ac2398d6cbc23e1a23 +50fdbd644eb241f29ec23f8596ac6e498939767ef4396441f9632b1e56397364 +510aaff733845d09d8e906ebcce3bebe416c0dc4e05ab59a1f5fffe5dfd93d37 +510e10f595bc131deb52b05ffa87350bafc8a87e25c80dfd60fcfb44830403c8 +511a45a865cbd4ab674f1ed46aba2be85177ae9a88ca9418466c14081a5b2ce9 +51375ebd641ed0390652772e436be7d0cd2ebe2d7b9933248685a4fb7485ee35 +51401010b73e63590bb2d683dd2d010569a4b884f532f6e0ad8afda4d069f11c +51427d69ebc4a0973d31546ec84518f0dd1109ab689869b9abf7a137ffd26879 +51465d8c9310069efb36ee596a97f05dc86a272e7fb97636340fb99ea0f65e30 +514baa6c0f522de2998c2f0ccef4da2293a76a5193d73c5c84cb89b488f8a347 +5150f67683f99b020732eb983b79a6b2e91cc5c98b04d64a66b22272d4ef7331 +5151e63adf2e08bd6903f9a6e67fd4297f64f687efb9c98ca467b11d9221ff80 +5152e716b48511d23e1ff7bdd054f0cfea34aff49d90be175c1a42a1e844114e +515a839423dbbed01b79889d904849182aa7103e7155d33a0916237d55cb7a38 +515b27c3e2fd6f19f807fa68f4b818b89a3a27a3ab439e100d2e68a0ef9a1ccd +515ce320ba99f1b868f3a0ff5279169c5bef7ce7ca11313d081a2d7ff43aec2d +5163e242cc5eaf06e79eb3d607d17d3f514a6d5f9a25ecaf547f1d24826f957c +5168df1442c1f6441e0c0010df6449c051124d62a8119cd4511e7f1b7985894b +5171c0438c87a9f944207005055b078e6d3783961545f28bbd30499cf4e9f7ae +518a7877b4c1f772c968b9279aaacebd4985a46da80699465bbb06f738713e90 +518c83ec88293011548f25aa47b30f8fb665476733eae9740fe17a8049473b70 +51950f518aa40143660d2870df1e40f12719789f92621fbf1fe4276ca82f3dd6 +51a0b8830472a47b67d9331af7e2140b637dc9422aa5d2eb77040c032ae3859a +51a8f5fb9c21cccef48fad422d3ac84fbf9855acbbc23eb6690857f16d85c34d +51ae6c24dd81b5afec8f1583ce5bfaf17456762c1ab1bae4871f6af0e20b3d9a +51af2ce31e1b5df7265b9601b76be28b1e647088bba686782ac58c0a4376a9f0 +51b5d2db1006349cef18e7f9c9b67ada5c7b9d95e13622b708e2a90b8c503db2 +51b6ab22264b5598edf8178db30715902b8f7f73fa3075485d78d8b11822dd42 +51b86e04be91a1334f2a0a94775b30e379dc1393eb4a1e96b5b5ac8c29566675 +51c0a302bacc869068fbf69e594e15247cf28f09abfc27a89997678b2d348294 +51cb19f6534304e8c0ac82c600b95954146bd40067b96d66067c07a07dc54a45 +51d783a301233cf0fc1bdcf74b276b9f7ca5fa64339af96db555c16a66c9ae9e +51db822476c333f6575d765a34dd0d6aebab1127f38b2842d5e2b17166537cb0 +51dd171bbd38f03261e669db6cf6c54b2c9fb5879ab1ec8bac345ca8460218dc +51ebbd53c2422db150104e1a2cfb1433e4105f8da9ca09084725f34da145b84b +51f646081861cebd1ee6c86ccf106d4c980ba0123f70795ab94ecc58327c99c8 +51fbd570e6ded203609d39c406419316f866e28662e0deb94f278ad89ef0d35e +52049553ec3a027726a83f650c50f904dc60e3f1d13e03d1627010c9e13ca51b +52050036f1748d7861b29a45e5bf06c227ddc55c03d3789c256ef9b491b4aa00 +521291ef1c11e2fd5bc9ae5a3cf90b122aa4015c19bbe1757201e682a3bc4384 +5214b68e8916c594af3be673084c4d656f8d45a73d7e598b9f7db55746c200bb +5217bd83dcaad57e16598f7467413b872db10b9c022db318fb18a20ebb093867 +521eefc5d4422e973ddcc870b72721b687f4f8786e9c1a6673b4ae5fb4df1794 +5221fb37603a47febc416976dd7efe88f247c4c87584dbf0cc5ab28ea0a5666f +522286323953b0ef12ce853d37b3bead114d61246e85b09e3e57f616cff8b502 +5225e1dedbd7d54bacfcf7959ba5ba09755e5774946eb4ad202af53aea2e18f3 +5225fabc3a6487ab3a1d49a768b59b041e4bf957581c13f2beaf52d30bae988e +5227f9da500290518b5e17fef8873447769c33ab111a581702ba5bfbd890084c +522a652e1e16b933c74395ff282166c703e7b061acdb6e3584631b3bb696ecd7 +522d2816b53fcb2c1594da70f9d053e4d330765097d5aa780443c57dfa4a7e9a +5242fac048f195052bbe63ea837ff0b5d524958190c7bf0801670861e628a549 +52435f96284473c5de798a0267498f5d1a995a51759bc967a3f8e1bb3fde6ae6 +5246738da1bc23e7dfda91539321cee8050c647165ab01a9277d2c5904abde41 +5248e74d0f9326449019c9d66e7f331558a996a564462d58339052f1902a5dbd +524b45bee2a8bcbfca2e5f94061fca9b305be5114ac98b6bf1a15f756545b315 +524d8a37e08f6e67881445f37ca404ef73f28644307518559c550b9680b1e670 +5260dd34ec28b5b50ca0f3399028e358acee385abaf63b4d275df102c9ca4ee6 +5262328ebffb0e50af52a2b7d2e1dcaf6d7417d8c8da31e82a0ec8076dd06b0c +5265785e5e7a573c7f5c2705cab49ca94feff144d50519224e4f9812f6cd44f9 +5279b28d2e6584d446837992f027b5653574f5617aae16d7e503b312eb3ca544 +527b65ff207fc182b03103e6c8365192aa3d3723b217e42cb5aded9a931f6ecf +528104d687f45d33a6e06fbeac4bb89ea4e9f5d73088bf6d4f5c1e17e60f54c1 +52968819482f60d3ecf74a2fc002ccf4cc898bfd91e5a3275c3155a0fe0f98f6 +529b54f41a65c2ad4660b39576b8d282ce08897d271f06a67fc85596584aa354 +52afc9cce9d3e99828f49053a8b2831e518f6f83aa162d881a8f31a9d714bff2 +52b2bf9da1951e085f945fe5858e4d8f612919fd3bec6560c3553b4ee18a6943 +52ba24bcb35fddaa18766ff046ee950dc364d26998f4042b31910fe58b100bd5 +52bbac7f86a748a9b4efa891a494d9d0b970f97e060056f252695293d353909f +52d42e569087d77559a855f1460c01d62b3e22a523a76fa233f05c8963c72f16 +52d8a4b2be67478f63370680bd828e49c675152cf70fa89d053084585d88c0be +52f463a3ad4dc9478e7298ad19130398f01cfa109dc4b11eb6243a5ddfe3ee35 +52fbef73b4f3c9abc0052d47f28a6c634461ef184e7ca6548ce319fa7dacfccf +52fc2be2d9affd3d245d47f64cea3034fe7a54306c092f4db6b3aecfa2ec1c12 +5300747b3147f90a60cc4d7cff7f01311de737fbd0f8c722b8957c0b120d0a32 +530d4183dc0e43bb102c467abaca564883f1de0b546d11ccc9bf955d1c7cc0d9 +530e15fa1718530b00bb17a63eb0fc3dd82d491bfcb90e952ed8767b5f69ba32 +531a8e890933de8a4091672a09e1d8877fbc0cc76b5d0e4737e2ca3a2bc1de7a +531e78796d4a6095e08d1acaa7d543f8f743364d7a688da18a632b17f6c0d8a3 +5321265dd2537208bb8747cef88326bfb7fc3ece0677b262d05a34fea9ec53fb +532c3f18f72a059dc45684520ac6defbc04aab26f48fe9d956d7b2dcdedb8115 +53377577c4dc081f5a9fd1ca1393042ae66639eeeda25bc880d49ff242b0493c +533b9a0523423326cb537b225964c07f31a30a6fc845eff54d4f17b63f33f19f +533d1ed5a2060501f3787c52c0be54b6e47d7e7b7753aabacc0e713443e30f8a +535a0e8c563cd2d65cfb63b7c372265a96326548c68f3bb5991dc28b63bc2b85 +537291bd94c04e9a0b31b086f7bcdfe81d1f15d88fc53e1354444954cbe6aa92 +5383d11e3615a211aee0aeef3b0555a2f7587c74395b6e496536236b63dd31dd +538d0d050a1230db6707e70a54b3f0a7f26d937779c8660580510f8ccf7f3ad5 +5394b5ee0c8b2fc1689da58a61db1d246de46ffa9ec642749091248c2acacec0 +53a4d5058fccaae514275845bb108569efe0cf0c52f7d3bdc10e6af6eb67d852 +53aa1029e7f41a66edf7328bc7f8a3a98c167a616bbfab71dd8fc291908908cb +53b4c1536c14a2807f1abb809d836ead60fd629aa0f3f3028e5e9b2d9628b6b5 +53ba97b06b29e5af8dbf01779ab20df0ce3c7d7f9defc9d1469d0244f7b86559 +53c363a2a4638a86ae3abf5938acf5bdea60e345568cfe88bb75f5c0a87e58ac +53cba20466e9e3d719e3abab13fd28d0a1fd1b6d91b1c14a9aebd7abf1eafbe3 +53cd227a92b1e0fefa2e5eeac548fc58c80c34e844b83f60531dc356c7984747 +53d12277d477b69ff1724da158c77e6e3658677904eca2e7b89928bbf30c11a8 +53fc00d14e8c3a0fa45d1b137ad16aeafc6d045ca4f4679be4adb28f4bec0584 +54017d99fdb8e1041e35bc71070501a7173f930bccba4f545994b3906ba59c70 +540b162184b75f720db02ae3eedd4736dbfa1bb6ec7db4a8555e2b7b772f69b5 +540ea0a3c72308995cc79031998e680887abae08ae823d6c687d58be2da7f8a8 +540f101ac5bd9fa93e3c897c5f3ed627de018e43e4145443a830e2e653a643fd +54144604de5814635b8c39902fd24bfc6a03d0373c7bcdcc6fa5a5bca0e51a6d +5416f82686d3036b113d759397731436ee8e2140bee26b6ab725ce8228cbcc7d +541793648e798f1b4d9f43734c5d090ead6447006f868173bee7eee34207aede +542093044e2e8ed2255b1aaa5b3fc9e8489113673117299604ea273d00455f7f +54242edb7c036449938d43629947b4a752e565318bccebf0f8ec4ee790bc389e +543667adc4b76135aa67b6fcb48ece0f34671ae6ce185f91109cb5916541be96 +543d5dd8e65d998166ae357377fdc3f8a7b0922a09b7d3d34b67147965494180 +54429006a8aa91ab4ae12a49298f629366c557798a4a9743540fc451718cb6be +545020c7ccbf1a3438c874a705a1e86374779f845c2ed640726f1e5985c83801 +54505bca67a3b34d279df560f2a15e840fbdb9239cb161952fb7e2e2d2c8ab1f +54507f0b29f0d616772eda42b05b0e2783f83828a84eef14f6c1e0c2f2c2a8e6 +54543c9c2afbba0c3ad4b30f78b365abbe5a641b79f840e0d010540d798aabca +545641231136def75e79e9a7688faaed31c2f01d7adb81a162c71d079560d830 +545729b88c1e19c88caf482b92024385e3877ef7aaae622869687122d6db6b21 +546e7f434e7967ec14c5635c7b5606b5da3bdf4c4976b98f4851006844cf895f +546ff64463585d171bf9e2dbda6121193c3f79ae9d9fba1dfcc43cc829ae7afa +54728a8b4ca8981cc9f36d854b32d6110f4efc209f3401631801e97e29a8e8c1 +5475ca79f8a06f0ac74654d2adee0943ca58e792c122e8f1d72016a6e2d6ff6a +548457c5f7720a0195496039eea67cc85bb250e31d9e131e1b3625726e9fb666 +548dd53fded681422f5bde9b6e126fea72ed47790ee962034287cbf2c05775af +548dfd48247eadccc6cb473f84c7d03ce700f08478c30504a083c96184682b4c +54a3b0db9cc64470b9a8eb30a32b6c8c96c86669de67ca3bc00bacc87b348f55 +54aae8032ca447ea81ed961764c67c63312bd00da834c40af39eaa8b0046b38b +54ae0f3694f9ee7a19f2ffce0c281d8b4ec0a189b71c4b2f08956bb74866f079 +54afdb77f9524b2b459ee2e7665cf85cbe218fc44c6af805222c19905ac53373 +54b74aab26ca14f89cfc96518a240f7be93a238858f710068f6c4b613bd4a0a2 +54bc65cf8ca59eca1f998fc0b0dba8a8c6fdecc510f185c5152bd0a3a602dafc +54beb5b3a0163f68dd0e5c735f008ff43c7c8c3ff44fa6a4d190c9a0aba77dcb +54c0e6fd9ae37974acc13af0f8933590383af87aadf9bf97279eed7795f49558 +54c14aa3c50b3f8870753e937b4ffc00d2baacf0a964be089383f2b64eaa4d98 +54c441e37dba43a7bc8c051c3128fd82310539cefd5f45d19abd9db2d3091e6d +54c7c3eb632cff16851b6ede0173bf4d006bb4d637ec732b25250a4d80a7c589 +54d2a980f54c77407fc220b3ae421eaaf2fc8e55a7407b15916529c8ed0d55e7 +54da3f0b844b0ce69c9f4f804b475ce61b862dc76cf257db413f5f2006178c2b +54de965ec9877f09717619cff6e887f85a76cc648a50e7adf364643e8742bb39 +54e33d30f759fd6518a0fd42af9cdf8961e2a0609920089fb2af8a02ee8c690f +54ebee04a913e7d9d1e731a3dad4903517bdec5e586501c4ab7cbb2374a06d71 +54ed72aa382f1e0859c33c31a96e656959a8ebc444badb0815fb2dbc7b2a9f77 +54ff8cf08f4cd26e6de71bea728419dfbc92bf3ba81bfc17f190163c20cc238f +551444030d364e1d2a64d1c6854caa5f62f683f63bde8f6279e40307eac7f010 +551e83421151db4672138239c2b0930224f64981f77de7081ad90e679bd612f1 +5520be3e5ce39a1b3bc8a29403563edcb707e049c7a92f7ab9865bc94fe51ddb +552eb7b0e1c740e38678bc5e90221debc669cbf6e39c0e0b35e78611eb0b8362 +553264c6b7a4393a96a1eb768ea70bc0e23efb4dec87fc768ed703941494a763 +55547e8ea4bc0a2424a661a7ac9aa6048eacb189fcd31af74485ba9db1e9b402 +5554dec89566d478fa4c7a4f4fdc852337a71088540791b35499c8b32228d92c +5566c26ff9b45f276e5003fcc4b2f448be35993f73c0377d2df07da294660c6f +556dc1bd99dddd552fbe828d719e128e839bf55823aa1d52b0bc5ba7c028d845 +55702a56b53670d10d248baab3518211bce633ff4ab1c48cfe54b08b23792053 +55778eb72c51f1b226e31b9cf485b512fe01164d609e1447d0d053588ec009ef +5580a676f7ceb6042398d030754025910bc4308ce46026f3fec11013d08fcd70 +55851a0906d31bd77c30180434c8a942c6e8ee56a16b76a66f5137d8e233f77e +55916854e256b14c5389fb25abc2c15dc33c40ffe5826334cb78e06f2e21daf5 +559629e03d980925803d35a2f409c3afb45d2f05c75820ee2d3767ded8102037 +559c732f5b6b6a151dfc6500069f83f18f1484df05cbc7f82914dfa0d2180991 +55a037b1d440fee919bc893ca58d76ad19f2cd4d2e57e34604dbbf88612d871c +55a1e35ba91908d2868aeac1e3d80c257fce3e513ea4e65bbedea8cc60d1e8b1 +55a4057b181f54c2f83ae0f2e9f2d5ecb9bb5fc967f58d368bf55475de3c417c +55a66bc4b672bb9ec4e09ef14c3b9035ca7a2f8ef306de249dcf897bd06f5624 +55a983a2eecb711cdd48c8ef41c5e88bace4366fcf064301a586d01d4aa61701 +55b9055156a04865f4e426a7abcc3c597c8c712223da65c625e5be1390874472 +55c4be13eb72aa52e72abed19adf06c01c4ebe6cd0ce0a3142833f7d5ed23f57 +55c9ab1661a6f58a5e0e973cba1b3307466ba418266e01738976f70bac1cf283 +55d452778061ea689ae0f2a73415d587b6c0c482475ad0064eada3f9483eab92 +55faff6eaf5001d994ba44829961c596e556cb7620e1dcbe7ef89a63ef6d4788 +560a173b866f7f62af2b2e8667be869714438d7045aefe80a8da5feb5301f838 +560bdc87251de5f9586a6a85bc170149a7065ab8e6a77e24bfa958bf1d3b97fb +560d3346427582414c8c7eab7904d6fde3d365f740ab964f5d674ee1839d762c +56175b002fbda20c3ebb3f4dd09e567dc6eed0367c4a3809f537696304dbbcf1 +56188380dff48c54388d0759bc928591ff8aeeb23133019933337a4371dfcc96 +562036e80f853626c20e1f1e68bb3710742c9d2ba5d3d5e1641e321cceaaaffc +562ffc5d906487ff180da9d3ba99a112b1b4cbc57268bff6c7df1ca083517471 +563b8e35ae307f55a4d4cbf146dde522f59acefcfcd1de91ef0069347ec2e382 +564ad3412f05e114ecdd4fd1809aa285e1077abeb21d87d2c4ce51af18c20724 +5653af5996003b1cbbba24e68da5b9ae7366400824f9d115bec4ad2ddf21db26 +565a8d396d284218b646ec89754ead145ad98600b920daa6c75acd983a502a63 +565dde9f4e4c286a8770e6f3b0efcaf6351c45fdb7b0cd02068060e834fc300a +567ce94894479b7b4e603dd7a73c6cfb9a72fbd74b7336b2bbc429bf2d3417df +5686c68d5ada8694a455c65707cd050e9a77f558e226c3546e0c5ebf3a381743 +568bc8f1a3e559d1ea66cb6bd88a3cadb56b233fc034a289432d7fc058a1712c +56957bfc3168520b2eb810889df518e57070bd6c00e45785913fecd223fffb6f +56977bd9a60f3b813baca7d4ad6c9b1bc72a6417926147807e51ca1503306fb8 +569bb870adffdd5e2bf9d2b39bbf679cc28bb8cc501f9b12ccb55ee1daeda9bf +56a637579ecce26805d52342f474606b6c1d56070a835079e5cd85d5376a72dd +56a6fa0b4230610e142d583ee514feae141422a7ede6313e940b26a1b2b3aac7 +56b26dd9d660e4d578a076dba14aa159fe314ac8bc3ba40037bf3a9a9eecd809 +56b90feda3936617bace9252ef4b04d31adb299921ec76775d1b6f96deb40d0e +56cb3713a9ac7d1c4cb43701f412410dfdc14029695f9559cdea4c01d484137f +56cbbcf6eda77e9e6acad8e96a6821449e8832de28b535550fa40d00aa3d6ba3 +56d337a3f300b509651196285fca0a38bc98963e7dd5790af3d322fbf4346663 +56d46cedd080845cbfb160a53fe74e92468906f99877a2019d619469ce1f104a +56d75ac1d992f110dbddf6e7ea5871eae2a4b3038e38cff5687edcb929ff7ac8 +56e01707851cebf8f3a287d5377c7864a85c1a5c5dab315919f6411d5ff9f2f2 +56eadfcc368f9b8c70408aa9d7c89afcac8caa810b2a11647c8a3f9a8e436ad8 +56efb3e00a58165e81328dfd2c0c2c50c67c03da22fac3cb06864aef96335d59 +56f94c0377ffb30a1783feac6fa7be5cca609cb113349fd27b45c9496ed861cd +56f9dfe8b325e73e9a069271a3039ac0a5868c66f3a95934d59fe934bb146e8b +570269b7d7ca7384a922093e20f8cb445a4f12419121f109f0775cbeba17ffa1 +5704e2033bd9df946c0299b0acdf2f79b711443139925cda334442f63c44e176 +570be1ba50639a51c2808ba9fb76bc7dd471267d377f80fd67ff7c9a2b691481 +570edbb005268265796df8aa73feba453104a0bfc04d5eb1fdf7ac2d4ee16f21 +571106555ddbafb1d18d42daf79f5fba6a6898eaf41789b195c32933ed8c95a7 +571a5e1487ca778dbcb4be0ecb439cbada432ed34e4cfe34e2664c56d3bd1175 +571acb93556b179ee98d2aea72b52775e947b8285b846a3f6add3906cd6c579d +571cbc6a1fdb82278ca9343e02f0ab1edbcf52e1b085d8513cf26e39f701941a +571d52a8f9996ed1a7a119d9acc6579d42d1b2919bff3237bd800dc03c8411d0 +5726d15aa1108540aadb58cfc1e125d50710db773f6e6f080b41c6f417d81fdc +57335d4024c9171eae830268eef474b0838aecc7922ff67a87549f3aa5278584 +573ed32c0150d96153c44bb7223a8dc9092793bb16b23cf75f82343e2802ff25 +573f8d04d3b6f50b88ad8bbe58e6973d8b35a3bfd5e7525b5d78a8feb86a91ed +5741940a8666c34e07d4b62e903ec7a15c2cbe8690c9bcc6ba06389622873be1 +57558ed38b191d075ed20d1295a941560aaf65e5e693a0186d69e42d77fb7027 +575d7046ee44537aca217d03ee37afa641991959d340e55ddeabbd54e23b320c +576aa78c6c0c35c6a446a05dd1855f8c8068e47db04ff22511da47d98686c7ef +576eb833973c3673ed7395965bf1c6dc5eee0df4fd6d47abc31601de41e2d1fd +5775d18f59fce6a6285eace4fbf09d1dab8e75a4dc51daa844fc36f0835cac0e +577ddaaa14578fd761584e04fcb047589f9af769587b8272988080d53e5db3a7 +577e0d64701b79c53386887a8d09883a64f1fba00ab8e641473ef45df37f540a +578baafd958259c85c4961eb12e11fa40aefccd26fd2094cdc1b0ef54873aa34 +5799c790fbd287cb5b2198780455007dec2582a35b11c52ee6d2a83039ab39ab +579df21ed6df8426b248506c0f605c7572f9361a4a73e5583642fab813767243 +57b6d3b961d5764be9b63ad9990c33029e0863697e7d1ac0235411f25d6cd3e6 +57b95faa8f298793317296960ec5421d37db94a756428a87cf79dd49ff1a142f +57bbd14dba31aceccdfcb017676d3fd564645743e43e6f871539dd12b8d14c76 +57e0b3cfa2c0ca610eea35e6682f3a6758fd336f687e3f29cd44feb11d3174f4 +57e151a1b3e22da615c392f7c0a2921472150988729a210ca0906bdc68ce212b +57e33109971f27914cff60097070c1b1da72beb555265b76764dcca2a408ca03 +57e6a0b1a4ffda2376cc2dc07d3fa49d2f3bee82c8374a6bfd0a19104172958c +57ec48cbc95e7d085e4fbee06189e7e8aebbb468de54993123d0f09d3f623ec3 +57fbadb1199efc6551b921d7a820147fbc97721a8d0fc0ae9617bfb2d2b8d3f2 +57fc1a35e2581a51cac637a57d82742499170a43a33fd24e3731f2bc1b7369ae +5801f6d2df47b1424ca9e0ef7c392c51db1c534dc6231189f4d95a8a74c00ce6 +5808a5268da7f77db97581a255e603839b354f0c78437dbf6c696522f39d69eb +580aa9be1f61de7b0e220f629a84397ed041997ef428086ebf148899764df580 +580b69a0a5bc226448abb1002b8c1b1529b1af677796dbedf4f637926278cc40 +580bd53bc208ff26f56a7343f904546a203a4b0bedfbf14e311809325278b239 +580bfbfd776546ce0977d8f205b83ce045d4f20c80155c408e25ce217a4e0c3c +580f77f3ddd1f786ccad17ca109bcd251e7ae52fdca370ba4d033cd24eccb2be +5847d8285e48ddfb854fb800f1a7be8d8df2bda9b7a3653219505fe430e2d230 +5847f53416e92694d92ac1301d1b0ffece9be522f675b8542cb512b71b522f42 +5856ea5a9effe2389d71afb29199b0c68dcff2e7ddc43e53cbd076250aba2cb5 +58583808fcda42aea8d85284ee5480b64503d9c91c45478615044befbee88711 +5858d66666fbdb51aee00628de6d3b4fd3482c17ad27032b843a31c8ef749537 +5859b21c57647424c0dfbf39c585302a42e57f09dc93c284209455711d529ce2 +5861c3daab9b17289dba96ae360728c22699b44f8ebcebe8076dbe8057a0debb +58620c7418cad3e2807696557fa3b89a5c1e402d554b28617f4bc7d0fa638c1b +5868a4f4c06af9a284478f2524a953ffa3db9d8c100b2a769b87b8b7a25deed4 +586cc702d87fa765d00673eee2b616196809167d8d3616449ae1a7abf30843e2 +586ef42b002eef460ff5ef3f24bdcb81b6af66be2a846f96b82b73b1151b29d0 +586f100db372d26c3dbf621f2cfc739903de1b466b684ede859a73f8d98e22e9 +587d295c7b1ebca94e0d736c309e4faed96cf582718116c8e5d7848120eb6f30 +588e84bd5825c603147ba1dbc60e7095585b861bf6b85e86172b5b0953cc8318 +5891e19d887891066b1e955d96c40abd661a9e4ee91aef841428990dbe38cad3 +5896cc54a1abe10abc961446f5c144b6025d34e704295a8173495855d4897a43 +5899466eab1ffba672d04654f062bfa385ed497a078fb104e1c1ed5a2e76f303 +589f95bc36c322b1e5588ad19d053b02f841dc80885fde126a9ee95e783f1670 +58aa5368aee8fa5f162f5999926ee2601b117f2d49626f163a18a51a74ad1888 +58aaee5dec65017894d0136afedf40cd43eab6731a830642981b77f54dca2c6f +58b17ec8fb1e851689fd91f0280ebb4d7f77a6a84b19c92e1bd81b3d58cc1b08 +58b2f5d2f05ee5628e9dfafb937602686c557d88249a6010c61f90bbdc4bf638 +58c5878877c9b9e4a9520eb8678841370b013412ecbd97692b6dd0c0955a568e +58c718a5aa97ce2b63108b8ea12c2e20f073489ad699e4cb0b912c84f249a8d0 +58cd6f2b6a9934144378688449e508c29c87641cf7d4a108bf3b02b8681f2a5c +58d2988725a1f5ebeeef40841609f9c56baa0782d57517848c21fb2fd8d15d2d +58dc45f337b74e346ead5ae70f529af066051869502a1f4da917e8768a348a2d +58e231b3ba9e22876aa56fcabc23e5f252779f679a38c4c47f0fb62f2b2adad8 +58e5104df14d0cd4466ab7a8823e1920d5fa191c016e0e27a92353313c7aeec7 +58f79391b5b5722c62b876841206263217436690618a61870736c55f69910105 +58faec27e4065b1f6b1e56ff82a966377f047190318be92af82d660de1780ebb +58fc09fa7c96e80ec3060e22d6d24a8f42572e18c64c58ddea28fd16db7492c4 +590796d6d4144b51cc0ac3ce2b7ddc88c16fefc7177cf2145a793cacb26b8801 +590c250bb5e4e587a67bc5bdc06fd5b07b3f72e1948ac689a771e2c8c4b10ecf +590ece10d493b34db52aa82a69d8e8606faaa7607ce3a5e1bb5b463a88758f51 +591220c3ffc2815759e1913950420ddc6bf3b2360d6b9878beb1add270c734ca +591585422201016cbe04d337128f07fc931ec0724e04f04c9de5372dc8b4e541 +59264be040a699f109c0a535ee3427d0acbde43c986bd62401287a680e079fae +59297625273b637be01253b3739a3b192497d007415837f49b18c549c7c509cf +593635d5baf25c8a493ee4fac6a8e2a6d6761c894f3e7d256abb433111faadac +5944f1c881bbef1a38beb8737b37cbf0c4aaea8db46c87fb019253b2df953690 +594893a64229fd71b8233c1e8d8b8accadc2461ee9abf929ad7a1fac37fa5160 +594a14d11ef6c5df8a4f0930b351654841f7a2503d211ce5e77dc4665ba8faf1 +594ef4751274b077ad995e10f2ffa2aeeef75dd3af83a86b2fa2362933464586 +59502893665c22f4adfd14b3c375ec024f02acf5d75fb6101d976f87b8ba3001 +5957ee0e9be1fa10ac6241d64371e0d960f59e0b76994ddf804c5727d3b2dc29 +595ae1c0a7922a301291abe9824aafdb934d35baec2200415e02f0d6270b0e9b +595c025aec56209ca2fb34520bbef56827074afc0f7090f3071fd65a5327259a +595ef29c342710e9f05a97388ec7835e0ced389404d7765121dcc8dbd9675c4a +5966ebc3b9348b953e0300c719cded656ca7cc9d4588bb8a225165ba11062f49 +59693670329d5a6329638c4d798a731370ae2c796eea19603dfb78c2cbdb0b86 +597969392988204ae1567c328d30806e7260d1e5e418b81e6eb0df262ba029da +5982cf5e9aa1c1fffafd1197b070570c04090030df98bf8ab97a919d42cbd82c +5989c8a729c7f18377790390b7d2fe0e5bd4745bd479323405eec2a7c48b9502 +5992530b3ebba6b7dad96ebb8c590a557188c4109c8c09072c1454156daaab27 +59926e9cffdb35ccced10eb7d17d7049a6ce57db5d8c9e597d019b59060c597d +59936f311ac08bc6850c6b3a384cb363827d6db862a0602d57cad42718a836bc +599fcf99d3f17795313cb1947fa1de18372af7c49d16a6270c97d759b49cca14 +59a14b7d85e0fd412fe09bc1b21e7cc59cdbf7063ded1556a2365857f280fda9 +59b49acb2297021fde1855a60aacfb09ba3b883cac662ddaed1602b8edad385b +59c2a043b396f189eed4388f40e70c4f48fa53eb0b8d978f44e54a2a0f5255ef +59caa825d96dcbb3c04109a433fa57bbad325c2ef1376ac5ed444bb0eccd20e0 +59d32e343b6670fc001e1c36002c0536257c2f296ec1f4b7ee359ae5125cd5c6 +59dd38577efbf8a1130e58e81d57245935e0bbfbf755517f3055fbb0e706a69b +59e32abfc0b470f9334bd1f4fae92e503251f42dcfcaf70a6272426b88e9dcbb +59e5ea8275b470609a18feb49decc136de01a321ec5592ea65fc8c094a328f24 +59e692da28646aa1b1cdbaeb11b6bc714f892ffcf2181760814dfbb1b61a7f3b +59f4ed0b7d505c65f889dee8d3bfc575168f4dc1a1fc8a1412f95835228c38fd +59fa823c998916ad02595bb3526128f94130e7b14357c1c8ec8889008c46b1cb +59ffd1332664631f852966ff8e7369d32cb31aabb5e7e8fdf0149226c1f8f1ad +5a1ab6d7473e5ff2d70045ec3d1a5488dc7fd19150dd2ff56f0b489318553c8f +5a1ddc4ea90773fd8daad8af3db501b2709ea1342f902f25c9a0a7c2ebd53028 +5a2b94fc649de3a7d41ccf69096f3bf0435de34f4a1c571c3f1ba60ecf35f71b +5a3031635a41fc488c2ca90530acb51f79aa95c73d5c47c78f4a1af80800729f +5a3416ff9e987fce76248306a1c7a05f916386756ced824b1be0c82c122a8a1d +5a345a5a4017bd473e9f7b2539374bf7fb7f039f00228730d4f62c8fc7843f57 +5a42a14443dc476bbf7f5be0eb9b96dd6fe8579424405b71d19987b42ec37972 +5a5073dfd7ac34f27077c3a06f1d7fa85a8aed80867dee2c9cadf341d3dcfe74 +5a5b9eec02526ec0b77bd3e5ae8a18c127f3637c40e8bc6ded26a7e3fdda553b +5a69b3457efa6401c8f1ace74d34003bdd0fef53a494d019bf9a7a2ffe936f20 +5a73da5fe9b7195dd9bfef48291e89ac22fd73c2d5883d672613af48cd88621b +5a8879954826bcf30daac4b8a0bfbc602475b9b9017153acec45a5ff4de796ba +5a957463bf4f69cff7de1b96a1c34de90f0a0397c58e64e69cac84c95ff6208f +5a9a6c94e531df76a9a4123548a2b0affe3a294a916a02070fa18d401052a1b5 +5aa455d6fb5a8cca5e58522088310ecb67c654fbdd663cd6305c6e877079d0ec +5aa4c46daaccbc44d2f9e11a2d42478e79f6dc47044da9ed88bd222a561f60b4 +5aab081d9c5f9ae996ab61e2d9c6db5d9b57658979534b6c031f4aa5ac72d349 +5ab71a938edb5f4ef7f978528f9ba088bba835125759053f555bc1164e78579b +5ac49ac4abfdf88dc18fafc6623e123658a5c65222172f4f60b3899e48c2fa4e +5ac6301f2bffcf50627f25851a7d8d7ae01d030afd391f68e9f65d3a0d5c8500 +5acbb601743cc930bf493f4131f3f54926f12befffdd11353f9a0303d78e6d85 +5ad39790084ff40aed9259d2f5973da7a7d7caef3f361f67ade54e527120641d +5ad802307e5ddf2c531a129edb6f5f32d74d9e90776ca4ce27314277a735c746 +5adb35af00c86fb3b1b0c42a62f4c55a031f1f223a1bf99233ec121338e02a85 +5ae080452c57218fb33d2f06b4e4aa47c4dd9bb53d04c0d2c0a73ab533f18e10 +5af801991724fa2705347104bafe59176a898df965afcd56602b6edb328593aa +5b01ff507bedec977c474ed31953e90fd82a88f77fbceef188f989c100271d69 +5b0b29083e083d5f92adde9bdf6297b7910434fccf71bc784522fb33f5ebccae +5b0d57d45d19ceedcecb5c55bc42b30001ce804dafc17a75aa37e8daec58609f +5b0d6d6262fa0478095f58cd8fc598a5fa4d99a6f6835e482e96db933efc2b16 +5b11aef17b9aebee07a9367d1e02c37ae04da2c861716b37a794648a6a237c38 +5b14f065103c3f3653d7404b00ba521048acefe1b5acaadcb9dc3052c57b556b +5b167654fb1679d82db6296f8502deedc79595cf5cfe5d17adb6f018b6a9d9ea +5b27a899c5ed10ca4a27ee6104c16068c281b41acf2775b2fc4df31bc044961f +5b2a949a1df4eaf826a6d21e61701e8a90a3ec345b954536335fe5d51ff05b46 +5b33ed29d9a4b7b354c365bfa59ad20682f29709e075e1a4483f43e9473bc7d0 +5b3abccbee47a33d453ce2beec7e8c1ca5b9bc950336cb75ef42951443e99b46 +5b3b31fe6db5e2d0d8778ae4d76b1a858a5a1c1ee9c3a81ebf7597ab8d3631a0 +5b3e694e26a46df94f79ef07c639dcec0be82ca770bc1c7f65a0b91a71819ba1 +5b4238e306c8d95a829cabe9375004605b788891017ccf437a042c3e93ff7bd0 +5b574b247ddfb7a2d83409315d0a4bd9520c0e35b96a700a98ec020b2e15f290 +5b593f4634f1de9fb00e2752d589a3d6497c90544fe564094b8a1a3a81635a59 +5b5a62cb05dbac1a2a127289a9930fc394cfcdd65627a03e70bf9a6fd2999bb0 +5b62a9f91fdaac8ba3ec76b9f3c21ff3d50d62f09a23b62ff333059932e5acf3 +5b77dfcdc1e26b7b64cbf8bd9e7667de9d724930fff3d8f247e9f3814cccd062 +5b7e7ab191f5362d76dba6a275780f033fe7dbbb9e4ca08cf240ebdcf3283f45 +5b7f1ca6da259b57572c9b9040b5952b72c858bda22f3bd0bbc5042edcf41392 +5b94f2be354b1b39bc68c91ea72a3dc910c1b5ab53138bb77e432df62fd7e782 +5bb757b9c26de06b27be480f26f7913a1ba88bdc0676abafe2f6d76c6d0fd0e2 +5bc6c2516e0733a7e6240a6f4b6c3a20c119dda5a64726d4a3d206bf86c68a37 +5bc9ef2b3d6411113c6065e74e4506c7ae7a9c2a89df2cc5e186c5d5fce0d0d6 +5bcdbac1120f964c2559501f5977fc9d927af6f0420a55f170ff99a75c7d217c +5bd015efcb0222547eb4a24e3d508bff5f733a451c6ec25a2325b4a3354939e4 +5bdaa6afc10eb40cf99e7fa379bef435f231fcecac7145565e21b4d0f1d81f2b +5bdc6443410eff135bff97c920e34d05bf2f32b9a9cc9316d34bf03e68da07fd +5be3dd3ab60b378125a9370331b63e9b8b2069f5a3661ad4f9d4230a2689a038 +5be7e93bf321cb66d2a8ff6cec2fecf0e5a041808b55d22b2730b76fa6fc15ce +5bf20315e9e710b3bf2e179f34f00e5ca49e2a44186faa3a529826902de17736 +5bf84456113000e2c398e84fac7b402f009c8a70cf7d517d819abcce4892b7ae +5c020caa5ca3e59b1424b687fbc912aab6ac2755545f8b314748e7cbee8b0863 +5c026fd11c32d31385fa82858035f492538a40aed7da70ce41d08130b35f98fb +5c0f7fc3aa3f1f1298b73dc7f251f9af80c1692e07047cc6cc8c7076b8d41086 +5c1f7a2b58ac3e18f50d252dbd35688bd32d33ac0cc6a3549a5e97a4751e6eb4 +5c24cc60bb209bd963778bc09bb6af6ce8505b4fa994163283b4046e4d7becb6 +5c2da7fa9b38ee464bd102499bfe48f7bf8fed0d77678269d4829705117818d9 +5c2e237077d63c3098cc0e2e0e8355a0a3699b0d3287e7a5a774021da452ebd7 +5c347158497075a3e0821860c9e4919109a9c0936bd350c5fdc696e449337e82 +5c441fb942ded260bdf07dce5c4d231c0c5444d2218c5d6e3b044127116cdd2b +5c4744b21a7fb59142f89a479771498aef1991536c3a50714f882e903883f4c5 +5c543e96efab2054f91286a1d467be18d35b2fa52228293441e5e1fbc68d1b6f +5c5f8f65f46475fdbe5d6fb3c28ba610a4976d2d6828b6ea57f0806640d8c276 +5c7d0b19d109a6411657af62253b781b8be7bedf211aa2b527de1f8c7eff48df +5c823618bdce0425a634db726fe161fa00bc56c7ba5141142a13985d187fd4ed +5c8f4edeb2e29bda483dad1a48bbb1e86d1e8076d3907d8a04a41b3c5d9ad10f +5c900257cd069cbeb56c25ed125844074b9f8053a250608880cc06df7ff21d64 +5c9cc44e65a976b8bcc43f1b65905d7b274a1bb13818de43832133523d5ad545 +5ca4ebeac8395fecf2111d37c8a2ee9397b803d590cf2742afd337ac856e1caf +5cb3648aba7f5513eb9e62b744920c17a3f1bf63261075c150e0ea41dbeef5cc +5cb81d676c2e45d5d075b0fc1b93756ef285531821f73ed8227910ac78d7be4a +5cca541162557c363387e9a59eaea04bc3ecc7d5eef0d56c5d7b0bd59a2c6ea3 +5ccb665d84fe49c98d994162b5e015d0047a722b34b3437d214f3112ee494962 +5cd6a376fdf51b20cf30df8ce9bc7d1bf93f0764a3f7bc2f7f5e9052d0fde99b +5cd79cc72e91cb15745658f99cb013d0ede8ef9744cb9b9e55c22615bb6b90fb +5cfd2f0f0ca24f996cf1928ecabe1f22d088dfe42784970e372093c75075aaa1 +5cfecf9d1163ba03106f5079407b675fdd0dfe6419451c32132ce586fa820988 +5d02181ad127acd57ef7daca5a2921801b90f331fc29c93d700cd4e3370a757d +5d07dbba4cdff6144d5eaae44214f3babda8b9779393c39a24cb329a51e6b37e +5d0824d7ea9c183891c6f6293ecc0d16ff49d2ade210b009050161e11a935fad +5d0a395b4c39d17126acb8477455f59e09eec5e5b5cc2a2f65a9c08eab9476f1 +5d1d4ba074339496e8f531535bc8894266bf2e17da5f8c3dc5d43897bbecb0bb +5d223df5a6d6f8f0517e20af78619d52d7c7ca427801c5d658db34ffa16bc0de +5d23cc4dccba3367869f812bae363de81d95dc716dc3dec51a404e024c956656 +5d2a3080a9b02e5341473041e69d53e87da29168572176a5b375c74619f48693 +5d2cba6abbd01b9fff6aa013462c7f429919b87e331f0136e0a2020da8a7ab1f +5d340ada40da150de9d1eecbf5c373dadb14212bc2550c5559a717025adf55ae +5d39de44d44a79cd2374a21f46d21f117500085ce464d321a391c9567c742807 +5d40d455a217a1a80a79bae6473ceeea02e7bd84c9c28701323f40339c6884e5 +5d41e451c374985675af83bee885b8757b1d4f83a834d2b12e3552a18516ab2b +5d59be2645a2ca3d35145fd64420067dfcb693d7349232ea68d4c95fd25c8586 +5d5da776f173ca7e626b915467224eafdf9dd1a962a5049979ca8bf66ac1f3aa +5d60d89904ce7a27602f86e701b080652396bc7cb2f6aa12fcbdfd7a0bd7eba2 +5d66acf71c77654450866af0a14bcb90bdd365df5664e200c3cea84c6ed0ebe6 +5d6715377d239eb459f0b1f30ececa93e368ec85c17c7279927b225af6d8749e +5d69d8054b4932095211fe50f6a966b990892bd228fe005897718356f67e35d2 +5d6e63d27991c6554198ae97187ad04efe51f941428a244edf821487101258f2 +5d72817a2ec58aee51f4cc34c95fb3687b4acd7cd8f218d89f6ad3bb4ffcffe3 +5d7c4509708534ea4c15ec0c7b13a1d679ca7893bc6f26bb8256d3b13efdc682 +5d7d8b5556061781eea13d7b9b92ffe53af2a35675e1276a46b1d8382cabca5d +5d80cec9d96e2a5f84008f21c2e8181a3b89f38f978ec6faae64c861ff1fa3d6 +5d8e4e701475f7c6983c55393a205746f9b6b2bce51d2713a427113c2ee2a031 +5d8f9337e792ef4682ec31ed7ffdad0246a9bc972bdc09557684cbb9dab313bc +5dab7e4184626bbd1bb72165a3b6f10b5c3d9dd67c3b083abdc29c478c5cbecc +5dc935c77f581e7817fbf25a14a8a314733e10ad322ccb427b12d33513ce7840 +5dcedf2b700c33d38b091ac7e1b2b5de9cf26742aabe5dbf9bae923ddfa24e1b +5dd385f594080846fd67c903009f2d978f332e1dd3be3053faed9ad6277d223a +5dd62104f054f83572c96644b44d5f4096ebb68c3c89550e39ce509ca30fcb29 +5dd7ce8f663eaa3f7927074230228ac553d077f71c62ea359dbb44c416656583 +5de42bcaaacf7d548bc38833ecb4d9996e8d8e3e1abf5ed4c93fd3f914a46552 +5de89c2b4830bd5513935d8fb9ba7354f10697aa8ecefdf5eaf81a66b3b27036 +5dec56940995b64f5eee9da9f9e18c659b55ae3baa88dcc055286e81fcedc20c +5df3e2a04fa1be7747418e4841bb06d890840f1b111ce2c7c56ac2db7f9e5e71 +5df62a236129a79ea75624d85f06929b1d4d59ffb62587d4ae1aeece147e81ba +5df9bafb9a2f02a84d37acbb5344d77d156bc5a25de8f90faeae1061f1a82b16 +5e0369ce160ce15e88db473df7f4cd7a1cd2804491b72036f429e25a65356385 +5e03c41e165f61366cb3a252f8aba8c694ec9651d7b39c75e04711ff1fe44d0d +5e09a1aaae3a08686a5009f349c656904ed476cf3e91e28b563f1b27d5a80039 +5e25a836af74db188f6ba6026629ce4c9667fe304398cce514fca30443f259c1 +5e3b54eb564b5e579c48835cced9d7f28eba161da73283757895931dee5e0d04 +5e3bc826bcbe50bb22f9b30d29795bb35502e30cd4a9e85555cf6f35240fb8d6 +5e3e7370a91b059b14308790c436814d3f2ac7ba26448ba8b096cdf6588a840d +5e453115224eac9fcb90782d8cee0dfeff89361eb614b725af90efb7f93d55f3 +5e482724ca8fa809e5033a8103641b28a28de9ec72bcc7e2dccb54895928a93f +5e5c1bdac5e2d74737fee7f2f59ae374c73c4bba7e799bd8d25dfab9e8f973dc +5e659ff260d039ca4c5d077362b9f7a886b5b506dd810bc2914199fee56057b1 +5e66b1cc9ea47a4988968e37706eeeb1dbfd132a6c102593337cacffafde102a +5e6e39caa9bca6e708414e9b59131adb8bb35099910097783961ffc84b39fc13 +5e74ce990c1599562c64902e82824e330f2489ba196025fc0a722845c99fda57 +5e7e8ab6c1305c5abc0ecd794c37b4d94ac33675dc4afe258a83d32dab6eb2ee +5e96e8bd2d9a1116bd31051400071126e03a67341a72b3133abe181e3b4f7d4f +5e9703663ec9191513e747eef2803b7ea54c281f107444db8687546cc5aecdf1 +5ea141cc5968343e55753b12e28f86d7e42e5218d0478117e684161f575bc6ba +5ea328db18e40759bbc2d36a6c36c8c0cc14642283ece5a14c6f7102f9b35ee7 +5ea72a9c56defa1b22c33568ac30105cb899d749a6d82fb579e7ff73f4a5cae1 +5ec05671cec9a478b8857e76d380ae20e23ae7d74bb32f347181494be15cfa5b +5ec310c20fe68e9f66726c0c1a73c56293c570f12cd086585f0f2acdd00aa2ff +5ec7330561524926a0fa41bdebe1684280afe5fdb4246b8f4c1944e5a2688ded +5ecc7b2416c79d4ad6b6b73e52d58c35ea922ae3d6e1b4ecbfb32b49e109676b +5ecdd692362cdd16e7fe94008b762ddd530ea9c1d9e96cf5e9f9ce030ae99860 +5ed2c0fce131b91b58071db46b9c3890de29f3ac824cc2c5f1bdf9775c804ea6 +5ed80be36d052d1518f637679738c695988fde1c8011e535bbca700cf491c1c0 +5ee779c837a947056434b72bb1baec747dea91ed37cf5cb8f39340154c597640 +5ee7c14ecba412069dd59101b0421a3abba988f2c1c98f1a20742c0853d1930a +5eeb79603e570a0cead8a7c9753a8b749b9f9a7aca9a13d15fd1dc6d0fcec5db +5eede6f8482fda3931a7db9e714956aecf23955cd9853251408efde57baf017e +5eedec8253f7eb6a2a77ff8dc102ed0a71e0d47cee4353d77964f9626381c183 +5ef965a1733b475e5a56f48e0b99db5d75a7dea8df0421629f8a803e52c344b8 +5efb2458046efa7e8c3dcc5ad0546efb785c75698824527076b31e8c1a384b09 +5efb40639a2c24669f06ef6622d94b9952dc92b14eb062749a95f11d2f8c1270 +5f061a9a22775c43ae13a0a9e3aa8fbb2004b4a97de7ac1ce7dd1526566a9feb +5f0db6f65c709713aae00817b601c17519e5edd348f4791eba7530bac76e1198 +5f1e02d051a862c65b87149484f34fb727d124a3d5885395df5f9ec9b75999ad +5f28490aa64922eb4180169269e7a1c3bfd1919b97d476c6fbe7dd49a049d218 +5f28904acd286b75af77f9936aba1bd1c7c8b3051f076d743a866fc30b6175c2 +5f28c4a99b78f2f2f71c6c96711d8271228d33bd9206eb7dad7261b5340bbbcc +5f2b20ee269e7121cc479c6447a7a78b7dc2690bdf20555111a4bd333e382824 +5f332834f176430709361564381cb79bec2d7047432188b88f9d66ef33187481 +5f3ef043a87834cfc1b23d1be3a5ca4c3ee01200c95540667bb7ebbd5de5b4f2 +5f4c1c18fdfef56dae9a22f2681006cfcd96c97522a83af2631147881dfb3966 +5f4d2612a9fc0976123eb88ea1625740064655f6d06b583ce6c9dd6dd33ef0f4 +5f5eea23d96385fe381310fda48c2387c80a348b9d8ff79799930ce9fe41a2cf +5f603c6d8bd0eb6b2fdba94945055de814a93924981ecad38ca0998c45a3f9e6 +5f6f5bf0221b7567186d7777bc20e57aa2f5529039bfb8d6d8d0b91362073115 +5f735f4de2f083a8e68847646033045ee800f86ef4ec7e72ed48bb46c95759cf +5f806c8e46919c8a05bf8077e44b0a0e92c21f58c916824e53abd4f121125a57 +5f83ccd5c94dbc4c12ff454d783c5261550d5e4ffa3a04476f3f9a74a8e682fd +5f9583320c348a2950d5b6aa70f328147ec9766dff25486f4167ba7acefdd7cd +5f99bfd9ad0878f55f568cf956611d1b395b5ffd3fc0f11c4085fa31126aa66f +5f9c382a9d8eea9fbee5749d676c55a20ed5a359df9c8c379095f4a5678224b8 +5facec55753ca90714878023e5779da5cda4527b43830251c7a2ee33d3e3197e +5fb07759f7d26deca30cbe2d16480a77850726511d109315267e78a057d6ffb6 +5fb7cba6c3b61aae1b6384784fe2d34dd72b501c2cc10dd5741c53c895720e03 +5fbeb84a4c2afd177f2277a83d09846755585eaff7c667057ef370a28b0b8790 +5fc03ebc9ee336c6ab5da55eeae6ce716cfd885fc1f80bf4e086bee96fc6a166 +5fc481b157a8ea087045f568e50e1466270631ffd957030bc3b0471af19afe5c +5fc9b2cd23b2a4fb4e87d0765885b651fda26fa46cc64a607f421091b33548db +5fcaef2b8ef61ca96fbe325ac5bb37f4f76eda7537c11b0da12afc04fd5aff22 +5fd81bd2180792fcb933d6ba93acd6c3ec2cce2f7d3df0d47e9b7a332a751885 +5fdefc118f1854d8b242953a17a3333eded138530f712fe8b29c218e3e90aa57 +5fe0e234b1c636fb216b8c8974e8de285c9f55d58ab133eebb15a4146490bacd +5fe57ea052c0704f586713194a8a8962eabc5b018abfcb3910bfbf79a7b814a8 +5feb5ba9fcd2331a2d0d0e75c8f4ffd985a234bfa57c9a6a2b93759c30b52ba3 +5feb8b274218a6d3373a134e5e72de6b42b98e7c28d9efd0fae088a48af0b682 +5fedecd269ed2df51bdae12a6ffe94270a5210b8763508990e6d60c6a93a50be +5feef68c095c8be8fccca6c2e1b5c2502053fa208396957b826f7053d2837b88 +5ff033f7ff7b50c44a97f5448124c718b10e3e457c2ba1a0c5df641c1cc913ff +600fb27af272550623c3b247ded004aeeed1b301a09c69799dbf9930aa186ac7 +6015a9a35d9044fe890f802f1290a1c435f240cb6c0d7fd0d9b81be6466b397a +6017ceead1f298d9e75c2d4ff5c40c5c7d8286cc5616648472c08ce5d5fb64bb +601ae4c899d09647348386641afa8dab380daefdfc0bc44075c825a6f9d1ca23 +60206b992028873003876e99487da8793ff570d38195a366d750fbe9a9bf4203 +60346656b2f47f8adda169b77ab8ba9ec5a22a0a1150c783f16849dea33f649e +60369a1b6a04ed313da804de98e0c26ddcfa74387b808734b58f4f95e95b27d4 +603a6a742f0ab25593451728cc41caacf81125f1a2a864add2cbcb0578b99287 +604d960f4bde6e179818bc8c0af6a9da8a49d61b660c650cc158b47de2733699 +60564a42803fc1032915ef848dc5455a6ed22159fce5485ceebeb23edb0aba38 +6057e91feed68d19c01db4a5cf0dd43b1646f7e61de3b5877c032aefdc2ae0d4 +60659ed54e0a028e42284a178c51207a286bd03d1a26214589f91856f689c95f +6068a8030879b8391b320d63a5d09cae5f75dad101f3f4200a84394675a5a2ca +606f398125cd67ff283a061f105edf0515b88c6989803f61763d1a28644af1b7 +607b2eab6e93f31ddb5e9794b1a573dd17ce590c14cfb024c990960bb6fc7884 +608242f96410f42ac7b33cd5bf7cee0d21c641da6f8d09988f538dffd736871e +608552540b3f0b41b4142b38a90b71f4a6902e03a7b098a8f055bf8c71441b69 +608bfd13ccc427aea005195cadafe5011a8a6c68b305690be2c4c10a11a4767d +609166e5e5d79cfde4f5cc07d1daac7405f48daa7952563c74f2fc387cf863bb +6096fdc5b8ee9d5c18a9c2653ba7a198a29c4bb22365e187e417eed3e7724ee4 +60b40ac800f1aa1cafa904526a9073892dea9f939b3c62ce6fd91f357acf1f80 +60b625642f4c632862025532b991ed9ca385b0151cd079ba202885328bf1541e +60be7f0ae72e1a6c7c5ee4d2b961e9dab0844c8d575b75aafc6d05ed4eb2f6cb +60c263ed5d632aa07541aca981cbcb5e3e7da85aa7626b1631170ac5154fa898 +60ced47299ec7f140aaca183458063f846259e03edeb8cbbe4916aa2c8f2e1fc +60dc688beaa0c9ae422c1275d07bd1e3b2bc4d1fbe9504758ecdd4b029e232b7 +60e0adbcc90d93a1d5c4eb6e4e34d4e012c1380e6903046549488e890de95424 +60e3400d35299e6fc5faeafcb01fb6e0ecbfa2aafbac6b68bbe0063728bab9ec +60e452073057281808cfe4a582d0975f2e8bac317ab623cbc3c8e53c899b43d8 +60ea4436452ba00c95717a342a4a98510802d8ee35b6c37ee0c2f3fc272f832d +60f2feb3a62705961fe206e6d84e7357ff1c646a2a88f615c99078ed86d59c16 +60fa2dcfba703f4148ac60577bb581eecf7620b37073d69cc367ef6820943faf +610e1ca14cffd23c38fd8ebe9c1ff9cc1f106ca6d1658ec21bd3da81dcfc2642 +610e80a81a3a17a8e873189dc677d9e1522ab1ad110e3daec08fdfdee03cd550 +61199dae99942223d65538015c8638a57b98e22abf3304c84959984dc8b88796 +61214312b2155ea524f7034d55cd678417d780b616b80ac657432d3a958ace35 +612babfccfc233318148ff3471b9aa27587f003ad9bc7ab171598829a5f84068 +612eea18da3b22743186b400534cde98296f387ea210d33f22c1853cbb5b25b5 +613078c0ff3cdd370f9b4c65f8a2476e4db1f33f4b9c8d40c40a58e2946cfe27 +613192c323a9626b8c8d75e310a6fc9089a580dcc34dd64642deb9b064a59ec6 +6151caa4def92f24f553f9f5187eb3a3c71f4e110343a29e7a7181886570792b +615477c07cb3d983d46a2d99f0ef25a1f5360ed3c71d07e667fbb4a264701eee +615f31ac2e13f9679e63e0c600447ce712fbc01d2ce5e2ca4ef8b386de54eb72 +61607040e22fa68fe243ebbfda14d6dbcc85b22a8b8094c0a708b0b23a05e14e +61661a668d2fa7fe087f95be8fa0618442a29a373adef09166e8e1fcce6b7868 +617211dffa0285db3f73c5d44234f408e0a094b7ecf2793942b20644c0f23054 +6172163f6f7294f978ee15c6409da65908a35a3331442679acae3cfbea7a8b4d +61889b163781b61952ade2804ad9db1f48d6a256a380619a6d88d7fcd4ce66ac +618bf95d749e28ec8e45f7e4c711f67844adef7e2cd4318477742b284d23069d +618e4255ee993f7048d70c66a1ab2f7e72dd465335ab48909caede8d1317e662 +61a0662121a87c1d7db831d035553831f7b8a57445e97b9414f065a4d7b259f2 +61a451fa0cce89831eacd119db3ee9ac79b2d73c4df10065965f0abfe469ce1e +61a862d8d4c5793628cbb05ea49d0606364a639ded7085f8588dbe1d1f1aab96 +61a9b4f701ac981ed76e2855a20f9af9715da9d0cd65f09fa1c23a813d8b857b +61a9bad1fe9e937d9b31ee3b9881b8365d4768ada54ab0585e716262dd8ee955 +61aa6a0720a1a624a399e0a8746a3fcf2075765f8b96b3eab622ee85c5763dc4 +61bbf3126ad54683deda8536a6c7053a90d50e6bac35cbc577054f411160168d +61c7f8635b5d52b85f8d707591843742dcef55c0ec014a4116b8424419f34daa +61cb70505d5658ab662819828ad07df48731e336bcf8ef7c19336b104997fa59 +61cc2926a25aeea855a5b0a99b66a2a429d12d68260b8c782bc9865a2f2e5696 +61cd0556fdf4264b436431f3a588632db1315d5108a7730063451589f36c7832 +61cf57bb39fe88ef9443a325bd265646aa3b2b4d82541a007b394f8e8f6ced1c +61e03f97708b3376109e95f58208e015a71eeaffb4b39efb0e1e9a4215394006 +61e0c6d1753c2c50865542761630b4a1792cc27c67cf2f069faa8596931ef9ae +61e1fc48776dc4045b3395217e3e8d4658fb446485fea5bbe30750824470eaf3 +61e40398bcf1a767df5771227bcea24547bbfb7187f5b31b6dac58358dc01764 +61f8beadb8f837d12265b64736e466eecebcf674fbbf701679c3f9bbd75ffda5 +61ff4f535bd14b2dffbde4e061c2c664f9a0aff2ae44cf51db793585d8425e3d +62050007761924ae320c8be9fc4f926ec7ee04b8dce730087128fb7d82b740b3 +620f135ec735e4e829d02faf902b3b3a4a4102c296698cf59daaa774b41f430f +620f54a957c5dc011488f1a78981ba170b9f5b2663a6fe48ff473ff0134b1ecf +6227456f1919e77ce75c1ee35b86287cdbbb8acd6aa12ac87ebdc6dce7544295 +622df28b1fe6e78bae215c2b52b7718b0e96abfeeacc677a9dfa2dadb76f5b4e +622f7004f5dc3f5a2e9b3c16e8733fe58aa323d580dacee615dd2f142905535a +6237885a861e2ab59d348c44412b2320c9095bc72d1f1661a6ed045f3cda6648 +623cdad7841068267a83caea1be5e992c54965ae0177bd00c908a6c19bf91999 +625288b395dcc8c7b01f5da9d0b1fc09fa00efaa8db4ffcf19785c93211ad010 +6252da087864a2413582d4a18919a7ed1d8fb59e4e900b32837725a2de951f46 +62535e3c2b78fe6c4811c3c1588cfeb4561b8550dc332dbaeb33ecbe6f7bd115 +6260cb8f248a19e7f4cf6d99e3c8a8a64f9c73c120a6ee1d2530ddedfdb39295 +626a5e896d4f87dddb223b66062ef229758cbb7655aba3af0e3a4b3e7bbf5350 +626f961771c000242dee537b9034df54c48acedeb83e0affdb344cf48ca62e2b +6280a90d500503c96480bef8a03d928d56561da2a990b3fac5825c60142aae14 +62850eda586840d3d7f9a0295dc3031d8aec9c5d14b320fdc4ef79ee51256220 +6287e3e8e99b11834615e11903aef1f8a753b6aa0081a2fd02580c0643c9ae21 +6288fd2e27be95dafd0f28ed9e3c9031bea68a15ee12068d910e2fe8489c155e +628b6321fe27774ee9f45f5cb6228f954453efaec5140c05f56ec63ac281f45c +628fe8181605a0324b4cfbabb266a7345e8efe8cffbf5f5ae1475c2a6a8fb29f +6294d8949bdc19ae94307a1ec9f41628a0e964501945dc542f7ae35e452565e5 +6294fef74e5ab547500aa537dd5d0af19ff0904a462dfaa2962423ee7bcef28e +6295c512edefe4d1534e4472af97b4ea02bd3024d0871fd6f48bf66e73fe96a8 +6298eabf7c24347df416bc43b0fa14a743053e0ee701743944ca8a2c21845743 +629b506fbe5ac3732e704b662781602029014b84d551a8011a6779e63842a869 +62a1c4eb34148627eee6ee5a6ccf2cc17be755816ef305516b352eeae72e5ae6 +62ae000d1084e1be097ca1754de5a233305588f2fd8419c95af9c342c90addc6 +62b433ce304cb4d7ebe838c64d636fe488b3c0037eaf9e0e663625967e067703 +62b5b5cccae4faee1e9466cf49862126c9f77721b42a384afdb291153e819dfb +62be39ce8db67bb058082f12a81ffce6ce7d00359e66ab8ff0d127e4ac72fa08 +62c2b692c72ed7d84a4273dfb6545d0ac1182080f46ef77778d0225cf01736eb +62c5250e3eb6a4ff29d1a2ce994ef030a51f150085bc3e7765814dc47159a1bc +62ccaf3196df33fba3bf3f64dc9da88af2771589e62e26c2e44d9e8ee391da42 +62f7eebfce4378e59abe3c361e35437f26c6ce2bc6aa50205d9dc6f363933961 +63006e92e2c599945d80bad153f5acf3f066b95f58afface9ba535cbc7222865 +6301a14e98c8fe385c8ac5d29033508270ac42e84c4d0ab468594738d73579fe +6310532a2d1380733975e88bc52fcc20b2ffb21c448845d67cc379e25b5733c9 +631695ac3c6b41404a09ddadd605c0d3e3bc464f86a8eb8be78f92a7112385a1 +631b8ed8a2db616799d45bc3b28401cdb7829ca699a11c48060aba193061378f +6327e282a3372e63c029883f4cd561af3336f9f8e0b236123eac2b1a6822bc42 +634fd2fd7d26414ba5abf67cd02f6cf121abf3bfa81b34ec9a5350e5f0af2db5 +635b7e5db8aefa1628f7524877876e7de10c127064a70218660d614c82bb3bf3 +63656c3fa12b9a814b1d92ade52a7df4039b441759bb3a5f222c8079a0daf69d +636a3bacd2387c6a3453ab3562d8b85bdd2abfc359541c8aca33765b63bb175a +63709af29a73a92cbe63f3b6ccad1cbbf7dbf456245e8b92e45a3a8977132c9a +637178a71c0403ac13f280da0207e2ce8ef6edd0543e43a1d33bbcb6b99c97ac +63788e9855b6afc2c82634f567a86680aa18ea6ed5d353d52328866a3cebf535 +6379579dde36a9e9e83072dc2a8124725ad9c7cf7c2e7d8bf2f055227b2fc377 +638810e320645a4b122a38529d21b17d5dfee0d848f74364e385b77ba5082041 +63933de0b958970a41686349bd373c2214c77985cc163a4675149a96d6a72cf4 +63a33610279114945eed8812829ca6cab6a7cbb3206ff406c108acf1eb4f62d7 +63a4637201ef1537cea43329334d1ec99ceac7f50499d0d7b4a5a46b326ddd11 +63a53f1ae4f8df6cf37c5f818d7c489ffa6f96bbd0a8b10d72b4d1e63f4ae95b +63b003bc3d4bd9e04bdd0f9976a968fb85bb94fbbd9d56849318fd80f6e82446 +63b1593157b285729de7a993a92a718ceaac40a074a2e1dc6f78fd139f7c4454 +63b491dcd9e2d53dd4f705fc8c8306bb6acb7d3a7d2a415c7ccb26bd7fd1e9bf +63b7a1cd3b2fbe368079ba7c59b07b472480d63674125a99566990108081fd83 +63b99dd03cd12474d25e00691f86a4f0b8b248016708b275efe3b51b6f8614c4 +63bdc99a95bd245ad234319586d2e37d1e53ec0a44a1c1815b04edd0302a1e71 +63c3a1a5b21aab7f375a31f399cb48e0195f8bc448388bdefed080bb452e4719 +63cb8dae41419d66f2ff49b6f270ad54d9f744cecf64a8ab54c092e7f751894a +63cc696b42e349089c801e1637b5428e2dee4fd72b4a4472d6f034daca8646b2 +63cc8c1da80b260cac3e458ff697598c528274407f57be3488dccc4adb7915da +63ccb12f426dea0ab9ce956a33e4607ba5070ffcbb25dac78154a11cb2b4274d +63ccf35d4727875e8531da4ee70ffb61f553ac7baf9ef7e66783b43f954c9fd4 +63cf4939ec482298f28d9184534b088b5ff2ad0e92b129044198a7818f8a5da3 +63d2a97bbcae4142e00a74b597b35e6c4f97c567627aaabae92603096290df76 +63e1f141caee64aba1f44b67bc8f2163da987a42d3d3154fdd23cf2b2e381031 +63e37576198bc57346f99f08c18beb6f81cedaaa6ff586b80e73388919e9e56f +63e7aeac07992a5511ba486fc076791f82bec3fafed8964943cbfaa54427f8c6 +63ea1c3ea2d91a77ed50414a277f2b142eefee3f9ccc88e3b6a5a0929039084d +63f1c63e19743b1aa934b742e00051ea0db3dc8e8b14a55cb4fc96f01c7d2342 +63f467fa1c47b0e5d64cd4a7eea7ec7880ee71e14addbe653167000c7fe6dac6 +63fcd7953d9765cc0be4e9b396ad6656a5824e9113d0f78247d2badbcb1fc26f +6404229eff7038876da3297e086f4536af09fa765abccbbc9e410b3f2ae38601 +64099770ee978e8c7144a1c66720e7cc00b5ab8dcd0877f74032380ab3a3f579 +6412876077f8e116e4ef8dc3b1beb00a86b8a190903bc36eb06a8b020956026a +64168d8e01c860b0d3364baaef94531f70bee1604ca584a492204f67e4f4e899 +64168f371fe25ef41eb78e6918e359868d7820e534f8dfb758068266920a1bbe +641760448a164c0e08cca388909b0286235b8d43ad4936928e62b443287e0fcb +641baab59d653a2ae7e92b7540d5d4340fa155cb39b70de210f00613e849ad8e +641dc4719e9d780e789c7310c8393d126785eec153635477fbe5c086b2fdfe54 +641f9ef2f766eda896c34cdecb3099e1e8da556032b1e934d1394c85fd9adbe3 +642f62c4519fe912e8bdd25cdcd3fec2b69b6c7d9f587dd35d18fa3b9abef1d3 +6436d4cbfa38722dcd529b4dafba45031681e5ceff718b1c744d9ba04127e48f +643bb66d5afa803ad22f9c62bd56a0c8a9c4ec66957972d1e148be687fda1899 +643e83a477180db5d234f457ad9fe39bb69a57aa9f0461a299507edc4ba8b690 +645e376b86854a68ac436b4bf1273dfec2366088f0ebf5a5348a39fce081a7e1 +6467ff64baf6506f0dad2e1047ae0dc0b87bc9eaeb284e165a417c5795fc45dc +647159bec5a04e358bbda77ba669d4dc9ed0e5ee908e1d8f6f111bea31de9360 +6474e5300f5c2adec0da6ef005a3512c6911773691ee99a6c00a50c2af796ee8 +64760ca5e092f1610e745e8a74a2902fb8669d24d49b4efa72ded358c6f9d0c6 +64801364f85b9c722f91d3b0b36ecf6322bbfea28908cd61cca4fc47299efef0 +64809cf8b782529bac4855853fb252de53887ec6c60d3ab4b75b087f9d359a50 +6481ef0dd259bc19fff92d128d1ed265b082e2d0bc15a8b09a50a3cc3b00176f +6490019755e03220857d952145903028ee16b3549893261a1ee8bda19fc72700 +649838b3cc9a2a30522ea0fad5e85bf6cca26193fc01a98208adf61c888927f7 +64a78c2613052836571480cc5bae8630ab153a799f8764964152f4bb5582701b +64a83114e221743fada9f2b2a114555410c01a018a2ca1ae036e225e3667a104 +64b510f92a7272fab363ee33467dcc8207dfa30f37437c4c46eebc8f4347c6ab +64c2d42a1b2b64407dbeef952e1c38c474316a9d51ebc69d3341d94ee63fd383 +64c3250a7073b34eb39e7661096dd266bab69718caec0ec71efd0a9e221a69b5 +64c83802f593501b1f546c0cc571d66d227c57837b80beb6e0eee7a9fdf2e036 +64dff90f0725111a0d89df1e66684e8b4c85f2925db5f17a613e5b5feb3c91f2 +64e667165ea9d802f3af3b4d1f29ae246a04e2e10e1179d434040bc961259b98 +64f0fbe05bc1a632807e12239fa9fe0ff18557511d214f973d4849f24f3ef66c +64f59959ad8e00ff52145ed374c7122719d89ea24335a439be0000e086d055a6 +64f7556be5c3ca9fa3a240e232141775e20cacd8d851ab48f30dff988d596796 +64f7b134ffe44b0923e9326cba83bdd7d018bde570a5118d1b8b3e29416e3088 +650189a88a226369c79c5b6d4d7432ff2694cbb6e3f960b534102e25744e33bc +65103c50d06091ddd114df913e5f74665d27df5404c07d4ae61db69097b79e16 +65490dc07994d27ce860401a1078a82a022d54229516a62d27ce0638fd15049a +6551f57a8fe2858db85d7d8aab1649af9f8c16661264716b7c223c423185592d +655659a8d484eb5514b0ad8033fe4814a1a2f0f8ff4a18c09cef8ce4aa9f4a65 +656507b85bc044dd26fb05fffa758ebcfc7ce298147fd5717b6b149ed6df9f6e +6576c09dbb9ab58291cf9bbce71c1ac8777e80507a5920aabfafe7defdd7ce40 +6576ffbc65ee0050214e7d3f78320ba9f57deaa903ed74ab0e542e85f06c867a +657983d2cf53d0ea72da20b0fce1952094b1ac499b1ad602b7144eb5384680ae +657af8228d3fd85fc11d3ad4e8de97101667feeab3b20a715ed4010a0d6c391c +65806142101ebc815697a0151424f0abfdbb86c518092c6f4bd0b96773dec843 +658d71c536bb5d29b7a2d4d25cc460a0c8513ad3776e4bd838d77e5b93e46e97 +65a042962edb8fbb4b45e3118dd190050d0f9096efed6d527db5c64b6a560141 +65a088295cefb58a4d356930b569f14ac6619ae21d5862ca6afe6c9b0e58111b +65a2ca5ee3bfc23d51ff8cafc6fc826d0ce666bac12499d193e74eb914a479c6 +65a71505d15fc03a8fbcc5fd0eeb185bbc5785c190683d2263faa9ccd59a35cd +65ae19211f09bd8e149f6f5abb5a0384707dd7b9f4bf3e66cee5afafc06253bc +65ae9a175db0affbe91d7212879bf9eca185c4dd971ba5273224b60af383e21c +65b63836dde98ad89ae08708f159cda54c31971f57ec0393e03e5e406683a9e6 +65b7324e1d4f28b13fddec2415826de701f2416b601eae94fcef95a9374351a5 +65bf5a80291bea917e08de59e336d3555421fa9730b8b10aefc52a518d22436d +65c025e45012a39cbd0e00fb7cd0700825122e9bdb167dd6596108e87cde583b +65c514dd8bed5ba8c5d0b70ecde2ba60f8a990ce6a603cf145058cddef58745b +65cba998ed1aa74162c3468e5026b65354ee62cc08071b13e2cc38806d2e87d8 +65d6e654b58ded84cefe7becb8e8f97e400f8bfad912935da74275937233b8e9 +65e0dc6b3529f625cb3ea56a36ee3c13fd8e298ea8634246bf4bba6c95e66507 +65e93457412bc4c835ed7f0e22156f84e8848bb78c4e53c3b3fb626d53010ce7 +65fb3fdf52ba136d42f14cb8596a6f9ce8db8490a9f28553b982a8717a9871aa +6606256a57e480d72645e8f66058252b9c4389e6d80a1780f376e3fef789467c +660933d3779b1724cd1bb57b57647433048046b09b7072d36f84b1d98c63172e +661b6a2b10ae0ee10eff56bbbd99f1e34c5168affe74b00789e6b07fc2286288 +663055e6f7db4f3d82b038849b63a1efb3920526db9391808246aacd85c94138 +663f2c6a97fa4715751441d59ad0198fd148c71370ea5d444da08cad0a76d623 +664323fd6f19f794ae8b5df8a9935ab1aae0e5025115b7ddabb68b74be9ac428 +6645c5fbcb88c3a475ff66909a999af650fb1cdee5284b4de6818a4a23d753ee +665014eab948f82f781bbdbd5725d3331e146244056dcd61fb3ae8c8cbfd270b +666670f7c8df4c3e28dd6aff0776b7df8e056f24b286e55b40c89556a6827b10 +666790d1682e32418b047abbc9cd27a4028ce697947b9d1ba865847e7712f477 +667eae83d8c101074419e7f198a9ee0c1629909d3ace4d71a1e00130444d7199 +667fa39c52d919e54fe3f74b6cf53d9a927976ee346636fe64d46a2a18d846b2 +668ff4924c7ec1934d95b2bf8216cfb5a4de274929866d2b22d965b74699512d +6690803681cad739a22e0b08eddd70c67367e145b7707f0b83955293e66889e9 +6698c2dff130a4bf316e24475e84ab03933ca7cfed95fc45fc90ee3641ccd11d +66a50d6f4b03dd197d5918125e8b08da11d9c34611a750255a06cc2350e1b1b4 +66aab03bcd8013f327e64d4fcc9e63b9143ce0e00c6c93b439a69272cd2232a3 +66b815f602c4f6705efe6b0a31acc95fb7f9fdd5ca8b9c6204d625a33f874d41 +66c3928dc11dcdac958ce5bd2533d44fd172e2ab25de55abe93dbbb0500ac298 +66c7dc83c1130215d62a960b3fe533a251e16addae63422793da944e33d90c58 +66cae1d4a583754bc1e4d9f257dcb2a2c8ed0044540f2b6c263d8dcbc33f2208 +66ce386b2d7f885e4a47278b74a64c874241f4cca9cf659aef85d180ab85aa8c +66cf71225f9af5d2d02883b5145b186ddab65c3e524e79f897e5efe5e592e89d +66d2e4f1c355379b01c12243d459999a59c05f6a8e4fa7b73b4efb4766fb0665 +66e9394c1c192ce5375b70fe37c628b4195ae933cea1252408b0339a77c51eb8 +66ea473fc38a87d503aefd9166a497395164a66cfca75549eb97bd4c2568d878 +66fdec063945c3cde3c20a0522e5ecc794a80641d8b788de63ba917bfc1936f0 +67016d76edfd9de94807cd91449db1d6ba295b7e0927370d3cfcdf4064fd4dd4 +67044defb061874ceeca24c6147e168f359fab873e48322876d9b65a4a3b31e3 +6714f157c69e0249ad777f4f811aaf7a4e77367f80a236f081c65af36ebff70e +6718969d8372d90394ee2f69d816f9cfc0e6acaa07d00f5683c73fea9f2321ce +671a208481fb7aa0bcd3ed64ef9b83a11002ee78ebea2be04b31dc241aaf9db9 +671a7dda336035bcca4fc9a64bcc1b738da3c605ae3034bd3424bdafea2fc6fd +671ad1d59ca86dda49b80adb02bfb5f199b23b2880845a0676d3fa16100e2a93 +671ad9b7306c4e563be231182dab3ef33e615dcda0461bd467b345fcb5c1293f +672924bdad8ad188dcc6c5bf45e347ee06416f5a684fcf057a5bd69bb2283fdb +672992b067ba38f2e353e67382104ce8822ff38f6093087c8a17666a33899d94 +67359ee3e0d044a109e3795fb1951b1dbaf33dc1f7be14ccb37772d482f890e1 +67378c81eb74f8f0d3f26b04e739e836286249171e7cd0ee40bdc4cac02e2ef8 +6753ce679e6f3becdbf040f3563b91c5c9e6d1817e914a8b5a0bc11d497319b7 +675acc8fe7cb03f17623c400bca1d9611b7b4dfaddd19430b04631569b02c25c +676df5b5f705dbc555add00b0b994bb8fc5526e247f481d5e18c93f911267781 +6778b4a61835b589f84931b41969ab862972d255485e7071be5f7ba5bf6ed178 +677942a0f7484d97a03a69d2bc7f342e6537c5352b0577d6e90ed5b03876ceb9 +677c55305584698b8cd87e039096b3ec3a12b355cf7346c93592316ae1ddb5e7 +67856d2c476840a3d20cdf76da0c6a1a8f505a57ee5b363848cdddfa3b4aec36 +678a47c916f731c0ccd3fbf39bb7f15572bd7877ca40a794bf1c62626e4228e7 +67a7610a385e5f81aad376c2ea4f0d8d648e5ea5457df93d7568dba719e97cb0 +67bda5c528c0815889ccecf534b436ea2e807083eb0353a47d7bf428ba620dc9 +67c624d5952d551775bf96f2b7e901f104f0171213622ee32dee266d480003fa +67c9a0d985a732e27fb900743f4eed1d33e947e47ad662c8a1d286e7f23df451 +67cbfd923aa4b270547b5f22079f83d9aa1de6243af62ec393f8c1b4bfcff4db +67cdbefb06f967e79a52a54ef71f21cebd86c545934bea0f60a265bab3d712f2 +67cfd8b12b1759a3e0f6991ed77ee7d9e90553ba93677e8074d91ab8c6330b0f +67d8ae12cca3096c3550846b65d0e2dd0fae9b25a3556793473fff5d88bca1fa +67de7d73fdc7e6c70196e781662657368c215861f8cd9c61eb6e67f5ba72066a +67e2fecfac3b9d6ff16189f6f5c8208844113c8db687270b27c2f608d48d8215 +67eb40c3fa680ff77078e0c15f78c64722d3202f82be03e86a6420962bd9161f +67f90a72e92b40cde583b8da66bdbad3f4b7c9ae086a98af179fdfe51879fab6 +67fb467051b9e4ae75635a64755a1247991877db2033231cb8f239d3d946ba1c +67fc4cf8845e1d7a1c1a47bd20389a1c89e17508298025eb200b40a179937ffd +680273f23ab8e2ec4a80765161750bc9fefd11c33205ace9b9790fc2aad36003 +681c06c74f1ff57e893c98bda0c0bde0722cf7d053d799273da5c58c632aca7f +681e5744a8544b3e7773aa856720c5d71e1d56b70997a6147d253056cd988a32 +6827a447d85df0c17e9b300ac8bc1f638770e840dad76a28fc7154351dfd5e30 +6838c4a41ea2d720ac53ec8b1e671bb8436e6058d503c7d08dc1441efcae1f32 +683ad98ee5754339f2a74ea8c334e5f98a0ca0c42d67c3c23699a8ceb427d060 +6842e0f715e4e9371c85b5391e3083f2aceab84c59b58050e940de64b887b44f +68551c3f040f2ad1616ae3303fb71e189658d1e3b91759537c015e066e40b647 +6855a94657409ef3f3b4b4e0f04d4ac352ca0407bb1bcc91242fdcf4ab65cfbe +6859df0ceff12c394a8e813e3426c8d21ab19d477964ac23e58119c0272b7026 +685b52d6f0651851588d293b07b0d85c8d160e8f0bbbc4a834b645556871a452 +686309405a0d7a28f37fbe9cb7c6a69bdf034c0284bd222ed283c49a9902380e +68648e3baf5b72d50cd27a61ff929688c046b9077eac0c3a3ef6da14d7df3f7e +686a54852b3a61c1fdb98d01a6f1d8e6a3d7d9ed25e9ddb11a70d83baa9de5d9 +68701ddc30a693b2851167288afa219e976452e963605feda5dc9a5323a575cb +687c57645a001c5727db4a23d62e97e2f934be7edfe40d42ef18d12554e899eb +687e3b0b9ee9bf527c4fa0c10f2cd7716c008cd0a2df2ef338592660ef0b06e6 +68842e1347340ca0ac5bb31f68130f18cc6b98f22e8d914cc2a65cc6d2d4910a +68865659da1218916ea016a0da622e562993045952cc89fc2461909b93c3b54c +6888b8d2f0d3e736b5fefe03401eccaa0a5bbd7bbee1e70f95064e68b104aaad +688908ea48e0f375abda62175b58c2e4eb12ad304df4fb690f6d03daa83ab2d6 +6895e44ec80f0e75b550fc0f38006579beff6a73fad00746a6181b0ac856e434 +689b4a18ea75ce12b0b24b2d237bfd2f17f8fc3162c1706fe5e0c24dd706e60e +68a8f97f5768bfe9da244306857a3e76df6e3342d2815de58a4658cc95de7846 +68b574da1729da3d57315bd45e8002617f3fde2ee0e0b158dfd4d85e3e9d0bab +68ba360152ece847b9a880a3d3f837c8a075b8588de2b9faf58f2eb64307e5f7 +68bd996b833292645361f9c3459700d7111bb3fa762ed7a2709a3aa7f1923e00 +68c32dd952dbdc9be940df63411409e2d7be2413939f111713749307238db2aa +68c6a84fe05b013841b99f9a565ae389473c8a784e1e2c1a98bf3de6e74baf5b +68ccae3307f1ff17e5feaece74b24ca2c55bff3116eabe09cde111148739f3e3 +68ceb211b8ab7f0eb24067819f8d0579f44e77ba5db8a8c4c55366b7d2949b5d +68d843c105ef65da95377a992def9cdb2a200370ac8ab6655faccd8e02302144 +68d994d4301e1d1d0ff851a40d6abdd51f91faff26c167b780e22f8eecf0e805 +68db58ea0798f61329c98594433f7e150ea28e04fa9cab90072f905cc76f1274 +68e0e25d5d77c7e8b74175533651648eb886789ce3ebdab10d2ece58f4e2612c +68eb67827d441d13be5811676224b8f92ab6461b16e9511d4924754f91ba8366 +68f39a5e2d19cace699b78f5155ee54e6a848c5340deb06943a32c8c3ae28702 +68f6886d1f172bd68333e2862652483ef3c555bdb5e2ec9fc0885145f38f83f0 +68fa2c23683c3db750552e7fa7384d812b957b3817031cdd616546a0917c2525 +68ff05947974193ea80fb3b08085ee9e59b22603b4e36eff64f7958e4d1441ad +6900f21cfa685153546737ce9ff65a6dea704c839a34fb5bd03200180ea50e6a +69064af91baad4eeaa5299a7bdb246a0269442800d9e5a874caff9b0bcd5b71a +690f4ce570b483534899aa092a6c7a13967db69540cab93e443089e3735f69a4 +691ec2cdf2a079c35059112b674d33307ee4261f1471268e709162e7cd5797a5 +6920b1c0fd0c1f2235ff56a57feaad3c89449838fabbc1b2ad8288a10cd000fc +6929707b6bc749b5fb0c8cd439c2822aca8ba68b0b179203ac2dcac5e48758f2 +693028a8f5caf65b12d5756e05501fe834b04c7270a9ed9b519cfebf4f0b8e20 +6939737ce97e4a9da9d23f0d674b1941d2fff940d6b5b73530cd6d082b395842 +69427c9b239caddd6bd2ccbf74a666f029fdcbb120adffcdc6fd5b6f82043e30 +694ecd5e438e37b7d565df1bf48f3790a13fd97fa6f1449a63df00fc13fb2149 +6958161bd18e2abe5113cbf707ab77b8e7be5b42bc69d91a38498e4853de00c5 +695a24ad34fbddbc4b51cd02cff6a67dfbbdf2069b6623d65a98429b16510b02 +6962e65b6f0f012d3925eb3da6338c56ef83c1c765379717470baaad5f994d58 +696bf32f0caeb5dfe4aa834083648bfbf5e64505b32c7f453e2bb9a2b8ad3d26 +6976e39de1f4ae91a60d48b70e5632f4c7105900d62e0f3fd0de2a97175e9a41 +697b82e618cc88b927c1aa277d872d5b29643da1294b8cd4a665c255380de21c +697cb515c2dda0ef4c08db4788d9c41198c24e10b3e997bc1abf4245f6f08887 +698983d5e3c02f79f26736975932707fa4e39f7172d5a4b4357f95317b0d6b03 +698dfa853f26a7fc20ad3235a660e6ac6d623d51d09814e04881563a743e71bf +698e3dc510ba7d9350b39898a33cca2f84265600e524ebbb627314ddf4691ced +698e65f8b7642f3d8d2d5aee968ef778cb2a4ea15526a07b6a4699ee33995f6e +6990dc2e259e597ceda47bd5dace00ce7764d475873bb56fc280c78159a9f87c +6995a324b129556f467a7f5b807bdf3bb4f4474d0682ba85e170375d9b7e3d36 +699cd5dead6c911ee72f230dd780b6a9de6ef7f2beeea05617cbe2b0adcc097d +69a8dab85426f2b9655203280b943e229fe2afc467f54288643c8e29ca5b7c15 +69b34b030c139259eeb8fbf87d33e1e856aba87ca0891b83385f3d535426c454 +69b39824874d5165a634011f31b600e719c6fe1ec9860c76f476682d60226494 +69d1ed2ea39f7b91e8d91626b63f934e7d283b699512c603180df33e4da62ac0 +69d3a9de44a4a017d9263c287d6f4129efbfe2766aa2e89e5db1e18e70057186 +69dd335cb3faae89a5b92a0dcde5ff4b1788b956e41e3a35525475a4bfe878e4 +69e0819bfdc35523215a9bcc4618fd19c23e24dfe11fd245134a43fd1836eff7 +69e3a95d4aaef5668285d820501ccff978cc972317e727cbe0478bfb987c34f2 +69e46dfeab1957c91db823e2cf939fd8985a95c1542ede099a1dfc2e0ef58fe7 +69e54c4b0b36656385e9047c05cf2cfa9e7625b6ffed6b451f5383a401ad6fa3 +69e8c1ed92522a3a5d2e22c593601583dff53b100079a68ec1bfcb91ee14d291 +69ec2165aa653c2d0e333a7b535abaf1c82a395562141ea0e3c360069c53ed28 +69f20ccd65c28abb6894a3c9fb4937104cf9c2f4df561e0f98b5c6e67ee303b6 +69f344721d527f213183e97c3828461c6b6c748b8f02ecd6c5e49b7db25180ef +69f92da7148037ec2a81e001c27242f3b3bd5a7f92a2f06c7cb7addfae3af1fb +69faad1449307c380add928aa0faddd86d8e4b49ebbc0eac2868b14fcdc8bdd8 +69fb875aaec7d2a3187e7e7ad41698e5853dcbf185bed85bc9b0878e2dae60e4 +6a03de07583363421541c72f26982d51003a77833120b9e7cad1fe188617ad8c +6a05f79655dd506747c460742de4274733d5fd1beaab503262e1e0a178dab5cb +6a0fa6d73b799df26e0bd2d1b871b60d73b1ec5e04d95904239498ad08afbf1a +6a15194b0396d92e8e2ae0791f8c4c783f01dc5c320a75e87007be34b5362d9d +6a23b9711a5563d971bcadbcba49c1a34087653f6114daf80e2309bdd058a3ce +6a361c487cab49cee4446d5fcca5fd0fb792241f25492b4ed1c53a1158daaa64 +6a37b90dcfb4aa347de061e7ad879d4911c540f989a77b0ed6cd05927c12bde1 +6a3b4e80ff50107312a0d6870790e1fba29b3f538bdb0714c37f376b4cd9ac2d +6a404fa96940245ff0345c853e8361568b2fcf8451537083caded7627a64175f +6a46e6d76386faa5d5c77e4ff6eb5468e4df7e4116298ab711010ccd37a1d261 +6a56cbeff1347aaa7e277c1f72739aab2f60f531da129304f78db5885dc56f25 +6a5aac75853522c81bfab8346a86aa43769002152256360b9b4e2c1172375124 +6a5e0aa012e338e595a05b686779aa4b113d36f1368f7c131a55d2a7fddde1bd +6a5efe5fd852dfe80fcab0d69153e8270c12c00cf93d6cbf2ffbe173a12835ee +6a651078a9d4e5773fd4a89b0e0110cd1b81e1aef539ef1a03229ad0d7464575 +6a6ce58276d00c34feb3db6bb384d14b9d2135501d1bd004e2bc0a7408e3f269 +6a6e1a3223973ed4300136f566395eda1782ffa7121264e6cc768821f9c00c6a +6a71b9e61f94227cd35117b51ab25a2ce3dde45ae131735b0e65779819e455e3 +6a8f18eb35455d35659f50f33e92df51288e71ccfdc32a8592e08d6d03bc5a2f +6a92487fd86b9eabd4cded9afe4b8343c23e62ab1a7dbeccc4a5bbe03ab26439 +6a98cabeff53565e34f88528454d4947ab75ef63ae3f62f65a2d20e49af8d0b0 +6a9ec4184d558120a80a05b96a1886f1bc992eb4775be263cfb1e9d77c71a0a5 +6aa696c970e2b7518f0e7f95cadcbd58bdb6c7de2db42e39e3650f50b073c189 +6aa9b747ccf024b0dd7a7a75cf16b639961d066952160b1f2afa571b615d0753 +6ac025d36495f29a0476f74dc449bd84619a01f1ab843d4746d8df45687e7a48 +6ac5abd74897e3f6a54afde53b319ab548046f0241305dff0e33f031ab36e350 +6acce706704a0b417da0b7190d6d0e8ae94c008c152710180ee3ea5af7527ee1 +6ace3b7a39912e63a3eda1d82fe3e6aad380c3e7949f45ef9416db43f8f87030 +6ae130505b0ae00f5a084fde89420ffe54fc63d1973559cabc49cb9cdf08f5d4 +6ae560e9b1a7f2daf8fea6ba22444df397934e7232057ab2338a404c0ead0704 +6aeedd07c760e8a2f2cc46751d57bd473137dc9d287ab3848f98cd160775aa09 +6af0205947aa91be146b639023fcddce33d176915753285adf81cacb0f83eddd +6af5d1d8c74bd5e2f2929c083135d95aa98eb18df5e32e9e334c4db95522151f +6b02fac902adceb3db594a4b8bba8687498d7da1d506e265c36bc6fe3348d4e2 +6b2a6bbb9a2adbc48df09312793ecdd9b888f65abd949963abb989d2f7a48316 +6b2a852953bce89207449ac2687132aba27d9e99bd1a7db8d8c43d15bd3893b4 +6b316ca55a665de4dcd497306c8a3a33d2f210f898f3d4efc3cd7f75d7e5f5d6 +6b3a5f230c31ecd47b3ada227f2d82c2faf19ccc906188d8ae7256d445ecb6f4 +6b499442e2bda2f344993d96780eba25cec4fffa1da0d3d75ecd29a0d37b35bb +6b6116b8d308f88d7d59424ad3267ea51bf4a811261b46496e868a7121460eec +6b61fd07a9edec4bce38a70c9d7543ba304f434aeb2bd9509af84b62f08bd6fb +6b662320b5b41f7e024a3dfae6a9aa4c3244b728879ef225268727ada1b42c39 +6b6d101e6e3c63429edf76bc9a0dbb428c436182eb32082f30806e0ad98b87c4 +6b6e5b66e0c151be4799c823f48bcd4adb397119235a4c2cab3fae2cbe5195ff +6b76aff49934fa5612f0257ff7b38b21d6582eed56a430e334fab17b96009474 +6b788b1696e70716863e8993d823039ce2d8cbd51a1eecc00b0dbdc5e202ae3d +6b80bef5175aec05865b51a73dad45b0a29ef7a3f2f8f0efbeec415e36b5c355 +6b88b52c9056c60c3113c26e511ead6551112e32b29d4d74844c4906cc0b0194 +6b8ad534948b842c4da8a358119cdf62bbdb04bfbef8e08f3b0047a8bbb05dd9 +6b8bd2138154a2a31b9cb9714e8f229a0e733464ddef92e1b651019bfee215d4 +6b981a1f988ae1a465edd01bef651952a84c7ed80ace8026524fb9bec29d853a +6ba4bbaa3d373c808ed06b387462b96d9e2365a9213dcf63aaaabaebc0ad1635 +6bb2782405a47c39e78857622e0a8830d9ab603ff356dc1d70eed231caae4dea +6bb3384e3447a62eec787e202fe6a34273a210b4593704ce5ecc638325390a9c +6bba100f21226964f7d3d69aaa8f61cf4cfc2b6ee1d218826656687d0f583e51 +6bc27efcfee892c6c20d0172760d8a550eb29d708f84b09d2f5ad2e3f87292f6 +6bc2d613bf8819c5fc154b048a0a613fd0f5f5986f2acc7a0adff0e591ff43e4 +6bc78c71a31ed6b344f155324abb2c304c06b41687e0f3e5fa1c97d5b0a772f7 +6bc8b1e36dbd4c184d435af19aacb2850d6242482ea0a29a2ed42021d19d6a05 +6bcae83dca1e521a0ffdaf7f0eafe04f54f964fa6c8452cc7e6278aec2201a70 +6bd933b3fffe4d51bc9dd08ad10a3f31b3583b01257b3bbb7f95f86a37f21bb8 +6be95b5ae404cc430cff337fc143eb3a6aaba9b26d3933e0334a5525752a3c7a +6beae6f227b0025e977b4f09f122a13e51ea0c456c7402743753d5f2dd1520e0 +6bf71d222d08ab9057bc4487d4477aa465e85ddc86ca54f43415ac670fd58a07 +6bf7a5cfa13f895f75f45d9467bde92e7a8f12e83990d730af2afa5df7fbefd6 +6c01f5bb8c40256f3f53c207e3877edec8f0238415b1863b7ed704b46c797c4a +6c031d712ce4ab8fd994226f26bb7ffa52ce34ef32035151422461883ac0529b +6c05b10396460fe7d94751ddf463077899230e5106ce131e9bf1dfef6a707188 +6c0a5e3071a34609fa58ce6ee1f06cde84873822df6ef76366592d8b0076fb35 +6c22f1c053314ac47120ee3217d637402cf0149d79863f914eb930a8c05302f4 +6c32905239920fc1bd9dd70c01462ed976faada718169ef28854346b00e9e6fb +6c38a67dc99914be9827558e03be25a4cea308db038621a26bcf113443b31559 +6c39431c5381d577172a7aa6de06d60375d3bf10f1ba9aec7bc0cc7bc1b19a46 +6c39c5e1b1b021a5e8a9378d628183f0f66128ec38fc740f01f2b03b95ac07fd +6c3a64cabd5a3f0cfc1002d2e615ca1412792a79c2313cbf484a81f85efff24f +6c3ec0ae81285b6f344c1f67eb9585d1252f349d64068e82ed482f5bb16f92eb +6c3f37b99d7d3d0b0ebbba6fd5b82aa895124658a62a698f545a26b5d7c6f222 +6c46892024f6b73ec335749f83835a63a5b708275ab9d32f3b6c9087919da56b +6c5cd60e37f18e4499b6346351dfd4f0c72316d5b8160cbdbe6b0c2eee96ff30 +6c5fac9d91d9c8036ce04e07a74042dedb52bed534facfad3247185b314340eb +6c76bf8de298411cfc63c0bc936cd4df38d5ecbb39eb91c9d8b38b6fb448c52a +6c8ff32c42cf5a4f96cfd7257222d6c5f0093d30b1eeff0dfa072a2cdec261b8 +6ca2a2172fa9263668c58f80a4ebcbb6ad3e9af9e70895162d92feee6352d98f +6ca9c928a8cdca347833e29667981c99b2ba87a46fbaa92e555f89a7407409ad +6caf01c70f96e0f295c754bbb87011de732a42721eda1d95f61ee26c16ac4807 +6cb383f7877b15c66b3a41468e1cd22810f66b08f967b36d04029af987577b05 +6cbddc8a93844ef341ca3f5cd70f6999c0ab5f64ea4c2254fdd5cc6947e66f46 +6cd0f0b8c421b5c0318efcb5e68194fed35f5b17f71b443b530eae444d196e68 +6ce40d768a9c1bc8094697274a25ee109b54589de2f96f659eae6d6b1ca3290d +6ce52196786150bfd428a3409c0c2589263d3d21e38b8ba5c11c3af452fb8308 +6ce5282b086acbdf1429c2e888a1e9f9e218fdcf9c96f8066add5fef37d665e0 +6cf06d6405edd049b83fd4f2047b6123466bac304add6c1a2ac1d70f3aa375ae +6cfd0b231ef966cc4139ccd420df8c78f64d19451f2ffa11b7d699ca52d6795d +6d025eabc9a499b085cb433bb89e3978548b0d3e2af1d84bb95950e8d68198f3 +6d05238e22289c9cca433130f6a4e37c4a835ca18c90ce74f47718ac11e02046 +6d0e4f3578a836faa0004f22543059bb96bf98640de5b60b682c4bbe088edeff +6d1059aaa574025baff8b9674702cbb0436a657671a1141b57427899bc45edd7 +6d36dfab38e13285e23d9df40723cb3a3e6a7225b8817eed84cbdb8f05cb367f +6d40cc589bd942aa4f29e61cdf7bb44e47328d62f289a009cd1915e1fe213ab4 +6d456b6f4b73b5e19964bc29287bedea2af1d91af646fe0f34fdc7d5b692c1c1 +6d482f902dc2a906d3327db35537e6ae3c8f0f9b080224c85a566d42a1e7bfc3 +6d52d4805e0cd6dcfc443cb91d1df4368a8ec0a593232729498c643cbc2d08d8 +6d5be59b35d629bf4562febf1c9425fc0855708ba4f4fb7fb96269d58ae67643 +6d5c69f358a95d2855dc722ec0c54042e10130ad2de498cf355610e6345db805 +6d5cfa35db2339724b894df53c83a93c9cb30a15b1a0f1cd1b14bc15d5ddbbce +6d5e47cddfc408af56ba1f4dcefe3088ff03dc750fd570956f995442741c05fc +6d6470236a3c7d20285129f5d5bfc88bdae8fbcbda7a9f7552767065077fbf15 +6d6ac9a05c42e31b007914441ec18aa638e6e8d5a2c3bf74d46ed4ebd8047092 +6d7a36a476c1d8a125dda7efe018b487108ce18901f0c3cfa2c3c98e8697df54 +6d7a6401e84de777bf2c099ea404858cca655b5643822ad455988b923941c26f +6d7da165c2792967f3fd60c101ff6c26b3f06c11cf1f02349afabea9703bd7b5 +6d8588af16983b078f9f17ac7f056452bd68d218545203668ca96ef1ddc7fe56 +6d974c0e20cd2f8bf361508a02f94f21db32c6263620b68e1325d4d99a064f4c +6d9849798cf05f2e13f2434baa6864cba95908fa8c586b303cf69bcf52ec996c +6da03409c657d5e0063b64849e616e69c11bc46974d255ad8ce6a1af546b6892 +6da5c3f6a1edf1076acbda13660e6ba97f1ad42197a7b12e4c519bbdf391f2a9 +6daf5c0b15634ce8756c8d66f4162556deee6ccc9cf3d5ad7f54eabef2f315c8 +6db21be3bb680d67413c8c2d6f0cd7fbe9444b7eb64f19c4d0d5ef5a6be1ca08 +6dcb005df37e7b0f450fe656aba45a92f99de5d77ce3d96850f25264bc8dbe01 +6dcd2cb8fb89118a804454f1b1b48a35bb2995af8605d3685fd1c9258e8f1e89 +6dcdf201b6f895a0ada9da5577127e726c64714eb3135cde083c93d201303c29 +6ddcd6f83a1fdcae6f66cdd616933b7d7e0a26b3d883a09db4ef600e7ce0d952 +6de35e65ce616cfbfb84bd6b91f43dca127701c91a7a4cab09e6bd9e53cb6646 +6ded36d301996d9accb2fc1b864148a428bc48c1ffd53f8477dde5c36903d8c2 +6dfc7f9c23f25d183243beeb0690fc2e1e6cb400d14fb11e6d785ca2ee354e79 +6dfec56e7aaefd80b1fd2dc621334ab9944ba38b7ae16b926f7244a579d5c96b +6e058d102bb752b018e15cb64b604436e84f176518f13f3be77302a10f80cb78 +6e08983983d992d8e89e435defcf1d17fec042594c62674f83a16102874c3f69 +6e0949cf67e40e3fdc7e9585f46c5a7cafb2fcfa14546abfab279df0230dc5e4 +6e1e4e8d01f2fcb896fef6bdb8aa40bd4f2b6de9c2aac845630fe5d5030cdc40 +6e23ecccf332f88bddbb4e83750bf8c28c90d2a4f73b978dc30cfe8f3af1a71f +6e2e112af09a198d82ca404478e185904dd9f90274ff4321edb0d894278935ba +6e47a56bd79d2fc63a6a02cc2f0e6a1835aa02fa1af97b959a4c73ad46bbb137 +6e496600e31c6eda3ef0415349169917cf596b748be2fba1403a8e6f13f4b3cd +6e4f2b4df712bb918e3753ec03a87716317eceb6b8f5603693d53c29de584f95 +6e57c8f620b9c2366353340db95015f613d151cf0b0e27ade9251ffc8279c928 +6e5c68fe951ad17da8bca9868ba86509ab159cf3fc7b36cb2a425dd2de81b1f6 +6e66b28a4abb279c5859cdd2dbd2e9b27d803f36690dc394e647beeba01131df +6e6a01dc3719da6bed956cabc99131c2702657bac7ddddec1d18acfc07059bec +6e73d68710a661aec9f8df3915e4c51b3313e5ee21478a4433e5909369f6d2bd +6e7c0f3649c9fb52580b39daaf318c3e6d747d8162e1591af08c0b267eb03486 +6e7cbabaadf558ee0f6bd14b596aff059e1a362f1f0e76cebfea4dab23e932ed +6e7e904af3ead70dc3fe2592e1ed387f06fc733a50b9825b800f7e748ef01872 +6e88d53a91f84db9af862dac2ccf473a3fde40173048fcbe1707e1d964395ec3 +6e8ce58b8599f409a121ac09d2a2bd0a4fafff8341fa7173ed9fed5216d0a296 +6e8ee5e047ad79edc2fe5864dd628687ab1790e5367e0f926138b1b30ade2445 +6e9967bd11a8e3fe7a2667394207a10c9689b9c3c3bfd92f2d0aa7ee5ab70a73 +6ea6b7084a73c2a63bd47f080c4a2970cfaf4a1740b666f25470cc3ca4785ca6 +6ea760278d11397f67113f775764a09b51be52e46e2ebf7844475b292bca7d56 +6eabe4b592a27ec599e06fa2bb7236573920fc607882733872bcfe3d6d30076f +6ead1a90da1d26560bf028665988b3772693c5b542ccb5ba85c9f0e0bb82369f +6eaf202b6388d2e4eb8efc913f798a780ffe38279aafff93ce58864c3fbf6e3b +6eb08b1da7258310b6e630c4432fbea422b863f0ee2b4dd88c02b29a7694652c +6eb23c2b56abe5493af35c0a7107a80f183ce7bf6789d751314e1f8d045f9ab3 +6eb4fa854f00108477c60d44c036a309aca5258b1d94f2548288eba0cdaf9594 +6eb5311ca42e97627da3079e0e01de23453b94920bed4055c8538a848c184910 +6ec4eb57986e44f71c8512e349b5d408078ea28bf059a2fb956566dce9376f8c +6ecc3091d198fd7c16cdd9cfc7f18921a50552c6749bb9311f08ea4ab563067d +6ed29f24cdd6113e07f1ed64c49558f2b1a9d6b934324f7b522c85f53fa40656 +6ee080d942299f59c4d20127fa7a5d08d28c1dee7ac57a6b5418f2d8f185c258 +6eeb63a333b86c375018a0873a4301ac6422e860887a96990f67d182686e6e02 +6eef11330d97c08d4e4c2969fd7cd86535139090684434f29411778b9b685f2e +6ef15a22a5d1b9a6a807d829cd8d04439ad3eb1cfc8bf742e22bb637ed7c2665 +6efaa1215360fda3e6b13232b469310d9b41a06581b3d3eb0e57f897411f6681 +6efb67032aa7fbb50f7ce3b54a5fbf930ac546f2195f01530f01fc40d70902c6 +6efb74bc76f74afd16971103a194c1867385be5b3b92f294e72d208d55a2113d +6f05733b6aa815f0fc73afcad6c34d6c9add7a81ed4656123aef03984e9fcdd2 +6f07778a8b087d6ca69dc47b3bce6cbf70affb974faafd7c44066c290172219b +6f09cafb873f7299231ba9494c32ae9b5ffc8616039ce2de8c9dadf52e5038c3 +6f0d65ce516c55176a6c7af46480d7b4ecb5e32fb2a2e7011829a15f75852ef3 +6f201745713a581502fb14b8757cd1230065713d65da0a052f868abf5dc307e3 +6f2eb1820aea77e81a06233f3ae5aa9ab4994881732b6720f67144777b4f71d5 +6f3617b21df86b5f4eb3b01ce72bda8a09b005d7615c4dae5635b4cd7378db24 +6f3fea44a04cc75620006e222361dc36360ad3ecb11c5f7c6c39447c0af2c9b9 +6f43d147145267e381eff3110353eaa955569476d4cef32b309d43890e7209e5 +6f4501105f8818e4d11288aead9614dec2861d68603463c3196fdd0a49679d02 +6f50f2f7cf42fb5b34bca004b80c19d85cbccf5db7e643e1e804f030bc7db4d3 +6f6b4bd8ca9c7e7a665d9e465596e7763db9f9261f5ca6ed8a38cf982c244edd +6f831bc07cca3b4926f15efc4addb64296df4097ab29c2a6b0382fa563dd9e21 +6f846674e8d602e524708a4121b2db0573a0e2f1237c33eaf778085a1e27bdaf +6f89bc9cc5c9360e1a83bda3430e4aa4ec30634ce9da0a2e4399bb68cceff20a +6f9e56d17acf84ba434b9860620ad29cece63669947771dc11bf912af95b48a1 +6faa2249290fedf5fc466be71717f665ca9241bb0cce72caad940e7aa3cca13c +6fadad6d3aef25981406769b6ca4da67ab85f991d0bb864e0be26aec5085feb3 +6fb29916a1232a9d30d53bdd0e5c145032208e080677c9895794cb2f26152c28 +6fc63ee38ec803aeb9e8a9287f4ccd6b2fc5cee6fd9ae64f7778653c028e23ac +6fe8dff952607fcd119291eb0b21fde436265f761b729da5c20fe0de238ff5c9 +6feb9c1c6b01263b22d3e7ff691bd21aa13e802beeea97dee3c28647b73ba4b4 +6ff0c24e665a6623a74ea854c8976d9001899ae258781d39ed7bb5c08c0e10e8 +6ff8660378b7a6012fbe6b62dc8f27ef38fca83d91512a7353eb4d7d76e6d099 +700178bdffc12fe9ee83f2094c58d6be40f1c353748a4661b0fe8f2a62e0f8c0 +70356d2a5f5e5e5f11e1c7d1fe820b6de04167918750df90e3a8d07d15ed9298 +703768dc8524b7c1b13017dc46a73c7f128b34c1ee6337985f4ba65960713cce +7043e8b26fe257d26f5409fe566a4fbf4470b36904e2877a2e72b91dd40cc4aa +7047d274601743db36626bed6bbdd703e66269667f1aa7eb78cc5a25e9924859 +7050f641d46740a3416b447e0e702c272f32a1eb24891bed55a78c75180ad0ee +70551e05b4acf2ce4f752dce75c496e3e4af264e28fd65d3c07737e8d930f476 +70584a68c07be86d0cecab6132d6a59c380d2ce117da13ed12c75321aab58cfe +705d9414f5f635ffe146d855f05807c35bc77422c3f2771369df670af3fc0a92 +70603d9a3cb3d7e222020580d6c75af19be0be65f4219c85491b8249cbf44adc +7065e81091ea86fbbd596a32c737cc6872c1b17c574420c58cf081b9e3979279 +706d260542e7682fc87f64561c92e43677418f7f7a303923f15b9e6e69623c13 +707bcf081484016f38c4c9282bbc35e6eed60912b1158c574185b575179799f9 +7080e415770879142efd8ca8456a04fd7c43b2f91444e556d9e3d9fa692496ce +70844b71201744750eab6894c38cdbfaa033c9a2ab6baf818a4f29e3db5a0569 +709568dfbe2ab93b47c16ccd43289e514954a11acb579b9c7219a5206b9e32ea +709e62141a4abf384b03045b4c636cebabf13bf4ff2b28fde3e06c62418043b9 +70aa7a960c496f7acf5d7285aa845bb0963f663465f76d0f42c1da85c5704111 +70abc377372f5e0346ee451d95d69b71db9f62afde8f647ce7dad658cc563126 +70b361283302d1c50f6cfbf3cf5f3fd0755ff1df11a3e812fd28bf728f293f6a +70b8d816bcf87edeb52dae41d1b871bad546f05a680f5489b1c78952fa8a7e0a +70c221ff62c7852566e7c9f4671a7c586cc7658e88de7e97e500ddefb6590207 +70c5665f6efbaf1d528b5bbc3a859a6d02648629cd56f27efae66919d9f31d36 +70ca90a694f55515b087667d16298bdb14536fc4a08236c902b0ad6858643812 +70ccec4edeb1003401f8ea034014678c1872b50524a8c15a36fb5e4259ab0f96 +70d10336907760230d34e147a00a66f77ef4d7cdab97c16e86e6daac9f9a76dc +70d1f0e45f6f2117fa0fae33769d4a0b1c0b580c170c3a764135b1e3af002786 +70da315d6d3dd1be85d534f72676805a9258603d05f12562226b0f4ab3a79284 +70db091f3ca1b2cd0c2d20c7c036c5e54f6cccfb20aeb681c42169af3999ba81 +7102e02fb92fc5955ae6067a613b4fd2b2a1b9c6176c257e49575bad45f0c7d4 +7105fe4644110cc9e0d59295673aaa7225348138307cd814209c3906f0a342dd +711e0666e99b19a4c96294415a840b971f9ea7a14ba990a03645537fa1d37252 +712c44661718dc597a4b32c2bde5de499874a7202d6ef82ba995da24dfd91881 +712e1fdf3a26ce0a381bf147eb918b22020d2cf6d22069714bdcc7e6571c64ba +7130e0b71594642ff221547702a5a9052ed5d8969ff657e4f107b0903f39f318 +71341bf98bfddf152ca6296929fcebd2df3009df7b3776d259091a8fd862bee9 +71365bfc9a65a581b9707540b3617ab7dd939546c878440b4c6d913f47d52a50 +713d0dc01c2c822d01142215e70b3b8a2a6ced72bcb96f2916a6059381354217 +714914352ea3909f6bff77c1c66170f5c39cd1ed8a56c85866eb03be3ec1a07a +7157724c1f9c3a42f41f85fd44842a4f8bc7d49097c99c42f13e8e4ee6e8219d +7158af073d512b102a0c230f64fb72f11d8dae127763c37539b0e1f8d08d95b6 +716e59aae1f58875312128f70f35c1fdeb9ad64043adef70d4531bd83d956273 +717aaaf275c2f00e54b6d701274ee63cfe38a0e93e908511f5053b76d02ec1e3 +7189b0c027f8febda24041185e288a3826fcfbc9ae9d0d6d9775fe00c2d20b55 +7193192ffd786d13f4b1af98b7b4f7d46a6b97fadb9ef56493377317ce57b6e8 +719c5f416aad5aa9f996f70ae01643d68bfb4b26e9253f80ec77da47a639dc66 +719cda0b5838f5b459e782981697cb4348e27840085a6e8f48f4ebbb8fdbb166 +719e766642d03d4b0b4146e8ea779394ff029fe34fb86e6b1a4374503c0e977e +71a31a0195e8fc1b54c103effc4bd3bb87e4bc36825fd707e63c7921a6ccd4fa +71a8be4c4f29cc5172cde3cdc32faee4d40afb265dfc6f03f882c7dc42d756b3 +71b4c857876bad0e308630fac800c31f11eefe922ed2460f0a1ed6b31f29074e +71b60f2e47e25172886b4a48a6b277baa215b5127920384e170aeca84bfa3499 +71b6ba2dd6ec028be5e24314feafd3215cbab5ed95f55f327b153181f216bfad +71badc4d61558b3b4a0cfd0fb5a7ca8bdeadef7c257212ea7598099c2d41292d +71bc66d87311f8fd18f75fa327c536263d42c8593b679b049f5afcfb5f57f380 +71be3e278b47755faff90a871e0e7980f012130608e0201c54668c75b1e3ac63 +71d00b26f98d7e3aa980ab577f8791545a34642e409ba36ace81569e8ae704b8 +71d8a3f48d73662d1a45f4d1b25265abc4614e424acb395c06424ca9fbd66a92 +71e30e8fdee6d7bca203d3761b84bbdac7f419cf2a664befe70782c152d7f033 +71e786afaaae83d3490e38bc1b0130c31ff3e21be9b6d352abb184c87327a310 +71e9e51f05185931cc8835478654376981b57307e0bbc101065d5a48ea7f3e8b +71f5c79d29f126a5bf4c9bbcc72ed81084abacd436dedfb12ffe747ad251909e +71f63ee06d1fb03f44d1a0baa9ceb59b3a2d7de5b449e1efaf8e323a7c15a295 +71f7766bbc5b55abd84100c314cba1513c66a07ac0499b05f4808df516fd4993 +71fcb2f5b6082de95fe250ad8d4ee7db9f5c24bbf19fa3031c21664803aa1bc5 +71fda2f63f1560267dec9a7fb856e407d68aec91138a1bbc9f026c4bc121ef50 +71fdb59b54c51ff6c6a80133fc6c50c4a3ba6675eeb95e3a8fac487488a53189 +7208809b357db5baef7ed8e8f60643b6c9a75f09977332192472a884545fefbd +7208dc776d43aed542fda6bfc718ef1fa46b2c4bd5107d3bbb12078b9c7c1f0a +720a41fee54b4fbc30036928a23307c3fcab8db5280782c9b46e1695da7754aa +720a6af7c3f111d4993ffdb1e8a188bf80bf4545070c6c0ee69d42749b011ed0 +72127b66c29d845cbd6ed8f9f099f42a7a8e5fc77d3b738f7e82e0802e4508db +72162a791bfb963133d4510f60d8d03d9eb93c9873a0def6bcc0a46eb1c00c56 +7219d0501b104b313528f05beac11baf24206f321c1ef7b42417421ac7d9adfa +721bf550cb69d22241351a03fa33690bbc04b91d8556bc3f30c741df1a2652e0 +721e3c630839803017bcb8375a1c79b36f5874c4e72d1fc262cff18cf174f70e +72354b88652e4f6a5b838cc3b75b9a4613239363efdc061abbcef67def686682 +72366d98ee392d507cfc2ab3c3cfeb6142599ed4bef7bd0e0791ddd6b487661b +7236e4ec5b81dad4a1e8494458cfdd00dcbefb9496ef0b4be90194032ce03582 +72486bf7b343dc8ce5bcbd1aff76db1c743023a829e832fcd75ef225a64b461b +7258ee6a213180cf912a34204d3371d0dbd164224ccb6195de1a3ca1f536ea22 +725b386c6429fbfba5d19037f583dd8f0d573a4dbd0c62dab52147b358069f10 +725e73093a82de4c580d6cee07e93af705a58fa66e195b429d758a58d46f68c2 +7261f80ede84be8a716b7dfe735cd9a0a8d5375d84f1ba3a35fed742fd168237 +7272d2e145dead2ffc755fc4679fb014f25b3c46c2a2b8f653a508fef806e1a0 +727461e76a1aa6274367c340fd8ebbd2ea08e9eecaa8ee7fad15da661b95a0bd +728015eb171689d8cab6a91e30c8577383960498a8738d62a117d9afab1cc4df +728138f2e719edc39daad0c75a56a0d3ad6508c8d5670efd87a704cf8b11b214 +7284b7f39dccb039181fb6d17415eaf00a627c219a5c952027329b4da12b29cc +7287892b76e48ac74d8965cb4aeb88831d802d9dd23d41c6dbeab70955672b34 +7289b5c62f50a4fd14715d7c290e62cc0355fff385f7fbc78e9b806815ac10aa +728d2b5f99ad1b649f102ef8b5089438a09ec261ded3340d968a06323a8c4f6d +72965e5d3cb228c9264cb69b2ef17575ac76c7b23df5a753198363e56a86a604 +729a5f2eed46cc929abfc25f7835422ea9169eadb3694c0510ad8fd8d0fc9304 +729b30d0033d00895e749977a7b1b76aa15c4ccdeef83b62f3792f14705377dc +72a1f93b78a47c81913e0d77b5497b7b7b6e06a40f231d18ed67e0a5ffcfcf3b +72af2cd73ff31ffa59b623322d7414969e7e5ca3bc4d8dd9b60a9e621214febb +72b492b110424e63fa71696121ad5593efa0eb3631c7766c7625395fb684a533 +72b7da18f2242c31a6110fd36c9194eb8a5df001eea53108d5721224e54f4afc +72b90086f95a200cfe83450e7223041bcff11d76ccc35ead582c3ab67d4e0834 +72ba52372b344c8bce29d25966b54f4e9576ae0ca084128d3090422aa6b76eb4 +72bedad8c1c89eebcdbc995ffaa2eab422d14e94722e318589e460e1a5351700 +72c6ae9b7a0e57a899875a328b0216fd24a0ab383697f07fd132439cbe53ed26 +72c6b70285a74e45b58e1f9926a5023985190e2c0a8abeef56ecafa440f78706 +72ceb3b1a161f5b3ebaf6692933cd6072b1e5bffbe686f7b854ebdc222d92314 +72cff9780fc45b0565080db857c9f70f717f071e1bd4e8f850e20960b81bcf90 +72daa3df7aec152467bc219a556503860e99c5207d3ed3a8973a48e6a43909f7 +72dbc1bc6a1af1ce97497862a714e1c1ad3723547bcd532902e6b258bee821e9 +72e1089e20763501ab01a9af24b2950ab2ce2424566366d94b967a5441bf5dd0 +72ee25f8e956e6e2f327704f30cda5ce0c6c4c16c361dcfa4b409e59f7787896 +72f267aaa294a358a72860f6c18f64d296daeb1ad6155dc8795ba8e6c71e8e51 +72f7ba094e74fd750448d9f4503509e04ee464069814df97f35a5d69b51e1545 +72fa228c6c5a336dc1343daf666736124f4f375d3dc7dfe7868cb74b178e42b3 +7306e14c516141da9c41dca895fd03a5cf0fc6c35797cbea2dcd209d36c812dc +730ad41074989844ced76260c9f6228b186783101fa3215af57daa0c9555f6e7 +73185d610bef8f2f9e909ffd4342fac7eb98f8fec31f4e5c7f53893a441dc069 +73208c0c4c261a0611089b629fcc2dc71694887fa1c74cc3233ebbe9a06e53d9 +7322a94d8dd10353837d2f202b69283b86dd49a351492a5df773d508b09f5968 +7324a1840685b707a8f2b27dbc2f13e05588a87079fc1d744935c8cc04c2b3bc +7326b847e09d78ae4b2f93a637f3107cfbab843579fd3740cf5f88364511e6ed +7327252c80bb5f73b297ba6fa4bcc420733d0600e098e7e3aac058d0558a6e98 +732e6ab3f6ef25fc8f2f06a4540a98e12d67e4b74c03eec3f3e39bed70c80714 +733332fa18002eff3ad91d66dccda61c6bed698d8a46435a354081e0dccae1b1 +73351ff0f36a25faea7ee68f96d938070d9407124b935010a2d8990802496521 +733c83d7fab13a1e0ab6241fecc2f29a44067e87a39ca4ffb3f2ccd3b5a767bf +7343df444949151c44c7cbaea8d884664b5e13f228c99d73f2adf70a37c32c20 +734a68f79a49f3399fd5c6efbc65b46eaee77c27047bed880a16bcdce0bed205 +734bcbf26debfc54078a1a3aaf781f10f41b702348dfb1584362cd5227998d03 +734e6416085636275476e491acb60354f59a27065396fbac80d764b5c08a43df +73603a7342531d7f577ddcae810bd036d2c6c94399bbc21cc3b1a41f9d698315 +7366695ee304fff4514a7a545a97470dacacff742d3ae5d963e8697d1fe77842 +736a36d09852296608f71378ccd87fa1631c25e1879248296554bfb11307f719 +736db43f385713fd112fe890e73c6acf4b627a739ca0524aed5406a20e2b85e4 +73881e0144969a66fcbdc65fc9344a6bd0a947ad287cec70bb7f7e9260d3ad5f +7389cc8c195dbba6bdfed64c45c3f743c6aee84a7dfb2dbf83bffa6c0b33621c +739af3ee2d8b5b2194e92b519bf46efdac452dd66e81f435a3b4b30d8aa9ee3f +739dffbf0b559afd51c4a64c4a29df7a571f3ca17486ec68c698d284d7d52026 +73a5ed1c294ec74dd1db8e57b801b22e8100e36c7c0be5ecb3082569327dc588 +73b350aaff04d309d4038bb6ac466c07b005cb90c587d8c0b7c8f1ab5b5e2683 +73b8abff9c13fc04519c78322296f31603e32af550b9f1ea443f77853343d9d4 +73c9c34e0238fd179ddcce388500f5e4cbb640ec3da1930f0f051029548f00d5 +73d04484d0b707a5fc853f0d5fa0d62ba060d6a09878073cbccf44f4ae19824d +73d4aea1544a80f89702195473467515490e2dc3da55b5c2e021a1b009f163d5 +73d8b4b32e0357583d373ed32b8ff105caf17182f55eb70b8693beccc486722c +73e581b278c345c0db21b71cd85993a70b9ac3ad329d81ac45c62dd2b50963df +73ede197bb8d74a19199fd12fc6a0ee7b163864e314f1334ac88170be10a88d6 +73f102683d14b8eb4594e3885ae495cef45d365b69dbd94d40ba792a01e3807b +73f577c43fb7181ec996210702a65c95d33799c3bbab9301dc50b81b60287d26 +73f83d0ad7a593ffcc870cea197414f2dae4b688ff74c59a204693e0b2132ebb +740a5ca588930dbd1c152c11b08d477986fcaeb74238e1dffb9c52a83874e7a1 +740bf5184d28f0b47bed4d45abd6e90f2a398e50024381188cd6f34d0cf82308 +741032d6bf99883c8acd90c1aeb04389f87897dc2e0e56e8d71b8e9a31207ef3 +74226bda84a267dfbd85e158d9772191e03f8da85b835315d73073cb2f599665 +742880a3809163ec1325edc364743687a5a838b564e5c7e362a8cba8dd327218 +742c44cf4e6a74cc10e8964ca1937bd0dfd2d12d9b29f21a8a4958e76b9efccc +743234cbf285bf1f719cff2d72e09835f49e9375144144469cabfb4a5c362dbf +74324eda97ce85d5318ecdbae72b98a3ade690a4057567f41ee1089f059557d0 +74336895dbc2e6611d9f7c20b88c4968aee3df7f6bac0f04d6d4895df45bba5b +74419d3cdfc909aacd0faab34fb98b6c54a402e5ed234b171b9fa6c6de7599ae +7445011b4c415fed9eb5f2d3fe771c31cdf4b2ceb28eae603d2cdf9db5e47d16 +7451992601e51f6cc4f7640c1ed513dca8147ac9559b8b6917a301c4839c8f3b +74551b22371ebc7a09b1d968fbe1cedef7fa8ef1c20dc7b9a743b044ff98a4a0 +745a8f695d6fb65679f61069b1ca30dee91a588ceb65dec06a9cc583877b5aec +745cfffdae66b415aa049a9ff5f8ec3ad173f2805a328d84bad1995647f4d750 +746ae252bf0b78092f6a9ad3fd59389b16709d70fd42814471d579c1accd7a17 +746d503332b46bd09bac28698688a88882e8387f0b27cbb731a9805e6b7c118f +749469924ec91633ae851f821210e92f5bb3facf39863a6a756dbb95142285d8 +749c80ff3826267667d3fb28fc97f100c1d8a1ef3c1397cd6333d4786e7f6e9e +74a0e9d9da7082a234129f4a04a4bdd8eda8f0b22dcc7cda10feb0bfbbbd0d3c +74a1012d0c227c8c0cdda4f4e8b12568d4644b76f6fa822bc1ce167110a845aa +74b4fb1aade46de01b879cac65ac3ad5d385cc1f624b9d426abfc0c48e355017 +74bd769fa3f961fe87143f1ea994da4ac0a1bb5806f2516568f2691d2f36c541 +74bf84842f10da466038a01912ad6186922faf576b646fb3eca8877e8fb64e44 +74c242292961f2cd5435e90ad728d097e686df530b83a5f13053ebde372e4572 +74c41ba047fadfb54b30741bb8ce4a4102bd06c9e6ac4731637f39d871843749 +74c56030bde4ef3bf94ed8c1aef481b72982c5d9e03a8b00df0ea495bd6d5c85 +74c7512336acf6430e1501a437f0a67f7d8579bc9ab5abac4e9893c59d831b06 +74d734d3d12243b3957368134ba15b1a63bccdd5b60b6fac13e617a4904bceca +74dcc6163120deb24bc7d231d8c3fe440fa86b0d6d6dbc77e4cd5fc789fb7ca1 +74e6fa70b385f11cd613ff2f48576c7355753d95a600508ca7e3a42fec745efa +74e9317bab0ba376127427589c943f33a59e4a4b44738ef82d481df94cf76493 +74eddb6c644900a35123af5c6ad5c0a065ada87b30045bf53ca7bc0ad2b2e395 +74ee9de7d33090a0e4976ea60dd72d0eb12604a1e96a99769ab51a3319cd4ee2 +74f4755060a5028ef297f3454ba1dde4e15c70eff389108e57fdade96f502126 +7501abe7f93f54630b087dc0d221ebf76e9e51cf80a07ecc7c214f5b760d25a6 +750dce75d5d74c56895123ac59baf38d9e81969dc6ae113ab5bd154d5ad1e681 +75128e208dec6e592850430e0c519abd3688de14e728a9afd15679e5eaa27044 +75135a28de4c6e022b29f4e0fd29f4303303396b33f076f57655feffc140560e +751c60dae3ac7aaf08c28750aeb4411ab8a489c3e967dcd08d346f510c43634c +751dcf72cb2c29e1d821d359df482e021c941eaae076ca2a2bcae05243126580 +75216bf53d16061a2338b9e3e69324ae741db48e6d51b2fba4620e299efe9433 +7525ec73fc824a173e02c700a9f54bc29310979d5f299371b6022648ee345a16 +7529ed145cb76c4bdde68d8c9028129f084e8a930725eb8a0a742b4d9b9e4fc9 +752bc250e67c77485a325d8e80f9355875e766dbf908ca6bd4484c127ae06202 +752d37b99721fd7096fe6e471ef13058110018bb1781d0c7f965a1a89b355586 +7548481ab9118273951599b35423c58230c38a93d2b4f76d0c3a7221f74221a0 +75550de7fe6950dc90660eda16be97c51dcfa65814965cf7ab5c074fb3e51dde +7559fcb3c9229e9524090d20379c5fa97f0932da8c8ae064549bbac55e06354c +755a04ba4238a1498f9a3a2846716e848514a4af7388b1ea32d2248dea71e14c +755a4df12a42f2ac316770b0fe9ae241e984cb30aa70d15ca813bb678253dfdb +7565f6c4f70fa1fe99c9c161e05a0fa795705ab5709d819b0093cf5115531cfc +75717c7d000a525bacb932aa35f9c214ddfab4db910477ee2b465d6612a5e0b4 +7574ba005b332231441421f1aef1f40bf5b370da63713454fb7c6afd8c883306 +7586686d7f4e224b2276a55e687113d8c856cf9fb6d2558651a7afa0385349a1 +7591e6c080b97cf9109387a2cef0b27f052269b2258a1604da25a12e9f2220a7 +7593e3b4a672ba77b7f49b79637ede2d6ed85424c7381fbd8d12f0b7db0e7996 +759540022bc85ea8fa16714cd5b4e9e59633268f46f69af2494904d63260730b +75a3db73d237fcee6d24f4d8f5e24778a97af54bae685d3208a8a110ac13a1fe +75a5096a379a751b4e36513fc3dc1a786444836f10ddf1208965868d10777c6a +75a61500f45081b688c9d8fc932417d3cc73657fcecd88a9308abc6f9113def5 +75b79856b1206ce2a07e9126e3c9992b814c5ca2b399f063867d47883eb7a7b0 +75bd9f8d5260e620d4ef8fc81a1cff0f3fc27fffa1bef32a8ee4ce169195b414 +75c2babca32f3317b6e56ef24dca37407f5d4114185a06c16d03ca9e0814fa19 +75c9e25149bc0c1033078b08b661822b2de4e2ea9375e7271436122714e6dc9b +75cc897b1d23f7179f17e9834afad2f5c95bef0be19aa7cb0eb3e6aef42af697 +75d286a7930d8539c3d406f0425a4193851b4982caf613aa719f0ac9fad959b0 +75d32b2c67b9006ec7e649f2a2ad059e86dc4d65da4a93eb7360e74b05010e89 +75d6ee917f302256dcc2744fb6a26dc798439b4b377cabbee225a35ad3375b86 +75d7507bbe012ee7113033b86c8d5b55bb0a5eeddfa3e1bead1518c5b20a5222 +75e7cf87d48cfff5d715c765e05139c872f9a04e18c35b7a945fb232e66507d3 +75fcc15775779241ff00b494970cf9809359a4b0510b7ac1fd9799a6d7d4b62c +7607f223203f978f2e2a54c8d4b3c084f0a33e9b5a27c1fcb2eb2124c5b2e70c +760cbc56b6494ba5127e3a3ae768e45b8327d06703d9362b95c4795b2dedac59 +761bb00f79842b3d45cb6554c1295e31dea9ab0e0774c648535f02382a5ed986 +7624e1710c3a12a936479df88ff0ea37fc77040116ef395e0054e809f946fe93 +7626562b03e7eff751425ca2e945871f158ab4442924b9b59903ea7f0c589a0b +762f79c8b5d49db05bba300cac24b34abdebd2cb71d2cfdb6a440175f79411dc +76322dd04d4ce9b158d6e99d42e8a37ddebefa7aee2e59ea61a80217f8267401 +7633631f3a349d443788a64779694993ed90ad33c070e3ff43ee32c54fd74ff9 +76392155ed084feacda86e90d6aa595e392e577d2f9c2a4acac9c1e6278583ca +763b1cad85fcc29cafd79512be37063294eac6e62ef466b87cb92495da62692a +763b53d95cc2c1365ce8228350407d4d8df8d1bd8d2c7cc8e42b2b5ba5b7f7e0 +76456c625c15b931111123bb47deaeb94130ba891c70210651c0b59a52215c2e +76530ceb1bb03e8d1c6b8180788da20a0fa7f8fdc2fdb443daa91a4eea626799 +76664abcc538931f041002bc31c96cf9d17d16c8bab218f7f2ea3d4b91e3313e +766a06ee2b919eb6fe5fbb29659605e3d5de786df549baca44bd0952d9362c07 +767376379c372a818a2ce625fac0d325f949935958f158f460b8a48522e71444 +76907578f67f33a1a4bc190e12b70194bc3f1ecd0eacc0ec72e3a29f73cde964 +7694f37bdf8fcad17f28be162c5b646689f79681022de86f079580b84e34db93 +7696351807b196c1b8c49cfe466a5ba16e69d027e79926ee02d45c04918b4480 +76997b03ffa2ffd0d0af5d3f5705721a24cf94e9be59f595984e89a0f54cdb00 +769aeb7aace3a9ff8c71010b0aac6e7667306cb95eea69d758ad651b4684f158 +76a1c19725cd1ff4382ae7244afba9154b44fa374f9efb32143b46fbac32cc24 +76a3df426c078efcacb5009a035a391cbd7511a0218de39ac5483ff02eaf57ca +76a54858e2745093fde89bc6c921089442c93cfaf1cadae3e8e548b77b469ca0 +76a92143c4761d3494d2f5a1be65a72e433f9d9d524ec2f75b6f5ae786b940ba +76b035609cbea3cf30156de7d6dfd8804e75c32d0dace1b39e623a3f9528aeea +76b6367f388701ee0378076f29e67fd0c26fa5664a65465ee930ca54df5c91ab +76b6b9ae57625fde5f28ac78b282ea46c56ccc5a474d77c20ab94c84d1bed63a +76b783695cb8b7ec37d3561bf55b5dbe4db1f8c6c1b7875cdb790f398e068654 +76c241c06dabdff6f41fe4c614149603562342f7503b8279cded559812622048 +76dd0a7f5188d5ce9042e633e936ab6b2c8a2e54e01d988dabeef4aeb8fb49bf +76e0fdb5bacb66c0b2b25fd1d0dcfd8b1b32968a3efe903408025f1dd723c518 +76e10860126d6992603482e523078cd8c6f2f98700b17b9d44793c2e24022414 +76e6936a6ea79188f00c33ce57e3596be64a6e7cc642f6c59893948be9ea0024 +76ef68e4fcddbb2ff7aacb41bc303efe527261dfc79545b392803f36a05df3cb +771d2d17e88129bc84f563e063fcf74ddbc9f079ffbd1ed062a4062bcbc4bebe +7738474b01b167f62d2b79a3e0325cc49056b13a174d740b3e28606d739dc373 +77485d79920af39d6f66d1b2b8665a0bfb76b3141fe8f0e892b6ff2c948f0d78 +7750f60cff073e07dd86ba2079f8f0ceda74ed213c79d930039ffe7de86d7811 +775306b235b8bb6ab9b9fccd72d53092dea923d1722dd87ff246d1ad8820a82e +77550ccee25e398e81583dbbfc3b8686483f62a07f2ee77fea588397f4b8a7b0 +7757dda644874d2dc118e704319833bb7bed0617542af13eb6dd2c711c1c8ee9 +775a8b8274b12830866d12f8aef3303c586970500b82a58917efa0a36e97c77e +775b4614685d28035cf6c6b13c188b8c261be8119ae6f1c1f59826fdd2c19d58 +775fbe305b56a0ec7485c037a4857b4c75f2d6f16c3823b33bc14c5dfc306170 +776a2e5d59b350634141d1703727a0b5cbcc34a503bef67e94c9c17ad006e5d9 +77827cffbeda4a0e964a7060255043b03cc6d98b57686e42bd83a7415cea2920 +7785af6e5a1d423e2ee75884be135ca37ba492bb799e049d71256d7364f5a726 +778f9dd398f07a18c4ea70b2483302fa9c8e71c022fe1bcf22b72d8b02525f9d +7794c6d1d257ce364ec7f717dc9fbde3402913e11b07993995fd20ca667f7e0d +77a0eca3b262327e33d1f229892ff07b2f20d41868b452e48cf6d014536d876a +77ac42f34921dc60a1c884f3bcd755a961958f4c5ef2f8f320f353ba1d188cee +77b1ba6f726e06de15fd9248fe0d7dda7a3b2199e9bcec9b6cc9b1de552fa433 +77b4de0e14c9cbfe953144684e77c113f7bb7cb779f79828b0c3c02dbb36c884 +77c46e45eadb3fd4e9dce0b469b548c18c833b8a6fd0eae802ee97f2bf42c181 +77d51b2c905298763c3f812cae659c368db268b7b04379e9afd33ed3c4193903 +77dffcb8a051bac52c6f65daa36251ae954eabc11d0277950f6df37b236a8d09 +77e73fefdca20b4ddfdc7f843eccc02224a6528a4450aa6edf7f369b5b7e9165 +77f4fe0092e81cf2b38a6585d0b482d3e60b6abed3ef068ff74fcb1f2ad48237 +77f6b9a7dc3c9303caf10db46850b22a5e1b13cc30f05b2933b6c6524dcf97c9 +7801e18e92eccfbca6f087277f006839a8d9b4995b710d1e94e617cc8d9d7866 +78065f75e9682979f01e70c2e99a89d3c709f81384557f8c92c3e4c1fec07079 +780aa55667a07d8ff28c657fd8f99142e71960a846e53ce7fb78d44e0fa2b6ea +7818f83ed9eb377600d4337074e34f8ef15d146b40defc5dff1a1269d71c47ab +781d8240f9db9a1cd229040dc43b805a830129f5fd6902900b98efadfcc60572 +781e84537d32a1aafa57aed7dc60ce0f837ef42eb8fe7eb31d0d89c645e3fd68 +7824c9440c2d39ed5812bbaadfdbfe8c491a9bdd1e58160fe95e57446eead195 +782db8350cc9eb5fd9b21c05f127c68b47d781548d9a4edd706e7fec11fedcd6 +782efc81282f92f31169caad53d7f1bebd47310b86be574845d636ae6a69c0e0 +782ff65828c5bc6e4fd50bb70828ef0ff8dc1912235abb134038322128d9e97b +7848cbcf9f084562b0249e54fd367e673e57755421783132e5c0db37089b79e4 +78503689a77ae9c290d02de820db7f108d67f84218c348f7004d57725f3e6c96 +7852c00550e2c09054a66b116e5c3906b2c3ba34de58de2c073f06abd9f3e83b +78654c1b73423ba84dc2b209cc1ec5a3d849f04633315acc836431aecf10d95f +786f350e4cc86935481fee3eb9e7e5483b0a1de7d0f819840416e1f3d058f47f +786ffb58c245297d4192e12341cbe02923bcaede3b4c30b9a872263323b1aa27 +78786fc42e8cb3ac162ccb40788aadfd99d1d914f698f7499cf6edd6ad9e5c82 +788457e99366b391cf241fe8ecebdc23fdebcec76bb80744b0c0579e195baf22 +788ea884a4d6966b3c21428c6ef3fec89d258db6d7f48eeac35aa8622cfddfaf +78a12a844800dc13363ba2b116cc57a1e04336bb1488e1364e4477bfa1c84718 +78aa1e6e9ec3d6af7d987f854dfb3f0735d2ae438e995311f1b2a9fcec035550 +78b51d7657310cc0e326edc7e43a64d916b5a3a38d3c9ebbddd42206db5e3e0c +78c0a70df20a356e8a67ca8bc1724e63557b6479773081f1823e61f5dbf01a89 +78c15550850d24ad16724cbc573869c940c701de7867a605a8eb429690aebb69 +78ccb10433d3741c728d78ae859c55cdb8090adba9f1a5547abb714f6fd46c87 +78d359beda2c2f703a07a7c7af6ffd4e052d336eb140e8d1ff88ca5e7338a7a1 +78d3c41a319f84565c8423023558678d52ad628570ebce28758c7047d320edc4 +78d43684db7aa8f0c899d8886bdfcbc5f9c604269d40eaa2b6290dc93b4c911f +78d472205f1b988812bc7d297d4bcfc0bf795e9e9a4aeac7af3e767d1ee85634 +78d83f03ef888baccbff2af28eff91a6fccd16c35989c1f8cca9762f0e001dd5 +78e501b28b5eca4d1ec50b3130d65e1850a76570b230b992ed93b4b2cc518bab +78f41c7d68713b7fc84307ea32f0251778fb309409a8a64cca1f4a48bfbc9248 +78f65051f5f0902d8ef66cb921c874ed0596282cc6086a6dc572066441325be8 +78fd99e5cb7c8b5a12cab7a854789b5a1427568a4750f08f01f211ad1647655c +7906458ac8e27a25b24cd7a1cafe7fa2e4585801741ab368c33212e50d83ec03 +79185162e055a04dbd00ba8c6604523164364831bc06779487ace21be0cf4a0b +7919fb8acfddde49d406187b9ed2f466dfc62ecd5d81623c1e159806adc5d17e +791ca6e1b6b021d96d66ed133366bdcc4db972306d698eb80092a25637dec999 +791f9d7acf60a6256a9fcfd79c271740c005d7f91d753ab2a2e55ef529ce0a37 +7921b75f55b1d8b4bbb60684b4b226ec3216b3b1a58f9f0811f4e1a8b1e3e951 +793311b8e13f864b471588c5e4e8ff1cb31ef2d7515cac879d8fa576df0f1f8e +793cf2513cdbcecc09639beecf78a4c580bddeabe392082d84e1c563198da512 +7944475889b919bf984121f0cb5842c5e6456c84b2b84fc51601a794f422abf8 +7946eb6dd4a45fe455b00e8209d0aedfa9a8658e78325fcd57cfeaa1455fc0e9 +794738dcf74f305d76649a8cc25cc7dc8597aa3cf0c5532b9f40bac63dae7584 +794de10effc8f6687f19b9d7ce088d350f992b9cce7e7fbc88715a1338278fbe +794f7f552c71747605ed2a85264a762871d7ac74e68bf2550de34093d145d245 +795448142babb02766ed68dd1b748f211f6bfdd0fbe03ffd88fb76df75a79330 +795a9447bb82530375b36b3f740ca7db8dff1b52c5b2857293c0657ca9a2ab32 +795e76cfd0477282dbdb22f389a2a80574c0b7af1f9f95a4f9e284afe24e66b2 +7962e5a30e75d41f72a0b88a2c91f67f47ddf003ed10200925bc6eedf611754f +796567635a55ce19c1374113e9575e4e46e6e8bdfa64bdbe7803be49abe23c30 +798e46492331cdb3331e1c645cfb0d0073c53f41a2dba09028aac5a6023cacc0 +798fad17531a347b087e5e7829fe2c351272460ac1b0c3c72bdb9975b017c938 +799b502a19379bb2120f0945c005b9bc7a12a4324aac0abad961a8a185ae49d9 +79a286b8fbbb750157e4bd785e75d802f51df378a1ebf1f4627c322adcdefa11 +79b002124e016bb884b2b4acee5ddc4080a32f3e107ba92e1b66a976597cae5f +79cb081c423b083abe165fd321d6427bdab670049c386da1b47db28462ad34a3 +79d30b55d74e404357f61caf8b12eb68ff888f1a35351c56bb26882bf3b29b8d +79df779acc146ac7f75db4207351507167c286725a9ff9bdc663114f1befaf0d +79e8a9aa04a211dc36bf1354f343592c06e5f1d0ecad00e393bb30f2d013a4d7 +79f5091e3fede40eef34427c8e7eda53cb91972e61d394036e2e76014dd9a024 +79fdf823ae23e498618685f3d4372b1200f85714ac6312ddb15ac5624ed8b802 +7a068e374ae976916222b1ef071f0151fcc97cde8378c12f40365e8065e0b4b8 +7a08831e7904b5f98f9a994fc7a2a93fe2ed8bb3226066c465e5b9dd54d4bc24 +7a0b7994b34a28374fa735752fa84fce7bf1281b91d99338d67514d4410a7675 +7a0d504329f76fac1df729fbe85b9194b125ffa98ffe90bd594ea602ed34f09b +7a13e3838de75bc597f760293f65afb7471572a6550e3635eee11349a757d2b4 +7a2185c1203a9b1b65b087925d20474201b71af13ab8ad70f4443f15b2854004 +7a2bfc4ab60f5be014fcae73db19dd3ba4315dd07450cdebef03cd5f2d6bec92 +7a2d400c67ac8d1eb01ea794f2fe1403ffa575fb32976987b722d63c32628ed5 +7a2f867e913bd2e726699ee77b0c61ed8ef6c948fcc2e693b5efe5a67d0d2e0d +7a3c0bef68b743cc1311013929dcd9fb098d898ae092e1ba1f00ec52115e2c1d +7a3e78d6ee5447a2345e807b402f5b50201cbcdbc5ac31b9c570bd832d93e926 +7a4083514ae9a2075832e2416523e7ee804861f8ebbf4bfb84ef67bfc6b5dfec +7a488f9aab6f276dfd1000dc7cdcfd2d414cc00ae1d156b713f82ac6792da57b +7a4fd4f06d20dab6ea1d9c79f5833d38edd65a545425d2c113555391bbb544c0 +7a584b54e4f29590ae75d30a809de4e9bbb23ad6a5b20377cbb4f0ebf2a06dd8 +7a59f6889f21daa39646acf772444d299fa8cd7553d40277f992c1d1d1fb1824 +7a7a0f3da3a9f3021b93e45a28a011b3f559cfda78cd17983a36feed63773e02 +7a7ccac5476621fa4b25bf277e8e9f3758643feb43b18cbc48f3cf694be70b74 +7a888d37c7893d03fc39da69cdb08736fd97e5c2f26fdf41951810369fff192d +7a991140d2579e66e9be48252ba369c48dbbfbcf68b9a8baf09e895932aa5d1c +7a9a4a81d4e6ffacae196b3490b72b4311d72e98c9891f4741758cedb20b5098 +7a9d1811d097dae80dcf4a18de0350b7460a04c8c51d01dda652878c2c2a10a8 +7aa05b0356cf7ec8e284e46f3f5caa060bb37ec18e39bbdd129f1a824362af03 +7aa9d3c1410220b047e15faa9c7d12a622d80e1fc6ce428dfd1a59cc4c1c49fd +7aad8014b05688606f0373a469559b093760da9fbfe2f2c233456df66415d574 +7ab159354469d3e43130e77b6e3672b05e379d2f38e665d691ab153bb672dc39 +7ab8203583159b35810cc08810a4b216b603923cd4c6185d2ad1b4f2c4be51fd +7ab9d03146f64957ed3f93fe7810ac8914e081b0bed0a9a40a1c782c82bf5076 +7ad4c1e481c301b89038c08382afa6881d2072d9695b0889805b307988b0c2a5 +7ad4e5c3b96725075e73d07e3668ebdac19da74401070dcb4863d0ddbced3bdb +7ad62d0b02b7e2ce658407731ee583cfdd9476bda39475f7cc38fa7bf80a4cf3 +7ada745728d3adc302210d2e0a383a5078c5b2a9ff224d6417ea1a82169824b8 +7ae193b494f42df4f0802499a99ff8923a40eb8609c13611dfa0d64a0a3e1a9d +7ae2a4742be7ea0190d28b5abcc69a46e8050478bfcb05def424324875784de6 +7af61d866fc90d9898bf7abbff6e349ed4b31545b5c3b9bc5c25402becd2aa67 +7afc8b0332be8725c59d22df3bf8d0220818d44266c8d814823991d24e560d3d +7b0771f78575c3780a481d58735d96749bcaa1d191ba6e67331b2905dca8e075 +7b104a8a691cb62c0b09767cdd70e9f8a818a2ca42ba526e4a5d07964bafef02 +7b12cd93dd2c5aa2b10f0fd21cf634efc3952f98e93ffb22b2c6ac86fdc0c299 +7b15a66036efbd09de2973a9e23828eeae94162c18b1dff57a09dd588a2dc42d +7b23636cee2c0cc654650d45a9a1de39d35f97507079bc45c910d322020b56d5 +7b3916244c6b12621756530c9047b2992c7734dd1e294b44c1ac298fb6107951 +7b4c95ccc04f82ee95a0275916a9683072399fdaea52f668a1bd6682b191ce35 +7b4f585c3379620045afb521c99296592dab858c6271914adb81750a130b17bd +7b5a055baae02099795d0e14c3ff0a6a1862c52b3ca2d8647def2a1f5704e181 +7b65ae179d014b7313affdc284a16ada85f2410301227a9decd9be7b0deddac2 +7b68beecf2a446863bfe336e71f6fdcd3aaef7ec5d2ae2ebce99ebea2c16cc79 +7b738179b255f64628b24c19e59e0d145f434d3c0dfb019b7572cdbbc93edf1f +7b7ef2468fddf757a15f2cf87f6b99d5f49d28edda4f2d751533cc9c58bce39b +7b9139623dc1c6828447a9da807abad81653e0a94d54627f768a2c62d4a3ba38 +7b94a63d2d9f3c6462bab7ce465da8dd3d76eee0e8167d9a68e748647fb63ba7 +7b9558fe45c41f1ff54fffd7f76611fbbb13094dac32c303314cb232a1b59aa8 +7ba09f0a9735de0ca1cb3d73da31c30cc61f9ef9c35419a8adbeb97ecfb05587 +7bb2bb3991372dc272d1742b0b7435c2f764ef603e107cb5cc7c41411dd6291f +7bc8f10a210ea617c26aee80bc84ad86a608821af4d1e039617e775acfbca8c3 +7bc9424a18a6bb1943b66a327647381281152037aa64081de5945e314038ed4f +7bce4d4ed5bc5c86495cc2a25b6b7b2f8b8539e8acc9e45e1f265fc5258bc319 +7be42a010698e525201f3da4d6be7b1787a50153737cc0358912b158463ae972 +7be530d62b2c8d768000f468d2174409d826fd7c6d666edfc2baaac26dff2cff +7be5498a7526bf2102ee286ca1d33566cac3208d56d63687dcc449d7ff2b4b16 +7bea56c1ba8319e03e5e0a1ecca7de0a90211e6b0c221ba27826c7907d98c9f9 +7bec6f20ce87c99d33071973220f4287b28598235899fce20638d1f2fb4ced2f +7bf7b5606b299f85efa2922c3635392b41553b9b0e7e70a9c3cdf4e645359520 +7c0b86848bf6292ff7239b936ed23d1de8666a4c8fdc1274ce42efce892c4ca2 +7c17011ee3547606760dfd7d0199d4837dd75fd4a7259b2a5ccb3574564df3f5 +7c27210fda86ba1470c1eed1ec37f157612a3eaef5e3be12ea91ebe5236108f9 +7c36eaf04bdd47bf051dad9d0981b39f0c14ecd88cc638e98df8d226758b910f +7c392b377e6b5f78f75c583c2cfd61659526f3ab5242cc276eda24e70a808eff +7c3e99c6155df2ea0599e72c700a15540e9a00267063f2a45329d0744e2db4cf +7c47aa6d4365807a10152a1c7221ef01e924518f78e5977475370517fe2ef8bf +7c4d1fe8d03b437db3a5e9aa9d0c0e3ced11e525433ea6b203feb41126f360d6 +7c4de95a4953517f3ef6d115e078ddaefe1666fdf29539ef18b26bfe02c43e19 +7c4e72d1a54ac687e32a34a50e8f1161d4220818fa081f54bf1569f0f781f7af +7c4ed3b1fab598d8da6a287be2c5974892f08e32f41d7c274ea6ac0c02a2fe17 +7c53bd4a1ce1ef7a72cad4d0a3e91c30f8b04625d2166ac21d7eabb986767340 +7c54a124067fb9a81bb071edeb60ffcf284a53698414c57e431c06eb0b3325c3 +7c56950f06bb00651f5e56eedaf59cd11682fe99a175148aafd858f037ac72bc +7c5b777f7e7dc09a91baf491e9a3427a9ca754f426ee2919c4cef52cec574a17 +7c5cb2903711e165995b11a6bd6e5fc5e47b454b36d0ed4b8b9ad8ff45127a6f +7c5e43554b5196aedd448177c79c80cd3f9cc16c86adb3544ac5fdd87a5d93ab +7c6804cf478cc191c7d0f092b73a218f6987ed9172033640121e2dc3a1723794 +7c6e487edd42deac7f5d4728cc9da7d8ec14e3de272a4644f6ed1eb597182a9c +7c703c59fe5eb580a47bbf8eef99e9bb59fb16fb0de7562ea1010f0fb0c793e6 +7c72f8dd135ea8352eed18d9c062ac4b0a6e0f0cb37ab11c8c8ef438559b5563 +7c7c05cab9c63223344c34746847080a34e12a73448e8e0b7ef0db43ed57c5f1 +7c8c1e845617023610ab5eb8b1e3bf9512d0b607143cc21ea055a2db7e5d42e9 +7cac30a06958aa0c4fa65a8c517ceb56439485ec54002a4c597a2955dc2aa0c1 +7cb8481aed6cb01404889b7322a681c5ccd3b3ab606182411def6d1b433c23ee +7cba48474900a9675a2b6bba57873cbd7916f5fd4afe2591df7f59a1b73c0ef1 +7cc8331f8f1104272d488b6b06f9aeaad82ca61fd6651dfab79ba0dea308a97e +7cce27dde79eaef3cf2db001730ca3a36b65465c36602ff548107c9a4bdd5773 +7cd8c862469178587b4d07b2b4e3d5aad003f7e147d9cbaa82d981c2f8bd839a +7cd8fcdcce0d1f104ea0215336d220d316130d868194ea4d694e13e9c74b85a7 +7cdbbfd7f57df3b798a3724c0f8ba1f058b422eac0be14b05cfd37b76e715eb2 +7cded0af8f2325fe6c9e0b775eaa69776d939d1d6e38df6f759cc5810424d9d4 +7cf68b59699ebeb2d9700ce205bc91ea2715e6a16b4a5014d148c601fdde8d83 +7cfa33904bb136b473743c6632d6f788e1725d589fc9d6ffd252cf6efea92236 +7d0ee206097584350a0240b7bb602ab080fc7fcb51f240ec78efff9a0f81ed91 +7d0f5def1b5dacc2edf43b5842e361ae55d56c4849657f0e573c7e561dfc3b32 +7d100eef87d040e0b1d5e95c88760189c23d31dff943a4edaf1b789d46c20383 +7d2ad99e6e7afc558b58ddd592e912f43bdb36f85abce484d6be096da543b2e5 +7d2b3f96b1251e45687258593ef5553cd484d129330522c26b66fded55eb3ad4 +7d2deba91a3f29114c2b2f1689b1ea37fa37f093b114fb8412a6e00f398c28af +7d2ea24fdc50612b61d67aaac4207f38e2792fbb4344e561b6b50924bb4d0162 +7d2ea6a6b3c4b0c21b589cb3ef8cedc46a839244c888f5eb14baf3796540c20c +7d3629a9ae8766fe317620c9eeade901010ac3b812317fa5579929194227cda5 +7d38ab1c4a94723f07f7152914f98e712789526d1fbd9cfa30fa79c7485b8f40 +7d3df64c162da15f9402d140d4e366079d1c891315e6301c144c377745239336 +7d404c1eda2d7c7abede6597c425c0268ca5dd2d61e7bf87736f301236590a57 +7d414ea21ff5fbb5e71397d9849c8e9c87092a0ed65ac8a9eb87c4d4ed42b896 +7d422f326768cbaa6adb4815560762efb60ee6d6ba05368dd7e92058f040b677 +7d450df2a6ca07ad73cee558e35453afb2a90fd2f43fdabdf718128949c59f3c +7d4c4b44c7ca20c70dfd53503493e00d11a43830f6df730ab892c2ac4e9954c5 +7d5233fce05cd9b71b752a98c07297849ee55cf7cb44049528dbc877d3998c20 +7d57290f7448e87a9f10eebb28817c551468176f2a06341c15be6c196369c3d8 +7d61562cd01b05c89608c0f5456252bda051ca84454bc43505822915cf134a50 +7d7d052c7295ea13f0e1f9ef48fc8b0eeff17950b1b881728f50de9b6d7bca70 +7d7fc9523bd06788d5f80cf4a33e9bf9c5da0cd3a9d8e44123dfb9e82d50cae6 +7d847e23a78ed67119b24e8d24a191ac156a4fb0ab0fbee18933edf56c1c35ad +7d85c415df8925b4a2834069bd473b13a96375e03753892bf25ccd7ff3ad8015 +7d8f4e7af272d76e9ef6ab90f7a7a46881befd79b97a3267564512780dae1f80 +7d90e578496c8040e2adc461d47f160bbb09d02babe50cc385e8d10e07883079 +7d91cecc9429bc47d7b5495e8ad367c021b4d2a8756f5337cc32009a4fbfc645 +7d93afe9fda9072be4bf9ed36b4a0e44cfbbe3bb91162a0242db965dd66298cc +7d9d17063a955a6f9a766a08478f7b4603a4cd287bba056f777db0ae0cb8b0fa +7d9eb7c67d3cca33ade3f74080981c49496152e3cf8c6ff1dc66ecf581ee5e51 +7d9f4b3491dededb7620be677645078bd49c19c6e0c4dddc3ed3f615f7cac00f +7da31f2b0f820272e236b187dc7f71112ce3f85518a37a0aae7915e2f0e5fa68 +7da61f5b5c7f5baa6c7a94f971b465f3a123cd10f477a6c18837f5bdbc225ed6 +7dab4072440fc200299b683a022b2510937ff90622f3535a940ba97eb5b7612a +7db6ce4dfabf251bd5bf04627ec048210d79b1e5c357589c2f75ce1bbc2ed9c1 +7dc5d473825448b9da75a92da6792a6bf19a90956b9acb405c918c6b3ba09658 +7dc5f407ee4f5125f2d56ce55e735ef96a03d959ea328323053a32ab2af53678 +7de8146a47bc379dc024c1b426ba643790480c7fe47e9838e4ad6ddd6ab569a2 +7ded7f9885884b31a16d49acf9be0d9d6cbb4984a13875a8ce0c09e53a3415ab +7e0ae66dd94e3fb780f7b361998bfda45f100f2c094d02e73dd25abf6cd117da +7e0c6397555de905018a91763916558ad4f47d4d27bff26a6363767ac20fb100 +7e14218fb42824dddfefe15400b168906d3ec8a222d6642d597f66cf6c48520d +7e152fd76130211cdd3b21e063ca7b21231d6e1112f7e58ee692c36eb3e2a7f2 +7e1930c8c940235b6583a5d8a1ee062ef1cebc09ed3066e3fb56a9e979e7cd49 +7e22413ebbef4626a5dc88b0a89961f51d6af3cd99a76b6396871907b4e26015 +7e240492cae7dbe09d118d5d5f6bc40292a6463d069222225d5e95ff68a4588b +7e27e1fa79b261dae6f33ed173ae1601f840276e32e7ff730381229ec5b1ae76 +7e3c10724d3f074d25e7fc67e04f98510322db532f848d29f7c57dbbb81d3d4f +7e452a290d6a8cd5f5d3e63dd8eb59f43d6bb1f47d1c233009c396a5f7995d64 +7e4655e5cb4204e302f695b7527fdc0d339564b169b39a766aa2b5e23fa168b5 +7e577c02708569695b0367f227f4517d54618c97b21c00f277108e7281fe8e2e +7e5e530f6e095c6aa889e48346854262cb4a86115c412510048a7a71ec9ed70c +7e625ea2f27ef9b359d77ebc1ff2d2545bc91ad5588835aa372017bbde201a81 +7e6adc4d583464cffb8b907a3d6780283ec353a11f8d54b9c90acdae11198be5 +7e6c4619b6de0aed514ee343c4993187ff5e477179dad9fe03e9a857161b5b0c +7e735c9740e3f1f4c9f59aa5336c4ac6a483a2accb6c3984783780eaa3804f6d +7e81fbc7cc147c12667da624b4563f9e88803c261c6cbb86fdae6ca555a8d0c2 +7e921ffa28b337527d8bcc3dadb90912ef440cdd60f6930221537d0dd0db40c9 +7e959d77ba0f16b353d46dd1ee6e5323431a0e3e97f926c37aabbe8928fac643 +7e9a805c91bc4548d1b148bc0c87a88ef136d10a0fc4017b470f9bfc3539a36e +7e9f359fecdada4058763abc936e29939e15a815bb936696a0bfcea3293bbd71 +7ea3c43de64d2da64bff3aa588518379f8820d86f1cd24dfa68f75945f0f340f +7eb5ca425f0cfbed00c8132ddbd145c142761b50cf53ac3521a3e23a61e1991a +7eb8c0362bec636cda7cb7521228063bc2e753810c168abc4d39a608b6b7134e +7eba08c449266b93ed87833169abe0206858c3be5580082af5ea3f7e2475515b +7ec04c92e102982770b745def804bfb41ce3c6c4ccf302aa381349127e84feae +7ec6e76284afbf509e52005bb6af22782c7e66dd87b44600e24f58ec166f460f +7ed4d7cfd3fd8a874324d101d852e92ee5049cd80bf251947171b04587e885e5 +7ed59431ec0a8fe762a9a1ed9c1f03979c59c722b4a7f4ca1a5fbcef20b2d013 +7ed618f42230406455f9e4595382fd904409e552982065e279a68cbdaa046ca5 +7ed8803ce8d425a40a8202e491fd5a3906cc74287ac9876661784c15318e7f21 +7ee6a7f6ccec49e3491b51baea965e599fe7d2c613c82141824e330ddce1ec69 +7ee77fbfc2a263530afc4c647482ef7ac1eabae2f2f76177c3e02657bf2080bb +7eef57cdbaeda2ce1f72e206dd3b8122c6117c8209dbdd6d5a0bc35c15dca5b0 +7ef6e6246fe8d26b4db15e8ce3a39f7b0c1e7b98a0d520379c3a221fdfd21ce2 +7f017278d14521e8097746f1a689a2a1fd4d5837b14211e8c06bf1bd3ecec754 +7f10770dbdfcfdbfa214455934eaa91909d59f66b1534fbfcea7a3beaf66a2fa +7f141fc62e434a75ad557ff67e38973fa298a0948cb17f1f400b3ad2c4a5aff0 +7f1510e6749bf0be72b344736ef0c325204e39b1f7cb14aa06a175e807e2e992 +7f2b4b3580cc7c2ad75dd954487bf8ba27a79cb70dfcaacdbab7e6b96bd74550 +7f2bc30aa23d5c527e27fab69bded2e503ac9c38caa3fbb8821f9822f27e54a0 +7f3a1bacac8b838b8b83e91a130a9251c8b3af1ab7167cfc06d787ccdf501938 +7f439caac6b1301bc2a6a7b4a383af5443b33094a691f8e6fe0df9816e21dad2 +7f4d0d71324678b0b52bdb45fb627fc75dd189a97322e776e2fb1afb22b2e19e +7f504bec4a3e97b19dfd006ac6e14d7d51dba9ac2feed4b0b41a710665d1dc1f +7f5cbff0a6d64d1f76d58b69c49504d3ce0f94017d9d7410c4d70042bcef5786 +7f65d4ee60b4e9df2b010380d0fda3cdc9e68e13aab5aeb9f11ee23e236cdc63 +7f730eaf87b3dbf7dcd8375a7eeca2217acd3d422ffd513c17334ae92d5af9aa +7f8503d3e41303697d25051663fa33d2b28264fbeb135d0969e1a658c30725de +7f8ea2535b61a527dee808ab114f6b3ee2f3814ba9878349834f7a15d7572568 +7f950201de2baa43e96d066264cd5a8a6faacedbea1f06ce2a3cc95649540886 +7f9690d624be939ac51f193319aff64d1534791a2e46ba7582caf5ba6922111d +7fa257db4b6933c1d863ffab09b96c225b2f14361d53a1842b2612fcb408f612 +7fba3898f4a1aa39b86583e5f4894dc6d070083c0ec183319f25c57a4037079f +7fbb7cac96be68f220e1810875494438703e50ea888239ed0e3c5378ab5f29e7 +7fc721f330ef4a9c70a38d809f860d861cff69ba124ecd5976e35003533e0023 +7fd1821678a009799b7d2d313169268602b16e86c2455ae79e4fde7fbe83dd6d +7fd4be779b63c2973c78c1f625a72fc3d8e1124a84da35754e254ca1e596b5ba +7fd5d18e32b3b454f6587ac7a8fe413da6a2987f9e9c6b9cc7765c9768fb8ac0 +7fda4074a39f4dafa8ffa5c8d8d5f8b4df7b825eea87ffc35b4b2bfe95f7eaea +7fdb5119312c5d85c037876d635e140240216b2cec3b8c2f2b407cee0aaaa1ac +7fde1677a059e9372a4d0e6d6558799ddd7697fd840837c5806c39c09db27ffc +7fdfcff149a32140db01044ed1e07c07c8c4394498621a27fb8be7d7f7b5642b +7fe05ba049d56c532a246bc560be64d3464d267f8010672dbd34718b4714d623 +7fe41c9837f8519701680de26a0207e78a296f01e3574869f6e3b94e8a0cb9eb +7fe5a31d5480390b104a345a05c7d907699a0ba7dc2a0bd8726fbeaae0644975 +7fe5b504be9e013570a8685482c9769ff5ee0e16dd31f5b43cdb12ea87ed42a5 +7ff60e3a86610905c6bf4aa7299b8fb4d6abd6d3368792a124b65eecf81a02c0 +7ff6878fe24db6a61a1e863a09d0a2505f329f4493c83d50d6e8e68366aa533a +7ff79b05386f1f23711e1750ce6cd5203d361ddc71c0bc06708ecde07e25f4a8 +800032e582d5ea857c292508b2acc9bc2a466635b12b15311e601f14be0ab28b +8031c5552a25d80280eca45ec2e84b6d99ddd2670e3ff61a8aa009adff2438d4 +803a12b89ebe2b9ee04ceabe39b20dd8ce8828e99ebf6d2f4d74724f819d332d +803cd5fd2f70306bc09fb815968c81d035013ff405bc7061e1e951dcdadb55f2 +80476b4be81e859fa1b11aadcad84101c131586b87eb0fd12c0cb7f446d4f6d5 +805021df7103e554d485dbd30ea1028d0d7e8efe4289cd4e7f19f138afe72e55 +8053a866aeba1b9aa056700b6769d0a2e848ba0de56c4071a6e33ac9ebbc4684 +805b320f0dbc6c0d3eb56a0992c23ab0577686de83c9fd40589c3482e6c0cbf4 +805cb62034d320650541f60d2a4382a4263098165371d69cba9dd0c0850d0240 +8063671ba8dc304ff7eb8596ed47daff8cd0df541e85c22aaef56655c9e5f139 +806ab17037265785ccaec5db5037a88dc24c6b79f304c561c433c5cf0e8958a5 +808058a2d680c41c92ce204fd1f58d6574aea4a7153d937348a3c31912b1183c +8099bce323ad271ab6e0aa3f666dbb4dad55a2c308cfaaf51974025d022f9ccc +80a2f9ffc964f4b40fcfb6e34b04312aab4f3d086b84300f374530353684b3bb +80a489e54b22093d6843f41ea5f23229fd50afa6895317802ffaef0c5abc5d75 +80af1ab68c88eb12d38f9ef80bad47ac08b6baabaf2f126816b2d8122cb3b2bb +80b0950b4251030247d08109628395b456ca1a55559952b55e73ad57bb07da40 +80b19b88ff1199b85bd5e66218f8934172523333f38637c9000f87bcdaa1a299 +80b1d8df12c08c98eecde5280500a90667e5131263a23549c1276d7b7a13a2c4 +80bc8b71000d7908816b39eeb0735a6b1bd52cda7216e5600134a46e4a51d438 +80c0f564f4de731159e978e80f909234500239488858aabc43f17e9df5bc6fc1 +80c842e5aab8e884b9b10699ce916ec53ae33c87851c9d1491bec4f2bd495fb5 +80ca6cb98903dd4c3bb1d27f0c7db5314f004e49fb8013aae0796418040eb03c +80cefcd8424f7873217fee519cd4b39aba1db2e5b3a70c5c5074005b267c8140 +80d4d64420954c8ac912baa1bae1504aa6cb3ffb169d705fa41ec29faac09977 +80d967ecce3d54562aa58b92e46d461579c7609071e1778d0e3dca879746a30d +80db1a13e3ccf9cb422b9d9117a98d65d8f89fcf55b5d8305c50e268b8cacb06 +80df81af12fe12c48910023eafc8d3efb07bd5e2c19f510aae3f4a5a5cd25af3 +80f41416e1e02d20468f32f8290aae23eabbe665773126e94ab6301c1325a388 +81039826610019d24323153d8070a35b9cbb70176c7547a5a2d1fb89f4928ce3 +8109c54e5c601a3ef686b2c902244535e194d765e0925311ef7b966bddb136bd +811dd95cf96d416bc2ae85bb757438cc6195f66a4844be85929fe0b26b462aae +813290c175f78fe5d7f3946744398636c8de53a6c633873d8a26a7932c421b4b +8136cba520d9c0d25caa9573fb3c8553ee7c37f023ebb58470b860cc4d9ec741 +813a0f00452244fdb41231f584f3c19283f3d2f19dc2138dd31b848830b2f77b +813b1b0872d2476e7f42d199499df65ebcf985beaa18b45d7e8b57b81156d1f1 +813c89bf251f436098608c5d5d638979fb264ad2c5ce64199c3658326f4d7877 +814dcd9874913f3962744720beb3879d3bcb34a15293699db0dbb9004edb35f0 +814e31e7610a162a83a8c730aef2a8281b14f85b12032889dec52cbb9c6386e3 +815c64424dc00118cd2775cc171325c32e14a861506bd1c9113c4522b345f1ac +815dabcc0bc21211ba892718e0f23fc6f6cc23d2f207209d1fe013c1f1428494 +816354ca9949fe17e7d8e3ca2b0d09913e92773518e6204065c8d8ed92e0dc24 +8163c42102165f1aea78e2c3ac18e36d0b385465770fd856eb97ab9ede216039 +816f59840d0e455729507d7163c788e66b140a0c26817db2bc022eb55038216b +817d37cbff0c8459ac21f991afd5c21a4831348306d20cae15fd6bde2e33f5e1 +8182e9e47fed431d340ae4ce865b94a8b40c23b8de01b01ca1aa16f39da0e198 +818df3dec357c19b384e29e1df6af8b1543cdd4743887f82ded9e67887d05097 +8194c9b78cb23dc6fffb214ead70ba246bed5093c7b15a339835fae7c02ff769 +819623730623a8b6007689b7748570293f312f8915b276005e42f7ea43fb02db +81a63a6c83374812b614b9d380079baa317748242fff3c6f20fb577d7c024b46 +81af07ded78346266ae14c1be99efa0a68af78c2551f193ce5491044ef746395 +81b0789153592aea8575df1672034e936a633fab96a4f050f3002359954a27a0 +81b0d7459c845c07dca8e81b7258b3c0739e36bab6e6d2173326d2019b3bb1b7 +81b6333b09f97caad7f6ccf12e010d3077995668582ee5dd0385e88c9af1d301 +81cf5e758c36cc06968975b84534cb2ceff6bbf10ac28caf455010e0182e6b5e +81dad76b75e9ab1a88164b988c76dfe358548e63903bdc28bc1d401c13e0e949 +81df2afab9903c7c0c5ae956d3647d54b766bc954cc0c905ae211adea574837c +81e56e333a7dc094ded6204fca7bf50f48135609f91d1f5a8c4c443c19d5b19e +81e6a52433429a1263cf40a44ca5eef94fb62fb8244b595e5c7d456c5a6ea52c +81e7c1d5b74435fa3237ba588a3a99bfcda1b2c5d2e84db3e4d472acd4884bad +81f31c02dc0e75443b7ea38c57de0bac3387bf0ab79cacbe8ad01bd75ec1323b +81fc1b263220ec775e3a731667b42bbb82d1d60d6cb50f60cb7c49abf014db5b +82009fdd1bd7b5e620828701288a39c8fcf438aace6e534408c20a8950ab632c +8207c4d3b3d4958a7c7ca0bb44dbd0e317985e76a65d64b38588dc17d49a1d1b +82105ad87b25a99fa012e5d74587257b2ab5c2d64f79304351e334479c8f61f9 +82179c27faaa854a9230c2f40fad536a58f17e2382020c5edd2857c8c51104a2 +82179d7b613043a216fd13871e761919fcb634260233289a4a24f94ba475be0f +82290ea0a8d5926ac113ee9b2be724059cb6cdbb0aa181d8516d19ba8894f4ee +8229e9751df88bce3b07256df10ee790b234552fa4257a3c5c680231576a2ff7 +823579ad79c0d79cef4f2bc410f43587a01314550c493ea4697fa2f140542c6e +824571045ce55f023ddcec2425f96bbdabb0e9318f7bf15e6f6dabf5c1157e4f +8258d5a18d36f5e8c13491378d560adb434562668d0d895a6138926d1c37e428 +825b5dec36dfe556b8454f93faf411ace2a38916f9b72dda81f9220a08930c15 +825d33389c390bd1575cbef4f56b0ec31ec5384f43ad8e9f50ca02307b2e3f2a +826aae1a76e7d56dcf7faae62b4362291366226ff533052033cd6c59ffc320c5 +826bf2ba6ab4978c4a51104edaab90663ccc645188c74ed95c2b0f96f4cbc18c +826cad06930938966b484b483c5bb5695ddc92bf9599dac57736d8ec5a5f05c3 +827365f30c13e68b31ceeb1665b1acf6d951e701d9a8d58edb9fdeb34e799501 +827db4e165f05e550a150842204f323c9971d82946538f47d06d1396af5c9c9c +82823ab845dc025cdb28b15b724e000220027c197c82781ddd38f6358060bb73 +828743953f0ecc96ec0c5f37660635fd11c5b3f765bbb9fb598bf30c7e94cfa6 +82997afdb246737538a54f5fa50b042136745c29da3154fc3b37d4c15ae23f75 +829aa5fbc4f14984a0b006e3f91ebeb9acf74d663ecf1fca8092b36d3c92e732 +82aa7068d802a3a67431be0459bb9ba93670db64fd84232a894c0b93be81ca02 +82ad9da18ce96d4c5d325bdfcb9bda311b8726d5f6333b3866f9eeb43d1e0158 +82b514f06e3558d5b7cd5843a7692bcf552a3ec592e7f5706b4dcaa81dd64768 +82b55ed76275137b1680657e2d545c4d9c5eeaf9d76099ddb8fb6dc5e4420ff4 +82c872c44575c5e6ad9b39fd9b1fea4f18f1a3b0b0dd576bb8ce4fe049c34b6c +82d221b6278732eff71117cf8f865c2296e67db87858ad7960a19b127fce7c7c +82d2c2cfeb76fbcc1d753227b59b3ddeadfc1aa90d84ec70d381459ab901b044 +82d6342f19fb1ff0eca9dc9b382750f89c581abe503587ce41e8974a7aa0763f +82d9e324508be3342bbe646cdcfadabcf8fad01d76993fb1b6bbc473b00dd14d +82da587627224f0c51749702e07350f5ee546584c914f5ef8126cf3ef996b4d7 +82e67d0c7502191ec8acfaba32d671c38c3adca5c124a772b536f36fce2f1cba +82fa5e641d581d7ffa624fd55e3eb7cd018ad1aca42b52220defa4d53dd21f30 +82fb60b15786533897a682365c292320057ecb685cefeb0ba69ed7453eff1cbb +830c0912b28dde4fb3b0d006ba412e0970d4c35f60adebdaed9bfc7208d96861 +8314bf261274ea77c50500cff02dd0171ab8305206225bdfbd8299e48d2d5b00 +831ad77f6ec3f2b03e3460ac1fa265bb6a5e906a27f0c78b6d75c670f483917d +83213c3783f6425da86a8c79c36e2e51dc7eeb82569aa4fad567a9fcaca1fea8 +832537270695dfb4f4428623e7311c1a9968e6207fadf6b487f95d3d82d40242 +832d8166938a361dcd454fe41b8213981ddb74b53ebf2d82dd3cdbbf9472920d +8330a01d29bb9ec5bdfd9f6491fc5c97df80a5246ce5cb61861f3a2e3dbbe59c +833e059b152c45cd2d843e42781f590c91a76e4466aa64ee6b4a4f58c00b25c3 +833e4b1fae26f8f8950dcbbe9137be08f24b07a988dea07bd9f737684eb5a6d9 +8340e5fe713ef462809ddc35bc7c4f48c4661051b38dd1f13c6b7f8863775510 +83468a45d168be82a1d03a7be475a4f46f83c2636a237301938e8b3f6ca3bd2f +834efe10771777305fe12fc5cc993fcd3010110cd5019275440e02e3b0323445 +83537400736423e572454e9e480098966050f5d3c914f747ce8a7f5974b7500f +8354fe9e9ade0d789a56662685f6efd2aa60db9611bedfae3af6d3d6d28e5deb +8366021977d8ca7712119af42a3a6976069c324bb317c8b63521321e03577d5f +837b4db352ddbdf3cab31d230706a9f948ce04d307b9b7107aa2b4839a5fba6c +837e86e3397262a5926f67e3f68e42ccd2a8d4194eaade6f8c507398876e3497 +838a2247f22e9ffd171b625bbfa79dccbd178b52538ab54e3584c66eebecc1c4 +83965703ccb1ec54a30fb419a22c21780ecf8f4a7fc162f665515dac7aa5c857 +83a35f55d7e9da4695f8ce27242f442964105edd9b6bcca8883a89c54b07fa8a +83a52101ebd8156e9cfb573e9814262555a12821a74c992c4aae9bb1b463e419 +83a6ae6d513e67519e9733aab543d115aad15879b4fbfc1f881755e2e71f6dd6 +83a6c6f5c7b1824782b14d42a295411dba8fc06c3e6a7da2a88e2e9a3a81df28 +83b26b686379c05fd9078ad00f5d8e16f2feac3ae23334541243f6bb21584f4e +83be45ed14854b0a87ca44554add07ea2f67e5bd614a2701b245f1bcb117281c +83c594d6ee1e33cb8c7fc0853f35d6faf57bbb6e6725c05bb8baf5dde50969da +83d1baf81a119344d175eaae5d17646bf4ffc97fcaf93760507afb64db7f5dfd +83d3a76c61b384beca179fbf8768639455ac9153b6c6d341e6e9e27537121513 +83e3ae34b75adcd6ffada0779d88f36d9e45ab6ac5e8f1ed7e6175d2837f9936 +83e3d01980a4a6d3b7eeb86c0908b477c4adf17c9656421972e065f84ff9fd43 +83e5ff5d34ec5a6eb527ba3a13aab6c5120554e6cdb9d9d8702b074d7fe4cf08 +83e7b0da48c30afaff4e70adb23a4f4b90a32eb0223dca750928665e467fade4 +83ec275caf75cd95ff4b6f45289ba0017279df6767f73ceb59d19ee0bbfbca06 +83f1621260371fe85227e7150c2ea1d3a2f08b5a1cb4501d204e3907833ab41c +83f389ed7b547cec4c4ff7a771ba0794c878a717423f7bd494fc2ba34aaebe1f +84006bd8dc24c1108c87b6ec8e0edb2a006b04e3bef8394f7b1f82eef7dd2591 +8408064f8097e4a445ae862e26eade9f4e16c4cf0d48a42957b97f3dd84abd98 +8417d603a5bb301a60d0e968e875096013fb777079f2a3a4fb5a4111dea57c27 +841e97842a9b235932ec2cf1200eb269063f1027907ae6b5b21de469bb122229 +842997f43616aaf65f702b1c1b865d8fba665fe579ade57a9d84b8622de0ebb7 +8433a2cd2c28d92741e569366f160e54d415f1f319b7605373fa9e7d78c80ee7 +843659180c5c7fd3e94cabc88f5be6c64483390f28c55a3b4540216e20b141f7 +843f27e9906376e56591a558c6cea777f932fb3f639a98b9c50bfddc219d5f7a +8443a7e471aafa87732b384dec23d492af63c4e149172702f63d8251cd690159 +8446fd0b19231d95581323261a5150ded9a00ccebe0fe13b0a4ef55fc9c85937 +845272b9bed64b013f0aa1722035af238e66785c6c8cf448470e100f59bf80e2 +845cf3aad0ed9c310f4ddec149f58ae286db1da55dbf652110db01bdf8c7bb1a +845f5f34c3e4a6c951ad7d1505478f05342d554de502116008d32e00f5059712 +8468e4079c63a535f7aac11183680c97e9ae4304519b488cf951c83b3affd0fe +8481320c4f8f4770b8dde4388475c2b9da8b79d357e26313a1fae9454c777c51 +8482381a5029864664180045b1eeef77e6dbd4f252fc198e602b9357a660fb66 +84856841cfb1ec12173f2f64db74a3024b5820491e622b4e3a5a4a160099998a +84895649e73fc3df8097729a5beafe0c2b8cb75c35af7cfbf3fad898c89c3e1c +848d12de5ba0e9d93d9c53b60aaad7422881d56094c2d621edddac482ca580f9 +8491ee81cbd5bb51f76977896457829c6e302498f6cd66be0f54f88dce04192e +849f658111db356a859424a255b03f2cc48334b2699823a18ebdce671f7f437a +84bef94bdea5fbef2351f6671fe888249d8826eba152f19e3dd0b082e70ab962 +84c57c98d23a308dbec9a7d1b657128bd77bb20d0890e2dd8737f5b44ff4872a +84d8bbf5503ee1ac97adeb366599bd1212cd569f0e95477c7f60c80530a6627a +84da61a27e777a0cd20dc721a47946d0ebc025ee524bb694d43a12c816c6d24f +84e22f84536c36f15ecca2f29802673ded2eea95fe850ebd2451c2ea95f77c8b +84e45bc1f32b9905237191f58b4a05b189fb779b87319a1ba4cc0a538ee9ebc0 +84f286b2a0051243cbbd5c0aaf323158948cf03b8b1be12bf44abacbe1fb5643 +84f475b70e0ee99b0fa6eee1fc8243701f346c447a1446466ed12ee0eee09d9a +8504152e2a3c3cbb7d63f3baa75d0ed7e7f272bfdf2666eb0e18fbfbe01dbea6 +8506c3fa4628603e190906ef29a3bb418987cbc925f1515605ce01cd96a822e6 +85134abd875d966fde282ea5a2617c98b1b1a6422eb6664b4372752d52d45cf2 +8517f9066cd2e6e489b53312c5e22ccc32664628b32cf4a2ca3cc24761222960 +851e83062c20c6b3f83bbe080f7ae2562b14e96f5ddc96985bbc9c331004aa76 +8520c802dd9e57cf57867db14855022abed8bdcce32f40e18f202496f7464def +852b219e3bd9fee50c0c31dcb5fbe5f25007de97292c20194aa2a8a4b2ce0e16 +853157759efbf3dc1fc56af5adf9338ec8163051ab61ac27e7ae5305276373f6 +853402091782f1cbd25a78792f1c731580651e8b3639311be8429137ee6d2c2f +8537b808744ee68b8aa546562c571392d6611f0cee26ed2b33a68e2697823ced +85471d228fe2af1103fea7076603c2505ab20a4290d2757a3c9528e10d8b9c29 +8548ccc1bf58bc6fa38299da784446b3aa3b46795e517591ce75349508c82e3a +854978fff1942577fd7a83dabbeee7125a819a9150b78a5acb3f2602ed38273f +8549ad888280e7ba5f4f6984f175508ded4b9ce852d61730f712a30fd417af63 +854b57ee720a69a252bfd614c80a5316707e69d411e4c6da8d58bd4a92f23b7a +854b96bd3731e0c0d68ac472283d32cb6836bde639d5b56d4aa9864c88ad2895 +854ddbb1225b306bd2686a8ff75a38eb19e4e667276367dcaf804e1e0439f17c +854e09cb0fa81067f8180ffd9b0a22004861b1ee7129cd94696ba4d8b614fbe0 +85558b757d45d0e55846db1b1cc80e6a6974e618bad8950c1ec8a3d3acdb8e73 +85589625e63dd226c872407e9634108b88f8454cdca00bc2e2b3a68851ef4c9a +855fae35c6173a85f0bfefeacbceb8b10e8adfcd5363dce915b0a1b75c69e3a9 +8561ec1d79b71ec536c11a7af019c6cf1148ffab0b5a3c42f6eb689ef5f5338f +856378e53d502fedc3f7d9ab0885c1f3e7cb6e80f61f3c150ff0fcacffa43a1e +8568a6d88f6f202ec0b5436629cdcc4385e1c3f58d69bc4dc6323c5a59009fe9 +856c52206c5bccdd2cc470e5f294903d266901318e0d29537043493e71adc75d +8570f9176e828df97983db2801a4c316331a5b883e6d1094581bf8f9b4fc537d +8574b57dce3f43e721d43bed62c31915bf1b50b12c5f23e0ff3a0b41f66a609a +857a2b8c51fa7ae85202348f1445354f47da1d54520069bbe02dae1c89dd29bc +8582370d15e9e6120fc7b4bbcd47d58ed5a70632457edf6f3a8ce8265085d344 +8587b7ead18b83ff2f80412e3df3475b3a924ef16a9aa8bef73cf6426f8ee6ac +858aef9c3a6d42d6ad15147a9dfd751118feeac436ea381db165c82948de6106 +85a04457af092ed67ecf9333e7fdd0a7c58491498f86f977f4b81de13bba7974 +85a19bfa5e852ea211c8ca69a9e3410a0314d62b2d8bf07314c8e5108bfa3e94 +85bd1aee967d749e3b88c1130be6b1f524e555db0fd8c906d12e678734e441cd +85bf4ef425fba7bd1f727d929d611c88375c7c0aab1d617a5b7033f46fb95740 +85bfe748a2850cc7bcdc5f8ecd249c5d84bef52a30872fdffc14cf0a41ae1fa2 +85c0acaf6ea94d58af83caac91fedd0aaf0f9fb8b9a94477abac79ded83dd86f +85c63d1d06a3b23d4fa9428832e988815d4ebce84589a9fc8e1c1b27c088f061 +85c6a634d2a74c4fdebd92efefa4402f69ed8fb4744c59423e032f3efe8efe32 +85c96d83074cbbc7044c1058af1997d5aec2da3abb0fcf6e4119be6fd88b3a46 +85d341c2b3478a8fbb18f8818c94373ab68eeb0bd3a7f9e84cbd115df7dfb2c5 +85d694613b2efe443bc1360d6f036031ec5882b1920f412f8c58549999c23ed4 +85da1b4df6443da615edd392296480b80420b44b782b6041d8262bdb7f4a3fef +85df8fda0c77d349c89eaeb86c3362ba9068b94bf5850c97249969d694ba2fbb +85e4ff6630aebe8ab821650062add6a2d5afc5b9dd464125fe640e178738f955 +85e543e70e3d04ba4f234b7292961c4a0871d7f89ec5cdab120d0d85212c41ec +85e73328bee95a43499fd15e381b067ca35004aae357079b4d99d8ea3258268e +85ee2b7f4c33d5228cea5034f214bfead6fe50396836b45e819ae93e5d5ed5a0 +85f2a3d30eed516d5815a4407dea2172ea401d06096952c18a31dce95c2b6a60 +85f510e563edf2b6cfaa9e69e8e71c4cb20e6ccb1a9a0f2a21e56b2dacbef133 +85f8bea05f53193b2d9dacf663fedbf55f1d546007bf3ed3910e60dc49a40b48 +85fc165d2e6b04d5c69e8a85cf7fc8394d6c1de59aeefb65938599dd1a103b85 +86046d77486c23a87d1a4b49759cb7afb1f5132767400a2a8e5c9504efcb238b +860548ad9bf887f1a3ede8bdacbd393d3eaed3af073bb0d2068ebf9117f72c10 +8607f3fa017b10c721c8c7117bb72e7b0e1c0aded022280fd1959e9e3ffd7b82 +8607f42b8f5b3b546c144f072573f0a5398d1095f3a3dfe80aaab563b1766406 +860af8b5c62f822da80bed19e6711f1964e6d83911b5e6378a708c9aa43c7b59 +8613f4ab751a08ce780f2460add0e323d59a8bfb69dd395c2cdb473925846831 +8618e989b0fcf23823772378391b06ef8c7a7931b35a9ce3f9a59c2cfb20cd6c +8625dbd83ceecd67aa8236918de1458674e9cfcdbdb0c66f13d22320b5618db9 +862a67dc229834b0d368629a3bf2a490f0fd03a1350ebc7785d7ac77457fb7f0 +862b5cdf24d16dd959e2585c9beee2496cc302bbe2f0844aea3595b383f95642 +862c37566fd856b3915471ebb0d3671bc644a472ce9dd5286c9c8fc985a473a1 +862cd522d01d55bf05f0ef40a9dbbf4f722867cfb5e82646e59388ba2c3e0bb7 +86306896edabbc3fa8b1faf65347991347912a4767202e9d4275ecce7a668583 +8647ad3beee126878b632688622e5a1e01df6115d147574fff56946f5928b614 +86551659adea0b4be0c9231c0308b191f480d9c875b7c2d93fca223170b59cca +8662f8eba856a393d856170204888d600821aa28a590524393b052328a3efac1 +86668d5ab1eb299129859efa774d517e45db8fa6476104e10908c22832e98756 +86701bd0e59e492f049becef8406509d1f4ae77db688aeda6b5258d0f5399523 +8676c5b7fc0318e528cc84201af9cae7844aebee4a84a478b8f5df193d1d204a +867cadfc919e5572e96ee3a3c667d20a201894adee9f2a713515d399e65ddead +86906b73f17339600415303a59f796f6d841afe00ee9333424b29eba3b198dff +86a0ca09c4d06d5f61bc1a52f0f304f8d9635a36297f724bd6d1fa3d87dea646 +86a356884da5989e123f663bb0f764c61b2af867f35a7ca0e841b97d3c7a760a +86ad9732b0e0e4b68fd7ca72978071dca6731756aeee4908885b2dbae52cb65a +86ae7928275ad239a048775adfa3271b41e08546704e1020d0406e240e6ba6b8 +86b02db07cf18176d139d828a916a8dfc259f769d42348c5acd4888e44f81a08 +86c0d37ac167f67ba45bfa33c7159020e4486cd09bcb3a50aa5f6f0916c65ef1 +86d56e43d4cb7fcebee82e0afc18ee93aab9a5f50d6a20d4fb8039581113ea52 +86d71e5ec2b9d83a3c14ae352590316873ff774cd4d1782c89f9400d9eee25c4 +86db19e6033fc7cf7e2e6f1f25070adace58e492df1f263b1e4af2740730cb02 +86e34791c0ddf8a9f085601698c2bcdf76a34c37aae14587a0ef35a6049b8f3d +86ed23779fbe7443d6dc181671baef433651af4e4765fec5fa2c054eaa58484f +86ed8657993fdf82c8d7f9445ddef9ebea33c3b101e7ddbf7ff1715fb4fe60c5 +86edfa2b5de6217f0f3bef0f46b74f9bd859650e838edb0efd7752529370be5f +86edfbe5d26b8e10bf56a94631920f4e40967ec1e407d795b7467b17ff2cadad +86f06466e77138928dc320f912722ead6fca4c26ea7a7b7490a124742993d1ce +87025376cc455ccc4162c2fd778eb3938a59c8394289cc8701ac40503f5d4c5b +8704feccbc7ea49cb4654950f5f2d8a2f8a991795b04d3328ec03634e9312fee +8707ae49633b064d033c02eb32f9077881a62cbd01f0f4a9c7c771e8f21d4e2e +870a32dd3397392af84b9b823b89db4937c96515b5369540709ebfbb3d76934b +871567ed77fa88ff53cf301841e4cc59b92db7c16a44d0b6f6553234e722549b +87232d9171cd2d073b779563f3eebdcb32fa0eaeb11aaf95e7ebff8243e2034b +872780e3fc218aae35d7540f073c92b040829b1571bc24bd207585d6e22c1e7d +87331a7a696760a69fc01a7d1da18c609e01ebac4024b91c7a21da99d0738055 +873bb75c0ed1d25fd1ed85777fa020dbe6ba10482653cad5d58a232329d50500 +875243de245a2081ecc3f40e8598be1cc5664d158a554f4ab33b3ab1086561e2 +87688b7721cf9984e9d486f92066b0dddad6b5261379583d270f7cfd4484d72e +877098387ee8cb2416023dd7572a7483e625c73a97d8d1776649737e694f094c +8770cee7ae7ca7501dc0d7b19c0fb2f84f531cf9416850c2974402c29501b2c7 +877e948c8663a6587d9ec6c2a3ba18da649c4f161ba1725b9c662a2ac0f7cb25 +87812ceca39991322821729694b04c40a6e55abd6e516eded1c9c898c8624059 +8784f68f0e0d9329b6c354aa9325a89dda70b7f933018be5d543d0f96e08e8fb +878cc754465d176e2a1426172cbdfc882e7fcddc3a402074f2c7251df45cea26 +8796079c4a18a5a60c8b497da5505f67e0dfd53657cbd9d1a939f757ce4202b5 +879934bd14635a85eb74049b3fa19fe24106624b4b77455ca716b556f6a1ec8a +87a6f450fcc01e5728ebd9fad3086cccb3577228a85520ba3a761987ab148310 +87ae46104885893bc99dbac4edc1e7db7e05e936609f59b778cb2ff41a758244 +87c8896cb4bf3bcf1d627a27f250e8a7f40c75f9c97a710e51e4dacba99981bf +87c93df5a9b69e2b201fdbe8b2d3593002e49edc0f109be78cb16c635e3e6e21 +87cd7194660b38acc82fee512937bb79be3bbf17b472652b7e8edff6c086dfc1 +87d4251a1cfd071781c6b30231aaa1b2f70c1aa5b9c9521a052a7e74bcd8b7d7 +87dbbf1028c082620f4558efec771b5ca1d7d55df0627a78c05175afb1d81390 +87dcdf0eab8b4b2d23264b6ade92a8315850a770e72c890f12c7a8e5b44aed5a +87e10e5c27f90794e4fe53395f886af01ecae6fe7430a7674fac22aa3531516b +87e4a5d944a7ae25fc9ee6b1f1c9cdb5adda30e68ca1fa956adaed3239b3c4c8 +87e84c0b3dcabf89a01b0a79ac71429b8dd0888984090f5b52973a97601fe5b4 +87eb9bb9c9c152f985fbb91146266b8c3521f8ccfea093f901085ce20f3408db +8804ed6ca3c6fd9c06bc513c7a449bce2f8c5d35fc46539bd7f2ca6cde57296c +88071955564b5ce1ab170ca07bdfaacd5499d20ddbc3f47420b6af7a5b99f14a +881073e9d0070f8057711d6c37210100284c30144c8adac76438c1dddbf5ebbc +882492cf9af08de5f2823bd32006fee99264890bb0cbe7ab120b079352908375 +8828b37967c74d7af40a5ffa950589aa23edb16ada3143be2b5f155a5cbc2eee +882d1e15924b59a3c3f666f952a6df3b3c90dae071187a794ac9400828e5b3fa +883332ef82ba41a7c0e5b15b25c6ae081ce8fd4690e90f9e4f5ae004fbc2c8c2 +883791b4e3f89a482a7957abf4aafaeb38a11198d7be9800c7c0aa61a4733afd +88443302039608c9f59c0f80335156022ec028b917cada88d64b28c607d0ed88 +884a26f679cfabf85310122c3690d432fcbd091b8e091df284ad73d72e43939e +8853c4ac1e8745950854d87da5daf1acdf895d8e4d21648c225686fe7889acad +885a32acd95bff685d479059daa882b9f278cbed0cae0fe774943d27dfb0444e +885d35be2913a402c9feaf255d75310b204edf5d1de2a793bf51b3cedd0033d0 +885d6e99d3b0de658f63c786410825b7f7474aacf61dda28a39f4bd19daf8925 +885e43f0f7bc7916b44fc54c48201752445783c0224e11eccc76a7a880c3855b +886257b0ae5b377537c8e8e394b7a31f9904b5cd1971c8216e353b2f93f97d60 +8869efc9cff4eccda4d730bf8cf27e1f87a1f3ff998d0b9348b53e38be8e5f76 +88813d751cd3c03e678de3ecda34cbe2f1a9d4020d3db9ae77ccf2e522ae3148 +88822cc4933d70f19f3cd004801efe2d0d5f75592936a2426d25c051129a652a +889192731990ecf68b3bcd4b98a71690a54dd8d5b129339b06719f6fdb04cfde +88b23e40dcace54067cbf851eb3fa2bf8a67d133ca0ec12152259617683a7ce3 +88c45f5110016afda2545ba5871ebbd7d06145c4a80dea23094b2ae60933f272 +88d6182b6ca0aa317baf4b87906a8567c3873e62c6d5d458bf830fdd49348cfc +88dfc516e159be1650d49337af5c8a2829cd89d72eadef430786cfad952656f2 +88e5cdeaa7450486db005e493b204c11350c30364450da4aa6b67baf7b3ab609 +88e86b3e74f3cf3dbdb340b9c468d17cd112c5e7066fea69c5b9afcd1ded796f +88ebe5c724f21b540fd547faeb7312ab05ec5ab928f056463682c3b43adb51eb +88ef3fd185b7a50dcaf841710ad87fa8109336658541a627e81c74e1c3f84cb1 +88f88c168675bc094a476a5376bc99a8d9ac5bdc11bb992ff8633e592a1f9406 +88fe7748bae8017aadc6a4ce977c4b49f97711dbc459da0dfbdd5e33fcbf427a +8906ffd831de533ea40dc4499b0eac91e9041b6994a0b480c8465e675d3ea2df +891053caee73359fe2e4fdeaf2c6731dda7916a6b267ac3944d66edfb53eeac6 +8910ed2873e3835a7d5f8e3b635395bf8705213dae239b36fbc3f6bdf85fef7d +891715a2bc1693952c61f4e51433a47301cbee7ac764038c25c57d8327179c97 +8918760f2f61ec15e40d24d270a15a26d83c054e9b2dd42e4172b56acb6ddf6e +892066e1b9124ec157ea11392e07ba3a1d8408f4891bb3efbbc7d44993f9eea4 +892392cca3506f5d393a0f97dd27a762d339a0884dbdb0e2e62c86937b632672 +8925b201f3fedf6838ad09024950744611f802c18a1924f71d7173bd4fe7f6c9 +8937937ed7e713ea138887375f5317e1437600a6c7c6d39e36b4aa88e40de2a2 +89390a82cc1ec2e8076a34dca2d539879bf5447aec7088eddcb47e3012498a75 +894f3038517037ddc32dcb895edd197c14b07f84238d27078b1fe7b4e646bb60 +8955065af4df1b777f9c5063bf3946e28c302e3ac37ec2057255c144eb9f614e +89563336d462dcb80d03a7f9ad05c5b5534cc4fb0e7ea0500c1c5e3cc046922b +8956be43a0932061bd256ea52534847e15cee84b3aba3139bb24e2ce166811c4 +89579b0cc3a0b67c519e3dbc0a880c1701f53046d7b94182a9c76ff215c8895b +8958ad0ace694dec7734d0146c645ff7d214da3ef31c4481d4f35a82931db49b +89596d86fb0e93695160bb1be677daa25f66098fe89485c4b3034b286dcaf2fc +8963e4cdbbd150e9109358c36a475fea24f97ca76fdc2ef887c488cd08dc9bdb +8967899ff027aa69bed2ec0fad77a9e2911b3cb866b769ee9a0255aa1a376fed +896e59b4350fce734343493c6da6b73c30981fe8f634525607689068c3ec4a5f +8973beba18071ca2f3e2212e85a2a2225b377bc3098d2143367c2ac109ec68d3 +897a58045d6b338cef4da1dce18ecdcdc92406b953826bd454fc558153f8db14 +897e6a683964d402277c64e0c7a3d2eb41743e3b692d7b29d5abb9ab89b52160 +8983c466be8aca62e5fcad8be6920a9609ba043f7ca78e2e30d940eae74df8ad +8986c0e2c58584a7b901fdf54b633cfcb380686b81af28d2527355fb9b9fbc55 +8986c438d35ebbad9a81fa42bcea6f2f2989728951ee5f855fc96ee47297040c +8988ec652b2084bfbff3d3d813308ab84969b6b47664abb4ede56a9b19d80827 +8994ff035af396162cf0b01c734d7f0b1d7ad585e0e875100fefce12a8cd5016 +899fbe3cfa2c3bd915b38d0abe30a29adc54013a2530d5754e8febf5fcbbb64b +89a28b9df52f64c16abb4fb118d4263b7ca6391866849fa4780ee59371f33607 +89aab9c9e712c9a6494f825a95626ab2f5820c410390c213b0362e5a6bce2be9 +89ae8d48888226111461f0180766d9e831771a331371c52bcd4ef55eebe7b696 +89bbfd22e9fa9b0ce8d5f25c27f86f76316248bf9f67003e01e5f001b1ed32b9 +89bdcf7a95747895a68d6960d8cb19938834908f6f3feecdd424851d8127519e +89beba49ac83440e74a42dc5240cb6bf1779c6cb8edb97ba0e2952ce25a70a83 +89c8ca72a441659cf5e20b9198e9c6f9d93c94fab31da663463b6a2f14036df5 +89e6884d6853204e8c4090756561b11964e51e3c7a1ec1c35d1852f639eb3596 +89e77eff3283d7b1fb5425fb7c53d14877f18b0f847923a2f673b7eb24d32d4c +89ea84c2efbc5a569bb246a6f7f5ae00f52ad225d8c55c4b0490faab289cc6cb +89f150d6538f3916d0759bd768f25e7b6e8b3456d11dc815621480109ee80a13 +89f2afe1051e1072b5cbbf1098f378469fe4666555de185c1a7dc13fd21c7350 +8a12255bb00bd77100a30ea72ce5ac1fac93ce84eadf21518e29073d5163be84 +8a1b0238d6ee8b8ee1af5220ba21f60ee3e0ee33f4b95b23f79e89c0300166cf +8a2d1eefaa60109cac4b64a61474bee1e730f1edef4c781151060de975eacc31 +8a2ff51d527b0314ed1f0992f89dcdab7337be52b09f561fc7cfe7148746d412 +8a3534d64c7c7816ed5e536e96235db4668d454842d3ae1ef2d45c951e51bf1c +8a3bcf9059d1fa5a6c04110751e45823ea3881a097d93360d4b00fbcceb6c302 +8a42d4df26be91beeb7dd8f9b1d0c2d5efa2bc2dc44da1041fa6a01e250961d4 +8a4d203a2df71db449bc6b99dd83e064365a5d3bfb8afe8b23f167d5f082e84e +8a4f16d441f35665efd3d5c9bc8886b148e72adb060f574f10a260a0fd79efc1 +8a6351f0d1f8b13a90fbbb25d38aebcf81809736b16ddb638fc497edb954e32a +8a76046294038bdbade8b930fa6f7e67e74a6729b3ee81196c7d32280475aa5b +8a7d833ccc381864f0cdb3b4d38bc2ecca9528b9f7936796d0b06efd8b0a5fc6 +8a98808d675daec74685fc3d6ff174e0010a82fe7513f7f0484cbed35d8a598f +8a9e50920d6adb98abcdb3c1c00e89175acbf2788115bec65d7a9c9caf71ee2a +8aa1bfd55517dfa4b8cd0749d9021eb1e8726f99c89ebe61b5e52fd9c74abe6a +8aa702700f42049d8d9bc18a54d0ff5ec269c13b383245a9286d7f70e0747084 +8aa95887b9a6871ce9f45f98b4f615e36b2b9f3b1a97e75ee0df0a2650de2c6a +8aa98430178e7da2b14b42ebad9fe68b33e21381a26d7b61e3fd23534217fb1d +8ab37cb4f6db794cf99e8d3b6973db2d264d9ebc7a8fb8dbbd8241ad84ab12b6 +8ab577c41b167d68bce78c4906da2b865f6dff7c5b02308207724c9b0092d3f9 +8ac360c0ed4ec3216bb58013b68a83ba956a4eaea45fd987b388cbe1b646759a +8ace04732d2444b763f985010bf63d072f134c5d13741d9d1c1a56fd74fb0cad +8add23f42bca4a7ed58e01429e4bbba7874df3a33f2320c773fb38ffed0d290b +8aeb6a7e35c5cd0752581d92f23ebba309cdb2f028d4dfcae07af0348cac3894 +8aeedce465fc1a0c2414e67e62cbcff47fbe2bfee39cbb7c33a1b6bddf87f5b0 +8af59583ba534da6da15a10b8b8847781fc2650d3ff98317cf67c69fddfb1d7b +8afa44cad69212bc43d534b0863229c55ec7bc05f9a84c5c57879b3089297072 +8b02d0f143f092924a38506985c7dd671693129fabb0eb8cd5158eb5226ea3a8 +8b11d7026cf3eaf4e5680b307d206b5419421db24938b16c6878f2a093b904a0 +8b13876d21e10ada406055aea75d0808e5efa902813987c27dc01497fef1d81b +8b2367191017105b2c14c88be0e68b0e26d36182e80daf9bdad4f2b4fd73e3ca +8b247efd445c6113db32c033c4967daf642f10a5ed10eb6fc76fffa36c8e1340 +8b266f86fa3d1488c2e779b50e716377e6d2871c985938a03d4ea9a2028620da +8b2de9f9d44ba375bf2e510390f4d7d60877ecf4c2e3188b3a045c520ada33b7 +8b36399af3b8da270db640587913092789fe84469de61f14144bfb18f462e689 +8b397a52015311114cac2ac189cd6764b820d6ef671328a225498c004f092be8 +8b4647a5a3a663ba8470c0efeb906208fb5c6a21dbbfaed1ccce1e675f01f6e3 +8b488e679bb216da5679e5db05b4616a5149cb5d2626fbbd29809f5352649dc8 +8b5881a5f1c759a1a1ae80fd661da1d252f24aaea13fa82087ff5272b63bcf4f +8b58edf4137036e3da30e94f3c92ceeab41a2ab241ca3b994d7bb3bf35abbfab +8b6203484bd189d8464296015659778eeab5c6da907bc5b083772043365efb4a +8b69af47b4184042aded239d4438155a454889f06ba9b86fde7c295f09917b65 +8b6e1e6b2d9b13f7cc7d3c927d515eb094fa324d66e267f73676d854de526dd1 +8b7026480afd91f98007bf673e70cec836cc453f77a3ac96cd31f52aec4e889e +8b76c3f6125321ef2d09d84a5ccd893792f8e20d5023665c7c109db28969c4b6 +8b82bb7a74813bfbc989d9a355008c717d42ff6af35f50c1a2a9d77fdac766dd +8b83751fa011fda23ea30e13e29dcc055d4ae77d3902a31137f80804e6dee5af +8b90e59aad2e8fcec69a839b0bd9593971351f22a27b43431bc6768f981d2597 +8b94ffd8147ab3bd5b41c554a7dbcca69ee9c053a44fce0cbc834901ae2c0bb4 +8b98064c448fa7522e92873b114647267c9fa7a20a86551c6f8d440927ca51f0 +8ba7a7d75e15644ed74fb8f842cd185aa1c03513196e76db800867d0c0aaa3c0 +8bc33f7d97163c8a3103885d707361183409a4d8920ebf9157ea5c57e38a05d3 +8bcf6e6cd16ecaf12c7e43aab0ef2b9a1c846d7dd45db3980ba2cdd31ead111b +8bd8506a521834c6b7ba8498347a39cf37bcaec7987734c4d8b5ffd363637da2 +8be20064b10dce4f6f81537c46a3519af7db1c13fb04f7a320cddeaf2e89fdab +8be3e402d1ba28437bba5f948d6637e0fae628e4db0f2123108a2db7628774e9 +8be63feb8d68ce26d902bcafc9eab1351ac577c100cba0fed6e097faf788afd4 +8bf4093ee644f9fff719fe9d3550ce5b31c6c21580d78306468a00acf4cd1979 +8bfc028b868ce3f4099f864d27bbc35459b754938c6e70938ff6e670cfc4a320 +8bfd96e92d3b6f1a0a17714328260a0c2eef9ebb2a3d8d7dcacbba30585349da +8c0112048461ce853ce823cc84d20a6d9c08effb74757a046bcf1b3e8ade224b +8c09df859d9f4d10b08426f20b6b4f7068eea07a1458687d08a3613d04f0afde +8c0bc2e8a19da1d8fb65ec30a6cafb134554a40041738c77ba29e97242231d4b +8c110194096d213ee7163def258310620aba47dbef8bbef96064b587d310d0e5 +8c1ddc8a39b0e5e42e92ee4fb81e2808083381f612c1ae507516ed790694a780 +8c2d6156ad49e0aa3c06b4d25b1d9cf90e6c4afbb70eb8e568f1a2b2dcb50ef5 +8c300152186a77e1042bb718b3cdc28de627acdb08731d5d5b1363f6fef88cfd +8c414b2644d9a809b78fa8cf4d4c388103c95118262959a3c473b1d48dba0689 +8c4167bcbd2ba55e79f06cb1f4f758b19248a8ff952805a8212604a3843c89c9 +8c4f86f14e3d4829db83fd50118fdfc7aa484eebe95f95086e614965ce4c2825 +8c4ff44e566ce0c55deb2b9f5005f6a53035e21c4a6b941b330624843618d1bc +8c5891772cde426cae8ab220956a61e73e151d186a0a544b3a79ebc56d6956f2 +8c64a11d47dc4a89d04475f8db75d1cbe9a640c41c97ea308dad7431728afa38 +8c6fe8387ff68120211876b8e39d392cb35390d675def8d8a4a09b373d131885 +8c70ba302e8dfdf413e72d09d6d53c2bdf9f44a33877840e8ae0c1f8aa3a86c2 +8c75fab0aec71d2f45601e0b53156452bdbcddd2d4b18e57baf4950ce4ff43dc +8c85f6fabe846b14b61e9dc52c2d25f881aafc675ddd9ca3551e238935157811 +8c8698ed9d24d31fbd1dd681a751442352178c5e6005629de9a4d6bf8bda073b +8c86c26010f99726d7b778b166f90be450a34a62620ccde86423b0a86fe59daf +8c88b614360d38f429cfa0d6450ca659a844446bc209abcde04f5b7a5f984ea0 +8c8afe70589eddf522af787fb9ad24d2bdb1ee81d37fec1256d5377be8e2637a +8c92df08bd15eb4837c56a4002fa54468a4a0be4914dceead4f8f94c06b610e8 +8ca34e3175e2298e0118986abf0343545d918eee9a0d644ae33be1df1e0b4ecd +8ca6a14d6ef0781a14c32d2d376715707450a23e56c7660acc91d5e6e3d21700 +8cc4e60d16a0be18db7af9b68c63ec31863803e62ba9f783cda07eb601f9556b +8cc866c5f949d01ea9bbccf6b3e67122d82dcccf8f5f8bac33f743aa1ccd0a7b +8ccf09c2e71d466690e42ebd8f7fa38c0ace73bb68acfe3c5dc91d10d4b77cf6 +8cd541e30833d3a8d4e2c132c362a6f893bcb8c43096620909cb9cd1ec3c292f +8cd65061b373b933b7248b8c4ded203872d16eb399e2bf168c3026fd776ff482 +8ce850585e50097ab54ed06ac90c07a6d676edd78090323e48cdc8871ca4b718 +8cea6a56fb33fde544d31ea352083dd57ceee735ca756c3b1269c61f9eb32399 +8cf02949ea03d3c50fd90c5d4bc16c7263ccd28a84fc86cca9629022a8b3e2e2 +8d028039df903855a6e450720c9472bbb1730f81edba243e8eb1dcb4f693d16d +8d05233bdad2a0bd6380ed1f2e0c885295627fe794f549400deb0fe92eae0060 +8d057ee7183b7c1764dd462883575b1ffa9087c26acfa429a516810b70e6ef3d +8d0babe83ba07126421df23085ec8e6ec48f955846ea0a6d8ea539cec173d679 +8d0e3a1a6e7b3bb763f6c852074a2d450debeb2a7fcd61b70c922ffc021e40a5 +8d1375f36cb6631eabe8de4fc0ef22f0cd949d0af4bbd9303756afe4e4758694 +8d14cecd96e0b387b5b6094c9262b866d02e6c43ae05b42331c1ceec4834893e +8d15ce86b92066ee66f0402146ad99af1d6125ac94899ddfce7ab6e9c0bb8347 +8d167a0e89469c6a7c415a992cacec369af68c0f81fb76685566f79e34c575f0 +8d17c91680a2e62ed6c9c3bca4a6488ba8aad16127d7ccc002d1c39bdebece11 +8d2dc2b103a2c696b6e3fe7686501bf8aba02ddb623fe1a96486109601cc6e23 +8d2e371d2c4c842acbba5170178c0fc369993eb0d327e5b97aefc451252400d6 +8d3c00da2ef5a0e8837e46e3148e99200143ad2638994816fab102f7453b28d7 +8d4c4722eefd8e1e5f8bfbedc65c354234e80455835f34890de19b277ef1143b +8d5101440673055a2112928a9d840b2f6035118f47d5e46b42361a3af1866b11 +8d57f63e55fa46edce3a4872c57fb7e882a1640738dcb1e08adf31b48ae0cc0a +8d69bb9dc108b85d62de16b4e0052b751e5a6991a2b21ff15c59f773bd5833af +8d6cf8b402705f1d2d05fa53bc2167dc587b5a63cd454de8186aea176eca9994 +8d72dea6e7274bc3f0a651e0c3feeaa2aac27290c66c926d790d8001139fe8ff +8d7f9fefd644404079b83dd098103431ec551d8e66378408319a77d8c99beda6 +8d83b273112990986fa90702aaf1844522d4512341e90b8eaea251e3f2e0db4b +8d92819324d80ec2b96be244096fb5197c2fb45f238f1bffd5be8a6692477614 +8d959dfa3310dd089c46c66f4a63c0e6a1138836f6330cce05201fb8a54237ac +8d9b77393663ae0124e7b4adb10171197a138cf5ba9d14f37ad0428b3e109727 +8d9da206f76c54727d956dc3da02dca44bb95f2503f16156e75395032ebc8200 +8db59f4024b3b52aef43ef0b79e261bc10611e3c5f61a4826e6cda91118042e6 +8dc81af526820507ddfcc0899808452146f1731cc547724a0d6fd18607a249c9 +8dc8cab0938e32f4c207eb05a732999a21a8a49c761d0d7f64c96246dd8d23eb +8dd8d737b2b090b120cc72e5cca243885b828dc5ab2e738ca04bfebcb8a3510f +8df216b2ed5fec55dfb8f36c316da7906e030c57b9fa315999c1538e5b0cc9c9 +8df4bac374081dacac8196fd8a19c4251acc47f59f1c805b65b35552512501b3 +8df9a8eb7192ca1441d9132bd620c7512114719a3c48a053f2e1cc54b2758a97 +8e01704982dfc199fcb52d426ce00582bf9f5a443c3c7b1d1c3bb9f668f9a322 +8e02cf1df6b3c6d45176abc45808e9674a88893f629a37ec5680ce770bafd1dd +8e145faed1bbbc4a62e079a084ca23417977862efaa516befa2af3d39c515305 +8e14cc56bc6324ea88a4cf2e6724820585be030a3ba13a147319310307afc680 +8e19dc9148dcb362541d88ffa0477b3864b18b791dad01bbaeefbac4ba56049b +8e1c4e12dd37992d49a783999f24c44b4daefce2d5b76537fd40e3030aaf8ca6 +8e2cb30929752fa3535c18d65d90cd11b9e0e4e1fee77b219c86a3df86fff597 +8e318a48e7080b26ea47f09dc60829afa0801a191950211857d227fdc80ee2c2 +8e33e2477ebe9ea601d62a915af5842583e34bf9312e5eef9329579ef56f29a5 +8e38f76553d0099343703a7ea2444fb449696d4f7e9d766d0705aba37f701731 +8e3913cbd1f305f7172197db1cc13d18dc5b23c5a281dcbf31296e5d4b655e09 +8e406d5ea8d58c91948cd19005ba18bdafa0e85117684a62c133a2808d750873 +8e50b1d05684be7f29a52cab5bc5d5e9650b3c747faf5c153c25a9657b7233c5 +8e552bf0bac38702fb3b1c4a9baf3adde6306e361d389cb3781b6f9677742936 +8e6f2ee2d675a0d65a0badad70f7415b7d1f76f4a52ad06e11f9c6b19186dee6 +8e76a854a923129f810264b9a9e22a2959a67d76c61b0293f200a47597a920f6 +8e7a8e0a9827f4f4599d81d24936decb0f05eb9c0dc8d4d923ef597820dfb431 +8e7ba429d748d92fce4aad01f0ddc2a56ca4a004ea0a4aa330e05f2bbac3bf42 +8e7d4884f4162747b177922732aa4f02e9dcd49a024cf18d888a07efbeb0663f +8e9ce4ad836cda3480c57a80d6d93a617ace04f31fc80603e2f56da2fbdf4fdb +8ea12a37c15a049e72f912dde5ba598703f6b85627faab62e189aaf66d53e4c7 +8ea29ce286d18eaf245fb44ff4c14e4c417f396707696a71bdbd015b00854652 +8ea57ee8aa6e4d7a5568def02656b06e9708111d31f7dde48dd50f704f24394b +8ea688ba33052e6b79e6bd88013d4297aef04a0b2f13e59545aa1267d2ca09bb +8eb4f27668a8b16054b816e45b50b245e03859eef136edf854128c90d0a4f13c +8eb7bcc479e09210b06878feeb122e81755881711caccfe08784820f01be26d4 +8ebb45992109631adb8625474350999fdd5745e43819e803570f7005c5975ab8 +8ec224812550fd4a251d230aadf90164304f5411fae4105d521914f7c34fbfbf +8ec27d43adc5e0f6f1e9585a3ba81d124621979a322eb4ff9eb3e3c5e3d1773d +8ec7a645b226dd825056cd9bac7fe7189895a5306b49110aaef8effb3432b855 +8edba483219d2807454401c124d19b2612742b96b3d412d40a0230daf8032ea2 +8eded787b93ffef641fce0552d795db81c3b8188d4ebfecac35798b33956e632 +8edf436f83b9127da385eda60d2a13fbf4109a240c3fa4801b0e9c5b36cf5b2e +8edf7f3270dcb73d1e238679d6ff054123ba5c1c540fe31287f93e1434ada0fa +8ee3c029918d5f201731e782da5541fe7b40dc86c441e3650672c844d97bea75 +8ef23470459d278fbafb0d53b1eb322ce02c11cc2816da86c363c7d1c8e69807 +8ef5eb6e17b78ab37bf70e5a9b14ffc65e714b69a90992584bc3c6f220095486 +8f0ee4ef4cb56d38086bf7eff4554405438457e68dc5ca69cd8d516fd5e931c7 +8f29765016fefc804c8c3bba30c2d45ee5c6bea090362f15864f896e5b529fa6 +8f2ac7efeb04c07f16751b9aa12a691a770f54e1536bb9cc935728c93c58dfbc +8f425fe30f7732b7d5222cabaa2d995c58b6250b949850b8930db607a2f94209 +8f4c24caff4ea6a40c23f123970821396646b5c19f45b82c425f50cbe4a44e66 +8f67bf1bfadcf48eb03e42eee2d1848774a561be0a70e12167e5d5df954cbfd1 +8f72e219818fb2256fd86dc6781a64afcc53eed983a6a06f7cfa30c4031fd76f +8f74bf29a0dad00ce12dea66e0f246bfc7fae3f757606e4cfa024dcb975ffff6 +8f7b2a73ac08cb51095a312909764240efef54af32eb007ebdf3bfdca1aa55d4 +8f81e2c6b95d8c32c38cb3411ccaec6dc9b75c896c3d09e1448f8b3956758287 +8f8543e0269d8acd01327c981b4faf950948c85a7029994460392307554c3477 +8f93de3fb49d4a22c2ab23dd36589ba43370b17e5298035fe0c55c751a35584f +8fb206c63d4263f8334b10a6ba5699c40aea95d52b8d10dc916e5ce7ebac5f94 +8fc16b1342ca1fdfaecebfc9bf71597cff9a5dc00cc5634c00b656e952884ebe +8fc3aa74ff84fac62b33bdcc6806d5c5c2adbfe71b1e3166a06c9ad2dd433cf9 +8fc9ba184229e85ebcf27d0c622111da892b4c6805603f48b9a7980892791181 +8fccc070d1335d6c2220a3331632c3c930659618ee756ee75a1cb8efe2410310 +8fcfe419d8450cbd1850dc08c4a7b7084356de2f74cd3671a42cc85a3ce4c370 +8fdde227a449432c0fe4a09ae894d2b8d5bff6d76f203869a4df992929a4bcb2 +8fe0b758585a8e861a53d45c43445342ac31b9a32e42f63bea5ce6ee2447a7e5 +8fe933b869eff0bfae92dde4497547462c9658d23b039dfae25f318f303baa23 +8fea93a27c0821be309f989a859df10d468c6e61d1910885c289e9e7850dbe82 +8fecad8ee2bcafa1c0dcf3fc2af22664ab8eea89bc2dabf40162ecc355e8c32b +8ffba7a6a1b2ec50b0878b884fab06966a3d2599d0443955b335b20014ed41e5 +8ffdfee23d8703e9ac820e88643181e3fdeb42633674776340520563739a59c0 +9005b7c56ec6161a93e35771d3a9ecae12ef4fdbf75bacff67a3223b277d4248 +90143d3d083237736ec3bf31e409ad2253f0f7da418c77e3ae8a9b15e23d5a42 +9018066c6d7182b501674ce4677f9370b8073fa581cf085564d00bc4f7b60b67 +902d4105c5122eac6a9cf7f9e37bc3cb8f9531cb3d580ca4bb3b2b0e736cb970 +9038ddfa38da760eb8628c447467e955d5e3738b94c576f139ea0a9bb9d86abb +903c5c701d85ebbbf88283fd091ae673a65b0f7d5c75b1496cd613f82c006239 +904946aa56ff52c86e2e7dcc891cef9fa197203e1a4df4ceaa3a3d7912cc9735 +904d812291211711db747226b6d65567aed2355060b4135a5625b47cc62b1b28 +904f0bf1141f5b85605bd69a231d3bd5f5b98a8e814e01f3cdbd2e8df0be19d1 +904f248629b50c08983f8bc322ba87fcb1d069fae1fa5ca8b1377455f0c5143a +9052ee07e6779bde40eea5cb07286e29bd2ac44638dba8fef9a88322105e17de +905d25a6e41778cbc01224b5872e1140a588e4a264e52493eec40f7f758eefb4 +9073e527d520a7820fb0d62464fc59e98e4b9f81bfb3c9f7104f5acc3b5f68d4 +9076a2ea346004159a0a30f83a066b463929db71f4dffe11fdb039f27100433f +9079bc6258ea704b6da722750004a445c2f9bad832375d9482bbe44b3269aeea +907d879cc49852af168c74d3f4e8dee849e0117409361031ed48c2cf0eea5898 +908141635012dfa09de724687428894e03aff476bd254361e141ce77fd35d933 +9083ad6d11aa80b450ae51e1062463b8ba03cffd7ba50e36584646fd1876ddf0 +908d727ec15ea41071d24bbc0f3ed1af66da261c7b7617bfa57687fae35ab38c +9091f3ac59e7a35babe626d5cba1a8060970271d8354e220668b0290bf4c8064 +909294ea66d6fa09fea0dbd189be92d452ad0b03f7bab893ffd1485180af5a67 +90a9f77f2b7c922b85f409aadc163fde14be3acd98663b1a23c638657067bd61 +90b6b07ebc02d6ed55859d0b80c712b902fdf3a80291d7cdce9a71a9a75f068e +90b7d6365b22dac621b9163766f629731b31d8a17f0f898e82580cdf47a8c8cc +90bb1775b7d90d9b395d38173415a0d7db0eabb7d4f7cdf7d95390a754966ca3 +90bd8212e142f79a76c2b5a0a79e638650d59e30d6b8857778695d0b14cd0228 +90c1e975293148ad14456bfcd431567364eef50a1dc5a46f193da370a1eba9b1 +90c3a2939ecfd598b61810f2a14bc1fb7780fe5da31f3acaaab478ce04e2f6f7 +90c45610806e38fbd17d0b4840fc21ab63385eae3c8df083be8f9a261c0e8674 +90c6c04363232b8c9750e9e8d677f2c7ef14e707cf24432d90e982d0a16d3ed1 +90c77f29087f2d51c548c00d01c2020845ff6651ec2b1fa78c546c9c878e0cb8 +90cf548d52b79ec48446c2ffd993cfa115d45e509a724734195caaebfbe5342a +90d01f259ab15e02f41197ba40cad65ff4abc5ccce2d02836a23d13c1fe634f0 +90d47dbae0a5de03b5edcd38a184cdaf80985aa3e1201e7fd5800371e3fb787c +90d4b19a9304a137ce97714afad820089dfc8c16e8bb8084ed9e03bad2e258ce +90d882e3c4fbbd67a464f6b2b459692e349a7aab0584df01442febacf513f096 +90e1a771d2629ad91f8959cd4e7590c971d7b1b6cc2f5cad4b4a3628cbf57114 +90e9b150aa4f3861dcb7e23037103804ab338807db1a72d661dc2152fd372a3f +90f54665bcbce0473c26eddeadb952b99577515a29d5008d50d815a0f983d508 +910286347c801d08e26fc711aa924612a1fe19f279edf5957a3584c90fcb155e +910892b55d083c7314cab65f53e5af2fd184e45257d3f080a7e529b3cd7c64d6 +910ce79dab8b92549da077fd2eb191aaa553494c0a6b9b3ef90223e63669740d +91123a4a14e06f9373d716c561a3d759e39d1b9277a8e4ca736db87b04ef7d41 +9118f64ee9dde3b118619232412649bc768a3358f19b067ca5e9cdeb3736d670 +9123798d4ab70ff062e904d578f7f600640c44cac30727bc37c4e6fbea8798eb +91287658aed90dec5be7f631a6f1a0cfc24622b1294420983c9c8e3d54604db0 +912c2310d5bf97c08b400a1d8ead4eb3803ab463e63c675985644dbab8294bca +912fe38d0465fbff6d74aff51d09182408f93f33ec1cdb0ef70b0a7561e3256d +91366c2e82731835a1717c1c63b3bb0e03b1ec90709adc0e4c2388e046be5313 +914977354cc1c4ddc881974559361f36429a9e28b230a8e982bb4878b9f659d7 +914a75d2ea9f9208bef2f47c296ab431f3e8ff30d835cd0f2a786b45a34d133e +915976a2d6a30717e51833fdf0bf5f18f4f6b714dc89b29efa089d12b5130e9d +915d3d2805af7b223bfaa4846944c3270cccdd8468ffda784b37591ff597c4aa +9160b26f5fa14e78eed89f9f3ccbb7e96ee2fcee99949364b4d7cf0885b171dd +9168e5997dedf5045b3239008b6160a8d118cda24b5449c0fc8bbc0b63276f67 +916dff862d20940646d0772f7756a46f9abdc4095a7487ce4373093849f4677c +9176f0e0ea0ab45ef37b462c9deac240a6b21564a812af62b031d5686b32a849 +917c4eab6bb396223cde628f21319e1699abae1897f169639f4fa34967907d3b +919524b54da24444a67677ce62a8325f8bd4b5cf4e4ff1fbf19fa989a8f8af99 +9197bd990ad08f021b6327f5bf74ce053d827b299d3c6a77b4fda9ae8d2da218 +919d9aacf1307de9478604195ed7ba7679fef9d9c3f46f6da9661f9161302a00 +91a8db396706f900c7e1dce1344143f03fc3d5cfabf0d00c545bb3bdfb15c62a +91a9c9a7fa89ab12b47ebdd5d2d36f636de4445afe3de12e417908cce92631e5 +91b6b7dc164359e8c0348d39fe5ee587ab3cabc2b5daf7850933ba15b06218a8 +91bb52e0120c0f6f0898f8d8160610d6caa1f83388af2c6cb9b98a9a0341f456 +91bb9d90ed05f519c00853c2f2d4353e419de012ddf989b109a0e15317ef0b5d +91bfffc008173c4fa273eb0536dd5f5243710ba19d9d3388a05e40699ba141b8 +91c063c59d13ad1b5f7d29e652e3f8f9f97ac1d6470570739fcd432f6cb46e4e +91ca7f46fd31cfc356cc13d52d1002d87c63bdf211be5a8d182f3c0a6ad7729e +91caf2c64d0300aec9ff9533803cc92a14fdd773772d0d97e1937580c40e144e +91d790208c96097b951b212b645db6ddb3f031e68f958bc274d3232429c64dd7 +91d9bd72e919283e6f64116416cac7fc159f81d6ec8b5b75e9ce0a4988c9f0bb +91dc4a3a849c094b32e6c3b9da5f5da57e5ad4c17033081e2ddc332afe16dfb4 +91de5bebefd8bcb5da331b5410f706104317bf087bdc9aecedc12b0a701960c5 +91e430bac374efb7d943f6019ba62122bab1097237ce4b01fd3a5e32a98a9fc0 +91e4c7d0558758bf1b048b19244cd422a20d912aa7b811c78999b9870df76426 +91e52e2229879a42714240bb58899e57ae9967b6b135fd9a0262874060c44ef7 +91e57b369502e522c899d9b99570085ad37b5195a583f55c8bba7bc04b212e34 +91ed61607ad7a6c40c829b590b78c2b83474fc218184821f8a449f903e553e36 +91edfbef4039de35e851102ec22894420932772e5c4e5482c2d914a07fa87e60 +9200b33a382ca158317a31ab06e5b90b6613cc74d207361a3889d54f488fbb4c +92102b5e17b86b06928ef26ea7799ab94c7e499677558214da7470683b6f81ef +921610a15b38d3d771df505482f5c9b40f98bfbadd70bddec39e0a974795e851 +922608ef3e569f55dcde69d2845e41292bce504225515acfc5b9be7508e5f63d +92267051aaaf439c9478cf47c2183a7fed9b9ae318afe4a1687e67925a5c96c1 +922afeaed2b7fe794b6532bab2692b5cbb0418f2370d2a6c2721d9013909a006 +9230c7fc62bb7e3599e14c30e773e7e8eabf472791bc9bfff4c447bedb676a17 +9233dde4f348c4e80352a951faada231e249c4b8d3fb4ef14ff54468915a9c54 +9234f2b9361b98e535ed38637bc12882b6771192b56ffb5f4a511ecd11a7efa5 +923b807154d0f895fd6d20d2db2121426dba739b8e68b421a08802bdeda8a09d +9241a5093fb3bbf3d51ff54ef7d289fa2a153ac861b0fb292a3655a9c97ec532 +92435a9972b00e130028968c199e39951ce32ece7700549f142cb764ee45e5a9 +92586fd604bd08b8f2c8e4af722f6b1d61e13ca236cc9326ad88fdbf85b3836b +9258f9b20db505812ae7dbd0d4acd75e08475592a4bbf84ad22b77267abf1952 +925f694e1ab7fa1a9337adeab960a6d294a246b0388d57374143d15da51c9bcb +92632485803799880195cf1b3e8de79e2a47dd9d9d7952cee4932205af5b809e +9270e12f9e99046968c7e9da4c1fac76a6ae8806f11844efb524007aa6d7b0d5 +9272ca788da480bcf5ed2e544e6a2a9b77ff4be64d2c3e1fac4c2a1e44d21b01 +9277ffe2641f04433c85d6074808e54c27bc95584edcfeb69fc68484649bd9c7 +9286dae8993a82a87f3d0328622fb61f07f7b1a27f425bf536596c05ce940e61 +9289b7bd0ce198b5f9db368abb2351f92151e8cedc219dde9969d95d27fbba29 +928a77616eef3f05e8b9e0f8410acee897f0b507bde9608b1e3f8bf62d5ac862 +92a77d0f84c52f2d6ea3c611e75ba46d8f1a03fa61d8a93594ff2d15ac460c2b +92c0b44993e59f3d09e7f7eb5539bae8cdbe6cba292d22cc48fd5a474c9d454f +92c97792f7214f4aa5b3b0f572a5250a1d32c85f240b7e65dbf804c817b7573d +92d24c6baa21a153df933aade4a8379d50a5b742c6eae4089dc8323a5877c971 +92d2ef8e380444eb8dcc54b30bdcd155f07d8890ad340a7e942d97fa087e28d3 +92da3a0bd45add320b3ee3648afb6128d835b53786fe489f6280a3c44892415b +92dd6882f3faeecad4877620f809f5620105875513f930466d6a36e27ff83db1 +92e24082d38841f951cf288e36c81217b702456c84f1e39b48494d02250bb75c +92ecc3b5aa8ee0b37e96f5a77ace94b6ec99a7c48f944e069123eebf3daabcdd +92f5cff06ef7c0cf1bac1d8e8781e4e5d76a7a98806e4d871b82654c453b9bfa +930953c7d3a91cad1745331e775c3461c2994b42b2183b9a933bb6d0e83f8cc6 +9309f90c24de34a8027d11438819f413b1e1efe8f7297a38ffd4a565b71851fc +9311ed577d4664cd72ba43fd1853b75bfb5849be00910a333d70a537f4c62588 +931a99410304372eb69c00c1b786383d3b58933ed2425ea6eae1770ef78b506c +931c98dfd1ef5fb28175867e7fef2a9d8ea3f2e5e87bbf0015ca55312ad3e9ee +9322dd9141262b8598e07ee5bfbd6ed0087f67f5cb07e04ecd1ad07e53e91926 +9323bfe67208af28b0a15762be64ae5b820e2aa9ffb0b7c581bd8a635b25aed3 +932b33c19a39baf69c0dac2c20162cd165a3150af4d4573bd4d92e3a76293504 +93346596eccc6f9932e65a95fa0feec4bb8c98369178ab4e11347eec5a1b3333 +93533f15cda6a6dff2a1f582b4e4c4a55738f2c07e99fad55835b1237ce6e7ce +936eacbaf31ab3b35e23a8f4a1a713564e2191d855275259c1941feb9737feaa +93747b750b272b9eed27c0e3fe48a96011af6955066777b0a80e68658c167f48 +9377f8820d94b1815e44e520d5a026030acab61e2c375b24f52568d400bd7c88 +937cc6c974855ceb8ad23530d737893f38c1d037350b2df920da37f164dfe2da +938d83fc49f8d8d7827237d44820ac864911d9203de05f3653d4d6143d4f704f +93a75b23ffa8c6da004c1921b2b12b15fd083fd86b2b2eac2830d0e27d12f4f3 +93aad0b6591d428808a81d7697a3d86bf4806661778beb6894f8a547d032aa1a +93b19d84e9aca692c75cb160a050fb53d1b211686b3f4c867a4e09a9b0380027 +93c2c439f48db81651d6e6c6241cf0865f08a52b5925309bc314348745f71cee +93c63ab5e0863e5ba86be4b1f9fa0463eb4b8d10f6a2ff3f6bf91d6e43ca5dd5 +93c86597394e3410565d1a310bf97099bb7c1883d4d5b5dcedfb95ecc33d70b4 +93d03ef70f78b59edc0cb0dd76c05a0e812ef4dc02a918aa755eec09f40ac26c +93d128126403257c96c8966a7bda097bb9188ef35c420696fbb82b5c82d01f7b +94102b4565cd4af3587aed1f4d3c83d8e63778b70bd4d1af6583204e15eeafa3 +94123ce5546af7e18dd6a7bef1391e5a9049d9c05d9d2f77eb283365f9f3015c +941e2af55e334cb43d53ead2254503679fd63426ae3ee4f7f3b800b3e586a0dd +942059bb56ef161541caebec257f0f02159e53c3539d9884c3d84fd2ab9470c9 +9427b78dfd6cfa14ea0770d31336e7215e1d6050bc8070d85519843049862332 +9427e8b5f2160ca54d7454658529bdfe182d04d7aa755d29426cace45447781e +94297b82934468d7b1f05773c5a978f25f1c0fe22fc8f528bfc03aeaf2ca60a1 +94313b8dee74f27cfc183e9dea118ca4073290dc98ff6e4190306dabd91ba661 +9432dc71bb273f82fe3c032b302d39d5f327968e5b4641287d7b7ff13598a1cb +94382144ef98f4aea8f3f3fd641beee9bc54c8d53c184512c9ffe1018634b6a8 +9446d788319fbdda6508ba91d2efcf0a06f051f5cd28fa3a3339836a1cb7a380 +944fcb5bb940e5bc701bdf4496008fe6844676287fd87e3478cdb4d67c8b5b78 +9452e4ae8e09a61dff1ff724c1732a16d3652bb4a3370365ae1fe4ebc65ba364 +94556743c4052ad8c972474182efeb90e659b6b1d1ad07e94d32d09ff4807d7a +945630484aec2124f7d216ca72372a68cdb556620eb435049e9452a4ec9b27be +94676bcf9e272874eda032e29344011d188615b4abac356fe646192d833e08d7 +94707406abbc7b6ec1b8974e70c7a70cfbec55b2ec2848ae321eb3b569b6bf5f +947330c773409cdb0988c8c06b7f466dc4ac2233c12e14a17276eefa17ae661b +947a2d589e5f1e57c1d8bc18879800704afc34fc15ef7b471c4d33c53f0167d1 +947aa58a33dbd9766881c00ccde4167fe1129e62d70f942d1859f8374acd970a +947ec5046a70816176d171c5d38af4a6d986b1d9634fb05aa3d77576fa441476 +9488e7d55e48447fb3a88b9e75fec9b3bb7692005315599080c91a8f6d2d52d5 +949a2b5d8d85b67fc936e00c80cb9c643565a1ebb25c27fb6f8d472ba23a2444 +949a74e0ce4d6ae0f2c73a3c510c73f4a9f466c652536e29c066b2eeb6259d3b +949f4be6f6f146f56ed3236bbfbe0cdb4369f2051ba5b9d4fce3f767bc33c884 +94a2f93130cabe4676817587e66ab026b0dd77601d16532b4922f60505c4faa2 +94a54709bdd59f1b204d797b058c77de702555e835eb29e5d93987c0261da860 +94b5cd8e9dfa77daffc009983f5b7b426c0158987532e80ecc018e4650d976f0 +94b633f40407f2854a8be1ee1571545eed87bd68ae055590ef6ed55d741ebb9f +94c8c9059143ddef3d628251896e81a7766f68363c02ac45131a092c125d9776 +94ccd8c9ffabc804e994c1ace40f00006e305af88138d49490020d1728d673c2 +94cd1cc90d6a27f2d3fd338e23577b18edbe9cb2be6f1f4f8e892ee73aa8cdae +94d66cbbfeb551ce717a1f1e58c0a87beef381cab451a3ec39673447e1ca864a +94e11228c54c36e819f758862fc4acd75b4b3d033d27717cd842aa3a3ed607ab +94e82c1f92d33e1d93575d28da618c7a8a4808487d9ad5e68e780fa453a51822 +94ed9fa21d06bb27c0a88a96dca84cb0a5ed94e1f3d11b819278bf804fe3d915 +94f197e92be351e981dbb3bafc8c9b5468f84022928e55995a73c32b333f74a3 +94f34d995c12435a9ea1671e3c4992f4d96af7f255c8f6d9236f01e19ebbadf4 +94f59b224825a6e489f057a385e5cd24fcf8d294f57a26c7ac2b2cc82b7a7d75 +94f5eefa4e58b497940134de85c5f3d33a0faf9a0c20ecbcf03218f607bf51d3 +950e5ac8c572ff506ff56ac57b42951aa149332103d1f7285edc84c55d297079 +951279a9e6a5e1aa00cded0870d1f88edde080ea3072886dae6c181cdda0089d +9513a80c715c4ab938ca90d31d2e20f06eb98549dafac082447fe86b23bc5c81 +9514dcaf92b5e5ca6e5e941eb44de031fa4e2f52223ee6230c4c5851e756f800 +951783855e759701d46ee10b5c82f7df5e8264cc21ce37ff56253d12e1c2174a +951e2c6fae90e3516cf27754fcb06a2b93242c5b6de7b781b9ce9438da363b81 +9520517233935fb57b90d11b801717a0076b77cbb8b6aadc29b4383047e46dce +9526edc8442043c655d2fb5ac939d0e5d203811fae0b437935a34a535616da6f +952af151eb660f7eeb77d42e3a86d455abab6c3384bb7dfb0db4925417eb1e70 +952efd4b6f05431f13dca7bb582bd771ebf27bdedde60b4353140c4aac10192e +9541a21c5de1b8b8ad7e6413f2735366a15d5b025a8575e69c14c3e212c669ee +9548219a8235997f7e60060f415ff1e8dc19d3ecaf3c00302f1730a793e67ea5 +954bd0d9a114ee1d2c96dcaecf06c3fe20697733f2d35eebec346c35370bb281 +954bf2711a14f7f6f6b6fc036baf757d05d8ed9fff8ec1590caf1c6112a7c462 +954ca5fd903a5e7daa24e75b295018a9bf5ba63bbf6f59bbe093134f9d09efa1 +957095eb388ac9f26a222cdf66759ce464343203f49e34a57306e2aa694372ac +957d098b0c2f6cda1f6806b96f72ff18ecb7e1a8e31c6cf20b0cfec1a467f451 +957d4c61c3322e4512350b2172eade8de2d7753a7c1d203c6289a1e6457b715e +957f448319660f30f53554bccc248172ef9ad23e168d5b64e4065a3cb66c859d +95866e8a99578410be5bb6accb3b2070413abacaf4fc257b74321ab76042a0bd +958785d307c5a2ca53deb5c3fd5d702c498152f04b649a59543f4674465d0219 +9598560f72878acdbd5ddc766a4075b93981d3898d4d56735a633f504e43dff7 +959ac17e68f21e18b4e5e78356be8607c25721b2fb1d3e601b099b4ad16f16f3 +95aa76a10ee2071d078adc7081b5dacd8e5aa7c07384e6938521ed6d1e88b55e +95b0cc24b563738634f69865ac84606fdf209d46f15d80e5e045676ef9ff5421 +95b464255e699c121f7162c080b30ca8792d52db9f9a12fa34f054c90ad1c38b +95b8acb0aeb747d0002cd31054abbab1341272d247667aa881d5c4861fe0c3e4 +95bb6dcc4a608e8db2b44c7a7fc52530980199c893d1285cd8e3dd35fae57cbf +95c6a9dda11fc029e041a590c7761b73ed843c99504bc3a10aa4e0b4cc15d4e8 +95cc4f1a32bfa7c7baffdc1982cace1505203bbda841105ce2c3fd2f5429c8f7 +95cd83186afa909c68b24bb212eca34cbcec24025316a3eb87a39e0e52dd506f +95d0f25476918e43924be137c17abf1839598010cfdebfd5a3436141b6a2a6d0 +95ead86e17cd26e9b7c8558e3d7e7902b25b5015b3f55c727c1f685ad6fe5d15 +95ee621d59d679324bf57926242ee1119f4fc83fbf47d8d529a71ef85f737321 +95ff63a1b05e7435f26a1ea1df089d07ee013f7af2bc10e79e986081f538425a +960da285833f3cae0a7327b46fcf33009c92be9fc455b5468cdd994da176c3a1 +9612b587f63e523e5c3908b450132342cec88dc06075b5ae1a71a40bf0f97273 +9620b848a9706b218b0625681949897c230387a278ed0f83002bc2e1da096add +9622b3054606d4d68bd0952bb5a1949f8481755ecf82bc83c270cd37eabdcb92 +962f8de1f73aafd650d469a4e011e84d965f5e7f814ff06f3ec921a761e910e9 +963266626fb122b429490aea0f79bbc7f6feb400a2fe74973c1bf631402a36a3 +9636c9eadfdf0f30c7169f6bb2a8d5656618f708d9d7cd9858ecddf9b449a405 +9644dd53030f22ffa3ba730610cd44a68150bf49df01be149bac53250352f2f6 +9644de0e6ab7e9cf07ac34dfa10ea802d85f9fbdbb2e7e0042e69bff3184f875 +9645ec53aab386da03056884d02932afd7510a8fd55706d35382043d2d47480e +9648fa2733e50d0c7d78d28e3b355ea7c0eb817c6580ddc1b7b19f7281762bc2 +964f980f7aaa138e70cbe1860943363b053489d95039d418c46908298130f502 +9658236a94b584f56c88d89ac6efe35d78e31ebfe81cc3cf91847cf80eeeda04 +965e4190bf473498fcb80d59821ba1a631a7384358da1a1616ec41fed7bb75e0 +96731e5a3869d90aea79b3b3e10186b3142a6d3ace02234fc4d4b082402dbc15 +967373357f8718de4b428fe70076b699a5c22570448a237b285bdf20c79c4509 +967a3266694e01d63e23058a3e25abf5d0d8079db662545f48b45fc4dcbd3018 +96836f80083438292c4938470c773dcb5f8ddbae5876e6cef09bc2289b01f9fa +968514d4e7abab05f288fee095237a874207475f70565a4a337f8b943341ba52 +96907ec095f1cbe8f8570577b7f028deb8c4425bc9d2c4864a849889920d0349 +969c43454345078215f9b056b6e373c34d29747d3c960d6bb31f2832444f4108 +96a1544b8f0d373aa682f0f368867666f1d9c062b2dc2d9f3cc458c1fde44ec0 +96a22e7c12b4f826c060ffab7b068cd8fbd2077797f512adec788f6a3464d2f5 +96a2d4b994e3daa9dd590a7a038441686ce337e181371649a075aeb00b677483 +96aa692860361bd4eb8aa7338fdf859aa78124c2c653ed2fadea4790fda53769 +96ab05eb57e3a8b383a695eaf74169605a300420c902ec7b6b5f2abf0d13aada +96ae76394785f68677e9bf1de4a3df75c851c16b4560176d47992cbd89e40f45 +96b1fcb8b96baba1c7431ccb0afefbf027fb49eaac3511cf3cbf4eb9e2181d35 +96b5bc336b60895d4bc7702d5d5d243a3403ace7af72271f86a0a7d7d6aac72d +96b60a1461c4ed533825320365546cd58c96fd1c364bcfe8a85179597ee1ae04 +96ca94acb8df57bd3c2d4210b125cf351ed2cbafe51a1a999eedb6da221fe877 +96d3d53d1753764187d1d22559d0c2229d1fc0971912b3dcf32bfdabf101cef1 +96dfbdb09f43649ce1616bd5cb26f23c0b82c1e53b1f08a61ed5d86890314dc5 +96e4689ecf197e113d3b99174923bfaa2a4723a1b243c79fe5e0389192a58d98 +97026bbf2ce7de17dab6e845f184cddf97810337984c9f6977b13cca02021a37 +970b972fe561f9738f0a87911f4873f19db5051f4cef5512624fc4e6dd140ec7 +972486fa949fb236cfdbd825f1ecb62cf9715e461dc7182776df47bab5618b5e +973b5a5ca6184f1c300bb894a0b8fdc5de0ade214ba2c7a199596996abdf1d9f +973cf8ef8e43230590355e3b54f68e18820571f86a3e1e7f5c106559f6e083aa +9742be6dac034ef552847c1422c9664d330a7d44dfc73d5984d97eba812c3d2f +9744eda1502d4576dc6f8f4af379464517c84c306dbcca750adc8c995b658d3f +9760b88a120c143805a1e0ac2b3708f079d41385d86e99ff265ae076f5f6ddb4 +9763d013c802c4f18cfda359493a9b40d4e6add8712f504fecfdd3c2ffb0d123 +976745390641b5b44370308e1098565065644e275f733eb019ae17edf18830cc +976ad0347e95f5d8e0b8b4e3f4b89521f63d15d0945960c435123b67d4421f7d +9772b3adb04f0b4c21d8a6096ea0fd1b8230e124cf000220838d88163d36a1e1 +9773b4856de6e213e79390614dbbb788ba70d825e0041bef40975edff9328c1f +977420dd3857610aefba5aff916fa87de8c6b556fd774c7bff0161120432ccbf +977daf61cf430466798f8fb18904f0231c579f8c6a53dd834c6aba069d0049ce +978a4b5e504a2118cb38daba92aa8c9660c3b35d17a2eb0077b51b6338457458 +978c5e9c27d60eb8ba2bf9afeede3f93e58f01779d2ab658e686baaf582d073c +97928935321e986972ca963ca633cb33c3733e36d1fd46fbca425a447dda5956 +9793b1dce616db5760aa2734238d08291e89c8dfa6e86d54648f2d5c0c15aea8 +9797df1d9ec1aaac62b30487189124744da4b4a736225dd7122d12f37a841431 +97980f311899c139c32ab8c6fa3a3fc3cc4a5abd5175c31d2e5f5390e7d243bd +97a245cbae50fdda2fb30092787e3b3298c009f006fc1df428cdeb9758bed743 +97a2c2cff16db628b6536249a42997b98d91dddc3fc19339b812cc787c61ccf2 +97af5cfe5a1fd01640e2cd6ebdb739f34afdabad0b6e839ae37ab67fc8502cf1 +97afa88c1acc47cb42f0e646dba38608a37f4c66181219d93ef1c9a660f16879 +97b7c3d9a7e1bf5cf82e00a77da5422212a2db2d80990008fab17875364320ff +97bbaa98b35a3188da63059f835ea280dd8d55002905728ae9e3f39804c4bf2f +97beb832ce301bcc93897f881235782cd363253f98c9271a9487c0e5b87bae6d +97c6c2e32f03678c70446379a3936eab2c656951a3455199e53f2d349269e812 +97ca5d20688b199f531ae8759488bbdc0c1ea4b574f7f1a4dd43e9b25fdc2eb2 +97d7bd891a6e96f35ab1dbb3ac2f335351796c820d0bb2254a4d9affe0880d2a +97d7c4442b12d9c2e307a2d597a8ebe011b1a856fdd52e52dd2a4cc23b7017ca +97dfbab59ca920c6229e5820515471fa7b23fc515f03bce512c857d4c6a3889f +97e6e1e29b1baf50bf71efb78f15f364ad660ab7ccaaad158a916ddfc10e7352 +9800dff8ac581c370764e0bf67e8ee7e2690dec616401bd2a4eecd944be9aab5 +98028a435afd31477db77eb44c8b7141cfa3ceaac10b6de3ca8e1551e85edf05 +98057578f14016906acc6e052f71887f9e446373046b0d37fe354ffa856f3818 +98164a6507099edd2e007f4677055045263f2384f8b1f070770365199b5377d3 +9818aea8be5faa64be197d264ef652b16e3bbed6d8b865308da9a1f04263de0d +98196f7a47f237ebae33a3aed9e3462fc787cc2c129c014d049ee8afad3d176a +981badc6ecf4547605c7b88a25e2839095c695f914b86bc98a369aba36bd3f8e +9831aa4c519fd0f8a855d0209afd1597b42564f0926a55c2c5e3ff2cc4b7d731 +9832c762842d7b4b7306d301f07f39b4a554a29abfe5b4895e9834c68f3dbcbf +9841f21652dcc1df339c6c89db9a788648f3cd2a77b5148bf61b57e959c27ace +9845a75b8831c3a53104b43c9bc898c986fcbdfa2119868304bb706beaa71376 +9859932cecd81281a790b528d1f627a41eaa5bd742f2a64e5006d35cee27045a +9862b15c6195d0dc9fbc1a6a44d549185155ea84bf37acdbc977b8c93c414d13 +9864014a690353fff549cc1c8a0f005beee6cbd91d150dd5b3f3e257f64b33a4 +9870b5c3484e4971c1458bafe9afcce02f9a68d4231ea3ace9d9fa2986d2d8da +988073da44602f9259bf13779af85b71d98838e1419944aa52e4c05b02198902 +988cd2c286dda7a518929ec0a19dea647fef38d9426a33cb34c4670abd027708 +988e2160ece7bc11172a8a7c9652b31d6b8c7a22789a8ea7e48eddabc0f86ea7 +9893c29f245610e57d7939025ed9eafcfee57af35566517711f580627366321f +989a100141ea4d58a6925d292becb8083ee979ca1ec3de671d2abdabfaf3c454 +989af2bbbdaa466bf985632c6b104e8f2e054363f126f77c31d1047a8881b3e4 +98a30b78fac80753414e0b0d8500f483483adb96b60a2702e35b73c7183fad3e +98a39cfa4192cc60d5536997539d8d584802d4e4436368c1ec24089b7b8d00d3 +98a7c68627357ff02082968ec3ad630e62de31aa1782cf0be4ac34f3bbcebd70 +98acb56d955563c241669b75c21fe4b9d2ea81a75c48f53e090bef5b5b86cc46 +98ad2b0d1ffdb2bf319d4b47dc2522c16fdf683b89b5c348327bb0be60c9e4e3 +98ad3697271df673f81d8068f5aa85fc4431231e5e3042f7d0459dc6a213d014 +98aebce4bc39d8986b1bc1fdc0a4beb63cfaa9c77ec47d226efd4a468ed39d3f +98b4ced7a8dfb9c23b42b934ca289d9a47802ad8396ee9d819a13b4140fbe295 +98b9501afb821ead6ff56512e29b5517a6f0f95de2ed5b0c93cbbd587fb6ab04 +98ca3eb5cb4de99474c0e8a6ad136ca74e285218bd9b54f02672fdecbec1db01 +98ca9b59c43cecbec8e04db91bb8bc1a8066e197815125aa08097b7db0637f8d +98d3458d9367213ce25507f3cf94741cd29fdb5c718d2b8261dfa4423dd222f3 +98d45bee0c2933f3922b047d0a1acca4f8ed40cf2f4839527002ff32f492631b +98d6a6c891cf9dfe4e1d06c88b92ba14c220e7728aaaea837a52693db44e8e91 +98dbda5f2501489e8ed33d9594513924da3b4568a0b294e2e72be7ba8ea3811a +98e62da79b3633b1243c5ee79ccbeabf6491dc13466a10faf2711cfa5e6d0979 +98eecf24ed681239a73e72f2ed7a401af9a07aaeafe3c7c7855a0445d22218ae +98f3bb9c8b56fc628b2c8d3aaf42dc0123143aeafda581b160ad35d49de73c7f +98f461285ad8fc54ee6468ac0d1d339418acf8048f8ea6bce185abc69120541c +98ffc61ccd8e212484b430a0d5386e2b924fda2be2e16fb34e72f92c567718d2 +99056578029a58d1fa1d9dc756b106e6148afd6415ce13f5985f1e5ee204d4d8 +99134543f1ed66dc91c9cbcc70a6d4f9f8ea1af2045f307b0ba03d08305018a8 +9915e3736ea8544f8d437fde32908b7655e3ac89d3f2fa8487cd06671030c962 +991bf7bdb2561cb1784ddf6e34b09a8cc157235737b1db45550d0cdf541fb90c +993726ddc3a041bd9e87688cb74bd53c94fbee90e050922b59841685d363385e +993782b18e1098833f9d1bf748fd8c6b4dd0cea376cc6750da6490ab39deb64d +993d1c3eff3aa9e4ffe5ca142bef6dbf1606f7ebce2e790b58970ee77fccc822 +99631396ccb5e1e66b1b6a462ba4f1fe6dd1977c79f42b0a4c638be3c0e05a69 +9968746ad1fe6db2ff4621b3d67b84022d9dcbe400ad922a1db3a0583c375db0 +9969669dc661e991ffaf6960c1d61148a63ad9ccb0c64568a066bf9f7864571a +9976922d4644dbf8a1a8e03e152409a9dbd5aaa992a6d2b7a9889f83ea3c9df7 +9979a528f5830d5b68f435bd5cf010a4e6b8a7c8eff874369095bd647a2c48c4 +9981d93d44b3c818371228ecf7dbacd9dc831f6fd383aae189cfdeee5fa0ff13 +9982a7556b650eb6c9e7e19c6681f881f126a74df922a7a0478bca87eafde5e1 +9987628506cc69a3db11eefb9f5d1ca6da499e418b6105804b3538b19a32d749 +99907c9a8e547db9d4cab9c3cf82f5660b60e3d4004e499a53b4d0a7bd8e34ee +99a143ff874ef6aa08fc3c5a864a8dacf2c5aa00ea8f07216c753b69bf5613e7 +99a8ee70d5a096062870e3a1e1a27ac8c1e6af9a19a73eadcbe37264d7d371bf +99ac0f61243006eaa9af81a43a09fba17b6e4b1448d38c9f1237cfc3d6744f41 +99b4b252b6e9645909d4bd0f258099dd02f799aec2b459d45e06bcb9bd079850 +99ba1120d1e730296941e02d47c71a244716d73d0a7f41ff83d62b236670840d +99cc4c5f36d0cbb22a1046bcd573bd481c62cc49e7ea041e2f511aecfbc42a7e +99d260b993865aeb1f58453ecce72f8592fa6d05434b839a6b8fdc07958a09f6 +99d44fa5e8701cb89fe974a6c2157e3a716b73fc8e79828b4674a556f9542077 +99dafa0429d4f6db6c0f573502cbfe1a8c7fe804c321be1afc32fa6a5a71743d +99db4b2a2832652c6ed3a81042bf730215d394c82f22369422dd278fa59ab9ca +99ec88d5038e97c56ab70bae41e2f9615668c55a83ecaae24e81ae760306119c +99fec4955dbcafe82051ac478af8010c2b17983d9e323e4922e8718b4b46dd89 +9a0564f64f4fa5cc4c6d79755bc1dc2c6a6f9e3f8cce8f25599e0639cf02663f +9a07bef61ab469b9bf1fc487a22db1e6efb87c186a0fa34b520c22cf5b414285 +9a17f88d0a22003c2cc67e8015f573fefc7dc00d432e8c6ac943c89d4dd37a11 +9a19193e8688668ea794b05460d4975fd931fc5de93e0ee33e6e1d2b0f399c21 +9a1cf27df5b0ee04bd21098bfec7dda525cdb2d766ec78265f3e7a57f9778870 +9a2945d59b1869ec12849f45b3a5edea11c09d9788b2358e2df7dd4e91cb0724 +9a31d444922c7f452160a2f64d140508bd8cc62accfc948d1db310ea72b45df3 +9a3997ec59bfdf981a5cec0cba87e2e4da1b3151a9822e6390ef9ed256430034 +9a3a6746eea5e18b56107efaed1156d125aea587fb62b03800498f04f3aa1fed +9a4a2ab501d6d9e3386b7828d8e718de8f2bf5350aa09b76255ecd50bc5d1401 +9a51f328b67d30404597bae74d981d25fa595e0345ab19fa32db8d1b942ff049 +9a55d0b480c02bccf63c408e041ffe2e4106a38864d5326805d883db6d0db236 +9a5cb3864fb25a94eab2005eb3aed44330df0963f9c157dfaa39e64a0e6e7fe1 +9a60252ed00eb47a4064a57141468711c33de4baa419ce6f5f0a899cfdec8009 +9a651db4d117c6f6359ce0bce16c7c1d27baac6bc3b9a194021c96e57dceba2a +9a6ddf7dd9170c459ac48a569a8f4ba617a6855f3e93a6668b5d3a60de955133 +9a6e6a18382fe7b4ece58a4880c18740f275193a5c31d9285cc5de9e2a9dd5e0 +9a73e26ab611b67e2441cd9baa419be547cc9b1aab94d76716e18ae038174695 +9a8832813e268a82a9f2a65782de0d40ed90aa8bf0ce5a1a3f068f28b9a9e972 +9a8aa1df12340078022ac5adf0c9579a792d8f5382571cd0e885d51c82bada4a +9a8e99e76be72fe4d11353bf29bd202ca5745c7f9b21859d4a382edec71a41f4 +9a8f0b4ab4018e98f1e7dd5a878c7583fe8b64b8dab43e93b632203491767fe2 +9a987fabd712f9e3b5cd425aa4a2fc0e16a283f6f9f72cce6096967928eed4c8 +9ab43aabdc8f1c836891994b5505e332bec2845c3a14e379c6561c5ffa59cf1f +9abd576d30d6f177a85e141aa3ed1eb75003377d13d9f13c782edf631ae78347 +9ac02e372bcec8a45d7853e24dc7cf9affd2f6625d7d18925d9b5e1d38592da3 +9ac54750446807c3debc8084d2aff1b783213860a8f00f711f3df41a666b3db9 +9ad885cf85be9a130dc8435a643b5933017ca828e0fc7dba28b0c38e5f659c18 +9adb6ead27a4055270cc5718cae5841db47ae92cc445af4b302e6291f43f7388 +9ae8169596b7742af8e30e809117edf699cb15fd32976b7c51e9a06fcec49557 +9aecd95591b3239ce9bf1d592d93b3fa61be23476981b6f31c383a61e2670a35 +9b19e2f33ef04c6722e6e3e5bdd84196912bbc974c2d2b0b1caca19e64b0d118 +9b2a740581a0c3d3413f6ef81422ef708ce2473cf97569c642e83bfbfdefadef +9b40f3fd3f1c0d79b2e2a5f7b0ed13cc92d8c80e387c4303aceb9298aeec946b +9b496210b907c2eb1eed6d12c1552654bbce4485417825a7eaa633e2d66db774 +9b511bc411fdf71b94a160c1e79b03fc5f622f82d9da530414216b052ca1c80f +9b60597fd0898b7e47b1e416551bf7340d8bbce4e4a5ca8e64d3b202514d5347 +9b61d9f01d5cfda0f4ad96972c77838814c613385778b8295667d981bf5b8ab1 +9b72a0bc3bdb4f8a95219ca95645823a24b13824bf9d671ba9eb794848bad1d3 +9b73fe3006c771634026be2e8f92935878b1d51a3bb6c2caf0bb22f70e4ab9b5 +9b79ecac8ddaa01f313cba7f8b3c0a42f1732e701e1b75a28ea0333692b20d34 +9b7c519295ee8c428454c627a813b32cfb71eb34b74e723a10c570f138edfa53 +9b7ff09861c931fef89745eb245846ccc0750ee4f939feaf8e510764402a7f1f +9b83017a1838daadbb085b1c393fea2e1ee6ccf4b8d66c3ef8c17069ec21fa01 +9b8303a71480dd60f4d9b64b8dbe6d810ccc7ab07e33d9b22db5905bef850a16 +9b85560f481e6cefe66210a40df8af78aa46767e7ec50a2e8103cebfba793946 +9b95a20a617046d62aa3a45c91786c74edb82c6825a3c71b07b985f4e13c7620 +9ba532490d81068b382c0f8e573024ec71a1f264e10ac8320888590fc1c1a3eb +9bae917ab83ba11f8b6102891b4dd8566598c3ac49ef4663d105074491427acd +9bb2f60df1309a6ccc599f4420e9d8f80f655238ea0fde3c054c076b538e3485 +9bb467761522cc8d5035cf93df88b28955bd7d3f91b33589596c605e4363a2e0 +9bba5ab796c916e428729c94a1efcdec132b442d0d74cf1660a6a96d4e95242e +9bc64e4b38daa31b182268a3ea9b64cc3a021dc53593111c0abfd9d088ad6abf +9bcf91e03b7dce8fff9732d699dde2d703ecf816cf2dd314424dcaa7359ccefc +9bd65d7491459391d0e946543e6cdfde011bffd08fdadd810149d17802bdf8c1 +9be440d0b4ce1ab3fcca5b8a15c648413a6c2c8562dab543bc2a7be672557690 +9be740064784156036c93c65bb737a72bafc244ec5f1e5379b94a1c8776ec9cc +9be7bf87edbbf32d10fe7c04283b751db954d8b6e88ac284bc2f122b3c674aa5 +9be7ddfbe6a25e519424b290b533e9b77c2586926672c271fd3ceaa4900326c9 +9be9bce50f32c1b97ecfa29945aea6f81f8005c9b7bca4210a2cd4e3d9dc5ea3 +9becd8be59c38c8add5774bd3dccac8c0e86e9e7b0b71cd891fcd9cd5e6c83ca +9bf00b9a6004b62bd676c3755954a6e3fac477fe988967dadec74fe65659559e +9bf1c739a16ca93980e7490e9b818092ab4c47f5f92fc29103f624184fd3e535 +9bf59fc5bbd27ec348e07e58719229bddf8ecbb1ae780cceb1c9a99ba2f16996 +9bf6929aa03c60dcf663dd2dabf69870f7be5f8f66b3725ed6ed021d427cd735 +9bf84653ed76f74fcfdc51d6301725593297ca2d27494757d52890f1ecd02925 +9bf9ca7b2b8fb88186574ec5108898a7f99e906a80c81b31b6e19c69b075e9d5 +9c00c94ca9a14e772b44eea8d0096aeac0dc3e1d7db1e4b2000e3762355f2ed7 +9c0bc9dc7e49dad20ca6d7eff9a540df56926bd6775b6b53bd9d271f1e4f8d85 +9c0caddde6cc3721310f24969960b19a96f9e117359446b1429504bab17749ab +9c1bb801d0f5cf05b4699ff7d84d991c5036af789d16e094503f8879ce58a1a7 +9c1bcfcd77bab3e5594c048c2dcbb09775cda61e831d4ed4cd31f909aefd248b +9c1f37abc657f480815ed27cff1b8e5607ee88cc3fa9ed4f769f34121a64b873 +9c1f5f5c6bac71ce97c17e9f79d49f8eecb8fe6eebf9798ab07bcd0a32f5f456 +9c2ef788b07d7b8ccb84a41dd6104a97800207a7d0003c5df7ff411c69a80402 +9c378a25c822978a0703e9290a494d1359aaa7afc94921fb9de6af1e8cd70928 +9c3a80d52ce9d58456a8b920850606e4ddeab0f31b352df4a3202f9508affd29 +9c4e08280130c68b5083aae9f5b8c60a3d16c1aeb1d0e00d5c3609c3974e877a +9c567b6f515e5cb5a54b4f1c3bb832a529884d2543c3c13da799898b3b7d078e +9c574f3051f6a73878621f2225d1f802263264e1a2ce8f67b57d36767f24e029 +9c740b196cc2f5dce487bc50b204a538b160399c30694e444b399f3ded530ecc +9c753c9b5d8d21dbfccd9d7bd095dbdf812f5de0b4979e6711ec440034812b08 +9c775e49e4ee0731e6bc35fcc2a734dbaa4dbd335f0a2c3b67d83012ce7b726e +9c7e4a044f391b06a21da2ab1176985d808f5810c88cb37daf919d6967dfef43 +9c8a5db9c0a754217524cc12a9e483d9daea2518f360e26d000eb63c12298e64 +9c95771541aed288682aa324a1bd687b1db07b11df7705ee31f9ccf21f2453fe +9c95efa694bc43492eb7a739a303e68173d302388b4a97a0703d0283ca65ae00 +9ca5251fc72982f702e7fcdec87680ccdb1f77e36e49cd05f41e9e51dbf77642 +9ca850d9a917c3237a67adf2c928dfce6fd514f68ce0b36c1400435db58e7d8d +9cb362b1bc3eaff402b59b74bb98c4846053aad3d43114c33ad018e779968aef +9cbb6451cf3d812f266b36e176f33b10d60cc521943aa6a8a29bf4680699b3b4 +9cd117f35fc6978471b9483137645f31a832f30711cd87bb937c31775ca4c057 +9cd75e4dc0989f481a5f02cf9fcf5c7646357f9ccc3fa76e0f99e376c393faa6 +9cd7c78b2c83b3996287d96eef0951a548d33f67fbc7e1aabd624b50898d5825 +9cd8ace79e3cb4c286c3ccc49543f904549ab64b2b3354ec0d6765c6132277a4 +9cd94dc4d9c8e95537afee1ef842e6b20e2ee0833931e0929cbcb4613aea1ae5 +9ce26f14c35e66f58774cd3fdd739d453a78e1a677498c9d84eb7e1b8fbef6e1 +9ced8aa942bcdc6c3ce514ce10c5f17e58ec29aec9bc36ae103869769fa3c7ee +9cf1ce8b87c4ade49c58214594c4432cb039850dfe658c4c96c1c62493c7850d +9cfb63a6749a2d1f1a25fef0bf8789fce5c4f7d1710376e4490c9f8323e61054 +9cfc3fec5867aee38cf85fc39e63c7c5ed5722728f7f6c1ff252dde6eefebdf1 +9cfdb557b5e6da25467af91f189c9b466fc9bc4c20ec32f6032b1796c2ef3917 +9cfeadbc3c160506a1db0b4029f2383b5ecb290df21b40a75b5875a1f08de717 +9d01b1d36748785039358f7f87b8fdaf84e8fdc576e5d5f9361a877677f5a42f +9d069f72a28c16b1e5860f54198186fe1a10677d39cc7ca7eeecf838d0813222 +9d0e16d9de15755557b7a173396221a25f2defbe8d131d3010392eb9be0859b8 +9d1d866e7dfa58341d98e1d271af22cb0c8198f51bae8d2b8c51b38efc30b01a +9d2b5137333f14c668998c20eb90346caa47fe59f3fa79bab437b6ec56f4ab03 +9d2b686a0f838c816a00e8390541bb35d37ded3e88b37e25643bac11be06ae7b +9d38d03501776d222d5286e0d7022e5b444a359dbb491f4c4dfbb90c8f60bf3e +9d3beb75def3464833a4b79e201085489ce91df42c2444e54d8db1c7cbcb3068 +9d4508150982e9c1ff0b1847e98292d2176e72c37903f7f6be2119d3427238a4 +9d4d01431ad08e982a4d1534dbb206f2e570e19b9e4f410f2186b8f2cddc89a0 +9d6b988065628058c6282ee14e05c0d464e8a4991d0bd2af3f8eeeb2ff259992 +9d6e13302d850082114d38576f0794cb931668ca7e2c4a66dc51ccc3e50891e9 +9d6f1fb41d24f76c3d9d18d0a3d0ac331476febf4ab4d2b806d9cfe6f4b2d36b +9d6fd9510b1af7086d072c82f1cf621b02aaa6d98fb28b97cac3883e24465b10 +9d7dad75bfe51d0e038d17fb931781867cc8662a5f325001dbef525f77bf0e8b +9d7f817fbd30d26eec069f25471b22e7a8f57e089aa09bcd5d1a18e958ad26a2 +9d875fe79eceb572f667868ba271ccd17acb809cca864ef420b90fd393f2d3ef +9d87b8f3cab4de3566cd1ec9fa2134617a8b41be9a9c5b5dbf0e45aa4467074c +9d89cf0a1f7d7b089503641f48c1427b04e732230736e3c5eea606fcae5761c6 +9d8b545ecc9914093519e29391efe3528c68179177019e4157eebbedce4588bc +9d9007efa2f9cc0b0b768d4ce837ddfea3fa5de25375a74d7880c1f608ee9357 +9d97cad6ba0ec3c5aad0df20ddd6a199a4246386c2f34b2c2ff535d489bea56f +9da2264046f0b7a4684778f6986c7ee5f26fe933619a4066160c841fad4675a6 +9dac8930cd02fdb26f4108a6fc532575f3c28224b9e9f15c19eaebeaf3d40cfe +9db11ae6a90c51331acae4716e660995dfac1c2c77d9df75a09a0bd4f6eb33d8 +9dba5c3bdf6e112db785e6cfbe8424a975e9471fa17d766e52d9675684febd3c +9dc32c5e6f1e4a2364b5683ead3d30a336b3fbb649917f49faed622466b77aab +9dcb513b0caf96efa6878030dcf3f66395142630af8cf16126c422b69372b87d +9de5b0f651dff3493d49116dd4b45cf7f85e3b8ac12b61418bc13f1ad7969ad6 +9de8dcb8294ce398711be99ab80dbddd4b2f11a14f029572b0e4354514df3e38 +9deab98efb0514fc03a4ae64d9feec5afb4f3ce5da31ea3d047f0dd048251894 +9dee01603ba9fdbd2ad736ef37c7aed3e7613fd2fa9d3c5c8d4c0cf43ae83472 +9df252950333cbf124d51510b72fdad4e4aa59a9a3e3450632803532b6eb26e4 +9df5b7f5e769acae1327adb4dccaeffe2baf37d85918a31925926ccdbc09697a +9dfa17cfb42169fe997c2e2729524b1f02428666405d688978f8f3c493a39e5c +9e0bf354827c0bfcef9079a10320f80e89c965556e0a48499d43077df4b640f8 +9e0d751d1e58924681ebfca99a03035bd00ed41696150261875f93091c57226f +9e0dd562aee58a81c9460b54fa8fb2ee9c9a1ae4ead1dd7a0ef952263c7e3e7d +9e1a4858c9f8b366179d490ba553fab867c4be7d6d837ad0846931dc56e05a36 +9e1cd2f6ddf40ef67254904d7358adadc8d791ec4bce2af78e90e9c9b9d9691c +9e1eb7a0423b297c7bdc28ceb58b70ddd88c01d63149131e62a4c11804750d3a +9e229d0cf8284d54ff7c62cf1f90b1a827cdfd25b2c1023ceec8f28bb6a72372 +9e2a6024a1a9e8a6e1a675b80fcb934cdf6700175b93198a5dd5c10673477a0f +9e2c1b170d12cf375c636009268078131743367cfc9b01028db3e7a16e3bdee3 +9e30e34f773524fedd7f856ae2bef12522dbcfd319bf31983f45d1a1a3e61011 +9e330360046b92bbc2e2e7d73f4eef8e8ee489159ec79c5ab79041611acafaa6 +9e368596caefd167d50271251f989858eae0489ab650fca4db4b488347c0b4ca +9e37fa0a01802a07875f879870ba892652c2f8f496afd10ce8282f279a4090aa +9e3b2571f5894ceef4ee0b1a706d4337f1eac9cab5c32f3273097fa81a02ab41 +9e474b65d29281728d1d5c901b3aad1f62920be812e0d48a4b5706f9a970e4b7 +9e49433ff12aafb55aa03ccb67a56bc701bccc92ed03e63ffb53a49b5dc1200f +9e5523b364150fe3059ce2594c8a2c3b2518df5c5721ebee7024534d69a46d7a +9e57b1fbc524f06d973d682a491cfa04774b48cca8eac40a6044101bf3ce7461 +9e5c197f0637ed960379bc2c289b7a04647f1540797b0143f428f7ea4e40cb81 +9e5ef356bfecc4aba521b1da87920689f16bd12d9eca6f91c583fb89a32f56ad +9e68eb7796771156acaefd42246e752be681980fb8db5bcdf3f92ee64cd01450 +9e6c706086e9016b61be5cac5e1e3d73a7b4e39da8c6c494314a66f9615739d5 +9e6e355e9b49d4d20f09fa0f13c55b83b2d3628bcb39731049b538dbb62cfd90 +9e872feecc516ad1785d472f74d96734fd651537f9bba37c7076325d95f84b62 +9e87f4218dbc8a2725aaae5f700d35e62f8ba285452224669910ce58ac0dcacf +9e911ea8a9737744d80e0b0995a632e5f538d7bda7bc23181809e37d5dc7609b +9e97a86cef9a829b947e049feec66360477621bd5148c9f5068d565eaea480c9 +9e98c94a126b99647c3e1c5ebc40a2c2d5e0458a2b4bfa4690adce18f1a1305f +9e9bf7cce4c052a736b050cc102865f2a51353cb3412add65d1ff45c45cd95be +9ea3497d0f66a39289fd7be112d0a685b2ee9676115f3eb2498720f22794135a +9ea56fa011c62ff4a63c234bd53c428056ff7f8e34d3dff3d8f1d10fea9782ad +9eac49a86a2dd3a68f75afb533f3401de53c7d73ed562d6961d0c036c4651986 +9eb4cbffdf04203f2bfbb0f8da51d39680acfa1407aca60c574c774baf0155cb +9eb66e09c9c476fec6de50e00742aebe738908f6605df9dcef55b0967f79f1bd +9eb9473e058aeb7c694d219c4f4dd2b012d6605ab6c654778f1c47ce23109bcf +9eb969cb22c79b7040004596c9ddf11fc2bfdf94942583ac2b37cc37ecb0d9e4 +9eba1920eaac83e01079681cc9e86ee16b54d360a791536d2c1c82e0b1b1f5fb +9ebdf1cbe42ed4f6196eb2bb3216c251e2830cc3ea2dc8a3f4c218cd5ea22e59 +9ebf018c89404da9ef9227752b6c1685e30f72297c9c58e1e34884ec9297c458 +9ec3d5a1ad10df074e3b9e6d648a02c77314c047e2f49a92a70b5c8cb6839757 +9ec714dfe1187d4c2e0952c239a4f44dfabd9506e17fbd6e9bb6e2da653f1e8b +9ecb6491863f486298a126f8ec61f35aa2046b5f0d212695b0fe7859b1e35662 +9ecec8bfe0a9660235372bcf925163f5c189ab91b4918ccfea03adf55fdd5be7 +9ede3acc6d2b983b1556d8d725a958c61ec5b56dc2cc91e631558ec72a521531 +9eec6d0e060428dcae68ca77404c97115d29bb1eef6f0c2dbcda82a1441be355 +9f0523f4c2d9942d58f2ea81f8a298a3319c749c6e5e0bc36e4ea90b6e5f2093 +9f0f5093675e40514d03678442656c3d150a1f0d7429e16a1f2bcc70c9c0fed4 +9f10c72271e7a4c026c873ef9cc7abc40687739734d930fbd190e01b18cb5b0a +9f1207a508deb0c08ab9df4482aa1c04cb918873933ef3e63d499052fb7a2b8b +9f247228d473a669276abcc1b2070fcf5cd3755dece32fb180ef650eed76ba2f +9f251d570ffeedee8b01f17688d0c0cbda3b16896378f5731fdc132a87a8e876 +9f260b40db319a909ac7557f874fffa9d65fb7ea4a7d657d720ac26d7f718dee +9f271b045da6faffe61924dd238a6c2157c347560be8d676abc10a7c2e566300 +9f28a87787f08017b7879aaa40d5badfa64757ad395cd8372878d62202015dd2 +9f336183e706632b3fc7d7f36cc7ba0145611c1e5614f33f684a435aed6415cd +9f3f47860f3e1a674573ea483ae4af0590dae65f9d694b3632c39ed0faeffe72 +9f4030f144e5b63743e9b633b89848a14970448b551ad8b3ccf364e552ce2213 +9f4dc37c535fac702e05d4bb01194838d2244eb59f5c096c126821f343298513 +9f51350e03b0e2673edefcab96275be43a8e7863ad0234d26affc90b18d6ed5d +9f65874da39e9ab2925fb0218cc674a50cc6ceacf48ebf9fb3e73b7c3513e537 +9f76a9bd8107de5c3f9ed43f732c7c8182a06623261fc8fc51643de6d2d78dc0 +9f8fb9010fc059b4e88b4646dc9e106dc31140468b8fb2dee877faee15f13fc6 +9fa885219c97f15ac29ed97f21255d39a12dabf56cbd1df001da53e3e4b0e0cb +9fb644ebb630d573f6860a2d1dcddfc981e3ac637e04af6913d537e9f686ca2c +9fb79586b7d6e7b115e2ca768534f53c3d183638284fef6be79fce2866ab2c2a +9fc40c88af5dcf4f40a7d85fffbfac4609a02288431eeeb970e23757a8089ecb +9fc4554cb462f3ace8abf1c67d43bfc6d541fc17e81463e8d3f0810df91e754c +9fc473fd402e3f9acee29b02f492294b382006f5d70eb1d78f2bfeb4f4779a07 +9fcb788a6ebd8de73e6570d73ab78daf5d6062b62e601bc7bb747efe6a2948e7 +9fd03f2c9504e0bd5421cb6eb0b50f55b9d635aa784a28375160b5c1b3b40c05 +9fd3595dadd18ec19bad19c45973d771d7712e209bdac36ca9735f03793a6d73 +9fd559a49517cc6e98ef88c11c0de522e6e5c8b359bb0b8b01e5a95f7fa5db35 +9fd85b5eb0414b6e13e433d77855a4bf02fa42282e99b9433a11b9befe940d7a +9fe4fbe58bc66f44e5c9a646d4f30c7834ee83f0c5cf6e740d82e6ceec28b2c1 +9fe55780e76f2c8cf075d6c7f8a52cdab28a0dc117f0693022dc34640b485cba +9fec2dc24e42aa40d49e02729a60b97049f301df1c70388796ba08c0d6747571 +9feda246a890933d4ff7fff913fa979e8b81d088972b3191e43b93ae0d4e912b +9ffe2aa2cf181d247b59c652c82dbb5b910e2dc5301cfe4b84580e117bfeccdf +a006bcbe1f55ca2635c9588f860432fd7d76ebff38bc7d7586782153f151a5c2 +a00c8b1074fe2fcb8631324d9e0370616b039e84cffe90141ceec977afc3033b +a010652084398eef7e419636061b8a8f0c86da50f20fa42b914c71c63c6d8617 +a012dcf6df1105e78293cb7588fc4a854f930649d44d132f1ac1fb8e3680bb4e +a02032e46018c95aa43c4f8eb119d916e1a4aa126b806842f150392df441c8ef +a0228c070c871d01e866e8fb689438d9da6cd5b74a6ca2556ce1781eaf9a5ec2 +a0242571670449b8f1fb5f9146d20b45ab4e45290cfeb4bf4c58cf48091dbc21 +a02c8196259f8718df4d0cbc662fa5bf359c2a42cafa062e1bb8a9300c391256 +a042b60b601b322eb10f244b8f7a863c548fe1eb73da3ccc0071051eeafca88a +a047b4d5c8c9f89c6a2bb11ae83829620cf36c7073d00904335b21f9b23eb828 +a055e918f3b77cfa9ac58351fbbb00af4fa08a0332c4cc9cad57fd68331f39ae +a05b2d4846ca580cf7fc9bd903ea363148ab02c34308c246853953009eedc571 +a06a69621df2b2fae4b529abb7a27946c1c3358e32f4e6293a38af56c0c4c6b2 +a06c95492703615ace7b8712e772f595d1fcfa9320ef6a81c269338d75c5dc62 +a08a53d697e1126694a3f490dfb07ea685d0d6045a0b21d07cc05ace0dca1f95 +a09d52421b93c09588543ecec88522fa254db7eab70d8196984564a6c92badbc +a0a156f037c192d5b2e4708a33585ef6bcf86b237e96b84d80e59c15f9f929a3 +a0abf3e221f807874e019fd69dafcc08a0ba8a81da9ae359392636bf945527c9 +a0ae4eaba59741bdad6325513fe29f521656ce2c76e31d0ad9505f07a1874b56 +a0ae6bd8e272fe4115f04b35f1f4ba2b9ad102a7392e17e7b0ebd12c6f11e1c1 +a0b24daeef6b034f5c01cb228201754ee35bd29b4be4f777beebb3b532be50f1 +a0b8e16381a58d454b44ec1079d9f499f16cb148b60d5697984fcea37befa84a +a0bb8b957da0a059f0c68f4e2099c8f2c1e71c4f6fce41bd2a88d8bafb102d36 +a0dd8afd5fb043eb27c51ce1e039dcb25464ef0e18a94f9cd66519d919645650 +a0dec67bac2e869256ed7962ba367c9ebec3d7ab7a292aa89bc953c338c3595a +a0e2997de41e7d6a57016c6c4b4638bc2053210ef62af1726d665bcbbcae0fe5 +a0e2c24b4553fe5b6e475a30cb5f71a5123615c3dd62fac4f53dc7cb040c6d22 +a0eeb74f415c015d453c324b38ee6f4d41ba11311c915157ec5a7836bb6cf4c9 +a0f91c95e8b7f2457a9ab3c732b48c48237b5def359d48dde079b5c57a218a4c +a0fd66ce23cf2d354ce8dd307c2bb84475705f818a19649d6cf844d95d321ac5 +a111602b3c2f1c92b9b353fc088ec01ac3c5fe988cb246686e76dbf847755b8b +a1180c620bea7bf21f9b803e81c18bfc488d44e29c8d027f41734480885613f0 +a12c5a55aebf377d189c1455d4a3f8d6094644ae85e18547ab619fabce3b6730 +a130550a7cf9eb8fa95cff15701e7a16ea2b06902c610b6d600e8f836ada83df +a13097c13aeaf31c1661f1dcc6fa7bc8ebe57b4b2bc6eef67b86c52da823705e +a1361cde3cc5c82459dd97e7d278a99a215bb31438d336addb3f4c030533cf28 +a143ac67620ba248dca85120c4268f1873040e74ab9528b4deed7486bd2e066d +a147891b3eeafbf54535bfbf120db3f92b1b9f9947ead1bc0d4517be926242d6 +a14bf841477e0854b7133769e5eba5f8944df4b01efd3650d0a225e69f5b95ad +a14c8b7a457699c02d8140df988c11a5f3e505289940555e863a09a19e6576d4 +a15b4f8a214028ad9c1e99372025208552bde9e5a86a126c8c164b5f504df1cd +a16a35a85f21d44da5a361415084b47b273705f71a0199fd9eff5d5c6d3a7b10 +a19770fed87e15bb183255ca12fb1ba2d67d5e9900ec5c3ab8ecbaa1ce2564ab +a1b46c8729a539efa2b329a695c97e4a8c325e1873d44e10f62a97681ebf54cb +a1bf41d7e0d8dc25486539d98f8b4a41217fd7b0de1a249d52f2093668efab3c +a1c75e90642b0ae0d8df315061073dccc7b29b9cc89118572865d234dc03be51 +a1c7c82a209ea60a54662d0c33f72b2e09ae9351c2f814e435c84de698d15903 +a1cc8c1577bebcc8e477995c67ab6dcbc48fb7a2c819919ac0dc9635455505ec +a1d23bc2ddea2a8f7a5146385ba3bc0ae62c4e1ec99e32a53b8adfa3b3ef8193 +a1d33865e7c4ba8f0364328fe602b088b03b4d914a9c96d0152a0736d2f4acb1 +a1d84bbf07a4c39ceada5e80dddd0b10841a8a09baed68409914b83305ddbdac +a1ecbe244e9dba97096392640c42183f6c15cce07d148d6c6e449e35a301d823 +a1f3ceee5c4cf303a54d23b900438455ddd2109f30b2d6bdbb712e8ce31675e9 +a1f72442c7bb978649ed34d46d9acc13731560633595fa7f92caf5e637ede52e +a1f94e83df8ec375a1c62b9ef389846489b1f3f2c22d3d32071f621ebe2a1673 +a1fdc0894cf3f30f6bb7b7939fd3cfd1d6b2ddefc368370fba84eadea62c52b4 +a1fec135551cfe990460e8a8fe9f282f6ded02b20987a69764e28f9c4933550a +a20468644dcebbdd1e7d175d34d1c0fc8b95d499f3ce27599794544c31aa3a23 +a20704be0fd2eb110554531746e5bc3a988e9d45472099bdac4635d8975d6ff7 +a20e58ddc0bd2a0ea7b6a8f991b64f509303700b3594f27a666b4c792aaab982 +a22503d864f3a04dcef580a645ee7f57c017289e7521c47314a04c55293d11b9 +a24d1781ec519ccdbd82051a6690c025b8f02ebcbf2545a09f105d6e33dccf86 +a24fea534154e96d0aff3001ff59abc0b95bf7a582a2f716e3a158aa24865ad7 +a25d02ef8c52e9b00a56cfd74eed6bf230b4541b0ddfac6a3e167aa8c4c58d88 +a261a8dcd7538514a387eb63d59f949c50044249da3b760c481f278603d28a71 +a2666b8a60a77112dc8a674b9d1d041ed8f915f5430260425ab01d7ed83ecc9d +a267417fb2fed516e019490582e04dcfe3af68f0b589c7ff0db01979d947401c +a2968ff575e6948f1c8c95ad28104301dadee206a52cb4ed3d6c14e629dfbbbc +a29847abcfa6d9e85200cb27ba5f7b2d02385f12ba22ac92bf5c30fdf37c693d +a2b0f2fcc088658fb57dcb63a267f6fccb6058f5a67a9f28e26789d3283552a6 +a2bc9094f43605a1db30e9950bb9f7e1cc6f2c35da94f691de8599ea49361fe3 +a2bf01dd13d825aea1b1f774d983f7589a3045d6ae51164a99ad201dcea9e916 +a2c02ee21be13af4f0ee49a79fdfc4e81c75c8c112be77e109a56d793540b692 +a2c2c0c62e775c6a8da6f906df3ef194165f4fa1b635fc1bfcaa92272f397360 +a2c96c0539768f926e9d3a68af52f8b782a2d5bb0c4ef2b609d856ff75584235 +a2cb870e2bde367b921c2fb54268ca6dbc886c4b882c5a31abeeb9f8c9ddf851 +a2d756fed32d8ab6a9fc0c5f98cd0a7eb86ccb01faa0c35f871fde51d8f418c3 +a2e024402bc690efe064713567410a074abc9a012a98f54cc20365f87fc47d52 +a2e5dd37e7bd697205848eea18f0c5c7d1c433a97044329d3c6e10d37cdc26a0 +a2e8bddcc45b6d7f56f7a9791a5f9b09132ecaa7288a25dc48bc4803e2d8aa84 +a2eb75a0e96f3ca0b52554edd37b20baa50c1912c820b12f427ef7b79a12795c +a2f8c50ea09e3d0820ea54290123d4097a6a4cd725255f1dc6a46cdeb4e6c097 +a2fa01eea4248e1d7cb28f145d55796f36d22041e745e608e322d99b45efc421 +a2fd7431d0822b78b6a6d662d1e043ee0b40cf6440e4f43c7e3a5757d60b46a6 +a2feb0f78a336564d2be3724866ca03227d372c106d1658ab9974daff64a0bf5 +a302824d8f6602586532d7099a18c5da97c4899f07599c80cca0c11a475e5a33 +a3030af3d3efd54a11d3e613a2d2896e446914b74bf57ccc276838ad66b216ee +a31070e3a684ad932d3cdfe8800b04800e6ae1491d677f2576904ff2536ca105 +a3127516ac2b88751aa24237baf87cc30bf30f98801308a0d852e0a2727b30bd +a3134f069a7d127fec546b06ea44ab02f4a184d13837dea1a764df1eca98c6e1 +a3203922eb4312f4969348ba8a2f64bda82dd87ff1d429ed6123d19b8f996671 +a32fa6ff793d369bca82a7e8000592e7145b15db7c8192b5b6a7131c23c60b6e +a3321bff7df67ed7a032d7e7a1c52afce5ac228213215482d76bba59cb350d35 +a33262a7f779d50b951670c166df32000074ee0b12dae39f98171229d260ea5e +a3334dd23c064c9ac8b405c28b0d2cae5931dc080955ec5f52d59102f2b62d9d +a34b218051fa18daceb62c96e5bde4f476de3f757971ac961f1d6127ea570ed3 +a34cbc49ce0e84d6c48fd0660b1f5385da4ace847ea79457be26242effb55e75 +a34d96f6fd537c18f08f7fdd6c43d3b3ee13bb48c0e9ff810fee3802dde078f4 +a353c8199b54750aafd9bcd2762749c7724e3b5b00c48ed4b0fb9d8821b8be73 +a356e46c65595946fec82713076fb32aec8c0caa88e81ccd602a09cd969b131a +a35bc469bb20afda2922a46429ffdec68675a24c30806b849beb439bb1ea7cdf +a360edcb925707a7260b88ed8f19f6c2c376f68d0ccf95a06f60f6a184fa90ea +a36d32dcd55e2f0cceed9c305c51babdffeacce0dca35778ecf3302993d937a2 +a3716c74ceb9184749aa6c1ad60a5223b5c14a497ef11fc97ac5ae1c6d79eb5a +a378e3affd20eba61f1bfba94f9170bceb4d6b0bf44fb5e41cfd798b61e7ea0e +a37f33c1894f2f98e7428c69cd562cccbbc3bb1d91c9324a31348710ee720fa0 +a3814f0a4e21129cb6566c3f4e705115a79c96be8655155573951ffb469a37e1 +a382685476d60e3daeed856f4c8269f43d33c34ffdba3699ca21c3b5903f95fd +a3864de4f53149dce8e1029fdef1c19c6f0033773bbd74459b51415c777719aa +a399bd0d87303ee9539643d048316beadb71749c9c42f7ce0b08c045b451c1f9 +a3a2352c3905bf8600825683dbf274af2b9bf793ce889adef8256b63c75a4038 +a3ac14a58135fe37c6df8ba2b8f41599864ae4771c166a23fff4296b038ef9b4 +a3adb47dd1e3884b664e2ad3e74e0d37d38d4206351dc9ba02e18ba3cc138d0e +a3b7862d68777a44f155ca769be17697c9bba25cd07e73992aadbaf2228ef28d +a3b792eca244df82a5066735310bdc2971fe6ebc4f216f03052332b4a3120a23 +a3b7d79c28322c758be0b26f4593ef09f9ea72aaee545babcbc4f5ef13fab222 +a3dde0e4f136b2cd72442ade8d6274b9461f648b44393901fe426ff6cbb6da25 +a3e771e0664f482e9017aa385ddd0e77950d33d278da2b80165bfa0d18136b4b +a3eb4d06520e2a3479d8b2f4277276c10f66a235994eb93142e72f1d7f3911d8 +a3ecc4a5bd2451e65e5a477f90f3a9a330c1a293a64427270318c88ee26d1043 +a3f0edfcd2e832abf28f46c1c2133e2a4e8d5758ed40f6495b8b7f2a14a2481a +a3f7f4e8bcdf69c2dde1cf86fc0f56033acd934a5d5cc0476a980e0c2203b1be +a3fae7f0370dcb4e7f0a47d2b1fc70175daeb583e328d918af1d1b8a894dc6a9 +a3fc26cc4247b74829f29eaca9ef8c7f2a577ef746efde4430446bf3afc1085b +a40028debe32421b1bf81bb3777e807f20e1e509c2a7d08295b2ba82a3f1c3e8 +a40d348e61ba9d08b76af34c9e075e87a5ef9f672d2bc1b28b52555b114347fc +a414e8488f32f32765a719f7a4b011fae0721d4a169032ee450d531d331f0673 +a415e714372a9a7caf979ea3833692f76d90397b93ea2496e1ab7ccad0ba25e3 +a419c95258790ddf9aa45b97109bd3d2e9a2556cc0c8e0b761c84d3f1eaefbb7 +a41dd6c8b1c5f0ae73a607ae16686920f40301bd10dc7a47541de977316ac44e +a44052c9de382ad5deeef2a17345b2ab5ec090d1679fe88b04418e5e2d8a062d +a448cee2cdd21b702c30f112b2a318e93f09d07be31987d50138f67fff0cda35 +a44937a3a99b237f45b32ae0455aff56150dec42d9cb4ce3325a39b415303803 +a44a1defe17230e75cada30d7305791a6e51ee5bd16e0ed66ac5f99a5ad0c01e +a454beb85ac97b209a9ec0b018f57c697de3cd65cbf69dee7180fe894cc06616 +a4595978d4ab4247e3aeea4f6c8f83272c3993355f8693c459dd928a6d3bcea8 +a45bca1288d5525b77b01f0804d829b42889a155cedf9420bbe291e7b8f9e549 +a45ccd612cdff8952720adfce4a12afbf3ba9048d6951080242812180ffc15df +a45f8042feb92c734226378c719acb94eb1c10e650aa54631a0e6575b2d23569 +a468a3cb6a26838634e3c0283aeb9b1c197659e284b4899b9beb896176905528 +a46cdd607f900cd9679356ea65e5f14bc0399940ecd3b7ea06131d533ae20602 +a475e00dd1a095c8c1533055bedabb9fce8dd3fe80ad9f44e6ae655c6a4605fd +a4763a745e974c12389c3bde33068661183be25806f62b88abc9becc8f080acf +a48ace20fd289f423d13d2bee3213d6dbdbeaa9e6efe50dca841846000daf1ed +a48b5e65ce31a0828c20d23bb730af8a966cff5e43e8217eb760799ea125ae4a +a498b9ea4e72299c9be3420d03cb8d656940e9637add3d82724743adee6e873d +a49c9e3f409ceddb41e41b7e03456a3995334d704c1b7b418f5ff0274e4a2e25 +a4a2bd8a702e897fdacab60504df9cdc44c2d61b2697c2e91105fa457d88e044 +a4b78dc74da652159eb72f61374f3570b101ea6adf5f9b473fe95f00a63017e6 +a4cb048346b6cf0f615a333912bf744c3ef6f9eb4f3b5575b2ae1e16d8e8cfdc +a4d2ce00dc4c94ea1b10b99340237d8d924a6ff413b1dfee44b0f74335e89976 +a4d45fdf196b1d2f0b27eed996a785dfeaba9359d9e37a1202e72353ea57eabb +a4e67955700c96da550fd917bdb44a04c588a463db01a69d30ac28d181ee0672 +a4e88ba1dbe71b57a73f589b3bf6c826bf0b7c8f1a403ad1e9fd64484aba149a +a4f079956f4dbd92671c877d90c0440a11620074980c91ab3e5c7337dd872f0c +a4fb8b6382cad907ed9fc734feeeca3f5221ab9d414e8ed4f728eee82fa2ed32 +a50073389291f03a62e0aa59727c175120c54a753b4ba276f861aedeca231281 +a500dbaf2eae318bee8882020c2e3e8dad504d179cc177ecbc782630e1664491 +a5047a4ad116104b2e718141c2f879abe6075d33a684018acb07880f1dd68225 +a5079c6b105e9182cf90bde90b38d3acdba2925c1c7eeff83d124b5a96b04ce8 +a5223f9df1caacbbc28ffcadb3ba39b0c78747c516c645a6b7dee5ecf0f4f203 +a530883a1e82c261d224c03fe6776ce4f3be56a04e8f76482adb3accf96bee35 +a535e586562ed345a5741f56bdba0fff4644b83e34c1c0662012793656ca8b27 +a53b747ccdb757f7bce9c0f12ea7530d44acb2002ea4c90d546395fc9bf884b9 +a53c38212e076b32adf9da14eb18c531f8b1ea9167a6b8c63e6b46604791900e +a5472a111e3a3f56aca3d0e8ecaf760fcd71a798e08164affd1e48cfd060ec71 +a5569201dfc9d4e52a00e947c88c1d1f086c84ec34cb0b5b0b06650d0052f946 +a55987958ed066695c302e9dce64be8b9eb7579232eb8e88e64fc452c162838d +a560e952f38637bd074af8a38a26077115fe9d0cdb93a1766840a57d2994d4e1 +a5790bdc36606bb055245713580fd91a65b669d4ccb45f01b3b4ee49f947bd04 +a583454f89c1821696ae1901d4e7c04e243fd95274c62c8547ec79674fe80148 +a584f724fb7cc4e2ef90a7f2baa197ccdfe418d7a11bee897c54ecad2d1dabcd +a58e57dc9ee19461816d9fdb729d0a9bbe038a76f9d50a8540c5aedf58ec6fba +a5945a9488fe7cbb8a3a07da70f9aa1ee49b9277301bee44c44b5efd4b067012 +a595a7ce1b1d6261fedebb015ca9e83d05c9b1be792caa6e1f3cda7f169f9714 +a5ab98b3a541bf112fde7f44f5167617f191997937052572523f596e973e0fc0 +a5aec28766e594ed0468b1803b9e8a496d909e7003f26f472d9bf25f24fd5f16 +a5b0ebead0880909230e3b06be64347c29b919dda0964a3ca32f3322b51b9013 +a5b4c807928bcca4c3f819b32439b1ed7e85e9ed7b80a7c230357f859d094c1a +a5bc351073c11ed7b429bf494375e103d97168636b5a711ca2dc40b64c4d4df2 +a5bd155590c4a39b7b484fb51bf7bb28ef25b5e520549fdc3dc61e411e458b30 +a5c17e4cc77d852f35f135a42e4958dd8c915e4ac5bb3c2c0c204099f69757c1 +a5c4a8488180cab0bc61b67b19a8600bb1a78b612ab2b9326387fae67efdba20 +a5c61f775600ad7db232b5e687279d9333b2b4074f056121420d877ed02a2ab4 +a5d8b4620d18f23d15fc081c7ef2e56b1156d6fe6a86c0f8ca48304b77505eb6 +a5e03129556e99e401f534608da8a50d89b6566b5f3aa5caa5d10ef4c2d92539 +a5e141aaa0afb0889893e76dc6b6fbeaad4156fa136df8b973751640b149bbf7 +a5ef9c9da3d7e04a55f7d25c1c04c687567be5dee75a90c744a42466e2204ba5 +a5f0e2cd4dede72d1bcc23f9419091517546434e6b747b3c2474673d2cc2ff90 +a5ff5a5e5f461587f3de7b816df2f5d92c1c32e8ad3a9223938b0efef12de6ec +a60208c131a814fbf1dcabbbd141441772ef5a81dd9a84590e4ab4254ef33520 +a6031580f7c3c70b69ef7239c658a14f573580e4c20ec0a0c5a86da720f34f84 +a6048a2246f0f633f7afa262d7c08a2b98f9ff0173d7a9a2536009e2b3ae2b5e +a60e9f1457fcad53938337537fe383a784535a316afd5077c0952e3c13a0cc17 +a612a3801adc05785638d1f80b783a5376697da9bf954bb578b141e3ec2ecdec +a6245bbbd798f5b5e87eb22e7a6036ea3ff855ce7846c44dd92a3bd4940ae037 +a624e6d3526b5e0784b69c58316158411a42a7e656bb06ea8efb0cd4e80a1a0d +a63109bdc131c09c0e00971964506cbdc42065425ecae7384191de37d421a3a4 +a631677b1956bd66de78dc773391bb190bd74851c0d73aa7bf298cfb34114307 +a63642883d59033e7556824418a5ba2d649bec7ff3e2ef75886c0b441ab50fba +a638965f6b2fd64afa90ae25d6b535e147cae164d69532c894dbd78e456fde24 +a6502b9ea7fbcb5da1073ed8e163b1643f46036a4c0ef1ee04ff690c4c799b50 +a65132987028dc70bde3907b06197613bb3a3d1c8e2bfe487275ea1b379a1846 +a659b1fad5b36f2f46bd3fd25c9da886b7fa8e7bede6af616d7d5f31ab588b73 +a65b6925486943ee872b054dd78dfb75798e33c2f85a2fa3a2fe76177cc07b75 +a66912028ba873a31061ceffb84e2abd51601266b6b47d540c714fc93d8e5c87 +a67a1ef9c038399cb558f8e765ba8d2b3ea6d8b818e4577fb0740a487949cad5 +a67b23bb95619a17037914e41da44f66d1f3b9dd7336ba066d2bb9228e022641 +a6804a6cb29f1d16af409662a31e309d69b24c4d505bfdcc6c24370598df9e69 +a6871d2bc34ea6e1e56c0d6adabe4e8ab53356cfd47b56d07456552b937dfccf +a69847bca6a71d34a858291dd49e0677db0c1d7fd0ff2da3ebd2503704173613 +a6a3b4ffef76ddd901e64de445986fd7c55e752c9d1a1f9d4367b2d65cef328a +a6a4074501a19c06c1a9418f74b7c7c902628b177d6c65a78ef29eea13f15d72 +a6a4cc4acdac66fc3d3655f98cd6f12cc6e19f1e097d8d13075d285d516bb190 +a6aaaccae60db6bddaa171031477f783f1bfa3d749e5ef1f046323841f6f4a81 +a6be8c3df5e8679ade7b264d16b19e3dcd3b473352285d36556684d90c61b564 +a6ceac5d56bc3d9b76210b86a9f8ecd0ebbeaa63881af770ae5471568f1a4afd +a6cfeaaafba2f72419d1582552f6f5652b0a894dcba09fc6ff963b73541c0352 +a6d048adabfe90f9e11de298ae836fc6bf06c7a43cec8251c7dfb0375d5c707a +a6d2c10fc81009befd4b14d32a26632e0e071664843525878adad4e34fa2f773 +a6d60dd2d0f3b9efc5be01b395459f4f09e230ce57e77e94c6872a87d36d673b +a6dcb1ea4fea9925009ebb2093d82b12867449db1a43342c9422de791e8045f7 +a6e7851c51e279f2321ab29d90a2d94d607cbc94fcf777c32e58b51c5ad5ac27 +a6f1415c28d45aa2dc4f9d72aa7da4800b99eec6a2df38029691fc4e16b13c6f +a6f243ecda83152b779fdf6ef02482025c68c755a20d3f45f45635bb47046251 +a6f6aed42e8b96f173b4702a6ff43e6d0912b4703fc9c78642d01f8bfe92a795 +a6feb2425a0e44310fce444b562a4f3eed32c50f37d94f210f2842bf467f2c25 +a700024323e2ba87d1ed295b7da469df518ccd5e15311368a9773d9a13278103 +a70af95c43fc4a8fcd58074b58b80639d5e04868a0e0b42f0e8c126a3effbca6 +a71095dbf1cc241c7959df24c8a9caace2ff42d8a5a73a7c464794bb290170db +a71730508c4f4bedb695762c1117f91c1a7ace543d2977dc89e28cae7fc7aa42 +a71cc1736568ecc0cebdf9270b748b00494c5904d267dbf92f51ea264377839f +a727f3b5b05f4aaee75db195ca179a6dcb3674b40ec68dfcf0dcbfb80a2e23eb +a72db3414386cfb839c4705d78251a8724ca2a32171515725b156558e59387b8 +a736502bf1b79e78302af307b6bdb4b26d38302ebde529d6451c63d2f58ec97e +a7366bfc736d00e52ebef886994713b619c38502851ed637c11f5c750c504c9a +a745bc907c9b4a3a02f5a52a2ba3133dc6b197544c7491266d7163e7f8c6613a +a74739ae341a7420a1e55234f309d12c469cf5b4f1bf56a8fc3fe35d49ec9cf0 +a747a4f230444c317aa29d217d27fef277e941f6bccf7340f3c0380703ee8f3e +a74b0c59d283f897c9f121bfdd80b79be736bb9c0062bd757ff9c71d1808372d +a74d5257cc9322c948d93504386c412b2aed29cc0b92fd2eeb9f764806dc0ff4 +a74d686ce274c2f13254395bcb09c644986c1fadd7ed7e1a43d84c3f552d9308 +a7571b88feb391e5e4636940e589b8a11daa5f8d61d535b37cdeeff6e73e10fd +a75913b0055b2d1e43f391610f285483c78f614d5d8349641fcbe92b3d2010c7 +a761e089ba8b69869fcf4bfe46b16ee8cad0fd901058a8248d93c199b99e3df4 +a761f878390de1fea236087377a2374808bf8262118131be8484b34555aa203b +a76e6880011ebfb312835afe0a14b7f8bb81ab6185ee62c7fe8b0698431ae857 +a771415ae51f3826d4d0bd0af8de7f7b38ffa18b9f35f529da1ede0105d0b566 +a77a3614dc3537013049b3a9ec683a254e6c38a5ff65e0f92f3094aa1b5a731b +a78a587b9b2788e34b11738e449ee239aa60d7a28b201b78c1277d30dfb4c7d6 +a78ff276078b34227e10ec133d8a571e1d61edf2e1b6e6233abedbc2b9b31fe5 +a79911a2512a2c3d3f5f85f2d022c467a4ff43cefd6cb6cb7531b0b713daa47d +a7b83891bbf331dfe7cbe5cbc9ead7ce233b64780ed2075e810457d860241189 +a7bd19ea4c36414d9a4cbc543a6d9b5d60a1c674720d99d7555121600869eece +a7c0fa9f3a0d764f31c8f5ec0aef88ebe945a8ae08c4bdcc5808b28e5025a7f0 +a7c8ab4bcf8890c93c29b6f9d337d83acc1754580db0c18155b40b59704a54fe +a7c9e12efa6283cabf6b97f5169441a17506ce57477f3ff350d5b31b52176f31 +a7d50f003862943a16207d59afffe265c82d849033269393b85869fa2650fb8e +a7dddb65900096ba36fa166e384809f997af2a630aac977b3bcd294d2396dab4 +a7f4f288d0ef37d91961c7fb5527c07a21947ede78559fb812771c9e2fe23990 +a7fd50811caa25eda3cecc61b0c63097ac646cef0a6c8ae8a574db6d7adb4a77 +a803698797efd783d058d4730030e6b08dbd5947bdd7dc52649f60ffd90d7264 +a80a13a469ffe0d8f20c4331a94ad57af391ce13c70dfb0094b3699b14b22a1b +a80ae099145bcd79bbb53d33e5581fb7bafb8fc7492c9b03afc55eb19a9a9cd5 +a816cff22ab324ed9d3c7793e8ae0c5dee7180e9094de1f35d4db1ec217b41e8 +a822a37c541d2b023131e758ae3c306bffa8eb8fb4b28cd70bbceb1106ed10b1 +a82656dd4560521bf7200c58d4a24cf5addf2d54ff1cafa2dcedffebbf234be1 +a82a3612c4e1d97adefd27ac61009914e0ed1fad1c73ea6d5d07d2ba4dbde279 +a83a90258bee19257d97bbd003e08b76258aa3931648b3392d454f9384c63ecc +a8473c2ab8337d4458499ae33be746e6d6ad5fb1897356d8808145a8f40321f5 +a847b15c8f7328011a27aef98dcf2496a5a31385d812c9b29a208a9fd6536121 +a847fb3ce38fb2ba26b053b55dfad3bff19bf4219a6021b098e8e40ea1d136be +a854fdaaf4f3b5f7ecf00e7635dcf6e330a402f03c8edeef7fa239aa682b9a46 +a8593a3ce39b864921fc2b49216a29f217e183c7fdda11681bade3fce04f0add +a85fafd53106b76fc40f9234c76ca4340b4855330d6afa41bf22ae685c5d4754 +a87be7d8c7da6c38833944139bcd9f08c692f285343cb74439a7489d7abdc9bb +a87cf80ebc83d8f3377da194b57ffa0e823e094d48785fdc24bf1fb62378c013 +a889f8cc8c5ee668061abd996f8caa78e515fd17e2072050b5bf629a15f0fa54 +a88ea904efe6077f2b07b37081a7a79a1b3ac15bdecddc74a23a9969bf7982c9 +a8946fb4dfe4cf465acd55f95757bb1264ac4c74499a1536814d4421324202ef +a8954ed45286c6709421135621334d0657ba7036930c64a9985b82eec48fe898 +a8959a94ef21707990a1b6ba32acdfbe3f8ed9cccb07d335932e7e0ab792be36 +a899d4e0cbacdcf9180979f4f0b3c1b5dac0b022a53fad775080e7d54cad2a91 +a8a1d3dfcc98099157ffda2e6c0449820dc00a20f9f6eabbe546893cda2ce2eb +a8b06554a97bb3cea3c5684e16f76d7d9104128f89119531079f52ca59636984 +a8b205d6cb8cc6f32a10f7fd776ffc06e4811014af35612d8c6296715376370f +a8b52378bf07d215690a0dcc2646c7c79116224bf17108c7a0b6af1e15e8b4e2 +a8b791a884a2ebf2eb9f9f0e9ccc67edcb50998a357d3dc602ac5b38bb4203b9 +a8b7acfb9d932c25086960825d603edc730a705b87672f82930f39027eeb310f +a8bfb3b697ba8b56e86543652638b8b74d83cf3138bcf9525f61dc5f6d91743b +a8c478fa541f6cb69c623bfb232de98fc23ee36139be4f9b6457e46c4bd6aed9 +a8d05cc17d3b0c5cbb3ef1cb2cba184dd37235c3e6669cde35fff23b373ff4fd +a8daa6ba603d5dd1a2c2222f8289697d0211ce4bc7e0915e3af45c93200b9504 +a8fe7b5aa73bca6fd7c5f17dea5c097298de3d0214886a9dfbae9d87dab6d534 +a90e565779ef0f3dca859e500eb9a8389421ea256a595585d0c87952e80ec91a +a914b80feb46dd8ac4c66611dfc2ec549965ae58b06251b56c70d5fd0a1af83d +a915ccd9bb38294de96aa72d483b677f634245c7ce295c80bc906688a0ffd8af +a91c819466da7b3ea8264340d9aa6bd5c4a5d298330152e385f91321a4959017 +a9319eb1baab749683bfbb9d5f8e16231fabc0e680d3d03a34c6dbc17218c9e1 +a9328fa26055be5dec49a28fb9f4418d37aab9ad3e1b2a9647318da6caf4480b +a9370fc545c16d859322bc7cf75f97408b91c4a4beddefb57895335898bb4ca1 +a937902311065cf0fc9770be0b46eea8d07ce9eed2472eedc9e59c11ecacc68c +a93f50c52fe593c1e7e07b71b0049aef82257d5f956fae0e563767002adabbe1 +a941606468e1b1ad03445cf4d5a5ecc223a629fe688022cf29b6e117ff8a035b +a942beae11eba4bf1adf17c9ea5df84a259ac98aa810c29142cc2978696473da +a942d229859e80b6a6f3d638f22802990e4597ecfcd1c5f7bdc50205ae83adbe +a94681bc03a7cb56185ccd490ec52ee3a3a1a9606815354d6ec64102b992c960 +a947d7696c1f3220e14db1dd6fb9ec8f196784a21a4434e5d07dc65b8abc29d2 +a94812163fdc2ffd3430d00340a91b8cb88de428c40bcc98b2b874a770cfb238 +a95d05665fc0de8f98c6e1c574e4fa8997a8958b78ae12e33fb1ff9f7f431c29 +a95da1d8d41e2e12bbf0fb4b7e4f1e0175c339367bc896420b46fe42ba1c9e54 +a964d8aa98e5fe91f8758382fe315fe86abeb6a82df9b1a2c47b6d63b33fee33 +a9664e44c077ae19d89f25250d970a4ed5491aca68ea44464f49e86e16ed92a2 +a968b23c78eaa52b10d70c6cba82ccce8966451c042134d80c57742d24b6a194 +a970988697f12e544fae4dd1d3aa183cab2619bf07744e716a6cb2ec7362bb4c +a98ced21d5c2b56d5512e577eef109c410ed6071aa10610349864f308ca29bb8 +a98efa5f4b8ece3dd49b321055e5b995af5c884df98ff6ea71cf25574ea4c1c3 +a99233d570011ebd1e8f769bb5455e34e2192908c3bb22616f3520e61fafd07a +a993556f47044cd77689d7ec00a4b4485ef52601929e34d3849b4c4318d4fc87 +a9ab4424b2e8279133de0ccdaf350cd280e16727528065c12578fe7e3d5f79de +a9abfffda7a5b262927712d2b69d0dad527384d358419f182eca71022cc201e8 +a9ac17b58521a6482792303fa44c919868ec6158cc0db91d4369a089ae0b8f07 +a9aefc6375880f31d5ea77071f9451d4fb50d30e96e9186d5796b0ae526f00e7 +a9b28c6a2e5c2efbc09d2656fd6729a9c007a1fcd293dd89a5678116a834cd35 +a9bd077d0701199cd102ad7c2a357abf7436352d663f5aa87139cdc95e8d4633 +a9cfefd3f653828603a24d5e7dfc741edfedcddf25b805b878330772de7a0f25 +a9dac0c6c8836e52ed6a6324d8be68ea7138f6071e20d3ef771cf55e99979bd9 +a9dd8731652eadec135651c3b882b4e4acc374eebf6ddca6b1ae99d05bc4a0a8 +a9ef163ddfaff097a24c4de33f050cd3f8875970e4ad5efecfeb8a1d48bf462d +a9f1b914606ef51464778f8b72d83ef85795af520d4fc70e87b46de4cdb96c5c +a9f215a07f322d2ec13fe5016b5b657390698f2a629f325b40dfa2df1072bb1e +a9f666dd6b1ea62ca0eff32d77f45c365f4e20d67f5002a70a379165752feaae +aa0d91671840c2c44c1103e4ffdaad2c45635bb796cae3417fc385bc23fcb3db +aa1eab199c68e0169541884c3e609a2cccc53c02eec0184d630e78eaafe1610e +aa34619c52f6feff55ebc6b4a430796e712de5213da609b0b077309b5745340f +aa3741994e3a75856a52a847b72b333caacc6c8ebd172d2534349e95afcf3d52 +aa38e8274f00174ee405a4c66ab4d4ed575856b1cbda8f131aa4b002c7da7f19 +aa3d73cb4510ea57f628d902f90a6c4d33640f7d628f02317f883d9bd8d1fdf6 +aa4315dcacd9d93eecf692dbd30875ff8880e2ef308c82933301c909cfb017a7 +aa46a02dc7574de38d0d5864dc1917e5b5d22916e166528dc7e75f8c6395c73e +aa4a40e1bc790238ac2733c5c194070c20f7ab3cb8e53c3136fb5aad32dc0bff +aa559de5583bcf69c6af34a4ed4dfd90046369b28539b74ac79a1efbd750135f +aa5ca0ebbe5c347fbeec549c3a729a3f5ec3bc672ab66622922b924420ef94af +aa682721ff13986e9a4e2d91fe0d8262d47cbbc3214c39670424010307a35a25 +aa7089049bf19f4abeefc1147abb90d686ae097543f0cb62fd4f07ed75a13430 +aa986b90f76b6d0ad8214e05b96af205d942e1c56fa058b5689cdba61c99ffb5 +aa9a6e82e610b5db5283a1af943d869b2eaf4f9ae458b579f2a4a65bab03d0c7 +aaa70051bded33c42c252907b8c36185ed0253c4e458756df02ac40ffc64e92c +aab81867c40d116109678db664ef41c99d68c30bcf96a4b84836a94879649e26 +aabb1df788e3e573907e45c36632f0c5c96714a7a03e5723fe9bc3ef3c848ad6 +aabeedbaff731a005d01152367d78d30525bfea50e682440b2ee14eaac4f9989 +aabfadef5444886ade7b33853c72ff2383593f5f204f0b63c7258ec908c31a02 +aac79b24271d761b92087cfeef0d14ac91d3dfaede0345069f2dd8631b2b5115 +aacee4170bb07985bf29645ca8406d3f04eab50a1b692d306378300ad87c6e8e +aadfea4a8fb0d9834b8e83af6eedeb13f7105e2758637269144e1992445cb0cb +aae0617d9470fd6e4531fbe7d2a02978ee18491f1eaefa8b2beb7a2f1a74c9e1 +aaec645667a113473f0546fea088abeae1ff7ed548a37caf1ac916fe0ee6c5a9 +aaeddd76248c12bb6ae73d1989e86cecc82aef1e39f16b7d9f0c618d3b8a42fd +aaeebbe62c656d0619a752c2a7f3c26c9e189bd9dfec871e98dbb7603e67526c +aaef03a96e6e65d5f39bdc09befc5f0bd45eecc397c16b0641f422d0b79eeef8 +aaf7583741735fb6006ba1e40a1d9f0d1d07fa8d1b04e49c97bb85634aa3e66e +aafb322e99a987fb44ea7d5a11acb4f9effc282b9bff2b2c03ec522d9597cf9f +aafd26e4420a309a40d1993b75dc8721783737f3d36724b09a298b8ee611dd7c +ab001952f1b0e9175d97bb1daf9ea0e0a6af231595bc592daa4f50044b38b7d1 +ab101fcaa36f685f6e46e84e87d31989e6e272a83d3149820660412fc9d6d52b +ab1ca6ec35c99a1981756f677d2a785077cab97e43cbc53f2de8acdaf99b96dd +ab39bb28cee3cb9ff9445fcb074d114d7ab4e21b6dcea069884b6af92de03b73 +ab3c142a084ca6f0620ac8bf80c7265fd1eb6d333876100364498414cf829623 +ab435f4eb2e50b13e4c0fa832aafaaf0c92c890fe9d0ab35074ec2c5188adf3c +ab4743ef30f09012b77810ad9ef4dabbcf812ee53776a83a6c6943b3eb76341c +ab483c0d4a126fceacee37299322fbb3ed265165c7ebfcd8f36edff75f0875b0 +ab538c54a0b94543edc4cfd40fb195254be70450649e0e6d413efa3a00fc3f0d +ab5df6c61747b98eadb2ccb80408b51fd194499538a8b2b92d7f53f8757ba477 +ab683c92b8aa2866522331c2f40099c4de78e486c8bdff536cde958a6a1c2871 +ab6df1b70c04819b51129aba0f16caa1d419ff34e9afceb78c8174a31cb3841c +ab6f0a028cac3844874c3dea3b46e3b474cbe8dfa02d843ae1d4c8ea82365a32 +ab8e4a5171a5291b0c8a5119efdd6e1033283542fa63a2407128b15a94cc6334 +ab95acd1332a4e2a4e1e697f9ff93a01410a1a633a4f864d9fb745ef1a3e3fdc +ab95e048a6637b0715ec9207d9625245f13cc76683d9c2db42d09c6f7cd5e548 +ab997ee043c7182e62677f1336462ce1934565953df2fc0d4f2d2a0756aaf07c +ab9bf9156219600f399eb989d429d5e0971e0a4a8bdde33fef4bc352beee6f79 +ab9e52a1b973c4d7ebc124f986ce8cabfae1c21508c895ab792cbc5141d07260 +ab9e52b08575dcde1efcaa7295e219b8f45bbdf9a0aa171b19b322da4f42990b +aba1bbea59675b7c42da774efff9306a765da80e11e0ff30fd7aab5134877f9f +aba5250d79d678c289f10495293f48820ae2cfbd06a3909e4ab40e9b26f94553 +aba67f3e2cfb3767a821991f36ab95bdca31edea97585870337ae37e84c02525 +aba90909ef60b9a09d8965e727fde1714a7f299d4252261beaf3a8b4a1df3cff +abb2d79f1ab6af2eb107593a6888f4727898d09fcbeab1e86c3ff254397d2cab +abb872cb69e9fcee5bd37eb034da01eb4236cfaa9fe89970ec03c235002e4241 +abbb390c99b027b5d9ba23860d8a8f6ac9ae725167410ef5c6baeadf8e9b6327 +abc23bd8ff2852c4bbe6edf64ad3fb6da6465b7153396e0ce9bd15c6566c747e +abd732ca9062d1d650b3f1ae49be5e0da9de608f514f4dcc8e64f7e5b9515bb7 +abe73c77c8c610ae48610df3717408c08b24a70594d69c4e940ba4316e243b05 +abe7c5ea9546b1f4e5daa2e074101237e7e830089115e2270756cafa6d6e4d96 +abf7f0f225be4582de2f4cfb3ef206098edbb5b1a130283160c70381d6e7637a +abfd7c4d83a72db00ef4b3a9c827fb5485917039b2b70cb826f41afe1b1e9d01 +ac0cc147f28cd7f7d0fffc005040908cddcdc6bd572ec2c0ef4c91d783bbb73f +ac0ecf1600dde95f337092156e12c64c9e50c903dad4f2f5bcba22b1f7e0fb42 +ac103c53b2ada73c865dd35654833f70720578c9a5ea84aa7a60100333847eac +ac12c67ec4a8c519e118483ca1d00b7317efef70dd79541fa945d6bd1d3e8d19 +ac1ae65c2d0b2fcfe45ee139f11e1299e53b77ab92f4ea7912d914241d6e4869 +ac1ef44b0b266fdd93bbf0250cd667f2572f2ad8da35af83389bb8a2e6e9f92e +ac22eada172880696b42f0eef008ce5d190e622ff022db4e8637d5fdd29696f4 +ac3aaba56f22ef62c63d0ede3c67d0f8379d7e2b94397875f726bdff3b69a616 +ac4c257ca87a2cf48c6d18e00e98a7c689bdfb9ba5292cb375a7e032e73c1281 +ac6679f5db92343cb449ab0818aa26468c5fe4ede6ffe9a5770a78b2ab8fec08 +ac6c5f1d88762a3f60a80453fd547b7fe32311398e603c141cb8409c1366066b +ac6df7380bc06cccd428abf2958bee8c9392c34ecba6600ebed6c6b677d88292 +ac75bffe93202b42db54f794aacf3c180366bfbc7d16c7a85ed5d1ccdefa5000 +ac778f5858aeff52d23dfe15d691e679979b499bb724240305751eee236fca6f +ac7bb367f6b13932fd78f38d9da1e2cda482a4a367ec4897ebf31846ddcd141c +ac8d26caa953f17d5deb081f28e63d3802a7c3a2aab51415f248eae00e64a6c7 +ac8ece7f8d76aa6f2ae5c65a4cad34ceb3cc361a705f7f8a47c9e57920e8c3d6 +ac9241b351158671fdd4b7e6aeef604c07b8d381f96f308ebea51bffc2c94fae +aca2a93298dd11b7ca96954b916b5da0da9d76a567ef1caea93b350626f09f10 +aca462b7544a24ca694a7f31f7dd5b6ee8886025258aa076342809d002852cd6 +acb2bdfe98f1e71332a6f65019fdf13c3c0b259d3ea6cf53fd69001a2ce765c4 +acc2d6c6e279e4faa6edb9ce7cfab9df406c6b02076810c17ac2f072faed08de +acc965e51faee0ed40592901cfc7cbfb20eead108a5661e949ee1a4383e1265e +acd1e29c89247ce271da4e19d534c8c7ea9b81cc321aa3cd6cb3291896976673 +acd7910fac925d12d6571aea4b093834f5f183d5dc3294f5807b3442c5d40e1a +acdb77fb004844727818e5207927e0330cc1f86dc9a5b0ca28150734e2c89a32 +ace1ee428018bb8c3ec5f6c651d5f803f411694c39409690e05aac4dab83444c +ace2731785dc592088eca07c3acefe5799ea453f2de33fe9a51cf314e123d386 +ace3ca9a4b181561faebf1b56073b9abba7fa68fd5d3443089e7b68ce12bafb2 +acf2ec416474b9c441eb5b31912d5369b3ccc7aafbc919495e97cc37a2601d95 +acf7944ad58aacb66a9315b1112894d6b6fbad005198c03ac44b55e727eb3c7f +acf94cdb48e7602af9a10b8d3055dec97da5b1fd1f3577ad61aceca15096db1b +ad00515327d09a6e56bff69c5221b2cdf2a8ada5de35cd86e8f535d9ca885b7f +ad0569b5464c6e28d1588c9b9c52ce039219e9be6564c46db82d5775a8bdc4c2 +ad1863de45cc622d9fa54ee3189313c5c14a53cbf13bc18f5bfd35344bc57471 +ad1e5845c0c7736fa0ad824349ba7ee156966c7ef0d0f42558a45157873f14d1 +ad1e8a111379c159209360bfc252ee24adcf8a794e2b687526bf491f6adea98b +ad424eb0694213290d24cabb620283a2ae854b75caced6436a866f3bb02bdee2 +ad4b06c69277a4279d91e209a2c0a5005843bb4e421366588a9fcdf6993d93cf +ad4f61bc139c3bd508fdedc3e3618bacd448258570393076166ccdad0bed8520 +ad673225d87ba1d3d0c7e81c3d6e72050cab6207ed1ce47464f7b56652fcc48d +ad6bdd9f17071cf9239939622e7e040dd90f3f2643847536fbb40b5d0d7e05d4 +ad7de7a5af27043029c53e6aec8a10a8416f6d26835c8718409eadfacaff6956 +ad801716be1116388db09cb07ee619ee1647db9e8b64e671f2e81497893cd198 +ad84379b37cafe250663a87a2c9c4de3853ba3a7187d8c5edc4161ea81d5b7c7 +ad92f469b9026caaefa6f9e493da64f4ecd7f8157ad2f117422f278816640b9c +ada17e734217374c128a85e46e09c06a5febf8da5fbef73565279b08cbce8510 +ada189becf583ab50fde37b564876aca67f13748e1427c0c2405754d108b070d +ada73edc7f522aaa2881eb19d7239cb57420e646262742cc897b4f8cb35c6336 +adb02281f4b91569a6aa0ea8990159648176048690842c69434b8ccfac107b7d +adb44fd81dc3bc405b8265c9791f50ae021f877da0a0ef26eefd31c035d986a4 +adb7f008b2cbe873a6cf056acb528ecebdb4eebd2ca1498443423c05bfe84f68 +add338d9d7f2a813f57de5b5d961cce1f08c5122a438d30edf90bd78f6ea5667 +ade31909049defa387d626a89edeea66f0cfdb82f5881caaa2a5ffad7cf56f81 +ade445206f3aa4d454672cadef19274506f56aff5bc5675b688bd0f344254e40 +ade53102c94a737bb5f37d8147cae9e89407a4a19e26f6184c7dbd593bc7373d +adedb6df641f9cfc6c52972aeb9147e4e8648c971a41ccd33cb3ef34a3277235 +adefb34a0aab124345ede9e8e4bd9b4bcac91b86ded810a6fdd67297c3db3be3 +ae0c48548e25e45ac07f9809d5f9d89cd0aa9c389379616043fb69f4320943d1 +ae11a5803e4866a1d8d12fe263d0d045f1b064b3150eae3594f8da5db780fb93 +ae1c1bd53f10813b22edfa5624422ecb519b15ff20c3cb64cbac02f20a79062b +ae205ef21807c6a1c5fb196dad3c7a81c0733f31598bbcfd6c2c965f5a42f48f +ae322fe184b852b2c8ccca9a55d4ea87190ecb6a572608d1066d94577bed5f1b +ae43785b3d68b7eae5ca45ef52e266d50e9aa4a08b08863bffd8841b29913eb0 +ae46914829ba12822886e04bf5716bdb602c5c6e8cc19a58cd6f3bb9ea510110 +ae5fe7d4332e98ff958c3f3e257bc1c9e345587e6ca6993d11cb12d6a2626d1f +ae689cf5011223a7ac12a88a2c46908c765639607e2d3341f3fca4fd2f502ea3 +ae6ae79196b2d68bd64a36be497837c2bbb8140fb68b5a386a706bfe146e8bfb +ae6d2347c4c22b4948d05c9b90c400640fc9a3b743496a73bf7ac30112f65bb3 +ae735d54896714d4d9101fa5597dabdab96fc7822eb5869d1fc9925a40c01946 +ae73b76d8239aa24bce6b801a90943597d10e4bb4b5c792264b91c395426f2e5 +ae784383310a9c73f55a49ec47738ede7d37fca800b473a8ae84fff3179c7dd8 +ae86a68719fde548e9187588e816c12c92feaf18fc2d8afc7f369271752e8177 +ae873040bce5f05563fab379f0198a93ed21566825b1984d380fbb745cc036a6 +ae8d8537624c2604e2dc4b26736c03f76f1c4e7655d489b694aaed3e0cfaf66b +ae8da980be4b7177bc9e7fda069d2023f016d7fdde3d301bcd1080dff1c7b054 +ae97c6bc4c3d5c72ec42ed7e9259ff1d629d3c7abef964a03b9539c760c3e5ce +aea5f4ae4ecf71ab6f6b72dcd68093c393cc2834b2f32e58be5dd5741269fee9 +aea771f88a170a422d136d34646aeea69be0f37244baf83969c1a676ac099970 +aeaa49ad685c7a55f08f5845ca9a6a7deb1855b3220eaf911a968e045dd05de5 +aecef1476b2c139b6635850f69fdbbb231e8cd1bb27476f66fe1dbba28a94d1a +aed4572e8210625479677e535ee71d50bf0cc745973f1eaecaee1d7aef1a418e +aed8f7040441086bb22b4972d590a7ff3726d666bc84992831f1a81a3f15d487 +aee3a86d4b7da89d86c0ffa8b5251d33200f0fd3899d5d826ecda5f60782e505 +aee8e80518abe5b191f2e5d13b89f28bfe58313609593a6585f78526a16122a2 +aeee3dafa6485f3337ef97ed82a3d77a65e70de35fdfd9ddfcad1ed3c9368c71 +aef747f21a9efe7dec0d6f11f0a19adafbb6ba37217b8d500fea34b1b15e9d67 +af06fa195df777dd27018f7831082911b2ffefc3a922b6d5d8ca2106d48c902c +af06fb0a4b5221b7eec2e19bdac65e8ff8fedfcd2acac7d0f1d7ebe4a45e3f89 +af0aee9b653a8c99bbd5841f7ef1d5bf85760649712211046af22a5c1e0085c9 +af0bc86d9918ca40265637ec04c7faf74ab3215929218b7816e12f82ded651e0 +af0de3088c45e5450df1ab2abae32e076f1bf836aada1619fba2d13b53cd6a9f +af15472e360e7c1e0affb3b02b05b29001126b3acd3819b9f1a5df65caaffc2f +af1d4e1ef16f7426d995f5dc76b55fdd0279f8434372b55581ad386d0cc818cc +af1d9e70dee3e24f8efd7fd64f60ced708028fbb4d75823a4409426de30b8199 +af204cd0bd29cc84d8097f82cbb87ac179a964feba4db8263fcbfce9b4f0bbc2 +af2e9335d97c58b4442a790315eb4288d1344a2530ea423edb4822fa9016835f +af3051d001bcf98cd2d024bff2b0f0e2b8dd30801e8bcd463239fdc8062eaa2e +af32c040f82e006832adddb0a9ef391cbddf30066fac80d7fcb0f6dfaae54352 +af34209956c3935ea1303ffae06ab62c40ab3f8ee59da649dcefa9151ba96c7b +af3840dac84b78aa749278e1f3f13e8947d8632f1809afadf17a1a45f5e7675f +af44609c18738cb922f5e533ded315fb0193c32564e4fc4aada48b1d0d88762a +af4d4c792bdeb8307f29b58e85cc80cfa7ef40ca8ff768b9cb5404a533941759 +af5256b323b8f77de1e72e496c05e1cecb828c808c2ae4b291eb8e0c6465052e +af5488d8dda5064bd84f4c16381eb500c3275bec1db4ff2352ad12d42c0152f7 +af715af24202e70e0613eb2cdc7ff5bb948d68375f2a874dbacafa32b6de78a8 +af740e7f04178851c0eb11b0abc0cab00438a2f620d08d9291cccade1b968cd5 +af7d18d517f929e7281e25da961cf9fda2321bbedc55c5fc1754e2bdfa383636 +af8006064cb570ac0c4ee45875c03d15baabba7733b9841d30012ee58d53a0e9 +af8356e1831025ff2b805a135b7684cf66a6c4f8b7d3e16228f7e162cf94e6d3 +afa565f464fdefc3a4bc6f81c17a7b664ac77c8f0ca49db65ec59b8623fc7e43 +afb0629d91641a2984e8cac1272ed11b1769fc475d66c3315d6ae586469d3ae5 +afcad3a96733dd9bece6f64f4d7fcab2c1d96790e2dae5031abbc870d240e28a +afdb7ba982c9430986542ddda1c61fc61e993c24cf770315de9cf1965285632c +aff0fbbbfc79d947417f293d2b8841e5706edbb7475a8e22c89f723a6f639d1b +aff2e712a93b1397063a6e883397293a2e42c7b870eced007cc071f34608b02f +aff549a4530dbc46a1c042452d5dc16cadfc5fa7a5b113f4ff3117d7bbb45102 +b0036e385ada5464b0f1b8ab577397ce9cb6df5dcfb971f3219c16657435d485 +b01469062737d762abb0044015307bf0ed87380c92e3eb94b0d323c715c7743d +b017b6b1fc158b35a6486d6e5492704cc02eb926dd41f26f6878f5a734048fbf +b01a3d828d98476b925fea26fb7cc06f7fe52dba081c5778fc99d0487b0ffaa7 +b01a4e51f7a8f96110535c5473f8d5c5b362c55e3750655d95f9560172837668 +b0201f76e8c968c7244fc34f29dd7e61a8e42c2396202853a9f36fe29ee6e880 +b0218fd1ecbc321d55b7527b3fa41ffa6862259964fa1ff4fa464501512ba24a +b027d1750fcc960f91f2cbca4aa9030dc3bf798e8cd18b857cc891811465fda3 +b0384c7d206c35f4716422aa2f900a056a05ef6454556e21c0156c7b98ee9df9 +b03bc8c9214de06d2961aed65d86346684abbf1c9bedb040b67609817a4d90b6 +b0489c657c72bd82607ca4d0e605f44f53ce1212d41d0d78f7ead210239aeb04 +b052c207e46638fd8873a3eb0f159b546db25577adbf424c8fae601a31039f51 +b057538f8e5730f6a3671d167614aaf60dc22541ca500f7eddbf3c2f7d58c6a7 +b05d0b0e2a8cf123b94cc00ec51861b0e47f3603021551566395b11dfbe8d708 +b061e6e77ec3a68b7df0c34b2fc1a12eaf05070352069b26288cf84a10e5b2bb +b067f9c1c8ff5b21953a6d1a43bd7c9acd30d095a4fe7f89ce25eaaed8404727 +b068dbcd053a30277e85e4d8ec60d15d497f3bcc8d4a5e3b7e080391bbf8688d +b06cb8a276a8fc30d6a4f5cd8548647966af30882b5bce59c89e5e9b82b6b360 +b06dd2c09e3e477f3e1a3067a242f09b7cf5a87c151d62469cddbd2b9adc1edd +b073a45be2c612cc2c6d00f5008031b858d69cb2f1c3c949a2a77a3c46fa1c09 +b07ac6b37df7e997d90d5bd72dff0bbc03363c044e7ce2c179e12c2683edcc65 +b07d0b7c75dc7fea118b227d2de1a9e99d4a0a6ccc989ef2dcdd238a15532f8b +b07d915295b1bfbf590a268b1414838ecfc44163460cd1e1382c07338cd7c85e +b08000b1dd2f21a0dff343fa74e392b9e781582085789007ab966a080abcee9c +b080d2c5b87e55f47e4fa795caab8bb73cb4d24b827c2ceb69589a531a48eb9c +b0819bb8b729e91a3faac1d9ad7e89b3b6f6a24bfe6b51fff9ee5e4ff3a342f8 +b095d5f6716c8f760dbb4c9c522a45db99e51afbdcebb19850eb00b058211248 +b0a3805506ace0335c0b47e6ea9013e6f8d5f487436acae01046469c4a0174d6 +b0b44723bbafe90c20a1b59adf33bd7224143fec063f0bb7df2945644db2d09b +b0c0945c7593034840d0337df74105e77062085b1f400c14c47b3cf030269027 +b0c20ab98167349c6e26c5f613393c05bc547a3869fd4fd0cc3e7f819593ec84 +b0cb3051459c2c3911bb3ec97756d7b56c22eb499c3e0933645988da10cfcb35 +b0d3772d907ffcf03100aed2d59620fddc02027609f4aba76e760f3480441af1 +b0d5a981181e9a354b3fd2fcdc4dc39d4de9996a386c63430c38fa7d1c58081e +b0e0288c2d519ef59eb16d2c910f90ad6f8d61bd9b3ccdf37f6a380f945005f6 +b0f59923380b0b23e0cfbc45da7aff49520de054a21e8fd356a1bcf907f0641c +b0fa7783efb69f757cd628e7187d224c7363b80064ddcfb3b371585ca3feb5e9 +b0fb0552b8e842073c4b97dffa61405c883a3e925cb43a41d7c379b2e86a5013 +b104775f677748ae310813608060406ee4111e80f1d1f157374c98bfe911eb36 +b1078795ef31f6ed26b7728f546ee36e048808defc16e1fd0ebda7c5634a8a40 +b10b81937dcbd3484e6f610b13775dc513e5fced64c95c8c4140971c3ad6f53e +b121cf78f9a957294d707b624ffde496d54e5deba9ee71810a10ea46bb4c3a77 +b13bb67609242c0ea87a5b581acca6c9ccdfceb52a68ee61eb9e79f0a2b4a0a0 +b1478911561708384180a91b58779a5d7ffbb20729f78ebb48090ca72621dc5d +b14c91683551741c5896d0d9e69734d2907e85c85fa94a5ebdb98cefa904ccf3 +b16213c86d10994c6d78c4ba2d53624ad36f1586a3dfa4016dc8941573dd7e28 +b164fe917c871534c3abcbe40242ad4d497eb6022be55bd91b51777f145d87b3 +b1705a53d8de97613a40e7ea45caf7f86c4578e781fae6409121e782a6bdd5eb +b1773e6310fe885409df468529d53c31cc539a8eb287f65261838af8b37f1358 +b189ce6f11664e2262c2fc2bbbe9fb60a6ce0790cef5d5836567feac0cff8256 +b1904adcdbb8fd454c8f8a94c2ce94dacabd61190f860c0b057ca37e9569d7de +b195dff26294f938033c26661c8c915e902fe2258be69a2aa03f9c3471daff42 +b196fd86ad22f7ca79e0f93dbd4ccc0a9f54493e07eca161337ec0bead96a65b +b19d9a6de4605686e8afe8efa9c5eae96087346ece98cbc8ffd7529a3a089ce8 +b1a012205d327ecafc2b2c941a3bbfac03bf2c66c2e658f9b7bff279f943b29a +b1a4212eaaf4fd2d0df25fa2cdd6a1a1980122d0ad3e2367ccf353d811dc34c6 +b1a9452e8be897d3ddbf626c33b0f2ca214c4326e5df5fc3d13a8cf114008ec3 +b1ab12c5d8c9524239a7728271402bc19d4a468238c851885751cae6cdaaa070 +b1b2300ee359a8ca271cb89397b5432272b18ed1f10794a094c66d368efb0a31 +b1b25178b1b027191e2951ffb8981f1cef31158491c6ed35a6d71583a474a8ca +b1b43bbcb629aaa02c1f12eb6a6f8f3d82457e2a9586df229e0c452d6cd22fef +b1bd77dbda0824fd7b8379f2e48adff0547f8608de4d0076b9d6dc08a5d1c93b +b1bf759bd303cddaa9e7d75faaf55c9dcaa70ac5d361837ef53daa242543051c +b1c2bcf1ab8c19c9ecb545447e5b8fb179a563e2f42572cbb9935978d6287e17 +b1c2eb120fb16704ad2cb1084b0d4371fd390a0fae5c2cb8dd362f3f68617e49 +b1c2f382d30800a8bf7dbed8616c44e4f46578e9cfca2c57d56fb6a89e68b897 +b1c4997d7e9025a404dec32147e73f516a36198b929b45178ea970c20e188e71 +b1cd22eba5f453f31f676f212f1c2268c57257ac41ef9d57e042faa1b4c382fd +b1e1dc6af906c29088aa7c91232ab71698310e5d83b7863ceb802acab934d5c3 +b1ec39189d90804a0e8b4a16b871b3dc09ceb2448702507116754086993464e8 +b1ee0fe29e4c49416464f7c956c16403a2340bfa51748c7e045697c7edb40551 +b1f60f58ba7bf0a82b9fb3adcd7817cea3b0d8fac2f35c76ff85ec93feb6a59d +b1f65a834f2e165a6791183a2ca05d018307560021e7c3a95d8386c06f0746cb +b1fc7f4a2a9de552d097645a30c4ae207eaa8c9ad7137f4fee59abebcdbf5acd +b1fe38663e6c7fce073fb329245f6115f58e9efc0e82942d191a9b20241eee04 +b20ba293fa4c9db89a48ee1a9c5d4be3f82bcd46688ca17b5a95a198d2e5830e +b20f53bb7317f104ef7e1b40ea65e824569f4551e63566f5ce6083a0c3b8f04c +b210691172b1deaaa071f812ff2460592ea3f9db2f4ed4c2a28aab312807360d +b2115f297cfadcd3a5a80503a8acc9c7d2b73e6400ab49e3a0ef77096765c5cf +b216e1139b38349d4f0d47dacceddbb9d471d50a36fcdde5952d7227104ab707 +b21cc78c4aef212bddd794134fedaa0e21fd6e7d4cab02f3063fd0e1c0931a2a +b228cdcae208506c45e0006ee658cc58a9b402894b38f25e042f6b1e887a12b6 +b22952def51efc7293084b09f0f25a42c7d04980e52ffd8f70ae2f11abc98b26 +b22fa00c290e98492a80b6994afd1daf3b8564871c2c1e6469d1bedbc6717178 +b230dd1d22a729065ee49bf301d82c50b103b91979e008f1af52835bbdda7ece +b244335ee6b3dda78c6cd670988a8967b8a6bb29b41e5543c353ca06d4871904 +b24ee3e04e313c2803399336f5fad820887cdf15b7f99713444fc00577726745 +b2535c2b94ab2540fa935777cb61092a26259e12b76ca1069d9d3e2b0121e4ce +b255c21d1bd8a3f264c84dbc91aa8be24cde5d8879069bc7d076209c7aa78f9c +b26eb776f00e7674924e8d58dc852f07c45898bc732b6a67b9b63c87c96e95a5 +b273d412b4d53c242f2f2f77332f8bf1eff4650305a67f3764643f25db535494 +b2778801042a04ea06ae7a1726f98ba833eefed74d88b5330983f367018ef03a +b2780014a0fe3abd4bf64dca70eb1bcc8d5fc9e9c8a6add5452309789eaf9ca1 +b27a1935ceb563da425cee1c47af0e657ca7e5d6fe96873fd3808557e35502b2 +b27b0366b4c5e9317871fd15bc5fe311df1f238d3ca14cddb683f5fee9923396 +b2836b79ee0b83f9840787e034787138ab241cccb8f349b3d81a2ee835de123a +b2871bbea950acc9fd6dbf46b13f9bfe94726a4d017eec4121fc4405f63c82cc +b28f30a0c487a1e8c2eceb819cd966f21af5c5118b7f03ac59fa8cbf0915b3ff +b297d136cd857263919dab2252d78413aaae4a449dc45f1c4ffea76302946e49 +b29ae16682afee0742675a418855f662ebfce70aeef68ff3c2468bd08e5c415b +b2a2f7a7a4bf71d296e896141998bd169e53fba403f984cbf8e34e5a959b9055 +b2ad0dccd0a7de8f6edc8a1f05535381783b7aa9036e470d74530b5795d948f0 +b2b06be3cad6e504629fb16e749406322aacefa4c2f46b89bdb57f3031fa4400 +b2b1926876f62094be68c35ae28899e1d1d488cf269e9f4f4957d7318bb1f7a1 +b2bb8c9f3ea41a662fbc930a2afc69b60f048e07d7df59ba0954dc6efba522be +b2bc2036305051747e4212df0ad5afbc31c4887c1d7ec1a19a4912146db31ca0 +b2d12fa949ab15218cc7540ed700c61ba0d1a8da473f7e722e6daa9e55e279ed +b2d4a7a3b8f81a4cf0a76e37234c1ae7fd8c5970c99296818cd0e956802904f7 +b2d6d4314272934de992011566394a13ef551c26bffaa988ab4a09d33e923b82 +b2d7ec6501f813a21f749f1d18793f692ba840198590658c39d4a73dabf01c87 +b2dd3b1129d89ea6aa72090a2830effbfc055bede0fc21b6e47db0b1701340b0 +b2e5719164c4211500e1bd286c26123939c98e06f6cd6f86ebc8de9ccfa02d35 +b2eb08ef27504c16d2d989fe7dc4204b4083a769528621ff2fddbf6addfcf733 +b2fcd12be8adf8c33b67fb97f9f948f770a7b0056ce5f0598cda7003c0919188 +b2fcdbf5f7bf6ebf2e31f8a52b2015b1a970835ef50f78bfebb5b686f1297f46 +b2ff81653cb54c09a1853fa75dfc4511bf202fbd3be04c2cbbc9a078cb2115cf +b30969285a0e20f94697c8f9e86c2807b0c0c1073d429cf8d4e8512827a0839b +b30f72ea5919ee37469a3e33eb80dea0dc3b976c549bc24340e1c2d265afa53a +b317d0bc1f36836b6c83724d688a1e95bdc20f9be0ba964a169d7ccea5862029 +b3185beb91dc5adbd48a4831a03a2f1557354b158eca6a07a681ef26cecb43a6 +b31c83b24b4aca9333c047add759f7e0a90040d4104990534956d490940d19f3 +b31cc7eecda3d4959b4de0e685b0a515d74fa87951584bf58dba49af680b1863 +b327ed167925eff9ab19aac76b70d6c966ac8be59ebd184deaccd6d768628c20 +b328dd194128ebaa36200af88cc623fd387d1b68f3d5001a5f45e7926630b9ad +b32c48ec9779810d05b3f3c796c76c25056cd71eeb213abb6c6fba43d8506758 +b32da176c7f1a46594d0566ec878dd12845a29f582e252a9f4be92c38f473f4f +b339fce54e17c775924ee7e228f569494bc917261dc02c36a49a6c332f358e37 +b349e837d48041a1af38c69b3f8721a8c1cfc9b9064a1908ef7533ea0e1c3a29 +b35cabe36684dd86b97c5fc3158915c3f3ef4e38bf8a4fa8a04934fdf591fa69 +b361c682e029490aa0acf199991d977ac6548bd8be9a45ef625384491ee310cb +b362e7788ecce392f934f3c0d10530f8dcd1f3cf4a0791ffb853faa8aa926a89 +b3681a59aeeab7281f34fa1911acf50aa31239e0ee67d0617c9e7fbf7a6e62f9 +b37068e5a654d5e6457e3b3d897a192bdf6a0283eb317483f40d9d43c0fee14d +b373dbebc8ecaf520afdaeef1f80e707dd2a8c688c52384777daec1d89b078ee +b39592c70d3efe54a7116d86b5138406147472036ab32d0d644b9ff14b4740f7 +b39c19666e5bf8863b8887786199abed724805316607554539995fed2a02e61b +b3a331c89db0a56050b2c20062bef6091351f98aa1d15962eeb91125b0c6b6a7 +b3a9e817219afef81a465ddecb496e2b4d2bc8f1b3ecca7cf55ad6ba0568c1c3 +b3af69b85bf9020f4e8e241724fb2226ea6d58429504297485e174c6fe19fe06 +b3b2585fbcda4de7c526a96cc77e42bc98c9827acc8777474c60d32c7e361a26 +b3c13b5718f6d8a89ed041b16fbed0dbfe721801c5db5c14e1705d82617a9d5e +b3cbffdec037fa4fc372c6651d4faca8d4b5523e5f50922c21755065ca68cbe5 +b3cdf59014838ab2320bf942fc886a472c1a893f6b691eeded7e6d4c19304e7c +b3dc31ee0805ac8e04e9aab80cdb615e84db496373856f9f56f2559e57d14604 +b3eafe55637efa550ef2686c3c924689f5a451a6e4350effd220fb7457e306d3 +b3f123e78a951479ddcd643ea3634b9776d278860cdc7a2a4f062aeac9b0fcfc +b40243a762d4f4a5b5aa0bd2395016e7664bcdf17a440506b024d36af0d1e82c +b409e2ae6b89eafa3a58d16fd3371aba3f84a9041165b95ba619f4934ea8e2c7 +b41bb9012cbbc9991d2e48fec6d934627fbfe3f3969cdb25027bd1f0f3672451 +b41bdf063adddc3e08192deb39dda994a612c06bff7c46d15a9b60cccb30dcd9 +b421f94416b8a3a2ef676d3dfb2b11f529b5fd74c5572f0d62c5c83674513a74 +b4282bec4d16d498ab56b9c7885e9b59df8a9f9800b5b5ffac22f706bdfefda5 +b429afa603ff6ca0fffb3f8f007d9f39a3570cc77b12ec88a9ed798f4009f1cd +b43375f888225d0bcd9d327e3abd0cb7790751b1f275df8066b12ae61f6b05fe +b433ea4e5631334de011549fc91e76c1b7d028a0e5b142eee637942b4e29a75e +b4395994babf1bae53176c9b70c26671408b9dc30e344113b203d3801b16a3ff +b43d5a26132b80f9aa3af9f88aac6109c4c1290ea7025b85cf2cda96263a39bd +b43e5fcb320850567f88b34caaa9bd635c17cc30455637332a562f6932f508ed +b441814a98f297c36b8221caef6abc33a7c728a4ce66189f97c714407c858e21 +b4521507de96cefec049f7ffc87724e72fb35bceff015e94b6141b12a2898b5f +b4630a53a8e03a57d9500a3c6e62761b17ac1fa86a22bc5e1a0ca985e6ef365c +b47aa42f7bad16982b0cb062d9551cbea90083a34fccf39a2f976e4be8659684 +b47cb82d3ac5a01100f42a5998a831ced3697558203b62d11fdcf374bb565d7a +b48967c2d2ce0760b24fb5e2c8cf1c04f73ea23b9fb8c5cba4b250c0ce89ad9a +b48c5aea51dc232e28727a112c3131c31fc87a3a523f0653ae178750c90bedb3 +b48fbc17c88c28fd46a8600c4dfa9419c89a089e8e3d3ade8cdae3f25141b0e4 +b4914149b7d67c756689ee019713d252d4636da56113564b57361570238b5589 +b49ce309bbed7e63ce1e253c09705303fd8772f36fbe865a1dff5141e01e3bde +b49fd774ddba9580bf920f182a2558fba0a34ef488c632ebe99be4a88009aa07 +b4a18d1bb7a88a4ea141ef61627bc580ec27b25754479321e6ebe2ff5cb22e07 +b4ad023ae5cffc087a2fd961373ade153570f5ade548ed9bd73e8df397633e58 +b4b3e928c5daf228f7705314f3d2dd5cb9efac2b34c7b3256c108c4617a20af5 +b4b3ffd465cd0d0a8c012a67a7bc042b6cc3582cc77c0e2df7b113fb43a0d910 +b4c705686e7fe424fdfe78b2d7af64c8f21ee4bff321c3d82c75266865ed6bbd +b4d65b50b3b8841d0c61f5613f7def344c67c272c8674a0cb90bd79738316778 +b4d84947d00e98ca1869303ab0aecbd28eeffb0bc132a7f9672377fd16c2e172 +b4d8978f9d84e32229b9dbf37240586cdb5c3b428f82813b609c271209ef348d +b4de0ee6889014860a25212cee96b1c2442d191f00a1d63a096253dd2c1d7590 +b4e1858267071c9b0c2f4366f16f724e8d43bb5381c7f3b1f1dce703548d842c +b4e4bd1a473d806abe4443728a916ef96c3435bc96e64b4462fa94fb64ec6b9f +b4e574e65fa528ea7fd7945e7f214630a499798d8b0f3b2b139c4752c3b486f8 +b4e605fdd48207d0b5b3b37954d421a4ffc4a9eb6ed30012679865678e19549f +b4f74bff5ece77c8eb513384708e41de3e3311918763bf70256d1b67c25df957 +b50a0b5d2b243eb4daabefcbcf58a97d4992a43f6ede4774eae2375d7d7af318 +b50b9deec044dc16eff2b0bee69073230d014c5fcab7d6d257ea134de94a3297 +b50ec5a738103634ffbd990beb0df493c6d93167651b5652340236455e09e35a +b51496adb4e5f07e8bc308fb63f60e6c54e79148d0f693a79140d908c5121dd0 +b51640c35fd32ae19f40738555e9fcfaa3c6a920404cca9a12304f925fe51da3 +b522cef5c7329e7677933d9aa24ba31ec14ffd2b977666192a66e3c688f63550 +b52366e9dc3325043d5e9fd51b8e5d144cb31e2570796fb50c937a6eb6996615 +b524c5b3091d6e55d61a6e98eaf64ab1906357c76ecce48864fa9607b0b511a3 +b52b673fe2eba96f7ca3ca6c0111a4f79c3f62fc6d8e2a0be862a4db096cd19f +b52e8016b699cf15396ca22a45887ef8b442c47af368b21fa8a2e93a6e97bf67 +b52eb28cb5cfc2657a85b9149f5fc28cac7af87a25784294cb69045a831c6687 +b532feaf49cc28dcda6f4bbfb78984812b371604e24657e1914b12625a992105 +b5351f77d8b6ae2b7df2b17e70a314e3ee4d3cd01edc1b3aab65fd99471226d3 +b536fe4025e366d9995d96be7ba5304149747cf4db92c848975bbd6eb4473370 +b53d45a09bed11116e31b07d85cc1b3d8062c36ffd63d87b8a20c4964a1ecf9e +b5418e5da436afe7a5b3efe53d70a4c35e74b0a471646c6c905e2c0137e0e1ba +b542b27b82ed3424992fb3c0bf5ac8699e84651a693bdea1a3300b084d51a9de +b55556d2c5b18e0265eba0b168536a84b76ee3694e6ed8ffa1b1e4e3affc312d +b5564d96de50d02e057c7d11ee02edf8d1f5a7db1f50fddf74a7730b94757dbb +b557afa24661a3b85324b2687844f69a3c781364a59e02f7e84da9f27a5d7842 +b5596d7142d80d49961ba9ee7a269eb89633cf815252f4be62040a637769d42f +b55a473586bde7ca668a6050236f5e35f290ce3f59f766f22ca264f9c3c7fd1d +b55d5bb5a3d84f4edaae7aa3e33a1f03dcbffef58c1338ff73f292861d77bdf4 +b5626ff0c956e850dbf0297d15864588d52930264880bedd66e3522fb15c7223 +b56581760e7055bdf5f7cd8a5d0e9629de47005718d5185182a8fa8542bf73e6 +b56ca5a9918ea7907e2d981e7ab45865eb867fdf67382dd8ced5735692b0bc03 +b58694ec7463696cb3caeb610f3d21f99bf303fca5c04da1fc48e8a33fc6ee1a +b588224c1a294af6469c1f88bb2d232240c30c27e7dbce9a05a14eee1259c663 +b58dab604072689933055ded08b5779cf47e891902657e2afc310308865dc46c +b58f8e33c35e59224a6674026c8f3931f16e292433b8b2489ad2acd71730e362 +b5919d497144af7dc91313fef93e058d6c8c150591fb471af6ee9f8451469c07 +b592e4dd201c360d8f3b6d9fa10c2b0c4873d82158ce3905fd474f45b0ee4fe0 +b59410bc39bdf56e0dbda48125b4c8f6413b63f9d018f8044fd771528e4ecf3e +b5965c2dd9d7e067bb185bf2c60c68a4c6e6845e4999cabefc0a578202229d04 +b5983b39220e9a826419d5f91f3c49ec620cb5dbfb48cbc345c585657b992f8f +b59dc88f18dd5d6ebf0d7235b7f7499a482aec6f9a1500d6b390f6f71064ae3b +b5a2481875cf58fc1ffc06a76c797ef505cc26ed44d233d1d1dcbe748b851580 +b5a4ba0ce0215d3f21f1c994cfeb0a6e06a559126b45bb4b16d4afeb7d65c736 +b5a862e44da3f495d5564b4c419d80b8fe06c81da8ec02dec4bbea7ce6ba0c06 +b5adef97b0cf965c100ad8c31678d8fbbe17151d80d40ec45635fcd1ad79891d +b5bba56b56e73762bb4f2e87acf071f75222ac7fd7325b7491239f4f1d32fd8c +b5d31316e6476d296d66a3a0577b6614bdce883e1b3a7a416b049f3ec67ec1a0 +b5d8749489704d7a021065f11fc1bfd63d3ea92211ec9f63f6b68ce25ab217f9 +b5d9516054f7f93174fc95473b242e084c609b63c81aa7ca516b68c5ded16b30 +b5dd88b4a868445ac5711e7ede11e44d97daf5a5aad7aff717840721c7f11932 +b5dfda921ecaa970343107018cc5a117ef108512a86a8e6b83d3d62a1b816d07 +b5e55ab8466bb83eb959381ab4272157c8b901d63c277bf7285d4088f500b040 +b5ebd64d669787d2387dbfb82857fcbda1845d9ba2fd468561ba716ac37828c7 +b5efaba4a474a3cc82fa025225d8ccfce2bdf82122bcb51f8f02ee5e2a6dd2e3 +b6024abe7f1601c67de29cb7696dd3ae74ab03ceccb42d0034cf08a6c5f4ea92 +b612c5b08a510ab3d485e6ab060ef46a4d1ac107f2812a6ec5d23b8ef9412bb1 +b6161a27a9acdab3cb7f11f99d76ee312d58f202f2644612d68ad6ef88e850ca +b623b24c155489091cda9504deb8eac1ed80d73fc3358f5b55f233fc6f36a0f7 +b623c4b1d4771f0ca0f7566663dc38efef0b5b22ca70a7b6925dfc423b3c72a9 +b62a94ab1da34dbf15fe97902ca6350afc34de964102a47e53e09c5ae92f7f65 +b62c06cc82fe41d142194c4d851127d028849d58637fa7b6084219d0573586ba +b6325770b6492bac7399199509a603d3fd34365a64f532b57de47f697d1317ff +b6333167365ef794112be238edaf3aa31f9e401b93e57d5afb7ab9b5bba5a9e2 +b636ea49ae04aebe384bce877590e616fe98ca4da42cd14fdd7d8cb1d8e48db8 +b63ad6b2c8b7e4eba9fbc7da32c7aa8dcb564108389d391c191f0d0f18b7fb4d +b642c0e422c4824bfa4be715fbc54f1935181c43b85f955b44107832fde79713 +b643d0beca0c5fe6481d0f30c7accb537885fdb9351a3c5cf3b6046031d6db2a +b64f4bb80397055e34e834368c934b640a9f2422863bff9f30e98eb0c36a473e +b655bcadcd032b1429259dab9e548189fd1adac54c8519ce749b2f041bb50004 +b65694ba4e1cc1d2a3925cdcca5fe466ba81d589246c4de56c3677bb0c220b9d +b656dc330e0589ed94627906e2e6625c29bd7cfb008199752c2dc9abb2af72f9 +b65c85b483a11499c13ac17cda2f88a5afa79c2d37ebe445d81a1165cff43ce2 +b65e191c6b129da4366ed806819ca4818d8d07c5b3410dc1db13d6ad91a932cc +b65fb815b4053ca560095f30df96df42998e5c1aeed3077689834f50597ee3bf +b663411c4a234e45903598130f9d38fb1999b8cc4116cea781e316914ab20ff8 +b68c3277564af75f3ef165f8961b7e0c568c92f55c07c29fd9177285c1130e65 +b68fdfd77c3ed85b67a4756c5a668c56b10cb0141aa50a206b1181ff87c2f447 +b699c6549e283567364b4f79d29e464d9ac3ec099ab6b63303d0ef6fa9a31841 +b69f7d5c5cc692f9304998ec4cb9811dd83ef0140d3490c6f55d0be31b58fdc4 +b6a397f023d7f53d1be23a5d10c3a153415334be60fbe0896b9ff1eac8055d22 +b6a3e3992ac0fac9a87424ccbad82d8f903ff1dc030a3bc21e9977bc2d8cc81d +b6a8d129c9a43dba453d4fae041f404139edec0ad4339ceec5c1a039c0d0b748 +b6abcd6d2321f81be3a3c60bd415cbbad80b49b0831721d8959c311d1e57fd5e +b6bbec15dc86b3522c80d1fbd189dbf9e533fb11f6ca959b9e40c58bdd2b46c8 +b6c98240f4ce7b95ba3fad096e3e1d96933f0de522b196a8e9186340858f68ea +b6cbfb62480c513d855ef2f200ad73f9a4e19cb0da1dfa4321af146aa3d253d7 +b6cc41655d299d899485ecf11ac8d02d04faf22663abe4e728ff4015aca13b00 +b6d1916e10c90de55cda2b870a3144cc16f8c7d9bd59e52538ccf716b53902d8 +b6d4c1531cd30b5cd1f2c382f4a4c4db7ca9033eb76f795b61fbe36b1c53fa7f +b6df802151e78569127b992a8a404807723193f7767d48fe5cdda6c1444136d4 +b6e06ae75b2a5f155d393a95854d7a173f2e2cccdfe24d4ef216f7df56b01bd1 +b6e840c8fe904be8b108dcb18148d38ef26618fc2464d2119237e6bf586b89e8 +b706ced1a8330f4f595bb6da20eb2fd4a39ec8c9f61bca706bffd5975542ed39 +b70956ab126c9918bcfde37b0e2548f9bb404300c67d9f2e67cf297d3c5f2d18 +b70a09aec60be3b328d453f286faf1a30bfd22161bf252e5fc1e87f3a0e36422 +b70ab83480335ad935b82e73c612be8d0d9bedeb0fe76511fcfa0de687d303c5 +b710150831c7553bec47e62fa3dbbb3979903d9679198087a46479e24e73b0f8 +b710930e9fecea8073509ac645c29c783011a02c8041c6dbb1bf0262a58b2da5 +b717f5b6778874da3028267c944bd0f5e41cdbf52a8821ccb1b39ddea031b7d6 +b72202dee5fb48f9f9070e736370112ff43587af7e69c8e26e9e4368e17f2dab +b728330d9f1929262a1bb3b36e15360b90e0d1fdf95f924d8a1fe03bbca28d12 +b72d7f1ae8bf7dff31c8002dc411491f4bc344593dbdff2e889155d0d02f7237 +b734b5b1a63a485b934fca64a993143c2c0a9a9812e973d1ae36db8736ed89d3 +b735c0960172adfd89307f209a16f3a702ab4fca5e58d6030e56d88fe8eab943 +b73b34878e1346a99dd35123f585df9ca6b5bd317826ff4ebd05fb850bbf8cc3 +b73f9349ba692b893f2e9971571f7288252bead44af1f4dc61c51fe517e6c723 +b74296deb8fdfb5c07ff2185815a0d214ae19a756c09e378db8e7222826899ba +b7583dafad0b04928f73d2a737c6f128d3412069662516a5eb4a78085845e400 +b75e156289444f0946fee7901bd4f201f7bfdc8bb01233b2081ca25cc2e4c569 +b772b72f9d30cb3d960942fb24d84ea8102c9204eebc95787c8826205d5d0a25 +b77b8441d39d0c0033641877b0d180bf3f293c1e079a40ba92b9b2ae03cee310 +b77c76b90a3e04793d79f493c8791c35ec39ea7d7ce3ecd60875ec80e0eec9f8 +b789222aa6f4d6454585b0d4c990a25a281c777af410a5d36a10e6a35f50a0a6 +b78b435e98f9ad2b05436616527c7e51b9a72872174472667b3471768454882d +b7940b49c9433afbe92c1ca0a555c0ca75650d9f40b010859d7731a59487dd89 +b795fc2938514972c4c0e7c3140fd25b4955da89d71ecd89134e155ca0355148 +b79ce3889c835da38059f64a07f8f4d42a0f9db40980fb56b0acd2fd204a689d +b79f73a41b322838f0d72901f37d4f08521246eb9d0a1a734e7a27ebfb41a90b +b7b72b71e0c0cce09bdb95aee9b705e861215b3735f361064b06e97f8981871e +b7b9d286e03081fbd3eebd7b9cc81424cbd2f3b3f86dca96f1687d1bfa14e55c +b7bfb961b12ba745fd1135f7300b4a18c020b8e8e35a1dedaaf3fd15f45ca736 +b7c13dc3c51c0522a5c84c1b784576d0fd63fc7debd9e14e8a7e5d0ba4aefe0c +b7cd71effdef3f3fc42115379a59148bbb5ab2c3e5c31b42c0d9d77a1e8bd9f3 +b7cfb80399dc09e25ee1b3905253deda8d98455b60d5ea3162a21766d2180226 +b7d391dd456303b1a2323c4ed5c9be4d74b60557e4d9cd192aecf7647132bee9 +b7d5e58c02f3a3f9cda7815aa3c38a4cf0eb222cb3746997147a8ed14fe62356 +b7de5a09c2bd3db80534615dd92aeb7d3fe5781bb3b40074c03fdcd6062589fe +b7e734e67a66b8a61893ab9717064e3a778355a49f84943f3f2b0f68ca0d7a4c +b7ec678e2bec400df3f2264b3b56482d789bbf879815d85f7034584078d9c335 +b7f47981af597e216e35380f242ebd0d2f575c34a8546e13b8949ced3bf5c264 +b7fcc0156199721e8f27fd7e2eed19db455adee226e42cf38c85fd0a57289367 +b7fead881d11f43f79f7f6609452c1a4d3be5316fd56422f6660e1ff57e496ea +b8018d30c4b165897fbf0e756bb6d8570207daaba314a02ec3bb42f9596211da +b8070263556177dade15ead3ce4bb4166af01342fb16cb1c6759c30121d36dcf +b80a3db83273be1ff40ae078130b6b7026f682537d2325446e8855a2879167f2 +b81f0ad33c2e11b832127d05c0650889b220fa3e11390ae4ce9b3d42227676a5 +b825c923cd9eb6e932a095551bd1cb8b32a4525acc6e90c21aa975ee413c9ff4 +b8278d0494a06e689851bb0c3b5c86755c94c4879a0b3563d79b515102db1d87 +b82838134bb5cb4efbaebb53bbd0f928ab810d5cda984e4c057a26a75fe0010f +b82a98b73fb121d206164a1b2a6a271f7bc5220cf683ae9bfd7ef3edeec965a1 +b82aaee436925d6f64e543e0039111b17b51806b0357d2e8e6cb039753877bd9 +b830bdb8182b9ef8bb088e51823080fdd24380e974fc2248f68cbcb6f4aed8b7 +b8470e0a77696987acbe8f21920c5a7baf2d0e1aecafe911dad00ae119be7109 +b84e875c082b730440541faabdf0fd7af120939c648e4db04cfbd78729f2b3a2 +b8605b396195bee8d3b8664d06dbaa7efb37cdaaec4df48adea60b35edc9b3fe +b860cd964429f220b006044a58cedcff7b892313f6f79d3d9a6e7fc4cc5f17dd +b8615d7661464ff324c41f336e3e6ab56fd1aa7aa44ff9adf3cdc00f85771f6b +b865bff32f149308b5bb75000a703851b70d6d4c92a1ce352c7b50c78c5c2ba5 +b87283c499cd5ff7dcf6bbac23f0cb32b1522d2cc8e6db4665ee424a2798c271 +b887a1844648455b718ad6a5f929d01de9c226e46e55c45dc3f82dacf58a18e5 +b88ea143aaf475ab0dd9318b2ff914bec7515641f9ddfcbf81456719cf3043fb +b895575311f3f9b09717ccf79395c51799924f1d60a6e53634e1a980619d1da9 +b89bceb3b9dee74719b3cc67949c4cc7dface6dc26bb2133358f3bd93635feff +b8a2e80c3fa4ae47fcdad28c46123f33276e44232d4256d2aed7509fa51bee17 +b8af75168706075dea9c8acdf2dedf2de45e46dbb375a467a247d0528fb73599 +b8b443e7230e652918012eaac8ff9add289fa4b185ef28f0476790aac284e1cb +b8c3ed246a80591c94e2d079f3f2204f5893f9fdf458cb2d7237463bd6b8a623 +b8c5f16c9fd21d1fd73ed40036c98c5e83d356e2d8e4d33a75b5672823bccad3 +b8d08da38c8e753b3f315bdab298888e4ec2e7bbb4471933ec1d1fb0ae49d399 +b8d4d880ca7e0e47795b5f23a887ddbb0d7ccd1e5dc0fc91a1dcb5379ac27586 +b8d624c80f57ba44b4d8224c6180cff8bbd2222a1655d3ec96b47feff5a5eaeb +b8df4a9456956f3ba0111cce24a37a6224dc06aea97adba9a074213b48b5bb1b +b8dff0477d895cd088ad2be3fdcdbec5b3fa433eb5a94030b99e50f1a5cc6ad6 +b8e14e076e1d9f51d8b127f36196f8af7034aaa476d13319ef7aa8a3511ace23 +b8e7ae9c43286f50ab10b32fd3d3cfde46b5c1e7be8958be325b9b6cbd28d08c +b8ef30114c6a8b050af3a9a670d8d12365d25946a2316203d6fabd86a85b5b0e +b8f37be6b019de1f903a28fb7799bdfe6ca9b392a9f738bd1d649ab95f655506 +b8f54e5b7d00f1afea9ccbc5b28ff3707a5a18f3a6d09398fccac1eb66ea582c +b8f96e6843a1867ef56a62389364c88dd11fd7a09f44170cc72458841ee8cfb4 +b8fb965a9e77c860232d3255f1e59ca845f9b5bd55351c343647fe95fa9314d4 +b9011f6cfdeb19e0e352136fdb1234c5f94ad6f3e2ab889f17eb6dbce8c5730b +b9022efdec95d143cdd39870f00ea1a318f0ae3a5cc0c8a47914cef968511ecd +b904dd521b70ee3a2d8bafa741106fcd309b9f6bc47c89c4eb5244169db01a8e +b909cb959eca886beef2c94d193004bf85486480a56637a2897db1c02cb1db1e +b90a2e8c188da72506224b404a8ffff0125bc5ce33166432591eaf556953748a +b90cafd21f73126c305f624dca599827c26b5df70f2a9c8b849110607175b18e +b90f0fb47dcb21cf105dda2e7295f47ae5f6acbbb2ab6751736edd04bed7df93 +b90f8a982fc1d2503daf63dacd35bfc714b22106590a11549fe4d74e85989cfe +b92a327d365d610f72b20bb27845e9390ad53e1066edaaf2bfa673ae274b12eb +b93092c25fc95895189b1ecc8f13fc99ebcb54911b361a093205edf3ec83cf9b +b930c6ceddb6ab9946cc60ccab66842b984468004fd68e404b9f8bd870cb1b84 +b93c720aa5467e92aa9edabe41b46a611dc96127343f8c13c876e6b4057da52f +b93f3ca6d04baa233fb9227923ff93beabbf9831214e8a4e24a6cdc996e719e2 +b946b4f853a943566fd25da6d965723eaa393b997f116a6d10d2df9c6cfea66e +b94e6265464d9be80a0fcb31446bf4442573b66ed41a0a420b1fa705d089d21e +b952b6a49e85ead3a34b331ef07b25eab62c5c4e1985b1b1e2e79e49a188d900 +b95a4966832ad096c0c9c02ff1812ebe191b9f8dfbae995e60de728b873737fa +b9618569184fcfc37fa5726a86d17591be79d7be356338e0207592bfc9a647a5 +b963cca41748f1805f52ba3a904e735f1b780529c4124c340a424c3df5e1346b +b967b949349f220d26451869ebbe4116ce64a419d62885baed877a979d22b6cd +b979115179c8d7742b4a61c6a66576ae2be2e6a63799d42352f1ae5844ab77dc +b97a2803770f4bbf508b13ecfd56455d32c8444f534a5e8befecb160cd916d7a +b98319ed4e8733d8da2868d70945e10aa0f0a8a29968a7a68f57ce53561c682e +b98d66d03de8dcc568e26060681fd18808db448f13cf91d3e9327a7485036d6b +b99189abab0f10c82bc2ffc0650ca085b7210a03506dc56f824dc5c2b20d4bc5 +b9983117d1992d0635f35fe81a650f7db5484b4d07d706abdd01e14fce32730a +b9a6db2c00fa08bcc921cf8561130022d02b3c61e343a1625a81d65c92cc86d8 +b9abfcacaa782b71fe24eb945df308eaf6cc0cb755a77856bef7a59ace16702d +b9c2bfe122ad75efa4e7f14bb983e6f7a6e489d5facc72721ca9c2de758713d2 +b9dce12865e44b2cc19fa6236593cbb48d70149fc2fbe7c01d7846367d30cd7e +b9dec7a938281c1dc3e0d25838bfdf65856369917ef16d5a17b447c609e1517e +b9dfae18b445c7e16c5e1db0fae4bacbef7c5adb69bbbf95b4d0ec2f4bb31be5 +b9f7fdb5b1a53ce2a949a495fe5b91a0b413f96aa6126552ccbcd53958f962cd +ba0604917bf3b9f0169d0b4a2edee9681932e9ef343af71e078711555a57a53f +ba172989eb7c823f72ff8e640e519945aad03f094dc5eef40b2e8d3c1d02dd6b +ba19acabb81652bdc5c38dc65e67534af7ffdf579bce77b9f16a21ab18ed7029 +ba1d0008f3298c9a529fa72190283b04e1cacd7f2aafeeb02d11c58efcfd67d8 +ba1da615185ea49c60937b7e64da9152fb5141c47b6a5969cd9d80679c294409 +ba205b242cda799f3eae71910331bc9521ae4572b63728e7a1e85111eaa6b560 +ba295f8bf687c9ee9e34d1be546e118cc61d41f3829beaf3350a7352374f2b53 +ba3dda8847fa1c5b2580365af27d42e5c2f425b9d57972aeeda9883b8420c9e1 +ba482207dbe2b217dc23d28197611fca0d83d166855f7259f8eb7dd48a1e4e57 +ba4bd9c9bfa6ca72b86b212ae78f3da0b4714f82da3db83849dbde77c67c07af +ba537ac477a66fd8a11f116a9cdf3eb8032d6cc3831a2277352798ae3a188fcb +ba595bbce71f08dcd7ac0b7beebb59ab3b4091aa50a1b00f10fdf8b70c569b39 +ba5c1a25f161412d7bd5d5c09c7111ec690a1c159093e83e9e052025ccc24afd +ba61a6caed94d9b539f086433c17a5fb3ee74932b657df1eedbe930c856d4255 +ba6e4e20669c82641ad0bb685e7cb657f690cb6c4e3f4647836542a76013a1d9 +ba876258f30b98ec3111bba6446a0b84967a1e68635f251b082cce4807c1784f +ba8c2e2f0eb621053311dbf1e09fa1a8be1ec64a600d8f6c883618ac0d744e87 +ba99b5bc4f77d41722772bec485db21e9966114531004c20d67791c60d9c75c1 +ba9aabe13028d3850b0c42f8e4f7949733e96942b283a72d53140ab189f2da85 +ba9c1d4307722d16c4b4c6e01ac8c2bcdfef4cee8002db0753d0e8db6d46f7d5 +baa87bfbca84d5e72cc479819f56f27a8960ae9ea0deccdaec23180b73542d5e +baaa6604fb4705739eaee1e962b176ebb3c96995ee245747db8ae7aaa15a52be +bab0f399111ca2186e1a326a41e1e5e79013acca6eff8a5d4bbfd8798ec3bd5d +bac559ef891b39bdebfad4a62681010aecbe5efc424ecb27d8e52070f84c5cb2 +baca686da5f7ad71044b235ab5f93aec8367ae3980874e9b3d4d6d27d6c622c2 +bacb25d0a7b08ac5e9cd17f238dacecfd5f371235cea64526dc46b63d47e2aec +bad6173a95c9288cd84601266ce420ddc4df5dd9a26a7d37444cc3703fb5ccc6 +bada558bde83bc4da875072e519c8544611df586296ad0b793d702872d2ec66d +badc155162d0cb4058d10f5ac6c5be70f08b6ea01a8db4af8b3a891cee1f4f31 +badeae0dbae8662147a007f9995d3b2fb5fff79e7a46804bf927480fede05b70 +badf1848c90123c9cfbab349a3f482bbb2223e73a292b2aa74d007da11f65b9a +bae379ff00c6b17aa91d5aa1d2604949a1bff87e8a94663a71f08729eb6f0e09 +bae93ae43cd6a20b037a4df22f89fa5e9b9fd129126af4dc8fe695b4fb0c206d +baed7622ce42cc9b16adfa6d24801f05180f7c747f1b3ae1de8b1f4dfd974e72 +baf543f87857a5d3d5f1c9c07df1eaf4f19fb70522a3b83ade108dceb838c94f +baf880aaa994aed203c50fbc4f76410e034478933116c87808199003db4653d9 +bb01523222666bd0fb737bc69336b12d1bfa08732c000d46376a85c243872988 +bb05985ac47e9d3f5beb30c3d387328f242264159109651550c1434a22fb0f0b +bb05c3321d121ad39df1d132b1366893ddb2db5274748baffb72a5d36b029356 +bb0d66c2c7fa456108f8b29ca3cd39360a0d04f9b9cf610aab04646a3e671874 +bb16899bc07b7bd9cc2048ad606c79c123e44508a555e9feb71a0ec71f966894 +bb2b7d1d38d405e25b783fd5c99b7bc185e28c507891d4cfb9cf9af4f532ecae +bb44f54b7bf09c38160d1f647750cbed3d72e0f5e13a21ea1baeec04db13e40b +bb56ccbb563e9d67205d47f5fedfd17784daee1031f2d5af5d61072f1b734881 +bb61344157a2c9f88896220e8893ab97ea2df598db58929249ba031ee346af57 +bb6bc2074ee51b086a6ff02b30d02fe3395b522aac03608cf8d4ac19abd2729d +bb76e4a8d4420f42563ead57e16d830b8c4a8c99a967883e39542d997b21e5c6 +bb7c009dbb5c234caa8f83170fe6184764d41400cb92c23c65235dd951de987f +bb83bc1e33280cddc3c5b8cfdc368f67bedd2b6edb4aa53323714bc70a4407b7 +bb83e476d2f1a6199a3f8b610caecaddc4fdbf99a1091d900ab9a0848493889f +bb91c6ad311d26bbed7aa8aad0aa90df11889cb5593c87bb6a2285ba88feaa8f +bb93e635173c40d8105d5bf3c710f69074c7b0731a90d9cf97d9eaf5fe072bbc +bb94e29b6e578bd46b02e66bf3b49743287f205c51982efaf192ff2a25ee0937 +bb95d85c831e6f14e07da384a7d22a1ba55a3a1643d1621faca2659a64528079 +bba42d1aded7dc8a226ae11b5061d3f1611f2233e995a5557615a3cc472f790a +bba43f6a41ab21bbdc61e4577d3dc8972c28aa89527832e45bfb26f0fb5440b5 +bbc1500ee416a90c4b8ce254a977d0ea0cfe585ec853c362a9485c8b1d2e4e90 +bbc2ff2c1c67e6bea96463f44b395cc6ddb4181ad6d81097fb85373dcfbb2904 +bbdaaf303391dc46957566e2a683eab6285241fe71a25aafe4bceb0a0020cbc3 +bbdb2c2e837ca7188c214f4476d01a6101806e1d53ab1fdf7dbc7bb870226d45 +bbddbe3bb077ec707f9a45e564c1c50b6b1c15b48f63b7e1f3c535312672c308 +bbe02e1200e2a871f16080d6de53dbc0aca4a73132e38079bade82c2d615172b +bbe5d0fd11205787d98ed8a1fc0579403acf1c19d228d602d6b58208a6025a97 +bbed75ab2b988407308bf8171e996100befc0295a780fe9f64da8d845934fc6b +bbf74fa3e504cd9eb2fe416d58943de16430f046df505b840913f9464a821dc5 +bbfcbc1e79c97ff661389e660e7ea00cf516ad7b9817ed40dda44f18d5236d83 +bc04cf51e2263925b5c6b6562379d77975a9cfda33ce01fdb4459b5fa36ba5b0 +bc0acb0993e95f4aef0fd95aa73a561fc8ff8041051ab9be2e1f90367d9ae166 +bc10ea43381942b6642755d2c4611703f2f738b97915027d051be3238a661b83 +bc15b34ea36e124614fc37abd37cbcf0cef2f6ef66a55a73e56c22c95ca7b29e +bc1ba99982ee64d19edaeb92d153b94fa3ea64f8fe6674e52760574d711c3bed +bc2abfc668e7c4a2989eb672a162ba97657e0560ba960fe3c01bce13bc990e8c +bc3292e5ae03554854a68f2afe80c2205a3875bcd766b20764d1d29ce6c9d79e +bc33ae8457e6105f67a465b835192b94df7bc136564ad646d0ae46447b28583e +bc504cb276daf4abef1269e4ddf003bbae4fcb73ac5413708a62f0bff23c698d +bc575873a2649a4f47c5b6bd7f280271ac59748c24e2bef0a8345c59a6500e69 +bc5b19871152dd2bbafca4fb505b4649bf93b65276124925326a6faacbd18fc1 +bc5f4d59baabfef6b9ee25956d043669365653505a23d67e8a057bc4e34e819d +bc7aad34bd6b1d5bd6ea64cf46cf79394e15b7952441fdbd17d5e12c975bd661 +bc7e6e6b157d938df4ff019e2eded0d91beee87bd77391fcfe43f554926db8ee +bc80d7df7599923ade141b9fb4d16a35fa19ea5f29008c8681c6d8458cbf05d2 +bc8db92eb6a2065f5a7709c72bce63fa0dd91e060216ab8ea9cd5e59f8060ce0 +bca3e64142c73ee0addbd2cdf18843520cafc5871e98405cb1ef28b8866aeb80 +bcb0e5019bfe65f958e22c9268d5732fc65933fd3d9ca1daee578c929ad5919a +bcc530cc03af35385b3b828400348fe8d681926681831da3a16312be388a4f10 +bcc56f48e2dba0c6acd65d7ca58a9cc0cf74f8eb5f9591f652b2428e3a1bf9ec +bcd5513c0fe26e384bc3c56ca005e29effb654e65195e1aae9be14586c7fbef1 +bcec5806a0105838819daabbac1d98b8381a5fa2a54849442fae02e0bc59b53c +bcf8ddc59c3395e03f29db19b8a9b009cf93efe5b35b09a597a12adb767e211c +bcfe77d466ac31a629321bb2b4ce83bb44c2bd3f292e33905a36adb09daceec7 +bd02a3c972dcc193cb3853ae36a1e177c49c91ba1a5f2f6851e4383918d2b1f8 +bd0894c3dc516640d0ab34821dc6e09a67b0850027920c2dfaaa4c88bd106f05 +bd09927e5524105d04aeacf5788dddfb75aeb4ec8eb15d89ed5feb2032d85c04 +bd0dc609e8fa6a4b45d82e42d6d8095409688573bc341dfff7c6acd7384a84bd +bd1043a072a1494f12902ffabfe00a8f6c18a040eacc7ba93258c43d67c88faf +bd2cc857e96a84d1601917fef2c72215aa3ee5d57c1052de168979f7c44856fe +bd3449d2a4953bf95ac0d1498de2a2fca5a09eda2bb064c4013455fbc1ab04c3 +bd3c35257c0cd601c515b6cca04f4fd7b6b7be476a6b0d244f3aef27531d1ea8 +bd3fb2c0a37da551866ed9af0ec6e1b83a8522631b00a11eae091fa9b79508c8 +bd41d0762c11445da824aaf0bc47b75936eca85e90d31557e07a578c9f6ef794 +bd4fcfde4b9684b675e9de6a3426b4db9502249183e89bae03a5df3223c4ceae +bd51c94e68f392e1a0cf40a3f98335d4c1a581614db1aeef203f7c9123ffb436 +bd5a5aa8146fb1570f4d50a887b07046f6955b21ad8f5a5b945044c98d99cd8a +bd60cfe1252cfe32b71fd51fdd8bb4e6238709580e2e22ee49a9daa4d9bb10c4 +bd6498934a37d930d2e93d8579f459752d41c92cdb024e35a7e4f13ee4cce006 +bd70ac3875b8309563d009a713397b8f0adc8160c38127dddfb905bd11418cb0 +bd785a65bb3a56ce45d7d94ad8ca2b2ceda88557772ba6df5a354826745c9ae8 +bd7d1e7c1304ac80967e5c856edc9f1e31b96b8d7034594360e6f3edd02ff5ae +bd85e6e0076caf5b69cd10c9632db87caa5ca609a94919ab6b30af248b7e876c +bd8841df4c0595a9e4e64ac5995fb50e2c6202a0ad1639d2fb796a3efeb10c6d +bd8ee5c2521d824fbcef1106169014c120b8694591e3bac0988236fe0075ba5a +bd93a02f219ef2569c7541ee5baefafc983a7b5b293c5177a21434e5a2168ac8 +bd98635999e6f2cd3f57916367510a077d69eaa4137fce7949ba17f8e4f89d8f +bd9e0f4fd94548ebee588ac4c5b62c8e0e29acb7c3779d4baec7f8b866c4f36c +bda2c2ee18dca25e1a5883b619585653c83f6335cb88421cde26be19298bd0eb +bdae1495721c53f4c6c4bed5c3179f63bfcad4374c8cc73d327ff0e4aea6f635 +bdb256b43f965a875d88ed33b61f3e657ef6fa253c99d58d9693ae6cfb00a564 +bdb897232474ffe0e682d39f25aec0b65082101ebc796f6a1315ba6275fa63c1 +bdb97cb9cc416b6808b68e322201a3bd8cf87864ffe33b043a2b6dc5b900d62b +bdbc7e01ca9486d2e1576862c3d8762a45f9bd33932b209937af0ff27c56cd5c +bdd187018cb62d3a90941db0b2e155af0f4add42dc220818390e15c413e7de4b +bdd5af9e6e47466dee8641805eecfc3b6da28a3d7798d4b98a628cff24c15bd0 +bde43f762113e77ecb522c21a4e54682fb4127cc0bbd53a21c2116f9a0388814 +bde9c0f4e2610141852952a42c25cd07dd5153ba74b44ebe323cfe3ad4b79589 +bdef4426fd357fa88e2842736711970f9b85d59e1c79faecd6d675b9ba0f0820 +bdfa2d18479215fc40876b4adfcc178123ba0dc7d33c7fbe78fe51b25aac49bb +bdff0d46472b7e21f71fc2f879a687db13984ca12176d6aa9fd2a84b52255f19 +be005ed68e141a2b69ce81d9632267c450e4a2ac8266be8f424fccba4bd6994a +be02175bcefaa92168fb0502aca8b116bd982d42e63160e383f8b2a240214ba2 +be272529727ad5facbf5ee39212aff865fe6347e14c3e651980a2368959fc0af +be2d93c2095258900306703ffcc6dcd460e8f774a10b1319d229d6b3c56e64f9 +be32d521051d06645a3ff563f7c1a28df7d3373be3706ce0553788cef0be64c2 +be5feab18cc219d6c3a7fe9e33e86ac4c6d8924f900cef675de5b9165e5396dd +be6d9e56592348560d20c94207f0acfef3b88095a661963bd97da5f17e235cde +be847daf0c03ff776ba3b523de0e1dd62be330d7c35a6584f764564424e1c0d5 +be88ece8d892857291f32c3b836fbd87a3183a829d20c8ef307fc5b7db42189b +be8cc60082f3d07a902b59db9ba6aab6cdd279f0725da387647a62642cd23829 +be8ffd8c48ea29e4df537cf7068695a54db2c2c89e3f49722851f7290059bfc8 +be9e1ef9cbeb1e5b69545c50ed011dc5237b92de26f84ce6786846cb53ec2ca6 +beb0308cd1e2be0191607373ae3794864b44126ae5ba4466dd7c6b0c071502f0 +beb6b0d1314af5c03b8ef7528e353a2e0171b0112959472c6a2e093b68f2c250 +bec46317c0cfe01dd007e8a27bf64439c5ca2ec8064b80b8194e0532511490c7 +bec74405e36ff63fbad4830ad89c47e710ab664ac21037acef4d08b1a40eb11c +bed0152995df4a62878e7bb3c59c8f564d0866436bcdc0a87a2abf9d73517926 +bed46ee65124c8fa3e53bf1df0b8b03d857947c7b59dfbfc0858feb8d26a20b4 +bee17fac582bc724bc0762837be9a2d56f7a6e21d5890c225ca19ef954d05dc8 +beea46c30a14b3ca82bbf7a9317a5e8f444272c8118ac8c1de7b794b91240053 +beea68b789439db718c9adbac5af942848fa0a8a1c0774fee354cc14c5d26d28 +bef6fa2915b27094125018eea5cdcd043e618102babe3a8367d898ab4846c8f6 +bf04aa5ec26e4b8fb015cf5abec1f7c6b77f4ef9a7f0f0e8dd6eb6e249dde31a +bf04cd2a71e1d740d4adedc5c5bb59f7fbe57f2148a963d1dc4cfff9a723122f +bf1630622a668fc8684b04a431649475dcce2920db5fa9e24219be8b74f5a23a +bf20ddf618553d14a76356589f8cf1787c7ca090b2e69d3765d172decd9112f0 +bf22a7414734a617bda1c4e48219337aecda7449d8731ad447c70b30d815798b +bf337adfafbe1cabe72972048882f0da56c6e02139fadac7f0e1f52b2432fdb7 +bf339dd0638634b8a4afc848b45997b258b9277d17061939c8b6676c376e48ad +bf38191e283d03d9de86018c76f31cecf4526a1afc8d0af15b5d892d3c46e4e5 +bf39d9e7491f874dc28e5c3c7aae37937062d888df3ca577203f563769888fe5 +bf471da5333f281ac6a8a9e8281b293411711e6113d42975e2b5cb40e8a481ed +bf4b92694008e5e954e703715e52861efd91d32f8b2964336649eb466ad9bf36 +bf519e6d973d8bc2c6cfb59195a63ca6a83adb9ef6cdb1b5e34d580db796d645 +bf6456591839ef4b1f65448554cdcd89709098617624ca71b257c45a6d09872f +bf6ab618c12f9e1525f6e3b702ad460bbf28babd98060ae1ac91f9690c064115 +bf70899865bfb8b3630850c9fe392f585966fba4f2b2639c6d416d7163a36a82 +bf79bffb6ddafb05babf4af8d11232e86f608d251538964cd96df71c1ad3ccba +bf7bb5039b9aa14335b053d255773c830e481cac46bd1e3b1776db8627c11653 +bf7e001309077704756b51209f5f78fd55b8aca6bc8838bc946bcd0c7b5cd6c9 +bf84dc18af096a13fbb3db728e8ae5af47e69318b907086d9a194a2f507f4d1f +bf8f4a0f89beaf545f64a04b90074c7e4a3dba482df3d8c8d9c3bfe1f859ff75 +bf90c7000f360a4d6844ccdb46f55cbba258d0fe44e4029980c6b559562150fd +bfb032b4b3b686899d00c4f1607a507d032b7ae9f6432ac56a459fa51af67ea6 +bfb0c97770e6fbce055baad77e050cf9ebb5345720213afc1963d84a590600c8 +bfb6e1f22ed632ab9de0b523d14abd93d3c221a3f6796c1e02418c33d6accd4c +bfb763221102e182e130368796778fc0be5eb7ca01322b24c2631dc34c90ad13 +bfbb4abd77b78d9e225ca2cc9e78edd1c5af7685d27d2fb1b0370b3e42e4262e +bfc379b212f88c83f22c38cbdc70b8fef9d91c26844423a92d537901044e4464 +bfc461bdbd869d48958885c1f2219998f9311bf52b805d49200ef50e9ca0dcc3 +bfcadf1bbea913e96d18b765e4233329c47e02be97e17dbd87718e1581609d5a +bfd3297a0658baee8083b4bea39aa9c54f685be60e169c4809f80b1fb8f6360d +bfd3bed2f62902612f378a0b80247939d09a6faf3e4d1f674ed93f697461736c +bfda193755905997348506a9503d49df9bc2da7b9386dd027e493380c8ab87ba +bfdedd3a03a682149fb5d97d4b519403631706e83a5e7403cd4efc5a80dcfbd2 +bfe3e39bc0efe8ee75e8116c3d6663d239ea6263235696194f5979942498723a +bfe930021f84c5ac8bb0fb27fca0d78eadb09fccb17aefd707d9dc87a31a2477 +bfea26e2f1f17b6137a7f85e0cbbdabb14834b3f4b01df50f327c3fef36e9b24 +bff1b32326a212c7b894e6c1b431604229ef0e26ccd2c17f64b30b06fd851aa9 +c0023113a5a79ae49779aedd311d7902c0d79ff60628e7e73de9476832b85700 +c005ae0b7cd14e6dc398a35b295a1a1624a7785f76e1af440a7b4d006969a0e9 +c006ef08932a813cd7beb4904971ed702234e3f7598529915393a8bf620c2048 +c0191bb33c1dccca63d1f75d70e9b7808a42b670e15b23b0de917bb45df53866 +c01d2baa56fe1e2311bcaec4f980a360c8526b104b06b14bc7614592c5af4132 +c022fc2974ff6341bda61f0333f82837685c692183cfd13b690945a9614ad841 +c02e91fad2d5b385a689a63b76b43b3ee224534c58678852d52cc87872bddca9 +c0379725872670014187198d083a342aaab1e6c253eb2a903d3116b0b93789a0 +c037ca8f3fff4b12188abda0a5ec12035e7a15306cc58cf2dab94c39bde904cf +c042fed7ad2fa74bccffb9444176d28ddb9f50008edc3e648176620a3d57895d +c057e3412c20931e008269457f488c013fc3eba32ef9ea7189674c77bdd80d91 +c05b80b99c92b2a6665092602410791aeacb1109f7f4ba9833ff87f36bbf8724 +c062aa813b7c2669c670de208fe212070f343c2e6fa285d358c4b5363526fb16 +c06334e37418113e50fb7994e7f571540dfece758ed620e2e6fc4afc28b28157 +c067e5a33981925333c6711eda40e3fa0ce8be6b11b48da8d359eadca46115e5 +c0692723895b4a1eda0b5c82fa8c53dc1f78abcb31cea6eb3874ecdf1f66299f +c07aaa6d04f5e55da7aae9205ad12651a6d4a812bb33251003249ee3ce28b49d +c09216e9b583a2ae677f18152af39063bbd9b1a32d44a244bc3e6e3a3efbc445 +c0984724549da8ff450a345f615e1114a08674298753e364b60e8ba95cd9d9df +c0a0c39300a268efcdeef375616c26a09fbb3bc132d338d20c59c27dd492aa65 +c0b37eb283dad3b49aa8b76fc700ff08de87296f5d5dee223e1490ae5888e693 +c0b7c6d3c75eee3d65472729316ab0a155af883a84d909c5fe3af3f798cb1b9d +c0cbf6fd5b6b6b08e6c3dea0f7211a7c644a59c0860cb4643a24e6d34719e48a +c0cfc210fa245a58bafd58b9453553cc87feaf5cca5a819a72802acce411d6fc +c0dabbd504fd3ebcd41508992a7e78ae1d0556b389eeca8a0c667fcf4f221df7 +c0f0921c67a87320f58aa2ec19fdce362c846b11f560a9fd2fb68c6a0fe13e84 +c0f2aad84dc178513ea7c139f84a7533176a65b225e8b4e63dadd6db46b0da7e +c0f7ee6c3974e1a805ceeccff2f550368b8894891534e7fae80a7a5c1610d61e +c0feed282be7b138abeb68b9b9c8c9388dbed3b5df1b55710661248a805c2e1b +c0ff501a6106ae3f0b90f2907b3f12398691a044d84280e61bbf875b9629f6b7 +c1080e633cdb83f33561a9487def92a5bb942d8aa19690770a3a35ecd865919c +c110e140e61098152fddbd6e808570a3e821aeba2258c15cc9d2ef41ab9d01bc +c1117cdd75f263b9605f988568c17ee9634a737ba133512be459f9c412266d17 +c1123ae7be4eea177d6e11ae7b7cc1b2aa7e67e9e9bbd2837eaf0a53326f9d52 +c124f5f9cd72b966b2f7a88508e5bbb5bfd8d8c24ddf6073695c5a5d638c8646 +c129d2cf6bf4eb55dc6afe3645eb50a0ba0a0c3a8407fc214492cd4e82db082c +c12b139d37e10beee15be82cf8c9c4362c492071b79a877e4b0c62acf00dcc5c +c13d59caee6c1b790f12ba3007d9dbf0fa4551f413b7555961237906431af0ff +c143bd4b4335bbac1b90fc88f6b3acdf9a54c4e27999c631ad76a62d1e907749 +c15af218494e9258ff5e5a3f6e766cdcabc058e5112355b3e0c17b52075ccd83 +c1635a41dc1c6a1f82bbe64142481ed5eef08542fdb32eb19328e4f540f8dd27 +c16c6f4bf728693f5f92c6f54619e9363591a9b2cc10e47a73f18c63e3d166a3 +c180cc825b6241f11d7786c3035b6eba1362ab34f1cd2ac65f1f862604ff15ad +c185285c4849b301e095fe63ca25185fb9742a355f08c474b3189fdd71991586 +c198ef2fadfc135599fc2ff85860e8376761436bf7d7295e00f70a1144d6bee6 +c1a585abcca9ca03ffe2a99695e4c975384ded45dda2783678476ad3d3e36a85 +c1b03e01d96b36566a1583d3571b18d973495a7bd1231512827c884ba2c23b0c +c1b2e348b9045f2f2b6fe70cdc55d94b25d246abe0ad2ba34bb88c23995494f4 +c1ba2d9a637fc6d09930698352f06e36f7d57322299151da435e68c623e57a3f +c1c2435b5b0598ae0add505145dd82e506f0cd2a4dc9cb44057a03ee5f3f981c +c1c57fd0d0522359fb3b9f622a8307b23c7369a4a7d0a5d83c2a71feb7ebdcb5 +c1c7aec8f65c3c55af11e400508a6add9b070c974f605f31dd80f56ccda3dad2 +c1c9d2d21acae1a6bcd5baf935f5452995087c3e5e6d05a7d7fb80e853fa6668 +c1c9fa8c865df3d8e3891603483cc46fc2d82f35efbc57bf78529c1a9e97da31 +c1d5d4292acc3780fd313609209a9c07a5dcc30c90d060c61deeb213db425c38 +c1e151dedecc9b3bc277d6b41143cdeeb58bc3ab067f94987c95f91549c27623 +c1e8d9204c66df0a2d1e4e8c614c811b747f4dc336fb65b866b4c42282b22b3c +c1f48416f8aeb9b060d42a50fd1808c59ec927b0f76890d606aaa59bdaaf7662 +c1f6ff65c137c66ec7fe4848ec83dea3a4af0de8be8de1227ddbed9440977e38 +c1f92d0276b6a3b379bc4a5f10c1c01be486c22994521d1c8d9a1838f72a6714 +c217ce4df0c70f9e74022e1f4dbcf5311648741ffefaf6ed49b42513a282245d +c217eaf424e82259332cc9f735297205d216580e7269fc75a1a96a6e243438d5 +c21bd98713fe2ade6a2e7b7dbb273b02e99f4c04b3f10823f0ed04ab7410f8bb +c22cf3960c084440b22c49809177144d4c6811d99ffe4d92ae63d2f4bb917737 +c232150b2c0faa38708a63010e0bd713fd80a7e16881b2a72b4a9a91ad654d8e +c25709b17eda01d25e2f5e1bb426f2bac4a35be40f4963f1549a9c67189bffbe +c2573a8c66ef0683ad986a2f704cac1308b811c513dd723a06f673a893065771 +c25bac5f3f7e98f0ab7abf1bfd59f5e66501ac9b1631cc18547b25a4af1dd656 +c25ed4010d851fde795135f2e659e59e973190147f01c3bab5e8758ea870e953 +c25ee1b04c968f82ba8da833be820a69bd03714eabf96cb3ee685375192608b1 +c266caa1298354042555c56410afd4e1e5c61eb4eff1510a02095a1bdb5f3b9d +c267cd5ed60482793513bb8314f6edde5ab1e3d99132cc69827550beeb3c5cd9 +c26c88c109f65a98c573bfae283ff68b001e1d9df9dc0473d441fa3d22739d89 +c27c5a65bde76dac5abfac8eaaf7837729a872d4c66f910fe65a20f2ee78bc16 +c27e97d9f04ba88aa86d6b78c21bf80123d98b6ebfa7f3dc8e9f9c04b882a7c9 +c283b30b5919f220009f2b6909c06011ec966642c52a045ea03d32e9bf09e9d4 +c28bfb0e757c42b5f5b52a0fa9e74547d0bf8395e050a7f90c027c56b465ce26 +c28cf133e257ef67c7d7be7012ac5ad659dd019e30427c4070465c3b1ea1a02d +c28e761ec954fcfd6a57706795487730a15b35a44ce67e21b2a41935f6c4bf94 +c28ea2d08b57fc5ae4acdd2920b34defb249b4e0719902a36752279422b28f74 +c292b3ff3c042c748e913f90f82bd452539ab22399d0651dd074a7d1c5e3364f +c2933c259ca8e0398ce96213abe78ad71027ff40f283880d43d0345fddc28b62 +c29386bc3494a459ad6e21ae3d3ba3d079f9c14725007a60fa9814b91fcad628 +c2aa0085ffdb0ae4ead485bfad97f80aff00ee1c01272dec5288c15db54fb786 +c2be405b8f7e4ad115e37b9b3a840eeb3588ae887cbf4d1e65bdfc8462bf397c +c2c0b766fa03804c1ea30a775174c789b6f7a55a74b801c6a82c655afe609dd1 +c2c4bce3d17b88de0cfaa1f13e30c099cce009f5de1f3fc4f175809b47461161 +c2d8a7f2db2600db67d4afcd08079b65a6b523db810e370a239369f8b075c792 +c2deb4bea61fb976d0a739b22bfbc01796cf00d8c27b8bffb45abcba44289a86 +c2e097972625d86713d10ae93c3d02d24cb142477d8bb3c62a2ae2d8d948a313 +c2e3c333204993c214bf62ace80c9ae304df850b108f2240956e2b05319be206 +c2e3de2705571a4752a8aed72b6aa59fa01dcee5b83dfa82e0101dfea3a05a5d +c2ee31026d9d4ff851f7f5e93b36db3dc57203b5564b4b4dbbbe4bb7253f2935 +c2ee455bcde94419d28d5744b80536df5061b6ca384334a168ce3dcf7e3051b9 +c2f05e77783c5a7be337424f587e63accac797210783ea4bafdb1a5340594611 +c2fd0def08a9c4e2be0ffce17072839dcb73c62c5d60fdfc0045531ee726ad60 +c2ff3e69bea93f5feebc12bdf643998f6fa8a2b54833a47ef88e97d19da6ccad +c2ffb9beabf63b3496cc642cb87b210a601131e1733f0158eced597979a70fb9 +c304319ea6957a85280a21b8bdba77903c5ba164e7c1bbee31c976a304ded63d +c304d6ec9e0c93e37abd898853544eac82cb87bbc743c71fabc9b7c4d94293b3 +c30798faf7cd2023a73a6a5f5a008e7390c99a7fdf5eed4efcda27d2103c89cd +c30da80c1db9c601023e208522e76b26aa36646ef4d19aa42a84e9e07a1c5870 +c31589714549076de8d879530039c97cd5bda35dfbef4dcfe3e7fb84c01af167 +c3290056f77dca461d2128744424b6cd216b3576eb86e9cce221266773ca8173 +c32cc4c433857e8ae2fccbfb0fbf05c4cc287b2c23675ed2d70d190326535955 +c32d325fd4196f11f78fe03ef9f533308a18b98a1364d4279cb48161a2ef4047 +c33b6acab0f27035fd1fa1d79678fe26958c6fd2aad27405df3825f5d5ddd5af +c33d40a49f7b17d29224da0759b6db66ab749c6fb4ee87b60ec7afb791b9008e +c33dcb8d3bcc146ffdf0c03ea10835cb67b454ba38191d10a00b0c7843650c79 +c341f0248a744c3d14de167234eb2b08719f7f009c0f231b2e4c4c805f40bd79 +c34564a1c4a8fd67043ae6d03d626b1f21b50eefc279fcce706ba392a2b083d5 +c34fe8e78ea339a2883d24b51b8bbe3c95943321b60194c8fec013bb5813aa8b +c365a44bd459cabf7055770f9ebae5c0a693a7614ecc92944c3a4fae3ce9ba7a +c367bf38a2870b3a51bf0f927b0ffdc04b2bc9c9bbd8ab1c37179c29ad622452 +c36da11603e572d7021300894110840e8712a068398c57d59f3bae1287552a63 +c3711bd48acc875c9efc2779df1349151fbfc39d322d06114e1bdea3ff387a7f +c374b24fae716658bb1df052bef144231846850a7765a1d8c80676d2323c1327 +c37ca4b35a0a86700525ee360f0332f115495bc2133f9f6d69953ea5c058f48b +c3978d348ddfe6a5c3082582c36f37ed1ab4e80294e9beaf2d08acd2d1acc6c3 +c3a6c340779ee11cd9ae1c278c6095b70cb25d6b43bba3bb0b405802190f98a1 +c3a9035d9a0039af790d9c66998c5cc14bfdec64bc5ed959f7b07b3aaac89c24 +c3b0f2f68c7fa7bed8a2583dd2676288f69ba41a348739ff9a08869362d14ed1 +c3b32c52fc802de0cdfcfb906ffe5ab10ec6e92d42aa9a213166730e070067ee +c3b984b36b56567ae98bd4c3f5e54c5a8b66f85f18e8d3b36a39aad791d77f65 +c3c05bf7c12a550a036b116b18c5a71bc846f7d602d3611f9225615bf46c7781 +c3d11542008cf261d609df1ab4b1a706893b79e5dc23462c7a75eb2c28d7ae7a +c3d72708446cd478bb067f459fa8a3f740b1b1c6d92101e5886e3566b9c000b2 +c3d9c13ec7a9732656a0d39616b2ba043daf1fa4dfbbb73ed154e142163697a2 +c3e6e466e14def4e731c1b51f9eeefc645f4b1e44871981e85677ed573afb413 +c3ea980ba8eacbfba3d5707ee476cab6fd925f8bbe4c288923be35f6eeebf26c +c3f74825c6c0b764c4ee75cebe30f2370c039be90cd87d2243d7327a7cec957e +c3fa496ab94954be418434a86188cc80eebb60ecbb8ae2898b56cd228d495582 +c3fbc3ce474573fed2ed2dcd4e2ed37ccb9eda475763aabf3cf809dcd8efd70e +c3ff22fcdd9307e1b087e4318cfd2be263d6c4d6df821a06174f4034dba7e305 +c402d36f0f3a2dd2965569a5f8cb99e82020d8be2b00e70e16e08042e4138c8b +c40d19e0366d54b8a9824ef7f3777085373f4e3dc5b5ae52350f41c66c6e49f7 +c40f740b719e468deba8c6406c418a13eecdfb1fafda0e4e011540ffadf9ab2c +c41a4df42bb26ccd3c9a663d8decb567bd90e5786ed782f15018a824acf8fc4a +c429eb77ace20f1f565479d1e08ca404680ba9e4db9f665d87b40e4bd3cfeaeb +c433713b70b9dc30c012147b1077614e4c3a3d8ece0e00915479b67ed2d8cf85 +c43623d586e809ccd5d4d12e00b5d08a4123fab69dac5533861e9f549f5c18fb +c437bfc38145d0a366c823d4fe77c07fb91d7c6d11dd06f903da665175236875 +c438d51edb4de50a119b2f37fc15831e7fd2d007efe025779fbbf8ae0c28ab8e +c4562edd4681ba099d8b73676634e2c491364b3ed2d8dd2c5fe2ece805df0fc6 +c4586d1b375eaf4a9cacc65f95c30dfff51dea770a4cf7bafa34636d8b6c0c84 +c45aee7cfe2324fe1a1271bce8b5c2eda77d8fdfbc7e61a6e701963e171fe0e8 +c465d814b032fc7d40dd129e19b528463c0697f95b1c5c3fd8616a96160cb7ed +c47365586c8234698a60ca0cdf83ca29b63d89877bd97e9e8f4ff4b3c812e8d0 +c47f3f671fc7889d02f8f49d3ad941606dc3746ae3a19084762288c15522d7e3 +c48a9e67442a69453a997e4571dc2d211f7092b0661d5abad588fd57324bb530 +c497af0d0a42e08217086c209f2e979792cff48be5c80c640ffac321b785e787 +c49aae504f9dd5f55ee8c5e2fa4b9ad43144754bb6f3308f1e445d840a4cf3e4 +c49b7b56b14060370705440445cfcd2f13c1c158d12980d952940ac3f6818b93 +c4a22f56aeb8ab50d70dd2941fea6823e9244e983c75f6fbd9c46b44261453b6 +c4a26d8303595c82181a569f5087b7e4beb10cdfa0030e84639e2fc0a59e39f4 +c4a691bba1b984578190216746bfa628afe9dee26f2de5b8cbf54d5d67d66e98 +c4b16a1f3b3faddedaa70b5e17a95789e9b0eb06f46571fd2a4cbc4914fa9939 +c4b6487114dac6e504d82d0cd600c0668545b9daacf7215a287eac5c46101a5b +c4bf915f6644bd5321226c299855ad1a422cdea8df11bbe629e5abfbbe4441da +c4c2ad991c146712b52584e11da011f4913f6d3c807ed1ca326b782be91d3249 +c4c3a25397ccf86d1ea4fbcbfaa763268c15f47a5eb77695ebd4d098ad978a27 +c4d6bf55353da3e12f9c15a78d7c7aa5571501c0f3466199fd26fb1c7f6be8d4 +c4dfb01dd24fdd0e3df355a333a0ed7da85f58752dce424ed408ee37f611a42a +c4e502cb637ff36010ae5f517bb4309c8c6abdcf7e717ee821086de00aeffa74 +c5106422bc4e2a7fd1ca029986a97774c447a86b422879115deb12f7abdcb61a +c5148d5ab99556c2b90156f66b957e8a140e89fe8684b99bb3c0a46f6c13edc4 +c51777d6fb11cc7461b52460a931a71716af3e5708bab9d23e6e5dd1d2dba232 +c517e2379955ed7d1887278f04ef13873c59f936025668d92bed6dccd3467201 +c51f7db1b479a7f0181495fd189f04d15fd4ecce5d1c8a3bb1d74f823f8e03f4 +c52b22509a5e2c83962a9d0de0b401ddf3a8c72df99bedf1e27d0083b8b0e4c9 +c52b65ff30c32ac1dcf4a3ee41549e2b91d3044f0ea13429c7e6759884083875 +c530bc503d51e5e9d1b2f40612f80fa629a9903aaba860e93844a0dc4458aa82 +c531a8bda5a3fd14b51f19a931b47178d4ea5f0d2c5eb71c5761761a488fd7d2 +c535d8dc45639c542ba01c455674522149611d8690c04cf0cd6e12a0ef11d5e5 +c535df030feb4c981a82f8710ed915da2f7bc0c41e732ac87e242f3c2a270e2a +c53c2d4a6054d2aca49197e15779a3064e448a35a9dc0908a6558198371c370d +c56402b0b41e50397ac6f3b757006dada64b79017d1da27ff45b374eae88cd7a +c5670f269c0bd6589169a54d0101202973799628f2c27c2328cb476881906a69 +c56b3777b3173d2bf166cf9864ca0e2692beeaa8b3f61d07277198b1d2d96310 +c56f022623c6010232758cbd4124e226e1717d734cc7e152f741e133fc1acd3d +c57f3a07365df8dbc448310b0c074e37e2d8c679ff1ab610a7b9df91d9a8ecfb +c580833c78712ad1c18ef12072d0af77c2b87935d238df49ae1d34be4dcb004a +c58338237caabb11691fafbdf23b2f078b4081f1e8591caeae0e2d688cadf6ff +c589cfbc023e7f7a56f6531146405d2c127564c28b3c18b933cdd8b509e9110b +c5a39aa254678fb9c9da125f1977ef5607310e983917d1bad3f266617980203b +c5a5ce51da9bb849c6148ba2eef280db83c96a31c2cf29ecfca79bf2af83a6d6 +c5a60b02de2b6578184aab58b70434b6cf66d84ef4fecbc98efbd8e6d7add71f +c5a7e507d2b9a50f2f755c57ad321bf8dce38cc993d7d7ca74f9717d29d363c7 +c5ad064c78475d141ee68f1dc4b276fb85daa63d3c3e9d2c3f92f533392ccd78 +c5af22231d9256216091214cd8a5f55cd3441fb86f9f7b044f0d8e06dcae7340 +c5b1423b7e189e94b28267abb9b5fa8ce6fac0949733e26d978d1c09a5b8b759 +c5c5e3bdd978d76f041bbc2f4a45cfca830e514b79aef190aaede0d4af85cd99 +c5cccc6dccf73e102426e3afc73e823667d42e5192e139f2b5fcf0375f9ea57f +c5d0b8eb717682293a4b00281fa5ea46813b59225278546c9b6ad2367d87c99a +c5d48f46c987b05242d9ca62412fe1aed86ffe763ef174a156be19f3ed35262b +c5d78ecadf3a154927c9f1186c21c2fe97974c23075a3a7f8b42b1f79bad725e +c5db6bb53fde1a0efc684316cc1470182972676874b4b954c591b3bee662481d +c5e18daaa71a8e49771b25f324973192dce0250876dba2914ddaf595bd7ac3c3 +c5e942ab47bf7af4469d1600304eb11d3633175323ed45338fc8761c11907c8c +c5ece4f585d337089ec5e579d979a968afd09f957e6b16285120045d785ba342 +c5f671960b105218514796ce45224fc6ce2da5a280faa54dd7ddf15a5beb07b9 +c601dcee185347f39ef66d338e08c702fc5ba6007c25368a6b54f66890790da3 +c608c1f531773a5acfcd44d728d352648c6f4743c0c1d2318f07ef22605de6a3 +c614ab0711ec4984d2ef6a7f945e066c2efe27678e37f555231737a7a40abaf7 +c61cd8f3369bd089749f1aa9a2598e661ee40e574f46032821da432c3a3409fb +c6213c0dbd9daad5a3d138b4f3c3b0c5947c557e649ae5b8c9fe08d7483b430a +c628c7bace1e4f2d1e68324f44709224c953b8b05f30d50fa28991a0eaf441a9 +c62943cd1e13c48e11341c161a248b62e2bca3988808e9c739ebb8115e553938 +c63d29eb6c05dd42ac58672d777f1269b3b842996b2eba604c075f2a15f0a288 +c6517722b3d2c7526a6508155475e4ef48dde0cd2e10e412e201e7f1022cdeaf +c6554ab0fafcc9933510c4688ace29ddc0dd5ba3ae3f51f36f9f058353d47ef9 +c663e6f197f940bc54719563b7b3aaf57cdb115a17a1e9de30e883a937cf7af1 +c66e247a286c0cd926381e02ab0dad2824944bc988fd165d27f669aaea4e1b87 +c672400a44aee864bb58064774c20b4bd962ca4d7b6d4d8e76dcffe573717db4 +c673d13c80285cc981fab0a9e3f753ceb14cbb9206d3621e10bfc92acd641ca3 +c6744a13ba88110b3212f57d162de2f6542b52ecc9aed13a5571fc35eb92fa10 +c677927782963fc060cf010d4f520fd0792ce603afd6a4199ebbe5ac575925cc +c687026d341a11501e2427870bbff4bbdd05c4d69ea468f7705ef6195dbfa639 +c689c3a53a05303d848d21e14576b9f34af79400fac1d36f9dc228fc113f0c0d +c693a29f356c221b709f9934f43d6da96b30e5260856f929d5f2e4630904b2d6 +c693ec3bd752b1b30fb75065eb458a3473720f2f4a21b2ec8a5acf116b8ec5b6 +c694ac4948600365b1f7f3fd6840568670c92b223b291880f457adddec5a4764 +c697ccb68686526c43779cf3d2063fcc76ffd76cd6831108d4d1a3251c69522b +c69857689f7cd9dfd12e66acf99243061d57e85f75d5d6c5656cd1adb2f92b1c +c6988d7342d397be313cb38518266ce1905bb57b0b5b27ff9b5ced896c9d3bd0 +c6a02d28c91ed71cc363f387ba6d95e8b8f9d5311e66bbd2d52090786d93a539 +c6a38ce24a9d1a801495a9449572431038d23a43c8de7320c6d2b280e5153d0e +c6aff63ad969b5d30075989e8edac515d2003af204948363bc63046c2115e24c +c6befdb85429747dc73ab0012a41799a0c0d7ec90206878b28d6c980e1597681 +c6c6657d79e549e55055cbb6ff3b0d18f65754526f7ff0409ba867db5f0bd0f1 +c6c8adb52f67d6bd3dbfa24891dbbb3d529decf93da2b0530573f9cbc9456cf3 +c6d0346aeb444e89e6ad0471ffded541ac5d6ce74d6d61ec945c79b99d31cc3c +c6db826613f1a643274cbc3a616e6bba78b3d2ccb93d7206b3fa135d95dc4a51 +c6e0e23d1e5a28aa78fc42b9f5818ef0e3f955641f321b6000d007532f7d4366 +c6eb9083b04e8ba058d2c51c9345592d364f47bf6640413d0e698e976c0acb3a +c6f32bb8811ad9530a1f8019482361e3ed399f49f77107c5a76420284cf7b734 +c6f480854def9357960dc6a3fb1223b73ad07e6a71627841ad71e4ada4713baf +c6f50ba7485fd50cf39bb0dc6146a72dd8bf546d89f0c571e68b4599e880a6a5 +c6fb800edb43fc29887604ebad38d4776099dbcbf0c91833e0cca4f3e2dcc3f7 +c6fe5944e2f6709c2bcd0e16658b2963fd1aa56dd40485439daaebc2f401a2f6 +c7001071aebbbe2c959aa991638e74e88ad586e5f2ad1067bd0d4a5b333a3b5d +c7019e71f62e560adad72051b1656ffb65067795506f7727e25876dd0b51f92d +c70d0354a6a19a417fae9f98e52d2a3fcffc32e73f7eec01b215857c8b69312b +c71345c6b7170620556ab227767a47234bb09ad74dade4ffd15458f39531925a +c719eaf14d93445ef16c959e7b130aad1842d0001f0d21f8c757b32bb09786b6 +c71c26e8981c0f65df9e2d093ad85566c08361198ce6b5006490e9ab93c2b1bb +c723c716a2a68d701b16c30683241508d43fbefe09be77d1c09edd1abac1855a +c725657ace22015787ec8673e2444640306f90ba87ef455172837c0a8efec601 +c726969bd74f11a8fd7cc93d35ecba3a27f5b38d2e52abe582817a071d3d0463 +c7377f6f9cbff4fa0efb9b73d1989906e4884b91fec4fda8500727c019d83cb3 +c748fa32a28ac1be1d6248b56568223b7e90002785d52c2defdd1b5c3619ac66 +c75e9cfa4667a25752cfbdd8cb4b650e9aa38651186b48d8eb61ba144eeaf845 +c777a35cd8a207b56c4b98104ec2784eab76b1db17149664e4253977670f1f3e +c77bf538c0b6c3ac3641b174a422e3c063e138688089c3ed807a8693b217cf74 +c784123e6783f7a8020d88185e6f7332fa85be2fab96b2312bcb58702eccb149 +c786d575950b0c9f06cb016faebb5d1b1bcd1f1da21f1bb57aff1a16e62447e7 +c789229335724ae47ab3c18860abd5fe1dfeb72061c3689527c96415e109f946 +c79715015c5aa89a1c0cf84887ba5b978584c63053b05bd98d6102c876f3759b +c7998e35204fe6c816fe4d5ef25c12c4bca1931d5a977e6ed51284105878fb2a +c79dccd698ed707e84c5a2f10715404d8938195591effd2c1acc4e731a68f789 +c7a877330ff3ada05103ee3af1cb4091969e6478cd9567ea1d2632c756850b01 +c7aa55d116e2687abd67b13bcd40076fcd5784840fc8dd282be4b9aa636cb63d +c7ab7f6df04d845196d7e36d46eccf8dc4e63f8881d89dbee9baa7a4b048b265 +c7b015d63f71d0802c585fe665b244bf1e5b41e97124a61fcc083bf9573a51d6 +c7b1b270743e3c6529ccae40ad4431f59a6b1bde4355774906bf6f054d45ebb9 +c7c2face5c077c9889964ffe96d05336bcc878ac96be1b2c5efa6e3cb245e2c2 +c7c74026851ae75cd99323da2bfcf918147f5b10f414789528d2a2e4c467ca00 +c7c80018fc084d2246ae3d088051ec1b674ef01eca0d7db884a9dc3540c5887c +c7cdc931fd688b50a79f09d91649e7a5fcc3dcb7d82d3b66395b1e522fb8d2a8 +c7d9752106e05861b3a2a99fd6b2a7a862028df215502d5d58ab55284071d01f +c7dd2182f1ef745df90de23c0386cd3476ac98c48674dd7a82e472170f4a1f00 +c7ea5c0bb44a1b7e3520b52af271fd8692274e30824c8bd279baf339f3651a8c +c7ed1d56e9ca1f549ab41aecda012a2f4a46eefdae904f12b87066d975045211 +c7f3312fac17621bb7423bf40934d380a1f158da9136609ec7ffd4537061b01e +c7f794025146651e6bb704cfdeb4bdae812f26c0c32a04e209fbcdee06b4239c +c7f8f7f1714bb0beae67bd69ce14fc8340f7f51bea7139117e2c8494cea711d2 +c7fb05a25fc913a608060e5dde2bcf0c06d3f3fe1e6291e1020c5e0b34c5ddaf +c7fe69bd3b13dd61248ef9e998381230d7c5508b6a38ccb1f8086d37a2aff48b +c8173bf30a4f98ca9757182ccaaf796380d81a97f97e0a3b0e7fd80c1180bc57 +c81cfbfd1cd1b69358f51609d1591b47bc181ab0e0929e9d8899574b888fd7c1 +c81e3eb2bf8edef1dc62c54f22039be333498dec6aacb8af71016b6104699050 +c82d4ad21615c351c228230c9c111191d21fb4245fb22efdb0a2725ca82a8765 +c8403ed0f7dda91fbf866093aacc975e4116f965b2693e3cb9a86bf3bf4fdbf2 +c845ce2e67d58d944b68ae313fdf47773cc60776fa4183aa3477633e11e2252d +c85d5baf711709e6f50385713814537c95eea5d57ff24a41a250289f78138358 +c85e2d5f190633ee5c1a6ce992589a631aa7961a195030e86c3c242165e38b9f +c8678d3343942915756abd34f11c6e2db3341c1faf3ddd25093a362ae2ee125b +c86a58e5cd03635f2c0fad92abce57ef5b988b3fc9e97fb797c7498f676c9037 +c86deaecd8ce50dbb2a8f4a9173300ff49bc264ba0bcd5bff4584e725ab92d5c +c8701dc8405ebf961a186ddb27a09dc6f13ae63f132d21d595e5d27366a74fd8 +c8775af2efa54481aa54867d955a5e33c14ad7c2ba29e3f0b9a036c629f9b7c5 +c882d9abab581edc1f7a79deb087343386fd1ecd7df812b805e83962fb1a89ed +c88c9f31171c73a4cd20e5c365823e51ebd4eec481cc6a34a8ff929cdc818fa5 +c8913be4e8997e1fb488ffee4e2b39933bb12545197b4609ac9969869626e52d +c893434203b27143875c120c4774eb3ab154ce5086cbb18e2f784fed5686b122 +c89e4c04e8a00acdd06e7746249569299f400b6aef42a702e31b7a2064b049fe +c8a5dbae4014df207ee8f957da98cf632bf5e9f48350f42238ea82689d382687 +c8ad605444e89abc492d1bc9f4ae426e4d9ed9542262801c88366a355a918a36 +c8b627d0f76fadd5e55c723d644e760c80b33488fd5f74afd6d121b48785454c +c8c324a517c7383024e7e3e814b7481664aa85a06d3822fafa758dba13993c9c +c8c81f859a135087451e52e8b5285e14640c57640a3b082f07573114ac7abf94 +c8cbfbf4121e79caa98d83a76a97050e900815218778d52b2bf2e35bd61463fd +c8d3039611136d3ee6ce1faf5e35eb0bb5147de8cf369f539311c181cdab4954 +c8eb42cdb909e5c0db6bb84af48e4399d2a07530566d6b28c523cc11a5425311 +c8ec535f10050434bcca85de676c47152cae7b494759cd1f3cefac2b9c5c3ad8 +c9027b9660c741f80a3718e7fb5b446ea25bdd22e2b49432eca92ba8f5958c6a +c90df76b21cad3ed49724801d3d8d6eba574934e7f5cf9910c5ade8ee25aca5d +c91927d8caacc849b454587fbc0607e2d19d849be145e6f364674adf15fed01d +c925c4dd3700d366e50cd0bb724cb341d581f5e8ea0534180156a7356c64afca +c92819a4f9e58335027afaf8bc35dcbed88c3c2aa4e71185b7a163f22f076a17 +c93c849c5f4bf810150368e8c038726b589ea7bf1f718dac7f1641e9dada917b +c93d34e05ab3383ee31e388dacb6b934ce5da45fabdbc9ddc31483304f7304ef +c9462664997b60a79d2fb866fe67bdf7c8e0b22c2597e58e390aaf6b1431b68c +c960f37b2e42c98b0ab787b0fcbcec6f46a252f6f061d42d69de2bb5e5c813fd +c96609f63123fa08d6247d3c1baf7b983e4a8317282f1f12416be006408f1096 +c98fe794c0921d39e28711d177f3a632cab6eabafe25f51eb48da4da26664b6a +c9925a2393a617cb496dac8557b2b3b18beb116ac2b3a8239004596482ecb441 +c99ccc6e7fdd6c2053a8f71371abcbf59b5b09e3e0a9ba9e1c675c6020b1de9a +c99f46d66cd4f63a8754c32c9661f0282643d5ceee8e156503b89392266136bf +c9b10c21779e12190f0c150de7aaa4fe8758a3013599e56d6b7ec8db17d97a00 +c9cabcc9b78fc04196720c737c826478218af50a6dbdfc877ab994c28380b12b +c9ceeb4977df35f40dd6cf27b60e8ddd75eb8344e2f5b088beb37890af361208 +c9d040bf4d263ba0168a53cdb8e49e8507e503592b7d8a65bcd870172dd3cc18 +c9e1175412f46f13c43352b4709e3db756281456c951bdc6793c5761ee832ce5 +c9e4079b4d3f3991e223e01cf79f77aa4a83bc6f43bdfc480efb952ee41b8aff +c9e76dbcd7f5cafe2fc0ee8db293a7c5254d2cbe56d4fe7cff8ac331492fa1aa +c9f425a59ded0fe16820c6688dddb2da14e3bed6d22fa7a060950baa4f748ccc +c9f758ee95dcd6a870794c04189ef5812114c5419ff238621c0e1dbe6719eeda +c9f7bc4c511de073dbc1050d9315faafe3bd27272955b7b1247f6f5662a1e227 +ca0c4d16bc95d6625e91dffe6a9b5d3c7b0aedf8774bd593bd793c4d656aac6d +ca12ee4b68f1c2929ac96143fe3012e2fd9d66306a2ef66c870fbd030238e437 +ca26db57baa233f7f26502ac132afdfe809928e433394518189c18c4db25a088 +ca296cd60521ee467f260f1f608316a4776a51dc525473ecb226e9d1f51546fa +ca352ea77b19eb477109875b3a94067a61b8a62785e70a5e943af206c4a76600 +ca3e1fe7f255e58703911ab60569d327f1eb177943093ae2be0be335755e8274 +ca446154688011ba24fb3b659f56e386f21ef4391469ecc3343f06541c6b9482 +ca50248e61d8ae0cd78d031052ea9d3cb4f4f8eb1f82a4e98624e63837f4e09e +ca52f3cebb7471e634c946e82009970a6e6f302f74b3d4c600238ba96f8cc73b +ca540da66d833782d083731fd5088c11930cbf2946af3c0921cd4c6608ad22da +ca557d7c7df7137a65ec12134e595cd1e9c43d8901516e4ddaf620d69900d91d +ca629ee783d9aaa6b6d3223fec5d875a5d8887cc7004bda298711011288e014a +ca64313be19481ba3092772ce4de5543362e20c363d772211d5d433a5b5c2ceb +ca6d3837771fe06dd208e925b648609e1f266313e1bb6d5cfdbb859992a1787d +ca70633db93d365e5932a96a6e293910cd069ddacf67388796e8eb1579cca6db +ca7108309884c11e370d457d690a48e35cc8608f5ad98bdb8a36cdd134368ec4 +ca72cb3cfa949fe52e54f8fbe319ecbef2cce6ca8a9ed07733ab1a562c2eee20 +ca7a592939f10233b3fe9e84a7f60f47a8804fd3b916d8c820e3ee0bc75f2ce7 +ca7aab4d0dbfdfeea4e03d69e5ed30590d548ff5e023dc67d81cac963f436fff +ca8054d8afbac4cc560e8496dbc7fd27d1d6562b00843a6eb717dbb9369db6be +ca8d1af0b808046c10e0587f6eb4c583016b5f03a145e34da11798632913885e +ca97fdf6f1d0deea6e72694f40262b3399aeab93ed8c59fc205d5b8db3d48684 +cab32c3a6f5d5e7e7ff6142609745f7bd7f9e5751a42157ded013151482c6473 +cab3ba1bbecbb8adc50e235e9ca005bfd49306fd84aec7d7d28c25b1dc87ffff +cab8a6b9bd3751b48691b290382bdf3b854265a3731650cfb7b9eed7d5e83130 +cabe210c69f369ed1dd66d5f6424fb704fcb0fd15b9afeb60659d70344f7a9e4 +cac25ac356c53f95de0343ebeeb11d7d0a5a49a6ecd03d71be7ac7b82d3896fd +cacd862469f322c4c829534e05ce15e5bcb0a99025366c3189af49e41a2ad947 +cacfdf52304337cd5ded84d09ae98c3e2fc3fa4dfbe67e7e9a95d1fc35a89dde +cad8d48ae8c7a72dbf6cf15721b593711de8dc464412f64b240ae6dc704f84be +cad8f4a6b20afb0059bf7af5dd30f65e38640f87022e849f892bb82ef2c4c722 +cadb7d18ce6816b0d435e781f9444ecb4631b64a0cdfa2f169852a5f9da81a89 +cadef24c47a16973529d632717e75e4af58cea8277e8da82b9c269e68893dbfb +cae0b90bf3b3d3708635edea51112830b2b18fbffcfce32121b808fed4253301 +caede997bf0c1db03b3271680a01628a11b2cece33f017f1971dd09805159f89 +cafe2d27bb352bc7ca1f4de544e207a141b939de2c85237dd59d06de689427a4 +cb0b0d9da61976d4e1a4ed57e0b52e140a741c4ca0b53ec227d4481886ddfbae +cb0f1bca151545a639d63856c1aa9b7d0a6eee8a1c015639dd37a2809321fcd6 +cb100b7dc02eab8ef8a6bb85ff9e52e64ed5224ba498bbdc71c9312c139113b4 +cb1d41c94d5344ca09cd8171e824368e895f956bc16ff3140f009253244e5887 +cb1d8d8d8ee8793bbe2459c1d2af86664d55bf14dc87fdef714a3cfa645addd5 +cb2a1cc5087d7268b948b8b39910d76efd655fe49bc3764a533d7b9e162200ad +cb2d298d502fe1026ca5e065600f08a993c92997ac45eb6d2d4c0ecd2cd7dcd3 +cb46581379f744badedec79c06a66baeba296990146882925cecbdf0910b367e +cb4931484d4077f7ce71447d10bac23e6bd9a5b69db5a08ccddac782dcdc20ab +cb4df9f46cebbb2046c3e9572d0773b91dc4a801c693d9501a31607eb865d161 +cb53015ed327a825970a03838fffa9a2dee360bafd0c46a3986e2d2ae82e481f +cb76a2db7e5295bbe60ed4a90baa8b4fc634eea29bd6f5e3548a1bf4c9894641 +cb81d4c1813f0af175a04db4a3def0cb5e81aa9c137b85d9242e70fb60fcfb53 +cb830bb73e8d8273e26177c1fafb8ae9bde1614ff17716bfc3c33e84e66878e3 +cb859ed9cc89750a0cdf974d50ed81a49168d65f9248dfd072dd800440ee8934 +cb8e26ed04954106989280d1d9c39aaf4f94f0048099e8cec9f337a7bf1a8dfd +cb927fd23d9c9c61c224ac025c5c1da644f118be5e34fa2587569879c09d785f +cbae248809b16fbbc1e418930d6e2e09a6e8330700fe7759b086418ed63a17c7 +cbaff4afa798934e35e85021b947c67c75460bced347be68d79359b46cc89233 +cbb4cccc7a811cee35c8f61517aac3f7957a8e3a41081f1c49cffa2f44d890cc +cbbcb6055dd6ff42fc92a8cd5f8753c4105b069d2c0ff989e2691720bdc6cde5 +cbbe94e5b909562176ae91539929ba0dafb00a222b325cbbda020b3379693d71 +cbbea8840a87478db72e6243767f9065f42316ca5f7ef9d6641f104ee9e32713 +cbd7931b762a56523d3ab44708df5341b93b31142fcd9d81a47df309f7f4df42 +cbf70f6aeaf0a61eb0f6b5a759ca08853b7f8cb7fc1939906fffeaffbfbe6b2e +cbf7e68b9f0579f868a045967ddf80457b87289d14a2009beb6cd62efade0701 +cbf840de606ec45738f270a9cc385611ccaf5edd3d20edbdf4b9a32f082141c3 +cbf9247608bb606454f6022c389e6ec61c7733ad36109c864bba92d910c7eca3 +cc05a49a15b7ad604973cfc667b7a3acf63555a8be2805f8e703ba8f5cbb1f5c +cc160193365769054c57c829e7cdc2e81bd0f7f03e96510264211b8f43724278 +cc22a913bfdf38fd423e42fa2be608fdee71f58b5fc7802768d1ee3c1f904939 +cc3216ceccaaa47b2f849e72e2cfa2b212202215abf8072b68ef3ec280c56d23 +cc42727b2c9c636888f924a151483380a44f72e585bf889cb39ebad634598437 +cc43b59d99a5ec46c6dbe72667d6867f9042b9b3aaf884c402ee5166616abb26 +cc455ee35a787e8b1e6a53df612a1368c5bbc13a61e9758cbf926a56fdeb94be +cc515faaeeab6f0fe6d55bd824efa9d016b9fcc69b4789d6709fdfd829841363 +cc55386e287515b96f09bf7496a785cf5d92b6f5f5fe553530b53dffb445a8f8 +cc5bbc6d0da8d83f597a77ca95fcdd4e93c17d1d00f903a097dbede7903f6d6f +cc5ef4f0705d4f00cd4a767758bc7f2bd89516945cee3f7df893e57f65f3d320 +cc626b15a145f9c5d37e2798683875e858fe4968f876d98e47528766418ba31b +cc6cb4391938ad38c8230fd1f83f62bbf5bd3a4c14b0f13571e03df9d3e98705 +cc78d4910e19d47664665443e2bd8854204f6ccd51639c02a3985b99a9cad611 +cc7b802288751c828dd0f77d0dac6d481671658dbc53ba62bcaaf9be9c6b241b +cc8364b194d22ab82ee342831a9cd37975b4b70a7e12758c576aef3706066000 +cc8662ea7b055c552b093a24dcea0c90fe72d27f1f4e6b82481a9b4c13dd5af0 +cc918893a9125c33b32891b436778544cddbda010885064782934ebf65a87263 +cc98b6ca89732e70b065a8f9b6cbfcc72973b233d9402c0181a30985752ed2e3 +cca747f521c9bfa023a58dff8c16738d9758aab98da5cf2e571f3874dbacf4cc +ccb51b0bfe1d09dc21f123fbc5b5ee599b444e89f48961c6d068472464c9d96c +ccdb9b53933481eb99ec13750c478ee65f7164a83115aff2b8060fb12800b216 +ccdd81b9cbbbb7598c64797d0b2c5c103e6114e5ba545fe6934d1488bef2f134 +cce3ca21b84eaf23bf0f2bb9aaeddbdb281251cb50acc062ba5bbf25a771e5dd +ccf7ddfa87bc2a8ad774e9e7d64011275f5a3c8be7855ed11baa522925a36224 +ccf9a42d3359d1a890381e562c6693e61e1a6baac68c131d172f551d9e98e206 +cd026cab0e012c9871a45279f369abd2d9dc185a98350c6972cce37911b6ed55 +cd029fff26883c46ebc9ae5cffbd53b420d1617458b8f6f62475be908625fedc +cd16f04c66f12329981ebc3e0e3565e9602f7797072b6b50d75522e5b8270a9d +cd2f47ca41a81ace5dc5be9ff08f042a5a6c653138be311fdb7bd34c5b7cd502 +cd37f0c22e62d1e7ec4d1748596ed6186c54b6de4efeb54777506cc75690db83 +cd392b0830ff29a09bc1f3c376bbd206895d0bf09ff534b679a2b3451daa08e0 +cd459756149b5030c79a66abc9c8631bb45617ab206543e17188ece3fc4298fb +cd4ad31e2e7bf7200e17d34f26b22b8d7bae8b4950931230e3da11e6e1b82c16 +cd4e6307bdac44bc7a6ba437f1b73bd97d97ca709499fbd9db6f889324263557 +cd54b9ac483fb05ce385bff39720ddb72f202d1f3e4264c7218510f41f9f61ef +cd68460544d5ae1e34bd6a9c3b12166ae42785c7714e829eba6af3c9d38d4b25 +cd6b481ddb831a18edf84a2e01696afa232681775c244301e890ba3a1dd042d4 +cd6c119a44a647652172b2d1f0bcb2ae1f280643af94fd0ddbd63f31f50a1fb6 +cd773590d44a2db9a6d86a91267966baedc6172fab992ee57b58d92dc186e644 +cd7e81a0f82a0d2eca840d09b7c816d2d94d25cfca2a3fafd4726b8c20e223b0 +cd8654da61224cf599fcbfa5bb8d08d9601ee7c96152b3f5a25baed004b5b24a +cd893819d51bab550f4e7cce44ed2872f6f7a62fcd7685dec9212e3b308e1689 +cd99f8588818325169334316e42160de0190c3d48aeb1db3edb9dae6a5d8fbc4 +cd9d011c7e7a249a634907be24f3973a1f2bcc8b0772cb5090c899a20608ac64 +cd9dbd569f759aaae24acf6eee5207fc8b4deda2eb0bd263811fa85f97b9e31c +cdb4d85b9b53363d73406a943028984f6cb879af88871149c5eedebfaa8ff945 +cdb6afaf6131633aa1be8c1100b802e01934add113e955531a71ea853bea75db +cdc2acf281d74cc61e84a59c59ac067aeff33e4aeb5773bb35a923e9a01ecb79 +cdc4cc68e676db421207971e341e6ac53a175860fc020bfa433f6c04f4c9d9e3 +cdcf1f6e89ab1eb71bfba29eee6e650b675b8268f1316b6c3d6d36c3044e154e +cdd2d825910d2ebaeeeb79b06ee80971dfb03e71ba5643e6ad6014de491fd645 +cdd816c2d9ccd4ff94301f363032d59f0c6fdd38969a3e90216a82e9aa14ced0 +cde27db34ce3f7f2203519fcb8d82d88645afbda2ac2035498e32d044528981b +cde6e12435d51f18d713c2262b848649abd9995fd004a2999ff91da043b91455 +cdee593568477f13230193caf838cfa187b037ae7b1fcdaea9525a9592c3a0dd +cdf5cdd72dd4dacdb4bea2fe98276eea8d972a216667b967931227da9a6ee620 +cdfd5d53d9cd4684a7fae8ee2b0e1a5e88eddb813dbf1bd6f3301baee2788fa2 +ce00216c6e9afb7febf26c69b441f237338f5379bf29b089a54608cfdf63dd5f +ce1fe14f7d954dad829cb243be1727356d047a8dfea47c230a673c0a4be1ae38 +ce2fcaaf2138bbac507e990e7b96ffb906999f665591ccb8a7490645ecfd6d12 +ce3f67bf2a3ab12c48c40091606c0ddc4c29c5aefef4ff7298573d485d314c97 +ce4f47eb63ed791883f97f9336bf274d53ebedeb21f646cdd516d29bb4d78966 +ce5c4f9e2355bad50ff4bf8f3e48b27567cf286ca47fb8e53bb3e6e3a5ee9df0 +ce5fa1b63d82f4697c0cd85280d526faf2a3dde12a3ae86979f6904258d68cf5 +ce643f977c75d033d709a822cc60bf91b3c8c71fcda78b63423c512c1f34b074 +ce68fa1c5d2d63c8071eeef19e505bdc6bbd89e19963d132e6d322f1b3a02692 +ce777fb091453887ce79cbc72da4681d23ff68c35a2a79bf12ba58641681501e +ce7f8732c48f1664d0a5e9f840e3abea575f088cb6b463c8882111b64c3c944e +ce84031bae32726c3c8707052ef5c0d5513be6e33013a1d3e7582d0c51aac77a +ce846949335e885dd180abe4fa32ccea837215395ce66bb78503ea0b51855b46 +ce9a530898bc9d88a0a6efd8d2ff83b4ef0adf68c28edcf11c8fce520141c0c6 +ce9a8da38a304479a77b3d18d6f0cc6accf5b4404916537ef4ab37fe04d3649a +ceb7dc754edb50d6f03e1a3dd491cd770fdba6704881743f1d29923349765b2f +cecd20828487500e495bc82cd43b7b4222e81424d9712b6351a0f41b3243e689 +ced6dce15c090ce16a673f46c0d5a2fd36fc28da8530ff0ae3f3de0a1d8598b8 +ceea8d0d58cf8d24c1eaf8fefdf5bf75fad9020be5576c73f4e9e71de6995cbc +ceeb246bef57838bcaab217e773e0a1f65fbe73ff7e654a10341e4229bda7e12 +cef3686bbf96841ba9dc34f3c0b786ba3613f4de57e4274d4b78710d6aff1ee7 +cf0d0226513705de05fd08cae3c6d29d7bdfc473291c68e7ed73ab2491bf03da +cf0ed6da8ec581a093bb55401c65ae27147e6ea0aa7e35da268e31c09b1bfcd3 +cf1de5675fe016d5daa32c3159291df1a81beb5b0e739e7efe0dec07da679625 +cf1ee1a6405255642f45a73852632443b4c13247db76069a99c1b58049b82c8f +cf20cecf4a289203eeb75e988d43b5ca1dd1244eb30763e5eada67935b2d754b +cf2c0b1d4c45f7def3e3928daf159159d1332ee5a4b82e7d95baf2e254575517 +cf3c8c5feb57a919ae2705f01ca5790b5962ede1f7efb161847786c446c4392f +cf54a8cd68772558c67678e061a0a5447f944c7db7269785d91e2683b27cc28b +cf5b296a47e579f639e0a13dfcbfa9f8df29e8ad04b0cea01b5990e746eff9b5 +cf5b96b5520e78f758d649982fac5b53968e20e95e00bd89db6438fa85e7df80 +cf648c8ea59212f0679eb975f096931ad043dc5a8bf1c95cfff0eb83a79ece6d +cf64ab0da866516ca2d0c6a4086f6054094fccff59bcac17b98473679105defc +cf6dac7825602b1b15c8f4c9c9becf5a46cb7da2523e4e6c62f35d02e72b4a11 +cf7515b6bb258693fb2fedd81813d04fcd11cd437b75b7ced3ab4ea9ea9378b7 +cf775a6e374d97b890647f1c4b214f71dbbe9493e1b4a5104c913e905fb98fae +cf88fa481f08f9b17f144f6a169228ce913219cb0e93e67ebcfe0fcd2911be75 +cf894a9f8b3294864ddf528809ba1b0c10da26efbb07dc21e10742ddfce295d9 +cf8c7efd7ee145bc97a71b7f39fa4c1073073b4651a728dca2d9d825c246fb8d +cf8f1bf780c93cae3d6ebca97bebfbe84078dfc2db0b0375ad5fb1641b98fa13 +cf90ff32976faf7fa5d20d1689619087332c578e7187cd482f00c2e19c7ec688 +cf92313057dda268b8e0576bdb1e5d22c485c366b327f7617579e565dceb0ddb +cf9b0718a7c346b7314e12356d540fdab1bb22eeff756d95c044773e78d898c2 +cf9d5f78ae30c80e2f32913a7e8b6f02cd5387b3333d036e592d961f4ef77109 +cfa1a06146e281e7e946c9097574a21eb8d89fc145611f43fb2de61b89ea73e9 +cfb13dd2b5246b142c24124e8ba4565bbcd8bec6ae471a6091c8bdb92f3c1c5d +cfb20198d2cb93457a1907e83872a4de7917d20a3b11967d321d05cfaab2097a +cfb6002995f46b1daa13e2a23de1e29fb6dc2b2ed22707fd337f491f402a8dd2 +cfc4057c0ac95a394c39d1efa3a290d9dcc5a429feacd647f7650740d96e3c79 +cfdaf6c189a7f1a47f854e95e7a53ecc7db89c04881606c755bf2009b1a1633d +cfe0dff80c8296f55069b04254fd20988df07630fed058ca9c0c2909612e4485 +cfe0fd9a37891d570edfd43e83ef95df48823e5763c247610f870e5ddf122873 +cfe1f14fa9a9bc52d0f8aab5b7efacbd082faa8b96bcf1f6af2003dd3d7e3cec +cfe428276f57f5e3aa51f8452ae0e8aab9a71c2ea764670d0c4432fad6c249ff +cfe92b9f65121f1f6e6c1c9f8fba4782c71cf9c5926cf940b14f45ba72c99734 +cfeb08fd79b5665735aa00e7ded33e2c5c7910d4a6348908616530dd562ffd85 +cfeb73bcb2171adde6abb358c3023c918e5b31c2cf08d513dece49770542958a +cff0a6c62fd549c52a102536e44cb982a763caf88fb8ccb1dfac4ae11291c05b +cff5b6630b7fa6b637ebec917aa3ebbe1df2274a0beab5b3c444b811334342a0 +cffb494fcfcbb04cfe05302158e905caae4312816791086a3c354041c94ba026 +cffdb23137c404799d6b3832798153e9b603c6564a5aae790736d6a79db1759f +d0006cceaa504ab14ec05eb04f9ff8e302311b9a4a5ade0442a585bde26bc33f +d002265af8d09020f20185a7df9fca1ac8267da456b69e1a133cd3151a90cab0 +d0158489fcee2307ee3dd030cacf942e029b1af1a988d32a62127eb700b17546 +d01596bdd6a266fd5ac930075783865d51d9b5a1206d1089f3e393bdcada383b +d015e398232551d50c62bb1463286141aad78df17a5685ea9220b17c78520bea +d017c3115851ccaf7abef7e0dc63eecfe8c1f5a92c59061e5617d94c30e681b3 +d01d03c5d3ff43097583dbd9f3a68eb5a5239093dfab409ed9a28f1816d80ac8 +d01e401943e4ba9754bebe932507b91184ddaa4bec2b507a257f6584238645c1 +d02a87ad20d80db0529d53292c95a15a1d77358bfbfa896d304eb94cd588fe3d +d04a9ec18406c4e0fb33932aaa029ca31336d48c3ff77948326021cf1f13e13d +d04eee02c542cb059950459e8db4a1725c8693f3bee9145864e0abc2d6adba82 +d0553858de3c33f67244103cdae1ab098fd338b18728d7333c303101bfcfb778 +d05e33c2667ec943ee4fbd1fa7ba96071369de0dcaa6493b3f0fe48b754b6691 +d05e7413d55dc72bd4ba216b7b268b9b4c588ce860ec6e507eeff7a6a330a4cc +d06289c15233defb30e5c42af6cd8ee024c9ed28c2e1228b384d4c8fb3c622eb +d062f8273d891431421333d9562317146460e1cc31d71696bdc3d118352c84b4 +d071e756e3f217e29fe3b6ebf215cbf61374b7066c4c6ae9054e0fa3e1d6ccbb +d075067885266778059cfb94b270d81ae570209c274b570cada5901d84925892 +d07c5f1d1ab0302d128f97ae800842d5accc2057c871347e08c100c9bf55c1b8 +d07c736412ffb86cf58efc5592c30e967cc99ebf426a767ea6339bf2b244a16b +d07f28fbe0e72e05ae00d308bcb298cc1bf238fdd106d48b9ec97831dca2925c +d084a6173a678ceec25657ef8e54b598076a391e5aa550d2ac09417324066089 +d085b38e42e3eee875709e8546e04acd05f7788e9a16b1bc0bd4cb26fdc4a983 +d08a134ebec0dbcb0da851ff4ee1b3650c4e9d7637e05a31cd62eb41107ac7b8 +d08e05c4e4f924b62441a7ed20d15fd870009f57a92a1e00f4a0c92e973c39cd +d091eaa1c126f6e931c9eecf018ae4b878a686ac664fed85fc6c5e88b167e0ba +d093682b674317ce1d1c09c2f099e1f8eab1ca0a5d0f9b8ffcaa9ea34cb4779b +d0a54e73f28a1b26a093352b91a1f2427b0aa516b8082793e46571ee915ecfa8 +d0ab5fab5a4e1baf0f9db3d1481c324a27fbe05c06ff5d27dcef7a1f1edba70b +d0af3743974d9d6141383b4301b43133c9811b1532da81b97233dcf3c8616172 +d0b88f38344be6e53d8f1f7d9abe6679832c162ba82f4ba3557870c224c9a60a +d0ba787551b89418b9bbc2b4cf55f0bbd756e8482ed1e7eba223a35d9d1a02df +d0bdd9bed819c0012089b185031409a065bc65830e3588b6ce334606a07ae861 +d0c58dd7952d128f7041a773beae772aa521bad66072adb4d86a14e4690918e5 +d0c59e31ebdde27a2b8c88a5354edaebeb6c50f3321c8bbae6976d75f4bde7f0 +d0db030d4a3bdaf0432d226003640c1cdd0f6f216ae6671626160d5559d9c52a +d0e59e209dee2c9f41725854e377eb8587d255abf2a855d97e99ef14fc0a2f3c +d0edaf6fcbb3b23783db117492a6154441e12c5aa8f40ec0e842f5c79ad21f3e +d0edb61e4b50f88c86626eec3a6b64b9bc7456b572ef337be00b47ef0aaf3cf9 +d0efa3d1931ca2b12a360b299ca85328b27d04c68c3b7243f16c6ede0182e6e6 +d0ff9e604f33fda3634372d10fa9ec3ecc9a8c626bbcd5cd99b05cbca61a6525 +d119e42e34f31664eb805e66d0a80b482380eb02de23083195a0c909424c7057 +d11c4bb0172e64c18eb9a7453e6f8085ac363aa7fdb768a2d1b5822913d0b368 +d11edcbcf262c35d7f91470da513650a48d494aca7b98c6fd2ab7b61ed39136a +d1252d2a7b464c8d1f1b6ac2ce1212fa561e0f68f50b07441febef536be8ef38 +d12a11119e413f62187b82b517d0e578411d553ecf5d3cde8f5f0d8677e145ad +d12fe2daa6dad80176ccdea7f3fc972e63389b4c70a609cde3143737ae410a65 +d13f0db1f26f98bfd2f3ecd17fd42e3fd7b6a24f47d5d97b3a75d0ee8a257b85 +d16e683c06871abd174e6bc0676f912cbc579f227e4a1ca09d58901c7e4759a2 +d18205410b8019a3c51be8e88b2d18b735e07654a219be423530418196fa115f +d1870841e289767f53d6477410c45333c67b60e45dd3446080e6203f69339eca +d189a2aa2eaa1dd22a5f3791744836eca188e5ce2a710778499b883739e0211b +d1a0e753b9a969a1347241783c57c31e979b4534c687b82ef164cc68a9154083 +d1a65831aa30b4437d7416bb395212a81cc4c109810d31a6a8e142d33eb1bdb2 +d1a74acff8c3113d3724e49d7b317b3e93f32e2f75ca388f0b13b8d0501d4ad6 +d1ab55841bd5cf20a824dd852e1e4e2fd00e8522125fd424852eb3bb03f366d3 +d1ba08c9a70d22cca125567e29c929af7ab7af5958adf2b9ad63fc59b9aa18d2 +d1c14b15a7faa51ab1dbbe08afece0bc929fbac483783bf06d8ae05ace3293ee +d1c64933016ee2ee734b175b4f0eb2aee20607fed4141fc381fbab93bdac13ec +d1c6ecc1cfbc23fc7f328dd1216bf2d925173aec6a63adadec534e05b2e5a05c +d1d5a90de947b7d539f4cf8f2d966cfc328b16251b6e4d26ae879a23cd605a31 +d1da4bc0dd45ceb9cae1900cf2c9e9544d24ef7c6b36243013e90d2b0a2ae335 +d1eabac3f153a378c4f04535a8cea8ab335fccbebf7cf6ce7b4733fb1f5c5115 +d1f5a719a3473bed7c7bb51f8d0a782aac5993c65a62f02fba429be6ac3d54cc +d1f606d5ff6bf9ce1b4e989c54e3f10b738324c4765aab9c796fca0b7b3753c2 +d206a3a53fdd4857e87895a307e012fad19421619936786961092f96e48bb1c9 +d208ae53a64427da37aaefbfc84ae9c657a12078d4450f0c14e0e1d238d1ed73 +d20a7de628370ac668d83d4695529ad83889e4877c94ffa52f3e50d73e2c45bc +d20cb1961cb8c1e7305257e15a0583941b379e8eac5744147624ed795a539724 +d21e5a86f743997bcfae2307eb19ed080a19cf8d8d4f909f3f65120be7bc799a +d228ed539df40b747d1543f38e6032bc2ef889262b7c6295642ff1089c5daf0c +d2337cb965c1341ccbf592b7109535f3b75526de617b71fda66f23023880f987 +d2380fe5c98250fd324912bd22e4b25e8fe03ccf41c2ea19a97680ac07ce9c3b +d2430ece22b9f65e7a052a4483093af7d48943af07bee7013b16c0c666bf808b +d24433a0cd15cdeee25e982000415e79930609379ff8e256b451d5a9cc4052e2 +d247785520f31cc190da0f79487be7f5e4f2ba6858c8ddec95b1b888e34963ae +d24febbce6163f598e1a4df843ce4ca4bb53f06f7dde08ccece8b22738128e72 +d258e53bb7acb1d199b7d1998272077d6a4ca72a46c1dd2011344b0e419c191c +d25a69090129316ed2a9f0b5b7fcae8cc31f12b89c10632dba15f4b19722dbfd +d263c5b9e67c5fad125b33d6cabe6589b007db275d54a94bddcc5495d51f8a1a +d265f82a03744833273339eb192e202d469aff9acff90ae7d205b00297438b80 +d2684bc4d29e9faa6195b1fe27b6f96328b814e9bb80576b38168e2436a2f50a +d26bd88fb7e5e996ef78451a9891771c404646e67ce1e222be510e61bb950871 +d26e85221177133c6391e53b3ae1c97a6c292c5df54599e013afcfbb630ca522 +d2720ff9b95f2d4732eef08c2ed8a6be3d1a393525ed031e5a4b92053079cc19 +d28fb30ad54af9a295a8d96bfc76d9ab5f5e17e76d077395aa5f3a7d3f99ccd0 +d2904ec875b2e2dbafd2fe14073b46c0c9797b1c0153e717198e5e29162c65bd +d29bc0faf208d948235ead21ad3f7b5246d99b6e4d7ee1378d18252be94f3936 +d2b20878662143b03e11143d5f9e17de8febc80d96bc749cc88f1a1164312d42 +d2b7bf0645cc0baf9727886e233cdec73800765b1e2142243b5b2682f3241e51 +d2bcc1eb10ae1241ffe141292e154f5b0a29f9e2ea645eb32dd49b461335f577 +d2d35c8acd0b03dd237176ad0cab0d17c78a67fb75f4d069ddad3cd2679d1d96 +d2ded6c2384eaeea22d98c3cd7b6a01730703710f4911f8e9715cc6b77e374e0 +d2e544ca93d7f63a76782c808da92b4da0e1621e441b342ab701b300da0e6499 +d2ed897e0a40e51dc3d3def9528bb21358b227a2d690b480e85908e5cb6496d9 +d2f2f07c12d7a7e5a389d063a6b75a14df801133cfe3d51ddd24cb1972f83590 +d2f48d00ffbfb3b86141013f198b2b5fc974101b796c3b7d0172cff49f207daf +d2fd5facff566e0e206d6fbfaf76491b175ab97cd81a31cdae9503dad128f49d +d3151e606bf9e1b66d89dd636ec47511d1eeece937dd75a84c4d5d1c5eb70f2a +d3461c3ceb9e06a512c70c4c059c3d4d8ee16d20cc6b0674e1e4014b36efce1b +d348275e39af9c2bde34c7d9a54d79cd4a009ef1735865d11ea0cb9c5ef7f426 +d34c7db29f620e09202db4bcbf8ab74fc72d8b15a58d5b6e76730b32170fa120 +d35cc84010be4e4ea952a30796817366976c7b347fd1ad3fce67017bd4f41bbd +d35f30194547e2d66066c3e77ee3cca58c78d1888eb930a7f8c85e62f3ee11b4 +d363984aa981bc3c5c50cd5697733dd91e3e5acc9f45f196a65a2828325c6d89 +d3768169ab630a35f7d79a55828369a8ce53ed02f4aeaecd68ff4255c05b6621 +d37779b5adaf76b6ac77fa35b3fe4520aaad6f7ca0442b0c166e50f90c73ad2e +d37a723dffc3b896a8a07a9bcf5acd055934a1a8580daf0d9260b1057c38168a +d37cbf48bf7e62da571be0c66b60e7a2ac08f70aa395f30e07c5df673f304a1b +d37d799546d59c24dc4da5290566d0dbabcc7b21ef4892254ddbc1967e1bca1f +d37dcb2b12a7582942bff593a98797e0cd2a70c0e61ae2ff1840c1c3750b4724 +d38b90e7110b3e88e7aea907b5151a8188f68736567b6db9eac886f7f2ea6828 +d39a1435c0833da935511183e8d42e6e3418403eb488ebaa434802061d39969f +d3b64aa7dc7f98785863165671d429a168585a234952384c76f065d233daa6e5 +d3b9cce54d7b5b264f04cb9d78e59b564ad58b9abca9b551df51eca0099e0f58 +d3bd234af290372991ac1c6135f97a04b3720c2f738b0c4158dbcbb6eca8f29b +d3bf489f44e2198951ff0c232412673988249020dfbdb8cd251b40f9cd84d9e0 +d3c3ce7b29b83008523d4153c720c091205a6052fe1c8af938ee6c65d3751d0a +d3c3fa92c9144d52b5703c7f2aa386c29a38dbeb3904a33ef76467d03136fe9d +d3c7def0c75b29fa9d6da5e4a0c9d41bbf832e05d13c324f0aa6e916ac2b49a4 +d3c8c6c94d275a88f42ec3bf32c1b0127a0b048df8ac15c1e19b00c0f2e76281 +d3c95670b4e034cf6f1a7f35c41b5d4c32572cd5bc8172aa08f637754b195cba +d3cc7dc55d950d36071bbdb856efda2598b288c835e8d820f5c2939fc69c0576 +d3d3368f7a426371d39b2d436286ad20a7772b7f728999e66d641091ae1ead9f +d3d3b4f0a5beafe5bf3f5fa8cef11b861d4c6ef1f16e4eb4a424aea050ef2334 +d3dca45eda88b18cfec048787e9bea0c02f0e4781d6df2ca81ba0bda62cd4b79 +d3e4d52837830eb1f47234b812ca0ea70b3bb7aee799945ef88359a00bf469b9 +d3f36aa28936ffd679f398d45a449de09d8406ff79038f08007ca794c3549bc3 +d3f37e195e83fff5d40c9805cff4fabe00313f452b25190484d92513c2b7b276 +d3f501e6aaf83203bc278ea08e643a14fa18601621610a8b696e9132a2ac1e25 +d3f5ea54cd5783bc708c9b537707cc1e765bc5d64cd3332453818aefaa182f02 +d401a36354757b758321476948b1bc2f393f73cf3cc68ed3743cbf4d461011d8 +d4066adec89d2e08d555f0e252b0ac15373120ad5c7193fddfe7b8a0f0dcd132 +d4096ac3baf9c4068d7185715cf466c143442c029fe182dba004c9e75d066eed +d40eac1e9fe7edd926f2a836859c56541bede995fcaaf3c4ae7ad39761c88de0 +d41a2ec226cf66332c3ed478a1038cdf76a2a3e1bd9cebc4d4197e6dc1e8e93f +d426b01f7be7af8da9d50d1e67d740ef167b75ba63c0b920b306703f1973cd87 +d4290e6a8eeedbdaf6b06deb77d5588094fb616c04f33dd67a9023a88c9182a3 +d430dd2fcbaa0b5dd7b24ce47d4349220f7f447a1de8dc49a96096c6171afbe4 +d43770660e79db5060ee1eca30e516ce2d146073913caa4bfbcad45d8ec5561e +d437b032752666328c9836008b9aaf2e3d3ed4fd5eb62d9e9408eb66c29e6730 +d442b3d9249ec3a0bc8f956805f7fd02476d89dbda623297d77f6eeb46eb41e0 +d458ccdd90202dea71db9309913dfaf8a021e03cc0168d4c61b079f9b2c72fdc +d4643e2262f71d0a20efb9e3e329d3c13406f8eec9d5a1a3e1e4753a04021f50 +d47b2d5dec08171699d61872c81f2035b7b5cfc02a1057385cdf07d4c2dfe14c +d4817870eb2781a18aa5c51719eb7a88b4d61aeedcb8132ffd57f0c445d1956f +d48388574a32a7f865b9f6dab37db00bd461648baa8effc2baad541b2ba7cb62 +d4890e15521dfd3cafe87fbc2dae5508440e189edf18e51f96769709f26247a8 +d49e374c8c496055993b04129dedc886975a9fcc4b45f8ff3cb8b3dfe1dacd74 +d4a0b6cb2b9bab1a3fcb413c9d6c632e98f6023765866a838651c28b27269e04 +d4af15cb298ee852a563b01459815d9b49c837a0a2f453462139cb529c86a401 +d4b040b1a1ad9ae64d723e776af00d784239c6176d5fed76c602426c675ececa +d4b439af014645b2528ace211b0f67a2b3acfa9cb22ea376cced5392ad212628 +d4be063bd3036ddd1d716eb5fb541913f3b52b5d09ca9126d0a2f21c6906d714 +d4c0b8843a1dbadcd74a6513a3b42aa6c221f632be89472e416be57ab48ed5e6 +d4c4ed0a716a2420596f3c5aac2d4973c438c95769b0fa3f5d89e535c10c7105 +d4e8682845bb4e1c630f211081160135f10fec0ae5ee645bae187ec6eb1277ba +d4eb6ce39711dfd8031f53252e5998c847c881da8e11160e2344bf97f02f0a7e +d4f10664383d8516ae19c729839012afdecaca78525bebbe69dc3ae6ee5a182f +d4f44d6569d94d9a60972decec10e44c20367a5248673f6ffdcacf15586db262 +d4f7d9a11fadaae862b9abd906c341d00566793995a2e7f600ae197bc22f29a8 +d4f97e37bf7a5e9ac80ba9fd2ec18146dcc9130e774daff409e5350a1be269d9 +d4fc2b8047c94795fe1c9a8904e37b0b2e40d6fb37a1473ededf26c2e5c479e5 +d504acbbd3ef226f78478675f4fde6e307451bb8809138d8f90ff98ae0449dc8 +d512026612848ab5436639694140ffbeeda7f48826423a8cbbdf282e225107a3 +d512a9aa0f968c3a3fc2436ee08c57c102dd515547ce7da475b8d8f8ed6d6c1a +d52f088c493afb0280d150f6e0e8dbdae3050ef9181f3e084f644f761ff426c7 +d53280fa129913850aa53d45d1f39695c12fcf1bd761ae537ec9f91175fb7399 +d542571c514dde6d1f80fba512a9778534ee30ca65368a6a515d949e0f11cf29 +d5438b653e704f6a47d0a101eacd8dfe5ab1499d7387a103b1e082802cf2a97f +d54d79003b08a1c9e27fef4cb8d97ee8db04e4dfaf09316bd33246c741b8a5ac +d57b0cd97779e9fce88a5180061b5b5acc27e4986c6e6a2c38676bb2b8a606c0 +d57c2b0ad7ddcac264c51d0c981266a6543eebef9f65477b9a6e297c851a1cac +d58e1a19cd9c781112fcac2be15fa5c2df635fdbc5d689afa9c95ef992d8c8ff +d597527f167877d92aa18fa6068ed2f648604a2b97bad62b9ac6dcbebb4248c3 +d59fa2aa3c1603f72949a768f5f979505d4ca3add4db024239f1b6a6c25c554b +d5a0cde2ca147365ae8c948566d867227c2c0e76ffa0fb919c27addbf1abec8d +d5afc01fc6e83daec1c0c27e5de7e1172fbfdb69d263082cf079515a64f5eb6c +d5b035207f6eb3ae2e9204a01692aa04b23766a799dab8e9bdda497b296f8894 +d5b1842661e89c3fce6f63031d0c2244f983cb5222fb9ce96778fbe3c71e3fb1 +d5be7646f88937d1ea4ea6ba66b33e16210afb784a5421db497d1f1e798b85bb +d5c1bd61f4d38b8abf204e62c8513c0d7fb21699965cd3361155b94b3ad7cd3a +d5d0951dc8b3f597f0d1730386b8ada675500fd8601f6480b164a84aa59eb1cc +d5dee52105eae293959ed642abe0007fa2cf4d6b147294e8b00bae00cbbfb82e +d5df45f8c42a15ccfc4d6a537a7c726db483b55a5e18a7949ba9efc233b0a227 +d5e1e9de16a3b79d7fdf5be2389e7b323c1b27283ae7eb64b927915c7fcd0467 +d5e44d143a69f29ee2154d00470443931a7ad91750b98a659edbc5aecc9c9dde +d5e640b0e99e18275819940fbf25b6ae08857c1af9bf680f0c62d4c2a58ace5b +d5ef23a88f16bafec1b6f5d3a74041cee8bb2164463c9c25f272769122875d52 +d5f4e3d67e8079346a5d7adf90cfb8b019ee5bf81f6e5ff56fb72ac763b8a464 +d5f9ad75c18758670d41a18cfe057d6ea7d65c490487a4d3575104ef9182cee7 +d5ffcae9121c0cada857f7069fa81d57a3e3fdd2a3553b1e8f9c7e369d208b1e +d605c54f2b27473726a35dfd74ef2c38f4ee8bde3c9c9274ec2134e1078f8730 +d61a6e3c6bf6c416c3cbb89533df0ecaf462b117afa54840f34d1b928914ae93 +d61c0dd1fa397556ba34e7d5da161f02fc58595d55f81339c0284d94b395645d +d61dcc64d0a1782e06cb3bddfbf597663843559b782cf17c6969bb347a8cef17 +d61ded516c3c0be95568ee618b7d7a04ea724cebc702b227dff74c3b608d495f +d627830ab2dbf4369a134e0f9291e65dfc9fbb0d67d86ae41c352abf952ac693 +d627ef18318854291e6594466a659e65ac5d6c8b41254eff6b604aaca3dc1548 +d635156c18ec40a89acfa68ccd98f51c8a34f366611d1d0cdffaf491bea1630c +d64dc4f0d3284b9757725fd6adc80a372bf8e94e7e2d1ba20a8bcf244899747c +d64f2f76cac19c2a36db3eff2bde5ecdd2f887cdad43ee137b1dc217f69eb944 +d64fc8cfd1eb00aa0fae15f9dcd60efd609546dc093a53715510858e4bfe2746 +d65afc7c703ea483f088ee06066d1d5956ffcfdfd55c51ffb18f34c4aef2025a +d65ff73ceaf0f59b41b0aee80d7ead2f3a82ef3b7879cdc4e4a707e02d00d592 +d666b50856e043456229f282ba380105beb94c818b809e514b8e01f836c1942e +d6725d5e38865ce3adb8ba92780294c691a1a0b66071a77d16e178187c099542 +d67a3909709591ee82d5b3ada4e51cf882ab1faee34475901baf0b9efd10085c +d681da0138a7e562e35a370497b6a6fee8463a8dd3f4437c863b42e90bdd877f +d69337966812f960b4ba0ae821a937e102dd4d95fd355ba7f00cef6f73a4bf95 +d69afc56565f0ddb430fed1d4ebbea3c4d62008cc7ba9e03159831810166cd52 +d69c8e89e2aef48b13a58d3f87b2cee355d770119fb2d178bb32341794f275b5 +d6aabccb385eb37b3e6245742668744917c07920f1c54e4b5f31fa519bba1a87 +d6adfd424411d1f4c186fe425362955c6e8ce37fa1791f23182644defe3aba9c +d6c17d211b4899869cd5c22ebaf4ebbce828de021b6a6c0eff3b32faa0da373d +d6c1f05132f67474ea223339ed35b53b193bedf203387c391bb75d0d8f3eaf18 +d6c9a3c9c3963ffe3d5128c8eeea96526afbf72aaf0b83e0638385b5c81d9ea6 +d6cb4cfac88031e7e4eb796c6801fa33d4d0e225d6be548894e9a447191f22a6 +d6d2b2fe64f0d05ab7418576f2a429bedcc9b64b2e232b4235e3e30d96e3ec78 +d6d867e5d0fedf4520a472f4a1a0d747a316d13032874c65bb63e720036c734b +d6f3ec276cd9fd540e4d4dcb7aa0b43ee3ee1c9ece9ca797c0683eedd589b85f +d6f86f0713a30593cce16daf465a21a2be637ed556af8abc0c88e1e6779392ee +d70fa8670b51075b278eda63362355a32d348bff82a2db2ca30b9084e91b5caa +d72cd04c91245462fa71de254ef424ca593edb68bded14876f7e0b613f7e218c +d72e172c01cb3ad8f88afca15540939325f2b415ce51330fa7578ab494410592 +d73a3eb18aee995709f88f0a9cdfae7b286e9b0e4c4fd2044bf5ab53e3e89f18 +d744caa93781f23b3b778dbdfce5ad9ef13fa4ce6b928670fd1cb064429a33f7 +d746edad6930914e4926a286b4e8f76474363b6267d24933a82668b3304b50e5 +d749ed6781e4497c69dc47ae0e00b79f3bd8cde4f6433e3925dd5aac38cf8911 +d74d4860a31d4f30a9404188729a18999f521df0dbb1e3741a7d29fb9b28ced9 +d75c26ae4dea18602f04f0026e2cb51ae40cc0ac26f3bf363e63057d184350cb +d7669b78b38402740d80253327313f1c01b40bbffeb2ebd18535f5db88f7248c +d767e55f47f5734eb14aa26ccbd6c26ceb24e928e223f02da8374acd4d06bd1a +d767ff8a0324c14909e229980dd69b1f74bb3081902024b5a48ec1234d872c1b +d76f25f64fee71c6061192ebda3fea6060fa4afff65edeee125224bc2da7b959 +d779a9e8d3712ee161f1c84a1f579e37801b8d7d19fce9266356c1d2c9d16bc5 +d77a9cdafa0a76168d716334f77910bef20050163f318e4708c715549ffd7e17 +d77dfe2d253a01658c88d0709b34bdebb7c469f7865ef22a6e7c220de9066940 +d77fbbbdcbb9e5cc9d9660bae8fbb52cdf4a16da604f4030f2e08fdbf2f534d1 +d7872d2be6322721d30d6a5e9562dd66db46945249c09dddc5de1a6149e86068 +d7a72675e2c3045b9f4c6c6e2f1857ce043f8143d919959fe49efe79ea9fc057 +d7ab09e4ccd1c3e353814bffcab9526803696cba830dc319c2ce60f7f90bd34e +d7ad951d85c2b9b6295a3ef6670747ae7d59c915d094f63b4af52e2f6d91e54e +d7b76a2e044962fa7c21f1079de891d475ce90c5c4a86aa7f7a531d8184fd296 +d7ba4e442f6c7da8b774a3963e6c08428e3651cc009c4975c2f2b31767dc82d9 +d7bce6d7ba26ca4e982f9a578cecc6b8bf910174dca511d7eadb07ba386ed32b +d7c5c3839ebaadb9668454901f937b1e435d2d2bbe0f5a6e9834f1688d5b4b3d +d7c7c2118fbfcca827b128b6dd0640b114e33c49912a76864e335ab92ad16321 +d7d03445a70169f9d79d0cf2527fc8eae46d1e6ca95af9284890db519be32bf3 +d7d498707cabce1dc4b668ffb768ef1cb1fe70332046ffe22e4db383d571b911 +d7d4d85b3e447d1d37cf02de7639859a7ac6741871cee53612a7cf919a63dacd +d7d6d10118f789f2313eee001fdb0799faf8dd6331307571e6b004141ba4b639 +d7dc42d98faf7390ecfdda87acc4191d5ec1efdbdd2772bce15f2e21c6e4f6ec +d7e2867a53a981c30e8816390010a786e14aa80116dfaf13c4232d530da83dae +d7f47606381469eabeec0e89b5be0a2cc261e535fac5c5f3fc76f557c7040f9e +d7f84292718d5f995566d024a1085b5d577080ebf1f3831c174b0de3ae016456 +d80fd9c60a00a48ab477e184e17db595913dd5e3f0684c001aacc7d63d091994 +d8150491dbc89e7e6180ec696aa9c1eed0f9cd47fb636ddc3ee5bd64f967948f +d817c4f64bc93614e5b2df64e9818e7a99855987d372d149e6469c497b199332 +d818cf310032c103bc0c58c96c015a76dd5732cf4a1039e66f0e237e6e8076e7 +d81aa203c26ee993fdb1376ee0a4eb1d8c45b14bd492d5fcfaad5ecf64c99921 +d81ada2fbf452f1deefe51be2fba7b4a82b64a5ae14c3c2f00a1f2e7579d8f37 +d81f677a942a2070f7ff19afdda0d76a5caf2a6d7aa1ec9e5bb8c37f16583914 +d8250921422ef7168a2203807106b6c4ea6a540d77b51940f559dc89118b1b7c +d82c6fe2efc833682b6e7fc3727e52ef505004772e6b9f51718e4b92053028e5 +d82e7e47fc5e260f5145c8391a6f18c925641c1f2bb12ea2e04b1a98fa5eac96 +d8372a154a8a4a6452e9cc27aa07ae05399cc77cef0c9ce05e35acf302a1ac35 +d838cfc738fee6fa658c094d27a11c343f236629d14b6e3880ef623d678e5b5c +d83caf72854dc1d1b753a6d2c0af7d3b87898b29531e03459b510aa6adfb35e5 +d843cc28abeff1d0f2b1f92e9da0b42657feaedb827c776b1f0ada16e20de079 +d845bf85e4389585819b95631ac25dd984e03f6732b923e1f00e7d2a2a8c05fe +d847a6617d7a7ed86b4691752013d581f9f23f73264f62da56d8fcefc6533b19 +d8562d212613f00cc209a8f468b7f42c46ee239deb54a2990da13773889c6e2b +d85925ee460ea01135b3c69a0203344baa9721bab5a369c47158f9f37e79c7ca +d85b0495aec10caa4f352e7e5f3c7c1f18e46be0bc46ecb673f485ab28d9a56d +d85f119e2d1f6e4edc07d5fc38fc76ed70b5c83be40ff50f9feeae9e56d1d90d +d863b235d971b2ebefa3686e7baedabc556ce674e451c2b6009e0a02b832b2a5 +d867603a16f9143928ed5e09fff0f0e537ce66893de803e12d244f75266c184f +d869ee7e3d74a1e0878875c7b47d9d7246e11da694d538ae18153583cf68ee97 +d86a5b114b7b0fe1208e924cf20225fd382f8cc7cc55423c766166b1554eb145 +d87541036e5dbdf608c7da595998905ce1779fb386857db663adf145a80dce6f +d875459acd3c55380af7c8c3cf2fc4bd7e18fd7314725a0e6a4b19d65b1eb74e +d87b6ed996e8911232ab4a2145ae335db203b4e009906ff4f512a2174be50e6e +d8890a7d24a0b1fb56f57c5e51ff5e260340c98d6b4b7faf34be0d55fa1b2673 +d893556377bc2efea5018240b524c52566042340f0039e4aa286b4cb209cccfe +d8941cb7a8922bcf9430b9765f2d7f8548668f5a600afcbc8f7d5d430b6da937 +d8956919e5407f93247428c5f14cd62e68b5e2b7d749bf175e83a97c5deafc3a +d899d7fd2a66fedfe5a4e81eb445bbff299275e5eae10dca41f25b6b152feb44 +d899e02103e3df065403b66ee98191224f30f8f51a4a95f8325d7fb534cddb22 +d89dc30e0617a5db2c52142b4e88b92fd672cc6850eea6659c017fd0614ba49d +d8a91fc26ba35b1d95f4881f439b90ea3a9129b6b632a9ccc132eb211001e797 +d8afc2bf670b02f298c07f459cb384e960a09bde41ffda24c8c9d3d3f4ed73c7 +d8b0eb99543cf3684baa2f72ef377fa0884358138c4b59cd052e4dd2c3ee838e +d8b59dae7e30bbff89e12a1e52fada87906980cddd9f11436ba139aaa85096bc +d8b7a9114a9b27f9fdba994bb9911da9d42dabbae9ac7408115a7b9d4a926eee +d8baab1da943b001cd746c004dc935a0e5f1cc5bc6ab8abecfc35cdf888d2f14 +d8bc0367c478666715c856513f560cedcf38477a316a1cf55c7470f7515e30a7 +d8be57d114474ee41c509bb385a21e402278af610635ea84cc7e9516c7f74a0e +d8bfe7cf5364acebe4983216301fe74c2d50e34b21cb441472e704810bbcdbe3 +d8c5af189c7208244bbdd2f5e107b84159efb208bb675c83f1b4d402e951d662 +d8ce1b23512930070a31a78495f82d79082d7c2199a5cf51a4615bf8b592e197 +d8d421fb2c9e8274001b6335ce0f895249d44b03ea008829d8fcee7eb2cc99bb +d8d699e0ad7128aa9b3758b8b55e27e4e6c453fa34da7b0b6ad686076d0bb033 +d8dad96e9fbe9babebccb20448bb8429d890c26114a2d279d82d65eb79bd637b +d8edffa6a4df4595562a615c5e699d6fdcf952ad763ba80e97d18a2dd9d8f525 +d8efdb5b466a3ed964f9c377995b6136010e6fe2604c2ad96039803263e8a698 +d8f2593bf71e51404d2a8dbf8064864f680cf57ee71267bcf15aa4c84ac6d30f +d90f9db297e4ad79344f5d0dafe018b39ddada049320a9dfcb058bfeeac18694 +d91348a7cbb41a388538dca62b4d18cd29c3a4b5f8987a1884bb16823cafabfb +d915ac79d8bf60cbe437ac7324c389a572589d32426056ad317454a3ee8d2cce +d91ac2e8029bea5f65b1d4e6f13a41e1051665ac07d14a1bef9a08fda57f2f5b +d92f7fcc8880e70cb61ab39a88985af6810bc533a5d7bd9331701484201617a7 +d92ff746e0c29e21713a66a31390fb360d20cf1294e8c09fcb7372e7fde2d0cf +d932adf3e250e8824e4e7d3b62a434e9123e8f0108285ab2ba0a21a10e06a10d +d93617eac26f7a742dec579bda95c59bd8df215c0e31e83225b0f70dc2a10609 +d936694c419e84ccf57d2f35d7df44a8cdeaf4f6af1a35830d7e63713876490a +d946ebf777d87abcd1f1ae9a483152093e3e4049209776bef83a38fe38245bc6 +d94cc9636a8063fccd7fa289f3a10b086d585429d20c906dfcb66145f8ee6331 +d957f28a73edaacb2edc1506fa3150648531a13a394b91eb758acc32bd3a98c8 +d959622f1df290de9c658c0289de5397f282d2af4050c863e9a6290746557325 +d95ab0c20527d7feca22c8b41f011a5c0a2e89a0e30190d4c8e58e5b9f10a6d0 +d95ccfeb3bde1410a490f1d9e64f0b8c9087da75b31c4f776b448b747b836c53 +d96193dda8b8d69d24a0c48d79571f46fa125baddc0044bcb110aee6a1cc55b6 +d9678ae6bb566e7fd804d37a180d5396af01ddf65d7eb5fc6a6bb2c85423c80f +d96a8107f458d06583f12dc302fddd90d2bb8d08e1cfa9931d451221bf7b5965 +d978a1b03586aa53768caaeeddc4f160d62944a89d96b83073f0b8290415a135 +d97a33275e92cb0ac7e8b4c3fb0e7b6a7796eaae0a28617b19d67c4b6e88da1d +d99b3253958c7ab1e4f2e292a9337e8fd88b217de0275eb180952a03c5d6c53d +d9a73ee56d20aaf146cef59f547720550b4a263c232b08e67d84e81caec4c100 +d9c816cbc69769dd35a2a22ed6813babe2a0b5572260277752c1645aa108ad14 +d9e93b7b244a0fbf95c066014ce742418098c0c98e95c5dc147cee97218f42a8 +d9f28dd8005bae4cae375a827b86d4b07f6ec3b7d2adfb85db1b298451e1dd93 +d9f73b8d03e29d8844631647277a9dfd78a96022f5d0009b06cfae068a7b0232 +da02d6ec3074a45c1f4859ae1b762ee49e13d71079fdfb27ea04fb6438c760bd +da2386fe27b324870904b01e9d7c40b2c8c11ee2b75792c16a8301f8bef7751d +da2f4c52dd8f87286c1d8a0598202c6cdcfc4fc37ca88233d33fe73204f7c562 +da2f5889581dc33def26009b633298cf1b0e4fc8a66b64517cbdb4b65cc66de6 +da390178b546f086b8079a0234bb843d474174e6b9b58f17e3a9c2c9883e98ab +da401d6e70a00d0029613e7875738ae5024dbaa128875ca5393e90220a234e80 +da43e0f542ab22623281b34cf659528a52d1d6f18a68d2c3bef52576bd9659f3 +da4c09ef658681d21cc2c7b08a25e9d3435c83469a0d432aaff02fc298034cf1 +da53c2a06f5905546a5ebb0deecf646f0db819d1b72487478cbc867d4e4f158a +da560f901646493b8ee35e84de2d268f5af60798b521c24f69192997c264c0b6 +da5cb26de4b0058bb0b06fd6d341312f6e45abf4830fcebcc8145954bb495792 +da5ec23821f1d664f98f12d8d6210c26669104705c5c3a1729edd9ac2feb7c28 +da6c902c43b9cf538e7b36f89f50b217a1fdf93de10cbb16bc538d1a8f548e60 +da6caf5b1a7f66a328dca0ebe3524085a837c98dd33a09085d5467b5be641875 +da720d354dc82516688e1c326d3ea49d02a12c68b930f4453dfa35665627d8ad +da7cb1611fe56e42619b590f19060ecb5df847c92ceafc68a1176db82407338f +da870ee36a9259d5032fc9be70db98e633e113e19b52f0e92c3ac00283d13385 +da9895f48f1075a2658da9e7b47b2c8836e10c4d8ff4b4202af43ffcf6860220 +da9bcef7d1eb341b478695f95ef59e9e7f78b4371613aa449a232b71d7ce2df2 +da9e90898f8a0e1a01676aab7fb9b8c09e3eb7d151455182fe9368ec6d64c7aa +daa28015ac380f4a299907589260e836b96daa676992161d0eec098ec70c86eb +daa3f636fc27fab3dc0d576ec37efd0e4b89af08c88236f56ec9c0856b725d73 +daa8992b3391f42d5bb2483ad5e60e170a3f38720a5eca160c369b9ea678f7ef +dabd3f9e10f62183c72dedb1ad7d3f1199ae4b49095215543d6fb72a22df7dc8 +dabda894259209767d014087034bfe7cd77479e7364f79ca73d976c0e875fad4 +dac150dc20867c314794186a19f91bb6a8ee29739ac8cdb3f49cdc2772dcd8f2 +dac9797eefffd3c5df81f3b2cc134c3ebbcc3882312b0bf13ec1368e1612940b +dacb71c3574a1be1b5aeadb17d6d9914a6ef765b38197f97d31568c1c31f225f +dadfa0db0664949e217dd16f89378800940a707f48f1e1cff10d57d97d81c547 +dae28ee9472be7e82f1f2367d552d64174a1c2dfc2c7c4b5b8747a8c2435157d +dae5de908ef168a61304eb5fae448b11a1ef03e0e4f5c0d5a1c604c3714bce10 +dae83c9a77988feb23fd418b86aeda4b7b4a40a6ff62116b52b4a1b6fffef767 +daed9aac49491991c27f0342036453ae92f8a6f191c6f95acf6d30a247d4ef26 +dafabe9df07bd6f4e04b2344057329bf97c430a4e894afc9fe27a89fe71d1d3c +db0acb70be9c1f5e356373c534e327c7bc0c6e2474e3aad23f21eeb30d666cf4 +db12cbb2a8d3e4402b9ad07bfe531c06ec1ec69364684069545bc87e1ab43f89 +db150eb99e20c5f78534a720daa3b35ac85599b229f75bc718f4351947b34983 +db1ddc496aa5f447e8ce161f6fdeb5011d7111ac87def075e7ec2ef4f698e1b5 +db23b258e4f9bb34b22d7a2e4c495999fd2b52e3e028da9113563600a68361b6 +db273fc40751925f2969a17b6ac168777a963516e96a1587a7c48548907ecfdb +db27a025184810c08855e14ea41c5aa59052fdf06668db8fbd7b732181146e97 +db3cf788359228775f31f753cc78595d1cbb2592b8f48b926f355836b8609954 +db4db797786e3b3f355e4a894d46675a6e5456e26a38db03b76df2387e9c7006 +db56638a90f0b36fa4bdca0e5c4eaa420cb67db967b294a7696e6a37a9cb7798 +db57ecf389c14af8fa5702dc48993474b15d7e344ed1d8ceed9194ab5dae4f0c +db5c6543b047a06cbcdba36f53393cf213318b2468cd32dd67d7873f61c562fc +db6aed2df8902eab1981805532322bc4f0c2d02e7aa12ef15a96a40ed8f75fcd +db6c2639861fe922c512f163f326ad3d70529b22acba651918ef0c96794c0c00 +db74697568902a232038690788c6b8d50024b994a1e1c3b0391d0709de220519 +db7555116cd31d4049385f3068f219bd30546c9e821458e98fd4a51f0fcb3407 +db7789efec85fce499ba8a7111639d3dae496ad38a510c7063df1e48edc89072 +db7c075097ce901ca4f64a7e00e6d25bf03c6f2d521dd9c2b7740bd7f8322bdd +db7f83440128904672783506c9d3d1c3fde2cc5b3eb9adf44cffca5f3a2f778a +db855e706871accaf50d5d38cd037773e4e4f9ea112aa1bb7267fa62b17ed6ea +db90f8cfa7b02cac54683d66d77f51503ae2c1e528b2248e19d1ab7789f489a5 +dba319368a3d15fa3ac07f28e2e57559be2f66df7983049a9342920c8efcd790 +dba394ae79aef959d72f2a11b6c6f1800eb8df55af2de2fdd6ae07e30078301c +dbc5515a8242df56ef615697efa3ca54459f22f49f78db6e91f96a25e0a90a20 +dbca77f74e0395b6fe66326d5f4f447cb6d69136e14aa789719934bb0705c021 +dbcab3fe207766e57f5827c115ce2d14ba81563678620027b0e7f5077c350845 +dbdc39d477ef6cde071d33da9f2279a33bc8f98793c984e9ab59985924c7d412 +dbdfb9d6e12a68c71ee00a5cc84c6979b99709e2b7582e30e8e6a0f9d311e002 +dbf05a510947fd07115ae594de9ccb46ff1b6175c30da127b08f636d0839004c +dbf1fc23ee23a1f1c785b94db9e55d9d07eba4f5755e826a7665240fafd0fa47 +dbf24e9d127adefbe4454242e42308eb9dfe9f8b999a9e6c01898523635b435c +dbfc7dc4aee8a2ce5fe3ef40b3df75fddff9435f9b6d778aba1f29d2d690e9e0 +dc180142593ac7e1653dcdf4a30ff330fba75f3ea86ec46e049d1290a31d1aef +dc1cd3cdf5c94f3e349eba31285d700588cabfd1061b65d48b3b27a46a5726e7 +dc1de9299a92b49df4bd1a230bb25aa35f43f16c2b5ea5953119e710d9faebaa +dc2175cd6ef9a5612200b47e3cee357ccd42fd093a4d2dcf199c26ed3f7fd3e7 +dc2890ab56309ec665b5e6672bf2c774487f07d8445ec9c0c0f18b5595414eff +dc28fbe695cc85a163e201bae3d3c5cd3faa0cfbbb4fb6b22838cb70079a1f8c +dc39715e4f4d744300b525690f00b767561638b553371601839d8909694e96eb +dc3ad24e6a0c43bb22a2076e5e01aa6ab5b5e2ecc40cb15b8121495f8fd09099 +dc4e3d13effdaf340cd89155ad26b67d91ad564b17308f0bb42c1e8b61fbef7b +dc59e4a18130683f7181292193166fe44438b7737aad48fe7112bdc26fe88848 +dc646a053fdc5d0fb9bc811ee78f5873f330870f73cb64a11a5b1d307815281d +dc72f4fe6a9f1905f2e18d10acc775a1086504c55d5a4d4aa065f3ea02b7ea66 +dc76857d1b640a3383342b6f9cf8fd3f216352cee5d447ca02551e96c9788d61 +dc79d8b0fd0dd7989c0a08f07c8f2b81bb1d2bdcddffb1c11186dc17d040ce52 +dc8926f87c12d7d3b308f6ab6392b8126266fe4fad79aa78feed90e942c2f5f9 +dc8df444b306ecdfb79cd8dcc6417feee073d3c450a34c3c4e1f66e1f2d478a6 +dcae47a8f0cd10ed1f1568b3b9d43b79adf86a3ad668d4aa6f7371b0f85a046e +dcbf430a4c7b2a9ea9eedd252a09d732aff6c4d8645c916a27d339992596398d +dcc16b793cfcf38cf024b39bb996aadd7c33a2a0d9c799bfe192262442142a39 +dcc8b184ec8379c35a96b46012d1ea0c7ec5db708c859be8d71d9fac12770209 +dcced4a3c023425ce24eba059d71c006e2b12f1513e0e834d6141292cc025aac +dccef20e6fd5af1b0ceb6f2591899e1db3e13a655a0ecb3dd0519fe2d39a1225 +dcd787b75e3bd24b67779a55478ed07b94821bc47e7360717295fc3f1d7db7da +dcddbe592c6425269b70720f6bbd3bb8214950b672fdd4a9caf5273d2a0b5126 +dce25b52eb9407894684e013f565717066f5adcbbc9518344cc2b82df818cf0b +dcedbcf2e34a5ec59cdd99bd24bf77240145aaf00b071f4cc488a81711fbe921 +dcf112ac7ac78bcd70b7dcd913966e5015ce80e40b1428330a78280a52c8d817 +dcf136e9156d1c2488cc93ec7a4ffa07f28ba76e6c0a48dc3608caeba9225b7e +dcf7bc681f12c5eca2459168bc8f2639388a2390c3ddd263396123fb116ed234 +dcf7c97d31297bcd262d9e628bca887e9e64735685e863716271c98b62daccf1 +dd0b94a4ef89877b42433fe81b3c1f525f09ca53050baf2b58ef4843e01366a3 +dd1854034a54e521be2830f67218f9824ae129c8f8331d715df2ebbea9a67363 +dd221201941638aa2879e723d3217d1813ff92fb822460b110892350852197c9 +dd24096a7e13e3760f19a53b617e6c2a6533374fed3157af1f7b0d4c1546be37 +dd266024b6bd795ba1711a21ab999f4983c54d9f37fa860d2e273dd7bd9611d4 +dd31984af777963aa15930c7314976954469626da3366e40ec30f00d665d99f6 +dd380080964c6d7e1f3dde0c9b9def4a91a0da911f3367267003c42952ccefed +dd5172780d4072aae735664aa9b6d6a6ebbf82e59b91ceb1a943a882a8d8fe20 +dd5b4bcbbbcaa65b85b5515ebbc0ee87c27b594e336f7065c77e96e136937561 +dd61de4204bd439fafc062db69c9e94079c6a30d8179b3da7ce122874a70d7b6 +dd639d9ade96bcd60424e79b9a1d7af5165ac09642b1705192bd8a3d8ee8a632 +dd7e2be93b804cb1d891eced45546632f520b0e3d37ef266194270f9d4b4643e +dd84004272631073994bdcde773fa6d200cca0720a85a22759163bda2d0f7908 +dd908787bcab9b4a155f79970959916216769769382b69a33f918a8a8770de96 +dd94e05068fe2530371911fd2d633c9805316dbf9b94b99c6f257b532426f9ee +dd998e006a2a76f9622c911d10a44391e9fd08f0923f202d146109ef8d00d5ad +dda15a181ce161e2e923dd706a4b76790c8c3226c0de6c9868ae7bca093f224b +dda46ad882c13f84a537ead2b824c8fc87b893f53db74c84ca55da1af251efd4 +ddaeba022e20bf6af720f6477fb520435c26400b55a6095ebcd60ec4edc8e9ff +ddb3d25da60487836ddcecd6249784cba660cc1f16cc123016eef350f1a28954 +ddb8c386d2bf789ddfa92b0e102893851b2761ae8734581e0726b47fe083554b +ddbc75bd97f27befd8a22fa1a4110ff41e9aff867fa41fc6adc246460ed1e5f7 +ddbd4ce0261538d5228bdfd160e6c236c5b85031c3e19d12b94d9a94c34d0767 +ddc6311ba2a6232c23ea24c286eee63025342bcb123d573198c2155ecc495c43 +ddcffae90fe35e5d99374f492274cc9f5330f8aeaca0637e51eb1577820e49d3 +ddd85d1978f7a85c38a0a7fb7d2318cda9d3185f5dc4620ad52efd35b3dfe490 +ddd91dbe5df2ec1b6eb9ab528590a81c9a164f5f0e28cf720aa6c232c255c647 +dddb0839f1c14e53b9f39dc1a54f00de60ad649e5e0da54ae503b144e7dc4077 +dddbc2104a958004d0e3f9d3a6574343aa578234be45c9428d1fc218c9be382c +dddced964d5c9de26e507a20678a70eebb388792bc5c742fc130745074bc481e +ddddb458a1bea6d1232022c70edbeefedce86f92174f9579a2f584b1f608eaba +ddddff24caf45818918a10140f9a0464fc67569bca2ece737d6ea4575f9994ae +ddf97d918fa3db0705e13f21ef1fecbada4984b6b2960093034db4098ba4316f +de070e22f0806e4a9eacd65234ee0ef0d642d2074222b55e34cb8acf771afb92 +de1aec8dddc28221b3cc8e617b41bbfb4fef08e53cfb2a049bd360e2f3de8415 +de2158b4f650078785ce801cd3c99aeac156d1cf735cef644d34dca9231b6e09 +de217606168bdba9a3f42afc4533fbce4020a228cfb5de63d2bb507a53ee863f +de28d3b0fec20c14ae5a29354f461fb56f28bd94bf36002d30f29312b8aed0d2 +de2c599239026038f475559485d273daa8466b7d7a996b3aa0ec91e94d382165 +de2dbe975210c088089d6c09b4d8b9ac1b51df2a8b6123cafce7b5850acfff2d +de314f211837989810c8fc1969e45bbd53e74c6d50b5f4e41904c43e91e42c84 +de328f39b382502e12fe7fca9c0f027edbef49aaa81fb115a56de182f3c91bca +de34d477a41cb4705c4178b5af9ff762c14f8fc3cee6c95154ed28874e9a8ec5 +de3bce9798b5c563aeabf4a7234d4d240ac20382ea166e1e23a59cd1fe3a1864 +de40bd4a88c345ac59092e3dc1321903f7025328b4333046b999dd022c445a8a +de42793ecc0c2f555f856e600653fd8e0b9cc09cbe595c72439290e8a5fa1780 +de477837a230c243eddd83b6f1ab7cadc6021a6952d55bb437708502a32b481b +de4a3081a6416bbea6f2beeae55ed525d13f2288683b4fb5ea34842b67f98c42 +de4a876d12553a06f79730e80574b800f3cb55d304c5ec3fb33a5adbd6d3949f +de4b036278d9912dc5818b41ed17b452d9bce043b4c87c792aee95e27279c591 +de5707f22264e22b47be9050f22e12b29397484cf93145636054b6546c4ca7b6 +de57446fe7c3c6da913c59a9975dfd9cf42368bc0bfbaba67e2b2a3909ad9c42 +de617b2dd98807c227400de1612fb77d0ad7818d6ccf9557d59b34c03e9b5420 +de64417e1a451458322a13f6a4e8c9481a2af2425ce01c149040778a63965976 +de64cbfbd781f8e86487bdfed13f88344ab5fdbb64687139b4d8b644c358a4d2 +de74a1a97dc5be158cc07771997492630fd54afe71cc07f677a2d64dff068e0b +de7701e8f7ee860bc3b2e75b108c355957c04e6351156d7734438862a7838e1e +de7fd3792e53c0d9dc1c6c22f09619edc3c6ffc3de57bcc7bc85490eb85ffacc +de94733d83ab47b933a22d2b51cd5d501c269abd534ee5af3645d3b47924d5cf +de96ef980259e07507e31fe7f24c0b93547ab4a737f321f7c76221eb2f5765cc +de994c81ad2d78150c4ca6f127159fa632b96e020c97dfa9f9929e491bcaf58c +de9e492c814f908ec970b237ccbb59823c83ada4f993ee7970b5b8a191efbbc0 +dea26b7bf413c88c32c21aa1a7e2dad685661edc93ae95892aea6223e92c5392 +dea6a53533517b6fdb87d951fb2d050c08dcac3bc2f58f8d739bd80c110e6435 +deab6369ae4391628b4db12c77f20254b5923312beca87eb39467e5bb5add7bd +deb6b2f8c88ae09617befe798843f94fc4e00fcc8d37c41825ae8d743012748b +deba3f68c234ac2f5eede972d8f50559378497b07b5bc98054e454f33d66c90a +debcd045e689fcbe3a4072de7e68466ea2748a69ee5128763f1ec8c7ed0c2ead +decb6a5d34c47ffcabdacb63a403a0bbdf99019b689812253919e5eb6cecd310 +ded359b49b3155bf77c5bcfc5497b7930007e1ce07c97b1d7d1ae6bd6825ab96 +ded44d5220954fee3566ddb51c9aeb224b7950b3c938a23ffb1c6902fbb63360 +dedc49dadafaee872402e111af1111dcfba59d679b43495ec0b2c2c50b38240d +dedfcbf44d12c9076ceae99b3f0266ffe308f0159f7cf4c84a9c6a3f53554a56 +dee2c0902f2477afae3af4b28d7eb2ed4a1568bf6be1e359968548876377bbda +dee4017a940c91e3e518f8aa02ba5012397d1eba06481078310bee329fcd5ebd +deecf2d76c40c5fa11631f74da70d0fe13a6e89d8835fecba39d7c77a5fbf18f +def23883e803a9ad61b30f055157801c22af59fbc9d895bc45fb07d8b7a73bfd +def9c696c788d879691f9592ffe6ab73abc7ac0a8b27cc2152295f7ea744bff9 +defd997e9875c7c46ded662aa77b63d815c7f2df91a36d68ea5f73aaabac6bab +df091a9ee5149b36cd1824116aeec427f6a652310ae42c0b98c94afbb7ced885 +df093975148ea4a263754e7d9ee661081de760c293cf0e07069ea199a139fb69 +df209d538e603fb88c6a48fb1460755dd3c604734e82e0b6490d2d4cb74f9f44 +df220c13333c38fd31cd9818328a8f8b3ecd9054f3d83f8fc049082ea8995fce +df25a773de4bbc5eedd20da8a683c11398c9a5c054d0d77b6202afd90fd2dbc3 +df32b4eea5962c3cb78ccf128a664d93c6f4f42ea42d965bd6aed46861152155 +df3877705d62bdf23e7538d5f9f86728f65768f4db9b496b491611306d0ee064 +df419779a6a351888c6f2b7dd164fd92c7b6b115adf084b7559087f49beda84b +df419e0a0548213c392dab092039f77fef6919299e3dbb5ca4fbcfde3cbbe109 +df491ec381aa8d4a21ee38e205c529ca237566212645bd90ef0866967cfcc976 +df5e286071e6fe2d25c9a26e7115461c7476dc34bd00a274c7dc95c964a1ce73 +df5f62c45d47db14284dc7ae2026cff567b3cc205f69c6550a89959a310dba3c +df6037ca36b4a20dc29e1449e0b1087e6e2f20abf2e56f6ef60ffc4038bbaf0a +df659f37348e19524cbde49f888bc123b34369724da50449c7b8cc8d7babd1c1 +df665e586a34b2b38775f5608da4fb450d568e0251b28900083e220df869fbb0 +df6c3753df2fecd5c920345d9fe1e8d17cd933972c2912e2daf71d76d21dcd4b +df6c634d86a6767bb603b99008eda3cfdd6eeae31f61f8bd733a96ceb30e2de2 +df70c99f25aad679725885402a35d431b72ffbcbff48199628ea665aa0258292 +df748aa395af972289b73a9be51d469431732c6b65d6241ee001b5218f238bdc +df7c3ad45f6df85a7f7b4d022cba7ab4bb65b583369cfd0c9ba304bc3ded0a19 +df871a595dfd256e2c349392e91acae9f415ca1f42ff1c77a2578fe62b634d24 +df891d2f66e48ed62d60c4b8c0fa3c087115d1f4b21858879096fd3b67caeda4 +df8c852f13df22a5fd00e53e28be9403a4574a705c1ff9b70dac5c2bd72c0d3c +df9092f42928fd125198503947d083d98bd965eb141592531891a96de4e2ec16 +df95688c1359556439961acbfedf32253c52818083b4c6d55416b4a16f3b44d4 +dfa1eab93e901da052953c4bfe73187f91e80d2199e83d6b6cf281c11fc62b29 +dfa6d91f313107bf23a4b70b8ccaf8df5b96f114e4efb0a1ec80ccf72e8b8fa8 +dfb3e964865d77f7ef3f83cbb513d28c23313e4705226d52dfb813104639c0b0 +dfc61e49ebb902c2f06f9cc3e76c44bc42a77be8b68666d90949560cca8e153e +dfc8fbf28f9e113fd8fbbf51f9a73ea7e3742655d0d227edc8222c487a50cdab +dfc9a0f0191f0c77c6c832548f87732f0a8bdcfe68a6601654c12ea688eeb0cc +dfd188aa6f6dd56aa7122d76585ca80293ed1ed732acdab0114e650e1e90506e +dfd28d28b21269e5b0b74d685ce2b5a717acc38297cd921bd93687678a8768f5 +dfd7d2388588884ad49a1b64e92d6e76ab1ebec2396061ff766756da32e69edf +dfd7fd3f94cd2c1fffddc6350fd557167dedd83494459f0820bb46c1f0539583 +dfdf1ec8d69071051305176c7e335a7460222b0bbba28a98f82a79837bd75b52 +dfe9bac7ef8c74ae6f38f569c6bc73e2109dbc9e1f7de203f7f36e2b9ca7ea54 +dfecc1d0ef1bb9052cc8d5a60f41e9ebc7a9957050c19be797c8d83736bdf449 +e00c53b7216c45c360db532124f7bf27577e204d13e04ea0207ba82145a4fb9e +e00ef970264f871a11c5e26cf51e5823fcead76811589b4585e625c7b7c1ee0a +e010f37c71f53ce41bad01c0aba0977008a2f9dc884e58742bd70c7f9e8617ef +e0190cde98023f7930417635446ec91f745d56c9f8adab137dab1be46bb7b8b2 +e01941ade3eb9f5bd5ce3e0872e4567294c72fa798610d85a62837bbf22d476d +e019e1c664d8f3018851dbc3dbd7571509a38a4e28a685b9d833441f3ead39f9 +e021c77dcd8bfc03bf2bc484f89944f456a1bf681890329aba5a3f0d04ab7eb9 +e02501e472c6f8f752b573eaf32ba7920cd72672b2267cfea6beebec9402615b +e027cd17bd57784c1856c5ce8e6d8428bd83471ee352ffa727a1a06c4c48400a +e02f749a8d85532caa55d94dd21d7c0b4deaef5c64974cebb53b0022ac490f13 +e035915ce906679129bbe005cff41134078f722691827d37f6e8f253938d00b1 +e039e6f518cf9b994d8721129b303e8958ca43ec4b4d9945d4daf39519360842 +e03e6824a73e092906ec14060e0d03c8e79c86e5a8180f1a154bce105a1e764a +e043a3eab461faf592eeb3cfdc39a489f14b9709d4102541b1d8d17938060a6f +e043b219c20780daca129d6efcc0162e4d50ca6dc759b5dcdc2a0cc418ccbc09 +e046c1a04b37cd9838100a3a9fd16a484242ba8bca5465a24714619a40b489ce +e04e5e8bb2c10a956dfe9aad3ab2630ac513ce6d277f72a3bb03820beb788bdb +e06ca4b85e83d5fed31df0cdcefec7137d6f1066b8ad36450ab5b6c9ad0c482c +e06d8c1b44e68aa63282daf40de8328ebba47f421ebd2a951bbea4f53f4c347f +e070069a4f9ad9cc18e77cd4dd99fd462b50a5c0c088b8a35997a0a4c6a9efa8 +e070dfb966d6a2c3bad0a83db1815400fa9a8f2eba0ffc23813c4292ae9c686d +e0819e2c5294e5f4ea80fb5f879293748806d76a24efe20fc0ea67a6c09cd0f5 +e086abe5b2b50917680e6666752648b53dd8e157a45be67cf5fe15eddcef3dd4 +e0882ee2fb9f31cebf41c7c756d0055ebdf660e38abb3cb30bdf265c6cbc156b +e08c7b013f007b41635b368a252b2b62692cfe00fa9859689e882c86948ba0d4 +e099d24fe6c2c0aa4184492bfc1313b62b2cd901fccb50124627e82ea85670ee +e09f0b7b39d8c4aba8efbc566ad30ca476bd61d1250c556c4b7d33fb93941fce +e0a4e11c840d2f0d7ec499a369744406a79d80f852cf79e80ec7c02a63b666df +e0af5a885e922bdacaa906baca218eceb8246b245a214372d9cc24a3c3739ad8 +e0b6b8fe5e00c6640ec52ca0caf4acf7a724e54d77397b23d78006269916f6e0 +e0cf1842e831fe26a6f03755d5e075cd38689646633ff60b99b3fca7f2299a6b +e0d038d9699ba76886be0d154644452dc1576dabb30a6e6ff6db477fda309e0b +e0d073cda5ac898a85c100a02aee2d2ebc1240c4daf98d929a7fe2aac589ec9f +e0d29d9e9b7493784d02508006237868ed52e21f7e11337166d89ead112a20c9 +e0dd090f6ac9462efb68d1dffa06d690e2ca6c5aaf4098294a95dce6375e52a3 +e0dfcb433f8230eb1db73cd6661efee6629e510a50d52be3bad6a649e8588516 +e0ea36f4715cf9ebdb65d00d5b56226d81eb0c30e06c557c87615910d573229b +e0ef539c3af785909d30d57f21cc891f79bf5937e5bae652751a0c9cc6b58b77 +e0efab415a700a40434381e0ebcff336a304745b1b59cc74cd2998ce90de2a5c +e0f63a9b23ad3934bc81f45c07ae4413d232f7159989ce9ce4ca0c4f1b3c2050 +e0fc8fcd5b755c2be2e763c15d29033fbbaa79da45824ae0faebe33b9af0d986 +e105055c863edc175843ee53d4d602c5b93152d0bab1fd658bcde55a2f610059 +e11733147f835da05b3861d300326b1d3e605acc6a3bb4eeea1b2a8f38675cf1 +e1275d77ea349718b27e18a9668f5559381b5c84c6f3ebf30b41d59629209e00 +e13d421168e920cddcb40da558d8403711d14c7c6ff93ee605504b8459f31f1b +e151c01853254c63634dd2becfc10168b3b8823777ab1d94e4880e46efcdfcbf +e15ad6ed2f69c2a59a77e8a6fb1696a7fde139e46dd842b64ca2ea3661d94256 +e15aff33108a7ef0dc2068e33a7859093844bacd6161e1ac3a45ce88ecf0dda1 +e15c87429bed591a83589f6ce9b68e931fac1b4e86f8f18f4bf548c79d2e7dcf +e15e43187f731caf4e1b9db898acbcfcef62bbc60dfd3059ac6902dd64f89594 +e15e7ae2dd2612139953c9d8a2ea53e6ac5c30d254141d116b5e52b124779216 +e161c33139311557186395d9676fd8e30d08f383a1b519769e2de9256920bc22 +e17366cabe496f3af9aa97726c92fff1ce05fc50f2006bbdd8323c1c343fdb52 +e17451f6ffea720d081d0b37bad2e8ee39b5d3aad2bad0301938bf352c8f31dd +e18c1f39b0c307c89230b9a79803f8c9abf5a2e61234af6ca38136a41ea7a8ad +e194d806e9af60da93cb3c975f86e0502b3a4613ec944884befef6a1657ff542 +e197d2d94db45503da05df2b90d482fdc754037810179718cfd674127561d0f8 +e1bdcaaf46314c5b2ce0c66eced44490b2edbdc46f80b7ea959c8f81b29d6428 +e1c0f2dc9933dc13091062494589da17f9c7c56d7c25c55c8fc2ea50af185d88 +e1c75e02001d4ee8bb09f0690795e42f01d27289560347bb8c618fbe107c5dd5 +e1c7d40c7993a1345f244e2b620daf7669830a242cc21a24ee15ca350502ed50 +e1c9e316d107fc7d3f4760eb063043c8c68065ebfc8abb985ab36e7898079bde +e1cdc7e0431d04f2acf0331aaada877337d180c8414bac8104c5cd346272e37e +e1ce5be3a72eecfa11f80065c7eced6c07291d7570e87a3e07088427925d1d54 +e1d81498edcacbf4c1d55e81e7bb88f837860e97f26d7618d638e890417b6d22 +e1e02215184d60290ae603baec2d0b0d286db28c8c75728762304a27dbc4a69d +e1e115e49bfbb633a33914699968391478e05cf81009bf099c990f14b0f7229c +e1e3f5c042a6339e4d437ff2436f2ba58df725c178558cac17c1d980eda67012 +e1e46ada33a8f3846751d1f006a60247adc46ecafd1a07bfd85d591258811f75 +e1f7502930bd999c5317f3d46eb41e178abe0db0f03a93845452cf2d68b3906d +e1fabd853e186046dff70bec69c604054f729432dbce74bf84aba1da2cba0a0f +e20c4d6bda60036b2546f453740dbeb6601e5a325389dde90277f0c2c8e4235e +e214f30ae6de0e651186692b2d083913a4311dcf6453730e972da322192464d7 +e21bc60e55dad2537aa5d9fccc576e77e3a5df0b68e9d9586e638ab58edf9396 +e21f1f432e224919fee8e6d8d38465dcd13950264795333ae2a8500b4820af81 +e222b6c2e22847c09d7c4940ec33f27544be7752cb046e4a81af75a97e81e42a +e22ed323d82327d55a75e7e4ede74d397ac0f652a7e652b1a5de8a94332af424 +e2326209a7b573877f82555ee53a889e6cc1b656e9f7d03616903ede551d2873 +e23d43af67521c837b9f1d74a733908bb817120d87546f67f570075fd2101f1d +e255fd2cde8b80d13d904cdb869d871325842b1be8c0401bc2b109b472675178 +e257a59aefb47451d779ffaf07d91744d7b807c10196cc122c6c54f231ae7fec +e2631911c9b039ade3760d25736ae3066b7bea891740fa68637a8818ecbfb75b +e26b9b1608ec6c2c693abbf931ab0ed4c2750dddd84becb47ab4ce1b969dda08 +e26d2c889cd81a9f69869afbc11ab8dba2246f18193d169c79ca5c38669c2b3b +e27cc7ebcdba15396fb4d2f1c6da3af9a5549bc9e65506ee9d6003848fdfa39f +e2809371de02cc90f4e22b2979dd66e6a738c16c004d784804dc0c027c0bf2a1 +e28a4a26033bcbc1d400940bfa76b108a161218797a8a1fcf88f314f61e668af +e28c8979784d5794bb46dc0c432460ffa1e74e8a2749c99242e4301a115273f0 +e292969e7aa860ed132c7269766acb9446529c68b89ccb684e2f9e3d4c38f2fe +e295a815838234fe183a87ced71554d1d1eaa24e27a42044f396545e23a73c76 +e29b15935fc33cbe00f6e5a057f48ff0a30f165f33f5c489dd7e7f3a05da4216 +e2a1525222bd89d979f78befe8d9c6089ae34e9cad4016110b13d6a3c5d7e5fb +e2a678086ac415db97491afda650e4de98760261bb8a146d23c672ebca1d092a +e2a817a53321777df1591a5198ca55c95087f87bc537ab947c7c1fec0d321e89 +e2a9a68c8f8c8f8a79da4c6883423b0276770bd43d7cf966100124f6f2dd8481 +e2baec9651ba2a98c3700d2e9ad45e885233608fb2b9784d1864f4ed24250642 +e2bc90d82aff164dd54445c8125aaaba57c0b879a3e4e13a45cd148f4acca564 +e2bead567029da25a6732289c347ea394d2d80d7fa91207501e7626b725fe7fb +e2bf9feb17fe784f91ae8fed66cf98746976c3902d6acba86c1264f59331df05 +e2c19bf8ea19eb0e7fd930f8898251c447c96269289c6bcd40b87293b2a509c6 +e2c6a3090eb27cb1c0b9fdcfe56e047d099767fe8255688fad9827f7ca1c3e6d +e2c793e12565f254896881ba04c07602e0b9cb69afc7a08c922d2684837d4b10 +e2d1c1fd0ed438959c12e422dab349e5be19ebcfe2d5656d0cfd58d571a836c9 +e2d42c4b0fc8cbb0a3d2349bb27371a12f7fdf191ca4281ddc83c74bed93b28b +e2dd120e0c2a12f6db71b1b54e2605adab2f4d118d1b5d66de2e452de7277936 +e2f8f82bea6c2def9543f1f561486b1faab90cd2ce9be379fe738acf1997944c +e2fa1640fb18ec42acfaaa0f1b5cc2efd53fcd1cbe79e6dff35b9f4df5f27989 +e2ff98571502bbd532da3f5f1b2804ef0021791064ecdca331d6f2d422e4ca5a +e306b28c80913193179fc80045a1138fd3804434c832265266df12932ba17ac6 +e30e5510c2b98fc91f54d6f0014cb42f04bebc3bb6ce6b9a10d1454923ccc673 +e3182b67ff04141147921d3f2b7aeb20f7b7776815e7176f7178428a06742a6b +e319418849ec3f90ca731bf9b28d641bc14077d7beb5dc84de80a541bb1a9b40 +e31e6111bde13a915cac10ead5782b4c55cbad37fb94d36e2419dd123037add5 +e3526800aeb82ea92e74dd02648200bd3118458771c23bb7cf7441b0607999d3 +e360929f2c54065ac2430e8df8f40b4f581cae1eff463cd0e8d835d22847e50f +e3667acfb491de68594a28f592991af3722d1ad17ff549b5dcdecca1a6c7319f +e36f49efbeb150957bd82bae812346f64aa2a6aae93ca1eebf305af6547d1732 +e372ff16b6dc52846fc7b8b47543461741cbf740b73614fbbc58ef49dc5dafd9 +e3740ed3f54c445d3f3b1bfe1c08dbe15cd5184b5b63c47dcf6d779ea106526f +e375ace8abf795b81134105c4cd6a7227b3d521d44a8d9e090eddbb8cf09b0a1 +e384a3fd861ef6df7713c854750f71a95fa77587902e61e85486791f14c45a9f +e392f43779319d016b408b5908dfec61bced28ba693d280f7fdd43e6f7d495e9 +e3969e3b356867ee54105b3b1290b77a1b341c4929d5df0498861c00f58d6616 +e39d0b294803b2ff8fbf024a6b345b2b75578a221608ad7f9a723c186b3f59e5 +e3a38d12401fa7f18c1cab21487c98cf20a7fdb050cdd74574dc14180e7f6c1b +e3a3a946d42641c1591cf740211891b8f9c0741d2055dbcd67f282a51983b2ec +e3b27fb5252372c57ae4faf6fb84b1cc2a7232ace49f451c6556f15527ae2826 +e3b391c5b38a6ed3e98e1d8ed6c27a8587cf36bbb5c04a6fa57c1d37933295a5 +e3bc324aef62420b8524583a6febe7c2b764709e47f4feec4f05186a486a91e8 +e3beab1aa775fbeeb58e29d87d2d914589cff1e904b62703f55681af605bd78c +e3d01f48e2e136924a71f1d7d4a1b0dcdece3f6049459f96814e87a35e1d56cd +e3db1bb5bb62a8f02f2bb93b756cf11c9885839f3c6a8ac73698d5ad0beb2a71 +e3e850dcd0cd893612c6c91d522b48d394abee3bc6db0919b3a5ef5722f29e85 +e3f1a29b7bade10fed9ef1d847cb5d0693f0cf9245bc092d3e35acfc6bab58eb +e3fd67f91dbdeec42c131d6fc7497afad0889bb96b9df10da91e609b5a627998 +e40626fc4fdbbcd7e95355c8b5374c3459eaccabff6d9f899deb6b9ea7d1ee7b +e4065314baf6af42d2f63cdec4268c6cbfcec96cec3d833cda2c61e57de82e30 +e406b951cd187b17e7d04dd3c4af77f70f45a0e5dd9292b587aab1da67ef35c3 +e40d624b29f56716cec8c9124522bae80a590ecd91fa623751124dead552c4c9 +e4127da4123e615e79bdac5cb0f2bb6e2bec65cd4d070bb51620c4f62c0e31c8 +e41b349a76bd5e0c0a78c5b2e1c4947af77637ac021eda84ed591cc9d08489fe +e41c10b6dabd191683ea42dae2eb327bed664a8b0ae2af07530ed21a804512b8 +e41c9f0acd0f4bd84b24146313f71a5b5e2a7a8205bfdb582d80bfb52f5e978d +e4241cb12c6ecc14e09e74881a71feb1f7ca900ed155296259c120c9bcb0dc32 +e42b66e27a8b05d84ffad1cefc817cab83b6d45ad969a619996ce3a4aee8552f +e432f67c0453c667147b2734f13c45617b484b863ee4548df0ad497b99539a49 +e43af18c43daaa7e51f76f90322173b153fc51c3a4af40cd872c14fe65b5023e +e46afbedefcbcb9194151984efc5061b0eeff72db865ba16254e7710275fa039 +e4707b887b7dbe9e6e757ad8c2295bc76e867b15189f56ad1c69cd4de92d1edc +e4718353705fb132a43704b5bf6fbec76818e1cad94316863fc38e70dd864a23 +e473be706a7c30068a7f76179b30a4267c3f836e755afa72e4d594e5e2f7cab7 +e47d67c65e8144f9f6c7dc25c83aab0ee5831ba8f1e2c968f26bd48a5d17e4af +e484656389ed33975328e27298b2e867ae6d4e31cc7f75b3d2344ab28206b2a6 +e498a40d1321cafbc260c66dc0ac163860a8a8c6e6799655c9e712eb0c9443ae +e498d2cd651888b3b8ce291dbaad3fe273f24f455d55a5499b0087f8e86cdc40 +e49d757cc53cc4401e9a92493eaf3560cc1d95a5566e8c53daa6b8b1917fb5d1 +e4a08024a237974b28bfca940cc9344582e04fba4de9d917f8bbfdb73d801a1c +e4a608d41e7c2def5549545ce7942513e6d8193c46e56d82b170a77cb5d6e793 +e4ab4b79835c24eef46851629305ca2928e45c3de8ba0778515fa7b16582d84d +e4af82449455e27e6cfd8b0a0e257c6aad68d0569c0eff6a25aad9eda50c872e +e4b0437b3a43c91f7a990be649ea73b41fe825d94f39be7a6aa3b0ed152496ad +e4b3a82f37cc23bf6c0f6b182f4a0d79533f96cd39adfec7cc8c281981fe7b3c +e4b778bf5aab9b78565b2fc58f740bfc44c3df980ff96113d51676db6f887c6e +e4c9229dfc69d83151624564f1f1401fa55ab31429b22895a3ae41a587fc511e +e4d826889c302d4cc3ccd3b4e49ec81d4f5712760b1c90d8144eb2857bbaeee1 +e4e24d89208299d7cec446a966f85773d944e9fb438a8b76f5ef60883eb06246 +e4f923f82fe12eaca9be3f69227185df663db6ba454aaf340f038c8c86f3fe6e +e504fc1368d3b771d810c312073c49ebb57379f8399b7dd80a625b78019ae7dc +e508a0de94e7a099d55baba9b02d2355cbf1a72f31c7d44b9f9ca3b2e0615921 +e50e558a8ef3ad7abde198b941e41e5d7043d46451bd4b6cab5c9497a3921979 +e521d94f73777b6ec76ee05b851e820219ad343e74d0f948869d40a153a124c9 +e5275e84b7bd01f6968c3ce232890322345d760292ec79b4dc2b8d30d999a4bc +e53f1e62298ad3e7d2370e855edb5e314445630b1dfbaa4a2b07ec06dc816c61 +e5443923198cef148d11542ee41a260a00a2cac0f3b8e7ca2741bfae687cf683 +e54ccc777f4de11343c0f7425adff4f26bf6db239c05bbdbcb9346667aa3d933 +e55273e63ad7d3c8be8976a940408728f3d6a3b1d3a9e870dbbe9c749cc366c7 +e554dc012b0a02df8e02fa1571200547947be8178a25564118c22db106b7f78c +e559e45795932b4ab634d3f5af65a6d4cfba8c1010cf7ad8275ba40a3d5dc6f5 +e55bc5f6520870ce7a1ef96592d3571bf5e330df3775f24f564b9889e051d476 +e565fc6b5001c8b26d9594fd905ac767a2c9fbb98c6284cee87d42a97c27667a +e56fbb68c08dbb9acaa61d6e83f726865617898ad4bfd5096096692b9510a6f5 +e57f88985d0e558cf1f5c7084dd5fb533c1208d27a9462846ce01d871b6fc40c +e57fad1f3f601dc484bf26c41c6408a5191b8521b0229d2bbb44b44e0a6552c9 +e5ab5a71efc05045e42563d411a5cea64d29c8e30cb03f6492e2db33a6b3ad01 +e5b405094bda608cbf74996b5202afe93fc4a8f60c92d3b931c607b58b85cbe7 +e5b4872602e8c8157584323ba1449467024fc6d840ba40e5d96a1d68255dea51 +e5c0fac6ce6294026cec63a90648cddad8326ab2af75c3316c1f605315e2a107 +e5c2bd4caea2c00064359ddebed3f7bebd69752c8ca0d6f2b30398756afaf53c +e5c619fe607110944c4429a2b9260707ab72fc13afdb9622ee54b281a944ff46 +e5cb42c5497bc75441218d04206f5ddede6549dd62b27b104c71d0073a7454ed +e5dba8c1f10247d742aee85329e24922ab09bbcfe936a4f4d480c43169935cdf +e5e5982cee3afa898a9be431f11bc4d5e2abe92c6435f7d0b7fedc0d6865278f +e5e8db6b13cd031bfce8fa24fdde97ed235d95df56cdb9bb1c6e31c52480bfc0 +e6016a7fa235437b5e53f2fab13083a61a356d670159db3fe768872203ebf410 +e60457a5bc50b7ab8af05f37a0761183e8cbc618d2471f699f797946279347e4 +e608085d71e748e434b66ea5bdfcee2a0e0f66d5c28261e4d2e322f0a0c621f4 +e608b1fccaaa5a00e158ba98ebbb646504bda79d86c28eb79ad6e83e4b854eef +e617a7d0a6083b9eff53219350a849e6b1a1541bd0979eb7de4979b5c85915af +e631893a5df86cdf222521ce5607a5dc3f9ed1113b4a6c3e665995a2e3d4496c +e6329e096ac52e48c6170ef2e31160e29b4dbd13c309adcbac0469ee4d0e6738 +e6335e43d8354c60c87b93b56be2178f29c526504ac48d0c9a742a8febb71bc1 +e63866472d3d223226f6e4e4e75a5796ab8bfba3a6a758e73907ea00d4194936 +e63c41599b310c56d85b5534db852df83d6a83028bf8e5e618215142ef994d10 +e63d1eb4ced02390a68d9eb8d4725bfade04764783b2c027fa4082aa2dfc7c03 +e63f525f2c7a5dbcd0790177df014f95e9b5a648282913636657fe228197f048 +e6447d38fb64a227ccba30310094c4b8954d5bed70d45e3c6a7947ef5d3508f0 +e64d1657d824ded865af12ed8b5d677bfdb0dabd33d89ffffaadda781c00870c +e65170885cd329f9b3c19358f75b719237b60316ba2fe5cc88996a141f4cb5b9 +e653ced61f72c5e155e9c3cfc227647656fff2e68477fab8e0677c5c6ba34761 +e65b05a3bf88a9fce99c73d8aab3a5613d33addb729018736700e98905dc976d +e6698318ed586c5c2ffff25faa5f29b57cd92dddcfb3d6416bdf9c409f8ee5d3 +e66d25de3eaf3f1ec7e5c707d05f68410f687727571a102892d3f40e46cd6aa1 +e679563b1787658cdb458af5e47aba31fa7e52326b291db230e74bb9e24437b9 +e67edb746d4f922f601b959626412bd0edf2a2fe354502a6997d5159e1d23186 +e6863f20d86598cc3fd258e86ca6e7cd938833811c80c614f1822d5f82fab1f6 +e68ff22a1886a6dcb4578c6ce3d2e59e5e21d36fda16eb1e759159724e4370a5 +e691313432ab6d149ac1255d1c37b19d7bdf24e08ab9e38ac86e71ae244b9f0b +e692e4d3d06d8bd2ee7a3c0bdeb6bc0957d0d727b763375db57e12ecd21c3189 +e69b19687e10c442111f6ad0c650c32cbe561d841453c0960a08f566ee7a8430 +e6a06247651fd260ad58a162ce3a4d6932f077307a610da4d82dcea42b7d3dc5 +e6a2312bdce6b381b89a19277800734e0d83927df62be46c967a994f193e3d6e +e6a5c2c0748cf3eaadd6399be6dc43f9a8c9b4717dccda23be73cfc7d22f559e +e6b1b3f98041a1a7498778f4d6a2b1e10c051ba7fd0d78729bc284b7b367a96b +e6b8ad93d19c69aa93ccc5c21474122fecd5d0dd41b57d8773588397f233f825 +e6b8f0d27d61b07037978e4be2dd2f337c369b1b4f1cff30c57d040aa336a25c +e6b9b5dec63c44e523ae2173a7804d138f7866000bfc94f6cadfc8efd117a6a4 +e6bb34bc14635ec08a2f292bb7a9d38c88cbaba52a42b17328ef2ce32dca4530 +e6bb5df4f339a2b9350504b53bfe2080d5dd39e13cb392ddfae032e8024ec9de +e6bbb9e68f49d674fa90e1b05919a8398a9cc37e8fd31815445de3b17f3b0b3d +e6c9285ad3255049efbb296ed38f8c3f024423c5ab7ffba5899ae8c7f18d534b +e6ce8fe4b95748363a50b3cef89aec7d3aa2420b86301479e0783bf5411c0a85 +e6dbc34ba2e0ffc55ceb78fdb783afa13f90196d31157c2fef17d7ef5663dcd4 +e6dd97ab827a4b1a9e005a4696ac7c8c0cdea7c5cf21d746d0a55653f977b284 +e6e69fba90a6835e4eeb33e885d592d3e22c1149aa2cb7da82e5a6f04c8c8bea +e6f6e0a9311985e18b35f6723298b35106752662e972d8cc19dc984d2e7638d1 +e6f84771cdd6cfe8c5388ad731fc9d5a7b850d4eb38b88ad89b30a0d8505529d +e6f8fd083bf3912cec89c84295bd4759235e289864aea3555d0f0831cfa23626 +e701c8f9e4ec7a1f845359636a99fbef89572f19076df717190b8610aefc7dba +e71213343e94fd32972b23c46c84bfec3740ef62b954dc9cc50eae4e4979fb8f +e71432789a9ce7ca0a606ef56491a93569534a551eaf827556fac7e2ca7e0385 +e71a18190f5675f315811a3655fceefb481c792c0327c8494d53226649997e23 +e720adaaa6a6655f44b43ac8e8b80d3c55bae74984231e1fb90559305151a257 +e720f9725e23baaccec27fb6f035ae2f360ad5bdc6655a6c90370319cf1ca15f +e7210c4fe93fa0a033e169995732f7379224aa6852264fb6a5baab3c85e819da +e72e2a5b59a9187152a75d47c7e42a8f3335732e055c396ee90eae741bd30583 +e739f6fbeae22b285484fab7b46b36d3723d7ef12bd498ad72cde3829ab3fa47 +e74a6d56492b7ba54b7e729f658a991450c0a5c4837ae3b79e40d962aa7b2b33 +e75ae0478419a31b43b8fc66a0fef52378e05d9e2e16827e1288177cea81cd10 +e75c798dac695a2428dd61259417a948779205776239f2b0351b69c27f465185 +e763d141d54e619bfbdf50ae1fc720bc9199f175ccc3a090a3e5a84f5b3f4ad8 +e767addeb5637491a60c4a2d9edddd0b74dc0667bcd0296d09a1357cc1c81450 +e7683b05678103111de2248fa1399c9119f70af24b521dc041bb43deaa0d9036 +e76aedb0652c1802455a66b5d34493ed1ef738f5c4ed556658f670d307b9b333 +e775b166bf0d3d47ee6dd5a3a36583a88aa28f0eb22b61c47589e7c015d17c9b +e77f0c5ceb1f5d9dc1427b8d7847f7ef9340076509c5f5ba508e75a86d2ef9bc +e77fc5f62aeb303dd4ccb45ee7556e997f643cc6f7043f806c4c19325223c395 +e7821f7cc97ff6217bee62797d3810c3775f2a2e4007377da4ccd6f6447c4c5e +e7885ff8aaeb1f51571e5abfeb315dec037782fc3928e881e237f5720a5b66d0 +e799ec331fa1f8511cfbd082b6a87f9b9484c28037e533eb3bbdce23b43652b1 +e7a0003b08a091cbbf0429cc1f8038e8d3edecda04e6ad74a0be86665706cff5 +e7a59f32c842fd33b2bc51eaae8d2ad833f5834dbe45175c76fe5fd2b3977a29 +e7a970793bdea42a5f789f18dff1d75a55aa47970155c39db68c4772d2535468 +e7b9a6d8dd35f05dfe7254ae3b7274ae71311f115fc3ca893c43b839a4145233 +e7cab4893a70bcedebfa9abdb62c82c18acc07c10fe46d7f380567df3a3990b2 +e7ce9c59a45e3c8790a019057cb3c30555de803a7595a301e9ceaf1709e9ed0e +e7cfc9a786413a17a9199702b5d83905b996fbd4da4d78d84bbe9301b3d9c7f3 +e7d00802b42a0fb54d47380142d5f869fc7151a0227c6f42c9e93e6428842e36 +e7e45b777de22879b9bdf0315527cf1428fa65d5fc15dc2d1dcc49aecb9c5d7a +e7e66a8f9ef9a05d8a7952c0b1bbff575619c067b4d9837ab5747dc05a52030a +e7e8122337e7b3d2c446e405dcfb482d15c34ec8ee7d85cbcb4bdf2ef1e7e285 +e7e9267cef35aa782324b7891bd0f8eb43e6b0488802573afa7342dcb80f5665 +e7ef9ac7c1ebb4d36d76b7049924efe178eb3c54d6995c4e55d08df0f2bb7e66 +e7f614afa66cac30a22ca555067eac62d393e0ccfa1090979ac126e6dbda851b +e7f98bb2f22b8999d52bfe4be89e147500f804018c336160abad7f135396b54c +e7fcf9d5821eac87b4b8904df761b9b824b3c3ad0e9cf1f77bc07abb4cacf447 +e7fe172e9f148ea185cc04137f91cd93df02f8812ec5cb93a3c0e0d65ef31ee8 +e8001c0ba49d1344096972839e1c10a5943fcc0ea7833bb376d5b928b38246e9 +e80500579af3265067976e56599c99d24d076a2c4395e7e06343e4afe59d0e09 +e80a01dc9e71c563868da37368e0e57d28cc26746f21c8295af414168c565a11 +e814dba7f90f4dd535edf54c9339ca6e6e69ae12c439a6f9c8669a72c8fe8d00 +e82b7606e3b6f32162852e48ad3edced75f2ae103fe0e576b1fbfc95cd6dfcde +e86843f057a097f910e5423737af0fbcc2442323a2b06b00fcde321c67c2ba60 +e86a717bf88bfc9ba4ffb827a66428f8c3ddc6714f8a7ba6adef247cf6e1981f +e86d16444e729a2361423c910263276c8914bd9576ac11cf6ec585cd812ff5d2 +e870d58f098d5781d9442db61ccde8bd224bddc410ac5be3724f7240cfdaddf6 +e8748e211747d613c508b88c1332ed7ce0068b3305f2e911a52d18a7f9e76e26 +e8756a124bb68504a2507e619cc5847882d6a17f487580bede620dde0a7c2cb7 +e889e62107016f59d53d106294084017f3e2789e9cb2f8a7116c70c957ad6d38 +e890828bce95a7a17baef248fbc1b651c78abc14f17227cdb2c53ef5c6ff5a04 +e89795164442ebfff06f3a2441c32f361bc4cf3b371a96fe0fb6cec27d919d91 +e897b493091d515b57776d13995a421df4e39148b3fbf745932ec27519cc3934 +e89c963b8522bbb39b4e49f2f957b8e9cd6ac14d5ba650a21d89fa7a32808ca2 +e8aa5b4fce54313c083bd328b8956008b37652079d8c95439456dd927e6a064c +e8aee0c4c42959c8b2d8970efe19fd25473744ff693b5bc790130feeecb0c640 +e8b8146acd4723a86ddbfa44fbeee6a49e7599b4c12965d26c97a1d7b51a5b37 +e8be5b8923b1b48579b39898207755fdb1deb144060db543c780722d86116371 +e8d0147be0cdbe9af572bfb7af01abdd70f88f94eae7ddf8e674ba4899cfe5f8 +e8da26b79235fc44dde6cfb5eefdda93e03d776c8417f7d92a6b3be03c74679b +e8e0dbccefa29d8337879e7adda0c2013f6e2963432621c4025e7a2d9d32f2f8 +e8e14e93923e96c1ebeda019dba08916f66d722cd7a03ff0b194a3b2bb093ac5 +e8e2b2393a3bce83046dbd6840155c65d2c78e4f746d88f114363c8e0b122704 +e8e558e1b09b3e322a2aa1740c537d7560e4c3cb9a3aadfee5638d07e144f0b4 +e8ef8ac108aaab7ebe6d6b56cf7005b0515acf603d01937edd990ad1ede83927 +e8f9e3b33eb24a9ac245b6281bd17ae9e0aa04963537d87c9cee5a85c37a4edb +e90193c1ff0bf3c80d9b58ae663cb64d9af61b58222b25f3788aef3ef66feb6a +e9040020dfcb61e5c4fb4ff3a409ab189d55d21ed37690b457583ecca8d51558 +e91748ffd0d02b3e106238a0f9788979a4706aaa27500b63b8b190db7873bc6e +e919ef314f1f753bf8004a29ac40652bf0941192fe18e26339c1b3a005ed584e +e91c7d216966ebfdc07b8df46e6c355210240d817c5f9e31ab4e37fd5e8aca7e +e91fc8f720a538d33ed3571e83d8b1ff9a3e42d6498c3e92bbbe7b9e194fbc33 +e92b7178b0cc5350bb7e2aa55a97cbc2484e5489d20a4077313a627bfe47c428 +e9364b30322b3826c668cce54aab9ee8c755e5764d04e00af06653c98e0a784a +e9441ae41fe13e2801063621c34807761d630994899d1dd2d3c1476f3838b8a1 +e963f121a493ae4b7699b15ba8008152e15ff2479947fd2ffd19da6a563bb5b5 +e96a6b96b3f42916526e10c611a87ab4014fa34ee32e5ee7603cea859b5551c0 +e96d38e63f662e88ca3d9a3cc9729bc789a25f4d07e1c480356888d143ae8517 +e96e0100e3a0974fd3b273b0fe58e8ec576ed3110b96a22c2e26c4438f74fc7d +e9768a1c9db24d6ed13684958b7bd81e3f0a948cbedd3ec15b0370a2fa7b0820 +e9842d042d284f171d5253d4fdbe5b6d241791787a02f0d818452b48da48d4ff +e985a64d7448457cf510dcb7c831ae6520a09acc0e8e6a3126d4c5f2a3c11565 +e98ff95732a579608f2dd825c8dded1cf9f276c5eb229662d5b09319dd4de664 +e990ea7414b1fbf6ed8c8c539e3a0e2d9014e6925f91a0c07024e7ced544da79 +e9a632d5a547cee8b2ede38bdaaa649265e28494569bcf16163952eb90964c0c +e9abc388ccd744d4d5cf17e22261c94c7ac0e46946fe52cc0ee44a7a2f12d8aa +e9b5804306f993a9910e5e6140daf3e46b1c71bacd2b274fac8b97c3cc9b288b +e9ba591d9c92c40d0ca8992f58c721d61cc67383940e5bccc6a759c40cd4fc15 +e9ba72b411c7da3cadc07aec039716468eb7692261a193d2c2eb6ab0ad8b4467 +e9c0f9bd3af62f95555aed592d5c187fd3ad7b07b8e96d5fb8be1a6eddf8ee9c +e9c15bd4ce57b98e0cf906315fceada24f017c72bc597969e22021a8401e2827 +e9d157238fabc0cf3b5969fc2554496e6c3816656f0c7ad42146a39c2aa7064c +e9d643556937b21109848b245c2fc7415bcf44ae9a1bccfcb7e8b51b747ee0f3 +e9d8d5e004c3b3c7872727766c785f31be7878343f87536f59b3a39c2f090a88 +e9df3c799a71abb5ed1609a45c1b56f7cad2e5b8588497387b2d8e6686c24458 +e9f1e9e1ee7fc349dbe1d1e18a692065a2fc306f0ed2ee805908cd1a1b6f14ee +e9f221ae53a10d4d2f195305ec1594c49041a33dc8f6c3167c982ff935ce44ce +e9fbbdbcf289d8bd55453365e6b36a5a449bcff8ec8e231c5ac9bce704accd79 +ea067654a270895fb8b2234cc0d54839274ab7ce993ff36b39f813495e575bba +ea0b657bafc14076bc01ef096e56fce73cadbf91e5aba2723c1588a0c439559d +ea0ebb2dfa162ea4008f50c9f4045af4ec86bf7b7013143e173572797555c02c +ea0edb95db903a75c6e77f5b132e10950513be5ba86db1b32bc0cdd75abe6e9f +ea16a4c9b5f8a3261caf6e23b35153bfd713d67ceebad470c2253d0bd4e8d64f +ea223d4af02f87e98d423e506f2502282915d057834e98e3fab820a0dfc4f1f5 +ea22d01ce22410179e0d5c84f0c55f27cb821113885c919432d944f8413bb4fd +ea40d77ac9a491fa14eb122b50166612b4044e0ecceb42a1058fe7ad975d7e9a +ea4161422802deeb8dee411727a0b1f358433f6b88aa31b02b357bc8be8e58c6 +ea43472e164d86342e1274ec3ae5d9beccfc67f616fa7da0d8285fc61a0992ff +ea55fbf3d365507a867285c100381a1682664734b06f0ce3eddcf2aa0ebeb9e1 +ea57e17defeeaedde7922c0b209f65b37241af667c43ee982f1fdfec00d277b0 +ea5a687aeeec7dff94dfef8feaea37be0284deefdc099c5c614bc73023efda61 +ea5ed575fea551c556ee5330f509040a52b4a4ebc5502f0526178d966cfc4ff4 +ea60a72cb152406e8147540809cd34f9a0bfb05c5a428b18d8244a715a0919af +ea68d44872f84b2bc79a48e2ee470ff43ba3869bb2de06553bc4e544e4be36a5 +ea74f47dff7d85d6d40242a07ac834771de28e66b891a34681321f866809f37a +ea7544a21de6bbbaa8b31a0ddae8c68314242140210a19c98ed9aa3ea87626c3 +ea7658c15c4db5b9c5b3106e1daf7bd0b9f544442a9d26765172f3a33744b3c0 +ea7975e3272552c5931dff35b832ed3090f358147d233537550ed2819f181fa3 +ea8095b51e63bab37c96bbaa25ab52cd367c1a797aaf9b95a8323d8e805682b3 +ea8b66a2c3b27d5f5ff7bb73fc67789d6a47dc14d6b61f6dc4ec18484085e45f +ea9658a789e336d286cbdb70c41297bc0b93fe568e61c54066635ec54c963c10 +ea990bf6f6f1665a58601c6ae52cb9ae976e163a08e6f3399c9fd3d412f5133d +ea9a731cdb9fa77cb6642e64f17e567c225f5a5b1f54c922e203350e95b8ede0 +eaaee5c0a5c5bce09d46ec744eb3ab2861abe7f667e72e0056f150464b4e5e8c +eabb9ac58d949568cf4585426f1f15309a49f8cd4d55cd76be8eb46169e57ec2 +eabbb9615b73090e3518d9c83348c95b160bf5ad3534af3465634b3f161fc8cf +eabffb638d85278e98b506d9859d2ebed0ee3f28038910945b34c771d5091fd5 +eac3c790655c02fe30e2792aa6d62eb2ab7b25fc9a5e6a75d0d91b93eed2bbac +eacc973ab852046e40817b1db84770e4a3e8b50b4b8a457d45aec1a3494e7f5c +ead797ccfbb75c7ee2f00cb8a2109cedf05acc96012aedc27f06a78726f26277 +eaebddc3741acf6a95d455c795463b698e02ea165775ae1d425dff5480d918c9 +eaec52bfb8da8e83fb3fa62e579090a90056462e4a1ad11956a9beec5c2b2d36 +eaed2ec1ece16c73495503ecc7b998b7ef910d34490d16c6bf3dfddb0eb15ea9 +eaedd99d714daa85379004c810fa19ec049eb3b73f603b487eccf16c73c55cf6 +eb0060c775386760fc8008cc6d9e44d9c89961b9b38db2b7f84f884e458ccaa8 +eb00de1344646284f076dff2a8a14833e71296373a6f6fa69c1e54c8bcb450e4 +eb11759223d13d742eef2817eb847fbc19dd2c7b42ff6fd0cb747187febca613 +eb1217785690aba35188f032af3421c3674857d0b730f4d17048016cfb942ebe +eb13674c54d2bdd0492b266fe9d57c468c71ee5895f38f73c15b63a6802437ee +eb1a21bba281728ab4bd5abb5b4f3eab6cb691b2e951fa449a196f72e5775502 +eb1d5d8b33a585d59b7f89502765fe4ca4750c9a8f37427bb4e1be9bad977a16 +eb2dbd26343cc5d7f91ab951710aeb7e9133400831475ab77a9adec920bbf67e +eb2ec79838c98d3958e3a5d0d780352a18a43b41d4fe2d60bc3a0f69763a497e +eb4b4d45379862a8af88a1625a10f9dbbb68ef33831354d1fd484fc4f6fab451 +eb4f3c43c034b4052205fc770f0db040f6146e531a6341991200597bb031d530 +eb5999511c370c8a3afc7b35989305e77d5acc5c3da8944011114a762e2a18d3 +eb60ae46b47260bca2e0aa47039e95484c262b028b7066d699174a3ea7a0d2ce +eb62bb6007c7c63c962b0d5a0b0a07f358d9e846b803f26e00290b9c56450da9 +eb62c4d77814b568773b8b4894f9361e02383c86c86406a7326a10b098ee3459 +eb639dd0c0aac077b01dec79b5f4425d9375a8982943d568b035be0b50bd525a +eb69f29d0ad0b7bf0724ab282188c4b42aeb878ad2a7600c2428769f298a59a8 +eb6fbe33ac40c19969f754b7f0b6be4083ede50e87c4b40eef7e75ad6c882a98 +eb8196fa6f1800d0ae2b03a778855d37b66db72cf6f078718cb32aabe1f38654 +eb872cc706a2d9280bd56068883e45594c2f518a872a5d8ebb2ba3cdb70fb9f8 +eb8a509d58505d6720b77f6823defa28a240e471b44f4653214714846856bceb +eb90b4ab505559c2a1751d8beb36dfc41c73c55f4d53752f8b5e89451c918084 +eb95de1c4a36c3b8ccd55f4b3bcec56edcadc50ede52ea6a62bdfc987b822d24 +ebca6d94db09c45d552d3d38f424261eb744054cd5dd001f8573e9187ff5bcb0 +ebd0a0b875561ae8b60dc2e4de44cf041ecda09471675cce6bb2ef4855709b30 +ebd2479ecc29491b5965db91671e889e628b6e0d571a51f4cc7d322094d718b5 +ebd92780b7870e3dee08fa3dd3f481dd5c4e6bf5ae075f74d32a80c5989915e2 +ebdf59aec4ebebe981ec2276ef09cb06c188be838622a57b7e4c65184bbd4b89 +ebe0f7290c5ff1402457beda3b61fd622431f7c41f5336c24337ff56d96c575b +ebea911680a435f6b0df520b4d9306bffa1860aca60b59a61d9dc3ecef8dcce9 +ec00f8110dab263185545606c79904556b56cf89fc659ebe3d7e9451e6808e4c +ec04cc71eb0bdb437fc1e69581637b3be986b33316218c72066de6801af1bc2d +ec0a46d746f6426adfa986becd5a61bfd06ae243073d41c6decebdb9bbb05db8 +ec0c694ff2272514f1a8c34f8166e90f1ce9b0325d21c2e8f73064bc2453f578 +ec0d6ee94b8b1ae99dcc52102927ad90ee2ce56577ae52853e80ac6febdfa5b8 +ec0fe7bf445049857fc98bf2f83f03a64a2bad7696d70d6805ea7dad2fecc614 +ec1210bbf5d135da4ec774949bcf63674157d80dbaee51a6a3d693ad5d0403f4 +ec15c0bee39f0865d6b5ab24608ebc36234b9f7672d23383e16d7485622d19a6 +ec176a91881b3cf650573ce76ca934a5f4ed343589e3538f8c2364676e3c3f8d +ec1b6ad916337b58a4cbc7bbfa6bb89bf980b5c70dce6a76006ea4578fcbdae3 +ec1cc18c5490f4328925b4fa414a44ea68ff9c0d3ca80fd6164416b48fa2fdcc +ec2373390d42fb08d13832816d4c20a3aded42b2464333e30d579cb7c667e58e +ec2a4e602ede703b7afdbd3cfce85ce8c331ef8f3ba34e81e362e6ad6a0b91ab +ec337223526531deffe197c8eb8ffa42cf0b50d6974d1f249a36edb45817542a +ec38a440886aab831ec1403b3b6ec0f84bba5e19146c7d2fcf4f1edcc052455d +ec3e699cebbf71b28c8a0b77f00f6c51721a3c502804969fead7de11dd030dbe +ec51720291552f4d725bb128d96b908e6c7a4b333adc88eac1e3ebd651e5dc67 +ec5c2b30820940cf06845a20f1a37d6900c0be2f6e47fc49b14120b39f636d57 +ec62601a80489a151b41b23302c26cb97e7e72b8e127fc007f1f70ad12a2480a +ec6491ccbbf57168fb34cf16958f5fa0d3fd924f47b4d70a85007d8e19b3f75d +ec66dda5aa72f57ea108f4522ecf2898b0044cc183983b9607005a52b596b3a8 +ec72389397c4a5e3ac828a699b1c8543285e2e1867b9483ba6a5c9734ba5eec5 +ec73ba1f94266cadb19c228d9b537c56b1efd5ed06080bacda5b39b63dfca48d +ec884d1f4032da7d3134c56380b44d4737efe4863b4c7505998508c5781e1b86 +ec8a6a845200313b3d86d6b7428ab3b7e369700789ad254211c548d6c73f4817 +ec8ba244d320eb12c26f200182ee8f139497b02675e9514edca7177a0d21decd +ec8e5bd246f8fcf623584d0c93d12d36c973934b13a26257715153a8134b3411 +ec9333dfce79f757ef4c85a13a45b52cf5cdb7eb585c16c04b8f1e86fb57ca14 +ec990854327d96986bfd212ba29eb932e784de2cf289ddc488e742a0a6f7f274 +ec9edeb7ea8bde0866e6bcb0c8fbcf639b481e21ace2de527275a421d883b9f3 +eca8f516792af653f8c5dd53f12846fb80676db5d975f2fdba7cfb8f3b3c5543 +ecc2881f5d6128c0b7f36895e7d03d3de42046d07dad5928925136f1b0099024 +ecd688231a157add4fcf6deb687add1d2c973ae0117697f0a9054bb6b4b96175 +ecda30135e19624f394d26e8107d792eea32ebb827d1f4d07cbcc4ebc87b34f4 +ecf1ce7eabb28d3d4ba5f0d0c438758cf1fca082b9ccd84df7a939a56b819f46 +ecf53ba340bf0160b4455d496a41aec308c74790a868b7a3089f226270173c90 +ed00a3ca77301127cdd10a03dd0208b3ff4c98b0f64a03555ced00479ff561fc +ed01175420b72d79a5929e10be3ed56eb2a26ea2a3167ec87ebce5157451a52e +ed0186178a11f8610bf58436f9a4cea06633234e3086605991a41cf046576363 +ed02f024c8b71336f4da89f0f436d94e7548b7e3ed59bc04e2a1020cdc7d7f5d +ed041675d2ffaa91a994873636fef8c46290ce448e87af745dbecded6de7f059 +ed0a55f5d5dfeb6d74e7ac01b70472edca2dd4d4a92faea197ade9575506c9ca +ed0f07bd4d3585657870bdcf585cb6ba69adfce996f62be0e888ef975519745e +ed12a6de5510a30b984bf7f6439a19adac4b541d1a30ed999a459d0cc0adda36 +ed1391c80679c27f14d7cf9d8a54194f7e48ef0678e1c1bf7089f8f821fefc13 +ed15c80a46a4b250a19826ca599bd33b36b0b2803c988a5ed099e46b1ca5158f +ed25562778ad9d53cf357bb4788d2ea64c0deb54958913f957028131519ffaa1 +ed265dc758e70a2f7eb4a5f9334bbd94e22890cf447b9c3613c05596221250e4 +ed29074fdda2a3d4c515bd490e0da86320d3a6590314c597cc9668275a8e1e53 +ed32eb67763a74db7d3c0456fbf3def860f392d9cbdbbafad95a8c8610b6950d +ed363df98bd32d509db66a98760b53e4f00f95d34e1e1ed459ab38f5beebbb63 +ed40c4fe184339306661f009dbc9027d6b3f117dff16990d503d20c14a688af1 +ed41ea6a32268fd9444d0624563359684a75350fd2daf5dc936c8c85d79051a5 +ed4262a85c69fc23d23eaf1131115cb925d9a6d6af4d52a31fc53c17a26e116b +ed4f055ea7f0949d0ae2555005a29902ecb2da57cffa9d230351408c548f1054 +ed5931bd81d4c694f16b3cc5b0126f4a21f9700882d56ee07ced44ce829399e6 +ed5f772487747fc5a4599be4540bdc66b9fa14cbda8f6ee23a9c2b49cc5c4014 +ed81c043e432b654904c1214bad8c5e13ee2091acd5e30762a32c9d732d640d5 +ed84932aa7ef218841414c86f115f11294f4d655f9d530cca561b13b392f0c7b +ed86d5dfe95e7260b06be4f60ec895e320f51b813e2d819ef2e09c6edf92b864 +ed8dbccbc63b89422c79e0ae172f0f3082f701eb67d285d1b7d4a12c88d83157 +ed91922645ac4a68668a6af5e4f1b8e46d316bca92e8b235990766344119f798 +ed91d15c55992636440628e02f4d2e9715f85c4b82bdf3084e912fb7ae7a8fa8 +ed932306247ca5fd7020bbd4a91dbbb82c27d7f177b1da3776095ddeacd1e53a +eda0d4f05dc397b26b456008733b3612f1baec3804b40526e7913b89a14581a8 +eda5fc73d9ec51bb42700c4e09d2277cc926f250616f9b7c0b6147e8d24cc5ca +eda91483f686b1fbee2806f3840a52e9acdec0a05827a769d3623536232ed432 +edb055a0fd667b87ef8f0bfd9a2f05989c243371ef550b2e852a3701f1d31fd5 +edb32f72c162133a2028d09d6da48c8139321f106fd00c450d71b28dd27f7089 +edb582fa7c973bc917f663fcea42b7fbc69770c40882c98e393f35112f208b53 +edcc103050b769c79dc20852568d96658fef5c0ab2080bb6c7874fe2828b6c3c +edd1363ee95377a86444147c1143ef277bfed61f8f48b3eb47d13d085792b4b8 +edd1c9356ebfb88f96b8badc1513f84873c615efc259a75b85983dbe2859d2c1 +eddca62dc58197a55bcbc9f7bd2a5898c36d32e059717da452241effea8185d1 +ede7cf1646ab2bcf4d862a0d6b9067490d827bb4dea7959bf6d79c465fbb8fbb +edec6b7f80ad374572de3a68147b11bd16be345fe074acce9c6af0365c2d68aa +edefa616c4e7596b649b30c32adde9e3542bbf54177586d51f7ee93ec56f796e +edf480b3491fa38dcec6f4b3a9ceb00d1a1b0b5ac0fbb8baa68413a61ba4cba5 +edf8235d4e80728bd16af95540902b1a9da66b5b2612be8b1377ca937b8c6cd5 +ee0018fafaad4421f964f1f6a771cd70055d7a864a649f81a7241dd13e01c413 +ee05cb22c66028de3c22a61bfa3066ae01d40718959b25562c65407261cfa598 +ee0bd36a6b663f9ce9b4bb6f78a58b8dc3378dbe6fe934693c4cdcd40125b24f +ee0dd8c890236df8081ec54d92704cd34f58a646fd2f5dbaabdff79c9a4bb8e7 +ee1044297784ed2fe053f3ea9241238c53b817f266dceb0159d4516a433eba94 +ee11d81ec21d93b76c7be4458335e2f4d9af9dc5c258ae945f2b0c277c80e8f0 +ee1339242485609b1bce2e5eefc04fcb6b9419e14e67b022c0785e073fab1e7b +ee1348efbda4b677454b971c03a51315f0f309971a5b464eff2d51ca65530a09 +ee1a4712ecbbdda59c5366d12b33fe4eb31fc57b0575741df9432bca38a0032a +ee1d6bfb981ba80af5ccb57ea71f31c0629c17eed6bdf60e0c6bd741c3de6f88 +ee1e607196b26bea9684a7efa9902085bcd97ba77e8536798fa369ee1b00408b +ee2bcd20e17eddf611b90e431721f9e38260963b3cdf5e2121cb20befa55dea3 +ee2f4a27cf3fb879aa19e4b0b091591b3ada03111efe31aa90df140a778f711a +ee45084afb98f6be31c64833c70f8361d68df389ab812e43f56f6b8ebb658402 +ee4649bbdf35bdb9c45c3f0c2844e5c29fb635c417f3f3384ab7c3ac64b2d503 +ee47c22b78182a5e15630e6451a63ebfd313222edcb688608cb3a630150356c1 +ee506eb25b4d823f75f013e82ce828da6a13f1f1eb620d922039fbebb418b851 +ee55ebccc472c190ed504e3f5d13d8a180fc2d7a3c3a22556c1cf57eae9de8b3 +ee583131e5ecb2002fa2af93aa6efae28937364010d38bffa1c47cfabef1aec9 +ee60fe9bfe66d72f681635c89a83ab4fbe4c5f09ed8e348ece0b799785372b33 +ee6c4e5f653608cc77abd8b8a9e786b55870a78d9f55094f745bc54d6aad10b1 +ee6ff1cf7e72ae218685803ee7c4880373f879325dc94aae71631e6f2a5bcbf1 +ee7d5179b5972f619828b70b6833d8ab4dc6919ae86632c76a979baf249ac0a5 +ee887099318569afb2b0cfc23f87968d0b685b17b11b6d52f8272af1f8641709 +ee933b1ef217833b57b5e3ba70b9cb3189f65fa5efb074f8746fa293f2104997 +ee9572890cc4673854cf8ce11ae0e5ea91cbeb4d235558fc0eb9c654841c2fa9 +eea7ee1bb76db52e69c02afd67d34ae59fb49a25121177d00ebbe44f4e0036ad +eeb2ac57d6bf49aae2074118f9bceb313b511ec24dd3a7a98a5b6f2a5b3af542 +eeb79373c37abfef14a762b0e25704709936972c228aa7b6b56b812dcbd040c5 +eed8cecb9617865f2c62c3c2f04162aedab3f3384b49ca390ff22efdcb1e7c36 +eedf38eb8ee7fcd5a8d4d6cd4703d4e72b00e87d783f847a1e79ada614d6fd03 +eef34237b79ba44a224deef61dc7870ca3f9f575aa17ef1c12c6b492962966f8 +eef5b74398c1df09f8c5ceb7f9b4e0453e41438ea5bc26b12e0933c4fa68e93e +eefee27866ef499a424ea0a9195320ad4f92352a36750ec352cc44d0f20fd71d +ef03bc54591b29010ff282cc886a6437f32ad19db33265dd15481028e95645f6 +ef13eb52baa66e8ad271fceb24597d50b64ba90dccf4e64d6da223b048e6c603 +ef2e0171587cd669b4beae4e48b44651aa03e2a1eeff34c9a710c16cbaabe356 +ef37596aef1fc36d90982fcba5b7619bb4b813ba8e912a12d7213a0f36762cdb +ef416f48e049fd6f3ef893dc4e8e65b9a04a3ddd610110e05eac2af685a3f004 +ef5cae572c5f87978a3e1b80b566c80d448e76ff08e3e51c706e7e3c5d9ceea3 +ef5d599c6c0219b3d234141c11c9f9a94edf766f66c60aa5d92dc7c43fb3808d +ef6877f6e77feccade4fd89efa0aa78f3fa41069a5298b5c538c4d8416782467 +ef6882e85da4df118e5186ceedf606c51b5109fc5ae77ff092a66ba284f60d6c +ef6b999963fae202eb5713dae536ca4031aba694c08d7192213c41708e1444b6 +ef772cad53f4fddccb2c9e28f73103e0f4d20a3626939a246d983cae836d9de4 +ef8242be22e8dd8b23797146d2e429e1f4c908bf163100fd27edf035e6e709a1 +ef85951160ae3d364cf3fd56b3eaab57a01985eb88a1ad6d34f06426dae5f681 +ef868cf5049c1b70df8cd59d06ae45de19d050d80626c0870027185ea60cb2fa +ef8c2b26ec7026da096bc15b0997f0d53c777711acf06684f136b0c85cdc6dab +efbdc3bca47895ade9ad71aea30b6767bfcdb5af09287f49e5800d8b11105338 +efbf4112a164336e00c6a6a6eacb339512df1245b8efd01528b9b0f107aeafa3 +efc30f56595c4c7d23cb5c7298d03bd34fc1295fc8a9d2a38e9cc93721738e52 +efc609aa68b3307555d82d1a9fb20a2e287f86f742791843fa461dc86a1ca4ec +efce449c943d68679020efd4f61db1ddd7afa24dfeb3d88146600b910c6d8664 +efd2c553a5182ca05b98b6135778ae9a3c5409fa69f430974afb931535927d28 +efd7c19eded4cd39c2a7a3240897ccbabc05565fb90427818b22ebb7bfe61866 +efe7aacee3de1a2a503664a4436f25be0a55fcbc3f99375e024a96f0527c4658 +effa581c20fbc614a4e4ce923f6f8fbbf9dc0e1d0ad722ba2a301f611d14b12d +efff37e9582dc818c678d01535f46c1519cdd88cde87549335a39b7864ac5a30 +f0019505e3d00e709c0e1f75e913dc38efb2ee87437dd99ad8ca1dafe0f5f4f0 +f002459850f75572d208458582809675094dd373bba64871f18b8168dd07152f +f00a221483c7afdd90419ccfd99c436e1e0c0cf3847d6f8b9cdd6f0f12c06157 +f0103983944e3617c08c9bc4287ecbc42831d66aa21bee297ea8e307e3cf4dad +f0149781fbe19495a8d19f74ce94a3a00b39f7c627b8ea80bf1d0b6f9e20b619 +f01b9ee5ae4c6cd0d786b4a1a9bfbef972466713fc243b906ffc12198810ecd2 +f02fe60fe1b46f4bf42688b05f9060b6cec7eeb667bec15f0551f7da3112aef6 +f030ff658ed95afdba4d6ee5fc5b2592ce1ee1eadfb16e87432fb6fea88347e1 +f04b3752ded6ccbe00ed57298b8d4b7180053a20a3daf7643b05da6344c577ca +f04cfbc126ce9463e80478268b97463f940203e9db34cf114515fd490452eaf8 +f051d3b98861345109806d19cfe3a364dbca165a41984f7026ce81a83091ed37 +f0555ed14abe3e020feb621316774ff4d5d8a7abe0f69291ab94206d1f3404a8 +f057b62dcac8b2c1cb9b28c025b7ac66792ce4d751ef2b16ea905cc1c06573c0 +f0591ad40a1ed57b63a6eaeacbf0dfbde6c8f656b6632b8fdafa093a369383f3 +f08280733b4d314f0da2f585cc212960f1b99c08120fdebccd387f98f486810f +f087fec64c7915b5eb2e4c74f010131b2a901db1924d521c54eabb83d5aa3015 +f089add8c256858166b8d54f5d6365cc6937589b42956de50c06d2d2b3b9eb19 +f089bd164eca8392c64ee2077fcc44df0423b8b686fbd8d969d30ae3bec7c507 +f092d96bdab98b800f0b5cf066a450a5d8105092c231ebb8ee6eacf708183ab6 +f09702a17d835c9c6186337c9da32d46cecb169adbfe32593d178b003291ebc5 +f097b7bdb330bcf5ecabadccb20f924badf4460800699c07594cf673e9c63f0f +f09e1edb4e4a7146f529ce7f284b794f7d5cc125946c76e1ef45d7ca55773d3a +f0ad7386df6b42bc0b5107084941b8d8a6e12faae62fcbddff365a509d289117 +f0ae64c358574f55a16fd6a6a733f887c6142f2cdf044e1649b15087bd87918f +f0b9d4aa6a0af768d915491267d255604044f058e5f347b5519bb74e2d5fdf0f +f0c30a3940ef93b3872df3521734dc1fcb4a9498dad72f02c2b12c1491a5b512 +f0cd3c561e4f37a7d9009fa215d8f236b76f6bb35b776dc22831e7c88e5a0a7c +f0cf1dbd898166e4f7aa729d037d7b4649940bac9ad0525d5470ae67d2b849d1 +f0d437a8d7753935b740e7d7a5d3e5338c919034dd3a1c0805130fbcd38fc24f +f0e0dd447097e81a8630a542510e0e1fcbc1a4e9bd9654bf69ddc88757c2f6b9 +f0ecef0b68842b90cd0151eec8b0c5dccf9b72c33eba0909503869de33166af8 +f0f1ddc77c17ca8851999a6043ea3b1a22fbc9bdde14d848c9403b6c0b95e7a6 +f0f202d025ba75e26b03b39d64360ee3ea3b7c85ca62e4d0cf4dae8d59732696 +f0f78b5c44e7a79b1549bbf05c1e8eafca57a4f0ad724a374d27f1af068d38d2 +f0fc89a5bf851906955a97882481e345ab1f62f52856b96e6a571f9a0a083582 +f0fe5b2ff448f35b8c79df5dec9549054071fee815b619c02d7c595afe03f5ed +f0fffac4c009e61079e5b0a5d95960090d1350f86287829cc4330c8fe5de2319 +f10144a9a42a35262bcd4e3064047be6a8635283b473600d6577c08b7290d1bb +f1093b178f71e8beae8b0f3314791260ddae5a869aaeb74e5a3d3155162dac5c +f10abe9599dc387693fc1afcd50e347840085d2840cb0a6f5333030256626bec +f10ae0d4d83018722b875949b654802cc90991ba237a2316c8441606adb2d646 +f12fe448626badc46c47fa2c06665ba5063e10fc8eb5082571ab554b7d7a7e90 +f146c046fa31dee8f7b8e630a13705b9670924a9b6711230a9de44628b3e8f4d +f14a31afc625c9223a65076dffd0554382577fb46bd1bdc93a4ce05de1d8f47d +f14f03e538f8bac90512ba3b917680653bd267ebcd2ecd5f72e0ccd0b49b11d2 +f1528351a702550cde25abe1c303f58e845a4ae3ee71ce40cd3338b43fa6ba0c +f159716991a5022f8b5cd9439c28e1e88e98f2e12c112bd20309f1d5e4bcfeb8 +f15c58a1cad5f6c12f30b56857e940bc1e79c2478a24326a82e1466972addcb5 +f15dce853ae0c3c21b48518f33dc90beb5c9f5f3b1ff06dbe1833b24c1541b7b +f16864ea868ce5a9d7936dd7a07b7d9a1d96e9df6e55afc6b5d8f8e25dc13f6a +f16fd5271fc39bc86fc3e763961933f6250a5f1dfc8ab7b5816dfe635e679de2 +f17ee9f1d34c2e342c477658285d14a8f19b58108617ebe7793728e964cd18a6 +f188d0d736418b1e36a52d6a445f6f6008d32dd2fa9e08f6f394ce7a03520661 +f1a241378a919bf9c15d3a443a036e0b68caa531115c53b40e8ad0d578bba3ab +f1a26bf68e8b412e7c8d7bb7c17305bc39398e6f8c2a6ff8c16254ed6af1a552 +f1a5ac360cc944603097449b61d770d55c01f9d3ee450104557c5594921ccc97 +f1a8641fcbcfe88e81b6c632c68a42aef8adde12164ed082b29369edc61e12b6 +f1c000cba6d25204705bf2d53b70a5be4d2b3a83f332e23e43590b89f13322cc +f1c6326e9fa4a387a224a2d0b5108cc8973883fe0a87392597626853d138e5dc +f1c9d6924ed05e56e8024c64b6fcbb6fffbf562118ee925e6423fddb36528da4 +f1e0cc415b4f01f1a56a883224ec44ebf137f7dcb930a75eac17fe47089ebf36 +f1e68e04a4c58ecccb253a31f7f7cdf9172c4b9df8c57239240a5cbf1d590a99 +f1ec36d999a80e66a1570cbb6c0153ed62f43d8bef32e46a513dde5d1da3ca00 +f1f8f981d765afea950bae5fb3f997d0f5fcdee9a8a965fa89886454683b93e8 +f200a5a6cb7452263341cb84213e00830a65f97f0daebc820f3414eb6509d852 +f2063dd66c634a10ea920eb3f593cba2365f50604e09b16c494f58fe5e511b67 +f21480e2fbcbc1cc881623763dd7886787e8675fe1e6d67317e083642cabea68 +f21ee32bfb5dba35f081b65a2c44f849fe603bacebf8b51e5e2cf1ea63c2a333 +f22399807a87aa72b907cb1f5bcee49b52b2467baa58b5772f645903cba378dd +f22c6a23ce90dbe90d1f2cc42a4e8db82f6ba90d9bdc3d0fbcbba3e960305323 +f23115acacbafe8965c90ed7575095bf8b13864690f944e5170c5671e8359234 +f23de5859fadef8c3efbed5acffec782d120117728b3d8ad9b6726cffe695c3f +f23e21950e241cca17d9fbd13e207863169762a096601e6c41f30686ccb8450e +f23f9c006633b59f383b927bdbbaba1a33ce8de3464480ff8be8690a2108ed8c +f23fd858c1748330c2d9fe117ffedaf005090a8a1d21565106b3e0376de0e402 +f2450e9c97db34cad6e342db1779b3b3d6e4585c6aa35390855847719e632501 +f247a6fbacec2dda2a6fa869ad45b39565fd63726d0bb41961a2e31c3214db43 +f248c35bb77a1642ed91938051848453f9605027d7934c804a19d290ea5cef9f +f25a329ec2fe819fd0f7ee004d5de7f52511c4cea3bc1ea6d951d20da8090050 +f25f20551758f4b98ca979783e440ae6ea9d95b026d187692cd853449e50094b +f26122aa16ed27d50204501069e6413d9b86f6eb668d43d86913671ad358f7fb +f277b4571969b5c2432ba1f9ac92b4830cf744e58b796d10d867d67ec2609b8d +f290efe4f6fd011a5598cb9650d6ee91bfedb15ce7edb4b7fe9b5fce98af3552 +f2924516b80faea98dc72d6ecee9514758c8dfbb32329eab4518a015cddf4aeb +f296075a94fabe06c404309730e88245c66166d265db70e40568d30dd81c70e0 +f2a8c99c7be1531df210aa5c7acb98d33b31f0948aa389a5e4d4120f0015ecb3 +f2aac7cfdb6cc99676bc383b0ffa8f6bef7412c430e9990d977f451057fc7c5b +f2abc196fb8379b792095d5283326d943d9e173889a8f712c087be70b612a9ab +f2ad01019f45e1e5ea7979c93bd4c8c92b45e661d786a7f0877ea17dbfe3aa4a +f2b0cc76d4825701f975adb3f8db2026aa7ff98a12c3318aefe7105728764fcd +f2b55966f5a130acb8bff4c971bff4f06ef282a76328f4072a9cebd4c76bea25 +f2b67eacc62440408f6dbff59585a18b88266c338a1e125861890ee09f43a8f3 +f2b8c6364c3c060cd23112e284cdd09c86e576d6f3648216da31e09a81f38237 +f2bacc46fc8373b08e19f042c8e5864f980624347c069d28f375b23077e9e3e6 +f2bc42503e6a737b808f833f7bb407e62eb7942d78898a1dcfc770e5ab1b65ca +f2be65640e0e133ddf2b4dbfed2ba8b730fdff8f477311fcdb0e405472c0dd2a +f2cce301aec9bc18080eb59213137da56917240e3112887cc8df57ca66b02d1b +f2d539c7ea497e235bd717b612a995fde3b0cecfc3a3ef4ea96ffb0114589206 +f2d83fe0d18fb819a250565ea7a485d665132c9fc845417a9d53f7b8c18d7eeb +f2d91f3ecc057b958282582500a7fc49421b21cd91d142b633c248e816fa4a04 +f2dd513799eebd4df328e10cc88e1e93e4dbd44f2450a1c83b8c9bcb562d90da +f2ddb181b3217d8aaa44ab8ac19fea86ed047fcb456f9560b9d754bbbf055532 +f2e6269c470e0d7eeb600e85612a94cd9a9b7149e5c8889dfe4ad195b3f7ccb8 +f30153812ddcd7c81e401bccd41409c727d3094339f605aa1c18bd64fcd99d94 +f312f85b1a0eae9a584c200681807a2c767d83b151a124cde14ccf015d621da0 +f31aeb2252d7308752391ea37c206ae13a7b3f9400a7bbce2b2a8dcf3b9be3e2 +f320e6aa921c22ff1f980c59c9946bedd7f8ae9602637b744525d20fb4d35da5 +f32376bb590c45c10fabb75f4144d4b3831101bcfb9eda31f3ea1c708972246f +f332655e779bf4826356d8855fecfd25d6f7e74c75ae138613669bb6555329d5 +f3329dc4a2b78c4a59e22a5e5947331370caffd260f12d6a2ba5db87d841e381 +f3389bcd5327a47657ed19e75bda76fe5ecf89d18c56251e4bcc92b08b2184a2 +f3538e40d35d313472c80807840032fcb30b66548d9ed490961958b81914c2d9 +f365ddacfb516608b38a0ffd6c0aa9a29c7fdc7e84579736f78586cbe3fb1d59 +f36c3164e1740a6ef2b2b474c0a4aeac837f7408078b475e4586305f3cff0ca4 +f374fe0546994a70dfab2acaef1241e2c84382755bc0beb28bb5985ebd094e6e +f37d7dec1312abc84ef12a23dfc6b1a43ae7c0a0f04424e2cc270a03c6e3bf28 +f37fcf98692250c6785113438d944361eea14d393a26130e9d48ced1a655a75b +f384963480ed1bc58820fa24f3533ad8bbe9dfd6ef0e01c9cb21e08e677e1791 +f388cb537dc67cd06eaaa50c2edb7166a358b51ae93510405eb5975cad814c8b +f38aab37397302e45022839443114da8ca509e794a88b470952077ef3ad6b405 +f38f85af9dd964d329305934c8a0d1131a53b8b4af2174b084820a81eb306668 +f39a8d0c44ddec55cd7c50a6e09d7a3fc1b447896f994c9283519a9eb6f86d72 +f39be354603f92c6b0af71db62a6e7baa947ff52e7cf3fc5e9bd10e26e766ddd +f3a28f551c78e6d20599d49a3fd28c5da3c8fbe120940e1a2290bd8ac3c0a9c6 +f3a5b3f255dd20b332adb3a6b85f62b0028e59aee5cdb9b0ae817a3c829fb3db +f3b190751cdc73664c7480a413b6e7ece69d56a56d53243c7932b90255aac6d2 +f3b36a4b56b8cc15cccda93c2ab39da760a514bbc78c0f9ff359d6a1940896d3 +f3b93b5fb98fe04e37fd96c45199d4d5ec902a071890df8605d61d5df2445788 +f3b952b4366953db72f794431b3728b56a69b5eceaf1ef651e4c4bd6968c18ec +f3bbeb3049921446f5311c07cd0cdb6c797416616bf5717af6de6c852e64f18d +f3c12207d2e0ed57ba5a01bd8e281570230d0cfab61412bbeb6be831ccfcc139 +f3cc1747b8734a06a0c94ee3b616ee3d4f7a919f566d73c4cfc18b846f01b804 +f3da41cde0a9d37b7c7d3fefa9e75b99bd67aefaf5369a4d12286322721f34eb +f3dd29bb90fcfb591f587f15d97a84a59867e73df1e786e66aba50092a347de2 +f3e9f0ac7ff8107f5c51e0d55087a056e4c8d6a32e1f7986227f9e777ee9bf76 +f3f85baf5559b23d8dce362621385cdb2d182aaf21d613af97df17bd4ac06c28 +f3fadfcb8ca8d3fa6c70e4f0e3c309f7645ee559449b812e110e54f8d0d9278a +f400d83a528bad2db943fa903a4b7bd13914c035a3eeccd113591bf217082b28 +f41a3a57f0535ab51d52776425a81a81d06583e1d297f543d98598e266654262 +f41dc7447c8aea4db5cc37c066d3e3b305db8ca4d0dd3dc40f4362f4e2607e60 +f42ccf62b347a3c11bb13689325d96d1781a2347aaa0411d15887fc1cac5b6b4 +f4340bf14755b34e6ca5b9ebd723f92a6fe2429c4f00dac45054a087478755b9 +f43af061e9963c9f034ce4523de227babe74699973497412e8b4f17455dfb388 +f44c664cf1f5e6053f627e6643620531cb043431eaa34bbc22f9322d9d68b31d +f44d15031c89373d59d82ac61b8bbcb1b4f78476b6bde3755521a12e5ddd0850 +f46330cab9daef8ac8d4f742239998f71feb6b8d0fa06aa23de0f97634178b72 +f4669cbbdf3eb65f6cf77e7270e526d16185eb6365ebf66dec14199bc841b43a +f46db3ed7ab3c11d93720c0999e4c3d66e3850be25cdf1dfc6817d0663a5d3fd +f46f94269f1979535d868dc5061c447ace6a494d9ec83a80a91524f4f72278eb +f471825500e5c9e75a40ad0f3eb3dfcfbfcea0d8d9ba979e72ec144d52a2058b +f47b6b6cf66535ec684254f28fb8912b0b4f5aa2d8b5eeb19ac5c90f22069f8b +f47f9b6528a546da80bef3eccd8dc123291fa6deabdcdabbc127133a9ace9095 +f4869020fffd3eb9cd8e69a82e85eabbf52c97c29acd566e09b37ac784bec62f +f4889c428cefe4a3e0fc54cd34e2ce2f1f9ecb324061a0e4617617ad0926525b +f489206e9c10e96b33c1b5ceede46806c60b76b1aa98c38e9d5916984638d853 +f48ae410f4a29c05332617fba2ad8411ebc615bee759160cd80b1f01bfdc7498 +f490415943d4ea46ac08dcbd5a63acd08e843cd130c60fbe1654759c830e5786 +f49301d93cfd60e0a098b211f0280a0660b1117dedfb7395239f992cd662f74f +f49e8ff00a65f52e463d1775337a4c620d4a725d605dff6048662545d9492cd1 +f4a137426d6143209400c799b3d22895d2b25dfc532d28318e1dcd8c9b24577b +f4a43ded2f986346f96db1f5d45d6f4afe271cf8807f67ff7d2180a41aca4099 +f4a6f283b051bcc823ac6280e868b5443d4493223a8c33fcbf75f66d1ee23b5c +f4b492ac636e66bcd70bd2717c86d5f64fcd2d923d7ec5ce646b8f47a13fd057 +f4ba7e39087bbe6adc263c9edce0b5126427890aea60465e2833bc140bc4e702 +f4c4f4878c73b42b7be9fe2c553a8111b9eed403cf8f349ec016a3b1a8c05a1a +f4cf355cfe7a75b8d03ad9e4ae3b558715e3f9bc352d0dc597b5fa14faa12474 +f4d0e35b37a69cfdbae745317c80c5b7cc833b0fed9f84e785423de7fe667f40 +f4dae1f4b5538719c4495abced0357bc4365abaebec85f26a19ed444d00e5517 +f4dcaf43d3372f21091495a4bd9c26af95b935fe59685fb8bd873109ff01c4f0 +f4e01e956361322ea2cdb53b7e885903ce90d63e6639c7ec09fe3f58daa70e39 +f4e7031c8c33466cc43844d618f73301bb069ad78c2afe384cbbbf1aa847db47 +f4ec13e75a8f33513fb27f64a947ee5ffd0bfcd51b2fdf881cf8e1bbaa7c05a0 +f4ec2ca5934c5a3ecc9b86b8245eee528ce46669739a7696b9f83e2491c5a37e +f4ef2a4c9c422d295f4dc55eaf9c5ba29a37b1a63665fdecf41a5c35164ae4ee +f4f6fbde68081a97f533e65c4f783fd4ca7c226ff8c5aad4541c77d731ffbafe +f4f9b647af0104e4eaeed34a59b05a20cf64bc91c826b8444122fa6d7d812a88 +f507b427fe4f20d8d246333dceb27a5e770e25dd94cac2675cfce918ebb424c3 +f507c53f5273ef7eaf62bfc10a56112333b55812ec0e13141587219aaac473a0 +f50ae9fa3b200e84c4e7002ea52802fbfe74998c36a3a523a36ed3d433686694 +f50ddca26e89c77ad400035eab1910e200ba9487893ce2f50d2f12c942daf87f +f51afbfaf9988edced7a0d47b9a7d32df67ccc62abd21fe197bdcb2c6813aee9 +f51f34849d60b51852bcb72aa6f51b59514176eb389c94756406e22a1a72bd08 +f52c6c1f9f103bd0ee7fc7d9caba7d2c93e2ac4f07a0a6d69a383a343bdf32cb +f53d7b90d97a6394032e44c7916178bd81c5ea0c6656033d5b6b5ea993721ea9 +f5402b9beffa88025ee79be05ef86aafc879b2d13d191df047cfa20292fde408 +f5424ad5f55d549d8e1dae40bf52cced664a1de33f0b141fc30263f28c7c69a7 +f542b3db965bfc2d07af654784982c1476beb499f6b75a6851b18c8d1e278977 +f546f14324efc36bd66e2f1c5b2d5a4cf3afecd96a829d6b1ffd0c4ab3ac309e +f54822fd2db39d9d3d6986eef28cb670b0d7b4ea5905de5d634351b463d276c3 +f54a4199b7de088793fe75fd9cdff73178fc08154139e072cadaf621f6a3af04 +f54de606bdf79bffc83a869239ed36a2916251946c38546f520b89b4189fac97 +f54e53fb7fa1793f311b81e4c71313cd213ed67605ed16fea58688574656af7d +f55dbabc0cc51d4dc128f446fd595e48133b0da5764526753608584a007b7c55 +f55fdeccae919fdea4fdf3bb271b20d780133831c7513074a16718afa1b95c8f +f5680cc0ff2a759f7c8a5bb374b17e384bb125a41df186d1621673cbc1b34031 +f56ddf88b76ec5de4a39c005eb5adf7d17e7bcbbdb987c9e087a6d2dc339198c +f570795f1d5bbe5995a955fe3117817767823d8561c4541394b0e518e9515290 +f577ac76a4b6b0156e07843876b07910982bab07b714949caec629fee34e5bd4 +f5797a4fc375f9e5abaad3a0dc08876c9ae184c8539a01379b1d30ddff9a0908 +f579ddfca9a728a805133195dafd0c716b7e64eddb4c56e257ac079908c626b0 +f58d9aebd101d301f400a14bddb76518e78a6047c868ad807d9ad19b2d0e3716 +f593193b19e389df1e992b1307883657f83cc3dc905b84cf0b4b42378c6c2077 +f59b36b1ea886f1f4512959a49736110aa37decee2d2ec7f28a1eb27e8a51dcc +f59b6f89d26ef3cf6d97bcac5e2294cf35f58b4014e48b662d31c3a7c5e37cde +f59d18872675e2c8abf57c21b50127c0f0a3e6edd8f4382bd8e5616d7cfb0f12 +f5a9ea97988a5037fb79b5ac0a49f2ef36453e59da2195ead1632ddf1626b5be +f5b33b1bd84e7058af036c93b1011d26d2299aa56b9a4cb8dac7eb1c5e34c7c5 +f5b39359a69d3148b514f8184a7f180f984a3d03fc0e446ac223488ef2f8eeaa +f5b8a613d83006fe33ce5cc2ef91870e8207a8f147aa55b971aa6463ff4ae7a0 +f5bea482f876c88bf318c9337811229aff11d75d2bb5e6cfb9e734bd3f1db5af +f5c588e9703f37b5f272e216329af97c251d78d13d3c7676a6a71f6ad78a1edb +f5c6fc9ba43f08cca87259e7a7dc1bce39a9f9ecd955ad7a3cc78e3714d49778 +f5c91c581481f3f53ca7f8987a1a3573e24770bb90483c86ee3fe1710dc5a54e +f5cc69b70eea7c995845a0414032e9917b134b11ac887a0d48298a5fde974957 +f5d1db3ef1ca0910cb468d3fdfb43586fe6f66f9b95ea40aaa0e94b863e2020d +f5dd4bf8ef5fab4c7f25e3359161dbaea342fdfc226d533302fd3065a27cd320 +f5e348425b2ddc52c26094a71dfbec6271c2f499d5f1f7f3151388e85bb06443 +f5f26726afc7fc19f40d38c7e6d9172616196c0401ae15da7d55741dc363f2bd +f602945730369dcce9d235cb56e8dafe74873e897f6b0739f4194a9adcecd8c8 +f6045160429f3b3ebd1fc646d04151d12341751344ef84e388d5b6ef894c82d6 +f60ed1587a857a8eecd493103a20338e0cc6d86d11ac991a3354b746f3ccae31 +f62397aca92872ede1db9c1e3d031f6e858b3400ac5d764744775c5b74ff61ca +f624622d8ba13d9c004afb53c469e8bb81316f0f210e06c1d9bccb7a4b06a521 +f62938bf2d7ebf154f1a06b2fd61a097390faee5f9bccba06a39966d1e08add4 +f6297f79e8ac113b75547628cd1fcb4c48ee3c30ad3b8d464f7a858614ba420f +f6461e948e90c0829e14d93fc247fb979c4a65ea2ede10c178fd13599fec5233 +f64b96540c9c2fdd4841ee4b0a794bdb0a56317d76a96ada71ae0058558a1bef +f652c2057175885d7970fdb76af37e08e4c435f1b43b2f05498ed01444449bd0 +f6549f8c752812db7e8c7b33643b8daec60e708c9da7485b0dbede89194c3675 +f668434a1379364deee0dac2f5a8ce330d80d1488c23c2b4c9a0eeab66b39f3a +f6698cff10f0eb0886f326e3902450d6d4554064a400841f07c2199587f0283d +f66a5f1457df3bfe890ec6a5e2fe5b675059d3fd51970d4fc9104056cd6adfcc +f67dfcce9126ca9be8e36ad020a5913d605987b05083309798a6af3892973a9b +f683662b61384118f64040cfc8e73b083ab2724f1dd23f74a5c9ba6c3e064103 +f68730a26ec0fda6cee1be1628c12d688dd8151be179f2391f59734957022ef5 +f688ce4ccb0d30d81d86b3d8822f68f61436353f6c1571618804620a2368bee8 +f690e579bc21372746d89a01128e150991bcad7e250877011632d02a56cf31dd +f69344ee99f6a13e181f0c4997f2a5102d188b08454c52100f6128c64276657d +f693b07ba5231154865f1d2576bb283ffb25361132867538d3bcd1e7215bf4bf +f697e7686a3f9261af955c3a58b7142471ed90c805413c8f9968b2d7ea9b5abb +f69ef20285c5048eb946032056d7666f18f8253c4a6d27ce101ae81960bcaba2 +f6a8cf514c7905fbf4630fc1df49de7b73d596557ced5d62d4a83bdc97efe2d2 +f6be1c5aaeb8766981128e4bae9790efc23313ce5885ef24af5acff4815c3f4b +f6bfc99c451547b16fc1d97f11b0f1de78d4356a256e6e2f160b6fac9993ae0f +f6c287b1b79350219f23a485f51bfce4a19deb9b0f673ab36552e2cfdd071c5d +f6c5c618e4a99ad78fd5c9916747abe485f96a2a27a6eef3fda47bf7632222d5 +f6c9f23098d0219b3e72d846779339b7d08fa9d42866652562c818ee20623e0b +f6d51d1a6740d635d4ca1a4a808c7691b41110ffa91e87ed973212c674f92d30 +f6d8897a6a2c517ed18bba27bc5447da3c37d108f09a68f36c38e2daeed4d628 +f6e188e8314bca908366a5c2a2ac527485813e5dad0f942094ba14081ec17579 +f6e3a7dfaea1fa722d9a255561fb52c6330c0d386b7f397b86684a2fe55f913f +f6e5921a8a73477f7457caab3ad9f1afebfb7b4ee81fb9cb963af494b1062e9d +f6f3df5059cbd3b308fc859dd9faa802395259f4a79a8e25475cc91e7838c236 +f6f8a69e5e973797f38b5691f9f088256f2543a9fe14acb1f733de1a936fd5de +f70d8d00f95518b2716e6c77c4c9893c8e4a94cdcccfe6c468122cf44f7a43cc +f710a1312c8d8e5676b707c05f869ff352fa0d652316586c43a665ae8d95e8bc +f71872b173763c63c797ceb8698acf0d48d2834b798e6e253618edaef499fbd0 +f73045af3f1f7b973c6e02a6cc25470f6c1ecac75788f4c2fdc047a337b1b36b +f730d24c04ae3fab39e0e7e6849cb10d84430243cd65fe0236707b349a90d261 +f7330df8832f8eca9f65ebe8a3614a4280a66e9af436b5869739f0019ac6981f +f73a3915394bcc9500033739859190fbd98d97042eefecab106b7f739bddaa53 +f73b76a978a3eea1f50bbe252d6f072b0888939e424e42db177709b3b102d644 +f740c0704a3192ad4d825f94f4699bbc8fb6da6472565684df14445ccd996b25 +f744e7f47e9b3fa7925bb64c978425bfde0c588a649969e421884417e32e66ba +f75c9512579c13674235d61c194e3dedba12819c8c3a4c2fbb33e52d45f9484c +f7666deee14b8ac178a659f63fc57a86ec62a86d004129b11d3c6743b0a556e1 +f76e0aa4c982d9255892e2cd84f7d35128c15ea78951fb66fe16f16fdf045705 +f7702e5b15137ede44d411bc27a4f2c009375eef7592ff8fba92cb9877f17704 +f7708651b0fdef4cbca3ea2d7ab54d0f4804b2be949cb350ab4830119204e82c +f7724488fbd424e1e081da22164ddbebc75df9765a556d148110d4a822a36495 +f77836a793916aeea63b8e4c6d0711af5aa3a7413d3d09e4ce8b2f394ffb4beb +f77c538d5f2c5ffad903142aa0a3b7989aecbd7efc9cebc3562c1ccf0bbf819e +f77da16ddcde365bdf6cd87dcb98fcf76a8bcdf6f2ba8b0837d576a544fc852a +f790a4b88bf26cb5d3620f4bb74b777d6badc268a9a89b567243a946bdfbccac +f794ed4d535e22471913abc1d8f4daf780c5bdabd4def86e510a945bc0956976 +f7999d0984d45c4097568570524fa5f527f2b1da67c452577e28de8ec5fb05ed +f79b620afcd78052a80fca9f8a58dc7015ee2cfc1a60d3fc96abcb5e170741b7 +f79deb828a16971cc6942d2d375d24912a48a3ec9acca804b58120e21b25f794 +f7a793ae93e86bda367ef380a525d6c20626563965b7cc11fdff80b860be90d8 +f7b269be12ae10d081a480ec47f2c6543c06fed28840e633a2297fa6e6f56bb3 +f7ba0a19e5f0d3e28198fd268bb48837827c6ce003ed888c03bbdc87bf0e67c1 +f7bf59b409b0756788057de2809d0cc87fe31bfa1fdfdc89b2a8dc5db326b79e +f7c538d348f6933e3de5bd3d6bf1767720da07996c2e9ee69bfd634fc25e951a +f7c746e7da259b409096faa71e346571c79639f8208eb73f1d765aaf210b855e +f7d678d7181921206751cb74e6a4ca95927e74a43ad4f02d29caa55e05f2bb90 +f7dedb9ee717f1742b5435b1f6fefbb4a0153f6e48039149ad8f29f2295122b1 +f7e08ef3bb6a5c44368d6d65475cfdf6b461d66c7ca43219ed18795a0992d757 +f7e39df83e75aded8881095f8e2cd6fb542572bea3d5f87a0f2887364d7b9f70 +f7e7224ea7794f0f3df700479aaabed5ff2f18fd3a09ed70bfa41dfda17c89df +f7ee1ae084b9a665e160b4b1be684969faeca69214513bd33f3bb6036e5861f8 +f7f2d7c3f7e21c24c153c95ad61749f687f1f65ad978762237003bd1a5b9619f +f803f9c6bf80e93ea5e1e7904ef8d61cfa5e5017e04065bc2ef2e01567fc1148 +f81290bc788b17dc6558722ea3df96a0ff93e93914c3a5d58a6827a1012b3482 +f81700e30368e0d5e94f7a8c25baf9e548fed9607644cfaa8c3013dce82fae74 +f81c98afb8f9fef750a9a2de507d4a7b41d548b3ef43d4ed253a41efacac3a35 +f8363eaec5b4d18f0f6a8d2de07b51fb9297fb8b529759c6e33d15274e615ca0 +f83ab32c3a4ed013b0c3242e54ae1ffd21015b96d97a2fdb9882bbe4e19af4a2 +f83caadc1f858c7b8d163bd1f851b393f4fb93f0ca30b4662074e4863bb9a4ac +f840aecb0c41ab7ad5155c96a48c92eeff06986801f77136ff24b92de2780ed1 +f84555115ef0644b7a7ccd0c39aac6e9e9eeec8f9834fc7631ee87788c133e7b +f8496c3a8400754fd5a3384b21c785b376ddaed92513927f74944a7387d34bc6 +f84b928f406e30b1b983295314a7de17caba8441d81bc7f6931005ec030840e6 +f85294d81d99024322744ad44745cd505d87c9c9c6b93a94ed5c0fcd6cab68b7 +f85bb0d402035530d6e45dd9ac25db43aef66c9473ce01a3fdfa0c5bc60aab2d +f86073b0ce6e3b18d5298e7eac1a82657db380c290c0f5fb5a5c775a19da2b71 +f8632cba817eb90196e2d7c75c12411b9d93fda0031eded498627961952931d9 +f86eaa3bbc056603cf1256815207882f01b1a87e71111603c4c3de070ce90282 +f872168a7a5e75719c8194cdd59470e1966c26bcebfc2d103e85c9ddfba27ccb +f880d6122c62b5e1cc290151da99ac193f607bb9c375453dc39aa47ddb82d523 +f881cda39660c642486db7320c228e2b7f5c942a88fecf0c1477eeefe448fbc5 +f88896499c2ab2ba5e638ac80e31131bb1babebf0fa20d76db2db7959b98f21d +f891e7d5fe190be22307783b3c54c2ae407d49a0884931d05f03b8ed1da1d4d6 +f8943a7cc02f0c8eb2930d44148f6bcadabc2ae829eea51862722f59fce25681 +f89b34c585b5a2788413ee88531d6acfed2ef7ed6f611758446862938341a35e +f8ac4f00d0420acd49cb22d5d2d5aca8d0647aab604a67d97f51d85e1a9540fa +f8bb62d63b902688cc2c845009be5cba51e8cb5f3099c8fe1ed7d133971ab185 +f8cd56b02638831219e2a70af7832947f7be61bae3209dd4620448d42af94af8 +f8d29dcc7f01646b87d05c48df522f6ca5b7eb0c677f1a27edd7b74947c5464a +f8d428d71dc81f9f509a004ff64bd0875ec0d3353a5093828f7bf68affcfe335 +f8d80b52ed0a2ddb8346cf3db334bafc5773c3c38ee48ffde8a9b7ad8762c72b +f8da3c63b218ba9ca3ee12da86fa80bab9099604d67ba82d3c4041863dd747dd +f8de9bfaae6ba0faae778468d9ff0f8d2ecec933e914fbb25216fb274e696b82 +f8e205db38a23bd3560893a106a368e068805fbf624abd6aa5abbaf9b03ff51b +f8f62db70eedd64b50c31264165dbd0cedbeafe8db9f6aa2df6a90f3a65990ef +f8f86785e23c7d74f0ac9565d76c9bc2ce5337f1a4c0620ac8f8607926e760a4 +f8fc7027474c1bdedb00d21b9e3e55279b968b1904c6e3918c184baada3cb2e9 +f905b25a1297085ca96f169f7121d4f5e7f80f82927dae766d7b91424c68b46b +f90784acf335acacfc441a9fca445c3e5a9379836c9b2573cd85cab5f90b2d8a +f90f8e7e55704040bf55055f322b929e6eb30978ce509609a3a610237622ac0a +f914c8321d6c94cd51ac3d213834196824e68d1362e741c2b2ff8b9335ff9c3b +f91739871f2140847a54e3945683232623b0fe2823542fd46b1647e8aa246819 +f926aeefd5a0cb342efc9cd729da520bbe0c357fc50f65393e655ebf5d6ba1ba +f9272f201d3cb9458d10c4d5a03a70a0b30509b3126dbb91f93a7c4b043a7fd1 +f93b76b276f8f18da5ad9f2e4eff4458870a86970f187ac247e1da0536312426 +f93b85235c1a57712e502c0b6f1e399c51dd534ce49df52efe2b0564e40a795a +f93c5b024da1a4a1b8843df74c75a39b38ccefc9324ecfb88778f69e5704d451 +f93dc6121641c60395ba83cbe114726860cca73739bbba5c5f2ce23e16c257b0 +f93ec2c88e541eabfc7500cb2a4468ed933f579e14625dbbc8664a1852e5c855 +f941063cbd735c4c397be7adeaf96aa58fa366f38101d14be18b267dcb3636f1 +f94929e43e37d37cf0a0d0d6e8ace9e4482658595306e7425ef096a673080e3b +f94f282dcd091a9a814d4ca4ccdc7e253fdc3aad1cd283dc921b4b005a8992b5 +f950800e908bb7e9d4580a2d3faa4949145c76cc2b73ce78ba2a5efdb030a1aa +f9521a443e08e4401346410625c83af3d853407fa39471505bc2262a85437180 +f958c617310f3d3654fc3755693cb34e2ac18f86d7fd3c58d94d5a54b17999c1 +f95bb46d36d1a535b81b74dc94d8b93ebff48f08127571524c5700d5d1c433a0 +f95eebc64a3ebd4f2a114dcb8d0353f1a8c6c04f169832d671c04acdbb56ebe1 +f95f23561c4fba63d61a3f26e8258a8b0f1b1bc63bfd4a4fbf340d4e7f19ab75 +f960bad7b9e755ddeab184a5f5aca421b2736cce7b7f2aba685715269bd7202a +f9627b10f4527bd30839f823743fe17f91d5b031735be0b1b076257b56c146b1 +f9649ce6cd1a02f70e80d78875fb6dd9b51910ac4eb12d498904cf1b63d2f7bc +f968235cf091b32ded752ae4322d5065a1913953aefb926adb06db2576aaf114 +f9690f7789ff6b4300ff489aab650b212dfd8735b28144b40317c2ed9f40b850 +f97da04dc6d9a319b14303758f22b227e0adaae9995661f18485fab1f12ba009 +f9a3fb4ce1973c8e046741a6dc064ff17cb128ee12f484593d15bde22518c94e +f9a7754f5f1a08bfe58e81cb27ca76a35bf1e603af4a2a8e8ed5cc80d4bdf817 +f9b4263aa509fb741476d78e673a1bc4d62d110cf09076830b46763b5776a972 +f9bbb981ad84017598a57bf27a0538bc4e25e1086acebd8de6f8fda37c260db5 +f9c3b44b6d68fc140720d52632848c8835a7f28bd4cb1221a5b59caad31d8e03 +f9cc05dbe17c2a540faa6f8bd8da73b5b35154cd655a02c576448cbda513558a +f9cc4c41cc6443991e89faece38f266cf0e55d16da8bdbd82c9720259425642b +f9d068214de0f189ab3c314b325514e814468fdc3f81f714929fe2561cdc3636 +f9db7446c2e2ba1ddf85638a2da69e51c5f9bc771317e9b3d865bc01796062fc +f9dd81cee59e7f22085bce8d06e75cdc6377c07d6cbb907d497f9c110938bb1c +f9eaa46c535c492ca185df074d0a37a3a09ed17efd159b347a0b6adc48df1d8f +f9eb345c7c377bd1adcd1f686abe69f721cc7b7073b4efda392c273b10362bba +f9efb407237ccd5b5b21a6e5c35f90414b6e8ea63c112e7e8f6430f7a35e4f8d +f9f83cf2ed9da021a15dbd5532a0b3c0410bda9d12a8d634cda5f900a0d27cf0 +f9f85048d9053a8dc610e300ffe123aea05fdb9a9c7d076af194dbf897275354 +f9fa611d27cbbb71df1e38beca4386c8e3f13488f53dc221ee0c96726d38cef2 +fa02dbc14ab3030c6162b37df132064ce3764d06491c092301e4919d2a81673f +fa15d9783f1f36c70e0394060638ed252f966bc3936f9f41e354ca14f87941de +fa1d96d603af9d4b2a6765801d6121cffb1a0ba8f666e3d102cb57006ce2a16f +fa231960860fb417b1b313d9977a41312482fc22ff2d722ea49e814f50774cdd +fa2521bb22b0d99a7eaaf262f83ab1b14f708ab40cfdc6f728b252546ecf161e +fa2665d9b180ea65ef2e7957def0254dcbd47a3901a43cc37fdae2562b966c5f +fa3c1921d20acb54fdb61c63c9a0180999ccd71e0bb38f4d07a5d40fd90bb802 +fa48c4d0efd796d5d45e13e11235706fe90d79936a1d3ec2b3b4eb85e8141a35 +fa4b407c6a06920011b7be348e28019a0bde8c24a0ffb461624d6bd255efe7f8 +fa514248afd309cc9dad89fbfc1e1ec12cd52a8d59e177435be3f8ff75a87b38 +fa5398527d67b5727a075156f65ac189c2cb25f1d8c7270c51d7cc78ef5a5722 +fa58dda6bd1e27ab928bb66cdc2e1c0a30311283d2b7cb0b455d2b95f704925c +fa6f56248a8253d739b03a9c17d1b9bb6bb53fa3dcce8b5a9a9f8a56df11c561 +fa7b9133f526d39be41324c65990b7c8285869d5a82b516298ac6cf2040ad866 +fa863b16961ab9575e8d2f2b985ba43c7b6e73b6f0f349e4858d0e70be2a3f3b +fa87a2c3d29f5f4c1607c8126e6f1646afe276f543b5ada61bf798fb02af394f +fa8fe0246ac6da2e342fcce3eb79d099412164cc3a2beb2fb231406debf3cc0c +fa90c0c043b27ac2359475f12fb1efceda6c2abf2b3c48e49bdf1fc09908790e +fa91e3eb2a73c2c325cc1ee415e9e54b35674552ce123b62f7936d295d2720d7 +faa5d9c76bab751e33745703280fd5ca1f13d578e99fe7e3d882cb47909b630f +faae3913831eaeab0ac09f2cda51890f6aa28e860d6f2fa8911638b4d05fd316 +fabc492f7e8a9c12d3a7a6b793c9567b44cadadeb499a5cced6d19534dfdc209 +fad3e060886ecf68bdf5868b0135b796b263ff883264461ec21faedb2fe0aa8a +fae418e9b8c1847767e0cb5318fc92d48967d0832a3bc736f5e1827211f540ea +fae79dde2b21ad8a96c0ebe585e1bc1832f6775c5c320b692a53003da141ced8 +faea26e337ac0d46aacecebb607d63e6a78c716be0057f1f2b82f84e83c454ce +faf649cc09b9c048a5ea0f07d4263b8d18fc9758c8a013e279f0e267401ef75d +fb019c07e9c7ca2bba0c9d33f4647fb393b688cdb91790eb28c81a712f800758 +fb04a3aa9e6851bf9af2b4fb8f2cbe9ae620dc5642fad2e90e63987df2af3cda +fb05fa2925727f63f10d6d55b4be452802d575f4d215c67d8368fd52c699d6f2 +fb0b497c168aeabb3749156db67c644743796362d40c95b98dc62978bfa45841 +fb131cf6a69f4e00cbea1a8d4eec1058dcc13c027f2bda5f08daa91e28566d7a +fb1bd9d85401d58abd23c47fe461ad0ac5098fb3dd1a3403a61e4061483bbb87 +fb248fb81ba250bc285b494276fccc264649177ac6ae7fb8a4990dff9176557b +fb26c2531c3deea5f203f584b8c0ec5aeffe4267f355d35434d719a2b9e95ef0 +fb2c0b67b37d07b57c33464833c8ca278f1afad545a922979e9e2209c98d543c +fb3bd7276c539627a5a186a18d5b165af16da5396ba20c673ea6f9bc0be6b2b7 +fb3e62a510b095eb00e69c4eb4f77286553db0eb4e26cf441497e3d1a0e47aeb +fb4868d8ad8e6b3b57bff4f215ee77d8c291228cc2b5b943a2158a088ca4f42d +fb51a6af920e7e57a2f59adfc5f44df27059e36aa673daaa5a1641f1cbdbca7d +fb5e1615d77591d36997e9c2fbb53a1bbe4e21b38fbfcb7c11b8c47233c0bd23 +fb65fce047b666317a2b42794288541665af8f36f603c7595c5943167648fd3e +fb6fe065d7c7a0dabc34ff9077db6725a6f6111446670391b5f4f99c0b0456d9 +fb74d3e66032f969da541522041412f2f78b26263467fc8752a7a5656bd11f80 +fb7ba21470a3d42ff46b3e3c1141a641dee7d45a7721fafc3c996d8b93f474d0 +fb83df3d48197a1f7d532aed222855eb1b2b00ac7f58fb2c23ee952e6b16427a +fb860f89466ffe742029e66f07b433bc6a2dbe3179f7ad9f918d4e36b8dedbec +fb8768b36637f98504faa4fc66d937d882719b6b5cb3f0fb43a1a41955717d26 +fb89cd8059513dc04102344a42ad3a76d4437fc8e3c9865de7e22d95b8937191 +fb90207fd88de379f79a836302b6f11334b58d8fa25e978fccee85073ab321fa +fb91b5d36e022e276c124d6e8ac394ac1dd6c666b3cb64147875926c351a13f9 +fb977ccef0160648cc5d79efba2d4a9c9ff7da8ca3446e9982317e83ea445203 +fb9dc6e56ef97a72d0ebf26b30f2f22b69f9b3af2d8fccc84f33a1e71b961b07 +fb9ed7beacdfa438709692e7185960f98aec296f8dc03a133b20a0628803cb69 +fbaebcb1996316aa17cdeef460fc853d0c79ffd3f1a23cc55c3c9d99c72ac69e +fbaf6b14eacab601dc563d5bd4fea95af360d8fbc4a53ad05ebe326902ebbe86 +fbc7b29300af140d16ad753b3170a3af613a02b05daaebea68612db94939d666 +fbcb794d3e2721b3e1bfadd97a19daa9bf3eeb0edbab37381764ab6468c2cd25 +fbe4b7cc33d5a45139d62c5f269652f553e28d66d377b2c3399b4ccbd12d1e11 +fbe73152c47766a79b2970f43cff060261585c395a36ba21c7a0309c3b7e9498 +fbebf5a994ea0975f1bdf6d6e5d6496e729b0a76967a52c60d22f3a962c15c61 +fbf8402c85c8d7fb45c0b66f5ebb321395beb32c676cb7656b5fab7636966370 +fbf9efc94331f6c996526ad4e7e6f14da20f89360ce3d013864f4385068faa40 +fbfb18a0ee34ee75a4e32933b607ae99faaa5949ad6306b6bac6ebd06765e9a8 +fc07c7ed7943d6ec479e9af81da25b9612a16cf8583b266bb1c294567a3382da +fc0cdfc5f03e78e35b928b04b65b8caec2529508dc9fc602661c9714e4a22dff +fc158b8731151de71e85e0e3670314c7f0a81fda23e7dbf509485158252f8653 +fc1965491f6694ee6bf38d44bf53d713c83a54747de2798e0631608beec9b0c0 +fc19e8df8833a97eb2624c95d9c22aee5f9daf80ca795b8bfd3a0e59cf5b5a14 +fc21f4218cbabd067ad690eca84e1502fc6dec087f51f90b09c9a594d4ee8625 +fc2cee74814e58993d9e80e933d9ebe9a54c9cc9791f7810b0858b073077bb6d +fc44d43577618f26ffe4f1717296957a424c9922d58a50578705b4aba705881c +fc4c82c16e315d1f53255314e43345e20e6e7b15a36730e29563049cf1904bac +fc4cf199a339c51ed9cfdac57797c0f66989cc386336d7fdc3c4664743fbc7bc +fc53fe6edb5bce16c6fc278112081fa938806c1cffd4021150e248c56bb799a2 +fc5dc904a89387aff15f0eb4dc6c99bea1f9c3a2ff64c12ff9d19ade270631bb +fc6281783c80da8fde94a3ba20e519ce94d3a71a290d2860cef7c0f9b8ad4dd4 +fc6fbe2fd509fd4998d5feb3d3d7a2ecbc3ed974929d1719566fb5c9a4895ab3 +fc78c63678da4d99afb63a16a714bf03d17a165d91d50760c4ff9afdc51a3343 +fc7e88c730888ab06ef665b0ac48a920f9e065e081bae88ad9d69d502d7d4fb2 +fc895e9d80055191dd511f6f44dcc57caf6d7a856523d413165862d79d205552 +fc8bd407f1d9fd3a2a75e8bdb672eb3cce1daf18bc4004341b6917f58eb752f6 +fc9204e1548e9def5c4b99dedee75f2769e92353dc30588c2324edb19cc76e16 +fca113bf8258b1499d5d40719cbf4b374739e4355f68bc2cf31bf9cbfee6cd89 +fca9d8a8a011a33b77280df9384e63df2e1b736a6ae769af6206e2076c53380e +fcb01ec83e183fe90c1b628773bec0c8cf23528571a4c5f3bbdd9c640dc5141f +fcbe1a5c36b2ed53110df6819fef6702bfa9a93c2f278001cc7cc6b14a26d3be +fccfdc3ea245adeac43098f9e8ce22b6937ee0233ed03bc31b9f3ac6c42ebde5 +fcda6a99f6e9f4f531b3351cecfb9cda678a07357b31ae57449c8276c1db0237 +fcdd7fbf928a5d88259fd9247909cccc7f017c06a5a5162a50212f69c57f22f7 +fce039f9497e4c6ee9634fd541399bce37826c180efe097dea45ebb8b32309c1 +fce54dcb036c0fd8990eb1b4660d5514ed008b71169ce4eab800b4b5bf1885ab +fce7c5ed6101de757c9b656deafbc97c66028937e526ff3b24d24a380c025632 +fcea912992cd312107f82f395b5f0a9b41890883aeecb7080242d06762be625e +fcf5e21ef4fe4c3f54856f1ff6105a4230a27c5b1ff389fe189881c3fa399151 +fd0712c3a4c1d336a4d372f212b2c901d165978aef415b2e812af46f2244f22d +fd1792a36ed915206ab1b55bfa402ad9fde61826ec74f809821572d883497c72 +fd20648f26e90e2bdc19e0cb8cc6201db1a877a5670376c1ee5756f33a268abc +fd301d1ca2326237474942948e470f97e0d7c91f720c31a15983d1fa4ec331ba +fd30e7d11f1a71e535d1891c27c05060f34b952e3c6dff711959d0fac315d825 +fd39d6a54c124f1356b219702e3841e72b1808c68efa798ebd5109095a1a2e5b +fd4d804bc2a3597a3ac64d5242bdb3bf870dc5eec2e94ef0513a340257d162f9 +fd524b4bac5a91fc9ab13c0b03466b52d4142433328e70456a46a39e5a53d8b7 +fd6507282131af41e0edbf58f941752d8803cce30508cfaaf9d003b091516cf7 +fd66021c1d2a2b88a83faa4711310fc60cc3d79a603a7430f4fa4dd5ee65a573 +fd6c84706e40530cc3f5cea1b0ac602d008be0b605ae54de5072936342927bc7 +fd713df6f7cba1d17a56d272fd817d44fe1bb8972a9728a3c81e5c86a095e391 +fd731681652240b74659e78c081eaddd4ae2cdcd7d7f76203fad74f14c63b825 +fd73c88411e41b0606c9e2fe0b6824b5fa90d17d165ac9aa28351c9c548af642 +fd75cf21ae2169fd7e04c32d539669f4c58dde907517cd0bedb2ae47fd850072 +fd7fb042d93dfd76b77bd113ff6948029a3b7959943d9cdbe9926b1969e7bf68 +fd80001356c6e8f5b66ab587843089092f3c295116302dc34418b132e6108340 +fd84cee9f00fa0e04e70d6da05137c02637d4820d1fd356b71c483ba2c75e885 +fd8db338459fde7474d1bea1e6c5c1a99bf3c91ceda50006a99286f9138a1c2f +fd935ec4ab965c154ba32a72563e0e889f7d91c9211e225d5f235cd816bd9027 +fd9675cb975f6f1e24289eca205402e737f6c76a9ce85f2f478336efb0ccca7d +fd9dd1fe2a6242e2c0203902b796abdf6813432bfcbb9523ea4d3bd0e04e6dce +fda3bb787118f549e3b661c2371533c66f760b9a5b15609efafa5a4bf7de9522 +fdb1ca7680960312e2c4665d59b0f6aac999325ddc2f1c795e700e838974558c +fdb71c35141c34b75e963b8206ac0258e022442dab8341436027971a360aa56d +fdbb84d62f8ef7a469b421c377d25283a5d68586c6f55f025cc629e0346b0bdc +fdbc4349074b4d6290aabdc08ca0bfbdeac55f78dddb047b2bf7d611ac84dded +fdbe890313309ca10f0f096c305a0f75b251fd726e99b7102152733e130e173d +fdca6600e968a669862900fb411903c609d97ba8868388ece1c8cc65bc844b8a +fdcc5f15ffd892f48511d9235d14bb5e06fb98d1b56f0271b2cb27c1e7ae7834 +fdcebfd2c7860564c7ac8015ab344a3d269978b0f9f88980be49650614f61a03 +fdced30e241210a4ac0ebdd1141c5ddc5ab2872a8db9ca8f20b26271940b5357 +fdd829c2129a94db71cc722c30b4864eacf6422d469fd2b0f616c914de86c28c +fdde3fa577a4dd64117b9a00b4e3822b52319d9b502141a3b4a73d232b47c924 +fde38bb02a528adbb55e691e7061ae3de17d86601510508e843277ccab8d05fa +fdf118e06c89f5115870493a7ff16bed46c0379841efc2ab169c02d18e843abb +fdf437b5b68289afc6a2aa8d11fa2cb7f46616514f296be055a85bf6102a7c2d +fe01a1845be03eea687caf37bf7eb9c2e3f56e688330d7ad40c514c5da04f7ff +fe0ddc9f1511445d5a2a1f67dd6a9e38366388a44fb49cb8838f424a65094309 +fe0f97cdf5bf23412338221b5e04648b957cd09e23a9c9c39903fd4eb6e4a750 +fe13121c1f6d5fec94d40d90ff2330833cdb49cd46a2d56273b1ede60d8bab8c +fe1e863ba73bb018e52dab20ddb7f1746c562892dcdcd1d3c4ec73f32021519b +fe22ca177ae8ab2eb6cbdc92cd59160e12c2d0ba9ec0e778dedac4da9785c996 +fe2f4225477134aeb60ed431b068a8c038c2c8e6f4b9800a9d37af060a5d117c +fe356756e3df9e3e1fe19490c7bd43c8584eff7e3fedda722c84196228a04b76 +fe380193776787c71aa799cc2856fbd0d34c73109a8fec64e4db0dde499b6052 +fe430485b2cb91f7f42669a47dfcbb13f61c53be14869cc5c5cca15c175363d4 +fe43f056c091898645d60e475de70d111f8ca733bb6c56ec70aafcbada232327 +fe50fe213f020e9a36f53990ebdfb3717159bef100b742875f3a1f5e18a28216 +fe53316323f7fda27551910df635316db275a137b426a9ed3c44722b9a3cb981 +fe6b634bf0075856f40fb1bd3e55c622d5835ba3d9437f83a5b6fabcc0878b1f +fe70e9523692057162585585afb726769c89a1b2533ec23f073b33f06553d96c +fe7204d27b92bdcc0bd2091dac904e86753544fd8966c33a5a99deab51d20c21 +fe7372a8ba6acd6ac4849e8d8ac5848a7d2e068bc6a1c7b609363f0764bf9559 +fe76538e8a45988cf7cc00a314d0fa39ea6404a37fcd4be2e987e55e646ebaeb +fe7c46c700b8b41ef3f1065ded7af1354415153e46bb4d3d3bf9ae386f69b098 +fe803773bad05fc8dd49bdb23bc4a51a4d0dfe865cb25247d04f920370329685 +fe83a5f65f38725fd6d92cdd2f1a9b7137c23f60ac0dbc1a5565c6bb98dd4a1e +fe8e12c1ba23064a78fefe21d1104161afe9f8303d99be56796226400b181ee2 +fe975c79428d644569fa0a514e65b9c42a5081d77c82ae5295369d89fbaa2fbc +fe9855b6b886273aa76c4184070b6fbed0a666bb79ae4ed5936de573e53aa1d5 +feaa379aa98cee8916f9e8beec2f2f40b01b041f615972229a92e245844377ed +feaa5257e12b1baf3c2ef611d53761f8ca956a528ef02cbf6ac4e40c9e0da0c2 +fead49bd2db2d86a70d8c125a9decc6554dc71a3309f81803f3bf9a3212a10e6 +feb7e61b5e84cc65bf15afdb8c9cdedff9beb3edf575fcdf81e691668a96e0ad +febae1ae5ad42572a2111c6903eecbeeecd1efd331546efb8ca5363e0eaad52d +fecb838d2c1fb5c9a076233d9c996dea42d3f74fade0cde2c2f5ac93f015373d +fed50b028fd82a613564abfa95e811f922fb94cf15208976efc3e39449df1355 +fedef8f1a87c8aa8007c95decdfc47e89525e6a523836ef6d2a91f7f8c3803bd +fee0d8604987f375f982353dcf382b1c5bea85ed02c2909861e1c0e9219d24ca +fee99edac0b595aa9d225646193bb4d4c7f08a187325163f7019da3bf82cdf5d +feeb68da57d6f7072750e5e3b9eab2de287c0a774d48e7e782b59f60f3077795 +fef8f2961d06c98b60cf43a97f86e7a00015acba07c9ac7323ed9c71dcc4c618 +ff1728808947af8b912fc29341632763c7f7c76e2cab717a08cfdf69b987b457 +ff216d99c3da6153c92650574c5c2536714427feb5129cd47c337665aa6e865b +ff25eb85bc7992c6b084082a66b077777be0b4db2e0b0c24fcc9622e78b27604 +ff3ce936f67444ecefa7f9738d041c6e2c240c11feceda0e78eb4da2e9064f51 +ff3e9797b0a74c34ab342afa67467a47abb70972050322f817ca75d98d1ce480 +ff5bfbb55535ec6567689039f75e85c375a83c9a242153e557506a51aa15b705 +ff5dc732102b0b889ced0422b760b3b9fb5263cbb76e5d1fab398a6001b505ca +ff704f919ca3c924ea981bba06fffac56d6fcb3a67323877320004cd4e6b0a14 +ff7b4095488574a6c17f4ef5ffd34e6e6860e32de02d93c3cbb9e5ffee986d75 +ff879c611f6594576032bf36dbaae7d89d5cc1069320ba9cd7bd724b6a6963c4 +ff8b6f47aca21b3861a6f52b737df710ea71e37d7c9c4a68f448ae2bab50f34e +ff930ef507a1e17166e669cc6eed813b7ef4840290d74c94decbe8e42984c0ff +ff9e3b671f9b3b0f158d005999be258d07c49edea03c9b5759013a7f82ff1ba8 +ffa54d64739b1b2abaf3d1489e0fcce10d39478cba4381e1f7a28363fcb79452 +ffaa8704235a2293fa16d49bdc1605156ca304d95a87e7472101cd98577d76c6 +ffab4e408f33e4a265bfb40f18b1f0f0d674db6ac1fd5d6105b8f50a7010ae87 +ffb282d6fa4352a269da0b1bb3e9b7ba1dbcdbcde5fdda9a855f85d3c8a85089 +ffb6f20d288513a0c7aaf1e0a68c4d3deb614e146d22d1a1524867b8cc25cde3 +ffbbbec6f56de422a9a683c73c3adad73a906b1aa57b61d3af2563b2e9b12f4d +ffc64c46ffadb41c4c1f69b704dc9343fb7a73c2015430137dbafd0d6f863855 +ffc9701af58947d292651a3f0c082391941d73e65046880f45c59cd0e4cbf98f +ffce3d71c545484edb9a1156512390aa54e16efff45e0616737c64e4b351d2fb +ffe720fee9d3d016d8f30e2755654b9b62b1b61969f626d8df47608ba523e2f2 +fff0120b04c7a7f0dfc4fb404a2f480cfa70b1ce7fd852a4e7e4fae28811c84c +fffa34d343bd6cee927abee100f61d0a27b8cf32b3e3a6a0834ba2d7d4129c5f +f56599f4353c6f5d4d01cf9a9c2548cc2a70d3684c127962515b681692ab2b3e diff --git a/configure_cake_wallet.sh b/configure_cake_wallet.sh new file mode 100755 index 000000000..837a002e9 --- /dev/null +++ b/configure_cake_wallet.sh @@ -0,0 +1,26 @@ +IOS="ios" +ANDROID="android" + +PLATFORMS=($IOS $ANDROID) +PLATFORM=$1 + +if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then + echo "specify platform: ./configure_cake_wallet.sh ios|android" + exit 1 +fi + +if [ "$PLATFORM" == "$IOS" ]; then + echo "Configuring for iOS" + cd scripts/ios +fi + +if [ "$PLATFORM" == "$ANDROID" ]; then + echo "Configuring for Android" + cd scripts/android +fi + +source ./app_env.sh cakewallet +./app_config.sh +cd ../.. && flutter pub get +flutter packages pub run tool/generate_localization.dart +./model_generator.sh diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index d06ffe402..73bc101c4 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -1,23 +1,23 @@ -import 'dart:typed_data'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:bitcoin_base/bitcoin_base.dart'; -String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { +String addressFromOutputScript(Script script, BasedUtxoNetwork network) { try { - return bitcoin.P2PKH( - data: PaymentData(output: script), - network: networkType) - .data - .address!; + switch (script.getAddressType()) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); + case P2shAddressType.p2pkhInP2sh: + return P2shAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); + case SegwitAddresType.p2tr: + return P2trAddress.fromScriptPubkey(script: script).toAddress(network); + default: + } } catch (_) {} - try { - return bitcoin.P2WPKH( - data: PaymentData(output: script), - network: networkType) - .data - .address!; - } catch(_) {} - return ''; -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/address_to_output_script.dart b/cw_bitcoin/lib/address_to_output_script.dart index 01c7b67a5..892f7a0d6 100644 --- a/cw_bitcoin/lib/address_to_output_script.dart +++ b/cw_bitcoin/lib/address_to_output_script.dart @@ -1,27 +1,12 @@ import 'dart:typed_data'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:bitcoin_flutter/src/utils/constants/op.dart'; -import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; -import 'package:bitcoin_flutter/src/address.dart'; +import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; -Uint8List p2shAddressToOutputScript(String address) { - final decodeBase58 = bs58check.decode(address); - final hash = decodeBase58.sublist(1); - return bscript.compile([OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]); -} - -Uint8List addressToOutputScript( - String address, bitcoin.NetworkType networkType) { +List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { try { - // FIXME: improve validation for p2sh addresses - // 3 for bitcoin - // m for litecoin - if (address.startsWith('3') || address.toLowerCase().startsWith('m')) { - return p2shAddressToOutputScript(address); + if (network == bitcoin.BitcoinCashNetwork.mainnet) { + return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes(); } - - return Address.addressToOutputScript(address, networkType); + return bitcoin.addressToOutputScript(address: address, network: network); } catch (err) { print(err); return Uint8List(0); diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 392771ab0..d1c3b6a61 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,40 +1,93 @@ import 'dart:convert'; -class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, - {required this.index, this.isHidden = false, bool isUsed = false}) - : _isUsed = isUsed; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/script_hash.dart' as sh; - factory BitcoinAddressRecord.fromJSON(String jsonSource) { +class BitcoinAddressRecord { + BitcoinAddressRecord( + this.address, { + required this.index, + this.isHidden = false, + int txCount = 0, + int balance = 0, + String name = '', + bool isUsed = false, + required this.type, + String? scriptHash, + required this.network, + }) : _txCount = txCount, + _balance = balance, + _name = name, + _isUsed = isUsed, + scriptHash = scriptHash ?? sh.scriptHash(address, network: network); + + factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool? ?? false, - isUsed: decoded['isUsed'] as bool? ?? false); + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + type: decoded['type'] != null && decoded['type'] != '' + ? BitcoinAddressType.values + .firstWhere((type) => type.toString() == decoded['type'] as String) + : SegwitAddresType.p2wpkh, + scriptHash: decoded['scriptHash'] as String?, + network: network, + ); } @override - bool operator ==(Object o) => - o is BitcoinAddressRecord && address == o.address; + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; final String address; - final bool isHidden; + bool isHidden; final int index; + int _txCount; + int _balance; + String _name; + bool _isUsed; + String? scriptHash; + BasedUtxoNetwork network; + + int get txCount => _txCount; + + String get name => _name; + + int get balance => _balance; + + set txCount(int value) => _txCount = value; + + set balance(int value) => _balance = value; + bool get isUsed => _isUsed; + void setAsUsed() => _isUsed = true; + void setNewName(String label) => _name = label; + @override int get hashCode => address.hashCode; - bool _isUsed; + BitcoinAddressType type; - void setAsUsed() => _isUsed = true; + String updateScriptHash(BasedUtxoNetwork network) { + scriptHash = sh.scriptHash(address, network: network); + return scriptHash!; + } - String toJSON() => - json.encode({ + String toJSON() => json.encode({ 'address': address, 'index': index, 'isHidden': isHidden, - 'isUsed': isUsed}); + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + 'scriptHash': scriptHash, + }); } diff --git a/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart b/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart index 3e21bae81..7bf488f3f 100644 --- a/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart +++ b/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart @@ -1,4 +1,8 @@ class BitcoinCommitTransactionException implements Exception { + String errorMessage; + BitcoinCommitTransactionException(this.errorMessage); + @override - String toString() => 'Transaction commit is failed.'; -} \ No newline at end of file + String toString() => errorMessage; +} + diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index f4ebd7e5d..4a01d6ddc 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -90,8 +90,7 @@ List prefixMatches(String source, List prefixes) { return prefixes.map((prefix) => hx.startsWith(prefix.toLowerCase())).toList(); } -Future generateMnemonic( - {int strength = 264, String prefix = segwit}) async { +Future generateElectrumMnemonic({int strength = 264, String prefix = segwit}) async { final wordBitlen = logBase(wordlist.length, 2).ceil(); final wordCount = strength / wordBitlen; final byteCount = ((wordCount * wordBitlen).ceil() / 8).ceil(); @@ -106,22 +105,29 @@ Future generateMnemonic( return result; } +Future checkIfMnemonicIsElectrum2(String mnemonic) async { + return prefixMatches(mnemonic, [segwit]).first; +} + +Future getMnemonicHash(String mnemonic) async { + final hmacSha512 = Hmac(sha512, utf8.encode('Seed version')); + final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic))); + final hx = digest.toString(); + return hx; +} + Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { - final pbkdf2 = cryptography.Pbkdf2( - macAlgorithm: cryptography.Hmac.sha512(), - iterations: 2048, - bits: 512); + final pbkdf2 = + cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); final text = normalizeText(mnemonic); // pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) final key = await pbkdf2.deriveKey( - secretKey: cryptography.SecretKey(text.codeUnits), - nonce: 'electrum'.codeUnits); + secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits); final bytes = await key.extractBytes(); return Uint8List.fromList(bytes); } -bool matchesAnyPrefix(String mnemonic) => - prefixMatches(mnemonic, [segwit]).any((el) => el); +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el); bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { @@ -208,10 +214,8 @@ String removeCJKSpaces(String source) { } String normalizeText(String source) { - final res = removeCombiningCharacters(unorm.nfkd(source).toLowerCase()) - .trim() - .split('/\s+/') - .join(' '); + final res = + removeCombiningCharacters(unorm.nfkd(source).toLowerCase()).trim().split('/\s+/').join(' '); return removeCJKSpaces(res); } @@ -2297,4 +2301,4 @@ final englishWordlist = [ 'zero', 'zone', 'zoo' -]; +]; \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart new file mode 100644 index 000000000..2d2339a41 --- /dev/null +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -0,0 +1,42 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/receive_page_option.dart'; + +class BitcoinReceivePageOption implements ReceivePageOption { + static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH) (Default)'); + static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)'); + static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); + static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); + static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + + const BitcoinReceivePageOption._(this.value); + + final String value; + + String toString() { + return value; + } + + static const all = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.p2tr, + BitcoinReceivePageOption.p2wsh, + BitcoinReceivePageOption.p2sh, + BitcoinReceivePageOption.p2pkh + ]; + + factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { + switch (type) { + case SegwitAddresType.p2tr: + return BitcoinReceivePageOption.p2tr; + case SegwitAddresType.p2wsh: + return BitcoinReceivePageOption.p2wsh; + case P2pkhAddressType.p2pkh: + return BitcoinReceivePageOption.p2pkh; + case P2shAddressType.p2wpkhInP2sh: + return BitcoinReceivePageOption.p2sh; + case SegwitAddresType.p2wpkh: + default: + return BitcoinReceivePageOption.p2wpkh; + } + } +} diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bd8f1763c..bda7c39ae 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -2,7 +2,8 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); + BitcoinTransactionCredentials(this.outputs, + {required this.priority, this.feeRate}); final List outputs; final BitcoinTransactionPriority? priority; diff --git a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart deleted file mode 100644 index d4397dead..000000000 --- a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart +++ /dev/null @@ -1,4 +0,0 @@ -class BitcoinTransactionNoInputsException implements Exception { - @override - String toString() => 'Not enough inputs available'; -} diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index d82ea429e..7c4dcfd5f 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,17 +1,18 @@ import 'package:cw_core/transaction_priority.dart'; -//import 'package:cake_wallet/generated/i18n.dart'; class BitcoinTransactionPriority extends TransactionPriority { const BitcoinTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); - static const List all = [fast, medium, slow]; + static const List all = [fast, medium, slow, custom]; static const BitcoinTransactionPriority slow = BitcoinTransactionPriority(title: 'Slow', raw: 0); static const BitcoinTransactionPriority medium = BitcoinTransactionPriority(title: 'Medium', raw: 1); static const BitcoinTransactionPriority fast = BitcoinTransactionPriority(title: 'Fast', raw: 2); + static const BitcoinTransactionPriority custom = + BitcoinTransactionPriority(title: 'Custom', raw: 3); static BitcoinTransactionPriority deserialize({required int raw}) { switch (raw) { @@ -21,6 +22,8 @@ class BitcoinTransactionPriority extends TransactionPriority { return medium; case 2: return fast; + case 3: + return custom; default: throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); } @@ -34,13 +37,16 @@ class BitcoinTransactionPriority extends TransactionPriority { switch (this) { case BitcoinTransactionPriority.slow: - label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs'; + label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; case BitcoinTransactionPriority.medium: label = 'Medium'; // S.current.transaction_priority_medium; break; case BitcoinTransactionPriority.fast: - label = 'Fast'; // S.current.transaction_priority_fast; + label = 'Fast'; + break; // S.current.transaction_priority_fast; + case BitcoinTransactionPriority.custom: + label = 'Custom'; break; default: break; @@ -49,7 +55,10 @@ class BitcoinTransactionPriority extends TransactionPriority { return label; } - String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; + String labelWithRate(int rate, int? customRate) { + final rateValue = this == custom ? customRate ??= 0 : rate; + return '${toString()} ($rateValue ${units}/byte)'; + } } class LitecoinTransactionPriority extends BitcoinTransactionPriority { @@ -100,4 +109,55 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { return label; } + } +class BitcoinCashTransactionPriority extends BitcoinTransactionPriority { + const BitcoinCashTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const BitcoinCashTransactionPriority slow = + BitcoinCashTransactionPriority(title: 'Slow', raw: 0); + static const BitcoinCashTransactionPriority medium = + BitcoinCashTransactionPriority(title: 'Medium', raw: 1); + static const BitcoinCashTransactionPriority fast = + BitcoinCashTransactionPriority(title: 'Fast', raw: 2); + + static BitcoinCashTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for BitcoinCashTransactionPriority deserialize'); + } + } + + @override + String get units => 'Satoshi'; + + @override + String toString() { + var label = ''; + + switch (this) { + case BitcoinCashTransactionPriority.slow: + label = 'Slow'; // S.current.transaction_priority_slow; + break; + case BitcoinCashTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case BitcoinCashTransactionPriority.fast: + label = 'Fast'; // S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } +} + diff --git a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart deleted file mode 100644 index 3f379bea0..000000000 --- a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; - -class BitcoinTransactionWrongBalanceException implements Exception { - BitcoinTransactionWrongBalanceException(this.currency); - - final CryptoCurrency currency; - - @override - String toString() => 'You do not have enough ${currency.title} to send this amount.'; -} \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index e5a0e8cac..52edea091 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,24 +1,14 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; -class BitcoinUnspent { - BitcoinUnspent(this.address, this.hash, this.value, this.vout) - : isSending = true, - isFrozen = false, - note = ''; +class BitcoinUnspent extends Unspent { + BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) + : bitcoinAddressRecord = addressRecord, + super(addressRecord.address, hash, value, vout, null); - factory BitcoinUnspent.fromJSON( - BitcoinAddressRecord address, Map json) => - BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, - json['tx_pos'] as int); + factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map json) => + BitcoinUnspent( + address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); - final BitcoinAddressRecord address; - final String hash; - final int value; - final int vout; - - bool get isP2wpkh => - address.address.startsWith('bc') || address.address.startsWith('ltc'); - bool isSending; - bool isFrozen; - String note; + final BitcoinAddressRecord bitcoinAddressRecord; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index c4675df1c..1d29307ca 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -11,42 +12,59 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:bip39/bip39.dart' as bip39; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { - BitcoinWalletBase( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required Uint8List seedBytes, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( + BitcoinWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + String? addressPageType, + BasedUtxoNetwork? networkParam, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + String? passphrase, + }) : super( mnemonic: mnemonic, + passphrase: passphrase, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - networkType: bitcoin.bitcoin, + networkType: networkParam == null + ? bitcoin.bitcoin + : networkParam == BitcoinNetwork.mainnet + ? bitcoin.bitcoin + : bitcoin.testnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, currency: CryptoCurrency.btc) { + // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) + // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) + String derivationPath = walletInfo.derivationInfo!.derivationPath!; + String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; + final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); walletAddresses = BitcoinWalletAddresses( - walletInfo, - electrumClient: electrumClient, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/1"), - networkType: networkType); + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd.derivePath(derivationPath), + sideHd: hd.derivePath(sideDerivationPath), + network: networkParam ?? network, + ); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } static Future create({ @@ -54,21 +72,42 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + String? passphrase, + String? addressPageType, + BasedUtxoNetwork? network, List? initialAddresses, ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0 + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, }) async { + late Uint8List seedBytes; + + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic); + break; + } return BitcoinWallet( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + mnemonic: mnemonic, + passphrase: passphrase ?? "", + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: addressPageType, + networkParam: network, + ); } static Future open({ @@ -77,16 +116,47 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + final network = walletInfo.network != null + ? BasedUtxoNetwork.fromName(walletInfo.network!) + : BitcoinNetwork.mainnet; + final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); + + walletInfo.derivationInfo ??= DerivationInfo( + derivationType: snp.derivationType ?? DerivationType.electrum, + derivationPath: snp.derivationPath, + ); + + // set the default if not present: + walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/1"; + + late Uint8List seedBytes; + + switch (walletInfo.derivationInfo!.derivationType) { + case DerivationType.electrum: + seedBytes = await mnemonicToSeedBytes(snp.mnemonic); + break; + case DerivationType.bip39: + default: + seedBytes = await bip39.mnemonicToSeed( + snp.mnemonic, + passphrase: snp.passphrase ?? '', + ); + break; + } + return BitcoinWallet( - mnemonic: snp.mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + mnemonic: snp.mnemonic, + password: password, + passphrase: snp.passphrase, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: seedBytes, + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: snp.addressPageType, + networkParam: network, + ); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index de3fdfbca..f12577492 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,39 +1,40 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet_addresses.g.dart'; -class BitcoinWalletAddresses = BitcoinWalletAddressesBase - with _$BitcoinWalletAddresses; +class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; -abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( - WalletInfo walletInfo, - {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + }) : super(walletInfo); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); -} \ No newline at end of file + String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { + if (addressType == P2pkhAddressType.p2pkh) + return generateP2PKHAddress(hd: hd, index: index, network: network); + + if (addressType == SegwitAddresType.p2tr) + return generateP2TRAddress(hd: hd, index: index, network: network); + + if (addressType == SegwitAddresType.p2wsh) + return generateP2WSHAddress(hd: hd, index: index, network: network); + + if (addressType == P2shAddressType.p2wpkhInP2sh) + return generateP2SHAddress(hd: hd, index: index, network: network); + + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 82173b2d2..981c7a466 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -2,14 +2,35 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { - BitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + BitcoinNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + DerivationType? derivationType, + String? derivationPath}) + : super( + name: name, + walletInfo: walletInfo, + ); } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { - BitcoinRestoreWalletFromSeedCredentials( - {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + BitcoinRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + required DerivationType derivationType, + required String derivationPath, + String? passphrase, + }) : super( + name: name, + password: password, + passphrase: passphrase, + walletInfo: walletInfo, + derivationInfo: DerivationInfo( + derivationType: derivationType, + derivationPath: derivationPath, + )); final String mnemonic; } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 398d68fc2..e0548771b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -11,11 +12,10 @@ 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:bip39/bip39.dart' as bip39; -class BitcoinWalletService extends WalletService< - BitcoinNewWalletCredentials, - BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials> { +class BitcoinWalletService extends WalletService { BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -25,12 +25,18 @@ class BitcoinWalletService extends WalletService< WalletType getType() => WalletType.bitcoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + final wallet = await BitcoinWalletBase.create( - mnemonic: await generateMnemonic(), - password: credentials.password!, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + mnemonic: await generateElectrumMnemonic(), + password: credentials.password!, + passphrase: credentials.passphrase, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + network: network, + ); await wallet.save(); await wallet.init(); return wallet; @@ -42,37 +48,80 @@ class BitcoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(name, getType()))!; - final wallet = await BitcoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); - await wallet.init(); - return wallet; + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + try { + final wallet = await BitcoinWalletBase.open( + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + final wallet = await BitcoinWalletBase.open( + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + return wallet; + } } @override - Future remove(String wallet) async => - File(await pathForWalletDir(name: wallet, type: WalletType.bitcoin)) - .delete(recursive: true); + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } @override - Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async => + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await BitcoinWalletBase.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, + {bool? isTestnet}) async => throw UnimplementedError(); @override - Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials) async { - if (!validateMnemonic(credentials.mnemonic)) { + Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { throw BitcoinMnemonicIsIncorrectException(); } + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + final wallet = await BitcoinWalletBase.create( - password: credentials.password!, - mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + password: credentials.password!, + passphrase: credentials.passphrase, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + network: network, + ); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 70b072f7b..0553170cc 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -2,15 +2,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:collection/collection.dart'; String jsonrpcparams(List params) { - final _params = params?.map((val) => '"${val.toString()}"')?.join(','); + final _params = params.map((val) => '"${val.toString()}"').join(','); return '[$_params]'; } @@ -22,10 +21,7 @@ String jsonrpc( '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { - SocketTask({ - required this.isSubscription, - this.completer, - this.subject}); + SocketTask({required this.isSubscription, this.completer, this.subject}); final Completer? completer; final BehaviorSubject? subject; @@ -37,6 +33,7 @@ class ElectrumClient { : _id = 0, _isConnected = false, _tasks = {}, + _errors = {}, unterminatedString = ''; static const connectionTimeout = Duration(seconds: 5); @@ -47,12 +44,12 @@ class ElectrumClient { void Function(bool)? onConnectionStatusChange; int _id; final Map _tasks; + final Map _errors; bool _isConnected; Timer? _aliveTimer; String unterminatedString; - Future connectToUri(Uri uri) async => - await connect(host: uri.host, port: uri.port); + Future connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port); Future connect({required String host, required int port}) async { try { @@ -66,54 +63,67 @@ class ElectrumClient { socket!.listen((Uint8List event) { try { final msg = utf8.decode(event.toList()); - final response = - json.decode(msg) as Map; - _handleResponse(response); - } on FormatException catch (e) { - final msg = e.message.toLowerCase(); - - if (e.source is String) { - unterminatedString += e.source as String; - } - - if (msg.contains("not a subtype of type")) { - unterminatedString += e.source as String; - return; - } - - if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; - _handleResponse(response); - unterminatedString = ''; - } - } on TypeError catch (e) { - if (!e.toString().contains('Map') && !e.toString().contains('Map')) { - return; - } - - final source = utf8.decode(event.toList()); - unterminatedString += source; - - if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; - _handleResponse(response); - // unterminatedString = null; - unterminatedString = ''; + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + _parseResponse(message); } } catch (e) { print(e.toString()); } }, onError: (Object error) { print(error.toString()); + unterminatedString = ''; _setIsConnected(false); }, onDone: () { + unterminatedString = ''; _setIsConnected(false); }); keepAlive(); } + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (e) { + print(e.toString()); + } + } + void keepAlive() { _aliveTimer?.cancel(); _aliveTimer = Timer.periodic(aliveTimerDuration, (_) async => ping()); @@ -128,8 +138,7 @@ class ElectrumClient { } } - Future> version() => - call(method: 'server.version').then((dynamic result) { + Future> version() => call(method: 'server.version').then((dynamic result) { if (result is List) { return result.map((dynamic val) => val.toString()).toList(); } @@ -164,11 +173,10 @@ class ElectrumClient { }); Future>> getListUnspentWithAddress( - String address, NetworkType networkType) => + String address, BasedUtxoNetwork network) => call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, networkType: networkType)]) - .then((dynamic result) { + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address, network: network)]).then((dynamic result) { if (result is List) { return result.map((dynamic val) { if (val is Map) { @@ -215,9 +223,8 @@ class ElectrumClient { return []; }); - Future> getTransactionRaw( - {required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash, true]) + Future> getTransactionRaw({required String hash}) async => + callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) .then((dynamic result) { if (result is Map) { return result; @@ -226,9 +233,8 @@ class ElectrumClient { return {}; }); - Future getTransactionHex( - {required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash, false]) + Future getTransactionHex({required String hash}) async => + callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) .then((dynamic result) { if (result is String) { return result; @@ -238,8 +244,13 @@ class ElectrumClient { }); Future broadcastTransaction( - {required String transactionRaw}) async => - call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) + {required String transactionRaw, + BasedUtxoNetwork? network, + Function(int)? idCallback}) async => + call( + method: 'blockchain.transaction.broadcast', + params: [transactionRaw], + idCallback: idCallback) .then((dynamic result) { if (result is String) { return result; @@ -248,19 +259,15 @@ class ElectrumClient { return ''; }); - Future> getMerkle( - {required String hash, required int height}) async => - await call( - method: 'blockchain.transaction.get_merkle', - params: [hash, height]) as Map; - - Future> getHeader({required int height}) async => - await call(method: 'blockchain.block.get_header', params: [height]) + Future> getMerkle({required String hash, required int height}) async => + await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map; + Future> getHeader({required int height}) async => + await call(method: 'blockchain.block.get_header', params: [height]) as Map; + Future estimatefee({required int p}) => - call(method: 'blockchain.estimatefee', params: [p]) - .then((dynamic result) { + call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { if (result is double) { return result; } @@ -300,20 +307,17 @@ class ElectrumClient { return []; }); - Future> feeRates() async { + Future> feeRates({BasedUtxoNetwork? network}) async { + if (network == BitcoinNetwork.testnet) { + return [1, 1, 1]; + } try { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); final bottomDoubleString = await estimatefee(p: 100); - final top = - (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) - .round(); - final middle = - (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) - .round(); - final bottom = - (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) - .round(); + final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); + final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); + final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); return [bottom, middle, top]; } catch (_) { @@ -321,6 +325,21 @@ class ElectrumClient { } } + // https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe + // example response: + // { + // "height": 520481, + // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" + // } + Future getCurrentBlockChainTip() => + call(method: 'blockchain.headers.subscribe').then((result) { + if (result is Map) { + return result["height"] as int; + } + + return null; + }); + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( @@ -330,25 +349,25 @@ class ElectrumClient { } BehaviorSubject? subscribe( - {required String id, - required String method, - List params = const []}) { + {required String id, required String method, List params = const []}) { try { final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); socket!.write(jsonrpc(method: method, id: _id, params: params)); return subscription; - } catch(e) { + } catch (e) { print(e.toString()); return null; } } - Future call({required String method, List params = const []}) async { + Future call( + {required String method, List params = const [], Function(int)? idCallback}) async { final completer = Completer(); _id += 1; final id = _id; + idCallback?.call(id); _registryTask(id, completer); socket!.write(jsonrpc(method: method, id: id, params: params)); @@ -356,9 +375,7 @@ class ElectrumClient { } Future callWithTimeout( - {required String method, - List params = const [], - int timeout = 4000}) async { + {required String method, List params = const [], int timeout = 4000}) async { try { final completer = Completer(); _id += 1; @@ -372,7 +389,7 @@ class ElectrumClient { }); return completer.future; - } catch(e) { + } catch (e) { print(e.toString()); } } @@ -383,8 +400,8 @@ class ElectrumClient { onConnectionStatusChange = null; } - void _registryTask(int id, Completer completer) => _tasks[id.toString()] = - SocketTask(completer: completer, isSubscription: false); + void _registryTask(int id, Completer completer) => + _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false); void _regisrySubscription(String id, BehaviorSubject subject) => _tasks[id] = SocketTask(subject: subject, isSubscription: true); @@ -405,8 +422,7 @@ class ElectrumClient { } } - void _methodHandler( - {required String method, required Map request}) { + void _methodHandler({required String method, required Map request}) { switch (method) { case 'blockchain.scripthash.subscribe': final params = request['params'] as List; @@ -433,15 +449,34 @@ class ElectrumClient { final id = response['id'] as String?; final result = response['result']; + try { + final error = response['error'] as Map?; + if (error != null) { + final errorMessage = error['message'] as String?; + if (errorMessage != null) { + _errors[id!] = errorMessage; + } + } + } catch (_) {} + + try { + final error = response['error'] as String?; + if (error != null) { + _errors[id!] = error; + } + } catch (_) {} + if (method is String) { _methodHandler(method: method, request: response); return; } - - if (id != null){ + + if (id != null) { _finish(id, result); } } + + String getErrorMessage(int id) => _errors[id.toString()] ?? ''; } // FIXME: move me diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 0a9a33d54..165ea447e 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/balance.dart'; @@ -30,7 +29,8 @@ class ElectrumBalance extends Balance { @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); - String get formattedFrozenBalance { + @override + String get formattedUnAvailableBalance { final frozenFormatted = bitcoinAmountToString(amount: frozen); return frozenFormatted == '0.0' ? '' : frozenFormatted; } diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart new file mode 100644 index 000000000..631805c54 --- /dev/null +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -0,0 +1,104 @@ +import 'package:cw_core/wallet_info.dart'; + +Map> electrum_derivations = { + DerivationType.electrum: [ + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'/0", + description: "Electrum", + scriptType: "p2wpkh", + ), + ], + DerivationType.bip39: [ + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/0'", + description: "Standard BIP44", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/0'", + description: "Standard BIP49 compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard legacy", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/0'", + description: "Samourai Deposit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/0'", + description: "Samourai Deposit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483644'", + description: "Samourai Bad Bank (toxic change)", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483645'", + description: "Samourai Whirlpool Pre Mix", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483646'", + description: "Samourai Whirlpool Post Mix", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/2147483647'", + description: "Samourai Ricochet legacy", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/2147483647'", + description: "Samourai Ricochet compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483647'", + description: "Samourai Ricochet native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/2'/0'", + description: "Default Litecoin", + scriptType: "p2wpkh", + ), + ], +}; diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 9174fb3f8..d478c3b12 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,15 +1,15 @@ import 'dart:convert'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_bitcoin/file.dart'; + import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/utils/file.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; part 'electrum_transaction_history.g.dart'; -const _transactionsHistoryFileName = 'transactions.json'; +const transactionsHistoryFileName = 'transactions.json'; class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$ElectrumTransactionHistory; @@ -35,14 +35,14 @@ abstract class ElectrumTransactionHistoryBase @override void addMany(Map transactions) => - transactions.forEach((_, tx) => _updateOrInsert(tx)); + transactions.forEach((_, tx) => _update(tx)); @override Future save() async { try { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$_transactionsHistoryFileName'; + final path = '$dirPath/$transactionsHistoryFileName'; final data = json.encode({'height': _height, 'transactions': transactions}); await writeData(path: path, password: _password, data: data); @@ -59,7 +59,7 @@ abstract class ElectrumTransactionHistoryBase Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$_transactionsHistoryFileName'; + final path = '$dirPath/$transactionsHistoryFileName'; final content = await read(path: path, password: _password); return json.decode(content) as Map; } @@ -67,14 +67,14 @@ abstract class ElectrumTransactionHistoryBase Future _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map ?? {}; + final txs = content['transactions'] as Map? ?? {}; txs.entries.forEach((entry) { final val = entry.value; if (val is Map) { final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); - _updateOrInsert(tx); + _update(tx); } }); @@ -84,18 +84,7 @@ abstract class ElectrumTransactionHistoryBase } } - void _updateOrInsert(ElectrumTransactionInfo transaction) { - - if (transactions[transaction.id] == null) { + void _update(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction; - } else { - final originalTx = transactions[transaction.id]; - originalTx?.confirmations = transaction.confirmations; - originalTx?.amount = transaction.amount; - originalTx?.height = transaction.height; - originalTx?.date ??= transaction.date; - originalTx?.isPending = transaction.isPending; - originalTx?.direction = transaction.direction; - } - } + } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b034c06b1..f980bd884 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,4 +1,4 @@ -import 'package:flutter/foundation.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; @@ -11,11 +11,9 @@ import 'package:cw_core/wallet_type.dart'; class ElectrumTransactionBundle { ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, - required this.confirmations, - this.time}); - final bitcoin.Transaction originalTransaction; - final List ins; + {required this.ins, required this.confirmations, this.time}); + final BtcTransaction originalTransaction; + final List ins; final int? time; final int confirmations; } @@ -26,6 +24,8 @@ class ElectrumTransactionInfo extends TransactionInfo { required int height, required int amount, int? fee, + List? inputAddresses, + List? outputAddresses, required TransactionDirection direction, required bool isPending, required DateTime date, @@ -33,6 +33,8 @@ class ElectrumTransactionInfo extends TransactionInfo { this.id = id; this.height = height; this.amount = amount; + this.inputAddresses = inputAddresses; + this.outputAddresses = outputAddresses; this.fee = fee; this.direction = direction; this.date = date; @@ -40,8 +42,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.confirmations = confirmations; } - factory ElectrumTransactionInfo.fromElectrumVerbose( - Map obj, WalletType type, + factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, {required List addresses, required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; @@ -59,10 +60,8 @@ class ElectrumTransactionInfo extends TransactionInfo { for (dynamic vin in vins) { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; - final outAddresses = - (out['scriptPubKey']['addresses'] as List?)?.toSet(); - inputsAmount += - stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); + final outAddresses = (out['scriptPubKey']['addresses'] as List?)?.toSet(); + inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -70,11 +69,9 @@ class ElectrumTransactionInfo extends TransactionInfo { } for (dynamic out in vout) { - final outAddresses = - out['scriptPubKey']['addresses'] as List? ?? []; + final outAddresses = out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount( - (out['value'] as double? ?? 0.0).toString()); + final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -97,46 +94,58 @@ class ElectrumTransactionInfo extends TransactionInfo { } factory ElectrumTransactionInfo.fromElectrumBundle( - ElectrumTransactionBundle bundle, - WalletType type, - bitcoin.NetworkType networkType, - {required Set addresses, - required int height}) { + ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, + {required Set addresses, required int height}) { final date = bundle.time != null - ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) - : DateTime.now(); + ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) + : DateTime.now(); var direction = TransactionDirection.incoming; var amount = 0; var inputAmount = 0; var totalOutAmount = 0; + List inputAddresses = []; + List outputAddresses = []; - for (var i = 0; i < bundle.originalTransaction.ins.length; i++) { - final input = bundle.originalTransaction.ins[i]; + for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; - final vout = input.index; - final outTransaction = inputTransaction.outs[vout!]; - final address = addressFromOutput(outTransaction.script!, networkType); - inputAmount += outTransaction.value!; - if (addresses.contains(address)) { + final outTransaction = inputTransaction.outputs[input.txIndex]; + inputAmount += outTransaction.amount.toInt(); + if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { direction = TransactionDirection.outgoing; + inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network)); } } - for (final out in bundle.originalTransaction.outs) { - totalOutAmount += out.value!; - final address = addressFromOutput(out.script!, networkType); - final addressExists = addresses.contains(address); + final receivedAmounts = []; + for (final out in bundle.originalTransaction.outputs) { + totalOutAmount += out.amount.toInt(); + final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); + outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network)); + + if (addressExists) { + receivedAmounts.add(out.amount.toInt()); + } + if ((direction == TransactionDirection.incoming && addressExists) || (direction == TransactionDirection.outgoing && !addressExists)) { - amount += out.value!; + amount += out.amount.toInt(); } } + if (receivedAmounts.length == bundle.originalTransaction.outputs.length) { + // Self-send + direction = TransactionDirection.incoming; + amount = receivedAmounts.reduce((a, b) => a + b); + } + final fee = inputAmount - totalOutAmount; return ElectrumTransactionInfo(type, - id: bundle.originalTransaction.getId(), + id: bundle.originalTransaction.txId(), height: height, isPending: bundle.confirmations == 0, + inputAddresses: inputAddresses, + outputAddresses: outputAddresses, fee: fee, direction: direction, amount: amount, @@ -153,8 +162,8 @@ class ElectrumTransactionInfo extends TransactionInfo { if (addresses != null) { tx.outs.forEach((out) { try { - final p2pkh = bitcoin.P2PKH( - data: PaymentData(output: out.script), network: bitcoin.bitcoin); + final p2pkh = + bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); exist = addresses.contains(p2pkh.data.address); if (exist) { @@ -164,9 +173,8 @@ class ElectrumTransactionInfo extends TransactionInfo { }); } - final date = timestamp != null - ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) - : DateTime.now(); + final date = + timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); return ElectrumTransactionInfo(type, id: tx.getId(), @@ -179,8 +187,7 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory ElectrumTransactionInfo.fromJson( - Map data, WalletType type) { + factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { return ElectrumTransactionInfo(type, id: data['id'] as String, height: data['height'] as int, @@ -189,6 +196,8 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, + inputAddresses: data['inputAddresses'] as List, + outputAddresses: data['outputAddresses'] as List, confirmations: data['confirmations'] as int); } @@ -217,9 +226,11 @@ class ElectrumTransactionInfo extends TransactionInfo { height: info.height, amount: info.amount, fee: info.fee, - direction: direction ?? info.direction, - date: date ?? info.date, - isPending: isPending ?? info.isPending, + direction: direction, + date: date, + isPending: isPending, + inputAddresses: inputAddresses, + outputAddresses: outputAddresses, confirmations: info.confirmations); } @@ -233,6 +244,8 @@ class ElectrumTransactionInfo extends TransactionInfo { m['isPending'] = isPending; m['confirmations'] = confirmations; m['fee'] = fee; + m['inputAddresses'] = inputAddresses; + m['outputAddresses'] = outputAddresses; return m; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index eac05378f..8342e4816 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,47 +1,53 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; -import 'package:mobx/mobx.dart'; -import 'package:rxdart/subjects.dart'; -import 'package:flutter/foundation.dart'; + +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_bitcoin/address_to_output_script.dart'; +import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base; +import 'package:collection/collection.dart'; +import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cw_bitcoin/electrum_transaction_history.dart'; -import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cw_bitcoin/file.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_transaction_history.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:hex/hex.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:collection/collection.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/file.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; +import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase with Store { +abstract class ElectrumWalletBase + extends WalletBase + with Store { ElectrumWalletBase( {required String password, required WalletInfo walletInfo, @@ -49,36 +55,50 @@ abstract class ElectrumWalletBase extends WalletBase? initialAddresses, ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency}) - : hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/0"), + : hd = currency == CryptoCurrency.bch + ? bitcoinCashHDWallet(seedBytes) + : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) + .derivePath(walletInfo.derivationInfo?.derivationPath ?? "m/0'/0"), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], _isTransactionUpdating = false, + isEnabledAutoGenerateSubaddress = true, unspentCoins = [], _scripthashesUpdateSubject = {}, - balance = ObservableMap.of( - currency != null - ? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, - frozen: 0)} - : {}), + balance = ObservableMap.of(currency != null + ? { + currency: + initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0) + } + : {}), this.unspentCoinsInfo = unspentCoinsInfo, + this.network = _getNetwork(networkType, currency), + this.isTestnet = networkType == bitcoin.testnet, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; - transactionHistory = - ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); } + static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => + bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0"); + static int estimatedTransactionSize(int inputsCount, int outputsCounts) => - inputsCount * 146 + outputsCounts * 33 + 8; + inputsCount * 68 + outputsCounts * 34 + 10; final bitcoin.HDWallet hd; final String mnemonic; + final String? passphrase; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; late ElectrumClient electrumClient; Box unspentCoinsInfo; @@ -94,14 +114,14 @@ abstract class ElectrumWalletBase extends WalletBase get scriptHashes => walletAddresses.addresses - .map((addr) => scriptHash(addr.address, networkType: networkType)) + List get scriptHashes => walletAddresses.addressesByReceiveType + .map((addr) => scriptHash(addr.address, network: network)) .toList(); - List get publicScriptHashes => walletAddresses.addresses - .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, networkType: networkType)) - .toList(); + List get publicScriptHashes => walletAddresses.allAddresses + .where((addr) => !addr.isHidden) + .map((addr) => scriptHash(addr.address, network: network)) + .toList(); String get xpub => hd.base58!; @@ -109,10 +129,14 @@ abstract class ElectrumWalletBase extends WalletBase mnemonic; bitcoin.NetworkType networkType; + BasedUtxoNetwork network; @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); + bool? isTestnet; + + @override + BitcoinWalletKeys get keys => + BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); String _password; List unspentCoins; @@ -120,6 +144,8 @@ abstract class ElectrumWalletBase extends WalletBase?> _scripthashesUpdateSubject; bool _isTransactionUpdating; + void Function(FlutterErrorDetails)? _onError; + Future init() async { await walletAddresses.init(); await transactionHistory.init(); @@ -131,15 +157,14 @@ abstract class ElectrumWalletBase extends WalletBase startSync() async { try { syncStatus = AttemptingSyncStatus(); - await walletAddresses.discoverAddresses(); await updateTransactions(); _subscribeForUpdates(); await updateUnspent(); await updateBalance(); - _feeRates = await electrumClient.feeRates(); + _feeRates = await electrumClient.feeRates(network: network); - Timer.periodic(const Duration(minutes: 1), - (timer) async => _feeRates = await electrumClient.feeRates()); + Timer.periodic( + const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); syncStatus = SyncedSyncStatus(); } catch (e, stacktrace) { @@ -167,193 +192,444 @@ abstract class ElectrumWalletBase extends WalletBase createTransaction( - Object credentials) async { - const minAmount = 546; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final inputs = []; - final outputs = transactionCredentials.outputs; - final hasMultiDestination = outputs.length > 1; - var allInputsAmount = 0; + int get _dustAmount => 546; - if (unspentCoins.isEmpty) { - await updateUnspent(); - } + bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; + + Future estimateSendAllTx( + List outputs, + int feeRate, { + String? memo, + int credentialsAmount = 0, + }) async { + final utxos = []; + List privateKeys = []; + int allInputsAmount = 0; + + bool spendsUnconfirmedTX = false; + + for (int i = 0; i < unspentCoins.length; i++) { + final utx = unspentCoins[i]; + + if (utx.isSending && !utx.isFrozen) { + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; - for (final utx in unspentCoins) { - if (utx.isSending) { allInputsAmount += utx.value; - inputs.add(utx); + + final address = addressTypeFromStr(utx.address, network); + final privkey = generateECPrivate( + hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: network); + + privateKeys.add(privkey); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + ), + ownerDetails: UtxoAddressDetails( + publicKey: privkey.getPublic().toHex(), + address: address, + ), + ), + ); } } - if (inputs.isEmpty) { + if (utxos.isEmpty) { throw BitcoinTransactionNoInputsException(); } - final allAmountFee = transactionCredentials.feeRate != null - ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) - : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); - - final allAmount = allInputsAmount - allAmountFee; - - var credentialsAmount = 0; - var amount = 0; - var fee = 0; - - if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll - || item.formattedCryptoAmount! <= 0)) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - credentialsAmount = outputs.fold(0, (acc, value) => - acc + value.formattedCryptoAmount!); - - if (allAmount - credentialsAmount < minAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - amount = credentialsAmount; - - if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, - outputsCount: outputs.length + 1); - } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount, - outputsCount: outputs.length + 1); - } + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network as BitcoinCashNetwork, + memo: memo, + ); } else { - final output = outputs.first; - credentialsAmount = !output.sendAll - ? output.formattedCryptoAmount! - : 0; + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } - if (credentialsAmount > allAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } + int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); - amount = output.sendAll || allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } - if (output.sendAll || amount == allAmount) { - fee = allAmountFee; - } else if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); - } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount); + // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change + int amount = allInputsAmount - fee; + + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(); + } + + // Attempting to send less than the dust limit + if (_isBelowDust(amount)) { + throw BitcoinTransactionNoDustException(); + } + + if (credentialsAmount > 0) { + final amountLeftForFee = amount - credentialsAmount; + if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) { + amount -= amountLeftForFee; + fee += amountLeftForFee; } } + outputs[outputs.length - 1] = + BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + + return EstimatedTxResult( + utxos: utxos, + privateKeys: privateKeys, + fee: fee, + amount: amount, + isSendAll: true, + hasChange: false, + memo: memo, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } + + Future estimateTxForAmount( + int credentialsAmount, + List outputs, + int feeRate, { + int? inputsCount, + String? memo, + bool? useUnconfirmed, + }) async { + final utxos = []; + List privateKeys = []; + int allInputsAmount = 0; + bool spendsUnconfirmedTX = false; + + int leftAmount = credentialsAmount; + final sendingCoins = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final unconfirmedCoins = sendingCoins.where((utx) => utx.confirmations == 0).toList(); + + for (int i = 0; i < sendingCoins.length; i++) { + final utx = sendingCoins[i]; + + final isUncormirmed = utx.confirmations == 0; + if (useUnconfirmed != true && isUncormirmed) continue; + + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = isUncormirmed; + + allInputsAmount += utx.value; + leftAmount = leftAmount - utx.value; + + final address = addressTypeFromStr(utx.address, network); + final privkey = generateECPrivate( + hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: network); + + privateKeys.add(privkey); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + ), + ownerDetails: UtxoAddressDetails( + publicKey: privkey.getPublic().toHex(), + address: address, + ), + ), + ); + + bool amountIsAcquired = leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } + } + + if (utxos.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + final spendingAllCoins = sendingCoins.length == utxos.length; + final spendingAllConfirmedCoins = + !spendsUnconfirmedTX && utxos.length == sendingCoins.length - unconfirmedCoins.length; + + // How much is being spent - how much is being sent + int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount; + + if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + ); + } + throw BitcoinTransactionWrongBalanceException(); + } + + final changeAddress = await walletAddresses.getChangeAddress(); + final address = addressTypeFromStr(changeAddress, network); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + )); + + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network as BitcoinCashNetwork, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } + + int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + if (fee == 0) { - throw BitcoinTransactionWrongBalanceException(currency); + throw BitcoinTransactionNoFeeException(); + } + + int amount = credentialsAmount; + final lastOutput = outputs.last; + final amountLeftForChange = amountLeftForChangeAndFee - fee; + + if (!_isBelowDust(amountLeftForChange)) { + // Here, lastOutput already is change, return the amount left without the fee to the user's address. + outputs[outputs.length - 1] = + BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange)); + } else { + // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change + outputs.removeLast(); + + // Still has inputs to spend before failing + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + ); + } + + final estimatedSendAll = await estimateSendAllTx( + outputs, + feeRate, + memo: memo, + ); + + if (estimatedSendAll.amount == credentialsAmount) { + return estimatedSendAll; + } + + // Estimate to user how much is needed to send to cover the fee + final maxAmountWithReturningChange = allInputsAmount - _dustAmount - fee - 1; + throw BitcoinTransactionNoDustOnChangeException( + bitcoinAmountToString(amount: maxAmountWithReturningChange), + bitcoinAmountToString(amount: estimatedSendAll.amount), + ); + } + + // Attempting to send less than the dust limit + if (_isBelowDust(amount)) { + throw BitcoinTransactionNoDustException(); } final totalAmount = amount + fee; - if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { - throw BitcoinTransactionWrongBalanceException(currency); + if (totalAmount > balance[currency]!.confirmed) { + throw BitcoinTransactionWrongBalanceException(); } - final txb = bitcoin.TransactionBuilder(network: networkType); - final changeAddress = await walletAddresses.getChangeAddress(); - var leftAmount = totalAmount; - var totalInputAmount = 0; + if (totalAmount > allInputsAmount) { + if (spendingAllCoins) { + throw BitcoinTransactionWrongBalanceException(); + } else { + if (amountLeftForChangeAndFee > fee) { + outputs.removeLast(); + } - inputs.clear(); + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + ); + } + } - for (final utx in unspentCoins) { - if (utx.isSending) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); + return EstimatedTxResult( + utxos: utxos, + privateKeys: privateKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: false, + memo: memo, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } - if (leftAmount <= 0) { - break; + @override + Future createTransaction(Object credentials) async { + try { + final outputs = []; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final hasMultiDestination = transactionCredentials.outputs.length > 1; + final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; + final memo = transactionCredentials.outputs.first.memo; + + int credentialsAmount = 0; + + for (final out in transactionCredentials.outputs) { + final outputAmount = out.formattedCryptoAmount!; + + if (!sendAll && _isBelowDust(outputAmount)) { + throw BitcoinTransactionNoDustException(); + } + + if (hasMultiDestination) { + if (out.sendAll) { + throw BitcoinTransactionWrongBalanceException(); + } + } + + credentialsAmount += outputAmount; + + final address = + addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network); + + if (sendAll) { + // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent + outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + } else { + outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); } } - } - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } + final feeRateInt = transactionCredentials.feeRate != null + ? transactionCredentials.feeRate! + : feeRate(transactionCredentials.priority!); - if (amount <= 0 || totalInputAmount < totalAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - txb.setVersion(1); - inputs.forEach((input) { - if (input.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData( - hd: input.address.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: input.address.index), - network: networkType) - .data; - - txb.addInput(input.hash, input.vout, null, p2wpkh.output); + EstimatedTxResult estimatedTx; + if (sendAll) { + estimatedTx = await estimateSendAllTx( + outputs, + feeRateInt, + memo: memo, + credentialsAmount: credentialsAmount, + ); } else { - txb.addInput(input.hash, input.vout); + estimatedTx = await estimateTxForAmount( + credentialsAmount, + outputs, + feeRateInt, + memo: memo, + ); } - }); - outputs.forEach((item) { - final outputAmount = hasMultiDestination - ? item.formattedCryptoAmount - : amount; - final outputAddress = item.isParsedAddress - ? item.extractedAddress! - : item.address; - txb.addOutput( - addressToOutputScript(outputAddress, networkType), - outputAmount!); - }); + BasedBitcoinTransacationBuilder txb; + if (network is BitcoinCashNetwork) { + txb = ForkedTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: outputs, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: !estimatedTx.spendsUnconfirmedTX, + ); + } else { + txb = BitcoinTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: outputs, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: !estimatedTx.spendsUnconfirmedTX, + ); + } - final estimatedSize = - estimatedTransactionSize(inputs.length, outputs.length + 1); - var feeAmount = 0; + bool hasTaprootInputs = false; - if (transactionCredentials.feeRate != null) { - feeAmount = transactionCredentials.feeRate! * estimatedSize; - } else { - feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; - } - - final changeValue = totalInputAmount - amount - feeAmount; + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + final key = estimatedTx.privateKeys + .firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); - if (changeValue > minAmount) { - txb.addOutput(changeAddress, changeValue); - } + if (key == null) { + throw Exception("Cannot find private key"); + } - for (var i = 0; i < inputs.length; i++) { - final input = inputs[i]; - final keyPair = generateKeyPair( - hd: input.address.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: input.address.index, - network: networkType); - final witnessValue = input.isP2wpkh ? input.value : null; - - txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); - } - - return PendingBitcoinTransaction(txb.build(), type, - electrumClient: electrumClient, amount: amount, fee: fee) - ..addListener((transaction) async { - transactionHistory.addOne(transaction); - await updateBalance(); + if (utxo.utxo.isP2tr()) { + hasTaprootInputs = true; + return key.signTapRoot(txDigest, sighash: sighash); + } else { + return key.signInput(txDigest, sigHash: sighash); + } }); + + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } catch (e) { + throw e; + } } String toJSON() => json.encode({ 'mnemonic': mnemonic, - 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), - 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), - 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance[currency]?.toJSON() + 'passphrase': passphrase ?? '', + 'account_index': walletAddresses.currentReceiveAddressIndexByType, + 'change_address_index': walletAddresses.currentChangeAddressIndexByType, + 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), + 'address_page_type': walletInfo.addressPageType == null + ? SegwitAddresType.p2wpkh.toString() + : walletInfo.addressPageType.toString(), + 'balance': balance[currency]?.toJSON(), + 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, + 'derivationPath': walletInfo.derivationInfo?.derivationPath, }); int feeRate(TransactionPriority priority) { @@ -363,34 +639,34 @@ abstract class ElectrumWalletBase extends WalletBase - feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount, + {int? size}) => + feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); - int feeAmountWithFeeRate(int feeRate, int inputsCount, - int outputsCount) => - feeRate * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) => + feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); @override int calculateEstimatedFee(TransactionPriority? priority, int? amount, - {int? outputsCount}) { + {int? outputsCount, int? size}) { if (priority is BitcoinTransactionPriority) { - return calculateEstimatedFeeWithFeeRate( - feeRate(priority), - amount, - outputsCount: outputsCount); + return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount, + outputsCount: outputsCount, size: size); } return 0; } - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, - {int? outputsCount}) { + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { + if (size != null) { + return feeAmountWithFeeRate(feeRate, 0, 0, size: size); + } + int inputsCount = 0; if (amount != null) { @@ -419,8 +695,7 @@ abstract class ElectrumWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + @override Future changePassword(String password) async { _password = password; @@ -437,9 +734,6 @@ abstract class ElectrumWalletBase extends WalletBase - generateKeyPair(hd: hd, index: index, network: networkType); - @override Future rescan({required int height}) async => throw UnimplementedError(); @@ -450,22 +744,27 @@ abstract class ElectrumWalletBase extends WalletBase makePath() async => - pathForWallet(name: walletInfo.name, type: walletInfo.type); + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { - final unspent = await Future.wait(walletAddresses - .addresses.map((address) => electrumClient - .getListUnspentWithAddress(address.address, networkType) - .then((unspent) => unspent - .map((unspent) { + List updatedUnspentCoins = []; + + final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + + await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient + .getListUnspentWithAddress(address.address, network) + .then((unspent) => Future.forEach>(unspent, (unspent) async { try { - return BitcoinUnspent.fromJSON(address, unspent); - } catch(_) { - return null; - } - }).whereNotNull()))); - unspentCoins = unspent.expand((e) => e).toList(); + final coin = BitcoinUnspent.fromJSON(address, unspent); + final tx = await fetchTransactionInfo( + hash: coin.hash, height: 0, myAddresses: addressesSet); + coin.isChange = tx?.direction == TransactionDirection.outgoing; + coin.confirmations = tx?.confirmations; + updatedUnspentCoins.add(coin); + } catch (_) {} + })))); + + unspentCoins = updatedUnspentCoins; if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); @@ -475,7 +774,9 @@ abstract class ElectrumWalletBase extends WalletBase - element.walletId.contains(id) && element.hash.contains(coin.hash)); + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -483,6 +784,7 @@ abstract class ElectrumWalletBase extends WalletBase _addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.address.address, - value: coin.value, - vout: coin.vout, + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.bitcoinAddressRecord.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, ); await unspentCoinsInfo.add(newInfo); @@ -510,12 +813,13 @@ abstract class ElectrumWalletBase extends WalletBase _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id)); + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((element) => element.walletId.contains(id)); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + final existUnspentCoins = unspentCoins + .where((coin) => element.hash.contains(coin.hash) && element.vout == coin.vout); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -531,19 +835,207 @@ abstract class ElectrumWalletBase extends WalletBase getTransactionExpanded( - {required String hash, required int height}) async { + Future canReplaceByFee(String hash) async { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); - final transactionHex = verboseTransaction['hex'] as String; - final original = bitcoin.Transaction.fromHex(transactionHex); - final ins = []; - final time = verboseTransaction['time'] as int?; final confirmations = verboseTransaction['confirmations'] as int? ?? 0; + final transactionHex = verboseTransaction['hex'] as String?; - for (final vin in original.ins) { - final id = HEX.encode(vin.hash!.reversed.toList()); - final txHex = await electrumClient.getTransactionHex(hash: id); - final tx = bitcoin.Transaction.fromHex(txHex); + if (confirmations > 0) return false; + + if (transactionHex == null) { + return false; + } + + final original = bitcoin.Transaction.fromHex(transactionHex); + + return original.ins + .any((element) => element.sequence != null && element.sequence! < 4294967293); + } + + Future isChangeSufficientForFee(String txId, int newFee) async { + final bundle = await getTransactionExpanded(hash: txId); + final outputs = bundle.originalTransaction.outputs; + + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + + // look for a change address in the outputs + final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( + (element) => element.address == addressFromOutputScript(output.scriptPubKey, network))); + + var allInputsAmount = 0; + + for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final vout = input.txIndex; + final outTransaction = inputTransaction.outputs[vout]; + allInputsAmount += outTransaction.amount.toInt(); + } + + int totalOutAmount = bundle.originalTransaction.outputs + .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); + + var currentFee = allInputsAmount - totalOutAmount; + + int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; + + return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0; + } + + Future replaceByFee(String hash, int newFee) async { + try { + final bundle = await getTransactionExpanded(hash: hash); + + final utxos = []; + List privateKeys = []; + + var allInputsAmount = 0; + + // Add inputs + for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final vout = input.txIndex; + final outTransaction = inputTransaction.outputs[vout]; + final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + allInputsAmount += outTransaction.amount.toInt(); + + final addressRecord = + walletAddresses.allAddresses.firstWhere((element) => element.address == address); + + final btcAddress = addressTypeFromStr(addressRecord.address, network); + final privkey = generateECPrivate( + hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: addressRecord.index, + network: network); + + privateKeys.add(privkey); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: input.txId, + value: outTransaction.amount, + vout: vout, + scriptType: _getScriptType(btcAddress), + ), + ownerDetails: + UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress), + ), + ); + } + + int totalOutAmount = bundle.originalTransaction.outputs + .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); + + var currentFee = allInputsAmount - totalOutAmount; + int remainingFee = newFee - currentFee; + + final outputs = []; + + // Add outputs and deduct the fees from it + for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) { + final out = bundle.originalTransaction.outputs[i]; + final address = addressFromOutputScript(out.scriptPubKey, network); + final btcAddress = addressTypeFromStr(address, network); + + int newAmount; + if (out.amount.toInt() >= remainingFee) { + newAmount = out.amount.toInt() - remainingFee; + remainingFee = 0; + + // if new amount of output is less than dust amount, then don't add this output as well + if (newAmount <= _dustAmount) { + continue; + } + } else { + remainingFee -= out.amount.toInt(); + continue; + } + + outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount))); + } + + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + + // look for a change address in the outputs + final changeOutput = outputs.firstWhereOrNull((output) => + changeAddresses.any((element) => element.address == output.address.toAddress(network))); + + // deduct the change amount from the output amount + if (changeOutput != null) { + totalOutAmount -= changeOutput.value.toInt(); + } + + final txb = BitcoinTransactionBuilder( + utxos: utxos, + outputs: outputs, + fee: BigInt.from(newFee), + network: network, + enableRBF: true, + ); + + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + final key = + privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); + + if (key == null) { + throw Exception("Cannot find private key"); + } + + if (utxo.utxo.isP2tr()) { + return key.signTapRoot(txDigest, sighash: sighash); + } else { + return key.signInput(txDigest, sigHash: sighash); + } + }); + + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: totalOutAmount, + fee: newFee, + network: network, + hasChange: changeOutput != null, + feeRate: newFee.toString(), + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } catch (e) { + throw e; + } + } + + Future getTransactionExpanded({required String hash}) async { + String transactionHex; + int? time; + int confirmations = 0; + if (network == BitcoinNetwork.testnet) { + // Testnet public electrum server does not support verbose transaction fetching + transactionHex = await electrumClient.getTransactionHex(hash: hash); + + final status = json.decode( + (await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body); + + time = status["block_time"] as int?; + final tip = await electrumClient.getCurrentBlockChainTip() ?? 0; + confirmations = tip - (status["block_height"] as int? ?? 0); + } else { + final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); + + transactionHex = verboseTransaction['hex'] as String; + time = verboseTransaction['time'] as int?; + confirmations = verboseTransaction['confirmations'] as int? ?? 0; + } + + final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex); + final ins = []; + + for (final vin in original.inputs) { + final txHex = await electrumClient.getTransactionHex(hash: vin.txId); + final tx = bitcoin_base.BtcTransaction.fromRaw(txHex); ins.add(tx); } @@ -551,66 +1043,121 @@ abstract class ElectrumWalletBase extends WalletBase fetchTransactionInfo( - {required String hash, required int height}) async { - try { - final tx = await getTransactionExpanded(hash: hash, height: height); - final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); - return ElectrumTransactionInfo.fromElectrumBundle( - tx, - walletInfo.type, - networkType, - addresses: addresses, - height: height); - } catch(_) { - return null; + {required String hash, + required int height, + required Set myAddresses, + bool? retryOnFailure}) async { + try { + return ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded(hash: hash), walletInfo.type, network, + addresses: myAddresses, height: height); + } catch (e) { + if (e is FormatException && retryOnFailure == true) { + await Future.delayed(const Duration(seconds: 2)); + return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses); } + return null; + } } @override Future> fetchTransactions() async { - final addressHashes = {}; - final normalizedHistories = >[]; - walletAddresses.addresses.forEach((addressRecord) { - final sh = scriptHash(addressRecord.address, networkType: networkType); - addressHashes[sh] = addressRecord; - }); - final histories = - addressHashes.keys.map((scriptHash) => electrumClient - .getHistory(scriptHash) - .then((history) => {scriptHash: history})); - final historyResults = await Future.wait(histories); - historyResults.forEach((history) { - history.entries.forEach((historyItem) { - if (historyItem.value.isNotEmpty) { - final address = addressHashes[historyItem.key]; - address?.setAsUsed(); - normalizedHistories.addAll(historyItem.value); - } - }); - }); - final historiesWithDetails = await Future.wait( - normalizedHistories - .map((transaction) { - try { - return fetchTransactionInfo( - hash: transaction['tx_hash'] as String, - height: transaction['height'] as int); - } catch(_) { - return Future.value(null); + try { + final Map historiesWithDetails = {}; + final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + final currentHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + + await Future.wait(ADDRESS_TYPES.map((type) { + final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); + + return Future.wait(addressesByType.map((addressRecord) async { + final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight); + final balance = await electrumClient.getBalance(addressRecord.scriptHash!); + + if (history.isNotEmpty) { + addressRecord.txCount = history.length; + addressRecord.balance = balance['confirmed'] as int? ?? 0; + historiesWithDetails.addAll(history); + + final matchedAddresses = + addressesByType.where((addr) => addr.isHidden == addressRecord.isHidden); + + final isLastUsedAddress = + history.isNotEmpty && addressRecord.address == matchedAddresses.last.address; + + if (isLastUsedAddress) { + await walletAddresses.discoverAddresses( + matchedAddresses.toList(), + addressRecord.isHidden, + (address, addressesSet) => + _fetchAddressHistory(address, addressesSet, currentHeight) + .then((history) => history.isNotEmpty ? address.address : null), + type: type); + } } })); - return historiesWithDetails.fold>( - {}, (acc, tx) { - if (tx == null) { - return acc; + })); + + return historiesWithDetails; + } catch (e) { + print(e.toString()); + return {}; + } + } + + Future> _fetchAddressHistory( + BitcoinAddressRecord addressRecord, Set addressesSet, int currentHeight) async { + try { + final Map historiesWithDetails = {}; + + final history = await electrumClient + .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)); + + if (history.isNotEmpty) { + addressRecord.setAsUsed(); + + await Future.wait(history.map((transaction) async { + final txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + final storedTx = transactionHistory.transactions[txid]; + + if (storedTx != null) { + if (height > 0) { + storedTx.height = height; + // the tx's block itself is the first confirmation so add 1 + storedTx.confirmations = currentHeight - height + 1; + storedTx.isPending = storedTx.confirmations == 0; + } + + historiesWithDetails[txid] = storedTx; + } else { + final tx = await fetchTransactionInfo( + hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true); + + if (tx != null) { + historiesWithDetails[txid] = tx; + + // Got a new transaction fetched, add it to the transaction history + // instead of waiting all to finish, and next time it will be faster + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + } + + return Future.value(null); + })); } - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); + + return historiesWithDetails; + } catch (e) { + print(e.toString()); + return {}; + } } Future updateTransactions() async { @@ -620,10 +1167,8 @@ abstract class ElectrumWalletBase extends WalletBase _fetchBalances() async { - final addresses = walletAddresses.addresses.toList(); + final addresses = walletAddresses.allAddresses.toList(); final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = scriptHash(addressRecord.address, networkType: networkType); + final sh = scriptHash(addressRecord.address, network: network); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -662,8 +1211,11 @@ abstract class ElectrumWalletBase extends WalletBase updateBalance() async { @@ -698,14 +1250,114 @@ abstract class ElectrumWalletBase extends WalletBase addr.isHidden) - .toList(); + var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList(); if (addresses.length < minCountOfHiddenAddresses) { - addresses = walletAddresses.addresses.toList(); + addresses = walletAddresses.allAddresses.toList(); } return addresses[random.nextInt(addresses.length)].address; } + + @override + void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; + + @override + String signMessage(String message, {String? address = null}) { + final index = address != null + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index + : null; + final HD = index == null ? hd : hd.derive(index); + return base64Encode(HD.signMessage(message)); + } + + static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) { + if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) { + return BitcoinCashNetwork.mainnet; + } + + if (networkType == litecoinNetwork) { + return LitecoinNetwork.mainnet; + } + + if (networkType == bitcoin.testnet) { + return BitcoinNetwork.testnet; + } + + return BitcoinNetwork.mainnet; + } +} + +class EstimateTxParams { + EstimateTxParams( + {required this.amount, + required this.feeRate, + required this.priority, + required this.outputsCount, + required this.size}); + + final int amount; + final int feeRate; + final TransactionPriority priority; + final int outputsCount; + final int size; +} + +class EstimatedTxResult { + EstimatedTxResult({ + required this.utxos, + required this.privateKeys, + required this.fee, + required this.amount, + required this.hasChange, + required this.isSendAll, + this.memo, + required this.spendsUnconfirmedTX, + }); + + final List utxos; + final List privateKeys; + final int fee; + final int amount; + final bool hasChange; + final bool isSendAll; + final String? memo; + final bool spendsUnconfirmedTX; +} + +BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { + if (network is BitcoinCashNetwork) { + if (!address.startsWith("bitcoincash:") && + (address.startsWith("q") || address.startsWith("p"))) { + address = "bitcoincash:$address"; + } + + return BitcoinCashAddress(address).baseAddress; + } + + if (P2pkhAddress.regex.hasMatch(address)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (P2shAddress.regex.hasMatch(address)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (P2wshAddress.regex.hasMatch(address)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (P2trAddress.regex.hasMatch(address)) { + return P2trAddress.fromAddress(address: address, network: network); + } else { + return P2wpkhAddress.fromAddress(address: address, network: network); + } +} + +BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { + if (type is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (type is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (type is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (type is P2trAddress) { + return SegwitAddresType.p2tr; + } else { + return SegwitAddresType.p2wpkh; + } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 741c2fe1c..c43d4988a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,94 +1,167 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase - with _$ElectrumWalletAddresses; +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; + +const List ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + P2pkhAddressType.p2pkh, + SegwitAddresType.p2tr, + SegwitAddresType.p2wsh, + P2shAddressType.p2wpkhInP2sh, +]; abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { - ElectrumWalletAddressesBase(WalletInfo walletInfo, - {required this.mainHd, - required this.sideHd, - required this.electrumClient, - required this.networkType, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : addresses = ObservableList.of( - (initialAddresses ?? []).toSet()), - receiveAddresses = ObservableList.of( - (initialAddresses ?? []) + ElectrumWalletAddressesBase( + WalletInfo walletInfo, { + required this.mainHd, + required this.sideHd, + required this.electrumClient, + required this.network, + List? initialAddresses, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + BitcoinAddressType? initialAddressPageType, + }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), + addressesByReceiveType = + ObservableList.of(([]).toSet()), + receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of( - (initialAddresses ?? []) + .toSet()), + changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - currentReceiveAddressIndex = initialRegularAddressIndex, - currentChangeAddressIndex = initialChangeAddressIndex, - super(walletInfo); + .toSet()), + currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, + currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, + _addressPageType = initialAddressPageType ?? + (walletInfo.addressPageType != null + ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + : SegwitAddresType.p2wpkh), + super(walletInfo) { + updateAddressesByMatch(); + } static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; static const gap = 20; - final ObservableList addresses; + final ObservableList _addresses; + // Matched by addressPageType + late ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; final ElectrumClient electrumClient; - final bitcoin.NetworkType networkType; + final BasedUtxoNetwork network; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; + @observable + late BitcoinAddressType _addressPageType; + + @computed + BitcoinAddressType get addressPageType => _addressPageType; + + @computed + List get allAddresses => _addresses; + @override @computed String get address { - if (receiveAddresses.isEmpty) { - return generateNewAddress().address; + String receiveAddress; + + final typeMatchingReceiveAddresses = + receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed); + + if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || + typeMatchingReceiveAddresses.isEmpty) { + receiveAddress = generateNewAddress().address; + } else { + final previousAddressMatchesType = + previousAddressRecord != null && previousAddressRecord!.type == addressPageType; + + if (previousAddressMatchesType && + typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { + receiveAddress = previousAddressRecord!.address; + } else { + receiveAddress = typeMatchingReceiveAddresses.first.address; + } } - return receiveAddresses.first.address; + return receiveAddress; + } + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + set address(String addr) { + final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); + + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); } @override - set address(String addr) => null; + String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType); - int currentReceiveAddressIndex; - int currentChangeAddressIndex; + Map currentReceiveAddressIndexByType; + + int get currentReceiveAddressIndex => + currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentReceiveAddressIndex(int index) => + currentReceiveAddressIndexByType[_addressPageType.toString()] = index; + + Map currentChangeAddressIndexByType; + + int get currentChangeAddressIndex => + currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentChangeAddressIndex(int index) => + currentChangeAddressIndexByType[_addressPageType.toString()] = index; + + @observable + BitcoinAddressRecord? previousAddressRecord; @computed - int get totalCountOfReceiveAddresses => - addresses.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + if (!addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); @computed - int get totalCountOfChangeAddresses => - addresses.fold(0, (acc, addressRecord) { - if (addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); - - Future discoverAddresses() async { - await _discoverAddresses(mainHd, false); - await _discoverAddresses(sideHd, true); - await updateAddressesInBox(); - } + int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + if (addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); @override Future init() async { - await _generateInitialAddresses(); + if (walletInfo.type == WalletType.bitcoinCash) { + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + } else if (walletInfo.type == WalletType.litecoin) { + await _generateInitialAddresses(); + } else if (walletInfo.type == WalletType.bitcoin) { + await _generateInitialAddresses(); + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + } + updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); await updateAddressesInBox(); @@ -105,16 +178,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future getChangeAddress() async { updateChangeAddresses(); - + if (changeAddresses.isEmpty) { - final newAddresses = await _createNewAddresses( - gap, - hd: sideHd, - startIndex: totalCountOfChangeAddresses > 0 - ? totalCountOfChangeAddresses - 1 - : 0, - isHidden: true); - _addAddresses(newAddresses); + final newAddresses = await _createNewAddresses(gap, + startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, + isHidden: true); + addAddresses(newAddresses); } if (currentChangeAddressIndex >= changeAddresses.length) { @@ -127,147 +196,179 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - BitcoinAddressRecord generateNewAddress( - {bitcoin.HDWallet? hd, bool isHidden = false}) { - currentReceiveAddressIndex += 1; - // FIX-ME: Check logic for whichi HD should be used here ??? + BitcoinAddressRecord generateNewAddress({String label = ''}) { + final newAddressIndex = addressesByReceiveType.fold( + 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); + final address = BitcoinAddressRecord( - getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd), - index: currentReceiveAddressIndex, - isHidden: isHidden); - addresses.add(address); + getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType), + index: newAddressIndex, + isHidden: false, + name: label, + type: addressPageType, + network: network, + ); + _addresses.add(address); + updateAddressesByMatch(); return address; } - String getAddress({required int index, required bitcoin.HDWallet hd}) => ''; + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + ''; @override Future updateAddressesInBox() async { try { addressesMap.clear(); addressesMap[address] = ''; + + allAddressesMap.clear(); + _addresses.forEach((addressRecord) { + allAddressesMap[addressRecord.address] = addressRecord.name; + }); await saveAddressesInBox(); } catch (e) { print(e.toString()); } } + @action + void updateAddress(String address, String label) { + final addressRecord = + _addresses.firstWhere((addressRecord) => addressRecord.address == address); + addressRecord.setNewName(label); + final index = _addresses.indexOf(addressRecord); + _addresses.remove(addressRecord); + _addresses.insert(index, addressRecord); + + updateAddressesByMatch(); + } + + @action + void updateAddressesByMatch() { + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); + } + @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAdresses = addresses - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); - receiveAddresses.addAll(newAdresses); + final newAddresses = + _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + receiveAddresses.addAll(newAddresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAdresses = addresses - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); - changeAddresses.addAll(newAdresses); + final newAddresses = _addresses.where((addressRecord) => + addressRecord.isHidden && + !addressRecord.isUsed && + // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); + changeAddresses.addAll(newAddresses); } - Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { - var hasAddrUse = true; - List addrs; - - if (addresses.isNotEmpty) { - addrs = addresses - .where((addr) => addr.isHidden == isHidden) - .toList(); - } else { - addrs = await _createNewAddresses( - isHidden - ? defaultChangeAddressesCount - : defaultReceiveAddressesCount, - startIndex: 0, - hd: hd, - isHidden: isHidden); + @action + Future discoverAddresses(List addressList, bool isHidden, + Future Function(BitcoinAddressRecord, Set) getAddressHistory, + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { + if (!isHidden) { + _validateSideHdAddresses(addressList.toList()); } - while(hasAddrUse) { - final addr = addrs.last.address; - hasAddrUse = await _hasAddressUsed(addr); + final newAddresses = await _createNewAddresses(gap, + startIndex: addressList.length, isHidden: isHidden, type: type); + addAddresses(newAddresses); - if (!hasAddrUse) { - break; - } + final addressesWithHistory = await Future.wait(newAddresses + .map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet()))); + final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; - final start = addrs.length; - final count = start + gap; - final batch = await _createNewAddresses( - count, - startIndex: start, - hd: hd, - isHidden: isHidden); - addrs.addAll(batch); - } - - if (addresses.length < addrs.length) { - _addAddresses(addrs); + if (isLastAddressUsed) { + discoverAddresses(addressList, isHidden, getAddressHistory, type: type); } } - Future _generateInitialAddresses() async { + Future _generateInitialAddresses( + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { var countOfReceiveAddresses = 0; var countOfHiddenAddresses = 0; - addresses.forEach((addr) { - if (addr.isHidden) { - countOfHiddenAddresses += 1; - return; - } + _addresses.forEach((addr) { + if (addr.type == type) { + if (addr.isHidden) { + countOfHiddenAddresses += 1; + return; + } - countOfReceiveAddresses += 1; + countOfReceiveAddresses += 1; + } }); if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfReceiveAddresses, - hd: mainHd, - isHidden: false); - addresses.addAll(newAddresses); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfReceiveAddresses, isHidden: false, type: type); + addAddresses(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfHiddenAddresses, - hd: sideHd, - isHidden: true); - addresses.addAll(newAddresses); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfHiddenAddresses, isHidden: true, type: type); + addAddresses(newAddresses); } } Future> _createNewAddresses(int count, - {required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false}) async { + {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { final list = []; for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - getAddress(index: i, hd: hd), - index: i, - isHidden: isHidden); + getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + index: i, + isHidden: isHidden, + type: type ?? addressPageType, + network: network, + ); list.add(address); } return list; } - void _addAddresses(Iterable addresses) { - final addressesSet = this.addresses.toSet(); + @action + void addAddresses(Iterable addresses) { + final addressesSet = this._addresses.toSet(); addressesSet.addAll(addresses); - this.addresses.removeRange(0, this.addresses.length); - this.addresses.addAll(addressesSet); + this._addresses.clear(); + this._addresses.addAll(addressesSet); + updateAddressesByMatch(); } - Future _hasAddressUsed(String address) async { - final sh = scriptHash(address, networkType: networkType); - final transactionHistory = await electrumClient.getHistory(sh); - return transactionHistory.isNotEmpty; + void _validateSideHdAddresses(List addrWithTransactions) { + addrWithTransactions.forEach((element) { + if (element.address != + getAddress(index: element.index, hd: mainHd, addressType: element.type)) + element.isHidden = true; + }); } -} \ No newline at end of file + + @action + Future setAddressType(BitcoinAddressType type) async { + _addressPageType = type; + updateAddressesByMatch(); + walletInfo.addressPageType = addressPageType.toString(); + await walletInfo.save(); + } + + bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) { + return _isAddressByType(addressRecord, addressPageType); + } + + bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; +} diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 6db0c23f2..218792e3c 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -1,12 +1,14 @@ import 'dart:convert'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/file.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; -class ElectrumWallletSnapshot { - ElectrumWallletSnapshot({ +class ElectrumWalletSnapshot { + ElectrumWalletSnapshot({ required this.name, required this.type, required this.password, @@ -14,46 +16,76 @@ class ElectrumWallletSnapshot { required this.addresses, required this.balance, required this.regularAddressIndex, - required this.changeAddressIndex}); + required this.changeAddressIndex, + required this.addressPageType, + this.passphrase, + this.derivationType, + this.derivationPath, + }); final String name; final String password; final WalletType type; + final String? addressPageType; String mnemonic; List addresses; ElectrumBalance balance; - int regularAddressIndex; - int changeAddressIndex; + Map regularAddressIndex; + Map changeAddressIndex; + String? passphrase; + DerivationType? derivationType; + String? derivationPath; - static Future load(String name, WalletType type, String password) async { + static Future load( + String name, WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String; + final passphrase = data['passphrase'] as String? ?? ''; final addresses = addressesTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) .toList(); final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - var regularAddressIndex = 0; - var changeAddressIndex = 0; + var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + + final derivationType = + DerivationType.values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; + final derivationPath = data['derivationPath'] as String? ?? "m/0'/0"; try { - regularAddressIndex = int.parse(data['account_index'] as String? ?? '0'); - changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0'); - } catch (_) {} + regularAddressIndexByType = { + SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') + }; + changeAddressIndexByType = { + SegwitAddresType.p2wpkh.toString(): + int.parse(data['change_address_index'] as String? ?? '0') + }; + } catch (_) { + try { + regularAddressIndexByType = data["account_index"] as Map? ?? {}; + changeAddressIndexByType = data["change_address_index"] as Map? ?? {}; + } catch (_) {} + } - return ElectrumWallletSnapshot( + return ElectrumWalletSnapshot( name: name, type: type, password: password, + passphrase: passphrase, mnemonic: mnemonic, addresses: addresses, balance: balance, - regularAddressIndex: regularAddressIndex, - changeAddressIndex: changeAddressIndex); + regularAddressIndex: regularAddressIndexByType, + changeAddressIndex: changeAddressIndexByType, + addressPageType: data['address_page_type'] as String?, + derivationType: derivationType, + derivationPath: derivationPath, + ); } } diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart new file mode 100644 index 000000000..979c1a433 --- /dev/null +++ b/cw_bitcoin/lib/exceptions.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/exceptions.dart'; + +class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { + BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc); +} + +class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} + +class BitcoinTransactionNoFeeException extends TransactionNoFeeException {} + +class BitcoinTransactionNoDustException extends TransactionNoDustException {} + +class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException { + BitcoinTransactionNoDustOnChangeException(super.max, super.min); +} + +class BitcoinTransactionCommitFailed extends TransactionCommitFailed { + BitcoinTransactionCommitFailed({super.errorMessage}); +} + +class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {} + +class BitcoinTransactionCommitFailedDustOutput extends TransactionCommitFailedDustOutput {} + +class BitcoinTransactionCommitFailedDustOutputSendAll + extends TransactionCommitFailedDustOutputSendAll {} + +class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6bf1c5735..4d166e47b 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -14,23 +15,25 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bip39/bip39.dart' as bip39; part 'litecoin_wallet.g.dart'; class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { - LitecoinWalletBase( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required Uint8List seedBytes, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( + LitecoinWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, @@ -41,38 +44,57 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { seedBytes: seedBytes, currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( - walletInfo, - electrumClient: electrumClient, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: bitcoin.HDWallet - .fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/1"), - networkType: networkType,); + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + network: network, + ); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } - static Future create({ - required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0 - }) async { + static Future create( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + String? passphrase, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex}) async { + late Uint8List seedBytes; + + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic); + break; + } return LitecoinWallet( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: addressPageType, + ); } static Future open({ @@ -81,17 +103,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load (name, walletInfo.type, password); + final snp = + await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); return LitecoinWallet( - mnemonic: snp.mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: await mnemonicToSeedBytes(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: snp.addressPageType, + ); } @override diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index a317fa9f2..993d17933 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,39 +1,28 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( - WalletInfo walletInfo, - {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + }) : super(walletInfo); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); -} \ No newline at end of file + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + generateP2WPKHAddress(hd: hd, index: index, network: network); +} diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 2093647fd..9143556ab 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/wallet_service.dart'; @@ -11,6 +11,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:collection/collection.dart'; +import 'package:bip39/bip39.dart' as bip39; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, @@ -25,10 +26,11 @@ class LitecoinWalletService extends WalletService< WalletType getType() => WalletType.litecoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await LitecoinWalletBase.create( - mnemonic: await generateMnemonic(), + mnemonic: await generateElectrumMnemonic(), password: credentials.password!, + passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); @@ -45,32 +47,68 @@ class LitecoinWalletService extends WalletService< Future openWallet(String name, String password) async { final walletInfo = walletInfoSource.values.firstWhereOrNull( (info) => info.id == WalletBase.idFor(name, getType()))!; - final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); - await wallet.init(); - return wallet; + + try { + final wallet = await LitecoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + final wallet = await LitecoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + return wallet; + } } @override - Future remove(String wallet) async => - File(await pathForWalletDir(name: wallet, type: getType())) - .delete(recursive: true); + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await LitecoinWalletBase.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } @override Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async => + BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => throw UnimplementedError(); @override Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials) async { - if (!validateMnemonic(credentials.mnemonic)) { - throw BitcoinMnemonicIsIncorrectException(); + BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { + throw LitecoinMnemonicIsIncorrectException(); } final wallet = await LitecoinWalletBase.create( password: credentials.password!, + passphrase: credentials.passphrase, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); diff --git a/cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart b/cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart similarity index 50% rename from cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart rename to cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart index 8d0583ce5..779bd3ea2 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart +++ b/cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart @@ -3,3 +3,9 @@ class BitcoinMnemonicIsIncorrectException implements Exception { String toString() => 'Bitcoin mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; } + +class LitecoinMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Litecoin mnemonic has incorrect format. Mnemonic should contain 24 words separated by space.'; +} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index e2dc10bfb..a59b4f429 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,5 +1,5 @@ -import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/exceptions.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; @@ -8,23 +8,35 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { - PendingBitcoinTransaction(this._tx, this.type, - {required this.electrumClient, - required this.amount, - required this.fee}) - : _listeners = []; + PendingBitcoinTransaction( + this._tx, + this.type, { + required this.electrumClient, + required this.amount, + required this.fee, + required this.feeRate, + this.network, + required this.hasChange, + this.isSendAll = false, + this.hasTaprootInputs = false, + }) : _listeners = []; final WalletType type; - final bitcoin.Transaction _tx; + final BtcTransaction _tx; final ElectrumClient electrumClient; final int amount; final int fee; + final String feeRate; + final BasedUtxoNetwork? network; + final bool hasChange; + final bool isSendAll; + final bool hasTaprootInputs; @override - String get id => _tx.getId(); + String get id => _tx.txId(); @override - String get hex => _tx.toHex(); + String get hex => _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -32,22 +44,45 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get feeFormatted => bitcoinAmountToString(amount: fee); + @override + int? get outputCount => _tx.outputs.length; + final List _listeners; @override Future commit() async { - final result = - await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); + int? callId; + + final result = await electrumClient.broadcastTransaction( + transactionRaw: hex, network: network, idCallback: (id) => callId = id); if (result.isEmpty) { - throw BitcoinCommitTransactionException(); + if (callId != null) { + final error = electrumClient.getErrorMessage(callId!); + + if (error.contains("dust")) { + if (hasChange) { + throw BitcoinTransactionCommitFailedDustChange(); + } else if (!isSendAll) { + throw BitcoinTransactionCommitFailedDustOutput(); + } else { + throw BitcoinTransactionCommitFailedDustOutputSendAll(); + } + } + + if (error.contains("bad-txns-vout-negative")) { + throw BitcoinTransactionCommitFailedVoutNegative(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); + } + + throw BitcoinTransactionCommitFailed(); } - _listeners?.forEach((listener) => listener(transactionInfo())); + _listeners.forEach((listener) => listener(transactionInfo())); } - void addListener( - void Function(ElectrumTransactionInfo transaction) listener) => + void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart index 76a1bfcf0..2130fcbbe 100644 --- a/cw_bitcoin/lib/script_hash.dart +++ b/cw_bitcoin/lib/script_hash.dart @@ -1,9 +1,9 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:crypto/crypto.dart'; +import 'package:cw_bitcoin/address_to_output_script.dart'; +import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; -String scriptHash(String address, {required bitcoin.NetworkType networkType}) { - final outputScript = - bitcoin.Address.addressToOutputScript(address, networkType); +String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) { + final outputScript = addressToOutputScript(address, network); final parts = sha256.convert(outputScript).toString().split(''); var res = ''; diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 0d5a413b3..b3707e764 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -1,55 +1,62 @@ import 'dart:typed_data'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; -bitcoin.PaymentData generatePaymentData( - {required bitcoin.HDWallet hd, required int index}) => - PaymentData( - pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); +bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, int? index}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return PaymentData(pubkey: Uint8List.fromList(HEX.decode(pubKey))); +} -bitcoin.ECPair generateKeyPair( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType network}) => - bitcoin.ECPair.fromWIF(hd.derive(index).wif!, network: network); +ECPrivate generateECPrivate( + {required bitcoin.HDWallet hd, required BasedUtxoNetwork network, int? index}) { + final wif = index != null ? hd.derive(index).wif! : hd.wif!; + return ECPrivate.fromWif(wif, netVersion: network.wifNetVer); +} -String generateP2WPKHAddress( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2WPKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), - network: networkType) - .data - .address!; +String generateP2WPKHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network); +} -String generateP2WPKHAddressByPath( - {required bitcoin.HDWallet hd, - required String path, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2WPKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey!))), - network: networkType) - .data - .address!; +String generateP2SHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network); +} -String generateP2PKHAddress( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2PKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), - network: networkType) - .data - .address!; +String generateP2WSHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network); +} + +String generateP2PKHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network); +} + +String generateP2TRAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network); +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4d864059f..86d58b9b1 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,35 +21,35 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.2" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" bech32: dependency: transitive description: path: "." - ref: "cake-0.2.1" - resolved-ref: cafd1c270641e95017d57d69f55cca9831d4db56 + ref: "cake-0.2.2" + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" url: "https://github.com/cake-tech/bech32.git" source: git - version: "0.2.1" + version: "0.2.2" bip32: dependency: transitive description: @@ -66,15 +66,41 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - bitcoin_flutter: + bitbox: + dependency: "direct main" + description: + path: "." + ref: Add-Support-For-OP-Return-data + resolved-ref: "57b78afb85bd2c30d3cdb9f7884f3878a62be442" + url: "https://github.com/cake-tech/bitbox-flutter.git" + source: git + version: "1.0.1" + bitcoin_base: dependency: "direct main" description: path: "." ref: cake-update-v2 - resolved-ref: "8f86453761c0c26e368392d0ff2c6f12f3b7397b" + resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769" + url: "https://github.com/cake-tech/bitcoin_base.git" + source: git + version: "4.2.0" + bitcoin_flutter: + dependency: "direct main" + description: + path: "." + ref: cake-update-v4 + resolved-ref: e19ffb7e7977278a75b27e0479b3c6f4034223b3 url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git - version: "2.0.2" + version: "2.1.0" + blockchain_utils: + dependency: "direct main" + description: + name: blockchain_utils + sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" boolean_selector: dependency: transitive description: @@ -95,10 +121,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -111,10 +137,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: @@ -127,18 +153,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -151,26 +177,26 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.9.2" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -183,42 +209,42 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cryptography: dependency: "direct main" description: name: cryptography - sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8 + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.5.0" cw_core: dependency: "direct main" description: @@ -235,13 +261,13 @@ packages: source: hosted version: "2.2.4" encrypt: - dependency: "direct main" + dependency: transitive description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -254,10 +280,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -283,10 +309,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -296,26 +322,26 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hex: dependency: transitive description: @@ -344,10 +370,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -368,10 +394,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -384,34 +410,34 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -424,10 +450,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -440,18 +466,26 @@ packages: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -464,82 +498,82 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.8.0" pool: dependency: transitive description: @@ -548,30 +582,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" rxdart: dependency: "direct main" description: @@ -584,23 +618,31 @@ packages: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -669,10 +711,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -685,10 +727,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unorm_dart: dependency: "direct main" description: @@ -709,42 +751,42 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.0.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.19.0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 455ceb4a7..632a3140a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -13,21 +13,29 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.11 - http: ^0.13.4 + http: ^1.1.0 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 cw_core: path: ../cw_core bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v2 + ref: cake-update-v4 + bitbox: + git: + url: https://github.com/cake-tech/bitbox-flutter.git + ref: Add-Support-For-OP-Return-data rxdart: ^0.27.5 unorm_dart: ^0.2.0 cryptography: ^2.0.5 - encrypt: ^5.0.1 - + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v2 + blockchain_utils: ^2.1.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/cw_bitcoin_cash/.gitignore b/cw_bitcoin_cash/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_bitcoin_cash/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_bitcoin_cash/.metadata b/cw_bitcoin_cash/.metadata new file mode 100644 index 000000000..4161da6ea --- /dev/null +++ b/cw_bitcoin_cash/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: b06b8b2710955028a6b562f5aa6fe62941d6febf + channel: stable + +project_type: package diff --git a/cw_bitcoin_cash/CHANGELOG.md b/cw_bitcoin_cash/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_bitcoin_cash/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_bitcoin_cash/LICENSE b/cw_bitcoin_cash/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_bitcoin_cash/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_bitcoin_cash/README.md b/cw_bitcoin_cash/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_bitcoin_cash/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_bitcoin_cash/analysis_options.yaml b/cw_bitcoin_cash/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_bitcoin_cash/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_bitcoin_cash/lib/cw_bitcoin_cash.dart b/cw_bitcoin_cash/lib/cw_bitcoin_cash.dart new file mode 100644 index 000000000..732474ac4 --- /dev/null +++ b/cw_bitcoin_cash/lib/cw_bitcoin_cash.dart @@ -0,0 +1,9 @@ +library cw_bitcoin_cash; + +export 'src/bitcoin_cash_base.dart'; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart new file mode 100644 index 000000000..5832835eb --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart @@ -0,0 +1,6 @@ +import 'package:bitbox/bitbox.dart' as bitbox; + +class AddressUtils { + static String getCashAddrFormat(String address) => bitbox.Address.toCashAddress(address); + static String toLegacyAddress(String address) => bitbox.Address.toLegacyAddress(address); +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart new file mode 100644 index 000000000..4699b1649 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart @@ -0,0 +1,7 @@ +export 'bitcoin_cash_wallet.dart'; +export 'bitcoin_cash_wallet_addresses.dart'; +export 'bitcoin_cash_wallet_creation_credentials.dart'; +export 'bitcoin_cash_wallet_service.dart'; +export 'exceptions/exceptions.dart'; +export 'mnemonic.dart'; +export 'bitcoin_cash_address_utils.dart'; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart new file mode 100644 index 000000000..1f04e5624 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -0,0 +1,178 @@ +import 'dart:convert'; + +import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; + +import 'bitcoin_cash_base.dart'; + +part 'bitcoin_cash_wallet.g.dart'; + +class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet; + +abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { + BitcoinCashWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + BitcoinAddressType? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: bitcoin.bitcoin, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.bch) { + walletAddresses = BitcoinCashWalletAddresses( + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"), + network: network, + initialAddressPageType: addressPageType, + ); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); + } + + static Future create( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex}) async { + return BitcoinCashWallet( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await Mnemonic.toSeed(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: P2pkhAddressType.p2pkh, + ); + } + + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + }) async { + final snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + return BitcoinCashWallet( + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses.map((addr) { + try { + BitcoinCashAddress(addr.address); + return BitcoinAddressRecord( + addr.address, + index: addr.index, + isHidden: addr.isHidden, + type: P2pkhAddressType.p2pkh, + network: BitcoinCashNetwork.mainnet, + ); + } catch (_) { + return BitcoinAddressRecord( + AddressUtils.getCashAddrFormat(addr.address), + index: addr.index, + isHidden: addr.isHidden, + type: P2pkhAddressType.p2pkh, + network: BitcoinCashNetwork.mainnet, + ); + } + }).toList(), + initialBalance: snp.balance, + seedBytes: await Mnemonic.toSeed(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: P2pkhAddressType.p2pkh, + ); + } + + bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) => + bitbox.ECPair.fromWIF(hd.derive(index).wif!); + + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { + int inputsCount = 0; + int totalValue = 0; + + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount++; + totalValue += input.value; + } + if (amount != null && totalValue >= amount) { + break; + } + } + + if (amount != null && totalValue < amount) return 0; + + final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); + + return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount); + } + + @override + int feeRate(TransactionPriority priority) { + if (priority is BitcoinCashTransactionPriority) { + switch (priority) { + case BitcoinCashTransactionPriority.slow: + return 1; + case BitcoinCashTransactionPriority.medium: + return 5; + case BitcoinCashTransactionPriority.fast: + return 10; + } + } + + return 0; + } + + @override + String signMessage(String message, {String? address = null}) { + final index = address != null + ? walletAddresses.allAddresses + .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) + .index + : null; + final HD = index == null ? hd : hd.derive(index); + return base64Encode(HD.signMessage(message)); + } +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart new file mode 100644 index 000000000..3164651f3 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -0,0 +1,29 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'bitcoin_cash_wallet_addresses.g.dart'; + +class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$BitcoinCashWalletAddresses; + +abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { + BitcoinCashWalletAddressesBase( + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + super.initialAddressPageType, + }) : super(walletInfo); + + @override + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + generateP2PKHAddress(hd: hd, index: index, network: network); +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart new file mode 100644 index 000000000..72caa6c58 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -0,0 +1,26 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class BitcoinCashNewWalletCredentials extends WalletCredentials { + BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { + BitcoinCashRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class BitcoinCashRestoreWalletFromWIFCredentials extends WalletCredentials { + BitcoinCashRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart new file mode 100644 index 000000000..df8e841f8 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart'; +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:collection/collection.dart'; +import 'package:hive/hive.dart'; + +class BitcoinCashWalletService extends WalletService { + BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + + @override + WalletType getType() => WalletType.bitcoinCash; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future create(credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final wallet = await BitcoinCashWalletBase.create( + mnemonic: await Mnemonic.generate(strength: strength), + password: credentials.password!, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); + await wallet.init(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + + try { + final wallet = await BitcoinCashWalletBase.open( + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + final wallet = await BitcoinCashWalletBase.open( + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + return wallet; + } + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await BitcoinCashWalletBase.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(credentials, {bool? isTestnet}) { + // TODO: implement restoreFromKeys + throw UnimplementedError('restoreFromKeys() is not implemented'); + } + + @override + Future restoreFromSeed(BitcoinCashRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!validateMnemonic(credentials.mnemonic)) { + throw BitcoinCashMnemonicIsIncorrectException(); + } + + final wallet = await BitcoinCashWalletBase.create( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); + await wallet.init(); + return wallet; + } +} diff --git a/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart b/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart new file mode 100644 index 000000000..7cce59085 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart @@ -0,0 +1,5 @@ +class BitcoinCashMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Bitcoin Cash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart b/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart new file mode 100644 index 000000000..746e3248a --- /dev/null +++ b/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart @@ -0,0 +1 @@ +export 'bitcoin_cash_mnemonic_is_incorrect_exception.dart'; \ No newline at end of file diff --git a/cw_bitcoin_cash/lib/src/mnemonic.dart b/cw_bitcoin_cash/lib/src/mnemonic.dart new file mode 100644 index 000000000..b1f1ee984 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/mnemonic.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'package:bip39/bip39.dart' as bip39; + +class Mnemonic { + /// Generate bip39 mnemonic + static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); + + /// Create root seed from mnemonic + static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic); +} diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart new file mode 100644 index 000000000..6d2ab4696 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -0,0 +1,86 @@ +import 'package:cw_bitcoin/exceptions.dart'; +import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_type.dart'; + +class PendingBitcoinCashTransaction with PendingTransaction { + PendingBitcoinCashTransaction(this._tx, this.type, + {required this.electrumClient, + required this.amount, + required this.fee, + required this.hasChange, + required this.isSendAll}) + : _listeners = []; + + final WalletType type; + final bitbox.Transaction _tx; + final ElectrumClient electrumClient; + final int amount; + final int fee; + final bool hasChange; + final bool isSendAll; + + @override + String get id => _tx.getId(); + + @override + String get hex => _tx.toHex(); + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => bitcoinAmountToString(amount: fee); + + final List _listeners; + + @override + Future commit() async { + int? callId; + + final result = await electrumClient.broadcastTransaction( + transactionRaw: hex, idCallback: (id) => callId = id); + + if (result.isEmpty) { + if (callId != null) { + final error = electrumClient.getErrorMessage(callId!); + + if (error.contains("dust")) { + if (hasChange) { + throw BitcoinTransactionCommitFailedDustChange(); + } else if (!isSendAll) { + throw BitcoinTransactionCommitFailedDustOutput(); + } else { + throw BitcoinTransactionCommitFailedDustOutputSendAll(); + } + } + + if (error.contains("bad-txns-vout-negative")) { + throw BitcoinTransactionCommitFailedVoutNegative(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); + } + + throw BitcoinTransactionCommitFailed(); + } + + _listeners.forEach((listener) => listener(transactionInfo())); + } + + void addListener(void Function(ElectrumTransactionInfo transaction) listener) => + _listeners.add(listener); + + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0, + fee: fee); +} diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml new file mode 100644 index 000000000..37827f1ba --- /dev/null +++ b/cw_bitcoin_cash/pubspec.yaml @@ -0,0 +1,80 @@ +name: cw_bitcoin_cash +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.19.0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + bip39: ^1.0.6 + bip32: ^2.0.0 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 + cw_core: + path: ../cw_core + cw_bitcoin: + path: ../cw_bitcoin + bitcoin_flutter: + git: + url: https://github.com/cake-tech/bitcoin_flutter.git + ref: cake-update-v4 + bitbox: + git: + url: https://github.com/cake-tech/bitbox-flutter.git + ref: Add-Support-For-OP-Return-data + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v2 + + + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.dev/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.dev/assets-and-images/#resolution-aware + +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# + diff --git a/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart b/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart new file mode 100644 index 000000000..f06646a8f --- /dev/null +++ b/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_core/lib/address_info.dart b/cw_core/lib/address_info.dart new file mode 100644 index 000000000..63dc023ab --- /dev/null +++ b/cw_core/lib/address_info.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'address_info.g.dart'; + +@HiveType(typeId: ADDRESS_INFO_TYPE_ID) +class AddressInfo extends HiveObject { + AddressInfo({required this.address, this.accountIndex, required this.label}); + + static const typeId = ADDRESS_INFO_TYPE_ID; + static const boxName = 'AddressInfo'; + + @HiveField(0) + int? accountIndex; + + @HiveField(1, defaultValue: '') + String address; + + @HiveField(2, defaultValue: '') + String label; +} diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index a11907ef2..249b87bd3 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -80,6 +80,8 @@ class AmountConverter { case CryptoCurrency.xmr: return _moneroAmountToString(amount); case CryptoCurrency.btc: + case CryptoCurrency.bch: + case CryptoCurrency.ltc: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 6145411c4..431aff515 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -9,5 +9,5 @@ abstract class Balance { String get formattedAdditionalBalance; - String get formattedFrozenBalance => ''; + String get formattedUnAvailableBalance => ''; } diff --git a/cw_core/lib/battery_optimization_native.dart b/cw_core/lib/battery_optimization_native.dart new file mode 100644 index 000000000..edd04d3f4 --- /dev/null +++ b/cw_core/lib/battery_optimization_native.dart @@ -0,0 +1,22 @@ +import 'package:flutter/services.dart'; + +const MethodChannel _channel = MethodChannel('com.cake_wallet/native_utils'); + +Future requestDisableBatteryOptimization() async { + try { + await _channel.invokeMethod('disableBatteryOptimization'); + } on PlatformException catch (e) { + print("Failed to disable battery optimization: '${e.message}'."); + } +} + +Future isBatteryOptimizationDisabled() async { + try { + final bool isDisabled = await _channel.invokeMethod('isBatteryOptimizationDisabled') as bool; + print('It\'s actually disabled? $isDisabled'); + return isDisabled; + } on PlatformException catch (e) { + print("Failed to check battery optimization status: '${e.message}'."); + return false; + } +} diff --git a/cw_core/lib/cake_hive.dart b/cw_core/lib/cake_hive.dart new file mode 100644 index 000000000..aadf6bf9a --- /dev/null +++ b/cw_core/lib/cake_hive.dart @@ -0,0 +1,4 @@ +import 'package:hive/hive.dart'; +import 'package:hive/src/hive_impl.dart'; + +final HiveInterface CakeHive = HiveImpl(); diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index da9b7f9e9..f1c1cd8ae 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -6,15 +6,22 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen String title = '', int raw = -1, required this.name, + required this.decimals, this.fullName, this.iconPath, - this.tag}) + this.tag, + this.enabled = false, + }) : super(title: title, raw: raw); final String name; final String? tag; final String? fullName; final String? iconPath; + final int decimals; + final bool enabled; + + set enabled(bool value) => this.enabled = value; static const all = [ CryptoCurrency.xmr, @@ -31,6 +38,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.trx, CryptoCurrency.usdt, CryptoCurrency.usdterc20, + CryptoCurrency.sol, + CryptoCurrency.maticpoly, CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.xhv, @@ -43,7 +52,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdttrc20, CryptoCurrency.hbar, CryptoCurrency.sc, - CryptoCurrency.sol, CryptoCurrency.usdc, CryptoCurrency.usdcsol, CryptoCurrency.zaddr, @@ -54,7 +62,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.dcr, CryptoCurrency.kmd, CryptoCurrency.mana, - CryptoCurrency.maticpoly, CryptoCurrency.matic, CryptoCurrency.mkr, CryptoCurrency.near, @@ -67,6 +74,35 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.uni, CryptoCurrency.stx, CryptoCurrency.btcln, + CryptoCurrency.shib, + CryptoCurrency.aave, + CryptoCurrency.arb, + CryptoCurrency.bat, + CryptoCurrency.comp, + CryptoCurrency.cro, + CryptoCurrency.ens, + CryptoCurrency.ftm, + CryptoCurrency.frax, + CryptoCurrency.gusd, + CryptoCurrency.gtc, + CryptoCurrency.grt, + CryptoCurrency.ldo, + CryptoCurrency.nexo, + CryptoCurrency.cake, + CryptoCurrency.pepe, + CryptoCurrency.storj, + CryptoCurrency.tusd, + CryptoCurrency.wbtc, + CryptoCurrency.weth, + CryptoCurrency.zrx, + CryptoCurrency.dydx, + CryptoCurrency.steth, + CryptoCurrency.banano, + CryptoCurrency.usdtPoly, + CryptoCurrency.usdcEPoly, + CryptoCurrency.kaspa, + CryptoCurrency.digibyte, + CryptoCurrency.usdtSol, ]; static const havenCurrencies = [ @@ -86,72 +122,101 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen ]; // title, tag (if applicable), fullName (if unique), raw, name, iconPath - static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/monero_icon.png'); - static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png'); - static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/bch_icon.png'); - static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/bnb_icon.png'); - static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/btc.png'); - static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/dai_icon.png'); - static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png'); - static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png'); - static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/eth_icon.png'); - static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/litecoin-ltc_icon.png'); - static const nano = CryptoCurrency(title: 'NANO', raw: 10, name: 'nano', iconPath: 'assets/images/nano.png'); - static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/trx_icon.png'); - static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/usdt_icon.png'); - static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/usdterc20_icon.png'); - static const xlm = CryptoCurrency(title: 'XLM', fullName: 'Stellar', raw: 14, name: 'xlm', iconPath: 'assets/images/xlm_icon.png'); - static const xrp = CryptoCurrency(title: 'XRP', fullName: 'Ripple', raw: 15, name: 'xrp', iconPath: 'assets/images/xrp_icon.png'); - static const xhv = CryptoCurrency(title: 'XHV', fullName: 'Haven Protocol', raw: 16, name: 'xhv', iconPath: 'assets/images/xhv_logo.png'); + static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/monero_icon.png', decimals: 12); + static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png', decimals: 6); + static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/bch_icon.png', decimals: 8); + static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/bnb_icon.png', decimals: 8); + static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/btc.png', decimals: 8); + static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/dai_icon.png', decimals: 18); + static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png', decimals: 8); + static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png', decimals: 4); + static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/eth_icon.png', decimals: 18); + static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/litecoin-ltc_icon.png', decimals: 8); + static const nano = CryptoCurrency(title: 'XNO', fullName: 'Nano', raw: 10, name: 'xno', iconPath: 'assets/images/nano_icon.png', decimals: 30); + static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/trx_icon.png', decimals: 6); + static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/usdterc20_icon.png', decimals: 6); + static const xlm = CryptoCurrency(title: 'XLM', fullName: 'Stellar', raw: 14, name: 'xlm', iconPath: 'assets/images/xlm_icon.png', decimals: 7); + static const xrp = CryptoCurrency(title: 'XRP', fullName: 'Ripple', raw: 15, name: 'xrp', iconPath: 'assets/images/xrp_icon.png', decimals: 6); + static const xhv = CryptoCurrency(title: 'XHV', fullName: 'Haven Protocol', raw: 16, name: 'xhv', iconPath: 'assets/images/xhv_logo.png', decimals: 12); - static const xag = CryptoCurrency(title: 'XAG', tag: 'XHV', raw: 17, name: 'xag'); - static const xau = CryptoCurrency(title: 'XAU', tag: 'XHV', raw: 18, name: 'xau'); - static const xaud = CryptoCurrency(title: 'XAUD', tag: 'XHV', raw: 19, name: 'xaud'); - static const xbtc = CryptoCurrency(title: 'XBTC', tag: 'XHV', raw: 20, name: 'xbtc'); - static const xcad = CryptoCurrency(title: 'XCAD', tag: 'XHV', raw: 21, name: 'xcad'); - static const xchf = CryptoCurrency(title: 'XCHF', tag: 'XHV', raw: 22, name: 'xchf'); - static const xcny = CryptoCurrency(title: 'XCNY', tag: 'XHV', raw: 23, name: 'xcny'); - static const xeur = CryptoCurrency(title: 'XEUR', tag: 'XHV', raw: 24, name: 'xeur'); - static const xgbp = CryptoCurrency(title: 'XGBP', tag: 'XHV', raw: 25, name: 'xgbp'); - static const xjpy = CryptoCurrency(title: 'XJPY', tag: 'XHV', raw: 26, name: 'xjpy'); - static const xnok = CryptoCurrency(title: 'XNOK', tag: 'XHV', raw: 27, name: 'xnok'); - static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28, name: 'xnzd'); - static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29, name: 'xusd'); + static const xag = CryptoCurrency(title: 'XAG', tag: 'XHV', raw: 17, name: 'xag', decimals: 12); + static const xau = CryptoCurrency(title: 'XAU', tag: 'XHV', raw: 18, name: 'xau', decimals: 12); + static const xaud = CryptoCurrency(title: 'XAUD', tag: 'XHV', raw: 19, name: 'xaud', decimals: 12); + static const xbtc = CryptoCurrency(title: 'XBTC', tag: 'XHV', raw: 20, name: 'xbtc', decimals: 12); + static const xcad = CryptoCurrency(title: 'XCAD', tag: 'XHV', raw: 21, name: 'xcad', decimals: 12); + static const xchf = CryptoCurrency(title: 'XCHF', tag: 'XHV', raw: 22, name: 'xchf', decimals: 12); + static const xcny = CryptoCurrency(title: 'XCNY', tag: 'XHV', raw: 23, name: 'xcny', decimals: 12); + static const xeur = CryptoCurrency(title: 'XEUR', tag: 'XHV', raw: 24, name: 'xeur', decimals: 12); + static const xgbp = CryptoCurrency(title: 'XGBP', tag: 'XHV', raw: 25, name: 'xgbp', decimals: 12); + static const xjpy = CryptoCurrency(title: 'XJPY', tag: 'XHV', raw: 26, name: 'xjpy', decimals: 12); + static const xnok = CryptoCurrency(title: 'XNOK', tag: 'XHV', raw: 27, name: 'xnok', decimals: 12); + static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28, name: 'xnzd', decimals: 12); + static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29, name: 'xusd', decimals: 12); - static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png'); - static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png'); - static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png'); - static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/bttbsc_icon.png'); - static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png'); - static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png'); - static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/usdttrc20_icon.png'); - static const hbar = CryptoCurrency(title: 'HBAR', fullName: 'Hedera', raw: 37, name: 'hbar', iconPath: 'assets/images/hbar_icon.png', ); - static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png'); - static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/sol_icon.png'); - static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/usdc_icon.png'); - static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdcsol_icon.png'); - static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 42, name: 'zaddr'); - static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 43, name: 'zec'); - static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png'); - static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png'); + static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png', decimals: 18); + static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', fullName: 'Avalanche', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png', decimals: 9); + static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png', decimals: 18); + static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/btt_icon.png', decimals: 18); + static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png', decimals: 8); + static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png', decimals: 8); + static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/usdttrc20_icon.png', decimals: 6); + static const hbar = CryptoCurrency(title: 'HBAR', fullName: 'Hedera', raw: 37, name: 'hbar', iconPath: 'assets/images/hbar_icon.png', decimals: 8); + static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png', decimals: 16); + static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/sol_icon.png', decimals: 9); + static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); + static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); - static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png'); - static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png'); - static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png'); - static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png'); - static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png'); - static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png'); - static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png'); - static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png'); - static const oxt = CryptoCurrency(title: 'OXT', tag: 'ETH', fullName: 'Orchid', raw: 54, name: 'oxt', iconPath: 'assets/images/oxt_icon.png'); - static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg_icon.png'); - static const pivx = CryptoCurrency(title: 'PIVX', raw: 56, name: 'pivx', iconPath: 'assets/images/pivx_icon.png'); - static const rune = CryptoCurrency(title: 'RUNE', fullName: 'Thorchain', raw: 57, name: 'rune', iconPath: 'assets/images/rune_icon.png'); - static const rvn = CryptoCurrency(title: 'RVN', fullName: 'Ravencoin', raw: 58, name: 'rvn', iconPath: 'assets/images/rvn_icon.png'); - static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png'); - static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); - static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png'); - static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png'); + static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8); + static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); + static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); + static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); + static const oxt = CryptoCurrency(title: 'OXT', tag: 'ETH', fullName: 'Orchid', raw: 54, name: 'oxt', iconPath: 'assets/images/oxt_icon.png', decimals: 18); + static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg_icon.png', decimals: 18); + static const pivx = CryptoCurrency(title: 'PIVX', raw: 56, name: 'pivx', iconPath: 'assets/images/pivx_icon.png', decimals: 8); + static const rune = CryptoCurrency(title: 'RUNE', fullName: 'Thorchain', raw: 57, name: 'rune', iconPath: 'assets/images/rune_icon.png', decimals: 18); + static const rvn = CryptoCurrency(title: 'RVN', fullName: 'Ravencoin', raw: 58, name: 'rvn', iconPath: 'assets/images/rvn_icon.png', decimals: 8); + static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png', decimals: 6); + static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png', decimals: 18); + static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png', decimals: 8); + static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png', decimals: 8); + static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png', decimals: 18); + static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png', decimals: 18); + static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/arb_icon.png', decimals: 18); + static const bat = CryptoCurrency(title: 'BAT', tag: 'ETH', fullName: 'Basic Attention Token', raw: 66, name: 'bat', iconPath: 'assets/images/bat_icon.png', decimals: 18); + static const comp = CryptoCurrency(title: 'COMP', tag: 'ETH', fullName: 'Compound', raw: 67, name: 'comp', iconPath: 'assets/images/comp_icon.png', decimals: 18); + static const cro = CryptoCurrency(title: 'CRO', tag: 'ETH', fullName: 'Crypto.com Cronos', raw: 68, name: 'cro', iconPath: 'assets/images/cro_icon.png', decimals: 8); + static const ens = CryptoCurrency(title: 'ENS', tag: 'ETH', fullName: 'Ethereum Name Service', raw: 69, name: 'ens', iconPath: 'assets/images/ens_icon.png', decimals: 18); + static const ftm = CryptoCurrency(title: 'FTM', tag: 'ETH', fullName: 'Fantom', raw: 70, name: 'ftm', iconPath: 'assets/images/ftm_icon.png', decimals: 18); + static const frax = CryptoCurrency(title: 'FRAX', tag: 'ETH', fullName: 'Frax', raw: 71, name: 'frax', iconPath: 'assets/images/frax_icon.png', decimals: 18); + static const gusd = CryptoCurrency(title: 'GUSD', tag: 'ETH', fullName: 'Gemini USD', raw: 72, name: 'gusd', iconPath: 'assets/images/gusd_icon.png', decimals: 2); + static const gtc = CryptoCurrency(title: 'GTC', tag: 'ETH', fullName: 'Gitcoin', raw: 73, name: 'gtc', iconPath: 'assets/images/gtc_icon.png', decimals: 18); + static const grt = CryptoCurrency(title: 'GRT', tag: 'ETH', fullName: 'The Graph', raw: 74, name: 'grt', iconPath: 'assets/images/grt_icon.png', decimals: 18); + static const ldo = CryptoCurrency(title: 'LDO', tag: 'ETH', fullName: 'Lido DAO', raw: 75, name: 'ldo', iconPath: 'assets/images/ldo_icon.png', decimals: 18); + static const nexo = CryptoCurrency(title: 'NEXO', tag: 'ETH', fullName: 'Nexo', raw: 76, name: 'nexo', iconPath: 'assets/images/nexo_icon.png', decimals: 18); + static const cake = CryptoCurrency(title: 'CAKE', tag: 'BSC', fullName: 'PancakeSwap', raw: 77, name: 'cake', iconPath: 'assets/images/cake_icon.png', decimals: 18); + static const pepe = CryptoCurrency(title: 'PEPE', tag: 'ETH', fullName: 'Pepe', raw: 78, name: 'pepe', iconPath: 'assets/images/pepe_icon.png', decimals: 18); + static const storj = CryptoCurrency(title: 'STORJ', tag: 'ETH', fullName: 'Storj', raw: 79, name: 'storj', iconPath: 'assets/images/storj_icon.png', decimals: 8); + static const tusd = CryptoCurrency(title: 'TUSD', tag: 'ETH', fullName: 'TrueUSD', raw: 80, name: 'tusd', iconPath: 'assets/images/tusd_icon.png', decimals: 18); + static const wbtc = CryptoCurrency(title: 'WBTC', tag: 'ETH', fullName: 'Wrapped Bitcoin', raw: 81, name: 'wbtc', iconPath: 'assets/images/wbtc_icon.png', decimals: 8); + static const weth = CryptoCurrency(title: 'WETH', tag: 'ETH', fullName: 'Wrapped Ethereum', raw: 82, name: 'weth', iconPath: 'assets/images/weth_icon.png', decimals: 18); + static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png', decimals: 18); + static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18); + static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18); + static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); + static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); + static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); + static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static final Map _rawCurrencyMap = @@ -175,7 +240,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen }); static CryptoCurrency deserialize({required int raw}) { - if (CryptoCurrency._rawCurrencyMap[raw] == null) { final s = 'Unexpected token: $raw for CryptoCurrency deserialize'; throw ArgumentError.value(raw, 'raw', s); @@ -183,7 +247,15 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen return CryptoCurrency._rawCurrencyMap[raw]!; } - static CryptoCurrency fromString(String name) { + // TODO: refactor this + static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) { + try { + return CryptoCurrency.all.firstWhere((element) => + element.title.toLowerCase() == name && + (element.tag == null || + element.tag == walletCurrency?.title || + element.tag == walletCurrency?.tag)); + } catch (_) {} if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromString'; @@ -193,14 +265,12 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen } static CryptoCurrency fromFullName(String name) { - - if (CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()] == null) { + if (CryptoCurrency._fullNameCurrencyMap[name.split("(").first.trim().toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromFullName'; throw ArgumentError.value(name, 'Fullname', s); } return CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()]!; } - @override String toString() => title; diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 3904fc049..58ee37669 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,18 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; + case WalletType.bitcoinCash: + return CryptoCurrency.bch; + case WalletType.nano: + return CryptoCurrency.nano; + case WalletType.banano: + return CryptoCurrency.banano; + case WalletType.polygon: + return CryptoCurrency.maticpoly; + case WalletType.solana: + return CryptoCurrency.sol; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/enumerate.dart b/cw_core/lib/enumerate.dart new file mode 100644 index 000000000..d92347e91 --- /dev/null +++ b/cw_core/lib/enumerate.dart @@ -0,0 +1,13 @@ +abstract class Enumerate { + String get value; + + @override + operator ==(other) { + if (identical(other, this)) return true; + if (other is! Enumerate) return false; + return other.runtimeType == runtimeType && value == other.value; + } + + @override + int get hashCode => value.hashCode; +} diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart new file mode 100644 index 000000000..f8c2afc06 --- /dev/null +++ b/cw_core/lib/erc20_token.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'erc20_token.g.dart'; + +@HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + @HiveField(1) + final String symbol; + @HiveField(2) + final String contractAddress; + @HiveField(3) + final int decimal; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + @HiveField(6) + final String? tag; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + this.tag, + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimal); + + Erc20Token.copyWith(Erc20Token other, String? icon, String? tag) + : this.name = other.name, + this.symbol = other.symbol, + this.contractAddress = other.contractAddress, + this.decimal = other.decimal, + this._enabled = other.enabled, + this.tag = tag, + this.iconPath = icon, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: tag, + iconPath: icon, + decimals: other.decimal, + ); + + static const typeId = ERC20_TOKEN_TYPE_ID; + static const boxName = 'Erc20Tokens'; + static const ethereumBoxName = 'EthereumErc20Tokens'; + static const polygonBoxName = 'PolygonErc20Tokens'; + + @override + bool operator ==(other) => + (other is Erc20Token && other.contractAddress == contractAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart new file mode 100644 index 000000000..d07da8109 --- /dev/null +++ b/cw_core/lib/exceptions.dart @@ -0,0 +1,34 @@ +import 'package:cw_core/crypto_currency.dart'; + +class TransactionWrongBalanceException implements Exception { + TransactionWrongBalanceException(this.currency); + + final CryptoCurrency currency; +} + +class TransactionNoInputsException implements Exception {} + +class TransactionNoFeeException implements Exception {} + +class TransactionNoDustException implements Exception {} + +class TransactionNoDustOnChangeException implements Exception { + TransactionNoDustOnChangeException(this.max, this.min); + + final String max; + final String min; +} + +class TransactionCommitFailed implements Exception { + final String? errorMessage; + + TransactionCommitFailed({this.errorMessage}); +} + +class TransactionCommitFailedDustChange implements Exception {} + +class TransactionCommitFailedDustOutput implements Exception {} + +class TransactionCommitFailedDustOutputSendAll implements Exception {} + +class TransactionCommitFailedVoutNegative implements Exception {} diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 819990e0a..6f3ccaf68 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -84,7 +84,44 @@ final dates = { "2020-8": 2153983, "2020-9": 2176466, "2020-10": 2198453, - "2020-11": 2220000 + "2020-11": 2220000, + "2020-12": 2242240, + "2021-1": 2264584, + "2021-2": 2286892, + "2021-3": 2307079, + "2021-4": 2329385, + "2021-5": 2351004, + "2021-6": 2373306, + "2021-7": 2394882, + "2021-8": 2417162, + "2021-9": 2439490, + "2021-10": 2461020, + "2021-11": 2483377, + "2021-12": 2504932, + "2022-1": 2527316, + "2022-2": 2549605, + "2022-3": 2569711, + "2022-4": 2591995, + "2022-5": 2613603, + "2022-6": 2635840, + "2022-7": 2657395, + "2022-8": 2679705, + "2022-9": 2701991, + "2022-10": 2723607, + "2022-11": 2745899, + "2022-12": 2767427, + "2023-1": 2789763, + "2023-2": 2811996, + "2023-3": 2832118, + "2023-4": 2854365, + "2023-5": 2875972, + "2023-6": 2898234, + "2023-7": 2919771, + "2023-8": 2942045, + "2023-9": 2964280, + "2023-10": 2985937, + "2023-11": 3008178, + "2023-12": 3029759 }; int getMoneroHeigthByDate({required DateTime date}) { diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart new file mode 100644 index 000000000..e0896bab1 --- /dev/null +++ b/cw_core/lib/hive_type_ids.dart @@ -0,0 +1,18 @@ +const CONTACT_TYPE_ID = 0; +const NODE_TYPE_ID = 1; +const TRANSACTION_TYPE_ID = 2; +const TRADE_TYPE_ID = 3; +const WALLET_INFO_TYPE_ID = 4; +const WALLET_TYPE_TYPE_ID = 5; +const TEMPLATE_TYPE_ID = 6; +const EXCHANGE_TEMPLATE_TYPE_ID = 7; +const ORDER_TYPE_ID = 8; +const UNSPENT_COINS_INFO_TYPE_ID = 9; +const ANONPAY_INVOICE_INFO_TYPE_ID = 10; +const ADDRESS_INFO_TYPE_ID = 11; +const ERC20_TOKEN_TYPE_ID = 12; +const NANO_ACCOUNT_TYPE_ID = 13; +const POW_NODE_TYPE_ID = 14; +const DERIVATION_TYPE_TYPE_ID = 15; +const SPL_TOKEN_TYPE_ID = 16; +const DERIVATION_INFO_TYPE_ID = 17; diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 7d569ef2f..98a7f134a 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -2,24 +2,33 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { - MoneroBalance({required this.fullBalance, required this.unlockedBalance}) + MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedFullBalance = moneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - moneroAmountToString(amount: unlockedBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedLockedBalance = + moneroAmountToString(amount: frozenBalance + fullBalance - unlockedBalance), super(unlockedBalance, fullBalance); MoneroBalance.fromString( {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) + required this.formattedUnlockedBalance, + this.formattedLockedBalance = '0.0'}) : fullBalance = moneroParseAmount(amount: formattedFullBalance), unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), + frozenBalance = moneroParseAmount(amount: formattedLockedBalance), super(moneroParseAmount(amount: formattedUnlockedBalance), moneroParseAmount(amount: formattedFullBalance)); final int fullBalance; final int unlockedBalance; + final int frozenBalance; final String formattedFullBalance; final String formattedUnlockedBalance; + final String formattedLockedBalance; + + @override + String get formattedUnAvailableBalance => + formattedLockedBalance == '0.0' ? '' : formattedLockedBalance; @override String get formattedAvailableBalance => formattedUnlockedBalance; diff --git a/cw_core/lib/monero_transaction_priority.dart b/cw_core/lib/monero_transaction_priority.dart index f5c00ecc7..81058f336 100644 --- a/cw_core/lib/monero_transaction_priority.dart +++ b/cw_core/lib/monero_transaction_priority.dart @@ -1,7 +1,4 @@ import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_type.dart'; -//import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cw_core/enumerable_item.dart'; class MoneroTransactionPriority extends TransactionPriority { const MoneroTransactionPriority({required String title, required int raw}) @@ -12,21 +9,20 @@ class MoneroTransactionPriority extends TransactionPriority { MoneroTransactionPriority.automatic, MoneroTransactionPriority.medium, MoneroTransactionPriority.fast, - MoneroTransactionPriority.fastest + MoneroTransactionPriority.fastest, ]; - static const slow = MoneroTransactionPriority(title: 'Slow', raw: 0); - static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 1); + static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 0); + static const slow = MoneroTransactionPriority(title: 'Slow', raw: 1); static const medium = MoneroTransactionPriority(title: 'Medium', raw: 2); static const fast = MoneroTransactionPriority(title: 'Fast', raw: 3); static const fastest = MoneroTransactionPriority(title: 'Fastest', raw: 4); - static const standard = slow; static MoneroTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: - return slow; - case 1: return automatic; + case 1: + return slow; case 2: return medium; case 3: diff --git a/cw_core/lib/monero_wallet_utils.dart b/cw_core/lib/monero_wallet_utils.dart index 4c9325daf..1b1988eb6 100644 --- a/cw_core/lib/monero_wallet_utils.dart +++ b/cw_core/lib/monero_wallet_utils.dart @@ -54,6 +54,17 @@ Future restoreWalletFiles(String name) async { } } +Future resetCache(String name) async { + await removeCache(name); + + final walletDirPath = await pathForWalletDir(name: name, type: WalletType.monero); + final cacheFilePath = '$walletDirPath/$name'; + final backupCacheFile = File(backupFileName(cacheFilePath)); + if (backupCacheFile.existsSync()) { + await backupCacheFile.copy(cacheFilePath); + } +} + Future backupWalletFilesExists(String name) async { final walletDirPath = await pathForWalletDir(name: name, type: WalletType.monero); final cacheFilePath = '$walletDirPath/$name'; @@ -63,9 +74,9 @@ Future backupWalletFilesExists(String name) async { final backupKeysFile = File(backupFileName(keysFilePath)); final backupAddressListFile = File(backupFileName(addressListFilePath)); - return backupCacheFile.existsSync() - && backupKeysFile.existsSync() - && backupAddressListFile.existsSync(); + return backupCacheFile.existsSync() && + backupKeysFile.existsSync() && + backupAddressListFile.existsSync(); } Future removeCache(String name) async { @@ -85,4 +96,4 @@ Future restoreOrResetWalletFiles(String name) async { } removeCache(name); -} \ No newline at end of file +} diff --git a/cw_core/lib/n2_node.dart b/cw_core/lib/n2_node.dart new file mode 100644 index 000000000..a2eb6e4d3 --- /dev/null +++ b/cw_core/lib/n2_node.dart @@ -0,0 +1,31 @@ +class N2Node { + N2Node({ + this.weight, + this.uptime, + this.score, + this.account, + this.alias, + }); + + String? uptime; + double? weight; + int? score; + String? account; + String? alias; + + factory N2Node.fromJson(Map json) => N2Node( + weight: double.tryParse((json['weight'] as num?).toString()), + uptime: json['uptime'] as String?, + score: json['score'] as int?, + account: json['rep_address'] as String?, + alias: json['alias'] as String?, + ); + + Map toJson() => { + 'uptime': uptime, + 'weight': weight, + 'score': score, + 'rep_address': account, + 'alias': alias, + }; +} diff --git a/cw_core/lib/nano_account.dart b/cw_core/lib/nano_account.dart new file mode 100644 index 000000000..91df62e75 --- /dev/null +++ b/cw_core/lib/nano_account.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'nano_account.g.dart'; + +@HiveType(typeId: NanoAccount.typeId) +class NanoAccount extends HiveObject { + NanoAccount({required this.label, required this.id, this.balance, this.isSelected = false}); + + static const typeId = NANO_ACCOUNT_TYPE_ID; + + @HiveField(0) + String label; + + @HiveField(1) + final int id; + + @HiveField(2) + bool isSelected; + + @HiveField(3) + String? balance; +} diff --git a/cw_core/lib/nano_account_info_response.dart b/cw_core/lib/nano_account_info_response.dart new file mode 100644 index 000000000..319bbb861 --- /dev/null +++ b/cw_core/lib/nano_account_info_response.dart @@ -0,0 +1,23 @@ +class AccountInfoResponse { + String frontier; + int confirmationHeight; + String balance; + String representative; + String? address; + + AccountInfoResponse({ + required this.frontier, + required this.balance, + required this.representative, + required this.confirmationHeight, + }); + + factory AccountInfoResponse.fromJson(Map json) { + return AccountInfoResponse( + frontier: json['frontier'] as String, + representative: json['representative'] as String, + balance: json['balance'] as String, + confirmationHeight: int.parse(json['confirmation_height'] as String), + ); + } +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 7df25d6a1..9d0806851 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -3,39 +3,48 @@ import 'package:cw_core/keyable.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/io_client.dart' as ioc; +// import 'package:tor/tor.dart'; part 'node.g.dart'; -Uri createUriFromElectrumAddress(String address) => - Uri.tryParse('tcp://$address')!; +Uri createUriFromElectrumAddress(String address, String path) => Uri.tryParse('tcp://$address$path')!; @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { - Node( - {this.login, - this.password, - this.useSSL, - this.trusted = false, - String? uri, - WalletType? type,}) { + Node({ + this.login, + this.password, + this.useSSL, + this.trusted = false, + this.socksProxyAddress, + String? uri, + String? path, + WalletType? type, + }) { if (uri != null) { uriRaw = uri; } if (type != null) { this.type = type; } + if (path != null) { + this.path = path; + } } Node.fromMap(Map map) : uriRaw = map['uri'] as String? ?? '', + path = map['path'] as String? ?? '', login = map['login'] as String?, password = map['password'] as String?, useSSL = map['useSSL'] as bool?, - trusted = map['trusted'] as bool? ?? false; + trusted = map['trusted'] as bool? ?? false, + socksProxyAddress = map['socksProxyPort'] as String?; - static const typeId = 1; + static const typeId = NODE_TYPE_ID; static const boxName = 'Nodes'; @HiveField(0, defaultValue: '') @@ -56,32 +65,54 @@ class Node extends HiveObject with Keyable { @HiveField(5, defaultValue: false) bool trusted; + @HiveField(6) + String? socksProxyAddress; + + @HiveField(7, defaultValue: '') + String? path; + bool get isSSL => useSSL ?? false; + bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; + Uri get uri { switch (type) { case WalletType.monero: - return Uri.http(uriRaw, ''); - case WalletType.bitcoin: - return createUriFromElectrumAddress(uriRaw); - case WalletType.litecoin: - return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + return createUriFromElectrumAddress(uriRaw, path ?? ''); + case WalletType.nano: + case WalletType.banano: + if (isSSL) { + return Uri.https(uriRaw, path ?? ''); + } else { + return Uri.http(uriRaw, path ?? ''); + } + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + return Uri.https(uriRaw, path ?? ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } } + bool get isValidProxyAddress => socksProxyAddress?.contains(':') ?? false; + @override bool operator ==(other) => other is Node && - (other.uriRaw == uriRaw && - other.login == login && - other.password == password && - other.typeRaw == typeRaw && - other.useSSL == useSSL && - other.trusted == trusted); + (other.uriRaw == uriRaw && + other.login == login && + other.password == password && + other.typeRaw == typeRaw && + other.useSSL == useSSL && + other.trusted == trusted && + other.socksProxyAddress == socksProxyAddress && + other.path == path); @override int get hashCode => @@ -90,7 +121,9 @@ class Node extends HiveObject with Keyable { password.hashCode ^ typeRaw.hashCode ^ useSSL.hashCode ^ - trusted.hashCode; + trusted.hashCode ^ + socksProxyAddress.hashCode ^ + path.hashCode; @override dynamic get keyIndex { @@ -108,13 +141,18 @@ class Node extends HiveObject with Keyable { try { switch (type) { case WalletType.monero: - return requestMoneroNode(); - case WalletType.bitcoin: - return requestElectrumServer(); - case WalletType.litecoin: - return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); + case WalletType.nano: + case WalletType.banano: + return requestNanoNode(); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + return requestElectrumServer(); default: return false; } @@ -124,30 +162,32 @@ class Node extends HiveObject with Keyable { } Future requestMoneroNode() async { + if (uri.toString().contains(".onion") || useSocksProxy) { + return await requestNodeWithProxy(); + } final path = '/json_rpc'; final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); final realm = 'monero-rpc'; - final body = { - 'jsonrpc': '2.0', - 'id': '0', - 'method': 'get_info' - }; + final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}; try { final authenticatingClient = HttpClient(); + authenticatingClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + authenticatingClient.addCredentials( - rpcUri, - realm, - HttpClientDigestCredentials(login ?? '', password ?? ''), + rpcUri, + realm, + HttpClientDigestCredentials(login ?? '', password ?? ''), ); final http.Client client = ioc.IOClient(authenticatingClient); final response = await client.post( - rpcUri, - headers: {'Content-Type': 'application/json'}, - body: json.encode(body), + rpcUri, + headers: {'Content-Type': 'application/json'}, + body: json.encode(body), ); client.close(); @@ -157,7 +197,48 @@ class Node extends HiveObject with Keyable { } catch (_) { return false; } -} + } + + Future requestNanoNode() async { + http.Response response = await http.post( + uri, + headers: {'Content-type': 'application/json'}, + body: json.encode( + { + "action": "block_count", + }, + ), + ); + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } + + Future requestNodeWithProxy() async { + if (!isValidProxyAddress /* && !Tor.instance.enabled*/) { + return false; + } + + String? proxy = socksProxyAddress; + + // if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { + // proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + // } + if (proxy == null) { + return false; + } + final proxyAddress = proxy!.split(':')[0]; + final proxyPort = int.parse(proxy.split(':')[1]); + try { + final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } Future requestElectrumServer() async { try { @@ -168,4 +249,17 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } } diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index e2b1201a8..9e3ac4ffc 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -7,7 +7,8 @@ class OutputInfo { this.formattedCryptoAmount, this.fiatAmount, this.note, - this.extractedAddress,}); + this.extractedAddress, + this.memo}); final String? fiatAmount; final String? cryptoAmount; @@ -17,4 +18,5 @@ class OutputInfo { final bool sendAll; final bool isParsedAddress; final int? formattedCryptoAmount; + final String? memo; } \ No newline at end of file diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart index af4838ffa..cfc33ef21 100644 --- a/cw_core/lib/pathForWallet.dart +++ b/cw_core/lib/pathForWallet.dart @@ -1,6 +1,5 @@ import 'dart:io'; import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; Future pathForWalletDir({required String name, required WalletType type}) async { diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index cc5686fc9..642db9c2c 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -2,7 +2,9 @@ mixin PendingTransaction { String get id; String get amountFormatted; String get feeFormatted; + String? feeRate; String get hex; + int? get outputCount => null; Future commit(); -} \ No newline at end of file +} diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart new file mode 100644 index 000000000..786d07bc5 --- /dev/null +++ b/cw_core/lib/receive_page_option.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/enumerate.dart'; + +class ReceivePageOption implements Enumerate { + static const mainnet = ReceivePageOption._('mainnet'); + static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); + static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); + + const ReceivePageOption._(this.value); + + final String value; + + String toString() { + return value; + } +} + +const ReceivePageOptions = [ + ReceivePageOption.mainnet, + ReceivePageOption.anonPayInvoice, + ReceivePageOption.anonPayDonationLink +]; diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart index 09e01556c..84096e2d6 100644 --- a/cw_core/lib/set_app_secure_native.dart +++ b/cw_core/lib/set_app_secure_native.dart @@ -1,7 +1,9 @@ import 'package:flutter/services.dart'; -const utils = const MethodChannel('com.cake_wallet/native_utils'); - void setIsAppSecureNative(bool isAppSecure) { - utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); -} \ No newline at end of file + try { + final utils = const MethodChannel('com.cake_wallet/native_utils'); + + utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); + } catch (_) {} +} diff --git a/cw_core/lib/transaction_history.dart b/cw_core/lib/transaction_history.dart index 508f3aeca..a88d6df95 100644 --- a/cw_core/lib/transaction_history.dart +++ b/cw_core/lib/transaction_history.dart @@ -14,6 +14,8 @@ abstract class TransactionHistoryBase { void addMany(Map transactions); + void clear() => transactions.clear(); + // bool _isUpdating; // @action diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index b8e4a5e0c..992582ff8 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -14,6 +14,10 @@ abstract class TransactionInfo extends Object with Keyable { String fiatAmount(); String? feeFormatted(); void changeFiatAmount(String amount); + String? to; + String? from; + List? inputAddresses; + List? outputAddresses; @override dynamic get keyIndex => id; diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 75c13f2cd..25abd3e48 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; part 'unspent_coins_info.g.dart'; @@ -12,9 +13,13 @@ class UnspentCoinsInfo extends HiveObject { required this.noteRaw, required this.address, required this.vout, - required this.value}); + required this.value, + this.keyImage = null, + this.isChange = false, + this.accountIndex = 0 + }); - static const typeId = 9; + static const typeId = UNSPENT_COINS_INFO_TYPE_ID; static const boxName = 'Unspent'; static const boxKey = 'unspentBoxKey'; @@ -42,7 +47,16 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(7, defaultValue: 0) int vout; + @HiveField(8, defaultValue: null) + String? keyImage; + + @HiveField(9, defaultValue: false) + bool isChange; + + @HiveField(10, defaultValue: 0) + int accountIndex; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; -} \ No newline at end of file +} diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart new file mode 100644 index 000000000..595df18f4 --- /dev/null +++ b/cw_core/lib/unspent_transaction_output.dart @@ -0,0 +1,21 @@ +class Unspent { + Unspent(this.address, this.hash, this.value, this.vout, this.keyImage) + : isSending = true, + isFrozen = false, + isChange = false, + note = ''; + + final String address; + final String hash; + final int value; + final int vout; + final String? keyImage; + + bool isChange; + bool isSending; + bool isFrozen; + int? confirmations; + String note; + + bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); +} diff --git a/cw_core/lib/utils/file.dart b/cw_core/lib/utils/file.dart new file mode 100644 index 000000000..0b1c5cffd --- /dev/null +++ b/cw_core/lib/utils/file.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write({required String path, required String password, required String data}) async => + writeData(path: path, password: password, data: data); + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index a34101a88..e987b5d0e 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,16 +1,26 @@ +import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) - : addressesMap = {}; + : addressesMap = {}, + allAddressesMap = {}, + addressInfos = {}; final WalletInfo walletInfo; String get address; + String? get primaryAddress => null; + set address(String address); Map addressesMap; + Map allAddressesMap; + + Map> addressInfos; + + Set usedAddresses = {}; Future init(); @@ -20,6 +30,8 @@ abstract class WalletAddresses { try { walletInfo.address = address; walletInfo.addresses = addressesMap; + walletInfo.addressInfos = addressInfos; + walletInfo.usedAddresses = usedAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); @@ -28,4 +40,7 @@ abstract class WalletAddresses { print(e.toString()); } } -} \ No newline at end of file + + bool containsAddress(String address) => + addressesMap.containsKey(address) || allAddressesMap.containsKey(address); +} diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 93821448c..037a26d38 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -13,9 +13,7 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletBase< - BalanceType extends Balance, - HistoryType extends TransactionHistoryBase, +abstract class WalletBase { WalletBase(this.walletInfo); @@ -42,7 +40,11 @@ abstract class WalletBase< set syncStatus(SyncStatus status); - String get seed; + String? get seed; + + String? get privateKey => null; + + String? get hexSeed => null; Object get keys; @@ -50,14 +52,22 @@ abstract class WalletBase< late HistoryType transactionHistory; + set isEnabledAutoGenerateSubaddress(bool value) {} + + bool get isEnabledAutoGenerateSubaddress => false; + Future connectToNode({required Node node}); + // there is a default definition here because only coins with a pow node (nano based) need to override this + Future connectToPowNode({required Node node}) async {} + Future startSync(); Future createTransaction(Object credentials); int calculateEstimatedFee(TransactionPriority priority, int? amount); + // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, // {void Function() onFinished}); @@ -73,4 +83,12 @@ abstract class WalletBase< Future changePassword(String password); Future? updateBalance(); + + void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; + + Future renameWalletFiles(String newWalletName); + + String signMessage(String message, {String? address = null}) => throw UnimplementedError(); + + bool? isTestnet; } diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index e028232e8..9b28680f9 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -4,11 +4,22 @@ abstract class WalletCredentials { WalletCredentials({ required this.name, this.height, + this.seedPhraseLength, this.walletInfo, - this.password}); + this.password, + this.passphrase, + this.derivationInfo, + }) { + if (this.walletInfo != null && derivationInfo != null) { + this.walletInfo!.derivationInfo = derivationInfo; + } + } final String name; final int? height; + int? seedPhraseLength; String? password; + String? passphrase; WalletInfo? walletInfo; + DerivationInfo? derivationInfo; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index a25702cf7..4892f6d1d 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,36 +1,112 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_core/wallet_type.dart'; import 'dart:async'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; part 'wallet_info.g.dart'; +@HiveType(typeId: DERIVATION_TYPE_TYPE_ID) +enum DerivationType { + @HiveField(0) + unknown, + @HiveField(1) + def, // default is a reserved word + @HiveField(2) + nano, + @HiveField(3) + bip39, + @HiveField(4) + electrum, +} + +@HiveType(typeId: DerivationInfo.typeId) +class DerivationInfo extends HiveObject { + DerivationInfo({ + this.derivationType, + this.derivationPath, + this.balance = "", + this.address = "", + this.transactionsCount = 0, + this.scriptType, + this.description, + }); + + static const typeId = DERIVATION_INFO_TYPE_ID; + + @HiveField(0, defaultValue: '') + String address; + + @HiveField(1, defaultValue: '') + String balance; + + @HiveField(2) + int transactionsCount; + + @HiveField(3) + DerivationType? derivationType; + + @HiveField(4) + String? derivationPath; + + @HiveField(5) + final String? scriptType; + + @HiveField(6) + final String? description; +} + @HiveType(typeId: WalletInfo.typeId) class WalletInfo extends HiveObject { - WalletInfo(this.id, this.name, this.type, this.isRecovery, this.restoreHeight, - this.timestamp, this.dirPath, this.path, this.address, this.yatEid, - this.yatLastUsedAddressRaw, this.showIntroCakePayCard) + WalletInfo( + this.id, + this.name, + this.type, + this.isRecovery, + this.restoreHeight, + this.timestamp, + this.dirPath, + this.path, + this.address, + this.yatEid, + this.yatLastUsedAddressRaw, + this.showIntroCakePayCard, + this.derivationInfo) : _yatLastUsedAddressController = StreamController.broadcast(); - factory WalletInfo.external( - {required String id, - required String name, - required WalletType type, - required bool isRecovery, - required int restoreHeight, - required DateTime date, - required String dirPath, - required String path, - required String address, - bool? showIntroCakePayCard, - String yatEid ='', - String yatLastUsedAddressRaw = ''}) { - return WalletInfo(id, name, type, isRecovery, restoreHeight, - date.millisecondsSinceEpoch, dirPath, path, address, - yatEid, yatLastUsedAddressRaw, showIntroCakePayCard); + factory WalletInfo.external({ + required String id, + required String name, + required WalletType type, + required bool isRecovery, + required int restoreHeight, + required DateTime date, + required String dirPath, + required String path, + required String address, + bool? showIntroCakePayCard, + String yatEid = '', + String yatLastUsedAddressRaw = '', + DerivationInfo? derivationInfo, + }) { + return WalletInfo( + id, + name, + type, + isRecovery, + restoreHeight, + date.millisecondsSinceEpoch, + dirPath, + path, + address, + yatEid, + yatLastUsedAddressRaw, + showIntroCakePayCard, + derivationInfo, + ); } - static const typeId = 4; + static const typeId = WALLET_INFO_TYPE_ID; static const boxName = 'WalletInfo'; @HiveField(0, defaultValue: '') @@ -72,6 +148,27 @@ class WalletInfo extends HiveObject { @HiveField(13) bool? showIntroCakePayCard; + @HiveField(14) + Map>? addressInfos; + + @HiveField(15) + List? usedAddresses; + + @HiveField(16) + DerivationType? derivationType; // no longer used + + @HiveField(17) + String? derivationPath; // no longer used + + @HiveField(18) + String? addressPageType; + + @HiveField(19) + String? network; + + @HiveField(20) + DerivationInfo? derivationInfo; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { @@ -82,7 +179,7 @@ class WalletInfo extends HiveObject { String get yatEmojiId => yatEid ?? ''; bool get isShowIntroCakePayCard { - if(showIntroCakePayCard == null) { + if (showIntroCakePayCard == null) { return type != WalletType.haven; } return showIntroCakePayCard!; diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index f66f39583..22981b9db 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -1,20 +1,43 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletService { +abstract class WalletService { WalletType getType(); - Future create(N credentials); + Future create(N credentials, {bool? isTestnet}); - Future restoreFromSeed(RFS credentials); + Future restoreFromSeed(RFS credentials, {bool? isTestnet}); - Future restoreFromKeys(RFK credentials); + Future restoreFromKeys(RFK credentials, {bool? isTestnet}); Future openWallet(String name, String password); Future isWalletExit(String name); Future remove(String wallet); + + Future rename(String currentName, String password, String newName); + + Future restoreWalletFilesFromBackup(String name) async { + final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType()); + final walletDirPath = await pathForWalletDir(name: name, type: getType()); + + if (File(backupWalletDirPath).existsSync()) { + await File(backupWalletDirPath).copy(walletDirPath); + } + } + + Future saveBackup(String name) async { + final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType()); + final walletDirPath = await pathForWalletDir(name: name, type: getType()); + + if (File(walletDirPath).existsSync()) { + await File(walletDirPath).copy(backupWalletDirPath); + } + } } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index e76e4539e..a63ddf37c 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; part 'wallet_type.g.dart'; @@ -7,11 +8,16 @@ const walletTypes = [ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, - WalletType.haven + WalletType.haven, + WalletType.ethereum, + WalletType.bitcoinCash, + WalletType.nano, + WalletType.banano, + WalletType.polygon, + WalletType.solana, ]; -const walletTypeTypeId = 5; -@HiveType(typeId: walletTypeTypeId) +@HiveType(typeId: WALLET_TYPE_TYPE_ID) enum WalletType { @HiveField(0) monero, @@ -26,7 +32,25 @@ enum WalletType { litecoin, @HiveField(4) - haven + haven, + + @HiveField(5) + ethereum, + + @HiveField(6) + nano, + + @HiveField(7) + banano, + + @HiveField(8) + bitcoinCash, + + @HiveField(9) + polygon, + + @HiveField(10) + solana } int serializeToInt(WalletType type) { @@ -39,6 +63,18 @@ int serializeToInt(WalletType type) { return 2; case WalletType.haven: return 3; + case WalletType.ethereum: + return 4; + case WalletType.nano: + return 5; + case WalletType.banano: + return 6; + case WalletType.bitcoinCash: + return 7; + case WalletType.polygon: + return 8; + case WalletType.solana: + return 9; default: return -1; } @@ -54,6 +90,18 @@ WalletType deserializeFromInt(int raw) { return WalletType.litecoin; case 3: return WalletType.haven; + case 4: + return WalletType.ethereum; + case 5: + return WalletType.nano; + case 6: + return WalletType.banano; + case 7: + return WalletType.bitcoinCash; + case 8: + return WalletType.polygon; + case 9: + return WalletType.solana; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -69,6 +117,18 @@ String walletTypeToString(WalletType type) { return 'Litecoin'; case WalletType.haven: return 'Haven'; + case WalletType.ethereum: + return 'Ethereum'; + case WalletType.bitcoinCash: + return 'Bitcoin Cash'; + case WalletType.nano: + return 'Nano'; + case WalletType.banano: + return 'Banano'; + case WalletType.polygon: + return 'Polygon'; + case WalletType.solana: + return 'Solana'; default: return ''; } @@ -77,13 +137,25 @@ String walletTypeToString(WalletType type) { String walletTypeToDisplayName(WalletType type) { switch (type) { case WalletType.monero: - return 'Monero'; + return 'Monero (XMR)'; case WalletType.bitcoin: - return 'Bitcoin (Electrum)'; + return 'Bitcoin (BTC)'; case WalletType.litecoin: - return 'Litecoin (Electrum)'; + return 'Litecoin (LTC)'; case WalletType.haven: - return 'Haven'; + return 'Haven (XHV)'; + case WalletType.ethereum: + return 'Ethereum (ETH)'; + case WalletType.bitcoinCash: + return 'Bitcoin Cash (BCH)'; + case WalletType.nano: + return 'Nano (XNO)'; + case WalletType.banano: + return 'Banano (BAN)'; + case WalletType.polygon: + return 'Polygon (MATIC)'; + case WalletType.solana: + return 'Solana (SOL)'; default: return ''; } @@ -99,7 +171,20 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; + case WalletType.bitcoinCash: + return CryptoCurrency.bch; + case WalletType.nano: + return CryptoCurrency.nano; + case WalletType.banano: + return CryptoCurrency.banano; + case WalletType.polygon: + return CryptoCurrency.maticpoly; + case WalletType.solana: + return CryptoCurrency.sol; default: - throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); + throw Exception( + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 70652ec35..678e57b54 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,34 +69,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.11" built_collection: dependency: transitive description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.8.1" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -141,18 +141,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -165,26 +165,26 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" dart_style: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.4" encrypt: dependency: "direct main" description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: "direct main" description: @@ -226,10 +226,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.0+2" flutter_test: dependency: "direct dev" description: flutter @@ -247,18 +247,18 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hive: dependency: transitive description: @@ -271,18 +271,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -303,10 +303,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -319,34 +319,34 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -375,18 +375,26 @@ packages: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.0+1" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: b26c7f9c20b38f0ea572c1ed3f29d8e027cb265538bbd1aed3ec198642cfca42 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.6.0+1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -399,82 +407,82 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.4" pool: dependency: transitive description: @@ -483,67 +491,75 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: "direct main" + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_span: dependency: transitive description: @@ -596,10 +612,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -612,10 +628,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" vector_math: dependency: transitive description: @@ -628,42 +644,42 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.0.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.19.0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index e33aeb803..36fe9967e 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -12,13 +12,18 @@ environment: dependencies: flutter: sdk: flutter - http: ^0.13.4 + http: ^1.1.0 file: ^6.1.4 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 encrypt: ^5.0.1 + socks5_proxy: ^1.0.4 +# tor: +# git: +# url: https://github.com/cake-tech/tor.git +# ref: main dev_dependencies: flutter_test: @@ -26,7 +31,8 @@ dev_dependencies: build_runner: ^2.1.11 build_resolvers: ^2.0.9 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_ethereum/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_ethereum/.metadata b/cw_ethereum/.metadata new file mode 100644 index 000000000..1e05dac7f --- /dev/null +++ b/cw_ethereum/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: eb6d86ee27deecba4a83536aa20f366a6044895c + channel: stable + +project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_ethereum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_ethereum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_ethereum/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_ethereum/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_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart new file mode 100644 index 000000000..af9ea7ee0 --- /dev/null +++ b/cw_ethereum/lib/cw_ethereum.dart @@ -0,0 +1,7 @@ +library cw_ethereum; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart new file mode 100644 index 000000000..c26ee1efc --- /dev/null +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -0,0 +1,305 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultEthereumErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Gitcoin", + symbol: "GTC", + contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Compound", + symbol: "COMP", + contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Aave Token", + symbol: "AAVE", + contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap", + symbol: "UNI", + contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentraland", + symbol: "MANA", + contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Storj", + symbol: "STORJ", + contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Maker", + symbol: "MKR", + contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Orchid", + symbol: "OXT", + contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Paxos Gold", + symbol: "PAXG", + contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Binance Coin", + symbol: "BNB", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "stETH", + symbol: "stETH", + contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Lido DAO", + symbol: "LDO", + contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Graph Token", + symbol: "GRT", + contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Frax", + symbol: "FRAX", + contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Gemini dollar", + symbol: "GUSD", + contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + decimal: 2, + enabled: false, + ), + Erc20Token( + name: "Compound Ether", + symbol: "cETH", + contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Binance USD", + symbol: "BUSD", + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "TrueUSD", + symbol: "TUSD", + contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Cronos Coin", + symbol: "CRO", + contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Pax Dollar", + symbol: "USDP", + contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Fantom Token", + symbol: "FTM", + contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BitTorrent", + symbol: "BTT", + contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Nexo", + symbol: "NEXO", + contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "dYdX", + symbol: "DYDX", + contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PancakeSwap Token", + symbol: "Cake", + contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BAT", + symbol: "BAT", + contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "1INCH Token", + symbol: "1INCH", + contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Ethereum Name Service", + symbol: "ENS", + contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ZRX", + symbol: "ZRX", + contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Verse", + symbol: "VERSE", + contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PayPal USD", + symbol: "PYUSD", + contractAddress: "0x6c3ea9036406852006290770bedfcaba0e23a0e8", + decimal: 6, + enabled: false, + ), + ]; + + List get initialErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + return Erc20Token.copyWith(token, iconPath, 'ETH'); + }).toList(); +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..f2b25bcdd --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:web3dart/web3dart.dart'; + +class EthereumClient extends EVMChainClient { + @override + int get chainId => 1; + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => + prependTransactionType(0x02, signedTransaction); + + @override + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) + .toList(); + } + + return []; + } catch (e) { + log(e.toString()); + return []; + } + } + + @override + Future> fetchInternalTransactions(String address) async { + try { + final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": "txlistinternal", + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) + .toList(); + } + + return []; + } catch (e) { + log(e.toString()); + return []; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics_exception.dart b/cw_ethereum/lib/ethereum_mnemonics_exception.dart new file mode 100644 index 000000000..b91a15c94 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart new file mode 100644 index 000000000..f774ae905 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -0,0 +1,18 @@ +import 'dart:core'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class EthereumTransactionHistory extends EVMChainTransactionHistory { + EthereumTransactionHistory({ + required super.walletInfo, + required super.password, + }); + + @override + String getTransactionHistoryFileName() => 'transactions.json'; + + @override + EVMChainTransactionInfo getTransactionInfo(Map val) => + EthereumTransactionInfo.fromJson(val); +} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart new file mode 100644 index 000000000..d5d3fea8d --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class EthereumTransactionInfo extends EVMChainTransactionInfo { + EthereumTransactionInfo({ + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.exponent, + }); + + factory EthereumTransactionInfo.fromJson(Map data) { + return EthereumTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + ); + } + + @override + String get feeCurrency => 'ETH'; +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart new file mode 100644 index 000000000..4604db662 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/file.dart'; + +class EthereumWallet extends EVMChainWallet { + EthereumWallet({ + required super.client, + required super.password, + required super.walletInfo, + super.mnemonic, + super.initialBalance, + super.privateKey, + }) : super(nativeCurrency: CryptoCurrency.eth); + + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens; + + for (var token in initialErc20Tokens) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } + + @override + Future checkIfScanProviderIsEnabled() async { + bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true; + return isEtherscanEnabled; + } + + @override + Future initErc20TokensBox() async { + // This is for ethereum wallets, + // Other wallets would override and initialize their respective boxes with their boxNames. + await movePreviousErc20BoxConfigsToNewBox(); + } + + /// Majorly for backward compatibility for previous configs that have been set. + Future movePreviousErc20BoxConfigsToNewBox() async { + // Opens a box specific to this wallet + evmChainErc20TokensBox = await CakeHive.openBox( + "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}"); + + //Open the previous token configs box + erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); + + // Check if it's empty, if it is, we stop the flow and return. + if (erc20TokensBox.isEmpty) { + // If it's empty, but the new wallet specific box is also empty, + // we load the initial tokens to the new box. + if (evmChainErc20TokensBox.isEmpty) addInitialTokens(); + return; + } + + final allValues = erc20TokensBox.values.toList(); + + // Clear and delete the old token box + await erc20TokensBox.clear(); + await erc20TokensBox.deleteFromDisk(); + + // Add all the previous tokens with configs to the new box + evmChainErc20TokensBox.addAll(allValues); + } + + @override + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address) { + final model = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + to: transactionModel.to, + from: transactionModel.from, + ); + return model; + } + + @override + String getTransactionHistoryFileName() => 'transactions.json'; + + @override + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "ETH", + iconPath: iconPath, + ); + } + + @override + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { + return EthereumTransactionHistory(walletInfo: walletInfo, password: password); + } + + static Future open( + {required String name, required String password, required WalletInfo walletInfo}) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + EVMChainERC20Balance(BigInt.zero); + + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + client: EthereumClient(), + ); + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart new file mode 100644 index 000000000..53c8bfea9 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -0,0 +1,122 @@ +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_mnemonics_exception.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; +import 'package:bip39/bip39.dart' as bip39; + +class EthereumWalletService extends EVMChainWalletService { + EthereumWalletService(super.walletInfoSource, {required this.client}); + + late EthereumClient client; + + @override + WalletType getType() => WalletType.ethereum; + + @override + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await EthereumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await EthereumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWallet.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { + final wallet = EthereumWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = EthereumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml new file mode 100644 index 000000000..649ec574b --- /dev/null +++ b/cw_ethereum/pubspec.yaml @@ -0,0 +1,35 @@ +name: cw_ethereum +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: ^2.7.1 + cw_core: + path: ../cw_core + cw_evm: + path: ../cw_evm + hive: ^2.2.3 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/cw_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart new file mode 100644 index 000000000..72026a4c0 --- /dev/null +++ b/cw_ethereum/test/cw_ethereum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_ethereum/cw_ethereum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_evm/.gitignore b/cw_evm/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_evm/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_evm/.metadata b/cw_evm/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_evm/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_evm/CHANGELOG.md b/cw_evm/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_evm/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_evm/LICENSE b/cw_evm/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_evm/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_evm/README.md b/cw_evm/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_evm/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_evm/analysis_options.yaml b/cw_evm/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_evm/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_evm/lib/cw_evm.dart b/cw_evm/lib/cw_evm.dart new file mode 100644 index 000000000..40f2bcaba --- /dev/null +++ b/cw_evm/lib/cw_evm.dart @@ -0,0 +1,7 @@ +library cw_evm; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart new file mode 100644 index 000000000..8f0df3926 --- /dev/null +++ b/cw_evm/lib/evm_chain_client.dart @@ -0,0 +1,302 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cw_core/node.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/crypto_currency.dart'; + +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:flutter/services.dart'; + +import 'package:http/http.dart'; +import 'package:erc20/erc20.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:hex/hex.dart' as hex; + +abstract class EVMChainClient { + final httpClient = Client(); + Web3Client? _client; + + //! To be overridden by all child classes + + int get chainId; + + Future> fetchTransactions(String address, + {String? contractAddress}); + + Future> fetchInternalTransactions(String address); + + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction); + + //! Common methods across all child classes + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future getBalance(EthereumAddress address) async { + try { + return await _client!.getBalance(address); + } catch (_) { + return EtherAmount.zero(); + } + } + + Future getGasUnitPrice() async { + try { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } catch (_) { + return 0; + } + } + + Future getEstimatedGas() async { + try { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } catch (_) { + return 0; + } + } + + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required BigInt amount, + required int gas, + required EVMChainTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + String? data, + }) async { + assert(currency == CryptoCurrency.eth || + currency == CryptoCurrency.maticpoly || + contractAddress != null); + + bool isEVMCompatibleChain = + currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; + + final price = _client!.getGasPrice(); + + final Transaction transaction = createTransaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(), + data: data != null ? hexToBytes(data) : null, + ); + + final signedTransaction = + await _client!.signTransaction(privateKey, transaction, chainId: chainId); + + final Function _sendTransaction; + + if (isEVMCompatibleChain) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = ERC20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + chainId: chainId, + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + amount, + credentials: privateKey, + transaction: transaction, + ); + }; + } + + return PendingEVMChainTransaction( + signedTransaction: signedTransaction, + amount: amount.toString(), + fee: BigInt.from(gas) * (await price).getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + }) { + return Transaction( + from: from, + to: to, + maxPriorityFeePerGas: maxPriorityFeePerGas, + value: amount, + data: data, + ); + } + + Future sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(const Duration(seconds: 1)); + } + + // Print the receipt information + log('Transaction Hash: ${receipt.transactionHash}'); + log('Block Hash: ${receipt.blockHash}'); + log('Block Number: ${receipt.blockNumber}'); + log('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] + I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] + I/flutter ( 4474): Block Number: 17120242 + I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + log("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(const Duration(seconds: 1)); + } + // Print the receipt information + log('Transaction Hash: ${transactionInformation.hash}'); + log('Block Hash: ${transactionInformation.blockHash}'); + log('Block Number: ${transactionInformation.blockNumber}'); + log('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 + I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 + I/flutter ( 4474): Block Number: 17120242 + I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return EVMChainERC20Balance(balance, exponent: exponent); + } + + Future getErc20Token(String contractAddress, String chainName) async { + try { + final uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/erc20/metadata', + { + "chain": chainName, + "addresses": contractAddress, + }, + ); + + final response = await httpClient.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); + + final decodedResponse = jsonDecode(response.body)[0] as Map; + + + final symbol = (decodedResponse['symbol'] ?? '') as String; + String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); + + final name = decodedResponse['name'] ?? ''; + final decimal = decodedResponse['decimals'] ?? '0'; + final iconPath = decodedResponse['logo'] ?? ''; + + return Erc20Token( + name: name, + symbol: filteredSymbol, + contractAddress: contractAddress, + decimal: int.tryParse(decimal) ?? 0, + iconPath: iconPath, + ); + } catch (e) { + try { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (_) {} + + return null; + } + } + + Uint8List hexToBytes(String hexString) { + return Uint8List.fromList( + hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString)); + } + + void stop() { + _client?.dispose(); + } + + Web3Client? getWeb3Client() { + return _client; + } + +// Future _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart new file mode 100644 index 000000000..8aa371b19 --- /dev/null +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -0,0 +1,22 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EVMChainTransactionCreationException implements Exception { + final String exceptionMessage; + + EVMChainTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} + + +class EVMChainTransactionFeesException implements Exception { + final String exceptionMessage; + + EVMChainTransactionFeesException() + : exceptionMessage = 'Current balance is less than the estimated fees for this transaction.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_evm/lib/evm_chain_formatter.dart b/cw_evm/lib/evm_chain_formatter.dart new file mode 100644 index 000000000..cb9b7346c --- /dev/null +++ b/cw_evm/lib/evm_chain_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const evmChainAmountLength = 12; +const evmChainAmountDivider = 1000000000000; +final evmChainAmountFormat = NumberFormat() + ..maximumFractionDigits = evmChainAmountLength + ..minimumFractionDigits = 1; + +class EVMChainFormatter { + static int parseEVMChainAmount(String amount) { + try { + return (double.parse(amount) * evmChainAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEVMChainAmountToDouble(int amount) { + try { + return amount / evmChainAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_evm/lib/evm_chain_mnemonics.dart b/cw_evm/lib/evm_chain_mnemonics.dart new file mode 100644 index 000000000..55fa4c3a8 --- /dev/null +++ b/cw_evm/lib/evm_chain_mnemonics.dart @@ -0,0 +1,2052 @@ +class EVMChainMnemonics { + static const englishWordlist = [ + '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_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart new file mode 100644 index 000000000..5b5bdf170 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; + +class EVMChainTransactionCredentials { + EVMChainTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); + + final List outputs; + final EVMChainTransactionPriority? priority; + final int? feeRate; + final CryptoCurrency currency; +} diff --git a/cw_evm/lib/evm_chain_transaction_history.dart b/cw_evm/lib/evm_chain_transaction_history.dart new file mode 100644 index 000000000..2f5c31e82 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_history.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'evm_chain_transaction_history.g.dart'; + +abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase + with _$EVMChainTransactionHistory; + +abstract class EVMChainTransactionHistoryBase + extends TransactionHistoryBase with Store { + EVMChainTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + String _password; + + final WalletInfo walletInfo; + + //! Method to be overridden by all child classes + + String getTransactionHistoryFileName(); + + EVMChainTransactionInfo getTransactionInfo(Map val); + + //! Common methods across all child classes + + Future init() async => await _load(); + + @override + Future save() async { + final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); + log(s.toString()); + } + } + + @override + void addOne(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + for (var entry in txs.entries) { + final val = entry.value; + + if (val is Map) { + final tx = getTransactionInfo(val); + _update(tx); + } + } + } catch (e) { + log(e.toString()); + } + } + + void _update(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart new file mode 100644 index 000000000..329061db2 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -0,0 +1,77 @@ +// ignore_for_file: overridden_fields, annotate_overrides + +import 'dart:math'; + +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +abstract class EVMChainTransactionInfo extends TransactionInfo { + EVMChainTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + required this.tokenSymbol, + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + required this.to, + required this.from, + }) : amount = ethAmount.toInt(), + fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + final String? to; + final String? from; + + //! Getter to be overridden in child classes + String get feeCurrency; + + @override + String amountFormatted() { + final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString()); + return '${amount.substring(0, min(10, amount.length))} $tokenSymbol'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() { + final amount = (ethFee / BigInt.from(10).pow(18)).toString(); + return '${amount.substring(0, min(10, amount.length))} $feeCurrency'; + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + }; +} diff --git a/cw_evm/lib/evm_chain_transaction_model.dart b/cw_evm/lib/evm_chain_transaction_model.dart new file mode 100644 index 000000000..dfdeab8f5 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_model.dart @@ -0,0 +1,48 @@ +class EVMChainTransactionModel { + final DateTime date; + final String hash; + final String from; + final String to; + final BigInt amount; + final int gasUsed; + final BigInt gasPrice; + final String contractAddress; + final int confirmations; + final int blockNumber; + final String? tokenSymbol; + final int? tokenDecimal; + final bool isError; + + EVMChainTransactionModel({ + required this.date, + required this.hash, + required this.from, + required this.to, + required this.amount, + required this.gasUsed, + required this.gasPrice, + required this.contractAddress, + required this.confirmations, + required this.blockNumber, + required this.tokenSymbol, + required this.tokenDecimal, + required this.isError, + }); + + factory EVMChainTransactionModel.fromJson(Map json, String defaultSymbol) => + EVMChainTransactionModel( + date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), + hash: json["hash"] ?? "", + from: json["from"] ?? "", + to: json["to"] ?? "", + amount: BigInt.parse(json["value"] ?? "0"), + gasUsed: int.parse(json["gasUsed"] ?? "0"), + gasPrice: BigInt.parse(json["gasPrice"] ?? "0"), + contractAddress: json["contractAddress"] ?? "", + confirmations: int.parse(json["confirmations"] ?? "0"), + blockNumber: int.parse(json["blockNumber"] ?? "0"), + tokenSymbol: json["tokenSymbol"] ?? defaultSymbol, + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_evm/lib/evm_chain_transaction_priority.dart b/cw_evm/lib/evm_chain_transaction_priority.dart new file mode 100644 index 000000000..b4ce55490 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EVMChainTransactionPriority extends TransactionPriority { + final int tip; + + const EVMChainTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const EVMChainTransactionPriority slow = + EVMChainTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EVMChainTransactionPriority medium = + EVMChainTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EVMChainTransactionPriority fast = + EVMChainTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EVMChainTransactionPriority 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 EVMChainTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EVMChainTransactionPriority.slow: + label = 'Slow'; + break; + case EVMChainTransactionPriority.medium: + label = 'Medium'; + break; + case EVMChainTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart new file mode 100644 index 000000000..558013252 --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -0,0 +1,542 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_exceptions.dart'; +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_wallet_addresses.dart'; +import 'package:cw_evm/file.dart'; +import 'package:hex/hex.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +import 'evm_chain_transaction_info.dart'; +import 'evm_erc20_balance.dart'; + +part 'evm_chain_wallet.g.dart'; + +abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; + +abstract class EVMChainWalletBase + extends WalletBase + with Store { + EVMChainWalletBase({ + required WalletInfo walletInfo, + required EVMChainClient client, + required CryptoCurrency nativeCurrency, + String? mnemonic, + String? privateKey, + required String password, + EVMChainERC20Balance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _isTransactionUpdating = false, + _client = client, + walletAddresses = EVMChainWalletAddresses(walletInfo), + balance = ObservableMap.of( + { + // Not sure of this yet, will it work? will it not? + nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero), + }, + ), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = setUpTransactionHistory(walletInfo, password); + + if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { + CakeHive.registerAdapter(Erc20TokenAdapter()); + } + + sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String? _mnemonic; + final String? _hexPrivateKey; + final String _password; + + late final Box erc20TokensBox; + + late final Box evmChainErc20TokensBox; + + late final EthPrivateKey _evmChainPrivateKey; + + EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey; + + late EVMChainClient _client; + + int? _gasPrice; + int? _estimatedGas; + bool _isTransactionUpdating; + + // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer sharedPrefs = Completer(); + + //! Methods to be overridden by every child + + void addInitialTokens(); + + // Future open({ + // required String name, + // required String password, + // required WalletInfo walletInfo, + // }); + + Future initErc20TokensBox(); + + String getTransactionHistoryFileName(); + + Future checkIfScanProviderIsEnabled(); + + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address); + + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath); + + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password); + + //! Common Methods across child classes + + String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; + + Future init() async { + await initErc20TokensBox(); + + await walletAddresses.init(); + await transactionHistory.init(); + _evmChainPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + walletAddresses.address = _evmChainPrivateKey.address.hexEip55; + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EVMChainTransactionPriority) { + final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed"); + } + + _client.setListeners(_evmChainPrivateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await _updateTransactions(); + _gasPrice = await _client.getGasUnitPrice(); + _estimatedGas = await _client.getEstimatedGas(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); + Timer.periodic(const Duration(seconds: 10), + (timer) async => _estimatedGas = await _client.getEstimatedGas()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as EVMChainTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + + final String? opReturnMemo = outputs.first.memo; + + String? hexOpReturnMemo; + if (opReturnMemo != null) { + hexOpReturnMemo = '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}'; + } + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == _credentials.currency.title); + + final erc20Balance = balance[transactionCurrency]!; + BigInt totalAmount = BigInt.zero; + int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; + num amountToEVMChainMultiplier = pow(10, exponent); + + // so far this can not be made with Ethereum as Ethereum does not support multiple recipients + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EVMChainTransactionCreationException(transactionCurrency); + } + + final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); + + if (erc20Balance.balance < totalAmount) { + throw EVMChainTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + // since the fees are taken from Ethereum + // then no need to subtract the fees from the amount if send all + final BigInt allAmount; + if (transactionCurrency is Erc20Token) { + allAmount = erc20Balance.balance; + } else { + final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + + if (estimatedFee > erc20Balance.balance) { + throw EVMChainTransactionFeesException(); + } + + allAmount = erc20Balance.balance - estimatedFee; + } + + if (output.sendAll) { + totalAmount = allAmount; + } else { + final totalOriginalAmount = + EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); + + totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); + } + + if (erc20Balance.balance < totalAmount) { + throw EVMChainTransactionCreationException(transactionCurrency); + } + } + + final pendingEVMChainTransaction = await _client.signTransaction( + privateKey: _evmChainPrivateKey, + toAddress: _credentials.outputs.first.isParsedAddress + ? _credentials.outputs.first.extractedAddress! + : _credentials.outputs.first.address, + amount: totalAmount, + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: transactionCurrency, + exponent: exponent, + contractAddress: + transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, + data: hexOpReturnMemo, + ); + + return pendingEVMChainTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + final isProviderEnabled = await checkIfScanProviderIsEnabled(); + + if (!isProviderEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _evmChainPrivateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + final internalTransactions = await _client.fetchInternalTransactions(address); + + final List>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + transactions.addAll(internalTransactions); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = getTransactionInfo(transactionModel, address); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String? get seed => _mnemonic; + + @override + String get privateKey => HEX.encode(_evmChainPrivateKey.privateKey); + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + Future _updateBalance() async { + balance[currency] = await _fetchEVMChainBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchEVMChainBalance() async { + final balance = await _client.getBalance(_evmChainPrivateKey.address); + return EVMChainERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in evmChainErc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _evmChainPrivateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future getPrivateKey( + {String? mnemonic, String? privateKey, required String password}) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + return EthPrivateKey.fromHex(privateKey); + } + + final seed = bip39.mnemonicToSeed(mnemonic!); + + final root = bip32.BIP32.fromSeed(seed); + + const hdPathEVMChain = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$hdPathEVMChain/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => evmChainErc20TokensBox.values.toList(); + + Future addErc20Token(Erc20Token token) async { + String? iconPath; + + if (token.iconPath == null || token.iconPath!.isEmpty) { + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + } else { + iconPath = token.iconPath; + } + + final newToken = createNewErc20TokenObject(token, iconPath); + + await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); + + if (newToken.enabled) { + balance[newToken] = await _client.fetchERC20Balances( + _evmChainPrivateKey.address, + newToken.contractAddress, + ); + } else { + balance.remove(newToken); + } + } + + Future deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + await _removeTokenTransactionsInHistory(token); + _updateBalance(); + } + + Future _removeTokenTransactionsInHistory(Erc20Token token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + + Future getErc20Token(String contractAddress, String chainName) async => + await _client.getErc20Token(contractAddress, chainName); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final transactionHistoryFileNameForWallet = getTransactionHistoryFileName(); + + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + /// Scan Providers: + /// + /// EtherScan for Ethereum. + /// + /// PolygonScan for Polygon. + void updateScanProviderUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } + + @override + String signMessage(String message, {String? address}) => + bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); + + Web3Client? getWeb3Client() => _client.getWeb3Client(); +} diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart new file mode 100644 index 000000000..4615d79ed --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'evm_chain_wallet_addresses.g.dart'; + +class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; + +abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { + EVMChainWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + @observable + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + log(e.toString()); + } + } +} diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart new file mode 100644 index 000000000..7c3271daf --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EVMChainNewWalletCredentials extends WalletCredentials { + EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { + EVMChainRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials { + EVMChainRestoreWalletFromPrivateKey({ + required String name, + required String password, + required this.privateKey, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); + + final String privateKey; +} diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart new file mode 100644 index 000000000..d77a3a81a --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; + +abstract class EVMChainWalletService extends WalletService< + EVMChainNewWalletCredentials, + EVMChainRestoreWalletFromSeedCredentials, + EVMChainRestoreWalletFromPrivateKey> { + EVMChainWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + WalletType getType(); + + @override + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}); + + @override + Future openWallet(String name, String password); + + @override + Future rename(String currentName, String password, String newName); + + @override + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet}); + + @override + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}); + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } +} diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart new file mode 100644 index 000000000..1727d7962 --- /dev/null +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/balance.dart'; + +class EVMChainERC20Balance extends Balance { + EVMChainERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), balance.toInt()); + + final BigInt balance; + final int exponent; + + @override + String get formattedAdditionalBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + @override + String get formattedAvailableBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + String toJSON() => json.encode({ + 'balanceInWei': balance.toString(), + 'exponent': exponent, + }); + + static EVMChainERC20Balance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return EVMChainERC20Balance( + BigInt.parse(decoded['balanceInWei']), + exponent: decoded['exponent'], + ); + } catch (e) { + return EVMChainERC20Balance(BigInt.zero); + } + } +} diff --git a/cw_bitcoin/lib/file.dart b/cw_evm/lib/file.dart similarity index 100% rename from cw_bitcoin/lib/file.dart rename to cw_evm/lib/file.dart diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart new file mode 100644 index 000000000..0b367da68 --- /dev/null +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -0,0 +1,50 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:hex/hex.dart' as Hex; + +class PendingEVMChainTransaction with PendingTransaction { + final Function sendTransaction; + final Uint8List signedTransaction; + final BigInt fee; + final String amount; + final int exponent; + + PendingEVMChainTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + required this.exponent, + }); + + @override + String get amountFormatted { + final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); + return _amount.substring(0, min(10, _amount.length)); + } + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted { + final _fee = (fee / BigInt.from(pow(10, 18))).toString(); + return _fee.substring(0, min(10, _fee.length)); + } + + @override + String get hex => bytesToHex(signedTransaction, include0x: true); + + @override + String get id { + final String eip1559Hex = '0x02${hex.substring(2)}'; + final Uint8List bytes = Uint8List.fromList(Hex.HEX.decode(eip1559Hex.substring(2))); + + var txid = keccak256(bytes); + + return '0x${Hex.HEX.encode(txid)}'; + } +} diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml new file mode 100644 index 000000000..c202cc72a --- /dev/null +++ b/cw_evm/pubspec.yaml @@ -0,0 +1,45 @@ +name: cw_evm +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: ^2.7.1 + erc20: ^1.0.1 + bip39: ^1.0.6 + bip32: ^2.0.0 + hex: ^0.2.0 + http: ^1.1.0 + hive: ^2.2.3 + collection: ^1.17.1 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + flutter_lints: ^2.0.0 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/cw_evm/test/cw_evm_test.dart b/cw_evm/test/cw_evm_test.dart new file mode 100644 index 000000000..6a4dea276 --- /dev/null +++ b/cw_evm/test/cw_evm_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_evm/cw_evm.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index 91da1b857..fb941f657 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.cw_haven' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart index 31c911edc..ae95b62dd 100644 --- a/cw_haven/lib/api/signatures.dart +++ b/cw_haven/lib/api/signatures.dart @@ -39,7 +39,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); typedef start_refresh = Void Function(); diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart index de9ff74a0..8c9dfdab2 100644 --- a/cw_haven/lib/api/types.dart +++ b/cw_haven/lib/api/types.dart @@ -39,7 +39,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); typedef StartRefresh = void Function(); diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart index bdf6a1af7..e6b75c0cc 100644 --- a/cw_haven/lib/api/wallet.dart +++ b/cw_haven/lib/api/wallet.dart @@ -154,9 +154,11 @@ bool setupNodeSync( String? login, String? password, bool useSSL = false, - bool isLightWallet = false}) { + bool isLightWallet = false, + String? socksProxyAddress}) { final addressPointer = address.toNativeUtf8(); Pointer? loginPointer; + Pointer? socksProxyAddressPointer; Pointer? passwordPointer; if (login != null) { @@ -167,6 +169,10 @@ bool setupNodeSync( passwordPointer = password.toNativeUtf8(); } + if (socksProxyAddress != null) { + socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); + } + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, @@ -174,6 +180,7 @@ bool setupNodeSync( passwordPointer, _boolToInt(useSSL), _boolToInt(isLightWallet), + socksProxyAddressPointer, errorMessagePointer) != 0; @@ -323,13 +330,15 @@ bool _setupNodeSync(Map args) { final password = (args['password'] ?? '') as String; final useSSL = args['useSSL'] as bool; final isLightWallet = args['isLightWallet'] as bool; + final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String; return setupNodeSync( address: address, login: login, password: password, useSSL: useSSL, - isLightWallet: isLightWallet); + isLightWallet: isLightWallet, + socksProxyAddress: socksProxyAddress); } bool _isConnected(Object _) => isConnectedSync(); @@ -343,13 +352,15 @@ Future setupNode( String? login, String? password, bool useSSL = false, + String? socksProxyAddress, bool isLightWallet = false}) => compute, void>(_setupNodeSync, { 'address': address, 'login': login, 'password': password, 'useSSL': useSSL, - 'isLightWallet': isLightWallet + 'isLightWallet': isLightWallet, + 'socksProxyAddress': socksProxyAddress }); Future store() => compute(_storeSync, 0); diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 2a72f078f..e639be4b9 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:io'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_haven/haven_transaction_creation_credentials.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -10,8 +12,7 @@ import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/transaction_history.dart' - as haven_transaction_history; +import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; //import 'package:cw_haven/wallet.dart'; import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/transaction_history.dart' as transaction_history; @@ -35,8 +36,8 @@ const moneroBlockSize = 1000; class HavenWallet = HavenWalletBase with _$HavenWallet; -abstract class HavenWalletBase extends WalletBase with Store { +abstract class HavenWalletBase + extends WalletBase with Store { HavenWalletBase({required WalletInfo walletInfo}) : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), _isTransactionUpdating = false, @@ -45,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase walletAddresses.account, - (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } @@ -94,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } @override @@ -113,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase connectToNode({required Node node}) async { try { @@ -123,7 +121,8 @@ abstract class HavenWalletBase extends WalletBase item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final moneroOutputs = outputs.map((output) => - MoneroOutput( - address: output.address, - amount: output.cryptoAmount!.replaceAll(',', '.'))) + final moneroOutputs = outputs + .map((output) => MoneroOutput( + address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.'))) .toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id); @@ -195,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: name, type: type); + final currentCacheFile = File(currentWalletPath); + final currentKeysFile = File('$currentWalletPath.keys'); + final currentAddressListFile = File('$currentWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + // Copies current wallet files into new wallet name's dir and files + if (currentCacheFile.existsSync()) { + await currentCacheFile.copy(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.copy('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.copy('$newWalletPath.address.txt'); + } + + // Delete old name's dir and files + await Directory(currentWalletPath).delete(recursive: true); + } + @override Future changePassword(String password) async { haven_wallet.setPasswordSync(password); @@ -280,16 +297,14 @@ abstract class HavenWalletBase extends WalletBase - haven_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { haven_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, HavenTransactionInfo tx) { + return _getAllTransactions(null) + .fold>({}, + (Map acc, HavenTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -313,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase _getAllTransactions(dynamic _) => haven_transaction_history - .getAllTransations() - .map((row) => HavenTransactionInfo.fromRow(row)) - .toList(); + .getAllTransations() + .map((row) => HavenTransactionInfo.fromRow(row)) + .toList(); void _setListeners() { _listener?.stop(); @@ -337,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); - Future _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); void _onNewBlock(int height, int blocksLeft, double ptc) async { try { @@ -377,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase + addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; +} diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index 137ade78f..d4808c2d6 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -68,7 +68,7 @@ class HavenWalletService extends WalletService< WalletType getType() => WalletType.haven; @override - Future create(HavenNewWalletCredentials credentials) async { + Future create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.createWallet( @@ -149,11 +149,32 @@ class HavenWalletService extends WalletService< if (isExist) { await file.delete(recursive: true); } + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename( + String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = HavenWallet(walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @override Future restoreFromKeys( - HavenRestoreWalletFromKeysCredentials credentials) async { + HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromKeys( @@ -177,7 +198,7 @@ class HavenWalletService extends WalletService< @override Future restoreFromSeed( - HavenRestoreWalletFromSeedCredentials credentials) async { + HavenRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromSeed( diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index a79d2d3cf..d84523539 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" build_resolvers: dependency: "direct dev" description: @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.2" convert: dependency: transitive description: @@ -286,10 +286,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -310,10 +310,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -326,10 +326,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -350,26 +350,26 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -406,58 +406,58 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -535,6 +535,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -555,10 +563,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -603,10 +611,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.6.0" timing: dependency: transitive description: @@ -632,13 +640,21 @@ packages: source: hosted version: "2.1.4" watcher: - dependency: transitive + dependency: "direct overridden" description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -672,5 +688,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml index 7a5ac6aa4..c215ab779 100644 --- a/cw_haven/pubspec.yaml +++ b/cw_haven/pubspec.yaml @@ -13,11 +13,11 @@ dependencies: flutter: sdk: flutter ffi: ^2.0.1 - http: ^0.13.4 + http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 cw_core: path: ../cw_core diff --git a/cw_monero/android/build.gradle b/cw_monero/android/build.gradle index 1d7ae93d8..fc4835e81 100644 --- a/cw_monero/android/build.gradle +++ b/cw_monero/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.monero' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.4' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index 19d9cef8f..ece0d4395 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.2" convert: dependency: transitive description: @@ -153,14 +153,30 @@ packages: description: flutter source: sdk version: "0.0.0" + hashlib: + dependency: transitive + description: + name: hashlib + sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + hashlib_codecs: + dependency: transitive + description: + name: hashlib_codecs + sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626" + url: "https://pub.dev" + source: hosted + version: "2.2.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_parser: dependency: transitive description: @@ -173,18 +189,18 @@ packages: dependency: transitive description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: @@ -197,26 +213,26 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mobx: dependency: transitive description: @@ -229,58 +245,58 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: transitive description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -301,10 +317,18 @@ packages: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" + polyseed: + dependency: transitive + description: + name: polyseed + sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f" + url: "https://pub.dev" + source: hosted + version: "0.0.2" process: dependency: transitive description: @@ -318,14 +342,22 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_span: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -362,10 +394,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.6.0" typed_data: dependency: transitive description: @@ -382,6 +414,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" win32: dependency: transitive description: @@ -399,5 +439,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=2.18.1 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 117214295..87be785ac 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include "thread" #include "CwWalletListener.h" #if __APPLE__ @@ -137,7 +139,7 @@ extern "C" int8_t direction; int8_t isPending; uint32_t subaddrIndex; - + char *hash; char *paymentId; @@ -152,7 +154,7 @@ extern "C" std::set::iterator it = transaction->subaddrIndex().begin(); subaddrIndex = *it; confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); + datetime = static_cast(transaction->timestamp()); direction = transaction->direction(); isPending = static_cast(transaction->isPending()); std::string *hash_str = new std::string(transaction->hash()); @@ -181,6 +183,61 @@ extern "C" } }; + struct CoinsInfoRow + { + uint64_t blockHeight; + char *hash; + uint64_t internalOutputIndex; + uint64_t globalOutputIndex; + bool spent; + bool frozen; + uint64_t spentHeight; + uint64_t amount; + bool rct; + bool keyImageKnown; + uint64_t pkIndex; + uint32_t subaddrIndex; + uint32_t subaddrAccount; + char *address; + char *addressLabel; + char *keyImage; + uint64_t unlockTime; + bool unlocked; + char *pubKey; + bool coinbase; + char *description; + + CoinsInfoRow(Monero::CoinsInfo *coinsInfo) + { + blockHeight = coinsInfo->blockHeight(); + std::string *hash_str = new std::string(coinsInfo->hash()); + hash = strdup(hash_str->c_str()); + internalOutputIndex = coinsInfo->internalOutputIndex(); + globalOutputIndex = coinsInfo->globalOutputIndex(); + spent = coinsInfo->spent(); + frozen = coinsInfo->frozen(); + spentHeight = coinsInfo->spentHeight(); + amount = coinsInfo->amount(); + rct = coinsInfo->rct(); + keyImageKnown = coinsInfo->keyImageKnown(); + pkIndex = coinsInfo->pkIndex(); + subaddrIndex = coinsInfo->subaddrIndex(); + subaddrAccount = coinsInfo->subaddrAccount(); + address = strdup(coinsInfo->address().c_str()) ; + addressLabel = strdup(coinsInfo->addressLabel().c_str()); + keyImage = strdup(coinsInfo->keyImage().c_str()); + unlockTime = coinsInfo->unlockTime(); + unlocked = coinsInfo->unlocked(); + pubKey = strdup(coinsInfo->pubKey().c_str()); + coinbase = coinsInfo->coinbase(); + description = strdup(coinsInfo->description().c_str()); + } + + void setUnlocked(bool unlocked); + }; + + Monero::Coins *m_coins; + Monero::Wallet *m_wallet; Monero::TransactionHistory *m_transaction_history; MoneroWalletListener *m_listener; @@ -188,6 +245,7 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; + std::list m_coins_info; std::mutex store_lock; bool is_storing = false; @@ -195,7 +253,7 @@ extern "C" { m_wallet = wallet; m_listener = nullptr; - + if (wallet != nullptr) { @@ -223,6 +281,17 @@ extern "C" { m_subaddress = nullptr; } + + m_coins_info = std::list(); + + if (wallet != nullptr) + { + m_coins = wallet->coins(); + } + else + { + m_coins = nullptr; + } } Monero::Wallet *get_current_wallet() @@ -305,6 +374,35 @@ extern "C" return true; } + bool restore_wallet_from_spend_key(char *path, char *password, char *seed, char *language, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createDeterministicWalletFromSpendKey( + std::string(path), + std::string(password), + std::string(language), + _networkType, + (uint64_t)restoreHeight, + std::string(spendKey)); + + // Cache Raw to support Polyseed + wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + bool load_wallet(char *path, char *password, int32_t nettype) { nice(19); @@ -369,6 +467,11 @@ extern "C" const char *seed() { + std::string _rawSeed = get_current_wallet()->getCacheAttribute("cakewallet.seed"); + if (!_rawSeed.empty()) + { + return strdup(_rawSeed.c_str()); + } return strdup(get_current_wallet()->seed().c_str()); } @@ -405,13 +508,14 @@ extern "C" return is_connected; } - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error) { nice(19); Monero::Wallet *wallet = get_current_wallet(); - + std::string _login = ""; std::string _password = ""; + std::string _socksProxyAddress = ""; if (login != nullptr) { @@ -423,7 +527,12 @@ extern "C" _password = std::string(password); } - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + if (socksProxyAddress != nullptr) + { + _socksProxyAddress = std::string(socksProxyAddress); + } + + bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress); if (!inited) { @@ -480,10 +589,19 @@ extern "C" } bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); - + + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -496,13 +614,13 @@ extern "C" if (amount != nullptr) { uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } else { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } - + int status = transaction->status(); if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) @@ -520,7 +638,9 @@ extern "C" } bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); @@ -534,6 +654,13 @@ extern "C" amounts++; } + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -748,6 +875,12 @@ extern "C" return m_transaction_history->count(); } + TransactionInfoRow* get_transaction(char * txId) + { + Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); + return new TransactionInfoRow(row); + } + int LedgerExchange( unsigned char *command, unsigned int cmd_len, @@ -793,6 +926,109 @@ extern "C" return m_wallet->trustedDaemon(); } + // Coin Control // + + CoinsInfoRow* coin(int index) + { + if (index >= 0 && index < m_coins_info.size()) { + std::list::iterator it = m_coins_info.begin(); + std::advance(it, index); + Monero::CoinsInfo* element = *it; + std::cout << "Element at index " << index << ": " << element << std::endl; + return new CoinsInfoRow(element); + } else { + std::cout << "Invalid index." << std::endl; + return nullptr; // Return a default value (nullptr) for invalid index + } + } + + void refresh_coins(uint32_t accountIndex) + { + m_coins_info.clear(); + + m_coins->refresh(); + for (const auto i : m_coins->getAll()) { + if (i->subaddrAccount() == accountIndex && !(i->spent())) { + m_coins_info.push_back(i); + } + } + } + + uint64_t coins_count() + { + return m_coins_info.size(); + } + + CoinsInfoRow** coins_from_account(uint32_t accountIndex) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (coinInfo->subaddrAccount == accountIndex) { + matchingCoins.push_back(coinInfo); + } + } + + CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (std::string(coinInfo->hash) == txid) { + matchingCoins.push_back(coinInfo); + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinsInfoRow = coin(i); + for (size_t j = 0; j < keyimageCount; j++) { + if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { + matchingCoins.push_back(coinsInfoRow); + break; + } + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + void freeze_coin(int index) + { + m_coins->setFrozen(index); + } + + void thaw_coin(int index) + { + m_coins->thaw(index); + } + + // Sign Messages // + + char *sign_message(char *message, char *address = "") + { + return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str()); + } + #ifdef __cplusplus } #endif diff --git a/cw_monero/ios/Classes/monero_api.h b/cw_monero/ios/Classes/monero_api.h index 74258ba4c..fa92a038d 100644 --- a/cw_monero/ios/Classes/monero_api.h +++ b/cw_monero/ios/Classes/monero_api.h @@ -32,7 +32,8 @@ void store(char *path); void set_trusted_daemon(bool arg); bool trusted_daemon(); +char *sign_message(char *message, char *address); #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart new file mode 100644 index 000000000..d7350a6e2 --- /dev/null +++ b/cw_monero/lib/api/coins_info.dart @@ -0,0 +1,35 @@ +import 'dart:ffi'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:cw_monero/api/monero_api.dart'; + +final refreshCoinsNative = moneroApi + .lookup>('refresh_coins') + .asFunction(); + +final coinsCountNative = moneroApi + .lookup>('coins_count') + .asFunction(); + +final coinNative = moneroApi + .lookup>('coin') + .asFunction(); + +final freezeCoinNative = moneroApi + .lookup>('freeze_coin') + .asFunction(); + +final thawCoinNative = moneroApi + .lookup>('thaw_coin') + .asFunction(); + +void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); + +int countOfCoins() => coinsCountNative(); + +CoinsInfoRow getCoin(int index) => coinNative(index).ref; + +void freezeCoin(int index) => freezeCoinNative(index); + +void thawCoin(int index) => thawCoinNative(index); diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 0b14fd557..bc4fc9d38 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -1,5 +1,7 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -9,8 +11,11 @@ typedef create_wallet = Int8 Function( typedef restore_wallet_from_seed = Int8 Function( Pointer, Pointer, Pointer, Int32, Int64, Pointer); -typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); +typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, Pointer, + Pointer, Pointer, Pointer, Int32, Int64, Pointer); + +typedef restore_wallet_from_spend_key = Int8 Function(Pointer, Pointer, Pointer, + Pointer, Pointer, Int32, Int64, Pointer); typedef is_wallet_exist = Int8 Function(Pointer); @@ -35,7 +40,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); typedef start_refresh = Void Function(); @@ -63,8 +68,7 @@ typedef subaddrress_refresh = Void Function(Int32); typedef subaddress_get_all = Pointer Function(); -typedef subaddress_add_new = Void Function( - Int32 accountIndex, Pointer label); +typedef subaddress_add_new = Void Function(Int32 accountIndex, Pointer label); typedef subaddress_set_label = Void Function( Int32 accountIndex, Int32 addressIndex, Pointer label); @@ -77,11 +81,12 @@ typedef account_get_all = Pointer Function(); typedef account_add_new = Void Function(Pointer label); -typedef account_set_label = Void Function( - Int32 accountIndex, Pointer label); +typedef account_set_label = Void Function(Int32 accountIndex, Pointer label); typedef transactions_refresh = Void Function(); +typedef get_transaction = Pointer Function(Pointer txId); + typedef get_tx_key = Pointer? Function(Pointer txId); typedef transactions_count = Int64 Function(); @@ -94,6 +99,8 @@ typedef transaction_create = Int8 Function( Pointer amount, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -104,6 +111,8 @@ typedef transaction_create_mult_dest = Int8 Function( Int32 size, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -123,10 +132,22 @@ typedef on_startup = Void Function(); typedef rescan_blockchain = Void Function(); -typedef get_subaddress_label = Pointer Function( - Int32 accountIndex, - Int32 addressIndex); +typedef get_subaddress_label = Pointer Function(Int32 accountIndex, Int32 addressIndex); typedef set_trusted_daemon = Void Function(Int8 trusted); -typedef trusted_daemon = Int8 Function(); \ No newline at end of file +typedef trusted_daemon = Int8 Function(); + +typedef refresh_coins = Void Function(Int32 accountIndex); + +typedef coins_count = Int64 Function(); + +// typedef coins_from_txid = Pointer Function(Pointer txid); + +typedef coin = Pointer Function(Int32 index); + +typedef freeze_coin = Void Function(Int32 index); + +typedef thaw_coin = Void Function(Int32 index); + +typedef sign_message = Pointer Function(Pointer message, Pointer address); diff --git a/cw_monero/lib/api/structs/coins_info_row.dart b/cw_monero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_monero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 0fc507500..73c8de801 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,15 +1,16 @@ import 'dart:ffi'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; +import 'package:cw_monero/api/types.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; final transactionsRefreshNative = moneroApi .lookup>('transactions_refresh') @@ -35,9 +36,12 @@ final transactionCommitNative = moneroApi .lookup>('transaction_commit') .asFunction(); -final getTxKeyNative = moneroApi - .lookup>('get_tx_key') - .asFunction(); +final getTxKeyNative = + moneroApi.lookup>('get_tx_key').asFunction(); + +final getTransactionNative = moneroApi + .lookup>('get_transaction') + .asFunction(); String getTxKey(String txId) { final txIdPointer = txId.toNativeUtf8(); @@ -56,7 +60,7 @@ void refreshTransactions() => transactionsRefreshNative(); int countOfTransactions() => transactionsCountNative(); -List getAllTransations() { +List getAllTransactions() { final size = transactionsCountNative(); final transactionsPointer = transactionsGetAllNative(); final transactionsAddresses = transactionsPointer.asTypedList(size); @@ -66,15 +70,31 @@ List getAllTransations() { .toList(); } +TransactionInfoRow getTransaction(String txId) { + final txIdPointer = txId.toNativeUtf8(); + return getTransactionNative(txIdPointer).ref; +} + PendingTransactionDescription createTransactionSync( {required String address, required String paymentId, required int priorityRaw, String? amount, - int accountIndex = 0}) { + int accountIndex = 0, + List preferredInputs = const []}) { final addressPointer = address.toNativeUtf8(); final paymentIdPointer = paymentId.toNativeUtf8(); final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + + final int preferredInputsSize = preferredInputs.length; + final List> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); final created = transactionCreateNative( @@ -83,10 +103,16 @@ PendingTransactionDescription createTransactionSync( amountPointer, priorityRaw, accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, errorMessagePointer, pendingTransactionRawPointer) != 0; + calloc.free(preferredInputsPointerPointer); + + preferredInputsPointers.forEach((element) => calloc.free(element)); + calloc.free(addressPointer); calloc.free(paymentIdPointer); @@ -111,15 +137,16 @@ PendingTransactionDescription createTransactionSync( PendingTransactionDescription createTransactionMultDestSync( {required List outputs, - required String paymentId, - required int priorityRaw, - int accountIndex = 0}) { + required String paymentId, + required int priorityRaw, + int accountIndex = 0, + List preferredInputs = const []}) { final int size = outputs.length; - final List> addressesPointers = outputs.map((output) => - output.address.toNativeUtf8()).toList(); + final List> addressesPointers = + outputs.map((output) => output.address.toNativeUtf8()).toList(); final Pointer> addressesPointerPointer = calloc(size); - final List> amountsPointers = outputs.map((output) => - output.amount.toNativeUtf8()).toList(); + final List> amountsPointers = + outputs.map((output) => output.amount.toNativeUtf8()).toList(); final Pointer> amountsPointerPointer = calloc(size); for (int i = 0; i < size; i++) { @@ -127,25 +154,38 @@ PendingTransactionDescription createTransactionMultDestSync( amountsPointerPointer[i] = amountsPointers[i]; } + final int preferredInputsSize = preferredInputs.length; + final List> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final paymentIdPointer = paymentId.toNativeUtf8(); final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); final created = transactionCreateMultDestNative( - addressesPointerPointer, - paymentIdPointer, - amountsPointerPointer, - size, - priorityRaw, - accountIndex, - errorMessagePointer, - pendingTransactionRawPointer) != + addressesPointerPointer, + paymentIdPointer, + amountsPointerPointer, + size, + priorityRaw, + accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, + errorMessagePointer, + pendingTransactionRawPointer) != 0; calloc.free(addressesPointerPointer); calloc.free(amountsPointerPointer); + calloc.free(preferredInputsPointerPointer); addressesPointers.forEach((element) => calloc.free(element)); amountsPointers.forEach((element) => calloc.free(element)); + preferredInputsPointers.forEach((element) => calloc.free(element)); calloc.free(paymentIdPointer); @@ -164,13 +204,12 @@ PendingTransactionDescription createTransactionMultDestSync( pointerAddress: pendingTransactionRawPointer.address); } -void commitTransactionFromPointerAddress({required int address}) => commitTransaction( - transactionPointer: Pointer.fromAddress(address)); +void commitTransactionFromPointerAddress({required int address}) => + commitTransaction(transactionPointer: Pointer.fromAddress(address)); void commitTransaction({required Pointer transactionPointer}) { final errorMessagePointer = calloc(); - final isCommited = - transactionCommitNative(transactionPointer, errorMessagePointer) != 0; + final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; if (!isCommited) { final message = errorMessagePointer.ref.getValue(); @@ -185,13 +224,15 @@ PendingTransactionDescription _createTransactionSync(Map args) { final amount = args['amount'] as String?; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; return createTransactionSync( address: address, paymentId: paymentId, amount: amount, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } PendingTransactionDescription _createTransactionMultDestSync(Map args) { @@ -199,12 +240,14 @@ PendingTransactionDescription _createTransactionMultDestSync(Map args) { final paymentId = args['paymentId'] as String; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; return createTransactionMultDestSync( outputs: outputs, paymentId: paymentId, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } Future createTransaction( @@ -212,23 +255,27 @@ Future createTransaction( required int priorityRaw, String? amount, String paymentId = '', - int accountIndex = 0}) => + int accountIndex = 0, + List preferredInputs = const []}) => compute(_createTransactionSync, { 'address': address, 'paymentId': paymentId, 'amount': amount, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); Future createTransactionMultDest( - {required List outputs, - required int priorityRaw, - String paymentId = '', - int accountIndex = 0}) => + {required List outputs, + required int priorityRaw, + String paymentId = '', + int accountIndex = 0, + List preferredInputs = const []}) => compute(_createTransactionMultDestSync, { 'outputs': outputs, 'paymentId': paymentId, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index c5918c12a..40a1e0321 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -1,5 +1,7 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -12,6 +14,9 @@ typedef RestoreWalletFromSeed = int Function( typedef RestoreWalletFromKeys = int Function(Pointer, Pointer, Pointer, Pointer, Pointer, Pointer, int, int, Pointer); +typedef RestoreWalletFromSpendKey = int Function(Pointer, Pointer, Pointer, + Pointer, Pointer, int, int, Pointer); + typedef IsWalletExist = int Function(Pointer); typedef LoadWallet = int Function(Pointer, Pointer, int); @@ -35,7 +40,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); typedef StartRefresh = void Function(); @@ -80,6 +85,8 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer label); typedef TransactionsRefresh = void Function(); +typedef GetTransaction = Pointer Function(Pointer txId); + typedef GetTxKey = Pointer? Function(Pointer txId); typedef TransactionsCount = int Function(); @@ -92,6 +99,8 @@ typedef TransactionCreate = int Function( Pointer amount, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -102,6 +111,8 @@ typedef TransactionCreateMultDest = int Function( int size, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -127,4 +138,16 @@ typedef GetSubaddressLabel = Pointer Function( typedef SetTrustedDaemon = void Function(int); -typedef TrustedDaemon = int Function(); \ No newline at end of file +typedef TrustedDaemon = int Function(); + +typedef RefreshCoins = void Function(int); + +typedef CoinsCount = int Function(); + +typedef GetCoin = Pointer Function(int); + +typedef FreezeCoin = void Function(int); + +typedef ThawCoin = void Function(int); + +typedef SignMessage = Pointer Function(Pointer, Pointer); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 7ddbf29dc..ffa5fe13b 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -8,7 +8,6 @@ import 'package:cw_monero/api/types.dart'; import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; int _boolToInt(bool value) => value ? 1 : 0; @@ -128,6 +127,10 @@ final trustedDaemonNative = moneroApi .lookup>('trusted_daemon') .asFunction(); +final signMessageNative = moneroApi + .lookup>('sign_message') + .asFunction(); + int getSyncingHeight() => getSyncingHeightNative(); bool isNeededToRefresh() => isNeededToRefreshNative() != 0; @@ -158,9 +161,11 @@ bool setupNodeSync( String? login, String? password, bool useSSL = false, - bool isLightWallet = false}) { + bool isLightWallet = false, + String? socksProxyAddress}) { final addressPointer = address.toNativeUtf8(); Pointer? loginPointer; + Pointer? socksProxyAddressPointer; Pointer? passwordPointer; if (login != null) { @@ -171,6 +176,10 @@ bool setupNodeSync( passwordPointer = password.toNativeUtf8(); } + if (socksProxyAddress != null) { + socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); + } + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, @@ -178,6 +187,7 @@ bool setupNodeSync( passwordPointer, _boolToInt(useSSL), _boolToInt(isLightWallet), + socksProxyAddressPointer, errorMessagePointer) != 0; @@ -289,7 +299,7 @@ class SyncListener { final bchHeight = await getNodeHeightOrUpdate(syncHeight); - if (_lastKnownBlockHeight == syncHeight || syncHeight == null) { + if (_lastKnownBlockHeight == syncHeight) { return; } @@ -304,7 +314,7 @@ class SyncListener { } // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; - onNewBlock?.call(syncHeight, left, ptc); + onNewBlock.call(syncHeight, left, ptc); }); } @@ -328,13 +338,15 @@ bool _setupNodeSync(Map args) { final password = (args['password'] ?? '') as String; final useSSL = args['useSSL'] as bool; final isLightWallet = args['isLightWallet'] as bool; + final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String; return setupNodeSync( address: address, login: login, password: password, useSSL: useSSL, - isLightWallet: isLightWallet); + isLightWallet: isLightWallet, + socksProxyAddress: socksProxyAddress); } bool _isConnected(Object _) => isConnectedSync(); @@ -348,13 +360,15 @@ Future setupNode( String? login, String? password, bool useSSL = false, + String? socksProxyAddress, bool isLightWallet = false}) => compute, void>(_setupNodeSync, { 'address': address, 'login': login , 'password': password, 'useSSL': useSSL, - 'isLightWallet': isLightWallet + 'isLightWallet': isLightWallet, + 'socksProxyAddress': socksProxyAddress }); Future store() => compute(_storeSync, 0); @@ -371,4 +385,15 @@ String getSubaddressLabel(int accountIndex, int addressIndex) { Future setTrustedDaemon(bool trusted) async => setTrustedDaemonNative(_boolToInt(trusted)); -Future trustedDaemon() async => trustedDaemonNative() != 0; \ No newline at end of file +Future trustedDaemon() async => trustedDaemonNative() != 0; + +String signMessage(String message, {String address = ""}) { + final messagePointer = message.toNativeUtf8(); + final addressPointer = address.toNativeUtf8(); + + final signature = convertUTF8ToString(pointer: signMessageNative(messagePointer, addressPointer)); + calloc.free(messagePointer); + calloc.free(addressPointer); + + return signature; +} diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 093d7e63b..0aa694e9a 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -1,15 +1,16 @@ import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; final createWalletNative = moneroApi .lookup>('create_wallet') @@ -25,6 +26,11 @@ final restoreWalletFromKeysNative = moneroApi 'restore_wallet_from_keys') .asFunction(); +final restoreWalletFromSpendKeyNative = moneroApi + .lookup>( + 'restore_wallet_from_spend_key') + .asFunction(); + final isWalletExistNative = moneroApi .lookup>('is_wallet_exist') .asFunction(); @@ -141,6 +147,44 @@ void restoreWalletFromKeysSync( } } +void restoreWalletFromSpendKeySync( + {required String path, + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) { + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final seedPointer = seed.toNativeUtf8(); + final languagePointer = language.toNativeUtf8(); + final spendKeyPointer = spendKey.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); + final isWalletRestored = restoreWalletFromSpendKeyNative( + pathPointer, + passwordPointer, + seedPointer, + languagePointer, + spendKeyPointer, + nettype, + restoreHeight, + errorMessagePointer) != + 0; + + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(languagePointer); + calloc.free(spendKeyPointer); + + storeSync(); + + if (!isWalletRestored) { + throw WalletRestoreFromKeysException( + message: convertUTF8ToString(pointer: errorMessagePointer)); + } +} + void loadWallet({ required String path, required String password, @@ -194,6 +238,23 @@ void _restoreFromKeys(Map args) { spendKey: spendKey); } +void _restoreFromSpendKey(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final seed = args['seed'] as String; + final language = args['language'] as String; + final spendKey = args['spendKey'] as String; + final restoreHeight = args['restoreHeight'] as int; + + restoreWalletFromSpendKeySync( + path: path, + password: password, + seed: seed, + language: language, + restoreHeight: restoreHeight, + spendKey: spendKey); +} + Future _openWallet(Map args) async => loadWallet(path: args['path'] as String, password: args['password'] as String); @@ -251,4 +312,22 @@ Future restoreFromKeys( 'restoreHeight': restoreHeight }); +Future restoreFromSpendKey( + {required String path, + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) async => + compute, void>(_restoreFromSpendKey, { + 'path': path, + 'password': password, + 'seed': seed, + 'language': language, + 'spendKey': spendKey, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + Future isWalletExist({required String path}) => compute(_isWalletExist, path); diff --git a/cw_monero/lib/monero_transaction_creation_exception.dart b/cw_monero/lib/exceptions/monero_transaction_creation_exception.dart similarity index 100% rename from cw_monero/lib/monero_transaction_creation_exception.dart rename to cw_monero/lib/exceptions/monero_transaction_creation_exception.dart diff --git a/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..453482e0a --- /dev/null +++ b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart @@ -0,0 +1,8 @@ +class MoneroTransactionNoInputsException implements Exception { + MoneroTransactionNoInputsException(this.inputsSize); + + int inputsSize; + + @override + String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control'; +} diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index 8d8eeb469..dbd1a89ae 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,19 +1,20 @@ -import 'package:cw_monero/api/structs/subaddress_row.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_core/subaddress.dart'; part 'monero_subaddress_list.g.dart'; -class MoneroSubaddressList = MoneroSubaddressListBase - with _$MoneroSubaddressList; +class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList; abstract class MoneroSubaddressListBase with Store { MoneroSubaddressListBase() - : _isRefreshing = false, - _isUpdating = false, - subaddresses = ObservableList(); + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList(); + + final List _usedAddresses = []; @observable ObservableList subaddresses; @@ -22,6 +23,8 @@ abstract class MoneroSubaddressListBase with Store { bool _isUpdating; void update({required int accountIndex}) { + refreshCoins(accountIndex); + if (_isUpdating) { return; } @@ -47,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store { subaddresses = [primary] + rest.toList(); } - return subaddresses - .map((subaddressRow) => Subaddress( + return subaddresses.map((subaddressRow) { + final hasDefaultAddressName = + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() || + subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName; + return Subaddress( id: subaddressRow.getId(), address: subaddressRow.getAddress(), - label: subaddressRow.getId() == 0 && - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() - ? 'Primary address' - : subaddressRow.getLabel())) - .toList(); + label: isPrimaryAddress + ? 'Primary address' + : hasDefaultAddressName + ? '' + : subaddressRow.getLabel()); + }).toList(); } Future addSubaddress({required int accountIndex, required String label}) async { - await subaddress_list.addSubaddress( - accountIndex: accountIndex, label: label); + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } @@ -86,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store { rethrow; } } + + Future updateWithAutoGenerate({ + required int accountIndex, + required String defaultLabel, + required List usedAddresses, + }) async { + _usedAddresses.addAll(usedAddresses); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + final newSubAddresses = + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel); + subaddresses.addAll(newSubAddresses); + } catch (e) { + rethrow; + } finally { + _isUpdating = false; + } + } + + Future> _getAllUnusedAddresses( + {required int accountIndex, required String label}) async { + final allAddresses = subaddress_list.getAllSubaddresses(); + + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) { + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); + if (!isAddressUnused) { + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); + } + } + + return allAddresses + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getId() == 0 && + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : subaddressRow.getLabel())) + .toList(); + } + + Future _newSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + + return subaddress_list + .getAllSubaddresses() + .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress())) + .isNotEmpty; + } } diff --git a/cw_monero/lib/monero_transaction_history.dart b/cw_monero/lib/monero_transaction_history.dart index 229357c1b..e23f70530 100644 --- a/cw_monero/lib/monero_transaction_history.dart +++ b/cw_monero/lib/monero_transaction_history.dart @@ -24,4 +24,5 @@ abstract class MoneroTransactionHistoryBase @override void addMany(Map transactions) => this.transactions.addAll(transactions); + } diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 90cc3c279..748b65329 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), isPending = parseBoolFromString(map['isPending'] as String), amount = map['amount'] as int, accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, confirmations = map['confirmations'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int ?? 0 { + fee = map['fee'] as int? ?? 0 { additionalInfo = { 'key': key, 'accountIndex': accountIndex, @@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, + direction = parseTransactionDirectionFromInt(row.direction), date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), isPending = row.isPending != 0, amount = row.getAmount(), diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart new file mode 100644 index 000000000..65b5c595d --- /dev/null +++ b/cw_monero/lib/monero_unspent.dart @@ -0,0 +1,20 @@ +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; + +class MoneroUnspent extends Unspent { + MoneroUnspent( + String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked) + : super(address, hash, value, 0, keyImage) { + this.isFrozen = isFrozen; + } + + factory MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => MoneroUnspent( + coinsInfoRow.getAddress(), + coinsInfoRow.getHash(), + coinsInfoRow.getKeyImage(), + coinsInfoRow.amount, + coinsInfoRow.frozen == 1, + coinsInfoRow.unlocked == 1); + + final bool isUnlocked; +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index eea490ba9..d00a54c8f 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,74 +1,94 @@ import 'dart:async'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_monero/monero_transaction_creation_exception.dart'; -import 'package:cw_monero/monero_transaction_info.dart'; -import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_monero/api/transaction_history.dart' - as monero_transaction_history; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/transaction_history.dart' as transaction_history; -import 'package:cw_monero/api/monero_output.dart'; -import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_monero/pending_monero_transaction.dart'; -import 'package:cw_core/monero_wallet_keys.dart'; -import 'package:cw_core/monero_balance.dart'; -import 'package:cw_monero/monero_transaction_history.dart'; +import 'dart:io'; + import 'package:cw_core/account.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_balance.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_wallet_utils.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/coins_info.dart'; +import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/transaction_history.dart' as transaction_history; +import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; +import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +import 'package:cw_monero/monero_transaction_creation_credentials.dart'; +import 'package:cw_monero/monero_transaction_history.dart'; +import 'package:cw_monero/monero_transaction_info.dart'; +import 'package:cw_monero/monero_unspent.dart'; +import 'package:cw_monero/monero_wallet_addresses.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; part 'monero_wallet.g.dart'; const moneroBlockSize = 1000; +// not sure if this should just be 0 but setting it higher feels safer / should catch more cases: +const MIN_RESTORE_HEIGHT = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({required WalletInfo walletInfo}) +abstract class MoneroWalletBase + extends WalletBase with Store { + MoneroWalletBase( + {required WalletInfo walletInfo, required Box unspentCoinsInfo}) : balance = ObservableMap.of({ - CryptoCurrency.xmr: MoneroBalance( + CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) - }), + }), _isTransactionUpdating = false, _hasSyncAfterStartup = false, - walletAddresses = MoneroWalletAddresses(walletInfo), + isEnabledAutoGenerateSubaddress = false, syncStatus = NotConnectedSyncStatus(), + unspentCoins = [], + this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { transactionHistory = MoneroTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account? account) { - if (account == null) { - return; - } + walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory); - balance = ObservableMap.of( - { - currency: MoneroBalance( + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { + if (account == null) return; + + balance = ObservableMap.of({ + currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)) - }); - walletAddresses.updateSubaddressList(accountIndex: account.id); + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); + _askForUpdateTransactionHistory(); + }); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + _updateSubAddress(enabled, account: walletAddresses.account); }); } static const int _autoSaveInterval = 30; + Box unspentCoinsInfo; + + void Function(FlutterErrorDetails)? onError; + @override - MoneroWalletAddresses walletAddresses; + late MoneroWalletAddresses walletAddresses; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; @override @observable @@ -88,20 +108,21 @@ abstract class MoneroWalletBase extends WalletBase unspentCoins; Future init() async { await walletAddresses.init(); - balance = ObservableMap.of( - { - currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), - unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) - }); + balance = ObservableMap.of({ + currency: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + }); _setListeners(); await updateTransactions(); @@ -109,15 +130,14 @@ abstract class MoneroWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } + @override Future? updateBalance() => null; @@ -137,7 +157,9 @@ abstract class MoneroWalletBase extends WalletBase startSync() async { try { _setInitialHeight(); - } catch (_) {} + } catch (_) { + // our restore height wasn't correct, so lets see if using the backup works: + try { + await resetCache(name); + _setInitialHeight(); + } catch (e) { + // we still couldn't get a valid height from the backup?!: + // try to use the date instead: + try { + _setHeightFromDate(); + } catch (_) { + // we still couldn't get a valid sync height :/ + } + } + } try { syncStatus = AttemptingSyncStatus(); @@ -168,10 +204,12 @@ abstract class MoneroWalletBase extends WalletBase createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; + final inputs = []; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -179,45 +217,52 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); if (unlockedBalance < totalAmount) { throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final moneroOutputs = outputs.map((output) { - final outputAddress = output.isParsedAddress - ? output.extractedAddress - : output.address; + if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) { + throw MoneroTransactionNoInputsException(inputs.length); + } - return MoneroOutput( - address: outputAddress!, - amount: output.cryptoAmount!.replaceAll(',', '.')); + final moneroOutputs = outputs.map((output) { + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; + + return MoneroOutput( + address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id); + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } else { final output = outputs.first; - final address = output.isParsedAddress - ? output.extractedAddress - : output.address; - final amount = output.sendAll - ? null - : output.cryptoAmount!.replaceAll(',', '.'); - final formattedAmount = output.sendAll - ? null - : output.formattedCryptoAmount; + final address = output.isParsedAddress ? output.extractedAddress : output.address; + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; if ((formattedAmount != null && unlockedBalance < formattedAmount) || (formattedAmount == null && unlockedBalance <= 0)) { @@ -227,12 +272,19 @@ abstract class MoneroWalletBase extends WalletBase save() async { + await walletAddresses.updateUsedSubaddress(); + + if (isEnabledAutoGenerateSubaddress) { + walletAddresses.updateUnusedSubaddress( + accountIndex: walletAddresses.account?.id ?? 0, + defaultLabel: walletAddresses.account?.label ?? ''); + } + await walletAddresses.updateAddressesInBox(); await backupWalletFiles(name); await monero_wallet.store(); } @override - Future changePassword(String password) async { - monero_wallet.setPasswordSync(password); + Future renameWalletFiles(String newWalletName) async { + final currentWalletDirPath = await pathForWalletDir(name: name, type: type); + + try { + // -- rename the waller folder -- + final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type)); + final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentWalletDir.rename(newWalletDirPath); + + // -- use new waller folder to rename files with old names still -- + final renamedWalletPath = newWalletDirPath + '/$name'; + + final currentCacheFile = File(renamedWalletPath); + final currentKeysFile = File('$renamedWalletPath.keys'); + final currentAddressListFile = File('$renamedWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + if (currentCacheFile.existsSync()) { + await currentCacheFile.rename(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.rename('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.rename('$newWalletPath.address.txt'); + } + + await backupWalletFiles(newWalletName); + } catch (e) { + final currentWalletPath = await pathForWallet(name: name, type: type); + + final currentCacheFile = File(currentWalletPath); + final currentKeysFile = File('$currentWalletPath.keys'); + final currentAddressListFile = File('$currentWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + // Copies current wallet files into new wallet name's dir and files + if (currentCacheFile.existsSync()) { + await currentCacheFile.copy(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.copy('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.copy('$newWalletPath.address.txt'); + } + + // Delete old name's dir and files + await Directory(currentWalletDirPath).delete(recursive: true); + } } + @override + Future changePassword(String password) async => monero_wallet.setPasswordSync(password); + Future getNodeHeight() async => monero_wallet.getNodeHeight(); Future isConnected() async => monero_wallet.isConnected(); @@ -295,17 +408,111 @@ abstract class MoneroWalletBase extends WalletBase updateUnspent() async { + try { + refreshCoins(walletAddresses.account!.id); + + unspentCoins.clear(); + + final coinCount = countOfCoins(); + for (var i = 0; i < coinCount; i++) { + final coin = getCoin(i); + if (coin.spent == 0) { + final unspent = MoneroUnspent.fromCoinsInfoRow(coin); + if (unspent.hash.isNotEmpty) { + unspent.isChange = transaction_history.getTransaction(unspent.hash).direction == 1; + } + unspentCoins.add(unspent); + } + } + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id && + element.keyImage!.contains(coin.keyImage!)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + _askForUpdateBalance(); + } catch (e, s) { + print(e.toString()); + onError?.call(FlutterErrorDetails( + exception: e, + stack: s, + library: this.runtimeType.toString(), + )); + } + } + + Future _addCoinInfo(MoneroUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.address, + value: coin.value, + vout: 0, + keyImage: coin.keyImage, + isChange: coin.isChange, + accountIndex: walletAddresses.account!.id); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } + } + String getTransactionAddress(int accountIndex, int addressIndex) => - monero_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, MoneroTransactionInfo tx) { + transaction_history.refreshTransactions(); + return _getAllTransactionsOfAccount(walletAddresses.account?.id) + .fold>({}, + (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -318,6 +525,7 @@ abstract class MoneroWalletBase extends WalletBase + monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactions(dynamic _) => - monero_transaction_history - .getAllTransations() - .map((row) => MoneroTransactionInfo.fromRow(row)) - .toList(); + List _getAllTransactionsOfAccount(int? accountIndex) => transaction_history + .getAllTransactions() + .map((row) => MoneroTransactionInfo.fromRow(row)) + .where((element) => element.accountIndex == (accountIndex ?? 0)) + .toList(); void _setListeners() { _listener?.stop(); _listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction); } + // check if the height is correct: void _setInitialHeight() { if (walletInfo.isRecovery) { return; } - final currentHeight = getCurrentHeight(); + final height = monero_wallet.getCurrentHeight(); - if (currentHeight <= 1) { - final height = _getHeightByDate(walletInfo.date); - monero_wallet.setRecoveringFromSeed(isRecovery: true); - monero_wallet.setRefreshFromBlockHeight(height: height); + if (height > MIN_RESTORE_HEIGHT) { + // the restore height is probably correct, so we do nothing: + return; } + + throw Exception("height isn't > $MIN_RESTORE_HEIGHT!"); + } + + void _setHeightFromDate() { + if (walletInfo.isRecovery) { + return; + } + + int height = 0; + try { + height = _getHeightByDate(walletInfo.date); + } catch (_) {} + + monero_wallet.setRecoveringFromSeed(isRecovery: true); + monero_wallet.setRefreshFromBlockHeight(height: height); } int _getHeightDistance(DateTime date) { - final distance = - DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; final daysTmp = (distance / 86400).round(); final days = daysTmp < 1 ? 1 : daysTmp; @@ -371,7 +593,8 @@ abstract class MoneroWalletBase extends WalletBase _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); - int _getFullBalance() => - monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + int _getFrozenBalance() { + var frozenBalance = 0; + + for (var coin in unspentCoinsInfo.values.where((element) => + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { + if (coin.isFrozen) frozenBalance += coin.value; + } + + return frozenBalance; + } + void _onNewBlock(int height, int blocksLeft, double ptc) async { try { if (walletInfo.isRecovery) { @@ -412,9 +646,9 @@ abstract class MoneroWalletBase extends WalletBase onError = e; + + @override + String signMessage(String message, {String? address}) { + final useAddress = address ?? ""; + return monero_wallet.signMessage(message, address: useAddress); + } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 2002e789a..f74e7dd5b 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -1,27 +1,32 @@ +import 'package:cw_core/account.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/account.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; -import 'package:cw_core/subaddress.dart'; +import 'package:cw_monero/monero_transaction_history.dart'; import 'package:mobx/mobx.dart'; part 'monero_wallet_addresses.g.dart'; -class MoneroWalletAddresses = MoneroWalletAddressesBase - with _$MoneroWalletAddresses; +class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses; abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { - MoneroWalletAddressesBase(WalletInfo walletInfo) - : accountList = MoneroAccountList(), - subaddressList = MoneroSubaddressList(), - address = '', - super(walletInfo); + MoneroWalletAddressesBase( + WalletInfo walletInfo, MoneroTransactionHistory moneroTransactionHistory) + : accountList = MoneroAccountList(), + _moneroTransactionHistory = moneroTransactionHistory, + subaddressList = MoneroSubaddressList(), + address = '', + super(walletInfo); + final MoneroTransactionHistory _moneroTransactionHistory; @override @observable String address; - + @observable Account? account; @@ -46,11 +51,15 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { final _subaddressList = MoneroSubaddressList(); addressesMap.clear(); + addressInfos.clear(); accountList.accounts.forEach((account) { _subaddressList.update(accountIndex: account.id); _subaddressList.subaddresses.forEach((subaddress) { addressesMap[subaddress.address] = subaddress.label; + addressInfos[account.id] ??= []; + addressInfos[account.id]?.add(AddressInfo( + address: subaddress.address, label: subaddress.label, accountIndex: account.id)); }); }); @@ -62,14 +71,14 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { bool validate() { accountList.update(); - final accountListLength = accountList.accounts.length ?? 0; + final accountListLength = accountList.accounts.length; if (accountListLength <= 0) { return false; } subaddressList.update(accountIndex: accountList.accounts.first.id); - final subaddressListLength = subaddressList.subaddresses.length ?? 0; + final subaddressListLength = subaddressList.subaddresses.length; if (subaddressListLength <= 0) { return false; @@ -83,4 +92,28 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { subaddress = subaddressList.subaddresses.first; address = subaddress!.address; } -} \ No newline at end of file + + Future updateUsedSubaddress() async { + final transactions = _moneroTransactionHistory.transactions.values.toList(); + + transactions.forEach((element) { + final accountIndex = element.accountIndex; + final addressIndex = element.addressIndex; + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); + }); + } + + Future updateUnusedSubaddress( + {required int accountIndex, required String defaultLabel}) async { + await subaddressList.updateWithAutoGenerate( + accountIndex: accountIndex, + defaultLabel: defaultLabel, + usedAddresses: usedAddresses.toList()); + subaddress = subaddressList.subaddresses.last; + address = subaddress!.address; + } + + @override + bool containsAddress(String address) => + addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; +} diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 095fe83bb..1f33dbb3d 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -1,22 +1,27 @@ import 'dart:io'; -import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_monero/monero_wallet.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; +import 'package:polyseed/polyseed.dart'; class MoneroNewWalletCredentials extends WalletCredentials { - MoneroNewWalletCredentials({required String name, required this.language, String? password}) + MoneroNewWalletCredentials( + {required String name, required this.language, required this.isPolyseed, String? password}) : super(name: name, password: password); final String language; + final bool isPolyseed; } class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -49,14 +54,13 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { final String spendKey; } -class MoneroWalletService extends WalletService< - MoneroNewWalletCredentials, - MoneroRestoreWalletFromSeedCredentials, - MoneroRestoreWalletFromKeysCredentials> { - MoneroWalletService(this.walletInfoSource); +class MoneroWalletService extends WalletService { + MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; - + final Box unspentCoinsInfoSource; + static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); @@ -64,14 +68,26 @@ class MoneroWalletService extends WalletService< WalletType getType() => WalletType.monero; @override - Future create(MoneroNewWalletCredentials credentials) async { + Future create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); + + if (credentials.isPolyseed) { + final polyseed = Polyseed.create(); + final lang = PolyseedLang.getByEnglishName(credentials.language); + + final heightOverride = + getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); + + return _restoreFromPolyseed( + path, credentials.password!, polyseed, credentials.walletInfo!, lang, + overrideHeight: heightOverride); + } + await monero_wallet_manager.createWallet( - path: path, - password: credentials.password!, - language: credentials.language); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + path: path, password: credentials.password!, language: credentials.language); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -96,6 +112,7 @@ class MoneroWalletService extends WalletService< @override Future openWallet(String name, String password) async { + MoneroWallet? wallet; try { final path = await pathForWallet(name: name, type: getType()); @@ -103,11 +120,10 @@ class MoneroWalletService extends WalletService< await repairOldAndroidWallet(name); } - await monero_wallet_manager - .openWalletAsync({'path': path, 'password': password}); - final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType())); - final wallet = MoneroWallet(walletInfo: walletInfo); + await monero_wallet_manager.openWalletAsync({'path': path, 'password': password}); + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -119,21 +135,39 @@ class MoneroWalletService extends WalletService< await wallet.init(); return wallet; - } catch (e) { + } catch (e, s) { // TODO: Implement Exception for wallet list service. - if ((e.toString().contains('bad_alloc') || + final bool isBadAlloc = e.toString().contains('bad_alloc') || (e is WalletOpeningException && - (e.message == 'std::bad_alloc' || - e.message.contains('bad_alloc')))) || - (e.toString().contains('does not correspond') || + (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); + + final bool doesNotCorrespond = e.toString().contains('does not correspond') || + (e is WalletOpeningException && e.message.contains('does not correspond')); + + final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || + (e is WalletOpeningException && e.message.contains('basic_string')); + + final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || + e.toString().contains('input stream error') || (e is WalletOpeningException && - e.message.contains('does not correspond')))) { - await restoreOrResetWalletFiles(name); - return openWallet(name, password); + (e.message.contains('input_stream') || e.message.contains('input stream error'))); + + final bool invalidSignature = e.toString().contains('invalid signature') || + (e is WalletOpeningException && e.message.contains('invalid signature')); + + if (!isBadAlloc && + !doesNotCorrespond && + !isMissingCacheFilesIOS && + !isMissingCacheFilesAndroid && + !invalidSignature && + wallet != null && + wallet.onError != null) { + wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); } - rethrow; + await restoreOrResetWalletFiles(name); + return openWallet(name, password); } } @@ -146,11 +180,31 @@ class MoneroWalletService extends WalletService< if (isExist) { await file.delete(recursive: true); } + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); } @override - Future restoreFromKeys( - MoneroRestoreWalletFromKeysCredentials credentials) async { + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = + MoneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromKeys( @@ -161,7 +215,8 @@ class MoneroWalletService extends WalletService< address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -173,8 +228,13 @@ class MoneroWalletService extends WalletService< } @override - Future restoreFromSeed( - MoneroRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + // Restore from Polyseed + if (Polyseed.isValidSeed(credentials.mnemonic)) { + return restoreFromPolyseed(credentials); + } + try { final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromSeed( @@ -182,7 +242,8 @@ class MoneroWalletService extends WalletService< password: credentials.password!, seed: credentials.mnemonic, restoreHeight: credentials.height!); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -193,22 +254,62 @@ class MoneroWalletService extends WalletService< } } + Future restoreFromPolyseed( + MoneroRestoreWalletFromSeedCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + final polyseedCoin = PolyseedCoin.POLYSEED_MONERO; + final lang = PolyseedLang.getByPhrase(credentials.mnemonic); + final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); + + return _restoreFromPolyseed( + path, credentials.password!, polyseed, credentials.walletInfo!, lang); + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('MoneroWalletsManager Error: $e'); + rethrow; + } + } + + Future _restoreFromPolyseed( + String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, + {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async { + final height = overrideHeight ?? + getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); + final spendKey = polyseed.generateKey(coin, 32).toHexString(); + final seed = polyseed.encode(lang, coin); + + walletInfo.isRecovery = true; + walletInfo.restoreHeight = height; + + await monero_wallet_manager.restoreFromSpendKey( + path: path, + password: password, + seed: seed, + language: lang.nameEnglish, + restoreHeight: height, + spendKey: spendKey); + + final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + + return wallet; + } + Future repairOldAndroidWallet(String name) async { try { if (!Platform.isAndroid) { return; } - final oldAndroidWalletDirPath = - await outdatedAndroidPathForWalletDir(name: name); + final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name); final dir = Directory(oldAndroidWalletDirPath); if (!dir.existsSync()) { return; } - final newWalletDirPath = - await pathForWalletDir(name: name, type: getType()); + final newWalletDirPath = await pathForWalletDir(name: name, type: getType()); dir.listSync().forEach((f) { final file = File(f.path); diff --git a/cw_monero/macos/Classes/monero_api.cpp b/cw_monero/macos/Classes/monero_api.cpp index 56548e79e..fe75dea98 100644 --- a/cw_monero/macos/Classes/monero_api.cpp +++ b/cw_monero/macos/Classes/monero_api.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include "thread" #include "CwWalletListener.h" #if __APPLE__ @@ -137,7 +139,7 @@ extern "C" int8_t direction; int8_t isPending; uint32_t subaddrIndex; - + char *hash; char *paymentId; @@ -152,7 +154,7 @@ extern "C" std::set::iterator it = transaction->subaddrIndex().begin(); subaddrIndex = *it; confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); + datetime = static_cast(transaction->timestamp()); direction = transaction->direction(); isPending = static_cast(transaction->isPending()); std::string *hash_str = new std::string(transaction->hash()); @@ -181,6 +183,61 @@ extern "C" } }; + struct CoinsInfoRow + { + uint64_t blockHeight; + char *hash; + uint64_t internalOutputIndex; + uint64_t globalOutputIndex; + bool spent; + bool frozen; + uint64_t spentHeight; + uint64_t amount; + bool rct; + bool keyImageKnown; + uint64_t pkIndex; + uint32_t subaddrIndex; + uint32_t subaddrAccount; + char *address; + char *addressLabel; + char *keyImage; + uint64_t unlockTime; + bool unlocked; + char *pubKey; + bool coinbase; + char *description; + + CoinsInfoRow(Monero::CoinsInfo *coinsInfo) + { + blockHeight = coinsInfo->blockHeight(); + std::string *hash_str = new std::string(coinsInfo->hash()); + hash = strdup(hash_str->c_str()); + internalOutputIndex = coinsInfo->internalOutputIndex(); + globalOutputIndex = coinsInfo->globalOutputIndex(); + spent = coinsInfo->spent(); + frozen = coinsInfo->frozen(); + spentHeight = coinsInfo->spentHeight(); + amount = coinsInfo->amount(); + rct = coinsInfo->rct(); + keyImageKnown = coinsInfo->keyImageKnown(); + pkIndex = coinsInfo->pkIndex(); + subaddrIndex = coinsInfo->subaddrIndex(); + subaddrAccount = coinsInfo->subaddrAccount(); + address = strdup(coinsInfo->address().c_str()) ; + addressLabel = strdup(coinsInfo->addressLabel().c_str()); + keyImage = strdup(coinsInfo->keyImage().c_str()); + unlockTime = coinsInfo->unlockTime(); + unlocked = coinsInfo->unlocked(); + pubKey = strdup(coinsInfo->pubKey().c_str()); + coinbase = coinsInfo->coinbase(); + description = strdup(coinsInfo->description().c_str()); + } + + void setUnlocked(bool unlocked); + }; + + Monero::Coins *m_coins; + Monero::Wallet *m_wallet; Monero::TransactionHistory *m_transaction_history; MoneroWalletListener *m_listener; @@ -188,6 +245,7 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; + std::list m_coins_info; std::mutex store_lock; bool is_storing = false; @@ -195,7 +253,7 @@ extern "C" { m_wallet = wallet; m_listener = nullptr; - + if (wallet != nullptr) { @@ -223,6 +281,17 @@ extern "C" { m_subaddress = nullptr; } + + m_coins_info = std::list(); + + if (wallet != nullptr) + { + m_coins = wallet->coins(); + } + else + { + m_coins = nullptr; + } } Monero::Wallet *get_current_wallet() @@ -305,6 +374,35 @@ extern "C" return true; } + bool restore_wallet_from_spend_key(char *path, char *password, char *seed, char *language, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createDeterministicWalletFromSpendKey( + std::string(path), + std::string(password), + std::string(language), + _networkType, + (uint64_t)restoreHeight, + std::string(spendKey)); + + // Cache Raw to support Polyseed + wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + bool load_wallet(char *path, char *password, int32_t nettype) { nice(19); @@ -369,6 +467,11 @@ extern "C" const char *seed() { + std::string _rawSeed = get_current_wallet()->getCacheAttribute("cakewallet.seed"); + if (!_rawSeed.empty()) + { + return strdup(_rawSeed.c_str()); + } return strdup(get_current_wallet()->seed().c_str()); } @@ -405,13 +508,14 @@ extern "C" return is_connected; } - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error) { nice(19); Monero::Wallet *wallet = get_current_wallet(); - + std::string _login = ""; std::string _password = ""; + std::string _socksProxyAddress = ""; if (login != nullptr) { @@ -423,7 +527,12 @@ extern "C" _password = std::string(password); } - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + if (socksProxyAddress != nullptr) + { + _socksProxyAddress = std::string(socksProxyAddress); + } + + bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress); if (!inited) { @@ -480,10 +589,19 @@ extern "C" } bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); - + + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -496,13 +614,13 @@ extern "C" if (amount != nullptr) { uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } else { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } - + int status = transaction->status(); if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) @@ -520,7 +638,9 @@ extern "C" } bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); @@ -534,6 +654,13 @@ extern "C" amounts++; } + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -748,6 +875,12 @@ extern "C" return m_transaction_history->count(); } + TransactionInfoRow* get_transaction(char * txId) + { + Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); + return new TransactionInfoRow(row); + } + int LedgerExchange( unsigned char *command, unsigned int cmd_len, @@ -793,6 +926,107 @@ extern "C" return m_wallet->trustedDaemon(); } + CoinsInfoRow* coin(int index) + { + if (index >= 0 && index < m_coins_info.size()) { + std::list::iterator it = m_coins_info.begin(); + std::advance(it, index); + Monero::CoinsInfo* element = *it; + std::cout << "Element at index " << index << ": " << element << std::endl; + return new CoinsInfoRow(element); + } else { + std::cout << "Invalid index." << std::endl; + return nullptr; // Return a default value (nullptr) for invalid index + } + } + + void refresh_coins(uint32_t accountIndex) + { + m_coins_info.clear(); + + m_coins->refresh(); + for (const auto i : m_coins->getAll()) { + if (i->subaddrAccount() == accountIndex && !(i->spent())) { + m_coins_info.push_back(i); + } + } + } + + uint64_t coins_count() + { + return m_coins_info.size(); + } + + CoinsInfoRow** coins_from_account(uint32_t accountIndex) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (coinInfo->subaddrAccount == accountIndex) { + matchingCoins.push_back(coinInfo); + } + } + + CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (std::string(coinInfo->hash) == txid) { + matchingCoins.push_back(coinInfo); + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinsInfoRow = coin(i); + for (size_t j = 0; j < keyimageCount; j++) { + if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { + matchingCoins.push_back(coinsInfoRow); + break; + } + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + void freeze_coin(int index) + { + m_coins->setFrozen(index); + } + + void thaw_coin(int index) + { + m_coins->thaw(index); + } + + // Sign Messages // + + char *sign_message(char *message, char *address = "") + { + return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str()); + } + #ifdef __cplusplus } #endif diff --git a/cw_monero/macos/Classes/monero_api.h b/cw_monero/macos/Classes/monero_api.h index 74258ba4c..fa92a038d 100644 --- a/cw_monero/macos/Classes/monero_api.h +++ b/cw_monero/macos/Classes/monero_api.h @@ -32,7 +32,8 @@ void store(char *path); void set_trusted_daemon(bool arg); bool trusted_daemon(); +char *sign_message(char *message, char *address); #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 1e33631d5..b736f80cb 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" build_resolvers: dependency: "direct dev" description: @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.2" convert: dependency: transitive description: @@ -266,6 +266,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + hashlib: + dependency: transitive + description: + name: hashlib + sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + hashlib_codecs: + dependency: transitive + description: + name: hashlib_codecs + sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626" + url: "https://pub.dev" + source: hosted + version: "2.2.0" hive: dependency: transitive description: @@ -286,10 +302,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -310,10 +326,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -326,10 +342,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -350,26 +366,26 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -406,58 +422,58 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -478,10 +494,18 @@ packages: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" + polyseed: + dependency: "direct main" + description: + name: polyseed + sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f" + url: "https://pub.dev" + source: hosted + version: "0.0.2" pool: dependency: transitive description: @@ -535,6 +559,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -555,10 +587,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -603,10 +635,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.6.0" timing: dependency: transitive description: @@ -632,13 +664,21 @@ packages: source: hosted version: "2.1.4" watcher: - dependency: transitive + dependency: "direct overridden" description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -672,5 +712,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 066a0d4c3..a6fe7f967 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -13,12 +13,13 @@ dependencies: flutter: sdk: flutter ffi: ^2.0.1 - http: ^0.13.4 + http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 encrypt: ^5.0.1 + polyseed: ^0.0.2 cw_core: path: ../cw_core diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart new file mode 100644 index 000000000..b904a35cb --- /dev/null +++ b/cw_nano/lib/banano_balance.dart @@ -0,0 +1,19 @@ +import 'package:cw_core/balance.dart'; +import 'package:nanoutil/nanoutil.dart'; + +class BananoBalance extends Balance { + final BigInt currentBalance; + final BigInt receivableBalance; + + BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); + + @override + String get formattedAvailableBalance { + return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano); + } + + @override + String get formattedAdditionalBalance { + return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano); + } +} diff --git a/cw_nano/lib/cw_nano.dart b/cw_nano/lib/cw_nano.dart new file mode 100644 index 000000000..08e23a232 --- /dev/null +++ b/cw_nano/lib/cw_nano.dart @@ -0,0 +1,7 @@ +library cw_nano; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_nano/lib/file.dart b/cw_nano/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_nano/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_nano/lib/nano_account_list.dart b/cw_nano/lib/nano_account_list.dart new file mode 100644 index 000000000..7207eafe1 --- /dev/null +++ b/cw_nano/lib/nano_account_list.dart @@ -0,0 +1,69 @@ +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; + +part 'nano_account_list.g.dart'; + +class NanoAccountList = NanoAccountListBase with _$NanoAccountList; + +abstract class NanoAccountListBase with Store { + NanoAccountListBase(this.address) + : accounts = ObservableList(), + _isRefreshing = false, + _isUpdating = false { + refresh(); + } + + @observable + ObservableList accounts; + bool _isRefreshing; + bool _isUpdating; + + String address; + + Future update(String? address) async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + + final accounts = await getAll(address: address ?? this.address); + + if (accounts.isNotEmpty) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + Future> getAll({String? address}) async { + final box = await CakeHive.openBox(address ?? this.address); + + // get all accounts in box: + return box.values.toList(); + } + + Future addAccount({required String label}) async { + final box = await CakeHive.openBox(address); + final account = NanoAccount(id: box.length, label: label, balance: "0.00", isSelected: false); + await box.add(account); + await account.save(); + } + + Future setLabelAccount({required int accountIndex, required String label}) async { + final box = await CakeHive.openBox(address); + final account = box.getAt(accountIndex); + account!.label = label; + await account.save(); + } + + void refresh() {} +} diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart new file mode 100644 index 000000000..8b8c93b33 --- /dev/null +++ b/cw_nano/lib/nano_balance.dart @@ -0,0 +1,35 @@ +import 'package:cw_core/balance.dart'; +import 'package:nanoutil/nanoutil.dart'; + +BigInt stringAmountToBigInt(String amount) { + return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano)); +} + +class NanoBalance extends Balance { + final BigInt currentBalance; + final BigInt receivableBalance; + + NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); + + NanoBalance.fromFormattedString( + {required String formattedCurrentBalance, required String formattedReceivableBalance}) + : currentBalance = stringAmountToBigInt(formattedCurrentBalance), + receivableBalance = stringAmountToBigInt(formattedReceivableBalance), + super(0, 0); + + NanoBalance.fromRawString( + {required String currentBalance, required String receivableBalance}) + : currentBalance = BigInt.parse(currentBalance), + receivableBalance = BigInt.parse(receivableBalance), + super(0, 0); + + @override + String get formattedAvailableBalance { + return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano); + } + + @override + String get formattedAdditionalBalance { + return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano); + } +} diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart new file mode 100644 index 000000000..064a0bdee --- /dev/null +++ b/cw_nano/lib/nano_client.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cw_core/n2_node.dart'; +import 'package:cw_nano/nano_balance.dart'; +import 'package:cw_nano/nano_transaction_model.dart'; +import 'package:http/http.dart' as http; +import 'package:nanodart/nanodart.dart'; +import 'package:cw_core/node.dart'; +import 'package:nanoutil/nanoutil.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class NanoClient { + static const Map CAKE_HEADERS = { + "Content-Type": "application/json", + "nano-app": "cake-wallet" + }; + + static const String N2_REPS_ENDPOINT = "https://rpc.nano.to"; + + NanoClient() { + SharedPreferences.getInstance().then((value) => prefs = value); + } + + late SharedPreferences prefs; + Node? _node; + Node? _powNode; + static const String _defaultDefaultRepresentative = + "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; + + String getRepFromPrefs() { + // from preferences_key.dart "defaultNanoRep" key: + return prefs.getString("default_nano_representative") ?? _defaultDefaultRepresentative; + } + + bool connect(Node node) { + try { + _node = node; + return true; + } catch (e) { + return false; + } + } + + bool connectPow(Node node) { + try { + _powNode = node; + return true; + } catch (e) { + return false; + } + } + + Future getBalance(String address) async { + final response = await http.post( + _node!.uri, + headers: CAKE_HEADERS, + body: jsonEncode( + { + "action": "account_balance", + "account": address, + }, + ), + ); + final data = await jsonDecode(response.body); + if (response.statusCode != 200 || + data["error"] != null || + data["balance"] == null || + data["receivable"] == null) { + throw Exception( + "Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}"); + } + final String currentBalance = data["balance"] as String; + final String receivableBalance = data["receivable"] as String; + final BigInt cur = BigInt.parse(currentBalance); + final BigInt rec = BigInt.parse(receivableBalance); + return NanoBalance(currentBalance: cur, receivableBalance: rec); + } + + Future getAccountInfo(String address) async { + try { + final response = await http.post( + _node!.uri, + headers: CAKE_HEADERS, + body: jsonEncode( + { + "action": "account_info", + "representative": "true", + "account": address, + }, + ), + ); + final data = await jsonDecode(response.body); + return AccountInfoResponse.fromJson(data as Map); + } catch (e) { + print("error while getting account info"); + return null; + } + } + + Future changeRep({ + required String privateKey, + required String repAddress, + required String ourAddress, + }) async { + AccountInfoResponse? accountInfo = await getAccountInfo(ourAddress); + + if (accountInfo == null) { + throw Exception( + "error while getting account info, you can't change the rep of an unopened account"); + } + + // construct the change block: + Map changeBlock = { + "type": "state", + "account": ourAddress, + "previous": accountInfo.frontier, + "representative": repAddress, + "balance": accountInfo.balance, + "link": "0000000000000000000000000000000000000000000000000000000000000000", + "link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp", + }; + + // sign the change block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + changeBlock["account"]!, + changeBlock["previous"]!, + changeBlock["representative"]!, + BigInt.parse(changeBlock["balance"]!), + changeBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(accountInfo.frontier); + + changeBlock["signature"] = signature; + changeBlock["work"] = work; + + try { + return await processBlock(changeBlock, "change"); + } catch (e) { + throw Exception("error while changing representative: $e"); + } + } + + Future requestWork(String hash) async { + final response = await http.post( + _powNode!.uri, + headers: CAKE_HEADERS, + body: json.encode( + { + "action": "work_generate", + "hash": hash, + }, + ), + ); + if (response.statusCode == 200) { + final Map decoded = json.decode(response.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String; + } else { + throw Exception("Received work error ${response.body}"); + } + } + + Future send({ + required String privateKey, + required String amountRaw, + required String destinationAddress, + }) async { + final Map sendBlock = await constructSendBlock( + privateKey: privateKey, + amountRaw: amountRaw, + destinationAddress: destinationAddress, + ); + + return await processBlock(sendBlock, "send"); + } + + Future processBlock(Map block, String subtype) async { + final processBody = jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": subtype, + "block": block, + }); + + final processResponse = await http.post( + _node!.uri, + headers: CAKE_HEADERS, + body: processBody, + ); + + final Map decoded = json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + + // return the hash of the transaction: + return decoded["hash"].toString(); + } + + Future> constructSendBlock({ + required String privateKey, + required String amountRaw, + required String destinationAddress, + BigInt? balanceAfterTx, + String? previousHash, + }) async { + // our address: + final String publicAddress = NanoDerivations.privateKeyToAddress(privateKey); + + // first get the current account balance: + if (balanceAfterTx == null) { + final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; + final BigInt txAmount = BigInt.parse(amountRaw); + balanceAfterTx = currentBalance - txAmount; + } + + // get the account info (we need the frontier and representative): + AccountInfoResponse? infoResponse = await getAccountInfo(publicAddress); + if (infoResponse == null) { + throw Exception( + "error while getting account info! (we probably don't have an open account yet)"); + } + + String frontier = infoResponse.frontier; + // override if provided: + if (previousHash != null) { + frontier = previousHash; + } + final String representative = infoResponse.representative; + // link = destination address: + final String link = NanoAccounts.extractPublicKey(destinationAddress); + final String linkAsAccount = destinationAddress; + + // construct the send block: + Map sendBlock = { + "type": "state", + "account": publicAddress, + "previous": frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + }; + + // sign the send block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + sendBlock["account"]!, + sendBlock["previous"]!, + sendBlock["representative"]!, + BigInt.parse(sendBlock["balance"]!), + sendBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(frontier); + + sendBlock["link_as_account"] = linkAsAccount; + sendBlock["signature"] = signature; + sendBlock["work"] = work; + + // ready to post send block: + return sendBlock; + } + + Future receiveBlock({ + required String blockHash, + required String source, + required String amountRaw, + required String destinationAddress, + required String privateKey, + }) async { + bool openBlock = false; + + // first check if the account is open: + // get the account info (we need the frontier and representative): + AccountInfoResponse? infoData = await getAccountInfo(destinationAddress); + String? frontier; + String? representative; + + if (infoData == null) { + // account is not open yet, we need to create an open block: + openBlock = true; + // we don't have a representative set yet: + representative = await getRepFromPrefs(); + // we don't have a frontier yet: + frontier = "0000000000000000000000000000000000000000000000000000000000000000"; + } else { + frontier = infoData.frontier; + representative = infoData.representative; + } + + // first get the account balance: + final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance; + final BigInt txAmount = BigInt.parse(amountRaw); + final BigInt balanceAfterTx = currentBalance + txAmount; + + // link = send block hash: + final String link = blockHash; + // this "linkAsAccount" is meaningless: + final String linkAsAccount = NanoAccounts.createAccount(NanoAccountType.NANO, blockHash); + + // construct the receive block: + Map receiveBlock = { + "type": "state", + "account": destinationAddress, + "previous": frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + "link_as_account": linkAsAccount, + }; + + // sign the receive block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + receiveBlock["account"]!, + receiveBlock["previous"]!, + receiveBlock["representative"]!, + BigInt.parse(receiveBlock["balance"]!), + receiveBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the receive block: + String? work; + if (openBlock) { + work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress)); + } else { + work = await requestWork(frontier); + } + receiveBlock["link_as_account"] = linkAsAccount; + receiveBlock["signature"] = signature; + receiveBlock["work"] = work; + + // process the receive block: + + final processBody = jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": "receive", + "block": receiveBlock, + }); + final processResponse = await http.post( + _node!.uri, + headers: CAKE_HEADERS, + body: processBody, + ); + + final Map decoded = json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + } + + // returns the number of blocks received: + Future confirmAllReceivable({ + required String destinationAddress, + required String privateKey, + }) async { + final receivableResponse = await http.post(_node!.uri, + headers: CAKE_HEADERS, + body: jsonEncode({ + "action": "receivable", + "account": destinationAddress, + "count": "-1", + "source": true, + })); + + final receivableData = await jsonDecode(receivableResponse.body); + if (receivableData["blocks"] == "" || receivableData["blocks"] == null) { + return 0; + } + + dynamic blocks; + if (receivableData["blocks"] is List) { + var listBlocks = receivableData["blocks"] as List; + if (listBlocks.isEmpty) { + return 0; + } + blocks = {for (var block in listBlocks) block['hash']: block}; + } else { + blocks = receivableData["blocks"] as Map; + } + + blocks = blocks as Map; + + // confirm all receivable blocks: + for (final blockHash in blocks.keys) { + final block = blocks[blockHash]; + final String amountRaw = block["amount"] as String; + final String source = block["source"] as String; + await receiveBlock( + blockHash: blockHash, + source: source, + amountRaw: amountRaw, + privateKey: privateKey, + destinationAddress: destinationAddress, + ); + // a bit of a hack: + await Future.delayed(const Duration(seconds: 2)); + } + + return blocks.keys.length; + } + + void stop() {} + + Future> fetchTransactions(String address) async { + try { + final response = await http.post(_node!.uri, + headers: CAKE_HEADERS, + body: jsonEncode({ + "action": "account_history", + "account": address, + "count": "100", + // "raw": true, + })); + final data = await jsonDecode(response.body); + final transactions = data["history"] is List ? data["history"] as List : []; + + // Map the transactions list to NanoTransactionModel using the factory + // reversed so that the DateTime is correct when local_timestamp is absent + return transactions.reversed + .map((transaction) => NanoTransactionModel.fromJson(transaction)) + .toList(); + } catch (e) { + print(e); + return []; + } + } + + Future> getN2Reps() async { + final response = await http.post( + Uri.parse(N2_REPS_ENDPOINT), + headers: CAKE_HEADERS, + body: jsonEncode({"action": "reps"}), + ); + try { + final List nodes = (json.decode(response.body) as List) + .map((dynamic e) => N2Node.fromJson(e as Map)) + .toList(); + return nodes; + } catch (error) { + return []; + } + } + + Future getRepScore(String rep) async { + final response = await http.post( + Uri.parse(N2_REPS_ENDPOINT), + headers: CAKE_HEADERS, + body: jsonEncode({ + "action": "rep_info", + "account": rep, + }), + ); + try { + final N2Node node = N2Node.fromJson(json.decode(response.body) as Map); + return node.score ?? 100; + } catch (error) { + return 100; + } + } +} diff --git a/cw_nano/lib/nano_mnemonic.dart b/cw_nano/lib/nano_mnemonic.dart new file mode 100644 index 000000000..2a06fe515 --- /dev/null +++ b/cw_nano/lib/nano_mnemonic.dart @@ -0,0 +1,2088 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:nanodart/nanodart.dart'; + +class NanoMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Nano mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class NanoMnemomics { + /// Converts a nano seed to a 24-word mnemonic word list + static List seedToMnemonic(String seed) { + if (!NanoSeeds.isValidSeed(seed)) { + throw Exception('Invalid Seed'); + } + String words = bip39.entropyToMnemonic(seed); + return words.split(' '); + } + + /// Convert a 24-word mnemonic word list to a nano seed + static String mnemonicListToSeed(List words) { + if (words.length != 24) { + throw Exception('Expected a 24-word list, got a ${words.length} list'); + } + return bip39.mnemonicToEntropy(words.join(' ')).toUpperCase(); + } + + /// Validate a mnemonic word list + static bool validateMnemonic(List words) { + return bip39.validateMnemonic(words.join(' ')); + } + + /// Validate a specific menmonic word + static bool isValidWord(String word) { + return WORDLIST.contains(word); + } + + static const WORDLIST = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo" + ]; +} diff --git a/cw_nano/lib/nano_transaction_credentials.dart b/cw_nano/lib/nano_transaction_credentials.dart new file mode 100644 index 000000000..6ede488a1 --- /dev/null +++ b/cw_nano/lib/nano_transaction_credentials.dart @@ -0,0 +1,7 @@ +import 'package:cw_core/output_info.dart'; + +class NanoTransactionCredentials { + NanoTransactionCredentials(this.outputs); + + final List outputs; +} diff --git a/cw_nano/lib/nano_transaction_history.dart b/cw_nano/lib/nano_transaction_history.dart new file mode 100644 index 000000000..dadd353c4 --- /dev/null +++ b/cw_nano/lib/nano_transaction_history.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_nano/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_nano/nano_transaction_info.dart'; + +part 'nano_transaction_history.g.dart'; +const transactionsHistoryFileName = 'transactions.json'; + +class NanoTransactionHistory = NanoTransactionHistoryBase with _$NanoTransactionHistory; + +abstract class NanoTransactionHistoryBase + extends TransactionHistoryBase with Store { + NanoTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e) { + print('Error while save nano transaction history: ${e.toString()}'); + } + } + + @override + void addOne(NanoTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = NanoTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(NanoTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_nano/lib/nano_transaction_info.dart b/cw_nano/lib/nano_transaction_info.dart new file mode 100644 index 000000000..9195a06ef --- /dev/null +++ b/cw_nano/lib/nano_transaction_info.dart @@ -0,0 +1,79 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:nanoutil/nanoutil.dart'; + +class NanoTransactionInfo extends TransactionInfo { + NanoTransactionInfo({ + required this.id, + required this.height, + required this.amountRaw, + this.tokenSymbol = "XNO", + required this.direction, + required this.confirmed, + required this.date, + required this.confirmations, + required this.to, + required this.from, + }) : this.amount = amountRaw.toInt(); + + final String id; + final int height; + final int amount; + final BigInt amountRaw; + final TransactionDirection direction; + final DateTime date; + final bool confirmed; + final int confirmations; + final String tokenSymbol; + final String? to; + final String? from; + String? _fiatAmount; + + bool get isPending => !this.confirmed; + + @override + String amountFormatted() { + final String amt = + NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano); + final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano); + return "$acc$amt $tokenSymbol"; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => "0 XNO"; + + factory NanoTransactionInfo.fromJson(Map data) { + return NanoTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + amountRaw: BigInt.parse(data['amountRaw'] as String), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + confirmed: data['confirmed'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'] as String, + from: data['from'] as String, + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amountRaw': amountRaw.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'confirmed': confirmed, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + }; +} diff --git a/cw_nano/lib/nano_transaction_model.dart b/cw_nano/lib/nano_transaction_model.dart new file mode 100644 index 000000000..e9c59da5a --- /dev/null +++ b/cw_nano/lib/nano_transaction_model.dart @@ -0,0 +1,39 @@ +class NanoTransactionModel { + final DateTime? date; + final String hash; + final bool confirmed; + final String account; + final BigInt amount; + final int height; + final String type; + + NanoTransactionModel({ + this.date, + required this.hash, + required this.height, + required this.amount, + required this.confirmed, + required this.type, + required this.account, + }); + + factory NanoTransactionModel.fromJson(dynamic json) { + DateTime? localTimestamp; + try { + localTimestamp = DateTime.fromMillisecondsSinceEpoch( + int.parse(json["local_timestamp"] as String) * 1000); + } catch (e) { + localTimestamp = DateTime.now(); + } + + return NanoTransactionModel( + date: localTimestamp, + hash: json["hash"] as String, + height: int.parse(json["height"] as String), + type: json["type"] as String, + amount: BigInt.parse(json["amount"] as String), + account: json["account"] as String, + confirmed: (json["confirmed"] as String) == "true", + ); + } +} diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart new file mode 100644 index 000000000..5efe3006d --- /dev/null +++ b/cw_nano/lib/nano_wallet.dart @@ -0,0 +1,503 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_nano/file.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/n2_node.dart'; +import 'package:cw_nano/nano_balance.dart'; +import 'package:cw_nano/nano_client.dart'; +import 'package:cw_nano/nano_transaction_credentials.dart'; +import 'package:cw_nano/nano_transaction_history.dart'; +import 'package:cw_nano/nano_transaction_info.dart'; +import 'package:cw_nano/nano_wallet_keys.dart'; +import 'package:cw_nano/pending_nano_transaction.dart'; +import 'package:mobx/mobx.dart'; +import 'dart:async'; +import 'package:cw_nano/nano_wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:nanodart/nanodart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:nanoutil/nanoutil.dart'; + +part 'nano_wallet.g.dart'; + +class NanoWallet = NanoWalletBase with _$NanoWallet; + +abstract class NanoWalletBase + extends WalletBase with Store { + NanoWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + NanoBalance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _derivationType = walletInfo.derivationInfo!.derivationType!, + _isTransactionUpdating = false, + _client = NanoClient(), + walletAddresses = NanoWalletAddresses(walletInfo), + balance = ObservableMap.of({ + CryptoCurrency.nano: initialBalance ?? + NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero) + }), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = NanoTransactionHistory(walletInfo: walletInfo, password: password); + if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) { + CakeHive.registerAdapter(NanoAccountAdapter()); + } + } + + String _mnemonic; + final String _password; + DerivationType _derivationType; + + String? _privateKey; + String? _publicAddress; + String? _hexSeed; + Timer? _receiveTimer; + + String? _representativeAddress; + int repScore = 100; + bool get isRepOk => repScore >= 90; + + late final NanoClient _client; + bool _isTransactionUpdating; + + @override + NanoWalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + static const int POLL_INTERVAL_SECONDS = 10; + + // initialize the different forms of private / public key we'll need: + Future init() async { + if (_derivationType == DerivationType.unknown) { + _derivationType = DerivationType.nano; + } + final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; + + // our "mnemonic" is actually a hex form seed: + if (!_mnemonic.contains(' ')) { + _hexSeed = _mnemonic; + _mnemonic = ""; + } + + if (_hexSeed == null) { + if (_derivationType == DerivationType.nano) { + _hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase(); + } else { + _hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' ')); + } + } + NanoDerivationType derivationType = + type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD; + _privateKey = await NanoDerivations.universalSeedToPrivate( + _hexSeed!, + index: 0, + type: derivationType, + ); + _publicAddress = await NanoDerivations.universalSeedToAddress( + _hexSeed!, + index: 0, + type: derivationType, + ); + this.walletInfo.address = _publicAddress!; + + await walletAddresses.init(); + await transactionHistory.init(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + return 0; // always 0 :) + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _receiveTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + final isConnected = _client.connect(node); + if (!isConnected) { + throw Exception("Nano Node connection failed"); + } + + try { + await _updateBalance(); + await updateTransactions(); + await _updateRep(); + await _receiveAll(); + } catch (e) { + print(e); + } + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future connectToPowNode({required Node node}) async { + _client.connectPow(node); + } + + @override + Future createTransaction(Object credentials) async { + credentials = credentials as NanoTransactionCredentials; + + BigInt runningAmount = BigInt.zero; + await _updateBalance(); + BigInt runningBalance = balance[currency]?.currentBalance ?? BigInt.zero; + + final List> blocks = []; + String? previousHash; + + for (var txOut in credentials.outputs) { + late BigInt amt; + if (txOut.sendAll) { + amt = balance[currency]?.currentBalance ?? BigInt.zero; + } else { + amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw( + txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ?? + BigInt.zero; + } + + if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance) { + throw Exception("Trying to send more than entire balance!"); + } + + runningBalance = runningBalance - amt; + + final block = await _client.constructSendBlock( + amountRaw: amt.toString(), + destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address, + privateKey: _privateKey!, + balanceAfterTx: runningBalance, + previousHash: previousHash, + ); + previousHash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + block["account"]!, + block["previous"]!, + block["representative"]!, + BigInt.parse(block["balance"]!), + block["link"]!, + ); + + blocks.add(block); + runningAmount += amt; + } + + try { + if (runningAmount > balance[currency]!.currentBalance || runningBalance < BigInt.zero) { + throw Exception(("Trying to send more than entire balance!")); + } + } catch (e) { + rethrow; + } + + return PendingNanoTransaction( + amount: runningAmount, + id: "", + nanoClient: _client, + blocks: blocks, + ); + } + + Future _receiveAll() async { + await _updateBalance(); + int blocksReceived = await this._client.confirmAllReceivable( + destinationAddress: _publicAddress!, + privateKey: _privateKey!, + ); + + if (blocksReceived > 0) { + await Future.delayed(Duration(seconds: 3)); + _updateBalance(); + updateTransactions(); + } + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return false; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + return true; + } catch (_) { + _isTransactionUpdating = false; + return false; + } + } + + @override + Future> fetchTransactions() async { + String address = _publicAddress!; + + final transactions = await _client.fetchTransactions(address); + + final Map result = {}; + + for (var transactionModel in transactions) { + final bool isSend = transactionModel.type == "send"; + result[transactionModel.hash] = NanoTransactionInfo( + id: transactionModel.hash, + amountRaw: transactionModel.amount, + height: transactionModel.height, + direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming, + confirmed: transactionModel.confirmed, + date: transactionModel.date ?? DateTime.now(), + confirmations: transactionModel.confirmed ? 1 : 0, + to: isSend ? transactionModel.account : address, + from: isSend ? address : transactionModel.account, + ); + } + + return result; + } + + @override + NanoWalletKeys get keys { + return NanoWalletKeys(seedKey: _hexSeed!); + } + + @override + String? get privateKey => _privateKey!; + + @override + Future rescan({required int height}) async { + updateTransactions(); + _updateBalance(); + return; + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String? get seed => _mnemonic.isNotEmpty ? _mnemonic : null; + + String get hexSeed => _hexSeed!; + + String get representative => _representativeAddress ?? ""; + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + + // setup a timer to receive transactions periodically: + _receiveTimer?.cancel(); + _receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async { + // get our balance: + await _updateBalance(); + // if we have anything to receive, process it: + if (balance[currency]!.receivableBalance > BigInt.zero) { + await _receiveAll(); + } + }); + + // also run once, immediately: + await _updateBalance(); + bool updateSuccess = await updateTransactions(); + if (!updateSuccess) { + syncStatus = FailedSyncStatus(); + return; + } + + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + rethrow; + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'seedKey': _hexSeed, + 'mnemonic': _mnemonic, + 'currentBalance': balance[currency]?.currentBalance.toString() ?? "0", + 'receivableBalance': balance[currency]?.receivableBalance.toString() ?? "0", + 'derivationType': _derivationType.toString() + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; + + final balance = NanoBalance.fromRawString( + currentBalance: data['currentBalance'] as String? ?? "0", + receivableBalance: data['receivableBalance'] as String? ?? "0", + ); + + DerivationType derivationType = DerivationType.nano; + if (data['derivationType'] == "DerivationType.bip39") { + derivationType = DerivationType.bip39; + } + + walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType); + if (walletInfo.derivationInfo!.derivationType == null) { + walletInfo.derivationInfo!.derivationType = derivationType; + } + + return NanoWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + // init() should always be run after this! + } + + Future _updateBalance() async { + var oldBalance = balance[currency]; + try { + balance[currency] = await _client.getBalance(_publicAddress!); + } catch (e) { + print("Failed to get balance $e"); + // if we don't have a balance, we should at least create one, since it's a late binding + // otherwise, it's better to just leave it as whatever it was before: + if (balance[currency] == null) { + balance[currency] = + NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero); + } + } + // don't save unnecessarily: + // trying to save too frequently can cause problems with the file system + // since nano is updated frequently this can be a problem, so we only save if there is a change: + if (oldBalance == null || + balance[currency]!.currentBalance != oldBalance.currentBalance || + balance[currency]!.receivableBalance != oldBalance.receivableBalance) { + await save(); + } + } + + Future _updateRep() async { + try { + AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!; + _representativeAddress = accountInfo.representative; + } catch (e) { + // account not found: + _representativeAddress = await _client.getRepFromPrefs(); + throw Exception("Failed to get representative address $e"); + } + + repScore = await _client.getRepScore(_representativeAddress!); + } + + Future regenerateAddress() async { + final NanoDerivationType type = (_derivationType == DerivationType.nano) + ? NanoDerivationType.STANDARD + : NanoDerivationType.HD; + _privateKey = await NanoDerivations.universalSeedToPrivate( + _hexSeed!, + index: this.walletAddresses.account!.id, + type: type, + ); + _publicAddress = await NanoDerivations.universalSeedToAddress( + _hexSeed!, + index: this.walletAddresses.account!.id, + type: type, + ); + + this.walletInfo.address = _publicAddress!; + this.walletAddresses.address = _publicAddress!; + } + + Future changeRep(String address) async { + try { + final String hash = await _client.changeRep( + privateKey: _privateKey!, + repAddress: address, + ourAddress: _publicAddress!, + ); + if (hash.isNotEmpty) { + _representativeAddress = address; + } + } catch (e) { + throw Exception("Failed to change representative address $e"); + } + } + + Future> getN2Reps() async { + return _client.getN2Reps(); + } + + Future? updateBalance() async => await _updateBalance(); + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } +} diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart new file mode 100644 index 000000000..cc532d2c7 --- /dev/null +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -0,0 +1,50 @@ +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_nano/nano_account_list.dart'; +import 'package:mobx/mobx.dart'; + +part 'nano_wallet_addresses.g.dart'; + +class NanoWalletAddresses = NanoWalletAddressesBase with _$NanoWalletAddresses; + +abstract class NanoWalletAddressesBase extends WalletAddresses with Store { + NanoWalletAddressesBase(WalletInfo walletInfo) + : accountList = NanoAccountList(walletInfo.address), + address = '', + super(walletInfo); + @override + @observable + String address; + + @observable + NanoAccount? account; + + NanoAccountList accountList; + + @override + Future init() async { + var box = await CakeHive.openBox(walletInfo.address); + try { + box.getAt(0); + } catch (e) { + box.add(NanoAccount(id: 0, label: "Primary Account", balance: "0.00")); + } + + await accountList.update(walletInfo.address); + account = accountList.accounts.first; + address = walletInfo.address; + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart new file mode 100644 index 000000000..4ee79ce48 --- /dev/null +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -0,0 +1,49 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class NanoNewWalletCredentials extends WalletCredentials { + NanoNewWalletCredentials({ + required String name, + String? password, + DerivationType? derivationType, + }) : super( + name: name, + password: password, + derivationInfo: DerivationInfo(derivationType: derivationType), + ); +} + +class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { + NanoRestoreWalletFromSeedCredentials({ + required String name, + required this.mnemonic, + String? password, + required DerivationType derivationType, + }) : super( + name: name, + password: password, + derivationInfo: DerivationInfo(derivationType: derivationType), + ); + + final String mnemonic; +} + +class NanoWalletLoadingException implements Exception { + @override + String toString() => 'Failure to load the wallet.'; +} + +class NanoRestoreWalletFromKeysCredentials extends WalletCredentials { + NanoRestoreWalletFromKeysCredentials({ + required String name, + required String password, + required DerivationType derivationType, + required this.seedKey, + }) : super( + name: name, + password: password, + derivationInfo: DerivationInfo(derivationType: derivationType), + ); + + final String seedKey; +} diff --git a/cw_nano/lib/nano_wallet_keys.dart b/cw_nano/lib/nano_wallet_keys.dart new file mode 100644 index 000000000..80a845e64 --- /dev/null +++ b/cw_nano/lib/nano_wallet_keys.dart @@ -0,0 +1,5 @@ +class NanoWalletKeys { + const NanoWalletKeys({required this.seedKey}); + + final String seedKey; +} diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart new file mode 100644 index 000000000..b1497a625 --- /dev/null +++ b/cw_nano/lib/nano_wallet_service.dart @@ -0,0 +1,177 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_nano/nano_mnemonic.dart' as nm; +import 'package:cw_nano/nano_wallet.dart'; +import 'package:cw_nano/nano_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:nanodart/nanodart.dart'; +import 'package:nanoutil/nanoutil.dart'; + +class NanoWalletService extends WalletService { + NanoWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + static bool walletFilesExist(String path) => + !File(path).existsSync() && !File('$path.keys').existsSync(); + + @override + WalletType getType() => WalletType.nano; + + @override + Future create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { + // nano standard: + String seedKey = NanoSeeds.generateSeed(); + String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); + + // ensure default if not present: + credentials.walletInfo!.derivationInfo ??= DerivationInfo(derivationType: DerivationType.nano); + + final wallet = NanoWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + + String randomWords = + (List.from(nm.NanoMnemomics.WORDLIST)..shuffle()).take(24).join(' '); + final currentWallet = + NanoWallet(walletInfo: currentWalletInfo, password: password, mnemonic: randomWords); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { + if (credentials.seedKey.contains(' ')) { + throw Exception("Invalid key!"); + } else { + if (credentials.seedKey.length != 64 && credentials.seedKey.length != 128) { + throw Exception("Invalid key length!"); + } + } + + String? mnemonic; + + // we can't derive the mnemonic from the key in all cases, only if it's a "nano" seed + if (credentials.seedKey.length == 64) { + try { + mnemonic = NanoDerivations.standardSeedToMnemonic(credentials.seedKey); + } catch (e) { + throw Exception("Wasn't a valid nano style seed!"); + } + } + + final wallet = await NanoWallet( + password: credentials.password!, + mnemonic: mnemonic ?? credentials.seedKey, + walletInfo: credentials.walletInfo!, + ); + await wallet.init(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + if (credentials.mnemonic.contains(' ')) { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw nm.NanoMnemonicIsIncorrectException(); + } + + if (!NanoMnemomics.validateMnemonic(credentials.mnemonic.split(' '))) { + throw nm.NanoMnemonicIsIncorrectException(); + } + } else { + if (credentials.mnemonic.length != 64 && credentials.mnemonic.length != 128) { + throw Exception("Invalid seed length"); + } + } + + DerivationType derivationType = + credentials.walletInfo?.derivationInfo?.derivationType ?? DerivationType.nano; + + credentials.walletInfo!.derivationInfo ??= DerivationInfo(derivationType: derivationType); + + final wallet = await NanoWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await NanoWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + final wallet = await NanoWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } +} diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart new file mode 100644 index 000000000..a027100fd --- /dev/null +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_nano/nano_client.dart'; +import 'package:nanoutil/nanoutil.dart'; + +class PendingNanoTransaction with PendingTransaction { + PendingNanoTransaction({ + required this.nanoClient, + required this.amount, + required this.id, + required this.blocks, + }); + + final NanoClient nanoClient; + final BigInt amount; + final String id; + final List> blocks; + String hex = "unused"; + + @override + String get amountFormatted { + final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano); + return amt; + } + + String get accurateAmountFormatted { + final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano); + final String acc = NanoAmounts.getRawAccuracy(amount.toString(), NanoAmounts.rawPerNano); + return "$acc$amt"; + } + + @override + String get feeFormatted => "0"; + + @override + Future commit() async { + for (var block in blocks) { + await nanoClient.processBlock(block, "send"); + } + } +} diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock new file mode 100644 index 000000000..0ebc5e75f --- /dev/null +++ b/cw_nano/pubspec.lock @@ -0,0 +1,842 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bip32: + dependency: "direct main" + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + bip39: + dependency: "direct main" + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + build: + dependency: transitive + description: + name: build + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" + source: hosted + version: "2.3.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: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + url: "https://pub.dev" + source: hosted + version: "7.2.7+1" + 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: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: transitive + description: + name: collection + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" + source: hosted + version: "1.17.2" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + decimal: + dependency: "direct main" + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + ed25519_hd_key: + dependency: "direct main" + description: + name: ed25519_hd_key + sha256: "326608234e986ea826a5db4cf4cd6826058d860875a3fff7926c0725fe1a604d" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + 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: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fixnum_nanodart: + dependency: transitive + description: + name: fixnum_nanodart + sha256: "4b0132d11ecddc0d2ca64b6d7dee6726db432ed02cac1349d7532a08be5c54fc" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + url: "https://pub.dev" + source: hosted + version: "2.0.6+5" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + 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: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + libcrypto: + dependency: "direct main" + description: + name: libcrypto + sha256: "18a97db8d88147b0b60d2755f29b5e4944181c4c1a9f52bd1ecbea1b0a5aab03" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mobx: + dependency: "direct main" + description: + name: mobx + sha256: "0afcf88b3ee9d6819890bf16c11a727fc8c62cf736fda8e5d3b9b4eace4e62ea" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + url: "https://pub.dev" + source: hosted + version: "2.3.0" + nanodart: + dependency: "direct main" + description: + name: nanodart + sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nanoutil: + dependency: "direct main" + description: + path: "." + ref: c37e72817cf0a28162f43124f79661d6c8e0098f + resolved-ref: c37e72817cf0a28162f43124f79661d6c8e0098f + url: "https://github.com/perishllc/nanoutil.git" + source: git + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.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: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 + url: "https://pub.dev" + source: hosted + version: "0.3.4" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + rational: + dependency: transitive + description: + name: rational + sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + 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: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: "direct overridden" + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + url: "https://pub.dev" + source: hosted + version: "1.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml new file mode 100644 index 000000000..a4b8732fd --- /dev/null +++ b/cw_nano/pubspec.yaml @@ -0,0 +1,74 @@ +name: cw_nano +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + nanodart: ^2.0.0 + decimal: ^2.3.3 + libcrypto: ^0.2.2 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^1.1.0 + shared_preferences: ^2.0.15 + nanoutil: + git: + url: https://github.com/perishllc/nanoutil.git + ref: c37e72817cf0a28162f43124f79661d6c8e0098f + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_nano/test/cw_nano_test.dart b/cw_nano/test/cw_nano_test.dart new file mode 100644 index 000000000..fbabc7b54 --- /dev/null +++ b/cw_nano/test/cw_nano_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_nano/cw_nano.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_polygon/.gitignore b/cw_polygon/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_polygon/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_polygon/.metadata b/cw_polygon/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_polygon/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_polygon/CHANGELOG.md b/cw_polygon/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_polygon/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_polygon/LICENSE b/cw_polygon/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_polygon/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_polygon/README.md b/cw_polygon/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_polygon/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_polygon/analysis_options.yaml b/cw_polygon/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_polygon/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_polygon/lib/cw_polygon.dart b/cw_polygon/lib/cw_polygon.dart new file mode 100644 index 000000000..5d4e447d1 --- /dev/null +++ b/cw_polygon/lib/cw_polygon.dart @@ -0,0 +1,7 @@ +library cw_polygon; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_polygon/lib/default_polygon_erc20_tokens.dart b/cw_polygon/lib/default_polygon_erc20_tokens.dart new file mode 100644 index 000000000..deff285c0 --- /dev/null +++ b/cw_polygon/lib/default_polygon_erc20_tokens.dart @@ -0,0 +1,82 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultPolygonErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Tether USD (PoS)", + symbol: "USDT", + contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin (POS)", + symbol: "USDC.e", + contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Avalanche Token", + symbol: "AVAX", + contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC (PoS)", + symbol: "WBTC", + contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Dai (PoS)", + symbol: "DAI", + contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "SHIBA INU (PoS)", + symbol: "SHIB", + contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap (PoS)", + symbol: "UNI", + contractAddress: "0xb33EaAd8d922B1083446DC23f610c2567fB5180f", + decimal: 18, + enabled: false, + ), + ]; + + List get initialPolygonErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => + element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase()) + .iconPath; + } catch (_) {} + + return Erc20Token.copyWith(token, iconPath, 'POLY'); + }).toList(); +} diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart new file mode 100644 index 000000000..d55ee2269 --- /dev/null +++ b/cw_polygon/lib/polygon_client.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; + +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class PolygonClient extends EVMChainClient { + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + }) { + return Transaction( + from: from, + to: to, + value: amount, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 137; + + @override + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.polygonScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map( + (e) => EVMChainTransactionModel.fromJson(e as Map, 'MATIC'), + ) + .toList(); + } + + return []; + } catch (e) { + return []; + } + } + + @override + Future> fetchInternalTransactions(String address) async { + try { + final response = await httpClient.get(Uri.https("api.polygonscan.io", "/api", { + "module": "account", + "action": "txlistinternal", + "address": address, + "apikey": secrets.polygonScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'MATIC')) + .toList(); + } + + return []; + } catch (_) { + return []; + } + } +} diff --git a/cw_polygon/lib/polygon_mnemonics_exception.dart b/cw_polygon/lib/polygon_mnemonics_exception.dart new file mode 100644 index 000000000..c1a2fcc84 --- /dev/null +++ b/cw_polygon/lib/polygon_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class PolygonMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Polygon mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_polygon/lib/polygon_transaction_history.dart b/cw_polygon/lib/polygon_transaction_history.dart new file mode 100644 index 000000000..8674882cd --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_history.dart @@ -0,0 +1,19 @@ +import 'dart:core'; + +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_polygon/polygon_transaction_info.dart'; + +class PolygonTransactionHistory extends EVMChainTransactionHistory { + PolygonTransactionHistory({ + required super.walletInfo, + required super.password, + }); + + @override + String getTransactionHistoryFileName() => 'polygon_transactions.json'; + + @override + EVMChainTransactionInfo getTransactionInfo(Map val) => + PolygonTransactionInfo.fromJson(val); +} diff --git a/cw_polygon/lib/polygon_transaction_info.dart b/cw_polygon/lib/polygon_transaction_info.dart new file mode 100644 index 000000000..1fbe1c5d4 --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_info.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class PolygonTransactionInfo extends EVMChainTransactionInfo { + PolygonTransactionInfo({ + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.exponent, + }); + + factory PolygonTransactionInfo.fromJson(Map data) { + return PolygonTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + ); + } + + @override + String get feeCurrency => 'MATIC'; +} diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart new file mode 100644 index 000000000..60c7ad2ff --- /dev/null +++ b/cw_polygon/lib/polygon_wallet.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/file.dart'; +import 'package:cw_polygon/default_polygon_erc20_tokens.dart'; +import 'package:cw_polygon/polygon_transaction_info.dart'; +import 'package:cw_polygon/polygon_client.dart'; +import 'package:cw_polygon/polygon_transaction_history.dart'; + +class PolygonWallet extends EVMChainWallet { + PolygonWallet({ + required super.walletInfo, + required super.password, + super.mnemonic, + super.initialBalance, + super.privateKey, + required super.client, + }) : super(nativeCurrency: CryptoCurrency.maticpoly); + + @override + Future initErc20TokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}"; + if (await CakeHive.boxExists(boxName)) { + evmChainErc20TokensBox = await CakeHive.openBox(boxName); + } else { + evmChainErc20TokensBox = await CakeHive.openBox(boxName.replaceAll(" ", "")); + } + } + + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; + + for (var token in initialErc20Tokens) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } + + @override + Future checkIfScanProviderIsEnabled() async { + bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true; + return isPolygonScanEnabled; + } + + @override + String getTransactionHistoryFileName() => 'polygon_transactions.json'; + + @override + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "MATIC", + iconPath: iconPath, + ); + } + + @override + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address) { + final model = PolygonTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "MATIC", + to: transactionModel.to, + from: transactionModel.from, + ); + return model; + } + + @override + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { + return PolygonTransactionHistory(walletInfo: walletInfo, password: password); + } + + static Future open( + {required String name, required String password, required WalletInfo walletInfo}) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + EVMChainERC20Balance(BigInt.zero); + + return PolygonWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + client: PolygonClient(), + ); + } +} diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart new file mode 100644 index 000000000..59e14abbf --- /dev/null +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -0,0 +1,126 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; +import 'package:cw_polygon/polygon_client.dart'; +import 'package:cw_polygon/polygon_mnemonics_exception.dart'; +import 'package:cw_polygon/polygon_wallet.dart'; + +class PolygonWalletService extends EVMChainWalletService { + PolygonWalletService( + super.walletInfoSource, { + required this.client, + }); + + late PolygonClient client; + + @override + WalletType getType() => WalletType.polygon; + + @override + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = PolygonWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await PolygonWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await PolygonWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { + final wallet = PolygonWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw PolygonMnemonicIsIncorrectException(); + } + + final wallet = PolygonWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await PolygonWallet.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml new file mode 100644 index 000000000..505838d7c --- /dev/null +++ b/cw_polygon/pubspec.yaml @@ -0,0 +1,68 @@ +name: cw_polygon +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_ethereum: + path: ../cw_ethereum + cw_evm: + path: ../cw_evm + web3dart: ^2.7.1 + hive: ^2.2.3 + bip39: ^1.0.6 + collection: ^1.17.1 + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.1.11 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_polygon/test/cw_polygon_test.dart b/cw_polygon/test/cw_polygon_test.dart new file mode 100644 index 000000000..554e28795 --- /dev/null +++ b/cw_polygon/test/cw_polygon_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_polygon/cw_polygon.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_shared_external/android/build.gradle b/cw_shared_external/android/build.gradle index d6cdaf658..8db51f0e6 100644 --- a/cw_shared_external/android/build.gradle +++ b/cw_shared_external/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.cw_shared_external' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/cw_shared_external/pubspec.yaml b/cw_shared_external/pubspec.yaml index b9a8ca1e0..71d5fcd5a 100644 --- a/cw_shared_external/pubspec.yaml +++ b/cw_shared_external/pubspec.yaml @@ -5,7 +5,7 @@ author: Cake Walelt homepage: https://cakewallet.com environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: diff --git a/cw_solana/.gitignore b/cw_solana/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_solana/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_solana/.metadata b/cw_solana/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_solana/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_solana/CHANGELOG.md b/cw_solana/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_solana/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_solana/LICENSE b/cw_solana/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_solana/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_solana/README.md b/cw_solana/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_solana/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_solana/analysis_options.yaml b/cw_solana/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_solana/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_solana/lib/cw_solana.dart b/cw_solana/lib/cw_solana.dart new file mode 100644 index 000000000..d04069b3b --- /dev/null +++ b/cw_solana/lib/cw_solana.dart @@ -0,0 +1,7 @@ +library cw_solana; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart new file mode 100644 index 000000000..7acad78e0 --- /dev/null +++ b/cw_solana/lib/default_spl_tokens.dart @@ -0,0 +1,107 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_solana/spl_token.dart'; + +class DefaultSPLTokens { + final List _defaultTokens = [ + SPLToken( + name: 'USDT Tether', + symbol: 'USDT', + mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + decimal: 6, + mint: 'usdtsol', + enabled: true, + ), + SPLToken( + name: 'USD Coin', + symbol: 'USDC', + mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + decimal: 6, + mint: 'usdcsol', + enabled: true, + ), + SPLToken( + name: 'Wrapped Ethereum (Sollet)', + symbol: 'soETH', + mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', + decimal: 6, + mint: 'soEth', + iconPath: 'assets/images/eth_icon.png', + ), + SPLToken( + name: 'Wrapped SOL', + symbol: 'WSOL', + mintAddress: 'So11111111111111111111111111111111111111112', + decimal: 9, + mint: 'WSOL', + iconPath: 'assets/images/sol_icon.png', + ), + SPLToken( + name: 'Wrapped Bitcoin (Sollet)', + symbol: 'BTC', + mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', + decimal: 6, + mint: 'btcsol', + iconPath: 'assets/images/btc.png', + ), + SPLToken( + name: 'Bonk', + symbol: 'Bonk', + mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', + decimal: 5, + mint: 'Bonk', + iconPath: 'assets/images/bonk_icon.png', + ), + SPLToken( + name: 'Helium Network Token', + symbol: 'HNT', + mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux', + decimal: 8, + mint: 'hnt', + iconPath: 'assets/images/hnt_icon.png', + ), + SPLToken( + name: 'Pyth Network', + symbol: 'PYTH', + mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', + decimal: 6, + mint: 'pyth', + ), + SPLToken( + name: 'Raydium', + symbol: 'RAY', + mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', + decimal: 6, + mint: 'ray', + iconPath: 'assets/images/ray_icon.png', + ), + SPLToken( + name: 'GMT', + symbol: 'GMT', + mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx', + decimal: 6, + mint: 'ray', + iconPath: 'assets/images/gmt_icon.png', + ), + SPLToken( + name: 'AvocadoCoin', + symbol: 'AVDO', + mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P', + decimal: 8, + mint: 'avdo', + iconPath: 'assets/images/avdo_icon.png', + ), + ]; + + List get initialSPLTokens => _defaultTokens.map((token) { + String? iconPath; + if (token.iconPath != null) return token; + + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + return SPLToken.copyWith(token, iconPath, 'SOL'); + }).toList(); +} diff --git a/cw_solana/lib/file.dart b/cw_solana/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_solana/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart new file mode 100644 index 000000000..38347ed13 --- /dev/null +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -0,0 +1,43 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:solana/encoder.dart'; + +class PendingSolanaTransaction with PendingTransaction { + final double amount; + final SignedTx signedTransaction; + final String destinationAddress; + final Function sendTransaction; + final double fee; + + PendingSolanaTransaction({ + required this.fee, + required this.amount, + required this.signedTransaction, + required this.destinationAddress, + required this.sendTransaction, + }); + + @override + String get amountFormatted { + String stringifiedAmount = amount.toString(); + + if (stringifiedAmount.toString().length >= 6) { + stringifiedAmount = stringifiedAmount.substring(0, 6); + } + + return stringifiedAmount; + } + + @override + Future commit() async { + return await sendTransaction(); + } + + @override + String get feeFormatted => fee.toString(); + + @override + String get hex => signedTransaction.encode(); + + @override + String get id => ''; +} diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart new file mode 100644 index 000000000..b1f0ef153 --- /dev/null +++ b/cw_solana/lib/solana_balance.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:cw_core/balance.dart'; + +class SolanaBalance extends Balance { + SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt()); + + final double balance; + + @override + String get formattedAdditionalBalance => _balanceFormatted(); + + @override + String get formattedAvailableBalance => _balanceFormatted(); + + String _balanceFormatted() { + String stringBalance = balance.toString(); + if (stringBalance.toString().length >= 6) { + stringBalance = stringBalance.substring(0, 6); + } + return stringBalance; + } + + static SolanaBalance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return SolanaBalance(decoded['balance']); + } catch (e) { + return SolanaBalance(0.0); + } + } + + String toJSON() => json.encode({'balance': balance.toString()}); +} diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart new file mode 100644 index 000000000..6ed8cab29 --- /dev/null +++ b/cw_solana/lib/solana_client.dart @@ -0,0 +1,553 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_solana/pending_solana_transaction.dart'; +import 'package:cw_solana/solana_balance.dart'; +import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:http/http.dart' as http; +import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; +import '.secrets.g.dart' as secrets; + +class SolanaWalletClient { + final httpClient = http.Client(); + SolanaClient? _client; + + bool connect(Node node) { + try { + Uri? rpcUri; + String webSocketUrl; + bool isModifiedNodeUri = false; + + if (node.uriRaw == 'rpc.ankr.com') { + isModifiedNodeUri = true; + String ankrApiKey = secrets.ankrApiKey; + + rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); + webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + } else { + webSocketUrl = 'wss://${node.uriRaw}'; + } + + _client = SolanaClient( + rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri, + websocketUrl: Uri.parse(webSocketUrl), + timeout: const Duration(minutes: 2), + ); + return true; + } catch (e) { + return false; + } + } + + Future getBalance(String address) async { + try { + final balance = await _client!.rpcClient.getBalance(address); + + final solBalance = balance.value / lamportsPerSol; + + return solBalance; + } catch (_) { + return 0.0; + } + } + + Future getSPLTokenAccounts(String mintAddress, String publicKey) async { + try { + final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner( + publicKey, + TokenAccountsFilter.byMint(mintAddress), + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + return tokenAccounts; + } catch (e) { + return null; + } + } + + Future getSplTokenBalance(String mintAddress, String publicKey) async { + // Fetch the token accounts (a token can have multiple accounts for various uses) + final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey); + + // Handle scenario where there is no token account + if (tokenAccounts == null || tokenAccounts.value.isEmpty) { + return null; + } + + // Sum the balances of all accounts with the specified mint address + double totalBalance = 0.0; + + for (var programAccount in tokenAccounts.value) { + final tokenAmountResult = + await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey); + + final balance = tokenAmountResult.value.uiAmountString; + + final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; + + totalBalance += balanceAsDouble; + } + + return SolanaBalance(totalBalance); + } + + Future getFeeForMessage(String message, Commitment commitment) async { + try { + final feeForMessage = + await _client!.rpcClient.getFeeForMessage(message, commitment: commitment); + final fee = (feeForMessage ?? 0.0) / lamportsPerSol; + return fee; + } catch (_) { + return 0.0; + } + } + + Future getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async { + const commitment = Commitment.confirmed; + + final message = + _getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol); + + final recentBlockhash = await _getRecentBlockhash(commitment); + + final estimatedFee = + _getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment); + return estimatedFee; + } + + /// Load the Address's transactions into the account + Future> fetchTransactions( + Ed25519HDPublicKey publicKey, { + String? splTokenSymbol, + int? splTokenDecimal, + }) async { + List transactions = []; + + try { + final response = await _client!.rpcClient.getTransactionsList( + publicKey, + commitment: Commitment.confirmed, + limit: 1000, + ); + + for (final tx in response) { + if (tx.transaction is ParsedTransaction) { + final parsedTx = (tx.transaction as ParsedTransaction); + final message = parsedTx.message; + + final fee = (tx.meta?.fee ?? 0) / lamportsPerSol; + + for (final instruction in message.instructions) { + if (instruction is ParsedInstruction) { + instruction.map( + system: (systemData) { + systemData.parsed.map( + transfer: (transferData) { + ParsedSystemTransferInformation transfer = transferData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + + double amount = transfer.lamports.toDouble() / lamportsPerSol; + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + blockTimeInInt: tx.blockTime!, + fee: fee, + programId: SystemProgram.programId, + tokenSymbol: 'SOL', + ), + ); + }, + transferChecked: (_) {}, + unsupported: (_) {}, + ); + }, + splToken: (splTokenData) { + if (splTokenSymbol != null) { + splTokenData.parsed.map( + transfer: (transferData) { + SplTokenTransferInfo transfer = transferData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + + double amount = (double.tryParse(transfer.amount) ?? 0.0) / + pow(10, splTokenDecimal ?? 9); + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + fee: fee, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + programId: TokenProgram.programId, + blockTimeInInt: tx.blockTime!, + tokenSymbol: splTokenSymbol, + ), + ); + }, + transferChecked: (transferCheckedData) { + SplTokenTransferCheckedInfo transfer = transferCheckedData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + double amount = + double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0; + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + fee: fee, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + programId: TokenProgram.programId, + blockTimeInInt: tx.blockTime!, + tokenSymbol: splTokenSymbol, + ), + ); + }, + generic: (genericData) {}, + ); + } + }, + memo: (_) {}, + unsupported: (a) {}, + ); + } + } + } + } + + return transactions; + } catch (err) { + return []; + } + } + + Future> getSPLTokenTransfers( + String address, + String splTokenSymbol, + int splTokenDecimal, + Ed25519HDKeyPair ownerKeypair, + ) async { + final tokenMint = Ed25519HDPublicKey.fromBase58(address); + + ProgramAccount? associatedTokenAccount; + + try { + associatedTokenAccount = await _client!.getAssociatedTokenAccount( + mint: tokenMint, + owner: ownerKeypair.publicKey, + commitment: Commitment.confirmed, + ); + } catch (_) {} + + if (associatedTokenAccount == null) return []; + + final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey); + + final tokenTransactions = await fetchTransactions( + accountPublicKey, + splTokenSymbol: splTokenSymbol, + splTokenDecimal: splTokenDecimal, + ); + + return tokenTransactions; + } + + void stop() {} + + SolanaClient? get getSolanaClient => _client; + + Future signSolanaTransaction({ + required String tokenTitle, + required int tokenDecimals, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + required bool isSendAll, + String? tokenMint, + List references = const [], + }) async { + const commitment = Commitment.confirmed; + + if (tokenTitle == CryptoCurrency.sol.title) { + final pendingNativeTokenTransaction = await _signNativeTokenTransaction( + tokenTitle: tokenTitle, + tokenDecimals: tokenDecimals, + inputAmount: inputAmount, + destinationAddress: destinationAddress, + ownerKeypair: ownerKeypair, + commitment: commitment, + isSendAll: isSendAll, + ); + return pendingNativeTokenTransaction; + } else { + final pendingSPLTokenTransaction = _signSPLTokenTransaction( + tokenTitle: tokenTitle, + tokenDecimals: tokenDecimals, + tokenMint: tokenMint!, + inputAmount: inputAmount, + destinationAddress: destinationAddress, + ownerKeypair: ownerKeypair, + commitment: commitment, + ); + return pendingSPLTokenTransaction; + } + } + + Future _getRecentBlockhash(Commitment commitment) async { + final latestBlockhash = + await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value; + + final recentBlockhash = RecentBlockhash( + blockhash: latestBlockhash.blockhash, + feeCalculator: const FeeCalculator(lamportsPerSignature: 500), + ); + + return recentBlockhash; + } + + Message _getMessageForNativeTransaction( + Ed25519HDKeyPair ownerKeypair, + String destinationAddress, + int lamports, + ) { + final instructions = [ + SystemInstruction.transfer( + fundingAccount: ownerKeypair.publicKey, + recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress), + lamports: lamports, + ), + ]; + + final message = Message(instructions: instructions); + return message; + } + + Future _getFeeFromCompiledMessage( + Message message, + Ed25519HDPublicKey feePayer, + RecentBlockhash recentBlockhash, + Commitment commitment, + ) async { + final compile = message.compile( + recentBlockhash: recentBlockhash.blockhash, + feePayer: feePayer, + ); + + final base64Message = base64Encode(compile.toByteArray().toList()); + + final fee = await getFeeForMessage(base64Message, commitment); + + return fee; + } + + Future _signNativeTokenTransaction({ + required String tokenTitle, + required int tokenDecimals, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + required Commitment commitment, + required bool isSendAll, + }) async { + // Convert SOL to lamport + int lamports = (inputAmount * lamportsPerSol).toInt(); + + Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports); + + final signers = [ownerKeypair]; + + RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment); + + final fee = await _getFeeFromCompiledMessage( + message, + signers.first.publicKey, + recentBlockhash, + commitment, + ); + + SignedTx signedTx; + if (isSendAll) { + final feeInLamports = (fee * lamportsPerSol).toInt(); + final updatedLamports = lamports - feeInLamports; + + final updatedMessage = + _getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports); + + signedTx = await _signTransactionInternal( + message: updatedMessage, + signers: signers, + commitment: commitment, + recentBlockhash: recentBlockhash, + ); + } else { + signedTx = await _signTransactionInternal( + message: message, + signers: signers, + commitment: commitment, + recentBlockhash: recentBlockhash, + ); + } + + sendTx() async => await sendTransaction( + signedTransaction: signedTx, + commitment: commitment, + ); + + final pendingTransaction = PendingSolanaTransaction( + amount: inputAmount, + signedTransaction: signedTx, + destinationAddress: destinationAddress, + sendTransaction: sendTx, + fee: fee, + ); + + return pendingTransaction; + } + + Future _signSPLTokenTransaction({ + required String tokenTitle, + required int tokenDecimals, + required String tokenMint, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + required Commitment commitment, + }) async { + final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); + final mint = Ed25519HDPublicKey.fromBase58(tokenMint); + + ProgramAccount? associatedRecipientAccount; + ProgramAccount? associatedSenderAccount; + + associatedRecipientAccount = await _client!.getAssociatedTokenAccount( + mint: mint, + owner: destinationOwner, + commitment: commitment, + ); + + associatedSenderAccount = await _client!.getAssociatedTokenAccount( + owner: ownerKeypair.publicKey, + mint: mint, + commitment: commitment, + ); + + // Throw an appropriate exception if the sender has no associated + // token account + if (associatedSenderAccount == null) { + throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58()); + } + + try { + associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount( + mint: mint, + owner: destinationOwner, + funder: ownerKeypair, + ); + } catch (e) { + throw Exception('Insufficient lamports balance to complete this transaction'); + } + + // Input by the user + final amount = (inputAmount * pow(10, tokenDecimals)).toInt(); + + final instruction = TokenInstruction.transfer( + source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey), + destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey), + owner: ownerKeypair.publicKey, + amount: amount, + ); + + final message = Message(instructions: [instruction]); + + final signers = [ownerKeypair]; + + RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment); + + final fee = await _getFeeFromCompiledMessage( + message, + signers.first.publicKey, + recentBlockhash, + commitment, + ); + + final signedTx = await _signTransactionInternal( + message: message, + signers: signers, + commitment: commitment, + recentBlockhash: recentBlockhash, + ); + + sendTx() async => await sendTransaction( + signedTransaction: signedTx, + commitment: commitment, + ); + + final pendingTransaction = PendingSolanaTransaction( + amount: inputAmount, + signedTransaction: signedTx, + destinationAddress: destinationAddress, + sendTransaction: sendTx, + fee: fee, + ); + return pendingTransaction; + } + + Future _signTransactionInternal({ + required Message message, + required List signers, + required Commitment commitment, + required RecentBlockhash recentBlockhash, + }) async { + final signedTx = await signTransaction(recentBlockhash, message, signers); + + return signedTx; + } + + Future sendTransaction({ + required SignedTx signedTransaction, + required Commitment commitment, + }) async { + try { + final signature = await _client!.rpcClient.sendTransaction( + signedTransaction.encode(), + preflightCommitment: commitment, + ); + + _client!.waitForSignatureStatus(signature, status: commitment); + + return signature; + } catch (e) { + print('Error while sending transaction: ${e.toString()}'); + throw Exception(e); + } + } + + Future getIconImageFromTokenUri(String uri) async { + try { + final response = await httpClient.get(Uri.parse(uri)); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300) { + return jsonResponse['image']; + } else { + return null; + } + } catch (e) { + print('Error occurred while fetching token image: \n${e.toString()}'); + return null; + } + } +} diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart new file mode 100644 index 000000000..7409b0500 --- /dev/null +++ b/cw_solana/lib/solana_exceptions.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/crypto_currency.dart'; + +class SolanaTransactionCreationException implements Exception { + final String exceptionMessage; + + SolanaTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Error creating ${currency.title} transaction.'; + + @override + String toString() => exceptionMessage; +} + +class SolanaTransactionWrongBalanceException implements Exception { + final String exceptionMessage; + + SolanaTransactionWrongBalanceException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_solana/lib/solana_mnemonics.dart b/cw_solana/lib/solana_mnemonics.dart new file mode 100644 index 000000000..21cbb613a --- /dev/null +++ b/cw_solana/lib/solana_mnemonics.dart @@ -0,0 +1,2058 @@ +class SolanaMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Solana mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class SolanaMnemonics { + static const englishWordlist = [ + '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_solana/lib/solana_transaction_credentials.dart b/cw_solana/lib/solana_transaction_credentials.dart new file mode 100644 index 000000000..bd0c97f0b --- /dev/null +++ b/cw_solana/lib/solana_transaction_credentials.dart @@ -0,0 +1,12 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; + +class SolanaTransactionCredentials { + SolanaTransactionCredentials( + this.outputs, { + required this.currency, + }); + + final List outputs; + final CryptoCurrency currency; +} diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart new file mode 100644 index 000000000..c03de19ad --- /dev/null +++ b/cw_solana/lib/solana_transaction_history.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_solana/file.dart'; +import 'package:cw_solana/solana_transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'solana_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'solana_transactions.json'; + +class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory; + +abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase + with Store { + SolanaTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + print('Error while saving solana transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = SolanaTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart new file mode 100644 index 000000000..7a0844e52 --- /dev/null +++ b/cw_solana/lib/solana_transaction_info.dart @@ -0,0 +1,75 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class SolanaTransactionInfo extends TransactionInfo { + SolanaTransactionInfo({ + required this.id, + required this.blockTime, + required this.to, + required this.from, + required this.direction, + required this.solAmount, + this.tokenSymbol = "SOL", + required this.isPending, + required this.txFee, + }) : amount = solAmount.toInt(); + + final String id; + final String? to; + final String? from; + final int amount; + final bool isPending; + final double solAmount; + final String tokenSymbol; + final DateTime blockTime; + final double txFee; + final TransactionDirection direction; + + String? _fiatAmount; + + @override + DateTime get date => blockTime; + + @override + String amountFormatted() { + String stringBalance = solAmount.toString(); + + return '$stringBalance $tokenSymbol'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${txFee.toString()} SOL'; + + factory SolanaTransactionInfo.fromJson(Map data) { + return SolanaTransactionInfo( + id: data['id'] as String, + solAmount: data['solAmount'], + direction: parseTransactionDirectionFromInt(data['direction'] as int), + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), + isPending: data['isPending'] as bool, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + txFee: data['txFee'], + ); + } + + Map toJson() => { + 'id': id, + 'solAmount': solAmount, + 'direction': direction.index, + 'blockTime': blockTime.millisecondsSinceEpoch, + 'isPending': isPending, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + 'txFee': txFee, + }; +} diff --git a/cw_solana/lib/solana_transaction_model.dart b/cw_solana/lib/solana_transaction_model.dart new file mode 100644 index 000000000..c16c49258 --- /dev/null +++ b/cw_solana/lib/solana_transaction_model.dart @@ -0,0 +1,47 @@ +class SolanaTransactionModel { + final String id; + + final String from; + + final String to; + + final double amount; + + // If this is an outgoing transaction + final bool isOutgoingTx; + + // The Program ID of this transaction, e.g, System Program, Token Program... + final String programId; + + // The DateTime from the UNIX timestamp of the block where the transaction was included + final DateTime blockTime; + + // The Transaction fee + final double fee; + + // The token symbol + final String tokenSymbol; + + SolanaTransactionModel({ + required this.id, + required this.to, + required this.from, + required this.amount, + required this.programId, + required int blockTimeInInt, + this.isOutgoingTx = false, + required this.tokenSymbol, + required this.fee, + }) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000); + + factory SolanaTransactionModel.fromJson(Map json) => SolanaTransactionModel( + id: json['id'], + blockTimeInInt: int.parse(json["timeStamp"]) * 1000, + from: json["from"], + to: json["to"], + amount: double.parse(json["value"]), + programId: json["programId"], + fee: json['fee'], + tokenSymbol: json['tokenSymbol'], + ); +} diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart new file mode 100644 index 000000000..f3eef465c --- /dev/null +++ b/cw_solana/lib/solana_wallet.dart @@ -0,0 +1,543 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_solana/default_spl_tokens.dart'; +import 'package:cw_solana/file.dart'; +import 'package:cw_solana/solana_balance.dart'; +import 'package:cw_solana/solana_client.dart'; +import 'package:cw_solana/solana_exceptions.dart'; +import 'package:cw_solana/solana_transaction_credentials.dart'; +import 'package:cw_solana/solana_transaction_history.dart'; +import 'package:cw_solana/solana_transaction_info.dart'; +import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:cw_solana/solana_wallet_addresses.dart'; +import 'package:cw_solana/spl_token.dart'; +import 'package:hex/hex.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:solana/base58.dart'; +import 'package:solana/metaplex.dart' as metaplex; +import 'package:solana/solana.dart'; + +part 'solana_wallet.g.dart'; + +class SolanaWallet = SolanaWalletBase with _$SolanaWallet; + +abstract class SolanaWalletBase + extends WalletBase with Store { + SolanaWalletBase({ + required WalletInfo walletInfo, + String? mnemonic, + String? privateKey, + required String password, + SolanaBalance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _client = SolanaWalletClient(), + walletAddresses = SolanaWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { + CakeHive.registerAdapter(SPLTokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _password; + final String? _mnemonic; + final String? _hexPrivateKey; + + // The Solana WalletPair + Ed25519HDKeyPair? _walletKeyPair; + + Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair; + + // To access the privateKey bytes. + Ed25519HDKeyPairData? _keyPairData; + + late SolanaWalletClient _client; + + @observable + double? estimatedFee; + + Timer? _transactionsUpdateTimer; + + late final Box splTokensBox; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer _sharedPrefs = Completer(); + + @override + Ed25519HDKeyPairData get keys { + if (_keyPairData == null) { + return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([])); + } + + return _keyPairData!; + } + + @override + String? get seed => _mnemonic; + + @override + String get privateKey { + final privateKeyBytes = _keyPairData!.bytes; + + final publicKeyBytes = _keyPairData!.publicKey.bytes; + + final encodedBytes = privateKeyBytes + publicKeyBytes; + + final privateKey = base58encode(encodedBytes); + + return privateKey; + } + + Future init() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; + + splTokensBox = await CakeHive.openBox(boxName); + + // Create WalletPair using either the mnemonic or the privateKey + _walletKeyPair = await getWalletPair( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + ); + + // Extract the keyPairData containing both the privateKey bytes and the publicKey hex. + _keyPairData = await _walletKeyPair!.extract(); + + walletInfo.address = _walletKeyPair!.address; + + await walletAddresses.init(); + await transactionHistory.init(); + await save(); + } + + Future getWalletPair({String? mnemonic, String? privateKey}) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + final privateKeyBytes = base58decode(privateKey); + return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList()); + } + + return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future changePassword(String password) => throw UnimplementedError("changePassword"); + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Solana Node connection failed"); + } + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future _getEstimatedFees() async { + try { + estimatedFee = await _client.getEstimatedFee(_walletKeyPair!); + } catch (e) { + estimatedFee = 0.0; + } + } + + @override + Future createTransaction(Object credentials) async { + final solCredentials = credentials as SolanaTransactionCredentials; + + final outputs = solCredentials.outputs; + + final hasMultiDestination = outputs.length > 1; + + await _updateBalance(); + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == solCredentials.currency.title); + + final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + + double totalAmount = 0.0; + + bool isSendAll = false; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + + final totalAmountFromCredentials = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + totalAmount = totalAmountFromCredentials.toDouble(); + + if (walletBalanceForCurrency < totalAmount) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + } else { + final output = outputs.first; + + isSendAll = output.sendAll; + + if (isSendAll) { + totalAmount = walletBalanceForCurrency; + } else { + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); + + totalAmount = totalOriginalAmount; + } + + if (walletBalanceForCurrency < totalAmount) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + } + + String? tokenMint; + // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions) + if (transactionCurrency.title != CryptoCurrency.sol.title) { + tokenMint = (transactionCurrency as SPLToken).mintAddress; + } + + final pendingSolanaTransaction = await _client.signSolanaTransaction( + tokenMint: tokenMint, + tokenTitle: transactionCurrency.title, + inputAmount: totalAmount, + ownerKeypair: _walletKeyPair!, + tokenDecimals: transactionCurrency.decimals, + destinationAddress: solCredentials.outputs.first.isParsedAddress + ? solCredentials.outputs.first.extractedAddress! + : solCredentials.outputs.first.address, + isSendAll: isSendAll, + ); + + return pendingSolanaTransaction; + } + + @override + Future> fetchTransactions() async => {}; + + /// Fetches the native SOL transactions linked to the wallet Public Key + Future _updateNativeSOLTransactions() async { + final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address); + + final transactions = await _client.fetchTransactions(address); + + await _addTransactionsToTransactionHistory(transactions); + } + + /// Fetches the SPL Tokens transactions linked to the token account Public Key + Future _updateSPLTokenTransactions() async { + // List splTokenTransactions = []; + + // Make a copy of keys to avoid concurrent modification + var tokenKeys = List.from(balance.keys); + + for (var token in tokenKeys) { + if (token is SPLToken) { + final tokenTxs = await _client.getSPLTokenTransfers( + token.mintAddress, + token.symbol, + token.decimal, + _walletKeyPair!, + ); + + // splTokenTransactions.addAll(tokenTxs); + await _addTransactionsToTransactionHistory(tokenTxs); + } + } + + // await _addTransactionsToTransactionHistory(splTokenTransactions); + } + + Future _addTransactionsToTransactionHistory( + List transactions, + ) async { + final Map result = {}; + + for (var transactionModel in transactions) { + result[transactionModel.id] = SolanaTransactionInfo( + id: transactionModel.id, + to: transactionModel.to, + from: transactionModel.from, + blockTime: transactionModel.blockTime, + direction: transactionModel.isOutgoingTx + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + solAmount: transactionModel.amount, + isPending: false, + txFee: transactionModel.fee, + tokenSymbol: transactionModel.tokenSymbol, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + } + + @override + Future rescan({required int height}) => throw UnimplementedError("rescan"); + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + + await Future.wait([ + _updateBalance(), + _updateNativeSOLTransactions(), + _updateSPLTokenTransactions(), + _getEstimatedFees(), + ]); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0); + + return SolanaWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchSOLBalance(); + await _fetchSPLTokensBalances(); + await save(); + } + + Future _fetchSOLBalance() async { + final balance = await _client.getBalance(_walletKeyPair!.address); + + return SolanaBalance(balance); + } + + Future _fetchSPLTokensBalances() async { + for (var token in splTokensBox.values) { + if (token.enabled) { + try { + final tokenBalance = + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? + balance[token] ?? + SolanaBalance(0.0); + balance[token] = tokenBalance; + } catch (e) { + print('Error fetching spl token (${token.symbol}) balance ${e.toString()}'); + } + } else { + balance.remove(token); + } + } + } + + @override + Future? updateBalance() async => await _updateBalance(); + + List get splTokenCurrencies => splTokensBox.values.toList(); + + void addInitialTokens() { + final initialSPLTokens = DefaultSPLTokens().initialSPLTokens; + + for (var token in initialSPLTokens) { + splTokensBox.put(token.mintAddress, token); + } + } + + Future addSPLToken(SPLToken token) async { + await splTokensBox.put(token.mintAddress, token); + + if (token.enabled) { + final tokenBalance = + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? + balance[token] ?? + SolanaBalance(0.0); + + balance[token] = tokenBalance; + } else { + balance.remove(token); + } + } + + Future deleteSPLToken(SPLToken token) async { + await token.delete(); + + balance.remove(token); + await _removeTokenTransactionsInHistory(token); + _updateBalance(); + } + + Future _removeTokenTransactionsInHistory(SPLToken token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + + Future getSPLToken(String mintAddress) async { + // Convert SPL token mint address to public key + final Ed25519HDPublicKey mintPublicKey; + try { + mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); + } catch (_) { + return null; + } + + // Fetch token's metadata account + try { + final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey); + + if (token == null) { + return null; + } + + String? iconPath; + try { + iconPath = await _client.getIconImageFromTokenUri(token.uri); + } catch (_) {} + + String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), ''); + + return SPLToken.fromMetadata( + name: token.name, + mint: token.mint, + symbol: filteredTokenSymbol, + mintAddress: mintAddress, + iconPath: iconPath, + ); + } catch (e) { + return null; + } + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { + _updateBalance(); + _updateNativeSOLTransactions(); + _updateSPLTokenTransactions(); + }); + } + + Future signSolanaMessage(String message) async { + // Convert the message to bytes + final messageBytes = utf8.encode(message); + + // Sign the message bytes with the wallet's private key + final signature = await _walletKeyPair!.sign(messageBytes); + + // Convert the signature to a hexadecimal string + final hex = HEX.encode(signature.bytes); + + return hex; + } + + SolanaClient? get solanaClient => _client.getSolanaClient; +} diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart new file mode 100644 index 000000000..97a76fb99 --- /dev/null +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'solana_wallet_addresses.g.dart'; + +class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses; + +abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { + SolanaWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart new file mode 100644 index 000000000..881c30abd --- /dev/null +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class SolanaNewWalletCredentials extends WalletCredentials { + SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { + SolanaRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class SolanaRestoreWalletFromPrivateKey extends WalletCredentials { + SolanaRestoreWalletFromPrivateKey( + {required String name, + required String password, + required this.privateKey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String privateKey; +} diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart new file mode 100644 index 000000000..83370ff73 --- /dev/null +++ b/cw_solana/lib/solana_wallet_service.dart @@ -0,0 +1,137 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_solana/solana_mnemonics.dart'; +import 'package:cw_solana/solana_wallet.dart'; +import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; + +class SolanaWalletService extends WalletService { + SolanaWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = SolanaWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + WalletType getType() => WalletType.solana; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await SolanaWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await SolanaWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { + final wallet = SolanaWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw SolanaMnemonicIsIncorrectException(); + } + + final wallet = SolanaWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await SolanaWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_solana/lib/spl_token.dart b/cw_solana/lib/spl_token.dart new file mode 100644 index 000000000..a40eb0b86 --- /dev/null +++ b/cw_solana/lib/spl_token.dart @@ -0,0 +1,147 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; +import 'package:solana/metaplex.dart'; + +part 'spl_token.g.dart'; + +@HiveType(typeId: SPLToken.typeId) +class SPLToken extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + + @HiveField(1) + final String symbol; + + @HiveField(2) + final String mintAddress; + + @HiveField(3) + final int decimal; + + @HiveField(4, defaultValue: true) + bool _enabled; + + @HiveField(5) + final String mint; + + @HiveField(6) + final String? iconPath; + + @HiveField(7) + final String? tag; + + SPLToken({ + required this.name, + required this.symbol, + required this.mintAddress, + required this.decimal, + required this.mint, + this.iconPath, + this.tag = 'SOL', + bool enabled = true, + }) : _enabled = enabled, + super( + name: mint.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimal, + ); + + factory SPLToken.fromMetadata({ + required String name, + required String mint, + required String symbol, + required String mintAddress, + String? iconPath + }) { + return SPLToken( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: 0, + mint: mint, + iconPath: iconPath, + ); + } + + factory SPLToken.cryptoCurrency({ + required String name, + required String symbol, + required int decimals, + required String iconPath, + required String mint, + }) { + return SPLToken( + name: name, + symbol: symbol, + decimal: decimals, + mint: mint, + iconPath: iconPath, + mintAddress: '', + ); + } + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + SPLToken.copyWith(SPLToken other, String? icon, String? tag) + : name = other.name, + symbol = other.symbol, + mintAddress = other.mintAddress, + decimal = other.decimal, + _enabled = other.enabled, + mint = other.mint, + tag = other.tag, + iconPath = icon, + super( + title: other.symbol.toUpperCase(), + name: other.symbol.toLowerCase(), + decimals: other.decimal, + fullName: other.name, + tag: other.tag, + iconPath: icon, + ); + + static const typeId = SPL_TOKEN_TYPE_ID; + static const boxName = 'SPLTokens'; + + @override + bool operator ==(other) => + (other is SPLToken && other.mintAddress == mintAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => mintAddress.hashCode; +} + +class NFT extends SPLToken { + final ImageInfo? imageInfo; + + NFT( + String mint, + String name, + String symbol, + String mintAddress, + int decimal, + String iconPath, + this.imageInfo, + ) : super( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: decimal, + mint: mint, + iconPath: iconPath, + ); +} + +class ImageInfo { + final String uri; + final OffChainMetadata? data; + + const ImageInfo(this.uri, this.data); +} diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml new file mode 100644 index 000000000..7e24983bf --- /dev/null +++ b/cw_solana/pubspec.yaml @@ -0,0 +1,36 @@ +name: cw_solana +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + solana: ^0.30.1 + cw_core: + path: ../cw_core + http: ^1.1.0 + hive: ^2.2.3 + bip39: ^1.0.6 + mobx: ^2.3.0+1 + shared_preferences: ^2.0.15 + bip32: ^2.0.0 + hex: ^0.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg diff --git a/cw_solana/test/cw_solana_test.dart b/cw_solana/test/cw_solana_test.dart new file mode 100644 index 000000000..42a5d8bdf --- /dev/null +++ b/cw_solana/test/cw_solana_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_solana/cw_solana.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/howto-build-android.md b/howto-build-android.md index 4ef385b9f..a2a4e4d9f 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -5,10 +5,10 @@ The following are the system requirements to build CakeWallet for your Android device. ``` -Ubuntu >= 16.04 -Android SDK 28 +Ubuntu >= 20.04 +Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 2 or above +Flutter 3.10.x or earlier ``` ## Building CakeWallet on Android @@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Need to install flutter with version `3.x.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,9 +66,9 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.x.x, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 28) -[✓] Android Studio (version 4.0) +[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) +[✓] Android Studio (version 4.0 or higher) ``` ### 5. Generate a secure keystore for Android diff --git a/ios/Podfile b/ios/Podfile index b29d40484..51622ff10 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.0' source 'https://github.com/CocoaPods/Specs.git' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -44,6 +44,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', @@ -57,16 +58,16 @@ post_install do |installer| 'PERMISSION_CONTACTS=0', ## dart: PermissionGroup.camera - 'PERMISSION_CAMERA=0', + 'PERMISSION_CAMERA=1', ## dart: PermissionGroup.microphone - 'PERMISSION_MICROPHONE=0', + 'PERMISSION_MICROPHONE=1', ## dart: PermissionGroup.speech 'PERMISSION_SPEECH_RECOGNIZER=0', ## dart: PermissionGroup.photos - 'PERMISSION_PHOTOS=0', + 'PERMISSION_PHOTOS=1', ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 'PERMISSION_LOCATION=0', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 62074faed..67c0c9ee8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,10 +4,10 @@ PODS: - MTBBarcodeScanner - SwiftProtobuf - BigInt (5.2.0) - - connectivity (0.0.1): + - connectivity_plus (0.0.1): - Flutter - - Reachability - - CryptoSwift (1.6.0) + - ReachabilitySwift + - CryptoSwift (1.8.1) - cw_haven (0.0.1): - cw_haven/Boost (= 0.0.1) - cw_haven/Haven (= 0.0.1) @@ -63,8 +63,6 @@ PODS: - Flutter - device_display_brightness (0.0.1): - Flutter - - device_info (0.0.1): - - Flutter - device_info_plus (0.0.1): - Flutter - devicelocale (0.0.1): @@ -104,17 +102,20 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): + - flutter_inappwebview_ios (0.0.1): - Flutter - - flutter_inappwebview/Core (= 0.0.1) + - flutter_inappwebview_ios/Core (= 0.0.1) - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - flutter_mailer (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast - in_app_review (0.2.0): - Flutter - local_auth_ios (0.0.1): @@ -123,24 +124,27 @@ PODS: - OrderedSet (5.0.0) - package_info (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.0.4): + - permission_handler_apple (9.1.1): - Flutter - - platform_device_id (0.0.1): + - ReachabilitySwift (5.0.0) + - SDWebImage (5.18.11): + - SDWebImage/Core (= 5.18.11) + - SDWebImage/Core (5.18.11) + - sensitive_clipboard (0.0.1): - Flutter - - Reachability (3.2) - - SDWebImage (5.15.5): - - SDWebImage/Core (= 5.15.5) - - SDWebImage/Core (5.15.5) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - SwiftProtobuf (1.21.0) + - SwiftProtobuf (1.25.2) - SwiftyGif (5.4.4) + - Toast (4.1.0) - uni_links (0.0.1): - Flutter - UnstoppableDomainsResolution (4.0.0): @@ -148,37 +152,41 @@ PODS: - CryptoSwift - url_launcher_ios (0.0.1): - Flutter - - wakelock (0.0.1): + - wakelock_plus (0.0.1): + - Flutter + - workmanager (0.0.1): - Flutter DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - - connectivity (from `.symlinks/plugins/connectivity/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - - device_info (from `.symlinks/plugins/device_info/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) + - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - wakelock (from `.symlinks/plugins/wakelock/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -188,17 +196,18 @@ SPEC REPOS: - DKPhotoGallery - MTBBarcodeScanner - OrderedSet - - Reachability + - ReachabilitySwift - SDWebImage - SwiftProtobuf - SwiftyGif + - Toast - UnstoppableDomainsResolution EXTERNAL SOURCES: barcode_scan2: :path: ".symlinks/plugins/barcode_scan2/ios" - connectivity: - :path: ".symlinks/plugins/connectivity/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" cw_haven: :path: ".symlinks/plugins/cw_haven/ios" cw_monero: @@ -207,8 +216,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" - device_info: - :path: ".symlinks/plugins/device_info/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: @@ -217,73 +224,82 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_mailer: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" package_info: :path: ".symlinks/plugins/package_info/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/ios" + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - platform_device_id: - :path: ".symlinks/plugins/platform_device_id/ios" + sensitive_clipboard: + :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/ios" + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - wakelock: - :path: ".symlinks/plugins/wakelock/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + workmanager: + :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 BigInt: f668a80089607f521586bbe29513d708491ef2f7 - connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 - CryptoSwift: 562f8eceb40e80796fffc668b0cad9313284cfa6 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + CryptoSwift: b9c701d6f5011df23794dbf7f2e480a77835d83d cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 - device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d - local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 + local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 - permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce - platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 - Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 + sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c - SwiftProtobuf: afced68785854575756db965e9da52bbf3dc45e7 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + Toast: ec33c32b8688982cecc6348adeae667c1b9938da uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: ae71bdf0eb731a1ffc399c122f6aa4dea0cb5f6f +PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 50b9da031..7a8b99b49 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,8 @@ 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44A7192518EF8000B570ED /* decrypt.swift */; }; 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D68C8264854B60011B691 /* secRandom.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 20ED0868E1BD7E12278C0CB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B26E3F56D69167FBB1DC160A /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C663361C56EBB242598F609 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -23,13 +23,13 @@ 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; 0C9986A3251A932F00D566FD /* CryptoSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; + 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 20F67A1B2C2FCB2A3BB048C1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 501EA9286675DC8636978EA4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3C663361C56EBB242598F609 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wakeLock.swift; sourceTree = ""; }; - 61CAA8652B54F23356F7592A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -40,7 +40,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B26E3F56D69167FBB1DC160A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -48,7 +48,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 20ED0868E1BD7E12278C0CB3 /* Pods_Runner.framework in Frameworks */, + 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -59,7 +59,7 @@ isa = PBXGroup; children = ( 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, - B26E3F56D69167FBB1DC160A /* Pods_Runner.framework */, + 3C663361C56EBB242598F609 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -77,9 +77,9 @@ 84389F1A05D5860790D82820 /* Pods */ = { isa = PBXGroup; children = ( - 20F67A1B2C2FCB2A3BB048C1 /* Pods-Runner.debug.xcconfig */, - 501EA9286675DC8636978EA4 /* Pods-Runner.release.xcconfig */, - 61CAA8652B54F23356F7592A /* Pods-Runner.profile.xcconfig */, + 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */, + 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */, + AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -138,13 +138,13 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 0843B0813AFBAF53935AD24E /* [CP] Check Pods Manifest.lock */, + B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - DD8DB3179CA4E511F9954A6F /* [CP] Embed Pods Frameworks */, + 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -203,26 +203,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0843B0813AFBAF53935AD24E /* [CP] Check Pods Manifest.lock */ = { + 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -232,6 +227,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -255,21 +251,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - DD8DB3179CA4E511F9954A6F /* [CP] Embed Pods Frameworks */ = { + B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -376,7 +377,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -390,7 +391,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; @@ -522,7 +523,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -537,7 +538,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; @@ -560,7 +561,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -574,7 +575,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 401509606..acdfa4346 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Flutter import UnstoppableDomainsResolution +import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -16,6 +17,15 @@ import UnstoppableDomainsResolution UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } + WorkmanagerPlugin.setPluginRegistrantCallback { registry in + // Registry in this case is the FlutterEngine that is created in Workmanager's + // performFetchWithCompletionHandler or BGAppRefreshTask. + // This will make other plugins available during a background operation. + GeneratedPluginRegistrant.register(with: registry) + } + + WorkmanagerPlugin.registerTask(withIdentifier: "com.fotolockr.cakewallet.monero_sync_task") + makeSecure() let controller : FlutterViewController = window?.rootViewController as! FlutterViewController diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 6d834d4ee..df5cf1cc5 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,21 +1,25 @@ { "images" : [ { + "filename" : "Icon-App-40x40@1x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@2x 1.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" @@ -26,6 +30,7 @@ "size" : "40x40" }, { + "filename" : "Icon-App-40x40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" @@ -43,26 +48,31 @@ "size" : "60x60" }, { + "filename" : "Icon-App-20x20@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-40x40@1x 1.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" @@ -73,16 +83,19 @@ "size" : "40x40" }, { + "filename" : "Icon-App-76x76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "Icon-App-76x76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "Icon-App-83.5x83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..369d8d9a4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..65ed7f3db Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..fb14bfc55 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..d24d594a3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x 1.png new file mode 100644 index 000000000..07acd0a82 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..07acd0a82 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..bdc20091d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x 1.png new file mode 100644 index 000000000..65ed7f3db Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..65ed7f3db Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..80e78be41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..e06998b67 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..78a2ccfb1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0ba8d647c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 29cd24cb4..a7f208870 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.fotolockr.cakewallet.monero_sync_task + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -32,6 +36,10 @@ cakewallet + + CFBundleTypeRole + Editor + CFBundleTypeRole Editor @@ -92,6 +100,96 @@ litecoin-wallet + + CFBundleTypeRole + Editor + CFBundleURLName + ethereum + CFBundleURLSchemes + + ethereum + + + + CFBundleTypeRole + Viewer + CFBundleURLName + ethereum-wallet + CFBundleURLSchemes + + ethereum-wallet + + + + CFBundleTypeRole + Editor + CFBundleURLName + nano + CFBundleURLSchemes + + nano + + + + CFBundleTypeRole + Viewer + CFBundleURLName + nano-wallet + CFBundleURLSchemes + + nano-wallet + + + + CFBundleTypeRole + Editor + CFBundleURLName + bitcoincash + CFBundleURLSchemes + + bitcoincash + + + + CFBundleTypeRole + Viewer + CFBundleURLName + bitcoincash-wallet + CFBundleURLSchemes + + bitcoincash-wallet + + + + CFBundleTypeRole + Editor + CFBundleURLName + polygon + CFBundleURLSchemes + + polygon + + + + CFBundleTypeRole + Viewer + CFBundleURLName + polygon-wallet + CFBundleURLSchemes + + polygon-wallet + + + + CFBundleTypeRole + Viewer + CFBundleURLName + solana-wallet + CFBundleURLSchemes + + solana-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) @@ -103,7 +201,7 @@ NSCameraUsageDescription - Used for scan QR code + Used for scanning QR code and can be used to capture images for identification purposes by third-party providers. NSDocumentsFolderUsageDescription We need access to documents folder for getting access to open/save backup file NSFaceIDUsageDescription @@ -113,6 +211,7 @@ UIBackgroundModes fetch + processing remote-notification UILaunchStoryboardName diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index bc6abc6e2..e46499407 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -182,6 +182,8 @@ class AnonPayApi { switch (currency) { case CryptoCurrency.usdt: return CryptoCurrency.btc.title.toLowerCase(); + case CryptoCurrency.eth: + return 'ERC20'; default: return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet'; } diff --git a/lib/anonpay/anonpay_invoice_info.dart b/lib/anonpay/anonpay_invoice_info.dart index 89613224e..bd6776d00 100644 --- a/lib/anonpay/anonpay_invoice_info.dart +++ b/lib/anonpay/anonpay_invoice_info.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/keyable.dart'; import 'package:hive/hive.dart'; @@ -35,7 +36,7 @@ class AnonpayInvoiceInfo extends HiveObject with Keyable implements AnonpayInfoB @HiveField(13) final String provider; - static const typeId = 10; + static const typeId = ANONPAY_INVOICE_INFO_TYPE_ID; static const boxName = 'AnonpayInvoiceInfo'; AnonpayInvoiceInfo({ diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630ecf27f..7ae01df1c 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -1,163 +1,443 @@ part of 'bitcoin.dart'; class CWBitcoin extends Bitcoin { - @override - TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; - - @override - WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({ + WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, - required String password}) - => BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); - - @override - WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({ - required String name, required String password, - required String wif, - WalletInfo? walletInfo}) - => BitcoinRestoreWalletFromWIFCredentials(name: name, password: password, wif: wif, walletInfo: walletInfo); - - @override - WalletCredentials createBitcoinNewWalletCredentials({ - required String name, - WalletInfo? walletInfo}) - => BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); - - @override - List getWordList() => wordlist; - - @override - Map getWalletKeys(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - final keys = bitcoinWallet.keys; - - return { - 'wif': keys.wif, - 'privateKey': keys.privateKey, - 'publicKey': keys.publicKey - }; - } - - @override - List getTransactionPriorities() - => BitcoinTransactionPriority.all; - - List getLitecoinTransactionPriorities() - => LitecoinTransactionPriority.all; - - @override - TransactionPriority deserializeBitcoinTransactionPriority(int raw) - => BitcoinTransactionPriority.deserialize(raw: raw); - - @override - TransactionPriority deserializeLitecoinTransactionPriority(int raw) - => LitecoinTransactionPriority.deserialize(raw: raw); - - @override - int getFeeRate(Object wallet, TransactionPriority priority) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.feeRate(priority); - } - - @override - Future generateNewAddress(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.walletAddresses.generateNewAddress(); - } - - @override - Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}) - => BitcoinTransactionCredentials( - outputs.map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - - @override - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}) - => BitcoinTransactionCredentials( - outputs, - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - - @override - List getAddresses(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.addresses - .map((BitcoinAddressRecord addr) => addr.address) - .toList(); - } - - @override - String getAddress(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.address; - } - - @override - String formatterBitcoinAmountToString({required int amount}) - => bitcoinAmountToString(amount: amount); - - @override - double formatterBitcoinAmountToDouble({required int amount}) - => bitcoinAmountToDouble(amount: amount); - - @override - int formatterStringDoubleToBitcoinAmount(String amount) - => stringDoubleToBitcoinAmount(amount); + required DerivationType derivationType, + required String derivationPath, + String? passphrase, + }) => + BitcoinRestoreWalletFromSeedCredentials( + name: name, + mnemonic: mnemonic, + password: password, + derivationType: derivationType, + derivationPath: derivationPath, + passphrase: passphrase, + ); @override - String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) - => (priority as BitcoinTransactionPriority).labelWithRate(rate); - - @override - List getUnspents(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins - .map((BitcoinUnspent bitcoinUnspent) => Unspent( - bitcoinUnspent.address.address, - bitcoinUnspent.hash, - bitcoinUnspent.value, - bitcoinUnspent.vout)) - .toList(); - } - - void updateUnspents(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.updateUnspent(); - } - - WalletService createBitcoinWalletService(Box walletInfoSource, Box unspentCoinSource) { - return BitcoinWalletService(walletInfoSource, unspentCoinSource); - } - - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource); - } - - @override - TransactionPriority getBitcoinTransactionPriorityMedium() - => BitcoinTransactionPriority.medium; + WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( + {required String name, + required String password, + required String wif, + WalletInfo? walletInfo}) => + BitcoinRestoreWalletFromWIFCredentials( + name: name, password: password, wif: wif, walletInfo: walletInfo); @override - TransactionPriority getLitecoinTransactionPriorityMedium() - => LitecoinTransactionPriority.medium; + WalletCredentials createBitcoinNewWalletCredentials( + {required String name, WalletInfo? walletInfo}) => + BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); @override - TransactionPriority getBitcoinTransactionPrioritySlow() - => BitcoinTransactionPriority.slow; - + TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; + @override - TransactionPriority getLitecoinTransactionPrioritySlow() - => LitecoinTransactionPriority.slow; -} \ No newline at end of file + List getWordList() => wordlist; + + @override + Map getWalletKeys(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + final keys = bitcoinWallet.keys; + + return { + 'wif': keys.wif, + 'privateKey': keys.privateKey, + 'publicKey': keys.publicKey + }; + } + + @override + List getTransactionPriorities() => BitcoinTransactionPriority.all; + + @override + List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; + + @override + TransactionPriority deserializeBitcoinTransactionPriority(int raw) => + BitcoinTransactionPriority.deserialize(raw: raw); + + @override + TransactionPriority deserializeLitecoinTransactionPriority(int raw) => + LitecoinTransactionPriority.deserialize(raw: raw); + + @override + int getFeeRate(Object wallet, TransactionPriority priority) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeRate(priority); + } + + @override + Future generateNewAddress(Object wallet, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.walletAddresses.generateNewAddress(label: label); + await wallet.save(); + } + + @override + Future updateAddress(Object wallet, String address, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + bitcoinWallet.walletAddresses.updateAddress(address, label); + await wallet.save(); + } + + @override + Object createBitcoinTransactionCredentials(List outputs, + {required TransactionPriority priority, int? feeRate}) { + final bitcoinFeeRate = + priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; + return BitcoinTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: bitcoinFeeRate); + } + + @override + Object createBitcoinTransactionCredentialsRaw(List outputs, + {TransactionPriority? priority, required int feeRate}) => + BitcoinTransactionCredentials(outputs, + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); + + @override + List getAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.addressesByReceiveType + .map((BitcoinAddressRecord addr) => addr.address) + .toList(); + } + + @override + @computed + List getSubAddresses(Object wallet) { + final electrumWallet = wallet as ElectrumWallet; + return electrumWallet.walletAddresses.addressesByReceiveType + .map((BitcoinAddressRecord addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + + @override + Future estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async { + try { + final sk = ECPrivate.random(); + final electrumWallet = wallet as ElectrumWallet; + + if (wallet.type == WalletType.bitcoinCash) { + final p2pkhAddr = sk.getPublic().toP2pkhAddress(); + final estimatedTx = await electrumWallet.estimateSendAllTx( + [BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)], + getFeeRate(wallet, priority as BitcoinCashTransactionPriority), + ); + + return estimatedTx.amount; + } + + final p2shAddr = sk.getPublic().toP2pkhInP2sh(); + final estimatedTx = await electrumWallet.estimateSendAllTx( + [BitcoinOutput(address: p2shAddr, value: BigInt.zero)], + getFeeRate( + wallet, + wallet.type == WalletType.litecoin + ? priority as LitecoinTransactionPriority + : priority as BitcoinTransactionPriority, + ), + ); + + return estimatedTx.amount; + } catch (_) { + return 0; + } + } + + @override + String getAddress(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.address; + } + + @override + String formatterBitcoinAmountToString({required int amount}) => + bitcoinAmountToString(amount: amount); + + @override + double formatterBitcoinAmountToDouble({required int amount}) => + bitcoinAmountToDouble(amount: amount); + + @override + int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); + + @override + String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, + {int? customRate}) => + (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate); + + @override + List getUnspents(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.unspentCoins; + } + + Future updateUnspents(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.updateUnspent(); + } + + WalletService createBitcoinWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return BitcoinWalletService(walletInfoSource, unspentCoinSource); + } + + WalletService createLitecoinWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource); + } + + @override + TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium; + + @override + TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom; + + @override + TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; + + @override + TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow; + + @override + TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; + + @override + Future setAddressType(Object wallet, dynamic option) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.walletAddresses.setAddressType(option as BitcoinAddressType); + } + + @override + ReceivePageOption getSelectedAddressType(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType); + } + + @override + List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + + @override + BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { + switch (option) { + case BitcoinReceivePageOption.p2pkh: + return P2pkhAddressType.p2pkh; + case BitcoinReceivePageOption.p2sh: + return P2shAddressType.p2wpkhInP2sh; + case BitcoinReceivePageOption.p2tr: + return SegwitAddresType.p2tr; + case BitcoinReceivePageOption.p2wsh: + return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.p2wpkh: + default: + return SegwitAddresType.p2wpkh; + } + } + + @override + Future> compareDerivationMethods( + {required String mnemonic, required Node node}) async { + if (await checkIfMnemonicIsElectrum2(mnemonic)) { + return [DerivationType.electrum]; + } + + return [DerivationType.bip39, DerivationType.electrum]; + } + + int _countOccurrences(String str, String charToCount) { + int count = 0; + for (int i = 0; i < str.length; i++) { + if (str[i] == charToCount) { + count++; + } + } + return count; + } + + @override + Future> getDerivationsFromMnemonic({ + required String mnemonic, + required Node node, + String? passphrase, + }) async { + List list = []; + + List types = await compareDerivationMethods(mnemonic: mnemonic, node: node); + if (types.length == 1 && types.first == DerivationType.electrum) { + return [ + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'/0", + description: "Electrum", + scriptType: "p2wpkh", + ) + ]; + } + + final electrumClient = ElectrumClient(); + await electrumClient.connectToUri(node.uri); + + late BasedUtxoNetwork network; + btc.NetworkType networkType; + switch (node.type) { + case WalletType.litecoin: + network = LitecoinNetwork.mainnet; + networkType = litecoinNetwork; + break; + case WalletType.bitcoin: + default: + network = BitcoinNetwork.mainnet; + networkType = btc.bitcoin; + break; + } + + for (DerivationType dType in electrum_derivations.keys) { + late Uint8List seedBytes; + if (dType == DerivationType.electrum) { + seedBytes = await mnemonicToSeedBytes(mnemonic); + } else if (dType == DerivationType.bip39) { + seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); + } + + for (DerivationInfo dInfo in electrum_derivations[dType]!) { + try { + DerivationInfo dInfoCopy = DerivationInfo( + derivationType: dInfo.derivationType, + derivationPath: dInfo.derivationPath, + description: dInfo.description, + scriptType: dInfo.scriptType, + ); + + String derivationPath = dInfoCopy.derivationPath!; + int derivationDepth = _countOccurrences(derivationPath, "/"); + + // the correct derivation depth is dependant on the derivation type: + // the derivation paths defined in electrum_derivations are at the ROOT level, i.e.: + // electrum's format doesn't specify subaddresses, just subaccounts: + + // for BIP44 + if (derivationDepth == 3) { + // we add "/0/0" so that we generate account 0, index 0 and correctly get balance + derivationPath += "/0/0"; + // we don't support sub-ACCOUNTS in bitcoin like we do monero, and so the path dInfoCopy + // expects should be ACCOUNT 0, index unspecified: + dInfoCopy.derivationPath = dInfoCopy.derivationPath! + "/0"; + } + + // var hd = bip32.BIP32.fromSeed(seedBytes).derivePath(derivationPath); + final hd = btc.HDWallet.fromSeed( + seedBytes, + network: networkType, + ).derivePath(derivationPath); + + String? address; + switch (dInfoCopy.scriptType) { + case "p2wpkh": + address = generateP2WPKHAddress(hd: hd, network: network); + break; + case "p2pkh": + address = generateP2PKHAddress(hd: hd, network: network); + break; + case "p2wpkh-p2sh": + address = generateP2SHAddress(hd: hd, network: network); + break; + default: + continue; + } + + final sh = scriptHash(address, network: network); + final history = await electrumClient.getHistory(sh); + + final balance = await electrumClient.getBalance(sh); + dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.address = address; + dInfoCopy.transactionsCount = history.length; + + list.add(dInfoCopy); + } catch (e) { + print(e); + } + } + } + + // sort the list such that derivations with the most transactions are first: + list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; + } + + @override + bool hasTaprootInput(PendingTransaction pendingTransaction) { + return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs; + } + + @override + Future replaceByFee( + Object wallet, String transactionHash, String fee) async { + final bitcoinWallet = wallet as ElectrumWallet; + return await bitcoinWallet.replaceByFee(transactionHash, int.parse(fee)); + } + + @override + Future canReplaceByFee(Object wallet, String transactionHash) async { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.canReplaceByFee(transactionHash); + } + + @override + Future isChangeSufficientForFee(Object wallet, String txId, String newFee) async { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.isChangeSufficientForFee(txId, int.parse(newFee)); + } + + @override + int getFeeAmountForPriority( + Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, + {int? size}) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeAmountForPriority( + priority as BitcoinTransactionPriority, inputsCount, outputsCount); + } + + @override + int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, + {int? outputsCount, int? size}) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.calculateEstimatedFeeWithFeeRate( + feeRate, + amount, + outputsCount: outputsCount, + size: size, + ); + } + + @override + int getMaxCustomFeeRate(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 1.1).round(); + } +} diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart new file mode 100644 index 000000000..6e169209f --- /dev/null +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -0,0 +1,39 @@ +part of 'bitcoin_cash.dart'; + +class CWBitcoinCash extends BitcoinCash { + @override + String getCashAddrFormat(String address) => AddressUtils.getCashAddrFormat(address); + + @override + WalletService createBitcoinCashWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return BitcoinCashWalletService(walletInfoSource, unspentCoinSource); + } + + @override + WalletCredentials createBitcoinCashNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + BitcoinCashRestoreWalletFromSeedCredentials( + name: name, mnemonic: mnemonic, password: password); + + @override + TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => + BitcoinCashTransactionPriority.deserialize(raw: raw); + + @override + TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; + + @override + List getTransactionPriorities() => BitcoinCashTransactionPriority.all; + + @override + TransactionPriority getBitcoinCashTransactionPrioritySlow() => + BitcoinCashTransactionPriority.slow; +} diff --git a/lib/buy/buy_exception.dart b/lib/buy/buy_exception.dart index edc6a7db0..c201b3b2d 100644 --- a/lib/buy/buy_exception.dart +++ b/lib/buy/buy_exception.dart @@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; class BuyException implements Exception { - BuyException({required this.description, required this.text}); + BuyException({required this.title, required this.content}); - final BuyProviderDescription description; - final String text; + final String title; + final String content; @override - String toString() => '${description.title}: $text'; + String toString() => '$title: $content'; } \ No newline at end of file diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 10a13ed94..4e4c113f4 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -1,27 +1,33 @@ import 'package:cake_wallet/buy/buy_amount.dart'; -import 'package:cake_wallet/buy/buy_provider_description.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; abstract class BuyProvider { - BuyProvider({required this.wallet, required this.isTestEnvironment}); + BuyProvider({ + required this.wallet, + required this.isTestEnvironment, + }); final WalletBase wallet; final bool isTestEnvironment; String get title; - BuyProviderDescription get description; - String get trackUrl; - WalletType get walletType => wallet.type; - String get walletAddress => wallet.walletAddresses.address; - String get walletId => wallet.id; + String get providerDescription; + + String get lightIcon; + + String get darkIcon; @override String toString() => title; - Future requestUrl(String amount, String sourceCurrency); - Future findOrderById(String id); - Future calculateAmount(String amount, String sourceCurrency); -} \ No newline at end of file + Future launchProvider(BuildContext context, bool? isBuyAction); + + Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); + + Future findOrderById(String id) => throw UnimplementedError(); + + Future calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError(); +} diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart new file mode 100644 index 000000000..bf67edd23 --- /dev/null +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -0,0 +1,174 @@ +import 'dart:convert'; + +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class DFXBuyProvider extends BuyProvider { + DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment); + + static const _baseUrl = 'api.dfx.swiss'; + // static const _signMessagePath = '/v1/auth/signMessage'; + static const _authPath = '/v1/auth'; + static const walletName = 'CakeWallet'; + + @override + String get title => 'DFX.swiss'; + + @override + String get providerDescription => S.current.dfx_option_description; + + @override + String get lightIcon => 'assets/images/dfx_light.png'; + + @override + String get darkIcon => 'assets/images/dfx_dark.png'; + + String get assetOut { + switch (wallet.type) { + case WalletType.bitcoin: + return 'BTC'; + case WalletType.bitcoinCash: + return 'BCH'; + case WalletType.litecoin: + return 'LTC'; + case WalletType.monero: + return 'XMR'; + case WalletType.ethereum: + return 'ETH'; + case WalletType.polygon: + return 'MATIC'; + default: + throw Exception("WalletType is not available for DFX ${wallet.type}"); + } + } + + String get blockchain { + switch (wallet.type) { + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.litecoin: + return 'Bitcoin'; + case WalletType.monero: + return 'Monero'; + case WalletType.ethereum: + return 'Ethereum'; + case WalletType.polygon: + return 'Polygon'; + default: + throw Exception("WalletType is not available for DFX ${wallet.type}"); + } + } + + String get walletAddress => + wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address; + + Future getSignMessage() async => + "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; + + // // Lets keep this just in case, but we can avoid this API Call + // Future getSignMessage() async { + // final uri = Uri.https(_baseUrl, _signMessagePath, {'address': walletAddress}); + // + // final response = await http.get(uri, headers: {'accept': 'application/json'}); + // + // if (response.statusCode == 200) { + // final responseBody = jsonDecode(response.body); + // return responseBody['message'] as String; + // } else { + // throw Exception( + // 'Failed to get sign message. Status: ${response.statusCode} ${response.body}'); + // } + // } + + Future auth() async { + final signMessage = getSignature(await getSignMessage()); + + final requestBody = jsonEncode({ + 'wallet': walletName, + 'address': walletAddress, + 'signature': signMessage, + }); + + final uri = Uri.https(_baseUrl, _authPath); + var response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: requestBody, + ); + + if (response.statusCode == 201) { + final responseBody = jsonDecode(response.body); + return responseBody['accessToken'] as String; + } else if (response.statusCode == 403) { + final responseBody = jsonDecode(response.body); + final message = responseBody['message'] ?? 'Service unavailable in your country'; + throw Exception(message); + } else { + throw Exception('Failed to sign up. Status: ${response.statusCode} ${response.body}'); + } + } + + String getSignature(String message) { + switch (wallet.type) { + case WalletType.ethereum: + case WalletType.polygon: + return wallet.signMessage(message); + case WalletType.monero: + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + return wallet.signMessage(message, address: walletAddress); + default: + throw Exception("WalletType is not available for DFX ${wallet.type}"); + } + } + + @override + Future launchProvider(BuildContext context, bool? isBuyAction) async { + try { + final assetOut = this.assetOut; + final blockchain = this.blockchain; + final actionType = isBuyAction == true ? '/buy' : '/sell'; + + final accessToken = await auth(); + + final uri = Uri.https('services.dfx.swiss', actionType, { + 'session': accessToken, + 'lang': 'en', + 'asset-out': assetOut, + 'blockchain': blockchain, + 'asset-in': 'EUR', + }); + + if (await canLaunchUrl(uri)) { + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]); + } else { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } else { + throw Exception('Could not launch URL'); + } + } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: "DFX.swiss", + alertContent: S.of(context).buy_provider_unavailable + ': $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + } +} diff --git a/lib/buy/moonpay/moonpay_buy_provider.dart b/lib/buy/moonpay/moonpay_buy_provider.dart deleted file mode 100644 index 372b6d6cc..000000000 --- a/lib/buy/moonpay/moonpay_buy_provider.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'dart:convert'; -import 'package:crypto/crypto.dart'; -import 'package:cake_wallet/buy/buy_exception.dart'; -import 'package:http/http.dart'; -import 'package:cake_wallet/buy/buy_amount.dart'; -import 'package:cake_wallet/buy/buy_provider.dart'; -import 'package:cake_wallet/buy/buy_provider_description.dart'; -import 'package:cake_wallet/buy/order.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cw_core/crypto_currency.dart'; - -class MoonPaySellProvider { - MoonPaySellProvider({this.isTest = false}) - : baseUrl = isTest ? _baseTestUrl : _baseProductUrl; - - static const _baseTestUrl = 'sell-staging.moonpay.com'; - static const _baseProductUrl = 'sell.moonpay.com'; - static String get _apiKey => secrets.moonPayApiKey; - static String get _secretKey => secrets.moonPaySecretKey; - final bool isTest; - final String baseUrl; - - Future requestUrl({required CryptoCurrency currency, required String refundWalletAddress}) async { - final originalUri = Uri.https( - baseUrl, '', { - 'apiKey': _apiKey, - 'defaultBaseCurrencyCode': currency.toString().toLowerCase(), - 'refundWalletAddress': refundWalletAddress - }); - final messageBytes = utf8.encode('?${originalUri.query}'); - final key = utf8.encode(_secretKey); - final hmac = Hmac(sha256, key); - final digest = hmac.convert(messageBytes); - final signature = base64.encode(digest.bytes); - - if (isTest) { - return originalUri; - } - - final query = Map.from(originalUri.queryParameters); - query['signature'] = signature; - final signedUri = originalUri.replace(queryParameters: query); - return signedUri; - } -} - -class MoonPayBuyProvider extends BuyProvider { - MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl, - super(wallet: wallet, isTestEnvironment: isTestEnvironment); - - static const _baseTestUrl = 'https://buy-staging.moonpay.com'; - static const _baseProductUrl = 'https://buy.moonpay.com'; - static const _apiUrl = 'https://api.moonpay.com'; - static const _currenciesSuffix = '/v3/currencies'; - static const _quoteSuffix = '/buy_quote'; - static const _transactionsSuffix = '/v1/transactions'; - static const _ipAddressSuffix = '/v4/ip_address'; - static const _apiKey = secrets.moonPayApiKey; - static const _secretKey = secrets.moonPaySecretKey; - - @override - String get title => 'MoonPay'; - - @override - BuyProviderDescription get description => BuyProviderDescription.moonPay; - - String get currencyCode => - walletTypeToCryptoCurrency(walletType).title.toLowerCase(); - - @override - String get trackUrl => baseUrl + '/transaction_receipt?transactionId='; - - String baseUrl; - - @override - Future requestUrl(String amount, String sourceCurrency) async { - final enabledPaymentMethods = - 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay' - '%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment'; - - final suffix = '?apiKey=' + _apiKey + '¤cyCode=' + - currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods + - '&walletAddress=' + walletAddress + - '&baseCurrencyCode=' + sourceCurrency.toLowerCase() + - '&baseCurrencyAmount=' + amount + '&lockAmount=true' + - '&showAllCurrencies=false' + '&showWalletAddressForm=false'; - - final originalUrl = baseUrl + suffix; - - final messageBytes = utf8.encode(suffix); - final key = utf8.encode(_secretKey); - final hmac = Hmac(sha256, key); - final digest = hmac.convert(messageBytes); - final signature = base64.encode(digest.bytes); - final urlWithSignature = originalUrl + - '&signature=${Uri.encodeComponent(signature)}'; - - return isTestEnvironment ? originalUrl : urlWithSignature; - } - - @override - Future calculateAmount(String amount, String sourceCurrency) async { - final url = _apiUrl + _currenciesSuffix + '/$currencyCode' + - _quoteSuffix + '/?apiKey=' + _apiKey + - '&baseCurrencyAmount=' + amount + - '&baseCurrencyCode=' + sourceCurrency.toLowerCase(); - final uri = Uri.parse(url); - final response = await get(uri); - - if (response.statusCode != 200) { - throw BuyException( - description: description, - text: 'Quote is not found!'); - } - - final responseJSON = json.decode(response.body) as Map; - final sourceAmount = responseJSON['totalAmount'] as double; - final destAmount = responseJSON['quoteCurrencyAmount'] as double; - final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int; - - return BuyAmount( - sourceAmount: sourceAmount, - destAmount: destAmount, - minAmount: minSourceAmount); - } - - @override - Future findOrderById(String id) async { - final url = _apiUrl + _transactionsSuffix + '/$id' + - '?apiKey=' + _apiKey; - final uri = Uri.parse(url); - final response = await get(uri); - - if (response.statusCode != 200) { - throw BuyException( - description: description, - text: 'Transaction $id is not found!'); - } - - final responseJSON = json.decode(response.body) as Map; - final status = responseJSON['status'] as String; - final state = TradeState.deserialize(raw: status); - final createdAtRaw = responseJSON['createdAt'] as String; - final createdAt = DateTime.parse(createdAtRaw).toLocal(); - final amount = responseJSON['quoteCurrencyAmount'] as double; - - return Order( - id: id, - provider: description, - transferId: id, - state: state, - createdAt: createdAt, - amount: amount.toString(), - receiveAddress: walletAddress, - walletId: walletId - ); - } - - static Future onEnabled() async { - final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; - var isBuyEnable = false; - final uri = Uri.parse(url); - final response = await get(uri); - - try { - final responseJSON = json.decode(response.body) as Map; - isBuyEnable = responseJSON['isBuyAllowed'] as bool; - } catch (e) { - isBuyEnable = false; - print(e.toString()); - } - - return isBuyEnable; - } -} \ No newline at end of file diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart new file mode 100644 index 000000000..fea8fdabd --- /dev/null +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -0,0 +1,306 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_exception.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:crypto/crypto.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MoonPayProvider extends BuyProvider { + MoonPayProvider({ + required SettingsStore settingsStore, + required WalletBase wallet, + bool isTestEnvironment = false, + }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl, + baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl, + this._settingsStore = settingsStore, + super(wallet: wallet, isTestEnvironment: isTestEnvironment); + + final SettingsStore _settingsStore; + + static const _baseSellTestUrl = 'sell-sandbox.moonpay.com'; + static const _baseSellProductUrl = 'sell.moonpay.com'; + static const _baseBuyTestUrl = 'buy-staging.moonpay.com'; + static const _baseBuyProductUrl = 'buy.moonpay.com'; + static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; + static const _apiUrl = 'https://api.moonpay.com'; + + @override + String get providerDescription => + 'MoonPay offers a fast and simple way to buy and sell cryptocurrencies'; + + @override + String get title => 'MoonPay'; + + @override + String get lightIcon => 'assets/images/moonpay_light.png'; + + @override + String get darkIcon => 'assets/images/moonpay_dark.png'; + + static String themeToMoonPayTheme(ThemeBase theme) { + switch (theme.type) { + case ThemeType.bright: + case ThemeType.light: + return 'light'; + case ThemeType.dark: + return 'dark'; + } + } + + static String get _apiKey => secrets.moonPayApiKey; + + final String baseBuyUrl; + final String baseSellUrl; + + String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); + + String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; + + static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; + + Future getMoonpaySignature(String query) async { + final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); + + final response = await post( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': _exchangeHelperApiKey, + }, + body: json.encode({'query': query}), + ); + + if (response.statusCode == 200) { + return (jsonDecode(response.body) as Map)['signature'] as String; + } else { + throw Exception( + 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}'); + } + } + + Future requestSellMoonPayUrl({ + required CryptoCurrency currency, + required String refundWalletAddress, + required SettingsStore settingsStore, + }) async { + final params = { + 'theme': themeToMoonPayTheme(settingsStore.currentTheme), + 'language': settingsStore.languageCode, + 'colorCode': settingsStore.currentTheme.type == ThemeType.dark + ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' + : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', + 'defaultCurrencyCode': _normalizeCurrency(currency), + 'refundWalletAddress': refundWalletAddress, + }; + + if (_apiKey.isNotEmpty) { + params['apiKey'] = _apiKey; + } + + final originalUri = Uri.https( + baseSellUrl, + '', + params, + ); + + if (isTestEnvironment) { + return originalUri; + } + + final signature = await getMoonpaySignature('?${originalUri.query}'); + + final query = Map.from(originalUri.queryParameters); + query['signature'] = signature; + final signedUri = originalUri.replace(queryParameters: query); + return signedUri; + } + + // BUY: + static const _currenciesSuffix = '/v3/currencies'; + static const _quoteSuffix = '/buy_quote'; + static const _transactionsSuffix = '/v1/transactions'; + static const _ipAddressSuffix = '/v4/ip_address'; + + Future requestBuyMoonPayUrl({ + required CryptoCurrency currency, + required SettingsStore settingsStore, + required String walletAddress, + String? amount, + }) async { + final params = { + 'theme': themeToMoonPayTheme(settingsStore.currentTheme), + 'language': settingsStore.languageCode, + 'colorCode': settingsStore.currentTheme.type == ThemeType.dark + ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' + : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', + 'defaultCurrencyCode': _normalizeCurrency(currency), + 'baseCurrencyCode': _normalizeCurrency(currency), + 'baseCurrencyAmount': amount ?? '0', + 'currencyCode': currencyCode, + 'walletAddress': walletAddress, + 'lockAmount': 'false', + 'showAllCurrencies': 'false', + 'showWalletAddressForm': 'false', + 'enabledPaymentMethods': + 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + }; + + if (_apiKey.isNotEmpty) { + params['apiKey'] = _apiKey; + } + + final originalUri = Uri.https( + baseBuyUrl, + '', + params, + ); + + if (isTestEnvironment) { + return originalUri; + } + + final signature = await getMoonpaySignature('?${originalUri.query}'); + final query = Map.from(originalUri.queryParameters); + query['signature'] = signature; + final signedUri = originalUri.replace(queryParameters: query); + return signedUri; + } + + Future calculateAmount(String amount, String sourceCurrency) async { + final url = _apiUrl + + _currenciesSuffix + + '/$currencyCode' + + _quoteSuffix + + '/?apiKey=' + + _apiKey + + '&baseCurrencyAmount=' + + amount + + '&baseCurrencyCode=' + + sourceCurrency.toLowerCase(); + final uri = Uri.parse(url); + final response = await get(uri); + + if (response.statusCode != 200) { + throw BuyException(title: providerDescription, content: 'Quote is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final sourceAmount = responseJSON['totalAmount'] as double; + final destAmount = responseJSON['quoteCurrencyAmount'] as double; + final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int; + + return BuyAmount( + sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount); + } + + Future findOrderById(String id) async { + final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; + final uri = Uri.parse(url); + final response = await get(uri); + + if (response.statusCode != 200) { + throw BuyException(title: providerDescription, content: 'Transaction $id is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final status = responseJSON['status'] as String; + final state = TradeState.deserialize(raw: status); + final createdAtRaw = responseJSON['createdAt'] as String; + final createdAt = DateTime.parse(createdAtRaw).toLocal(); + final amount = responseJSON['quoteCurrencyAmount'] as double; + + return Order( + id: id, + provider: BuyProviderDescription.moonPay, + transferId: id, + state: state, + createdAt: createdAt, + amount: amount.toString(), + receiveAddress: wallet.walletAddresses.address, + walletId: wallet.id); + } + + static Future onEnabled() async { + final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; + var isBuyEnable = false; + final uri = Uri.parse(url); + final response = await get(uri); + + try { + final responseJSON = json.decode(response.body) as Map; + isBuyEnable = responseJSON['isBuyAllowed'] as bool; + } catch (e) { + isBuyEnable = false; + print(e.toString()); + } + + return isBuyEnable; + } + + @override + Future launchProvider(BuildContext context, bool? isBuyAction) async { + try { + late final Uri uri; + if (isBuyAction ?? true) { + uri = await requestBuyMoonPayUrl( + currency: wallet.currency, + walletAddress: wallet.walletAddresses.address, + settingsStore: _settingsStore, + ); + } else { + uri = await requestSellMoonPayUrl( + currency: wallet.currency, + refundWalletAddress: wallet.walletAddresses.address, + settingsStore: _settingsStore, + ); + } + + if (await canLaunchUrl(uri)) { + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]); + } else { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } else { + throw Exception('Could not launch URL'); + } + } catch (e) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'MoonPay', + alertContent: 'The MoonPay service is currently unavailable: $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + if (currency == CryptoCurrency.maticpoly) { + return "MATIC_POLYGON"; + } + + return currency.toString().toLowerCase(); + } +} diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index faf2e6da7..3819f074d 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -1,31 +1,58 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; -class OnRamperBuyProvider { - OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) - : this._settingsStore = settingsStore, - this._wallet = wallet; - - final SettingsStore _settingsStore; - final WalletBase _wallet; +class OnRamperBuyProvider extends BuyProvider { + OnRamperBuyProvider(this._settingsStore, + {required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment); static const _baseUrl = 'buy.onramper.com'; + final SettingsStore _settingsStore; + + @override + String get title => 'Onramper'; + + @override + String get providerDescription => S.current.onramper_option_description; + + @override + String get lightIcon => 'assets/images/onramper_light.png'; + + @override + String get darkIcon => 'assets/images/onramper_dark.png'; + String get _apiKey => secrets.onramperApiKey; String get _normalizeCryptoCurrency { - switch (_wallet.currency) { + switch (wallet.currency) { case CryptoCurrency.ltc: return "LTC_LITECOIN"; + case CryptoCurrency.xmr: + return "XMR_MONERO"; + case CryptoCurrency.bch: + return "BCH_BITCOINCASH"; + case CryptoCurrency.nano: + return "XNO_NANO"; default: - return _wallet.currency.title; + return wallet.currency.title; } } - Uri requestUrl() { + String getColorStr(Color color) { + return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); + } + + Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) { String primaryColor, secondaryColor, primaryTextColor, @@ -33,47 +60,45 @@ class OnRamperBuyProvider { containerColor, cardColor; - switch (_settingsStore.currentTheme.type) { - case ThemeType.bright: - primaryColor = '815dfbff'; - secondaryColor = 'ffffff'; - primaryTextColor = '141519'; - secondaryTextColor = '6b6f80'; - containerColor = 'ffffff'; - cardColor = 'f2f0faff'; - break; - case ThemeType.light: - primaryColor = '2194ffff'; - secondaryColor = 'ffffff'; - primaryTextColor = '141519'; - secondaryTextColor = '6b6f80'; - containerColor = 'ffffff'; - cardColor = 'e5f7ff'; - break; - case ThemeType.dark: - primaryColor = '456effff'; - secondaryColor = '1b2747ff'; - primaryTextColor = 'ffffff'; - secondaryTextColor = 'ffffff'; - containerColor = '19233C'; - cardColor = '232f4fff'; - break; + primaryColor = getColorStr(Theme.of(context).primaryColor); + secondaryColor = getColorStr(Theme.of(context).colorScheme.background); + primaryTextColor = + getColorStr(Theme.of(context).extension()!.titleColor); + secondaryTextColor = getColorStr( + Theme.of(context).extension()!.secondaryTextColor); + containerColor = getColorStr(Theme.of(context).colorScheme.background); + cardColor = getColorStr(Theme.of(context).cardColor); + + if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { + cardColor = getColorStr(Colors.white); } + final networkName = + wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); return Uri.https(_baseUrl, '', { 'apiKey': _apiKey, 'defaultCrypto': _normalizeCryptoCurrency, - 'defaultFiat': _settingsStore.fiatCurrency.title, - 'wallets': '${_wallet.currency.title}:${_wallet.walletAddresses.address}', - 'supportSell': "false", + 'sell_defaultCrypto': _normalizeCryptoCurrency, + 'networkWallets': '${networkName}:${wallet.walletAddresses.address}', 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, 'primaryTextColor': primaryTextColor, 'secondaryTextColor': secondaryTextColor, 'containerColor': containerColor, - 'cardColor': cardColor + 'cardColor': cardColor, + 'mode': isBuyAction == true ? 'buy' : 'sell', }); } + + Future launchProvider(BuildContext context, bool? isBuyAction) async { + final uri = requestOnramperUrl(context, isBuyAction); + if (DeviceInfo.instance.isMobile) { + Navigator.of(context) + .pushNamed(Routes.webViewPage, arguments: [title, uri]); + } else { + await launchUrl(uri); + } + } } diff --git a/lib/buy/order.dart b/lib/buy/order.dart index 387fbcd34..5a677d291 100644 --- a/lib/buy/order.dart +++ b/lib/buy/order.dart @@ -1,7 +1,8 @@ import 'package:cake_wallet/buy/buy_provider_description.dart'; -import 'package:hive/hive.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; part 'order.g.dart'; @@ -26,7 +27,7 @@ class Order extends HiveObject { } } - static const typeId = 8; + static const typeId = ORDER_TYPE_ID; static const boxName = 'Orders'; static const boxKey = 'ordersBoxKey'; @@ -66,4 +67,4 @@ class Order extends HiveObject { BuyProviderDescription.deserialize(raw: providerRaw); String amountFormatted() => formatAmount(amount); -} \ No newline at end of file +} diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart new file mode 100644 index 000000000..7610e51f3 --- /dev/null +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class RobinhoodBuyProvider extends BuyProvider { + RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment); + + static const _baseUrl = 'applink.robinhood.com'; + static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; + + @override + String get title => 'Robinhood Connect'; + + @override + String get providerDescription => S.current.robinhood_option_description; + + @override + String get lightIcon => 'assets/images/robinhood_light.png'; + + @override + String get darkIcon => 'assets/images/robinhood_dark.png'; + + String get _applicationId => secrets.robinhoodApplicationId; + + String get _apiSecret => secrets.exchangeHelperApiKey; + + String getSignature(String message) { + switch (wallet.type) { + case WalletType.ethereum: + case WalletType.polygon: + return wallet.signMessage(message); + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + return wallet.signMessage(message, address: wallet.walletAddresses.address); + default: + throw Exception("WalletType is not available for Robinhood ${wallet.type}"); + } + } + + Future getConnectId() async { + final walletAddress = wallet.walletAddresses.address; + final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10; + final message = "$_apiSecret:${valid_until}"; + + final signature = getSignature(message); + + final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); + + var response = await http.post(uri, + headers: {'Content-Type': 'application/json'}, + body: json + .encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature})); + + if (response.statusCode == 200) { + return (jsonDecode(response.body) as Map)['connectId'] as String; + } else { + throw Exception( + 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}'); + } + } + + Future requestProviderUrl() async { + final connectId = await getConnectId(); + final networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_"); + + return Uri.https(_baseUrl, '/u/connect', { + 'applicationId': _applicationId, + 'connectId': connectId, + 'walletAddress': wallet.walletAddresses.address, + 'userIdentifier': wallet.walletAddresses.address, + 'supportedNetworks': networkName + }); + } + + Future launchProvider(BuildContext context, bool? isBuyAction) async { + try { + final uri = await requestProviderUrl(); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: "Robinhood Connect", + alertContent: S.of(context).buy_provider_unavailable, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + } +} diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index 652c92f58..4dd091c33 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:cake_wallet/buy/buy_exception.dart'; +import 'package:flutter/src/widgets/framework.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; @@ -12,10 +13,8 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; class WyreBuyProvider extends BuyProvider { WyreBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : baseApiUrl = isTestEnvironment - ? _baseTestApiUrl - : _baseProductApiUrl, - super(wallet: wallet, isTestEnvironment: isTestEnvironment); + : baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl, + super(wallet: wallet, isTestEnvironment: isTestEnvironment); static const _baseTestApiUrl = 'https://api.testwyre.com'; static const _baseProductApiUrl = 'https://api.sendwyre.com'; @@ -35,26 +34,27 @@ class WyreBuyProvider extends BuyProvider { String get title => 'Wyre'; @override - BuyProviderDescription get description => BuyProviderDescription.wyre; + String get providerDescription => ''; @override - String get trackUrl => isTestEnvironment - ? _trackTestUrl - : _trackProductUrl; + String get lightIcon => 'assets/images/robinhood_light.png'; + + @override + String get darkIcon => 'assets/images/robinhood_dark.png'; + + String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl; String baseApiUrl; - @override Future requestUrl(String amount, String sourceCurrency) async { final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final url = baseApiUrl + _ordersSuffix + _reserveSuffix + - _timeStampSuffix + timestamp; + final url = baseApiUrl + _ordersSuffix + _reserveSuffix + _timeStampSuffix + timestamp; final uri = Uri.parse(url); final body = { 'amount': amount, 'sourceCurrency': sourceCurrency, - 'destCurrency': walletTypeToCryptoCurrency(walletType).title, - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title, + 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address, 'referrerAccountId': _accountId, 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] }; @@ -67,9 +67,7 @@ class WyreBuyProvider extends BuyProvider { body: json.encode(body)); if (response.statusCode != 200) { - throw BuyException( - description: description, - text: 'Url $url is not found!'); + throw BuyException(title: providerDescription, content: 'Url $url is not found!'); } final responseJSON = json.decode(response.body) as Map; @@ -77,14 +75,13 @@ class WyreBuyProvider extends BuyProvider { return urlFromResponse; } - @override Future calculateAmount(String amount, String sourceCurrency) async { final quoteUrl = _baseProductApiUrl + _ordersSuffix + _quoteSuffix; final body = { 'amount': amount, 'sourceCurrency': sourceCurrency, - 'destCurrency': walletTypeToCryptoCurrency(walletType).title, - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title, + 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address, 'accountId': _accountId, 'country': _countryCode }; @@ -98,9 +95,7 @@ class WyreBuyProvider extends BuyProvider { body: json.encode(body)); if (response.statusCode != 200) { - throw BuyException( - description: description, - text: 'Quote is not found!'); + throw BuyException(title: providerDescription, content: 'Quote is not found!'); } final responseJSON = json.decode(response.body) as Map; @@ -108,58 +103,55 @@ class WyreBuyProvider extends BuyProvider { final destAmount = responseJSON['destAmount'] as double; final achAmount = responseJSON['sourceAmountWithoutFees'] as double; - return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount, achSourceAmount: achAmount); + return BuyAmount( + sourceAmount: sourceAmount, destAmount: destAmount, achSourceAmount: achAmount); } - @override Future findOrderById(String id) async { final orderUrl = baseApiUrl + _ordersSuffix + '/$id'; final orderUri = Uri.parse(orderUrl); final orderResponse = await get(orderUri); if (orderResponse.statusCode != 200) { - throw BuyException( - description: description, - text: 'Order $id is not found!'); + throw BuyException(title: providerDescription, content: 'Order $id is not found!'); } - final orderResponseJSON = - json.decode(orderResponse.body) as Map; + final orderResponseJSON = json.decode(orderResponse.body) as Map; final transferId = orderResponseJSON['transferId'] as String; final from = orderResponseJSON['sourceCurrency'] as String; final to = orderResponseJSON['destCurrency'] as String; final status = orderResponseJSON['status'] as String; final state = TradeState.deserialize(raw: status.toLowerCase()); final createdAtRaw = orderResponseJSON['createdAt'] as int; - final createdAt = - DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); + final createdAt = DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); - final transferUrl = - baseApiUrl + _transferSuffix + transferId + _trackSuffix; + final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; final transferUri = Uri.parse(transferUrl); final transferResponse = await get(transferUri); if (transferResponse.statusCode != 200) { - throw BuyException( - description: description, - text: 'Transfer $transferId is not found!'); + throw BuyException(title: providerDescription, content: 'Transfer $transferId is not found!'); } - final transferResponseJSON = - json.decode(transferResponse.body) as Map; + final transferResponseJSON = json.decode(transferResponse.body) as Map; final amount = transferResponseJSON['destAmount'] as double; return Order( id: id, - provider: description, + provider: BuyProviderDescription.wyre, transferId: transferId, from: from, to: to, state: state, createdAt: createdAt, amount: amount.toString(), - receiveAddress: walletAddress, - walletId: walletId - ); + receiveAddress: wallet.walletAddresses.address, + walletId: wallet.id); } -} \ No newline at end of file + + @override + Future launchProvider(BuildContext context, bool? isBuyAction) { + // TODO: implement launchProvider + throw UnimplementedError(); + } +} diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 064efa11b..967cf9bf0 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -1,19 +1,24 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, useAdditionalValidation: type == CryptoCurrency.btc - ? bitcoin.Address.validateAddress + ? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet) : null, pattern: getPattern(type), length: getLength(type)); static String getPattern(CryptoCurrency type) { + if (type is Erc20Token) { + return '0x[0-9a-zA-Z]'; + } switch (type) { case CryptoCurrency.xmr: return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; @@ -21,11 +26,15 @@ class AddressValidator extends TextValidator { return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - return '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$'; + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; + case CryptoCurrency.banano: + return '[0-9a-zA-Z_]'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: + case CryptoCurrency.usdtPoly: + case CryptoCurrency.usdcEPoly: case CryptoCurrency.ape: case CryptoCurrency.avaxc: case CryptoCurrency.eth: @@ -36,6 +45,27 @@ class AddressValidator extends TextValidator { case CryptoCurrency.oxt: case CryptoCurrency.paxg: case CryptoCurrency.uni: + case CryptoCurrency.aave: + case CryptoCurrency.bat: + case CryptoCurrency.comp: + case CryptoCurrency.cro: + case CryptoCurrency.ens: + case CryptoCurrency.ftm: + case CryptoCurrency.frax: + case CryptoCurrency.gusd: + case CryptoCurrency.gtc: + case CryptoCurrency.grt: + case CryptoCurrency.ldo: + case CryptoCurrency.nexo: + case CryptoCurrency.pepe: + case CryptoCurrency.storj: + case CryptoCurrency.tusd: + case CryptoCurrency.wbtc: + case CryptoCurrency.weth: + case CryptoCurrency.zrx: + case CryptoCurrency.dydx: + case CryptoCurrency.steth: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -61,7 +91,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dai: case CryptoCurrency.dash: case CryptoCurrency.eos: + return '[0-9a-zA-Z]'; case CryptoCurrency.bch: + return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: return '[0-9a-zA-Z]'; case CryptoCurrency.ltc: @@ -96,47 +128,84 @@ class AddressValidator extends TextValidator { } static List? getLength(CryptoCurrency type) { + if (type is Erc20Token) { + return [42]; + } + + if (solana != null) { + final length = solana!.getValidationLength(type); + if (length != null) return length; + } + switch (type) { case CryptoCurrency.xmr: return null; case CryptoCurrency.ada: return null; - case CryptoCurrency.ape: - return [42]; - case CryptoCurrency.avaxc: - return [42]; - case CryptoCurrency.bch: - return [42]; - case CryptoCurrency.bnb: - return [42]; case CryptoCurrency.btc: return null; - case CryptoCurrency.dai: - return [42]; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: return [42]; case CryptoCurrency.eth: + case CryptoCurrency.usdcpoly: + case CryptoCurrency.usdtPoly: + case CryptoCurrency.usdcEPoly: + case CryptoCurrency.mana: + case CryptoCurrency.matic: + case CryptoCurrency.maticpoly: + case CryptoCurrency.mkr: + case CryptoCurrency.oxt: + case CryptoCurrency.paxg: + case CryptoCurrency.uni: + case CryptoCurrency.dai: + case CryptoCurrency.ape: + case CryptoCurrency.usdc: + case CryptoCurrency.usdterc20: + case CryptoCurrency.aave: + case CryptoCurrency.bat: + case CryptoCurrency.comp: + case CryptoCurrency.cro: + case CryptoCurrency.ens: + case CryptoCurrency.ftm: + case CryptoCurrency.frax: + case CryptoCurrency.gusd: + case CryptoCurrency.gtc: + case CryptoCurrency.grt: + case CryptoCurrency.ldo: + case CryptoCurrency.nexo: + case CryptoCurrency.pepe: + case CryptoCurrency.storj: + case CryptoCurrency.tusd: + case CryptoCurrency.wbtc: + case CryptoCurrency.weth: + case CryptoCurrency.zrx: + case CryptoCurrency.dydx: + case CryptoCurrency.steth: + case CryptoCurrency.shib: + case CryptoCurrency.avaxc: + return [42]; + case CryptoCurrency.bch: + return [42, 43, 44, 54, 55]; + case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; + case CryptoCurrency.banano: + return [64, 65]; case CryptoCurrency.sc: return [76]; case CryptoCurrency.sol: + case CryptoCurrency.usdtSol: + case CryptoCurrency.usdcsol: return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.trx: return [34]; - case CryptoCurrency.usdc: - return [42]; - case CryptoCurrency.usdcsol: - return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.usdt: return [34]; - case CryptoCurrency.usdterc20: - return [42]; case CryptoCurrency.usdttrc20: return [34]; case CryptoCurrency.xlm: @@ -159,11 +228,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xusd: return [98, 99, 106]; case CryptoCurrency.btt: - return [34]; case CryptoCurrency.bttc: - return [34]; case CryptoCurrency.doge: - return [34]; case CryptoCurrency.firo: return [34]; case CryptoCurrency.hbar: @@ -184,15 +250,6 @@ class AddressValidator extends TextValidator { return [35]; case CryptoCurrency.stx: return [40, 41, 42]; - case CryptoCurrency.usdcpoly: - case CryptoCurrency.mana: - case CryptoCurrency.matic: - case CryptoCurrency.maticpoly: - case CryptoCurrency.mkr: - case CryptoCurrency.oxt: - case CryptoCurrency.paxg: - case CryptoCurrency.uni: - return [42]; case CryptoCurrency.rune: return [43]; case CryptoCurrency.scrt: @@ -200,9 +257,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.near: return [64]; case CryptoCurrency.btcln: - return null; + case CryptoCurrency.kaspa: default: - return []; + return null; } } @@ -213,17 +270,41 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)'; + return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type + '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type case CryptoCurrency.ltc: return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.eth: + return '0x[0-9a-zA-Z]{42}'; + case CryptoCurrency.maticpoly: + return '0x[0-9a-zA-Z]{42}'; + case CryptoCurrency.nano: + return 'nano_[0-9a-zA-Z]{60}'; + case CryptoCurrency.banano: + return 'ban_[0-9a-zA-Z]{60}'; + case CryptoCurrency.bch: + return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' + '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.sol: + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; default: + if (type.tag == CryptoCurrency.eth.title) { + return '0x[0-9a-zA-Z]{42}'; + } + if (type.tag == CryptoCurrency.maticpoly.tag) { + return '0x[0-9a-zA-Z]{42}'; + } + if (type.tag == CryptoCurrency.sol.title) { + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + } + return null; } } diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index fb5214d54..38983dfb2 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -34,6 +34,10 @@ class AmountValidator extends TextValidator { late final DecimalAmountValidator decimalAmountValidator; String? call(String? value) { + if (value == null || value.isEmpty) { + return S.current.error_text_amount; + } + //* Validate for Text(length, symbols, decimals etc) final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value); diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 8091740e6..48610784c 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -1,3 +1,7 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -25,6 +29,10 @@ class AuthService with Store { Routes.setupPin, Routes.setup_2faPage, Routes.modify2FAPage, + Routes.newWallet, + Routes.newWalletType, + Routes.addressBookAddContact, + Routes.restoreOptions, ]; final FlutterSecureStorage secureStorage; @@ -34,6 +42,11 @@ class AuthService with Store { Future setPassword(String password) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPassword = encodedPinCode(pin: password); + // secure storage has a weird bug on macOS, where overwriting a key doesn't work, unless + // we delete what's there first: + if (Platform.isMacOS) { + await secureStorage.delete(key: key); + } await secureStorage.write(key: key, value: encodedPassword); } @@ -53,7 +66,7 @@ class AuthService with Store { Future authenticate(String pin) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final encodedPin = await secureStorage.read(key: key); + final encodedPin = await readSecureStorage(secureStorage, key); final decodedPin = decodedPinCode(pin: encodedPin!); return decodedPin == pin; @@ -61,11 +74,12 @@ class AuthService with Store { void saveLastAuthTime() { int timestamp = DateTime.now().millisecondsSinceEpoch; - sharedPreferences.setInt(PreferencesKey.lastAuthTimeMilliseconds, timestamp); + secureStorage.write(key: SecureKey.lastAuthTimeMilliseconds, value: timestamp.toString()); } - bool requireAuth() { - final timestamp = sharedPreferences.getInt(PreferencesKey.lastAuthTimeMilliseconds); + Future requireAuth() async { + final timestamp = + int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0'); final duration = _durationToRequireAuth(timestamp ?? 0); final requiredPinInterval = settingsStore.pinTimeOutDuration; @@ -81,30 +95,34 @@ class AuthService with Store { } Future authenticateAction(BuildContext context, - {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async { + {Function(bool)? onAuthSuccess, + String? route, + Object? arguments, + required bool conditionToDetermineIfToUse2FA}) async { assert(route != null || onAuthSuccess != null, 'Either route or onAuthSuccess param must be passed.'); - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { - if (onAuthSuccess != null) { - onAuthSuccess(true); - } else { - Navigator.of(context).pushNamed( - route ?? '', - arguments: arguments, - ); + if (!conditionToDetermineIfToUse2FA) { + if (!(await requireAuth()) && !_alwaysAuthenticateRoutes.contains(route)) { + if (onAuthSuccess != null) { + onAuthSuccess(true); + } else { + Navigator.of(context).pushNamed( + route ?? '', + arguments: arguments, + ); + } + return; } - return; } - Navigator.of(context).pushNamed(Routes.auth, arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (!isAuthenticatedSuccessfully) { onAuthSuccess?.call(false); return; } else { - if (settingsStore.useTOTP2FA) { + if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) { auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( @@ -131,8 +149,6 @@ class AuthService with Store { } } } - - }); - + }); } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2870c4488..2ec5f293d 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -9,6 +11,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; +import 'package:cw_core/cake_hive.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -16,11 +19,12 @@ import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/wallet_types.g.dart'; + import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService(this._flutterSecureStorage, this._walletInfoSource, - this._keyService, this._sharedPreferences) + BackupService( + this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -67,9 +71,8 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _exportBackupV1(String password, - {String nonce = secrets.backupSalt}) async - => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); + Future _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async => + throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); Future _exportBackupV2(String password) async { final zipEncoder = ZipFileEncoder(); @@ -112,8 +115,7 @@ class BackupService { return await _encryptV2(content, password); } - Future _importBackupV1(Uint8List data, String password, - {required String nonce}) async { + Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -161,10 +163,8 @@ class BackupService { Future _verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); - _correctWallets = walletInfoSource - .values - .where((info) => availableWalletTypes.contains(info.type)) - .toList(); + _correctWallets = + walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); if (_correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); @@ -173,14 +173,14 @@ class BackupService { Future> _reloadHiveWalletInfoBox() async { final appDir = await getApplicationDocumentsDirectory(); - await Hive.close(); - Hive.init(appDir.path); + await CakeHive.close(); + CakeHive.init(appDir.path); - if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { - Hive.registerAdapter(WalletInfoAdapter()); + if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) { + CakeHive.registerAdapter(WalletInfoAdapter()); } - return await Hive.openBox(WalletInfo.boxName); + return await CakeHive.openBox(WalletInfo.boxName); } Future _importPreferencesDump() async { @@ -191,14 +191,12 @@ class BackupService { return; } - final data = - json.decode(preferencesFile.readAsStringSync()) as Map; + final data = json.decode(preferencesFile.readAsStringSync()) as Map; String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; final isCorrentCurrentWallet = _correctWallets - .any((info) => info.name == currentWalletName && - info.type.index == currentWalletType); + .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { currentWalletName = _correctWallets.first.name; @@ -212,138 +210,179 @@ class BackupService { final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; - final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; + final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; + final currentTransactionPriorityKeyLegacy = + data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; + final currentBitcoinElectrumSererId = + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; - final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; + final currentDefaultSettingsMigrationVersion = + data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; + final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; + final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; + final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; + final defaultNanoRep = data[PreferencesKey.defaultNanoRep] as String?; + final defaultBananoRep = data[PreferencesKey.defaultBananoRep] as String?; + final lookupsTwitter = data[PreferencesKey.lookupsTwitter] as bool?; + final lookupsMastodon = data[PreferencesKey.lookupsMastodon] as bool?; + final lookupsYatService = data[PreferencesKey.lookupsYatService] as bool?; + final lookupsUnstoppableDomains = data[PreferencesKey.lookupsUnstoppableDomains] as bool?; + final lookupsOpenAlias = data[PreferencesKey.lookupsOpenAlias] as bool?; + final lookupsENS = data[PreferencesKey.lookupsENS] as bool?; + final syncAll = data[PreferencesKey.syncAllKey] as bool?; + final syncMode = data[PreferencesKey.syncModeKey] as int?; + final autoGenerateSubaddressStatus = + data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; - await _sharedPreferences.setString(PreferencesKey.currentWalletName, - currentWalletName); + await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); if (currentNodeId != null) - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, - currentNodeId); + await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); if (currentBalanceDisplayMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, - currentBalanceDisplayMode); + await _sharedPreferences.setInt( + PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - await _sharedPreferences.setInt(PreferencesKey.currentWalletType, - currentWalletType); + await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); if (currentFiatCurrency != null) - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, - currentFiatCurrency); + await _sharedPreferences.setString( + PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); if (shouldSaveRecipientAddress != null) await _sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, - shouldSaveRecipientAddress); + PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); if (isAppSecure != null) - await _sharedPreferences.setBool( - PreferencesKey.isAppSecureKey, - isAppSecure); + await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); if (disableBuy != null) - await _sharedPreferences.setBool( - PreferencesKey.disableBuyKey, - disableBuy); + await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); if (disableSell != null) - await _sharedPreferences.setBool( - PreferencesKey.disableSellKey, - disableSell); + await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); + + if (defaultBuyProvider != null) + await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKeyLegacy, - currentTransactionPriorityKeyLegacy); - - if (allowBiometricalAuthentication != null) - await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, - allowBiometricalAuthentication); - + PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); + if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - currentBitcoinElectrumSererId); + PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); if (currentLanguageCode != null) - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, - currentLanguageCode); + await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); if (displayActionListMode != null) - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, - displayActionListMode); + await _sharedPreferences.setInt( + PreferencesKey.displayActionListModeKey, displayActionListMode); if (fiatApiMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, - fiatApiMode); + await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); + if (autoGenerateSubaddressStatus != null) + await _sharedPreferences.setInt( + PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus); if (currentPinLength != null) - await _sharedPreferences.setInt(PreferencesKey.currentPinLength, - currentPinLength); + await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); - if (currentTheme != null) - await _sharedPreferences.setInt( - PreferencesKey.currentTheme, currentTheme); + if (currentTheme != null && DeviceInfo.instance.isMobile) { + await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); + // enforce dark theme on desktop platforms until the design is ready: + } else if (DeviceInfo.instance.isDesktop) { + await _sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw); + } if (exchangeStatus != null) - await _sharedPreferences.setInt( - PreferencesKey.exchangeStatusKey, exchangeStatus); + await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) - await _sharedPreferences.setInt( - PreferencesKey.currentDefaultSettingsMigrationVersion, - currentDefaultSettingsMigrationVersion); + await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, + currentDefaultSettingsMigrationVersion); if (moneroTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority, - moneroTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); if (bitcoinTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, - bitcoinTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); + + if (sortBalanceTokensBy != null) + await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + + if (pinNativeTokenAtTop != null) + await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + + if (useEtherscan != null) + await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + + if (defaultNanoRep != null) + await _sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep); + + if (defaultBananoRep != null) + await _sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep); + + if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + if (lookupsTwitter != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter); + + if (lookupsMastodon != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon); + + if (lookupsYatService != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService); + + if (lookupsUnstoppableDomains != null) + await _sharedPreferences.setBool( + PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains); + + if (lookupsOpenAlias != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias); + + if (lookupsENS != null) await _sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); + + if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + if (syncMode != null) await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); await preferencesFile.delete(); } Future _importKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async { + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV1( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.delete(key: backupPasswordKey); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.delete(key: pinCodeKey); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -352,27 +391,26 @@ class BackupService { {String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV2( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.delete(key: backupPasswordKey); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.delete(key: pinCodeKey); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -386,35 +424,26 @@ class BackupService { @Deprecated('Use v2 instead') Future _exportKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async - => throw Exception('Deprecated'); + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => + throw Exception('Deprecated'); Future _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); - final wallets = - await Future.wait(_walletInfoSource.values.map((walletInfo) async { + final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': - await _keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); - final backupPassword = - await _flutterSecureStorage.read(key: backupPasswordKey); - final data = utf8.encode(json.encode({ - 'pin': decodedPin, - 'wallets': wallets, - backupPasswordKey: backupPassword - })); - final encrypted = await _encryptV2( - Uint8List.fromList(data), '$keychainSalt$password'); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); + final data = utf8.encode( + json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); + final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; } @@ -423,46 +452,56 @@ class BackupService { final preferences = { PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), - PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType), + PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + PreferencesKey.currentBalanceDisplayModeKey: + _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), + PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), - PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences - .getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences - .getBool(PreferencesKey.disableSellKey), - PreferencesKey.isDarkThemeLegacy: - _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), - PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength), - PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey), - PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + PreferencesKey.shouldSaveRecipientAddressKey: + _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), + PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), + PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), + PreferencesKey.defaultBuyProvider: + _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider), + PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), + PreferencesKey.currentTransactionPriorityKeyLegacy: + _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + PreferencesKey.currentBitcoinElectrumSererIdKey: + _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: - _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), - PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), + PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), + PreferencesKey.currentDefaultSettingsMigrationVersion: + _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.pinNativeTokenAtTop: + _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: _sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.defaultNanoRep: _sharedPreferences.getString(PreferencesKey.defaultNanoRep), + PreferencesKey.defaultBananoRep: + _sharedPreferences.getString(PreferencesKey.defaultBananoRep), + PreferencesKey.lookupsTwitter: _sharedPreferences.getBool(PreferencesKey.lookupsTwitter), + PreferencesKey.lookupsMastodon: _sharedPreferences.getBool(PreferencesKey.lookupsMastodon), + PreferencesKey.lookupsYatService: + _sharedPreferences.getBool(PreferencesKey.lookupsYatService), + PreferencesKey.lookupsUnstoppableDomains: + _sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), + PreferencesKey.lookupsOpenAlias: _sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), + PreferencesKey.lookupsENS: _sharedPreferences.getBool(PreferencesKey.lookupsENS), + PreferencesKey.syncModeKey: _sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), + PreferencesKey.autoGenerateSubaddressStatusKey: + _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; return json.encode(preferences); @@ -476,28 +515,23 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _encryptV1( - Uint8List data, String secretKeySource, String nonceBase64) async - => throw Exception('Deprecated'); + Future _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async => + throw Exception('Deprecated'); - Future _decryptV1( - Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { + Future _decryptV1(Uint8List data, String secretKeySource, String nonceBase64, + {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); final nonce = base64.decode(nonceBase64).toList(); - final box = SecretBox( - Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), - nonce: nonce, - mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); + final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), + nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); final plainData = await _cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } - Future _encryptV2( - Uint8List data, String passphrase) async - => cake_backup.encrypt(passphrase, data, version: _v2); + Future _encryptV2(Uint8List data, String passphrase) async => + cake_backup.encrypt(passphrase, data, version: _v2); - Future _decryptV2( - Uint8List data, String passphrase) async - => cake_backup.decrypt(passphrase, data); + Future _decryptV2(Uint8List data, String passphrase) async => + cake_backup.decrypt(passphrase, data); } diff --git a/lib/core/create_trade_result.dart b/lib/core/create_trade_result.dart new file mode 100644 index 000000000..0e873d51e --- /dev/null +++ b/lib/core/create_trade_result.dart @@ -0,0 +1,9 @@ +class CreateTradeResult { + bool result; + String? errorMessage; + + CreateTradeResult({ + required this.result, + this.errorMessage, + }); +} diff --git a/lib/core/execution_state.dart b/lib/core/execution_state.dart index 18dc81030..6bc906010 100644 --- a/lib/core/execution_state.dart +++ b/lib/core/execution_state.dart @@ -14,4 +14,13 @@ class FailureState extends ExecutionState { FailureState(this.error); final String error; +} + +class AwaitingConfirmationState extends ExecutionState { + AwaitingConfirmationState({this.title, this.message, this.onConfirm, this.onCancel}); + + final String? title; + final String? message; + final Function()? onConfirm; + final Function()? onCancel; } \ No newline at end of file diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 096b8c855..654bb05d0 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -10,18 +10,18 @@ const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4 const _fiatApiPath = '/v2/rates'; Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as CryptoCurrency; - final fiat = args['fiat'] as FiatCurrency; + final crypto = args['crypto'] as String; + final fiat = args['fiat'] as String; final torOnly = args['torOnly'] as bool; final Map queryParams = { 'interval_count': '1', - 'base': crypto.toString(), - 'quote': fiat.toString(), + 'base': crypto.split(".").first, + 'quote': fiat, 'key': secrets.fiatApiKey, }; - double price = 0.0; + num price = 0.0; try { late final Uri uri; @@ -41,12 +41,12 @@ Future _fetchPrice(Map args) async { final results = responseJSON['results'] as Map; if (results.isNotEmpty) { - price = results.values.first as double; + price = results.values.first as num; } - return price; + return price.toDouble(); } catch (e) { - return price; + return price.toDouble(); } } @@ -96,7 +96,11 @@ Future _fetchHistoricalPrice(Map args) async { } Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); + compute(_fetchPrice, { + 'fiat': fiat.toString(), + 'crypto': crypto.toString(), + 'torOnly': torOnly, + }); Future _fetchHistoricalAsync( CryptoCurrency crypto, FiatCurrency fiat, bool torOnly, DateTime date) async => diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart index c9a9fac57..93803dd9d 100644 --- a/lib/core/generate_wallet_password.dart +++ b/lib/core/generate_wallet_password.dart @@ -1,4 +1,3 @@ -import 'package:uuid/uuid.dart'; import 'package:cw_core/key.dart'; String generateWalletPassword() { diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart index 1fe99623e..f829c22b5 100644 --- a/lib/core/key_service.dart +++ b/lib/core/key_service.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/encrypt.dart'; @@ -10,7 +11,7 @@ class KeyService { Future getWalletPassword({required String walletName}) async { final key = generateStoreKeyFor( key: SecretStoreKey.moneroWalletPassword, walletName: walletName); - final encodedPassword = await _secureStorage.read(key: key); + final encodedPassword = await readSecureStorage(_secureStorage, key); return decodeWalletPassword(password: encodedPassword!); } @@ -19,6 +20,14 @@ class KeyService { key: SecretStoreKey.moneroWalletPassword, walletName: walletName); final encodedPassword = encodeWalletPassword(password: password); + await _secureStorage.delete(key: key); await _secureStorage.write(key: key, value: encodedPassword); } + + Future deleteWalletPassword({required String walletName}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + + await _secureStorage.delete(key: key); + } } diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index 0e034dabc..0c8a0c37c 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -8,3 +8,12 @@ class NodeAddressValidator extends TextValidator { 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.\-]+\$'); } + +class NodePathValidator extends TextValidator { + NodePathValidator() + : super( + errorMessage: S.current.error_text_node_address, + pattern: '^([/0-9a-zA-Z.\-]+)?\$', + isAutovalidate: true, + ); +} diff --git a/lib/core/secure_storage.dart b/lib/core/secure_storage.dart new file mode 100644 index 000000000..4d9334a10 --- /dev/null +++ b/lib/core/secure_storage.dart @@ -0,0 +1,27 @@ +import 'dart:async'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +// For now, we can create a utility function to handle this. +// +// However, we could look into abstracting the entire FlutterSecureStorage package +// so the app doesn't depend on the package directly but an absraction. +// It'll make these kind of modifications to read/write come from a single point. + +Future readSecureStorage(FlutterSecureStorage secureStorage, String key) async { + String? result; + const maxWait = Duration(seconds: 3); + const checkInterval = Duration(milliseconds: 200); + + DateTime start = DateTime.now(); + + while (result == null && DateTime.now().difference(start) < maxWait) { + result = await secureStorage.read(key: key); + + if (result != null) { + break; + } + + await Future.delayed(checkInterval); + } + + return result; +} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index fe9a25f85..6d04055ba 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,9 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/utils/language_list.dart'; class SeedValidator extends Validator { @@ -25,6 +29,17 @@ class SeedValidator extends Validator { return monero!.getMoneroWordList(language); case WalletType.haven: return haven!.getMoneroWordList(language); + case WalletType.ethereum: + return ethereum!.getEthereumWordList(language); + case WalletType.bitcoinCash: + return getBitcoinWordList(language); + case WalletType.nano: + case WalletType.banano: + return nano!.getNanoWordList(language); + case WalletType.polygon: + return polygon!.getPolygonWordList(language); + case WalletType.solana: + return solana!.getSolanaWordList(language); default: return []; } diff --git a/lib/core/socks_proxy_node_address_validator.dart b/lib/core/socks_proxy_node_address_validator.dart new file mode 100644 index 000000000..eb1f78f1d --- /dev/null +++ b/lib/core/socks_proxy_node_address_validator.dart @@ -0,0 +1,10 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class SocksProxyNodeAddressValidator extends TextValidator { + SocksProxyNodeAddressValidator() + : super( + errorMessage: S.current.error_text_node_proxy_address, + pattern: + '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]+\$'); +} diff --git a/lib/core/wallet_change_listener_view_model.dart b/lib/core/wallet_change_listener_view_model.dart new file mode 100644 index 000000000..6735afee5 --- /dev/null +++ b/lib/core/wallet_change_listener_view_model.dart @@ -0,0 +1,30 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; + +part 'wallet_change_listener_view_model.g.dart'; + +class WalletChangeListenerViewModel = WalletChangeListenerViewModelBase + with _$WalletChangeListenerViewModel; + +abstract class WalletChangeListenerViewModelBase with Store { + WalletChangeListenerViewModelBase({ + required AppStore appStore, + }) : _wallet = appStore.wallet! { + reaction((_) => appStore.wallet, (WalletBase? wallet) { + _wallet = wallet!; + onWalletChange(wallet); + }); + } + + void onWalletChange(WalletBase wallet) {} + + @observable + WalletBase, TransactionInfo> _wallet; + @computed + WalletBase, TransactionInfo> get wallet => + _wallet; +} diff --git a/lib/core/wallet_connect/chain_service/chain_service.dart b/lib/core/wallet_connect/chain_service/chain_service.dart new file mode 100644 index 000000000..1e3ce3efd --- /dev/null +++ b/lib/core/wallet_connect/chain_service/chain_service.dart @@ -0,0 +1,5 @@ +abstract class ChainService { + String getNamespace(); + String getChainId(); + List getEvents(); +} diff --git a/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart b/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart new file mode 100644 index 000000000..0be21b1b2 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart'; + +enum EVMChainId { + ethereum, + polygon, + goerli, + mumbai, + arbitrum, +} + +extension EVMChainIdX on EVMChainId { + String chain() { + String name = ''; + + switch (this) { + case EVMChainId.ethereum: + name = '1'; + break; + case EVMChainId.polygon: + name = '137'; + break; + case EVMChainId.goerli: + name = '5'; + break; + case EVMChainId.arbitrum: + name = '42161'; + break; + case EVMChainId.mumbai: + name = '80001'; + break; + } + + return '${EvmChainServiceImpl.namespace}:$name'; + } +} diff --git a/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart b/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart new file mode 100644 index 000000000..6f3c8fa98 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart @@ -0,0 +1,304 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; +import 'package:convert/convert.dart'; +import 'package:eth_sig_util/eth_sig_util.dart'; +import 'package:eth_sig_util/util/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import 'package:web3dart/web3dart.dart'; +import '../chain_service.dart'; +import '../../wallet_connect_key_service.dart'; + +class EvmChainServiceImpl implements ChainService { + final AppStore appStore; + final BottomSheetService bottomSheetService; + final Web3Wallet wallet; + final WalletConnectKeyService wcKeyService; + + static const namespace = 'eip155'; + static const pSign = 'personal_sign'; + static const eSign = 'eth_sign'; + static const eSignTransaction = 'eth_signTransaction'; + static const eSignTypedData = 'eth_signTypedData_v4'; + static const eSendTransaction = 'eth_sendTransaction'; + + final EVMChainId reference; + + final Web3Client ethClient; + + EvmChainServiceImpl({ + required this.reference, + required this.appStore, + required this.wcKeyService, + required this.bottomSheetService, + required this.wallet, + Web3Client? web3Client, + }) : ethClient = web3Client ?? + Web3Client( + appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(), + http.Client(), + ) { + for (final String event in getEvents()) { + wallet.registerEventEmitter(chainId: getChainId(), event: event); + } + wallet.registerRequestHandler( + chainId: getChainId(), + method: pSign, + handler: personalSign, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSign, + handler: ethSign, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSignTransaction, + handler: ethSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSendTransaction, + handler: ethSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSignTypedData, + handler: ethSignTypedData, + ); + } + + @override + String getNamespace() { + return namespace; + } + + @override + String getChainId() { + return reference.chain(); + } + + @override + List getEvents() { + return ['chainChanged', 'accountsChanged']; + } + + Future requestAuthorization(String? text) async { + // Show the bottom sheet + final bool? isApproved = await bottomSheetService.queueBottomSheet( + widget: Web3RequestModal( + child: ConnectionWidget( + title: S.current.signTransaction, + info: [ + ConnectionModel( + text: text, + ), + ], + ), + ), + ) as bool?; + + if (isApproved != null && isApproved == false) { + return 'User rejected signature'; + } + + return null; + } + + Future personalSign(String topic, dynamic parameters) async { + log('received personal sign request: $parameters'); + + final String message; + if (parameters[0] == null) { + message = ''; + } else { + message = parameters[0].toString().utf8Message; + } + + final String? authError = await requestAuthorization(message); + + if (authError != null) { + return authError; + } + + try { + // Load the private key + final List keys = wcKeyService + .getKeysForChain(appStore.wallet!); + + final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + final String signature = hex.encode( + credentials.signPersonalMessageToUint8List(Uint8List.fromList(utf8.encode(message))), + ); + + return '0x$signature'; + } catch (e) { + log(e.toString()); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorGettingCredentials} ${e.toString()}', + ), + ); + return 'Failed: Error while getting credentials'; + } + } + + Future ethSign(String topic, dynamic parameters) async { + log('received eth sign request: $parameters'); + + final String message; + if (parameters[1] == null) { + message = ''; + } else { + message = parameters[1].toString().utf8Message; + } + + final String? authError = await requestAuthorization(message); + if (authError != null) { + return authError; + } + + try { + // Load the private key + final List keys = wcKeyService + .getKeysForChain(appStore.wallet!); + + final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + final String signature = hex.encode( + credentials.signPersonalMessageToUint8List( + Uint8List.fromList(utf8.encode(message)), + chainId: getChainIdBasedOnWalletType(appStore.wallet!.type), + ), + ); + log(signature); + + return '0x$signature'; + } catch (e) { + log('error: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: '${S.current.error}: ${e.toString()}'), + ); + return 'Failed'; + } + } + + Future ethSignTransaction(String topic, dynamic parameters) async { + log('received eth sign transaction request: $parameters'); + + final paramsData = parameters[0] as Map; + + final message = _convertToReadable(paramsData); + + final String? authError = await requestAuthorization(message); + + if (authError != null) { + return authError; + } + + // Load the private key + final List keys = wcKeyService + .getKeysForChain(appStore.wallet!); + + final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + WCEthereumTransactionModel ethTransaction = + WCEthereumTransactionModel.fromJson(parameters[0] as Map); + + final transaction = Transaction( + from: EthereumAddress.fromHex(ethTransaction.from), + to: EthereumAddress.fromHex(ethTransaction.to), + maxGas: ethTransaction.gasLimit != null ? int.tryParse(ethTransaction.gasLimit ?? "") : null, + gasPrice: ethTransaction.gasPrice != null + ? EtherAmount.inWei(BigInt.parse(ethTransaction.gasPrice ?? "")) + : null, + value: EtherAmount.inWei(BigInt.parse(ethTransaction.value)), + data: hexToBytes(ethTransaction.data ?? ""), + nonce: ethTransaction.nonce != null ? int.tryParse(ethTransaction.nonce ?? "") : null, + ); + + try { + final result = await ethClient.sendTransaction( + credentials, + transaction, + chainId: getChainIdBasedOnWalletType(appStore.wallet!.type), + ); + + log('Result: $result'); + + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.awaitDAppProcessing, + isError: false, + ), + ); + + return result; + } catch (e) { + log('An error has occurred while signing transaction: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorSigningTransaction}: ${e.toString()}', + ), + ); + return 'Failed'; + } + } + + Future ethSignTypedData(String topic, dynamic parameters) async { + log('received eth sign typed data request: $parameters'); + final String? data = parameters[1] as String?; + + final String? authError = await requestAuthorization(data); + + if (authError != null) { + return authError; + } + + final List keys = wcKeyService + .getKeysForChain(appStore.wallet!); + + return EthSigUtil.signTypedData( + privateKey: keys[0].privateKey, + jsonData: data ?? '', + version: TypedDataVersion.V4, + ); + } + + String _convertToReadable(Map data) { + final tokenName = getTokenNameBasedOnWalletType(appStore.wallet!.type); + String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString(); + String value = data['value'] != null + ? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() + + ' $tokenName' + : '0 $tokenName'; + String from = data['from'] as String; + String to = data['to'] as String; + + return ''' + Gas: $gas\n + Value: $value\n + From: $from\n + To: $to + '''; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart new file mode 100644 index 000000000..e462adbb5 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart @@ -0,0 +1,28 @@ +class SolanaSignMessage { + final String pubkey; + final String message; + + SolanaSignMessage({ + required this.pubkey, + required this.message, + }); + + factory SolanaSignMessage.fromJson(Map json) { + return SolanaSignMessage( + pubkey: json['pubkey'] as String, + message: json['message'] as String, + ); + } + + Map toJson() { + return { + 'pubkey': pubkey, + 'message': message, + }; + } + + @override + String toString() { + return 'SolanaSignMessage(pubkey: $pubkey, message: $message)'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart new file mode 100644 index 000000000..2cdf4697e --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart @@ -0,0 +1,106 @@ +class SolanaSignTransaction { + final String? feePayer; + final String? recentBlockhash; + final String transaction; + final List? instructions; + + SolanaSignTransaction({ + required this.feePayer, + required this.recentBlockhash, + required this.instructions, + required this.transaction, + }); + + factory SolanaSignTransaction.fromJson(Map json) { + return SolanaSignTransaction( + feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null, + recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null, + instructions:json['instructions']!=null? (json['instructions'] as List) + .map((e) => SolanaInstruction.fromJson(e as Map)) + .toList(): null, + transaction: json['transaction'] as String, + ); + } + + Map toJson() { + return { + 'feePayer': feePayer, + 'recentBlockhash': recentBlockhash, + 'instructions': instructions, + 'transaction': transaction, + }; + } + + @override + String toString() { + return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)'; + } +} + +class SolanaInstruction { + final String programId; + final List keys; + final List data; + + SolanaInstruction({ + required this.programId, + required this.keys, + required this.data, + }); + + factory SolanaInstruction.fromJson(Map json) { + return SolanaInstruction( + programId: json['programId'] as String, + keys: (json['keys'] as List) + .map((e) => SolanaKeyMetadata.fromJson(e as Map)) + .toList(), + data: (json['data'] as List).map((e) => e as int).toList(), + ); + } + + Map toJson() { + return { + 'programId': programId, + 'keys': keys, + 'data': data, + }; + } + + @override + String toString() { + return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)'; + } +} + +class SolanaKeyMetadata { + final String pubkey; + final bool isSigner; + final bool isWritable; + + SolanaKeyMetadata({ + required this.pubkey, + required this.isSigner, + required this.isWritable, + }); + + factory SolanaKeyMetadata.fromJson(Map json) { + return SolanaKeyMetadata( + pubkey: json['pubkey'] as String, + isSigner: json['isSigner'] as bool, + isWritable: json['isWritable'] as bool, + ); + } + + Map toJson() { + return { + 'pubkey': pubkey, + 'isSigner': isSigner, + 'isWritable': isWritable, + }; + } + + @override + String toString() { + return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart new file mode 100644 index 000000000..ed80a4f3f --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart @@ -0,0 +1,30 @@ +import 'solana_chain_service.dart'; + +enum SolanaChainId { + mainnet, + // testnet, + // devnet, +} + +extension SolanaChainIdX on SolanaChainId { + String chain() { + String name = ''; + + switch (this) { + case SolanaChainId.mainnet: + name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'; + // solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp + break; + // case SolanaChainId.devnet: + // name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K'; + // // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 + // break; + // case SolanaChainId.testnet: + // name = ''; + // // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z + // break; + } + + return '${SolanaChainServiceImpl.namespace}:$name'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart new file mode 100644 index 000000000..efbf9df74 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart @@ -0,0 +1,179 @@ +import 'dart:developer'; + +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:solana/base58.dart'; +import 'package:solana/solana.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import '../chain_service.dart'; +import '../../wallet_connect_key_service.dart'; +import 'entities/solana_sign_transaction.dart'; + +class SolanaChainServiceImpl implements ChainService { + final BottomSheetService bottomSheetService; + final Web3Wallet wallet; + final WalletConnectKeyService wcKeyService; + + static const namespace = 'solana'; + static const solSignTransaction = 'solana_signTransaction'; + static const solSignMessage = 'solana_signMessage'; + + final SolanaChainId reference; + + final SolanaClient solanaClient; + + final Ed25519HDKeyPair? ownerKeyPair; + + SolanaChainServiceImpl({ + required this.reference, + required this.wcKeyService, + required this.bottomSheetService, + required this.wallet, + required this.ownerKeyPair, + required String webSocketUrl, + required Uri rpcUrl, + SolanaClient? solanaClient, + }) : solanaClient = solanaClient ?? + SolanaClient( + rpcUrl: rpcUrl, + websocketUrl: Uri.parse(webSocketUrl), + timeout: const Duration(minutes: 5), + ) { + for (final String event in getEvents()) { + wallet.registerEventEmitter(chainId: getChainId(), event: event); + } + wallet.registerRequestHandler( + chainId: getChainId(), + method: solSignTransaction, + handler: solanaSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: solSignMessage, + handler: solanaSignMessage, + ); + } + + @override + String getNamespace() { + return namespace; + } + + @override + String getChainId() { + return reference.chain(); + } + + @override + List getEvents() { + return ['chainChanged', 'accountsChanged']; + } + + Future requestAuthorization(String? text) async { + // Show the bottom sheet + final bool? isApproved = await bottomSheetService.queueBottomSheet( + widget: Web3RequestModal( + child: ConnectionWidget( + title: S.current.signTransaction, + info: [ + ConnectionModel( + text: text, + ), + ], + ), + ), + ) as bool?; + + if (isApproved != null && isApproved == false) { + return 'User rejected signature'; + } + + return null; + } + + Future solanaSignTransaction(String topic, dynamic parameters) async { + log('received solana sign transaction request $parameters'); + + final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map); + + final String? authError = await requestAuthorization('Confirm request to sign transaction?'); + + if (authError != null) { + return authError; + } + + try { + final message = + await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction); + + final sign = await ownerKeyPair?.signMessage( + message: message, + recentBlockhash: solanaSignTx.recentBlockhash ?? '', + ); + + if (sign == null) { + return ''; + } + + String signature = await solanaClient.sendAndConfirmTransaction( + message: message, + signers: [ownerKeyPair!], + commitment: Commitment.confirmed, + ); + + print(signature); + + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.awaitDAppProcessing, + isError: false, + ), + ); + + return signature; + } catch (e) { + log('An error has occurred while signing transaction: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorSigningTransaction}: ${e.toString()}', + ), + ); + return 'Failed'; + } + } + + Future solanaSignMessage(String topic, dynamic parameters) async { + log('received solana sign message request: $parameters'); + + final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map); + + final String? authError = await requestAuthorization('Confirm request to sign message?'); + + if (authError != null) { + return authError; + } + Signature? sign; + + try { + sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message)); + } catch (e) { + print(e); + } + + if (sign == null) { + return ''; + } + + String signature = sign.toBase58(); + + return signature; + } +} diff --git a/lib/core/wallet_connect/eth_transaction_model.dart b/lib/core/wallet_connect/eth_transaction_model.dart new file mode 100644 index 000000000..deb33586f --- /dev/null +++ b/lib/core/wallet_connect/eth_transaction_model.dart @@ -0,0 +1,60 @@ +class WCEthereumTransactionModel { + final String from; + final String to; + final String value; + final String? nonce; + final String? gasPrice; + final String? maxFeePerGas; + final String? maxPriorityFeePerGas; + final String? gas; + final String? gasLimit; + final String? data; + + WCEthereumTransactionModel({ + required this.from, + required this.to, + required this.value, + this.nonce, + this.gasPrice, + this.maxFeePerGas, + this.maxPriorityFeePerGas, + this.gas, + this.gasLimit, + this.data, + }); + + factory WCEthereumTransactionModel.fromJson(Map json) { + return WCEthereumTransactionModel( + from: json['from'] as String, + to: json['to'] as String, + value: json['value'] as String, + nonce: json['nonce'] as String?, + gasPrice: json['gasPrice'] as String?, + maxFeePerGas: json['maxFeePerGas'] as String?, + maxPriorityFeePerGas: json['maxPriorityFeePerGas'] as String?, + gas: json['gas'] as String?, + gasLimit: json['gasLimit'] as String?, + data: json['data'] as String?, + ); + } + + Map toJson() { + return { + 'from': from, + 'to': to, + 'value': value, + 'nonce': nonce, + 'gasPrice': gasPrice, + 'maxFeePerGas': maxFeePerGas, + 'maxPriorityFeePerGas': maxPriorityFeePerGas, + 'gas': gas, + 'gasLimit': gasLimit, + 'data': data, + }; + } + + @override + String toString() { + return 'EthereumTransactionModel(from: $from, to: $to, nonce: $nonce, gasPrice: $gasPrice, maxFeePerGas: $maxFeePerGas, maxPriorityFeePerGas: $maxPriorityFeePerGas, gas: $gas, gasLimit: $gasLimit, value: $value, data: $data)'; + } +} diff --git a/lib/core/wallet_connect/models/auth_request_model.dart b/lib/core/wallet_connect/models/auth_request_model.dart new file mode 100644 index 000000000..f7fd984c8 --- /dev/null +++ b/lib/core/wallet_connect/models/auth_request_model.dart @@ -0,0 +1,16 @@ +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +class AuthRequestModel { + final String iss; + final AuthRequest request; + + AuthRequestModel({ + required this.iss, + required this.request, + }); + + @override + String toString() { + return 'AuthRequestModel(iss: $iss, request: $request)'; + } +} diff --git a/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart new file mode 100644 index 000000000..49eecac0f --- /dev/null +++ b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; + +class BottomSheetQueueItemModel { + final Widget widget; + final bool isModalDismissible; + final Completer completer; + + BottomSheetQueueItemModel({ + required this.widget, + required this.completer, + this.isModalDismissible = false, + }); + + @override + String toString() { + return 'BottomSheetQueueItemModel(widget: $widget, completer: $completer)'; + } +} diff --git a/lib/core/wallet_connect/models/chain_key_model.dart b/lib/core/wallet_connect/models/chain_key_model.dart new file mode 100644 index 000000000..5cd2764da --- /dev/null +++ b/lib/core/wallet_connect/models/chain_key_model.dart @@ -0,0 +1,16 @@ +class ChainKeyModel { + final List chains; + final String privateKey; + final String publicKey; + + ChainKeyModel({ + required this.chains, + required this.privateKey, + required this.publicKey, + }); + + @override + String toString() { + return 'ChainKeyModel(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)'; + } +} diff --git a/lib/core/wallet_connect/models/connection_model.dart b/lib/core/wallet_connect/models/connection_model.dart new file mode 100644 index 000000000..63cc8260f --- /dev/null +++ b/lib/core/wallet_connect/models/connection_model.dart @@ -0,0 +1,18 @@ +class ConnectionModel { + final String? title; + final String? text; + final List? elements; + final Map? elementActions; + + ConnectionModel({ + this.title, + this.text, + this.elements, + this.elementActions, + }); + + @override + String toString() { + return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)'; + } +} diff --git a/lib/core/wallet_connect/models/session_request_model.dart b/lib/core/wallet_connect/models/session_request_model.dart new file mode 100644 index 000000000..0c7a5d876 --- /dev/null +++ b/lib/core/wallet_connect/models/session_request_model.dart @@ -0,0 +1,14 @@ +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +class SessionRequestModel { + final ProposalData request; + + SessionRequestModel({ + required this.request, + }); + + @override + String toString() { + return 'SessionRequestModel(request: $request)'; + } +} diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/core/wallet_connect/wallet_connect_key_service.dart new file mode 100644 index 000000000..f05adad97 --- /dev/null +++ b/lib/core/wallet_connect/wallet_connect_key_service.dart @@ -0,0 +1,80 @@ +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/solana/solana.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; + +abstract class WalletConnectKeyService { + /// Returns a list of all the keys. + List getKeys(WalletBase wallet); + + /// Returns a list of all the keys for a given chain id. + /// If the chain is not found, returns an empty list. + /// - [chain]: The chain to get the keys for. + List getKeysForChain(WalletBase wallet); +} + +class KeyServiceImpl implements WalletConnectKeyService { + static String _getPrivateKeyForWallet(WalletBase wallet) { + switch (wallet.type) { + case WalletType.ethereum: + return ethereum!.getPrivateKey(wallet); + case WalletType.polygon: + return polygon!.getPrivateKey(wallet); + case WalletType.solana: + return solana!.getPrivateKey(wallet); + default: + return ''; + } + } + + static String _getPublicKeyForWallet(WalletBase wallet) { + switch (wallet.type) { + case WalletType.ethereum: + return ethereum!.getPublicKey(wallet); + case WalletType.polygon: + return polygon!.getPublicKey(wallet); + case WalletType.solana: + return solana!.getPublicKey(wallet); + default: + return ''; + } + } + + @override + List getKeys(WalletBase wallet) { + final keys = [ + ChainKeyModel( + chains: [ + 'eip155:1', + 'eip155:5', + 'eip155:137', + 'eip155:42161', + 'eip155:80001', + ], + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), + ), + ChainKeyModel( + chains: [ + 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // main-net + 'solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K', // test-net + ], + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), + ), + ]; + return keys; + } + + @override + List getKeysForChain(WalletBase wallet) { + final chain = getChainNameSpaceAndIdBasedOnWalletType(wallet.type); + + final keys = getKeys(wallet); + + return keys.where((e) => e.chains.contains(chain)).toList(); + } +} diff --git a/lib/core/wallet_connect/wc_bottom_sheet_service.dart b/lib/core/wallet_connect/wc_bottom_sheet_service.dart new file mode 100644 index 000000000..3da8660f0 --- /dev/null +++ b/lib/core/wallet_connect/wc_bottom_sheet_service.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:cake_wallet/core/wallet_connect/models/bottom_sheet_queue_item_model.dart'; +import 'package:flutter/material.dart'; + +abstract class BottomSheetService { + abstract final ValueNotifier currentSheet; + + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }); + + void resetCurrentSheet(); +} + +class BottomSheetServiceImpl implements BottomSheetService { + + @override + final ValueNotifier currentSheet = ValueNotifier(null); + + @override + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }) async { + // Create the bottom sheet queue item + final completer = Completer(); + final queueItem = BottomSheetQueueItemModel( + widget: widget, + completer: completer, + isModalDismissible: isModalDismissible, + ); + + currentSheet.value = queueItem; + + return await completer.future; + } + + @override + void resetCurrentSheet() { + currentSheet.value = null; + } +} diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart new file mode 100644 index 000000000..adb516817 --- /dev/null +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -0,0 +1,408 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart'; +import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:eth_sig_util/eth_sig_util.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +import 'chain_service/solana/solana_chain_id.dart'; +import 'chain_service/solana/solana_chain_service.dart'; +import 'wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +part 'web3wallet_service.g.dart'; + +class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService; + +abstract class Web3WalletServiceBase with Store { + final AppStore appStore; + final SharedPreferences sharedPreferences; + final BottomSheetService _bottomSheetHandler; + final WalletConnectKeyService walletKeyService; + + late Web3Wallet _web3Wallet; + + @observable + bool isInitialized; + + /// The list of requests from the dapp + /// Potential types include, but aren't limited to: + /// [SessionProposalEvent], [AuthRequest] + @observable + ObservableList pairings; + + @observable + ObservableList sessions; + + @observable + ObservableList auth; + + Web3WalletServiceBase( + this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences) + : pairings = ObservableList(), + sessions = ObservableList(), + auth = ObservableList(), + isInitialized = false; + + @action + void create() { + // Create the web3wallet client + _web3Wallet = Web3Wallet( + core: Core(projectId: secrets.walletConnectProjectId), + metadata: const PairingMetadata( + name: 'Cake Wallet', + description: 'Cake Wallet', + url: 'https://cakewallet.com', + icons: ['https://cakewallet.com/assets/image/cake_logo.png'], + ), + ); + + // Setup our accounts + List chainKeys = walletKeyService.getKeys(appStore.wallet!); + for (final chainKey in chainKeys) { + for (final chainId in chainKey.chains) { + _web3Wallet.registerAccount( + chainId: chainId, + accountAddress: chainKey.publicKey, + ); + } + } + + // Setup our listeners + log('Created instance of web3wallet'); + _web3Wallet.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid); + _web3Wallet.core.pairing.onPairingCreate.subscribe(_onPairingCreate); + _web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete); + _web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete); + _web3Wallet.pairings.onSync.subscribe(_onPairingsSync); + _web3Wallet.onSessionProposal.subscribe(_onSessionProposal); + _web3Wallet.onSessionProposalError.subscribe(_onSessionProposalError); + _web3Wallet.onSessionConnect.subscribe(_onSessionConnect); + _web3Wallet.onAuthRequest.subscribe(_onAuthRequest); + } + + @action + Future init() async { + // Await the initialization of the web3wallet + log('Intializing web3wallet'); + if (!isInitialized) { + try { + await _web3Wallet.init(); + log('Initialized'); + isInitialized = true; + } catch (e) { + log('Experimentallllll: $e'); + isInitialized = false; + } + } + + _refreshPairings(); + + final newSessions = _web3Wallet.sessions.getAll(); + sessions.addAll(newSessions); + + final newAuthRequests = _web3Wallet.completeRequests.getAll(); + auth.addAll(newAuthRequests); + + if (isEVMCompatibleChain(appStore.wallet!.type)) { + for (final cId in EVMChainId.values) { + EvmChainServiceImpl( + reference: cId, + appStore: appStore, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + wallet: _web3Wallet, + ); + } + } + + if (appStore.wallet!.type == WalletType.solana) { + for (final cId in SolanaChainId.values) { + final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); + + Uri? rpcUri; + String webSocketUrl; + bool isModifiedNodeUri = false; + + if (node.uriRaw == 'rpc.ankr.com') { + isModifiedNodeUri = true; + + //A better way to handle this instead of adding this to the general secrets? + String ankrApiKey = secrets.ankrApiKey; + + rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); + webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + } else { + webSocketUrl = 'wss://${node.uriRaw}'; + } + + SolanaChainServiceImpl( + reference: cId, + rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri, + webSocketUrl: webSocketUrl, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + wallet: _web3Wallet, + ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!), + ); + } + } + } + + @action + FutureOr onDispose() { + log('web3wallet dispose'); + _web3Wallet.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid); + _web3Wallet.pairings.onSync.unsubscribe(_onPairingsSync); + _web3Wallet.onSessionProposal.unsubscribe(_onSessionProposal); + _web3Wallet.onSessionProposalError.unsubscribe(_onSessionProposalError); + _web3Wallet.onSessionConnect.unsubscribe(_onSessionConnect); + _web3Wallet.onAuthRequest.unsubscribe(_onAuthRequest); + _web3Wallet.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete); + _web3Wallet.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete); + isInitialized = false; + } + + Web3Wallet getWeb3Wallet() { + return _web3Wallet; + } + + void _onPairingsSync(StoreSyncEvent? args) { + if (args != null) { + _refreshPairings(); + } + } + + void _onPairingDelete(PairingEvent? event) { + _refreshPairings(); + } + + Future _onSessionProposalError(SessionProposalErrorEvent? args) async { + log(args.toString()); + } + + void _onSessionProposal(SessionProposalEvent? args) async { + if (args != null) { + final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type); + final Widget modalWidget = Web3RequestModal( + child: ConnectionRequestWidget( + chaindIdNamespace: chaindIdNamespace, + wallet: _web3Wallet, + sessionProposal: SessionRequestModel(request: args.params), + ), + ); + // show the bottom sheet + final bool? isApproved = await _bottomSheetHandler.queueBottomSheet( + widget: modalWidget, + ) as bool?; + + if (isApproved != null && isApproved) { + _web3Wallet.approveSession( + id: args.id, + namespaces: args.params.generatedNamespaces!, + ); + } else { + _web3Wallet.rejectSession( + id: args.id, + reason: Errors.getSdkError( + Errors.USER_REJECTED, + ), + ); + } + } + } + + @action + void _onPairingInvalid(PairingInvalidEvent? args) { + log('Pairing Invalid Event: $args'); + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: '${S.current.pairingInvalidEvent}: $args'), + ); + } + + @action + Future pairWithUri(Uri uri) async { + try { + log('Pairing with URI: $uri'); + await _web3Wallet.pair(uri: uri); + } on WalletConnectError catch (e) { + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: e.message), + ); + } catch (e) { + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: e.toString()), + ); + } + } + + @action + void _refreshPairings() { + print('Refreshing pairings'); + pairings.clear(); + + final allPairings = _web3Wallet.pairings.getAll(); + + final keyForWallet = getKeyForStoringTopicsForWallet(); + + final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet); + + final filteredPairings = + allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList(); + + pairings.addAll(filteredPairings); + } + + void _onPairingCreate(PairingEvent? args) { + log('Pairing Create Event: $args'); + } + + @action + Future _onSessionConnect(SessionConnect? args) async { + if (args != null) { + log('Session Connected $args'); + + await savePairingTopicToLocalStorage(args.session.pairingTopic); + + sessions.add(args.session); + + _refreshPairings(); + } + } + + @action + Future _onAuthRequest(AuthRequest? args) async { + if (args != null) { + final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type); + List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); + // Create the message to be signed + final String iss = 'did:pkh:$chaindIdNamespace:${chainKeys.first.publicKey}'; + final Widget modalWidget = Web3RequestModal( + child: ConnectionRequestWidget( + chaindIdNamespace: chaindIdNamespace, + wallet: _web3Wallet, + authRequest: AuthRequestModel(iss: iss, request: args), + ), + ); + final bool? isAuthenticated = await _bottomSheetHandler.queueBottomSheet( + widget: modalWidget, + ) as bool?; + + if (isAuthenticated != null && isAuthenticated) { + final String message = _web3Wallet.formatAuthMessage( + iss: iss, + cacaoPayload: CacaoRequestPayload.fromPayloadParams( + args.payloadParams, + ), + ); + + final String sig = EthSigUtil.signPersonalMessage( + message: Uint8List.fromList(message.codeUnits), + privateKey: chainKeys.first.privateKey, + ); + + await _web3Wallet.respondAuthRequest( + id: args.id, + iss: iss, + signature: CacaoSignature( + t: CacaoSignature.EIP191, + s: sig, + ), + ); + } else { + await _web3Wallet.respondAuthRequest( + id: args.id, + iss: iss, + error: Errors.getSdkError( + Errors.USER_REJECTED_AUTH, + ), + ); + } + } + } + + @action + Future disconnectSession(String topic) async { + final session = sessions.firstWhere((element) => element.pairingTopic == topic); + + await _web3Wallet.core.pairing.disconnect(topic: topic); + await _web3Wallet.disconnectSession( + topic: session.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED)); + } + + @action + List getSessionsForPairingInfo(PairingInfo pairing) { + return sessions.where((element) => element.pairingTopic == pairing.topic).toList(); + } + + String getKeyForStoringTopicsForWallet() { + List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); + + final keyForPairingTopic = + PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey); + + return keyForPairingTopic; + } + + List getPairingTopicsForWallet(String key) { + // Get the JSON-encoded string from shared preferences + final jsonString = sharedPreferences.getString(key); + + // If the string is null, return an empty list + if (jsonString == null) { + return []; + } + + // Decode the JSON string to a list of strings + final List jsonList = jsonDecode(jsonString) as List; + + // Cast each item to a string + return jsonList.map((item) => item as String).toList(); + } + + Future savePairingTopicToLocalStorage(String pairingTopic) async { + // Get key specific to the current wallet + final key = getKeyForStoringTopicsForWallet(); + + // Get all pairing topics attached to this key + final pairingTopicsForWallet = getPairingTopicsForWallet(key); + + print(pairingTopicsForWallet); + + bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic); + print('Is Pairing Topic Saved: $isPairingTopicAlreadySaved'); + + if (!isPairingTopicAlreadySaved) { + // Update the list with the most recent pairing topic + pairingTopicsForWallet.add(pairingTopic); + + // Convert the list of updated pairing topics to a JSON-encoded string + final jsonString = jsonEncode(pairingTopicsForWallet); + + // Save the encoded string to shared preferences + await sharedPreferences.setString(key, jsonString); + } + } +} diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3b28f36c3..31a893ad6 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,6 +18,7 @@ class WalletCreationService { required this.secureStorage, required this.keyService, required this.sharedPreferences, + required this.settingsStore, required this.walletInfoSource}) : type = initialType { changeWalletType(type: type); @@ -26,6 +27,7 @@ class WalletCreationService { WalletType type; final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; + final SettingsStore settingsStore; final KeyService keyService; final Box walletInfoSource; WalletService? _service; @@ -39,15 +41,11 @@ class WalletCreationService { bool exists(String name) { final walletName = name.toLowerCase(); - return walletInfoSource - .values - .any((walletInfo) => walletInfo.name.toLowerCase() == walletName); + return walletInfoSource.values.any((walletInfo) => walletInfo.name.toLowerCase() == walletName); } bool typeExists(WalletType type) { - return walletInfoSource - .values - .any((walletInfo) => walletInfo.type == type); + return walletInfoSource.values.any((walletInfo) => walletInfo.type == type); } void checkIfExists(String name) { @@ -56,55 +54,49 @@ class WalletCreationService { } } - Future create(WalletCredentials credentials) async { + Future create(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); - final wallet = await _service!.create(credentials); + if (type == WalletType.bitcoinCash || type == WalletType.ethereum) { + credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; + } + await keyService.saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service!.create(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; } - Future restoreFromKeys(WalletCredentials credentials) async { + Future restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); - final wallet = await _service!.restoreFromKeys(credentials); + await keyService.saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service!.restoreFromKeys(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; } - Future restoreFromSeed(WalletCredentials credentials) async { + Future restoreFromSeed(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); - final wallet = await _service!.restoreFromSeed(credentials); + await keyService.saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service!.restoreFromSeed(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 761c6acce..1f17a7a1c 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -1,49 +1,99 @@ import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletLoadingService { - WalletLoadingService( - this.sharedPreferences, - this.keyService, - this.walletServiceFactory); - - final SharedPreferences sharedPreferences; - final KeyService keyService; - final WalletService Function(WalletType type) walletServiceFactory; + WalletLoadingService(this.sharedPreferences, this.keyService, this.walletServiceFactory); - Future load(WalletType type, String name) async { - final walletService = walletServiceFactory.call(type); - final password = await keyService.getWalletPassword(walletName: name); - final wallet = await walletService.openWallet(name, password); + final SharedPreferences sharedPreferences; + final KeyService keyService; + final WalletService Function(WalletType type) walletServiceFactory; - if (type == WalletType.monero) { - await updateMoneroWalletPassword(wallet); - } + Future renameWallet(WalletType type, String name, String newName) async { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); - return wallet; - } + // Save the current wallet's password to the new wallet name's key + await keyService.saveWalletPassword(walletName: newName, password: password); + // Delete previous wallet name from keyService to keep only new wallet's name + // otherwise keeps duplicate (old and new names) + await keyService.deleteWalletPassword(walletName: name); - Future updateMoneroWalletPassword(WalletBase wallet) async { - final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name); - var isPasswordUpdated = sharedPreferences.getBool(key) ?? false; + await walletService.rename(name, password, newName); - if (isPasswordUpdated) { - return; - } + // set shared preferences flag based on previous wallet name + if (type == WalletType.monero) { + final oldNameKey = PreferencesKey.moneroWalletUpdateV1Key(name); + final isPasswordUpdated = sharedPreferences.getBool(oldNameKey) ?? false; + final newNameKey = PreferencesKey.moneroWalletUpdateV1Key(newName); + await sharedPreferences.setBool(newNameKey, isPasswordUpdated); + } + } - final password = generateWalletPassword(); - // Save new generated password with backup key for case where - // wallet will change password, but it will fail to update in secure storage - final bakWalletName = '#__${wallet.name}_bak__#'; - await keyService.saveWalletPassword(walletName: bakWalletName, password: password); - await wallet.changePassword(password); - await keyService.saveWalletPassword(walletName: wallet.name, password: password); - isPasswordUpdated = true; - await sharedPreferences.setBool(key, isPasswordUpdated); - } -} \ No newline at end of file + Future load(WalletType type, String name) async { + try { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); + final wallet = await walletService.openWallet(name, password); + + if (type == WalletType.monero) { + await updateMoneroWalletPassword(wallet); + } + + return wallet; + } catch (error, stack) { + ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + + // try opening another wallet that is not corrupted to give user access to the app + final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + + for (var walletInfo in walletInfoSource.values) { + try { + final walletService = walletServiceFactory.call(walletInfo.type); + final password = await keyService.getWalletPassword(walletName: walletInfo.name); + final wallet = await walletService.openWallet(walletInfo.name, password); + + if (walletInfo.type == WalletType.monero) { + await updateMoneroWalletPassword(wallet); + } + + await sharedPreferences.setString(PreferencesKey.currentWalletName, wallet.name); + await sharedPreferences.setInt( + PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + + return wallet; + } catch (_) {} + } + + // if all user's wallets are corrupted throw exception + throw error; + } + } + + Future updateMoneroWalletPassword(WalletBase wallet) async { + final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name); + var isPasswordUpdated = sharedPreferences.getBool(key) ?? false; + + if (isPasswordUpdated) { + return; + } + + final password = generateWalletPassword(); + // Save new generated password with backup key for case where + // wallet will change password, but it will fail to update in secure storage + final bakWalletName = '#__${wallet.name}_bak__#'; + await keyService.saveWalletPassword(walletName: bakWalletName, password: password); + await wallet.changePassword(password); + await keyService.saveWalletPassword(walletName: wallet.name, password: password); + isPasswordUpdated = true; + await sharedPreferences.setBool(key, isPasswordUpdated); + } +} diff --git a/lib/di.dart b/lib/di.dart index 27f622278..151ab8311 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,26 +1,47 @@ import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cw_core/receive_page_option.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; -import 'package:cake_wallet/src/screens/buy/onramper_page.dart'; -import 'package:cake_wallet/src/screens/buy/payfura_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; +import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; +import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; +import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -28,18 +49,28 @@ import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dar import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; +import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; +import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; +import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; +import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; +import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; @@ -55,10 +86,14 @@ import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dar import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; import 'package:cake_wallet/src/screens/ionia/ionia.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; +import 'package:cake_wallet/view_model/seed_type_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; @@ -66,7 +101,13 @@ import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/nano_account.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -82,7 +123,6 @@ import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; -import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; @@ -159,14 +199,13 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; -import 'package:cake_wallet/view_model/wallet_restoration_from_keys_vm.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/wallet_type.dart'; @@ -181,7 +220,7 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; @@ -193,36 +232,42 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; +import 'src/screens/settings/desktop_settings/desktop_settings_page.dart'; final getIt = GetIt.instance; var _isSetupFinished = false; late Box _walletInfoSource; late Box _nodeSource; +late Box _powNodeSource; late Box _contactSource; late Box _tradesSource; late Box