Merge branch 'main' into zano-pr

This commit is contained in:
cyan 2024-11-11 12:39:45 +01:00 committed by GitHub
commit f22ab329c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
140 changed files with 5524 additions and 926 deletions

View file

@ -62,10 +62,22 @@ jobs:
/opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/cw_haven/android/.cxx
/opt/android/cake_wallet/scripts/monero_c/release /opt/android/cake_wallet/scripts/monero_c/release
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
name: Generate Externals name: Generate Externals
run: | run: |
cd /opt/android/cake_wallet/scripts/android/ cd /opt/android/cake_wallet/scripts/android/
source ./app_env.sh cakewallet source ./app_env.sh cakewallet
./build_monero_all.sh ./build_monero_all.sh
- name: Cache Keystore
id: cache-keystore
uses: actions/cache@v3
with:
path: /opt/android/cake_wallet/android/app/key.jks
key: $STORE_PASS
- if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
name: Generate KeyStore
run: |
cd /opt/android/cake_wallet/android/app
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS

View file

@ -116,6 +116,14 @@ jobs:
cd /opt/android/cake_wallet/scripts/android/ cd /opt/android/cake_wallet/scripts/android/
./build_mwebd.sh --dont-install ./build_mwebd.sh --dont-install
# - name: Cache Keystore
# id: cache-keystore
# uses: actions/cache@v3
# with:
# path: /opt/android/cake_wallet/android/app/key.jks
# key: $STORE_PASS
#
# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
- name: Generate KeyStore - name: Generate KeyStore
run: | run: |
cd /opt/android/cake_wallet/android/app cd /opt/android/cake_wallet/android/app
@ -193,6 +201,8 @@ jobs:
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
@ -202,6 +212,36 @@ jobs:
run: | run: |
echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
# Step 3: Download previous build number
- name: Download previous build number
id: download-build-number
run: |
# Download the artifact if it exists
if [[ ! -f build_number.txt ]]; then
echo "1" > build_number.txt
fi
# Step 4: Read and Increment Build Number
- name: Increment Build Number
id: increment-build-number
run: |
# Read current build number from file
BUILD_NUMBER=$(cat build_number.txt)
BUILD_NUMBER=$((BUILD_NUMBER + 1))
echo "New build number: $BUILD_NUMBER"
# Save the incremented build number
echo "$BUILD_NUMBER" > build_number.txt
# Export the build number to use in later steps
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
# Step 5: Update pubspec.yaml with new build number
- name: Update build number
run: |
cd /opt/android/cake_wallet
sed -i "s/^version: .*/version: 1.0.$BUILD_NUMBER/" pubspec.yaml
- name: Build - name: Build
run: | run: |
cd /opt/android/cake_wallet cd /opt/android/cake_wallet
@ -232,6 +272,13 @@ jobs:
with: with:
path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/
# Re-upload updated build number for the next run
- name: Upload updated build number
uses: actions/upload-artifact@v3
with:
name: build_number
path: build_number.txt
- name: Send Test APK - name: Send Test APK
continue-on-error: true continue-on-error: true
uses: adrey/slack-file-upload-action@1.0.5 uses: adrey/slack-file-upload-action@1.0.5
@ -242,3 +289,4 @@ jobs:
title: "${{ env.BRANCH_NAME }}.apk" title: "${{ env.BRANCH_NAME }}.apk"
filename: ${{ env.BRANCH_NAME }}.apk filename: ${{ env.BRANCH_NAME }}.apk
initial_comment: ${{ github.event.head_commit.message }} initial_comment: ${{ github.event.head_commit.message }}

View file

@ -175,6 +175,8 @@ jobs:
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart

View file

@ -92,3 +92,8 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
} }
configurations {
implementation.exclude module:'proto-google-common-protos'
implementation.exclude module:'protolite-well-known-types'
implementation.exclude module:'protobuf-javalite'
}

View file

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx3072M
android.enableR8=true android.enableR8=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/images/bank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/images/buy_sell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

1
assets/images/card.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?><!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px"><path style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/><rect x="3" y="16" width="44" height="5"/><line style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px">
<path style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/>
<rect x="3" y="16" width="44" height="5" style="fill:white;stroke:#ffffff;stroke-width:2;"/>
<line style="fill:none;stroke:#ffffff;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View file

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<g id="Outline">
<g data-name="Outline" id="Outline-2">
<path d="M39,36.852a6.8,6.8,0,0,0-6.793-6.793h-.319A3.716,3.716,0,1,1,35.6,26.344a1,1,0,0,0,2,0,5.725,5.725,0,0,0-4.561-5.6V18.09a1,1,0,0,0-2,0V20.7a5.712,5.712,0,0,0,.846,11.361h.319a4.793,4.793,0,1,1-4.793,4.793,1,1,0,0,0-2,0A6.8,6.8,0,0,0,31.451,43.6v2.947a1,1,0,0,0,2,0v-3c0-.008,0-.014,0-.021A6.8,6.8,0,0,0,39,36.852Z"/>
<path d="M32,2A30,30,0,1,0,62,32,30.034,30.034,0,0,0,32,2Zm0,58A28,28,0,1,1,60,32,28.032,28.032,0,0,1,32,60Z"/>
<path d="M49.655,16.793a3.172,3.172,0,1,0-3.172,3.172,3.137,3.137,0,0,0,1.263-.266A19.994,19.994,0,0,1,22.692,49.707a1,1,0,0,0-.933,1.769,21.986,21.986,0,0,0,27.47-33.124A3.141,3.141,0,0,0,49.655,16.793Zm-4.344,0a1.172,1.172,0,1,1,1.172,1.172A1.172,1.172,0,0,1,45.311,16.793Z"/>
<path d="M16.793,44.035a3.164,3.164,0,0,0-.692.081A19.779,19.779,0,0,1,12,32,20.023,20.023,0,0,1,32,12a19.811,19.811,0,0,1,8.463,1.874,1,1,0,0,0,.848-1.812A21.989,21.989,0,0,0,14.39,45.16a3.141,3.141,0,0,0-.769,2.047,3.172,3.172,0,1,0,3.172-3.172Zm0,4.344a1.172,1.172,0,1,1,1.172-1.172A1.172,1.172,0,0,1,16.793,48.379Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,30 @@
<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.7" d="M19.9526 41.9076L7.3877 34.655V26.1226L19.9526 33.3751V41.9076Z" fill="url(#paint0_linear_2113_32117)"/>
<path opacity="0.7" d="M19.9521 41.9076L32.5171 34.655V26.1226L19.9521 33.3751V41.9076Z" fill="url(#paint1_linear_2113_32117)"/>
<path opacity="0.7" d="M39.9095 7.34521V21.8562L32.5166 26.1225V11.6114L39.9095 7.34521Z" fill="url(#paint2_linear_2113_32117)"/>
<path d="M39.9099 7.34536L27.345 0.0927734L19.9521 4.359L32.5171 11.6116L39.9099 7.34536Z" fill="url(#paint3_linear_2113_32117)"/>
<path d="M0 7.34536L12.5649 0.0927734L19.9519 4.359L7.387 11.6116L0 7.34536Z" fill="#F969D3"/>
<path opacity="0.7" d="M0 7.34521V21.8562L7.387 26.1225V11.6114L0 7.34521Z" fill="url(#paint4_linear_2113_32117)"/>
<defs>
<linearGradient id="paint0_linear_2113_32117" x1="18.6099" y1="41.8335" x2="7.73529" y2="8.31842" gradientUnits="userSpaceOnUse">
<stop stop-color="#E98ADA"/>
<stop offset="1" stop-color="#7E4DBD"/>
</linearGradient>
<linearGradient id="paint1_linear_2113_32117" x1="26.2346" y1="26.1226" x2="26.2346" y2="41.9076" gradientUnits="userSpaceOnUse">
<stop stop-color="#719DED"/>
<stop offset="1" stop-color="#2545BE"/>
</linearGradient>
<linearGradient id="paint2_linear_2113_32117" x1="36.213" y1="7.34521" x2="36.213" y2="26.1225" gradientUnits="userSpaceOnUse">
<stop stop-color="#93EBFF"/>
<stop offset="1" stop-color="#197DDB"/>
</linearGradient>
<linearGradient id="paint3_linear_2113_32117" x1="29.931" y1="0.0927734" x2="38.2156" y2="14.8448" gradientUnits="userSpaceOnUse">
<stop stop-color="#F969D3"/>
<stop offset="1" stop-color="#4F51C0"/>
</linearGradient>
<linearGradient id="paint4_linear_2113_32117" x1="18.1251" y1="44.2539" x2="-7.06792" y2="15.2763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E98ADA"/>
<stop offset="1" stop-color="#7E4DBD"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/images/revolut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

15
assets/images/skrill.svg Normal file
View file

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(127,33,99); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="69.59,36.9 69.59,54.86 74.5,54.86 74.5,36.02 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="62.42,36.9 67.33,36.02 67.33,54.87 62.42,54.87 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<rect x="55.43" y="41.08" rx="0" ry="0" width="4.91" height="13.78" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<path d="M 57.879 39.76 c 1.332 0 2.425 -1.082 2.425 -2.415 s -1.082 -2.425 -2.425 -2.425 c -1.332 0 -2.415 1.082 -2.415 2.425 C 55.465 38.677 56.547 39.76 57.879 39.76 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 52.665 40.884 c -4.538 0.146 -6.838 2.186 -6.838 6.234 v 7.754 h 4.954 v -6.328 c 0 -2.425 0.312 -3.466 3.195 -3.559 v -4.038 C 53.477 40.853 52.665 40.884 52.665 40.884 L 52.665 40.884 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 39.344 41.071 c -0.104 0.271 -0.895 2.498 -2.8 4.798 v -9.845 l -5.068 0.999 v 17.838 h 5.068 v -5.516 c 1.467 2.206 2.196 5.516 2.196 5.516 h 6.068 c -0.604 -2.498 -3.226 -7.098 -3.226 -7.098 c 2.352 -2.987 3.393 -6.172 3.559 -6.702 h -5.797 L 39.344 41.071 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 22.973 43.028 c -0.624 -0.042 -2.061 -0.135 -2.061 -1.426 c 0 -1.561 2.071 -1.561 2.841 -1.561 c 1.363 0 3.133 0.406 4.392 0.781 c 0 0 0.708 0.25 1.301 0.5 l 0.052 0.01 v -4.267 l -0.073 -0.021 c -1.488 -0.52 -3.216 -1.02 -6.432 -1.02 c -5.537 0 -7.493 3.226 -7.493 5.984 c 0 1.592 0.687 5.339 7.025 5.776 c 0.541 0.031 1.967 0.114 1.967 1.457 c 0 1.103 -1.166 1.759 -3.133 1.759 c -2.154 0 -4.236 -0.552 -5.506 -1.072 v 4.402 c 1.894 0.5 4.038 0.749 6.546 0.749 c 5.412 0 7.837 -3.049 7.837 -6.078 C 30.237 45.567 27.531 43.34 22.973 43.028 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
<path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 978 B

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512"><path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z"/></svg>

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,6 +1,7 @@
- -
uri: xmr-node.cakewallet.com:18081 uri: xmr-node.cakewallet.com:18081
is_default: true is_default: true
trusted: true
- -
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
is_default: false is_default: false

View file

@ -11,7 +11,6 @@ import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart';
@ -597,8 +596,8 @@ abstract class ElectrumWalletBase
UtxoDetails _createUTXOS({ UtxoDetails _createUTXOS({
required bool sendAll, required bool sendAll,
required int credentialsAmount,
required bool paysToSilentPayment, required bool paysToSilentPayment,
int credentialsAmount = 0,
int? inputsCount, int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) { }) {
@ -732,13 +731,11 @@ abstract class ElectrumWalletBase
List<BitcoinOutput> outputs, List<BitcoinOutput> outputs,
int feeRate, { int feeRate, {
String? memo, String? memo,
int credentialsAmount = 0,
bool hasSilentPayment = false, bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async { }) async {
final utxoDetails = _createUTXOS( final utxoDetails = _createUTXOS(
sendAll: true, sendAll: true,
credentialsAmount: credentialsAmount,
paysToSilentPayment: hasSilentPayment, paysToSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom, coinTypeToSpendFrom: coinTypeToSpendFrom,
); );
@ -764,23 +761,11 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
} }
if (amount <= 0) {
throw BitcoinTransactionWrongBalanceException();
}
// Attempting to send less than the dust limit // Attempting to send less than the dust limit
if (_isBelowDust(amount)) { if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException(); throw BitcoinTransactionNoDustException();
} }
if (credentialsAmount > 0) {
final amountLeftForFee = amount - credentialsAmount;
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
amount -= amountLeftForFee;
fee += amountLeftForFee;
}
}
if (outputs.length == 1) { if (outputs.length == 1) {
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
} }
@ -810,6 +795,11 @@ abstract class ElectrumWalletBase
bool hasSilentPayment = false, bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async { }) async {
// Attempting to send less than the dust limit
if (_isBelowDust(credentialsAmount)) {
throw BitcoinTransactionNoDustException();
}
final utxoDetails = _createUTXOS( final utxoDetails = _createUTXOS(
sendAll: false, sendAll: false,
credentialsAmount: credentialsAmount, credentialsAmount: credentialsAmount,
@ -894,7 +884,43 @@ abstract class ElectrumWalletBase
final lastOutput = updatedOutputs.last; final lastOutput = updatedOutputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee; final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (!_isBelowDust(amountLeftForChange)) { if (_isBelowDust(amountLeftForChange)) {
// If has change that is lower than dust, will end up with tx rejected by network rules
// so remove the change amount
updatedOutputs.removeLast();
outputs.removeLast();
if (amountLeftForChange < 0) {
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
throw BitcoinTransactionWrongBalanceException();
}
}
return EstimatedTxResult(
utxos: utxoDetails.utxos,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
publicKeys: utxoDetails.publicKeys,
fee: fee,
amount: amount,
hasChange: false,
isSendAll: spendingAllCoins,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
spendsSilentPayment: utxoDetails.spendsSilentPayment,
);
} else {
// Here, lastOutput already is change, return the amount left without the fee to the user's address. // Here, lastOutput already is change, return the amount left without the fee to the user's address.
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
address: lastOutput.address, address: lastOutput.address,
@ -908,75 +934,6 @@ abstract class ElectrumWalletBase
isSilentPayment: lastOutput.isSilentPayment, isSilentPayment: lastOutput.isSilentPayment,
isChange: true, isChange: true,
); );
} else {
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
updatedOutputs.removeLast();
outputs.removeLast();
// Still has inputs to spend before failing
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
final estimatedSendAll = await estimateSendAllTx(
updatedOutputs,
feeRate,
memo: memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
if (estimatedSendAll.amount == credentialsAmount) {
return estimatedSendAll;
}
// Estimate to user how much is needed to send to cover the fee
final maxAmountWithReturningChange = utxoDetails.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 + balance[currency]!.secondConfirmed)) {
throw BitcoinTransactionWrongBalanceException();
}
if (totalAmount > utxoDetails.allInputsAmount) {
if (spendingAllCoins) {
throw BitcoinTransactionWrongBalanceException();
} else {
updatedOutputs.removeLast();
outputs.removeLast();
return estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
}
return EstimatedTxResult( return EstimatedTxResult(
utxos: utxoDetails.utxos, utxos: utxoDetails.utxos,
@ -985,12 +942,13 @@ abstract class ElectrumWalletBase
fee: fee, fee: fee,
amount: amount, amount: amount,
hasChange: true, hasChange: true,
isSendAll: false, isSendAll: spendingAllCoins,
memo: memo, memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
spendsSilentPayment: utxoDetails.spendsSilentPayment, spendsSilentPayment: utxoDetails.spendsSilentPayment,
); );
} }
}
Future<int> calcFee({ Future<int> calcFee({
required List<UtxoWithAddress> utxos, required List<UtxoWithAddress> utxos,
@ -1080,15 +1038,20 @@ abstract class ElectrumWalletBase
: feeRate(transactionCredentials.priority!); : feeRate(transactionCredentials.priority!);
EstimatedTxResult estimatedTx; EstimatedTxResult estimatedTx;
final updatedOutputs = final updatedOutputs = outputs
outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); .map((e) => BitcoinOutput(
address: e.address,
value: e.value,
isSilentPayment: e.isSilentPayment,
isChange: e.isChange,
))
.toList();
if (sendAll) { if (sendAll) {
estimatedTx = await estimateSendAllTx( estimatedTx = await estimateSendAllTx(
updatedOutputs, updatedOutputs,
feeRateInt, feeRateInt,
memo: memo, memo: memo,
credentialsAmount: credentialsAmount,
hasSilentPayment: hasSilentPayment, hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom, coinTypeToSpendFrom: coinTypeToSpendFrom,
); );

View file

@ -153,4 +153,9 @@ class PendingBitcoinTransaction with PendingTransaction {
inputAddresses: _tx.inputs.map((input) => input.txId).toList(), inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
outputAddresses: outputAddresses, outputAddresses: outputAddresses,
fee: fee); fee: fee);
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -85,4 +85,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
fee: fee, fee: fee,
isReplaced: false, isReplaced: false,
); );
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -38,3 +38,34 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
} }
} }
WalletType? walletTypeForCurrency(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.btc:
return WalletType.bitcoin;
case CryptoCurrency.xmr:
return WalletType.monero;
case CryptoCurrency.ltc:
return WalletType.litecoin;
case CryptoCurrency.xhv:
return WalletType.haven;
case CryptoCurrency.eth:
return WalletType.ethereum;
case CryptoCurrency.bch:
return WalletType.bitcoinCash;
case CryptoCurrency.nano:
return WalletType.nano;
case CryptoCurrency.banano:
return WalletType.banano;
case CryptoCurrency.maticpoly:
return WalletType.polygon;
case CryptoCurrency.sol:
return WalletType.solana;
case CryptoCurrency.trx:
return WalletType.tron;
case CryptoCurrency.wow:
return WalletType.wownero;
default:
return null;
}
}

View file

