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: # 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 # 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 # 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/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/Dockerfile
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34-ndk/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 # 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 \ ENV ANDROID_HOME=/opt/android-sdk-linux \
LANG=en_US.UTF-8 \ LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \
LANGUAGE=en_US:en LANGUAGE=en_US:en
# Set Android SDK paths
ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ ENV ANDROID_SDK_ROOT=$ANDROID_HOME \
PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator 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 \ RUN set -o xtrace \
&& cd /opt \ && cd /opt \
&& apt-get update \ && apt-get install -y --no-install-recommends --no-install-suggests \
&& apt-get upgrade -y \ # Core dependencies
&& apt-get install -y jq \ 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 \
&& 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 \
# for x86 emulators # for x86 emulators
&& apt-get install -y libxtst6 libnss3-dev libnspr4 libxss1 libatk-bridge2.0-0 libgtk-3-0 libgdk-pixbuf2.0-0 \ libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \
&& apt-get install -y -qq xxd \ # Linux desktop dependencies
&& apt-get install -y lftp \ clang cmake libgtk-3-dev ninja-build pkg-config \
&& apt-get install -qq -y sqlite3 libsqlite3-dev \
# linux desktop dependencies
&& apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev \
# monero_c dependencies # 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 # extra stuff for KVM
&& apt-get install -y udev qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils \ bridge-utils libvirt-clients libvirt-daemon-system qemu-kvm udev \
# for linux tests # Linux test dependencies
&& apt-get install -y xvfb network-manager ffmpeg x11-utils \ ffmpeg network-manager x11-utils xvfb psmisc \
# for aarch64-linux-gnu # aarch64-linux-gnu dependencies
&& apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ g++-aarch64-linux-gnu gcc-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/* \ && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ && sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \
&& locale-gen \ && locale-gen \
&& update-locale LANG=en_US.UTF-8 && update-locale LANG=en_US.UTF-8
# install nodejs for actions # Install nodejs for Github Actions
RUN apt-get update && \ RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \
apt-get install -y curl && \ apt-get install -y --no-install-recommends nodejs && \
curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
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 Go
ENV PATH=${PATH}:/usr/local/go/bin:${HOME}/go/bin ENV PATH=${PATH}:/usr/local/go/bin:${HOME}/go/bin
ENV GOROOT=/usr/local/go ENV GOROOT=/usr/local/go
ENV GOPATH=${HOME}/go ENV GOPATH=${HOME}/go
RUN go install golang.org/x/mobile/cmd/gomobile@latest RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\
RUN gomobile init 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 \ 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/ \ && mkdir -p ${ANDROID_HOME}/cmdline-tools/ \
&& unzip -q android-sdk-tools.zip -d ${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.email "czarek@cakewallet.com" \
&& git config --global user.name "CakeWallet CI" && 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 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 \ RUN yes | sdkmanager \
"platforms;android-$ANDROID_PLATFORM_VERSION" \ "platforms;android-$ANDROID_PLATFORM_VERSION" \
"build-tools;$ANDROID_BUILD_TOOLS_VERSION" \ "build-tools;$ANDROID_BUILD_TOOLS_VERSION" \
@ -107,26 +111,26 @@ RUN yes | sdkmanager \
"build-tools;33.0.0" \ "build-tools;33.0.0" \
"build-tools;35.0.0" "build-tools;35.0.0"
# Install extra NDK dependency for sp_scanner
ENV ANDROID_NDK_VERSION=27.2.12479018 ENV ANDROID_NDK_VERSION=27.2.12479018
# Extra ndk dependency for sp_scanner
RUN yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \ RUN yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \
"ndk;27.0.12077973" "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" \ RUN yes | sdkmanager "system-images;android-29;default;x86" \
"system-images;android-29;default;x86_64" \ "system-images;android-29;default;x86_64" \
"system-images;android-31;default;x86_64" \ "system-images;android-31;default;x86_64" \
"platforms;android-29" "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) && \ RUN (addgroup kvm || true) && \
adduser root kvm && \ adduser root kvm && \
mkdir -p /etc/udev/rules.d/ && \ 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 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} ENV PATH=${HOME}/.cargo/bin:${PATH}
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \
cargo install cargo-ndk && \ 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; \ 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; \ rustup target add --toolchain stable $target; \
done done
# Download and install Flutter
ENV HOME=${HOME} ENV HOME=${HOME}
ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION}
ENV FLUTTER_ROOT=$FLUTTER_HOME ENV FLUTTER_ROOT=$FLUTTER_HOME
ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin 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 git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \
&& yes | flutter doctor --android-licenses \
RUN yes | flutter doctor --android-licenses \
&& flutter doctor \ && flutter doctor \
&& chown -R root:root ${FLUTTER_HOME} && chown -R root:root ${FLUTTER_HOME}
# Download and pre-cache necessary Flutter artifacts to speed up builds
RUN flutter precache 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(); }).toList();
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
// sort the unconfirmed coins so that mweb coins are first: // sort the unconfirmed coins so that mweb coins are last:
availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1); availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1);
for (int i = 0; i < availableInputs.length; i++) { for (int i = 0; i < availableInputs.length; i++) {
final utx = availableInputs[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 version: 1.0.0
socks5_proxy: ^1.0.4 socks5_proxy: ^1.0.4
unorm_dart: ^0.3.0 unorm_dart: ^0.3.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
# tor: # tor:
# git: # git:
# url: https://github.com/cake-tech/tor.git # url: https://github.com/cake-tech/tor.git

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:solana/metaplex.dart';
part 'spl_token.g.dart'; part 'spl_token.g.dart';
@ -55,7 +54,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
required String mint, required String mint,
required String symbol, required String symbol,
required String mintAddress, required String mintAddress,
String? iconPath String? iconPath,
}) { }) {
return SPLToken( return SPLToken(
name: name, name: name,
@ -117,31 +116,3 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
@override @override
int get hashCode => mintAddress.hashCode; 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: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
solana: ^0.31.0+1
cw_core: cw_core:
path: ../cw_core path: ../cw_core
http: ^1.1.0 http: ^1.1.0
@ -21,6 +20,14 @@ dependencies:
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
bip32: ^2.0.0 bip32: ^2.0.0
hex: ^0.2.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: dev_dependencies:
flutter_test: flutter_test:

View file

@ -144,7 +144,7 @@ abstract class TronWalletBase
if (!hasKeysFile) rethrow; 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; final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme // Migrate wallet from the old scheme to then new .keys file scheme

View file

@ -17,7 +17,7 @@ dependencies:
path: ../cw_evm path: ../cw_evm
on_chain: on_chain:
git: git:
url: https://github.com/cake-tech/On_chain url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2 ref: cake-update-v2
blockchain_utils: blockchain_utils:
git: 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(); exchangeTradePageRobot.hasInformationDialog();
await exchangeTradePageRobot.onGotItButtonPressed(); await exchangeTradePageRobot.onGotItButtonPressed();
await exchangeTradePageRobot.onConfirmSendingButtonPressed(); await exchangeTradePageRobot.onSendFromCakeButtonPressed();
await exchangeTradePageRobot.handleConfirmSendResult(); await exchangeTradePageRobot.handleConfirmSendResult();

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:developer'; 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/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/chain_service/solana/solana_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
@ -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/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/connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; import 'package:cake_wallet/src/screens/wallet_connect/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:cw_core/utils/print_verbose.dart';
import 'package:solana/base58.dart'; import 'package:on_chain/solana/solana.dart';
import 'package:solana/solana.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import '../chain_service.dart'; import '../chain_service.dart';
import '../../wallet_connect_key_service.dart'; import '../../wallet_connect_key_service.dart';
@ -27,25 +29,19 @@ class SolanaChainServiceImpl implements ChainService {
final SolanaChainId reference; final SolanaChainId reference;
final SolanaClient solanaClient; final SolanaRPC solanaProvider;
final Ed25519HDKeyPair? ownerKeyPair; final SolanaPrivateKey? ownerPrivateKey;
SolanaChainServiceImpl({ SolanaChainServiceImpl({
required this.reference, required this.reference,
required this.wcKeyService, required this.wcKeyService,
required this.bottomSheetService, required this.bottomSheetService,
required this.wallet, required this.wallet,
required this.ownerKeyPair, required this.ownerPrivateKey,
required String webSocketUrl, required String formattedRPCUrl,
required Uri rpcUrl, SolanaRPC? solanaProvider,
SolanaClient? solanaClient, }) : solanaProvider = solanaProvider ?? SolanaRPC(SolanaRPCHTTPService(url: formattedRPCUrl)) {
}) : solanaClient = solanaClient ??
SolanaClient(
rpcUrl: rpcUrl,
websocketUrl: Uri.parse(webSocketUrl),
timeout: const Duration(minutes: 5),
) {
for (final String event in getEvents()) { for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event); wallet.registerEventEmitter(chainId: getChainId(), event: event);
} }
@ -110,26 +106,20 @@ class SolanaChainServiceImpl implements ChainService {
} }
try { try {
final message = // Convert transaction string to bytes
await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction); List<int> transactionBytes = base64Decode(solanaSignTx.transaction);
final sign = await ownerKeyPair?.signMessage( final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
message: message,
recentBlockhash: solanaSignTx.recentBlockhash ?? '', 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( bottomSheetService.queueBottomSheet(
isModalDismissible: true, isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget( widget: BottomSheetMessageDisplayWidget(
@ -161,10 +151,10 @@ class SolanaChainServiceImpl implements ChainService {
if (authError != null) { if (authError != null) {
return authError; return authError;
} }
Signature? sign; List<int>? sign;
try { try {
sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message)); sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
} catch (e) { } catch (e) {
printV(e); printV(e);
} }
@ -173,7 +163,7 @@ class SolanaChainServiceImpl implements ChainService {
return ''; return '';
} }
String signature = sign.toBase58(); final signature = Base58Encoder.encode(sign);
return signature; 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:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:on_chain/solana/solana.dart' hide Store;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.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) { for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
Uri rpcUri = node.uri; String formattedUrl;
String webSocketUrl = 'wss://${node.uriRaw}'; String protocolUsed = node.isSSL ? "https" : "http";
if (node.uriRaw == 'rpc.ankr.com') { if (node.uriRaw == 'rpc.ankr.com') {
String ankrApiKey = secrets.ankrApiKey; String ankrApiKey = secrets.ankrApiKey;
rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
} else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
String chainStackApiKey = secrets.chainStackApiKey; String chainStackApiKey = secrets.chainStackApiKey;
rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; } else {
formattedUrl = '$protocolUsed://${node.uriRaw}';
} }
SolanaChainServiceImpl( SolanaChainServiceImpl(
reference: cId, reference: cId,
rpcUrl: rpcUri, formattedRPCUrl: formattedUrl,
webSocketUrl: webSocketUrl,
wcKeyService: walletKeyService, wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler, bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet, 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/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.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/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_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_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/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_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'; 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/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.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/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:cw_core/receive_page_option.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/qr_view_data.dart';
@ -748,8 +755,9 @@ Future<void> setup({
getIt.get<ContactListViewModel>(), getIt.get<ContactListViewModel>(),
_transactionDescriptionBox, _transactionDescriptionBox,
getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null, getIt.get<AppStore>().wallet!.isHardwareWallet ? getIt.get<LedgerViewModel>() : null,
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb,
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom), getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
getIt.get<FeesViewModel>(),
), ),
); );
@ -897,7 +905,10 @@ Future<void> setup({
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!); 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(() { getIt.registerFactory(() {
return OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!, 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.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
getIt.registerFactory(() => ExchangeViewModel( getIt.registerFactory(
() => ExchangeViewModel(
getIt.get<AppStore>(), getIt.get<AppStore>(),
_tradesSource, _tradesSource,
getIt.get<ExchangeTemplateStore>(), getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(), getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore, getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(), 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!, wallet: getIt.get<AppStore>().wallet!,
trades: _tradesSource, trades: _tradesSource,
tradesStore: getIt.get<TradesStore>(), tradesStore: getIt.get<TradesStore>(),
sendViewModel: getIt.get<SendViewModel>())); sendViewModel: getIt.get<SendViewModel>(),
feesViewModel: getIt.get<FeesViewModel>(),
),
);
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>( getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) { (PaymentRequest? paymentRequest, __) {
@ -1051,6 +1077,9 @@ Future<void> setup({
getIt.registerFactory( getIt.registerFactory(
() => ExchangeTradePage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>())); () => ExchangeTradePage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(
() => ExchangeTradeExternalSendPage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>())); getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>()));
getIt.registerFactoryParam<WalletService, WalletType, void>((WalletType param1, __) { getIt.registerFactoryParam<WalletService, WalletType, void>((WalletType param1, __) {
@ -1260,7 +1289,8 @@ Future<void> setup({
getIt.registerFactoryParam<OrderDetailsPage, Order, void>( getIt.registerFactoryParam<OrderDetailsPage, Order, void>(
(Order order, _) => OrderDetailsPage(getIt.get<OrderDetailsViewModel>(param1: order))); (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>())); getIt.registerFactory(() => SupportPage(getIt.get<SupportViewModel>()));

View file

@ -86,6 +86,7 @@ class PreferencesKey {
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection'; static const exchangeProvidersSelection = 'exchange-providers-selection';
static const trocadorProviderStatesKey = 'trocador_provider_states';
static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status';
static const moneroSeedType = 'monero_seed_type'; static const moneroSeedType = 'monero_seed_type';
static const bitcoinSeedType = 'bitcoin_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'); ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
static const chainflip = static const chainflip =
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png'); 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}) { static ExchangeProviderDescription deserialize({required int raw}) {
switch (raw) { switch (raw) {
@ -63,6 +65,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return stealthEx; return stealthEx;
case 12: case 12:
return chainflip; return chainflip;
case 13:
return xoSwap;
default: default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
} }

View file

@ -19,7 +19,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
super(pairList: supportedPairs(_notSupported)); super(pairList: supportedPairs(_notSupported));
bool useTorOnly; bool useTorOnly;
final Map<String, bool> providerStates; Map<String, bool> providerStates;
static const List<String> availableProviders = [ static const List<String> availableProviders = [
'Swapter', 'Swapter',
@ -59,6 +59,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
static const createTradePath = '/new_trade'; static const createTradePath = '/new_trade';
static const tradePath = '/trade'; static const tradePath = '/trade';
static const coinPath = '/coin'; static const coinPath = '/coin';
static const providersListPath = '/exchanges';
String _lastUsedRateId; String _lastUsedRateId;
List<dynamic> _provider; List<dynamic> _provider;
@ -147,7 +149,14 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final rateId = responseJSON['trade_id'] as String? ?? ''; final rateId = responseJSON['trade_id'] as String? ?? '';
var quotes = responseJSON['quotes']['quotes'] as List; 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; if (rateId.isNotEmpty) _lastUsedRateId = rateId;
@ -190,20 +199,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId; params['id'] = _lastUsedRateId;
} }
String firstAvailableProvider = ''; if (_provider.isEmpty) {
for (var provider in _provider) {
if (providerStates.containsKey(provider) && providerStates[provider] == true) {
firstAvailableProvider = provider as String;
break;
}
}
if (firstAvailableProvider.isEmpty) {
throw Exception('No available provider is enabled'); throw Exception('No available provider is enabled');
} }
params['provider'] = firstAvailableProvider; params['provider'] = _provider.first as String;
final uri = await _getUri(createTradePath, params); final uri = await _getUri(createTradePath, params);
final response = await get(uri, headers: {'API-Key': apiKey}); 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) { String _networkFor(CryptoCurrency currency) {
switch (currency) { switch (currency) {
case CryptoCurrency.eth: 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': case 'waiting':
return waiting; return waiting;
case 'processing': case 'processing':
case 'inProgress':
return processing; return processing;
case 'waitingPayment': case 'waitingPayment':
return 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) { bool isWalletConnectCompatibleChain(WalletType walletType) {
switch (walletType) { switch (walletType) {
case WalletType.polygon: case WalletType.polygon:
@ -55,7 +66,7 @@ String getChainNameBasedOnWalletType(WalletType walletType) {
case WalletType.polygon: case WalletType.polygon:
return 'polygon'; return 'polygon';
case WalletType.solana: case WalletType.solana:
return 'solana'; return 'mainnet';
default: default:
return ''; 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_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_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_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/exchange_trade/exchange_trade_page.dart';
import 'package:cake_wallet/src/screens/faq/faq_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'; 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: case Routes.nftDetailsPage:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => NFTDetailsPage( builder: (_) => NFTDetailsPage(
nftAsset: settings.arguments as NFTAssetModel, arguments: settings.arguments as NFTDetailsPageArguments,
dashboardViewModel: getIt.get<DashboardViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>(),
), ),
); );
@ -813,6 +814,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<SeedVerificationPage>(), builder: (_) => getIt.get<SeedVerificationPage>(),
); );
case Routes.exchangeTradeExternalSendPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ExchangeTradeExternalSendPage>(),);
default: default:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => Scaffold( builder: (_) => Scaffold(

View file

@ -118,4 +118,5 @@ class Routes {
static const walletGroupDescription = '/wallet_group_description'; static const walletGroupDescription = '/wallet_group_description';
static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page'; static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page';
static const walletSeedVerificationPage = '/wallet_seed_verification_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; String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
@override @override
String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58(); String getPublicKey(WalletBase wallet) =>
(wallet as SolanaWallet).solanaPublicKey.toAddress().address;
@override
Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair;
Object createSolanaTransactionCredentials( Object createSolanaTransactionCredentials(
List<Output> outputs, { List<Output> outputs, {
required CryptoCurrency currency, required CryptoCurrency currency,

View file

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

View file

@ -23,13 +23,13 @@ class BalancePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Observer( return Observer(
builder: (context) { builder: (context) {
final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type);
return DefaultTabController( return DefaultTabController(
key: ValueKey<bool>(isEVMCompatible), key: ValueKey<bool>(isNFTActivated),
length: isEVMCompatible ? 2 : 1, length: isNFTActivated ? 2 : 1,
child: Column( child: Column(
children: [ children: [
if (isEVMCompatible) if (isNFTActivated)
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
@ -76,7 +76,7 @@ class BalancePage extends StatelessWidget {
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: [ children: [
CryptoBalanceWidget(dashboardViewModel: dashboardViewModel), 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/entities/wallet_nft_response.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.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/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_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:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class NFTDetailsPage extends BasePage { class NFTDetailsPage extends BasePage {
NFTDetailsPage({required this.dashboardViewModel, required this.nftAsset}); NFTDetailsPage({
required this.dashboardViewModel,
required this.arguments,
Key? key,
});
final DashboardViewModel dashboardViewModel; final DashboardViewModel dashboardViewModel;
final NFTAssetModel nftAsset; final NFTDetailsPageArguments arguments;
@override @override
bool get gradientBackground => true; bool get gradientBackground => true;
@override @override
Widget Function(BuildContext, Widget) get rootWrapper => Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
GradientBackground(scaffold: scaffold);
@override @override
bool get resizeToAvoidBottomInset => false; bool get resizeToAvoidBottomInset => false;
@ -30,15 +34,14 @@ class NFTDetailsPage extends BasePage {
@override @override
Widget get endDrawer => MenuWidget( Widget get endDrawer => MenuWidget(
dashboardViewModel, dashboardViewModel,
ValueKey('nft_details_page_menu_widget_key'), const ValueKey('nft_details_page_menu_widget_key'),
); );
@override @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
final menuButton = Image.asset( final menuButton = Image.asset(
'assets/images/menu.png', 'assets/images/menu.png',
color: color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
); );
return Container( return Container(
@ -58,119 +61,200 @@ class NFTDetailsPage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Container(
children: [ width: double.infinity,
Container( margin: const EdgeInsets.all(16),
width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 16),
margin: const EdgeInsets.all(16), decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(vertical: 16), borderRadius: BorderRadius.circular(30.0),
decoration: BoxDecoration( border: Border.all(
borderRadius: BorderRadius.circular(30.0), color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
border: Border.all( width: 1,
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 ?? '---',
),
],
),
), ),
], 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 { class _NFTSingleInfoTile extends StatelessWidget {
final String infoType;
final String infoValue;
const _NFTSingleInfoTile({ const _NFTSingleInfoTile({
required this.infoType, required this.infoType,
required this.infoValue, required this.infoValue,
}); });
final String infoType;
final String infoValue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final balanceTheme = Theme.of(context).extension<BalancePageTheme>()!;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
infoType, infoType,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Theme.of(context) color: balanceTheme.labelTextColor,
.extension<BalancePageTheme>()!
.labelTextColor,
height: 1, height: 1,
), ),
), ),
SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
infoValue, infoValue,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context) color: balanceTheme.assetTitleColor,
.extension<BalancePageTheme>()!
.assetTitleColor,
height: 1, 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/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/seed_widget_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:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
@ -68,7 +69,6 @@ class _ImportNFTPage extends BasePage {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
S.current.address, S.current.address,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -76,8 +76,7 @@ class _ImportNFTPage extends BasePage {
fontSize: 16, fontSize: 16,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
height: 1, height: 1,
), ),
), ),
@ -92,9 +91,7 @@ class _ImportNFTPage extends BasePage {
tokenAddressController.text = tokenAddress; tokenAddressController.text = tokenAddress;
} }
}, },
borderColor: Theme.of(context) borderColor: Theme.of(context).extension<CakeTextTheme>()!.textfieldUnderlineColor,
.extension<CakeTextTheme>()!
.textfieldUnderlineColor,
iconColor: Theme.of(context).primaryColor, iconColor: Theme.of(context).primaryColor,
placeholder: '0x...', placeholder: '0x...',
textStyle: TextStyle( textStyle: TextStyle(
@ -108,46 +105,45 @@ class _ImportNFTPage extends BasePage {
color: PaletteDark.darkCyanBlue, color: PaletteDark.darkCyanBlue,
), ),
), ),
if (nftViewModel.appStore.wallet!.type != WalletType.solana) ...[
SizedBox(height: 48), SizedBox(height: 48),
Text( Text(
S.current.tokenID, S.current.tokenID,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor, color: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
height: 1, height: 1,
),
), ),
), AddressTextField(
AddressTextField( controller: tokenIDController,
controller: tokenIDController, options: [AddressTextFieldOption.paste],
options: [AddressTextFieldOption.paste], onPushPasteButton: (context) async {
onPushPasteButton: (context) async { final clipboard = await Clipboard.getData('text/plain');
final clipboard = await Clipboard.getData('text/plain'); final tokenID = clipboard?.text ?? '';
final tokenID = clipboard?.text ?? '';
if (tokenID.isNotEmpty) { if (tokenID.isNotEmpty) {
tokenIDController.text = tokenID; tokenIDController.text = tokenID;
} }
}, },
borderColor: Theme.of(context) borderColor: Theme.of(context).extension<CakeTextTheme>()!.textfieldUnderlineColor,
.extension<CakeTextTheme>()! iconColor: Theme.of(context).primaryColor,
.textfieldUnderlineColor, placeholder: S.current.enterTokenID,
iconColor: Theme.of(context).primaryColor, textStyle: TextStyle(
placeholder: S.current.enterTokenID, fontSize: 16,
textStyle: TextStyle( fontWeight: FontWeight.w400,
fontSize: 16, color: PaletteDark.darkCyanBlue,
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(), Spacer(),
Observer(builder: (context) { Observer(builder: (context) {
return LoadingPrimaryButton( 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/generated/i18n.dart';
import 'package:cake_wallet/routes.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/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/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.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/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_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:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:flutter/material.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class NFTListingPage extends StatelessWidget { class NFTListingPage extends StatelessWidget {
final NFTViewModel nftViewModel; final NFTViewModel nftViewModel;
const NFTListingPage({super.key, required this.nftViewModel}); const NFTListingPage({super.key, required this.nftViewModel});
@override @override
Widget build(BuildContext context) { 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( return Observer(
builder: (context) { builder: (context) {
return Column( return Column(
children: [ children: [
SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 16), padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: PrimaryButton( child: PrimaryButton(
text: S.current.import, text: S.current.import,
color: Theme.of(context) color: syncIndicatorTheme.syncedBackgroundColor,
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
textColor: Colors.white, textColor: Colors.white,
onPressed: () => Navigator.pushNamed( onPressed: () => Navigator.pushNamed(
context, context,
@ -39,46 +44,75 @@ class NFTListingPage extends StatelessWidget {
Expanded( Expanded(
child: Center( child: Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
backgroundColor: Theme.of(context).extension<DashboardPageTheme>()!.textColor, backgroundColor: dashboardTheme.textColor,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context) exchangeTheme.firstGradientBottomPanelColor,
.extension<ExchangePageTheme>()!
.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/entities/wallet_nft_response.dart';
import 'package:cake_wallet/routes.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/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
@ -12,7 +13,14 @@ class NFTTileWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return InkWell(
onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, arguments: nftAsset), onTap: () => Navigator.pushNamed(
context,
Routes.nftDetailsPage,
arguments: NFTDetailsPageArguments(
isSolanaNFT: false,
nftAsset: nftAsset,
),
),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(left: 16, right: 16), 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 fetching = 'fetching';
static const String finished = 'finished'; static const String finished = 'finished';
static const String success = 'success'; static const String success = 'success';
static const String complete = 'complete';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -47,6 +48,7 @@ class SyncIndicatorIcon extends StatelessWidget {
break; break;
case finished: case finished:
case success: case success:
case complete:
indicatorColor = PaletteDark.brightGreen; indicatorColor = PaletteDark.brightGreen;
break; break;
default: default:

View file

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

View file

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

View file

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

View file

@ -1,22 +1,22 @@
import 'package:cake_wallet/routes.dart'; 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/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'dart:ui'; 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/request_review_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/execution_state.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/exchange_trade/information_page.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.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/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.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/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/base_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
@ -30,8 +30,7 @@ void showInformation(
final walletName = exchangeTradeViewModel.wallet.name; final walletName = exchangeTradeViewModel.wallet.name;
final information = exchangeTradeViewModel.isSendable final information = exchangeTradeViewModel.isSendable
? S.current.exchange_result_confirm( ? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) +
trade.amount, trade.from.toString(), walletName) +
exchangeTradeViewModel.extraInfo exchangeTradeViewModel.extraInfo
: S.current.exchange_result_description( : S.current.exchange_result_description(
trade.amount, trade.from.toString()) + trade.amount, trade.from.toString()) +
@ -50,7 +49,22 @@ class ExchangeTradePage extends BasePage {
final ExchangeTradeViewModel exchangeTradeViewModel; final ExchangeTradeViewModel exchangeTradeViewModel;
@override @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 @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
@ -74,14 +88,20 @@ class ExchangeTradePage extends BasePage {
} }
@override @override
Widget body(BuildContext context) => Widget body(BuildContext context) => ExchangeTradeForm(
ExchangeTradeForm(exchangeTradeViewModel); exchangeTradeViewModel,
currentTheme,
);
} }
class ExchangeTradeForm extends StatefulWidget { class ExchangeTradeForm extends StatefulWidget {
ExchangeTradeForm(this.exchangeTradeViewModel); ExchangeTradeForm(
this.exchangeTradeViewModel,
this.currentTheme,
);
final ExchangeTradeViewModel exchangeTradeViewModel; final ExchangeTradeViewModel exchangeTradeViewModel;
final ThemeBase currentTheme;
@override @override
ExchangeTradeState createState() => ExchangeTradeState(); ExchangeTradeState createState() => ExchangeTradeState();
@ -115,119 +135,77 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
@override @override
Widget build(BuildContext context) { 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(); _setEffects();
return Container( return Container(
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(top: 10, bottom: 16), contentPadding: EdgeInsets.only(top: 10, bottom: 16),
content: Observer(builder: (_) { content: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade; final trade = widget.exchangeTradeViewModel.trade;
return Column( return Column(
children: <Widget>[ children: <Widget>[
trade.expiredAt != null trade.expiredAt != null
? Row( ? Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( Text(
S.of(context).offer_expires_in, S.of(context).offer_expires_in,
style: TextStyle( style: TextStyle(
fontSize: 14.0, fontSize: 14.0,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor), color: Theme.of(context)
), .extension<TransactionTradeTheme>()!
if (trade.expiredAt != null) .detailsTitlesColor),
TimerWidget(trade.expiredAt!, ),
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor) if (trade.expiredAt != null)
]) TimerWidget(trade.expiredAt!,
: Offstage(), color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)
Padding( ])
padding: EdgeInsets.only(top: 32), : Offstage(),
child: Row(children: <Widget>[ _ExchangeTradeItemsCardSection(
Spacer(flex: 3), viewModel: widget.exchangeTradeViewModel,
Flexible( currentTheme: widget.currentTheme,
flex: 4, ),
child: Center( ],
child: AspectRatio( );
aspectRatio: 1.0, }),
child: Container( bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
padding: EdgeInsets.all(5), bottomSection: Column(
decoration: BoxDecoration( children: [
border: Border.all( PrimaryButton(
width: 3, key: ValueKey('exchange_trade_page_send_from_external_button_key'),
color: Theme.of(context).extension<ExchangePageTheme>()!.qrCodeColor text: S.current.send_from_external_wallet,
) onPressed: () async {
), Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
child: QrImage(data: trade.inputAddress ?? fetchingLabel), },
)))), color: Theme.of(context).cardColor,
Spacer(flex: 3) textColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
]), ),
), SizedBox(height: 16),
Padding( Observer(
padding: EdgeInsets.only(top: 16), builder: (_) {
child: ListView.separated( final trade = widget.exchangeTradeViewModel.trade;
shrinkWrap: true, final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
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;
final content = ListRow( return widget.exchangeTradeViewModel.isSendable &&
title: item.title, !(sendingState is TransactionCommitted)
value: value, ? LoadingPrimaryButton(
valueFontSize: 14, key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
image: item.isCopied ? copyImage : null, isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
); isLoading: sendingState is IsExecutingState,
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
return item.isCopied text:S.current.send_from_cake_wallet,
? Builder( color: Theme.of(context).primaryColor,
builder: (context) => GestureDetector( textColor: Colors.white,
onTap: () { )
Clipboard.setData( : Offstage();
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();
})),
); );
} }
@ -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/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/confirm_sending_alert.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_card.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/add_template_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.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/picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.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/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart';
import 'package:cake_wallet/src/widgets/trail_button.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/seed_widget_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.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:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -93,7 +97,7 @@ class SendPage extends BasePage {
return MergeSemantics( return MergeSemantics(
child: SizedBox( child: SizedBox(
height: isMobileView ? 37 : 45, height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45, width: isMobileView ? 47: 45,
child: ButtonTheme( child: ButtonTheme(
minWidth: double.minPositive, minWidth: double.minPositive,
child: Semantics( child: Semantics(
@ -114,18 +118,6 @@ class SendPage extends BasePage {
@override @override
AppBarStyle get appBarStyle => AppBarStyle.transparent; 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 @override
void onClose(BuildContext context) { void onClose(BuildContext context) {
sendViewModel.onClose(); sendViewModel.onClose();
@ -174,285 +166,316 @@ class SendPage extends BasePage {
Widget body(BuildContext context) { Widget body(BuildContext context) {
_setEffects(context); _setEffects(context);
return GestureDetector( return Observer(builder: (_) {
onLongPress: () => List<Widget> sendCards = [];
sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing, List<KeyboardActionsItem> keyboardActions = [];
onLongPressUp: () => for (var output in sendViewModel.outputs) {
sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing, var cryptoAmountFocus = FocusNode();
child: Form( var fiatAmountFocus = FocusNode();
key: _formKey, sendCards.add(SendCard(
child: ScrollableWithBottomSection( currentTheme: currentTheme,
contentPadding: EdgeInsets.only(bottom: 24), key: output.key,
content: FocusTraversalGroup( output: output,
policy: OrderedTraversalPolicy(), sendViewModel: sendViewModel,
child: Column( initialPaymentRequest: initialPaymentRequest,
children: <Widget>[ cryptoAmountFocus: cryptoAmountFocus,
Container( fiatAmountFocus: fiatAmountFocus,
height: _sendCardHeight(context), ));
child: Observer( keyboardActions.add(KeyboardActionsItem(
builder: (_) { focusNode: cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]));
return PageView.builder( keyboardActions.add(KeyboardActionsItem(
scrollDirection: Axis.horizontal, focusNode: fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]));
controller: controller, }
itemCount: sendViewModel.outputs.length, return Stack(
itemBuilder: (context, index) { children: [
final output = sendViewModel.outputs[index]; 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( return count > 1
key: output.key, ? Semantics(
output: output, label: 'Page Indicator',
sendViewModel: sendViewModel, hint: 'Swipe to change receiver',
initialPaymentRequest: initialPaymentRequest, 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( bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
height: 40, bottomSection: Column(
width: double.infinity, children: [
padding: EdgeInsets.only(left: 24), if (sendViewModel.hasCurrecyChanger)
child: SingleChildScrollView( Observer(
scrollDirection: Axis.horizontal, builder: (_) => Padding(
child: Observer( 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: (_) { builder: (_) {
final templates = sendViewModel.templates; return LoadingPrimaryButton(
final itemCount = templates.length; 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( return;
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; final notValidItems = sendViewModel.outputs
i < template.additionalRecipients!.length; .where(
i++) { (item) => item.address.isEmpty || item.cryptoAmount.isEmpty)
Output output; .toList();
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate( if (notValidItems.isNotEmpty) {
context, showErrorValidationAlert(context);
output: output, return;
template: template.additionalRecipients![i], }
);
} if (sendViewModel.wallet.isHardwareWallet) {
} else { if (!sendViewModel.ledgerViewModel!.isConnected) {
final output = _defineCurrentOutput(); await Navigator.of(context).pushNamed(Routes.connectDevices,
await _setInputsFromTemplate( arguments: ConnectDevicePageParams(
context, walletType: sendViewModel.walletType,
output: output, onConnectDevice: (BuildContext context, _) {
template: template, sendViewModel.ledgerViewModel!
); .setLedger(sendViewModel.wallet);
} Navigator.of(context).pop();
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());
}, },
); ));
}, } 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; BuildContext? dialogContext;
@ -525,13 +548,12 @@ class SendPage extends BasePage {
if (state is TransactionCommitted) { if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
final successMessage = S.of(context).send_success( final successMessage =
sendViewModel.selectedCryptoCurrency.toString()); S.of(context).send_success(sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(context).waitFewSecondForTxUpdate}' ? '. ${S.of(context).waitFewSecondForTxUpdate}'
@ -539,10 +561,8 @@ class SendPage extends BasePage {
String alertContent = "$successMessage$waitMessage"; String alertContent = "$successMessage$waitMessage";
await Navigator.of(context).pushNamed( await Navigator.of(context)
Routes.transactionSuccessPage, .pushNamed(Routes.transactionSuccessPage, arguments: alertContent);
arguments: alertContent
);
newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); newContactAddress = newContactAddress ?? sendViewModel.newContactAddress();
if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) { if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) {
@ -562,7 +582,7 @@ class SendPage extends BasePage {
leftButtonText: S.of(_dialogContext).ignor, leftButtonText: S.of(_dialogContext).ignor,
alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'), alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'),
alertRightActionButtonKey: alertRightActionButtonKey:
ValueKey('send_page_sent_dialog_add_contact_button_key'), ValueKey('send_page_sent_dialog_add_contact_button_key'),
actionRightButton: () { actionRightButton: () {
Navigator.of(_dialogContext).pop(); Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview(); RequestReviewHandler.requestReview();

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; 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/screens/receive/widgets/currency_input_field.dart';
import 'package:cake_wallet/src/widgets/picker.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/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.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/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/transaction_priority.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:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.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/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.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 { class SendCard extends StatefulWidget {
SendCard({ SendCard({
Key? key, Key? key,
required this.output, required this.output,
required this.sendViewModel, required this.sendViewModel,
required this.currentTheme,
this.initialPaymentRequest, this.initialPaymentRequest,
this.cryptoAmountFocus,
this.fiatAmountFocus,
}) : super(key: key); }) : super(key: key);
final Output output; final Output output;
final SendViewModel sendViewModel; final SendViewModel sendViewModel;
final PaymentRequest? initialPaymentRequest; final PaymentRequest? initialPaymentRequest;
final FocusNode? cryptoAmountFocus;
final FocusNode? fiatAmountFocus;
final ThemeBase currentTheme;
@override @override
SendCardState createState() => SendCardState( SendCardState createState() => SendCardState(
output: output, output: output,
sendViewModel: sendViewModel, sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest, initialPaymentRequest: initialPaymentRequest,
currentTheme: currentTheme
// cryptoAmountFocus: cryptoAmountFocus ?? FocusNode(),
// fiatAmountFocus: fiatAmountFocus ?? FocusNode(),
// cryptoAmountFocus: FocusNode(),
// fiatAmountFocus: FocusNode(),
); );
} }
class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> { class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> {
SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest}) SendCardState({
: addressController = TextEditingController(), required this.output,
required this.sendViewModel,
this.initialPaymentRequest,
required this.currentTheme,
}) : addressController = TextEditingController(),
cryptoAmountController = TextEditingController(), cryptoAmountController = TextEditingController(),
fiatAmountController = TextEditingController(), fiatAmountController = TextEditingController(),
noteController = TextEditingController(), noteController = TextEditingController(),
extractedAddressController = TextEditingController(), extractedAddressController = TextEditingController(),
cryptoAmountFocus = FocusNode(),
fiatAmountFocus = FocusNode(),
addressFocusNode = FocusNode(); addressFocusNode = FocusNode();
static const prefixIconWidth = 34.0; static const prefixIconWidth = 34.0;
static const prefixIconHeight = 34.0; static const prefixIconHeight = 34.0;
final ThemeBase currentTheme;
final Output output; final Output output;
final SendViewModel sendViewModel; final SendViewModel sendViewModel;
final PaymentRequest? initialPaymentRequest; final PaymentRequest? initialPaymentRequest;
@ -67,8 +87,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
final TextEditingController fiatAmountController; final TextEditingController fiatAmountController;
final TextEditingController noteController; final TextEditingController noteController;
final TextEditingController extractedAddressController; final TextEditingController extractedAddressController;
final FocusNode cryptoAmountFocus;
final FocusNode fiatAmountFocus;
final FocusNode addressFocusNode; final FocusNode addressFocusNode;
bool _effectsInstalled = false; bool _effectsInstalled = false;
@ -101,310 +119,336 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
super.build(context); super.build(context);
_setEffects(context); _setEffects(context);
return Stack( // return Stack(
children: [ // children: [
KeyboardActions( // return KeyboardActions(
config: KeyboardActionsConfig( // config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, // keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor, // keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false, // nextFocus: false,
actions: [ // actions: [
KeyboardActionsItem( // KeyboardActionsItem(
focusNode: cryptoAmountFocus, // focusNode: cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()], // 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, : null,
toolbarButtons: [(_) => KeyboardDoneButton()], child: Padding(
) padding: EdgeInsets.fromLTRB(
], 24,
), responsiveLayoutUtil.shouldRenderMobileUI ? 110 : 55,
child: Container( 24,
height: 0, responsiveLayoutUtil.shouldRenderMobileUI ? 32 : 0,
color: Colors.transparent,
),
), ),
Container( child: Observer(
decoration: responsiveLayoutUtil.shouldRenderMobileUI builder: (_) => Column(
? BoxDecoration( mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.only( children: <Widget>[
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), Observer(builder: (_) {
gradient: LinearGradient( final validator = output.isParsedAddress
colors: [ ? sendViewModel.textValidator
Theme.of(context).extension<SendPageTheme>()!.firstGradientColor, : sendViewModel.addressValidator;
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;
return AddressTextField( return AddressTextField(
addressKey: ValueKey('send_page_address_textfield_key'), addressKey: ValueKey('send_page_address_textfield_key'),
focusNode: addressFocusNode, focusNode: addressFocusNode,
controller: addressController, controller: addressController,
onURIScanned: (uri) { onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri); final paymentRequest = PaymentRequest.fromUri(uri);
addressController.text = paymentRequest.address; addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount; cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note; noteController.text = paymentRequest.note;
}, },
options: [ options: [
AddressTextFieldOption.paste, AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode, AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook AddressTextFieldOption.addressBook
], ],
buttonColor: buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
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: borderColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor, Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle( validator: sendViewModel.addressValidator)),
fontSize: 14, CurrencyAmountTextField(
fontWeight: FontWeight.w500, currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'),
color: amountTextfieldKey: ValueKey('send_page_amount_textfield_key'),
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor), sendAllButtonKey: ValueKey('send_page_send_all_button_key'),
onPushPasteButton: (context) async { currencyAmountTextFieldWidgetKey:
output.resetParsedAddress(); ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
await output.fetchParsedAddress(context); selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
}, amountFocusNode: widget.cryptoAmountFocus,
onPushAddressBookButton: (context) async { amountController: cryptoAmountController,
output.resetParsedAddress(); isAmountEditable: true,
}, onTapPicker: () => _presentPicker(context),
onSelectedContact: (contact) { isPickerEnable: sendViewModel.hasMultipleTokens,
output.loadContact(contact); tag: sendViewModel.selectedCryptoCurrency.tag,
}, allAmountButton:
validator: validator, !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
selectedCurrency: sendViewModel.selectedCryptoCurrency, currencyValueValidator: output.sendAll
); ? sendViewModel.allAmountValidator
}), : sendViewModel.amountValidator,
if (output.isParsedAddress) allAmountCallback: () async => output.setSendAll(sendViewModel.balance)),
Padding( Divider(
padding: const EdgeInsets.only(top: 20), height: 1,
child: BaseTextFormField( color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
controller: extractedAddressController, Observer(
readOnly: true, builder: (_) => Padding(
borderColor: Theme.of(context) padding: EdgeInsets.only(top: 10),
.extension<SendPageTheme>()! child: Row(
.textFieldBorderColor, mainAxisSize: MainAxisSize.max,
textStyle: TextStyle( mainAxisAlignment: MainAxisAlignment.spaceBetween,
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), children: <Widget>[
validator: sendViewModel.addressValidator)), Expanded(
CurrencyAmountTextField( child: Text(
currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'), S.of(context).available_balance + ':',
amountTextfieldKey: ValueKey('send_page_amount_textfield_key'), style: TextStyle(
sendAllButtonKey: ValueKey('send_page_send_all_button_key'), fontSize: 12,
currencyAmountTextFieldWidgetKey: fontWeight: FontWeight.w600,
ValueKey('send_page_crypto_currency_amount_textfield_widget_key'), color:
selectedCurrency: sendViewModel.selectedCryptoCurrency.title, Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
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),
)
],
), ),
), ),
), Text(
if (!sendViewModel.isFiatDisabled) sendViewModel.balance,
CurrencyAmountTextField( style: TextStyle(
amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'), fontSize: 12,
currencyAmountTextFieldWidgetKey: fontWeight: FontWeight.w600,
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,
color: color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor), Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
), )
), ],
if (sendViewModel.hasFees) ),
Observer( ),
builder: (_) => GestureDetector( ),
key: ValueKey('send_page_select_fee_priority_button_key'), if (!sendViewModel.isFiatDisabled)
onTap: sendViewModel.hasFeesPriority CurrencyAmountTextField(
? () => pickTransactionPriority(context) amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'),
: () {}, currencyAmountTextFieldWidgetKey:
child: Container( ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
padding: EdgeInsets.only(top: 24), 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Column(
S.of(context).send_estimated_fee, mainAxisAlignment: MainAxisAlignment.start,
style: TextStyle( crossAxisAlignment: CrossAxisAlignment.end,
fontSize: 12, children: [
fontWeight: FontWeight.w500, Text(
color: Colors.white), output.estimatedFee.toString() +
), ' ' +
Container( sendViewModel.currency.toString(),
child: Row( style: TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 12,
children: <Widget>[ fontWeight: FontWeight.w600,
Column( color: Colors.white,
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,
),
),
),
],
), ),
Padding( ),
padding: EdgeInsets.only(top: 2, left: 5), Padding(
child: Icon( padding: EdgeInsets.only(top: 5),
Icons.arrow_forward_ios, child: sendViewModel.isFiatDisabled
size: 12, ? const SizedBox(height: 14)
color: Colors.white, : 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 { Future<void> pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(sendViewModel.walletType); final items = priorityForWalletType(sendViewModel.walletType);
final selectedItem = items.indexOf(sendViewModel.transactionPriority); final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItemIndex = sendViewModel.getCustomPriorityIndex(items); final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items);
final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin; final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null; double? customFeeRate = isBitcoinWallet ? sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp<void>( await showPopUp<void>(
context: context, context: context,
@ -541,7 +585,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
return Picker( return Picker(
items: items, items: items,
displayItem: (TransactionPriority priority) => displayItem: (TransactionPriority priority) =>
sendViewModel.displayFeeRate(priority, customFeeRate?.round()), sendViewModel.feesViewModel.displayFeeRate(priority, customFeeRate?.round()),
selectedAtIndex: selectedIdx, selectedAtIndex: selectedIdx,
customItemIndex: customItemIndex, customItemIndex: customItemIndex,
maxValue: maxCustomFeeRate, maxValue: maxCustomFeeRate,
@ -552,7 +596,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
sliderValue: customFeeRate, sliderValue: customFeeRate,
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue), onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
onItemSelected: (TransactionPriority priority) { onItemSelected: (TransactionPriority priority) {
sendViewModel.setTransactionPriority(priority); sendViewModel.feesViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(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) { 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/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.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'; import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart';
@ -15,23 +15,92 @@ class TrocadorProvidersPage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
final availableProviders = TrocadorExchangeProvider.availableProviders;
final providerStates = trocadorProvidersViewModel.providerStates;
return Container( return Container(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: ListView.builder( child: Observer(
itemCount: availableProviders.length, builder: (_) {
itemBuilder: (_, index) { if (trocadorProvidersViewModel.isLoading) {
String provider = availableProviders[index]; return Center(child: CircularProgressIndicator());
return Observer( }
builder: (_) => SettingsSwitcherCell( var providerStates = trocadorProvidersViewModel.providerStates;
title: provider, final providerRatings = trocadorProvidersViewModel.providerRatings;
value: providerStates[provider] ?? false, if (providerStates.isEmpty) {
onValueChange: (BuildContext _, bool value) { return Center(child: Text(S.of(context).no_providers_available));
trocadorProvidersViewModel.toggleProviderState(provider); }
})); 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_list.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -30,7 +29,13 @@ class SettingsSwitcherCell extends StandardListRow {
height: 56, height: 56,
padding: EdgeInsets.only(left: 12, right: 12), padding: EdgeInsets.only(left: 12, right: 12),
child: TextButton( child: TextButton(
onPressed: () => onValueChange?.call(context, !value), onPressed: () {
if (onTap != null) {
onTap!.call(context);
} else {
onValueChange?.call(context, !value);
}
},
style: ButtonStyle( style: ButtonStyle(
//backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), //backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor),
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
@ -45,7 +50,7 @@ class SettingsSwitcherCell extends StandardListRow {
children: <Widget>[ children: <Widget>[
if (leading != null) leading, if (leading != null) leading,
buildCenter(context, hasLeftOffset: leading != null), buildCenter(context, hasLeftOffset: leading != null),
if (trailing != null) trailing, trailing,
], ],
), ),
), ),

View file

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

View file

@ -11,11 +11,15 @@ class ChatwootWidget extends StatefulWidget {
this.secureStorage, { this.secureStorage, {
required this.supportUrl, required this.supportUrl,
required this.appVersion, required this.appVersion,
required this.fiatApiMode,
required this.walletType,
}); });
final SecureStorage secureStorage; final SecureStorage secureStorage;
final String supportUrl; final String supportUrl;
final String appVersion; final String appVersion;
final String fiatApiMode;
final String walletType;
@override @override
ChatwootWidgetState createState() => ChatwootWidgetState(); ChatwootWidgetState createState() => ChatwootWidgetState();
@ -43,8 +47,11 @@ class ChatwootWidgetState extends State<ChatwootWidget> {
if (eventType == 'loaded') { if (eventType == 'loaded') {
final authToken = parsedMessage["config"]["authToken"]; final authToken = parsedMessage["config"]["authToken"];
_storeCookie(authToken as String); _storeCookie(authToken as String);
_setCustomAttributes( _setCustomAttributes(controller, {
controller, {"app_version": widget.appVersion}); "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({ Widget _buildBottomActionPanel({
required String titleForClipboard, required String titleForClipboard,
required String dataToCopy, 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:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class ListRow extends StatelessWidget { class ListRow extends StatelessWidget {
ListRow( ListRow({
{required this.title, required this.title,
required this.value, required this.value,
this.titleFontSize = 14, this.titleFontSize = 14,
this.valueFontSize = 16, this.valueFontSize = 16,
this.image}); this.image,
this.padding,
this.color,
this.hintTextColor,
this.mainTextColor
});
final String title; final String title;
final String value; final String value;
final double titleFontSize; final double titleFontSize;
final double valueFontSize; final double valueFontSize;
final Image? image; final Image? image;
final EdgeInsetsGeometry? padding;
final Color? color;
final Color? hintTextColor;
final Color? mainTextColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
width: double.infinity, width: double.infinity,
color: Theme.of(context).colorScheme.background, color: color ?? Theme.of(context).colorScheme.background,
child: Padding( child: Padding(
padding: padding: padding ?? const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -32,7 +39,7 @@ class ListRow extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: titleFontSize, fontSize: titleFontSize,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor), color: hintTextColor ?? Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
textAlign: TextAlign.left), textAlign: TextAlign.left),
Padding( Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
@ -46,7 +53,7 @@ class ListRow extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: valueFontSize, fontSize: valueFontSize,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)), color: mainTextColor ?? Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
), ),
image != null image != null
? Padding( ? Padding(

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
@ -224,12 +225,11 @@ abstract class SettingsStoreBase with Store {
if (initialZanoTransactionPriority != null) { if (initialZanoTransactionPriority != null) {
priority[WalletType.zano] = initialZanoTransactionPriority; priority[WalletType.zano] = initialZanoTransactionPriority;
} }
if (initialCakePayCountry != null) { if (initialCakePayCountry != null) {
selectedCakePayCountry = initialCakePayCountry; selectedCakePayCountry = initialCakePayCountry;
} }
initializeTrocadorProviderStates();
reaction( reaction(
(_) => fiatCurrency, (_) => fiatCurrency,
(FiatCurrency fiatCurrency) => sharedPreferences.setString( (FiatCurrency fiatCurrency) => sharedPreferences.setString(
@ -1708,16 +1708,47 @@ abstract class SettingsStoreBase with Store {
powNodes[walletType] = node; powNodes[walletType] = node;
} }
void initializeTrocadorProviderStates() { @action
for (var provider in TrocadorExchangeProvider.availableProviders) { Future<void> updateAllTrocadorProviderStates(List<String> availableProviders) async {
final savedState = _sharedPreferences.getBool(provider) ?? true; final jsonKey = PreferencesKey.trocadorProviderStatesKey;
trocadorProviderStates[provider] = savedState; 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) { @action
_sharedPreferences.setBool(providerName, state); Future<void> setTrocadorProviderState(String providerName, bool state) async {
trocadorProviderStates[providerName] = state; 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 { static Future<String?> _getDeviceName() async {

View file

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

View file

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

View file

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

View file

@ -2,8 +2,10 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; 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/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.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:http/http.dart' as http;
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/.secrets.g.dart' as secrets;
@ -19,7 +21,8 @@ abstract class NFTViewModelBase with Store {
NFTViewModelBase(this.appStore, this.bottomSheetService) NFTViewModelBase(this.appStore, this.bottomSheetService)
: isLoading = false, : isLoading = false,
isImportNFTLoading = false, isImportNFTLoading = false,
nftAssetByWalletModels = ObservableList() { nftAssetByWalletModels = ObservableList(),
solanaNftAssetModels = ObservableList() {
getNFTAssetByWallet(); getNFTAssetByWallet();
reaction((_) => appStore.wallet, (_) => getNFTAssetByWallet()); reaction((_) => appStore.wallet, (_) => getNFTAssetByWallet());
@ -36,31 +39,44 @@ abstract class NFTViewModelBase with Store {
ObservableList<NFTAssetModel> nftAssetByWalletModels; ObservableList<NFTAssetModel> nftAssetByWalletModels;
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
@action @action
Future<void> getNFTAssetByWallet() async { 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; final walletAddress = appStore.wallet!.walletInfo.address;
log('Fetching wallet NFTs for $walletAddress'); 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 [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses // the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if // the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded // the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet // and used within the wallet
// the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response. // the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response.
final uri = Uri.https(
'deep-index.moralis.io', Uri uri;
'/api/v2.2/$walletAddress/nft', if (walletType == WalletType.solana) {
{ uri = Uri.https(
"chain": chainName, 'solana-gateway.moralis.io',
"format": "decimal", '/account/$chainName/$walletAddress/nft',
"media_items": "false", );
"exclude_spam": "true", } else {
"normalizeMetadata": "true", 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 { try {
isLoading = true; 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; isLoading = false;
} catch (e) { } 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 @action
Future<void> importNFT(String tokenAddress, String tokenId) async { Future<void> importNFT(String tokenAddress, String? tokenId) async {
final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
// the [chain] refers to the chain network that the nft is on // the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses // the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if // the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded // the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet // 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 { try {
isImportNFTLoading = true; isImportNFTLoading = true;
final response = await http.get( if (appStore.wallet!.type == WalletType.solana) {
uri, final result = await getSolanaNFTDetails(tokenAddress, chainName);
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
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) { } catch (e) {
isImportNFTLoading = false;
bottomSheetService.queueBottomSheet( bottomSheetService.queueBottomSheet(
isModalDismissible: true, isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget( widget: BottomSheetMessageDisplayWidget(
message: e.toString(), 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/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_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/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/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.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/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:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.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; class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel;
abstract class ExchangeTradeViewModelBase with Store { abstract class ExchangeTradeViewModelBase with Store {
ExchangeTradeViewModelBase( ExchangeTradeViewModelBase({
{required this.wallet, required this.wallet,
required this.trades, required this.trades,
required this.tradesStore, required this.tradesStore,
required this.sendViewModel}) required this.sendViewModel,
: trade = tradesStore.trade!, required this.feesViewModel,
}) : trade = tradesStore.trade!,
isSendable = _checkIfCanSend(tradesStore, wallet), isSendable = _checkIfCanSend(tradesStore, wallet),
items = ObservableList<ExchangeTradeItem>() { items = ObservableList<ExchangeTradeItem>() {
setUpOutput();
switch (trade.provider) { switch (trade.provider) {
case ExchangeProviderDescription.changeNow: case ExchangeProviderDescription.changeNow:
_provider = _provider =
@ -64,6 +69,8 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.chainflip: case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades); _provider = ChainflipExchangeProvider(tradesStore: trades);
break; break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
} }
_updateItems(); _updateItems();
@ -78,6 +85,9 @@ abstract class ExchangeTradeViewModelBase with Store {
final Box<Trade> trades; final Box<Trade> trades;
final TradesStore tradesStore; final TradesStore tradesStore;
final SendViewModel sendViewModel; final SendViewModel sendViewModel;
final FeesViewModel feesViewModel;
late Output output;
@observable @observable
Trade trade; Trade trade;
@ -109,16 +119,18 @@ abstract class ExchangeTradeViewModelBase with Store {
Timer? timer; Timer? timer;
@action void setUpOutput() {
Future<void> confirmSending() async {
if (!isSendable) return;
sendViewModel.clearOutputs(); sendViewModel.clearOutputs();
final output = sendViewModel.outputs.first; output = sendViewModel.outputs.first;
output.address = trade.inputAddress ?? ''; output.address = trade.inputAddress ?? '';
output.setCryptoAmount(trade.amount); output.setCryptoAmount(trade.amount);
if (_provider is ThorChainExchangeProvider) output.memo = trade.memo; if (_provider is ThorChainExchangeProvider) output.memo = trade.memo;
if (trade.isSendAll == true) output.sendAll = true; if (trade.isSendAll == true) output.sendAll = true;
}
@action
Future<void> confirmSending() async {
if (!isSendable) return;
sendViewModel.selectedCryptoCurrency = trade.from; sendViewModel.selectedCryptoCurrency = trade.from;
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider); final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
if (_provider is ThorChainExchangeProvider) { if (_provider is ThorChainExchangeProvider) {
@ -151,7 +163,7 @@ abstract class ExchangeTradeViewModelBase with Store {
void _updateItems() { void _updateItems() {
final tagFrom = 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}' + ' ' : ''; final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : '';
items.clear(); items.clear();
@ -161,6 +173,8 @@ abstract class ExchangeTradeViewModelBase with Store {
title: "${trade.provider.title} ${S.current.id}", title: "${trade.provider.title} ${S.current.id}",
data: '${trade.id}', data: '${trade.id}',
isCopied: true, isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
), ),
); );
@ -168,28 +182,51 @@ abstract class ExchangeTradeViewModelBase with Store {
ExchangeTradeItem( ExchangeTradeItem(
title: S.current.amount, title: S.current.amount,
data: '${trade.amount} ${trade.from}', data: '${trade.amount} ${trade.from}',
isCopied: true, isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
), ),
ExchangeTradeItem( ExchangeTradeItem(
title: S.current.estimated_receive_amount + ':', title: S.current.you_will_receive_estimated_amount + ':',
data: '${tradesStore.trade?.receiveAmount} ${trade.to}', data: '${tradesStore.trade?.receiveAmount} ${trade.to}',
isCopied: true, isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
), ),
ExchangeTradeItem( ExchangeTradeItem(
title: S.current.send_to_this_address('${tradesStore.trade!.from}', tagFrom) + ':', title: S.current.send_to_this_address('${tradesStore.trade!.from}', tagFrom) + ':',
data: trade.inputAddress ?? '', data: trade.inputAddress ?? '',
isCopied: true, isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
), ),
]); ]);
if (trade.extraId != null) { if (trade.extraId != null) {
final title = trade.from == CryptoCurrency.xrp final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm;
? 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)); 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( items.add(
@ -197,6 +234,8 @@ abstract class ExchangeTradeViewModelBase with Store {
title: S.current.arrive_in_this_address('${tradesStore.trade!.to}', tagTo) + ':', title: S.current.arrive_in_this_address('${tradesStore.trade!.to}', tagTo) + ':',
data: trade.payoutAddress ?? '', data: trade.payoutAddress ?? '',
isCopied: true, 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/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_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/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/crypto_currency.dart';
import 'package:cw_core/sync_status.dart'; import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.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/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart'; 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/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.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_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.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.dart';
import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/generated/i18n.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/app_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
@ -71,6 +69,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
this._settingsStore, this._settingsStore,
this.sharedPreferences, this.sharedPreferences,
this.contactListViewModel, this.contactListViewModel,
this.feesViewModel,
) : _cryptoNumberFormat = NumberFormat(), ) : _cryptoNumberFormat = NumberFormat(),
isSendAllEnabled = false, isSendAllEnabled = false,
isFixedRateMode = false, isFixedRateMode = false,
@ -116,7 +115,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
.toList()); .toList());
_setAvailableProviders(); _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()); bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate());
@ -144,9 +152,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_defineIsReceiveAmountEditable(); _defineIsReceiveAmountEditable();
loadLimits(); loadLimits();
reaction((_) => isFixedRateMode, (Object _) { reaction((_) => isFixedRateMode, (Object _) {
bestRate = 0.0;
loadLimits(); loadLimits();
bestRate = 0;
calculateBestRate();
}); });
if (isElectrumWallet) { if (isElectrumWallet) {
@ -160,8 +167,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
wallet.type == WalletType.bitcoinCash; wallet.type == WalletType.bitcoinCash;
bool get hideAddressAfterExchange => bool get hideAddressAfterExchange =>
wallet.type == WalletType.monero || wallet.type == WalletType.monero || wallet.type == WalletType.wownero;
wallet.type == WalletType.wownero;
bool _useTorOnly; bool _useTorOnly;
final Box<Trade> trades; final Box<Trade> trades;
@ -179,10 +185,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
SwapTradeExchangeProvider(), SwapTradeExchangeProvider(),
LetsExchangeExchangeProvider(), LetsExchangeExchangeProvider(),
StealthExExchangeProvider(), StealthExExchangeProvider(),
XOSwapExchangeProvider(),
TrocadorExchangeProvider( TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
]; ];
@observable @observable
ExchangeProvider? provider; ExchangeProvider? provider;
@ -199,6 +207,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final List<ExchangeProvider> _tradeAvailableProviders = []; final List<ExchangeProvider> _tradeAvailableProviders = [];
Map<ExchangeProvider, Limits> _providerLimits = {};
@observable @observable
ObservableList<ExchangeProvider> selectedProviders; ObservableList<ExchangeProvider> selectedProviders;
@ -306,27 +316,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bool get isMoneroWallet => wallet.type == WalletType.monero; 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; List<CryptoCurrency> receiveCurrencies;
@ -338,6 +328,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final ContactListViewModel contactListViewModel; final ContactListViewModel contactListViewModel;
final FeesViewModel feesViewModel;
@observable @observable
double bestRate = 0.0; double bestRate = 0.0;
@ -421,10 +413,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return true; 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) .where((element) => !isFixedRateMode || element.supportsFixedRate)
.toList(); .toList();
@ -462,6 +470,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action @action
Future<void> loadLimits() async { Future<void> loadLimits() async {
if (depositCurrency == receiveCurrency) {
limitsState = LimitsLoadedSuccessfully(limits: Limits(min: 0, max: 0));
return;
};
if (selectedProviders.isEmpty) return; if (selectedProviders.isEmpty) return;
limitsState = LimitsIsLoading(); limitsState = LimitsIsLoading();
@ -473,23 +485,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
double? highestMax = 0.0; double? highestMax = 0.0;
try { try {
final result = await Future.wait( final futures = selectedProviders
selectedProviders.where((provider) => providersForCurrentPair().contains(provider)).map( .where((provider) => providersForCurrentPair().contains(provider))
(provider) => provider .map((provider) async {
.fetchLimits( final limits = await provider
from: from, .fetchLimits(
to: to, from: from,
isFixedRateMode: isFixedRateMode, to: to,
) isFixedRateMode: isFixedRateMode,
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite)) )
.timeout( .onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
Duration(seconds: 7), .timeout(
onTimeout: () => Limits(max: 0.0, min: double.maxFinite), 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!) { if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) {
lowestMin = tempLimits.min; lowestMin = tempLimits.min;
} }
@ -514,10 +530,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
} else { } else {
limitsState = LimitsLoadedFailure(error: 'Limits loading failed'); limitsState = LimitsLoadedFailure(error: 'Limits loading failed');
} }
calculateBestRate();
} }
@action @action
Future<void> createTrade() async { 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) { if (isSendAllEnabled) {
await calculateDepositAllAmount(); await calculateDepositAllAmount();
final amount = double.tryParse(depositAmount); final amount = double.tryParse(depositAmount);
@ -695,10 +719,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
void _onPairChange() { void _onPairChange() {
depositAmount = ''; depositAmount = '';
receiveAmount = ''; receiveAmount = '';
bestRate = 0.0;
loadLimits(); loadLimits();
_setAvailableProviders(); _setAvailableProviders();
bestRate = 0;
calculateBestRate();
} }
void _initialPairBasedOnWallet() { void _initialPairBasedOnWallet() {
@ -792,9 +815,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
receiveAmount = ''; receiveAmount = '';
isFixedRateMode = false; isFixedRateMode = false;
_defineIsReceiveAmountEditable(); _defineIsReceiveAmountEditable();
bestRate = 0.0;
loadLimits(); loadLimits();
bestRate = 0;
calculateBestRate();
final Map<String, dynamic> exchangeProvidersSelection = final Map<String, dynamic> exchangeProvidersSelection =
json.decode(sharedPreferences.getString(PreferencesKey.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 { bool get isAvailableInSelected {
final providersForPair = providersForCurrentPair(); final providersForPair = providersForCurrentPair();
return selectedProviders return selectedProviders
@ -823,35 +856,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
selectedProviders.where((provider) => providersForCurrentPair().contains(provider))); 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() { void _setProviders() {
if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly)
providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList();
@ -966,4 +970,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return false; 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/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_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/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/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/zano/zano.dart';
@ -76,8 +77,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
this.contactListViewModel, this.contactListViewModel,
this.transactionDescriptionBox, this.transactionDescriptionBox,
this.ledgerViewModel, this.ledgerViewModel,
this.unspentCoinsListViewModel, { this.unspentCoinsListViewModel,
this.coinTypeToSpendFrom = UnspentCoinType.any, this.feesViewModel, {
this.coinTypeToSpendFrom = UnspentCoinType.nonMweb,
}) : state = InitialExecutionState(), }) : state = InitialExecutionState(),
currencies = appStore.wallet!.balance.keys.toList(), currencies = appStore.wallet!.balance.keys.toList(),
selectedCryptoCurrency = appStore.wallet!.currency, selectedCryptoCurrency = appStore.wallet!.currency,
@ -89,16 +91,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
_settingsStore = appStore.settingsStore, _settingsStore = appStore.settingsStore,
fiatFromSettings = appStore.settingsStore.fiatCurrency, fiatFromSettings = appStore.settingsStore.fiatCurrency,
super(appStore: appStore) { 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 outputs
.add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
@ -112,7 +104,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
ObservableList<Output> outputs; ObservableList<Output> outputs;
final UnspentCoinType coinTypeToSpendFrom; @observable
UnspentCoinType coinTypeToSpendFrom;
bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
@ -135,6 +128,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
addOutput(); addOutput();
} }
@action
void setAllowMwebCoins(bool allow) {
if (wallet.type == WalletType.litecoin) {
coinTypeToSpendFrom = allow ? UnspentCoinType.any : UnspentCoinType.nonMweb;
}
}
@computed @computed
bool get isBatchSending => outputs.length > 1; bool get isBatchSending => outputs.length > 1;
@ -197,38 +197,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
FiatCurrency get fiat => _settingsStore.fiatCurrency; 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; CryptoCurrency get currency => wallet.currency;
Validator<String> get amountValidator => Validator<String> get amountValidator =>
@ -291,16 +259,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
wallet.type == WalletType.litecoin || wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash; 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 @observable
CryptoCurrency selectedCryptoCurrency; CryptoCurrency selectedCryptoCurrency;
@ -323,6 +281,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final BalanceViewModel balanceViewModel; final BalanceViewModel balanceViewModel;
final ContactListViewModel contactListViewModel; final ContactListViewModel contactListViewModel;
final LedgerViewModel? ledgerViewModel; final LedgerViewModel? ledgerViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore; final FiatConversionStore _fiatConversationStore;
final Box<TransactionDescription> transactionDescriptionBox; 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]) { Object _credentials([ExchangeProvider? provider]) {
final priority = _settingsStore.priority[wallet.type]; final priority = _settingsStore.priority[wallet.type];
@ -544,14 +499,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return bitcoin!.createBitcoinTransactionCredentials( return bitcoin!.createBitcoinTransactionCredentials(
outputs, outputs,
priority: priority!, priority: priority!,
feeRate: customBitcoinFeeRate, feeRate:feesViewModel. customBitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom, coinTypeToSpendFrom: coinTypeToSpendFrom,
); );
case WalletType.litecoin: case WalletType.litecoin:
return bitcoin!.createBitcoinTransactionCredentials( return bitcoin!.createBitcoinTransactionCredentials(
outputs, outputs,
priority: priority!, priority: priority!,
feeRate: customBitcoinFeeRate, feeRate:feesViewModel. customBitcoinFeeRate,
// if it's an exchange flow then disable sending from mweb coins // if it's an exchange flow then disable sending from mweb coins
coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom, 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) => bool _isEqualCurrency(String currency) =>
wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase()); 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; void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
@action @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:cake_wallet/store/settings_store.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -6,9 +7,37 @@ part 'trocador_providers_view_model.g.dart';
class TrocadorProvidersViewModel = TrocadorProvidersViewModelBase with _$TrocadorProvidersViewModel; class TrocadorProvidersViewModel = TrocadorProvidersViewModelBase with _$TrocadorProvidersViewModel;
abstract class TrocadorProvidersViewModelBase with Store { abstract class TrocadorProvidersViewModelBase with Store {
TrocadorProvidersViewModelBase(this._settingsStore); TrocadorProvidersViewModelBase(this._settingsStore, this.trocadorExchangeProvider) {
fetchTrocadorPartners();
}
final SettingsStore _settingsStore; 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 @computed
Map<String, bool> get providerStates => _settingsStore.trocadorProviderStates; Map<String, bool> get providerStates => _settingsStore.trocadorProviderStates;
@ -16,6 +45,6 @@ abstract class TrocadorProvidersViewModelBase with Store {
@action @action
void toggleProviderState(String providerName) { void toggleProviderState(String providerName) {
final currentState = providerStates[providerName] ?? false; 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/.secrets.g.dart' as secrets;
import 'package:cake_wallet/generated/i18n.dart'; 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/store/settings_store.dart';
import 'package:cake_wallet/view_model/settings/link_list_item.dart'; import 'package:cake_wallet/view_model/settings/link_list_item.dart';
import 'package:cake_wallet/view_model/settings/settings_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; class SupportViewModel = SupportViewModelBase with _$SupportViewModel;
abstract class SupportViewModelBase with Store { abstract class SupportViewModelBase with Store {
final SettingsStore settingsStore; final SettingsStore _settingsStore;
final AppStore _appStore;
SupportViewModelBase(this.settingsStore) SupportViewModelBase(this._settingsStore, this._appStore)
: items = [ : items = [
LinkListItem( LinkListItem(
title: 'Email', title: 'Email',
@ -120,7 +122,11 @@ abstract class SupportViewModelBase with Store {
} }
String get appVersion => 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; 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/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_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/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/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.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: case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades); _provider = ChainflipExchangeProvider(tradesStore: trades);
break; break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
break;
} }
_updateItems(); _updateItems();
@ -104,6 +108,8 @@ abstract class TradeDetailsViewModelBase with Store {
return 'https://stealthex.io/exchange/?id=${trade.id}'; return 'https://stealthex.io/exchange/?id=${trade.id}';
case ExchangeProviderDescription.chainflip: case ExchangeProviderDescription.chainflip:
return 'https://scan.chainflip.io/channels/${trade.id}'; return 'https://scan.chainflip.io/channels/${trade.id}';
case ExchangeProviderDescription.xoSwap:
return 'https://orders.xoswap.com/${trade.id}';
} }
return null; return null;
} }

View file

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

View file

@ -106,12 +106,19 @@ dependencies:
flutter_svg: ^2.0.9 flutter_svg: ^2.0.9
polyseed: ^0.0.6 polyseed: ^0.0.6
nostr_tools: ^1.0.9 nostr_tools: ^1.0.9
solana: ^0.31.0+1
ledger_flutter_plus: ledger_flutter_plus:
git: git:
url: https://github.com/vespr-wallet/ledger-flutter-plus url: https://github.com/vespr-wallet/ledger-flutter-plus
ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76
hashlib: ^1.19.2 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: dev_dependencies:
flutter_test: flutter_test:

View file

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

View file

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

View file

@ -148,6 +148,9 @@
"close": "zavřít", "close": "zavřít",
"coin_control": "Volba mincí (nepovinné)", "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", "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", "color_theme": "Barevný motiv",
"commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}", "commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}",
"confirm": "Potvrdit", "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_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_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_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", "expired": "Vypršelo",
"expires": "Vyprší", "expires": "Vyprší",
"expiresOn": "Vyprší dne", "expiresOn": "Vyprší dne",
@ -394,6 +398,7 @@
"light_theme": "Světlý", "light_theme": "Světlý",
"litecoin_enable_mweb_sync": "Povolit skenování MWeb", "litecoin_enable_mweb_sync": "Povolit skenování MWeb",
"litecoin_mweb": "MWeb", "litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "Povolte mweb mince",
"litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", "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_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí",
"litecoin_mweb_dismiss": "Propustit", "litecoin_mweb_dismiss": "Propustit",
@ -429,6 +434,7 @@
"methods": "Metody", "methods": "Metody",
"min_amount": "Min: ${value}", "min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Adresa máty",
"minutes_to_pin_code": "${minute} minutách", "minutes_to_pin_code": "${minute} minutách",
"mm": "MM", "mm": "MM",
"modify_2fa": "Upravte Cake 2FA", "modify_2fa": "Upravte Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nová peněženka", "new_wallet": "Nová peněženka",
"newConnection": "Nové připojení", "newConnection": "Nové připojení",
"no_cards_found": "Žádné karty nenalezeny", "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_needed": "Žádné ID není potřeba!",
"no_id_required": "Žádní ID není potřeba. Dobijte si a utrácejte kdekoliv", "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_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": "Žá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.", "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_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_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", "select_your_country": "Vyberte prosím svou zemi",
"selected_trocador_provider": "Vybraný poskytovatel Trocador",
"sell": "Prodat", "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_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", "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_error_minimum_value": "Minimální částka je 0,01",
"send_estimated_fee": "Odhadovaný poplatek:", "send_estimated_fee": "Odhadovaný poplatek:",
"send_fee": "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_name": "Název",
"send_new": "Nová", "send_new": "Nová",
"send_payment_id": "ID platby (nepovinné)", "send_payment_id": "ID platby (nepovinné)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Nyní máte debetní kartu", "you_now_have_debit_card": "Nyní máte debetní kartu",
"you_pay": "Zaplatíte", "you_pay": "Zaplatíte",
"you_will_get": "Směnit na", "you_will_get": "Směnit na",
"you_will_receive_estimated_amount": "Obdržíte(odhadovaný )",
"you_will_send": "Směnit z", "you_will_send": "Směnit z",
"yy": "YY" "yy": "YY"
} }

View file

@ -148,6 +148,9 @@
"close": "Schließen", "close": "Schließen",
"coin_control": "Coin Control (optional)", "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.", "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", "color_theme": "Farbthema",
"commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}", "commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}",
"confirm": "Bestätigen", "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_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_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_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", "expired": "Abgelaufen",
"expires": "Läuft ab", "expires": "Läuft ab",
"expiresOn": "Läuft aus am", "expiresOn": "Läuft aus am",
@ -394,6 +398,7 @@
"light_theme": "Hell", "light_theme": "Hell",
"litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen",
"litecoin_mweb": "MWeb", "litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "MWEB -Münzen zulassen",
"litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", "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_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt",
"litecoin_mweb_dismiss": "Zurückweisen", "litecoin_mweb_dismiss": "Zurückweisen",
@ -429,6 +434,7 @@
"methods": "Methoden", "methods": "Methoden",
"min_amount": "Min: ${value}", "min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Minzadresse",
"minutes_to_pin_code": "${minute} Minuten", "minutes_to_pin_code": "${minute} Minuten",
"mm": "MM", "mm": "MM",
"modify_2fa": "Cake 2FA ändern", "modify_2fa": "Cake 2FA ändern",
@ -456,8 +462,10 @@
"new_wallet": "Neue Wallet", "new_wallet": "Neue Wallet",
"newConnection": "Neue Verbindung", "newConnection": "Neue Verbindung",
"no_cards_found": "Keine Karten gefunden", "no_cards_found": "Keine Karten gefunden",
"no_extra_detail": "Keine zusätzlichen Details verfügbar",
"no_id_needed": "Keine ID erforderlich!", "no_id_needed": "Keine ID erforderlich!",
"no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben", "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_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": "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.", "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_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_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", "select_your_country": "Bitte wählen Sie Ihr Land aus",
"selected_trocador_provider": "Ausgewählter Trocador -Anbieter",
"sell": "Verkaufen", "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_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", "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_error_minimum_value": "Der Mindestbetrag ist 0,01",
"send_estimated_fee": "Geschätzte Gebühr:", "send_estimated_fee": "Geschätzte Gebühr:",
"send_fee": "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_name": "Name",
"send_new": "Neu", "send_new": "Neu",
"send_payment_id": "Zahlungs-ID (optional)", "send_payment_id": "Zahlungs-ID (optional)",
@ -1003,6 +1014,7 @@
"you_now_have_debit_card": "Sie haben jetzt eine Debitkarte", "you_now_have_debit_card": "Sie haben jetzt eine Debitkarte",
"you_pay": "Sie bezahlen", "you_pay": "Sie bezahlen",
"you_will_get": "Konvertieren zu", "you_will_get": "Konvertieren zu",
"you_will_receive_estimated_amount": "Sie erhalten(geschätzt )",
"you_will_send": "Konvertieren von", "you_will_send": "Konvertieren von",
"yy": "YY" "yy": "YY"
} }

View file

@ -148,6 +148,9 @@
"close": "Close", "close": "Close",
"coin_control": "Coin control (optional)", "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", "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", "color_theme": "Color theme",
"commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}", "commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}",
"confirm": "Confirm", "confirm": "Confirm",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Please copy or write down your ID shown above.", "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_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_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", "expired": "Expired",
"expires": "Expires", "expires": "Expires",
"expiresOn": "Expires on", "expiresOn": "Expires on",
@ -394,6 +398,7 @@
"light_theme": "Light", "light_theme": "Light",
"litecoin_enable_mweb_sync": "Enable MWEB scanning", "litecoin_enable_mweb_sync": "Enable MWEB scanning",
"litecoin_mweb": "MWEB", "litecoin_mweb": "MWEB",
"litecoin_mweb_allow_coins": "Allow MWEB coins",
"litecoin_mweb_always_scan": "Set MWEB always scanning", "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_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin",
"litecoin_mweb_dismiss": "Dismiss", "litecoin_mweb_dismiss": "Dismiss",
@ -429,6 +434,7 @@
"methods": "Methods", "methods": "Methods",
"min_amount": "Min: ${value}", "min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Mint Address",
"minutes_to_pin_code": "${minute} minutes", "minutes_to_pin_code": "${minute} minutes",
"mm": "MM", "mm": "MM",
"modify_2fa": "Modify Cake 2FA", "modify_2fa": "Modify Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "New Wallet", "new_wallet": "New Wallet",
"newConnection": "New Connection", "newConnection": "New Connection",
"no_cards_found": "No cards found", "no_cards_found": "No cards found",
"no_extra_detail": "No extra details available",
"no_id_needed": "No ID needed!", "no_id_needed": "No ID needed!",
"no_id_required": "No ID required. Top up and spend anywhere", "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_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": "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.", "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_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_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", "select_your_country": "Please select your country",
"selected_trocador_provider": "selected Trocador provider",
"sell": "Sell", "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_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", "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_error_minimum_value": "Minimum value of amount is 0.01",
"send_estimated_fee": "Estimated fee:", "send_estimated_fee": "Estimated fee:",
"send_fee": "Fee:", "send_fee": "Fee:",
"send_from_cake_wallet": "Send from Cake Wallet",
"send_from_external_wallet": "Send from External Wallet",
"send_name": "Name", "send_name": "Name",
"send_new": "New", "send_new": "New",
"send_payment_id": "Payment ID (optional)", "send_payment_id": "Payment ID (optional)",
@ -1001,6 +1012,7 @@
"you_now_have_debit_card": "You now have a debit card", "you_now_have_debit_card": "You now have a debit card",
"you_pay": "You Pay", "you_pay": "You Pay",
"you_will_get": "Convert to", "you_will_get": "Convert to",
"you_will_receive_estimated_amount": "You will receive (estimated)",
"you_will_send": "Convert from", "you_will_send": "Convert from",
"yy": "YY" "yy": "YY"
} }

View file

@ -148,6 +148,9 @@
"close": "Cerca", "close": "Cerca",
"coin_control": "Control de monedas (opcional)", "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", "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", "color_theme": "Tema de color",
"commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}",
"confirm": "Confirmar", "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_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_result_write_down_trade_id": "Por favor, copia o escribe el ID.",
"exchange_sync_alert_content": "Espere hasta que su billetera esté sincronizada", "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", "expired": "Muerto",
"expires": "Caduca", "expires": "Caduca",
"expiresOn": "Expira el", "expiresOn": "Expira el",
@ -394,6 +398,7 @@
"light_theme": "Ligero", "light_theme": "Ligero",
"litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb",
"litecoin_mweb": "Mweb", "litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Permitir monedas mweb",
"litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", "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_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", "litecoin_mweb_dismiss": "Despedir",
@ -429,6 +434,7 @@
"methods": "Métodos", "methods": "Métodos",
"min_amount": "Mínimo: ${value}", "min_amount": "Mínimo: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Dirección menta",
"minutes_to_pin_code": "${minute} minutos", "minutes_to_pin_code": "${minute} minutos",
"mm": "mm", "mm": "mm",
"modify_2fa": "Modificar 2FA", "modify_2fa": "Modificar 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nueva billetera", "new_wallet": "Nueva billetera",
"newConnection": "Nueva conexión", "newConnection": "Nueva conexión",
"no_cards_found": "No se encuentran cartas", "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_needed": "¡No se necesita identificación!",
"no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar", "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_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": "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.", "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_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_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", "select_your_country": "Seleccione su país",
"selected_trocador_provider": "Proveedor de Trocador seleccionado",
"sell": "Vender", "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_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", "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_error_minimum_value": "El valor mínimo de la cantidad es 0.01",
"send_estimated_fee": "Tarifa estimada:", "send_estimated_fee": "Tarifa estimada:",
"send_fee": "Cuota:", "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_name": "Nombre",
"send_new": "Nuevo", "send_new": "Nuevo",
"send_payment_id": "ID de pago (opcional)", "send_payment_id": "ID de pago (opcional)",
@ -1001,6 +1012,7 @@
"you_now_have_debit_card": "Ahora tienes una tarjeta de débito", "you_now_have_debit_card": "Ahora tienes una tarjeta de débito",
"you_pay": "Tú pagas", "you_pay": "Tú pagas",
"you_will_get": "Convertir a", "you_will_get": "Convertir a",
"you_will_receive_estimated_amount": "Recibirá(estimado )",
"you_will_send": "Convertir de", "you_will_send": "Convertir de",
"yy": "YY" "yy": "YY"
} }

View file

@ -148,6 +148,9 @@
"close": "Fermer", "close": "Fermer",
"coin_control": "Contrôle optionnel des pièces (coins)", "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", "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", "color_theme": "Thème",
"commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}", "commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}",
"confirm": "Confirmer", "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_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_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_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", "expired": "Expirée",
"expires": "Expire", "expires": "Expire",
"expiresOn": "Expire le", "expiresOn": "Expire le",
@ -394,6 +398,7 @@
"light_theme": "Clair", "light_theme": "Clair",
"litecoin_enable_mweb_sync": "Activer la numérisation MWEB", "litecoin_enable_mweb_sync": "Activer la numérisation MWEB",
"litecoin_mweb": "Mweb", "litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Autoriser les pièces MWeb",
"litecoin_mweb_always_scan": "Définir MWEB Score Scanning", "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_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin",
"litecoin_mweb_dismiss": "Rejeter", "litecoin_mweb_dismiss": "Rejeter",
@ -429,6 +434,7 @@
"methods": "Méthodes", "methods": "Méthodes",
"min_amount": "Min : ${value}", "min_amount": "Min : ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Adresse de la menthe",
"minutes_to_pin_code": "${minute} minutes", "minutes_to_pin_code": "${minute} minutes",
"mm": "MM", "mm": "MM",
"modify_2fa": "Modifier les paramètres Cake 2FA", "modify_2fa": "Modifier les paramètres Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Nouveau Portefeuille (Wallet)", "new_wallet": "Nouveau Portefeuille (Wallet)",
"newConnection": "Nouvelle connexion", "newConnection": "Nouvelle connexion",
"no_cards_found": "Pas de cartes trouvées", "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_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_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_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": "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.", "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_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_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", "select_your_country": "Veuillez sélectionner votre pays",
"selected_trocador_provider": "fournisseur de trocador sélectionné",
"sell": "Vendre", "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_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", "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_error_minimum_value": "La valeur minimale du montant est 0.01",
"send_estimated_fee": "Estimation des frais :", "send_estimated_fee": "Estimation des frais :",
"send_fee": "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_name": "Nom",
"send_new": "Nouveau", "send_new": "Nouveau",
"send_payment_id": "ID de paiement (optionnel)", "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_now_have_debit_card": "Vous avez maintenant une carte de débit",
"you_pay": "Vous payez", "you_pay": "Vous payez",
"you_will_get": "Convertir vers", "you_will_get": "Convertir vers",
"you_will_receive_estimated_amount": "Vous recevrez ( estimé )",
"you_will_send": "Convertir depuis", "you_will_send": "Convertir depuis",
"yy": "AA" "yy": "AA"
} }

View file

@ -148,6 +148,9 @@
"close": "Rufa", "close": "Rufa",
"coin_control": "Sarrafa tsabar kuɗi (na zaɓi)", "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", "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", "color_theme": "Jigon launi",
"commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}", "commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}",
"confirm": "Tabbatar", "confirm": "Tabbatar",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Don Allah kwafi ko rubuta ID dake nuna sama.", "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_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_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", "expired": "Karewa",
"expires": "Ya ƙare", "expires": "Ya ƙare",
"expiresOn": "Yana ƙarewa", "expiresOn": "Yana ƙarewa",
@ -394,6 +398,7 @@
"light_theme": "Haske", "light_theme": "Haske",
"litecoin_enable_mweb_sync": "Kunna binciken Mweb", "litecoin_enable_mweb_sync": "Kunna binciken Mweb",
"litecoin_mweb": "Mweb", "litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Bada izinin Coins na Mweb",
"litecoin_mweb_always_scan": "Saita Mweb koyaushe", "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_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", "litecoin_mweb_dismiss": "Tuɓe \\ sallama",
@ -429,6 +434,7 @@
"methods": "Hanyoyin", "methods": "Hanyoyin",
"min_amount": "Min: ${value}", "min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Adireshin Mint",
"minutes_to_pin_code": "${minute} minti", "minutes_to_pin_code": "${minute} minti",
"mm": "MM", "mm": "MM",
"modify_2fa": "Gyara Cake 2FA", "modify_2fa": "Gyara Cake 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Sabuwar Wallet", "new_wallet": "Sabuwar Wallet",
"newConnection": "Sabuwar Haɗi", "newConnection": "Sabuwar Haɗi",
"no_cards_found": "Babu katunan da aka samo", "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_needed": "Babu ID da ake buƙata!",
"no_id_required": "Babu ID da ake buƙata. Yi da kuma ciyar a ko'ina", "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_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": "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.", "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_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_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", "select_your_country": "Da fatan za a zabi ƙasarku",
"selected_trocador_provider": "Zabi mai bada TORACAD",
"sell": "sayar", "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_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", "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_error_minimum_value": "Mafi ƙarancin ƙimar adadin shine 0.01",
"send_estimated_fee": "Ƙimar kuɗi:", "send_estimated_fee": "Ƙimar kuɗi:",
"send_fee": "Kudin:", "send_fee": "Kudin:",
"send_from_cake_wallet": "Aika daga Wallet Caza",
"send_from_external_wallet": "Aika daga walat na waje",
"send_name": "Sunan", "send_name": "Sunan",
"send_new": "Sabon", "send_new": "Sabon",
"send_payment_id": "ID na biyan kuɗi (optional)", "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_now_have_debit_card": "Yanzu kana da katin zare kudi",
"you_pay": "Ka Bayar", "you_pay": "Ka Bayar",
"you_will_get": "Maida zuwa", "you_will_get": "Maida zuwa",
"you_will_receive_estimated_amount": "Za ku (karɓi )",
"you_will_send": "Maida daga", "you_will_send": "Maida daga",
"yy": "YY" "yy": "YY"
} }

View file

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

View file

@ -148,6 +148,9 @@
"close": "Zatvoriti", "close": "Zatvoriti",
"coin_control": "Kontrola novca (nije obavezno)", "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", "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", "color_theme": "Shema boja",
"commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}", "commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}",
"confirm": "Potvrdi", "confirm": "Potvrdi",
@ -305,6 +308,7 @@
"exchange_result_write_down_ID": "*Molimo kopirajte ili zapišite svoj ID prikazan ispod.", "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_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_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", "expired": "Isteklo",
"expires": "Ističe", "expires": "Ističe",
"expiresOn": "Istječe", "expiresOn": "Istječe",
@ -394,6 +398,7 @@
"light_theme": "Svijetla", "light_theme": "Svijetla",
"litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje",
"litecoin_mweb": "MWeb", "litecoin_mweb": "MWeb",
"litecoin_mweb_allow_coins": "Dopustite MWeb kovanice",
"litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", "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_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije",
"litecoin_mweb_dismiss": "Odbaciti", "litecoin_mweb_dismiss": "Odbaciti",
@ -429,6 +434,7 @@
"methods": "Metode", "methods": "Metode",
"min_amount": "Minimalno: ${value}", "min_amount": "Minimalno: ${value}",
"min_value": "Min.: ${value} ${currency}", "min_value": "Min.: ${value} ${currency}",
"mint_address": "Adresa metvice",
"minutes_to_pin_code": "${minute} minuta", "minutes_to_pin_code": "${minute} minuta",
"mm": "MM", "mm": "MM",
"modify_2fa": "Izmijenite tortu 2FA", "modify_2fa": "Izmijenite tortu 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Novi novčanik", "new_wallet": "Novi novčanik",
"newConnection": "Nova veza", "newConnection": "Nova veza",
"no_cards_found": "Nisu pronađene kartice", "no_cards_found": "Nisu pronađene kartice",
"no_extra_detail": "Nema dostupnih dodatnih detalja",
"no_id_needed": "Nije potreban ID!", "no_id_needed": "Nije potreban ID!",
"no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", "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_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": "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.", "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_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_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", "select_your_country": "Odaberite svoju zemlju",
"selected_trocador_provider": "Odabrani pružatelj usluga trokadora",
"sell": "Prodavati", "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_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", "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_error_minimum_value": "Minimalna vrijednost iznosa je 0.01",
"send_estimated_fee": "Procijenjena naknada:", "send_estimated_fee": "Procijenjena naknada:",
"send_fee": "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_name": "Ime",
"send_new": "Novi", "send_new": "Novi",
"send_payment_id": "ID plaćanja (nije obvezno)", "send_payment_id": "ID plaćanja (nije obvezno)",
@ -1000,6 +1011,7 @@
"you_now_have_debit_card": "Sada imate debitnu karticu", "you_now_have_debit_card": "Sada imate debitnu karticu",
"you_pay": "Vi plaćate", "you_pay": "Vi plaćate",
"you_will_get": "Razmijeni u", "you_will_get": "Razmijeni u",
"you_will_receive_estimated_amount": "Primit ćete(procijenjeno )",
"you_will_send": "Razmijeni iz", "you_will_send": "Razmijeni iz",
"yy": "GG" "yy": "GG"
} }

View file

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

View file

@ -148,6 +148,9 @@
"close": "Menutup", "close": "Menutup",
"coin_control": "Kontrol koin (opsional)", "coin_control": "Kontrol koin (opsional)",
"cold_or_recover_wallet": "Tambahkan dompet hanya baca dari Cupcake atau dompet dingin atau memulihkan dompet kertas", "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", "color_theme": "Tema warna",
"commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}", "commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}",
"confirm": "Konfirmasi", "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_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_result_write_down_trade_id": "Silakan salin atau tulis ID perdagangan untuk melanjutkan.",
"exchange_sync_alert_content": "Silakan tunggu sampai dompet Anda tersinkronisasi", "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", "expired": "Kedaluwarsa",
"expires": "Kadaluarsa", "expires": "Kadaluarsa",
"expiresOn": "Kadaluarsa pada", "expiresOn": "Kadaluarsa pada",
@ -394,6 +398,7 @@
"light_theme": "Terang", "light_theme": "Terang",
"litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB",
"litecoin_mweb": "Mweb", "litecoin_mweb": "Mweb",
"litecoin_mweb_allow_coins": "Izinkan koin mWeb",
"litecoin_mweb_always_scan": "Atur mWeb selalu memindai", "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_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin",
"litecoin_mweb_dismiss": "Membubarkan", "litecoin_mweb_dismiss": "Membubarkan",
@ -429,6 +434,7 @@
"methods": "Metode", "methods": "Metode",
"min_amount": "Min: ${value}", "min_amount": "Min: ${value}",
"min_value": "Min: ${value} ${currency}", "min_value": "Min: ${value} ${currency}",
"mint_address": "Alamat mint",
"minutes_to_pin_code": "${minute} menit", "minutes_to_pin_code": "${minute} menit",
"mm": "MM", "mm": "MM",
"modify_2fa": "Ubah Kue 2FA", "modify_2fa": "Ubah Kue 2FA",
@ -456,8 +462,10 @@
"new_wallet": "Dompet Baru", "new_wallet": "Dompet Baru",
"newConnection": "Koneksi Baru", "newConnection": "Koneksi Baru",
"no_cards_found": "Tidak ada kartu yang ditemukan", "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_needed": "Tidak perlu ID!",
"no_id_required": "Tidak perlu ID. Isi ulang dan belanja di mana saja", "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_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": "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.", "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_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_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", "select_your_country": "Pilih negara Anda",
"selected_trocador_provider": "Penyedia Trocador Terpilih",
"sell": "Jual", "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_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", "sell_monero_com_alert_content": "Menjual Monero belum didukung",
@ -689,6 +698,8 @@
"send_error_minimum_value": "Nilai minimum jumlah adalah 0.01", "send_error_minimum_value": "Nilai minimum jumlah adalah 0.01",
"send_estimated_fee": "Biaya yang diperkirakan:", "send_estimated_fee": "Biaya yang diperkirakan:",
"send_fee": "Biaya:", "send_fee": "Biaya:",
"send_from_cake_wallet": "Kirim dari Dompet Kue",
"send_from_external_wallet": "Kirim dari dompet eksternal",
"send_name": "Nama", "send_name": "Nama",
"send_new": "Baru", "send_new": "Baru",
"send_payment_id": "ID Pembayaran (opsional)", "send_payment_id": "ID Pembayaran (opsional)",
@ -1003,6 +1014,7 @@
"you_now_have_debit_card": "Anda sekarang memiliki kartu debit", "you_now_have_debit_card": "Anda sekarang memiliki kartu debit",
"you_pay": "Anda Membayar", "you_pay": "Anda Membayar",
"you_will_get": "Konversi ke", "you_will_get": "Konversi ke",
"you_will_receive_estimated_amount": "Anda akan menerima(estimasi )",
"you_will_send": "Konversi dari", "you_will_send": "Konversi dari",
"yy": "YY" "yy": "YY"
} }

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