Merge remote-tracking branch 'origin/main' into CW-981-rebase-zano-patches-to-support-ssl

This commit is contained in:
cyan 2025-03-14 21:52:20 +00:00
commit e29c75b237
119 changed files with 4896 additions and 2538 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
*

View file

@ -1,83 +1,87 @@
# Usage:
# docker build . -f Dockerfile.linux -t ghcr.io/cake-tech/cake_wallet:main-linux
# docker build . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:main-linux
# docker push ghcr.io/cake-tech/cake_wallet:main-linux
FROM --platform=linux/amd64 docker.io/debian:12
LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet
ENV GOLANG_VERSION=1.23.4
# comes from https://developer.android.com/studio/#command-tools
ENV ANDROID_SDK_TOOLS_VERSION=11076708
# https://developer.android.com/studio/releases/build-tools
ENV ANDROID_PLATFORM_VERSION=34
ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0
ENV FLUTTER_VERSION=3.24.0
# If we ever need to migrate the home directory...
RUN sed -i 's|^root:[^:]*:[^:]*:[^:]*:[^:]*:/root:|root:x:0:0:root:/root:|' /etc/passwd
# mkdir -p /root && rm -rf /root && cp -a /root /root
ENV HOME=/root
# Heavily inspired by cirrusci images
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34/Dockerfile
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34-ndk/Dockerfile
# https://github.com/cirruslabs/docker-images-flutter/blob/master/sdk/Dockerfile
FROM --platform=linux/amd64 docker.io/debian:12
LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet
# Set necessary environment variables
# Set Go version to latest known-working version
ENV GOLANG_VERSION=1.23.4
# Pin Flutter version to latest known-working version
ENV FLUTTER_VERSION=3.24.4
# Pin Android Studio, platform, and build tools versions to latest known-working version
# Comes from https://developer.android.com/studio/#command-tools
ENV ANDROID_SDK_TOOLS_VERSION=11076708
# Comes from https://developer.android.com/studio/releases/build-tools
ENV ANDROID_PLATFORM_VERSION=34
ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0
# If we ever need to migrate the home directory...
RUN sed -i 's|^root:[^:]*:[^:]*:[^:]*:[^:]*:/root:|root:x:0:0:root:/root:|' /etc/passwd
# mkdir -p /root && rm -rf /root && cp -a /root /root
ENV HOME=/root
ENV ANDROID_HOME=/opt/android-sdk-linux \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
LANGUAGE=en_US:en
# Set Android SDK paths
ENV ANDROID_SDK_ROOT=$ANDROID_HOME \
PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator
# Upgrade base image
RUN apt-get update \
&& apt-get upgrade -y
# Install all build dependencies
RUN set -o xtrace \
&& cd /opt \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y jq \
&& apt-get install -y default-jdk \
&& apt-get install -y sudo wget zip unzip git openssh-client curl bc software-properties-common build-essential ruby-full ruby-bundler libstdc++6 libpulse0 libglu1-mesa locales lcov libsqlite3-dev --no-install-recommends \
&& apt-get install -y --no-install-recommends --no-install-suggests \
# Core dependencies
bc build-essential curl default-jdk git jq lcov libglu1-mesa libpulse0 libsqlite3-dev libstdc++6 locales openssh-client ruby-bundler ruby-full software-properties-common sudo unzip wget zip \
# for x86 emulators
&& apt-get install -y libxtst6 libnss3-dev libnspr4 libxss1 libatk-bridge2.0-0 libgtk-3-0 libgdk-pixbuf2.0-0 \
&& apt-get install -y -qq xxd \
&& apt-get install -y lftp \
&& apt-get install -qq -y sqlite3 libsqlite3-dev \
# linux desktop dependencies
&& apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev \
libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \
# Linux desktop dependencies
clang cmake libgtk-3-dev ninja-build pkg-config \
# monero_c dependencies
&& apt-get install -y ccache build-essential autoconf libtool gperf llvm \
autoconf automake build-essential ccache gperf libtool llvm \
# extra stuff for KVM
&& apt-get install -y udev qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils \
# for linux tests
&& apt-get install -y xvfb network-manager ffmpeg x11-utils \
# for aarch64-linux-gnu
&& apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/* \
bridge-utils libvirt-clients libvirt-daemon-system qemu-kvm udev \
# Linux test dependencies
ffmpeg network-manager x11-utils xvfb psmisc \
# aarch64-linux-gnu dependencies
g++-aarch64-linux-gnu gcc-aarch64-linux-gnu \
&& apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \
&& locale-gen \
&& update-locale LANG=en_US.UTF-8
# install nodejs for actions
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\
rm -rf /usr/local/go &&\
tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz
# Install nodejs for Github Actions
RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Go
ENV PATH=${PATH}:/usr/local/go/bin:${HOME}/go/bin
ENV GOROOT=/usr/local/go
ENV GOPATH=${HOME}/go
RUN go install golang.org/x/mobile/cmd/gomobile@latest
RUN gomobile init
RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\
rm -rf /usr/local/go &&\
tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz && \
go install golang.org/x/mobile/cmd/gomobile@latest && \
gomobile init
# Install Android SDK commandline tools and emulator
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip -O android-sdk-tools.zip \
&& mkdir -p ${ANDROID_HOME}/cmdline-tools/ \
&& unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \
@ -94,10 +98,10 @@ RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-${AN
&& git config --global user.email "czarek@cakewallet.com" \
&& git config --global user.name "CakeWallet CI"
# emulator is not available on linux/arm64 (https://issuetracker.google.com/issues/227219818)
# Handle emulator not being available on linux/arm64 (https://issuetracker.google.com/issues/227219818)
RUN if [ $(uname -m) == "x86_64" ]; then sdkmanager emulator ; fi
# Extra dependencies to not download them for cake wallet build
# Pre-install extra Android SDK dependencies in order to not have to download them for each build
RUN yes | sdkmanager \
"platforms;android-$ANDROID_PLATFORM_VERSION" \
"build-tools;$ANDROID_BUILD_TOOLS_VERSION" \
@ -107,26 +111,26 @@ RUN yes | sdkmanager \
"build-tools;33.0.0" \
"build-tools;35.0.0"
# Install extra NDK dependency for sp_scanner
ENV ANDROID_NDK_VERSION=27.2.12479018
# Extra ndk dependency for sp_scanner
RUN yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \
"ndk;27.0.12077973"
# https://github.com/ReactiveCircus/android-emulator-runner dependencies for tests
# Install dependencies for tests
# Comes from https://github.com/ReactiveCircus/android-emulator-runner
RUN yes | sdkmanager "system-images;android-29;default;x86" \
"system-images;android-29;default;x86_64" \
"system-images;android-31;default;x86_64" \
"platforms;android-29"
# fake the KVM status so android emulator doesn't complain (that much)
# Fake the KVM status so the Android emulator doesn't complain (that much)
RUN (addgroup kvm || true) && \
adduser root kvm && \
mkdir -p /etc/udev/rules.d/ && \
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | tee /etc/udev/rules.d/99-kvm4all.rules
# Install rustup, rust toolchains, and cargo-ndk
ENV PATH=${HOME}/.cargo/bin:${PATH}
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \
cargo install cargo-ndk && \
for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu; \
@ -134,17 +138,16 @@ RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \
rustup target add --toolchain stable $target; \
done
# Download and install Flutter
ENV HOME=${HOME}
ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION}
ENV FLUTTER_ROOT=$FLUTTER_HOME
ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin
RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME}
RUN yes | flutter doctor --android-licenses \
RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \
&& yes | flutter doctor --android-licenses \
&& flutter doctor \
&& chown -R root:root ${FLUTTER_HOME}
# Download and pre-cache necessary Flutter artifacts to speed up builds
RUN flutter precache

26
assets/images/xoswap.svg Normal file
View file

@ -0,0 +1,26 @@
<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20.5" cy="20.5" r="20" fill="url(#paint0_linear_46_2768)"/>
<path opacity="0.3" d="M35.4175 29.1152L37.8232 30.5049C34.3648 36.4818 27.902 40.5028 20.5 40.5028C13.0987 40.5028 6.63636 36.4825 3.17773 30.5065L5.58379 29.1175C8.5621 34.2633 14.1268 37.7251 20.5 37.7251C26.8741 37.7251 32.4395 34.2623 35.4175 29.1152Z" fill="url(#paint1_linear_46_2768)"/>
<path opacity="0.5" d="M20.5 0.5C31.5452 0.5 40.4995 9.45431 40.4995 20.5C40.4995 24.1435 39.5252 27.5594 37.823 30.5015L35.4172 29.1119C36.8829 26.5785 37.7217 23.6372 37.7217 20.5C37.7217 10.9886 30.0113 3.27808 20.5 3.27778V0.5Z" fill="url(#paint2_linear_46_2768)"/>
<path opacity="0.15" d="M20.4995 0.5V3.27722L20.2152 3.28009C10.835 3.43217 3.27778 11.0836 3.27778 20.5C3.27778 23.6377 4.11686 26.5794 5.58288 29.113L3.17714 30.5026C1.47452 27.5603 0.5 24.144 0.5 20.5C0.5 9.4545 9.45402 0.5003 20.4995 0.5Z" fill="url(#paint3_linear_46_2768)"/>
<path d="M28.3176 15.9228C27.5969 15.5799 26.8383 15.4041 26.0416 15.401C25.1349 15.397 24.7278 15.3978 23.8717 15.4026C23.7244 15.4033 23.5233 15.4928 23.4243 15.6013C21.9498 17.2192 20.4792 18.8411 19.007 20.4621C18.931 20.5461 18.8542 20.63 18.7718 20.7203C18.7219 20.7749 18.7219 20.8597 18.7718 20.9143C19.1099 21.2833 19.4378 21.6421 19.7656 22C20.9987 23.3463 22.2309 24.6925 23.4679 26.0356C23.5415 26.1156 23.6912 26.1814 23.7997 26.1821C24.6874 26.1885 25.0715 26.1909 25.9838 26.1853C28.6747 26.1687 30.8755 24.295 31.3253 21.6413C31.7197 19.3162 30.4724 16.9468 28.3168 15.9228H28.3176ZM25.6963 23.9862C23.9311 23.9862 22.5009 22.5552 22.5009 20.7908C22.5009 19.0264 23.9319 17.5954 25.6963 17.5954C27.4607 17.5954 28.8917 19.0264 28.8917 20.7908C28.8917 22.5552 27.4607 23.9862 25.6963 23.9862Z" fill="white"/>
<path d="M17.7312 20.8805C17.6987 20.8449 17.6987 20.791 17.7312 20.7554C19.281 19.048 20.8236 17.3486 22.3837 15.6301C22.4605 15.5454 22.4019 15.4084 22.2871 15.4068C22.0685 15.4036 20.2162 15.3989 19.1067 15.4052C19.0331 15.4052 18.9404 15.4527 18.8897 15.5081C17.9323 16.5527 16.9796 17.6012 16.0262 18.6489C16.008 18.6695 15.989 18.6893 15.962 18.717C15.9193 18.7613 15.844 18.7542 15.8147 18.7004C15.8036 18.6806 15.7918 18.6624 15.7775 18.6465C14.8367 17.6059 13.8935 16.5701 12.9551 15.5295C12.8728 15.4385 12.792 15.3965 12.6661 15.3965C11.5883 15.402 10.5097 15.3997 9.43108 15.3997H9.4295C9.34872 15.3997 9.30675 15.4955 9.36139 15.5557C9.7328 15.9643 10.0812 16.3492 10.4305 16.7332C11.6318 18.0557 12.8316 19.379 14.0353 20.6984C14.1177 20.7887 14.1216 20.8433 14.0353 20.9367C12.5172 22.5871 11.0022 24.2414 9.48652 25.8949C9.44138 25.944 9.39703 25.9939 9.34793 26.0486C9.30358 26.0985 9.33843 26.1784 9.40574 26.1784C9.41287 26.1784 9.41999 26.1784 9.42712 26.1784C10.5311 26.1784 11.6176 26.1832 12.7215 26.1895C12.8458 26.1895 12.9187 26.1389 12.9947 26.0549C13.9355 25.0167 14.8787 23.9801 15.8211 22.9427C15.8448 22.9165 15.8607 22.8833 15.8892 22.8389C16.1299 23.1018 16.3485 23.3386 16.5647 23.5762C17.3289 24.4148 18.0915 25.2543 18.8589 26.0898C18.9016 26.1365 18.9769 26.1808 19.037 26.1816C20.1869 26.1872 21.2117 26.1856 22.3615 26.1848C22.3726 26.1848 22.4257 26.1848 22.4827 26.1832C22.5286 26.1824 22.5516 26.1278 22.5207 26.0945C20.9218 24.3499 19.3237 22.6164 17.7328 20.8797L17.7312 20.8805Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_46_2768" x1="40.5" y1="40.5" x2="0.5" y2="0.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#420BE6"/>
<stop offset="1" stop-color="#A16BB3"/>
</linearGradient>
<linearGradient id="paint1_linear_46_2768" x1="36.4999" y1="30.0201" x2="4.63786" y2="30.296" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_46_2768" x1="20.6374" y1="1.18966" x2="37.2481" y2="29.0165" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_46_2768" x1="4.08621" y1="30.569" x2="20.2018" y2="1.65204" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,176 +0,0 @@
# Building CakeWallet for Linux
## Requirements and Setup
The following are the system requirements to build CakeWallet for your Linux device.
```
Ubuntu >= 16.04
Flutter 3.10.x
```
## Building CakeWallet on Linux
These steps will help you configure and execute a build of CakeWallet from its source code.
### 1. Installing Package Dependencies
CakeWallet requires some packages to be installed on your build system. You may easily install them on your build system with the following command:
`$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool`
> [!WARNING]
>
> ### Check gcc version
>
> It is needed to use gcc 10 or 9 to successfully link dependencies with flutter.\
> To check what gcc version you are using:
>
> ```bash
> $ gcc --version
> $ g++ --version
> ```
>
> If you are using gcc version newer than 10, then you need to downgrade to version 10.4.0:
>
> ```bash
> $ sudo apt install gcc-10 g++-10
> $ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
> $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
> ```
> [!NOTE]
>
> Alternatively, you can use the [nix-shell](https://nixos.org/) with the `gcc10.nix` file\
> present on `scripts/linux` like so:
> ```bash
> $ nix-shell gcc10.nix
> ```
> This will get you in a nix environment with all the required dependencies that you can use to build the software from,\
> and it works in any linux distro.
### 2. Installing Flutter
Need to install flutter. For this please check section [How to install flutter on Linux](https://docs.flutter.dev/get-started/install/linux).
### 3. Verify Installations
Verify that the Flutter has been correctly installed on your system with the following command:
`$ flutter doctor`
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8)
```
### 4. Acquiring the CakeWallet Source Code
Download CakeWallet source code
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch linux/password-direct-input`
Proceed into the source code before proceeding with the next steps:
`$ cd cake_wallet/scripts/linux/`
To configure some project properties run:
`$ ./cakewallet.sh`
Build the Monero libraries and their dependencies:
`$ ./build_all.sh`
Now the dependencies need to be copied into the CakeWallet project with this command:
`$ ./setup.sh`
It is now time to change back to the base directory of the CakeWallet source code:
`$ cd ../../`
Install Flutter package dependencies with this command:
`$ flutter pub get`
> #### If you will get an error like:
>
> ```
> The plugin `cw_shared_external` requires your app to be migrated to the Android embedding v2. Follow the steps on the migration doc above and re-run
> this command.
> ```
>
> Then need to config Android project settings. For this open `scripts/android` (`$ cd scripts/android`) directory and run followed commands:
>
> ```
> $ source ./app_env.sh cakewallet
> $ ./app_config.sh
> $ cd ../..
> ```
>
> Then re-configure Linux project again. For this open `scripts/linux` (`$cd scripts/linux`) directory and run:
> `$ ./cakewallet.sh`
> and back to project root directory:
> `$ cd ../..`
> and fetch dependencies again
> `$ flutter pub get`
Your CakeWallet binary will be built with some specific keys for iterate with 3rd party services. You may generate these secret keys placeholders with the following command:
`$ dart run tool/generate_new_secrets.dart`
We will generate mobx models for the project.
`$ ./model_generator.sh`
Then we need to generate localization files.
`$ dart run tool/generate_localization.dart`
### 5. Build!
`$ flutter build linux --release`
Path to executable file will be:
`build/linux/x64/release/bundle/cake_wallet`
> ### Troubleshooting
>
> If you got an error while building the application with `$ flutter build linux --release` command, add `-v` argument to the command (`$ flutter build linux -v --release`) to get details.\
> If you got in flutter build logs: undefined reference to `hid_free_enumeration`, or another error with undefined reference to `hid_*`, then rebuild monero lib without hidapi lib. Check does exists `libhidapi-dev` in your scope and remove it from your scope for build without it.
# Flatpak
For package the built application into flatpak you need firstly to install `flatpak` and `flatpak-builder`:
`$ sudo apt install flatpak flatpak-builder`
Then need to [add flathub](https://flatpak.org/setup/Ubuntu) (or just `$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`). Then need to install freedesktop runtime and sdk:
`$ flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08`
To build with using of `flatpak-build` directory run next:
`$ flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml`
And then export bundle:
`$ flatpak build-export export flatpak-build`
`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet`
Result file: `cake_wallet.flatpak` should be generated in the current directory.
For install generated flatpak file use:
`$ flatpak --user install cake_wallet.flatpak`
For run the installed application run:
`$ flatpak run com.cakewallet.CakeWallet`
Copyright (c) 2023 Cake Technologies LLC.

View file

@ -1,38 +0,0 @@
# Building CakeWallet for Windows
## Requirements and Setup
The following are the system requirements to build CakeWallet for your Windows PC.
```
Windows 10 or later (64-bit), x86-64 based
Flutter 3 or above
```
## Building CakeWallet on Windows
These steps will help you configure and execute a build of CakeWallet from its source code.
### 1. Installing Package Dependencies
For build CakeWallet windows application from sources you will be needed to have:
> [Install Flutter]Follow installation guide (https://docs.flutter.dev/get-started/install/windows) and install do not miss to dev tools (install https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) which are required for windows desktop development (need to install Git for Windows and Visual Studio 2022). Then install `Desktop development with C++` packages via GUI Visual Studio 2022, or Visual Studio Build Tools 2022 including: `C++ Build Tools core features`, `C++ 2022 Redistributable Update`, `C++ core desktop features`, `MVC v143 - VS 2022 C++ x64/x86 build tools`, `C++ CMake tools for Windows`, `Testing tools core features - Build Tools`, `C++ AddressSanitizer`.
> [Install WSL] for building monero dependencies need to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu):
`$ sudo apt update `
`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config`
### 2. Pull CakeWallet source code
You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git by following next command:
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart`
OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip)
### 3. Build Monero, Monero_c and their dependencies
For use monero in the application need to build Monero wrapper - Monero_C which will be used by monero.dart package. For that need to run shell (bash - typically same named utility should be available after WSL is enabled in your system) with previously installed WSL, then change current directory to the application project directory with your used shell and then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`.
### 4. Configure and build CakeWallet application
To configure the application open directory where you have downloaded or unarchived CakeWallet sources and run `cakewallet.bat`.
Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL.
After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application.

View file

@ -632,8 +632,8 @@ abstract class ElectrumWalletBase
}).toList();
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
// sort the unconfirmed coins so that mweb coins are first:
availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1);
// sort the unconfirmed coins so that mweb coins are last:
availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1);
for (int i = 0; i < availableInputs.length; i++) {
final utx = availableInputs[i];

View file

@ -0,0 +1,26 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:on_chain/solana/solana.dart';
class SolanaRPCHTTPService implements SolanaJSONRPCService {
SolanaRPCHTTPService(
{required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)})
: client = client ?? Client();
@override
final String url;
final Client client;
final Duration defaultRequestTimeout;
@override
Future<Map<String, dynamic>> call(SolanaRequestDetails params, [Duration? timeout]) async {
final response = await client.post(
Uri.parse(url),
body: params.toRequestBody(),
headers: {
'Content-Type': 'application/json',
},
).timeout(timeout ?? defaultRequestTimeout);
final data = json.decode(response.body) as Map<String, dynamic>;
return data;
}
}

View file

@ -27,6 +27,10 @@ dependencies:
version: 1.0.0
socks5_proxy: ^1.0.4
unorm_dart: ^0.3.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
# tor:
# git:
# url: https://github.com/cake-tech/tor.git

View file

@ -121,7 +121,7 @@ class PolygonWallet extends EVMChainWallet {
if (!hasKeysFile) rethrow;
}
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ??
final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ??
EVMChainERC20Balance(BigInt.zero);
final WalletKeysData keysData;

View file

@ -26,7 +26,7 @@ class DefaultSPLTokens {
decimal: 5,
mint: 'Bonk',
iconPath: 'assets/images/bonk_icon.png',
enabled: true,
enabled: false,
),
SPLToken(
name: 'Raydium',
@ -35,7 +35,7 @@ class DefaultSPLTokens {
decimal: 6,
mint: 'ray',
iconPath: 'assets/images/ray_icon.png',
enabled: true,
enabled: false,
),
SPLToken(
name: 'Wrapped Ethereum (Sollet)',

View file

@ -1,9 +1,8 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:solana/encoder.dart';
class PendingSolanaTransaction with PendingTransaction {
final double amount;
final SignedTx signedTransaction;
final String serializedTransaction;
final String destinationAddress;
final Function sendTransaction;
final double fee;
@ -11,7 +10,7 @@ class PendingSolanaTransaction with PendingTransaction {
PendingSolanaTransaction({
required this.fee,
required this.amount,
required this.signedTransaction,
required this.serializedTransaction,
required this.destinationAddress,
required this.sendTransaction,
});
@ -36,7 +35,7 @@ class PendingSolanaTransaction with PendingTransaction {
String get feeFormatted => fee.toString();
@override
String get hex => signedTransaction.encode();
String get hex => serializedTransaction;
@override
String get id => '';

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,9 @@ class SolanaTransactionInfo extends TransactionInfo {
@override
String amountFormatted() {
String stringBalance = solAmount.toString();
if (stringBalance.toString().length >= 12) {
stringBalance = stringBalance.substring(0, 12);
}
return '$stringBalance $tokenSymbol';
}

View file

@ -30,9 +30,9 @@ import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/base58.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:on_chain/solana/solana.dart' hide Store;
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
part 'solana_wallet.g.dart';
@ -77,14 +77,6 @@ abstract class SolanaWalletBase
final String? _hexPrivateKey;
final EncryptionFileUtils encryptionFileUtils;
// The Solana WalletPair
Ed25519HDKeyPair? _walletKeyPair;
Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
// To access the privateKey bytes.
Ed25519HDKeyPairData? _keyPairData;
late final SolanaWalletClient _client;
@observable
@ -108,29 +100,23 @@ abstract class SolanaWalletBase
final Completer<SharedPreferences> _sharedPrefs = Completer();
@override
Ed25519HDKeyPairData get keys {
if (_keyPairData == null) {
return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
}
Object get keys => throw UnimplementedError("keys");
return _keyPairData!;
}
late final SolanaPrivateKey _solanaPrivateKey;
late final SolanaPublicKey _solanaPublicKey;
SolanaPublicKey get solanaPublicKey => _solanaPublicKey;
SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey;
String get solanaAddress => _solanaPublicKey.toAddress().address;
@override
String? get seed => _mnemonic;
@override
String get privateKey {
final privateKeyBytes = _keyPairData!.bytes;
final publicKeyBytes = _keyPairData!.publicKey.bytes;
final encodedBytes = privateKeyBytes + publicKeyBytes;
final privateKey = base58encode(encodedBytes);
return privateKey;
}
String get privateKey => _solanaPrivateKey.seedHex();
@override
WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey);
@ -140,35 +126,47 @@ abstract class SolanaWalletBase
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
// Create WalletPair using either the mnemonic or the privateKey
_walletKeyPair = await getWalletPair(
// Create the privatekey using either the mnemonic or the privateKey
_solanaPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
passphrase: passphrase,
);
// Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
_keyPairData = await _walletKeyPair!.extract();
// Extract the public key and wallet address
_solanaPublicKey = _solanaPrivateKey.publicKey();
walletInfo.address = _walletKeyPair!.address;
walletInfo.address = _solanaPublicKey.toAddress().address;
await walletAddresses.init();
await transactionHistory.init();
await save();
}
Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
Future<SolanaPrivateKey> getPrivateKey({
String? mnemonic,
String? privateKey,
String? passphrase,
}) async {
assert(mnemonic != null || privateKey != null);
if (mnemonic != null) {
return Wallet.fromMnemonic(mnemonic, account: 0, change: 0);
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
// Derive a Solana private key from the seed
final bip44 = Bip44.fromSeed(seed, Bip44Coins.solana);
final childKey = bip44.deriveDefaultPath.change(Bip44Changes.chainExt);
return SolanaPrivateKey.fromSeed(childKey.privateKey.raw);
}
try {
final privateKeyBytes = base58decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList());
final keypairBytes = Base58Decoder.decode(privateKey!);
return SolanaPrivateKey.fromSeed(keypairBytes);
} catch (_) {
final privateKeyBytes = HEX.decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
return SolanaPrivateKey.fromBytes(privateKeyBytes);
}
}
@ -206,7 +204,8 @@ abstract class SolanaWalletBase
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed);
printV(estimatedFee.toString());
} catch (e) {
estimatedFee = 0.0;
}
@ -274,7 +273,7 @@ abstract class SolanaWalletBase
tokenMint: tokenMint,
tokenTitle: transactionCurrency.title,
inputAmount: totalAmount,
ownerKeypair: _walletKeyPair!,
ownerPrivateKey: _solanaPrivateKey,
tokenDecimals: transactionCurrency.decimals,
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
@ -291,9 +290,7 @@ abstract class SolanaWalletBase
/// Fetches the native SOL transactions linked to the wallet Public Key
Future<void> _updateNativeSOLTransactions() async {
final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
final transactions = await _client.fetchTransactions(address);
final transactions = await _client.fetchTransactions(_solanaPublicKey.toAddress());
await _addTransactionsToTransactionHistory(transactions);
}
@ -308,10 +305,10 @@ abstract class SolanaWalletBase
for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
token.symbol,
token.decimal,
_walletKeyPair!,
mintAddress: token.mintAddress,
splTokenSymbol: token.symbol,
splTokenDecimal: token.decimal,
privateKey: _solanaPrivateKey,
);
// splTokenTransactions.addAll(tokenTxs);
@ -387,6 +384,7 @@ abstract class SolanaWalletBase
'mnemonic': _mnemonic,
'private_key': _hexPrivateKey,
'balance': balance[currency]!.toJSON(),
'passphrase': passphrase,
});
static Future<SolanaWallet> open({
@ -407,15 +405,16 @@ abstract class SolanaWalletBase
if (!hasKeysFile) rethrow;
}
final balance = SolanaBalance.fromJSON(data?['balance'] as String) ?? SolanaBalance(0.0);
final balance = SolanaBalance.fromJSON(data?['balance'] as String?) ?? SolanaBalance(0.0);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final passphrase = data['passphrase'] as String?;
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey);
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
@ -428,6 +427,7 @@ abstract class SolanaWalletBase
return SolanaWallet(
walletInfo: walletInfo,
password: password,
passphrase: keysData.passphrase,
mnemonic: keysData.mnemonic,
privateKey: keysData.privateKey,
initialBalance: balance,
@ -442,7 +442,7 @@ abstract class SolanaWalletBase
}
Future<SolanaBalance> _fetchSOLBalance() async {
final balance = await _client.getBalance(_walletKeyPair!.address);
final balance = await _client.getBalance(solanaAddress);
return SolanaBalance(balance);
}
@ -451,10 +451,9 @@ abstract class SolanaWalletBase
for (var token in splTokensBox.values) {
if (token.enabled) {
try {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} catch (e) {
printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
@ -482,10 +481,9 @@ abstract class SolanaWalletBase
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} else {
@ -507,37 +505,10 @@ abstract class SolanaWalletBase
}
Future<SPLToken?> getSPLToken(String mintAddress) async {
// Convert SPL token mint address to public key
final Ed25519HDPublicKey mintPublicKey;
try {
mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
} catch (_) {
return null;
}
// Fetch token's metadata account
try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
return null;
}
String? iconPath;
try {
iconPath = await _client.getIconImageFromTokenUri(token.uri);
} catch (_) {}
String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), '');
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: filteredTokenSymbol,
mintAddress: mintAddress,
iconPath: iconPath,
);
} catch (e) {
return await _client.fetchSPLTokenInfo(mintAddress);
} catch (e, s) {
printV('Error fetching token: ${e.toString()}, ${s.toString()}');
return null;
}
}
@ -582,7 +553,7 @@ abstract class SolanaWalletBase
final messageBytes = utf8.encode(message);
// Sign the message bytes with the wallet's private key
final signature = (await _walletKeyPair!.sign(messageBytes)).toString();
final signature = (_solanaPrivateKey.sign(messageBytes)).toString();
return HEX.encode(utf8.encode(signature)).toUpperCase();
}
@ -596,7 +567,7 @@ abstract class SolanaWalletBase
final base58EncodedPublicKeyString = match.group(2)!;
final sigBytes = bytesString.split(', ').map(int.parse).toList();
List<int> pubKeyBytes = base58decode(base58EncodedPublicKeyString);
List<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
return [sigBytes, pubKeyBytes];
} else {
@ -619,19 +590,18 @@ abstract class SolanaWalletBase
}
// make sure the address derived from the public key provided matches the one we expect
final pub = Ed25519HDPublicKey(pubKeyBytes);
if (address != pub.toBase58()) {
final pub = SolanaPublicKey.fromBytes(pubKeyBytes);
if (address != pub.toAddress().address) {
return false;
}
return await verifySignature(
return pub.verify(
message: messageBytes,
signature: sigBytes,
publicKey: Ed25519HDPublicKey(pubKeyBytes),
);
}
SolanaClient? get solanaClient => _client.getSolanaClient;
SolanaRPC? get solanaProvider => _client.getSolanaProvider;
@override
String get password => _password;

View file

@ -33,6 +33,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
walletInfo: credentials.walletInfo!,
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -118,6 +119,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);

View file

@ -1,7 +1,6 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
import 'package:solana/metaplex.dart';
part 'spl_token.g.dart';
@ -55,7 +54,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
required String mint,
required String symbol,
required String mintAddress,
String? iconPath
String? iconPath,
}) {
return SPLToken(
name: name,
@ -117,31 +116,3 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
@override
int get hashCode => mintAddress.hashCode;
}
class NFT extends SPLToken {
final ImageInfo? imageInfo;
NFT(
String mint,
String name,
String symbol,
String mintAddress,
int decimal,
String iconPath,
this.imageInfo,
) : super(
name: name,
symbol: symbol,
mintAddress: mintAddress,
decimal: decimal,
mint: mint,
iconPath: iconPath,
);
}
class ImageInfo {
final String uri;
final OffChainMetadata? data;
const ImageInfo(this.uri, this.data);
}

View file

@ -11,7 +11,6 @@ environment:
dependencies:
flutter:
sdk: flutter
solana: ^0.31.0+1
cw_core:
path: ../cw_core
http: ^1.1.0
@ -21,6 +20,14 @@ dependencies:
shared_preferences: ^2.0.15
bip32: ^2.0.0
hex: ^0.2.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
dev_dependencies:
flutter_test:

View file

@ -144,7 +144,7 @@ abstract class TronWalletBase
if (!hasKeysFile) rethrow;
}
final balance = TronBalance.fromJSON(data?['balance'] as String) ?? TronBalance(BigInt.zero);
final balance = TronBalance.fromJSON(data?['balance'] as String?) ?? TronBalance(BigInt.zero);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme

View file

@ -17,7 +17,7 @@ dependencies:
path: ../cw_evm
on_chain:
git:
url: https://github.com/cake-tech/On_chain
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
blockchain_utils:
git:

60
docs/builds/ANDROID.md Normal file
View file

@ -0,0 +1,60 @@
# Building Cake Wallet for Android
## Requirements and Setup
As we use Docker with a custom Dockerfile to build Cake Wallet, the only dependency for building Cake on your local host is the Docker Engine.
You can find the latest instructions for installing Docker on your given OS on the official website:
- <https://docs.docker.com/engine/install/>
NOTE: If building on a Mac with an M-series CPU (arm64), you may encounter segmentation faults when building. If you do, simply retry the build.
## Building Cake Wallet or Monero.com
### Using the pre-built builder image
In order to build the latest version of Cake Wallet, simply run the following:
```bash
git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:main-linux . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:main-linux bash -x << EOF
set -x -e
pushd scripts/android
source ./app_env.sh cakewallet
# source ./app_env.sh monero.com # Uncomment this line to build monero.com
./app_config.sh
./build_monero_all.sh
./build_mwebd.sh --dont-install
popd
pushd android/app
[[ -f key.jks ]] || 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 hunter1 -keypass hunter1
popd
flutter clean
./model_generator.sh
dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=hunter1 keyPassword=hunter1
dart run tool/generate_localization.dart
dart run tool/generate_new_secrets.dart
flutter build apk --release --split-per-abi
EOF
```
You should see the command complete with similar output:
```bash
Running Gradle task 'assembleRelease'... 519.1s
✓ Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (56.3MB)
✓ Built build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (55.8MB)
✓ Built build/app/outputs/flutter-apk/app-x86_64-release.apk (56.4MB)
```
Final builds can be found in `build/app/outputs/flutter-apk/` as seen above.
## Signing builds
While properly signing builds is outside of the scope of this guide (very few users want or need to run their own built APKs), to learn more about how to sign APKs you can check out the Zeus team's fantastic guide:
- <https://github.com/ZeusLN/zeus/blob/master/docs/ReproducibleBuilds.md#signing-apks>

143
docs/builds/IOS.md Normal file
View file

@ -0,0 +1,143 @@
# Building Cake Wallet for iOS
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your iOS device.
```txt
macOS 15.3.1
Xcode 16.2
Flutter 3.24.4
```
NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team.
### 1. Installing dependencies
For installing dependency tools you can use brew [Install brew](https://brew.sh).
You may easily install them on your build system with the following command:
```zsh
brew install automake ccache cmake cocoapods go libtool pkgconfig xz
sudo softwareupdate --install-rosetta --agree-to-license
```
### 2. Installing Xcode
Download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store.
Run the following to properly initialize Xcode:
```zsh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
```
To enable iOS build support for Xcode, perform the following:
1. Open Xcode
2. Navigate to settings
3. Open Components tab
4. Click "Get" next to iOS 18.2 (or any other version that is showing up as default)
### 3. Installing Flutter
Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.24.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust
Install Rust from the [rustup.rs](https://rustup.rs/) website.
```zsh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### 5. Verify Flutter and Xcode installation
Verify that Flutter and Xcode have been correctly installed on your system with the following command:
`flutter doctor`
The output of this command should appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```zsh
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
```
### 6. Acquiring the Cake Wallet source code
Download the latest release tag of Cake Wallet and enter the source code directory:
```zsh
git clone https://github.com/cake-tech/cake_wallet.git --branch main
cd cake_wallet/scripts/ios/
```
NOTE: Replace `main` with the latest release tag available at <https://github.com/cake-tech/cake_wallet/releases/latest>.
### 7. Setup and build Cake Wallet from source
We need to generate project settings like app name, app icon, package name, etc, including what specific variant of the app we want to build.
To build Cake Wallet from source, run the following:
```zsh
source ./app_env.sh cakewallet
```
For Monero.com, instead do:
```zsh
source ./app_env.sh monero.com
```
Build the necessary libraries and their dependencies:
```zsh
./build_monero_all.sh
./build_mwebd.sh
```
NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book!
Then run the configuration script to setup app name, app icon, etc:
```zsh
./app_config.sh
```
### 8. Prepare Flutter
Change back to the root directory of the Cake Wallet source code and install Flutter package dependencies:
```zsh
cd ../../
flutter pub get
```
Generate secrets as placeholders for official API keys etc. along with localization files and mobx models:
```zsh
dart run tool/generate_new_secrets.dart
dart run tool/generate_localization.dart
./model_generator.sh
```
### 9. Build
```zsh
flutter build ios --release --no-codesign
```
Then you can open `ios/Runner.xcworkspace` with Xcode to archive the application.
If you want to run on a connected device, simply run:
```zsh
flutter run
```

96
docs/builds/LINUX.md Normal file
View file

@ -0,0 +1,96 @@
# Building Cake Wallet for Linux
## Requirements and Setup
As we use Docker with a custom Dockerfile to build Cake Wallet, the only dependency for building Cake on your local host is the Docker Engine.
You can find the latest instructions for installing Docker on your given OS on the official website:
- <https://docs.docker.com/engine/install/>
NOTE: If building on a Mac with an M-series CPU (arm64), you may encounter segmentation faults when building. If you do, simply retry the build.
## Building Cake Wallet or Monero.com
### Using the pre-built builder image
In order to build the latest version of Cake Wallet, simply run the following:
```bash
git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:main-linux . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:main-linux bash -x << EOF
set -x -e
pushd scripts
./gen_android_manifest.sh
popd
pushd scripts/linux
source ./app_env.sh cakewallet
# source ./app_env.sh monero.com # Uncomment this line to build monero.com
./app_config.sh
./build_monero_all.sh
popd
flutter clean
./model_generator.sh
dart run tool/generate_localization.dart
dart run tool/generate_new_secrets.dart
flutter build linux
EOF
```
You should see the command complete with similar output:
```bash
+ dart run tool/generate_localization.dart
+ dart run tool/generate_new_secrets.dart
+ flutter build linux
Building Linux application...
✓ Built build/linux/x64/release/bundle/cake_wallet
```
Final builds can be found in `build/linux/x64/release/bundle/` as seen above.
## Flatpak (optional)
To package the built binaries as a flatpak, you need first to install `flatpak` and `flatpak-builder`:
```bash
sudo apt install flatpak flatpak-builder
```
Add the necessary Flathub:
```bash
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
```
Then need to install freedesktop runtime and sdk:
```bash
flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08
```
Next, build the flatpak bundle:
```bash
flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml
```
And then export bundle:
```bash
flatpak build-export export flatpak-build
flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet
```
The Flatpak file, `cake_wallet.flatpak`, should be generated in the current directory.
To install the newly built Flatpak, run:
```bash
flatpak --user install cake_wallet.flatpak
```

135
docs/builds/MACOS.md Normal file
View file

@ -0,0 +1,135 @@
# Building Cake Wallet for macOS
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your macOS device.
```txt
macOS 15.3.1
Xcode 16.2
Flutter 3.24.4
```
### 1. Installing dependencies
For installing dependency tools you can use brew [Install brew](https://brew.sh).
You may easily install them on your build system with the following command:
```zsh
brew install autoconf automake binutils ccache cmake cocoapods go libtool pigz pkg-config
sudo softwareupdate --install-rosetta --agree-to-license
```
### 2. Installing Xcode
Download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store.
Run the following to properly initialize Xcode:
```zsh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
```
### 3. Installing Flutter
Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.24.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust
Install Rust from the [rustup.rs](https://rustup.rs/) website.
```zsh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### 5. Verify Flutter and Xcode installation
Verify that Flutter and Xcode have been correctly installed on your system with the following command:
`flutter doctor`
The output of this command should appear like this, indicating successful installations. If there are problems with your installation of Flutter or Xcode, they **must** be corrected before proceeding.
```zsh
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on macOS 15.x.x)
...
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
...
```
### 6. Acquiring the Cake Wallet source code
Download the latest release tag of Cake Wallet and enter the source code directory:
```zsh
git clone https://github.com/cake-tech/cake_wallet.git --branch main
cd cake_wallet/scripts/macos/
```
NOTE: Replace `main` with the latest release tag available at <https://github.com/cake-tech/cake_wallet/releases/latest>.
### 7. Setup and build Cake Wallet from source
We need to generate project settings like app name, app icon, package name, etc, including what specific variant of the app we want to build.
To build Cake Wallet from source, run the following:
```zsh
source ./app_env.sh cakewallet
```
For Monero.com, instead do:
```zsh
source ./app_env.sh monero.com
```
Build the necessary libraries and their dependencies:
```zsh
./build_monero_all.sh
```
NOTE: This step will take quite a while, so be sure you grab a cup of coffee or a good book!
Then run the configuration script to setup app name, app icon, etc:
```zsh
./app_config.sh
```
### 8. Prepare Flutter
Change back to the root directory of the Cake Wallet source code and install Flutter package dependencies:
```zsh
cd ../../
flutter pub get
```
Generate secrets as placeholders for official API keys etc. along with localization files and mobx models:
```zsh
dart run tool/generate_new_secrets.dart
dart run tool/generate_localization.dart
./model_generator.sh
```
### 9. Build
```zsh
flutter build macos --release
```
Then you can open `macos/Runner.xcworkspace` with Xcode to archive the application.
If you want to run on a connected device, simply run:
```zsh
flutter run
```

92
docs/builds/WINDOWS.md Normal file
View file

@ -0,0 +1,92 @@
# Building Cake Wallet for Windows
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your Windows PC.
```txt
Windows 10 or later (64-bit), x86-64 based
Flutter 3.24.4
```
### 1. Installing Flutter
Install Flutter, specifically version `3.24.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows).
In order for Flutter to function, you'll also need to enable Developer Mode:
Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode.
NOTE: as `3.24.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 2. Install Development Tools
Install Git for Windows and Visual Studio 2022:
1. Follow the [Development Tools](https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) installation instructions
1. NOTE: Be sure to install the `Desktop Development with C++` workload in Visual Studio as outlined in the docs.
2. Add `git` to your path by going to Start Menu > search "environment" > Environment Variables > double-click Path > Add `C:\Program Files\Git\bin\` on a new line.
Lastly, you'll need to install Nuget separately:
1. Download the exe from <https://dist.nuget.org/win-x86-commandline/latest/nuget.exe>
2. Create a new directory, `C:\Program Files\Nuget\`
3. Move or copy the `nuget.exe` binary you just downloaded into the newly created directory above.
4. Add `nuget` to your path by going to Start Menu > search "environment" > Environment Variables > double-click Path > Add `C:\Program Files\Nuget\` on a new line.
### 3. Installing WSL (Windows Subsystem for Linux)
For building Monero dependencies, it is required to install Windows [WSL](https://learn.microsoft.com/en-us/windows/wsl) and required packages for WSL (Ubuntu).
1. Open a Powershell window by going to the Start Menu and searching for "Powershell"
2. Install WSL with the command `wsl --install`
3. Install the necessary Ubuntu dependencies
```powershell
wsl --install
wsl sudo apt update
wsl sudo apt install -y autoconf build-essential ccache cmake curl gcc gcc-mingw-w64-x86-64 git g++ g++-mingw-w64-x86-64 gperf lbzip2 libtool make pkg-config pigz
```
### 4. Installing Rust
Install Rust and other Rust-related dependencies using [rustup.rs](https://rustup.rs/#) by running the following command:
```bash
wsl curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### 5. Acquiring the Cake Wallet source code
Download the latest release tag of Cake Wallet and enter the source code directory:
```powershell
git clone https://github.com/cake-tech/cake_wallet.git --branch main
cd cake_wallet
```
NOTE: Replace `main` with the latest release tag available at <https://github.com/cake-tech/cake_wallet/releases/latest>.
### 6. Build Monero, monero_c, and dependencies
To use Monero in Cake Wallet, you must build the Monero_C wrapper which will be used by monero.dart package.
Run the following in a WSL terminal window (set the Git username and email as desired):
```powershell
wsl
git config --global user.email "builds@cakewallet.com"
git config --global user.name "builds"
./build_all.sh
```
### 7. Configure and build Cake Wallet application
To configure the application, run the following:
```powershell
exit
.\cakewallet.bat
```
After running the script above, you should get `Cake Wallet.zip` in the project's root directory which will contain `CakeWallet.exe` and other needed files for running the application. Now you can extract files from `Cake Wallet.zip` archive and run the application.

View file

@ -1,149 +0,0 @@
# Building Cake Wallet for Android
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your Android device.
```
Ubuntu >= 20.04
Android SDK 29 or higher (better to have the latest one 33)
Android NDK 17c
Flutter 3.24.4
```
### 1. Installing Package Dependencies
CakeWallet cannot be built without the following packages installed on your system.
- curl
- unzip
- automake
- build-essential
- file
- pkg-config
- git
- python
- libtool
- libtinfo5
- cmake
- openjdk-8-jre-headless
- clang
You may easily install them on your build system with the following command:
`$ sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless clang`
### 2. Installing Android Studio and Android toolchain
You may download and install the latest version of Android Studio [here](https://developer.android.com/studio#downloads). After installing, start Android Studio, and go through the "Setup Wizard." This installs the latest Android SDK, Android SDK Command-line Tools, and Android SDK Build-Tools, which are required by Cake Wallet. **Be sure you are installing SDK version 28 or later when stepping through the wizard**
### 3. Installing Flutter
Install Flutter with version `3.24.4`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually).
### 4. Installing rustup
Install rustup from the [rustup.rs](https://rustup.rs/) website.
### 5. Verify Installations
Verify that the Android toolchain, Flutter, and Android Studio have been correctly installed on your system with the following command:
`$ flutter doctor`
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher)
[✓] Android Studio (version 4.0 or higher)
```
### 6. Generate a secure keystore for Android
`$ keytool -genkey -v -keystore $HOME/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key`
You will be prompted to create two passwords. First you will be prompted for the "store password", followed by a "key password" towards the end of the creation process. **TAKE NOTE OF THESE PASSWORDS!** You will need them in later steps.
### 7. Acquiring the Cake Wallet Source Code
Create the directory that will be use to store the Cake Wallet source...
```
$ sudo mkdir -p /opt/android
$ sudo chown $USER /opt/android
$ cd /opt/android
```
..and download the source code into that directory.
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main`
Proceed into the source code before proceeding with the next steps:
`$ cd cake_wallet/scripts/android/`
### 8. Installing Android NDK
`$ ./install_ndk.sh`
### 9. Execute Build & Setup Commands for Cak eWallet
We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files.
Please pick what app you want to build: cakewallet or monero.com.
`$ source ./app_env.sh <cakewallet OR monero.com>`
(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`)
Then run configuration script for setup app name, app icon and etc:
`$ ./app_config.sh`
Build the Monero libraries and their dependencies:
`$ ./build_all.sh`
It is now time to change back to the base directory of the Cake Wallet source code:
`$ cd ../../`
Install Flutter package dependencies with this command:
`$ flutter pub get`
Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command:
`$ dart run tool/generate_new_secrets.dart`
Next, we must generate key properties based on the secure keystore you generated for Android (in step 5). **MODIFY THE FOLLOWING COMMAND** with the "store password" and "key password" you assigned when creating your keystore (in step 5).
`$ dart run tool/generate_android_key_properties.dart keyAlias=key storeFile=$HOME/key.jks storePassword=<store password> keyPassword=<key password>`
**REMINDER:** The *above* command will **not** succeed unless you replaced the `storePassword` and `keyPassword` variables with the correct passwords for your keystore.
Then we need to generate localization files.
`$ dart run tool/generate_localization.dart`
Finally build mobx models for the app:
`$ ./model_generator.sh`
### 10. Build!
`$ flutter build apk --release`
Copyright (c) 2024 Cake Labs LLC

View file

@ -1,101 +0,0 @@
# Building Cake Wallet for iOS
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your iOS device.
```
macOS >= 14.0
Xcode 15.3
Flutter 3.24.4
```
### 1. Installing Package Dependencies
Cake Wallet cannot be built without the following packages installed on your build system.
For installing dependency tools you can use brew [Install brew](https://brew.sh).
You may easily install them on your build system with the following command:
`$ brew install cmake xz cocoapods`
### 2. Installing Xcode
You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store.
### 3. Installing Flutter
Need to install flutter with version `3.24.4`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/mobile-ios?tab=download).
### 4. Installing rustup
Install rustup from the [rustup.rs](https://rustup.rs/) website.
### 5. Verify Installations
Verify that the Flutter and Xcode have been correctly installed on your system with the following command:
`$ flutter doctor`
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
```
### 6. Acquiring the CakeWallet source code
Download the source code.
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main`
Proceed into the source code before proceeding with the next steps:
`$ cd cake_wallet/scripts/ios/`
### 7. Execute Build & Setup Commands for Cake Wallet
We need to generate project settings like app name, app icon, package name, etc. For this, we need to setup environment variables and configure project files.
Please pick what app you want to build: cakewallet or monero.com.
`$ source ./app_env.sh <cakewallet OR monero.com>`
(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`)
Then run configuration script for setup app name, app icon and etc:
`$ ./app_config.sh`
Build the Monero libraries and their dependencies:
`$ ./build_monero_all.sh`
It is now time to change back to the base directory of the Cake Wallet source code:
`$ cd ../../`
Install Flutter package dependencies with this command:
`$ flutter pub get`
Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command:
`$ dart run tool/generate_new_secrets.dart`
Then we need to generate localization files and mobx models.
`$ ./configure_cake_wallet.sh ios`
### 8. Build!
`$ flutter build ios --release`
Then you can open `ios/Runner.xcworkspace` with Xcode and you can archive the application.
Or if you want to run to connected device:
`$ flutter run --release`
Copyright (c) 2024 Cake Labs LLC

View file

@ -1,112 +0,0 @@
# Building Cake Wallet for macOS
## Requirements and Setup
The following are the system requirements to build Cake Wallet for your macOS device.
```
macOS >= 14.0
Xcode 15.3
Flutter 3.24.4
```
### 1. Installing Package Dependencies
Cake Wallet cannot be built without the following packages installed on your build system.
For installing dependency tools you can use brew [Install brew](https://brew.sh).
You may easily install them on your build system with the following command:
`$ brew install cmake xz automake autoconf libtool boost@1.76 zmq cocoapods`
`$ brew link boost@1.76`
### 2. Installing Xcode
You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store.
### 3. Installing Flutter
Need to install flutter with version `3.24.4`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
### 4. Installing rustup
Install rustup from the [rustup.rs](https://rustup.rs/) website.
### 5. Verify Installations
Verify that Flutter and Xcode have been correctly installed on your system with the following command:
`$ flutter doctor`
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.4, on macOS 14.x.x)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
```
### 6. Acquiring the Cake Wallet source code
Download the source code.
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main`
Proceed into the source code before proceeding with the next steps:
`$ cd cake_wallet/scripts/macos/`
### 7. Execute Build & Setup Commands for Cake Wallet
We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files.
Please pick what app you want to build: cakewallet or monero.com.
`$ source ./app_env.sh <cakewallet OR monero.com>`
(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`)
Then run configuration script for setup app name, app icon and etc:
`$ ./app_config.sh`
Build the Monero libraries and their dependencies:
`$ ./build_monero_all.sh`
If you be needed to build universal monero lib, then it will require additional steps. Steps for build universal monero lib on mac with Apple Silicon (arm64):
- Need to install Rosetta: `$ softwareupdate --install-rosetta`
- Need to install [Brew](https://brew.sh/) with rosetta: `$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` (or take another way to install brew, but be use that you have installed it into /usr/local as it's using for x86_64 macs)
- Install dependencies for build monero wallet lib for x86_64 with brew: `$ arch -x86_64 /usr/local/bin/brew install automake autoconf libtool openssl boost@1.76 zmq` and link installed boost@1.76 for x86_64 `$ arch -x86_64 /usr/local/bin/brew link boost@1.76`
- Run building script with additional argument: `$ ./build_monero_all.sh universal`
If you will be needed to build monero wallet lib only for x86_64 on arm64 mac, then you need use steps above, but run build script with rosetta without arguments: `$ arch -x86_64 ./build_monero_all.sh`.
It is now time to change back to the base directory of the Cake Wallet source code:
`$ cd ../../`
Install Flutter package dependencies with this command:
`$ flutter pub get`
Your Cake Wallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command:
`$ dart run tool/generate_new_secrets.dart`
Then we need to generate localization files and mobx models.
`$ ./configure_cake_wallet.sh macos`
### 8. Build!
`$ flutter build macos --release`
Then you can open `macos/Runner.xcworkspace` with Xcode and you can to archive the application.
Or if you want to run to connected device:
`$ flutter run --release`
Copyright (c) 2024 Cake Labs LLC

View file

@ -1,57 +0,0 @@
# Building Cake Wallet for Windows
## Requirements and Setup
The following are the system requirements to build CakeWallet for your Windows PC.
```
Windows 10 or later (64-bit), x86-64 based
Flutter 3.24.4
```
### 1. Installing Flutter
Install Flutter with version `3.24.4`. Follow the Flutter [installation guide](https://docs.flutter.dev/get-started/install/windows).
### 2. Install Development Tools
Install Git for Windows and Visual Studio 2022. Follow the [Development Tools](https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) installation instructions.
Then install `Desktop development with C++` packages via Visual Studio 2022, or Visual Studio Build Tools 2022 including:
- `C++ Build Tools core features`
- `C++ 2022 Redistributable Update`
- `C++ core desktop features`
- `MVC v143 - VS 2022 C++ x64/x86 build tools`
- `C++ CMake tools for Windows`
- `Testing tools core features - Build Tools`
- `C++ AddressSanitizer`.
### 3. Installing rustup
Install rustup from the [rustup.rs](https://rustup.rs/#) website. Download and run the 64-bit rustup-init.exe
### 4. Installing WSL (Windows Subsystem for Linux)
For building monero dependencies, it is required to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu):
`$ sudo apt update `
`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config`
### 5. Pull Cake Wallet source code
You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git:
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart`
OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip)
### 6. Build Monero, monero_c and their dependencies
To use Monero in Cake Wallet, you must build the Monero_C wrapper which will be used by monero.dart package.
For that you need to run the shell (bash - typically same named utility should be available after WSL is enabled in your system) with the previously installed WSL install, then change current directory to the application project directory with your shell then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`.
### 7. Configure and build Cake Wallet application
To configure the application, open the directory where you have downloaded or unarchived Cake Wallet sources and run `cakewallet.bat`.
Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL.
After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contain `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application.
Copyright (c) 2024 Cake Labs LLC.

View file

@ -84,7 +84,7 @@ void main() {
exchangeTradePageRobot.hasInformationDialog();
await exchangeTradePageRobot.onGotItButtonPressed();
await exchangeTradePageRobot.onConfirmSendingButtonPressed();
await exchangeTradePageRobot.onSendFromCakeButtonPressed();
await exchangeTradePageRobot.handleConfirmSendResult();

View file

@ -27,11 +27,11 @@ class ExchangeTradePageRobot {
await commonTestCases.defaultSleepTime();
}
Future<void> onConfirmSendingButtonPressed() async {
tester.printToConsole('Now confirming sending');
Future<void> onSendFromCakeButtonPressed() async {
tester.printToConsole('Now sending from cake');
await commonTestCases.tapItemByKey(
'exchange_trade_page_confirm_sending_button_key',
'exchange_trade_page_send_from_cake_button_key',
shouldPumpAndSettle: false,
);
@ -134,7 +134,7 @@ class ExchangeTradePageRobot {
await commonTestCases.defaultSleepTime();
await onConfirmSendingButtonPressed();
await onSendFromCakeButtonPressed();
tester.printToConsole('Confirm sending button tapped');
hasError = await hasErrorWhileSending();

View file

@ -51,7 +51,7 @@ class SendPageRobot {
commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key');
}
if (sendViewModel.hasFees) {
if (sendViewModel.feesViewModel.hasFees) {
commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
}
@ -104,12 +104,12 @@ class SendPageRobot {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
if (!sendViewModel.hasFees || priority == null) return;
if (!sendViewModel.feesViewModel.hasFees || priority == null) return;
final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
if (priority == sendViewModel.transactionPriority) {
if (priority == sendViewModel.feesViewModel.transactionPriority) {
await commonTestCases
.tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
return;
@ -188,8 +188,8 @@ class SendPageRobot {
await authPageRobot.enterPinCode(CommonTestConstants.pin);
}
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
}
}

View file

@ -3,38 +3,8 @@ PODS:
- Flutter
- ReachabilitySwift
- CryptoSwift (1.8.3)
- cw_haven (0.0.1):
- cw_haven/Boost (= 0.0.1)
- cw_haven/Haven (= 0.0.1)
- cw_haven/OpenSSL (= 0.0.1)
- cw_haven/Sodium (= 0.0.1)
- cw_shared_external
- Flutter
- cw_haven/Boost (0.0.1):
- cw_shared_external
- Flutter
- cw_haven/Haven (0.0.1):
- cw_shared_external
- Flutter
- cw_haven/OpenSSL (0.0.1):
- cw_shared_external
- Flutter
- cw_haven/Sodium (0.0.1):
- cw_shared_external
- Flutter
- cw_mweb (0.0.1):
- Flutter
- cw_shared_external (0.0.1):
- cw_shared_external/Boost (= 0.0.1)
- cw_shared_external/OpenSSL (= 0.0.1)
- cw_shared_external/Sodium (= 0.0.1)
- Flutter
- cw_shared_external/Boost (0.0.1):
- Flutter
- cw_shared_external/OpenSSL (0.0.1):
- Flutter
- cw_shared_external/Sodium (0.0.1):
- Flutter
- device_display_brightness (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@ -136,9 +106,7 @@ PODS:
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- CryptoSwift
- cw_haven (from `.symlinks/plugins/cw_haven/ios`)
- cw_mweb (from `.symlinks/plugins/cw_mweb/ios`)
- cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`)
- device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
@ -179,12 +147,8 @@ SPEC REPOS:
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cw_haven:
:path: ".symlinks/plugins/cw_haven/ios"
cw_mweb:
:path: ".symlinks/plugins/cw_mweb/ios"
cw_shared_external:
:path: ".symlinks/plugins/cw_shared_external/ios"
device_display_brightness:
:path: ".symlinks/plugins/device_display_brightness/ios"
device_info_plus:
@ -239,9 +203,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a
cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3
cw_shared_external: 2972d872b8917603478117c9957dfca611845a92
device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926

View file

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
@ -8,9 +10,9 @@ import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_w
import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cw_core/solana_rpc_http_service.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:solana/base58.dart';
import 'package:solana/solana.dart';
import 'package:on_chain/solana/solana.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import '../chain_service.dart';
import '../../wallet_connect_key_service.dart';
@ -27,25 +29,19 @@ class SolanaChainServiceImpl implements ChainService {
final SolanaChainId reference;
final SolanaClient solanaClient;
final SolanaRPC solanaProvider;
final Ed25519HDKeyPair? ownerKeyPair;
final SolanaPrivateKey? ownerPrivateKey;
SolanaChainServiceImpl({
required this.reference,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
required this.ownerKeyPair,
required String webSocketUrl,
required Uri rpcUrl,
SolanaClient? solanaClient,
}) : solanaClient = solanaClient ??
SolanaClient(
rpcUrl: rpcUrl,
websocketUrl: Uri.parse(webSocketUrl),
timeout: const Duration(minutes: 5),
) {
required this.ownerPrivateKey,
required String formattedRPCUrl,
SolanaRPC? solanaProvider,
}) : solanaProvider = solanaProvider ?? SolanaRPC(SolanaRPCHTTPService(url: formattedRPCUrl)) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
}
@ -110,26 +106,20 @@ class SolanaChainServiceImpl implements ChainService {
}
try {
final message =
await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction);
// Convert transaction string to bytes
List<int> transactionBytes = base64Decode(solanaSignTx.transaction);
final sign = await ownerKeyPair?.signMessage(
message: message,
recentBlockhash: solanaSignTx.recentBlockhash ?? '',
final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
final sign = ownerPrivateKey!.sign(message.serialize());
final signature = solanaProvider.request(
SolanaRPCSendTransaction(
encodedTransaction: Base58Encoder.encode(sign),
commitment: Commitment.confirmed,
),
);
if (sign == null) {
return '';
}
String signature = await solanaClient.sendAndConfirmTransaction(
message: message,
signers: [ownerKeyPair!],
commitment: Commitment.confirmed,
);
printV(signature);
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
@ -161,10 +151,10 @@ class SolanaChainServiceImpl implements ChainService {
if (authError != null) {
return authError;
}
Signature? sign;
List<int>? sign;
try {
sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message));
sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
} catch (e) {
printV(e);
}
@ -173,7 +163,7 @@ class SolanaChainServiceImpl implements ChainService {
return '';
}
String signature = sign.toBase58();
final signature = Base58Encoder.encode(sign);
return signature;
}

View file

@ -22,6 +22,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:on_chain/solana/solana.dart' hide Store;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
@ -140,29 +141,28 @@ abstract class Web3WalletServiceBase with Store {
for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
Uri rpcUri = node.uri;
String webSocketUrl = 'wss://${node.uriRaw}';
String formattedUrl;
String protocolUsed = node.isSSL ? "https" : "http";
if (node.uriRaw == 'rpc.ankr.com') {
String ankrApiKey = secrets.ankrApiKey;
rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
} else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
String chainStackApiKey = secrets.chainStackApiKey;
rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey');
webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey';
formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
} else {
formattedUrl = '$protocolUsed://${node.uriRaw}';
}
SolanaChainServiceImpl(
reference: cId,
rpcUrl: rpcUri,
webSocketUrl: webSocketUrl,
formattedRPCUrl: formattedUrl,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!),
ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)),
);
}
}

View file

@ -33,10 +33,16 @@ import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.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/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
@ -48,6 +54,7 @@ import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
@ -748,8 +755,9 @@ Future<void> setup({
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null,
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any,
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb,
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
getIt.get<FeesViewModel>(),
),
);
@ -897,7 +905,10 @@ Future<void> setup({
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
});
getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get<SettingsStore>()));
getIt.registerFactory(() => TrocadorExchangeProvider());
getIt.registerFactory(() => TrocadorProvidersViewModel(
getIt.get<SettingsStore>(), getIt.get<TrocadorExchangeProvider>()));
getIt.registerFactory(() {
return OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
@ -1026,20 +1037,35 @@ Future<void> setup({
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
getIt.registerFactory(() => ExchangeViewModel(
getIt.registerFactory(
() => ExchangeViewModel(
getIt.get<AppStore>(),
_tradesSource,
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(),
getIt.get<ContactListViewModel>()));
getIt.get<ContactListViewModel>(),
getIt.get<FeesViewModel>(),
),
);
getIt.registerFactory(() => ExchangeTradeViewModel(
getIt.registerFactory<FeesViewModel>(
() => FeesViewModel(
getIt.get<AppStore>(),
getIt.get<BalanceViewModel>(),
),
);
getIt.registerFactory(
() => ExchangeTradeViewModel(
wallet: getIt.get<AppStore>().wallet!,
trades: _tradesSource,
tradesStore: getIt.get<TradesStore>(),
sendViewModel: getIt.get<SendViewModel>()));
sendViewModel: getIt.get<SendViewModel>(),
feesViewModel: getIt.get<FeesViewModel>(),
),
);
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) {
@ -1051,6 +1077,9 @@ Future<void> setup({
getIt.registerFactory(
() => ExchangeTradePage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(
() => ExchangeTradeExternalSendPage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>()));
getIt.registerFactoryParam<WalletService, WalletType, void>((WalletType param1, __) {
@ -1260,7 +1289,8 @@ Future<void> setup({
getIt.registerFactoryParam<OrderDetailsPage, Order, void>(
(Order order, _) => OrderDetailsPage(getIt.get<OrderDetailsViewModel>(param1: order)));
getIt.registerFactory(() => SupportViewModel(getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
SupportViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>()));
getIt.registerFactory(() => SupportPage(getIt.get<SupportViewModel>()));

View file

@ -86,6 +86,7 @@ class PreferencesKey {
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection';
static const trocadorProviderStatesKey = 'trocador_provider_states';
static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status';
static const moneroSeedType = 'monero_seed_type';
static const bitcoinSeedType = 'bitcoin_seed_type';

View file

@ -0,0 +1,130 @@
class SolanaNFTAssetModel {
String? address;
String? mint;
String? standard;
String? name;
String? symbol;
String? description;
String? imageOriginalUrl;
String? externalUrl;
String? metadataOriginalUrl;
String? totalSupply;
Metaplex? metaplex;
Collection? collection;
Contract? contract;
SolanaNFTAssetModel({
this.address,
this.mint,
this.standard,
this.name,
this.symbol,
this.description,
this.imageOriginalUrl,
this.externalUrl,
this.metadataOriginalUrl,
this.totalSupply,
this.metaplex,
this.collection,
this.contract,
});
factory SolanaNFTAssetModel.fromJson(Map<String, dynamic> json) {
return SolanaNFTAssetModel(
address: json['address'] as String?,
mint: json['mint'] as String?,
standard: json['standard'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metadataOriginalUrl: json['metadataOriginalUrl'] as String?,
totalSupply: json['totalSupply'] as String?,
metaplex: json['metaplex'] != null
? Metaplex.fromJson(json['metaplex'] as Map<String, dynamic>)
: null,
collection: json['collection'] != null
? Collection.fromJson(json['collection'] as Map<String, dynamic>)
: null,
contract: json['contract'] != null
? Contract.fromJson(json['contract'] as Map<String, dynamic>)
: null,
);
}
}
class Metaplex {
String? metadataUri;
String? updateAuthority;
int? sellerFeeBasisPoints;
int? primarySaleHappened;
bool? isMutable;
bool? masterEdition;
Metaplex(
{this.metadataUri,
this.updateAuthority,
this.sellerFeeBasisPoints,
this.primarySaleHappened,
this.isMutable,
this.masterEdition});
factory Metaplex.fromJson(Map<String, dynamic> json) {
return Metaplex(
metadataUri: json['metadataUri'] as String?,
updateAuthority: json['updateAuthority'] as String?,
sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?,
primarySaleHappened: json['primarySaleHappened'] as int?,
isMutable: json['isMutable'] as bool?,
masterEdition: json['masterEdition'] as bool?,
);
}
}
class Collection {
String? collectionAddress;
String? name;
String? description;
String? imageOriginalUrl;
String? externalUrl;
String? metaplexMint;
int? sellerFeeBasisPoints;
Collection(
{this.collectionAddress,
this.name,
this.description,
this.imageOriginalUrl,
this.externalUrl,
this.metaplexMint,
this.sellerFeeBasisPoints});
factory Collection.fromJson(Map<String, dynamic> json) {
return Collection(
collectionAddress: json['collectionAddress'] as String?,
name: json['name'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metaplexMint: json['metaplexMint'] as String?,
sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?,
);
}
}
class Contract {
String? type;
String? name;
String? symbol;
Contract({this.type, this.name, this.symbol});
factory Contract.fromJson(Map<String, dynamic> json) {
return Contract(
type: json['type'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
);
}
}

View file

@ -34,6 +34,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
static const chainflip =
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
static const xoSwap =
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
static ExchangeProviderDescription deserialize({required int raw}) {
switch (raw) {
@ -63,6 +65,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return stealthEx;
case 12:
return chainflip;
case 13:
return xoSwap;
default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
}

View file

@ -19,7 +19,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
super(pairList: supportedPairs(_notSupported));
bool useTorOnly;
final Map<String, bool> providerStates;
Map<String, bool> providerStates;
static const List<String> availableProviders = [
'Swapter',
@ -59,6 +59,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
static const createTradePath = '/new_trade';
static const tradePath = '/trade';
static const coinPath = '/coin';
static const providersListPath = '/exchanges';
String _lastUsedRateId;
List<dynamic> _provider;
@ -147,7 +149,14 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final rateId = responseJSON['trade_id'] as String? ?? '';
var quotes = responseJSON['quotes']['quotes'] as List;
_provider = quotes.map((quote) => quote['provider']).toList();
_provider = quotes
.where((quote) => providerStates[quote['provider']] != false)
.map((quote) => quote['provider'])
.toList();
if (_provider.isEmpty) {
throw Exception('No enabled providers found for the selected trade.');
}
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
@ -190,20 +199,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId;
}
String firstAvailableProvider = '';
for (var provider in _provider) {
if (providerStates.containsKey(provider) && providerStates[provider] == true) {
firstAvailableProvider = provider as String;
break;
}
}
if (firstAvailableProvider.isEmpty) {
if (_provider.isEmpty) {
throw Exception('No available provider is enabled');
}
params['provider'] = firstAvailableProvider;
params['provider'] = _provider.first as String;
final uri = await _getUri(createTradePath, params);
final response = await get(uri, headers: {'API-Key': apiKey});
@ -288,6 +288,24 @@ class TrocadorExchangeProvider extends ExchangeProvider {
});
}
Future<List<TrocadorPartners>> fetchProviders() async {
final uri = await _getUri(providersListPath, {'api_key': apiKey});
final response = await get(uri);
if (response.statusCode != 200)
throw Exception('Unexpected http status: ${response.statusCode}');
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final providersJsonList = responseJSON['list'] as List<dynamic>;
final filteredProvidersList = providersJsonList
.map((providerJson) => TrocadorPartners.fromJson(providerJson as Map<String, dynamic>))
.where((provider) => provider.rating != 'D')
.toList();
filteredProvidersList.sort((a, b) => a.rating.compareTo(b.rating));
return filteredProvidersList;
}
String _networkFor(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.eth:
@ -343,3 +361,29 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
}
}
class TrocadorPartners {
final String name;
final String rating;
final double? insurance;
final bool? enabledMarkup;
final double? eta;
TrocadorPartners({
required this.name,
required this.rating,
required this.insurance,
required this.enabledMarkup,
required this.eta,
});
factory TrocadorPartners.fromJson(Map<String, dynamic> json) {
return TrocadorPartners(
name: json['name'] as String? ?? '',
rating: json['rating'] as String? ?? 'N/A',
insurance: json['insurance'] as double?,
enabledMarkup: json['enabledmarkup'] as bool?,
eta: json['eta'] as double?,
);
}
}

View file

@ -0,0 +1,309 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
class XOSwapExchangeProvider extends ExchangeProvider {
XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
static const List<CryptoCurrency> _notSupported = [];
static const _apiAuthority = 'exchange.exodus.io';
static const _apiPath = '/v3';
static const _pairsPath = '/pairs';
static const _ratePath = '/rates';
static const _orders = '/orders';
static const _assets = '/assets';
static const _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'};
final _networks = <String, String>{
'POL': 'matic',
'ETH': 'ethereum',
'BTC': 'bitcoin',
'BSC': 'bsc',
'SOL': 'solana',
'TRX': 'tronmainnet',
'ZEC': 'zcash',
'ADA': 'cardano',
'DOGE': 'dogecoin',
'XMR': 'monero',
'BCH': 'bcash',
'BSV': 'bitcoinsv',
'XRP': 'ripple',
'LTC': 'litecoin',
'EOS': 'eosio',
'XLM': 'stellar',
};
@override
String get title => 'XOSwap';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => true;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.xoSwap;
@override
Future<bool> checkIsAvailable() async => true;
Future<String?> _getAssets(CryptoCurrency currency) async {
if (currency.tag == null) return currency.title;
try {
final normalizedNetwork = _networks[currency.tag];
if (normalizedNetwork == null) return null;
final uri = Uri.https(_apiAuthority, _apiPath + _assets,
{'networks': normalizedNetwork, 'query': currency.title});
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}');
}
final assets = json.decode(response.body) as List<dynamic>;
final asset = assets.firstWhere(
(asset) {
final assetSymbol = (asset['symbol'] as String).toUpperCase();
return assetSymbol == currency.title.toUpperCase();
},
orElse: () => null,
);
return asset != null ? asset['id'] as String : null;
} catch (e) {
printV(e.toString());
return null;
}
}
Future<List<dynamic>> getRatesForPair({
required CryptoCurrency from,
required CryptoCurrency to,
}) async {
try {
final curFrom = await _getAssets(from);
final curTo = await _getAssets(to);
if (curFrom == null || curTo == null) return [];
final pairId = curFrom + '_' + curTo;
final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath');
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) return [];
return json.decode(response.body) as List<dynamic>;
} catch (e) {
printV(e.toString());
return [];
}
}
Future<Limits> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
}) async {
final rates = await getRatesForPair(from: from, to: to);
if (rates.isEmpty) return Limits(min: 0, max: 0);
double minLimit = double.infinity;
double maxLimit = 0;
for (var rate in rates) {
final double currentMin = double.parse(rate['min']['value'].toString());
final double currentMax = double.parse(rate['max']['value'].toString());
if (currentMin < minLimit) minLimit = currentMin;
if (currentMax > maxLimit) maxLimit = currentMax;
}
return Limits(min: minLimit, max: maxLimit);
}
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount,
}) async {
try {
final rates = await getRatesForPair(from: from, to: to);
if (rates.isEmpty) return 0;
if (!isFixedRateMode) {
double bestOutput = 0.0;
for (var rate in rates) {
final double minVal = double.parse(rate['min']['value'].toString());
final double maxVal = double.parse(rate['max']['value'].toString());
if (amount >= minVal && amount <= maxVal) {
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
final double minerFee = double.parse(rate['minerFee']['value'].toString());
final double outputAmount = (amount * rateMultiplier) - minerFee;
if (outputAmount > bestOutput) {
bestOutput = outputAmount;
}
}
}
return bestOutput > 0 ? (bestOutput / amount) : 0;
} else {
double bestInput = double.infinity;
for (var rate in rates) {
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
final double minerFee = double.parse(rate['minerFee']['value'].toString());
final double minVal = double.parse(rate['min']['value'].toString());
final double maxVal = double.parse(rate['max']['value'].toString());
final double requiredSend = (amount + minerFee) / rateMultiplier;
if (requiredSend >= minVal && requiredSend <= maxVal) {
if (requiredSend < bestInput) {
bestInput = requiredSend;
}
}
}
return bestInput < double.infinity ? amount / bestInput : 0;
}
} catch (e) {
printV(e.toString());
return 0;
}
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
try {
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders');
final payload = {
'fromAmount': request.fromAmount,
'fromAddress': request.refundAddress,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'pairId': '${request.fromCurrency.title}_${request.toCurrency.title}',
};
final response = await http.post(uri, headers: _headers, body: json.encode(payload));
if (response.statusCode != 201) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] ?? 'Unknown error';
final message = responseJSON['message'] ?? '';
throw Exception('$error\n$message');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
final orderId = responseJSON['id'] as String;
final from = request.fromCurrency;
final to = request.toCurrency;
final payoutAddress = responseJSON['toAddress'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final refundAddress = responseJSON['fromAddress'] as String;
final depositAmount = _toDouble(amount['value']);
final receiveAmount = toAmount['value'] as String;
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['createdAt'] as String;
final extraId = responseJSON['payInAddressTag'] as String?;
final createdAt = DateTime.parse(createdAtString).toLocal();
return Trade(
id: orderId,
from: from,
to: to,
provider: description,
inputAddress: depositAddress,
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount.toString(),
receiveAmount: receiveAmount.toString(),
payoutAddress: payoutAddress,
extraId: extraId,
);
} catch (e) {
printV(e.toString());
throw TradeNotCreatedException(description);
}
}
@override
Future<Trade> findTradeById({required String id}) async {
try {
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id');
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') {
throw Exception('Trade not found');
}
final error = responseJSON['error'] ?? 'Unknown error';
final message = responseJSON['message'] ?? responseJSON['details'] ?? '';
throw Exception('$error\n$message');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final pairId = responseJSON['pairId'] as String;
final pairParts = pairId.split('_');
final CryptoCurrency fromCurrency =
CryptoCurrency.fromString(pairParts.isNotEmpty ? pairParts[0] : "");
final CryptoCurrency toCurrency =
CryptoCurrency.fromString(pairParts.length > 1 ? pairParts[1] : "");
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
final orderId = responseJSON['id'] as String;
final depositAmount = amount['value'] as String;
final receiveAmount = toAmount['value'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final payoutAddress = responseJSON['toAddress'] as String;
final refundAddress = responseJSON['fromAddress'] as String;
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['createdAt'] as String;
final createdAt = DateTime.parse(createdAtString).toLocal();
final extraId = responseJSON['payInAddressTag'] as String?;
return Trade(
id: orderId,
from: fromCurrency,
to: toCurrency,
provider: description,
inputAddress: depositAddress,
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount,
receiveAmount: receiveAmount,
payoutAddress: payoutAddress,
extraId: extraId,
);
} catch (e) {
printV(e.toString());
throw TradeNotCreatedException(description);
}
}
double _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else {
return 0.0;
}
}
}

View file

@ -100,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'waiting':
return waiting;
case 'processing':
case 'inProgress':
return processing;
case 'waitingPayment':
return waitingPayment;

View file

@ -12,6 +12,17 @@ bool isEVMCompatibleChain(WalletType walletType) {
}
}
bool isNFTACtivatedChain(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:
case WalletType.ethereum:
case WalletType.solana:
return true;
default:
return false;
}
}
bool isWalletConnectCompatibleChain(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:
@ -55,7 +66,7 @@ String getChainNameBasedOnWalletType(WalletType walletType) {
case WalletType.polygon:
return 'polygon';
case WalletType.solana:
return 'solana';
return 'mainnet';
default:
return '';
}

View file

@ -40,6 +40,7 @@ import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
import 'package:cake_wallet/src/screens/faq/faq_page.dart';
import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart';
@ -771,7 +772,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.nftDetailsPage:
return MaterialPageRoute<void>(
builder: (_) => NFTDetailsPage(
nftAsset: settings.arguments as NFTAssetModel,
arguments: settings.arguments as NFTDetailsPageArguments,
dashboardViewModel: getIt.get<DashboardViewModel>(),
),
);
@ -813,6 +814,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<SeedVerificationPage>(),
);
case Routes.exchangeTradeExternalSendPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ExchangeTradeExternalSendPage>(),);
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(

View file

@ -118,4 +118,5 @@ class Routes {
static const walletGroupDescription = '/wallet_group_description';
static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page';
static const walletSeedVerificationPage = '/wallet_seed_verification_page';
static const exchangeTradeExternalSendPage = '/exchange_trade_external_send_page';
}

View file

@ -52,11 +52,8 @@ class CWSolana extends Solana {
String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
@override
String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58();
@override
Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair;
String getPublicKey(WalletBase wallet) =>
(wallet as SolanaWallet).solanaPublicKey.toAddress().address;
Object createSolanaTransactionCredentials(
List<Output> outputs, {
required CryptoCurrency currency,

View file

@ -187,7 +187,6 @@ class _DashboardPageView extends BasePage {
int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0;
ObservableList<Widget> pages = ObservableList<Widget>();
bool _isEffectsInstalled = false;
StreamSubscription<bool>? _onInactiveSub;
@override
Widget body(BuildContext context) {
@ -275,7 +274,7 @@ class _DashboardPageView extends BasePage {
}
void _setEffects(BuildContext context) async {
if (_isEffectsInstalled) {
if (_isEffectsInstalled || !context.mounted) {
return;
}
if (dashboardViewModel.shouldShowMarketPlaceInDashboard) {
@ -305,11 +304,9 @@ class _DashboardPageView extends BasePage {
_showHavenPopup(context);
var needToPresentYat = false;
var isInactive = false;
_onInactiveSub = rootKey.currentState?.isInactive.listen(
rootKey.currentState?.isInactive.listen(
(inactive) {
isInactive = inactive;
if (needToPresentYat) {
Future<void>.delayed(Duration(milliseconds: 500)).then(

View file

@ -23,13 +23,13 @@ class BalancePage extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) {
final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type);
final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type);
return DefaultTabController(
key: ValueKey<bool>(isEVMCompatible),
length: isEVMCompatible ? 2 : 1,
key: ValueKey<bool>(isNFTActivated),
length: isNFTActivated ? 2 : 1,
child: Column(
children: [
if (isEVMCompatible)
if (isNFTActivated)
Align(
alignment: Alignment.centerLeft,
child: Padding(
@ -76,7 +76,7 @@ class BalancePage extends StatelessWidget {
physics: NeverScrollableScrollPhysics(),
children: [
CryptoBalanceWidget(dashboardViewModel: dashboardViewModel),
if (isEVMCompatible) NFTListingPage(nftViewModel: nftViewModel)
if (isNFTActivated) NFTListingPage(nftViewModel: nftViewModel)
],
),
),

View file

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
@ -8,21 +10,23 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class NFTDetailsPage extends BasePage {
NFTDetailsPage({required this.dashboardViewModel, required this.nftAsset});
NFTDetailsPage({
required this.dashboardViewModel,
required this.arguments,
Key? key,
});
final DashboardViewModel dashboardViewModel;
final NFTAssetModel nftAsset;
final NFTDetailsPageArguments arguments;
@override
bool get gradientBackground => true;
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) =>
GradientBackground(scaffold: scaffold);
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
@override
bool get resizeToAvoidBottomInset => false;
@ -30,15 +34,14 @@ class NFTDetailsPage extends BasePage {
@override
Widget get endDrawer => MenuWidget(
dashboardViewModel,
ValueKey('nft_details_page_menu_widget_key'),
const ValueKey('nft_details_page_menu_widget_key'),
);
@override
Widget trailing(BuildContext context) {
final menuButton = Image.asset(
'assets/images/menu.png',
color:
Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
);
return Container(
@ -58,119 +61,200 @@ class NFTDetailsPage extends BasePage {
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0),
border: Border.all(
color: Theme.of(context)
.extension<BalancePageTheme>()!
.cardBorderColor,
width: 1,
),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: MediaQuery.sizeOf(context).height / 2.5,
width: double.infinity,
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: Theme.of(context)
.extension<BalancePageTheme>()!
.cardBorderColor,
width: 1,
),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
),
child: CakeImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl,
),
),
SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.name,
infoValue: nftAsset.normalizedMetadata?.name ?? '---',
),
if (nftAsset.normalizedMetadata?.description != null) ...[
SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.description,
infoValue: nftAsset.normalizedMetadata?.description ?? '---',
),
],
SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.contractName,
infoValue: nftAsset.name ?? '---',
),
SizedBox(height: 8),
_NFTSingleInfoTile(
infoType: S.current.contractSymbol,
infoValue: nftAsset.symbol ?? '---',
),
],
),
child: Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0),
border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1,
),
],
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
),
child: arguments.isSolanaNFT
? SolanaNFTDetailsWidget(
solanaNftAsset: arguments.solanaNFTAssetModel,
)
: EVMChainNFTDetailsWidget(
nftAsset: arguments.nftAsset,
),
),
);
}
}
class _NFTImageWidget extends StatelessWidget {
final String? imageUrl;
const _NFTImageWidget({Key? key, this.imageUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
final balanceTheme = Theme.of(context).extension<BalancePageTheme>()!;
final syncTheme = Theme.of(context).extension<SyncIndicatorTheme>()!;
return Container(
height: MediaQuery.sizeOf(context).height / 2.5,
width: double.infinity,
margin: const EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: balanceTheme.cardBorderColor,
width: 1,
),
color: syncTheme.syncedBackgroundColor,
),
child: CakeImageWidget(imageUrl: imageUrl),
);
}
}
class EVMChainNFTDetailsWidget extends StatelessWidget {
final NFTAssetModel? nftAsset;
const EVMChainNFTDetailsWidget({Key? key, this.nftAsset}) : super(key: key);
@override
Widget build(BuildContext context) {
if (nftAsset == null) {
return Center(child: Text(S.current.no_extra_detail));
}
final metadata = nftAsset!.normalizedMetadata;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_NFTImageWidget(imageUrl: metadata?.imageUrl),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.name,
infoValue: metadata?.name ?? '---',
),
if (metadata?.description != null) ...[
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.description,
infoValue: metadata!.description ?? '---',
),
],
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.contractName,
infoValue: nftAsset!.name ?? '---',
),
const SizedBox(height: 8),
_NFTSingleInfoTile(
infoType: S.current.contractSymbol,
infoValue: nftAsset!.symbol ?? '---',
),
],
);
}
}
class SolanaNFTDetailsWidget extends StatelessWidget {
final SolanaNFTAssetModel? solanaNftAsset;
const SolanaNFTDetailsWidget({Key? key, this.solanaNftAsset}) : super(key: key);
@override
Widget build(BuildContext context) {
if (solanaNftAsset == null) {
return Center(child: Text(S.current.no_extra_detail));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_NFTImageWidget(imageUrl: solanaNftAsset?.imageOriginalUrl),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.name,
infoValue: solanaNftAsset?.name ?? '---',
),
if (solanaNftAsset?.description != null) ...[
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.description,
infoValue: solanaNftAsset!.description ?? '---',
),
],
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.mint_address,
infoValue: solanaNftAsset?.mint ?? '---',
),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.contractName,
infoValue: solanaNftAsset?.contract?.name ?? '---',
),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.contractSymbol,
infoValue: solanaNftAsset?.contract?.symbol ?? '---',
),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.collection_name,
infoValue: solanaNftAsset?.collection?.name ?? '---',
),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.collection_description,
infoValue: solanaNftAsset?.collection?.description ?? '---',
),
const SizedBox(height: 16),
_NFTSingleInfoTile(
infoType: S.current.collection_address,
infoValue: solanaNftAsset?.collection?.collectionAddress ?? '---',
),
const SizedBox(height: 16),
],
);
}
}
class _NFTSingleInfoTile extends StatelessWidget {
final String infoType;
final String infoValue;
const _NFTSingleInfoTile({
required this.infoType,
required this.infoValue,
});
final String infoType;
final String infoValue;
@override
Widget build(BuildContext context) {
final balanceTheme = Theme.of(context).extension<BalancePageTheme>()!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
infoType,
infoType,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
color: balanceTheme.labelTextColor,
height: 1,
),
),
SizedBox(height: 8),
const SizedBox(height: 8),
Text(
infoValue,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.assetTitleColor,
color: balanceTheme.assetTitleColor,
height: 1,
),
),
@ -179,3 +263,15 @@ infoType,
);
}
}
class NFTDetailsPageArguments {
NFTDetailsPageArguments({
this.nftAsset,
this.solanaNFTAssetModel,
required this.isSolanaNFT,
});
final NFTAssetModel? nftAsset;
final SolanaNFTAssetModel? solanaNFTAssetModel;
final bool isSolanaNFT;
}

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -68,7 +69,6 @@ class _ImportNFTPage extends BasePage {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.current.address,
textAlign: TextAlign.center,
@ -76,8 +76,7 @@ class _ImportNFTPage extends BasePage {
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w800,
color:
Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
height: 1,
),
),
@ -92,9 +91,7 @@ class _ImportNFTPage extends BasePage {
tokenAddressController.text = tokenAddress;
}
},
borderColor: Theme.of(context)
.extension<CakeTextTheme>()!
.textfieldUnderlineColor,
borderColor: Theme.of(context).extension<CakeTextTheme>()!.textfieldUnderlineColor,
iconColor: Theme.of(context).primaryColor,
placeholder: '0x...',
textStyle: TextStyle(
@ -108,46 +105,45 @@ class _ImportNFTPage extends BasePage {
color: PaletteDark.darkCyanBlue,
),
),
SizedBox(height: 48),
Text(
S.current.tokenID,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w800,
color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
height: 1,
if (nftViewModel.appStore.wallet!.type != WalletType.solana) ...[
SizedBox(height: 48),
Text(
S.current.tokenID,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w800,
color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
height: 1,
),
),
),
AddressTextField(
controller: tokenIDController,
options: [AddressTextFieldOption.paste],
onPushPasteButton: (context) async {
final clipboard = await Clipboard.getData('text/plain');
final tokenID = clipboard?.text ?? '';
AddressTextField(
controller: tokenIDController,
options: [AddressTextFieldOption.paste],
onPushPasteButton: (context) async {
final clipboard = await Clipboard.getData('text/plain');
final tokenID = clipboard?.text ?? '';
if (tokenID.isNotEmpty) {
tokenIDController.text = tokenID;
}
},
borderColor: Theme.of(context)
.extension<CakeTextTheme>()!
.textfieldUnderlineColor,
iconColor: Theme.of(context).primaryColor,
placeholder: S.current.enterTokenID,
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.darkCyanBlue,
if (tokenID.isNotEmpty) {
tokenIDController.text = tokenID;
}
},
borderColor: Theme.of(context).extension<CakeTextTheme>()!.textfieldUnderlineColor,
iconColor: Theme.of(context).primaryColor,
placeholder: S.current.enterTokenID,
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.darkCyanBlue,
),
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.darkCyanBlue,
),
),
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.darkCyanBlue,
),
),
],
Spacer(),
Observer(builder: (context) {
return LoadingPrimaryButton(
@ -161,6 +157,7 @@ class _ImportNFTPage extends BasePage {
},
);
}),
SizedBox(height: 16),
],
),
);

View file

@ -1,32 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/nft_tile_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/solana_nft_tile_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cw_core/wallet_type.dart';
class NFTListingPage extends StatelessWidget {
final NFTViewModel nftViewModel;
const NFTListingPage({super.key, required this.nftViewModel});
@override
Widget build(BuildContext context) {
final dashboardTheme = Theme.of(context).extension<DashboardPageTheme>()!;
final syncIndicatorTheme = Theme.of(context).extension<SyncIndicatorTheme>()!;
final exchangeTheme = Theme.of(context).extension<ExchangePageTheme>()!;
return Observer(
builder: (context) {
return Column(
children: [
SizedBox(height: 16),
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 16),
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: PrimaryButton(
text: S.current.import,
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
color: syncIndicatorTheme.syncedBackgroundColor,
textColor: Colors.white,
onPressed: () => Navigator.pushNamed(
context,
@ -39,46 +44,75 @@ class NFTListingPage extends StatelessWidget {
Expanded(
child: Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).extension<DashboardPageTheme>()!.textColor,
backgroundColor: dashboardTheme.textColor,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context)
.extension<ExchangePageTheme>()!
.firstGradientBottomPanelColor,
exchangeTheme.firstGradientBottomPanelColor,
),
),
),
),
if (!nftViewModel.isLoading)
Expanded(
child: nftViewModel.nftAssetByWalletModels.isEmpty
? Center(
child: Text(
S.current.noNFTYet,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.pageTitleTextColor,
height: 1,
),
),
)
: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 16),
separatorBuilder: (context, index) => SizedBox(height: 8),
itemCount: nftViewModel.nftAssetByWalletModels.length,
itemBuilder: (context, index) {
final nftAsset = nftViewModel.nftAssetByWalletModels[index];
return NFTTileWidget(nftAsset: nftAsset);
},
),
)
else
Expanded(
child: NFTListWidget(nftViewModel: nftViewModel),
),
],
);
},
);
}
}
class NFTListWidget extends StatelessWidget {
const NFTListWidget({required this.nftViewModel, super.key});
final NFTViewModel nftViewModel;
@override
Widget build(BuildContext context) {
return Observer(
builder: (context) {
final isSolana = nftViewModel.appStore.wallet!.type == WalletType.solana;
final emptyMessage = Center(
child: Text(
S.current.noNFTYet,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
height: 1,
),
),
);
if (isSolana) {
if (nftViewModel.solanaNftAssetModels.isEmpty) return emptyMessage;
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: nftViewModel.solanaNftAssetModels.length,
itemBuilder: (context, index) {
final nftAsset = nftViewModel.solanaNftAssetModels[index];
return SolanaNFTTileWidget(nftAsset: nftAsset);
},
);
} else {
if (nftViewModel.nftAssetByWalletModels.isEmpty) return emptyMessage;
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: nftViewModel.nftAssetByWalletModels.length,
itemBuilder: (context, index) {
final nftAsset = nftViewModel.nftAssetByWalletModels[index];
return NFTTileWidget(nftAsset: nftAsset);
},
);
}
},
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
@ -12,7 +13,14 @@ class NFTTileWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, arguments: nftAsset),
onTap: () => Navigator.pushNamed(
context,
Routes.nftDetailsPage,
arguments: NFTDetailsPageArguments(
isSolanaNFT: false,
nftAsset: nftAsset,
),
),
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(left: 16, right: 16),

View file

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
class SolanaNFTTileWidget extends StatelessWidget {
const SolanaNFTTileWidget({super.key, required this.nftAsset});
final SolanaNFTAssetModel nftAsset;
@override
Widget build(BuildContext context) {
final balanceTheme = Theme.of(context).extension<BalancePageTheme>()!;
final syncTheme = Theme.of(context).extension<SyncIndicatorTheme>()!;
return InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.nftDetailsPage,
arguments: NFTDetailsPageArguments(
isSolanaNFT: true,
solanaNFTAssetModel: nftAsset,
),
);
},
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: balanceTheme.cardBorderColor,
width: 1,
),
color: syncTheme.syncedBackgroundColor,
),
child: Row(
children: [
Container(
height: 100,
width: 100,
margin: const EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: balanceTheme.cardBorderColor,
width: 1,
),
color: syncTheme.syncedBackgroundColor,
),
child: CakeImageWidget(
imageUrl: nftAsset.imageOriginalUrl,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Symbol: ${nftAsset.symbol ?? '---'}',
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: balanceTheme.labelTextColor,
height: 1,
),
),
const SizedBox(height: 8),
Text(
(nftAsset.name?.isNotEmpty ?? false)
? nftAsset.name!
: (nftAsset.symbol ?? '---'),
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w900,
color: balanceTheme.assetTitleColor,
height: 1,
),
),
],
),
)
],
),
),
);
}
}

View file

@ -21,6 +21,7 @@ class SyncIndicatorIcon extends StatelessWidget {
static const String fetching = 'fetching';
static const String finished = 'finished';
static const String success = 'success';
static const String complete = 'complete';
@override
Widget build(BuildContext context) {
@ -47,6 +48,7 @@ class SyncIndicatorIcon extends StatelessWidget {
break;
case finished:
case success:
case complete:
indicatorColor = PaletteDark.brightGreen;
break;
default:

View file

@ -248,7 +248,7 @@ class ExchangePage extends BasePage {
Observer(
builder: (_) => LoadingPrimaryButton(
key: ValueKey('exchange_page_exchange_button_key'),
text: exchangeViewModel.isAvailableInSelected ? S.of(context).exchange : S.of(context).change_selected_exchanges,
text: exchangeViewModel.isAvailableInSelected ? S.of(context).swap : S.of(context).change_selected_exchanges,
onPressed: exchangeViewModel.isAvailableInSelected ? () {
FocusScope.of(context).unfocus();
@ -377,7 +377,7 @@ class ExchangePage extends BasePage {
return;
}
if (exchangeViewModel.isLowFee) {
if (exchangeViewModel.feesViewModel.isLowFee) {
_showFeeAlert(context);
}
@ -640,7 +640,7 @@ class ExchangePage extends BasePage {
}) ??
false;
if (confirmed) {
exchangeViewModel.setDefaultTransactionPriority();
exchangeViewModel.feesViewModel .setDefaultTransactionPriority();
}
}

View file

@ -35,10 +35,11 @@ class PresentProviderPicker extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(S.of(context).exchange,
Text(S.of(context).swap,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Colors.white)),
Observer(
builder: (_) => Text(

View file

@ -21,7 +21,7 @@ class ExchangeConfirmPage extends BasePage {
final Trade trade;
@override
String get title => S.current.copy_id;
String get title => S.current.swap;
@override
Widget body(BuildContext context) {
@ -40,7 +40,7 @@ class ExchangeConfirmPage extends BasePage {
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
),
)),
Container(
@ -97,22 +97,39 @@ class ExchangeConfirmPage extends BasePage {
],
),
),
Flexible(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
(trade.provider.image?.isNotEmpty ?? false)
? ImageUtil.getImageFromPath(
imagePath: trade.provider.image, width: 50)
: const SizedBox(),
if (!trade.provider.horizontalLogo)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(trade.provider.title),
Expanded(
child: Column(
children: [
if (trade.provider == ExchangeProviderDescription.trocador)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).selected_trocador_provider +':${trade.providerName}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
),
),
Flexible(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
(trade.provider.image?.isNotEmpty ?? false)
? Image.asset(trade.provider.image, height: 50)
: const SizedBox(),
if (!trade.provider.horizontalLogo)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(trade.provider.title),
),
],
),
],
),
),
),
],
),
),
],

View file

@ -0,0 +1,143 @@
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
class ExchangeTradeExternalSendPage extends BasePage {
ExchangeTradeExternalSendPage({required this.exchangeTradeViewModel});
final ExchangeTradeViewModel exchangeTradeViewModel;
@override
String get title => S.current.swap;
@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;
final fetchingLabel = S.current.fetching;
@override
Widget body(BuildContext context) {
final copyImage = Image.asset(
'assets/images/copy_content.png',
height: 16,
width: 16,
color: Theme.of(context).extension<SendPageTheme>()!.estimatedFeeColor,
);
return Container(
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(top: 36, bottom: 24),
content: Observer(
builder: (_) {
return Column(
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<SendPageTheme>()!.firstGradientColor,
Theme.of(context).extension<SendPageTheme>()!.secondGradientColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: EdgeInsets.fromLTRB(24, 110, 24, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Spacer(flex: 3),
Flexible(
flex: 6,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context)
.extension<ExchangePageTheme>()!
.qrCodeColor,
),
),
child: QrImage(
data:
exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel,
),
),
),
),
),
Spacer(flex: 3)
],
),
SizedBox(height: 24),
...exchangeTradeViewModel.items
.where((item) => item.isExternalSendDetail)
.map(
(item) => TradeItemRowWidget(
currentTheme: currentTheme,
title: item.title,
value: item.data,
isCopied: true,
copyImage: copyImage,
),
)
.toList(),
],
),
),
],
);
},
),
bottomSection: Observer(
builder: (_) {
final trade = exchangeTradeViewModel.trade;
final sendingState = exchangeTradeViewModel.sendViewModel.state;
return exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_external_send_page_continue_button_key'),
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
text: S.current.continue_text,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
)
: Offstage();
},
),
),
);
}
}

View file

@ -1,13 +1,15 @@
import 'package:flutter/cupertino.dart';
class ExchangeTradeItem {
ExchangeTradeItem({
required this.title,
required this.data,
required this.isCopied,
required this.isReceiveDetail,
required this.isExternalSendDetail,
});
String title;
String data;
bool isCopied;
bool isReceiveDetail;
bool isExternalSendDetail;
}

View file

@ -1,22 +1,22 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'dart:ui';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/utils/request_review_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
@ -30,8 +30,7 @@ void showInformation(
final walletName = exchangeTradeViewModel.wallet.name;
final information = exchangeTradeViewModel.isSendable
? S.current.exchange_result_confirm(
trade.amount, trade.from.toString(), walletName) +
? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) +
exchangeTradeViewModel.extraInfo
: S.current.exchange_result_description(
trade.amount, trade.from.toString()) +
@ -50,7 +49,22 @@ class ExchangeTradePage extends BasePage {
final ExchangeTradeViewModel exchangeTradeViewModel;
@override
String get title => S.current.exchange;
String get title => S.current.swap;
@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
Widget trailing(BuildContext context) {
@ -74,14 +88,20 @@ class ExchangeTradePage extends BasePage {
}
@override
Widget body(BuildContext context) =>
ExchangeTradeForm(exchangeTradeViewModel);
Widget body(BuildContext context) => ExchangeTradeForm(
exchangeTradeViewModel,
currentTheme,
);
}
class ExchangeTradeForm extends StatefulWidget {
ExchangeTradeForm(this.exchangeTradeViewModel);
ExchangeTradeForm(
this.exchangeTradeViewModel,
this.currentTheme,
);
final ExchangeTradeViewModel exchangeTradeViewModel;
final ThemeBase currentTheme;
@override
ExchangeTradeState createState() => ExchangeTradeState();
@ -115,119 +135,77 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
@override
Widget build(BuildContext context) {
final copyImage = Image.asset('assets/images/copy_content.png',
height: 16,
width: 16,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor);
_setEffects();
return Container(
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(top: 10, bottom: 16),
content: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
contentPadding: EdgeInsets.only(top: 10, bottom: 16),
content: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
return Column(
children: <Widget>[
trade.expiredAt != null
? Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
S.of(context).offer_expires_in,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
),
if (trade.expiredAt != null)
TimerWidget(trade.expiredAt!,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)
])
: Offstage(),
Padding(
padding: EdgeInsets.only(top: 32),
child: Row(children: <Widget>[
Spacer(flex: 3),
Flexible(
flex: 4,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context).extension<ExchangePageTheme>()!.qrCodeColor
)
),
child: QrImage(data: trade.inputAddress ?? fetchingLabel),
)))),
Spacer(flex: 3)
]),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: widget.exchangeTradeViewModel.items.length,
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).extension<ExchangePageTheme>()!.dividerCodeColor,
),
itemBuilder: (context, index) {
final item = widget.exchangeTradeViewModel.items[index];
final value = item.data;
return Column(
children: <Widget>[
trade.expiredAt != null
? Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
S.of(context).offer_expires_in,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<TransactionTradeTheme>()!
.detailsTitlesColor),
),
if (trade.expiredAt != null)
TimerWidget(trade.expiredAt!,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)
])
: Offstage(),
_ExchangeTradeItemsCardSection(
viewModel: widget.exchangeTradeViewModel,
currentTheme: widget.currentTheme,
),
],
);
}),
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
bottomSection: Column(
children: [
PrimaryButton(
key: ValueKey('exchange_trade_page_send_from_external_button_key'),
text: S.current.send_from_external_wallet,
onPressed: () async {
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
},
color: Theme.of(context).cardColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
SizedBox(height: 16),
Observer(
builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
final content = ListRow(
title: item.title,
value: value,
valueFontSize: 14,
image: item.isCopied ? copyImage : null,
);
return item.isCopied
? Builder(
builder: (context) => GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: value));
showBar<void>(context,
S.of(context).copied_to_clipboard);
},
child: content,
))
: content;
},
),
),
],
);
}),
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
bottomSection: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
final sendingState =
widget.exchangeTradeViewModel.sendViewModel.state;
return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_confirm_sending_button_key'),
isDisabled: trade.inputAddress == null ||
trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () =>
widget.exchangeTradeViewModel.confirmSending(),
text: S.of(context).confirm,
color: Theme.of(context).primaryColor,
textColor: Colors.white)
: Offstage();
})),
return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
text:S.current.send_from_cake_wallet,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
)
: Offstage();
},
),
],
),
),
);
}
@ -412,3 +390,40 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
}
}
}
class _ExchangeTradeItemsCardSection extends StatelessWidget {
const _ExchangeTradeItemsCardSection({
required this.viewModel,
required this.currentTheme,
});
final ExchangeTradeViewModel viewModel;
final ThemeBase currentTheme;
@override
Widget build(BuildContext context) {
final firstExchangeCard = ExchangeTradeCardItemWidget(
currentTheme: currentTheme,
isReceiveDetailsCard: true,
exchangeTradeViewModel: viewModel,
);
final secondExchangeCard = ExchangeTradeCardItemWidget(
currentTheme: currentTheme,
isReceiveDetailsCard: false,
exchangeTradeViewModel: viewModel,
);
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return MobileExchangeCardsSection(
firstExchangeCard: firstExchangeCard,
secondExchangeCard: secondExchangeCard,
);
}
return DesktopExchangeCardsSection(
firstExchangeCard: firstExchangeCard,
secondExchangeCard: secondExchangeCard,
);
}
}

View file

@ -0,0 +1,312 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../../../../themes/extensions/cake_text_theme.dart';
import '../../../../themes/extensions/transaction_trade_theme.dart';
import '../../../../themes/theme_base.dart';
class ExchangeTradeCardItemWidget extends StatelessWidget {
ExchangeTradeCardItemWidget({
required this.isReceiveDetailsCard,
required this.exchangeTradeViewModel,
required this.currentTheme,
Key? key,
}) : feesViewModel = exchangeTradeViewModel.feesViewModel,
output = exchangeTradeViewModel.output;
final Output output;
final bool isReceiveDetailsCard;
final FeesViewModel feesViewModel;
final ExchangeTradeViewModel exchangeTradeViewModel;
final ThemeBase currentTheme;
@override
Widget build(BuildContext context) {
final copyImage = Image.asset(
'assets/images/copy_content.png',
height: 16,
width: 16,
color: Theme.of(context).extension<SendPageTheme>()!.estimatedFeeColor,
);
return Container(
width: double.infinity,
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 10),
...exchangeTradeViewModel.items
.where((item) => item.isReceiveDetail == isReceiveDetailsCard)
.map(
(item) => TradeItemRowWidget(
currentTheme: currentTheme,
title: item.title,
value: item.data,
isCopied: item.isCopied,
copyImage: copyImage,
),
)
.toList(),
if (!isReceiveDetailsCard && exchangeTradeViewModel.isSendable) ...[
if (feesViewModel.hasFees)
FeeSelectionWidget(
feesViewModel: feesViewModel,
output: output,
onTap: () => pickTransactionPriority(context),
),
if (exchangeTradeViewModel.sendViewModel.hasCoinControl)
CoinControlWidget(
onTap: () => Navigator.of(context).pushNamed(
Routes.unspentCoinsList,
arguments: exchangeTradeViewModel.sendViewModel.coinTypeToSpendFrom,
),
),
],
],
),
);
}
Future<void> pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(feesViewModel.walletType);
final selectedItem = items.indexOf(feesViewModel.transactionPriority);
final customItemIndex = feesViewModel.getCustomPriorityIndex(items);
final isBitcoinWallet = feesViewModel.walletType == WalletType.bitcoin;
final maxCustomFeeRate = feesViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? feesViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
int selectedIdx = selectedItem;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Picker(
items: items,
displayItem: (TransactionPriority priority) =>
feesViewModel.displayFeeRate(priority, customFeeRate?.round()),
selectedAtIndex: selectedIdx,
customItemIndex: customItemIndex,
maxValue: maxCustomFeeRate,
title: S.of(context).please_select,
headerEnabled: !isBitcoinWallet,
closeOnItemSelected: !isBitcoinWallet,
mainAxisAlignment: MainAxisAlignment.center,
sliderValue: customFeeRate,
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
onItemSelected: (TransactionPriority priority) {
feesViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(priority));
if (feesViewModel.isLowFee) {
_showFeeAlert(context);
}
},
);
},
);
},
);
if (isBitcoinWallet) {
feesViewModel.customBitcoinFeeRate = customFeeRate!.round();
if (feesViewModel.showAlertForCustomFeeRate()) {
_showFeeAlert(context);
}
}
}
void _showFeeAlert(BuildContext context) async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).low_fee,
alertContent: S.of(context).low_fee_alert,
leftButtonText: S.of(context).ignor,
rightButtonText: S.of(context).use_suggested,
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true));
}) ??
false;
if (confirmed) {
feesViewModel.setDefaultTransactionPriority();
}
}
}
class TradeItemRowWidget extends StatelessWidget {
final String title;
final String value;
final bool isCopied;
final Image copyImage;
final ThemeBase currentTheme;
const TradeItemRowWidget({
required this.title,
required this.value,
required this.isCopied,
required this.copyImage,
required this.currentTheme,
});
@override
Widget build(BuildContext context) {
final hintTextColor = currentTheme == ThemeType.bright
? Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor
: Colors.white.withAlpha(175);
final mainTextColor = currentTheme == ThemeType.bright
? Theme.of(context).extension<CakeTextTheme>()!.titleColor
: Colors.white;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: GestureDetector(
onTap: () {
if (!isCopied) return;
Clipboard.setData(ClipboardData(text: value));
showBar<void>(context, S.of(context).transaction_details_copied(title));
},
child: ListRow(
padding: EdgeInsets.zero,
title: title,
value: value,
image: isCopied ? copyImage : null,
color: Colors.transparent,
hintTextColor: hintTextColor,
mainTextColor: mainTextColor,
),
),
);
}
}
class FeeSelectionWidget extends StatelessWidget {
final FeesViewModel feesViewModel;
final Output output;
final VoidCallback onTap;
const FeeSelectionWidget({
required this.feesViewModel,
required this.output,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) => GestureDetector(
key: ValueKey('exchange_trade_card_item_widget_select_fee_priority_button_key'),
onTap: feesViewModel.hasFeesPriority ? onTap : () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).send_estimated_fee,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${output.estimatedFee} ${feesViewModel.currency}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (!feesViewModel.isFiatDisabled)
Padding(
padding: EdgeInsets.only(top: 5),
child: Text(
'${output.estimatedFeeFiatAmount} ${feesViewModel.fiat.title}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor,
),
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 2, left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
),
],
),
],
),
),
),
);
}
}
class CoinControlWidget extends StatelessWidget {
final VoidCallback onTap;
const CoinControlWidget({required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
key: ValueKey('exchange_trade_card_item_widget_unspent_coin_button_key'),
onTap: onTap,
child: Container(
color: Colors.transparent,
padding: EdgeInsets.only(top: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).coin_control,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
],
),
),
);
}
}

View file

@ -14,14 +14,17 @@ import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_card.dart';
import 'package:cake_wallet/src/widgets/adaptable_page_view.dart';
import 'package:cake_wallet/src/widgets/add_template_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/template_tile.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
@ -38,6 +41,7 @@ import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/crypto_currency.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';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:url_launcher/url_launcher.dart';
@ -93,7 +97,7 @@ class SendPage extends BasePage {
return MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
width: isMobileView ? 47: 45,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
@ -114,18 +118,6 @@ class SendPage extends BasePage {
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
double _sendCardHeight(BuildContext context) {
double initialHeight = 480;
if (sendViewModel.hasCoinControl) {
initialHeight += 55;
}
if (!responsiveLayoutUtil.shouldRenderMobileUI) {
return initialHeight - 66;
}
return initialHeight;
}
@override
void onClose(BuildContext context) {
sendViewModel.onClose();
@ -174,285 +166,316 @@ class SendPage extends BasePage {
Widget body(BuildContext context) {
_setEffects(context);
return GestureDetector(
onLongPress: () =>
sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing,
onLongPressUp: () =>
sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: <Widget>[
Container(
height: _sendCardHeight(context),
child: Observer(
builder: (_) {
return PageView.builder(
scrollDirection: Axis.horizontal,
controller: controller,
itemCount: sendViewModel.outputs.length,
itemBuilder: (context, index) {
final output = sendViewModel.outputs[index];
return Observer(builder: (_) {
List<Widget> sendCards = [];
List<KeyboardActionsItem> keyboardActions = [];
for (var output in sendViewModel.outputs) {
var cryptoAmountFocus = FocusNode();
var fiatAmountFocus = FocusNode();
sendCards.add(SendCard(
currentTheme: currentTheme,
key: output.key,
output: output,
sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest,
cryptoAmountFocus: cryptoAmountFocus,
fiatAmountFocus: fiatAmountFocus,
));
keyboardActions.add(KeyboardActionsItem(
focusNode: cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]));
keyboardActions.add(KeyboardActionsItem(
focusNode: fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]));
}
return Stack(
children: [
KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.ALL,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false,
actions: keyboardActions,
),
child: Container(
height: 0,
color: Colors.transparent,
),
),
GestureDetector(
onLongPress: () => sendViewModel.balanceViewModel.isReversing =
!sendViewModel.balanceViewModel.isReversing,
onLongPressUp: () => sendViewModel.balanceViewModel.isReversing =
!sendViewModel.balanceViewModel.isReversing,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: <Widget>[
PageViewHeightAdaptable(
controller: controller,
children: sendCards,
),
SizedBox(height: 10),
Padding(
padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
builder: (_) {
final count = sendViewModel.outputs.length;
return SendCard(
key: output.key,
output: output,
sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest,
return count > 1
? Semantics(
label: 'Page Indicator',
hint: 'Swipe to change receiver',
excludeSemantics: true,
child: SmoothPageIndicator(
controller: controller,
count: count,
effect: ScrollingDotsEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.extension<SendPageTheme>()!
.indicatorDotColor,
activeDotColor: Theme.of(context)
.extension<SendPageTheme>()!
.templateBackgroundColor),
))
: Offstage();
},
),
),
),
Container(
height: 40,
width: double.infinity,
padding: EdgeInsets.only(left: 24),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Observer(
builder: (_) {
final templates = sendViewModel.templates;
final itemCount = templates.length;
return Row(
children: <Widget>[
AddTemplateButton(
key: ValueKey('send_page_add_template_button_key'),
onTap: () =>
Navigator.of(context).pushNamed(Routes.sendTemplate),
currentTemplatesLength: templates.length,
),
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (context, index) {
final template = templates[index];
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients:
template.additionalRecipients != null &&
template.additionalRecipients!.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
from: template.isCurrencySelected
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
sendViewModel.state = IsExecutingState();
if (template.additionalRecipients?.isNotEmpty ??
false) {
sendViewModel.clearOutputs();
for (int i = 0;
i < template.additionalRecipients!.length;
i++) {
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate(
context,
output: output,
template: template.additionalRecipients![i],
);
}
} else {
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template,
);
}
sendViewModel.state = InitialExecutionState();
},
onRemove: () {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).template,
alertContent:
S.of(context).confirm_delete_template,
rightButtonText: S.of(context).delete,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
Navigator.of(dialogContext).pop();
sendViewModel.sendTemplateViewModel
.removeTemplate(template: template);
},
actionLeftButton: () =>
Navigator.of(dialogContext).pop());
},
);
},
);
},
),
],
);
});
},
)),
Padding(
padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
builder: (_) {
final count = sendViewModel.outputs.length;
return count > 1
? Semantics(
label: 'Page Indicator',
hint: 'Swipe to change receiver',
excludeSemantics: true,
child: SmoothPageIndicator(
controller: controller,
count: count,
effect: ScrollingDotsEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.extension<SendPageTheme>()!
.indicatorDotColor,
activeDotColor: Theme.of(context)
.extension<SendPageTheme>()!
.templateBackgroundColor),
))
: Offstage();
},
),
},
),
),
),
],
),
),
Container(
height: 40,
width: double.infinity,
padding: EdgeInsets.only(left: 24),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Observer(
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(
children: [
if (sendViewModel.hasCurrecyChanger)
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
key: ValueKey('send_page_change_asset_button_key'),
onPressed: () => presentCurrencyPicker(context),
text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
color: Colors.transparent,
textColor:
Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
),
),
),
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
key: ValueKey('send_page_add_receiver_button_key'),
onPressed: () {
sendViewModel.addOutput();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(sendViewModel.outputs.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor:
Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
isDottedBorder: true,
borderColor: Theme.of(context)
.extension<SendPageTheme>()!
.templateDottedBorderColor,
)),
Observer(
builder: (_) {
final templates = sendViewModel.templates;
final itemCount = templates.length;
return LoadingPrimaryButton(
key: ValueKey('send_page_send_button_key'),
onPressed: () async {
if (sendViewModel.state is IsExecutingState) return;
if (_formKey.currentState != null &&
!_formKey.currentState!.validate()) {
if (sendViewModel.outputs.length > 1) {
showErrorValidationAlert(context);
}
return Row(
children: <Widget>[
AddTemplateButton(
key: ValueKey('send_page_add_template_button_key'),
onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate),
currentTemplatesLength: templates.length,
),
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (context, index) {
final template = templates[index];
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients: template.additionalRecipients != null &&
template.additionalRecipients!.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
from: template.isCurrencySelected
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
sendViewModel.state = IsExecutingState();
if (template.additionalRecipients?.isNotEmpty ?? false) {
sendViewModel.clearOutputs();
return;
}
for (int i = 0;
i < template.additionalRecipients!.length;
i++) {
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
final notValidItems = sendViewModel.outputs
.where(
(item) => item.address.isEmpty || item.cryptoAmount.isEmpty)
.toList();
await _setInputsFromTemplate(
context,
output: output,
template: template.additionalRecipients![i],
);
}
} else {
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template,
);
}
sendViewModel.state = InitialExecutionState();
},
onRemove: () {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).template,
alertContent: S.of(context).confirm_delete_template,
rightButtonText: S.of(context).delete,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
Navigator.of(dialogContext).pop();
sendViewModel.sendTemplateViewModel
.removeTemplate(template: template);
},
actionLeftButton: () =>
Navigator.of(dialogContext).pop());
if (notValidItems.isNotEmpty) {
showErrorValidationAlert(context);
return;
}
if (sendViewModel.wallet.isHardwareWallet) {
if (!sendViewModel.ledgerViewModel!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendViewModel.walletType,
onConnectDevice: (BuildContext context, _) {
sendViewModel.ledgerViewModel!
.setLedger(sendViewModel.wallet);
Navigator.of(context).pop();
},
);
},
);
));
} else {
sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet);
}
}
if (sendViewModel.wallet.type == WalletType.monero) {
int amount = 0;
for (var item in sendViewModel.outputs) {
amount += item.formattedCryptoAmount;
}
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
arguments: 'export-outputs');
await Future.delayed(
Duration(seconds: 1)); // wait for monero to refresh the state
}
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
return;
}
}
final check = sendViewModel.shouldDisplayTotp();
authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: check,
onAuthSuccess: (value) async {
if (value) {
await sendViewModel.createTransaction();
}
},
),
],
);
},
text: S.of(context).send,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: sendViewModel.state is IsExecutingState ||
sendViewModel.state is TransactionCommitting ||
sendViewModel.state is IsAwaitingDeviceResponseState,
isDisabled: !sendViewModel.isReadyForSend,
);
},
),
),
),
],
),
)
],
)),
),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(
children: [
if (sendViewModel.hasCurrecyChanger)
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
key: ValueKey('send_page_change_asset_button_key'),
onPressed: () => presentCurrencyPicker(context),
text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
color: Colors.transparent,
textColor: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
),
),
),
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
key: ValueKey('send_page_add_receiver_button_key'),
onPressed: () {
sendViewModel.addOutput();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(sendViewModel.outputs.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
isDottedBorder: true,
borderColor:
Theme.of(context).extension<SendPageTheme>()!.templateDottedBorderColor,
)),
Observer(
builder: (_) {
return LoadingPrimaryButton(
key: ValueKey('send_page_send_button_key'),
onPressed: () async {
if (sendViewModel.state is IsExecutingState) return;
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
if (sendViewModel.outputs.length > 1) {
showErrorValidationAlert(context);
}
return;
}
final notValidItems = sendViewModel.outputs
.where((item) => item.address.isEmpty || item.cryptoAmount.isEmpty)
.toList();
if (notValidItems.isNotEmpty) {
showErrorValidationAlert(context);
return;
}
if (sendViewModel.wallet.isHardwareWallet) {
if (!sendViewModel.ledgerViewModel!.isConnected) {
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendViewModel.walletType,
onConnectDevice: (BuildContext context, _) {
sendViewModel.ledgerViewModel!
.setLedger(sendViewModel.wallet);
Navigator.of(context).pop();
},
));
} else {
sendViewModel.ledgerViewModel!
.setLedger(sendViewModel.wallet);
}
}
if (sendViewModel.wallet.type == WalletType.monero) {
int amount = 0;
for (var item in sendViewModel.outputs) {
amount += item.formattedCryptoAmount;
}
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state
}
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
return;
}
}
final check = sendViewModel.shouldDisplayTotp();
authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: check,
onAuthSuccess: (value) async {
if (value) {
await sendViewModel.createTransaction();
}
},
);
},
text: S.of(context).send,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: sendViewModel.state is IsExecutingState ||
sendViewModel.state is TransactionCommitting ||
sendViewModel.state is IsAwaitingDeviceResponseState,
isDisabled: !sendViewModel.isReadyForSend,
);
},
)
],
)),
),
);
),
],
);
});
}
BuildContext? dialogContext;
@ -525,13 +548,12 @@ class SendPage extends BasePage {
if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!context.mounted) {
return;
}
final successMessage = S.of(context).send_success(
sendViewModel.selectedCryptoCurrency.toString());
final successMessage =
S.of(context).send_success(sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(context).waitFewSecondForTxUpdate}'
@ -539,10 +561,8 @@ class SendPage extends BasePage {
String alertContent = "$successMessage$waitMessage";
await Navigator.of(context).pushNamed(
Routes.transactionSuccessPage,
arguments: alertContent
);
await Navigator.of(context)
.pushNamed(Routes.transactionSuccessPage, arguments: alertContent);
newContactAddress = newContactAddress ?? sendViewModel.newContactAddress();
if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) {
@ -562,7 +582,7 @@ class SendPage extends BasePage {
leftButtonText: S.of(_dialogContext).ignor,
alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'),
alertRightActionButtonKey:
ValueKey('send_page_sent_dialog_add_contact_button_key'),
ValueKey('send_page_sent_dialog_add_contact_button_key'),
actionRightButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@ -12,6 +13,7 @@ import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -24,40 +26,58 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import '../../../../themes/extensions/cake_text_theme.dart';
import '../../../../themes/theme_base.dart';
class SendCard extends StatefulWidget {
SendCard({
Key? key,
required this.output,
required this.sendViewModel,
required this.currentTheme,
this.initialPaymentRequest,
this.cryptoAmountFocus,
this.fiatAmountFocus,
}) : super(key: key);
final Output output;
final SendViewModel sendViewModel;
final PaymentRequest? initialPaymentRequest;
final FocusNode? cryptoAmountFocus;
final FocusNode? fiatAmountFocus;
final ThemeBase currentTheme;
@override
SendCardState createState() => SendCardState(
output: output,
sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest,
currentTheme: currentTheme
// cryptoAmountFocus: cryptoAmountFocus ?? FocusNode(),
// fiatAmountFocus: fiatAmountFocus ?? FocusNode(),
// cryptoAmountFocus: FocusNode(),
// fiatAmountFocus: FocusNode(),
);
}
class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> {
SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest})
: addressController = TextEditingController(),
SendCardState({
required this.output,
required this.sendViewModel,
this.initialPaymentRequest,
required this.currentTheme,
}) : addressController = TextEditingController(),
cryptoAmountController = TextEditingController(),
fiatAmountController = TextEditingController(),
noteController = TextEditingController(),
extractedAddressController = TextEditingController(),
cryptoAmountFocus = FocusNode(),
fiatAmountFocus = FocusNode(),
addressFocusNode = FocusNode();
static const prefixIconWidth = 34.0;
static const prefixIconHeight = 34.0;
final ThemeBase currentTheme;
final Output output;
final SendViewModel sendViewModel;
final PaymentRequest? initialPaymentRequest;
@ -67,8 +87,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
final TextEditingController fiatAmountController;
final TextEditingController noteController;
final TextEditingController extractedAddressController;
final FocusNode cryptoAmountFocus;
final FocusNode fiatAmountFocus;
final FocusNode addressFocusNode;
bool _effectsInstalled = false;
@ -101,310 +119,336 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
super.build(context);
_setEffects(context);
return Stack(
children: [
KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
// return Stack(
// children: [
// return KeyboardActions(
// config: KeyboardActionsConfig(
// keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
// keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
// nextFocus: false,
// actions: [
// KeyboardActionsItem(
// focusNode: cryptoAmountFocus,
// toolbarButtons: [(_) => KeyboardDoneButton()],
// ),
// KeyboardActionsItem(
// focusNode: fiatAmountFocus,
// toolbarButtons: [(_) => KeyboardDoneButton()],
// )
// ],
// ),
// // child: Container(
// // height: 0,
// // color: Colors.transparent,
// // ), child:
// child: SizedBox(
// height: 100,
// width: 100,
// child: Text('Send Card'),
// ),
// );
return Container(
decoration: responsiveLayoutUtil.shouldRenderMobileUI
? BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<SendPageTheme>()!.firstGradientColor,
Theme.of(context).extension<SendPageTheme>()!.secondGradientColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
KeyboardActionsItem(
focusNode: fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
)
],
),
child: Container(
height: 0,
color: Colors.transparent,
),
)
: null,
child: Padding(
padding: EdgeInsets.fromLTRB(
24,
responsiveLayoutUtil.shouldRenderMobileUI ? 110 : 55,
24,
responsiveLayoutUtil.shouldRenderMobileUI ? 32 : 0,
),
Container(
decoration: responsiveLayoutUtil.shouldRenderMobileUI
? BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<SendPageTheme>()!.firstGradientColor,
Theme.of(context).extension<SendPageTheme>()!.secondGradientColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
)
: null,
child: Padding(
padding: EdgeInsets.fromLTRB(
24,
responsiveLayoutUtil.shouldRenderMobileUI ? 110 : 55,
24,
responsiveLayoutUtil.shouldRenderMobileUI ? 32 : 0,
),
child: SingleChildScrollView(
child: Observer(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Observer(builder: (_) {
final validator = output.isParsedAddress
? sendViewModel.textValidator
: sendViewModel.addressValidator;
child: Observer(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Observer(builder: (_) {
final validator = output.isParsedAddress
? sendViewModel.textValidator
: sendViewModel.addressValidator;
return AddressTextField(
addressKey: ValueKey('send_page_address_textfield_key'),
focusNode: addressFocusNode,
controller: addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
return AddressTextField(
addressKey: ValueKey('send_page_address_textfield_key'),
focusNode: addressFocusNode,
controller: addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
onPushPasteButton: (context) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
output.resetParsedAddress();
},
onSelectedContact: (contact) {
output.loadContact(contact);
},
validator: validator,
selectedCurrency: sendViewModel.selectedCryptoCurrency,
);
}),
if (output.isParsedAddress)
Padding(
padding: const EdgeInsets.only(top: 20),
child: BaseTextFormField(
controller: extractedAddressController,
readOnly: true,
borderColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
onPushPasteButton: (context) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
output.resetParsedAddress();
},
onSelectedContact: (contact) {
output.loadContact(contact);
},
validator: validator,
selectedCurrency: sendViewModel.selectedCryptoCurrency,
);
}),
if (output.isParsedAddress)
Padding(
padding: const EdgeInsets.only(top: 20),
child: BaseTextFormField(
controller: extractedAddressController,
readOnly: true,
borderColor: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldBorderColor,
textStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
validator: sendViewModel.addressValidator)),
CurrencyAmountTextField(
currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'),
amountTextfieldKey: ValueKey('send_page_amount_textfield_key'),
sendAllButtonKey: ValueKey('send_page_send_all_button_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
amountFocusNode: cryptoAmountFocus,
amountController: cryptoAmountController,
isAmountEditable: true,
onTapPicker: () => _presentPicker(context),
isPickerEnable: sendViewModel.hasMultipleTokens,
tag: sendViewModel.selectedCryptoCurrency.tag,
allAmountButton:
!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
currencyValueValidator: output.sendAll
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
allAmountCallback: () async => output.setSendAll(sendViewModel.balance)),
Divider(
height: 1,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
S.of(context).available_balance + ':',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldHintColor),
),
),
Text(
sendViewModel.balance,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldHintColor),
)
],
validator: sendViewModel.addressValidator)),
CurrencyAmountTextField(
currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'),
amountTextfieldKey: ValueKey('send_page_amount_textfield_key'),
sendAllButtonKey: ValueKey('send_page_send_all_button_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
amountFocusNode: widget.cryptoAmountFocus,
amountController: cryptoAmountController,
isAmountEditable: true,
onTapPicker: () => _presentPicker(context),
isPickerEnable: sendViewModel.hasMultipleTokens,
tag: sendViewModel.selectedCryptoCurrency.tag,
allAmountButton:
!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
currencyValueValidator: output.sendAll
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
allAmountCallback: () async => output.setSendAll(sendViewModel.balance)),
Divider(
height: 1,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
S.of(context).available_balance + ':',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
),
),
),
if (!sendViewModel.isFiatDisabled)
CurrencyAmountTextField(
amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.fiat.title,
amountFocusNode: fiatAmountFocus,
amountController: fiatAmountController,
hintText: '0.00',
isAmountEditable: true,
allAmountButton: false),
Divider(
height: 1,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding(
padding: EdgeInsets.only(top: 20),
child: BaseTextFormField(
key: ValueKey('send_page_note_textfield_key'),
controller: noteController,
keyboardType: TextInputType.multiline,
maxLines: null,
borderColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintText: S.of(context).note_optional,
placeholderTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
Text(
sendViewModel.balance,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
),
),
if (sendViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
key: ValueKey('send_page_select_fee_priority_button_key'),
onTap: sendViewModel.hasFeesPriority
? () => pickTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
)
],
),
),
),
if (!sendViewModel.isFiatDisabled)
CurrencyAmountTextField(
amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.fiat.title,
amountFocusNode: widget.fiatAmountFocus,
amountController: fiatAmountController,
hintText: '0.00',
isAmountEditable: true,
allAmountButton: false),
Divider(
height: 1,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding(
padding: EdgeInsets.only(top: 20),
child: BaseTextFormField(
key: ValueKey('send_page_note_textfield_key'),
controller: noteController,
keyboardType: TextInputType.multiline,
maxLines: null,
borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintText: S.of(context).note_optional,
placeholderTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
),
),
if (sendViewModel.feesViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
key: ValueKey('send_page_select_fee_priority_button_key'),
onTap: sendViewModel.feesViewModel.hasFeesPriority
? () => pickTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).send_estimated_fee,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).send_estimated_fee,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white),
),
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
output.estimatedFee.toString() +
' ' +
sendViewModel.currency.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Padding(
padding: EdgeInsets.only(top: 5),
child: sendViewModel.isFiatDisabled
? const SizedBox(height: 14)
: Text(
output.estimatedFeeFiatAmount +
' ' +
sendViewModel.fiat.title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldHintColor,
),
),
),
],
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
output.estimatedFee.toString() +
' ' +
sendViewModel.currency.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
Padding(
padding: EdgeInsets.only(top: 2, left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
)
],
),
Padding(
padding: EdgeInsets.only(top: 5),
child: sendViewModel.isFiatDisabled
? const SizedBox(height: 14)
: Text(
output.estimatedFeeFiatAmount +
' ' +
sendViewModel.fiat.title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldHintColor,
),
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 2, left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
)
],
),
),
),
)
],
),
if (sendViewModel.hasCoinControl)
Padding(
padding: EdgeInsets.only(top: 6),
child: GestureDetector(
key: ValueKey('send_page_unspent_coin_button_key'),
onTap: () => Navigator.of(context).pushNamed(
Routes.unspentCoinsList,
arguments: widget.sendViewModel.coinTypeToSpendFrom,
),
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).coin_control,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
],
),
),
),
),
],
),
),
),
),
),
if (sendViewModel.hasCoinControl)
Padding(
padding: EdgeInsets.only(top: 6),
child: GestureDetector(
key: ValueKey('send_page_unspent_coin_button_key'),
onTap: () => Navigator.of(context).pushNamed(
Routes.unspentCoinsList,
arguments: widget.sendViewModel.coinTypeToSpendFrom,
),
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).coin_control,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
],
),
),
),
),
if (sendViewModel.currency == CryptoCurrency.ltc)
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 14),
child: GestureDetector(
key: ValueKey('send_page_unspent_coin_button_key'),
onTap: () {
bool value =
widget.sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any;
sendViewModel.setAllowMwebCoins(!value);
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StandardCheckbox(
caption: S.of(context).litecoin_mweb_allow_coins,
captionColor: Colors.white,
borderColor: currentTheme.type == ThemeType.bright
? Colors.white
: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
iconColor: currentTheme.type == ThemeType.bright
? Colors.white
: Theme.of(context).primaryColor,
value:
widget.sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any,
onChanged: (bool? value) {
sendViewModel.setAllowMwebCoins(value ?? false);
},
),
],
),
),
),
),
),
],
),
)
],
),
),
);
}
@ -526,11 +570,11 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
Future<void> pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(sendViewModel.walletType);
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
final customItemIndex = sendViewModel.getCustomPriorityIndex(items);
final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items);
final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null;
final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp<void>(
context: context,
@ -541,7 +585,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
return Picker(
items: items,
displayItem: (TransactionPriority priority) =>
sendViewModel.displayFeeRate(priority, customFeeRate?.round()),
sendViewModel.feesViewModel.displayFeeRate(priority, customFeeRate?.round()),
selectedAtIndex: selectedIdx,
customItemIndex: customItemIndex,
maxValue: maxCustomFeeRate,
@ -552,7 +596,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
sliderValue: customFeeRate,
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
onItemSelected: (TransactionPriority priority) {
sendViewModel.setTransactionPriority(priority);
sendViewModel.feesViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(priority));
},
);
@ -560,7 +604,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
);
},
);
if (isBitcoinWallet) sendViewModel.customBitcoinFeeRate = customFeeRate!.round();
if (isBitcoinWallet) sendViewModel.feesViewModel.customBitcoinFeeRate = customFeeRate!.round();
}
void _presentPicker(BuildContext context) {

View file

@ -1,4 +1,4 @@
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart';
@ -15,23 +15,92 @@ class TrocadorProvidersPage extends BasePage {
@override
Widget body(BuildContext context) {
final availableProviders = TrocadorExchangeProvider.availableProviders;
final providerStates = trocadorProvidersViewModel.providerStates;
return Container(
padding: EdgeInsets.only(top: 10),
child: ListView.builder(
itemCount: availableProviders.length,
itemBuilder: (_, index) {
String provider = availableProviders[index];
return Observer(
builder: (_) => SettingsSwitcherCell(
title: provider,
value: providerStates[provider] ?? false,
onValueChange: (BuildContext _, bool value) {
trocadorProvidersViewModel.toggleProviderState(provider);
}));
child: Observer(
builder: (_) {
if (trocadorProvidersViewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
var providerStates = trocadorProvidersViewModel.providerStates;
final providerRatings = trocadorProvidersViewModel.providerRatings;
if (providerStates.isEmpty) {
return Center(child: Text(S.of(context).no_providers_available));
}
return ListView.builder(
itemCount: providerStates.length,
itemBuilder: (_, index) {
final providerName = providerStates.keys.elementAt(index);
final providerEnabled = providerStates[providerName] ?? true;
return SettingsSwitcherCell(
title: providerName,
value: providerEnabled,
leading: Badge(
title: 'KYC \nRATING',
subTitle: providerRatings[providerName] ?? 'N/A',
textColor: Colors.white,
backgroundColor: Theme.of(context).primaryColor,
),
onValueChange: (BuildContext _, value) =>
trocadorProvidersViewModel.toggleProviderState(providerName),
);
},
);
},
),
);
}
}
class Badge extends StatelessWidget {
Badge({required this.textColor, required this.backgroundColor, required this.title, required this.subTitle});
final String title;
final String subTitle;
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: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: textColor,
fontSize: 7,
fontWeight: FontWeight.w600,
),
),
VerticalDivider(
color: textColor,
thickness: 1,
),
Text(
subTitle,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
}
}

View file

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:flutter/material.dart';
@ -30,7 +29,13 @@ class SettingsSwitcherCell extends StandardListRow {
height: 56,
padding: EdgeInsets.only(left: 12, right: 12),
child: TextButton(
onPressed: () => onValueChange?.call(context, !value),
onPressed: () {
if (onTap != null) {
onTap!.call(context);
} else {
onValueChange?.call(context, !value);
}
},
style: ButtonStyle(
//backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor),
shape: MaterialStateProperty.all(
@ -45,7 +50,7 @@ class SettingsSwitcherCell extends StandardListRow {
children: <Widget>[
if (leading != null) leading,
buildCenter(context, hasLeftOffset: leading != null),
if (trailing != null) trailing,
trailing,
],
),
),

View file

@ -26,6 +26,8 @@ class SupportChatPage extends BasePage {
secureStorage,
supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!),
appVersion: supportViewModel.appVersion,
fiatApiMode: supportViewModel.fiatApiMode,
walletType: supportViewModel.walletType,
);
return Container();
},

View file

@ -11,11 +11,15 @@ class ChatwootWidget extends StatefulWidget {
this.secureStorage, {
required this.supportUrl,
required this.appVersion,
required this.fiatApiMode,
required this.walletType,
});
final SecureStorage secureStorage;
final String supportUrl;
final String appVersion;
final String fiatApiMode;
final String walletType;
@override
ChatwootWidgetState createState() => ChatwootWidgetState();
@ -43,8 +47,11 @@ class ChatwootWidgetState extends State<ChatwootWidget> {
if (eventType == 'loaded') {
final authToken = parsedMessage["config"]["authToken"];
_storeCookie(authToken as String);
_setCustomAttributes(
controller, {"app_version": widget.appVersion});
_setCustomAttributes(controller, {
"app_version": widget.appVersion,
"fiat_api_mode": widget.fiatApiMode,
"wallet_type": widget.walletType,
});
}
}
},

View file

@ -326,7 +326,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
),
);
}
Widget _buildBottomActionPanel({
required String titleForClipboard,
required String dataToCopy,

View file

@ -0,0 +1,202 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const _firstLayoutMaxHeight = 10000.0;
class PageViewHeightAdaptable extends StatefulWidget {
const PageViewHeightAdaptable({
super.key,
required this.controller,
required this.children,
}) : assert(children.length > 0, 'children must not be empty');
final PageController controller;
final List<Widget> children;
@override
State<PageViewHeightAdaptable> createState() => _PageViewHeightAdaptableState();
}
class _PageViewHeightAdaptableState extends State<PageViewHeightAdaptable> {
final _sizes = <int, Size>{};
@override
void didUpdateWidget(PageViewHeightAdaptable oldWidget) {
super.didUpdateWidget(oldWidget);
_sizes.clear();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.controller,
builder: (context, child) => _SizingContainer(
sizes: _sizes,
page: widget.controller.hasClients ? widget.controller.page ?? 0 : 0,
child: child!,
),
child: LayoutBuilder(
builder: (context, constraints) => PageView(
controller: widget.controller,
children: [
for (final (i, child) in widget.children.indexed)
Stack(
alignment: Alignment.topCenter,
clipBehavior: Clip.hardEdge,
children: [
SizedBox.fromSize(size: _sizes[i]),
Positioned(
left: 0,
top: 0,
right: 0,
child: _SizeAware(
child: child,
// don't setState, we'll use it in the layout phase
onSizeLaidOut: (size) {
_sizes[i] = size;
},
),
),
],
),
],
),
),
);
}
}
typedef _OnSizeLaidOutCallback = void Function(Size);
class _SizingContainer extends SingleChildRenderObjectWidget {
const _SizingContainer({
super.child,
required this.sizes,
required this.page,
});
final Map<int, Size> sizes;
final double page;
@override
_RenderSizingContainer createRenderObject(BuildContext context) {
return _RenderSizingContainer(
sizes: sizes,
page: page,
);
}
@override
void updateRenderObject(
BuildContext context,
_RenderSizingContainer renderObject,
) {
renderObject
..sizes = sizes
..page = page;
}
}
class _RenderSizingContainer extends RenderProxyBox {
_RenderSizingContainer({
RenderBox? child,
required Map<int, Size> sizes,
required double page,
}) : _sizes = sizes,
_page = page,
super(child);
Map<int, Size> _sizes;
Map<int, Size> get sizes => _sizes;
set sizes(Map<int, Size> value) {
if (_sizes == value) return;
_sizes = value;
markNeedsLayout();
}
double _page;
double get page => _page;
set page(double value) {
if (_page == value) return;
_page = value;
markNeedsLayout();
}
@override
void performLayout() {
if (child case final child?) {
child.layout(
constraints.copyWith(
minWidth: constraints.maxWidth,
minHeight: 0,
maxHeight: constraints.hasBoundedHeight ? null : _firstLayoutMaxHeight,
),
parentUsesSize: true,
);
final a = sizes[page.floor()]!;
final b = sizes[page.ceil()]!;
final height = lerpDouble(a.height, b.height, page - page.floor());
child.layout(
constraints.copyWith(minHeight: height, maxHeight: height),
parentUsesSize: true,
);
size = child.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
}
class _SizeAware extends SingleChildRenderObjectWidget {
const _SizeAware({
required Widget child,
required this.onSizeLaidOut,
}) : super(child: child);
final _OnSizeLaidOutCallback onSizeLaidOut;
@override
_RenderSizeAware createRenderObject(BuildContext context) {
return _RenderSizeAware(
onSizeLaidOut: onSizeLaidOut,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSizeAware renderObject) {
renderObject.onSizeLaidOut = onSizeLaidOut;
}
}
class _RenderSizeAware extends RenderProxyBox {
_RenderSizeAware({
RenderBox? child,
required _OnSizeLaidOutCallback onSizeLaidOut,
}) : _onSizeLaidOut = onSizeLaidOut,
super(child);
_OnSizeLaidOutCallback? _onSizeLaidOut;
_OnSizeLaidOutCallback get onSizeLaidOut => _onSizeLaidOut!;
set onSizeLaidOut(_OnSizeLaidOutCallback value) {
if (_onSizeLaidOut == value) return;
_onSizeLaidOut = value;
markNeedsLayout();
}
@override
void performLayout() {
super.performLayout();
onSizeLaidOut(
getDryLayout(
constraints.copyWith(maxHeight: double.infinity),
),
);
}
}

View file

@ -1,30 +1,37 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class ListRow extends StatelessWidget {
ListRow(
{required this.title,
required this.value,
this.titleFontSize = 14,
this.valueFontSize = 16,
this.image});
ListRow({
required this.title,
required this.value,
this.titleFontSize = 14,
this.valueFontSize = 16,
this.image,
this.padding,
this.color,
this.hintTextColor,
this.mainTextColor
});
final String title;
final String value;
final double titleFontSize;
final double valueFontSize;
final Image? image;
final EdgeInsetsGeometry? padding;
final Color? color;
final Color? hintTextColor;
final Color? mainTextColor;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: Theme.of(context).colorScheme.background,
color: color ?? Theme.of(context).colorScheme.background,
child: Padding(
padding:
const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
padding: padding ?? const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@ -32,7 +39,7 @@ class ListRow extends StatelessWidget {
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
color: hintTextColor ?? Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
textAlign: TextAlign.left),
Padding(
padding: const EdgeInsets.only(top: 12),
@ -46,7 +53,7 @@ class ListRow extends StatelessWidget {
style: TextStyle(
fontSize: valueFontSize,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
color: mainTextColor ?? Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
),
image != null
? Padding(

View file

@ -9,6 +9,7 @@ class StandardCheckbox extends StatelessWidget {
this.gradientBackground = false,
this.borderColor,
this.iconColor,
this.captionColor,
required this.onChanged});
final bool value;
@ -16,6 +17,7 @@ class StandardCheckbox extends StatelessWidget {
final bool gradientBackground;
final Color? borderColor;
final Color? iconColor;
final Color? captionColor;
final Function(bool) onChanged;
@override
@ -68,7 +70,7 @@ class StandardCheckbox extends StatelessWidget {
fontSize: 16.0,
fontFamily: 'Lato',
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
color: captionColor ?? Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),

View file

@ -20,6 +20,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = true,
displayLetsExchange = true,
displayStealthEx = true,
displayXOSwap = true,
displaySwapTrade = true;
@observable
@ -45,7 +46,7 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayChainflip;
@observable
bool displayThorChain;
@ -55,6 +56,9 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayStealthEx;
@observable
bool displayXOSwap;
@observable
bool displaySwapTrade;
@ -64,11 +68,12 @@ abstract class TradeFilterStoreBase with Store {
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayExolix &&
displayChainflip &&
displayThorChain &&
displayLetsExchange &&
displayStealthEx &&
displayXOSwap &&
displaySwapTrade;
@action
@ -107,8 +112,12 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.stealthEx:
displayStealthEx = !displayStealthEx;
break;
case ExchangeProviderDescription.swapTrade:
case ExchangeProviderDescription.xoSwap:
displayXOSwap = !displayXOSwap;
break;
case ExchangeProviderDescription.swapTrade:
displaySwapTrade = !displaySwapTrade;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@ -122,6 +131,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = false;
displayLetsExchange = false;
displayStealthEx = false;
displayXOSwap = false;
displaySwapTrade = false;
} else {
displayChangeNow = true;
@ -135,6 +145,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = true;
displayLetsExchange = true;
displayStealthEx = true;
displayXOSwap = true;
displaySwapTrade = true;
}
break;
@ -168,6 +179,7 @@ abstract class TradeFilterStoreBase with Store {
(displayLetsExchange &&
item.trade.provider == ExchangeProviderDescription.letsExchange) ||
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) ||
(displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) ||
(displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade))
.toList()
: _trades;

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
@ -224,12 +225,11 @@ abstract class SettingsStoreBase with Store {
if (initialZanoTransactionPriority != null) {
priority[WalletType.zano] = initialZanoTransactionPriority;
}
if (initialCakePayCountry != null) {
selectedCakePayCountry = initialCakePayCountry;
}
initializeTrocadorProviderStates();
reaction(
(_) => fiatCurrency,
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
@ -1708,16 +1708,47 @@ abstract class SettingsStoreBase with Store {
powNodes[walletType] = node;
}
void initializeTrocadorProviderStates() {
for (var provider in TrocadorExchangeProvider.availableProviders) {
final savedState = _sharedPreferences.getBool(provider) ?? true;
trocadorProviderStates[provider] = savedState;
@action
Future<void> updateAllTrocadorProviderStates(List<String> availableProviders) async {
final jsonKey = PreferencesKey.trocadorProviderStatesKey;
String? serializedData = await _sharedPreferences.getString(jsonKey);
if (serializedData == null) {
final Map<String, bool> migratedStates = {};
for (final provider in TrocadorExchangeProvider.availableProviders) {
final oldState = _sharedPreferences.getBool(provider) ?? true;
migratedStates[provider] = oldState;
}
trocadorProviderStates
..clear()
..addAll(migratedStates);
await saveMapToString(jsonKey, trocadorProviderStates);
} else {
final decodedMap = json.decode(serializedData) as Map<String, dynamic>;
final oldMap = decodedMap.map((k, v) => MapEntry(k, v == true));
final Map<String, bool> newStates = {
for (final provider in availableProviders) provider: oldMap[provider] ?? true
};
trocadorProviderStates
..clear()
..addAll(newStates);
await saveMapToString(jsonKey, trocadorProviderStates);
}
}
void saveTrocadorProviderState(String providerName, bool state) {
_sharedPreferences.setBool(providerName, state);
@action
Future<void> setTrocadorProviderState(String providerName, bool state) async {
trocadorProviderStates[providerName] = state;
await saveMapToString(PreferencesKey.trocadorProviderStatesKey, trocadorProviderStates);
}
Future<void> saveMapToString(String key, Map<String, bool> map) async {
String serializedData = json.encode(map);
await _sharedPreferences.setString(key, serializedData);
}
static Future<String?> _getDeviceName() async {

View file

@ -217,6 +217,7 @@ class ExceptionHandler {
"invalid signature",
"invalid password",
"NetworkImage._loadAsync",
"SSLV3_ALERT_BAD_RECORD_MAC",
// Temporary ignored, More context: Flutter secure storage reads the values as null some times
// probably when the device was locked and then opened on Cake
// this is solved by a restart of the app

View file

@ -78,6 +78,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
WalletType.ethereum,
WalletType.polygon,
WalletType.tron,
WalletType.solana,
WalletType.monero,
WalletType.wownero,
WalletType.zano,

View file

@ -152,6 +152,11 @@ abstract class DashboardViewModelBase with Store {
caption: ExchangeProviderDescription.stealthEx.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
FilterItem(
value: () => tradeFilterStore.displayXOSwap,
caption: ExchangeProviderDescription.xoSwap.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
FilterItem(
value: () => tradeFilterStore.displaySwapTrade,
caption: ExchangeProviderDescription.swapTrade.title,
@ -258,33 +263,34 @@ abstract class DashboardViewModelBase with Store {
_checkMweb();
});
connectMapToListWithTransform(
appStore.wallet!.transactionHistory.transactions,
transactions,
(TransactionInfo? transaction) => TransactionListItem(
transaction: transaction!,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore,
key: ValueKey(
'${_wallet.type.name}_transaction_history_item_${transaction.id}_key',
_transactionDisposer?.reaction.dispose();
_transactionDisposer = reaction(
(_) => appStore.wallet!.transactionHistory.transactions.values.toList(),
(List<TransactionInfo> txs) {
transactions.clear();
transactions.addAll(
txs.where((tx) {
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id;
}
return true;
}).map(
(tx) => TransactionListItem(
transaction: tx,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore,
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
),
), filter: (TransactionInfo? transaction) {
if (transaction == null) {
return false;
}
final wallet = _wallet;
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(transaction) ==
monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getTransactionInfoAccountId(transaction) ==
wow.wownero!.getCurrentAccount(wallet).id;
}
return true;
});
),
);
}
);
if (hasSilentPayments) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
@ -583,6 +589,8 @@ abstract class DashboardViewModelBase with Store {
ReactionDisposer? _onMoneroBalanceChangeReaction;
ReactionDisposer? _transactionDisposer;
@computed
bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(wallet.type);
@ -687,32 +695,34 @@ abstract class DashboardViewModelBase with Store {
);
}
connectMapToListWithTransform(
appStore.wallet!.transactionHistory.transactions,
transactions,
(TransactionInfo? transaction) => TransactionListItem(
transaction: transaction!,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore,
key: ValueKey(
'${wallet.type.name}_transaction_history_item_${transaction.id}_key',
_transactionDisposer?.reaction.dispose();
_transactionDisposer = reaction(
(_) => appStore.wallet!.transactionHistory.transactions.values.toList(),
(List<TransactionInfo> txs) {
transactions.clear();
transactions.addAll(
txs.where((tx) {
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getTransactionInfoAccountId(tx) == wow.wownero!.getCurrentAccount(wallet).id;
}
return true;
}).map(
(tx) => TransactionListItem(
transaction: tx,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore,
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
),
), filter: (TransactionInfo? tx) {
if (tx == null) {
return false;
}
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getTransactionInfoAccountId(tx) ==
wow.wownero!.getCurrentAccount(wallet).id;
}
return true;
});
),
);
}
);
}
@action

View file

@ -2,8 +2,10 @@ import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
@ -19,7 +21,8 @@ abstract class NFTViewModelBase with Store {
NFTViewModelBase(this.appStore, this.bottomSheetService)
: isLoading = false,
isImportNFTLoading = false,
nftAssetByWalletModels = ObservableList() {
nftAssetByWalletModels = ObservableList(),
solanaNftAssetModels = ObservableList() {
getNFTAssetByWallet();
reaction((_) => appStore.wallet, (_) => getNFTAssetByWallet());
@ -36,31 +39,44 @@ abstract class NFTViewModelBase with Store {
ObservableList<NFTAssetModel> nftAssetByWalletModels;
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
@action
Future<void> getNFTAssetByWallet() async {
if (!isEVMCompatibleChain(appStore.wallet!.type)) return;
final walletType = appStore.wallet!.type;
if (!isNFTACtivatedChain(walletType)) return;
final walletAddress = appStore.wallet!.walletInfo.address;
log('Fetching wallet NFTs for $walletAddress');
final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
final chainName = getChainNameBasedOnWalletType(walletType);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
// the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response.
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/$walletAddress/nft',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
);
Uri uri;
if (walletType == WalletType.solana) {
uri = Uri.https(
'solana-gateway.moralis.io',
'/account/$chainName/$walletAddress/nft',
);
} else {
uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/$walletAddress/nft',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
);
}
try {
isLoading = true;
@ -73,13 +89,30 @@ abstract class NFTViewModelBase with Store {
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
final decodedResponse = jsonDecode(response.body);
final result = WalletNFTsResponseModel.fromJson(decodedResponse).result ?? [];
if (walletType == WalletType.solana) {
final results = await Future.wait(
(decodedResponse as List<dynamic>).map(
(x) {
final data = x as Map<String, dynamic>;
final mint = data['mint'] as String? ?? '';
return getSolanaNFTDetails(mint, chainName);
},
).toList(),
);
nftAssetByWalletModels.clear();
solanaNftAssetModels.clear();
nftAssetByWalletModels.addAll(result);
solanaNftAssetModels.addAll(results);
} else {
final result =
WalletNFTsResponseModel.fromJson(decodedResponse as Map<String, dynamic>).result ?? [];
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
}
isLoading = false;
} catch (e) {
@ -94,51 +127,76 @@ abstract class NFTViewModelBase with Store {
}
}
Future<SolanaNFTAssetModel> getSolanaNFTDetails(String address, String chainName) async {
final uri = Uri.https(
'solana-gateway.moralis.io',
'/nft/$chainName/$address/metadata',
);
final response = await http.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
return SolanaNFTAssetModel.fromJson(decodedResponse);
}
@action
Future<void> importNFT(String tokenAddress, String tokenId) async {
Future<void> importNFT(String tokenAddress, String? tokenId) async {
final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/nft/$tokenAddress/$tokenId',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
);
try {
isImportNFTLoading = true;
final response = await http.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
if (appStore.wallet!.type == WalletType.solana) {
final result = await getSolanaNFTDetails(tokenAddress, chainName);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
solanaNftAssetModels.add(result);
} else {
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/nft/$tokenAddress/$tokenId',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
);
final nftAsset = NFTAssetModel.fromJson(decodedResponse);
final response = await http.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
nftAssetByWalletModels.add(nftAsset);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
isImportNFTLoading = false;
final nftAsset = NFTAssetModel.fromJson(decodedResponse);
nftAssetByWalletModels.add(nftAsset);
}
} catch (e) {
isImportNFTLoading = false;
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: e.toString(),
),
);
} finally {
isImportNFTLoading = false;
}
}
}

View file

@ -11,10 +11,13 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
@ -27,14 +30,16 @@ part 'exchange_trade_view_model.g.dart';
class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel;
abstract class ExchangeTradeViewModelBase with Store {
ExchangeTradeViewModelBase(
{required this.wallet,
required this.trades,
required this.tradesStore,
required this.sendViewModel})
: trade = tradesStore.trade!,
ExchangeTradeViewModelBase({
required this.wallet,
required this.trades,
required this.tradesStore,
required this.sendViewModel,
required this.feesViewModel,
}) : trade = tradesStore.trade!,
isSendable = _checkIfCanSend(tradesStore, wallet),
items = ObservableList<ExchangeTradeItem>() {
setUpOutput();
switch (trade.provider) {
case ExchangeProviderDescription.changeNow:
_provider =
@ -64,6 +69,8 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades);
break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
}
_updateItems();
@ -78,6 +85,9 @@ abstract class ExchangeTradeViewModelBase with Store {
final Box<Trade> trades;
final TradesStore tradesStore;
final SendViewModel sendViewModel;
final FeesViewModel feesViewModel;
late Output output;
@observable
Trade trade;
@ -109,16 +119,18 @@ abstract class ExchangeTradeViewModelBase with Store {
Timer? timer;
@action
Future<void> confirmSending() async {
if (!isSendable) return;
void setUpOutput() {
sendViewModel.clearOutputs();
final output = sendViewModel.outputs.first;
output = sendViewModel.outputs.first;
output.address = trade.inputAddress ?? '';
output.setCryptoAmount(trade.amount);
if (_provider is ThorChainExchangeProvider) output.memo = trade.memo;
if (trade.isSendAll == true) output.sendAll = true;
}
@action
Future<void> confirmSending() async {
if (!isSendable) return;
sendViewModel.selectedCryptoCurrency = trade.from;
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
if (_provider is ThorChainExchangeProvider) {
@ -151,7 +163,7 @@ abstract class ExchangeTradeViewModelBase with Store {
void _updateItems() {
final tagFrom =
tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : '';
items.clear();
@ -161,6 +173,8 @@ abstract class ExchangeTradeViewModelBase with Store {
title: "${trade.provider.title} ${S.current.id}",
data: '${trade.id}',
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
@ -168,28 +182,51 @@ abstract class ExchangeTradeViewModelBase with Store {
ExchangeTradeItem(
title: S.current.amount,
data: '${trade.amount} ${trade.from}',
isCopied: true,
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
ExchangeTradeItem(
title: S.current.estimated_receive_amount + ':',
title: S.current.you_will_receive_estimated_amount + ':',
data: '${tradesStore.trade?.receiveAmount} ${trade.to}',
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
ExchangeTradeItem(
title: S.current.send_to_this_address('${tradesStore.trade!.from}', tagFrom) + ':',
data: trade.inputAddress ?? '',
isCopied: true,
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
]);
if (trade.extraId != null) {
final title = trade.from == CryptoCurrency.xrp
? S.current.destination_tag
: trade.from == CryptoCurrency.xlm
? S.current.memo
: S.current.extra_id;
final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm;
items.add(ExchangeTradeItem(title: title, data: '${trade.extraId}', isCopied: true));
if (shouldAddExtraId) {
final title = trade.from == CryptoCurrency.xrp
? S.current.destination_tag
: trade.from == CryptoCurrency.xlm
? S.current.memo
: S.current.extra_id;
items.add(
ExchangeTradeItem(
title: title,
data: '${trade.extraId}',
isCopied: true,
isReceiveDetail: (trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm)
? false
: true,
isExternalSendDetail:
(trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm)
? true
: false,
),
);
}
}
items.add(
@ -197,6 +234,8 @@ abstract class ExchangeTradeViewModelBase with Store {
title: S.current.arrive_in_this_address('${tradesStore.trade!.to}', tagTo) + ':',
data: trade.payoutAddress ?? '',
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
}

View file

@ -7,6 +7,8 @@ import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
@ -21,12 +23,10 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
@ -43,8 +43,6 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -71,6 +69,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
this._settingsStore,
this.sharedPreferences,
this.contactListViewModel,
this.feesViewModel,
) : _cryptoNumberFormat = NumberFormat(),
isSendAllEnabled = false,
isFixedRateMode = false,
@ -116,7 +115,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
.toList());
_setAvailableProviders();
calculateBestRate();
autorun((_) {
if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) {
final trocadorProvider =
selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
as TrocadorExchangeProvider;
updateAllTrocadorProviderStates(trocadorProvider);
}
});
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate());
@ -144,9 +152,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_defineIsReceiveAmountEditable();
loadLimits();
reaction((_) => isFixedRateMode, (Object _) {
bestRate = 0.0;
loadLimits();
bestRate = 0;
calculateBestRate();
});
if (isElectrumWallet) {
@ -160,8 +167,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
wallet.type == WalletType.bitcoinCash;
bool get hideAddressAfterExchange =>
wallet.type == WalletType.monero ||
wallet.type == WalletType.wownero;
wallet.type == WalletType.monero || wallet.type == WalletType.wownero;
bool _useTorOnly;
final Box<Trade> trades;
@ -179,10 +185,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
SwapTradeExchangeProvider(),
LetsExchangeExchangeProvider(),
StealthExExchangeProvider(),
XOSwapExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
];
@observable
ExchangeProvider? provider;
@ -199,6 +207,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final List<ExchangeProvider> _tradeAvailableProviders = [];
Map<ExchangeProvider, Limits> _providerLimits = {};
@observable
ObservableList<ExchangeProvider> selectedProviders;
@ -306,27 +316,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bool get isMoneroWallet => wallet.type == WalletType.monero;
bool get isLowFee {
switch (wallet.type) {
case WalletType.monero:
case WalletType.wownero:
case WalletType.haven:
case WalletType.zano:
return transactionPriority == monero!.getMoneroTransactionPrioritySlow();
case WalletType.bitcoin:
return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow();
case WalletType.litecoin:
return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow();
case WalletType.ethereum:
return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow();
case WalletType.bitcoinCash:
return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow();
case WalletType.polygon:
return transactionPriority == polygon!.getPolygonTransactionPrioritySlow();
default:
return false;
}
}
List<CryptoCurrency> receiveCurrencies;
@ -338,6 +328,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final ContactListViewModel contactListViewModel;
final FeesViewModel feesViewModel;
@observable
double bestRate = 0.0;
@ -421,10 +413,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return true;
}
Future<void> calculateBestRate() async {
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
final _providers = _tradeAvailableProviders
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
return;
}
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount)
?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
final limits = _providerLimits[provider];
if (limits == null) return false;
if (limits.min != null && amount < limits.min!) return false;
if (limits.max != null && amount > limits.max!) return false;
return true;
}).toList();
final _providers = validProvidersForAmount
.where((element) => !isFixedRateMode || element.supportsFixedRate)
.toList();
@ -462,6 +470,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> loadLimits() async {
if (depositCurrency == receiveCurrency) {
limitsState = LimitsLoadedSuccessfully(limits: Limits(min: 0, max: 0));
return;
};
if (selectedProviders.isEmpty) return;
limitsState = LimitsIsLoading();
@ -473,23 +485,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
double? highestMax = 0.0;
try {
final result = await Future.wait(
selectedProviders.where((provider) => providersForCurrentPair().contains(provider)).map(
(provider) => provider
.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode,
)
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
.timeout(
Duration(seconds: 7),
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
),
),
);
final futures = selectedProviders
.where((provider) => providersForCurrentPair().contains(provider))
.map((provider) async {
final limits = await provider
.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode,
)
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
.timeout(
Duration(seconds: 7),
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
);
return MapEntry(provider, limits);
}).toList();
result.forEach((tempLimits) {
final entries = await Future.wait(futures);
_providerLimits = Map.fromEntries(entries);
_providerLimits.values.forEach((tempLimits) {
if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) {
lowestMin = tempLimits.min;
}
@ -514,10 +530,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
} else {
limitsState = LimitsLoadedFailure(error: 'Limits loading failed');
}
calculateBestRate();
}
@action
Future<void> createTrade() async {
if (depositCurrency == receiveCurrency) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: 'Can\'t exchange the same currency');
return;
}
if (isSendAllEnabled) {
await calculateDepositAllAmount();
final amount = double.tryParse(depositAmount);
@ -695,10 +719,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
void _onPairChange() {
depositAmount = '';
receiveAmount = '';
bestRate = 0.0;
loadLimits();
_setAvailableProviders();
bestRate = 0;
calculateBestRate();
}
void _initialPairBasedOnWallet() {
@ -792,9 +815,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
receiveAmount = '';
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
bestRate = 0.0;
loadLimits();
bestRate = 0;
calculateBestRate();
final Map<String, dynamic> exchangeProvidersSelection =
json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}")
@ -810,6 +832,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
);
}
@action
Future<void> updateAllTrocadorProviderStates(TrocadorExchangeProvider trocadorProvider) async {
try {
var providers = await trocadorProvider.fetchProviders();
var providerNames = providers.map((e) => e.name).toList();
await _settingsStore.updateAllTrocadorProviderStates(providerNames);
} catch (e) {
printV('Error updating trocador provider states: $e');
}
}
bool get isAvailableInSelected {
final providersForPair = providersForCurrentPair();
return selectedProviders
@ -823,35 +856,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
selectedProviders.where((provider) => providersForCurrentPair().contains(provider)));
}
@action
void setDefaultTransactionPriority() {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.wownero:
case WalletType.zano:
_settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic();
break;
case WalletType.bitcoin:
_settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium();
break;
case WalletType.litecoin:
_settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium();
break;
case WalletType.ethereum:
_settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority();
break;
case WalletType.bitcoinCash:
_settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority();
break;
case WalletType.polygon:
_settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority();
break;
default:
break;
}
}
void _setProviders() {
if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly)
providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList();
@ -966,4 +970,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return false;
}
}
double initialAmountByAssets (CryptoCurrency ticker) {
final amount = switch (ticker) {
CryptoCurrency.trx => 1000,
CryptoCurrency.nano => 10,
CryptoCurrency.zano => 10,
CryptoCurrency.wow => 1000,
CryptoCurrency.ada => 1000,
CryptoCurrency.dash => 10,
CryptoCurrency.rune => 10,
_ => 1
};
return amount.toDouble();
}
}

View file

@ -0,0 +1,191 @@
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:collection/collection.dart';
part 'fees_view_model.g.dart';
class FeesViewModel = FeesViewModelBase with _$FeesViewModel;
abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Store {
FeesViewModelBase(
AppStore appStore,
this.balanceViewModel,
) : _settingsStore = appStore.settingsStore,
super(appStore: appStore) {
if (wallet.type == WalletType.bitcoin &&
_settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) {
setTransactionPriority(bitcoinTransactionPriorityMedium);
}
final priority = _settingsStore.priority[wallet.type];
final priorities = priorityForWalletType(wallet.type);
if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) {
_settingsStore.priority[wallet.type] = priorities.first;
}
}
@computed
WalletType get walletType => wallet.type;
CryptoCurrency get currency => wallet.currency;
FiatCurrency get fiat => _settingsStore.fiatCurrency;
bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
final BalanceViewModel balanceViewModel;
TransactionPriority get transactionPriority {
final priority = _settingsStore.priority[wallet.type];
if (priority == null) {
throw Exception('Unexpected type ${wallet.type}');
}
return priority;
}
int? getCustomPriorityIndex(List<TransactionPriority> priorities) {
if (wallet.type == WalletType.bitcoin) {
final customItem = priorities
.firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom());
return customItem != null ? priorities.indexOf(customItem) : null;
}
return null;
}
int? get maxCustomFeeRate {
if (wallet.type == WalletType.bitcoin) {
return bitcoin!.getMaxCustomFeeRate(wallet);
}
return null;
}
bool get isLowFee {
switch (wallet.type) {
case WalletType.monero:
case WalletType.wownero:
case WalletType.haven:
case WalletType.zano:
return transactionPriority == monero!.getMoneroTransactionPrioritySlow();
case WalletType.bitcoin:
return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow();
case WalletType.litecoin:
return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow();
case WalletType.ethereum:
return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow();
case WalletType.bitcoinCash:
return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow();
case WalletType.polygon:
return transactionPriority == polygon!.getPolygonTransactionPrioritySlow();
default:
return false;
}
}
@computed
int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate;
void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value;
@computed
bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;
@computed
bool get hasFeesPriority =>
wallet.type != WalletType.nano &&
wallet.type != WalletType.banano &&
wallet.type != WalletType.solana &&
wallet.type != WalletType.tron;
@computed
bool get isElectrumWallet =>
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash;
String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name;
@computed
FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;
final SettingsStore _settingsStore;
@action
void setTransactionPriority(TransactionPriority priority) =>
_settingsStore.priority[wallet.type] = priority;
bool showAlertForCustomFeeRate() {
if (wallet.type != WalletType.bitcoin || isLowFee) {
return false;
}
if (transactionPriority != bitcoinTransactionPriorityCustom) {
return false;
}
final mediumRate = bitcoin!.getFeeRate(wallet, bitcoinTransactionPriorityMedium);
return customBitcoinFeeRate < mediumRate;
}
String displayFeeRate(dynamic priority, int? customValue) {
final _priority = priority as TransactionPriority;
if (wallet.type == WalletType.bitcoin) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue);
}
if (isElectrumWallet) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
}
return priority.toString();
}
TransactionPriority get bitcoinTransactionPriorityCustom =>
bitcoin!.getBitcoinTransactionPriorityCustom();
TransactionPriority get bitcoinTransactionPriorityMedium =>
bitcoin!.getBitcoinTransactionPriorityMedium();
@action
void setDefaultTransactionPriority() {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.wownero:
case WalletType.zano:
_settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic();
break;
case WalletType.bitcoin:
_settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium();
break;
case WalletType.litecoin:
_settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium();
break;
case WalletType.ethereum:
_settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority();
break;
case WalletType.bitcoinCash:
_settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority();
break;
case WalletType.polygon:
_settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority();
break;
default:
break;
}
}
}

View file

@ -18,6 +18,7 @@ import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart';
@ -76,8 +77,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
this.contactListViewModel,
this.transactionDescriptionBox,
this.ledgerViewModel,
this.unspentCoinsListViewModel, {
this.coinTypeToSpendFrom = UnspentCoinType.any,
this.unspentCoinsListViewModel,
this.feesViewModel, {
this.coinTypeToSpendFrom = UnspentCoinType.nonMweb,
}) : state = InitialExecutionState(),
currencies = appStore.wallet!.balance.keys.toList(),
selectedCryptoCurrency = appStore.wallet!.currency,
@ -89,16 +91,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
_settingsStore = appStore.settingsStore,
fiatFromSettings = appStore.settingsStore.fiatCurrency,
super(appStore: appStore) {
if (wallet.type == WalletType.bitcoin &&
_settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) {
setTransactionPriority(bitcoinTransactionPriorityMedium);
}
final priority = _settingsStore.priority[wallet.type];
final priorities = priorityForWalletType(wallet.type);
if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) {
_settingsStore.priority[wallet.type] = priorities.first;
}
outputs
.add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
@ -112,7 +104,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
ObservableList<Output> outputs;
final UnspentCoinType coinTypeToSpendFrom;
@observable
UnspentCoinType coinTypeToSpendFrom;
bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
@ -135,6 +128,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
addOutput();
}
@action
void setAllowMwebCoins(bool allow) {
if (wallet.type == WalletType.litecoin) {
coinTypeToSpendFrom = allow ? UnspentCoinType.any : UnspentCoinType.nonMweb;
}
}
@computed
bool get isBatchSending => outputs.length > 1;
@ -197,38 +197,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
FiatCurrency get fiat => _settingsStore.fiatCurrency;
TransactionPriority get transactionPriority {
final priority = _settingsStore.priority[wallet.type];
if (priority == null) {
throw Exception('Unexpected type ${wallet.type}');
}
return priority;
}
int? getCustomPriorityIndex(List<TransactionPriority> priorities) {
if (wallet.type == WalletType.bitcoin) {
final customItem = priorities
.firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom());
return customItem != null ? priorities.indexOf(customItem) : null;
}
return null;
}
int? get maxCustomFeeRate {
if (wallet.type == WalletType.bitcoin) {
return bitcoin!.getMaxCustomFeeRate(wallet);
}
return null;
}
@computed
int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate;
void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value;
CryptoCurrency get currency => wallet.currency;
Validator<String> get amountValidator =>
@ -291,16 +259,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash;
@computed
bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;
@computed
bool get hasFeesPriority =>
wallet.type != WalletType.nano &&
wallet.type != WalletType.banano &&
wallet.type != WalletType.solana &&
wallet.type != WalletType.tron;
@observable
CryptoCurrency selectedCryptoCurrency;
@ -323,6 +281,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final BalanceViewModel balanceViewModel;
final ContactListViewModel contactListViewModel;
final LedgerViewModel? ledgerViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore;
final Box<TransactionDescription> transactionDescriptionBox;
@ -523,10 +482,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
@action
void setTransactionPriority(TransactionPriority priority) =>
_settingsStore.priority[wallet.type] = priority;
Object _credentials([ExchangeProvider? provider]) {
final priority = _settingsStore.priority[wallet.type];
@ -544,14 +499,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return bitcoin!.createBitcoinTransactionCredentials(
outputs,
priority: priority!,
feeRate: customBitcoinFeeRate,
feeRate:feesViewModel. customBitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
case WalletType.litecoin:
return bitcoin!.createBitcoinTransactionCredentials(
outputs,
priority: priority!,
feeRate: customBitcoinFeeRate,
feeRate:feesViewModel. customBitcoinFeeRate,
// if it's an exchange flow then disable sending from mweb coins
coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom,
);
@ -589,32 +544,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
String displayFeeRate(dynamic priority, int? customValue) {
final _priority = priority as TransactionPriority;
if (walletType == WalletType.bitcoin) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue);
}
if (isElectrumWallet) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
}
return priority.toString();
}
bool _isEqualCurrency(String currency) =>
wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());
TransactionPriority get bitcoinTransactionPriorityCustom =>
bitcoin!.getBitcoinTransactionPriorityCustom();
TransactionPriority get bitcoinTransactionPriorityMedium =>
bitcoin!.getBitcoinTransactionPriorityMedium();
@action
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
@action

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:mobx/mobx.dart';
@ -6,9 +7,37 @@ part 'trocador_providers_view_model.g.dart';
class TrocadorProvidersViewModel = TrocadorProvidersViewModelBase with _$TrocadorProvidersViewModel;
abstract class TrocadorProvidersViewModelBase with Store {
TrocadorProvidersViewModelBase(this._settingsStore);
TrocadorProvidersViewModelBase(this._settingsStore, this.trocadorExchangeProvider) {
fetchTrocadorPartners();
}
final SettingsStore _settingsStore;
final TrocadorExchangeProvider trocadorExchangeProvider;
@observable
ObservableFuture<Map<String, bool>>? fetchProvidersFuture;
Map<String, String> providerRatings = {};
@computed
bool get isLoading => fetchProvidersFuture?.status == FutureStatus.pending;
@action
Future<void> fetchTrocadorPartners() async {
fetchProvidersFuture =
ObservableFuture(trocadorExchangeProvider.fetchProviders().then((providers) {
var providerNames = providers.map((e) => e.name).toList();
providerRatings = {
for (var provider in providers)
provider.name: provider.rating
};
return _settingsStore
.updateAllTrocadorProviderStates(providerNames)
.then((_) => _settingsStore.trocadorProviderStates);
}));
}
@computed
Map<String, bool> get providerStates => _settingsStore.trocadorProviderStates;
@ -16,6 +45,6 @@ abstract class TrocadorProvidersViewModelBase with Store {
@action
void toggleProviderState(String providerName) {
final currentState = providerStates[providerName] ?? false;
_settingsStore.saveTrocadorProviderState(providerName, !currentState);
_settingsStore.setTrocadorProviderState(providerName, !currentState);
}
}
}

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/settings/link_list_item.dart';
import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
@ -11,9 +12,10 @@ part 'support_view_model.g.dart';
class SupportViewModel = SupportViewModelBase with _$SupportViewModel;
abstract class SupportViewModelBase with Store {
final SettingsStore settingsStore;
final SettingsStore _settingsStore;
final AppStore _appStore;
SupportViewModelBase(this.settingsStore)
SupportViewModelBase(this._settingsStore, this._appStore)
: items = [
LinkListItem(
title: 'Email',
@ -120,7 +122,11 @@ abstract class SupportViewModelBase with Store {
}
String get appVersion =>
"${isMoneroOnly ? "Monero.com" : "Cake Wallet"} - ${settingsStore.appVersion}";
"${isMoneroOnly ? "Monero.com" : "Cake Wallet"} - ${_settingsStore.appVersion}";
String get fiatApiMode => _settingsStore.fiatApiMode.title;
String get walletType => _appStore.wallet?.type.name ?? 'Unknown';
List<SettingsListItem> items;
}

View file

@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
@ -72,6 +73,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades);
break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
break;
}
_updateItems();
@ -104,6 +108,8 @@ abstract class TradeDetailsViewModelBase with Store {
return 'https://stealthex.io/exchange/?id=${trade.id}';
case ExchangeProviderDescription.chainflip:
return 'https://scan.chainflip.io/channels/${trade.id}';
case ExchangeProviderDescription.xoSwap:
return 'https://orders.xoswap.com/${trade.id}';
}
return null;
}

View file

@ -576,11 +576,11 @@ abstract class TransactionDetailsViewModelBase with Store {
}
final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
final customItem = priorities
.firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom);
final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItem = priorities.firstWhereOrNull(
(element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom);
final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble();
final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
RBFListItems.add(
StandardPickerListItem(
@ -594,7 +594,7 @@ abstract class TransactionDetailsViewModelBase with Store {
selectedIdx: selectedItem,
customItemIndex: customItemIndex ?? 0,
displayItem: (dynamic priority, double sliderValue) =>
sendViewModel.displayFeeRate(priority, sliderValue.round()),
sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()),
onSliderChanged: (double newValue) =>
setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item, double sliderValue) {
@ -788,7 +788,8 @@ abstract class TransactionDetailsViewModelBase with Store {
final comment = tx.additionalInfo['comment'] as String?;
items.addAll([
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(title: 'Asset ID', value: tx.additionalInfo['assetId'] as String? ?? "Unknown asset id"),
StandartListItem(
title: 'Asset ID', value: tx.additionalInfo['assetId'] as String? ?? "Unknown asset id"),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
@ -798,5 +799,5 @@ abstract class TransactionDetailsViewModelBase with Store {
if (comment != null && comment.isNotEmpty)
StandartListItem(title: S.current.transaction_details_title, value: comment),
]);
}
}
}

View file

@ -119,9 +119,6 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/${
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.so"
COMPONENT Runtime)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/zano/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "zano_libwallet2_api_c.so"
COMPONENT Runtime)
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)

View file

@ -106,12 +106,19 @@ dependencies:
flutter_svg: ^2.0.9
polyseed: ^0.0.6
nostr_tools: ^1.0.9
solana: ^0.31.0+1
ledger_flutter_plus:
git:
url: https://github.com/vespr-wallet/ledger-flutter-plus
ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76
hashlib: ^1.19.2
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
dev_dependencies:
flutter_test:

View file

@ -148,6 +148,9 @@
"close": "يغلق",
"coin_control": "التحكم في العملة (اختياري)",
"cold_or_recover_wallet": "أضف محفظة للقراءة فقط من Cupcake أو محفظة باردة أو استعاد محفظة ورقية",
"collection_address": "عنوان التجميع",
"collection_description": "وصف المجموعة",
"collection_name": "اسم المجموعة",
"color_theme": "سمة اللون",
"commit_transaction_amount_fee": "تنفيذ الصفقة\nالمبلغ: ${amount}\nالرسوم: ${fee}",
"confirm": "تأكيد",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "* يرجى نسخ أو كتابة معرف هويتك الأعلى لحفظة.",
"exchange_result_write_down_trade_id": "يرجى نسخ أو كتابة معرّف العملية للمتابعة.",
"exchange_sync_alert_content": "يرجى الانتظار حتى تتم مزامنة محفظتك",
"exchange_trade_result_confirm": "بالضغط على إرسال من Cake Wallet ، ستقوم بإرسال ${fetchingLabel} ${from} من محفظتك التي تسمى ${walletName} إلى العنوان الموضح أدناه. أو يمكنك إرسالها من محفظتك الخارجية إلى العنوان / QR رمز الإرسال من صفحة تفاصيل المحفظة الخارجية. \n\n يرجى الضغط على أي من الأزرار للمتابعة أو العودة لتغيير المبالغ.",
"expired": "منتهي الصلاحية",
"expires": "تنتهي",
"expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ",
@ -394,6 +398,7 @@
"light_theme": "فاتح",
"litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي",
"litecoin_mweb": "mweb",
"litecoin_mweb_allow_coins": "السماح للعملات المعدنية MWEB",
"litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي",
"litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin",
"litecoin_mweb_dismiss": "رفض",
@ -429,6 +434,7 @@
"methods": " ﻕﺮﻃُ",
"min_amount": "الحد الأدنى: ${value}",
"min_value": "الحد الأدنى: ${value} ${currency}",
"mint_address": "عنوان النعناع",
"minutes_to_pin_code": "${minutes} دقيقة",
"mm": "MM",
"modify_2fa": "تعديل 2 عامل المصادقة",
@ -456,8 +462,10 @@
"new_wallet": "إنشاء محفظة جديدة",
"newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ",
"no_cards_found": "لم يتم العثور على بطاقات",
"no_extra_detail": "لا توجد تفاصيل إضافية متاحة",
"no_id_needed": "لا حاجة لID!",
"no_id_required": "لا ID مطلوب. اشحن وانفق في أي مكان",
"no_providers_available": "لا مقدمي الخدمات المتاحة",
"no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ",
"no_relays": "ﺕﻼﺣﺮﻤﻟﺍ ﻻ",
"no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ",
@ -674,6 +682,7 @@
"select_hw_account_below": "الرجاء تحديد حساب الاستعادة أدناه:",
"select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ",
"select_your_country": "الرجاء تحديد بلدك",
"selected_trocador_provider": "مزود تروكادور المختار",
"sell": "بيع",
"sell_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .Litecoin ﻭ",
"sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن",
@ -686,6 +695,8 @@
"send_error_minimum_value": "الحد الأدنى لقيمة المبلغ هو 0.01",
"send_estimated_fee": "الرسوم المقدرة:",
"send_fee": "الرسوم:",
"send_from_cake_wallet": "أرسل من محفظة الكيك",
"send_from_external_wallet": "أرسل من محفظة خارجية",
"send_name": "الأسم",
"send_new": "جديد",
"send_payment_id": "معرف عملية الدفع (اختياري)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "لديك الآن بطاقة ائتمان",
"you_pay": "انت تدفع",
"you_will_get": "حول الى",
"you_will_receive_estimated_amount": "سوف تتلقى(ooded )",
"you_will_send": "تحويل من",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "затвори",
"coin_control": "Управление на монетите (не е задължително)",
"cold_or_recover_wallet": "Добавете портфейл само за четене от Cupcake или студен портфейл или възстановете хартиен портфейл",
"collection_address": "Адрес на колекцията",
"collection_description": "Описание на колекцията",
"collection_name": "Име на колекцията",
"color_theme": "Цвят",
"commit_transaction_amount_fee": "Изпълняване на транзакция\nСума: ${amount}\nТакса: ${fee}",
"confirm": "Потвърждаване",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Please copy or write down your ID shown above.",
"exchange_result_write_down_trade_id": "Моля, запишете trade ID-то, за да продължите.",
"exchange_sync_alert_content": "Моля, изчакайте синхронизирането на Вашия портфейл",
"exchange_trade_result_confirm": "Натискайки изпращане от портфейла за торта, ще изпращате ${fetchingLabel} ${from} от вашия портфейл, наречен ${walletName} на адреса, показан по -долу. Или можете да изпратите от външния си портфейл до адреса / QR код на страницата за изпращане от външния портфейл.",
"expired": "Изтекло",
"expires": "Изтича",
"expiresOn": "Изтича на",
@ -394,6 +398,7 @@
"light_theme": "Светло",
"litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb",
"litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Позволете на MWeb монети",
"litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране",
"litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin",
"litecoin_mweb_dismiss": "Уволнение",
@ -429,6 +434,7 @@
"methods": "Методи",
"min_amount": "Мин: ${value}",
"min_value": "Мин: ${value} ${currency}",
"mint_address": "Адрес на мента",
"minutes_to_pin_code": "${minute} минути",
"mm": "мм",
"modify_2fa": "Модифициране на тортата 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Нов портфейл",
"newConnection": "Нова връзка",
"no_cards_found": "Не са намерени карти",
"no_extra_detail": "Няма налични допълнителни подробности",
"no_id_needed": "Без нужда от документ за самоличност!",
"no_id_required": "Без нужда от документ за самоличност. Използвайте навсякъде",
"no_providers_available": "Няма налични доставчици",
"no_relay_on_domain": "Няма реле за домейна на потребителя или релето не е налично. Моля, изберете реле, което да използвате.",
"no_relays": "Без релета",
"no_relays_message": "Намерихме запис Nostr NIP-05 за този потребител, но той не съдържа релета. Моля, инструктирайте получателя да добави релета към своя Nostr запис.",
@ -674,6 +682,7 @@
"select_hw_account_below": "Моля, изберете кой акаунт да възстановите по -долу:",
"select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.",
"select_your_country": "Моля, изберете вашата страна",
"selected_trocador_provider": "Избран доставчик на трокадор",
"sell": "Продаване",
"sell_alert_content": "В момента поддържаме само продажбата на Bitcoin, Ethereum и Litecoin. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum или Litecoin.",
"sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа",
@ -686,6 +695,8 @@
"send_error_minimum_value": "Минималната сума е 0.01",
"send_estimated_fee": "Изчислена такса:",
"send_fee": "Такса:",
"send_from_cake_wallet": "Изпратете от портфейла за торта",
"send_from_external_wallet": "Изпратете от външен портфейл",
"send_name": "Име",
"send_new": "Ново",
"send_payment_id": "Payment ID (не е задължително)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Вече имате дебитна карта",
"you_pay": "Вие плащате",
"you_will_get": "Обръщане в",
"you_will_receive_estimated_amount": "Ще получите(прогнозно )",
"you_will_send": "Обръщане от",
"yy": "гг"
}

View file

@ -148,6 +148,9 @@
"close": "zavřít",
"coin_control": "Volba mincí (nepovinné)",
"cold_or_recover_wallet": "Přidejte peněženku pouze pro čtení z Cupcake nebo studené peněženky nebo obnovte papírovou peněženku",
"collection_address": "Sběrná adresa",
"collection_description": "Popis sbírky",
"collection_name": "Název sbírky",
"color_theme": "Barevný motiv",
"commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}",
"confirm": "Potvrdit",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Prosím zkopírujte si, nebo zapište si výše uvedené ID.",
"exchange_result_write_down_trade_id": "Prosím zkopírujte si, nebo zapište si ID transakce (trade ID) pro pokračování.",
"exchange_sync_alert_content": "Prosím počkejte, dokud nebude vaše peněženka synchronizována",
"exchange_trade_result_confirm": "Stisknutím odeslání z peněženky Cake Wallet posíláte ${fetchingLabel} ${from} z peněženky nazvané ${walletName} na níže uvedenou adresu. Nebo můžete odeslat z externí peněženky na adresu / qr kód na stránce Odeslat ze stránky externích detailů peněženky. \n\n Stisknutím některého z tlačítek pokračujte nebo se vrátíte a změňte částky.",
"expired": "Vypršelo",
"expires": "Vyprší",
"expiresOn": "Vyprší dne",
@ -394,6 +398,7 @@
"light_theme": "Světlý",
"litecoin_enable_mweb_sync": "Povolit skenování MWeb",
"litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "Povolte mweb mince",
"litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování",
"litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí",
"litecoin_mweb_dismiss": "Propustit",
@ -429,6 +434,7 @@
"methods": "Metody",
"min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Adresa máty",
"minutes_to_pin_code": "${minute} minutách",
"mm": "MM",
"modify_2fa": "Upravte Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nová peněženka",
"newConnection": "Nové připojení",
"no_cards_found": "Žádné karty nenalezeny",
"no_extra_detail": "K dispozici nejsou žádné další podrobnosti",
"no_id_needed": "Žádné ID není potřeba!",
"no_id_required": "Žádní ID není potřeba. Dobijte si a utrácejte kdekoliv",
"no_providers_available": "Žádní poskytovatelé jsou k dispozici",
"no_relay_on_domain": "Pro doménu uživatele neexistuje přenos nebo je přenos nedostupný. Vyberte relé, které chcete použít.",
"no_relays": "Žádná relé",
"no_relays_message": "Pro tohoto uživatele jsme našli záznam Nostr NIP-05, který však neobsahuje žádná relé. Požádejte příjemce, aby přidal přenosy do svého záznamu Nostr.",
@ -674,6 +682,7 @@
"select_hw_account_below": "Níže vyberte, který účet chcete obnovit:",
"select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.",
"select_your_country": "Vyberte prosím svou zemi",
"selected_trocador_provider": "Vybraný poskytovatel Trocador",
"sell": "Prodat",
"sell_alert_content": "V současné době podporujeme pouze prodej bitcoinů, etherea a litecoinů. Vytvořte nebo přepněte na svou bitcoinovou, ethereum nebo litecoinovou peněženku.",
"sell_monero_com_alert_content": "Prodej Monero zatím není podporován",
@ -686,6 +695,8 @@
"send_error_minimum_value": "Minimální částka je 0,01",
"send_estimated_fee": "Odhadovaný poplatek:",
"send_fee": "Poplatek:",
"send_from_cake_wallet": "Odeslat z peněženky",
"send_from_external_wallet": "Odeslat z externí peněženky",
"send_name": "Název",
"send_new": "Nová",
"send_payment_id": "ID platby (nepovinné)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Nyní máte debetní kartu",
"you_pay": "Zaplatíte",
"you_will_get": "Směnit na",
"you_will_receive_estimated_amount": "Obdržíte(odhadovaný )",
"you_will_send": "Směnit z",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "Schließen",
"coin_control": "Coin Control (optional)",
"cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Wallet von Cupcake, eine Cold-Wallet oder eine Papier-Wallet hinzu.",
"collection_address": "Sammeladresse",
"collection_description": "Sammlung Beschreibung",
"collection_name": "Sammlungsname",
"color_theme": "Farbthema",
"commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}",
"confirm": "Bestätigen",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Bitte kopieren oder notieren Sie sich die oben gezeigte ID.",
"exchange_result_write_down_trade_id": "Bitte kopieren oder notieren Sie die Handels-ID, um fortzufahren.",
"exchange_sync_alert_content": "Bitte warten Sie, bis Ihre Wallet synchronisiert ist",
"exchange_trade_result_confirm": "Durch Drücken von Send von Cake Wallet senden Sie ${fetchingLabel} ${from} von Ihrer Brieftasche mit dem Namen ${walletName} an die unten gezeigte Adresse. Oder Sie können von Ihrem externen Brieftaschen an den Adresse \n\nqR auf der Seite \"Senden von externen Brieftaschen\" senden.",
"expired": "Abgelaufen",
"expires": "Läuft ab",
"expiresOn": "Läuft aus am",
@ -394,6 +398,7 @@
"light_theme": "Hell",
"litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen",
"litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "MWEB -Münzen zulassen",
"litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen",
"litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt",
"litecoin_mweb_dismiss": "Zurückweisen",
@ -429,6 +434,7 @@
"methods": "Methoden",
"min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Minzadresse",
"minutes_to_pin_code": "${minute} Minuten",
"mm": "MM",
"modify_2fa": "Cake 2FA ändern",
@ -456,8 +462,10 @@
"new_wallet": "Neue Wallet",
"newConnection": "Neue Verbindung",
"no_cards_found": "Keine Karten gefunden",
"no_extra_detail": "Keine zusätzlichen Details verfügbar",
"no_id_needed": "Keine ID erforderlich!",
"no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben",
"no_providers_available": "Keine Anbieter verfügbar",
"no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus.",
"no_relays": "Keine Relais",
"no_relays_message": "Wir haben einen Nostr NIP-05-Eintrag für diesen Benutzer gefunden, der jedoch keine Relays enthält. Bitte weisen Sie den Empfänger an, Relays zu seinem Nostr-Datensatz hinzuzufügen.",
@ -675,6 +683,7 @@
"select_hw_account_below": "Bitte wählen Sie unten, welches Konto unten wiederhergestellt werden soll:",
"select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.",
"select_your_country": "Bitte wählen Sie Ihr Land aus",
"selected_trocador_provider": "Ausgewählter Trocador -Anbieter",
"sell": "Verkaufen",
"sell_alert_content": "Wir unterstützen derzeit nur den Verkauf von Bitcoin, Ethereum und Litecoin. Bitte erstellen Sie Ihr Bitcoin-, Ethereum- oder Litecoin-Wallet oder wechseln Sie zu diesem.",
"sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt",
@ -687,6 +696,8 @@
"send_error_minimum_value": "Der Mindestbetrag ist 0,01",
"send_estimated_fee": "Geschätzte Gebühr:",
"send_fee": "Gebühr:",
"send_from_cake_wallet": "Senden Sie aus Kuchenbrieftasche",
"send_from_external_wallet": "Senden Sie aus der Außenschreibe",
"send_name": "Name",
"send_new": "Neu",
"send_payment_id": "Zahlungs-ID (optional)",
@ -1003,6 +1014,7 @@
"you_now_have_debit_card": "Sie haben jetzt eine Debitkarte",
"you_pay": "Sie bezahlen",
"you_will_get": "Konvertieren zu",
"you_will_receive_estimated_amount": "Sie erhalten(geschätzt )",
"you_will_send": "Konvertieren von",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "Close",
"coin_control": "Coin control (optional)",
"cold_or_recover_wallet": "Add a read-only wallet from Cupcake or a cold wallet or recover a paper wallet",
"collection_address": "Collection Address",
"collection_description": "Collection Description",
"collection_name": "Collection Name",
"color_theme": "Color theme",
"commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}",
"confirm": "Confirm",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Please copy or write down your ID shown above.",
"exchange_result_write_down_trade_id": "Please copy or write down the trade ID to continue.",
"exchange_sync_alert_content": "Please wait until your wallet is synchronized",
"exchange_trade_result_confirm": "By pressing send from cake wallet, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the address/QR code on the send from external wallet details page.\n\nPlease press either of the buttons to continue or go back to change the amounts.",
"expired": "Expired",
"expires": "Expires",
"expiresOn": "Expires on",
@ -394,6 +398,7 @@
"light_theme": "Light",
"litecoin_enable_mweb_sync": "Enable MWEB scanning",
"litecoin_mweb": "MWEB",
"litecoin_mweb_allow_coins": "Allow MWEB coins",
"litecoin_mweb_always_scan": "Set MWEB always scanning",
"litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin",
"litecoin_mweb_dismiss": "Dismiss",
@ -429,6 +434,7 @@
"methods": "Methods",
"min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Mint Address",
"minutes_to_pin_code": "${minute} minutes",
"mm": "MM",
"modify_2fa": "Modify Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "New Wallet",
"newConnection": "New Connection",
"no_cards_found": "No cards found",
"no_extra_detail": "No extra details available",
"no_id_needed": "No ID needed!",
"no_id_required": "No ID required. Top up and spend anywhere",
"no_providers_available": "No providers available",
"no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use.",
"no_relays": "No relays",
"no_relays_message": "We found a Nostr NIP-05 record for this user, but it does not contain any relays. Please instruct the recipient to add relays to their Nostr record.",
@ -675,6 +683,7 @@
"select_hw_account_below": "Please select which account to restore below:",
"select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.",
"select_your_country": "Please select your country",
"selected_trocador_provider": "selected Trocador provider",
"sell": "Sell",
"sell_alert_content": "We currently only support the sale of Bitcoin, Ethereum and Litecoin. Please create or switch to your Bitcoin, Ethereum or Litecoin wallet.",
"sell_monero_com_alert_content": "Selling Monero is not supported yet",
@ -687,6 +696,8 @@
"send_error_minimum_value": "Minimum value of amount is 0.01",
"send_estimated_fee": "Estimated fee:",
"send_fee": "Fee:",
"send_from_cake_wallet": "Send from Cake Wallet",
"send_from_external_wallet": "Send from External Wallet",
"send_name": "Name",
"send_new": "New",
"send_payment_id": "Payment ID (optional)",
@ -1001,6 +1012,7 @@
"you_now_have_debit_card": "You now have a debit card",
"you_pay": "You Pay",
"you_will_get": "Convert to",
"you_will_receive_estimated_amount": "You will receive (estimated)",
"you_will_send": "Convert from",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "Cerca",
"coin_control": "Control de monedas (opcional)",
"cold_or_recover_wallet": "Agregue una billetera de solo lectura de Cupcake o una billetera en frío o recupere una billetera de papel",
"collection_address": "Dirección de recolección",
"collection_description": "Descripción de la colección",
"collection_name": "Nombre de colección",
"color_theme": "Tema de color",
"commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}",
"confirm": "Confirmar",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Copie o escriba su identificación que se muestra arriba.",
"exchange_result_write_down_trade_id": "Por favor, copia o escribe el ID.",
"exchange_sync_alert_content": "Espere hasta que su billetera esté sincronizada",
"exchange_trade_result_confirm": "Al presionar el envío de la billetera de la torta, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra a continuación. O puede enviar desde su billetera externa a la dirección / Código QR en la página Enviar desde la página Detalles de la billetera externa. \n\n Por favor presione cualquiera de los botones para continuar o regrese para cambiar las cantidades.",
"expired": "Muerto",
"expires": "Caduca",
"expiresOn": "Expira el",
@ -394,6 +398,7 @@
"light_theme": "Ligero",
"litecoin_enable_mweb_sync": "Habilitar el escaneo mweb",
"litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Permitir monedas mweb",
"litecoin_mweb_always_scan": "Establecer mweb siempre escaneo",
"litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin",
"litecoin_mweb_dismiss": "Despedir",
@ -429,6 +434,7 @@
"methods": "Métodos",
"min_amount": "Mínimo: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Dirección menta",
"minutes_to_pin_code": "${minute} minutos",
"mm": "mm",
"modify_2fa": "Modificar 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nueva billetera",
"newConnection": "Nueva conexión",
"no_cards_found": "No se encuentran cartas",
"no_extra_detail": "No hay detalles adicionales disponibles",
"no_id_needed": "¡No se necesita identificación!",
"no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar",
"no_providers_available": "No hay proveedores disponibles",
"no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elige un relay para usar.",
"no_relays": "Sin relays",
"no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relay. Indica al destinatario que agregue retransmisiones a su registro Nostr.",
@ -675,6 +683,7 @@
"select_hw_account_below": "Seleccione qué cuenta restaurar a continuación:",
"select_sell_provider_notice": "Selecciona un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.",
"select_your_country": "Seleccione su país",
"selected_trocador_provider": "Proveedor de Trocador seleccionado",
"sell": "Vender",
"sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin, Ethereum y Litecoin. Cree o cambie a su billetera Bitcoin, Ethereum o Litecoin.",
"sell_monero_com_alert_content": "Aún no se admite la venta de Monero",
@ -687,6 +696,8 @@
"send_error_minimum_value": "El valor mínimo de la cantidad es 0.01",
"send_estimated_fee": "Tarifa estimada:",
"send_fee": "Cuota:",
"send_from_cake_wallet": "Enviar desde la billetera de pastel",
"send_from_external_wallet": "Enviar desde la billetera externa",
"send_name": "Nombre",
"send_new": "Nuevo",
"send_payment_id": "ID de pago (opcional)",
@ -1001,6 +1012,7 @@
"you_now_have_debit_card": "Ahora tienes una tarjeta de débito",
"you_pay": "Tú pagas",
"you_will_get": "Convertir a",
"you_will_receive_estimated_amount": "Recibirá(estimado )",
"you_will_send": "Convertir de",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "Fermer",
"coin_control": "Contrôle optionnel des pièces (coins)",
"cold_or_recover_wallet": "Ajoutez un portefeuille en lecture seule de Cupcake ou d'un portefeuille froid ou récupérez un portefeuille en papier",
"collection_address": "Adresse de collecte",
"collection_description": "Description de la collection",
"collection_name": "Nom de collection",
"color_theme": "Thème",
"commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}",
"confirm": "Confirmer",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Merci de copier ou écrire votre ID présenté ci-dessus.",
"exchange_result_write_down_trade_id": "Merci de copier ou d'écrire l'ID d'échange pour continuer.",
"exchange_sync_alert_content": "Merci d'attendre que votre portefeuille (wallet) soit synchronisé",
"exchange_trade_result_confirm": "En appuyant sur Send de Cake Wallet, vous enverrez ${fetchingLabel} ${from} de votre portefeuille appelé ${walletName} à l'adresse ci-dessous. Ou vous pouvez envoyer à partir de votre portefeuille externe à l'adresse / QR Code sur la page Send from External Wallet Détails. \n\n Veuillez appuyer sur l'un des boutons pour continuer ou revenir pour modifier les montants.",
"expired": "Expirée",
"expires": "Expire",
"expiresOn": "Expire le",
@ -394,6 +398,7 @@
"light_theme": "Clair",
"litecoin_enable_mweb_sync": "Activer la numérisation MWEB",
"litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Autoriser les pièces MWeb",
"litecoin_mweb_always_scan": "Définir MWEB Score Scanning",
"litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin",
"litecoin_mweb_dismiss": "Rejeter",
@ -429,6 +434,7 @@
"methods": "Méthodes",
"min_amount": "Min : ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Adresse de la menthe",
"minutes_to_pin_code": "${minute} minutes",
"mm": "MM",
"modify_2fa": "Modifier les paramètres Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nouveau Portefeuille (Wallet)",
"newConnection": "Nouvelle connexion",
"no_cards_found": "Pas de cartes trouvées",
"no_extra_detail": "Aucun détail supplémentaire disponible",
"no_id_needed": "Aucune pièce d'identité nécessaire !",
"no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où",
"no_providers_available": "Aucun fournisseur disponible",
"no_relay_on_domain": "Il n'existe pas de relais pour le domaine de l'utilisateur ou le relais n'est pas disponible. Veuillez choisir un relais à utiliser.",
"no_relays": "Pas de relais",
"no_relays_message": "Nous avons trouvé un enregistrement Nostr NIP-05 pour cet utilisateur, mais il ne contient aucun relais. Veuillez demander au destinataire d'ajouter des relais à son enregistrement Nostr.",
@ -674,6 +682,7 @@
"select_hw_account_below": "Veuillez sélectionner le compte à restaurer ci-dessous:",
"select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.",
"select_your_country": "Veuillez sélectionner votre pays",
"selected_trocador_provider": "fournisseur de trocador sélectionné",
"sell": "Vendre",
"sell_alert_content": "Nous ne prenons actuellement en charge que la vente de Bitcoin, Ethereum et Litecoin. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum ou Litecoin.",
"sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge",
@ -686,6 +695,8 @@
"send_error_minimum_value": "La valeur minimale du montant est 0.01",
"send_estimated_fee": "Estimation des frais :",
"send_fee": "Frais:",
"send_from_cake_wallet": "Envoyer du portefeuille à gâteau",
"send_from_external_wallet": "Envoyer du portefeuille externe",
"send_name": "Nom",
"send_new": "Nouveau",
"send_payment_id": "ID de paiement (optionnel)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Vous avez maintenant une carte de débit",
"you_pay": "Vous payez",
"you_will_get": "Convertir vers",
"you_will_receive_estimated_amount": "Vous recevrez ( estimé )",
"you_will_send": "Convertir depuis",
"yy": "AA"
}

View file

@ -148,6 +148,9 @@
"close": "Rufa",
"coin_control": "Sarrafa tsabar kuɗi (na zaɓi)",
"cold_or_recover_wallet": "Aara wani walat mai karanta-kawai Cupcake ko walat ɗin mai sanyi ko murmurewa takarda takarda",
"collection_address": "Adireshin tarin tarin",
"collection_description": "Bayanin tarin",
"collection_name": "Sunan tattara",
"color_theme": "Jigon launi",
"commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}",
"confirm": "Tabbatar",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Don Allah kwafi ko rubuta ID dake nuna sama.",
"exchange_result_write_down_trade_id": "Da fatan za a kwafa ko rubuta ID ɗin ciniki don ci gaba.",
"exchange_sync_alert_content": "Da fatan za a jira har sai an daidaita walat ɗin ku",
"exchange_trade_result_confirm": "Ta latsa Aika daga Wallet Caza, zaku aika ${fetchingLabel} ${from} daga walat ɗinku da ake kira ${walletName} zuwa adireshin da aka nuna a ƙasa. Ko zaka iya aika daga walat na waje zuwa adireshin / QR Code a kan Shafin Wallake Daga Shafin Tallar waje. \n\n Latsa don Allah a danna Duk da haka.",
"expired": "Karewa",
"expires": "Ya ƙare",
"expiresOn": "Yana ƙarewa",
@ -394,6 +398,7 @@
"light_theme": "Haske",
"litecoin_enable_mweb_sync": "Kunna binciken Mweb",
"litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Bada izinin Coins na Mweb",
"litecoin_mweb_always_scan": "Saita Mweb koyaushe",
"litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin",
"litecoin_mweb_dismiss": "Tuɓe \\ sallama",
@ -429,6 +434,7 @@
"methods": "Hanyoyin",
"min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Adireshin Mint",
"minutes_to_pin_code": "${minute} minti",
"mm": "MM",
"modify_2fa": "Gyara Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Sabuwar Wallet",
"newConnection": "Sabuwar Haɗi",
"no_cards_found": "Babu katunan da aka samo",
"no_extra_detail": "Babu ƙarin cikakkun bayanai",
"no_id_needed": "Babu ID da ake buƙata!",
"no_id_required": "Babu ID da ake buƙata. Yi da kuma ciyar a ko'ina",
"no_providers_available": "Babu masu samar da wadatar",
"no_relay_on_domain": "Babu gudun ba da sanda ga yankin mai amfani ko kuma ba a samu ba. Da fatan za a zaɓi gudun ba da sanda don amfani.",
"no_relays": "Babu relays",
"no_relays_message": "Mun sami rikodin Nostr NIP-05 don wannan mai amfani, amma ba ya ƙunshe da kowane relays. Da fatan za a umurci mai karɓa ya ƙara relays zuwa rikodin su na Nostr.",
@ -676,6 +684,7 @@
"select_hw_account_below": "Da fatan za a zabi wanda asusun zai gyara a ƙasa:",
"select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.",
"select_your_country": "Da fatan za a zabi ƙasarku",
"selected_trocador_provider": "Zabi mai bada TORACAD",
"sell": "sayar",
"sell_alert_content": "A halin yanzu muna tallafawa kawai siyar da Bitcoin, Ethereum da Litecoin. Da fatan za a ƙirƙiri ko canza zuwa walat ɗin ku na Bitcoin, Ethereum ko Litecoin.",
"sell_monero_com_alert_content": "Selling Monero bai sami ƙarshen mai bukatar samun ba",
@ -688,6 +697,8 @@
"send_error_minimum_value": "Mafi ƙarancin ƙimar adadin shine 0.01",
"send_estimated_fee": "Ƙimar kuɗi:",
"send_fee": "Kudin:",
"send_from_cake_wallet": "Aika daga Wallet Caza",
"send_from_external_wallet": "Aika daga walat na waje",
"send_name": "Sunan",
"send_new": "Sabon",
"send_payment_id": "ID na biyan kuɗi (optional)",
@ -1002,6 +1013,7 @@
"you_now_have_debit_card": "Yanzu kana da katin zare kudi",
"you_pay": "Ka Bayar",
"you_will_get": "Maida zuwa",
"you_will_receive_estimated_amount": "Za ku (karɓi )",
"you_will_send": "Maida daga",
"yy": "YY"
}

View file

@ -148,6 +148,9 @@
"close": "बंद करना",
"coin_control": "सिक्का नियंत्रण (वैकल्पिक)",
"cold_or_recover_wallet": "Cupcake या एक कोल्ड वॉलेट से एक रीड-ओनली वॉलेट जोड़ें या एक पेपर वॉलेट को पुनर्प्राप्त करें",
"collection_address": "संग्रह पता",
"collection_description": "संग्रह विवरण",
"collection_name": "संग्रह नाम",
"color_theme": "रंग विषय",
"commit_transaction_amount_fee": "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}",
"confirm": "की पुष्टि करें",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*कृपया ऊपर दिखाए गए अपने ID को कॉपी या लिख लें.",
"exchange_result_write_down_trade_id": "जारी रखने के लिए कृपया ट्रेड ID की प्रतिलिपि बनाएँ या लिखें.",
"exchange_sync_alert_content": "कृपया प्रतीक्षा करें जब तक आपका बटुआ सिंक्रनाइज़ नहीं किया जाता है",
"exchange_trade_result_confirm": "केक वॉलेट से भेजने से, आप नीचे दिखाए गए पते पर ${walletName} नामक अपने वॉलेट से ${fetchingLabel} ${from} भेजेंगे। या आप अपने बाहरी वॉलेट से एक्सटर्नल वॉलेट विवरण पृष्ठ से भेजने पर पते / QR कोड पर भेज सकते हैं। \n\n कृपया जारी रखने के लिए या वापस जाने के लिए वापस बटन में से किसी एक को दबाएं।",
"expired": "समय सीमा समाप्त",
"expires": "समाप्त हो जाता है",
"expiresOn": "पर समय सीमा समाप्त",
@ -394,6 +398,7 @@
"light_theme": "रोशनी",
"litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें",
"litecoin_mweb": "मावली",
"litecoin_mweb_allow_coins": "MWEB सिक्कों की अनुमति दें",
"litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें",
"litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है",
"litecoin_mweb_dismiss": "नकार देना",
@ -429,6 +434,7 @@
"methods": "तरीकों",
"min_amount": "न्यूनतम: ${value}",
"min_value": "मिन: ${value} ${currency}",
"mint_address": "टकसाल पता",
"minutes_to_pin_code": "${minute} मिनट",
"mm": "एमएम",
"modify_2fa": "केक 2FA संशोधित करें",
@ -456,8 +462,10 @@
"new_wallet": "नया बटुआ",
"newConnection": "नया कनेक्शन",
"no_cards_found": "कोई कार्ड नहीं मिला",
"no_extra_detail": "कोई अतिरिक्त विवरण उपलब्ध नहीं है",
"no_id_needed": "कोई आईडी नहीं चाहिए!",
"no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें",
"no_providers_available": "कोई प्रदाता उपलब्ध नहीं है",
"no_relay_on_domain": "उपयोगकर्ता के डोमेन के लिए कोई रिले नहीं है या रिले अनुपलब्ध है। कृपया उपयोग करने के लिए एक रिले चुनें।",
"no_relays": "कोई रिले नहीं",
"no_relays_message": "हमें इस उपयोगकर्ता के लिए एक Nostr NIP-05 रिकॉर्ड मिला, लेकिन इसमें कोई रिले नहीं है। कृपया प्राप्तकर्ता को अपने नॉस्ट्र रिकॉर्ड में रिले जोड़ने का निर्देश दें।",
@ -507,8 +515,8 @@
"paste": "पेस्ट करें",
"pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।",
"payment_id": "भुगतान ID: ",
"payment_was_received": "आपका भुगतान प्राप्त हुआ था।",
"Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
"payment_was_received": "आपका भुगतान प्राप्त हुआ था।",
"pending": " (अपूर्ण)",
"percentageOf": "${amount} का",
"pin_at_top": "शीर्ष पर ${token} पिन करें",
@ -676,6 +684,7 @@
"select_hw_account_below": "कृपया नीचे पुनर्स्थापित करने के लिए कौन सा खाता चुनें:",
"select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।",
"select_your_country": "कृपया अपने देश का चयन करें",
"selected_trocador_provider": "चयनित ट्रोकैडर प्रदाता",
"sell": "बेचना",
"sell_alert_content": "हम वर्तमान में केवल बिटकॉइन, एथेरियम और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम या लाइटकॉइन वॉलेट बनाएं या उसमें स्विच करें।",
"sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है",
@ -688,6 +697,8 @@
"send_error_minimum_value": "राशि का न्यूनतम मूल्य 0.01 है",
"send_estimated_fee": "अनुमानित शुल्क:",
"send_fee": "शुल्क:",
"send_from_cake_wallet": "केक वॉलेट से भेजें",
"send_from_external_wallet": "बाहरी बटुए से भेजें",
"send_name": "नाम",
"send_new": "नया",
"send_payment_id": "भुगतान ID (ऐच्छिक)",
@ -1002,6 +1013,7 @@
"you_now_have_debit_card": "अब आपके पास डेबिट कार्ड है",
"you_pay": "आप भुगतान करते हैं",
"you_will_get": "में बदलें",
"you_will_receive_estimated_amount": "आपको#अनुमानित ( प्राप्त होगा)",
"you_will_send": "से रूपांतरित करें",
"yy": "वाईवाई"
}

View file

@ -148,6 +148,9 @@
"close": "Zatvoriti",
"coin_control": "Kontrola novca (nije obavezno)",
"cold_or_recover_wallet": "Dodajte novčanik samo za čitanje od Cupcake ili hladnog novčanika ili oporavite papirni novčanik",
"collection_address": "Adresa prikupljanja",
"collection_description": "Zbirka Opis opisa",
"collection_name": "Naziv kolekcije",
"color_theme": "Shema boja",
"commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}",
"confirm": "Potvrdi",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Molimo kopirajte ili zapišite svoj ID prikazan ispod.",
"exchange_result_write_down_trade_id": "Molimo kopirajte ili zapišite transakcijski ID za nastavak.",
"exchange_sync_alert_content": "Molimo pričekajte dok se Vaš novčanik ne sinkronizira.",
"exchange_trade_result_confirm": "Pritiskom na WALLET WALLET, šaljet ćete ${fetchingLabel} ${from} iz vašeg novčanika nazvanu ${walletName} na adresu prikazanu u nastavku. Ili možete poslati iz svog vanjskog novčanika na adresu / QR kôd na stranici Slanje s Detalji vanjskih novčanika. \n\n Pritisnite bilo koji od gumba da biste nastavili ili se vratili da biste promijenili količine.",
"expired": "Isteklo",
"expires": "Ističe",
"expiresOn": "Istječe",
@ -394,6 +398,7 @@
"light_theme": "Svijetla",
"litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje",
"litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "Dopustite MWeb kovanice",
"litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje",
"litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije",
"litecoin_mweb_dismiss": "Odbaciti",
@ -429,6 +434,7 @@
"methods": "Metode",
"min_amount": "Minimalno: ${value}",
"min_value": "Min.: ${value} ${currency}",
"mint_address": "Adresa metvice",
"minutes_to_pin_code": "${minute} minuta",
"mm": "MM",
"modify_2fa": "Izmijenite tortu 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Novi novčanik",
"newConnection": "Nova veza",
"no_cards_found": "Nisu pronađene kartice",
"no_extra_detail": "Nema dostupnih dodatnih detalja",
"no_id_needed": "Nije potreban ID!",
"no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje",
"no_providers_available": "Nema dostupnih pružatelja usluga",
"no_relay_on_domain": "Ne postoji relej za korisničku domenu ili je relej nedostupan. Odaberite relej za korištenje.",
"no_relays": "Nema releja",
"no_relays_message": "Pronašli smo zapis Nostr NIP-05 za ovog korisnika, ali on ne sadrži nikakve releje. Uputite primatelja da doda releje u svoj Nostr zapis.",
@ -674,6 +682,7 @@
"select_hw_account_below": "Molimo odaberite koji će se račun vratiti u nastavku:",
"select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.",
"select_your_country": "Odaberite svoju zemlju",
"selected_trocador_provider": "Odabrani pružatelj usluga trokadora",
"sell": "Prodavati",
"sell_alert_content": "Trenutno podržavamo samo prodaju Bitcoina, Ethereuma i Litecoina. Izradite ili prijeđite na svoj Bitcoin, Ethereum ili Litecoin novčanik.",
"sell_monero_com_alert_content": "Prodaja Monera još nije podržana",
@ -686,6 +695,8 @@
"send_error_minimum_value": "Minimalna vrijednost iznosa je 0.01",
"send_estimated_fee": "Procijenjena naknada:",
"send_fee": "Naknada:",
"send_from_cake_wallet": "Pošaljite iz novčanika",
"send_from_external_wallet": "Pošaljite iz vanjskog novčanika",
"send_name": "Ime",
"send_new": "Novi",
"send_payment_id": "ID plaćanja (nije obvezno)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Sada imate debitnu karticu",
"you_pay": "Vi plaćate",
"you_will_get": "Razmijeni u",
"you_will_receive_estimated_amount": "Primit ćete(procijenjeno )",
"you_will_send": "Razmijeni iz",
"yy": "GG"
}

View file

@ -148,6 +148,9 @@
"close": "Փակել",
"coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)",
"cold_or_recover_wallet": "Cupcake կամ ցուրտ դրամապանակից ավելացնել միայն ընթերցված դրամապանակ կամ վերականգնել թղթի դրամապանակը",
"collection_address": "Հավաքածուի հասցե",
"collection_description": "Հավաքածուի նկարագրությունը",
"collection_name": "Հավաքածուի անուն",
"color_theme": "Գույների տեսք",
"commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}",
"confirm": "Հաստատել",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Խնդրում ենք պատճենել կամ գրել ձեր ID-ն վերևում",
"exchange_result_write_down_trade_id": "Խնդրում ենք պատճենել կամ գրել առևտրի ID-ն շարունակելու համար",
"exchange_sync_alert_content": "Խնդրում ենք սպասել մինչև ձեր հաշվեհամարը համաժամացվի",
"exchange_trade_result_confirm": "Տորթի դրամապանակից ուղարկելու միջոցով ձեր դրամապանակից ${fetchingLabel} կուղարկեք ${from} անունով ${walletName}, ստորեւ նշված հասցեով: Կամ դուք կարող եք ձեր արտաքին դրամապանակից ուղարկել / QR կոդ, արտաքին դրամապանակի մանրամասների էջից ուղարկելու համար: \n\n Խնդրում ենք սեղմել կոճակներից կամ վերադառնալ, գումարները շարունակելու կամ վերադառնալու համար:",
"expired": "Վավերականությունը լրացել է",
"expires": "Վավերականությունը լրանում է",
"expiresOn": "Վավերականությունը լրանում է",
@ -394,6 +398,7 @@
"light_theme": "Լուսավոր",
"litecoin_enable_mweb_sync": "Միացնել MWEB սկան",
"litecoin_mweb": "Մուեբ",
"litecoin_mweb_allow_coins": "Թույլ տվեք MWeb մետաղադրամներ",
"litecoin_mweb_always_scan": "Սահմանեք Mweb Միշտ սկանավորում",
"litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN",
"litecoin_mweb_dismiss": "Հեռացնել",
@ -429,6 +434,7 @@
"methods": "Մեթոդներ",
"min_amount": "Նվազը: ${value}",
"min_value": "Նվազը: ${value} ${currency}",
"mint_address": "Անանուխի հասցե",
"minutes_to_pin_code": "${minute} րոպե",
"mm": "ԱԱ",
"modify_2fa": "Փոփոխել Cake 2FA",
@ -456,6 +462,7 @@
"new_wallet": "Նոր դրամապանակ",
"newConnection": "Նոր կապ",
"no_cards_found": "Ոչ մի քարտ չի գտնվել",
"no_extra_detail": "Լրացուցիչ մանրամասներ մատչելի չեն",
"no_id_needed": "Ոչ մի փաստաթուղթ չի պահանջվում!",
"no_id_required": "Ոչ մի փաստաթուղթ չի պահանջվում։ Լրացրեք և ծախսեք ամենուր",
"no_relay_on_domain": "Տիրույթի համար ընդունող չկա կամ անհասանելի է։ Խնդրում ենք ընտրել ընդունող",
@ -686,6 +693,8 @@
"send_error_minimum_value": "Քանակի նվազագույն արժեքը 0.01 է",
"send_estimated_fee": "Գնահատված վարձը՝",
"send_fee": "Վարձը՝",
"send_from_cake_wallet": "Ուղարկել տորթի դրամապանակից",
"send_from_external_wallet": "Ուղարկել արտաքին դրամապանակից",
"send_name": "Անվանում",
"send_new": "Նոր",
"send_payment_id": "Վճարման ID (կամավոր)",
@ -1000,6 +1009,7 @@
"you_now_have_debit_card": "Դուք այժմ ունեք դեբետային քարտ",
"you_pay": "Դուք վճարում եք",
"you_will_get": "Ստացեք",
"you_will_receive_estimated_amount": "Դուք կստանաք ( գնահատված )",
"you_will_send": "Փոխանակեք",
"yy": "ՏՏ"
}

View file

@ -148,6 +148,9 @@
"close": "Menutup",
"coin_control": "Kontrol koin (opsional)",
"cold_or_recover_wallet": "Tambahkan dompet hanya baca dari Cupcake atau dompet dingin atau memulihkan dompet kertas",
"collection_address": "Alamat Koleksi",
"collection_description": "Deskripsi Koleksi",
"collection_name": "Nama Koleksi",
"color_theme": "Tema warna",
"commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}",
"confirm": "Konfirmasi",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Silakan salin atau tulis ID Anda yang ditampilkan di atas.",
"exchange_result_write_down_trade_id": "Silakan salin atau tulis ID perdagangan untuk melanjutkan.",
"exchange_sync_alert_content": "Silakan tunggu sampai dompet Anda tersinkronisasi",
"exchange_trade_result_confirm": "Dengan menekan kirim dari dompet kue, Anda akan mengirim ${fetchingLabel} ${from} dari dompet Anda yang disebut ${walletName} ke alamat yang ditunjukkan di bawah ini. Atau Anda dapat mengirim dari dompet eksternal Anda ke alamat / Kode QR pada halaman Dompet Dompet Kirim dari Eksternal. \n\n Tekan salah satu tombol untuk melanjutkan atau kembali untuk mengubah jumlah.",
"expired": "Kedaluwarsa",
"expires": "Kadaluarsa",
"expiresOn": "Kadaluarsa pada",
@ -394,6 +398,7 @@
"light_theme": "Terang",
"litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB",
"litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Izinkan koin mWeb",
"litecoin_mweb_always_scan": "Atur mWeb selalu memindai",
"litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin",
"litecoin_mweb_dismiss": "Membubarkan",
@ -429,6 +434,7 @@
"methods": "Metode",
"min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}",
"mint_address": "Alamat mint",
"minutes_to_pin_code": "${minute} menit",
"mm": "MM",
"modify_2fa": "Ubah Kue 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Dompet Baru",
"newConnection": "Koneksi Baru",
"no_cards_found": "Tidak ada kartu yang ditemukan",
"no_extra_detail": "Tidak ada detail tambahan yang tersedia",
"no_id_needed": "Tidak perlu ID!",
"no_id_required": "Tidak perlu ID. Isi ulang dan belanja di mana saja",
"no_providers_available": "Tidak ada penyedia yang tersedia",
"no_relay_on_domain": "Tidak ada relai untuk domain pengguna atau relai tidak tersedia. Silakan pilih relai yang akan digunakan.",
"no_relays": "Tidak ada relay",
"no_relays_message": "Kami menemukan catatan Nostr NIP-05 untuk pengguna ini, tetapi tidak berisi relay apa pun. Harap instruksikan penerima untuk menambahkan relay ke catatan Nostr mereka.",
@ -677,6 +685,7 @@
"select_hw_account_below": "Pilih akun mana yang akan dikembalikan di bawah ini:",
"select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.",
"select_your_country": "Pilih negara Anda",
"selected_trocador_provider": "Penyedia Trocador Terpilih",
"sell": "Jual",
"sell_alert_content": "Saat ini kami hanya mendukung penjualan Bitcoin, Ethereum, dan Litecoin. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, atau Litecoin Anda.",
"sell_monero_com_alert_content": "Menjual Monero belum didukung",
@ -689,6 +698,8 @@
"send_error_minimum_value": "Nilai minimum jumlah adalah 0.01",
"send_estimated_fee": "Biaya yang diperkirakan:",
"send_fee": "Biaya:",
"send_from_cake_wallet": "Kirim dari Dompet Kue",
"send_from_external_wallet": "Kirim dari dompet eksternal",
"send_name": "Nama",
"send_new": "Baru",
"send_payment_id": "ID Pembayaran (opsional)",
@ -1003,6 +1014,7 @@
"you_now_have_debit_card": "Anda sekarang memiliki kartu debit",
"you_pay": "Anda Membayar",
"you_will_get": "Konversi ke",
"you_will_receive_estimated_amount": "Anda akan menerima(estimasi )",
"you_will_send": "Konversi dari",
"yy": "YY"
}

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