@ -1,10 +1,12 @@
class MoneroWalletKeys { class MoneroWalletKeys {
const MoneroWalletKeys( const MoneroWalletKeys(
{required this.privateSpendKey, {required this.primaryAddress,
required this.privateSpendKey,
required this.privateViewKey, required this.privateViewKey,
required this.publicSpendKey, required this.publicSpendKey,
required this.publicViewKey}); required this.publicViewKey});
final String primaryAddress;
final String publicViewKey; final String publicViewKey;
final String privateViewKey; final String privateViewKey;
final String publicSpendKey; final String publicSpendKey;

View file

@ -14,5 +14,8 @@ mixin PendingTransaction {
int? get outputCount => null; int? get outputCount => null;
PendingChange? change; PendingChange? change;
bool shouldCommitUR() => false;
Future<void> commit(); Future<void> commit();
Future<String?> commitUR();
} }

View file

@ -47,4 +47,9 @@ class PendingEVMChainTransaction with PendingTransaction {
return '0x${Hex.HEX.encode(txid)}'; return '0x${Hex.HEX.encode(txid)}';
} }
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -2,4 +2,9 @@ class WalletRestoreFromKeysException implements Exception {
WalletRestoreFromKeysException({required this.message}); WalletRestoreFromKeysException({required this.message});
final String message; final String message;
@override
String toString() {
return message;
}
} }

View file

@ -73,6 +73,7 @@ abstract class HavenWalletBase
@override @override
MoneroWalletKeys get keys => MoneroWalletKeys( MoneroWalletKeys get keys => MoneroWalletKeys(
primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0),
privateSpendKey: haven_wallet.getSecretSpendKey(), privateSpendKey: haven_wallet.getSecretSpendKey(),
privateViewKey: haven_wallet.getSecretViewKey(), privateViewKey: haven_wallet.getSecretViewKey(),
publicSpendKey: haven_wallet.getPublicSpendKey(), publicSpendKey: haven_wallet.getPublicSpendKey(),

View file

@ -48,4 +48,9 @@ class PendingHavenTransaction with PendingTransaction {
rethrow; rethrow;
} }
} }
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -2,6 +2,7 @@ import 'package:cw_monero/api/wallet.dart';
import 'package:monero/monero.dart' as monero; import 'package:monero/monero.dart' as monero;
monero.wallet? wptr = null; monero.wallet? wptr = null;
bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
int _wlptrForW = 0; int _wlptrForW = 0;
monero.WalletListener? _wlptr = null; monero.WalletListener? _wlptr = null;

View file

@ -13,7 +13,13 @@ import 'package:mutex/mutex.dart';
String getTxKey(String txId) { String getTxKey(String txId) {
return monero.Wallet_getTxKey(wptr!, txid: txId); final txKey = monero.Wallet_getTxKey(wptr!, txid: txId);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final error = monero.Wallet_errorString(wptr!);
return txId+"_"+error;
}
return txKey;
} }
final txHistoryMutex = Mutex(); final txHistoryMutex = Mutex();
monero.TransactionHistory? txhistory; monero.TransactionHistory? txhistory;
@ -178,12 +184,13 @@ PendingTransactionDescription createTransactionMultDestSync(
); );
} }
void commitTransactionFromPointerAddress({required int address}) => String? commitTransactionFromPointerAddress({required int address, required bool useUR}) =>
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address)); commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR);
void commitTransaction({required monero.PendingTransaction transactionPointer}) { String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) {
final txCommit = useUR
final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); ? monero.PendingTransaction_commitUR(transactionPointer, 120)
: monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
final String? error = (() { final String? error = (() {
final status = monero.PendingTransaction_status(transactionPointer.cast()); final status = monero.PendingTransaction_status(transactionPointer.cast());
@ -196,6 +203,11 @@ void commitTransaction({required monero.PendingTransaction transactionPointer})
if (error != null) { if (error != null) {
throw CreationTransactionException(message: error); throw CreationTransactionException(message: error);
} }
if (useUR) {
return txCommit as String?;
} else {
return null;
}
} }
Future<PendingTransactionDescription> _createTransactionSync(Map args) async { Future<PendingTransactionDescription> _createTransactionSync(Map args) async {

View file

@ -425,3 +425,5 @@ Future<void> restoreFromSpendKey(
}); });
bool isWalletExist({required String path}) => _isWalletExist(path); bool isWalletExist({required String path}) => _isWalletExist(path);
bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;

View file

@ -19,6 +19,7 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/coins_info.dart';
import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/monero_output.dart';
import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart';
@ -121,6 +122,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override @override
MoneroWalletKeys get keys => MoneroWalletKeys( MoneroWalletKeys get keys => MoneroWalletKeys(
primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
privateSpendKey: monero_wallet.getSecretSpendKey(), privateSpendKey: monero_wallet.getSecretSpendKey(),
privateViewKey: monero_wallet.getSecretViewKey(), privateViewKey: monero_wallet.getSecretViewKey(),
publicSpendKey: monero_wallet.getPublicSpendKey(), publicSpendKey: monero_wallet.getPublicSpendKey(),
@ -230,6 +232,36 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
} }
} }
Future<bool> submitTransactionUR(String ur) async {
final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err");
}
return retStatus;
}
bool importKeyImagesUR(String ur) {
final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
throw Exception("unable to import key images: $err");
}
return retStatus;
}
String exportOutputsUR(bool all) {
final str = monero.Wallet_exportOutputsUR(wptr!, all: all);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
throw MoneroTransactionCreationException("unable to export UR: $err");
}
return str;
}
@override @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as MoneroTransactionCreationCredentials; final _credentials = credentials as MoneroTransactionCreationCredentials;

View file

@ -1,3 +1,4 @@
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart';
import 'package:cw_monero/api/transaction_history.dart' import 'package:cw_monero/api/transaction_history.dart'
as monero_transaction_history; as monero_transaction_history;
@ -35,11 +36,32 @@ class PendingMoneroTransaction with PendingTransaction {
String get feeFormatted => AmountConverter.amountIntToString( String get feeFormatted => AmountConverter.amountIntToString(
CryptoCurrency.xmr, pendingTransactionDescription.fee); CryptoCurrency.xmr, pendingTransactionDescription.fee);
bool shouldCommitUR() => isViewOnly;
@override @override
Future<void> commit() async { Future<void> commit() async {
try { try {
monero_transaction_history.commitTransactionFromPointerAddress( monero_transaction_history.commitTransactionFromPointerAddress(
address: pendingTransactionDescription.pointerAddress); address: pendingTransactionDescription.pointerAddress,
useUR: false);
} catch (e) {
final message = e.toString();
if (message.contains('Reason: double spend')) {
throw DoubleSpendException();
}
rethrow;
}
}
@override
Future<String?> commitUR() async {
try {
final ret = monero_transaction_history.commitTransactionFromPointerAddress(
address: pendingTransactionDescription.pointerAddress,
useUR: true);
return ret;
} catch (e) { } catch (e) {
final message = e.toString(); final message = e.toString();

View file

@ -37,4 +37,9 @@ class PendingNanoTransaction with PendingTransaction {
await nanoClient.processBlock(block, "send"); await nanoClient.processBlock(block, "send");
} }
} }
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -0,0 +1,189 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
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: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View file

@ -40,4 +40,9 @@ class PendingSolanaTransaction with PendingTransaction {
@override @override
String get id => ''; String get id => '';
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -30,4 +30,9 @@ class PendingTronTransaction with PendingTransaction {
@override @override
String get id => ''; String get id => '';
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -3,5 +3,6 @@ class WalletRestoreFromKeysException implements Exception {
final String message; final String message;
@override
String toString() => message; String toString() => message;
} }

View file

@ -50,4 +50,9 @@ class PendingWowneroTransaction with PendingTransaction {
rethrow; rethrow;
} }
} }
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
} }

View file

@ -120,6 +120,7 @@ abstract class WowneroWalletBase
@override @override
MoneroWalletKeys get keys => MoneroWalletKeys( MoneroWalletKeys get keys => MoneroWalletKeys(
primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
privateSpendKey: wownero_wallet.getSecretSpendKey(), privateSpendKey: wownero_wallet.getSecretSpendKey(),
privateViewKey: wownero_wallet.getSecretViewKey(), privateViewKey: wownero_wallet.getSecretViewKey(),
publicSpendKey: wownero_wallet.getPublicSpendKey(), publicSpendKey: wownero_wallet.getPublicSpendKey(),

View file

@ -1,6 +1,10 @@
import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,14 +27,38 @@ abstract class BuyProvider {
String get darkIcon; String get darkIcon;
bool get isAggregator;
@override @override
String toString() => title; String toString() => title;
Future<void> launchProvider(BuildContext context, bool? isBuyAction); Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) =>
null;
Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
Future<Order> findOrderById(String id) => throw UnimplementedError(); Future<Order> findOrderById(String id) => throw UnimplementedError();
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError(); Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) =>
throw UnimplementedError();
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async =>
[];
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async =>
null;
} }

302
lib/buy/buy_quote.dart Normal file
View file

@ -0,0 +1,302 @@
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cw_core/crypto_currency.dart';
enum ProviderRecommendation { bestRate, lowKyc, successRate }
extension RecommendationTitle on ProviderRecommendation {
String get title {
switch (this) {
case ProviderRecommendation.bestRate:
return 'BEST RATE';
case ProviderRecommendation.lowKyc:
return 'LOW KYC';
case ProviderRecommendation.successRate:
return 'SUCCESS RATE';
}
}
}
ProviderRecommendation? getRecommendationFromString(String title) {
switch (title) {
case 'BEST RATE':
return ProviderRecommendation.bestRate;
case 'LowKyc':
return ProviderRecommendation.lowKyc;
case 'SuccessRate':
return ProviderRecommendation.successRate;
default:
return null;
}
}
class Quote extends SelectableOption {
Quote({
required this.rate,
required this.feeAmount,
required this.networkFee,
required this.transactionFee,
required this.payout,
required this.provider,
required this.paymentType,
required this.recommendations,
this.isBuyAction = true,
this.quoteId,
this.rampId,
this.rampName,
this.rampIconPath,
this.limits,
}) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
final double rate;
final double feeAmount;
final double networkFee;
final double transactionFee;
final double payout;
final PaymentType paymentType;
final BuyProvider provider;
final String? quoteId;
final List<ProviderRecommendation> recommendations;
String? rampId;
String? rampName;
String? rampIconPath;
bool _isSelected = false;
bool _isBestRate = false;
bool isBuyAction;
Limits? limits;
late FiatCurrency _fiatCurrency;
late CryptoCurrency _cryptoCurrency;
bool get isSelected => _isSelected;
bool get isBestRate => _isBestRate;
FiatCurrency get fiatCurrency => _fiatCurrency;
CryptoCurrency get cryptoCurrency => _cryptoCurrency;
@override
bool get isOptionSelected => this._isSelected;
@override
String get lightIconPath =>
provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon;
@override
String get darkIconPath =>
provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon;
@override
List<String> get badges => recommendations.map((e) => e.title).toList();
@override
String get topLeftSubTitle =>
this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : '';
@override
String get bottomLeftSubTitle {
if (limits != null) {
final min = limits!.min;
final max = limits!.max;
return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}';
}
return '';
}
String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString();
String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString();
@override
String? get topRightSubTitle => '';
@override
String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
@override
String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
String get quoteTitle => '${provider.title} - ${paymentType.name}';
String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}';
set setIsSelected(bool isSelected) => _isSelected = isSelected;
set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate;
set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency;
set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency;
set setLimits(Limits limits) => this.limits = limits;
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
Map<String, dynamic> metaData, PaymentType paymentType) {
final rate = _toDouble(json['rate']) ?? 0.0;
final networkFee = _toDouble(json['networkFee']) ?? 0.0;
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2));
final rampId = json['ramp'] as String? ?? '';
final rampData = metaData[rampId] ?? {};
final rampName = rampData['displayName'] as String? ?? '';
final rampIconPath = rampData['svg'] as String? ?? '';
final recommendations = json['recommendations'] != null
? List<String>.from(json['recommendations'] as List<dynamic>)
: <String>[];
final enumRecommendations = recommendations
.map((e) => getRecommendationFromString(e))
.whereType<ProviderRecommendation>()
.toList();
final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
double minLimit = 0.0;
double maxLimit = double.infinity;
for (var paymentMethod in availablePaymentMethods) {
if (paymentMethod is Map<String, dynamic>) {
final details = paymentMethod['details'] as Map<String, dynamic>?;
if (details != null) {
final limits = details['limits'] as Map<String, dynamic>?;
if (limits != null && limits.isNotEmpty) {
final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
if (firstLimitEntry != null) {
minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
break;
}
}
}
}
}
return Quote(
rate: rate,
feeAmount: feeAmount,
networkFee: networkFee,
transactionFee: transactionFee,
payout: json['payout'] as double? ?? 0.0,
rampId: rampId,
rampName: rampName,
rampIconPath: rampIconPath,
paymentType: paymentType,
quoteId: json['quoteId'] as String? ?? '',
recommendations: enumRecommendations,
provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
factory Quote.fromMoonPayJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final rate = isBuyAction
? json['quoteCurrencyPrice'] as double? ?? 0.0
: json['baseCurrencyPrice'] as double? ?? 0.0;
final fee = _toDouble(json['feeAmount']) ?? 0.0;
final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0;
final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0;
final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2));
final baseCurrency = json['baseCurrency'] as Map<String, dynamic>?;
double minLimit = 0.0;
double maxLimit = double.infinity;
if (baseCurrency != null) {
minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit;
maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit;
}
return Quote(
rate: rate,
feeAmount: feeAmount,
networkFee: networkFee,
transactionFee: transactionFee,
payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0,
paymentType: paymentType,
recommendations: [],
quoteId: json['signature'] as String? ?? '',
provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
factory Quote.fromDFXJson(
Map<String, dynamic> json,
bool isBuyAction,
PaymentType paymentType,
) {
final rate = _toDouble(json['exchangeRate']) ?? 0.0;
final fees = json['fees'] as Map<String, dynamic>;
final minVolume = _toDouble(json['minVolume']) ?? 0.0;
final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
return Quote(
rate: isBuyAction ? rate : 1 / rate,
feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
networkFee: _toDouble(fees['network']) ?? 0.0,
transactionFee: _toDouble(fees['rate']) ?? 0.0,
payout: _toDouble(json['payout']) ?? 0.0,
paymentType: paymentType,
recommendations: [ProviderRecommendation.lowKyc],
provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!,
isBuyAction: isBuyAction,
limits: Limits(min: minVolume, max: maxVolume),
);
}
factory Quote.fromRobinhoodJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final networkFee = json['networkFee'] as Map<String, dynamic>;
final processingFee = json['processingFee'] as Map<String, dynamic>;
final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0;
final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0;
final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2));
return Quote(
rate: _toDouble(json['price']) ?? 0.0,
feeAmount: feeAmount,
networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0,
transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0,
payout: _toDouble(json['cryptoAmount']) ?? 0.0,
paymentType: paymentType,
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}
factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final quotes = json['quotes'][0] as Map<String, dynamic>;
return Quote(
rate: quotes['exchangeRate'] as double? ?? 0.0,
feeAmount: quotes['totalFee'] as double? ?? 0.0,
networkFee: quotes['networkFee'] as double? ?? 0.0,
transactionFee: quotes['transactionFee'] as double? ?? 0.0,
payout: quotes['payout'] as double? ?? 0.0,
paymentType: paymentType,
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.meld)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}
static double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else if (value is String) {
return double.tryParse(value);
}
return null;
}
}

View file

@ -1,13 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.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:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,10 +19,12 @@ import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class DFXBuyProvider extends BuyProvider { class DFXBuyProvider extends BuyProvider {
DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) DFXBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
static const _baseUrl = 'api.dfx.swiss'; static const _baseUrl = 'api.dfx.swiss';
// static const _signMessagePath = '/v1/auth/signMessage'; // static const _signMessagePath = '/v1/auth/signMessage';
static const _authPath = '/v1/auth'; static const _authPath = '/v1/auth';
static const walletName = 'CakeWallet'; static const walletName = 'CakeWallet';
@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider {
@override @override
String get darkIcon => 'assets/images/dfx_dark.png'; String get darkIcon => 'assets/images/dfx_dark.png';
String get assetOut { @override
switch (wallet.type) { bool get isAggregator => false;
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 { String get blockchain {
switch (wallet.type) { switch (wallet.type) {
@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider {
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
case WalletType.litecoin: case WalletType.litecoin:
return 'Bitcoin'; return 'Bitcoin';
case WalletType.monero:
return 'Monero';
case WalletType.ethereum:
return 'Ethereum';
case WalletType.polygon:
return 'Polygon';
default: default:
throw Exception("WalletType is not available for DFX ${wallet.type}"); return walletTypeToString(wallet.type);
} }
} }
String get walletAddress =>
wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address;
Future<String> getSignMessage() async => Future<String> getSignMessage(String walletAddress) async =>
"By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; "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 // // Lets keep this just in case, but we can avoid this API Call
@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider {
// } // }
// } // }
Future<String> auth() async { Future<String> auth(String walletAddress) async {
final signMessage = await getSignature(await getSignMessage()); final signMessage = await getSignature(
await getSignMessage(walletAddress), walletAddress);
final requestBody = jsonEncode({ final requestBody = jsonEncode({
'wallet': walletName, 'wallet': walletName,
@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider {
} }
} }
Future<String> getSignature(String message) async { Future<String> getSignature(String message, String walletAddress) async {
switch (wallet.type) { switch (wallet.type) {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider {
} }
} }
Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
final url = Uri.https(_baseUrl, '/v1/fiat');
try {
final response = await http.get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
for (final item in data) {
if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
}
log('DFX does not support fiat: $fiatCurrency');
return {};
} else {
log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
return {};
}
} catch (e) {
print('DFX Error fetching fiat currencies: $e');
return {};
}
}
Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
try {
final response = await http.get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
if (responseData is List && responseData.isNotEmpty) {
return responseData.first as Map<String, dynamic>;
} else if (responseData is Map<String, dynamic>) {
return responseData;
} else {
log('DFX: Does not support this asset name : ${blockchain}');
}
} else {
log('DFX: Failed to fetch assets: ${response.statusCode}');
}
} catch (e) {
log('DFX: Error fetching assets: $e');
}
return {};
}
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final List<PaymentMethod> paymentMethods = [];
if (isBuyAction) {
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
if (fiatBuyCredentials.isNotEmpty) {
fiatBuyCredentials.forEach((key, value) {
if (key == 'limits') {
final limits = value as Map<String, dynamic>;
limits.forEach((paymentMethodKey, paymentMethodValue) {
final min = _toDouble(paymentMethodValue['minVolume']);
final max = _toDouble(paymentMethodValue['maxVolume']);
if (min != null && max != null && min > 0 && max > 0) {
final paymentMethod = PaymentMethod.fromDFX(
paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
paymentMethods.add(paymentMethod);
}
});
}
});
}
} else {
final assetCredentials = await fetchAssetCredential(cryptoCurrency);
if (assetCredentials.isNotEmpty) {
if (assetCredentials['sellable'] == true) {
final availablePaymentTypes = [
PaymentType.bankTransfer,
PaymentType.creditCard,
PaymentType.sepa
];
availablePaymentTypes.forEach((element) {
final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
paymentMethods.add(paymentMethod);
});
}
}
}
return paymentMethods;
}
@override @override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async { Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
/// if buying with any currency other than eur or chf then DFX is not supported
if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
return null;
}
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
} else {
paymentMethod = 'Bank';
}
final action = isBuyAction ? 'buy' : 'sell';
if (isBuyAction && cryptoCurrency != wallet.currency) return null;
final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
if (fiatCredentials['id'] == null) return null;
final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
if (assetCredentials['id'] == null) return null;
log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
final url = Uri.https(_baseUrl, '/v1/$action/quote');
final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
final body = jsonEncode({
'currency': {'id': fiatCredentials['id'] as int},
'asset': {'id': assetCredentials['id']},
'amount': amount,
'targetAmount': 0,
'paymentMethod': paymentMethod,
'discountCode': ''
});
try {
final response = await http.put(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
if (responseData is Map<String, dynamic>) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
print('DFX: Unexpected data type: ${responseData.runtimeType}');
return null;
}
} else {
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
print('DFX Error: ${responseData['message']}');
} else {
print('DFX Failed to fetch buy quote: ${response.statusCode}');
}
return null;
}
} catch (e) {
print('DFX Error fetching buy quote: $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) { if (wallet.isHardwareWallet) {
if (!ledgerVM!.isConnected) { if (!ledgerVM!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices, await Navigator.of(context).pushNamed(Routes.connectDevices,
@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider {
} }
try { try {
final assetOut = this.assetOut; final actionType = isBuyAction ? '/buy' : '/sell';
final blockchain = this.blockchain;
final actionType = isBuyAction == true ? '/buy' : '/sell';
final accessToken = await auth(); final accessToken = await auth(cryptoCurrencyAddress);
final uri = Uri.https('services.dfx.swiss', actionType, { final uri = Uri.https('services.dfx.swiss', actionType, {
'session': accessToken, 'session': accessToken,
'lang': 'en', 'lang': 'en',
'asset-out': assetOut, 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
'blockchain': blockchain, 'blockchain': blockchain,
'asset-in': 'EUR', 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
'amount': amount.toString() //TODO: Amount does not work
}); });
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else { } else {
throw Exception('Could not launch URL'); throw Exception('Could not launch URL');
} }
@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider {
}); });
} }
} }
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.bankTransfer:
return 'Bank';
case PaymentType.creditCard:
return 'Card';
case PaymentType.sepa:
return 'Instant';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'Bank':
return PaymentType.bankTransfer;
case 'Card':
return PaymentType.creditCard;
case 'Instant':
return PaymentType.sepa;
default:
return PaymentType.all;
}
}
double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
}
return null;
}
} }

View file

@ -0,0 +1,266 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class MeldBuyProvider extends BuyProvider {
MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
static const _isProduction = false;
static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io';
static const _providersProperties = '/service-providers/properties';
static const _paymentMethodsPath = '/payment-methods';
static const _quotePath = '/payments/crypto/quote';
static const String sandboxUrl = 'sb.fluidmoney.xyz';
static const String productionUrl = 'fluidmoney.xyz';
static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl;
static String get _testApiKey => secrets.meldTestApiKey;
static String get _testPublicKey => '' ; //secrets.meldTestPublicKey;
@override
String get title => 'Meld';
@override
String get providerDescription => 'Meld Buy Provider';
@override
String get lightIcon => 'assets/images/meld_logo.svg';
@override
String get darkIcon => 'assets/images/meld_logo.svg';
@override
bool get isAggregator => true;
@override
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
final path = '$_providersProperties$_paymentMethodsPath';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await http.get(
url,
headers: {
'Authorization': _isProduction ? '' : _testApiKey,
'Meld-Version': '2023-12-19',
'accept': 'application/json',
'content-type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
final paymentMethods =
data.map((e) => PaymentMethod.fromMeldJson(e as Map<String, dynamic>)).toList();
return paymentMethods;
} else {
print('Meld: Failed to fetch payment types');
return List<PaymentMethod>.empty();
}
} catch (e) {
print('Meld: Failed to fetch payment types: $e');
return List<PaymentMethod>.empty();
}
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
final url = Uri.https(_baseUrl, _quotePath);
final headers = {
'Authorization': _testApiKey,
'Meld-Version': '2023-12-19',
'accept': 'application/json',
'content-type': 'application/json',
};
final body = jsonEncode({
'countryCode': countryCode,
'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title,
'sourceAmount': amount,
'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name,
if (paymentMethod != null) 'paymentMethod': paymentMethod,
});
try {
final response = await http.post(url, headers: headers, body: body);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?);
final quote = Quote.fromMeldJson(data, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
return null;
}
} catch (e) {
print('Error fetching buy quote: $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final actionType = isBuyAction ? 'BUY' : 'SELL';
final params = {
'publicKey': _isProduction ? '' : _testPublicKey,
'countryCode': countryCode,
//'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType),
'sourceAmount': amount.toString(),
'sourceCurrencyCode': quote.fiatCurrency,
'destinationCurrencyCode': quote.cryptoCurrency,
'walletAddress': cryptoCurrencyAddress,
'transactionType': actionType
};
final uri = Uri.https(_baseWidgetUrl, '', params);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: "Meld",
alertContent: S.of(context).buy_provider_unavailable + ': $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}
String? normalizePaymentMethod(PaymentType paymentType) {
switch (paymentType) {
case PaymentType.creditCard:
return 'CREDIT_DEBIT_CARD';
case PaymentType.applePay:
return 'APPLE_PAY';
case PaymentType.googlePay:
return 'GOOGLE_PAY';
case PaymentType.neteller:
return 'NETELLER';
case PaymentType.skrill:
return 'SKRILL';
case PaymentType.sepa:
return 'SEPA';
case PaymentType.sepaInstant:
return 'SEPA_INSTANT';
case PaymentType.ach:
return 'ACH';
case PaymentType.achInstant:
return 'INSTANT_ACH';
case PaymentType.Khipu:
return 'KHIPU';
case PaymentType.ovo:
return 'OVO';
case PaymentType.zaloPay:
return 'ZALOPAY';
case PaymentType.zaloBankTransfer:
return 'ZA_BANK_TRANSFER';
case PaymentType.gcash:
return 'GCASH';
case PaymentType.imps:
return 'IMPS';
case PaymentType.dana:
return 'DANA';
case PaymentType.ideal:
return 'IDEAL';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod?.toUpperCase()) {
case 'CREDIT_DEBIT_CARD':
return PaymentType.creditCard;
case 'APPLE_PAY':
return PaymentType.applePay;
case 'GOOGLE_PAY':
return PaymentType.googlePay;
case 'NETELLER':
return PaymentType.neteller;
case 'SKRILL':
return PaymentType.skrill;
case 'SEPA':
return PaymentType.sepa;
case 'SEPA_INSTANT':
return PaymentType.sepaInstant;
case 'ACH':
return PaymentType.ach;
case 'INSTANT_ACH':
return PaymentType.achInstant;
case 'KHIPU':
return PaymentType.Khipu;
case 'OVO':
return PaymentType.ovo;
case 'ZALOPAY':
return PaymentType.zaloPay;
case 'ZA_BANK_TRANSFER':
return PaymentType.zaloBankTransfer;
case 'GCASH':
return PaymentType.gcash;
case 'IMPS':
return PaymentType.imps;
case 'DANA':
return PaymentType.dana;
case 'IDEAL':
return PaymentType.ideal;
default:
return PaymentType.all;
}
}
}

View file

@ -1,19 +1,20 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets; 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_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.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/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider {
static const _baseBuyProductUrl = 'buy.moonpay.com'; static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
static const _apiUrl = 'https://api.moonpay.com'; static const _apiUrl = 'https://api.moonpay.com';
static const _baseUrl = 'api.moonpay.com';
static const _currenciesPath = '/v3/currencies';
static const _buyQuote = '/buy_quote';
static const _sellQuote = '/sell_quote';
static const _transactionsSuffix = '/v1/transactions';
final String baseBuyUrl;
final String baseSellUrl;
@override @override
String get providerDescription => String get providerDescription =>
@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider {
@override @override
String get darkIcon => 'assets/images/moonpay_dark.png'; String get darkIcon => 'assets/images/moonpay_dark.png';
@override
bool get isAggregator => false;
static String get _apiKey => secrets.moonPayApiKey;
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
static String themeToMoonPayTheme(ThemeBase theme) { static String themeToMoonPayTheme(ThemeBase theme) {
switch (theme.type) { switch (theme.type) {
case ThemeType.bright: case ThemeType.bright:
@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider {
} }
} }
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<String> getMoonpaySignature(String query) async { Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
final response = await post( final response = await post(uri,
uri, headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
headers: { body: json.encode({'query': query}));
'Content-Type': 'application/json',
'x-api-key': _exchangeHelperApiKey,
},
body: json.encode({'query': query}),
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String; return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider {
} }
} }
Future<Uri> requestSellMoonPayUrl({ Future<Map<String, dynamic>> fetchFiatCredentials(
required CryptoCurrency currency, String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
required String refundWalletAddress, final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
required SettingsStore settingsStore,
}) async { if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
print('MoonPay does not support fiat: $fiatCurrency');
return {};
}
} catch (e) {
print('MoonPay Error fetching fiat currencies: $e');
return {};
}
}
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final List<PaymentMethod> paymentMethods = [];
if (isBuyAction) {
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
if (fiatBuyCredentials.isNotEmpty) {
final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
paymentMethods.add(PaymentMethod.fromMoonPayJson(
fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
return paymentMethods;
}
}
return paymentMethods;
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
} else {
paymentMethod = 'credit_debit_card';
}
final action = isBuyAction ? 'buy' : 'sell';
final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
final baseCurrencyCode =
isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
final params = { final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme), 'baseCurrencyCode': baseCurrencyCode,
'language': settingsStore.languageCode, 'baseCurrencyAmount': amount.toString(),
'colorCode': settingsStore.currentTheme.type == ThemeType.dark 'amount': amount.toString(),
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' 'paymentMethod': paymentMethod,
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', 'areFeesIncluded': 'false',
'defaultCurrencyCode': _normalizeCurrency(currency), 'apiKey': _apiKey
'refundWalletAddress': refundWalletAddress,
}; };
if (_apiKey.isNotEmpty) { log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
params['apiKey'] = _apiKey;
final quotePath = isBuyAction ? _buyQuote : _sellQuote;
final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
// Check if the response is for the correct fiat currency
if (isBuyAction) {
final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
return null;
} else {
final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
return null;
} }
final originalUri = Uri.https( final paymentMethods = data['paymentMethod'] as String?;
baseSellUrl, final quote =
'', Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
params,
);
if (isTestEnvironment) { quote.setFiatCurrency = fiatCurrency;
return originalUri; quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
print('Moon Pay: Error fetching buy quote: ');
return null;
}
} catch (e) {
print('Moon Pay: Error fetching buy quote: $e');
return null;
}
} }
final signature = await getMoonpaySignature('?${originalUri.query}'); @override
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final query = Map<String, dynamic>.from(originalUri.queryParameters); final Map<String, String> params = {
query['signature'] = signature; 'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
final signedUri = originalUri.replace(queryParameters: query); 'language': _settingsStore.languageCode,
return signedUri; 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
}
// BUY:
static const _currenciesSuffix = '/v3/currencies';
static const _quoteSuffix = '/buy_quote';
static const _transactionsSuffix = '/v1/transactions';
static const _ipAddressSuffix = '/v4/ip_address';
Future<Uri> 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.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'baseCurrencyCode': settingsStore.fiatCurrency.title, 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
'baseCurrencyAmount': amount ?? '0', 'baseCurrencyAmount': amount.toString(),
'currencyCode': _normalizeCurrency(currency), 'walletAddress': cryptoCurrencyAddress,
'walletAddress': walletAddress,
'lockAmount': 'false', 'lockAmount': 'false',
'showAllCurrencies': 'false', 'showAllCurrencies': 'false',
'showWalletAddressForm': 'false', 'showWalletAddressForm': 'false',
'enabledPaymentMethods': if (isBuyAction)
'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
}; };
if (_apiKey.isNotEmpty) { if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
params['apiKey'] = _apiKey; if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
}
final originalUri = Uri.https( try {
baseBuyUrl, {
'', final uri = await requestMoonPayUrl(
params, walletAddress: cryptoCurrencyAddress,
settingsStore: _settingsStore,
isBuyAction: isBuyAction,
amount: amount.toString(),
params: params);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
}
} catch (e) {
if (context.mounted) {
await showDialog<void>(
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(),
);
},
); );
if (isTestEnvironment) {
return originalUri;
} }
}
}
Future<Uri> requestMoonPayUrl({
required String walletAddress,
required SettingsStore settingsStore,
required bool isBuyAction,
required Map<String, String> params,
String? amount,
}) async {
if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
final originalUri = Uri.https(baseUrl, '', params);
if (isTestEnvironment) return originalUri;
final signature = await getMoonpaySignature('?${originalUri.query}'); final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters); final query = Map<String, dynamic>.from(originalUri.queryParameters);
@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider {
return signedUri; return signedUri;
} }
Future<BuyAmount> 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<String, dynamic>;
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<Order> findOrderById(String id) async { Future<Order> findOrderById(String id) async {
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
final uri = Uri.parse(url); final uri = Uri.parse(url);
@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider {
walletId: wallet.id); walletId: wallet.id);
} }
static Future<bool> 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<String, dynamic>;
isBuyEnable = responseJSON['isBuyAllowed'] as bool;
} catch (e) {
isBuyEnable = false;
print(e.toString());
}
return isBuyEnable;
}
@override
Future<void> 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) {
if (context.mounted) {
await showDialog<void>(
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) { String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) { if (currency.tag == 'POLY') {
return "POL_POLYGON"; return '${currency.title.toLowerCase()}_polygon';
} else if (currency == CryptoCurrency.matic) { }
return "POL";
if (currency.tag == 'TRX') {
return '${currency.title.toLowerCase()}_trx';
} }
return currency.toString().toLowerCase(); return currency.toString().toLowerCase();
} }
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.creditCard:
return 'credit_debit_card';
case PaymentType.debitCard:
return 'credit_debit_card';
case PaymentType.ach:
return 'ach_bank_transfer';
case PaymentType.applePay:
return 'apple_pay';
case PaymentType.googlePay:
return 'google_pay';
case PaymentType.sepa:
return 'sepa_bank_transfer';
case PaymentType.paypal:
return 'paypal';
case PaymentType.sepaOpenBankingPayment:
return 'sepa_open_banking_payment';
case PaymentType.gbpOpenBankingPayment:
return 'gbp_open_banking_payment';
case PaymentType.lowCostAch:
return 'low_cost_ach';
case PaymentType.mobileWallet:
return 'mobile_wallet';
case PaymentType.pixInstantPayment:
return 'pix_instant_payment';
case PaymentType.yellowCardBankTransfer:
return 'yellow_card_bank_transfer';
case PaymentType.fiatBalance:
return 'fiat_balance';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'ach_bank_transfer':
return PaymentType.ach;
case 'apple_pay':
return PaymentType.applePay;
case 'credit_debit_card':
return PaymentType.creditCard;
case 'fiat_balance':
return PaymentType.fiatBalance;
case 'gbp_open_banking_payment':
return PaymentType.gbpOpenBankingPayment;
case 'google_pay':
return PaymentType.googlePay;
case 'low_cost_ach':
return PaymentType.lowCostAch;
case 'mobile_wallet':
return PaymentType.mobileWallet;
case 'paypal':
return PaymentType.paypal;
case 'pix_instant_payment':
return PaymentType.pixInstantPayment;
case 'sepa_bank_transfer':
return PaymentType.sepa;
case 'sepa_open_banking_payment':
return PaymentType.sepaOpenBankingPayment;
case 'yellow_card_bank_transfer':
return PaymentType.yellowCardBankTransfer;
default:
return PaymentType.all;
}
}
} }

View file

@ -1,13 +1,19 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.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/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.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/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class OnRamperBuyProvider extends BuyProvider { class OnRamperBuyProvider extends BuyProvider {
@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider {
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
static const _baseUrl = 'buy.onramper.com'; static const _baseUrl = 'buy.onramper.com';
static const _baseApiUrl = 'api.onramper.com';
static const quotes = '/quotes';
static const paymentTypes = '/payment-types';
static const supported = '/supported';
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
String get _apiKey => secrets.onramperApiKey;
@override @override
String get title => 'Onramper'; String get title => 'Onramper';
@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider {
@override @override
String get darkIcon => 'assets/images/onramper_dark.png'; String get darkIcon => 'assets/images/onramper_dark.png';
String get _apiKey => secrets.onramperApiKey; @override
bool get isAggregator => true;
String get _normalizeCryptoCurrency { Future<List<PaymentMethod>> getAvailablePaymentTypes(
switch (wallet.currency) { String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
case CryptoCurrency.ltc: final params = {
return "LTC_LITECOIN"; 'fiatCurrency': fiatCurrency,
case CryptoCurrency.xmr: 'type': isBuyAction ? 'buy' : 'sell',
return "XMR_MONERO"; 'isRecurringPayment': 'false'
case CryptoCurrency.bch: };
return "BCH_BITCOINCASH";
case CryptoCurrency.nano: final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
return "XNO_NANO";
default: try {
return wallet.currency.title; final response =
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> message = data['message'] as List<dynamic>;
return message
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
.toList();
} else {
print('Failed to fetch available payment types');
return [];
}
} catch (e) {
print('Failed to fetch available payment types: $e');
return [];
} }
} }
String getColorStr(Color color) { Future<Map<String, dynamic>> getOnrampMetadata() async {
return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
try {
final response =
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> onramps = data['message'] as List<dynamic>;
final Map<String, dynamic> result = {
for (var onramp in onramps)
(onramp['id'] as String): {
'displayName': onramp['displayName'] as String,
'svg': onramp['icons']['svg'] as String
}
};
return result;
} else {
print('Failed to fetch onramp metadata');
return {};
}
} catch (e) {
print('Error occurred: $e');
return {};
}
} }
Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) { @override
String primaryColor, Future<List<Quote>?> fetchQuote(
secondaryColor, {required CryptoCurrency cryptoCurrency,
primaryTextColor, required FiatCurrency fiatCurrency,
secondaryTextColor, required double amount,
containerColor, required bool isBuyAction,
cardColor; required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
primaryColor = getColorStr(Theme.of(context).primaryColor); if (paymentType != null && paymentType != PaymentType.all) {
secondaryColor = getColorStr(Theme.of(context).colorScheme.background); paymentMethod = normalizePaymentMethod(paymentType);
primaryTextColor = if (paymentMethod == null) paymentMethod = paymentType.name;
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor); }
secondaryTextColor = getColorStr(
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor); final actionType = isBuyAction ? 'buy' : 'sell';
containerColor = getColorStr(Theme.of(context).colorScheme.background);
cardColor = getColorStr(Theme.of(context).cardColor); final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency);
final params = {
'amount': amount.toString(),
if (paymentMethod != null) 'paymentMethod': paymentMethod,
'clientName': 'CakeWallet',
'type': actionType,
'walletAddress': walletAddress,
'isRecurringPayment': 'false',
'input': 'source',
};
log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
try {
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
if (data.isEmpty) return null;
List<Quote> validQuotes = [];
final onrampMetadata = await getOnrampMetadata();
for (var item in data) {
if (item['errors'] != null) continue;
final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
final rampId = item['ramp'] as String?;
final rampMetaData = onrampMetadata[rampId] as Map<String, dynamic>?;
if (rampMetaData == null) continue;
final quote = Quote.fromOnramperJson(
item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod));
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
validQuotes.add(quote);
}
if (validQuotes.isEmpty) return null;
return validQuotes;
} else {
print('Onramper: Failed to fetch rate');
return null;
}
} catch (e) {
print('Onramper: Failed to fetch rate $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final actionType = isBuyAction ? 'buy' : 'sell';
final prefix = actionType == 'sell' ? actionType + '_' : '';
final primaryColor = getColorStr(Theme.of(context).primaryColor);
final secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
final primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
final secondaryTextColor =
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
final containerColor = getColorStr(Theme.of(context).colorScheme.background);
var cardColor = getColorStr(Theme.of(context).cardColor);
if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) {
cardColor = getColorStr(Colors.white); cardColor = getColorStr(Colors.white);
} }
final networkName = final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency);
wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
return Uri.https(_baseUrl, '', <String, dynamic>{ final paymentMethod = normalizePaymentMethod(quote.paymentType);
final uri = Uri.https(_baseUrl, '', {
'apiKey': _apiKey, 'apiKey': _apiKey,
'defaultCrypto': _normalizeCryptoCurrency, 'mode': actionType,
'sell_defaultCrypto': _normalizeCryptoCurrency, '${prefix}defaultFiat': quote.fiatCurrency.name,
'networkWallets': '${networkName}:${wallet.walletAddresses.address}', '${prefix}defaultCrypto': defaultCrypto,
'${prefix}defaultAmount': amount.toString(),
if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod,
'onlyOnramps': quote.rampId,
'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress',
'walletAddress': cryptoCurrencyAddress,
'supportSwap': "false", 'supportSwap': "false",
'primaryColor': primaryColor, 'primaryColor': primaryColor,
'secondaryColor': secondaryColor, 'secondaryColor': secondaryColor,
'containerColor': containerColor,
'primaryTextColor': primaryTextColor, 'primaryTextColor': primaryTextColor,
'secondaryTextColor': secondaryTextColor, 'secondaryTextColor': secondaryTextColor,
'containerColor': containerColor,
'cardColor': cardColor, 'cardColor': cardColor,
'mode': isBuyAction == true ? 'buy' : 'sell',
}); });
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
} }
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async { List<CryptoCurrency> mainCurrency = [
final uri = requestOnramperUrl(context, isBuyAction); CryptoCurrency.btc,
if (DeviceInfo.instance.isMobile) { CryptoCurrency.eth,
Navigator.of(context) CryptoCurrency.sol,
.pushNamed(Routes.webViewPage, arguments: [title, uri]); ];
} else {
await launchUrl(uri); String _tagToNetwork(String tag) {
switch (tag) {
case 'OMNI':
return tag;
case 'POL':
return 'POLYGON';
default:
return CryptoCurrency.fromString(tag).fullName ?? tag;
} }
} }
String _getNormalizeCryptoCurrency(Currency currency) {
if (currency is CryptoCurrency) {
if (!mainCurrency.contains(currency)) {
final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!);
return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase();
}
return currency.title.toUpperCase();
}
return currency.name.toUpperCase();
}
String? normalizePaymentMethod(PaymentType paymentType) {
switch (paymentType) {
case PaymentType.bankTransfer:
return 'banktransfer';
case PaymentType.creditCard:
return 'creditcard';
case PaymentType.debitCard:
return 'debitcard';
case PaymentType.applePay:
return 'applepay';
case PaymentType.googlePay:
return 'googlepay';
case PaymentType.revolutPay:
return 'revolutpay';
case PaymentType.neteller:
return 'neteller';
case PaymentType.skrill:
return 'skrill';
case PaymentType.sepa:
return 'sepabanktransfer';
case PaymentType.sepaInstant:
return 'sepainstant';
case PaymentType.ach:
return 'ach';
case PaymentType.achInstant:
return 'iach';
case PaymentType.Khipu:
return 'khipu';
case PaymentType.palomaBanktTansfer:
return 'palomabanktransfer';
case PaymentType.ovo:
return 'ovo';
case PaymentType.zaloPay:
return 'zalopay';
case PaymentType.zaloBankTransfer:
return 'zalobanktransfer';
case PaymentType.gcash:
return 'gcash';
case PaymentType.imps:
return 'imps';
case PaymentType.dana:
return 'dana';
case PaymentType.ideal:
return 'ideal';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String paymentMethod) {
switch (paymentMethod.toLowerCase()) {
case 'banktransfer':
return PaymentType.bankTransfer;
case 'creditcard':
return PaymentType.creditCard;
case 'debitcard':
return PaymentType.debitCard;
case 'applepay':
return PaymentType.applePay;
case 'googlepay':
return PaymentType.googlePay;
case 'revolutpay':
return PaymentType.revolutPay;
case 'neteller':
return PaymentType.neteller;
case 'skrill':
return PaymentType.skrill;
case 'sepabanktransfer':
return PaymentType.sepa;
case 'sepainstant':
return PaymentType.sepaInstant;
case 'ach':
return PaymentType.ach;
case 'iach':
return PaymentType.achInstant;
case 'khipu':
return PaymentType.Khipu;
case 'palomabanktransfer':
return PaymentType.palomaBanktTansfer;
case 'ovo':
return PaymentType.ovo;
case 'zalopay':
return PaymentType.zaloPay;
case 'zalobanktransfer':
return PaymentType.zaloBankTransfer;
case 'gcash':
return PaymentType.gcash;
case 'imps':
return PaymentType.imps;
case 'dana':
return PaymentType.dana;
case 'ideal':
return PaymentType.ideal;
default:
return PaymentType.all;
}
}
String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
} }

287
lib/buy/payment_method.dart Normal file
View file

@ -0,0 +1,287 @@
import 'dart:ui';
import 'package:cake_wallet/core/selectable_option.dart';
enum PaymentType {
all,
bankTransfer,
creditCard,
debitCard,
applePay,
googlePay,
revolutPay,
neteller,
skrill,
sepa,
sepaInstant,
ach,
achInstant,
Khipu,
palomaBanktTansfer,
ovo,
zaloPay,
zaloBankTransfer,
gcash,
imps,
dana,
ideal,
paypal,
sepaOpenBankingPayment,
gbpOpenBankingPayment,
lowCostAch,
mobileWallet,
pixInstantPayment,
yellowCardBankTransfer,
fiatBalance,
bancontact,
}
extension PaymentTypeTitle on PaymentType {
String? get title {
switch (this) {
case PaymentType.all:
return 'All Payment Methods';
case PaymentType.bankTransfer:
return 'Bank Transfer';
case PaymentType.creditCard:
return 'Credit Card';
case PaymentType.debitCard:
return 'Debit Card';
case PaymentType.applePay:
return 'Apple Pay';
case PaymentType.googlePay:
return 'Google Pay';
case PaymentType.revolutPay:
return 'Revolut Pay';
case PaymentType.neteller:
return 'Neteller';
case PaymentType.skrill:
return 'Skrill';
case PaymentType.sepa:
return 'SEPA';
case PaymentType.sepaInstant:
return 'SEPA Instant';
case PaymentType.ach:
return 'ACH';
case PaymentType.achInstant:
return 'ACH Instant';
case PaymentType.Khipu:
return 'Khipu';
case PaymentType.palomaBanktTansfer:
return 'Paloma Bank Transfer';
case PaymentType.ovo:
return 'OVO';
case PaymentType.zaloPay:
return 'Zalo Pay';
case PaymentType.zaloBankTransfer:
return 'Zalo Bank Transfer';
case PaymentType.gcash:
return 'GCash';
case PaymentType.imps:
return 'IMPS';
case PaymentType.dana:
return 'DANA';
case PaymentType.ideal:
return 'iDEAL';
case PaymentType.paypal:
return 'PayPal';
case PaymentType.sepaOpenBankingPayment:
return 'SEPA Open Banking Payment';
case PaymentType.gbpOpenBankingPayment:
return 'GBP Open Banking Payment';
case PaymentType.lowCostAch:
return 'Low Cost ACH';
case PaymentType.mobileWallet:
return 'Mobile Wallet';
case PaymentType.pixInstantPayment:
return 'PIX Instant Payment';
case PaymentType.yellowCardBankTransfer:
return 'Yellow Card Bank Transfer';
case PaymentType.fiatBalance:
return 'Fiat Balance';
case PaymentType.bancontact:
return 'Bancontact';
default:
return null;
}
}
String? get lightIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_light.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_light.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_light.svg';
default:
return null;
}
}
String? get darkIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_dark.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card_dark.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_dark.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_dark.svg';
default:
return null;
}
}
String? get description {
switch (this) {
default:
return null;
}
}
}
class PaymentMethod extends SelectableOption {
PaymentMethod({
required this.paymentMethodType,
required this.customTitle,
required this.customIconPath,
this.customDescription,
}) : super(title: paymentMethodType.title ?? customTitle);
final PaymentType paymentMethodType;
final String customTitle;
final String customIconPath;
final String? customDescription;
bool isSelected = false;
@override
String? get description => paymentMethodType.description ?? customDescription;
@override
String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath;
@override
String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath;
@override
bool get isOptionSelected => isSelected;
factory PaymentMethod.all() {
return PaymentMethod(
paymentMethodType: PaymentType.all,
customTitle: 'All Payment Methods',
customIconPath: 'assets/images/dollar_coin.svg');
}
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
return PaymentMethod(
paymentMethodType: type,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?);
}
factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: paymentMethod,
customIconPath: 'assets/images/card.png');
}
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: json['paymentMethod'] as String,
customIconPath: 'assets/images/card.png');
}
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
final logos = json['logos'] as Map<String, dynamic>;
return PaymentMethod(
paymentMethodType: type,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?);
}
static PaymentType getPaymentTypeId(String? type) {
switch (type?.toLowerCase()) {
case 'banktransfer':
case 'bank':
case 'yellow_card_bank_transfer':
return PaymentType.bankTransfer;
case 'creditcard':
case 'card':
case 'credit_debit_card':
return PaymentType.creditCard;
case 'debitcard':
return PaymentType.debitCard;
case 'applepay':
case 'apple_pay':
return PaymentType.applePay;
case 'googlepay':
case 'google_pay':
return PaymentType.googlePay;
case 'revolutpay':
return PaymentType.revolutPay;
case 'neteller':
return PaymentType.neteller;
case 'skrill':
return PaymentType.skrill;
case 'sepabanktransfer':
case 'sepa':
case 'sepa_bank_transfer':
return PaymentType.sepa;
case 'sepainstant':
case 'sepa_instant':
return PaymentType.sepaInstant;
case 'ach':
case 'ach_bank_transfer':
return PaymentType.ach;
case 'iach':
case 'instant_ach':
return PaymentType.achInstant;
case 'khipu':
return PaymentType.Khipu;
case 'palomabanktransfer':
return PaymentType.palomaBanktTansfer;
case 'ovo':
return PaymentType.ovo;
case 'zalopay':
return PaymentType.zaloPay;
case 'zalobanktransfer':
case 'za_bank_transfer':
return PaymentType.zaloBankTransfer;
case 'gcash':
return PaymentType.gcash;
case 'imps':
return PaymentType.imps;
case 'dana':
return PaymentType.dana;
case 'ideal':
return PaymentType.ideal;
case 'paypal':
return PaymentType.paypal;
case 'sepa_open_banking_payment':
return PaymentType.sepaOpenBankingPayment;
case 'bancontact':
return PaymentType.bancontact;
default:
return PaymentType.all;
}
}
}

View file

@ -1,13 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,7 +20,8 @@ import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class RobinhoodBuyProvider extends BuyProvider { class RobinhoodBuyProvider extends BuyProvider {
RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) RobinhoodBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
static const _baseUrl = 'applink.robinhood.com'; static const _baseUrl = 'applink.robinhood.com';
@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider {
@override @override
String get darkIcon => 'assets/images/robinhood_dark.png'; String get darkIcon => 'assets/images/robinhood_dark.png';
@override
bool get isAggregator => false;
String get _applicationId => secrets.robinhoodApplicationId; String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.exchangeHelperApiKey; String get _apiSecret => secrets.exchangeHelperApiKey;
@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider {
}); });
} }
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async { Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) { if (wallet.isHardwareWallet) {
if (!ledgerVM!.isConnected) { if (!ledgerVM!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices, await Navigator.of(context).pushNamed(Routes.connectDevices,
@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider {
}); });
} }
} }
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
final action = isBuyAction ? 'buy' : 'sell';
log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod');
final queryParams = {
'applicationId': _applicationId,
'fiatCode': fiatCurrency.name,
'assetCode': cryptoCurrency.title,
'fiatAmount': amount.toString(),
if (paymentMethod != null) 'paymentMethod': paymentMethod,
};
final uri =
Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams);
try {
final response = await http.get(uri, headers: {'accept': 'application/json'});
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode == 200) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
if (responseData.containsKey('message')) {
log('Robinhood Error: ${responseData['message']}');
} else {
print('Robinhood Failed to fetch $action quote: ${response.statusCode}');
}
return null;
}
} catch (e) {
log('Robinhood: Failed to fetch $action quote: $e');
return null;
}
// buying_power
// crypto_balance
// debit_card
// bank_transfer
}
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.creditCard:
return 'debit_card';
case PaymentType.debitCard:
return 'debit_card';
case PaymentType.bankTransfer:
return 'bank_transfer';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'debit_card':
return PaymentType.debitCard;
case 'bank_transfer':
return PaymentType.bankTransfer;
default:
return PaymentType.all;
}
}
} }

View file

@ -0,0 +1,20 @@
abstract class PaymentMethodLoadingState {}
class InitialPaymentMethod extends PaymentMethodLoadingState {}
class PaymentMethodLoading extends PaymentMethodLoadingState {}
class PaymentMethodLoaded extends PaymentMethodLoadingState {}
class PaymentMethodFailed extends PaymentMethodLoadingState {}
abstract class BuySellQuotLoadingState {}
class InitialBuySellQuotState extends BuySellQuotLoadingState {}
class BuySellQuotLoading extends BuySellQuotLoadingState {}
class BuySellQuotLoaded extends BuySellQuotLoadingState {}
class BuySellQuotFailed extends BuySellQuotLoadingState {}

View file

@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider {
@override @override
String get darkIcon => 'assets/images/robinhood_dark.png'; String get darkIcon => 'assets/images/robinhood_dark.png';
@override
bool get isAggregator => false;
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl; String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
String baseApiUrl; String baseApiUrl;
@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider {
receiveAddress: wallet.walletAddresses.address, receiveAddress: wallet.walletAddresses.address,
walletId: wallet.id); walletId: wallet.id);
} }
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
// TODO: implement launchProvider
throw UnimplementedError();
}
} }

View file

@ -1,4 +1,3 @@
class CakePayOrder { class CakePayOrder {
final String orderId; final String orderId;
final List<OrderCard> cards; final List<OrderCard> cards;

View file

@ -82,11 +82,13 @@ class CakePayService {
} }
/// Logout /// Logout
Future<void> logout(String email) async { Future<void> logout([String? email]) async {
await secureStorage.delete(key: cakePayUsernameStorageKey); await secureStorage.delete(key: cakePayUsernameStorageKey);
await secureStorage.delete(key: cakePayUserTokenKey); await secureStorage.delete(key: cakePayUserTokenKey);
if (email != null) {
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey); await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
} }
}
/// Purchase Gift Card /// Purchase Gift Card
Future<CakePayOrder> createOrder( Future<CakePayOrder> createOrder(

View file

@ -268,9 +268,7 @@ class BackupService {
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?; final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?; final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
final disableSell = data[PreferencesKey.disableSellKey] as bool?;
final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?;
final currentTransactionPriorityKeyLegacy = final currentTransactionPriorityKeyLegacy =
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
final currentBitcoinElectrumSererId = final currentBitcoinElectrumSererId =
@ -323,14 +321,8 @@ class BackupService {
if (isAppSecure != null) if (isAppSecure != null)
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
if (disableBuy != null) if (disableTradeOption != null)
await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
if (disableSell != null)
await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
if (defaultBuyProvider != null)
await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider);
if (currentTransactionPriorityKeyLegacy != null) if (currentTransactionPriorityKeyLegacy != null)
await _sharedPreferences.setInt( await _sharedPreferences.setInt(
@ -516,10 +508,7 @@ class BackupService {
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
PreferencesKey.shouldSaveRecipientAddressKey: PreferencesKey.shouldSaveRecipientAddressKey:
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
PreferencesKey.defaultBuyProvider:
_sharedPreferences.getInt(PreferencesKey.defaultBuyProvider),
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
PreferencesKey.currentTransactionPriorityKeyLegacy: PreferencesKey.currentTransactionPriorityKeyLegacy:
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),

View file

@ -0,0 +1,47 @@
abstract class SelectableItem {
SelectableItem({required this.title});
final String title;
}
class OptionTitle extends SelectableItem {
OptionTitle({required String title}) : super(title: title);
}
abstract class SelectableOption extends SelectableItem {
SelectableOption({required String title}) : super(title: title);
String get lightIconPath;
String get darkIconPath;
String? get description => null;
String? get topLeftSubTitle => null;
String? get topLeftSubTitleIconPath => null;
String? get topRightSubTitle => null;
String? get topRightSubTitleLightIconPath => null;
String? get topRightSubTitleDarkIconPath => null;
String? get bottomLeftSubTitle => null;
String? get bottomLeftSubTitleIconPath => null;
String? get bottomRightSubTitle => null;
String? get bottomRightSubTitleLightIconPath => null;
String? get bottomRightSubTitleDarkIconPath => null;
List<String> get badges => [];
bool get isOptionSelected => false;
set isOptionSelected(bool isSelected) => false;
}

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.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/core/wallet_connect/wc_bottom_sheet_service.dart';
@ -34,6 +35,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
@ -61,7 +64,6 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
@ -125,6 +127,7 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d
import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_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/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
@ -134,6 +137,8 @@ import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.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/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
@ -247,6 +252,8 @@ import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'cake_pay/cake_pay_payment_credantials.dart'; import 'cake_pay/cake_pay_payment_credantials.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
@ -905,6 +912,11 @@ Future<void> setup({
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>())); getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>( getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
@ -998,6 +1010,10 @@ Future<void> setup({
wallet: getIt.get<AppStore>().wallet!, wallet: getIt.get<AppStore>().wallet!,
)); ));
getIt.registerFactory<MeldBuyProvider>(() => MeldBuyProvider(
wallet: getIt.get<AppStore>().wallet!,
));
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri)); getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider( getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
@ -1189,8 +1205,25 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel()); getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactoryParam<BuySellOptionsPage, bool, void>( getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
(isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
final confirmOption = args[2] as void Function(BuildContext contex)?;
return BuyOptionsPage(
items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
});
getIt.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableOption>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
return PaymentMethodOptionsPage(
items: items, pickAnOption: pickAnOption);
});
getIt.registerFactory(() { getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet; final wallet = getIt.get<AppStore>().wallet;

View file

@ -261,9 +261,13 @@ Future<void> defaultSettingsMigration(
updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences);
break; break;
case 43: case 43:
_updateCakeXmrNode(nodes);
break;
case 44:
await addZanoNodeList(nodes: nodes); await addZanoNodeList(nodes: nodes);
await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
break; break;
default: default:
break; break;
} }
@ -278,6 +282,15 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
} }
void _updateCakeXmrNode(Box<Node> nodes) {
final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri);
if (node != null && !node.trusted) {
node.trusted = true;
node.save();
}
}
void updateBtcElectrumNodeToUseSSL(Box<Node> nodes, SharedPreferences sharedPreferences) { void updateBtcElectrumNodeToUseSSL(Box<Node> nodes, SharedPreferences sharedPreferences) {
final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri);
@ -854,7 +867,7 @@ Future<void> changeDefaultMoneroNode(
} }
}); });
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true);
await nodeSource.add(newCakeWalletNode); await nodeSource.add(newCakeWalletNode);

View file

@ -23,31 +23,18 @@ class MainActions {
}); });
static List<MainActions> all = [ static List<MainActions> all = [
buyAction, showWalletsAction,
receiveAction, receiveAction,
exchangeAction, exchangeAction,
sendAction, sendAction,
sellAction, tradeAction,
]; ];
static MainActions buyAction = MainActions._( static MainActions showWalletsAction = MainActions._(
name: (context) => S.of(context).buy, name: (context) => S.of(context).wallets,
image: 'assets/images/buy.png', image: 'assets/images/wallet_new.png',
isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
canShow: (viewModel) => viewModel.hasBuyAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async { onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledBuyAction) { Navigator.pushNamed(context, Routes.walletList);
return;
}
final defaultBuyProvider = viewModel.defaultBuyProvider;
try {
defaultBuyProvider != null
? await defaultBuyProvider.launchProvider(context, true)
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true);
} catch (e) {
await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString());
}
}, },
); );
@ -79,39 +66,15 @@ class MainActions {
}, },
); );
static MainActions sellAction = MainActions._(
name: (context) => S.of(context).sell, static MainActions tradeAction = MainActions._(
image: 'assets/images/sell.png', name: (context) => '${S.of(context).buy} / ${S.of(context).sell}',
isEnabled: (viewModel) => viewModel.isEnabledSellAction, image: 'assets/images/buy_sell.png',
canShow: (viewModel) => viewModel.hasSellAction, isEnabled: (viewModel) => viewModel.isEnabledTradeAction,
canShow: (viewModel) => viewModel.hasTradeAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async { onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledSellAction) { if (!viewModel.isEnabledTradeAction) return;
return; await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
}
final defaultSellProvider = viewModel.defaultSellProvider;
try {
defaultSellProvider != null
? await defaultSellProvider.launchProvider(context, false)
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
} catch (e) {
await _showErrorDialog(context, defaultSellProvider.toString(), e.toString());
}
},
);
static Future<void> _showErrorDialog(
BuildContext context, String title, String errorMessage) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: title,
alertContent: errorMessage,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
}, },
); );
} }
}

View file

@ -22,10 +22,8 @@ class PreferencesKey {
static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const shouldSaveRecipientAddressKey = 'save_recipient_address';
static const isAppSecureKey = 'is_app_secure'; static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy'; static const disableTradeOption = 'disable_buy';
static const disableSellKey = 'disable_sell';
static const disableBulletinKey = 'disable_bulletin'; static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order'; static const walletListOrder = 'wallet_list_order';
static const contactListOrder = 'contact_list_order'; static const contactListOrder = 'contact_list_order';
static const walletListAscending = 'wallet_list_ascending'; static const walletListAscending = 'wallet_list_ascending';

View file

@ -1,24 +1,18 @@
import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
import 'package:cake_wallet/buy/meld/meld_buy_provider.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart';
enum ProviderType { enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
askEachTime,
robinhood,
dfx,
onramper,
moonpay,
}
extension ProviderTypeName on ProviderType { extension ProviderTypeName on ProviderType {
String get title { String get title {
switch (this) { switch (this) {
case ProviderType.askEachTime:
return 'Ask each time';
case ProviderType.robinhood: case ProviderType.robinhood:
return 'Robinhood Connect'; return 'Robinhood Connect';
case ProviderType.dfx: case ProviderType.dfx:
@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType {
return 'Onramper'; return 'Onramper';
case ProviderType.moonpay: case ProviderType.moonpay:
return 'MoonPay'; return 'MoonPay';
case ProviderType.meld:
return 'Meld';
} }
} }
String get id { String get id {
switch (this) { switch (this) {
case ProviderType.askEachTime:
return 'ask_each_time_provider';
case ProviderType.robinhood: case ProviderType.robinhood:
return 'robinhood_connect_provider'; return 'robinhood_connect_provider';
case ProviderType.dfx: case ProviderType.dfx:
@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType {
return 'onramper_provider'; return 'onramper_provider';
case ProviderType.moonpay: case ProviderType.moonpay:
return 'moonpay_provider'; return 'moonpay_provider';
case ProviderType.meld:
return 'meld_provider';
} }
} }
} }
@ -52,14 +48,13 @@ class ProvidersHelper {
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
case WalletType.wownero: case WalletType.wownero:
return [ProviderType.askEachTime, ProviderType.onramper]; return [ProviderType.onramper];
case WalletType.monero: case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; return [ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.polygon: case WalletType.polygon:
case WalletType.ethereum: case WalletType.ethereum:
return [ return [
ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.dfx, ProviderType.dfx,
ProviderType.robinhood, ProviderType.robinhood,
@ -68,10 +63,13 @@ class ProvidersHelper {
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
case WalletType.solana: case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; return [
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay
];
case WalletType.tron: case WalletType.tron:
return [ return [
ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.robinhood, ProviderType.robinhood,
ProviderType.moonpay, ProviderType.moonpay,
@ -89,28 +87,24 @@ class ProvidersHelper {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
return [ return [
ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.moonpay, ProviderType.moonpay,
ProviderType.dfx, ProviderType.dfx,
]; ];
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpay]; return [ProviderType.moonpay];
case WalletType.solana: case WalletType.solana:
return [ return [
ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay, ProviderType.moonpay,
]; ];
case WalletType.tron: case WalletType.tron:
return [ return [
ProviderType.askEachTime,
ProviderType.robinhood,
ProviderType.moonpay, ProviderType.moonpay,
]; ];
case WalletType.monero: case WalletType.monero:
return [ProviderType.dfx];
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
case WalletType.none: case WalletType.none:
@ -131,7 +125,9 @@ class ProvidersHelper {
return getIt.get<OnRamperBuyProvider>(); return getIt.get<OnRamperBuyProvider>();
case ProviderType.moonpay: case ProviderType.moonpay:
return getIt.get<MoonPayProvider>(); return getIt.get<MoonPayProvider>();
case ProviderType.askEachTime: case ProviderType.meld:
return getIt.get<MeldBuyProvider>();
default:
return null; return null;
} }
} }

View file

@ -1,15 +1,376 @@
import 'package:barcode_scan2/barcode_scan2.dart'; import 'dart:math';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:fast_scanner/fast_scanner.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
var isQrScannerShown = false; var isQrScannerShown = false;
Future<String> presentQRScanner() async { Future<String> presentQRScanner(BuildContext context) async {
isQrScannerShown = true; isQrScannerShown = true;
try { try {
final result = await BarcodeScanner.scan(); final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder:(context) {
return BarcodeScannerSimple();
},
),
);
isQrScannerShown = false; isQrScannerShown = false;
return result.rawContent.trim(); return result??'';
} catch (e) { } catch (e) {
isQrScannerShown = false; isQrScannerShown = false;
rethrow; rethrow;
} }
} }
// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart
class BarcodeScannerSimple extends StatefulWidget {
const BarcodeScannerSimple({super.key});
@override
State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
}
class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
Barcode? _barcode;
bool popped = false;
List<String> urCodes = [];
late var ur = URQRToURQRData(urCodes);
void _handleBarcode(BarcodeCapture barcodes) {
try {
_handleBarcodeInternal(barcodes);
} catch (e) {
showPopUp<void>(
context: context,
builder: (context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonAction: () {
Navigator.of(context).pop();
},
);
},
);
print(e);
}
}
void _handleBarcodeInternal(BarcodeCapture barcodes) {
for (final barcode in barcodes.barcodes) {
// don't handle unknown QR codes
if (barcode.rawValue?.trim().isEmpty??false == false) continue;
if (barcode.rawValue!.startsWith("ur:")) {
if (urCodes.contains(barcode.rawValue)) continue;
setState(() {
urCodes.add(barcode.rawValue!);
ur = URQRToURQRData(urCodes);
});
if (ur.progress == 1) {
setState(() {
popped = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(ur.inputs.join("\n"));
});
};
}
}
if (urCodes.isNotEmpty) return;
if (mounted) {
setState(() {
_barcode = barcodes.barcodes.firstOrNull;
});
if (_barcode != null && popped != true) {
setState(() {
popped = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(_barcode?.rawValue ?? "");
});
}
}
}
final MobileScannerController ctrl = MobileScannerController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scan'),
actions: [
SwitchCameraButton(controller: ctrl),
ToggleFlashlightButton(controller: ctrl),
],
),
backgroundColor: Colors.black,
body: Stack(
children: [
MobileScanner(
onDetect: _handleBarcode,
controller: ctrl,
),
if (ur.inputs.length != 0)
Center(child:
Text(
"${ur.inputs.length}/${ur.count}",
style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white)
),
),
SizedBox(
child: Center(
child: SizedBox(
width: 250,
height: 250,
child: CustomPaint(
painter: ProgressPainter(
urQrProgress: URQrProgress(
expectedPartCount: ur.count - 1,
processedPartsCount: ur.inputs.length,
receivedPartIndexes: _urParts(),
percentage: ur.progress,
),
),
),
),
),
),
],
),
);
}
List<int> _urParts() {
List<int> l = [];
for (var inp in ur.inputs) {
try {
l.add(int.parse(inp.split("/")[1].split("-")[0]));
} catch (e) {}
}
return l;
}
}
class ToggleFlashlightButton extends StatelessWidget {
const ToggleFlashlightButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
switch (state.torchState) {
case TorchState.auto:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_auto),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.off:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_off),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.on:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_on),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.unavailable:
return const Icon(
Icons.no_flash,
color: Colors.grey,
);
}
},
);
}
}
class SwitchCameraButton extends StatelessWidget {
const SwitchCameraButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
final int? availableCameras = state.availableCameras;
if (availableCameras != null && availableCameras < 2) {
return const SizedBox.shrink();
}
final Widget icon;
switch (state.cameraDirection) {
case CameraFacing.front:
icon = const Icon(Icons.camera_front);
case CameraFacing.back:
icon = const Icon(Icons.camera_rear);
}
return IconButton(
iconSize: 32.0,
icon: icon,
onPressed: () async {
await controller.switchCamera();
},
);
},
);
}
}
class URQRData {
URQRData(
{required this.tag,
required this.str,
required this.progress,
required this.count,
required this.error,
required this.inputs});
final String tag;
final String str;
final double progress;
final int count;
final String error;
final List<String> inputs;
Map<String, dynamic> toJson() {
return {
"tag": tag,
"str": str,
"progress": progress,
"count": count,
"error": error,
"inputs": inputs,
};
}
}
URQRData URQRToURQRData(List<String> urqr_) {
final urqr = urqr_.toSet().toList();
urqr.sort((s1, s2) {
final s1s = s1.split("/");
final s1frameStr = s1s[1].split("-");
final s1curFrame = int.parse(s1frameStr[0]);
final s2s = s2.split("/");
final s2frameStr = s2s[1].split("-");
final s2curFrame = int.parse(s2frameStr[0]);
return s1curFrame - s2curFrame;
});
String tag = '';
int count = 0;
String bw = '';
for (var elm in urqr) {
final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix
final s2 = s.split("/");
tag = s2[0];
final frameStr = s2[1].split("-");
// final curFrame = int.parse(frameStr[0]);
count = int.parse(frameStr[1]);
final byteWords = s2[2];
bw += byteWords;
}
String? error;
return URQRData(
tag: tag,
str: bw,
progress: count == 0 ? 0 : (urqr.length / count),
count: count,
error: error ?? "",
inputs: urqr,
);
}
class ProgressPainter extends CustomPainter {
final URQrProgress urQrProgress;
ProgressPainter({required this.urQrProgress});
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2.0, size.height / 2.0);
final radius = size.width * 0.9;
final rect = Rect.fromCenter(center: c, width: radius, height: radius);
const fullAngle = 360.0;
var startAngle = 0.0;
for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) {
var sweepAngle =
(1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0;
drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect,
startAngle, sweepAngle);
startAngle += sweepAngle;
}
}
void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle,
double sweepAngle) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = isActive ? const Color(0xffff6600) : Colors.white70;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
@override
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
return urQrProgress != oldDelegate.urQrProgress;
}
}
class URQrProgress {
int expectedPartCount;
int processedPartsCount;
List<int> receivedPartIndexes;
double percentage;
URQrProgress({
required this.expectedPartCount,
required this.processedPartsCount,
required this.receivedPartIndexes,
required this.percentage,
});
bool equals(URQrProgress? progress) {
if (progress == null) {
return false;
}
return processedPartsCount == progress.processedPartsCount;
}
}

View file

@ -248,6 +248,7 @@ class CWMonero extends Monero {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
final keys = moneroWallet.keys; final keys = moneroWallet.keys;
return <String, String>{ return <String, String>{
'primaryAddress': keys.primaryAddress,
'privateSpendKey': keys.privateSpendKey, 'privateSpendKey': keys.privateSpendKey,
'privateViewKey': keys.privateViewKey, 'privateViewKey': keys.privateViewKey,
'publicSpendKey': keys.publicSpendKey, 'publicSpendKey': keys.publicSpendKey,
@ -358,8 +359,31 @@ class CWMonero extends Monero {
return monero_wallet_api.getCurrentHeight(); return monero_wallet_api.getCurrentHeight();
} }
@override
bool importKeyImagesUR(Object wallet, String ur) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.importKeyImagesUR(ur);
}
@override
Future<bool> commitTransactionUR(Object wallet, String ur) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.submitTransactionUR(ur);
}
@override
String exportOutputsUR(Object wallet, bool all) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.exportOutputsUR(all);
}
@override @override
void monerocCheck() { void monerocCheck() {
checkIfMoneroCIsFine(); checkIfMoneroCIsFine();
} }
bool isViewOnly() {
return isViewOnlyBySpendKey();
}
} }

View file

@ -17,8 +17,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
@ -96,6 +97,7 @@ import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dar
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
@ -128,7 +130,8 @@ import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'src/screens/dashboard/pages/nft_import_page.dart'; import 'src/screens/dashboard/pages/nft_import_page.dart';
late RouteSettings currentRouteSettings; late RouteSettings currentRouteSettings;
@ -570,7 +573,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.buySellPage: case Routes.buySellPage:
final args = settings.arguments as bool; final args = settings.arguments as bool;
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellOptionsPage>(param1: args)); return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellPage>(param1: args));
case Routes.buyOptionsPage:
final args = settings.arguments as List;
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuyOptionsPage>(param1: args));
case Routes.paymentMethodOptionsPage:
final args = settings.arguments as List;
return MaterialPageRoute<void>(builder: (_) => getIt.get<PaymentMethodOptionsPage>(param1: args));
case Routes.buyWebView: case Routes.buyWebView:
final args = settings.arguments as List; final args = settings.arguments as List;
@ -732,6 +743,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.setup2faInfoPage: case Routes.setup2faInfoPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>()); return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
case Routes.urqrAnimatedPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
case Routes.homeSettings: case Routes.homeSettings:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments), builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),

View file

@ -59,6 +59,8 @@ class Routes {
static const supportOtherLinks = '/support/other'; static const supportOtherLinks = '/support/other';
static const orderDetails = '/order_details'; static const orderDetails = '/order_details';
static const buySellPage = '/buy_sell_page'; static const buySellPage = '/buy_sell_page';
static const buyOptionsPage = '/buy_sell_options';
static const paymentMethodOptionsPage = '/payment_method_options';
static const buyWebView = '/buy_web_view'; static const buyWebView = '/buy_web_view';
static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsList = '/unspent_coins_list';
static const unspentCoinsDetails = '/unspent_coins_details'; static const unspentCoinsDetails = '/unspent_coins_details';
@ -108,6 +110,7 @@ class Routes {
static const signPage = '/sign_page'; static const signPage = '/sign_page';
static const connectDevices = '/device/connect'; static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';
static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page';
static const walletGroupDescription = '/wallet_group_description'; static const walletGroupDescription = '/wallet_group_description';
} }

View file

@ -1,74 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class BuySellOptionsPage extends BasePage {
BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
final DashboardViewModel dashboardViewModel;
final bool isBuyAction;
@override
String get title => isBuyAction ? S.current.buy : S.current.sell;
@override
AppBarStyle get appBarStyle => AppBarStyle.regular;
@override
Widget body(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
final availableProviders = isBuyAction
? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders;
return ScrollableWithBottomSection(
content: Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
],
),
),
),
),
bottomSection: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class BuyOptionsPage extends SelectOptionsPage {
BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
final Function(BuildContext context)? confirmOption;
@override
String get pageTitle => S.current.choose_a_provider;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
@override
double? get imageHeight => 40;
@override
double? get imageWidth => 40;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
}

View file

@ -0,0 +1,469 @@
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class BuySellPage extends BasePage {
BuySellPage(this.buySellViewModel);
final BuySellViewModel buySellViewModel;
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
final _fiatAmountFocus = FocusNode();
final _cryptoAmountFocus = FocusNode();
final _cryptoAddressFocus = FocusNode();
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
);
final arrowBottomCakeGreen = Image.asset(
'assets/images/arrow_bottom_cake_green.png',
color: Colors.white,
height: 8,
);
late final String? depositWalletName;
late final String? receiveWalletName;
@override
String get title => S.current.buy + '/' + S.current.sell;
@override
bool get gradientBackground => true;
@override
bool get gradientAll => true;
@override
bool get resizeToAvoidBottomInset => false;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
@override
Widget trailing(BuildContext context) => TrailButton(
caption: S.of(context).clear,
onPressed: () {
_formKey.currentState?.reset();
buySellViewModel.reset();
});
@override
Widget? leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: titleColor(context),
size: 16,
);
final _closeButton =
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
return MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
),
),
),
),
);
}
@override
Widget body(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
KeyboardActionsItem(
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
]),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Observer(
builder: (_) => Column(children: [
_exchangeCardsSection(context),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 12),
_buildPaymentMethodTile(context),
],
),
),
])),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: [
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.current.choose_a_provider,
onPressed: () async {
if(!_formKey.currentState!.validate()) return;
buySellViewModel.onTapChoseProvider(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: !buySellViewModel.isReadyToTrade)),
]),
)),
));
}
Widget _buildPaymentMethodTile(BuildContext context) {
if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
buySellViewModel.paymentMethodState is InitialPaymentMethod) {
return OptionTilePlaceholder(
withBadge: false,
withSubtitle: false,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
leadingIcon: Icons.arrow_forward_ios,
isDarkTheme: buySellViewModel.isDarkTheme);
}
if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
buySellViewModel.selectedPaymentMethod != null) {
return Observer(builder: (_) {
final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
return ProviderOptionTile(
lightImagePath: selectedPaymentMethod.lightIconPath,
darkImagePath: selectedPaymentMethod.darkIconPath,
title: selectedPaymentMethod.title,
onPressed: () => _pickPaymentMethod(context),
leadingIcon: Icons.arrow_forward_ios,
isLightMode: !buySellViewModel.isDarkTheme,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
titleTextStyle:
textLargeBold(color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
);
});
}
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
void _pickPaymentMethod(BuildContext context) async {
final currentOption = buySellViewModel.selectedPaymentMethod;
await Navigator.of(context).pushNamed(
Routes.paymentMethodOptionsPage,
arguments: [
buySellViewModel.paymentMethods,
buySellViewModel.changeOption,
],
);
buySellViewModel.selectedPaymentMethod;
if (currentOption != null &&
currentOption.paymentMethodType !=
buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
await buySellViewModel.calculateBestRate();
}
}
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
if (_isReactionsSet) {
return;
}
final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
_onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
_onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
reaction(
(_) => buySellViewModel.wallet.name,
(String _) =>
_onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.cryptoCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.fiatCurrency,
(FiatCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
reaction((_) => buySellViewModel.fiatAmount, (String amount) {
if (fiatCurrencyKey.currentState!.amountController.text != amount) {
fiatCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) {
cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => buySellViewModel.cryptoAmount, (String amount) {
if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
cryptoCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
if (cryptoAddressController != address) {
cryptoCurrencyKey.currentState!.addressController.text = address;
}
});
fiatAmountController.addListener(() {
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
});
cryptoAmountController.addListener(() {
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
}
});
cryptoAddressController.addListener(() {
buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
});
_cryptoAddressFocus.addListener(() async {
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
final domain = cryptoAddressController.text;
buySellViewModel.cryptoCurrencyAddress =
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
}
});
reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
cryptoCurrencyKey.currentState!.changeAddress(address: address);
}
});
reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
if (isReady) {
if (cryptoAmountController.text.isNotEmpty &&
cryptoAmountController.text != S.current.fetching) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
} else if (fiatAmountController.text.isNotEmpty &&
fiatAmountController.text != S.current.fetching) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
}
});
_isReactionsSet = true;
}
void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(buySellViewModel.wallet.name);
key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange;
} else if (key.currentState!.addressController.text ==
buySellViewModel.wallet.walletAddresses.addressForExchange) {
key.currentState!.changeWalletName('');
key.currentState!.addressController.text = '';
}
}
void disposeBestRateSync() => {};
Widget _exchangeCardsSection(BuildContext context) {
final fiatExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'fiat_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _fiatAmountFocus,
key: fiatCurrencyKey,
title: 'FIAT ${S.of(context).amount}',
initialCurrency: buySellViewModel.fiatCurrency,
initialWalletName: '',
initialAddress: '',
initialIsAmountEditable: true,
isAmountEstimated: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
showAddressField: false,
showLimitsField: false,
currencies: buySellViewModel.fiatCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeFiatCurrency(currency: currency),
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
final cryptoExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'crypto_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _cryptoAmountFocus,
addressFocusNode: _cryptoAddressFocus,
key: cryptoCurrencyKey,
title: 'Crypto ${S.of(context).amount}',
initialCurrency: buySellViewModel.cryptoCurrency,
initialWalletName: '',
initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
? buySellViewModel.wallet.walletAddresses.addressForExchange
: buySellViewModel.cryptoCurrencyAddress,
initialIsAmountEditable: true,
isAmountEstimated: true,
showLimitsField: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
currencies: buySellViewModel.cryptoCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeCryptoCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return MobileExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return MobileExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
},
);
}
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return DesktopExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
);
} else {
return DesktopExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
);
}
},
);
}
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
final address = await extractAddressFromParsed(context, parsedAddress);
return address;
}
}

View file

@ -0,0 +1,47 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class PaymentMethodOptionsPage extends SelectOptionsPage {
PaymentMethodOptionsPage({required this.items, this.pickAnOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
@override
String get pageTitle => S.current.choose_a_payment_method;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
@override
double? get imageHeight => null;
@override
double? get imageWidth => null;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => null;
}

View file

@ -258,7 +258,11 @@ class CakePayBuyCardDetailPage extends BasePage {
if (!isLogged) { if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage); Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else { } else {
try {
await cakePayPurchaseViewModel.createOrder(); await cakePayPurchaseViewModel.createOrder();
} catch (_) {
await cakePayPurchaseViewModel.cakePayService.logout();
}
} }
} }
@ -343,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage {
rightButtonText: S.of(popupContext).send, rightButtonText: S.of(popupContext).send,
leftButtonText: S.of(popupContext).cancel, leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async { actionRightButton: () async {
Navigator.of(popupContext).pop(); Navigator.of(context).pop();
await cakePayPurchaseViewModel.sendViewModel.commitTransaction(); await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
}, },
actionLeftButton: () => Navigator.of(popupContext).pop())); actionLeftButton: () => Navigator.of(popupContext).pop()));
}, },

View file

@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
return Column( return Column(
children: [ children: [
const SizedBox(height: 16), const SizedBox(height: 16),
DesktopActionButton(
title: MainActions.showWalletsAction.name(context),
image: MainActions.showWalletsAction.image,
canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
onTap: () async =>
await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
),
DesktopActionButton( DesktopActionButton(
title: MainActions.exchangeAction.name(context), title: MainActions.exchangeAction.name(context),
image: MainActions.exchangeAction.image, image: MainActions.exchangeAction.image,
@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: DesktopActionButton( child: DesktopActionButton(
title: MainActions.buyAction.name(context), title: MainActions.tradeAction.name(context),
image: MainActions.buyAction.image, image: MainActions.tradeAction.image,
canShow: MainActions.buyAction.canShow?.call(dashboardViewModel), canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel), isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel), onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
),
),
Expanded(
child: DesktopActionButton(
title: MainActions.sellAction.name(context),
image: MainActions.sellAction.image,
canShow: MainActions.sellAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel),
), ),
), ),
], ],

View file

@ -103,6 +103,9 @@ class MenuWidgetState extends State<MenuWidget> {
if (!widget.dashboardViewModel.hasSilentPayments) { if (!widget.dashboardViewModel.hasSilentPayments) {
items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings);
} }
if (!widget.dashboardViewModel.isMoneroViewOnly) {
items.removeWhere((element) => element.name(context) == S.of(context).export_outputs);
}
if (!widget.dashboardViewModel.hasMweb) { if (!widget.dashboardViewModel.hasMweb) {
items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings);
} }
@ -191,7 +194,6 @@ class MenuWidgetState extends State<MenuWidget> {
index--; index--;
final item = items[index]; final item = items[index];
final isLastTile = index == itemCount - 1; final isLastTile = index == itemCount - 1;
return SettingActionButton( return SettingActionButton(

View file

@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
class ExchangeCard extends StatefulWidget { class ExchangeCard<T extends Currency> extends StatefulWidget {
ExchangeCard({ ExchangeCard({
Key? key, Key? key,
required this.initialCurrency, required this.initialCurrency,
@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
this.borderColor = Colors.transparent, this.borderColor = Colors.transparent,
this.hasAllAmount = false, this.hasAllAmount = false,
this.isAllAmountEnabled = false, this.isAllAmountEnabled = false,
this.showAddressField = true,
this.showLimitsField = true,
this.amountFocusNode, this.amountFocusNode,
this.addressFocusNode, this.addressFocusNode,
this.allAmount, this.allAmount,
this.currencyRowPadding,
this.addressRowPadding,
this.onPushPasteButton, this.onPushPasteButton,
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onDispose, this.onDispose,
required this.cardInstanceName, required this.cardInstanceName,
}) : super(key: key); }) : super(key: key);
final List<CryptoCurrency> currencies; final List<T> currencies;
final Function(CryptoCurrency) onCurrencySelected; final Function(T) onCurrencySelected;
final String title; final String title;
final CryptoCurrency initialCurrency; final T initialCurrency;
final String initialWalletName; final String initialWalletName;
final String initialAddress; final String initialAddress;
final bool initialIsAmountEditable; final bool initialIsAmountEditable;
@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
final FocusNode? amountFocusNode; final FocusNode? amountFocusNode;
final FocusNode? addressFocusNode; final FocusNode? addressFocusNode;
final bool hasAllAmount; final bool hasAllAmount;
final bool showAddressField;
final bool showLimitsField;
final bool isAllAmountEnabled; final bool isAllAmountEnabled;
final VoidCallback? allAmount; final VoidCallback? allAmount;
final EdgeInsets? currencyRowPadding;
final EdgeInsets? addressRowPadding;
final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton; final void Function(BuildContext context)? onPushAddressBookButton;
final Function()? onDispose; final Function()? onDispose;
final String cardInstanceName; final String cardInstanceName;
@override @override
ExchangeCardState createState() => ExchangeCardState(); ExchangeCardState<T> createState() => ExchangeCardState<T>();
} }
class ExchangeCardState extends State<ExchangeCard> { class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
ExchangeCardState() ExchangeCardState()
: _title = '', : _title = '',
_min = '', _min = '',
@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
_isAmountEditable = false, _isAmountEditable = false,
_isAddressEditable = false, _isAddressEditable = false,
_walletName = '', _walletName = '',
_selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false, _isAmountEstimated = false,
_isMoneroWallet = false, _isMoneroWallet = false,
_cardInstanceName = ''; _cardInstanceName = '';
@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
String _title; String _title;
String? _min; String? _min;
String? _max; String? _max;
CryptoCurrency _selectedCurrency; late T _selectedCurrency;
String _walletName; String _walletName;
bool _isAmountEditable; bool _isAmountEditable;
bool _isAddressEditable; bool _isAddressEditable;
@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
_selectedCurrency = widget.initialCurrency; _selectedCurrency = widget.initialCurrency;
_isAmountEstimated = widget.isAmountEstimated; _isAmountEstimated = widget.isAmountEstimated;
_isMoneroWallet = widget.isMoneroWallet; _isMoneroWallet = widget.isMoneroWallet;
addressController.text = widget.initialAddress; addressController.text = _normalizeAddressFormat(widget.initialAddress);
super.initState(); super.initState();
} }
@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
}); });
} }
void changeSelectedCurrency(CryptoCurrency currency) { void changeSelectedCurrency(T currency) {
setState(() => _selectedCurrency = currency); setState(() => _selectedCurrency = currency);
} }
@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
} }
void changeAddress({required String address}) { void changeAddress({required String address}) {
setState(() => addressController.text = address); setState(() => addressController.text = _normalizeAddressFormat(address));
} }
void changeAmount({required String amount}) { void changeAmount({required String amount}) {
@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor), Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding( Padding(
padding: EdgeInsets.only(top: 5), padding: EdgeInsets.only(top: 5),
child: Container( child: widget.showLimitsField ? Container(
height: 15, height: 15,
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null _min != null
@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
), ),
) )
: Offstage(), : Offstage(),
])), ])) : Offstage(),
), ),
!_isAddressEditable && widget.hasRefundAddress !_isAddressEditable && widget.hasRefundAddress
? Padding( ? Padding(
@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
)) ))
: Offstage(), : Offstage(),
_isAddressEditable _isAddressEditable
? widget.showAddressField
? FocusTraversalOrder( ? FocusTraversalOrder(
order: NumericFocusOrder(2), order: NumericFocusOrder(2),
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 20), padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
child: AddressTextField( child: AddressTextField(
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
focusNode: widget.addressFocusNode, focusNode: widget.addressFocusNode,
@ -280,19 +289,21 @@ class ExchangeCardState extends State<ExchangeCard> {
widget.amountFocusNode?.requestFocus(); widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount; amountController.text = paymentRequest.amount;
}, },
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null, placeholder:
widget.hasRefundAddress ? S.of(context).refund_address : null,
options: [ options: [
AddressTextFieldOption.paste, AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode, AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook, AddressTextFieldOption.addressBook,
], ],
isBorderExist: false, isBorderExist: false,
textStyle: textStyle: TextStyle(
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor), color:
Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
buttonColor: widget.addressButtonsColor, buttonColor: widget.addressButtonsColor,
validator: widget.addressTextFieldValidator, validator: widget.addressTextFieldValidator,
onPushPasteButton: widget.onPushPasteButton, onPushPasteButton: widget.onPushPasteButton,
@ -300,6 +311,7 @@ class ExchangeCardState extends State<ExchangeCard> {
selectedCurrency: _selectedCurrency), selectedCurrency: _selectedCurrency),
), ),
) )
: Offstage()
: Padding( : Padding(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Builder( child: Builder(
@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
hintText: S.of(context).search_currency, hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet, isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress, isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency), onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
), ),
); );
} }
@ -424,4 +436,10 @@ class ExchangeCardState extends State<ExchangeCard> {
actionLeftButton: () => Navigator.of(dialogContext).pop()); actionLeftButton: () => Navigator.of(dialogContext).pop());
}); });
} }
String _normalizeAddressFormat(String address) {
if (address.startsWith('bitcoincash:')) address = address.substring(12);
return address;
} }
}

View file

@ -1,20 +1,29 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MobileExchangeCardsSection extends StatelessWidget { class MobileExchangeCardsSection extends StatelessWidget {
final Widget firstExchangeCard; final Widget firstExchangeCard;
final Widget secondExchangeCard; final Widget secondExchangeCard;
final bool isBuySellOption;
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const MobileExchangeCardsSection({ const MobileExchangeCardsSection({
Key? key, Key? key,
required this.firstExchangeCard, required this.firstExchangeCard,
required this.secondExchangeCard, required this.secondExchangeCard,
this.isBuySellOption = false,
this.onBuyTap,
this.onSellTap,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(bottom: 32), padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomLeft: Radius.circular(24),
@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
), ),
padding: EdgeInsets.fromLTRB(24, 100, 24, 32), padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
child: firstExchangeCard, child: Column(
children: [
if (isBuySellOption) Column(
children: [
const SizedBox(height: 16),
BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
],
),
firstExchangeCard,
],
),
), ),
Padding( Padding(
padding: EdgeInsets.only(top: 29, left: 24, right: 24), padding: EdgeInsets.only(top: 29, left: 24, right: 24),
@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget {
); );
} }
} }
class BuySellOptionButtons extends StatefulWidget {
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const BuySellOptionButtons({this.onBuyTap, this.onSellTap});
@override
_BuySellOptionButtonsState createState() => _BuySellOptionButtonsState();
}
class _BuySellOptionButtonsState extends State<BuySellOptionButtons> {
bool isBuySelected = true;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).buy,
isSelected: isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/buy.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = true);
if (widget.onBuyTap != null) widget.onBuyTap!();
},
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).sell,
isSelected: !isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/sell.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: !isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = false);
if (widget.onSellTap != null) widget.onSellTap!();
},
),
),
Expanded(flex: 2, child: SizedBox()),
],
),
);
}
}

View file

@ -277,7 +277,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
actionRightButton: () async { actionRightButton: () async {
Navigator.of(popupContext).pop(); Navigator.of(popupContext).pop();
await widget.exchangeTradeViewModel.sendViewModel await widget.exchangeTradeViewModel.sendViewModel
.commitTransaction(); .commitTransaction(context);
transactionStatePopup(); transactionStatePopup();
}, },
actionLeftButton: () => Navigator.of(popupContext).pop(), actionLeftButton: () => Navigator.of(popupContext).pop(),

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart'; import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
class PreSeedPage extends InfoPage { class PreSeedPage extends InfoPage {

View file

@ -0,0 +1,199 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
abstract class SelectOptionsPage extends BasePage {
SelectOptionsPage();
String get pageTitle;
EdgeInsets? get contentPadding;
EdgeInsets? get tilePadding;
EdgeInsets? get innerPadding;
double? get imageHeight;
double? get imageWidth;
Color? get selectedBackgroundColor;
double? get tileBorderRadius;
String get bottomSectionText;
bool get primaryButtonEnabled => true;
String get primaryButtonText => '';
List<SelectableItem> get items;
void Function(SelectableOption option)? get onOptionTap;
void Function(BuildContext context)? get primaryButtonAction;
@override
String get title => pageTitle;
@override
Widget body(BuildContext context) {
return ScrollableWithBottomSection(
content: BodySelectOptionsPage(
items: items,
onOptionTap: onOptionTap,
tilePadding: tilePadding,
tileBorderRadius: tileBorderRadius,
imageHeight: imageHeight,
imageWidth: imageWidth,
innerPadding: innerPadding),
bottomSection: Padding(
padding: contentPadding ?? EdgeInsets.zero,
child: Column(
children: [
Text(
bottomSectionText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
if (primaryButtonEnabled)
LoadingPrimaryButton(
text: primaryButtonText,
onPressed: () {
primaryButtonAction != null
? primaryButtonAction!(context)
: Navigator.pop(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: false)
],
),
),
);
}
}
class BodySelectOptionsPage extends StatefulWidget {
const BodySelectOptionsPage({
required this.items,
this.onOptionTap,
this.tilePadding,
this.tileBorderRadius,
this.imageHeight,
this.imageWidth,
this.innerPadding,
});
final List<SelectableItem> items;
final void Function(SelectableOption option)? onOptionTap;
final EdgeInsets? tilePadding;
final double? tileBorderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? innerPadding;
@override
_BodySelectOptionsPageState createState() => _BodySelectOptionsPageState();
}
class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
late List<SelectableItem> _items;
@override
void initState() {
super.initState();
_items = widget.items;
}
void _handleOptionTap(SelectableOption option) {
setState(() {
for (var item in _items) {
if (item is SelectableOption) {
item.isOptionSelected = false;
}
}
option.isOptionSelected = true;
});
widget.onOptionTap?.call(option);
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
Color titleColor =
isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 350),
child: Column(
children: _items.map((item) {
if (item is OptionTitle) {
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 8),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: titleColor,
width: 1,
),
),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
item.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: titleColor,
),
),
),
),
);
} else if (item is SelectableOption) {
return Padding(
padding: widget.tilePadding ?? const EdgeInsets.only(top: 24),
child: ProviderOptionTile(
title: item.title,
lightImagePath: item.lightIconPath,
darkImagePath: item.darkIconPath,
imageHeight: widget.imageHeight,
imageWidth: widget.imageWidth,
padding: widget.innerPadding,
description: item.description,
topLeftSubTitle: item.topLeftSubTitle,
topRightSubTitle: item.topRightSubTitle,
rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
bottomLeftSubTitle: item.bottomLeftSubTitle,
badges: item.badges,
isSelected: item.isOptionSelected,
borderRadius: widget.tileBorderRadius,
isLightMode: isLightMode,
onPressed: () => _handleOptionTap(item),
),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
}

View file

@ -498,7 +498,7 @@ class SendPage extends BasePage {
ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
actionRightButton: () async { actionRightButton: () async {
Navigator.of(_dialogContext).pop(); Navigator.of(_dialogContext).pop();
sendViewModel.commitTransaction(); sendViewModel.commitTransaction(context);
await showPopUp<void>( await showPopUp<void>(
context: context, context: context,
builder: (BuildContext _dialogContext) { builder: (BuildContext _dialogContext) {

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/setting_action_button.dart';
import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart';
import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/router.dart' as Router;
import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart';
@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State<DesktopSettingsPage> {
return Container(); return Container();
} }
if (!widget.dashboardViewModel.hasMweb && if ((!widget.dashboardViewModel.isMoneroViewOnly &&
item.name(context) == S.of(context).litecoin_mweb_settings) { item.name(context) == S.of(context).export_outputs) ||
(!widget.dashboardViewModel.hasMweb &&
item.name(context) == S.of(context).litecoin_mweb_settings)) {
return Container(); return Container();
} }

View file

@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) => handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.changeRep), Navigator.of(context).pushNamed(Routes.changeRep),
), ),
if(_otherSettingsViewModel.isEnabledBuyAction)
SettingsPickerCell(
title: S.current.default_buy_provider,
items: _otherSettingsViewModel.availableBuyProvidersTypes,
displayItem: _otherSettingsViewModel.getBuyProviderType,
selectedItem: _otherSettingsViewModel.buyProviderType,
onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
),
if(_otherSettingsViewModel.isEnabledSellAction)
SettingsPickerCell(
title: S.current.default_sell_provider,
items: _otherSettingsViewModel.availableSellProvidersTypes,
displayItem: _otherSettingsViewModel.getSellProviderType,
selectedItem: _otherSettingsViewModel.sellProviderType,
onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
),
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions, title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) => handler: (BuildContext context) =>

View file

@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setIsAppSecure(value); _privacySettingsViewModel.setIsAppSecure(value);
}), }),
SettingsSwitcherCell( SettingsSwitcherCell(
title: S.current.disable_buy, title: S.current.disable_trade_option,
value: _privacySettingsViewModel.disableBuy, value: _privacySettingsViewModel.disableTradeOption,
onValueChange: (BuildContext _, bool value) { onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBuy(value); _privacySettingsViewModel.setDisableTradeOption(value);
}),
SettingsSwitcherCell(
title: S.current.disable_sell,
value: _privacySettingsViewModel.disableSell,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
}), }),
SettingsSwitcherCell( SettingsSwitcherCell(
title: S.current.disable_bulletin, title: S.current.disable_bulletin,

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart'; import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
class Setup2FAInfoPage extends InfoPage { class Setup2FAInfoPage extends InfoPage {

View file

@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage {
leftButtonText: S.of(popupContext).cancel, leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async { actionRightButton: () async {
Navigator.of(popupContext).pop(); Navigator.of(popupContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction(); await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try { try {
Navigator.of(popupContext).pop(); Navigator.of(popupContext).pop();
} catch (_) {} } catch (_) {}

View file

@ -0,0 +1,184 @@
import 'dart:async';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/wallet_balance.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// ur:xmr-txunsigned - unsigned transaction
// should show a scanner afterwards.
class AnimatedURPage extends BasePage {
final bool isAll;
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) {
if (urQr == "export-outputs") {
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false);
} else if (urQr == "export-outputs-all") {
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
} else {
this.urQr = urQr;
}
}
late String urQr;
final AnimatedURModel animatedURmodel;
String get urQrType {
final first = urQr.trim().split("\n")[0];
return first.split('/')[0];
}
@override
Widget body(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 64.0),
child: URQR(
frames: urQr.trim().split("\n"),
),
),
SizedBox(height: 32),
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: () => _continue(context),
text: "Continue",
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
),
),
SizedBox(height: 32),
if (urQrType == "ur:xmr-output" && !isAll) Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: () => _exportAll(context),
text: "Export all",
color: Theme.of(context).colorScheme.secondary,
textColor: Colors.white,
),
),
),
],
);
}
void _exportAll(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) {
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true);
},
),
);
}
Future<void> _continue(BuildContext context) async {
try {
switch (urQrType) {
case "ur:xmr-txunsigned": // ur:xmr-txsigned
final ur = await presentQRScanner(context);
final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur);
if (result) {
Navigator.of(context).pop(true);
}
break;
case "ur:xmr-output": // xmr-keyimage
final ur = await presentQRScanner(context);
final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur);
if (result) {
Navigator.of(context).pop(true);
}
break;
default:
throw UnimplementedError("unable to handle UR: ${urQrType}");
}
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: e.toString(),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.pop(context, true));
});
}
}
}
class URQR extends StatefulWidget {
URQR({super.key, required this.frames});
List<String> frames;
@override
// ignore: library_private_types_in_public_api
_URQRState createState() => _URQRState();
}
const urFrameTime = 1000 ~/ 5;
class _URQRState extends State<URQR> {
Timer? t;
int frame = 0;
@override
void initState() {
super.initState();
setState(() {
t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) {
_nextFrame();
});
});
}
void _nextFrame() {
setState(() {
frame++;
});
}
@override
void dispose() {
t?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: QrImage(
data: widget.frames[frame % widget.frames.length], version: -1,
size: 400,
),
),
],
);
}
}

View file

@ -65,7 +65,7 @@ class WCPairingsWidget extends BasePage {
bool isCameraPermissionGranted = bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context); await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return; if (!isCameraPermissionGranted) return;
uri = await presentQRScanner(); uri = await presentQRScanner(context);
} else { } else {
uri = await _showEnterWalletConnectURIPopUp(context); uri = await _showEnterWalletConnectURIPopUp(context);
} }

View file

@ -1,20 +1,21 @@
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField extends StatelessWidget {
class AddressTextField<T extends Currency> extends StatelessWidget{
AddressTextField({ AddressTextField({
required this.controller, required this.controller,
this.isActive = true, this.isActive = true,
@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
final Function(BuildContext context)? onPushAddressBookButton; final Function(BuildContext context)? onPushAddressBookButton;
final Function(BuildContext context)? onPushAddressPickerButton; final Function(BuildContext context)? onPushAddressPickerButton;
final Function(ContactBase contact)? onSelectedContact; final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency; final T? selectedCurrency;
final Key? addressKey; final Key? addressKey;
@override @override
@ -231,7 +232,7 @@ class AddressTextField extends StatelessWidget {
bool isCameraPermissionGranted = bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context); await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return; if (!isCameraPermissionGranted) return;
final code = await presentQRScanner(); final code = await presentQRScanner(context);
if (code.isEmpty) { if (code.isEmpty) {
return; return;
} }

View file

@ -0,0 +1,527 @@
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class ProviderOptionTile extends StatelessWidget {
const ProviderOptionTile({
required this.onPressed,
required this.lightImagePath,
required this.darkImagePath,
required this.title,
this.topLeftSubTitle,
this.topRightSubTitle,
this.bottomLeftSubTitle,
this.bottomRightSubTitle,
this.leftSubTitleIconPath,
this.rightSubTitleLightIconPath,
this.rightSubTitleDarkIconPath,
this.description,
this.badges,
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.titleTextStyle,
this.firstSubTitleTextStyle,
this.secondSubTitleTextStyle,
this.leadingIcon,
this.selectedBackgroundColor,
this.isSelected = false,
required this.isLightMode,
});
final VoidCallback onPressed;
final String lightImagePath;
final String darkImagePath;
final String title;
final String? topLeftSubTitle;
final String? topRightSubTitle;
final String? bottomLeftSubTitle;
final String? bottomRightSubTitle;
final String? leftSubTitleIconPath;
final String? rightSubTitleLightIconPath;
final String? rightSubTitleDarkIconPath;
final String? description;
final List<String>? badges;
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final TextStyle? titleTextStyle;
final TextStyle? firstSubTitleTextStyle;
final TextStyle? secondSubTitleTextStyle;
final IconData? leadingIcon;
final Color? selectedBackgroundColor;
final bool isSelected;
final bool isLightMode;
@override
Widget build(BuildContext context) {
final backgroundColor = isSelected
? isLightMode
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final textColor = isSelected
? isLightMode
? Colors.white
: Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeColor = isSelected
? Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeTextColor = isSelected
? Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final imagePath = isSelected
? isLightMode
? darkImagePath
: lightImagePath
: isLightMode
? lightImagePath
: darkImagePath;
final rightSubTitleIconPath = isSelected
? isLightMode
? rightSubTitleDarkIconPath
: rightSubTitleLightIconPath
: isLightMode
? rightSubTitleLightIconPath
: rightSubTitleDarkIconPath;
return GestureDetector(
onTap: onPressed,
child: Container(
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)),
border: isSelected && !isLightMode ? Border.all(color: textColor) : null,
color: backgroundColor,
),
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
getImage(imagePath, height: imageHeight, width: imageWidth),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
children: [
Expanded(
child: Text(title,
style: titleTextStyle ?? textLargeBold(color: textColor))),
Row(
children: [
if (leadingIcon != null)
Icon(leadingIcon, size: 16, color: textColor),
],
)
],
),
),
),
],
),
if (topLeftSubTitle != null || topRightSubTitle != null)
subTitleWidget(
leftSubTitle: topLeftSubTitle,
subTitleIconPath: leftSubTitleIconPath,
textColor: textColor,
rightSubTitle: topRightSubTitle,
rightSubTitleIconPath: rightSubTitleIconPath),
if (bottomLeftSubTitle != null || bottomRightSubTitle != null)
subTitleWidget(
leftSubTitle: bottomLeftSubTitle,
textColor: textColor,
subTitleFontSize: 12),
if (badges != null && badges!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(children: [
...badges!
.map((badge) => Badge(
title: badge, textColor: badgeTextColor, backgroundColor: badgeColor))
.toList()
]),
)
],
),
),
),
);
}
}
class subTitleWidget extends StatelessWidget {
const subTitleWidget({
super.key,
this.leftSubTitle,
this.subTitleIconPath,
required this.textColor,
this.rightSubTitle,
this.rightSubTitleIconPath,
this.subTitleFontSize = 16,
});
final String? leftSubTitle;
final String? subTitleIconPath;
final Color textColor;
final String? rightSubTitle;
final String? rightSubTitleIconPath;
final double subTitleFontSize;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
leftSubTitle != null || subTitleIconPath != null
? Row(
children: [
if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 6),
child: getImage(subTitleIconPath!),
),
Text(
leftSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize,
fontWeight: FontWeight.w700,
color: textColor),
),
],
)
: Offstage(),
rightSubTitle != null || rightSubTitleIconPath != null
? Row(
children: [
if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 4),
child: getImage(rightSubTitleIconPath!, imageColor: textColor),
),
Text(
rightSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor),
),
],
)
: Offstage(),
],
);
}
}
class Badge extends StatelessWidget {
Badge({required this.textColor, required this.backgroundColor, required this.title});
final String title;
final Color textColor;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FittedBox(
fit: BoxFit.fitHeight,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}
Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
final bool isSvg = imagePath.endsWith('.svg');
final double imageHeight = height ?? 35;
final double imageWidth = width ?? 35;
if (isNetworkImage) {
return isSvg
? SvgPicture.network(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
placeholderBuilder: (BuildContext context) => Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(),
),
),
)
: Image.network(
imagePath,
height: imageHeight,
width: imageWidth,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
return Container(
height: imageHeight,
width: imageWidth,
);
},
);
} else {
return isSvg
? SvgPicture.asset(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
)
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
}
}
class OptionTilePlaceholder extends StatefulWidget {
OptionTilePlaceholder({
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.leadingIcon,
this.withBadge = true,
this.withSubtitle = true,
this.isDarkTheme = false,
this.errorText,
});
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final IconData? leadingIcon;
final bool withBadge;
final bool withSubtitle;
final bool isDarkTheme;
final String? errorText;
@override
_OptionTilePlaceholderState createState() => _OptionTilePlaceholderState();
}
class _OptionTilePlaceholderState extends State<OptionTilePlaceholder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.linear,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final backgroundColor = Theme.of(context).cardColor;
final titleColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor.withOpacity(0.4);
return widget.errorText != null
? Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
children: [
Text(
widget.errorText!,
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
if (widget.withSubtitle) SizedBox(height: 8),
Text(
'',
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
],
),
)
: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Stack(
children: [
Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Container(
height: widget.imageHeight ?? 35,
width: widget.imageWidth ?? 35,
decoration: BoxDecoration(
color: titleColor,
shape: BoxShape.circle,
),
),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
if (widget.leadingIcon != null)
Icon(widget.leadingIcon, size: 16, color: titleColor),
],
),
),
),
],
),
if (widget.withSubtitle)
Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 170,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
],
),
),
if (widget.withBadge)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
children: [
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
SizedBox(width: 8),
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
],
),
),
],
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
gradient: LinearGradient(
begin: Alignment(-2, -4),
end: Alignment(2, 4),
stops: [
_animation.value - 0.2,
_animation.value,
_animation.value + 0.2,
],
colors: [
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
],
),
),
),
),
],
);
},
);
}
}

View file

@ -21,6 +21,7 @@ class SettingActions {
addressBookSettingAction, addressBookSettingAction,
silentPaymentsSettingAction, silentPaymentsSettingAction,
litecoinMwebSettingAction, litecoinMwebSettingAction,
exportOutputsAction,
securityBackupSettingAction, securityBackupSettingAction,
privacySettingAction, privacySettingAction,
displaySettingAction, displaySettingAction,
@ -50,6 +51,16 @@ class SettingActions {
}, },
); );
static SettingActions exportOutputsAction = SettingActions._(
key: ValueKey('dashboard_page_menu_widget_export_outputs_settings_button_key'),
name: (context) => S.of(context).export_outputs,
image: 'assets/images/monero_menu.png',
onTap: (BuildContext context) {
Navigator.pop(context);
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
},
);
static SettingActions litecoinMwebSettingAction = SettingActions._( static SettingActions litecoinMwebSettingAction = SettingActions._(
key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'), key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'),
name: (context) => S.of(context).litecoin_mweb_settings, name: (context) => S.of(context).litecoin_mweb_settings,

View file

@ -65,8 +65,7 @@ abstract class SettingsStoreBase with Store {
required BitcoinSeedType initialBitcoinSeedType, required BitcoinSeedType initialBitcoinSeedType,
required NanoSeedType initialNanoSeedType, required NanoSeedType initialNanoSeedType,
required bool initialAppSecure, required bool initialAppSecure,
required bool initialDisableBuy, required bool initialDisableTrade,
required bool initialDisableSell,
required FilterListOrderType initialWalletListOrder, required FilterListOrderType initialWalletListOrder,
required FilterListOrderType initialContactListOrder, required FilterListOrderType initialContactListOrder,
required bool initialDisableBulletin, required bool initialDisableBulletin,
@ -155,8 +154,7 @@ abstract class SettingsStoreBase with Store {
useTOTP2FA = initialUseTOTP2FA, useTOTP2FA = initialUseTOTP2FA,
numberOfFailedTokenTrials = initialFailedTokenTrial, numberOfFailedTokenTrials = initialFailedTokenTrial,
isAppSecure = initialAppSecure, isAppSecure = initialAppSecure,
disableBuy = initialDisableBuy, disableTradeOption = initialDisableTrade,
disableSell = initialDisableSell,
disableBulletin = initialDisableBulletin, disableBulletin = initialDisableBulletin,
walletListOrder = initialWalletListOrder, walletListOrder = initialWalletListOrder,
contactListOrder = initialContactListOrder, contactListOrder = initialContactListOrder,
@ -183,9 +181,7 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
currentSyncMode = initialSyncMode, currentSyncMode = initialSyncMode,
currentSyncAll = initialSyncAll, currentSyncAll = initialSyncAll,
priority = ObservableMap<WalletType, TransactionPriority>(), priority = ObservableMap<WalletType, TransactionPriority>() {
defaultBuyProviders = ObservableMap<WalletType, ProviderType>(),
defaultSellProviders = ObservableMap<WalletType, ProviderType>() {
//this.nodes = ObservableMap<WalletType, Node>.of(nodes); //this.nodes = ObservableMap<WalletType, Node>.of(nodes);
if (initialMoneroTransactionPriority != null) { if (initialMoneroTransactionPriority != null) {
@ -229,30 +225,6 @@ abstract class SettingsStoreBase with Store {
initializeTrocadorProviderStates(); initializeTrocadorProviderStates();
WalletType.values.forEach((walletType) {
final key = 'buyProvider_${walletType.toString()}';
final providerId = sharedPreferences.getString(key);
if (providerId != null) {
defaultBuyProviders[walletType] = ProviderType.values.firstWhere(
(provider) => provider.id == providerId,
orElse: () => ProviderType.askEachTime);
} else {
defaultBuyProviders[walletType] = ProviderType.askEachTime;
}
});
WalletType.values.forEach((walletType) {
final key = 'sellProvider_${walletType.toString()}';
final providerId = sharedPreferences.getString(key);
if (providerId != null) {
defaultSellProviders[walletType] = ProviderType.values.firstWhere(
(provider) => provider.id == providerId,
orElse: () => ProviderType.askEachTime);
} else {
defaultSellProviders[walletType] = ProviderType.askEachTime;
}
});
reaction( reaction(
(_) => fiatCurrency, (_) => fiatCurrency,
(FiatCurrency fiatCurrency) => sharedPreferences.setString( (FiatCurrency fiatCurrency) => sharedPreferences.setString(
@ -275,20 +247,6 @@ abstract class SettingsStoreBase with Store {
reaction((_) => shouldShowRepWarning, reaction((_) => shouldShowRepWarning,
(bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val)); (bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val));
defaultBuyProviders.observe((change) {
final String key = 'buyProvider_${change.key.toString()}';
if (change.newValue != null) {
sharedPreferences.setString(key, change.newValue!.id);
}
});
defaultSellProviders.observe((change) {
final String key = 'sellProvider_${change.key.toString()}';
if (change.newValue != null) {
sharedPreferences.setString(key, change.newValue!.id);
}
});
priority.observe((change) { priority.observe((change) {
final String? key; final String? key;
switch (change.key) { switch (change.key) {
@ -340,13 +298,8 @@ abstract class SettingsStoreBase with Store {
}); });
} }
reaction((_) => disableBuy, reaction((_) => disableTradeOption,
(bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy)); (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption));
reaction(
(_) => disableSell,
(bool disableSell) =>
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
reaction( reaction(
(_) => disableBulletin, (_) => disableBulletin,
@ -691,10 +644,7 @@ abstract class SettingsStoreBase with Store {
bool isAppSecure; bool isAppSecure;
@observable @observable
bool disableBuy; bool disableTradeOption;
@observable
bool disableSell;
@observable @observable
FilterListOrderType contactListOrder; FilterListOrderType contactListOrder;
@ -780,12 +730,6 @@ abstract class SettingsStoreBase with Store {
@observable @observable
ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>(); ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
@observable
ObservableMap<WalletType, ProviderType> defaultBuyProviders;
@observable
ObservableMap<WalletType, ProviderType> defaultSellProviders;
@observable @observable
SortBalanceBy sortBalanceBy; SortBalanceBy sortBalanceBy;
@ -973,8 +917,7 @@ abstract class SettingsStoreBase with Store {
final shouldSaveRecipientAddress = final shouldSaveRecipientAddress =
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false; final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false;
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
final walletListOrder = final walletListOrder =
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
@ -1279,8 +1222,7 @@ abstract class SettingsStoreBase with Store {
initialBitcoinSeedType: bitcoinSeedType, initialBitcoinSeedType: bitcoinSeedType,
initialNanoSeedType: nanoSeedType, initialNanoSeedType: nanoSeedType,
initialAppSecure: isAppSecure, initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy, initialDisableTrade: disableTradeOption,
initialDisableSell: disableSell,
initialDisableBulletin: disableBulletin, initialDisableBulletin: disableBulletin,
initialWalletListOrder: walletListOrder, initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending, initialWalletListAscending: walletListAscending,
@ -1435,8 +1377,7 @@ abstract class SettingsStoreBase with Store {
numberOfFailedTokenTrials = numberOfFailedTokenTrials =
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
disableBulletin = disableBulletin =
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder = walletListOrder =

View file

@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color);
TextStyle textLarge({Color? color}) => _cakeRegular(18, color); TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
TextStyle textLargeBold({Color? color}) => _cakeBold(18, color);
TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color); TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color);
TextStyle textXLarge({Color? color}) => _cakeRegular(32, color); TextStyle textXLarge({Color? color}) => _cakeRegular(32, color);

View file

@ -0,0 +1,10 @@
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
class AnimatedURModel with Store {
AnimatedURModel(this.appStore)
: wallet = appStore.wallet!;
final AppStore appStore;
final WalletBase wallet;
}

View file

@ -0,0 +1,446 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
part 'buy_sell_view_model.g.dart';
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
) : _cryptoNumberFormat = NumberFormat(),
cryptoAmount = '',
fiatAmount = '',
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
cryptoCurrencies = <CryptoCurrency>[],
fiatCurrencies = <FiatCurrency>[],
paymentMethodState = InitialPaymentMethod(),
buySellQuotState = InitialBuySellQuotState(),
cryptoCurrency = appStore.wallet!.currency,
fiatCurrency = appStore.settingsStore.fiatCurrency,
providerList = [],
sortedRecommendedQuotes = ObservableList<Quote>(),
sortedQuotes = ObservableList<Quote>(),
paymentMethods = ObservableList<PaymentMethod>(),
settingsStore = appStore.settingsStore,
super(appStore: appStore) {
const excludeFiatCurrencies = [];
const excludeCryptoCurrencies = [];
fiatCurrencies =
FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList();
cryptoCurrencies = CryptoCurrency.all
.where((currency) => !excludeCryptoCurrencies.contains(currency))
.toList();
_initialize();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
final NumberFormat _cryptoNumberFormat;
late Timer bestRateSync;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
List<BuyProvider> get availableSellProviders {
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
@override
void onWalletChange(wallet) {
cryptoCurrency = wallet.currency;
}
bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark;
double get amount {
final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0;
final formattedCryptoAmount =
double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
return isBuyAction ? formattedFiatAmount : formattedCryptoAmount;
}
SettingsStore settingsStore;
Quote? bestRateQuote;
Quote? selectedQuote;
@observable
List<CryptoCurrency> cryptoCurrencies;
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
@observable
List<BuyProvider> providerList;
@observable
ObservableList<Quote> sortedRecommendedQuotes;
@observable
ObservableList<Quote> sortedQuotes;
@observable
ObservableList<PaymentMethod> paymentMethods;
@observable
FiatCurrency fiatCurrency;
@observable
CryptoCurrency cryptoCurrency;
@observable
String cryptoAmount;
@observable
String fiatAmount;
@observable
String cryptoCurrencyAddress;
@observable
bool isCryptoCurrencyAddressEnabled;
@observable
PaymentMethod? selectedPaymentMethod;
@observable
PaymentMethodLoadingState paymentMethodState;
@observable
BuySellQuotLoadingState buySellQuotState;
@computed
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
final hasSelectedPaymentMethod = selectedPaymentMethod != null;
final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
return hasSelectedQuote &&
hasSelectedPaymentMethod &&
isPaymentMethodLoaded &&
isBuySellQuotLoaded;
}
@action
void reset() {
cryptoCurrency = wallet.currency;
fiatCurrency = settingsStore.fiatCurrency;
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
_initialize();
}
@action
void changeBuySellAction() {
isBuyAction = !isBuyAction;
_initialize();
}
@action
void changeFiatCurrency({required FiatCurrency currency}) {
fiatCurrency = currency;
_onPairChange();
}
@action
void changeCryptoCurrency({required CryptoCurrency currency}) {
cryptoCurrency = currency;
_onPairChange();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
@action
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
@action
Future<void> changeFiatAmount({required String amount}) async {
fiatAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
cryptoAmount = S.current.fetching;
return;
}
if (bestRateQuote != null) {
_cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals;
cryptoAmount = _cryptoNumberFormat
.format(enteredAmount / bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
Future<void> changeCryptoAmount({required String amount}) async {
cryptoAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
fiatAmount = S.current.fetching;
}
if (bestRateQuote != null) {
fiatAmount = _cryptoNumberFormat
.format(enteredAmount * bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
void changeOption(SelectableOption option) {
if (option is Quote) {
sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false);
sortedQuotes.forEach((element) => element.setIsSelected = false);
option.setIsSelected = true;
selectedQuote = option;
} else if (option is PaymentMethod) {
paymentMethods.forEach((element) => element.isSelected = false);
option.isSelected = true;
selectedPaymentMethod = option;
} else {
throw ArgumentError('Unknown option type');
}
}
void onTapChoseProvider(BuildContext context) async {
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
await calculateBestRate();
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
for (var quote in newQuotes) quote.limits = null;
final newQuoteProviders = newQuotes
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
.toSet();
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
return !newQuoteProviders.contains(
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
}).map((missingQuote) {
final quote = Quote(
rate: missingQuote.rate,
feeAmount: missingQuote.feeAmount,
networkFee: missingQuote.networkFee,
transactionFee: missingQuote.transactionFee,
payout: missingQuote.payout,
rampId: missingQuote.rampId,
rampName: missingQuote.rampName,
rampIconPath: missingQuote.rampIconPath,
paymentType: missingQuote.paymentType,
quoteId: missingQuote.quoteId,
recommendations: missingQuote.recommendations,
provider: missingQuote.provider,
isBuyAction: missingQuote.isBuyAction,
limits: missingQuote.limits,
);
quote.setFiatCurrency = missingQuote.fiatCurrency;
quote.setCryptoCurrency = missingQuote.cryptoCurrency;
return quote;
}).toList();
final updatedQuoteOptions = List<SelectableItem>.from([
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
...sortedQuotes,
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
...outOfLimitQuotes,
]);
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
updatedQuoteOptions,
changeOption,
launchTrade,
],
).then((value) => calculateBestRate());
}
void _onPairChange() {
_initialize();
}
void _setProviders() =>
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
cryptoAmount = '';
fiatAmount = '';
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
paymentMethodState = InitialPaymentMethod();
buySellQuotState = InitialBuySellQuotState();
await _getAvailablePaymentTypes();
await calculateBestRate();
}
String _getInitialCryptoCurrencyAddress() {
return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : '';
}
@action
Future<void> _getAvailablePaymentTypes() async {
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) => element
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
.timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
for (var methods in result) {
for (var method in methods) {
uniquePaymentMethods[method.paymentMethodType] = method;
}
}
paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
paymentMethodState = PaymentMethodFailed();
}
}
@action
Future<void> calculateBestRate() async {
buySellQuotState = BuySellQuotLoading();
final result = await Future.wait<List<Quote>?>(providerList.map((element) => element
.fetchQuote(
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
walletAddress: wallet.walletAddresses.address,
)
.timeout(
Duration(seconds: 10),
onTimeout: () => null,
)));
sortedRecommendedQuotes.clear();
sortedQuotes.clear();
final validQuotes = result
.where((element) => element != null && element.isNotEmpty)
.expand((element) => element!)
.toList();
if (validQuotes.isEmpty) {
buySellQuotState = BuySellQuotFailed();
return;
}
validQuotes.sort((a, b) => a.rate.compareTo(b.rate));
final Set<String> addedProviders = {};
final List<Quote> uniqueProviderQuotes = validQuotes.where((element) {
if (addedProviders.contains(element.provider.title)) return false;
addedProviders.add(element.provider.title);
return true;
}).toList();
sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
sortedQuotes = ObservableList.of(
validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList());
if (sortedRecommendedQuotes.isNotEmpty) {
sortedRecommendedQuotes.first
..setIsBestRate = true
..recommendations.insert(0, ProviderRecommendation.bestRate);
bestRateQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.sort((a, b) {
if (a.provider is OnRamperBuyProvider) return -1;
if (b.provider is OnRamperBuyProvider) return 1;
return 0;
});
selectedQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.first.setIsSelected = true;
}
buySellQuotState = BuySellQuotLoaded();
}
@action
Future<void> launchTrade(BuildContext context) async {
final provider = selectedQuote!.provider;
await provider.launchProvider(
context: context,
quote: selectedQuote!,
amount: amount,
isBuyAction: isBuyAction,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
}
}

View file

@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store {
required this.anonpayTransactionsStore, required this.anonpayTransactionsStore,
required this.sharedPreferences, required this.sharedPreferences,
required this.keyService}) required this.keyService})
: hasSellAction = false, : hasTradeAction = false,
hasBuyAction = false,
hasExchangeAction = false, hasExchangeAction = false,
isShowFirstYatIntroduction = false, isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false, isShowSecondYatIntroduction = false,
@ -393,6 +392,12 @@ abstract class DashboardViewModelBase with Store {
wallet.type == WalletType.wownero || wallet.type == WalletType.wownero ||
wallet.type == WalletType.haven; wallet.type == WalletType.haven;
@computed
bool get isMoneroViewOnly {
if (wallet.type != WalletType.monero) return false;
return monero!.isViewOnly();
}
@computed @computed
String? get getMoneroError { String? get getMoneroError {
if (wallet.type != WalletType.monero) return null; if (wallet.type != WalletType.monero) return null;
@ -515,37 +520,8 @@ abstract class DashboardViewModelBase with Store {
Map<String, List<FilterItem>> filterItems; Map<String, List<FilterItem>> filterItems;
BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType(
settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime);
BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType(
settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime);
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty;
List<BuyProvider> get availableSellProviders {
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
bool get hasSellProviders =>
ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty;
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
@action @action
@ -558,16 +534,10 @@ abstract class DashboardViewModelBase with Store {
bool hasExchangeAction; bool hasExchangeAction;
@computed @computed
bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders; bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
@observable @observable
bool hasBuyAction; bool hasTradeAction;
@computed
bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders;
@observable
bool hasSellAction;
@computed @computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin; bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
@ -771,8 +741,7 @@ abstract class DashboardViewModelBase with Store {
void updateActions() { void updateActions() {
hasExchangeAction = !isHaven; hasExchangeAction = !isHaven;
hasBuyAction = !isHaven; hasTradeAction = !isHaven;
hasSellAction = !isHaven;
} }
@computed @computed

View file

@ -214,7 +214,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
bool isCameraPermissionGranted = bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context); await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return; if (!isCameraPermissionGranted) return;
String code = await presentQRScanner(); String code = await presentQRScanner(context);
if (code.isEmpty) { if (code.isEmpty) {
throw Exception('Unexpected scan QR code value: value is empty'); throw Exception('Unexpected scan QR code value: value is empty');

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -32,6 +34,16 @@ class RestoredWallet {
final String? privateKey; final String? privateKey;
factory RestoredWallet.fromKey(Map<String, dynamic> json) { factory RestoredWallet.fromKey(Map<String, dynamic> json) {
try {
final codeParsed = jsonDecode(json['raw_qr'].toString());
if (codeParsed["version"] == 0) {
json['address'] = codeParsed["primaryAddress"];
json['view_key'] = codeParsed["privateViewKey"];
json['height'] = codeParsed["restoreHeight"].toString();
}
} catch (e) {
// fine, we don't care, it is only for monero anyway
}
final height = json['height'] as String?; final height = json['height'] as String?;
return RestoredWallet( return RestoredWallet(
restoreMode: json['mode'] as WalletRestoreMode, restoreMode: json['mode'] as WalletRestoreMode,
@ -39,7 +51,7 @@ class RestoredWallet {
address: json['address'] as String?, address: json['address'] as String?,
spendKey: json['spend_key'] as String?, spendKey: json['spend_key'] as String?,
viewKey: json['view_key'] as String?, viewKey: json['view_key'] as String?,
height: height != null ? int.parse(height) : 0, height: height != null ? int.tryParse(height)??0 : 0,
privateKey: json['private_key'] as String?, privateKey: json['private_key'] as String?,
); );
} }

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/core/seed_validator.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/qr_scanner.dart';
@ -51,6 +53,17 @@ class WalletRestoreFromQRCode {
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
if (extracted == null) {
// Special case for view-only monero wallet
final codeParsed = json.decode(code);
if (codeParsed["version"] == 0 &&
codeParsed["primaryAddress"] != null &&
codeParsed["privateViewKey"] != null &&
codeParsed["restoreHeight"] != null) {
return WalletType.monero;
}
}
return _walletTypeMap[extracted]; return _walletTypeMap[extracted];
} }
@ -78,7 +91,7 @@ class WalletRestoreFromQRCode {
} }
static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async { static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
String code = await presentQRScanner(); String code = await presentQRScanner(context);
if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty');
WalletType? walletType; WalletType? walletType;
@ -112,7 +125,7 @@ class WalletRestoreFromQRCode {
queryParameters['address'] = _extractAddressFromUrl(code, walletType!); queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
} }
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters}; Map<String, dynamic> credentials = {'type': walletType, ...queryParameters, 'raw_qr': code};
credentials['mode'] = _determineWalletRestoreMode(credentials); credentials['mode'] = _determineWalletRestoreMode(credentials);
@ -208,6 +221,17 @@ class WalletRestoreFromQRCode {
return WalletRestoreMode.keys; return WalletRestoreMode.keys;
} }
if (type == WalletType.monero) {
final codeParsed = json.decode(credentials['raw_qr'].toString());
if (codeParsed["version"] != 0) throw UnimplementedError("Found view-only restore with unsupported version");
if (codeParsed["primaryAddress"] == null ||
codeParsed["privateViewKey"] == null ||
codeParsed["restoreHeight"] == null) {
throw UnimplementedError("Missing one or more attributes in the JSON");
}
return WalletRestoreMode.keys;
}
throw Exception('Unexpected restore mode: restore params are invalid'); throw Exception('Unexpected restore mode: restore params are invalid');
} }
} }

Some files were not shown because too many files have changed in this diff Show more