diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index db9d1056626..1724b9a1b7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,5 +3,5 @@ # @global-owner1 and @global-owner2 will be requested for # review when someone opens a pull request. -* @milerius @satoshiotomakan @lamafab -kotlin/ @MaximPestryakov +* @milerius @satoshiotomakan @ar-g +kotlin/ @ar-g @JaimeToca @rkokhatskyi diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index a575fe792ea..b8d6bb34c8a 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: build: - runs-on: [ self-hosted, macOS, ARM64, wallet-core ] + runs-on: macos-latest-large if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 @@ -29,6 +29,15 @@ jobs: - name: Install system dependencies run: | tools/install-sys-dependencies-mac + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install Rust dependencies + run: | tools/install-rust-dependencies - name: Install Android Dependencies @@ -39,7 +48,7 @@ jobs: uses: actions/cache@v3 with: path: build/local - key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }} + key: ${{ runner.os }}-${{ runner.arch }}-internal-${{ hashFiles('tools/install-dependencies') }} - name: Install internal dependencies run: tools/install-dependencies @@ -51,8 +60,21 @@ jobs: - name: Build Kotlin doc run: tools/kotlin-doc - - name: Run test - run: tools/android-test + - name: Build tests + run: | + pushd android + ./gradlew assembleAndroidTest + popd + + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 30 + target: google_apis + arch: x86 + ndk: 23.1.7779620 + cmake: 3.18.1 + script: cd android; ./gradlew connectedAndroidTest - name: Build sample app run: tools/samples-build android diff --git a/.github/workflows/codegen-v2.yml b/.github/workflows/codegen-v2.yml index 108b8e42448..50446778240 100644 --- a/.github/workflows/codegen-v2.yml +++ b/.github/workflows/codegen-v2.yml @@ -3,22 +3,45 @@ name: Codegen-v2 Tests on: push: branches: [ dev, master ] - paths: - - 'codegen-v2/**' pull_request: branches: [ dev, master ] - paths: - - 'codegen-v2/**' + +env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" jobs: test: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - uses: actions-rs/cargo@v1 - with: - command: test - args: --manifest-path codegen-v2/Cargo.toml + - uses: actions/checkout@v3 + - name: Install system dependencies + run: | + tools/install-sys-dependencies-linux + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + + - name: Install Rust dependencies + run: | + tools/install-rust-dependencies + + - name: Run codegen-v2 tests + run: | + cargo test --all + working-directory: codegen-v2 + + # Generate files for a blockchain. + # Please note the blockchain should not be implemented in Rust at the moment of running this step, + # otherwise consider either generating files for another blockchain or removing this step at all. + - name: Test codegen-v2 new-blockchain-rust + run: | + cargo run -- new-blockchain-rust iotex + working-directory: codegen-v2 + + # Check if `new-blockchain-rust` command has generated files that do not break project compilation. + - name: Check Rust compiles + run: | + cargo check --tests + working-directory: rust diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 7dcc89a194b..649b15e84a2 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -12,24 +12,37 @@ concurrency: jobs: build: - runs-on: [ self-hosted, macOS, ARM64, wallet-core ] + runs-on: macos-latest-xlarge if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 + - name: Install system dependencies run: | tools/install-sys-dependencies-mac + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install Rust dependencies + run: | tools/install-rust-dependencies + - name: Cache internal dependencies id: internal_cache uses: actions/cache@v3 with: path: build/local key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }} + - name: Install internal dependencies run: | tools/install-dependencies if: steps.internal_cache.outputs.cache-hit != 'true' + - name: Run codegen tests run: tools/codegen-test @@ -37,6 +50,7 @@ jobs: run: | tools/generate-files ios tools/ios-test + - name: Build sample app run: | tools/samples-build ios diff --git a/.github/workflows/kotlin-ci.yml b/.github/workflows/kotlin-ci.yml index 0a517a03ae8..1198c20ae3c 100644 --- a/.github/workflows/kotlin-ci.yml +++ b/.github/workflows/kotlin-ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: build: - runs-on: [ self-hosted, macOS, ARM64, wallet-core ] + runs-on: macos-latest-xlarge if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 @@ -23,6 +23,9 @@ jobs: java-version: '17' distribution: 'temurin' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: @@ -31,6 +34,15 @@ jobs: - name: Install system dependencies run: | tools/install-sys-dependencies-mac + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install Rust dependencies + run: | tools/install-rust-dependencies - name: Install emsdk diff --git a/.github/workflows/kotlin-sample-ci.yml b/.github/workflows/kotlin-sample-ci.yml index 4c42e44dcd5..f96b2a4b280 100644 --- a/.github/workflows/kotlin-sample-ci.yml +++ b/.github/workflows/kotlin-sample-ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: build: - runs-on: [ self-hosted, macOS, ARM64, wallet-core ] + runs-on: macos-latest-xlarge if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 @@ -23,6 +23,9 @@ jobs: java-version: '17' distribution: 'temurin' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Install Kotlin Dependencies run: tools/install-kotlin-dependencies diff --git a/.github/workflows/linux-ci-rust.yml b/.github/workflows/linux-ci-rust.yml index 9a27424cea2..36d3e57931a 100644 --- a/.github/workflows/linux-ci-rust.yml +++ b/.github/workflows/linux-ci-rust.yml @@ -15,7 +15,8 @@ concurrency: cancel-in-progress: true jobs: - build: + # Check formatting, clippy warnings, run tests and check code coverage. + build-and-test: permissions: contents: read checks: write @@ -40,9 +41,6 @@ jobs: run: | tools/install-rust-dependencies dev - - name: Install emsdk - run: tools/install-wasm-dependencies - - name: Check code formatting run: | cargo fmt --check @@ -55,22 +53,109 @@ jobs: - name: Run tests run: | - cargo llvm-cov nextest --profile ci --no-fail-fast --lcov --output-path coverage.info - working-directory: rust + tools/rust-coverage + + - name: Gather and check Rust code coverage + run: | + tools/check-coverage rust/coverage.stats rust/coverage.info + + # Run Rust tests in WASM. + test-wasm: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v3 + - name: Install system dependencies + run: | + tools/install-sys-dependencies-linux + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install Rust dependencies + run: | + tools/install-rust-dependencies + + - name: Install emsdk + run: tools/install-wasm-dependencies - name: Run tests in WASM run: tools/rust-test wasm - - name: Rust Test Report - uses: dorny/test-reporter@v1 - if: success() || failure() - continue-on-error: true + check-binary-sizes: + permissions: + contents: read + pull-requests: write + runs-on: macos-latest-xlarge + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v3 + - name: Install system dependencies + run: | + tools/install-sys-dependencies-mac + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 with: - name: Rust Tests - path: | - rust/target/nextest/ci/junit.xml - reporter: java-junit + workspaces: | + rust - - name: Gather and check Rust code coverage + - name: Install Rust dependencies + run: tools/install-rust-dependencies + + - name: Install emsdk + run: tools/install-wasm-dependencies + + - name: Compile release binaries run: | - tools/check-coverage rust/coverage.stats rust/coverage.info + mkdir -p build/local/lib + source emsdk/emsdk_env.sh + tools/rust-bindgen + + - name: Generate release report + run: | + ./tools/release-size measure-rust > release-report.json + + - name: Upload release report + uses: actions/upload-artifact@v4 + with: + name: release_report + path: release-report.json + + # Download previous release report, compare the release binary sizes, and post/update a comment at the Pull Request. + - name: Download previous release report + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + uses: dawidd6/action-download-artifact@v6 + with: + commit: ${{github.event.pull_request.base.sha}} + path: previous + if_no_artifact_found: warn + # Same artifact name as at the "Upload release report" step. + name: release_report + # Ignore status or conclusion in the search. + workflow_conclusion: "" + + - name: Craft Comment Body + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + run: | + # Please note `previous/release-report.json` may not exist if the previous report was not found. + ./tools/release-size compare --before previous/release-report.json --current release-report.json > report-diff.md + + - name: Create or Update Comment + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + uses: edumserrano/find-create-or-update-comment@v2 + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: "Binary size comparison" + comment-author: 'github-actions[bot]' + edit-mode: replace + body-path: 'report-diff.md' diff --git a/.github/workflows/linux-ci-sonarcloud.yml b/.github/workflows/linux-ci-sonarcloud.yml index d8154a0c9b4..775486d741a 100644 --- a/.github/workflows/linux-ci-sonarcloud.yml +++ b/.github/workflows/linux-ci-sonarcloud.yml @@ -16,16 +16,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Install system dependencies run: | tools/install-sys-dependencies-linux tools/install-rust-dependencies + - name: Cache internal dependencies id: internal_cache uses: actions/cache@v3 with: path: build/local key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }} + - name: Install internal dependencies run: | tools/install-dependencies @@ -33,12 +42,14 @@ jobs: CC: /usr/bin/clang CXX: /usr/bin/clang++ if: steps.internal_cache.outputs.cache-hit != 'true' + - name: Code generation run: | tools/generate-files native env: CC: /usr/bin/clang CXX: /usr/bin/clang++ + - name: CMake (coverage/clang-tidy/clang-asan) run: | cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DTW_CODE_COVERAGE=ON -DTW_ENABLE_CLANG_TIDY=ON -DTW_CLANG_ASAN=ON -GNinja @@ -46,6 +57,7 @@ jobs: env: CC: /usr/bin/clang CXX: /usr/bin/clang++ + - name: SonarCloud Scan run: | ./tools/sonarcloud-analysis diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 8eb3d94358e..653fbf0c135 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -18,14 +18,14 @@ jobs: - uses: actions/checkout@v3 - name: Install system dependencies run: | - tools/install-sys-dependencies-linux + tools/install-sys-dependencies-linux ci tools/install-rust-dependencies - name: Cache internal dependencies id: internal_cache uses: actions/cache@v3 with: path: build/local - key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }} + key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-sys-dependencies-linux') }}-${{ hashFiles('tools/install-dependencies') }} - name: Install internal dependencies run: | tools/install-dependencies diff --git a/.github/workflows/linux-sampleapp-ci.yml b/.github/workflows/linux-sampleapp-ci.yml index ec1b8ec665d..da1aa11656b 100644 --- a/.github/workflows/linux-sampleapp-ci.yml +++ b/.github/workflows/linux-sampleapp-ci.yml @@ -18,14 +18,14 @@ jobs: - uses: actions/checkout@v3 - name: Install system dependencies run: | - tools/install-sys-dependencies-linux + tools/install-sys-dependencies-linux ci tools/install-rust-dependencies - name: Cache internal dependencies id: internal_cache uses: actions/cache@v3 with: path: build/local - key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }} + key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-sys-dependencies-linux') }}-${{ hashFiles('tools/install-dependencies') }} - name: Install internal dependencies run: | tools/install-dependencies diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cba2961f0a..6fec5513326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,6 @@ -# Copyright © 2017-2022 Trust Wallet. +# SPDX-License-Identifier: Apache-2.0 # -# This file is part of Trust. The full Trust copyright notice, including -# terms governing use, modification, and redistribution, is contained in the -# file LICENSE at the root of the source code distribution tree. +# Copyright © 2017 Trust Wallet. cmake_minimum_required(VERSION 3.18 FATAL_ERROR) diff --git a/Dockerfile b/Dockerfile index 7db09d1bd5b..082d5fd1969 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ ENV CXX=/usr/bin/clang++-14 RUN wget "https://sh.rustup.rs" -O rustup.sh \ && sh rustup.sh -y ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup default nightly-2024-06-13 RUN cargo install --force cbindgen \ && rustup target add wasm32-unknown-emscripten diff --git a/LICENSE b/LICENSE index 261eeb9e9f8..591e7191a60 100644 --- a/LICENSE +++ b/LICENSE @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2017 Trust Wallet Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Package.swift b/Package.swift index 90ec9e38022..38e74510f2f 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,13 @@ let package = Package( targets: [ .binaryTarget( name: "WalletCore", - url: "https://github.com/trustwallet/wallet-core/releases/download/3.2.1/WalletCore.xcframework.zip", - checksum: "b7f9fe6b5aa1fd216d43f676400c9db6999c6494e0120db73b2afb49dcc1f013" + url: "https://github.com/trustwallet/wallet-core/releases/download/4.0.33/WalletCore.xcframework.zip", + checksum: "2fb8b833047b9697bba6ade66a9bdeede622b2fe0fb7a9b90cb9edb4651ec866" ), .binaryTarget( name: "SwiftProtobuf", - url: "https://github.com/trustwallet/wallet-core/releases/download/3.2.1/SwiftProtobuf.xcframework.zip", - checksum: "12bbc92dc2225c661e301cc2826e034fcb69b2a144f8c4ff2e51ce6ccf122124" + url: "https://github.com/trustwallet/wallet-core/releases/download/4.0.33/SwiftProtobuf.xcframework.zip", + checksum: "05557735dd607c5a369dc378eb3f299504b880614ef13f136a028ecd320b0e4d" ) ] ) diff --git a/README.md b/README.md index 3452964595f..7456046acbd 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,13 @@ It is a core part of the popular [Trust Wallet](https://trustwallet.com), and so Most of the code is C++ with a set of strict C interfaces, and idiomatic interfaces for supported languages: Swift for iOS and Java (Kotlin) for Android. -![iOS CI](https://github.com/trustwallet/wallet-core/workflows/iOS%20CI/badge.svg) -![Android CI](https://github.com/trustwallet/wallet-core/workflows/Android%20CI/badge.svg) -![Linux CI](https://github.com/trustwallet/wallet-core/workflows/Linux%20CI/badge.svg) -![Wasm CI](https://github.com/trustwallet/wallet-core/workflows/Wasm%20CI/badge.svg) -![Kotlin CI](https://github.com/trustwallet/wallet-core/workflows/Kotlin%20CI/badge.svg) -![Docker CI](https://github.com/trustwallet/wallet-core/workflows/Docker%20CI/badge.svg) +[![iOS CI](https://github.com/trustwallet/wallet-core/actions/workflows/ios-ci.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/ios-ci.yml) +[![Android CI](https://github.com/trustwallet/wallet-core/actions/workflows/android-ci.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/android-ci.yml) +[![Linux CI](https://github.com/trustwallet/wallet-core/actions/workflows/linux-ci.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/linux-ci.yml) +[![Rust CI](https://github.com/trustwallet/wallet-core/actions/workflows/linux-ci-rust.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/linux-ci-rust.yml) +[![Wasm CI](https://github.com/trustwallet/wallet-core/actions/workflows/wasm-ci.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/wasm-ci.yml) +[![Kotlin CI](https://github.com/trustwallet/wallet-core/actions/workflows/kotlin-ci.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/kotlin-ci.yml) +[![Docker CI](https://github.com/trustwallet/wallet-core/actions/workflows/docker.yml/badge.svg)](https://github.com/trustwallet/wallet-core/actions/workflows/docker.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=TrustWallet_wallet-core&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=TrustWallet_wallet-core) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/trustwallet/wallet-core) @@ -24,6 +25,10 @@ Swift for iOS and Java (Kotlin) for Android. For comprehensive documentation, see [developer.trustwallet.com](https://developer.trustwallet.com/wallet-core). +# Audit Reports + +Security Audit reports can be found in the [audit](audit) directory. + # Supported Blockchains Wallet Core supports more than **130** blockchains: Bitcoin, Ethereum, BNB, Cosmos, Solana, and most major blockchain platforms. @@ -97,16 +102,16 @@ Please check out the [Kotlin Multiplatform sample](https://github.com/trustwalle Projects using Trust Wallet Core. Add yours too! -[Trust Wallet](https://trustwallet.com) +[Trust Wallet](https://trustwallet.com) [Coinpaprika](https://coinpaprika.com/) | [crypto.com](https://crypto.com) -| [Alice](https://www.alicedapp.com/) | [Frontier](https://frontier.xyz/) | [Tokenary](https://tokenary.io/) | [MemesWallet](https://planetmemes.com/) | [xPortal](https://xportal.com/) | [Slingshot](https://slingshot.finance/) +| [ECOIN Wallet](https://play.google.com/store/apps/details?id=org.ecoinwallet&pcampaignid=web_share) # Community @@ -118,7 +123,8 @@ There are a few community-maintained projects that extend Wallet Core to some ad # Contributing -The best way to submit feedback and report bugs is to [open a GitHub issue](https://github.com/trustwallet/wallet-core/issues/new). +The best way to submit feedback and report bugs related to WalletCore is to [open a GitHub issue](https://github.com/trustwallet/wallet-core/issues/new). +If the bug is not related to WalletCore but to the TrustWallet app, please [create a Customer Support ticket](https://support.trustwallet.com/en/support/tickets/new). If you want to contribute code please see [Contributing](https://developer.trustwallet.com/wallet-core/contributing). If you want to add support for a new blockchain also see [Adding Support for a New Blockchain](https://developer.trustwallet.com/wallet-core/newblockchain), make sure you have read the [requirements](https://developer.trustwallet.com/wallet-core/newblockchain#requirements) section. diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt index eb93523e4eb..9064a873e7c 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt @@ -42,10 +42,14 @@ class CoinAddressDerivationTests { CALLISTO -> assertEquals("0x3E6FFC80745E6669135a76F4A7ce6BCF02436e04", address) DASH -> assertEquals("XqHiz8EXYbTAtBEYs4pWTHh7ipEDQcNQeT", address) DIGIBYTE -> assertEquals("dgb1qtjgmerfqwdffyf8ghcrkgy52cghsqptynmyswu", address) + ETHEREUM, SMARTCHAIN, POLYGON, OPTIMISM, ZKSYNC, ARBITRUM, ARBITRUMNOVA, ECOCHAIN, AVALANCHECCHAIN, XDAI, FANTOM, CELO, CRONOSCHAIN, SMARTBITCOINCASH, KUCOINCOMMUNITYCHAIN, BOBA, METIS, - AURORA, EVMOS, MOONRIVER, MOONBEAM, KAVAEVM, KLAYTN, METER, OKXCHAIN, POLYGONZKEVM, SCROLL, - CONFLUXESPACE, ACALAEVM, OPBNB, NEON, BASE, LINEA, GREENFIELD, MANTLE, ZENEON -> assertEquals("0x8f348F300873Fd5DA36950B2aC75a26584584feE", address) + AURORA, EVMOS, MOONRIVER, MOONBEAM, KAVAEVM, KAIA, METER, OKXCHAIN, POLYGONZKEVM, SCROLL, + CONFLUXESPACE, ACALAEVM, OPBNB, NEON, BASE, LINEA, GREENFIELD, MANTLE, ZENEON, MANTAPACIFIC, + ZETAEVM, MERLIN, LIGHTLINK, BLAST, BOUNCEBIT, ZKLINKNOVA, + -> assertEquals("0x8f348F300873Fd5DA36950B2aC75a26584584feE", address) + RONIN -> assertEquals("ronin:8f348F300873Fd5DA36950B2aC75a26584584feE", address) ETHEREUMCLASSIC -> assertEquals("0x078bA3228F3E6C08bEEac9A005de0b7e7089aD1c", address) GOCHAIN -> assertEquals("0x5940ce4A14210d4Ccd0ac206CE92F21828016aC2", address) @@ -57,7 +61,7 @@ class CoinAddressDerivationTests { XRP -> assertEquals("rPwE3gChNKtZ1mhH3Ko8YFGqKmGRWLWXV3", address) TEZOS -> assertEquals("tz1acnY9VbMagps26Kj3RfoGRWD9nYG5qaRX", address) THUNDERCORE -> assertEquals("0x4b92b3ED6d8b24575Bf5ce4C6a86ED261DA0C8d7", address) - TOMOCHAIN -> assertEquals("0xC74b6D8897cBa9A4b659d43fEF73C9cA852cE424", address) + VICTION -> assertEquals("0xC74b6D8897cBa9A4b659d43fEF73C9cA852cE424", address) TRON -> assertEquals("TQ5NMqJjhpQGK7YJbESKtNCo86PJ89ujio", address) VECHAIN -> assertEquals("0x1a553275dF34195eAf23942CB7328AcF9d48c160", address) WANCHAIN -> assertEquals("0xD5ca90b928279FE5D06144136a25DeD90127aC15", address) @@ -118,8 +122,8 @@ class CoinAddressDerivationTests { NATIVEEVMOS -> assertEquals("evmos13u6g7vqgw074mgmf2ze2cadzvkz9snlwstd20d", address) NERVOS -> assertEquals("ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdtyq04tvp02wectaumxn0664yw2jd53lqk4mxg3", address) EVERSCALE -> assertEquals("0:0c39661089f86ec5926ea7d4ee4223d634ba4ed6dcc2e80c7b6a8e6d59f79b04", address) - TON -> assertEquals("EQDgEMqToTacHic7SnvnPFmvceG5auFkCcAw0mSCvzvKUfk9", address) - APTOS -> assertEquals("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", address) + TON -> assertEquals("UQDgEMqToTacHic7SnvnPFmvceG5auFkCcAw0mSCvzvKUaT4", address) + APTOS -> assertEquals("0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", address) NEBL -> assertEquals("NgDVaXAwNgBwb88xLiFKomfBmPkEh9F2d7", address) SUI -> assertEquals("0xada112cfb90b44ba889cc5d39ac2bf46281e4a91f7919c693bcd9b8323e81ed2", address) HEDERA -> assertEquals("0.0.302a300506032b657003210049eba62f64d0d941045595d9433e65d84ecc46bcdb1421de55e05fcf2d8357d5", address) @@ -146,5 +150,9 @@ class CoinAddressDerivationTests { NOBLE -> assertEquals("noble142j9u5eaduzd7faumygud6ruhdwme98qc8l3wa", address) ROOTSTOCK -> assertEquals("0xA2D7065F94F838a3aB9C04D67B312056846424Df", address) SEI -> assertEquals("sei142j9u5eaduzd7faumygud6ruhdwme98qagm0sj", address) + INTERNETCOMPUTER -> assertEquals("6f8e568160a3c8362789848dc0fa52891964473c045cc25208a305fb35b7c4ab", address) + TIA -> assertEquals("celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7", address) + NATIVEZETACHAIN -> assertEquals("zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304", address) + DYDX -> assertEquals("dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky", address) } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt index 5b92dd34849..d565a1a08be 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt @@ -32,7 +32,7 @@ class TestCoinType { assertEquals(CoinType.POANETWORK.value(), 178) assertEquals(CoinType.VECHAIN.value(), 818) assertEquals(CoinType.ICON.value(), 74) - assertEquals(CoinType.TOMOCHAIN.value(), 889) + assertEquals(CoinType.VICTION.value(), 889) assertEquals(CoinType.TEZOS.value(), 1729) assertEquals(CoinType.QTUM.value(), 2301) assertEquals(CoinType.NEBULAS.value(), 2718) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaAddress.kt index b94158bace6..5e50f515ab4 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.acala diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaSigner.kt index a066009144d..ad99985b4ec 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acala/TestAcalaSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.acala diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acalaevm/TestAcalaEVMAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acalaevm/TestAcalaEVMAddress.kt index 81274311cdd..6845bea3e89 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acalaevm/TestAcalaEVMAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/acalaevm/TestAcalaEVMAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.acalaevm diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricAddress.kt index 9cabca65f15..1164db16e14 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.agoric diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricSigner.kt index 7567a61f21f..ecdf4372f3a 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/agoric/TestAgoricSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.agoric diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosAddress.kt index 180617ff5b1..c8bed69c5d8 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.aptos diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt index f16a3be8300..e536821658b 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.aptos diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainAddress.kt index d988eed5bb1..ec361735357 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bandchain diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainSigner.kt index 662773bba9c..001e9d90631 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bandchain/TestBandChainSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bandchain diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/binance/TestBinanceWalletConnectSigning.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/binance/TestBinanceWalletConnectSigning.kt new file mode 100644 index 00000000000..a66c2dc9776 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/binance/TestBinanceWalletConnectSigning.kt @@ -0,0 +1,46 @@ +package com.trustwallet.core.app.blockchains.binance + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.toHexBytes +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.proto.Binance.SigningOutput +import wallet.core.jni.proto.WalletConnect +import wallet.core.jni.* +import wallet.core.jni.CoinType.BINANCE +import wallet.core.java.AnySigner +import wallet.core.jni.proto.Common + +class TestBinanceWalletConnectSigning { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testSignBinanceTransactionFromWalletConnectRequest() { + // Step 1: Parse a signing request received through WalletConnect. + + val parsingInput = WalletConnect.ParseRequestInput.newBuilder().apply { + method = WalletConnect.Method.CosmosSignAmino + payload = "{\"signerAddress\":\"bnb1grpf0955h0ykzq3ar5nmum7y6gdfl6lxfn46h2\",\"signDoc\":{\"account_number\":\"19\",\"chain_id\":\"chain-bnb\",\"memo\":\"\",\"data\":null,\"msgs\":[{\"inputs\":[{\"address\":\"bnb1grpf0955h0ykzq3ar5nmum7y6gdfl6lxfn46h2\",\"coins\":[{\"amount\":1001000000,\"denom\":\"BNB\"}]}],\"outputs\":[{\"address\":\"bnb13zeh6hs97d5eu2s5qerguhv8ewwue6u4ywa6yf\",\"coins\":[{\"amount\":1001000000,\"denom\":\"BNB\"}]}]}],\"sequence\":\"23\",\"source\":\"1\"}}" + }.build() + + val parsingOutputBytes = WalletConnectRequest.parse(BINANCE, parsingInput.toByteArray()) + val parsingOutput = WalletConnect.ParseRequestOutput.parseFrom(parsingOutputBytes) + + assertEquals(parsingOutput.error, Common.SigningError.OK) + + // Step 2: Set missing fields. + + val signingInput = parsingOutput.binance.toBuilder().apply { + privateKey = ByteString.copyFrom("95949f757db1f57ca94a5dff23314accbe7abee89597bf6a3c7382c84d7eb832".toHexBytes()) + }.build() + + // Step 3: Sign the transaction. + + val output = AnySigner.sign(signingInput, BINANCE, SigningOutput.parser()) + + assertEquals(output.error, Common.SigningError.OK) + assertEquals(output.signatureJson, "{\"pub_key\":{\"type\":\"tendermint/PubKeySecp256k1\",\"value\":\"Amo1kgCI2Yw4iMpoxT38k/RWRgJgbLuH8P5e5TPbOOUC\"},\"signature\":\"PCTHhMa7+Z1U/6uxU+3LbTxKd0k231xypdMolyVvjgYvMg+0dTMC+wqW8IxHWXTSDt/Ronu+7ac1h/WN3JWJdQ==\"}") + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt deleted file mode 100644 index 15d4ee2d593..00000000000 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.trustwallet.core.app.blockchains.bitcoin - -import com.trustwallet.core.app.utils.Numeric -import com.trustwallet.core.app.utils.toHexBytes -import org.junit.Assert.assertEquals -import org.junit.Test -import wallet.core.jni.BitcoinFee - -class TestBitcoinFee { - - init { - System.loadLibrary("TrustWalletCore") - } - - @Test - fun P2pkhCalculateFee() { - val satVb = "10" - val tx = Numeric.hexStringToByteArray("02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 191 * satVb.toLong()) - } - - @Test - fun P2wpkhCalculateFee() { - val satVb = "12" - val tx = Numeric.hexStringToByteArray("020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 189 * satVb.toLong()) - } - - @Test - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 - // - // Fee/VB 19.608 sat/vByte - // Size 235 Bytes - // Weight 610 - fun Brc20TransferCommitCalculateFee() { - val satVb = "19" - val tx = Numeric.hexStringToByteArray("02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 153 * satVb.toLong()) // 153 = ceil(610/4) - } -} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt new file mode 100644 index 00000000000..884120800ca --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt @@ -0,0 +1,108 @@ +package com.trustwallet.core.app.blockchains.bitcoin + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHexBytesInByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.BitcoinScript +import wallet.core.jni.BitcoinSigHashType +import wallet.core.jni.CoinType +import wallet.core.jni.CoinType.BITCOIN +import wallet.core.jni.Hash +import wallet.core.jni.PrivateKey +import wallet.core.jni.PublicKey +import wallet.core.jni.PublicKeyType +import wallet.core.jni.proto.Bitcoin +import wallet.core.jni.proto.BitcoinV2 +import wallet.core.jni.proto.Common.SigningError + +class TestBitcoinPsbt { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testSignThorSwap() { + // Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32 + + val privateKey = "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytesInByteString() + val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString() + + val input = BitcoinV2.SigningInput.newBuilder() + .setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt)) + .addPrivateKeys(privateKey) + .build() + + val legacyInput = Bitcoin.SigningInput.newBuilder() + .setSigningV2(input) + .build() + + val legacyOutput = AnySigner.sign(legacyInput, BITCOIN, Bitcoin.SigningOutput.parser()) + val output = legacyOutput.signingResultV2 + + assertEquals(output.error, SigningError.OK) + assertEquals( + output.psbt.psbt.toByteArray().toHex(), + "0x70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000" + ) + assertEquals( + output.encoded.toByteArray().toHex(), + "0x02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000" + ) + assertEquals( + output.txid.toByteArray().toHex(), + "0x634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32" + ) + } + + @Test + fun testPlanThorSwap() { + // Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32 + + val privateKey = PrivateKey("f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytes()) + val publicKey = privateKey.getPublicKeySecp256k1(true) + val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString() + + val input = BitcoinV2.SigningInput.newBuilder() + .setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt)) + .addPublicKeys(ByteString.copyFrom(publicKey.data())) + .build() + + val legacyInput = Bitcoin.SigningInput.newBuilder() + .setSigningV2(input) + .build() + + val legacyPlan = AnySigner.plan(legacyInput, BITCOIN, Bitcoin.TransactionPlan.parser()) + val plan = legacyPlan.planningResultV2 + + assertEquals(plan.error, SigningError.OK) + + assertEquals(plan.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + assertEquals(plan.getInputs(0).value, 66_406) + + // Vault transfer + assertEquals(plan.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0") + assertEquals(plan.getOutputs(0).value, 60_000) + + // OP_RETURN + assertEquals( + plan.getOutputs(1).customScriptPubkey.toByteArray().toHex(), + "0x6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530" + ) + assertEquals(plan.getOutputs(1).value, 0) + + // Change output + assertEquals(plan.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + assertEquals(plan.getOutputs(2).value, 4_670) + + assertEquals(plan.feeEstimate, 1736) + // Please note that `change` in PSBT planning is always 0. + // That's because we aren't able to determine which output is an actual change from PSBT. + assertEquals(plan.change, 0) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt index 0aa1ddac322..abd187f8150 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt @@ -16,6 +16,7 @@ import wallet.core.jni.PublicKey import wallet.core.jni.PublicKeyType import wallet.core.jni.proto.Bitcoin import wallet.core.jni.proto.Bitcoin.SigningOutput +import wallet.core.jni.proto.BitcoinV2 import wallet.core.jni.proto.Common.SigningError class TestBitcoinSigning { @@ -160,842 +161,202 @@ class TestBitcoinSigning { Numeric.toHexString(encoded.toByteArray())); } - @Test - fun testSignBrc20Transfer() { - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 - val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val fullAmount = 26400 - val minerFee = 3000 - val brcInscribeAmount = 7000 - val dustSatoshis = 546 - val forFeeAmount = fullAmount - brcInscribeAmount - minerFee - val txIdInscription = Numeric.hexStringToByteArray("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca").reversedArray() - val txIDForFees = Numeric.hexStringToByteArray("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reversedArray() - - val privateKey = PrivateKey(privateKeyData) - val publicKey = privateKey.getPublicKeySecp256k1(true) - val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) - val bobPubkey = PublicKey(Numeric.hexStringToByteArray("02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e"), PublicKeyType.SECP256K1) - val bobPubkeyHash = Hash.ripemd(Hash.sha256(bobPubkey.data())) - - val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputP2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(bobPubkeyHash) - - val input = Bitcoin.SigningInput.newBuilder() - .setIsItBrcOperation(true) - .addPrivateKey(ByteString.copyFrom(privateKeyData)) - - val unspentOutputPoint0 = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txIdInscription)) - .setIndex(0) - .build() - val utxo0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(dustSatoshis.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .setOutPoint(unspentOutputPoint0) - .build() - - val unspentOutputPoint1 = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txIDForFees)) - .setIndex(1) - .build() - val utxo1 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(forFeeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .setOutPoint(unspentOutputPoint1) - .build() - - input.addUtxo(utxo0) - input.addUtxo(utxo1) - - val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(outputP2wpkh.data())) - .setAmount(dustSatoshis.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - val utxoPlan1 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount((forFeeAmount - minerFee).toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan0) - .addUtxos(utxoPlan1) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) - - assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7") - assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) - } - @Test fun testSignBrc20Commit() { // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val fullAmount = 26400 - val minerFee = 3000 - val brcInscribeAmount = 7000 - val forFeeAmount = fullAmount - brcInscribeAmount - minerFee - val txId = Numeric.hexStringToByteArray("089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e") - val brc20Ticker = "oadf" - val brc20Amount = "20" + val dustSatoshis = 546.toLong() + val txId = Numeric.hexStringToByteArray("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008").reversedArray() val privateKey = PrivateKey(privateKeyData) - val publicKey = privateKey.getPublicKeySecp256k1(true) - val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) - - val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) - val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) - - val input = Bitcoin.SigningInput.newBuilder() - .setIsItBrcOperation(true) - .addPrivateKey(ByteString.copyFrom(privateKeyData)) - - val unspentOutputPoint = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txId)) - .setIndex(1) - .build() - - val utxo0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(fullAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .setOutPoint(unspentOutputPoint) - .build() - input.addUtxo(utxo0) - - val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(brcInscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.BRC20TRANSFER) - .build() - val utxoPlan1 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(forFeeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan0) - .addUtxos(utxoPlan1) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txId) + vout = 1 + }) + .setValue(26_400) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(7_000) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + brc20Inscribe = BitcoinV2.Output.OutputBrc20Inscription.newBuilder().apply { + inscribeTo = publicKey + ticker = "oadf" + transferAmount = "20" + }.build() + }) + + val changeOutput = BitcoinV2.Output.newBuilder() + .setValue(16_400) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val builder = BitcoinV2.TransactionBuilder.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addInputs(utxo0) + .addOutputs(out0) + .addOutputs(changeOutput) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setFixedDustThreshold(dustSatoshis) + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setBuilder(builder) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser()) assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") - assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") } @Test fun testSignBrc20Reveal() { // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val brcInscribeAmount = 7000 - val dustSatoshis = 546 - val txId = Numeric.hexStringToByteArray("b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d79") - val brc20Ticker = "oadf" - val brc20Amount = "20" - - val privateKey = PrivateKey(privateKeyData) - val publicKey = privateKey.getPublicKeySecp256k1(true) - val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) - val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) - val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) - - val input = Bitcoin.SigningInput.newBuilder() - .setIsItBrcOperation(true) - .addPrivateKey(ByteString.copyFrom(privateKeyData)) - - val unspentOutputPoint0 = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txId)) - .setIndex(0) - .build() - val utxo0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(brcInscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.BRC20TRANSFER) - .setSpendingScript(outputInscribeProto.spendingScript) - .setOutPoint(unspentOutputPoint0) - .build() - - input.addUtxo(utxo0) - - val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(dustSatoshis.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan0) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) - - assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") - val encodedHex = Numeric.toHexString(output.encoded.toByteArray()) - assert(encodedHex.startsWith("0x02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340")) - assert(encodedHex.endsWith("5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")) - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) - } - - @Test - fun testSignNftInscriptionCommit() { - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117 - - val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val fullAmount = 32400 - val minerFee = 1300 - val inscribeAmount = fullAmount - minerFee; - val txId = Numeric.hexStringToByteArray("992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c3909557") - val payload = Numeric.hexStringToByteArray(nftInscriptionImageData) + val dustSatoshis = 546.toLong() + val txIdCommit = Numeric.hexStringToByteArray("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reversedArray() val privateKey = PrivateKey(privateKeyData) - val publicKey = privateKey.getPublicKeySecp256k1(true) - val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) - - val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputInscribe = BitcoinScript.buildOrdinalNftInscription("image/png", payload, publicKey.data()) - val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) - - val input = Bitcoin.SigningInput.newBuilder() - .setIsItBrcOperation(true) - .addPrivateKey(ByteString.copyFrom(privateKeyData)) - - val unspentOutputPoint = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txId)) - .setIndex(0) - .build() - - val utxo = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(fullAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .setOutPoint(unspentOutputPoint) - .build() - input.addUtxo(utxo) - - val utxoPlan = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(inscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.NFTINSCRIPTION) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdCommit) + vout = 0 + }) + .setValue(7_000) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + brc20Inscribe = BitcoinV2.Input.InputBrc20Inscription.newBuilder().apply { + inscribeTo = publicKey + ticker = "oadf" + transferAmount = "20" + }.build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(dustSatoshis) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val builder = BitcoinV2.TransactionBuilder.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addInputs(utxo0) + .addOutputs(out0) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setFixedDustThreshold(dustSatoshis) + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setBuilder(builder) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser()) assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117") - assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000101992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c39095570000000000ffffffff017c790000000000002251202ac69a7e9dba801e9fcba826055917b84ca6fba4d51a29e47d478de603eedab602473044022054212984443ed4c66fc103d825bfd2da7baf2ab65d286e3c629b36b98cd7debd022050214cfe5d3b12a17aaaf1a196bfeb2f0ad15ffb320c4717eb7614162453e4fe0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03406a35548b8fa4620028e021a944c1d3dc6e947243a7bfc901bf63fefae0d2460efa149a6440cab51966aa4f09faef2d1e5efcba23ab4ca6e669da598022dbcfe35b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") } @Test - fun testSignNftInscriptionReveal() { - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac - + fun testSignBrc20Transfer() { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val inscribeAmount = 31100 - val dustSatoshis = 546 - val txId = Numeric.hexStringToByteArray("1771decbce2766b39d8fe66f4dc11737b3146c71f8cc6ae1397384c5e508e7f1") - val payload = Numeric.hexStringToByteArray(nftInscriptionImageData) + val dustSatoshis = 546.toLong() + val txIdInscription = Numeric.hexStringToByteArray("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca").reversedArray() + val txIdForFees = Numeric.hexStringToByteArray("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reversedArray() val privateKey = PrivateKey(privateKeyData) - val publicKey = privateKey.getPublicKeySecp256k1(true) - val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) - val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputInscribe = BitcoinScript.buildOrdinalNftInscription("image/png", payload, publicKey.data()) - val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) - - val input = Bitcoin.SigningInput.newBuilder() - .setIsItBrcOperation(true) - .addPrivateKey(ByteString.copyFrom(privateKeyData)) - - val unspentOutputPoint0 = Bitcoin.OutPoint.newBuilder() - .setHash(ByteString.copyFrom(txId)) - .setIndex(0) - .build() - - val utxo = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(inscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.NFTINSCRIPTION) - .setSpendingScript(outputInscribeProto.spendingScript) - .setOutPoint(unspentOutputPoint0) - .build() - - input.addUtxo(utxo) - - val utxoPlan = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(dustSatoshis.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + val bobAddress = "bc1qazgc2zhu2kmy42py0vs8d7yff67l3zgpwfzlpk" + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdInscription) + vout = 0 + }) + .setValue(dustSatoshis) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val utxo1 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdForFees) + vout = 1 + }) + .setValue(16_400) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(dustSatoshis) + .setToAddress(bobAddress) + + val changeOutput = BitcoinV2.Output.newBuilder() + .setValue(13_400) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val builder = BitcoinV2.TransactionBuilder.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addInputs(utxo0) + .addInputs(utxo1) + .addOutputs(out0) + .addOutputs(changeOutput) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setFixedDustThreshold(dustSatoshis) + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setBuilder(builder) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser()) assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac") - - val encodedHex = Numeric.toHexString(output.encoded.toByteArray()) - val expectedHex = nftInscriptionRawHex - - // Offset is the 0x prefix. - val offset = 2; - assertEquals(encodedHex.length, 15658 + offset) - assertEquals(encodedHex.substring(0 + offset, 164 + offset), expectedHex.substring(0, 164)) - assertEquals(encodedHex.substring(292 + offset, 15658 + offset), expectedHex.substring(292, 15658)) - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7") } } - -const val nftInscriptionImageData = -"89504e470d0a1a0a0000000d4948445200000360000002be0803000000f3" + -"0f8d7d000000d8504c54450000003070bf3070af3173bd3078b73870b730" + -"70b73575ba3075ba3075b53070ba3078bb3474bb3474b73074bb3074b733" + -"76b93076b93373bc3373b93073b93376bc3275ba3075ba3572ba3272ba33" + -"75bc3276b83474bb3274bb3274b83276bd3276bb3474bd3474bb3276b934" + -"74bb3474b93274bb3274b93476bb3276bb3375ba3275ba3373bc3273ba32" + -"77bc3375bc3375ba3373bc3374ba3174b93376bb3375bc3375bb3375b933" + -"75bb3374bb3376bb3475bb3375bb3375ba3374bb3276bc3276ba3375bc32" + -"75bc3375bb3275bb3374bc3374bb3375bb7edf10e10000004774524e5300" + -"10101f20202030303030404040404050505050505f606060606f70707070" + -"7f7f7f7f80808080808f8f909090909f9f9f9fa0a0afafafb0bfbfcfcfcf" + -"cfcfdfdfdfdfefefefef6a89059600001c294944415478daeddd6b7bd3d6" + -"ba2ee0d8350eb00c81ec5533cbac69d6da99383334dbe510904b66129383" + -"feff3fda17506888751892255b52eefb634b1259d2a3f1ead5d0f0d61600" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000084eaef3c9b4e274f87f604546e" + -"27ba8abf3a19db1b50a96114dfb01031a8d0cf57f18f0e7a760a54e4b778" + -"c9c2ad1854a27f1c27586cdb3350c1edd7224eb667dfc0aa9e5fc569666e" + -"c460b5f2f0df71063762b08afb8b389b3211ca978771aec82006a5fcf870" + -"39cd8587ce5066f8ba8ac3cc0c6250d0cec738d8c53fed2f2820bb79a89d" + -"08ab185fc545a91321b03a8ce21216ea44c8372c152f2fb1400d375fb722" + -"b6630f427abcf6aee2d544220675c54bc420adb5318b2be25e0c6ec72b8a" + -"2bb4186bdac3dfb5e1495cb5d903fb15bed48657711d4ec67d3b973b6eb8" + -"574fbafe1ac6343cb8cba5e1248a6bb69031eeead8557bba648c3b3a74ed" + -"1c9cc56b74f54e5b913b13aebde82a5ebf9399af8da0ebe17a7ab09170fd" + -"1db2b1ee3dddbce57a3a79b77a5978383e5fb95c8c0e9eba29a33ba3d683" + -"6793d94925e3d6f9686b6bf0a69ab12c3a98ec2819696baa86c39d67cfa6" + -"b377671556846fbe3e385e7d10fbdb59f4ee60fa6c6767e899340d8dd2b3" + -"835974d3c9d9592db759974fbefdc98a06b1a5b09d9dfdf041a2d94429c9" + -"66ed44ebea4f1cde1c63aa1cc4b29fa1ed2923d9dce8b5b6787dbefbbaa9" + -"a641ccbb2f3448eec2f1955587fbcbb748bbeb1ac4e203479a8de46b5d4f" + -"b4e683c4bfbf7fbda6bf3f73ac59bfe16233d5e106ea446318eb77bc9eea" + -"f0d7ac6d189cac27618f1c6ed66cbca99baf5b9bb1965bb185e3cd9a9dae" + -"bb35bfd18819c258af876be82d0c4207d3fa231639e2acd5a431f15a4bc4" + -"2e7cdd3a6bf57ec3f75eeb8e98b75ce8cc2d58f1787d89d8bcce80fde290" + -"b34ef5bdf13f2f3d37a9cee762024617027679385a65ab06b5558a0246fb" + -"03369facfe5ad6e88d802160950f5e3787b1530143c07e48d7eb51955b37" + -"383c1730da2caa345d35ac22bf5d6dc6048c7606ecf27054d77a18db93ea" + -"6ac5270e39ed0bd87c7f54ef660ec6c7972623721703f6e970773d4b398d" + -"267301e34e05ecf470cddfdd359acc2f058c3b10b04faf27a3cd2c42b8bd" + -"7b583e6502c65a9598ec7b393fdc54b66ea66c7a7c2a6034dddb2283d6e9" + -"ebe978bb496be76eef4e0ee7a702467b03f6e9d3fcf8f574321edd6bee87" + -"e88f46e3e9e1f1fcf4536ee9e875159a12b07fddbb77af7d1fa87fefde76" + -"c6f4949f1c721a12b0f64e7a1030044cc0b8038ed2cfc51702062b9aa69f" + -"8bd3d67ea86b0143c0ea93d14574c411300143c0040c5609d82b01831565" + -"7cf7c31f020602b66490b174b6238e80d51730df5f848009187721601f04" + -"0c56f43863259bb67ea68cef3cfbd31147c07c2604cc6782554ec63f050c" + -"3404040c015ba72e764611b03604ec0f479ca604ec42c060551d9c183b11" + -"3004ac3e9d7cc70d011330b8ad830bc40818cdd1c125ce8e040c01ab4f27" + -"175345c0040c6e3bedde57fdbc17301a23ea5ec03af891e862c09e0818a8" + -"a7040c1d81353af3059708d84602e6cb5558b30e3e95f5ed45344707e715" + -"59391b011330ee84eebd3cd513309aa37bafff5ad8977604ec8380c18a76" + -"3bb7c65917977aa4b51edfa580591691757bd8b9824ac070c7e2ae923b1f" + -"b08bce05ccaa6dac5bf79e1a4d058c06b94b01b3e60d6bd7b9a9b11695a2" + -"493af77247c60b382f1c6e1a14b076be9e68cd1b9aa4732fd877709511ba" + -"19b05f5c31a0be5b967606ec4cc0d013d848c0ac18c0da75aeab9df160af" + -"e770b36e19cf655f752d608e366bd7b5a97b0301a325016be5e4f3875e68" + -"a649baf64ab3179a69cb09b9e8d8e7f1be258dba676965c0bc6f89a6408d" + -"265e07a3513af6dcc8eb60344bc75e087b2b60344ac7de57f1b60acdd2b1" + -"af41f7b60aad39237f71bd80fa6e5ada389dde17c8d22c1d9b4e1f7b5b85" + -"46e9d63a82be1d8c86e9d674fa879d5ba998ee06ac8593f71e9b4c8f53b2" + -"3ebbe6fad22cddfafa8789b9beb4a72dd0bec988befa81a6e95463db5444" + -"9aa6538f66232bd3d330a75d9a2b652a224df3be4b177debfae2b6c50d25" + -"7748971a6f5645a471ba3495e3b1995234cd6e87a672ec5a1591a6e9d2fc" + -"d8899952b4e9bea56d53398e4ce4a071e2ee3c697e6f22078d73d69d87b3" + -"9135a568d359d9b627cdd79e33d3386fbbf31d7cb1256fd019a8cd43df1f" + -"4bf38c3bf3f0c873661a68b733a7a5c7603450771e841d593080e6e975a6" + -"35f0be3bed1a3ae4ac2b4f8f4ebdcf4cbbcecb76cd7f883d06a381de76a4" + -"4fffd063309a68da913efd63af5bd244e38ef4e927de06a36da5559b56b2" + -"38f2188c26caead3b7693e7da44b4f235d77a3bd7dad4b4f239d76a28dd8" + -"d3a5a799de76a23bf0d8a28834d3a4136dc489b9f434d36e27aefdef3511" + -"69a64127da88a7e6d2d350d75d6870c79a883454d4818bff634d449aeaa8" + -"03fd8189053968aa7107ba1c334d449a2a6b36625bdeb93cd544a4a9b226" + -"41bcf211604519ab0644edef71f87a66362c63b2d4453b3a0453af33d35c" + -"93d6f7b8233d0edad9e568c7c237b11e070d76ddf29bb0c7adbf42d06951" + -"cb6fc2a6e671d064472d3f4123f33868b2dd763f46ca7a0a6645299ca177" + -"fbfa40f79db57a3ae2cc6366da7b13d6fcd7a916195b3f7070d9bcac09f5" + -"8d6fd43f760b468b6fc21adf879bb905a3e94e5b5c232edc82d1e69bb086" + -"d7888f634fc168ba517bcfd299898834df756ba7f35d5b508ae6cb78272c" + -"fed8e40dcf6a807a178c569ca74d9e8f98350ff1dc71a521321bf50d6e73" + -"0c634d7ada206a679b63167b558536c85a37a0b96d8ecc014c85484b6ac4" + -"c6be76993980cd1c555a5223367408cb1cc0f4106992710b87b0990a91d6" + -"d488d7ad1bc2b207b0b1634a931c659eaec3b60d605e05a35932e72336f1" + -"59d8fdcc0df6c596b4a9cdd1c0373f162ddb5eeeb8cc4761f1a2697d8edf" + -"622d0ebad3e668da63a5ec0e871607cdf332fb9cfd6793b6b59f5d206a71" + -"d0c0212cfb9cbd68522731bb836816074df4266ecb6d58f60d98018c461a" + -"e59cb68de9d5ff1c1bc068a1a81d27eefd2b03185d1cc2e2bd56e4cb0046" + -"3befc21a91b0dc7c19c068aac175dcf4d161e72a3680d1562ff3cedef8dd" + -"667b893fe76ee0b9018cc6eae50e61f16293cfc37ecbddbcf8574791e69a" + -"e49fc18b8d4da4ed1fe76f9d5988345a947f0e6faad5717f11b06ddb0e21" + -"edee73c4717cb28932f179c086e970d0fe3ec7e73271edd3d5872143ab0e" + -"07dd2812e378b6de41ecf955d0568d1c3e3a5124c6f1628defaf840d5f71" + -"fc2f478fe69b849dcd6b6bd8f7f7023748079156f83df0848e0fd611b19d" + -"45e0d65cba01a3157a27a109abbfd9713f0add16eb04d0b1dbb02f11dba9" + -"b53afc77f086b801a33db6c3cfeb1afb89fdbdabf0cd78e3a8d11ee378f3" + -"117bb228b00dffe93b68b4c87e91842d6a983cb51315d9024f98e972c22a" + -"ef760c0bc54bbee87ac22a8d5891de867c713712164715dd8a15ea6dc817" + -"77266195743b0ac74bbeb833095b3d62e3452c5fdc1593c2095bada158ac" + -"75f8c55c7f9ef61a5d178f58e96ec7fde2f18a0f1d23da6c705efca42fb7" + -"6a47d1d6a1f951742261c725cefbe2b762c57b1b9fe7cf7bc192bbd8ea28" + -"1eb1e2bd8dcfd3a3b437e8c48d588932b1d01bcf257a1b6ebfe85099f8a6" + -"4c0042bb1dc35999dfae3ca44326d775d589a56ebe74e7e9da2056aa8a8b" + -"f7f296b2df59941abe2c904de706b1f3eaebc461b9d8ce75377027f6edfb" + -"5886155787e74f1c0b3a697c5eae4eacb23a8c0fdd7dd159a59e8925d689" + -"fde372d5e17d070175626e3ff179a9eaf0d2d26ca813f307b172cd8dcb7d" + -"d5212296ffc2f35ea951f058ef903b5227febe4ab363e763a99b2f333770" + -"2b96ff7511a5de4a89cfc58b3b16b179a941ac546fdec40ddc8ad5466f03" + -"11ab8f07cbdc5dfb7547ccb443743bea8b97de06225657bcb40ea1744351" + -"eb1036d6edd03a84fa2236132ff8a14edcd7db8016743bf43620d1fdf32a" + -"6ebeec47a8eb56ccbc0dc8aa137f77f3057546ecbd275fd0bc3a517508b5" + -"d589aa43088fd8b9ea106a54e8eb22ac67034507b1379e2cc3e69b1d9a1b" + -"50db2066f882fa0631c317d4368819bea0be41ccf005b50d62862fa844e2" + -"33b163c3175434889d9bba0135dab79e28ace94eecc4175542e5111b1f9f" + -"7efaf4e9f850730300000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000068bff16c49effbfffc9fe5ff698f41016fe3253f7d" + -"ff9fd1f2ffb4c740c040c040c0046c15b3aa0ceedcae1b26ec8583b01f7d" + -"b2f483e3b01f7cbef483ff2b608d1657e5d19ddb75bdebd2bb61f9bcbd08" + -"fb9bcb7f7226600276778aab781a34f695dd7fbb35ec770113b0861a25ec" + -"8628e407c7093ff82aa8a05ffab173f7600276a76ac49f4a55888135620d" + -"15a28009585dfefbd992b5d488c3b23b30a1427cd2fa80f5968fc28e8075" + -"22602b9f3d256bc471e20e0ca81167657b234d0ed860f94ffc216002965a" + -"23f64a55884159a9a3421430016b6cc0126bc417e52ac4803d584b852860" + -"02d6dc808dcbd488e3943d987bf7564b85286002d6dc8095aa11a3943db8" + -"c8fb6b8b3a2a440113b0e6062c312c2fca5588b9bbf0f1f24f8c054cc03a" + -"1db01235e23875174e8b5688010d150113b036072ca946bce895aa107393" + -"b95c217ed8123001eb74c012e3f24bb90a316716484d15a2800958930356" + -"b8461c67ecc3e9062a4401abd728d5ff4938030ed3ff79ff6e06ac708d78" + -"9a11b0680315a2806dcaa068f17327039658233e2a5721660e4975558802" + -"d6ad800dd734d2f587c3dc3f55c9d933890b9d2093cc80bd2854210e56fb" + -"f8b5062cfcef0b5858c03e1fd21ffdb083fb3b93d9c9591cc7f3bf767fd6" + -"3fcefdd5c3f403fbec60767276f56d83cfa2e860b2d3af3160bdb8508d78" + -"9a19b0a84885384fd8530f6e7ffc77d3a70fd619b0efc7f9b3ab93e820ef" + -"cf0b5860c01e67eca6fe24baba795a14dca7d3c0226c78e3cfdc9a24f16e" + -"b25353c08ad588c39c5651af7c85387c363b4bfea557efc6c3b5042cf900" + -"2c664f05acce80ed1c5fddbaeed611b0fe5e9473f2465f4fb383e89b8493" + -"21baa5648df8aa5c85985123e654883b7b27d9bff8ddd3ba03d69fa41f80" + -"abd952c2bfefe2840d5fdc3a0a0702961eb061b45cd8541fb0fede55c8b3" + -"84689cf5a0372e35a2f58abc7a729ab781652ac47e5ebabe9eb5e33a0396" + -"7b00a25b254491a33017b0d4803dbf8aeb0f5860bcbe9c66412763a192b1" + -"408d38ccfd83bdc215e2f020f4c32f86b505ec79c036fc388a09581501eb" + -"1f27eeac8a03b6b388eb52b68ff8aa5c8598ba87532bc4e1acc8c7d9ab27" + -"603b1f8bff7901ab2060c38ff11a02f63c8e371ab00235e269ee1f8c8a55" + -"88e143f75727c31a02167e006e8ca102b67ac0fa8b780d01fb2dde70c0c2" + -"6bc4fc0a31a5c39f52210e3f16fe40cb65e2aa01eb17b9a95d6c0b587501" + -"3b8ed710b0c2f95a541eb049e8b4c2e57f781db68b8f922bc4de79bc7ac2" + -"560cd8b0607dfeb3805515b0ab780d012b9caf595479c0926ac4455085f8" + -"e751d88e48ab10c72506e545afca800d0bdffffe2c6015052c5e43c08685" + -"cfaf41f5010bad119737f6c528a8467c98da432c3184dd5e6760a5800d4b" + -"f4971e09587b02b6287e76d510b0c01a719250e75d8724f328f529739921" + -"ecd65254ab04ac4cbee28ba180b52560c5cfaf411d01eb85b503972bc4a4" + -"ecfc115221fef9ed7f9519c27e1c245709585426dff1494fc05a12b01203" + -"581d014bfc9d3fe55788e3a4c5812f422ac4ef53aa4665cef06945012bdb" + -"c03d10b076042cf96f5ebe9eec6edffb6c7bb43b3d3ebdf57375042ca946" + -"7c1152216e85d488e9156272043e7d9acf4f3f850e61e5039676037c7a38" + -"feb2ff479337e7a9b76102d682801d25fdfed1d2839addefc779be554fc0" + -"826ac4d3c4f3e6287f16484685786b08bb3c9eecdefbf67fb677d3ceef1f" + -"c25f3e6089f5c3e5e1f6cd6d1f9fa73c4e17b016042c6162c4bf927fc5e8" + -"cdf7dedbd1fc9b84b7fde7b784eeaca4d4f6022ac4901af1617640a2bf4f" + -"eda56b4bcaf9fd43f84b072c7102c77c10b409e3adbff77142486f1d05b3" + -"e9730336ff7665bd77afc28005e7ebf3dfdc3fbffd6d7515be0fff32bf46" + -"7c995ce7f5726bc4495685f83da1cb23f7d7b1f5f7c4e3d15b3d60891dc4" + -"5f93f6fc49e66342ef83ad1cb0cbfd7eeeaf2e13b0e54b7bf6f73dee3fa9" + -"2d6083fc1a719152f8bccdab114fb32ac4bf3e464abcbe7cea9c1ab16cc0" + -"923a1cff480ef971ea733c01ab2060fbfd805f5d2660cb7fb2d83a4b55ae" + -"e8925b233e4c3bc7f26ac48456c28f091cc5e7a3ac4d4b1ac3fe583d608b" + -"b0f1eb4bc24e32ae3e02b65ac0920e7e6d01fb636301cbad118fd2eabcbc" + -"1a31a142bcb5ccc5ef39cbcb649768250336ce9d2272f380677c46015b29" + -"60e783ad3506eccf8d052cb7465ca4b6c6a2ece754a7050be1840e4fe6e8" + -"5a326051d8a14e8de32b01ab22609783ad7506ecfb5cedb5072ce997dd7c" + -"dcf430fd2e649c35bee4578801aeb306c172011b66dd5685ec9e85805511" + -"b0f1568d014bdac27f6e2a602fb377d7517a2730bb46ccaf10f31d656d5a" + -"b9808d836629670d613f09d8ea017bbf5567c0121f562e0e1e6c24603935" + -"e222e3e169668db87a859878bff462c580bd2f7007967c15792160ab07ec" + -"51bd014b5d073075add1fa0296b4317f5fd41f668dede38c64565121263d" + -"aa9eae18b045ce1cfd25efd38eb880950fd89f5bf506ec28e3d1f655c692" + -"beb504ec65d605e628eb59712fe3141f075fb58a1dca57ab05ac97fdf03b" + -"c124ede410b0f2017b5173c0f2a7929f640d66d5066c94f5b8699139bd2e" + -"a3468c8a5588fd074f9f3d9bde7694b5bf4b052ce1684fb31da73ded13b0" + -"f2017b5073c002df863a3978daaf3f60893562488598d4c888d22bc4b47b" + -"9de1647612bec6d48a012bf5a267f25f11b0d201bbd8aa3b6093e0a3194d" + -"8675072ca3463cca2ea87aa9cfa9422bc49d8382ebb7ad18b06905017b20" + -"602b066c5e7bc00abd7b727bf1e6aa03965423be0aa91093b6e445810ab1" + -"3f39297c76af18b0a30a02f648c0560cd887fa033628f4cafcadaf20a8fa" + -"dbe5526bc48779cf07d36ac4a00af149998531560cd8db0a02f68b80ad18" + -"b069fd01dbda2e94b08b719d014bad118ff25a6e6935624085d83f2e7576" + -"0b98800505ace018f6c3f2e855072cb546ccab10536bc4fc0a7158725d7e" + -"0113b0b0806d0dde944d58e55f407c9d5c23e656886935627e8558365f4d" + -"08d813016b48c08e720aa571a141ec9ff505ec28b946ccad10d36ac4dc0a" + -"b174be9a10304d8ea604ec7dee9d48a1883daa2d604935e234a4424cda96" + -"5f022ac459bca18069d3b72d6059ef229f063c0c1abd09ce58545bc0926a" + -"c445488598d42089f22bc4151ef7ae18b0490501f3a0799d01cb7a553270" + -"16f1f6f8f8bcd010567dc0926ac407b390697bcbbbe4a2975b21a61488a7" + -"afa7d3f14d93ca03b6bbfcdfffefb8a02d015b63c02e0afde6d409af83db" + -"2b8d665dbdab0f58628d18522126d68851ce5e4a1cc0e6a37eb192bc54c0" + -"06e1134f4b9c6702b662c07a71d6410d6c1d64d8de9dbe9e5fa63f0dab2d" + -"604935e2554885985823e6558809ff2079f99bea0396f04123016b4cc092" + -"8abef40be0a270c0be8d6687f3ccdbeb1a0216348568103af8a575b6532f" + -"5329cb62d410b0f7a97b55c01a10b0b338fc02388e4b06eccb8748ec2ffe" + -"525bc04262320fde27d91562caf7caae27609390495c02b6a98025dd260d" + -"0b0c60455e3adc4fddb01a0296542306a6e065c05cca9c87d3e75b6b0bd8" + -"60b5832260f506ec6d1c7c01fc2d5e31600927c9abfa027654b2420c1afc" + -"9ee4fca90feb0b58d2ff59f4caecb15eb16736021612b0497068eec72b07" + -"6c9c7676bd2fd26aa9ac469c17a89b333badef8377f86e0d017b19172e12" + -"9f6f07de902f046cc580259e868b8422f1fe555ec0a271de07495d00f86d" + -"75bde60235e2b8f4e0f72170640e18c3570f58efba60c2faffefaf2f8f0d" + -"d8633d015b2d608947272161cf73a73b8de378b193fd4126691b7654a0d5" + -"525d8d38283df88df346b03fc2db442b072cf9839ea4dd496f0d3fa65c43" + -"9346eea980ad16b0b43791f77ef8473b51e07cc245e6287692364e8d736f" + -"736aa811e7e507bf5ede8d6cf2d3fac419c1ab076c90b88d2987a2bf7795" + -"5aa52454ea1743015b2d606973d916b3bf1681eaefec4501b39d1e7fffb9" + -"d4a730bfa5fef4c3a4fb9c9bf7093b653acfd7252bc4dcc1ef43fe3f4fba" + -"3cf43fc6b5046cebf7383462dfe295dc0839ca1b099f8f05ac68c07a1927" + -"d2d5d9d959de0dffb780cd12b2f9e3e53be985dfcc8dd81b7e8f57a99b81" + -"b7252bc4dcc16f9c5ff9250c10c38f714d01eb5da75e259fded88e0793e8" + -"e68df4492fa0057323a6fdff8e2a28dcef5cc08a7d53726ac06ecf373f39" + -"98ecfc7d6cfb3b9328732848de88ab683a9d1ebcbb2a7933302a5b21e60d" + -"7ebd8003b494b09d455c57c09293f17d2746b3d96c169de53742522eb657" + -"ef0ea6d3d9d7703e10b0a2011b5512b0e4f7a1cecea2283a39bbca1d0af2" + -"9fed7e2cf15ce7ba64859853237e08cbe3cd857dfacfa29a5e57f9ea4da9" + -"63372b7eb19d0a58d180ad38843d4a1cc0c29c0715aae56727bc2d320c85" + -"5f7596a399727d880e9eedececec3c3b88aef2df28582960bdf35207602f" + -"f0863ce85d0b01ab65087b94318015e876e7a7bcf8372d647eb20fe51b24" + -"83a02314aa9280155d71282561bdeb3aae73773d60694da802011b5e97f9" + -"d179a1945f146f73649d2fe3d2835fd2cddbf1a603b6b55dea10dcfe16b7" + -"fc523d12b0c2012b595fdc08d86fa50ac441b142b5c4dc8eb7252bc4ccc0" + -"27457370bde980954cd83f0a0f613d012b7c6bba7dbd62c0ca2ca97479bf" + -"e0295ae2da392a5b21669d6983228f1397ae2ad77505ac5495f88fe29f63" + -"2a60c5774ae86dd87f0e13035666c997cb27f90f9352cfb6d56bc471e9c1" + -"2fa5bd1fd8c71b9fd516b0ad41d185f12fff51e273440256e2aab31b3486" + -"cdfbd3c480bd2c9eaff3fb4bdbb05fc3b5f37de9426754309abda0b3fb70" + -"abc68005ecc2bc6310f2391e0958895333a4be384c5a85efcbee1e15ad4e" + -"e6497556dee951a2453c2e5921660c7e6913407a018d8e375bf506ac5099" + -"78dc2f77a578256065aefdb9eb5e5ffeba951ab0adadf1bc4869f26bf236" + -"4c2abf76a6c524604e5d547802c87e40be6a0ed8d6d6243062e74fca7695" + -"2f7a0256aab8ca5e94f7eb98334d3fe907a10b8e5eeef7cb8da39725a69a" + -"46a55b61e3e2d1ccd9fc2f9795ba03b635d83f5fe918e49e09ff190858c9" + -"bb97f41d3bff6b11b269e6a8b21b90b193cc439b756c67fd12fb6e5cb242" + -"4c1dfc06652f52b3af3f597bc002ca89f92467570ef62fd3a3a9c9b1427b" + -"20f1d05cbeffbec6df34af6c1b4d3396418ce7fba3fc4d382e7ec92d1a93" + -"71e9c16f9edb2e4adefcc36fc15c47c03eafe1355fe5187cfe05a7a5a2d9" + -"4abd84d58e0785fef9fdf03fd6df3dbc1991cbd3c39b0bd4de0fd990edcf" + -"8bfade8ad9e5e9ebc9a85f6a13e24fc79351e99db79bb45674d05dc420e9" + -"27ef17dffccb1f4ee9a5edf9fb2df0ffcadad0ff4a5df13ab50f3ab97d14" + -"3e1d1fee86076430fe7169e64faf3b99ae8de86f8fbe1cc2d1bd157ec7bd" + -"d1d75f32de1ddd2b7e64faa36f9bd0caa3fa7df3b7fb9b3e92bb7f6d48a9" + -"63f9d741dc5de54c00000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000000000000008066fbffddd184" + -"8d4adc88950000000049454e44ae426082" - -const val nftInscriptionRawHex = -"020000000001011771decbce2766b39d8fe66f4dc11737b3146c71f8cc6a" + -"e1397384c5e508e7f10000000000ffffffff012202000000000000160014" + -"e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340cc1e7b0b5fa18b28" + -"dce702e4e8ed2e91069d682b8daa3a773774bfc7d0e6f737d403016a9016" + -"b58a92592ad0b41682e6209167444eb56605532b28e9be922d3afdda1d00" + -"63036f7264010109696d6167652f706e67004d080289504e470d0a1a0a00" + -"00000d4948445200000360000002be0803000000f30f8d7d000000d8504c" + -"54450000003070bf3070af3173bd3078b73870b73070b73575ba3075ba30" + -"75b53070ba3078bb3474bb3474b73074bb3074b73376b93076b93373bc33" + -"73b93073b93376bc3275ba3075ba3572ba3272ba3375bc3276b83474bb32" + -"74bb3274b83276bd3276bb3474bd3474bb3276b93474bb3474b93274bb32" + -"74b93476bb3276bb3375ba3275ba3373bc3273ba3277bc3375bc3375ba33" + -"73bc3374ba3174b93376bb3375bc3375bb3375b93375bb3374bb3376bb34" + -"75bb3375bb3375ba3374bb3276bc3276ba3375bc3275bc3375bb3275bb33" + -"74bc3374bb3375bb7edf10e10000004774524e530010101f202020303030" + -"30404040404050505050505f606060606f707070707f7f7f7f8080808080" + -"8f8f909090909f9f9f9fa0a0afafafb0bfbfcfcfcfcfcfdfdfdfdfefefef" + -"ef6a89059600001c294944415478daeddd6b7bd3d6ba2ee0d8350eb00c81" + -"ec5533cbac69d6da99383334dbe510904b66129383feff3fda1750688875" + -"1892255b52eefb634b1259d2a3f1ead5d0f0d61600000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"004d08020000000000000084eaef3c9b4e274f87f604546e27ba8abf3a19" + -"db1b50a96114dfb01031a8d0cf57f18f0e7a760a54e4b778c9c2ad1854a2" + -"7f1c27586cdb3350c1edd7224eb667dfc0aa9e5fc569666ec460b5f2f0df" + -"71063762b08afb8b389b3211ca978771aec82006a5fcf87039cd8587ce50" + -"66f8ba8ac3cc0c6250d0cec738d8c53fed2f2820bb79a89d08ab185fc545" + -"a91321b03a8ce21216ea44c8372c152f2fb1400d375fb722b6630f427abc" + -"f6aee2d544220675c54bc420adb5318b2be25e0c6ec72b8a2bb4186bdac3" + -"dfb5e1495cb5d903fb15bed48657711d4ec67d3b973b6eb8574fbafe1ac6" + -"343cb8cba5e1248a6bb69031eeead8557bba648c3b3a74ed1c9cc56b74f5" + -"4e5b913b13aebde82a5ebf9399af8da0ebe17a7ab09170fd1db2b1ee3ddd" + -"bce57a3a79b77a5978383e5fb95c8c0e9eba29a33ba3d6836793d94925e3" + -"d6f9686b6bf0a69ab12c3a98ec2819696baa86c39d67cfa6b37767155684" + -"6fbe3e385e7d10fbdb59f4ee60fa6c6767e899340d8dd2b3835974d3c9d9" + -"592db759974fbefdc98a06b1a5b09d9dfdf041a2d94429c966ed44ebea4f" + -"1cde1c63aa1cc4b29fa1ed2923d9dce8b5b6787dbefbbaa9a641ccbb2f34" + -"48eec2f1955587fbcbb748bbeb1ac4e203479a8de46b5d4fb4e683c4bfbf" + -"7fbda6bf3f73ac59bfe16233d5e106ea446318eb77bc9eeaf0d7ac6d189c" + -"ac27618f1c6ed66cbca99baf5b9b4d0802b1965bb185e3cd9a9daebb35bf" + -"d18819c258af876be82d0c4207d3fa231639e2acd5a431f15a4bc42e7cdd" + -"3a6bf57ec3f75eeb8e98b75ce8cc2d58f1787d89d8bcce80fde290b34ef5" + -"bdf13f2f3d37a9cee762024617027679385a65ab06b5558a0246fb03369f" + -"acfe5ad6e88d802160950f5e3787b1530143c07e48d7eb51955b37383c17" + -"30da2caa345d35ac22bf5d6dc6048c7606ecf27054d77a18db93ea6ac527" + -"0e39ed0bd87c7f54ef660ec6c7972623721703f6e970773d4b398d267301" + -"e34e05ecf470cddfdd359acc2f058c3b10b04faf27a3cd2c42b8bd7b583e" + -"6502c65a9598ec7b393fdc54b66ea66c7a7c2a6034dddb2283d6e9ebe978" + -"bb496be76eef4e0ee7a702467b03f6e9d3fcf8f574321edd6bee87e88f46" + -"e3e9e1f1fcf4536ee9e875159a12b07fddbb77af7d1fa87fefde76c6f494" + -"9f1c721a12b0f64e7a1030044cc0b8038ed2cfc51702062b9aa69f8bd3d6" + -"7ea86b0143c0ea93d14574c411300143c0040c5609d82b018315657cf7c3" + -"1f020602b66490b174b6238e80d51730df5f848009187721601f040c56f4" + -"3863259bb67ea68cef3cfbd31147c07c2604cc6782554ec63f050c340404" + -"0c015ba72e764611b03604ec0f479ca604ec42c060551d9c183b113004ac" + -"3e9d7cc70d011330b8ad830bc40818cdd1c125ce8e040c01ab4f27175345" + -"c0040c6e3bedde57fdbc17301a23ea5ec03af891e862c09e0818a84d0802" + -"a7040c1d81353af3059708d84602e6cb5558b30e3e95f5ed45344707e715" + -"59391b011330ee84eebd3cd513309aa37bafff5ad8977604ec8380c18a76" + -"3bb7c65917977aa4b51edfa580591691757bd8b9824ac070c7e2ae923b1f" + -"b08bce05ccaa6dac5bf79e1a4d058c06b94b01b3e60d6bd7b9a9b11695a2" + -"493af77247c60b382f1c6e1a14b076be9e68cd1b9aa4732fd877709511ba" + -"19b05f5c31a0be5b967606ec4cc0d013d848c0ac18c0da75aeab9df160af" + -"e770b36e19cf655f752d608e366bd7b5a97b0301a325016be5e4f3875e68" + -"a649baf64ab3179a69cb09b9e8d8e7f1be258dba676965c0bc6f89a6408d" + -"265e07a3513af6dcc8eb60344bc75e087b2b60344ac7de57f1b60acdd2b1" + -"af41f7b60aad39237f71bd80fa6e5ada389dde17c8d22c1d9b4e1f7b5b85" + -"46e9d63a82be1d8c86e9d674fa879d5ba998ee06ac8593f71e9b4c8f53b2" + -"3ebbe6fad22cddfafa8789b9beb4a72dd0bec988befa81a6e95463db5444" + -"9aa6538f66232bd3d330a75d9a2b652a224df3be4b177debfae2b6c50d25" + -"7748971a6f5645a471ba3495e3b1995234cd6e87a672ec5a1591a6e9d2fc" + -"d8899952b4e9bea56d53398e4ce4a071e2ee3c697e6f22078d73d69d87b3" + -"9135a568d359d9b627cdd79e33d3386fbbf31d7cb1256fd019a8cd43df1f" + -"4bf38c3bf3f0c873661a68b733a7a5c7603450771e841d593080e6e975a6" + -"35f0be3bed1a3ae4ac2b4d08024f8f4ebdcf4cbbcecb76cd7f883d06a381" + -"de76a44fffd063309a68da913efd63af5bd244e38ef4e927de06a36da555" + -"9b56b238f2188c26caead3b7693e7da44b4f235d77a3bd7dad4b4f239d76" + -"a28dd8d3a5a799de76a23bf0d8a28834d3a4136dc489b9f434d36e27aefd" + -"ef351169a64127da88a7e6d2d350d75d6870c79a883454d4818bff634d44" + -"9aeaa803fd8189053968aa7107ba1c334d449a2a6b36625bdeb93cd544a4" + -"a9b22641bcf211604519ab0644edef71f87a66362c63b2d4453b3a0453af" + -"33d35c93d6f7b8233d0edad9e568c7c237b11e070d76ddf29bb0c7adbf42" + -"d06951cb6fc2a6e671d064472d3f4123f33868b2dd763f46ca7a0a664529" + -"9ca177fbfa40f79db57a3ae2cc6366da7b13d6fcd7a916195b3f7070d9bc" + -"ac09f58d6fd43f760b468b6fc21adf879bb905a3e94e5b5c232edc82d1e6" + -"9bb086d7888f634fc168ba517bcfd299898834df756ba7f35d5b508ae6cb" + -"78272cfed8e40dcf6a807a178c569ca74d9e8f98350ff1dc71a521321bf5" + -"0d6e730c634d7ada206a679b63167b558536c85a37a0b96d8ecc014c8548" + -"4b6ac4c6be76993980cd1c555a5223367408cb1cc0f4106992710b87b099" + -"0a91d6d488d7ad1bc2b207b0b1634a931c659eaec3b60d605e05a35932e7" + -"2336f159d8fdcc0df6c596b4a9cdd1c0373f162ddb5eeeb8cc4761f1a269" + -"7d8edf622d0ebad3e668da63a5ec0e871607cdf332fb9c4d0802fd6793b6" + -"b59f5d206a71d0c0212cfb9cbd68522731bb836816074df4266ecb6d58f6" + -"0d98018c461ae59cb68de9d5ff1c1bc068a1a81d27eefd2b03185d1cc2e2" + -"bd56e4cb00463befc21a91b0dc7c19c068aac175dcf4d161e72a3680d156" + -"2ff3cedef8dd667b893fe76ee0b9018cc6eae50e61f16293cfc37ecbddbc" + -"f8574791e69ae49fc18b8d4da4ed1fe76f9d5988345a947f0e6faad5717f" + -"11b06ddb0e21edee73c4717cb28932f179c086e970d0fe3ec7e73271edd3" + -"d5872143ab0e07dd2812e378b6de41ecf955d0568d1c3e3a5124c6f1628d" + -"efaf840d5f71fc2f478fe69b849dcd6b6bd8f7f7023748079156f83df084" + -"8e0fd611b19d45e0d65cba01a3157a27a109abbfd9713f0add16eb04d0b1" + -"dbb02f11dba9b53afc77f086b801a33db6c3cfeb1afb89fdbdabf0cd78e3" + -"a8d11ee378f3117bb228b00dffe93b68b4c87e91842d6a983cb51315d902" + -"4f98e972c22aef760c0bc54bbee87ac22a8d5891de867c713712164715dd" + -"8a15ea6dc81777266195743b0ac74bbeb833095b3d62e3452c5fdc1593c2" + -"095bada158ac75f8c55c7f9ef61a5d178f58e96ec7fde2f18a0f1d23da6c" + -"705efca42fb76a47d1d6a1f951742261c725cefbe2b762c57b1b9fe7cf7b" + -"c192bbd8ea281eb1e2bd8dcfd3a3b437e8c48d588932b1d01bcf257a1b6e" + -"bfe85099f8a64c0042bb1dc35999dfae3ca44326d775d589a56ebe74e7e9" + -"da2056aa8a8b4d0802f7f296b2df59941abe2c904de706b1f3eaebc461b9" + -"d8ce75377027f6edfb5886155787e74f1c0b3a697c5eae4eacb23a8c0fdd" + -"7dd159a59e8925d689fde372d5e17d070175626e3ff179a9eaf0d2d26ca8" + -"13f307b172cd8dcb7dd5212296ffc2f35ea951f058ef903b5227febe4ab3" + -"63e763a99b2f3337702b96ff7511a5de4a89cfc58b3b16b179a941ac546f" + -"dec40ddc8ad5466f0311ab8f07cbdc5dfb7547ccb443743bea8b97de0622" + -"5657bcb40ea1744351eb1036d6edd03a84fa2236132ff8a14edcd7db8016" + -"743bf43620d1fdf32a6ebeec47a8eb56ccbc0dc8aa137f77f3057546ecbd" + -"275fd0bc3a517508b5d589aa43088fd8b9ea106a54e8eb22ac67034507b1" + -"379e2cc3e69b1d9a1b50db2066f882fa0631c317d4368819bea0be41ccf0" + -"05b50d62862fa844e233b163c3175434889d9bba0135dab79e28ace94eec" + -"c4175542e5111b1f9f7efaf4e9f850730300000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000068bff16c49effbfffc9fe5ff" + -"698f41016fe3253f7dff9fd1f2ffb4c740c040c040c0046c15b3aa0ceedc" + -"ae1b26ec8583b01f7db2f483e3b01f7cbef483ff2b608d1657e5d19ddb75" + -"bdebd2bb61f9bcbd08fb9bcb7f7226600276778aab781a34f695dd7fbb35" + -"ec770113b0861a25ec8628e407c7093ff82aa84d0802a05ffab173f76002" + -"76a76ac49f4a55888135620d15a28009585dfefbd992b5d488c3b23b30a1" + -"427cd2fa80f5968fc28e807522602b9f3d256bc471e20e0ca81167657b23" + -"4d0ed860f94ffc216002965a23f64a55884159a9a3421430016b6cc0126b" + -"c417e52ac4803d584b85286002d6dc808dcbd488e3943d987bf7564b8528" + -"6002d6dc8095aa11a3943db8c8fb6b8b3a2a440113b0e6062c312c2fca55" + -"88b9bbf0f1f24f8c054cc03a1db01235e23875174e8b5688010d150113b0" + -"36072ca946bce895aa107393b95c217ed8123001eb74c012e3f24bb90a31" + -"6716484d15a2800958930356b8461c67ecc3e9062a4401abd728d5ff4938" + -"030ed3ff79ff6e06ac708d789a11b0680315a2806dcaa068f17327039658" + -"233e2a5721660e4975558802d6ad800dd734d2f587c3dc3f55c9d933890b" + -"9d2093cc80bd2854210e56fbf8b5062cfcef0b5858c03e1fd21ffdb083fb" + -"3b93d9c9591cc7f3bf767fd63fcefdd5c3f403fbec60767276f56d83cfa2" + -"e860b2d3af3160bdb8508d789a19b0a84885384fd8530f6e7ffc77d3a70f" + -"d619b0efc7f9b3ab93e820efcf0b5860c01e67eca6fe24baba795a14dca7" + -"d3c0226c78e3cfdc9a24f16eb25353c08ad588c39c5651af7c85387c363b" + -"4bfea557efc6c3b5042cf9002c664f05acce80ed1c5fddbaeed611b0fe5e" + -"9473f2465f4fb383e89b849321baa5648df8aa5c85985123e654883b7b27" + -"d9bf4d0802f8ddd3ba03d69fa41f80abd952c2bfefe2840d5fdc3a0a0702" + -"961eb061b45cd8541fb0fede55c8b384689cf5a0372e35a2f58abc7a729a" + -"b781652ac47e5ebabe9eb5e33a03967b00a25b254491a33017b0d4803dbf" + -"8aeb0f5860bcbe9c66412763a192b1408d38ccfd83bdc215e2f020f4c32f" + -"86b505ec79c036fc388a09581501eb1f27eeac8a03b6b388eb52b68ff8aa" + -"5c8598ba87532bc4e1acc8c7d9ab27603b1f8bff7901ab2060c38ff11a02" + -"f63c8e371ab00235e269ee1f8c8a5588e143f75727c31a02167e006e8ca1" + -"02b67ac0fa8b780d01fb2dde70c0c26bc4fc0a31a5c39f52210e3f16fe40" + -"cb65e2aa01eb17b9a95d6c0b5875013b8ed710b0c2f95a541eb049e8b4c2" + -"e57f781db68b8f922bc4de79bc7ac2560cd8b0607dfeb3805515b0ab780d" + -"012b9caf595479c0926ac4455085f8e751d88e48ab10c72506e545afca80" + -"0d0bdffffe2c6015052c5e43c08685cfaf41f5010bad119737f6c528a846" + -"7c98da432c3184dd5e6760a5800d4bf4971e09587b02b6287e76d510b0c0" + -"1a719250e75d8724f328f529739921ecd65254ab04ac4cbee28ba180b525" + -"60c5cfaf411d01eb85b503972bc4a4ecfc115221fef9ed7f9519c27e1c24" + -"5709585426dff1494fc05a12b01203581d014bfc9d3fe55788e3a4c5812f" + -"422ac4ef53aa4665cef06945012bdbc03d10b076042cf96f5ebe9eec6edf" + -"fb6c7bb43b3d3ebdf57375042ca9464d08027c1152216e85d488e9156272" + -"043e7d9acf4f3f850e61e5039676037c7a38feb2ff479337e7a9b76102d6" + -"82801d25fdfed1d2839addefc779be554fc0826ac4d3c4f3e6287f164846" + -"85786b08bb3c9eecdefbf67fb677d3ceef1fc25f3e6089f5c3e5e1f6cd6d" + -"1f9fa73c4e17b016042c6162c4bf927fc5e8cdf7dedbd1fc9b84b7fde7b7" + -"84eeaca4d4f6022ac4901af1617640a2bf4feda56b4bcaf9fd43f84b072c" + -"7102c77c10b409e3adbff77142486f1d05b3e9730336ff7665bd77afc280" + -"05e7ebf3dfdc3fbffd6d7515be0fff32bf467c995ce7f5726bc4495685f8" + -"3da1cb23f7d7b1f5f7c4e3d15b3d60891dc45f93f6fc49e66342ef83ad1c" + -"b0cbfd7eeeaf2e13b0e54b7bf6f73dee3fa92d6083fc1a719152f8bccdab" + -"114fb32ac4bf3e464abcbe7cea9c1ab16cc0923a1cff480ef971ea733c01" + -"ab2060fbfd805f5d2660cb7fb2d83a4b55aee8925b233e4c3bc7f26ac484" + -"56c28f091cc5e7a3ac4d4b1ac3fe583d608bb0f1eb4bc24e32ae3e02b65a" + -"c0920e7e6d01fb636301cbad118fd2eabcbc1a31a142bcb5ccc5ef39cbcb" + -"649768250336ce9d2272f380677c46015b2960e783ad3506eccf8d052cb7" + -"465ca4b6c6a2ece754a7050be1840e4fe6e85a326051d8a14e8de32b01ab" + -"22609783ad7506ecfb5cedb5072ce997dd7cdcf430fd2e649c35bee45788" + -"01aeb306c172011b66dd5685ec9e85805511b0f1568d014bdac27f6e4d08" + -"022a602fb377d7517a2730bb46ccaf10f31d656d5ab9808d836629670d61" + -"3f09d8ea017bbf5567c0121f562e0e1e6c24603935e222e3e169668db87a" + -"859878bff462c580bd2f7007967c15792160ab07ec51bd014b5d073075ad" + -"d1fa0296b4317f5fd41f668dede38c64565121263daa9eae18b045ce1cfd" + -"25efd38eb880950fd89f5bf506ec28e3d1f655c692beb504ec65d605e628" + -"eb59712fe3141f075fb58a1dca57ab05ac97fdf03bc124ede410b0f2017b" + -"5173c0f2a7929f640d66d5066c94f5b8699139bd2ea3468c8a5588fd074f" + -"9f3d9bde7694b5bf4b052ce1684fb31da73ded13b0f2017b5073c002df86" + -"3a3978daaf3f60893562488598d4c888d22bc4b47b9de1647612bec6d48a" + -"012bf5a267f25f11b0d201bbd8aa3b6093e0a3194d8675072ca3463cca2e" + -"a87aa9cfa9422bc49d8382ebb7ad18b06905017b20602b066c5e7bc00abd" + -"7b727bf1e6aa03965423be0aa91093b6e445810ab13f39297c76af18b0a3" + -"0a02f648c0560cd887fa033628f4cafcadaf20a8fadbe5526bc48779cf07" + -"d36ac4a00af149998531560cd8db0a02f68b80ad18b069fd01dbda2e94b0" + -"8b719d014bad118ff25a6e6935624085d83f2e75760b98800505ace018f6" + -"c3f2e855072cb546ccab10536bc4fc0a7158725d7e0113b0b0806d0dde94" + -"4d58e55f407c9d5c23e656886935627e8558365f4d08d813016b48c08e72" + -"0aa571a141ec9ff505ec284d0802b946ccad10d36ac4dc0ab174be9a1030" + -"4d8ea604ec7dee9d48a1883daa2d604935e234a4424cda965f022ac459bc" + -"a18069d3b72d6059ef229f063c0c1abd09ce58545bc0926ac445488598d4" + -"2089f22bc4151ef7ae18b0490501f3a0799d01cb7a55327016f1f6f8f8bc" + -"d010567dc0926ac407b390697bcbbbe4a2975b21a61488a7afa7d3f14d93" + -"ca03b6bbfcdfffefb8a02d015b63c02e0afde6d409af83db2b8d665dbdab" + -"0f58628d18522126d68851ce5e4a1cc0e6a37eb192bc54c006e1134f4b9c" + -"6702b662c07a71d6410d6c1d64d8de9dbe9e5fa63f0dab2d604935e25548" + -"85985823e6558809ff2079f99bea0396f04123016b4cc0928abef40be0a2" + -"70c0be8d6687f3ccdbeb1a0216348568103af8a575b6532f5329cb62d410" + -"b0f7a97b55c01a10b0b338fc02388e4b06eccb8748ec2ffe525bc0426232" + -"0fde27d91562caf7caae27609390495c02b6a98025dd260d0b0c60455e3a" + -"dc4fddb01a0296542306a6e065c05cca9c87d3e75b6b0bd860b5832260f5" + -"06ec6d1c7c01fc2d5e31600927c9abfa027654b2420c1afc9ee4fca90feb" + -"0b58d2ff59f4caecb15eb16736021612b0497068eec72b076c9c7676bd2f" + -"d26aa9ac469c17a89b333badef8377f86e0d017b19172e129f6f07de902f" + -"046cc580259e868b8422f1fe555ec0a271de07495d00f86d75bde60235e2" + -"b8f4e0f72170640e18c3570f58efba60c2faffefaf2f8f0d4d0802d8633d" + -"015b2d608947272161cf73a73b8de378b193fd4126691b7654a0d5525d8d" + -"38283df88df346b03fc2db442b072cf9839ea4dd496f0d3fa65c439346ee" + -"a980ad16b0b43791f77ef8473b51e07cc245e6287692364e8d736f736aa8" + -"11e7e507bf5ede8d6cf2d3fac419c1ab076c90b88d2987a2bf77955aa524" + -"54ea1743015b2d606973d916b3bf1681eaefec4501b39d1e7fffb9d4a730" + -"bfa5fef4c3a4fb9c9bf7093b653acfd7252bc4dcc1ef43fe3f4fba3cf43f" + -"c6b5046cebf7383462dfe295dc0839ca1b099f8f05ac68c07a1927d2d5d9" + -"d959de0dffb780cd12b2f9e3e53be985dfcc8dd81b7e8f57a99b81b7252b" + -"c4dcc16f9c5ff9250c10c38f714d01eb5da75e259fded88e0793e8e68df4" + -"492fa0057323a6fdff8e2a28dcef5cc08a7d53726ac06ecf373f3998ecfc" + -"7d6cfb3b9328732848de88ab683a9d1ebcbb2a7933302a5b21e60d7ebd80" + -"03b494b09d455c57c09293f17d2746b3d96c169de53742522eb657ef0ea6" + -"d3d9d7703e10b0a2011b5512b0e4f7a1cecea2283a39bbca1d0af29fed7e" + -"2cf15ce7ba64859853237e08cbe3cd857dfacfa29a5e57f9ea4da963372b" + -"7eb19d0a58d180ad38843d4a1cc0c29c0715aae56727bc2d320c855f7596" + -"a399727d880e9eedececec3c3b88aef2df28582960bdf35207602ff0863c" + -"e85d0b01ab65087b94318015e876e7a7bcf8372d647eb20fe51b2483a023" + -"14aa9280155d714d0802282561bdeb3aae73773d60694da802011b5e97f9" + -"d179a1945f146f73649d2fe3d2835fd2cddbf1a603b6b55dea10dcfe16b7" + -"fc523d12b0c2012b595fdc08d86fa50ac441b142b5c4dc8eb7252bc4ccc0" + -"27457370bde980954cd83f0a0f613d012b7c6bba7dbd62c0ca2ca97479bf" + -"e0295ae2da392a5b21669d6983228f1397ae2ad77505ac5495f88fe29f63" + -"2a60c5774ae86dd87f0e13035666c997cb27f90f9352cfb6d56bc471e9c1" + -"2fa5bd1fd8c71b9fd516b0ad41d185f12fff51e273440256e2aab31b3486" + -"cdfbd3c480bd2c9eaff3fb4bdbb05fc3b5f37de9426754309abda0b3fb70" + -"abc68005ecc2bc6310f2391e0958895333a4be384c5a85efcbee1e15ad4e" + -"e6497556dee951a2453c2e5921660c7e6913407a018d8e375bf506ac5099" + -"78dc2f77a578256065aefdb9eb5e5ffeba951ab0adadf1bc4869f26bf236" + -"4c2abf76a6c524604e5d547802c87e40be6a0ed8d6d6243062e74fca7695" + -"2f7a0256aab8ca5e94f7eb98334d3fe907a10b8e5eeef7cb8da39725a69a" + -"46a55b61e3e2d1ccd9fc2f9795ba03b635d83f5fe918e49e09ff190858c9" + -"bb97f41d3bff6b11b269e6a8b21b90b193cc439b756c67fd12fb6e5cb242" + -"4c1dfc06652f52b3af3f597bc002ca89f92467570ef62fd3a3a9c9b1427b" + -"20f1d05cbeffbec6df34af6c1b4d3396418ce7fba3fc4d382e7ec92d1a93" + -"71e9c16f9edb2e4adefcc36fc15c47c03eafe1354d29015fe5187cfe05a7" + -"a5a2d94abd84d58e0785fef9fdf03fd6df3dbc1991cbd3c39b0bd4de0fd9" + -"90edcf8bfade8ad9e5e9ebc9a85f6a13e24fc79351e99db79bb45674d05d" + -"c420e927ef17dffccb1f4ee9a5edf9fb2df0ffcadad0ff4a5df13ab50f3a" + -"b97d143e1d1fee86076430fe7169e64faf3b99ae8de86f8fbe1cc2d1bd15" + -"7ec7bdd1d75f32de1ddd2b7e64faa36f9bd0caa3fa7df3b7fb9b3e92bb7f" + -"6d48a963f9d741dc5de54c00000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000000000000000000008066fbff" + -"ddd1848d4adc88950000000049454e44ae4260826821c00f209b6ada5edb" + -"42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000" diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondAddress.kt index d8f768eeaec..497b2d6dedc 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bitcoindiamond diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondSigner.kt index eea39e3a269..3bbbd9a77fd 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoindiamond/TestBitcoinDiamondSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bitcoindiamond diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleAddress.kt index 99baa611f71..d8eb3424893 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bluzelle diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleSigner.kt index ba4d7e99238..bf40f0dcc67 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bluzelle/TestBluzelleSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.bluzelle diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoAddress.kt index 6ac0f1c4d63..a505e201176 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.cardano diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoSigning.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoSigning.kt index 2e56bab0838..d9450e9881c 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoSigning.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cardano/TestCardanoSigning.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.cardano diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/confluxespace/TestConfluxeSpaceAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/confluxespace/TestConfluxeSpaceAddress.kt index 5186e4730ea..25d5ae1c81e 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/confluxespace/TestConfluxeSpaceAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/confluxespace/TestConfluxeSpaceAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.confluxespace diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cosmos/TestCosmosTransactions.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cosmos/TestCosmosTransactions.kt index ed829d1ce76..4de24f9b60f 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cosmos/TestCosmosTransactions.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cosmos/TestCosmosTransactions.kt @@ -67,7 +67,7 @@ class TestCosmosTransactions { assertEquals( output.serialized, - "{\"mode\":\"BROADCAST_MODE_BLOCK\",\"tx_bytes\":\"CvgBCvUBCh4vY29zbW9zLmF1dGh6LnYxYmV0YTEuTXNnR3JhbnQS0gEKLWNvc21vczEzazBxMGw3bGcya3IzMmt2dDdseTIzNnBwbGR5OHY5ZHp3aDNnZBItY29zbW9zMWZzN2x1MjhoeDVtOWFrbTdycDBjMjQyMmNuOHIyZjdndXJ1amhmGnIKaAoqL2Nvc21vcy5zdGFraW5nLnYxYmV0YTEuU3Rha2VBdXRob3JpemF0aW9uEjoSNgo0Y29zbW9zdmFsb3BlcjFnanR2bHk5bGVsNnpza3Z3dHZsZzV2aHdwdTljOXdhdzdzeHp3eCABEgYI4LD6pgYSZwpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohA/fcQw1hCVUx904t+kCXTiiziaLIY8lyssu1ENfzaN1KEgQKAggBGAUSEwoNCgV1YXRvbRIEMjQxOBCp8wUaQIFyfuijGKf87Hz61ZqxasfLI1PZnNge4RDq/tRyB/tZI6p80iGRqHecoV6+84EQkc9GTlNRQOSlApRCsivT9XI=\"}" + "{\"mode\":\"BROADCAST_MODE_BLOCK\",\"tx_bytes\":\"CvgBCvUBCh4vY29zbW9zLmF1dGh6LnYxYmV0YTEuTXNnR3JhbnQS0gEKLWNvc21vczEzazBxMGw3bGcya3IzMmt2dDdseTIzNnBwbGR5OHY5ZHp3aDNnZBItY29zbW9zMWZzN2x1MjhoeDVtOWFrbTdycDBjMjQyMmNuOHIyZjdndXJ1amhmGnIKaAoqL2Nvc21vcy5zdGFraW5nLnYxYmV0YTEuU3Rha2VBdXRob3JpemF0aW9uEjogARI2CjRjb3Ntb3N2YWxvcGVyMWdqdHZseTlsZWw2enNrdnd0dmxnNXZod3B1OWM5d2F3N3N4end4EgYI4LD6pgYSZwpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohA/fcQw1hCVUx904t+kCXTiiziaLIY8lyssu1ENfzaN1KEgQKAggBGAUSEwoNCgV1YXRvbRIEMjQxOBCp8wUaQEAN1nIfDawlHnep2bNEm14w+g7tYybJJT3htcGVS6s9D7va3ed1OUEIk9LZoc3G//VenJ+KLw26SRVBaRukgVI=\"}" ) assertEquals(output.errorMessage, "") } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgAddress.kt index 52e06a041d8..214c391624d 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.cryptoorg diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgSigner.kt index 25197f82a53..74090e73a29 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/cryptoorg/TestCryptoorgSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.cryptoorg diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/dydx/TestDydxAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/dydx/TestDydxAddress.kt new file mode 100644 index 00000000000..408f8fb4ae3 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/dydx/TestDydxAddress.kt @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.dydx + +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestDydxAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + val key = PrivateKey("a498a9ee41af9bab5ef2a8be63d5c970135c3c109e70efc8c56c534e6636b433".toHexByteArray()) + val pubKey = key.getPublicKeySecp256k1(true) + val address = AnyAddress(pubKey, CoinType.DYDX) + val expected = AnyAddress("dydx1mry47pkga5tdswtluy0m8teslpalkdq0hc72uz", CoinType.DYDX) + + assertEquals(address.description(), expected.description()) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumMessageSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumMessageSigner.kt index 6b86f698769..6245751d124 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumMessageSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumMessageSigner.kt @@ -36,6 +36,17 @@ class TestEthereumMessageSigner { assertTrue(EthereumMessageSigner.verifyMessage(publicKey, msg, signature)) } + @Test + fun testEthereumSignAndVerifyMessageLegacyHex() { + val data = Numeric.hexStringToByteArray("9066aa168c379a403becb235c15e7129c133c244e56a757ab07bc369288bcab0") + val privateKey = PrivateKey(data) + val publicKey = privateKey.getPublicKey(CoinType.ETHEREUM) + val msg = "0xc0a96273d5c3fbe4d4000491f08daef9c17f88df846c1d6f57eb5f33c1fbd035" + val signature = EthereumMessageSigner.signMessage(privateKey, msg) + assertEquals(signature, "b18a666ad08bf9bfcd39920b26b5a5d1486b67b45119810b3c7bda22e41e5c4c1bfbe0c932f6c14df4947a18ba310831a37b7307d724a3ac2a4935b99d7075141b"); + assertTrue(EthereumMessageSigner.verifyMessage(publicKey, msg, signature)) + } + @Test fun testEthereumSignAndVerifyMessage712Legacy() { val data = Numeric.hexStringToByteArray("03a9ca895dca1623c7dfd69693f7b4111f5d819d2e145536e0b03c136025a25d") diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleAddress.kt index b09cfaff057..5922fa9f191 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.everscale diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleSigner.kt index cb1d7266f92..a07ba7c46b4 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/everscale/TestEverscaleSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.everscale diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOAddress.kt index 6b6461e78d1..4a9bef5fedb 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.fio diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOSigner.kt index 7037706254b..6992aa0b9f8 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/fio/TestFIOSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.fio diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/greenfield/TestGreenfieldSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/greenfield/TestGreenfieldSigner.kt index 757bdaea895..cd7caf5e2ac 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/greenfield/TestGreenfieldSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/greenfield/TestGreenfieldSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.greenfield diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaAddress.kt index be92015f825..925c36c7851 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.hedera diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaSigner.kt index dec11a37bcb..c01f813e2d0 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/hedera/TestHederaSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.hedera diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerAddress.kt new file mode 100644 index 00000000000..2861aea3a4a --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerAddress.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.internetcomputer + +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestInternetComputerAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + val key = PrivateKey("ee42eaada903e20ef6e5069f0428d552475c1ea7ed940842da6448f6ef9d48e7".toHexByteArray()) + val pubkey = key.getPublicKeySecp256k1(false); + val address = AnyAddress(pubkey, CoinType.INTERNETCOMPUTER) + val expected = AnyAddress("2f25874478d06cf68b9833524a6390d0ba69c566b02f46626979a3d6a4153211", CoinType.INTERNETCOMPUTER) + + assertEquals(pubkey.data().toHex(), "0x048542e6fb4b17d6dfcac3948fe412c00d626728815ee7cc70509603f1bc92128a6e7548f3432d6248bc49ff44a1e50f6389238468d17f7d7024de5be9b181dbc8") + assertEquals(address.description(), expected.description()) + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerSigner.kt new file mode 100644 index 00000000000..d3e97de972e --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/internetcomputer/TestInternetComputerSigner.kt @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.internetcomputer + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHexBytesInByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.* +import wallet.core.jni.proto.InternetComputer +import wallet.core.jni.proto.InternetComputer.SigningOutput + +class TestInternetComputerSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun InternetComputerTransactionSigning() { + val key = PrivateKey("227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be".toHexByteArray()) + + val input = InternetComputer.SigningInput.newBuilder() + .setTransaction(InternetComputer.Transaction.newBuilder().apply { + transfer = InternetComputer.Transaction.Transfer.newBuilder().apply { + toAccountIdentifier = "943d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826a" + amount = 100000000 + memo = 0 + currentTimestampNanos = 1691709940000000000 + }.build() + }.build()) + .setPrivateKey(ByteString.copyFrom(key.data())) + val output = AnySigner.sign(input.build(), CoinType.INTERNETCOMPUTER, SigningOutput.parser()) + assertEquals(output.signedTransaction.toByteArray().toHex(), "0x81826b5452414e53414354494f4e81a266757064617465a367636f6e74656e74a66c726571756573745f747970656463616c6c6e696e67726573735f6578706972791b177a297215cfe8006673656e646572581d971cd2ddeecd1cf1b28be914d7a5c43441f6296f1f9966a7c8aff68d026b63616e69737465725f69644a000000000000000201016b6d6574686f645f6e616d656773656e645f706263617267583b0a0012070a050880c2d72f2a220a20943d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826a3a0a088090caa5a3a78abd176d73656e6465725f7075626b65799858183018561830100607182a1886184818ce183d02010605182b188104000a0318420004183d18ab183a182118a81838184d184c187e1852188a187e18dc18d8184418ea18cd18c5189518ac188518b518bc181d188515186318bc18e618ab18d2184318d3187c184f18cd18f018de189b18b5181918dd18ef1889187218e71518c40418d4189718881843187218c611182e18cc18e6186b182118630218356a73656e6465725f736967984013186f18b9181c189818b318a8186518b2186118d418971618b1187d18eb185818e01826182f1873183b185018cb185d18ef18d81839186418b3183218da1824182f184e18a01880182718c0189018c918a018fd18c418d9189e189818b318ef1874183b185118e118a51864185918e718ed18c71889186c1822182318ca6a726561645f7374617465a367636f6e74656e74a46c726571756573745f747970656a726561645f73746174656e696e67726573735f6578706972791b177a297215cfe8006673656e646572581d971cd2ddeecd1cf1b28be914d7a5c43441f6296f1f9966a7c8aff68d0265706174687381824e726571756573745f7374617475735820e8fbc2d5b0bf837b3a369249143e50d4476faafb2dd620e4e982586a51ebcf1e6d73656e6465725f7075626b65799858183018561830100607182a1886184818ce183d02010605182b188104000a0318420004183d18ab183a182118a81838184d184c187e1852188a187e18dc18d8184418ea18cd18c5189518ac188518b518bc181d188515186318bc18e618ab18d2184318d3187c184f18cd18f018de189b18b5181918dd18ef1889187218e71518c40418d4189718881843187218c611182e18cc18e6186b182118630218356a73656e6465725f7369679840182d182718201888188618ce187f0c182a187a18d718e818df18fb18d318d41118a5186a184b18341842185318d718e618e8187a1828186c186a183618461418f3183318bd18a618a718bc18d918c818b7189d186e1865188418ff18fd18e418e9187f181b18d705184818b21872187818d6181c161833184318a2") + } + + @Test + fun InternetComputerTransactionSigningWithInvalidToAccountIdentifier() { + val key = PrivateKey("227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be".toHexByteArray()) + + val input = InternetComputer.SigningInput.newBuilder() + .setTransaction(InternetComputer.Transaction.newBuilder().apply { + transfer = InternetComputer.Transaction.Transfer.newBuilder().apply { + toAccountIdentifier = "643d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826b" + amount = 100000000 + memo = 0 + currentTimestampNanos = 1691709940000000000 + }.build() + }.build()) + .setPrivateKey(ByteString.copyFrom(key.data())) + val output = AnySigner.sign(input.build(), CoinType.INTERNETCOMPUTER, SigningOutput.parser()) + assertEquals(output.error.number, 16) + } + + @Test + fun InternetComputerTransactionSigningWithInvalidAmount() { + val key = PrivateKey("227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be".toHexByteArray()) + + val input = InternetComputer.SigningInput.newBuilder() + .setTransaction(InternetComputer.Transaction.newBuilder().apply { + transfer = InternetComputer.Transaction.Transfer.newBuilder().apply { + toAccountIdentifier = "943d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826a" + amount = 0 + memo = 0 + currentTimestampNanos = 1691709940000000000 + }.build() + }.build()) + .setPrivateKey(ByteString.copyFrom(key.data())) + val output = AnySigner.sign(input.build(), CoinType.INTERNETCOMPUTER, SigningOutput.parser()) + assertEquals(output.error.number, 23) + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/juno/TestJunoAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/juno/TestJunoAddress.kt index eb9edcfb89d..39c7022b743 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/juno/TestJunoAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/juno/TestJunoAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.juno diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kcc/TestKuCoinCommunityChainAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kcc/TestKuCoinCommunityChainAddress.kt index 0d2fe4a7446..9a2ecfc3811 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kcc/TestKuCoinCommunityChainAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kcc/TestKuCoinCommunityChainAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.kcc diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaAddress.kt index 23f6b258315..39fc302d0ff 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.kusama diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaSigner.kt index 388a38bd150..7bec37b4f0e 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/kusama/TestKusamaSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.polkadot diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXAddress.kt index 6cb38fef175..ab3b1ec2483 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.multiversx diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXSigner.kt index fc6bd12637f..811c8a6b76f 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/multiversx/TestMultiversXSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.multiversx diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveAddress.kt index fc0a81474eb..8c70eaf1dfe 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.nativeinjective diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveSigner.kt index 8892bac9d7f..b1295e2d823 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativeinjective/TestNativeInjectiveSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.nativeinjective diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainAddress.kt new file mode 100644 index 00000000000..93a2fbd2a3a --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainAddress.kt @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.nativezetachain + +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestNativeZetaChainAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + val key = PrivateKey("8d2a3bd62d300a148c89dc8635f87b7a24a951bd1c4e78675fe40e1a640d46ed".toHexByteArray()) + val pubKey = key.getPublicKeySecp256k1(false) + val address = AnyAddress(pubKey, CoinType.NATIVEZETACHAIN) + val expected = AnyAddress("zeta14py36sx57ud82t9yrks9z6hdsrpn5x6kmxs0ne", CoinType.NATIVEZETACHAIN) + + assertEquals(address.description(), expected.description()) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainSigner.kt new file mode 100644 index 00000000000..2dd00e4f2f8 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nativezetachain/TestNativeZetaChainSigner.kt @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.nativezetachain + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.AnyAddress +import wallet.core.jni.CoinType +import wallet.core.jni.PrivateKey +import wallet.core.jni.proto.Cosmos + +class TestNativeZetaChainSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun NativeZetaChainTransactionSigning() { + val key = PrivateKey("8d2a3bd62d300a148c89dc8635f87b7a24a951bd1c4e78675fe40e1a640d46ed".toHexByteArray()) + val publicKey = key.getPublicKeySecp256k1(false) + val from = AnyAddress(publicKey, CoinType.NATIVEZETACHAIN).description() + + val transferAmount = Cosmos.Amount.newBuilder().apply { + // 0.3 ZETA + amount = "300000000000000000" + denom = "azeta" + }.build() + + val message = Cosmos.Message.newBuilder().apply { + sendCoinsMessage = Cosmos.Message.Send.newBuilder().apply { + fromAddress = from + toAddress = "zeta1cscf4ldnkkz7f0wpveur6dpd0d6p2zxnsuu70y" + addAllAmounts(listOf(transferAmount)) + }.build() + }.build() + + val transferFee = Cosmos.Fee.newBuilder().apply { + gas = 200000 + }.build() + + val signingInput = Cosmos.SigningInput.newBuilder().apply { + signingMode = Cosmos.SigningMode.Protobuf + accountNumber = 2726346 + chainId = "athens_7001-1" + sequence = 2 + fee = transferFee + privateKey = ByteString.copyFrom(key.data()) + txHasher = Cosmos.TxHasher.Keccak256 + signerInfo = Cosmos.SignerInfo.newBuilder().apply { + // Zetachain requires a compressed public key to sign a transaction, + // however an uncompressed public key is used to generate address. + publicKeyType = Cosmos.SignerPublicKeyType.Secp256k1 + jsonType = "ethermint/PubKeyEthSecp256k1" + protobufType = "/ethermint.crypto.v1.ethsecp256k1.PubKey" + }.build() + addAllMessages(listOf(message)) + }.build() + + val output = AnySigner.sign(signingInput, CoinType.NATIVEZETACHAIN, Cosmos.SigningOutput.parser()) + + // Successfully broadcasted (testnet): + // https://explorer.zetachain.com/cosmos/tx/A2FC8816657856ED274C4418C3CAEAEE645561275F6C63AB5F8B1DCFB37341A0 + assertEquals(output.serialized, "{\"mode\":\"BROADCAST_MODE_BLOCK\",\"tx_bytes\":\"CpoBCpcBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEncKK3pldGExNHB5MzZzeDU3dWQ4MnQ5eXJrczl6Nmhkc3JwbjV4NmtteHMwbmUSK3pldGExY3NjZjRsZG5ra3o3ZjB3cHZldXI2ZHBkMGQ2cDJ6eG5zdXU3MHkaGwoFYXpldGESEjMwMDAwMDAwMDAwMDAwMDAwMBJhClkKTwooL2V0aGVybWludC5jcnlwdG8udjEuZXRoc2VjcDI1NmsxLlB1YktleRIjCiECho5+FjRBfbKt/Z/jggW/oP6gGJin/TBWXRP3BWo3wGUSBAoCCAEYAhIEEMCaDBpAgGvqca0w2N9wnHnnxS9HiVud4aQ9lNCumzgNIW6wOR4kvPScacGS1G3kwCw7wyI2NJL8M1eVYjafFIt2FpKl3w==\"}") + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/neo/TestsNEOAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/neo/TestsNEOAddress.kt index dd1ef2e86dc..8eb3f6ac165 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/neo/TestsNEOAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/neo/TestsNEOAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.neo diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosAddress.kt index 64f1c492ada..f77ce3858cd 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.nervos diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosSigner.kt index 3d0a149e5ae..d556f883f17 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/nervos/TestNervosSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.nervos diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisAddress.kt index 2cdc8dfcd4a..00b315bc40a 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.oasis diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisSigner.kt index f9962ebb13c..ef3ad829694 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/oasis/TestOasisSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.oasis diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisAddress.kt index 1d881bbdef1..88b33d1b9fd 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.osmosis diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisSigner.kt index e8498fb0ab6..679583594f7 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/osmosis/TestOsmosisSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.osmosis diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotAddress.kt index 82477b4bbbe..7ab39ef55f5 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.Polkadot diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotSigner.kt index 9da312ca3ea..da571e5ca1a 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polkadot/TestPolkadotSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.polkadot diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polygon/TestPolygonAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polygon/TestPolygonAddress.kt index f6b21b1780c..ab807796d5a 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polygon/TestPolygonAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polygon/TestPolygonAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.polygon diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/scroll/TestScrollAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/scroll/TestScrollAddress.kt index da490466d1c..23d77ff4ccb 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/scroll/TestScrollAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/scroll/TestScrollAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.scroll diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretAddress.kt index 26d66ee5883..5315c57c9ad 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.secret diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretSigner.kt index d9286ccd4f2..3f1ef6330a0 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/secret/TestSecretSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.secret diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartbitcoincash/TestSmartBitcoinCashAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartbitcoincash/TestSmartBitcoinCashAddress.kt index 95acfd23453..64c33ba9c58 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartbitcoincash/TestSmartBitcoinCashAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartbitcoincash/TestSmartBitcoinCashAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.smartbitcoincash diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartchain/TestBinanceSmartChainAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartchain/TestBinanceSmartChainAddress.kt index 142cbc0babe..a9a83132c36 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartchain/TestBinanceSmartChainAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/smartchain/TestBinanceSmartChainAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.binancesmartchain diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt new file mode 100644 index 00000000000..c2f8a3d461c --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt @@ -0,0 +1,84 @@ +package com.trustwallet.core.app.blockchains.solana + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.Base58 +import wallet.core.java.AnySigner +import wallet.core.jni.Base64 +import wallet.core.jni.CoinType +import wallet.core.jni.CoinType.SOLANA +import wallet.core.jni.SolanaTransaction +import wallet.core.jni.DataVector +import wallet.core.jni.TransactionDecoder +import wallet.core.jni.proto.Common.SigningError +import wallet.core.jni.proto.Solana +import wallet.core.jni.proto.Solana.DecodingTransactionOutput +import wallet.core.jni.proto.Solana.SigningInput +import wallet.core.jni.proto.Solana.SigningOutput + +class TestSolanaTransaction { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testUpdateBlockhashAndSign() { + val encodedTx = "AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG" + val newBlockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg" + + val myPrivateKey = "7f0932159226ddec9e1a4b0b8fe7cdc135049f9e549a867d722aa720dd64f32e".toHexByteArray() + val feePayerPrivateKey = "4b9d6f57d28b06cbfa1d4cc710953e62d653caf853415c56ffd9d150acdeb7f7".toHexByteArray() + + val privateKeys = DataVector() + privateKeys.add(myPrivateKey) + privateKeys.add(feePayerPrivateKey) + + val outputData = SolanaTransaction.updateBlockhashAndSign(encodedTx, newBlockhash, privateKeys) + val output = SigningOutput.parseFrom(outputData) + + assertEquals(output.error, SigningError.OK) + val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG" + assertEquals(output.encoded, expectedString) + } + + @Test + fun testDecodeUpdateBlockhashAndSign() { + // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet + val encodedTx = Base64.decode("AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG") + val newBlockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg" + + val senderPrivateKeyData = "7f0932159226ddec9e1a4b0b8fe7cdc135049f9e549a867d722aa720dd64f32e".toHexByteArray() + val feePayerPrivateKeyData = "4b9d6f57d28b06cbfa1d4cc710953e62d653caf853415c56ffd9d150acdeb7f7".toHexByteArray() + + // Step 1: Decode the transaction. + + val decodedData = TransactionDecoder.decode(SOLANA, encodedTx) + val decodedOutput = DecodingTransactionOutput.parseFrom(decodedData) + + assertEquals(decodedOutput.error, SigningError.OK) + assert(decodedOutput.transaction.hasLegacy()) + + // Step 2: Update recent blockhash. + + val rawTx = decodedOutput.transaction.toBuilder().apply { + legacy = decodedOutput.transaction.legacy.toBuilder().setRecentBlockhash(newBlockhash).build() + }.build() + + // Step 3: Re-sign the updated transaction. + + val signingInput = SigningInput.newBuilder().apply { + rawMessage = rawTx + privateKey = ByteString.copyFrom(senderPrivateKeyData) + feePayerPrivateKey = ByteString.copyFrom(feePayerPrivateKeyData) + txEncoding = Solana.Encoding.Base64 + }.build() + + val output = AnySigner.sign(signingInput, SOLANA, SigningOutput.parser()) + val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG" + assertEquals(output.encoded, expectedString) + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaWalletConnectSigning.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaWalletConnectSigning.kt new file mode 100644 index 00000000000..51360e7b5af --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaWalletConnectSigning.kt @@ -0,0 +1,52 @@ +package com.trustwallet.core.app.blockchains.solana + +import com.google.protobuf.ByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.Base58 +import wallet.core.jni.CoinType.SOLANA +import wallet.core.jni.WalletConnectRequest +import wallet.core.jni.proto.Common.SigningError +import wallet.core.jni.proto.Solana.Encoding +import wallet.core.jni.proto.Solana.SigningOutput +import wallet.core.jni.proto.WalletConnect + +class TestSolanaWalletConnectSigning { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testSignSolanaTransactionFromWalletConnectRequest() { + // Step 1: Parse a signing request received through WalletConnect. + + val parsingInput = WalletConnect.ParseRequestInput.newBuilder().apply { + method = WalletConnect.Method.SolanaSignTransaction + payload = "{\"transaction\":\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA=\"}" + }.build() + + val parsingOutputBytes = WalletConnectRequest.parse(SOLANA, parsingInput.toByteArray()) + val parsingOutput = WalletConnect.ParseRequestOutput.parseFrom(parsingOutputBytes) + + assertEquals(parsingOutput.error, SigningError.OK) + + // Step 2: Set missing fields. + + val signingInput = parsingOutput.solana.toBuilder().apply { + privateKey = ByteString.copyFrom(Base58.decodeNoCheck("A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr")) + txEncoding = Encoding.Base64 + }.build() + + // Step 3: Sign the transaction. + + val output = AnySigner.sign(signingInput, SOLANA, SigningOutput.parser()) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.encoded, "AQPWaOi7dMdmQpXi8HyQQKwiqIftrg1igGQxGtZeT50ksn4wAnyH4DtDrkkuE0fqgx80LTp4LwNN9a440SrmoA8BAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA=") + + assertEquals(output.getSignatures(0).pubkey, "7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q") + assertEquals(output.getSignatures(0).signature, "5T6uZBHnHFd8uWErDBTFRVkbKuhbcm94K5MJ2beTYDruzqv4FjS7EMKvC94ZfxNAiWUXZ6bZxS3WXUbhJwYNPWn") + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeAddress.kt index bc568dccae3..54577d7971d 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.stargaze diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeSigner.kt index 94a2fbfc7c2..63a7a166b60 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/stargaze/TestStargazeSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.stargaze diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt index b4ec636a5eb..503e6e8c8ad 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.sui diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt index 327e8a2b19b..84ab0914391 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.sui @@ -24,7 +22,7 @@ class TestSuiSigner { } @Test - fun SuiTransactionSigning() { + fun testSuiDirectSigning() { // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet val txBytes = """ AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA @@ -39,4 +37,37 @@ class TestSuiSigner { assertEquals(result.unsignedTx, txBytes); assertEquals(result.signature, expectedSignature) } + + @Test + fun testSuiTransfer() { + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + val txBytes = """ + AAAEAAjoAwAAAAAAAAAIUMMAAAAAAAAAIKcXWr3V7ZLr4605DbNmxqcGR4zfUXzebPmGMAZc2jd6ACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgMCAAIBAAABAQABAQMAAAAAAQIAAQEDAAABAAEDAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayAWNgILOn3HsRw6pvQZsX+KnBLn95ox0b3S3mcLTt1jAFeHEaBQAAAAAgGGuNnxrqusosgjP3gQ3jBjnhapGNBlcU0yTaupXpa0BU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAwMYtAAAAAAAA + """.trimIndent() + val key = + "7e6682f7bf479ef0f627823cffd4e1a940a7af33e5fb39d9e0f631d2ecc5daff".toHexBytesInByteString() + + val paySui = Sui.PaySui.newBuilder() + .addInputCoins(Sui.ObjectRef.newBuilder().apply { + objectId = "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005" + version = 85619064 + objectDigest = "2eKuWbZSVfpFVfg8FXY9wP6W5AFXnTchSoUdp7obyYZ5" + }) + .addRecipients("0xa7175abdd5ed92ebe3ad390db366c6a706478cdf517cde6cf98630065cda377a") + .addRecipients("0x54e80d76d790c277f5a44f3ce92f53d26f5894892bf395dee6375988876be6b2") + .addAmounts(1000) + .addAmounts(50000) + + val signingInput = Sui.SigningInput.newBuilder() + .setPaySui(paySui) + .setPrivateKey(key) + .setGasBudget(3000000) + .setReferenceGasPrice(750) + .build() + + val result = AnySigner.sign(signingInput, CoinType.SUI, Sui.SigningOutput.parser()) + val expectedSignature = "AEh44B7iGArEHF1wOLAQJMLNgGnaIwn3gKPC92vtDJqITDETAM5z9plaxio1xomt6/cZReQ5FZaQsMC6l7E0BwmF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g==" + assertEquals(result.unsignedTx, txBytes); + assertEquals(result.signature, expectedSignature) + } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt index d2f341dc62f..f37d4c7e4e9 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.theopennetwork @@ -24,27 +22,58 @@ class TestTheOpenNetworkAddress { val publicKey = privateKey.getPublicKeyEd25519() val address = AnyAddress(publicKey, CoinType.TON) assertEquals(publicKey.data().toHex(), "0xf42c77f931bea20ec5d0150731276bbb2e2860947661245b2319ef8133ee8d41") - assertEquals(address.description(), "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") + assertEquals(address.description(), "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV") } @Test fun testAddressFromPublicKey() { val publicKey = PublicKey("f42c77f931bea20ec5d0150731276bbb2e2860947661245b2319ef8133ee8d41".toHexByteArray(), PublicKeyType.ED25519) val address = AnyAddress(publicKey, CoinType.TON) - assertEquals(address.description(), "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") + assertEquals(address.description(), "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV") } @Test fun testAddressFromRawString() { val addressString = "0:66fbe3c5c03bf5c82792f904c9f8bf28894a6aa3d213d41c20569b654aadedb3" val address = AnyAddress(addressString, CoinType.TON) - assertEquals(address.description(), "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") + assertEquals(address.description(), "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV") } @Test - fun testAddressFromUserFriendlyString() { + fun testAddressFromBounceableString() { val addressString = "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q" val address = AnyAddress(addressString, CoinType.TON) - assertEquals(address.description(), "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") + assertEquals(address.description(), "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV") + } + + @Test + fun testAddressFromUserFriendlyString() { + val addressString = "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV" + val address = AnyAddress(addressString, CoinType.TON) + assertEquals(address.description(), "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV") + } + + @Test + fun testAddressToBounceable() { + val addressString = "UQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts4DV" + val bounceable = true + val testnet = false + val address = TONAddressConverter.toUserFriendly(addressString, bounceable, testnet) + assertEquals(address, "EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") + } + + @Test + fun testGenerateJettonAddress() { + val mainAddress = "UQBjKqthWBE6GEcqb_epTRFrQ1niS6Z1Z1MHMwR-mnAYRoYr" + val mainAddressBoc = TONAddressConverter.toBoc(mainAddress) + assertEquals(mainAddressBoc, "te6cckEBAQEAJAAAQ4AMZVVsKwInQwjlTf71KaItaGs8SXTOrOpg5mCP004DCNAptHQU") + + // curl --location 'https://toncenter.com/api/v2/runGetMethod' --header 'Content-Type: application/json' --data \ + // '{"address":"EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT","method":"get_wallet_address","method":"get_wallet_address","stack":[["tvm.Slice","te6ccgICAAEAAQAAACQAAABDgAxlVWwrAidDCOVN/vUpoi1oazxJdM6s6mDmYI/TTgMI0A=="]]}' + + // Parse the `get_wallet_address` RPC response. + val jettonAddressBocEncoded = "te6cckEBAQEAJAAAQ4AFvT5rqwxcbKfITqnkwL+go4Zi9bulRHAtLt4cjjFdK7B8L+Cq" + val jettonAddress = TONAddressConverter.fromBoc(jettonAddressBocEncoded) + assertEquals(jettonAddress, "UQAt6fNdWGLjZT5CdU8mBf0FHDMXrd0qI4FpdvDkcYrpXV5H") } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt new file mode 100644 index 00000000000..64c4627cb97 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.theopennetwork + +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.PrivateKey +import wallet.core.jni.TONMessageSigner +import wallet.core.jni.TONWallet + +class TestTheOpenNetworkMessageSigner { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun TheOpenNetworkMessageSignerSignMessage() { + // The private key has been derived by using [ton-mnemonic](https://www.npmjs.com/package/tonweb-mnemonic/v/0.0.2) + // from the following mnemonic: + // document shield addict crime broom point story depend suit satisfy test chicken valid tail speak fortune sound drill seek cube cheap body music recipe + val privateKey = PrivateKey("112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18".toHexByteArray()) + val message = "Hello world" + val signature = TONMessageSigner.signMessage(privateKey, message) + // The following signature has been computed by calling `window.ton.send("ton_personalSign", { data: "Hello world" });`. + assertEquals(signature, "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504") + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt index 887c3516493..59b3fcdd465 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.theopennetwork @@ -27,24 +25,24 @@ class TestTheOpenNetworkSigner { val privateKey = PrivateKey("c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0".toHexByteArray()) val transfer = TheOpenNetwork.Transfer.newBuilder() - .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) .setDest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") .setAmount(10) - .setSequenceNumber(6) .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) - .setExpireAt(1671132440) .setBounceable(true) .build() val input = TheOpenNetwork.SigningInput.newBuilder() - .setTransfer(transfer) .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(6) + .setExpireAt(1671132440) + .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) .build() val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) // tx: https://tonscan.org/tx/3Z4tHpXNLyprecgu5aTQHWtY7dpHXEoo11MAX61Xyg0= - val expectedString = "te6ccgICAAQAAQAAALAAAAFFiAGwt/q8k4SrjbFbQCjJZfQr64ExRxcUMsWqaQODqTUijgwAAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAAIBYmIAM33x4uAd+uQTyXyCZPxflESlNVHpCeoOECtNsqVW9tmIUAAAAAAAAAAAAAAAAAEAAwAA" + val expectedString = "te6cckEBBAEArQABRYgBsLf6vJOEq42xW0AoyWX0K+uBMUcXFDLFqmkDg6k1Io4MAQGcEUPkil2aZ4s8KKparSep/OKHMC8vuXafFbW2HGp/9AcTRv0J5T4dwyW1G0JpHw+g5Ov6QI3Xo0O9RFr3KidICimpoxdjm3UYAAAABgADAgFiYgAzffHi4B365BPJfIJk/F+URKU1UekJ6g4QK02ypVb22YhQAAAAAAAAAAAAAAAAAQMAAA08Nzs=" assertEquals(output.encoded, expectedString) } @@ -53,33 +51,77 @@ class TestTheOpenNetworkSigner { fun TheOpenNetworkJettonTransferSigning() { val privateKey = PrivateKey("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee".toHexByteArray()) - val transferData = TheOpenNetwork.Transfer.newBuilder() - .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) - .setDest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja") - .setAmount(100 * 1000 * 1000) - .setSequenceNumber(1) - .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) - .setExpireAt(1787693046) - .setComment("test comment") - .setBounceable(true) - val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder() - .setTransfer(transferData) .setJettonAmount(500 * 1000 * 1000) .setToOwner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8") .setResponseAddress("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk") .setForwardAmount(1) .build() - val input = TheOpenNetwork.SigningInput.newBuilder() + val transfer = TheOpenNetwork.Transfer.newBuilder() + .setDest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja") + .setAmount(100 * 1000 * 1000) + .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) + .setComment("test comment") + .setBounceable(true) .setJettonTransfer(jettonTransfer) + + val input = TheOpenNetwork.SigningInput.newBuilder() .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(1) + .setExpireAt(1787693046) + .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) .build() val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) // tx: https://testnet.tonscan.org/tx/Er_oT5R3QK7D-qVPBKUGkJAOOq6ayVls-mgEphpI9Ck= - val expectedString = "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ=" + val expectedString = "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=" + + assertEquals(output.encoded, expectedString) + } + + @Test + fun TheOpenNetworkTransferCustomPayload() { + val privateKey = PrivateKey("5525e673087587bc0efd7ab09920ef7d3c1bf6b854a661430244ca59ab19e9d1".toHexByteArray()) + + // Doge chatbot contract payload to be deployed. + // Docs: https://docs.ton.org/develop/dapps/ton-connect/transactions#smart-contract-deployment + val dogeChatbotStateInit = "te6cckEBBAEAUwACATQBAgEU/wD0pBP0vPLICwMAEAAAAZDrkbgQAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AO4ioYU=" + // Doge chatbot's address after the contract is deployed. + val dogeChatbotDeployingAddress = "0:3042cd5480da232d5ac1d9cbe324e3c9eb58f167599f6b7c20c6e638aeed0335" + + // The comment has nothing to do with Doge chatbot. + // It's just used to attach the following ASCII comment to the transaction: + // "This transaction deploys Doge Chatbot contract" + val commentPayload = "te6cckEBAQEANAAAZAAAAABUaGlzIHRyYW5zYWN0aW9uIGRlcGxveXMgRG9nZSBDaGF0Ym90IGNvbnRyYWN0v84vSg==" + + val customPayload = TheOpenNetwork.CustomPayload.newBuilder() + .setStateInit(dogeChatbotStateInit) + .setPayload(commentPayload) + .build() + + val transfer = TheOpenNetwork.Transfer.newBuilder() + .setDest(dogeChatbotDeployingAddress) + // 0.069 TON + .setAmount(69_000_000) + .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) + .setBounceable(false) + .setCustomPayload(customPayload) + + val input = TheOpenNetwork.SigningInput.newBuilder() + .setPrivateKey(ByteString.copyFrom(privateKey.data())) + .addMessages(transfer) + .setSequenceNumber(4) + .setExpireAt(1721939714) + .setWalletVersion(TheOpenNetwork.WalletVersion.WALLET_V4_R2) + .build() + + val output = AnySigner.sign(input, CoinType.TON, SigningOutput.parser()) + + // Successfully broadcasted: https://tonviewer.com/transaction/f4b7ed2247b1adf54f33dd2fd99216fbd61beefb281542d0b330ccea9b8d0338 + val expectedString = "te6cckECCAEAATcAAUWIAfq4NsPLegfou/MPhtHE9YuzV3gnI/q6jm3MRJh2PtpaDAEBnPbyCSsWrOZpEjb7ZFxz5yYi+an6M6Lnq7rI7TFWdDS76LEtGBrVVrhMGziwxuy6LCVtsMBikI7RPVQ89FCIAAYpqaMXZqK3AgAAAAQAAwICaUIAGCFmqkBtEZatYOzl8ZJx5PWseLOsz7W+EGNzHFd2gZqgIObaAAAAAAAAAAAAAAAAAAPAAwQCATQFBgBkAAAAAFRoaXMgdHJhbnNhY3Rpb24gZGVwbG95cyBEb2dlIENoYXRib3QgY29udHJhY3QBFP8A9KQT9LzyyAsHABAAAAGQ65G4EABq0zABgghpSSC5kTDg0NMDMfpAMItGRvZ2WHAggBjIywVQBM8WgEX6AhPLahLLHwHPFslz+wAa2r/S" assertEquals(output.encoded, expectedString) } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt new file mode 100644 index 00000000000..9305072bb75 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.theopennetwork + +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.PublicKey +import wallet.core.jni.PublicKeyType +import wallet.core.jni.TONWallet + +class TestTheOpenNetworkWallet { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun TheOpenNetworkWalletBuildV4R2StateInit() { + val publicKey = PublicKey("f229a9371fa7c2108b3d90ea22c9be705ff5d0cfeaee9cbb9366ff0171579357".toHexByteArray(), PublicKeyType.ED25519) + val baseWorkchain = 0 + val defaultWalletId = 0x29a9a317 + val stateInit = TONWallet.buildV4R2StateInit(publicKey, baseWorkchain, defaultWalletId) + + val expected = "te6cckECFgEAAwQAAgE0AQIBFP8A9KQT9LzyyAsDAFEAAAAAKamjF/IpqTcfp8IQiz2Q6iLJvnBf9dDP6u6cu5Nm/wFxV5NXQAIBIAQFAgFIBgcE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8ICQoLAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNDA0CASAODwBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgBwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AAAK9ADJ7VQAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABgCKUASBAQj0WTDtRNCBAUDXIMgBzxb0AMntVAFysI4jghBkc3Rygx6xcIAYUAXLBVADzxYj+gITy2rLH8s/yYBA+wCSXwPiAgEgEBEAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAIBWBITABG4yX7UTQ1wsfgAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGACASAUFQAZrc52omhAIGuQ64X/wAAZrx32omhAEGuQ64WPwEXtMkg=" + assertEquals(stateInit, expected) + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thetafuel/TestThetaFuelAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thetafuel/TestThetaFuelAddress.kt index 876fdefebb4..17e60b89aeb 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thetafuel/TestThetaFuelAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thetafuel/TestThetaFuelAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.thetafuel diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainAddress.kt index 8a1c5768d90..0974995d3f1 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.thorchain diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainSigner.kt index c12ad29b2f1..fde6325529f 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/thorchain/TestTHORChainSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2021 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.thorchain diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenAddress.kt index bd7fcdfed69..8398b636928 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.zen diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenSigner.kt index 0b42106409a..36682ac0293 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/zen/TestZenSigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.zen diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt index 64b3bb82987..ae4b47b4bd0 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.utils diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAsnParser.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAsnParser.kt index ecdf9db3d9c..7c896d7860c 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAsnParser.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAsnParser.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.utils diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt new file mode 100644 index 00000000000..4b5bc226ce9 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt @@ -0,0 +1,47 @@ +package com.trustwallet.core.app.utils + +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHex +import org.junit.Assert.* +import org.junit.Test +import wallet.core.jni.* + +class TestCryptoBox { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testEncryptDecryptEasy() { + val mySecret = CryptoBoxSecretKey() + val myPubkey = mySecret.publicKey + + val otherSecret = CryptoBoxSecretKey() + val otherPubkey = otherSecret.publicKey + + val message = "Well done is better than well said. -Benjamin Franklin" + val encrypted = CryptoBox.encryptEasy(mySecret, otherPubkey, message.toByteArray()) + + // Step 2. Make sure the Box can be decrypted by the other side. + val decrypted = CryptoBox.decryptEasy(otherSecret, myPubkey, encrypted) + assertEquals(decrypted.toString(Charsets.UTF_8), message) + } + + @Test + fun testSecretKeyFromToBytes() { + val secretBytesHex = "0xdd87000d4805d6fbd89ae1352f5e4445648b79d5e901c92aebcb610e9be468e4" + val secretBytes = secretBytesHex.toHexByteArray() + assert(CryptoBoxSecretKey.isValid(secretBytes)) + val secret = CryptoBoxSecretKey(secretBytes) + assertEquals(secret.data().toHex(), secretBytesHex) + } + + @Test + fun testPublicKeyFromToBytes() { + val publicBytesHex = "0xafccabc5b28a8a1fd1cd880516f9c854ae2498d0d1b978b53a59f38e4ae55747" + val publicBytes = publicBytesHex.toHexByteArray() + assert(CryptoBoxPublicKey.isValid(publicBytes)) + val pubkey = CryptoBoxPublicKey(publicBytes) + assertEquals(pubkey.data().toHex(), publicBytesHex) + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt index 22af2bfc37f..e3ec6f29b12 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2022 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.utils diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestLiquidStaking.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestLiquidStaking.kt index a4bcbf35957..44701d5f2b1 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestLiquidStaking.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestLiquidStaking.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.utils @@ -31,7 +29,7 @@ class TestLiquidStaking { stake = LiquidStaking.Stake.newBuilder().apply { amount = "1000000000000000000" asset = LiquidStaking.Asset.newBuilder().apply { - stakingToken = LiquidStaking.Coin.MATIC + stakingToken = LiquidStaking.Coin.POL }.build() }.build() } diff --git a/audit/2023-09-15_TrustWallet_SecureCodeReviewReport_Public_v2.00.pdf b/audit/2023-09-15_TrustWallet_SecureCodeReviewReport_Public_v2.00.pdf new file mode 100644 index 00000000000..d0efac0de8d Binary files /dev/null and b/audit/2023-09-15_TrustWallet_SecureCodeReviewReport_Public_v2.00.pdf differ diff --git a/cmake/Protobuf.cmake b/cmake/Protobuf.cmake index 131bd7678b8..8e0a93251d2 100644 --- a/cmake/Protobuf.cmake +++ b/cmake/Protobuf.cmake @@ -1,8 +1,6 @@ -# Copyright © 2017-2022 Trust Wallet. +# SPDX-License-Identifier: Apache-2.0 # -# This file is part of Trust. The full Trust copyright notice, including -# terms governing use, modification, and redistribution, is contained in the -# file LICENSE at the root of the source code distribution tree. +# Copyright © 2017 Trust Wallet. set(protobuf_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../build/local/src/protobuf/protobuf-3.19.2) set(protobuf_source_dir ${CMAKE_CURRENT_LIST_DIR}/../build/local/src/protobuf/protobuf-3.19.2) diff --git a/codegen-v2/Cargo.lock b/codegen-v2/Cargo.lock index 32a0596b5ae..6ed11b1c48a 100644 --- a/codegen-v2/Cargo.lock +++ b/codegen-v2/Cargo.lock @@ -3,10 +3,13 @@ version = 3 [[package]] -name = "autocfg" -version = "1.1.0" +name = "aho-corasick" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] [[package]] name = "block-buffer" @@ -27,18 +30,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "codegen-v2" version = "0.1.0" dependencies = [ + "aho-corasick", + "convert_case", "handlebars", "heck", + "pathdiff", "serde", "serde_json", "serde_yaml", + "toml_edit", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", ] [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -55,14 +71,20 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "generic-array" version = "0.14.7" @@ -75,9 +97,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.3.6" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" dependencies = [ "log", "pest", @@ -89,9 +111,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -101,56 +123,66 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "indexmap" -version = "1.9.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" -version = "0.4.17" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pathdiff" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pest" -version = "2.5.7" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.5.7" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" dependencies = [ "pest", "pest_generator", @@ -158,9 +190,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.7" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" dependencies = [ "pest", "pest_meta", @@ -171,9 +203,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.5.7" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" dependencies = [ "once_cell", "pest", @@ -182,42 +214,42 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -226,9 +258,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -237,9 +269,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.21" +version = "0.9.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" +checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" dependencies = [ "indexmap", "itoa", @@ -250,9 +282,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -261,9 +293,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.14" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -272,50 +304,82 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unsafe-libyaml" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/codegen-v2/Cargo.toml b/codegen-v2/Cargo.toml index a8906c0ae94..43c2afea1c6 100644 --- a/codegen-v2/Cargo.toml +++ b/codegen-v2/Cargo.toml @@ -3,8 +3,6 @@ name = "codegen-v2" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] name = "libparser" path = "src/lib.rs" @@ -14,8 +12,12 @@ name = "parser" path = "src/main.rs" [dependencies] -serde = { version = "1.0.159", features = ["derive"] } -serde_json = "1.0.95" +aho-corasick = "1.1.2" +convert_case = "0.6.0" +pathdiff = "0.2.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" serde_yaml = "0.9.21" +toml_edit = "0.21.0" handlebars = "4.3.6" heck = "0.4.1" diff --git a/codegen-v2/README.md b/codegen-v2/README.md index 11490caee31..33e018ac407 100644 --- a/codegen-v2/README.md +++ b/codegen-v2/README.md @@ -2,7 +2,7 @@ This is a _work-in-progress_ parser meant to deprecate the existing Ruby parser in `codegen/`. As of now, we only support Swift binding generation. This project -will progess over multiple stages (PRs). +will progress over multiple stages (PRs). ## Execution diff --git a/codegen-v2/manifest/TWCoinType.yaml b/codegen-v2/manifest/TWCoinType.yaml index 872c3050766..af01f64569b 100644 --- a/codegen-v2/manifest/TWCoinType.yaml +++ b/codegen-v2/manifest/TWCoinType.yaml @@ -192,7 +192,7 @@ enums: value: 10001284 - name: kavaEvm value: 10002222 - - name: klaytn + - name: kaia value: 10008217 - name: meter value: 18000 diff --git a/codegen-v2/manifest/TWEthereumChainID.yaml b/codegen-v2/manifest/TWEthereumChainID.yaml index ec004a4bdf1..a8c93f297cc 100644 --- a/codegen-v2/manifest/TWEthereumChainID.yaml +++ b/codegen-v2/manifest/TWEthereumChainID.yaml @@ -63,7 +63,7 @@ enums: value: 2222 - name: iotexevm value: 4689 - - name: klaytn + - name: kaia value: 8217 - name: avalanchec value: 43114 diff --git a/codegen-v2/src/codegen/cpp/blockchain_dispatcher_generator.rs b/codegen-v2/src/codegen/cpp/blockchain_dispatcher_generator.rs new file mode 100644 index 00000000000..0871d545995 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/blockchain_dispatcher_generator.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::cpp_source_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const COIN_INCLUDES_END: &str = "end_of_coin_includes_marker_do_not_modify"; +const COIN_DISPATCHER_DECLARATIONS_END: &str = + "end_of_coin_dipatcher_declarations_marker_do_not_modify"; +const COIN_DISPATCHER_SWITCH_END: &str = "end_of_coin_dipatcher_switch_marker_do_not_modify"; + +fn dispatcher_coin_cpp_path() -> PathBuf { + cpp_source_directory().join("Coin.cpp") +} + +/// Represents `Coin.cpp`. +pub struct BlockchainDispatcherGenerator; + +impl BlockchainDispatcherGenerator { + pub fn generate_new_blockchain_type_dispatching(coin: &CoinItem) -> Result<()> { + let dispatcher_path = dispatcher_coin_cpp_path(); + println!("[EDIT] {dispatcher_path:?}"); + let mut file_content = FileContent::read(dispatcher_path)?; + + Self::generate_include_of_blockchain_entry(coin, &mut file_content)?; + Self::generate_blockchain_entry_constant(coin, &mut file_content)?; + Self::generate_blockchain_dispatcher_case(coin, &mut file_content)?; + + file_content.write() + } + + fn generate_include_of_blockchain_entry( + coin: &CoinItem, + file_content: &mut FileContent, + ) -> Result<()> { + let blockchain_type = coin.blockchain_type(); + + let mut line_marker = file_content.rfind_line(|line| line.contains(COIN_INCLUDES_END))?; + line_marker.push_line_before(format!(r#"#include "{blockchain_type}/Entry.h""#)); + + Ok(()) + } + + fn generate_blockchain_entry_constant( + coin: &CoinItem, + file_content: &mut FileContent, + ) -> Result<()> { + let blockchain_type = coin.blockchain_type(); + + let mut entries_region = + file_content.rfind_line(|line| line.contains(COIN_DISPATCHER_DECLARATIONS_END))?; + entries_region.push_line_before(format!("{blockchain_type}::Entry {blockchain_type}DP;")); + + Ok(()) + } + + fn generate_blockchain_dispatcher_case( + coin: &CoinItem, + file_content: &mut FileContent, + ) -> Result<()> { + let blockchain_type = coin.blockchain_type(); + + let mut entries_region = + file_content.rfind_line(|line| line.contains(COIN_DISPATCHER_SWITCH_END))?; + entries_region.push_line_before(format!( + " case TWBlockchain{blockchain_type}: entry = &{blockchain_type}DP; break;" + )); + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/cpp/entry_generator.rs b/codegen-v2/src/codegen/cpp/entry_generator.rs new file mode 100644 index 00000000000..639ee6c0488 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/entry_generator.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::cpp_source_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::registry::CoinItem; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const ENTRY_HEADER_TEMPLATE: &str = include_str!("templates/Entry.h"); + +pub fn coin_source_directory(coin: &CoinItem) -> PathBuf { + cpp_source_directory().join(coin.blockchain_type()) +} + +pub struct EntryGenerator; + +impl EntryGenerator { + pub fn generate(coin: &CoinItem) -> Result { + let blockchain_dir = coin_source_directory(coin); + let entry_header_path = blockchain_dir.join("Entry.h"); + + if blockchain_dir.exists() { + println!("[SKIP] Entry file already exists: {blockchain_dir:?}"); + return Ok(blockchain_dir); + } + + fs::create_dir_all(&blockchain_dir)?; + + println!("[ADD] {entry_header_path:?}"); + TemplateGenerator::new(ENTRY_HEADER_TEMPLATE) + .write_to(entry_header_path.clone()) + .with_default_patterns(coin) + .write()?; + + Ok(entry_header_path) + } +} diff --git a/codegen-v2/src/codegen/cpp/mod.rs b/codegen-v2/src/codegen/cpp/mod.rs new file mode 100644 index 00000000000..1ef7188d86f --- /dev/null +++ b/codegen-v2/src/codegen/cpp/mod.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::registry::CoinItem; +use std::env; +use std::path::PathBuf; + +pub mod blockchain_dispatcher_generator; +pub mod entry_generator; +pub mod new_blockchain; +pub mod new_cosmos_chain; +pub mod new_evmchain; +pub mod tw_any_address_tests_generator; +pub mod tw_any_signer_tests_generator; +pub mod tw_blockchain; +pub mod tw_coin_address_derivation_tests_generator; +pub mod tw_coin_type_generator; +pub mod tw_coin_type_tests_generator; + +pub fn cpp_source_directory() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("src") +} + +pub fn cpp_include_directory() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("include") + .join("TrustWalletCore") +} + +pub fn integration_tests_directory() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("tests") +} + +pub fn coin_integration_tests_directory(coin: &CoinItem) -> PathBuf { + integration_tests_directory() + .join("chains") + .join(coin.coin_type()) +} + +pub fn cosmos_coin_integration_tests_directory(coin: &CoinItem) -> PathBuf { + integration_tests_directory() + .join("chains") + .join("Cosmos") + .join(coin.coin_type()) +} diff --git a/codegen-v2/src/codegen/cpp/new_blockchain.rs b/codegen-v2/src/codegen/cpp/new_blockchain.rs new file mode 100644 index 00000000000..df7c94c62bd --- /dev/null +++ b/codegen-v2/src/codegen/cpp/new_blockchain.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::blockchain_dispatcher_generator::BlockchainDispatcherGenerator; +use crate::codegen::cpp::entry_generator::EntryGenerator; +use crate::codegen::cpp::tw_any_address_tests_generator::TWAnyAddressTestsGenerator; +use crate::codegen::cpp::tw_any_signer_tests_generator::TWAnySignerTestsGenerator; +use crate::codegen::cpp::tw_blockchain::TWBlockchainGenerator; +use crate::codegen::cpp::tw_coin_address_derivation_tests_generator::CoinAddressDerivationTestsGenerator; +use crate::codegen::cpp::tw_coin_type_generator::TWCoinTypeGenerator; +use crate::codegen::cpp::tw_coin_type_tests_generator::TWCoinTypeTestsGenerator; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_blockchain(coin: &CoinItem) -> Result<()> { + // Generate C++ files. + EntryGenerator::generate(coin)?; + + // Add the new coin type to the `TWCoinType` enum. + TWCoinTypeGenerator::generate_coin_type_variant(coin)?; + // Add the new blockchain type to the `TWBlockchain` enum. + TWBlockchainGenerator::generate_blockchain_type_variant(coin)?; + // Add the blockchain entry to the dispatcher `Coin.cpp`. + BlockchainDispatcherGenerator::generate_new_blockchain_type_dispatching(coin)?; + + // Add integration tests. + TWCoinTypeTestsGenerator::generate(coin)?; + TWAnyAddressTestsGenerator::generate(coin)?; + TWAnySignerTestsGenerator::generate(coin)?; + CoinAddressDerivationTestsGenerator::generate_new_coin_type_case(coin)?; + + Ok(()) +} diff --git a/codegen-v2/src/codegen/cpp/new_cosmos_chain.rs b/codegen-v2/src/codegen/cpp/new_cosmos_chain.rs new file mode 100644 index 00000000000..08c4406f994 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/new_cosmos_chain.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::tw_coin_address_derivation_tests_generator::CoinAddressDerivationTestsGenerator; +use crate::codegen::cpp::tw_coin_type_generator::TWCoinTypeGenerator; +use crate::codegen::cpp::tw_coin_type_tests_generator::TWCoinTypeTestsGenerator; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_cosmos_chain(coin: &CoinItem) -> Result<()> { + // Add the new coin type to the `TWCoinType` enum. + TWCoinTypeGenerator::generate_coin_type_variant(coin)?; + + // Add integration tests. + TWCoinTypeTestsGenerator::generate(coin)?; + CoinAddressDerivationTestsGenerator::generate_new_coin_type_case(coin)?; + + Ok(()) +} diff --git a/codegen-v2/src/codegen/cpp/new_evmchain.rs b/codegen-v2/src/codegen/cpp/new_evmchain.rs new file mode 100644 index 00000000000..2368c797ab3 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/new_evmchain.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::tw_coin_address_derivation_tests_generator::CoinAddressDerivationTestsGenerator; +use crate::codegen::cpp::tw_coin_type_generator::TWCoinTypeGenerator; +use crate::codegen::cpp::tw_coin_type_tests_generator::TWCoinTypeTestsGenerator; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_evmchain(coin: &CoinItem) -> Result<()> { + // Add the new coin type to the `TWCoinType` enum. + TWCoinTypeGenerator::generate_coin_type_variant(coin)?; + + // Add integration tests. + TWCoinTypeTestsGenerator::generate(coin)?; + CoinAddressDerivationTestsGenerator::generate_new_evm_coin_type_case(coin)?; + + Ok(()) +} diff --git a/codegen-v2/src/codegen/cpp/templates/Entry.h b/codegen-v2/src/codegen/cpp/templates/Entry.h new file mode 100644 index 00000000000..2c2520f9ccb --- /dev/null +++ b/codegen-v2/src/codegen/cpp/templates/Entry.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/RustCoinEntry.h" + +namespace TW::{BLOCKCHAIN} { + +/// Entry point for {BLOCKCHAIN} coin. +/// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file +class Entry : public Rust::RustCoinEntry { +}; + +} // namespace TW::{BLOCKCHAIN} + diff --git a/codegen-v2/src/codegen/cpp/templates/TWAnyAddressTests.cpp b/codegen-v2/src/codegen/cpp/templates/TWAnyAddressTests.cpp new file mode 100644 index 00000000000..139234a20e4 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/templates/TWAnyAddressTests.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include +#include "HexCoding.h" + +#include "TestUtilities.h" +#include + +using namespace TW; + +// TODO: Finalize tests + +TEST(TW{COIN_TYPE}, Address) { + // TODO: Finalize test implementation + + auto string = STRING("__ADD_VALID_ADDRESS_HERE__"); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinType{COIN_TYPE})); + auto string2 = WRAPS(TWAnyAddressDescription(addr.get())); + EXPECT_TRUE(TWStringEqual(string.get(), string2.get())); + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "__CORRESPONDING_ADDRESS_DATA__"); +} diff --git a/codegen-v2/src/codegen/cpp/templates/TWAnySignerTests.cpp b/codegen-v2/src/codegen/cpp/templates/TWAnySignerTests.cpp new file mode 100644 index 00000000000..ce5b56f0467 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/templates/TWAnySignerTests.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include +#include "HexCoding.h" + +#include "TestUtilities.h" +#include + +using namespace TW; + +// TODO: Finalize tests + +TEST(TWAnySigner{COIN_TYPE}, Sign) { + // TODO: Finalize test implementation +} diff --git a/codegen-v2/src/codegen/cpp/templates/TWCoinTypeTests.cpp b/codegen-v2/src/codegen/cpp/templates/TWCoinTypeTests.cpp new file mode 100644 index 00000000000..8e917150dd4 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/templates/TWCoinTypeTests.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include +#include + +TEST(TW{COIN_TYPE}CoinType, TWCoinType) { + const auto coin = TWCoinType{COIN_TYPE}; + const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin)); + const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin)); + const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin)); + const auto txId = WRAPS(TWStringCreateWithUTF8Bytes("{EXPLORER_SAMPLE_TX}")); + const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get())); + const auto accId = WRAPS(TWStringCreateWithUTF8Bytes("{EXPLORER_SAMPLE_ACCOUNT}")); + const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get())); + + assertStringsEqual(id, "{COIN_ID}"); + assertStringsEqual(name, "{COIN_NAME}"); + assertStringsEqual(symbol, "{SYMBOL}"); + ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), {DECIMALS}); + ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchain{BLOCKCHAIN}); + ASSERT_EQ(TWCoinTypeP2pkhPrefix(coin), {P2PKH_PREFIX}); + ASSERT_EQ(TWCoinTypeP2shPrefix(coin), {P2SH_PREFIX}); + ASSERT_EQ(TWCoinTypeStaticPrefix(coin), {STATIC_PREFIX}); + assertStringsEqual(txUrl, "{EXPLORER_URL}{EXPLORER_TX_PATH}{EXPLORER_SAMPLE_TX}"); + assertStringsEqual(accUrl, "{EXPLORER_URL}{EXPLORER_ACCOUNT_PATH}{EXPLORER_SAMPLE_ACCOUNT}"); +} diff --git a/codegen-v2/src/codegen/cpp/tw_any_address_tests_generator.rs b/codegen-v2/src/codegen/cpp/tw_any_address_tests_generator.rs new file mode 100644 index 00000000000..599ea265235 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_any_address_tests_generator.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::coin_integration_tests_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::registry::CoinItem; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const TW_ANY_ADDRESS_TESTS_TEMPLATE: &str = include_str!("templates/TWAnyAddressTests.cpp"); + +pub fn tw_any_address_tests_path(coin: &CoinItem) -> PathBuf { + coin_integration_tests_directory(coin).join("TWAnyAddressTests.cpp") +} + +pub struct TWAnyAddressTestsGenerator; + +impl TWAnyAddressTestsGenerator { + pub fn generate(coin: &CoinItem) -> Result<()> { + let coin_tests_dir = coin_integration_tests_directory(coin); + let tw_any_address_tests_path = coin_tests_dir.join("TWAnyAddressTests.cpp"); + + fs::create_dir_all(coin_tests_dir)?; + if tw_any_address_tests_path.exists() { + println!("[SKIP] {tw_any_address_tests_path:?} already exists"); + return Ok(()); + } + + println!("[ADD] {tw_any_address_tests_path:?}"); + TemplateGenerator::new(TW_ANY_ADDRESS_TESTS_TEMPLATE) + .write_to(tw_any_address_tests_path) + .with_default_patterns(coin) + .write()?; + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/cpp/tw_any_signer_tests_generator.rs b/codegen-v2/src/codegen/cpp/tw_any_signer_tests_generator.rs new file mode 100644 index 00000000000..9ea4337616a --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_any_signer_tests_generator.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::coin_integration_tests_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::registry::CoinItem; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const TW_ANY_SIGNER_TESTS_TEMPLATE: &str = include_str!("templates/TWAnySignerTests.cpp"); + +pub fn tw_any_signer_tests_path(coin: &CoinItem) -> PathBuf { + coin_integration_tests_directory(coin).join("TWAnySignerTests.cpp") +} + +pub struct TWAnySignerTestsGenerator; + +impl TWAnySignerTestsGenerator { + pub fn generate(coin: &CoinItem) -> Result<()> { + let coin_tests_dir = coin_integration_tests_directory(coin); + let tw_any_signer_tests_path = coin_tests_dir.join("TWAnySignerTests.cpp"); + + fs::create_dir_all(coin_tests_dir)?; + if tw_any_signer_tests_path.exists() { + println!("[SKIP] {tw_any_signer_tests_path:?} already exists"); + return Ok(()); + } + + println!("[ADD] {tw_any_signer_tests_path:?}"); + TemplateGenerator::new(TW_ANY_SIGNER_TESTS_TEMPLATE) + .write_to(tw_any_signer_tests_path) + .with_default_patterns(coin) + .write()?; + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/cpp/tw_blockchain.rs b/codegen-v2/src/codegen/cpp/tw_blockchain.rs new file mode 100644 index 00000000000..a76a73ab055 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_blockchain.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::cpp_include_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +/// An offset because of removed blockchain enum types. +const BLOCKCHAIN_TYPE_OFFSET: usize = 2; + +pub fn tw_blockchain_path() -> PathBuf { + cpp_include_directory().join("TWBlockchain.h") +} + +/// Represents `TWBlockchain.h`. +pub struct TWBlockchainGenerator; + +impl TWBlockchainGenerator { + pub fn generate_blockchain_type_variant(coin: &CoinItem) -> Result<()> { + let coin_type = coin.blockchain_type(); + let tw_blockchain_type_path = tw_blockchain_path(); + + println!("[EDIT] {tw_blockchain_type_path:?}"); + let mut tw_blockchain_type_rs = FileContent::read(tw_blockchain_type_path)?; + + { + let mut enum_region = + tw_blockchain_type_rs.find_region_with_prefix(" TWBlockchain")?; + // Add an offset because of removed blockchain enum types. + let new_blockchain_id = enum_region.count_lines() + BLOCKCHAIN_TYPE_OFFSET; + enum_region.push_line(format!( + " TWBlockchain{coin_type} = {new_blockchain_id}," + )); + } + + tw_blockchain_type_rs.write() + } +} diff --git a/codegen-v2/src/codegen/cpp/tw_coin_address_derivation_tests_generator.rs b/codegen-v2/src/codegen/cpp/tw_coin_address_derivation_tests_generator.rs new file mode 100644 index 00000000000..91321dffb05 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_coin_address_derivation_tests_generator.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::integration_tests_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const COIN_ADDRESS_DERIVATION_TESTS_END: &str = + "end_of_coin_address_derivation_tests_marker_do_not_modify"; +const EVM_ADDRESS_DERIVATION_TESTS_END: &str = + "end_of_evm_address_derivation_tests_marker_do_not_modify"; + +pub fn coin_address_derivation_tests_path() -> PathBuf { + integration_tests_directory() + .join("common") + .join("CoinAddressDerivationTests.cpp") +} + +/// Represents `CoinAddressDerivationTests.cpp`. +pub struct CoinAddressDerivationTestsGenerator; + +impl CoinAddressDerivationTestsGenerator { + pub fn generate_new_coin_type_case(coin: &CoinItem) -> Result<()> { + let coin_type = coin.coin_type(); + let test_path = coin_address_derivation_tests_path(); + println!("[EDIT] {test_path:?}"); + + let mut coin_address_derivation_test_rs = FileContent::read(test_path)?; + + { + let mut switch_case_region = coin_address_derivation_test_rs + .rfind_line(|line| line.contains(COIN_ADDRESS_DERIVATION_TESTS_END))?; + + #[rustfmt::skip] + let test_case = format!( +r#" case TWCoinType{coin_type}: + EXPECT_EQ(address, "__TODO__"); + break;"# +); + + switch_case_region.push_paragraph_before(test_case); + } + + coin_address_derivation_test_rs.write() + } + + pub fn generate_new_evm_coin_type_case(coin: &CoinItem) -> Result<()> { + let coin_type = coin.coin_type(); + let test_path = coin_address_derivation_tests_path(); + println!("[EDIT] {test_path:?}"); + + let mut evm_address_derivation_test_rs = FileContent::read(test_path)?; + + { + let mut switch_case_region = evm_address_derivation_test_rs + .rfind_line(|line| line.contains(EVM_ADDRESS_DERIVATION_TESTS_END))?; + switch_case_region.push_line_before(format!(" case TWCoinType{coin_type}:")); + } + + evm_address_derivation_test_rs.write() + } +} diff --git a/codegen-v2/src/codegen/cpp/tw_coin_type_generator.rs b/codegen-v2/src/codegen/cpp/tw_coin_type_generator.rs new file mode 100644 index 00000000000..0271838af03 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_coin_type_generator.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::cpp_include_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const TW_COIN_TYPE_END: &str = "end_of_tw_coin_type_marker_do_not_modify"; + +pub fn tw_coin_type_path() -> PathBuf { + cpp_include_directory().join("TWCoinType.h") +} + +/// Represents `TWCoinType.h`. +pub struct TWCoinTypeGenerator; + +impl TWCoinTypeGenerator { + pub fn generate_coin_type_variant(coin: &CoinItem) -> Result<()> { + let coin_type = coin.coin_type(); + let coin_id_number = coin.coin_id_number; + let tw_coin_type_file_path = tw_coin_type_path(); + + println!("[EDIT] {tw_coin_type_file_path:?}"); + let mut tw_coin_type_rs = FileContent::read(tw_coin_type_file_path)?; + + { + let mut enum_region = + tw_coin_type_rs.rfind_line(|line| line.contains(TW_COIN_TYPE_END))?; + enum_region.push_line_before(format!(" TWCoinType{coin_type} = {coin_id_number},")); + } + + tw_coin_type_rs.write() + } +} diff --git a/codegen-v2/src/codegen/cpp/tw_coin_type_tests_generator.rs b/codegen-v2/src/codegen/cpp/tw_coin_type_tests_generator.rs new file mode 100644 index 00000000000..a3f9f50c5da --- /dev/null +++ b/codegen-v2/src/codegen/cpp/tw_coin_type_tests_generator.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::cpp::{ + coin_integration_tests_directory, cosmos_coin_integration_tests_directory, +}; +use crate::codegen::template_generator::TemplateGenerator; +use crate::registry::CoinItem; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const TW_COIN_TYPE_TESTS_TEMPLATE: &str = include_str!("templates/TWCoinTypeTests.cpp"); + +pub fn tw_coin_type_tests_path(coin: &CoinItem) -> PathBuf { + coin_integration_tests_directory(coin).join("TWCoinTypeTests.cpp") +} + +pub struct TWCoinTypeTestsGenerator; + +impl TWCoinTypeTestsGenerator { + pub fn generate(coin: &CoinItem) -> Result<()> { + let coin_tests_dir = coin_integration_tests_directory(coin); + Self::generate_at(coin, coin_tests_dir) + } + + pub fn generate_cosmos(coin: &CoinItem) -> Result<()> { + let cosmos_coin_tests_dir = cosmos_coin_integration_tests_directory(coin); + Self::generate_at(coin, cosmos_coin_tests_dir) + } + + fn generate_at(coin: &CoinItem, coin_tests_dir: PathBuf) -> Result<()> { + let tw_coin_type_tests_path = coin_tests_dir.join("TWCoinTypeTests.cpp"); + + fs::create_dir(coin_tests_dir)?; + if tw_coin_type_tests_path.exists() { + println!("[SKIP] {tw_coin_type_tests_path:?} already exists"); + return Ok(()); + } + + println!("[ADD] {tw_coin_type_tests_path:?}"); + TemplateGenerator::new(TW_COIN_TYPE_TESTS_TEMPLATE) + .write_to(tw_coin_type_tests_path) + .with_default_patterns(coin) + .write()?; + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/mod.rs b/codegen-v2/src/codegen/mod.rs index 52487305973..1d8522e6b9f 100644 --- a/codegen-v2/src/codegen/mod.rs +++ b/codegen-v2/src/codegen/mod.rs @@ -1,7 +1,9 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. +pub mod cpp; +pub mod proto; +pub mod rust; pub mod swift; +pub mod template_generator; diff --git a/codegen-v2/src/codegen/proto/mod.rs b/codegen-v2/src/codegen/proto/mod.rs new file mode 100644 index 00000000000..47d3e1a2dd5 --- /dev/null +++ b/codegen-v2/src/codegen/proto/mod.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::env; +use std::path::PathBuf; + +pub mod new_blockchain; +pub mod proto_generator; + +pub fn proto_source_directory() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("src") + .join("proto") +} diff --git a/codegen-v2/src/codegen/proto/new_blockchain.rs b/codegen-v2/src/codegen/proto/new_blockchain.rs new file mode 100644 index 00000000000..9e5f51a5387 --- /dev/null +++ b/codegen-v2/src/codegen/proto/new_blockchain.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::proto::proto_generator::ProtoGenerator; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_blockchain(coin: &CoinItem) -> Result<()> { + ProtoGenerator::generate(coin) +} diff --git a/codegen-v2/src/codegen/proto/proto_generator.rs b/codegen-v2/src/codegen/proto/proto_generator.rs new file mode 100644 index 00000000000..f2cdfd94a86 --- /dev/null +++ b/codegen-v2/src/codegen/proto/proto_generator.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::proto::proto_source_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::registry::CoinItem; +use crate::Result; +use std::path::PathBuf; + +const PROTO_TEMPLATE: &str = include_str!("templates/Blockchain.proto"); + +pub fn blockchain_proto_path(coin: &CoinItem) -> PathBuf { + let blockchain_type = coin.blockchain_type(); + proto_source_directory().join(format!("{blockchain_type}.proto")) +} + +pub struct ProtoGenerator; + +impl ProtoGenerator { + pub fn generate(coin: &CoinItem) -> Result<()> { + let proto_path = blockchain_proto_path(coin); + + if proto_path.exists() { + println!("[SKIP] Protobuf interface already exists: {proto_path:?}"); + return Ok(()); + } + + println!("[ADD] {proto_path:?}"); + TemplateGenerator::new(PROTO_TEMPLATE) + .write_to(proto_path) + .with_default_patterns(coin) + .write()?; + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/proto/templates/Blockchain.proto b/codegen-v2/src/codegen/proto/templates/Blockchain.proto new file mode 100644 index 00000000000..9c4c6e34478 --- /dev/null +++ b/codegen-v2/src/codegen/proto/templates/Blockchain.proto @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +syntax = "proto3"; + +package TW.{BLOCKCHAIN}.Proto; +option java_package = "wallet.core.jni.proto"; + +import "Common.proto"; + +// TODO: typical balance transfer, add more fields needed to sign +message TransferMessage { + int64 amount = 1; + int64 fee = 2; + string to = 3; +} + +// TODO: Input data necessary to create a signed transaction. +message SigningInput { + bytes private_key = 1; + + oneof message_oneof { + TransferMessage transfer = 2; + } +} + +// Transaction signing output. +message SigningOutput { + // Signed and encoded transaction bytes. + bytes encoded = 1; + + // A possible error, `OK` if none. + Common.Proto.SigningError error = 2; + + string error_message = 3; +} diff --git a/codegen-v2/src/codegen/rust/blockchain_dispatcher_generator.rs b/codegen-v2/src/codegen/rust/blockchain_dispatcher_generator.rs new file mode 100644 index 00000000000..92f98bac940 --- /dev/null +++ b/codegen-v2/src/codegen/rust/blockchain_dispatcher_generator.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::coin_registry_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const BLOCKCHAIN_ENTRIES_START: &str = "start_of_blockchain_entries"; +const BLOCKCHAIN_ENTRIES_END: &str = "end_of_blockchain_entries"; +const BLOCKCHAIN_DISPATCHER_START: &str = "start_of_blockchain_dispatcher"; +const BLOCKCHAIN_DISPATCHER_END: &str = "end_of_blockchain_dispatcher"; + +pub fn dispatcher_path() -> PathBuf { + coin_registry_directory().join("src").join("dispatcher.rs") +} + +pub struct BlockchainDispatcherGenerator; + +impl BlockchainDispatcherGenerator { + pub fn generate_new_blockchain_type_dispatching(coin: &CoinItem) -> Result<()> { + let dispatcher_rs_path = dispatcher_path(); + println!("[EDIT] {dispatcher_rs_path:?}"); + let mut dispatcher_rs = FileContent::read(dispatcher_rs_path)?; + + Self::generate_use_of_blockchain_entry(coin, &mut dispatcher_rs)?; + Self::generate_blockchain_entry_constant(coin, &mut dispatcher_rs)?; + Self::generate_blockchain_dispatch(coin, &mut dispatcher_rs)?; + + dispatcher_rs.write() + } + + fn generate_use_of_blockchain_entry( + coin: &CoinItem, + file_content: &mut FileContent, + ) -> Result<()> { + let import_pattern = "use "; + let blockchain_entry = coin.blockchain_entry(); + let tw_crate_name = coin.id.to_tw_crate_name(); + + let mut last_entry = file_content.rfind_line(|line| line.contains(import_pattern))?; + last_entry.push_line_after(format!("use {tw_crate_name}::entry::{blockchain_entry};")); + + Ok(()) + } + + fn generate_blockchain_entry_constant( + coin: &CoinItem, + file_content: &mut FileContent, + ) -> Result<()> { + let blockchain_entry = coin.blockchain_entry(); + let blockchain_entry_const = coin.blockchain_entry_upper_snake(); + + let mut entries_region = file_content + .find_region_with_comments(BLOCKCHAIN_ENTRIES_START, BLOCKCHAIN_ENTRIES_END)?; + entries_region.push_line(format!( + "const {blockchain_entry_const}: {blockchain_entry} = {blockchain_entry};" + )); + entries_region.sort(); + + Ok(()) + } + + fn generate_blockchain_dispatch(coin: &CoinItem, file_content: &mut FileContent) -> Result<()> { + let blockchain_type = coin.blockchain_type(); + let blockchain_entry_const = coin.blockchain_entry_upper_snake(); + + let mut dispatcher_region = file_content + .find_region_with_comments(BLOCKCHAIN_DISPATCHER_START, BLOCKCHAIN_DISPATCHER_END)?; + dispatcher_region.push_line(format!( + " BlockchainType::{blockchain_type} => Ok(&{blockchain_entry_const})," + )); + dispatcher_region.sort(); + + Ok(()) + } +} diff --git a/codegen-v2/src/codegen/rust/blockchain_type_generator.rs b/codegen-v2/src/codegen/rust/blockchain_type_generator.rs new file mode 100644 index 00000000000..4895bf8a923 --- /dev/null +++ b/codegen-v2/src/codegen/rust/blockchain_type_generator.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::coin_registry_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const BLOCKCHAIN_TYPE_START: &str = "start_of_blockchain_type"; +const BLOCKCHAIN_TYPE_END: &str = "end_of_blockchain_type"; + +pub fn blockchain_type_path() -> PathBuf { + coin_registry_directory() + .join("src") + .join("blockchain_type.rs") +} + +/// Represents `BlockchainType` enum generator. +pub struct BlockchainTypeGenerator; + +impl BlockchainTypeGenerator { + pub fn add_new_blockchain_type(coin: &CoinItem) -> Result<()> { + let blockchain_type_rs_path = blockchain_type_path(); + let blockchain_type = coin.blockchain_type(); + + println!("[EDIT] {blockchain_type_rs_path:?}"); + let mut blockchain_type_rs = FileContent::read(blockchain_type_rs_path)?; + + { + let mut enum_region = blockchain_type_rs + .find_region_with_comments(BLOCKCHAIN_TYPE_START, BLOCKCHAIN_TYPE_END)?; + enum_region.push_line(format!(" {blockchain_type},")); + enum_region.sort(); + } + + blockchain_type_rs.write() + } +} diff --git a/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs b/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs new file mode 100644 index 00000000000..4d339cb05ad --- /dev/null +++ b/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::tw_tests_directory; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::path::PathBuf; + +const COIN_ADDRESS_DERIVATION_TEST_END: &str = + "end_of_coin_address_derivation_tests_marker_do_not_modify"; +const EVM_ADDRESS_DERIVATION_TEST_END: &str = + "end_of_evm_address_derivation_tests_marker_do_not_modify"; + +pub fn coin_address_derivation_test_path() -> PathBuf { + tw_tests_directory() + .join("tests") + .join("coin_address_derivation_test.rs") +} + +pub struct CoinAddressDerivationTestGenerator; + +impl CoinAddressDerivationTestGenerator { + pub fn generate_new_coin_type_case(coin: &CoinItem) -> Result<()> { + let test_path = coin_address_derivation_test_path(); + let coin_type = coin.coin_type(); + + println!("[EDIT] {test_path:?}"); + let mut coin_address_derivation_test_rs = FileContent::read(test_path)?; + + { + let mut end_of_test = coin_address_derivation_test_rs + .rfind_line(|line| line.contains(COIN_ADDRESS_DERIVATION_TEST_END))?; + end_of_test.push_line_before(format!(" CoinType::{coin_type} => todo!(),")); + } + + coin_address_derivation_test_rs.write() + } + + pub fn generate_new_evm_coin_type_case(coin: &CoinItem) -> Result<()> { + let test_path = coin_address_derivation_test_path(); + let coin_type = coin.coin_type(); + + println!("[EDIT] {test_path:?}"); + let mut coin_address_derivation_test_rs = FileContent::read(test_path)?; + + { + let mut end_of_test = coin_address_derivation_test_rs + .rfind_line(|line| line.contains(EVM_ADDRESS_DERIVATION_TEST_END))?; + end_of_test.push_line_before(format!(" | CoinType::{coin_type}")); + } + + coin_address_derivation_test_rs.write() + } +} diff --git a/codegen-v2/src/codegen/rust/coin_crate.rs b/codegen-v2/src/codegen/rust/coin_crate.rs new file mode 100644 index 00000000000..0e1b133f171 --- /dev/null +++ b/codegen-v2/src/codegen/rust/coin_crate.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::chains_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::coin_id::CoinId; +use crate::registry::CoinItem; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const BLOCKCHAIN_ADDRESS_TEMPLATE: &str = include_str!("templates/blockchain_crate/address.rs"); +const BLOCKCHAIN_COMPILER_TEMPLATE: &str = include_str!("templates/blockchain_crate/compiler.rs"); +const BLOCKCHAIN_ENTRY_TEMPLATE: &str = include_str!("templates/blockchain_crate/entry.rs"); +const BLOCKCHAIN_MANIFEST_TEMPLATE: &str = include_str!("templates/blockchain_crate/Cargo.toml"); +const BLOCKCHAIN_LIB_TEMPLATE: &str = include_str!("templates/blockchain_crate/lib.rs"); +const BLOCKCHAIN_SIGNER_TEMPLATE: &str = include_str!("templates/blockchain_crate/signer.rs"); + +pub fn coin_source_directory(id: &CoinId) -> PathBuf { + chains_directory().join(id.to_tw_crate_name()) +} + +pub struct CoinCrate { + coin: CoinItem, +} + +impl CoinCrate { + pub fn new(coin: CoinItem) -> CoinCrate { + CoinCrate { coin } + } + + /// Creates a Cargo crate with `entry.rs` file. + /// Returns the path to the create crate. + pub fn create(self) -> Result { + let blockchain_path = coin_source_directory(&self.coin.id); + let blockchain_toml_path = blockchain_path.join("Cargo.toml"); + + let blockchain_src_path = blockchain_path.join("src"); + let blockchain_lib_rs_path = blockchain_src_path.join("lib.rs"); + let blockchain_entry_path = blockchain_src_path.join("entry.rs"); + let blockchain_compiler_path = blockchain_src_path.join("compiler.rs"); + let blockchain_address_rs_path = blockchain_src_path.join("address.rs"); + let blockchain_signer_rs_path = blockchain_src_path.join("signer.rs"); + + if blockchain_path.exists() { + let tw_crate_name = self.coin.id.to_tw_crate_name(); + println!( + "[SKIP] '{tw_crate_name}' blockchain crate already exists: {blockchain_path:?}" + ); + return Ok(blockchain_path); + } + + fs::create_dir_all(&blockchain_path)?; + fs::create_dir_all(&blockchain_src_path)?; + + println!("[ADD] {blockchain_toml_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_MANIFEST_TEMPLATE) + .write_to(blockchain_toml_path) + .with_default_patterns(&self.coin) + .write()?; + + println!("[ADD] {blockchain_lib_rs_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_LIB_TEMPLATE) + .write_to(blockchain_lib_rs_path) + .with_default_patterns(&self.coin) + .write()?; + + println!("[ADD] {blockchain_entry_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_ENTRY_TEMPLATE) + .write_to(blockchain_entry_path) + .with_default_patterns(&self.coin) + .write()?; + + println!("[ADD] {blockchain_compiler_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_COMPILER_TEMPLATE) + .write_to(blockchain_compiler_path) + .with_default_patterns(&self.coin) + .write()?; + + println!("[ADD] {blockchain_address_rs_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_ADDRESS_TEMPLATE) + .write_to(blockchain_address_rs_path) + .with_default_patterns(&self.coin) + .write()?; + + println!("[ADD] {blockchain_signer_rs_path:?}"); + TemplateGenerator::new(BLOCKCHAIN_SIGNER_TEMPLATE) + .write_to(blockchain_signer_rs_path) + .with_default_patterns(&self.coin) + .write()?; + + Ok(blockchain_path) + } +} diff --git a/codegen-v2/src/codegen/rust/coin_integration_tests.rs b/codegen-v2/src/codegen/rust/coin_integration_tests.rs new file mode 100644 index 00000000000..067462aacfc --- /dev/null +++ b/codegen-v2/src/codegen/rust/coin_integration_tests.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::tw_tests_directory; +use crate::codegen::template_generator::TemplateGenerator; +use crate::coin_id::CoinId; +use crate::registry::CoinItem; +use crate::utils::FileContent; +use crate::Result; +use std::fs; +use std::path::PathBuf; + +const ADDRESS_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/address_tests.rs"); +const COSMOS_ADDRESS_TESTS_TEMPLATE: &str = + include_str!("templates/integration_tests/cosmos_address_tests.rs"); +const COMPILE_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/compile_tests.rs"); +const MOD_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/mod.rs"); +const MOD_ADDRESS_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/mod_address.rs"); +const SIGN_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/sign_tests.rs"); + +pub fn chains_integration_tests_directory() -> PathBuf { + tw_tests_directory().join("tests").join("chains") +} + +pub fn coin_integration_tests_directory(id: &CoinId) -> PathBuf { + chains_integration_tests_directory().join(id.as_str()) +} + +pub fn coin_address_derivation_test_path() -> PathBuf { + tw_tests_directory() + .join("tests") + .join("coin_address_derivation_test.rs") +} + +pub struct CoinIntegrationTests { + coin: CoinItem, +} + +impl CoinIntegrationTests { + pub fn new(coin: CoinItem) -> CoinIntegrationTests { + CoinIntegrationTests { coin } + } + + pub fn create(self) -> Result { + let blockchain_tests_path = self.coin_tests_directory(); + if blockchain_tests_path.exists() { + println!("[SKIP] integration tests already exists: {blockchain_tests_path:?}"); + return Ok(blockchain_tests_path); + } + + fs::create_dir_all(&blockchain_tests_path)?; + + self.list_blockchain_in_chains_mod()?; + self.create_address_tests(ADDRESS_TESTS_TEMPLATE)?; + self.create_compile_tests()?; + self.create_sign_tests()?; + self.create_chain_tests_mod_rs(MOD_TESTS_TEMPLATE)?; + + Ok(blockchain_tests_path) + } + + /// For a Cosmos chain, it's enough to create address tests only, but with additional Bech32 prefix tests. + pub fn create_cosmos(self) -> Result { + let blockchain_tests_path = self.coin_tests_directory(); + if blockchain_tests_path.exists() { + println!("[SKIP] integration tests already exists: {blockchain_tests_path:?}"); + return Ok(blockchain_tests_path); + } + + fs::create_dir_all(&blockchain_tests_path)?; + + self.list_blockchain_in_chains_mod()?; + self.create_address_tests(COSMOS_ADDRESS_TESTS_TEMPLATE)?; + // `mod.rs` should contain `{COIN_TYPE}_address` module only. + self.create_chain_tests_mod_rs(MOD_ADDRESS_TESTS_TEMPLATE)?; + + Ok(blockchain_tests_path) + } + + fn coin_tests_directory(&self) -> PathBuf { + coin_integration_tests_directory(&self.coin.id) + } + + fn create_address_tests(&self, template: &'static str) -> Result<()> { + let coin_id = self.coin.id.as_str(); + let address_tests_path = self + .coin_tests_directory() + .join(format!("{coin_id}_address.rs")); + + println!("[ADD] {address_tests_path:?}"); + TemplateGenerator::new(template) + .write_to(address_tests_path) + .with_default_patterns(&self.coin) + .write() + } + + fn create_compile_tests(&self) -> Result<()> { + let coin_id = self.coin.id.as_str(); + let compile_tests_path = self + .coin_tests_directory() + .join(format!("{coin_id}_compile.rs")); + + println!("[ADD] {compile_tests_path:?}"); + TemplateGenerator::new(COMPILE_TESTS_TEMPLATE) + .write_to(compile_tests_path) + .with_default_patterns(&self.coin) + .write() + } + + fn create_sign_tests(&self) -> Result<()> { + let coin_id = self.coin.id.as_str(); + let sign_tests_path = self + .coin_tests_directory() + .join(format!("{coin_id}_sign.rs")); + + println!("[ADD] {sign_tests_path:?}"); + TemplateGenerator::new(SIGN_TESTS_TEMPLATE) + .write_to(sign_tests_path) + .with_default_patterns(&self.coin) + .write() + } + + fn create_chain_tests_mod_rs(&self, template: &'static str) -> Result<()> { + let blockchain_tests_mod_path = self.coin_tests_directory().join("mod.rs"); + + println!("[ADD] {blockchain_tests_mod_path:?}"); + TemplateGenerator::new(template) + .write_to(blockchain_tests_mod_path) + .with_default_patterns(&self.coin) + .write() + } + + fn list_blockchain_in_chains_mod(&self) -> Result<()> { + let chains_mod_path = chains_integration_tests_directory().join("mod.rs"); + let chain_id = self.coin.id.as_str(); + + println!("[EDIT] {chains_mod_path:?}"); + let mut chains_mod_rs = FileContent::read(chains_mod_path)?; + + { + let mod_pattern = "mod "; + let mut mod_region = chains_mod_rs.find_region_with_prefix(mod_pattern)?; + mod_region.push_line(format!("mod {chain_id};")); + mod_region.sort(); + } + + chains_mod_rs.write() + } +} diff --git a/codegen-v2/src/codegen/rust/coin_registry_manifest_generator.rs b/codegen-v2/src/codegen/rust/coin_registry_manifest_generator.rs new file mode 100644 index 00000000000..2a63e90e336 --- /dev/null +++ b/codegen-v2/src/codegen/rust/coin_registry_manifest_generator.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::coin_registry_directory; +use crate::codegen::rust::toml_editor::Dependencies; +use crate::registry::CoinItem; +use crate::Result; +use std::path::Path; + +pub struct CoinRegistryManifestGenerator; + +impl CoinRegistryManifestGenerator { + pub fn add_dependency(coin: &CoinItem, path_to_new_blockchain_crate: &Path) -> Result<()> { + let path_to_cargo_manifest = coin_registry_directory().join("Cargo.toml"); + println!("[EDIT] {path_to_cargo_manifest:?}"); + Dependencies::new(path_to_cargo_manifest) + .insert_dependency(&coin.id.to_tw_crate_name(), path_to_new_blockchain_crate) + } +} diff --git a/codegen-v2/src/codegen/rust/mod.rs b/codegen-v2/src/codegen/rust/mod.rs new file mode 100644 index 00000000000..a9de11f38fa --- /dev/null +++ b/codegen-v2/src/codegen/rust/mod.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::env; +use std::path::PathBuf; + +pub mod blockchain_dispatcher_generator; +pub mod blockchain_type_generator; +pub mod coin_address_derivation_test_generator; +pub mod coin_crate; +pub mod coin_integration_tests; +pub mod coin_registry_manifest_generator; +pub mod new_blockchain; +pub mod new_cosmos_chain; +pub mod new_evmchain; +pub mod toml_editor; + +pub fn rust_source_directory() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("rust") +} + +pub fn chains_directory() -> PathBuf { + rust_source_directory().join("chains") +} + +pub fn tw_tests_directory() -> PathBuf { + rust_source_directory().join("tw_tests") +} + +pub fn workspace_toml_path() -> PathBuf { + rust_source_directory().join("Cargo.toml") +} + +pub fn coin_registry_directory() -> PathBuf { + rust_source_directory().join("tw_coin_registry") +} diff --git a/codegen-v2/src/codegen/rust/new_blockchain.rs b/codegen-v2/src/codegen/rust/new_blockchain.rs new file mode 100644 index 00000000000..ca27f77b08e --- /dev/null +++ b/codegen-v2/src/codegen/rust/new_blockchain.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::blockchain_dispatcher_generator::BlockchainDispatcherGenerator; +use crate::codegen::rust::blockchain_type_generator::BlockchainTypeGenerator; +use crate::codegen::rust::coin_address_derivation_test_generator::CoinAddressDerivationTestGenerator; +use crate::codegen::rust::coin_crate::CoinCrate; +use crate::codegen::rust::coin_integration_tests::CoinIntegrationTests; +use crate::codegen::rust::coin_registry_manifest_generator::CoinRegistryManifestGenerator; +use crate::codegen::rust::toml_editor::Workspace; +use crate::codegen::rust::workspace_toml_path; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_blockchain(coin: &CoinItem) -> Result<()> { + // Create blockchain's crate. + let blockchain_crate_path = CoinCrate::new(coin.clone()).create()?; + + // Insert the created crate to the workspace. + Workspace::new(workspace_toml_path()).insert_crate(&blockchain_crate_path)?; + + // Create integration tests. + CoinIntegrationTests::new(coin.clone()).create()?; + CoinAddressDerivationTestGenerator::generate_new_coin_type_case(coin)?; + + // Add the new blockchain to the `tw_coin_registry`. + BlockchainTypeGenerator::add_new_blockchain_type(coin)?; + CoinRegistryManifestGenerator::add_dependency(coin, &blockchain_crate_path)?; + BlockchainDispatcherGenerator::generate_new_blockchain_type_dispatching(coin)?; + + Ok(()) +} diff --git a/codegen-v2/src/codegen/rust/new_cosmos_chain.rs b/codegen-v2/src/codegen/rust/new_cosmos_chain.rs new file mode 100644 index 00000000000..96151ff6446 --- /dev/null +++ b/codegen-v2/src/codegen/rust/new_cosmos_chain.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::coin_address_derivation_test_generator::CoinAddressDerivationTestGenerator; +use crate::codegen::rust::coin_integration_tests::CoinIntegrationTests; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_cosmos_chain(coin: &CoinItem) -> Result<()> { + // Create integration tests. + CoinIntegrationTests::new(coin.clone()).create_cosmos()?; + CoinAddressDerivationTestGenerator::generate_new_coin_type_case(coin)?; + + Ok(()) +} diff --git a/codegen-v2/src/codegen/rust/new_evmchain.rs b/codegen-v2/src/codegen/rust/new_evmchain.rs new file mode 100644 index 00000000000..f754fa5ba1b --- /dev/null +++ b/codegen-v2/src/codegen/rust/new_evmchain.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::codegen::rust::coin_address_derivation_test_generator::CoinAddressDerivationTestGenerator; +use crate::registry::CoinItem; +use crate::Result; + +pub fn new_evmchain(coin: &CoinItem) -> Result<()> { + // Modify integration tests. + CoinAddressDerivationTestGenerator::generate_new_evm_coin_type_case(coin) +} diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml b/codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml new file mode 100644 index 00000000000..9d5a7a7d04a --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "{TW_CRATE_NAME}" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/address.rs b/codegen-v2/src/codegen/rust/templates/blockchain_crate/address.rs new file mode 100644 index 00000000000..3ec4f8516b5 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/address.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +pub struct {BLOCKCHAIN}Address { + // TODO add necessary fields. +} + +impl CoinAddress for {BLOCKCHAIN}Address { + #[inline] + fn data(&self) -> Data { + todo!() + } +} + +impl FromStr for {BLOCKCHAIN}Address { + type Err = AddressError; + + fn from_str(_s: &str) -> Result { + todo!() + } +} + +impl fmt::Display for {BLOCKCHAIN}Address { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } +} diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/compiler.rs b/codegen-v2/src/codegen/rust/templates/blockchain_crate/compiler.rs new file mode 100644 index 00000000000..63d51f1df50 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/compiler.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::{BLOCKCHAIN}::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct {BLOCKCHAIN}Compiler; + +impl {BLOCKCHAIN}Compiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + todo!() + } +} diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/entry.rs b/codegen-v2/src/codegen/rust/templates/blockchain_crate/entry.rs new file mode 100644 index 00000000000..9b1194bbb68 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/entry.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::{BLOCKCHAIN}Address; +use crate::compiler::{BLOCKCHAIN}Compiler; +use crate::signer::{BLOCKCHAIN}Signer; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::{BLOCKCHAIN}::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct {BLOCKCHAIN}Entry; + +impl CoinEntry for {BLOCKCHAIN}Entry { + type AddressPrefix = NoPrefix; + type Address = {BLOCKCHAIN}Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + _address: &str, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + {BLOCKCHAIN}Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + _public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + {BLOCKCHAIN}Signer::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + {BLOCKCHAIN}Compiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + {BLOCKCHAIN}Compiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/lib.rs b/codegen-v2/src/codegen/rust/templates/blockchain_crate/lib.rs new file mode 100644 index 00000000000..c41edeb4471 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/lib.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod entry; +pub mod signer; diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/signer.rs b/codegen-v2/src/codegen/rust/templates/blockchain_crate/signer.rs new file mode 100644 index 00000000000..3a64018e599 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/blockchain_crate/signer.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::{BLOCKCHAIN}::Proto; + +pub struct {BLOCKCHAIN}Signer; + +impl {BLOCKCHAIN}Signer { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } +} diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs new file mode 100644 index 00000000000..1d417a77ed4 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/address_tests.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_{COIN_ID}_address_derive() { + test_address_derive(CoinType::{COIN_TYPE}, "PRIVATE_KEY", "EXPECTED ADDRESS"); +} + +#[test] +fn test_{COIN_ID}_address_normalization() { + test_address_normalization(CoinType::{COIN_TYPE}, "DENORMALIZED", "EXPECTED"); +} + +#[test] +fn test_{COIN_ID}_address_is_valid() { + test_address_valid(CoinType::{COIN_TYPE}, "VALID ADDRESS"); +} + +#[test] +fn test_{COIN_ID}_address_invalid() { + test_address_invalid(CoinType::{COIN_TYPE}, "INVALID ADDRESS"); +} + +#[test] +fn test_{COIN_ID}_address_get_data() { + test_address_get_data(CoinType::{COIN_TYPE}, "ADDRESS", "HEX(DATA)"); +} diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/compile_tests.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/compile_tests.rs new file mode 100644 index 00000000000..fad7f69330a --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/compile_tests.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_{COIN_ID}_compile() { + todo!() +} diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/cosmos_address_tests.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/cosmos_address_tests.rs new file mode 100644 index 00000000000..6bf6079ac9d --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/cosmos_address_tests.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_bech32_is_valid, test_address_create_bech32_with_public_key, + test_address_get_data, test_address_invalid, test_address_normalization, test_address_valid, + AddressBech32IsValid, AddressCreateBech32WithPublicKey, +}; +use tw_coin_registry::coin_type::CoinType; +use tw_keypair::tw::PublicKeyType; + +#[test] +fn test_{COIN_ID}_address_normalization() { + test_address_normalization(CoinType::{COIN_TYPE}, "DENORMALIZED", "EXPECTED"); +} + +#[test] +fn test_{COIN_ID}_address_is_valid() { + test_address_valid(CoinType::{COIN_TYPE}, "VALID {COIN_TYPE} ADDRESS"); +} + +#[test] +fn test_{COIN_ID}_address_invalid() { + test_address_invalid(CoinType::{COIN_TYPE}, "INVALID ADDRESS"); + // Cosmos has a different `hrp`. + test_address_invalid(CoinType::Cosmos, "VALID {COIN_TYPE} ADDRESS"); +} + +#[test] +fn test_{COIN_ID}_address_get_data() { + test_address_get_data(CoinType::{COIN_TYPE}, "ADDRESS", "HEX(DATA)"); +} + +#[test] +fn test_{COIN_ID}_is_valid_bech32() { + // {COIN_TYPE} address must be valid if its Base32 prefix passed. + test_address_bech32_is_valid(AddressBech32IsValid { + coin: CoinType::{COIN_TYPE}, + address: "{COIN_TYPE} ADDRESS", + hrp: "{HRP}", + }); + // {COIN_TYPE} address must be valid for the standard Cosmos hub if its Base32 prefix passed. + test_address_bech32_is_valid(AddressBech32IsValid { + coin: CoinType::Cosmos, + address: "{COIN_TYPE} ADDRESS", + hrp: "{HRP}", + }); + // Cosmos address must be valid with "cosmos" hrp. + test_address_bech32_is_valid(AddressBech32IsValid { + coin: CoinType::{COIN_TYPE}, + address: "cosmos1hsk6jryyqjfhp5dhc55tc9jtckygx0eph6dd02", + hrp: "cosmos", + }); +} + +#[test] +fn test_any_address_create_bech32_with_public_key() { + test_address_create_bech32_with_public_key(AddressCreateBech32WithPublicKey { + // TODO consider using `CoinType::{COIN_TYPE}` if the chain's `addressHasher` is different from Cosmos's. + coin: CoinType::Cosmos, + private_key: "PRIVATE_KEY", + // TODO consider using another `PublicKeyType` if the chain's `publicKeyType` is different from Cosmos's. + public_key_type: PublicKeyType::Secp256k1, + hrp: "{HRP}", + expected: "{COIN_TYPE} ADDRESS", + }); +} + diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/mod.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/mod.rs new file mode 100644 index 00000000000..15dccad969f --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod {COIN_ID}_address; +mod {COIN_ID}_compile; +mod {COIN_ID}_sign; diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/mod_address.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/mod_address.rs new file mode 100644 index 00000000000..6f501e180bc --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/mod_address.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod {COIN_ID}_address; diff --git a/codegen-v2/src/codegen/rust/templates/integration_tests/sign_tests.rs b/codegen-v2/src/codegen/rust/templates/integration_tests/sign_tests.rs new file mode 100644 index 00000000000..38f01a4e0e1 --- /dev/null +++ b/codegen-v2/src/codegen/rust/templates/integration_tests/sign_tests.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_{COIN_ID}_sign() { + todo!() +} diff --git a/codegen-v2/src/codegen/rust/toml_editor.rs b/codegen-v2/src/codegen/rust/toml_editor.rs new file mode 100644 index 00000000000..0811182365d --- /dev/null +++ b/codegen-v2/src/codegen/rust/toml_editor.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::{Error, Result}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use toml_edit::{Document, InlineTable, Item, Value}; + +const NEW_LINE_TAB_DECORATOR: &str = "\n "; +const NO_DECORATOR: &str = ""; + +pub struct Workspace { + path_to_toml: PathBuf, +} + +impl Workspace { + pub fn new(path_to_toml: PathBuf) -> Workspace { + Workspace { path_to_toml } + } + + pub fn insert_crate(self, path_to_crate: &Path) -> Result<()> { + let manifest = fs::read_to_string(&self.path_to_toml)?; + let mut manifest = Document::from_str(&manifest)?; + + let members = manifest["workspace"]["members"] + .as_array_mut() + .ok_or(Error::TomlFormat( + "Invalid 'workspace' TOML format".to_string(), + ))?; + + // Try to get a path to the crate relative to the `Cargo.toml`. + let relative_path_to_crate = relative_path_to_crate(&self.path_to_toml, path_to_crate)?; + + // Push the new member, sort and save the manifest. + + let relative_path_to_crate_decorated = + Value::from(relative_path_to_crate).decorated(NEW_LINE_TAB_DECORATOR, NO_DECORATOR); + + members.push_formatted(relative_path_to_crate_decorated); + members.sort_by(|x, y| x.as_str().cmp(&y.as_str())); + + fs::write(self.path_to_toml, manifest.to_string())?; + Ok(()) + } +} + +pub struct Dependencies { + path_to_toml: PathBuf, +} + +impl Dependencies { + pub fn new(path_to_toml: PathBuf) -> Dependencies { + Dependencies { path_to_toml } + } + + pub fn insert_dependency(self, dep_name: &str, path_to_dep_crate: &Path) -> Result<()> { + let manifest = fs::read_to_string(&self.path_to_toml)?; + let mut manifest = Document::from_str(&manifest)?; + + let dependencies = manifest["dependencies"] + .as_table_like_mut() + .ok_or(Error::TomlFormat("Invalid 'Cargo.toml' format".to_string()))?; + + // Try to get a path to the crate relative to the `Cargo.toml`. + let relative_path_to_crate = relative_path_to_crate(&self.path_to_toml, path_to_dep_crate)?; + + // Create the new dependency member (aka a TOML inline table with `path` key-value). + let mut new_member = InlineTable::new(); + new_member.insert("path", relative_path_to_crate.into()); + + // Push the new member, sort and save the manifest. + dependencies.insert(dep_name, Item::Value(Value::InlineTable(new_member))); + dependencies.sort_values(); + + fs::write(self.path_to_toml, manifest.to_string())?; + + Ok(()) + } +} + +/// Returns a path to the dependency accordingly to the Cargo manifest file. +/// The result string can be put to `Cargo.toml` as: +/// ```toml +/// tw_foo = { path = "" } +/// ``` +fn relative_path_to_crate( + path_to_cargo_manifest: &Path, + path_to_dependency: &Path, +) -> Result { + let absolute_path_to_crate_directory = path_to_cargo_manifest + .parent() + .ok_or_else(|| Error::io_error_other("Cannot get a parent directory".to_string()))? + .canonicalize()?; + let absolute_path_to_dependency = path_to_dependency.canonicalize()?; + + let relative_path_to_dependency = pathdiff::diff_paths( + absolute_path_to_dependency, + absolute_path_to_crate_directory, + ) + .ok_or_else(|| { + Error::io_error_other("Cannot get a relative path to the dependency".to_string()) + })? + .to_str() + .ok_or_else(|| Error::io_error_other("Invalid path to the crate".to_string()))? + .to_string(); + + Ok(relative_path_to_dependency) +} diff --git a/codegen-v2/src/codegen/swift/functions.rs b/codegen-v2/src/codegen/swift/functions.rs index 08573893865..1f7e4aa8d39 100644 --- a/codegen-v2/src/codegen/swift/functions.rs +++ b/codegen-v2/src/codegen/swift/functions.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use super::*; use crate::manifest::{FunctionInfo, TypeVariant}; diff --git a/codegen-v2/src/codegen/swift/inits.rs b/codegen-v2/src/codegen/swift/inits.rs index 07de55e037d..a1f140c326e 100644 --- a/codegen-v2/src/codegen/swift/inits.rs +++ b/codegen-v2/src/codegen/swift/inits.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use super::*; use crate::manifest::InitInfo; diff --git a/codegen-v2/src/codegen/swift/mod.rs b/codegen-v2/src/codegen/swift/mod.rs index bb19c2d7478..4bc70e7e416 100644 --- a/codegen-v2/src/codegen/swift/mod.rs +++ b/codegen-v2/src/codegen/swift/mod.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use self::functions::process_methods; use self::inits::process_inits; diff --git a/codegen-v2/src/codegen/swift/properties.rs b/codegen-v2/src/codegen/swift/properties.rs index 0f19db983ba..61d73e7171f 100644 --- a/codegen-v2/src/codegen/swift/properties.rs +++ b/codegen-v2/src/codegen/swift/properties.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use super::*; use crate::manifest::PropertyInfo; diff --git a/codegen-v2/src/codegen/swift/render.rs b/codegen-v2/src/codegen/swift/render.rs index b7f68716b72..465a7277682 100644 --- a/codegen-v2/src/codegen/swift/render.rs +++ b/codegen-v2/src/codegen/swift/render.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use super::{inits::process_deinits, *}; diff --git a/codegen-v2/src/codegen/swift/templates/WalletCore.h b/codegen-v2/src/codegen/swift/templates/WalletCore.h index 98e432e9066..76f61b9fa74 100644 --- a/codegen-v2/src/codegen/swift/templates/WalletCore.h +++ b/codegen-v2/src/codegen/swift/templates/WalletCore.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #import diff --git a/codegen-v2/src/codegen/swift/templates/enum.hbs b/codegen-v2/src/codegen/swift/templates/enum.hbs index e9fd0b59cf7..5363fb776b4 100644 --- a/codegen-v2/src/codegen/swift/templates/enum.hbs +++ b/codegen-v2/src/codegen/swift/templates/enum.hbs @@ -1,8 +1,6 @@ -// Copyright © 2017-{{current_year}} Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/codegen/swift/templates/extension.hbs b/codegen-v2/src/codegen/swift/templates/extension.hbs index de51b918f76..ddae1f95ba6 100644 --- a/codegen-v2/src/codegen/swift/templates/extension.hbs +++ b/codegen-v2/src/codegen/swift/templates/extension.hbs @@ -1,8 +1,6 @@ -// Copyright © 2017-{{current_year}} Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/codegen/swift/templates/proto.hbs b/codegen-v2/src/codegen/swift/templates/proto.hbs index aad05bf9191..23e1c541698 100644 --- a/codegen-v2/src/codegen/swift/templates/proto.hbs +++ b/codegen-v2/src/codegen/swift/templates/proto.hbs @@ -1,8 +1,6 @@ -// Copyright © 2017-{{current_year}} Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/codegen/swift/templates/struct.hbs b/codegen-v2/src/codegen/swift/templates/struct.hbs index 6d7f9bb63ce..eccc0499fd7 100644 --- a/codegen-v2/src/codegen/swift/templates/struct.hbs +++ b/codegen-v2/src/codegen/swift/templates/struct.hbs @@ -1,8 +1,6 @@ -// Copyright © 2017-{{current_year}} Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/codegen/template_generator.rs b/codegen-v2/src/codegen/template_generator.rs new file mode 100644 index 00000000000..7b725ba74e7 --- /dev/null +++ b/codegen-v2/src/codegen/template_generator.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::registry::CoinItem; +use crate::{current_year, Error, Result}; +use std::fs; +use std::path::PathBuf; + +const PATTERNS_CAPACITY: usize = 20; + +pub struct TemplateGenerator { + template_content: &'static str, + write_to: Option, + to_replace: Vec, + replace_with: Vec, +} + +impl TemplateGenerator { + pub fn new(template_content: &'static str) -> TemplateGenerator { + TemplateGenerator { + template_content, + write_to: None, + to_replace: Vec::with_capacity(PATTERNS_CAPACITY), + replace_with: Vec::with_capacity(PATTERNS_CAPACITY), + } + } + + pub fn write_to(mut self, write_to: PathBuf) -> TemplateGenerator { + self.write_to = Some(write_to); + self + } + + /// Use default patterns. + pub fn with_default_patterns(self, coin: &CoinItem) -> TemplateGenerator { + self.add_pattern("{YEAR}", current_year()) + .add_pattern("{BLOCKCHAIN}", coin.blockchain_type()) + .add_pattern("{TW_CRATE_NAME}", coin.id.to_tw_crate_name()) + .add_pattern("{COIN_ID}", coin.id.as_str()) + .add_pattern("{COIN_TYPE}", coin.coin_type()) + .add_pattern("{COIN_NAME}", if coin.display_name.len() > 0 { &coin.display_name } else { &coin.name }) + .add_pattern("{SYMBOL}", &coin.symbol) + .add_pattern("{DECIMALS}", coin.decimals) + .add_pattern("{P2PKH_PREFIX}", coin.p2pkh_prefix) + .add_pattern("{P2SH_PREFIX}", coin.p2sh_prefix) + .add_pattern("{HRP}", coin.hrp.as_str()) + .add_pattern("{STATIC_PREFIX}", coin.static_prefix) + .add_pattern("{EXPLORER_URL}", &coin.explorer.url) + .add_pattern("{EXPLORER_TX_PATH}", &coin.explorer.tx_path) + .add_pattern("{EXPLORER_ACCOUNT_PATH}", &coin.explorer.account_path) + .add_pattern("{EXPLORER_SAMPLE_TX}", &coin.explorer.sample_tx) + .add_pattern("{EXPLORER_SAMPLE_ACCOUNT}", &coin.explorer.sample_account) + } + + pub fn add_pattern( + mut self, + to_replace: K, + replace_with: V, + ) -> TemplateGenerator { + self.to_replace.push(to_replace.to_string()); + self.replace_with.push(replace_with.to_string()); + self + } + + pub fn write(self) -> Result<()> { + let write_to_path = self.write_to.ok_or_else(|| { + Error::io_error_other("Incorrect use of 'TemplateGenerator'".to_string()) + })?; + let file_to_write = fs::File::create(write_to_path)?; + + aho_corasick::AhoCorasick::new(self.to_replace) + .map_err(|e| Error::io_error_other(format!("Invalid patterns: {e}")))? + .try_stream_replace_all( + self.template_content.as_bytes(), + file_to_write, + &self.replace_with, + ) + .map_err(Error::from) + } +} diff --git a/codegen-v2/src/coin_id.rs b/codegen-v2/src/coin_id.rs new file mode 100644 index 00000000000..87a6b9e13c4 --- /dev/null +++ b/codegen-v2/src/coin_id.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::{Error, Result}; +use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer}; + +#[derive(Clone, Eq, PartialEq)] +pub struct CoinId(String); + +impl CoinId { + /// Returns `Ok` if only the given `id` is a valid Rust identifier. + pub fn new(id: String) -> Result { + let first_letter = id + .chars() + .next() + .ok_or(Error::RegistryError("Invalid 'id'".to_string()))?; + let valid_chars = id.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_'); + + if first_letter.is_numeric() || !valid_chars { + return Err(Error::RegistryError("Invalid 'id'".to_string())); + } + Ok(CoinId(id)) + } + + pub fn to_tw_crate_name(&self) -> String { + format!("tw_{}", self.0) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl<'de> Deserialize<'de> for CoinId { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let id = String::deserialize(deserializer)?; + CoinId::new(id).map_err(|e| SerdeError::custom(format!("{e:?}"))) + } +} diff --git a/codegen-v2/src/lib.rs b/codegen-v2/src/lib.rs index ca6556bb44d..3e2c2e2e59d 100644 --- a/codegen-v2/src/lib.rs +++ b/codegen-v2/src/lib.rs @@ -1,20 +1,23 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #[macro_use] extern crate serde; use handlebars::{RenderError, TemplateError}; use serde_yaml::Error as YamlError; +use std::io; use std::io::Error as IoError; +use toml_edit::TomlError; pub mod codegen; +pub mod coin_id; pub mod manifest; +pub mod registry; #[cfg(test)] mod tests; +pub mod utils; pub type Result = std::result::Result; @@ -25,9 +28,17 @@ pub enum Error { RenderError(RenderError), TemplateError(TemplateError), BadFormat(String), + RegistryError(String), + TomlFormat(String), InvalidCommand, } +impl Error { + pub fn io_error_other(err: String) -> Error { + Error::IoError(IoError::new(io::ErrorKind::Other, err)) + } +} + impl From for Error { fn from(err: IoError) -> Self { Error::IoError(err) @@ -52,6 +63,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: TomlError) -> Self { + Error::TomlFormat(err.to_string()) + } +} + fn current_year() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/codegen-v2/src/main.rs b/codegen-v2/src/main.rs index 1be255b2f20..53fd140cc65 100644 --- a/codegen-v2/src/main.rs +++ b/codegen-v2/src/main.rs @@ -1,11 +1,12 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use libparser::codegen::swift::RenderIntput; +use libparser::codegen::{cpp, proto, rust}; +use libparser::coin_id::CoinId; use libparser::manifest::parse_dir; +use libparser::registry::read_coin_from_registry; use libparser::{Error, Result}; use std::fs::read_to_string; @@ -17,11 +18,66 @@ fn main() -> Result<()> { } match args[1].as_str() { + "new-blockchain-rust" => new_blockchain_rust(&args[2..]), + "new-blockchain" => new_blockchain(&args[2..]), + "new-evmchain" => new_evmchain(&args[2..]), + "new-cosmos-chain" => new_cosmos_chain(&args[2..]), "swift" => generate_swift_bindings(), _ => Err(Error::InvalidCommand), } } +fn new_blockchain_rust(args: &[String]) -> Result<()> { + let coin_str = args.iter().next().ok_or_else(|| Error::InvalidCommand)?; + let coin_id = CoinId::new(coin_str.clone())?; + let coin_item = read_coin_from_registry(&coin_id)?; + + println!("New Rust blockchain template for coin '{coin_str}' requested"); + rust::new_blockchain::new_blockchain(&coin_item)?; + + Ok(()) +} + +fn new_blockchain(args: &[String]) -> Result<()> { + let coin_str = args.iter().next().ok_or_else(|| Error::InvalidCommand)?; + let coin_id = CoinId::new(coin_str.clone())?; + let coin_item = read_coin_from_registry(&coin_id)?; + + println!("New '{coin_str}' blockchain template requested"); + + proto::new_blockchain::new_blockchain(&coin_item)?; + rust::new_blockchain::new_blockchain(&coin_item)?; + cpp::new_blockchain::new_blockchain(&coin_item)?; + + Ok(()) +} + +fn new_evmchain(args: &[String]) -> Result<()> { + let coin_str = args.iter().next().ok_or_else(|| Error::InvalidCommand)?; + let coin_id = CoinId::new(coin_str.clone())?; + let coin_item = read_coin_from_registry(&coin_id)?; + + println!("New '{coin_str}' EVM chain template requested"); + + rust::new_evmchain::new_evmchain(&coin_item)?; + cpp::new_evmchain::new_evmchain(&coin_item)?; + + Ok(()) +} + +fn new_cosmos_chain(args: &[String]) -> Result<()> { + let coin_str = args.iter().next().ok_or_else(|| Error::InvalidCommand)?; + let coin_id = CoinId::new(coin_str.clone())?; + let coin_item = read_coin_from_registry(&coin_id)?; + + println!("New '{coin_str}' Cosmos chain template requested"); + + rust::new_cosmos_chain::new_cosmos_chain(&coin_item)?; + cpp::new_cosmos_chain::new_cosmos_chain(&coin_item)?; + + Ok(()) +} + fn generate_swift_bindings() -> Result<()> { // NOTE: The paths will be configurable, eventually. const OUT_DIR: &str = "bindings/"; diff --git a/codegen-v2/src/manifest.rs b/codegen-v2/src/manifest.rs index 91ef48f1392..f70c24b2e5a 100644 --- a/codegen-v2/src/manifest.rs +++ b/codegen-v2/src/manifest.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use super::Result; use std::fs; diff --git a/codegen-v2/src/registry.rs b/codegen-v2/src/registry.rs new file mode 100644 index 00000000000..c83ef174dd0 --- /dev/null +++ b/codegen-v2/src/registry.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::coin_id::CoinId; +use crate::{Error, Result}; +use convert_case::{Case, Casing}; +use std::path::PathBuf; +use std::{env, fs}; + +pub fn registry_json_path() -> PathBuf { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("registry.json") +} + +#[derive(Clone, Deserialize)] +pub struct CoinExplorer { + pub url: String, + #[serde(rename = "txPath")] + pub tx_path: String, + #[serde(rename = "accountPath")] + pub account_path: String, + #[serde(rename = "sampleTx")] + #[serde(default)] + pub sample_tx: String, + #[serde(rename = "sampleAccount")] + #[serde(default)] + pub sample_account: String, +} + +#[derive(Clone, Deserialize)] +pub struct CoinItem { + pub id: CoinId, + pub name: String, + #[serde(rename = "displayName")] + #[serde(default)] + pub display_name: String, + #[serde(rename = "coinId")] + pub coin_id_number: u32, + pub symbol: String, + pub decimals: u8, + pub blockchain: String, + #[serde(rename = "p2pkhPrefix")] + #[serde(default)] + pub p2pkh_prefix: u8, + #[serde(rename = "p2shPrefix")] + #[serde(default)] + pub p2sh_prefix: u8, + #[serde(rename = "staticPrefix")] + #[serde(default)] + pub static_prefix: u8, + #[serde(default)] + pub hrp: String, + pub explorer: CoinExplorer, +} + +impl CoinItem { + /// Transforms a coin name to a Rust name. + /// https://github.com/trustwallet/wallet-core/blob/3769f31b7d0c75126b2f426bb065364429aaa379/codegen/lib/coin_skeleton_gen.rb#L15-L22 + pub fn coin_type(&self) -> String { + self.name.replace([' ', '.', '-'], "") + } + + /// Returns the blockchain type in `UpperCamel` case. + pub fn blockchain_type(&self) -> String { + self.blockchain.to_case(Case::UpperCamel) + } + + /// Returns the blockchain type in `UPPER_SNAKE` case. + pub fn blockchain_entry_upper_snake(&self) -> String { + self.blockchain.to_case(Case::UpperSnake) + } + + /// Returns a Rust blockchain entry of the blockchain. + pub fn blockchain_entry(&self) -> String { + format!("{}Entry", self.blockchain_type()) + } +} + +pub fn read_coin_from_registry(coin: &CoinId) -> Result { + let registry_path = registry_json_path(); + + let registry_bytes = fs::read(registry_path)?; + let coins: Vec = + serde_json::from_slice(®istry_bytes).map_err(|e| Error::RegistryError(e.to_string()))?; + + coins + .into_iter() + .find(|item| item.id == *coin) + .ok_or(Error::InvalidCommand) +} diff --git a/codegen-v2/src/tests/mod.rs b/codegen-v2/src/tests/mod.rs index 266d3913191..6a889017da4 100644 --- a/codegen-v2/src/tests/mod.rs +++ b/codegen-v2/src/tests/mod.rs @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. use crate::codegen::swift::{render_to_strings, RenderIntput}; use crate::manifest::parse_str; diff --git a/codegen-v2/src/tests/samples/class.output.swift b/codegen-v2/src/tests/samples/class.output.swift index 0b5b7fbb3a1..ed7c0b000ac 100644 --- a/codegen-v2/src/tests/samples/class.output.swift +++ b/codegen-v2/src/tests/samples/class.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/enum.output.swift b/codegen-v2/src/tests/samples/enum.output.swift index ebc0cd0b491..0e5b8346d6f 100644 --- a/codegen-v2/src/tests/samples/enum.output.swift +++ b/codegen-v2/src/tests/samples/enum.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/enum_extension.output.swift b/codegen-v2/src/tests/samples/enum_extension.output.swift index afdd1d794cc..706368ee760 100644 --- a/codegen-v2/src/tests/samples/enum_extension.output.swift +++ b/codegen-v2/src/tests/samples/enum_extension.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/enum_private.output.swift b/codegen-v2/src/tests/samples/enum_private.output.swift index b1f199054fe..b2cb22aea3a 100644 --- a/codegen-v2/src/tests/samples/enum_private.output.swift +++ b/codegen-v2/src/tests/samples/enum_private.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/non-associated.output.swift b/codegen-v2/src/tests/samples/non-associated.output.swift index d4e8ee82477..478b8b1d6b5 100644 --- a/codegen-v2/src/tests/samples/non-associated.output.swift +++ b/codegen-v2/src/tests/samples/non-associated.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/optional.output.swift b/codegen-v2/src/tests/samples/optional.output.swift index e2cef180240..a33b4afea58 100644 --- a/codegen-v2/src/tests/samples/optional.output.swift +++ b/codegen-v2/src/tests/samples/optional.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/private_class.output.swift b/codegen-v2/src/tests/samples/private_class.output.swift index 0ce8618172f..2362367af00 100644 --- a/codegen-v2/src/tests/samples/private_class.output.swift +++ b/codegen-v2/src/tests/samples/private_class.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/tests/samples/struct.output.swift b/codegen-v2/src/tests/samples/struct.output.swift index 927ffd1eac1..34f310c1eb8 100644 --- a/codegen-v2/src/tests/samples/struct.output.swift +++ b/codegen-v2/src/tests/samples/struct.output.swift @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen-v2/src/utils.rs b/codegen-v2/src/utils.rs new file mode 100644 index 00000000000..a61222fc3fd --- /dev/null +++ b/codegen-v2/src/utils.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::{Error, Result}; +use std::fs; +use std::path::{Path, PathBuf}; + +pub fn read_lines>(path: P) -> Result> { + let lines = fs::read_to_string(path)? + .split('\n') + .map(|line| line.to_string()) + .collect(); + Ok(lines) +} + +pub fn write_lines>(path: P, lines: Vec) -> Result<()> { + let content = lines.join("\n"); + fs::write(path, content).map_err(Error::from) +} + +pub struct FileContent { + path: PathBuf, + lines: Vec, +} + +impl FileContent { + pub fn read(path: PathBuf) -> Result { + read_lines(&path).map(|lines| FileContent { path, lines }) + } + + pub fn find_region_with_prefix(&mut self, prefix: &str) -> Result> { + // Find the first line that starts with the `prefix`. + let region_starts_at = self + .lines + .iter() + .position(|line| line.starts_with(prefix)) + .ok_or_else(|| Error::io_error_other(format!("Cannot find the `{prefix}` region")))?; + + // Find the last line that starts with the `prefix`. + let region_ends_at = self + .lines + .iter() + .rposition(|line| line.starts_with(prefix)) + .ok_or_else(|| Error::io_error_other(format!("Cannot find the `{prefix}` region")))?; + + Ok(FileRegion { + lines: &mut self.lines, + region_starts_at, + region_ends_at, + }) + } + + pub fn find_region_with_comments( + &mut self, + start_comment: &str, + end_comment: &str, + ) -> Result> { + // Find the position of the `start_comment`. + let start_comment_at = self + .lines + .iter() + .position(|line| line.contains(start_comment)) + .ok_or_else(|| { + Error::io_error_other(format!("Cannot find the `{start_comment}` line")) + })?; + let end_comment_at = self + .lines + .iter() + .skip(start_comment_at) + .position(|line| line.contains(end_comment)) + .ok_or_else(|| { + Error::io_error_other(format!("Cannot find the `{end_comment}` line")) + })? + + start_comment_at; + + let region_starts_at = start_comment_at + 1; + let region_ends_at = end_comment_at - 1; + + if region_starts_at > region_ends_at { + return Err(Error::io_error_other(format!( + "There must be the content between {start_comment} and {end_comment}" + ))); + } + + Ok(FileRegion { + lines: &mut self.lines, + region_starts_at, + region_ends_at, + }) + } + + pub fn rfind_line(&mut self, f: F) -> Result> + where + F: Fn(&str) -> bool, + { + let line_idx = self.lines.iter().rposition(|line| f(line)).ok_or_else(|| { + Error::io_error_other(format!( + "{:?} file does not contain a required pattern", + self.path + )) + })?; + Ok(LinePointer { + lines: &mut self.lines, + line_idx, + }) + } + + pub fn write(self) -> Result<()> { + write_lines(self.path, self.lines) + } +} + +pub struct FileRegion<'a> { + lines: &'a mut Vec, + region_starts_at: usize, + region_ends_at: usize, +} + +impl<'a> FileRegion<'a> { + pub fn push_line(&mut self, line: String) { + self.lines.insert(self.region_ends_at + 1, line); + self.region_ends_at += 1; + } + + pub fn sort(&mut self) { + self.lines[self.region_starts_at..=self.region_ends_at].sort() + } + + pub fn count_lines(&self) -> usize { + self.region_ends_at - self.region_starts_at + } +} + +pub struct LinePointer<'a> { + lines: &'a mut Vec, + line_idx: usize, +} + +impl<'a> LinePointer<'a> { + /// Please note that the line pointer will be shifted to the same line on which it pointed before. + pub fn push_line_before(&mut self, line: String) { + self.lines.insert(self.line_idx, line); + self.line_idx += 1; + } + + pub fn push_paragraph_before(&mut self, paragraph: String) { + for line in paragraph.split("\n") { + self.push_line_before(line.to_string()); + } + } + + /// Please note that the line pointer will not be shifted to the pushed element. + pub fn push_line_after(&mut self, line: String) { + self.lines.insert(self.line_idx + 1, line); + } +} diff --git a/codegen/bin/newcoin b/codegen/bin/newcoin index aeeec609d73..ee14b241f7a 100755 --- a/codegen/bin/newcoin +++ b/codegen/bin/newcoin @@ -18,4 +18,4 @@ end coin_id = command_line_args[0] -generate_skeleton(coin_id, "") +generate_skeleton(coin_id, "full") diff --git a/codegen/bin/newcoin-mobile-tests b/codegen/bin/newcoin-mobile-tests new file mode 100755 index 00000000000..d6ea29a84c9 --- /dev/null +++ b/codegen/bin/newcoin-mobile-tests @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +# Sript for creating new skeleton files for new coin mobile tests. See also `newcoin` or `newevmchain`. +# It is considered to be used by codegen-v2 tool until Swift and Android tests generating supported. +# 1. Add relevant entry to registry.json (in order to minimize merge conflict, don't add at the very end) +# 2. Invoke this script with the id of the coin, e.g.: codegen/bin/newcoin-mobile-tests ethereum + +require 'fileutils' + +CurrentDir = File.dirname(__FILE__) +$LOAD_PATH.unshift(File.join(CurrentDir, '..', 'lib')) +require 'coin_skeleton_gen' + +command_line_args = ARGV +if command_line_args.length < 1 + puts "Usage: newcoin-mobile-tests " + return +end + +coin_id = command_line_args[0] + +generate_skeleton(coin_id, "mobile-tests") diff --git a/codegen/lib/coin_skeleton_gen.rb b/codegen/lib/coin_skeleton_gen.rb index 29335e4fdb0..b0bcbe85e80 100755 --- a/codegen/lib/coin_skeleton_gen.rb +++ b/codegen/lib/coin_skeleton_gen.rb @@ -90,6 +90,37 @@ def self.insert_target_line(target_file, target_line, original_line) return true end +def generate_blockchain_files(coin) + name = format_name(coin) + + generate_file("newcoin/Address.h.erb", "src/#{name}", "Address.h", coin) + generate_file("newcoin/Address.cpp.erb", "src/#{name}", "Address.cpp", coin) + generate_file("newcoin/Entry.h.erb", "src/#{name}", "Entry.h", coin) + generate_file("newcoin/Entry.cpp.erb", "src/#{name}", "Entry.cpp", coin) + generate_file("newcoin/Proto.erb", "src/proto", "#{name}.proto", coin) + generate_file("newcoin/Signer.h.erb", "src/#{name}", "Signer.h", coin) + generate_file("newcoin/Signer.cpp.erb", "src/#{name}", "Signer.cpp", coin) + + generate_file("newcoin/AddressTests.cpp.erb", "tests/chains/#{name}", "AddressTests.cpp", coin) + generate_file("newcoin/SignerTests.cpp.erb", "tests/chains/#{name}", "SignerTests.cpp", coin) + generate_file("newcoin/TransactionCompilerTests.cpp.erb", "tests/chains/#{name}", "TransactionCompilerTests.cpp", coin) + generate_file("newcoin/TWAddressTests.cpp.erb", "tests/chains/#{name}", "TWAnyAddressTests.cpp", coin) + generate_file("newcoin/TWSignerTests.cpp.erb", "tests/chains/#{name}", "TWAnySignerTests.cpp", coin) +end + +def generate_mobile_tests(coin) + name = format_name(coin) + + generate_file("newcoin/AddressTests.kt.erb", "android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/#{format_name_lowercase(coin)}", "Test#{name}Address.kt", coin) + generate_file("newcoin/SignerTests.kt.erb", "android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/#{format_name_lowercase(coin)}", "Test#{name}Signer.kt", coin) + generate_file("newcoin/Tests.swift.erb", "swift/Tests/Blockchains", "#{name}Tests.swift", coin) +end + +def generate_coin_type_tests(coin) + coin_test_gen = CoinTestGen.new() + coin_test_gen.generate_coin_test_file(coin, 'TWCoinTypeTests.cpp.erb', true) +end + def generate_skeleton(coin_id, mode) puts "New coin template for coin '#{coin_id}' #{mode} requested" @@ -103,8 +134,6 @@ def generate_skeleton(coin_id, mode) @coins = coins - coin_test_gen = CoinTestGen.new() - # Find coin in list of coins, by Id coinSelect = coins.select {|c| c['id'] == coin_id} if coinSelect.length() == 0 @@ -112,32 +141,19 @@ def generate_skeleton(coin_id, mode) return end coin = coinSelect.first - name = format_name(coin) - - insert_coin_type(coin, mode) - if (mode != "evm") + if (mode == "full") + insert_coin_type(coin, mode) insert_coin_entry(coin) - - generate_file("newcoin/Address.h.erb", "src/#{name}", "Address.h", coin) - generate_file("newcoin/Address.cpp.erb", "src/#{name}", "Address.cpp", coin) - generate_file("newcoin/Entry.h.erb", "src/#{name}", "Entry.h", coin) - generate_file("newcoin/Entry.cpp.erb", "src/#{name}", "Entry.cpp", coin) - generate_file("newcoin/Proto.erb", "src/proto", "#{name}.proto", coin) - generate_file("newcoin/Signer.h.erb", "src/#{name}", "Signer.h", coin) - generate_file("newcoin/Signer.cpp.erb", "src/#{name}", "Signer.cpp", coin) - - generate_file("newcoin/AddressTests.cpp.erb", "tests/chains/#{name}", "AddressTests.cpp", coin) - generate_file("newcoin/SignerTests.cpp.erb", "tests/chains/#{name}", "SignerTests.cpp", coin) - generate_file("newcoin/TransactionCompilerTests.cpp.erb", "tests/chains/#{name}", "TransactionCompilerTests.cpp", coin) - generate_file("newcoin/TWAddressTests.cpp.erb", "tests/chains/#{name}", "TWAnyAddressTests.cpp", coin) - generate_file("newcoin/TWSignerTests.cpp.erb", "tests/chains/#{name}", "TWAnySignerTests.cpp", coin) - generate_file("newcoin/AddressTests.kt.erb", "android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/#{format_name_lowercase(coin)}", "Test#{name}Address.kt", coin) - generate_file("newcoin/SignerTests.kt.erb", "android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/#{format_name_lowercase(coin)}", "Test#{name}Signer.kt", coin) - generate_file("newcoin/Tests.swift.erb", "swift/Tests/Blockchains", "#{name}Tests.swift", coin) + generate_blockchain_files(coin) + generate_mobile_tests(coin) + generate_coin_type_tests(coin) + elsif (mode == "evm") + insert_coin_type(coin, mode) + generate_coin_type_tests(coin) + elsif (mode == "mobile-tests") + generate_mobile_tests(coin) end - coin_test_gen.generate_coin_test_file(coin, 'TWCoinTypeTests.cpp.erb', true) - puts "please tools/generate-files to generate Swift/Java/Protobuf files" end diff --git a/codegen/lib/templates/CoinInfoData.cpp.erb b/codegen/lib/templates/CoinInfoData.cpp.erb index 4803af6dff6..d2bbb5d6b71 100644 --- a/codegen/lib/templates/CoinInfoData.cpp.erb +++ b/codegen/lib/templates/CoinInfoData.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/TWCoinTypeTests.cpp.erb b/codegen/lib/templates/TWCoinTypeTests.cpp.erb index 3784f589eb4..bb8cbdadb79 100644 --- a/codegen/lib/templates/TWCoinTypeTests.cpp.erb +++ b/codegen/lib/templates/TWCoinTypeTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here MAY BE LOST. // Generated one-time (codegen/bin/cointests) diff --git a/codegen/lib/templates/TWDerivation.h.erb b/codegen/lib/templates/TWDerivation.h.erb index 30ba65483da..e2c9a53a494 100644 --- a/codegen/lib/templates/TWDerivation.h.erb +++ b/codegen/lib/templates/TWDerivation.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST. // @@ -20,14 +18,12 @@ enum TWDerivation { TWDerivationCustom = 1, // custom, for any coin <% enum_count += 1 -%> <% coins.each do |coin| -%> -<% if coin['derivation'].count > 1 -%> <% coin['derivation'].each_with_index do |deriv, index| -%> <% if index > 0 or !deriv['name'].nil? -%> <%= derivation_enum_name(deriv, coin) %> = <% enum_count += 1 -%><%= enum_count %>, <% end -%> <% end -%> <% end -%> -<% end -%> }; TW_EXTERN_C_END diff --git a/codegen/lib/templates/TWEthereumChainID.h.erb b/codegen/lib/templates/TWEthereumChainID.h.erb index 7d3657b7a8c..3e6385e1b77 100644 --- a/codegen/lib/templates/TWEthereumChainID.h.erb +++ b/codegen/lib/templates/TWEthereumChainID.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/copyright_header.erb b/codegen/lib/templates/copyright_header.erb index e8de6a21997..39239c27440 100644 --- a/codegen/lib/templates/copyright_header.erb +++ b/codegen/lib/templates/copyright_header.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/hrp.cpp.erb b/codegen/lib/templates/hrp.cpp.erb index ecca3779ced..c3705bd5a9f 100644 --- a/codegen/lib/templates/hrp.cpp.erb +++ b/codegen/lib/templates/hrp.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/hrp.h.erb b/codegen/lib/templates/hrp.h.erb index a346bf8333c..4691fbafc5b 100644 --- a/codegen/lib/templates/hrp.h.erb +++ b/codegen/lib/templates/hrp.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/java/header.erb b/codegen/lib/templates/java/header.erb index 13ea5d56c17..3c5da1f81ed 100644 --- a/codegen/lib/templates/java/header.erb +++ b/codegen/lib/templates/java/header.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. // // This is a GENERATED FILE, changes made here WILL BE LOST. // diff --git a/codegen/lib/templates/newcoin/Address.cpp.erb b/codegen/lib/templates/newcoin/Address.cpp.erb index 88de42de169..ea1c354db0e 100644 --- a/codegen/lib/templates/newcoin/Address.cpp.erb +++ b/codegen/lib/templates/newcoin/Address.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "Address.h" diff --git a/codegen/lib/templates/newcoin/Address.h.erb b/codegen/lib/templates/newcoin/Address.h.erb index 302a491a0aa..7fe392b92df 100644 --- a/codegen/lib/templates/newcoin/Address.h.erb +++ b/codegen/lib/templates/newcoin/Address.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/codegen/lib/templates/newcoin/AddressTests.cpp.erb b/codegen/lib/templates/newcoin/AddressTests.cpp.erb index 4c20321c6fd..27c4c09ab89 100644 --- a/codegen/lib/templates/newcoin/AddressTests.cpp.erb +++ b/codegen/lib/templates/newcoin/AddressTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "HexCoding.h" #include "<%= format_name(coin) %>/Address.h" diff --git a/codegen/lib/templates/newcoin/AddressTests.kt.erb b/codegen/lib/templates/newcoin/AddressTests.kt.erb index 4185afbb9dd..20bbd774cc9 100644 --- a/codegen/lib/templates/newcoin/AddressTests.kt.erb +++ b/codegen/lib/templates/newcoin/AddressTests.kt.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.<%= format_name_lowercase(coin) %> diff --git a/codegen/lib/templates/newcoin/Entry.cpp.erb b/codegen/lib/templates/newcoin/Entry.cpp.erb index 5e4c4aaf25d..793f5bf8f3f 100644 --- a/codegen/lib/templates/newcoin/Entry.cpp.erb +++ b/codegen/lib/templates/newcoin/Entry.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "Entry.h" diff --git a/codegen/lib/templates/newcoin/Entry.h.erb b/codegen/lib/templates/newcoin/Entry.h.erb index 29ba0783596..32b48e0e42c 100644 --- a/codegen/lib/templates/newcoin/Entry.h.erb +++ b/codegen/lib/templates/newcoin/Entry.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/codegen/lib/templates/newcoin/Proto.erb b/codegen/lib/templates/newcoin/Proto.erb index 9609e2d2089..bbd242d66be 100644 --- a/codegen/lib/templates/newcoin/Proto.erb +++ b/codegen/lib/templates/newcoin/Proto.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. syntax = "proto3"; diff --git a/codegen/lib/templates/newcoin/Signer.cpp.erb b/codegen/lib/templates/newcoin/Signer.cpp.erb index fd81ac314e5..aeedceda863 100644 --- a/codegen/lib/templates/newcoin/Signer.cpp.erb +++ b/codegen/lib/templates/newcoin/Signer.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "Signer.h" #include "Address.h" diff --git a/codegen/lib/templates/newcoin/Signer.h.erb b/codegen/lib/templates/newcoin/Signer.h.erb index 7d660796723..f77982f3ac3 100644 --- a/codegen/lib/templates/newcoin/Signer.h.erb +++ b/codegen/lib/templates/newcoin/Signer.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/codegen/lib/templates/newcoin/SignerTests.cpp.erb b/codegen/lib/templates/newcoin/SignerTests.cpp.erb index 26150a8face..d88ad46994f 100644 --- a/codegen/lib/templates/newcoin/SignerTests.cpp.erb +++ b/codegen/lib/templates/newcoin/SignerTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "<%= format_name(coin) %>/Signer.h" #include "<%= format_name(coin) %>/Address.h" diff --git a/codegen/lib/templates/newcoin/SignerTests.kt.erb b/codegen/lib/templates/newcoin/SignerTests.kt.erb index 5bf1f4ed8bb..4add9add89f 100644 --- a/codegen/lib/templates/newcoin/SignerTests.kt.erb +++ b/codegen/lib/templates/newcoin/SignerTests.kt.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core.app.blockchains.<%= format_name_lowercase(coin) %> diff --git a/codegen/lib/templates/newcoin/TWAddressTests.cpp.erb b/codegen/lib/templates/newcoin/TWAddressTests.cpp.erb index 9ad574fec94..91a0cbf0729 100644 --- a/codegen/lib/templates/newcoin/TWAddressTests.cpp.erb +++ b/codegen/lib/templates/newcoin/TWAddressTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include "HexCoding.h" diff --git a/codegen/lib/templates/newcoin/TWSignerTests.cpp.erb b/codegen/lib/templates/newcoin/TWSignerTests.cpp.erb index 456b6f08b8d..8e8193db07c 100644 --- a/codegen/lib/templates/newcoin/TWSignerTests.cpp.erb +++ b/codegen/lib/templates/newcoin/TWSignerTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include "HexCoding.h" diff --git a/codegen/lib/templates/newcoin/Tests.swift.erb b/codegen/lib/templates/newcoin/Tests.swift.erb index 28c2ef8339e..e28d0ec1bc2 100644 --- a/codegen/lib/templates/newcoin/Tests.swift.erb +++ b/codegen/lib/templates/newcoin/Tests.swift.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. import WalletCore import XCTest diff --git a/codegen/lib/templates/newcoin/TransactionCompilerTests.cpp.erb b/codegen/lib/templates/newcoin/TransactionCompilerTests.cpp.erb index 8150081c49e..c996dd2c6ab 100644 --- a/codegen/lib/templates/newcoin/TransactionCompilerTests.cpp.erb +++ b/codegen/lib/templates/newcoin/TransactionCompilerTests.cpp.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include "<%= format_name(coin) %>/Signer.h" #include "<%= format_name(coin) %>/Address.h" diff --git a/codegen/lib/templates/swift/TrustWalletCore.h.erb b/codegen/lib/templates/swift/TrustWalletCore.h.erb index f1246a2a970..5e2ac88537a 100644 --- a/codegen/lib/templates/swift/TrustWalletCore.h.erb +++ b/codegen/lib/templates/swift/TrustWalletCore.h.erb @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #import diff --git a/docs/registry-fields.md b/docs/registry-fields.md index e1c7098e332..ff6b0e539e6 100644 --- a/docs/registry-fields.md +++ b/docs/registry-fields.md @@ -189,7 +189,7 @@ Link to the default implementation of the node or RPC gateway that can be used b Optional URL to an available public RPC service. **`info/documentation`** -Main porject documentation site/subsite. +Main project documentation site/subsite. **`deprecated`** If set to `true`, the project is considered deprecated: its info is kept here, but it will not be supported. diff --git a/docs/registry.md b/docs/registry.md index d1a15c32038..7606e14a45b 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -31,11 +31,13 @@ This list is generated from [./registry.json](../registry.json) | 148 | Stellar | XLM | | | | 156 | Bitcoin Gold | BTG | | | | 165 | Nano | XNO | | | +| 169 | Manta Pacific | ETH | | | | 175 | Ravencoin | RVN | | | | 178 | POA Network | POA | | | | 194 | EOS | EOS | | | | 195 | Tron | TRX | | | | 204 | OpBNB | BNB | | | +| 223 | Internet Computer | ICP | | | | 235 | FIO | FIO | | | | 242 | Nimiq | NIM | | | | 283 | Algorand | ALGO | | | @@ -70,10 +72,10 @@ This list is generated from [./registry.json](../registry.json) | 818 | VeChain | VET | | | | 820 | Callisto | CLO | | | | 888 | NEO | NEO | | | -| 889 | TomoChain | TOMO | | | +| 889 | Viction | VIC | | | | 899 | eCash | XEC | | | | 931 | THORChain | RUNE | | | -| 966 | Polygon | MATIC | | | +| 966 | Polygon | POL | | | | 996 | OKX Chain | OKT | | | | 999 | Bitcoin Diamond | BCD | | | | 1001 | ThunderCore | TT | | | @@ -82,11 +84,14 @@ This list is generated from [./registry.json](../registry.json) | 1030 | Conflux eSpace | CFX | | | | 1729 | Tezos | XTZ | | | | 1815 | Cardano | ADA | | | +| 1890 | Lightlink Phoenix | ETH | | | | 2301 | Qtum | QTUM | | | | 2718 | Nebulas | NAS | | | | 3030 | Hedera | HBAR | | | +| 4200 | Merlin | BTC | | | | 5000 | Mantle | MNT | | | | 5600 | BNB Greenfield | BNB | | | +| 6001 | BounceBit | BB | | | | 6060 | GoChain | GO | | | | 7332 | Zen EON | ZEN | | | | 8453 | Base | ETH | | | @@ -96,13 +101,15 @@ This list is generated from [./registry.json](../registry.json) | 19167 | Flux | FLUX | | | | 52752 | Celo | CELO | | | | 59144 | Linea | ETH | | | +| 81457 | Blast | ETH | | | | 105105 | Stratis | STRAX | | | -| 534353 | Scroll | ETH | | | +| 534352 | Scroll | ETH | | | +| 810180 | zkLink Nova Mainnet | ETH | | | | 5718350 | Wanchain | WAN | | | | 5741564 | Waves | WAVES | | | | 10000025 | Cronos Chain | CRO | | | | 10000060 | Native Injective | INJ | | | -| 10000070 | Optimism Ethereum | ETH | | | +| 10000070 | OP Mainnet | ETH | | | | 10000100 | Gnosis Chain | xDAI | | | | 10000118 | Osmosis | OSMO | | | | 10000145 | Smart Bitcoin Cash | BCH | | | @@ -121,8 +128,9 @@ This list is generated from [./registry.json](../registry.json) | 10002020 | Ronin | RON | | | | 10002222 | KavaEvm | KAVA | | | | 10004689 | IoTeX EVM | IOTX | | | +| 10007000 | NativeZetaChain | ZETA | | | | 10007700 | NativeCanto | CANTO | | | -| 10008217 | Klaytn | KLAY | | | +| 10008217 | Kaia | KLAY | | | | 10009000 | Avalanche C-Chain | AVAX | | | | 10009001 | Evmos | EVMOS | | | | 10042170 | Arbitrum Nova | ETH | | | @@ -138,7 +146,10 @@ This list is generated from [./registry.json](../registry.json) | 19000118 | Sei | SEI | | | | 20000118 | Stargaze | STARS | | | | 20000714 | BNB Smart Chain | BNB | | | +| 20007000 | Zeta EVM | ZETA | | | | 20009001 | Native Evmos | EVMOS | | | +| 21000118 | Celestia | TIA | | | +| 22000118 | dYdX | DYDX | | | | 30000118 | Juno | JUNO | | | | 30000714 | TBNB | BNB | | | | 40000118 | Stride | STRD | | | diff --git a/include/TrustWalletCore/TWAES.h b/include/TrustWalletCore/TWAES.h index 383804cedb1..e4a30c656d1 100644 --- a/include/TrustWalletCore/TWAES.h +++ b/include/TrustWalletCore/TWAES.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWAESPaddingMode.h b/include/TrustWalletCore/TWAESPaddingMode.h index b9e7c707950..da271a5b989 100644 --- a/include/TrustWalletCore/TWAESPaddingMode.h +++ b/include/TrustWalletCore/TWAESPaddingMode.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWAccount.h b/include/TrustWalletCore/TWAccount.h index adb6f8a97e3..a5cc0fc0e2f 100644 --- a/include/TrustWalletCore/TWAccount.h +++ b/include/TrustWalletCore/TWAccount.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWAnyAddress.h b/include/TrustWalletCore/TWAnyAddress.h index ce501489c22..49b604aa9fc 100644 --- a/include/TrustWalletCore/TWAnyAddress.h +++ b/include/TrustWalletCore/TWAnyAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -10,6 +8,7 @@ #include "TWCoinType.h" #include "TWData.h" #include "TWFilecoinAddressType.h" +#include "TWFiroAddressType.h" #include "TWString.h" TW_EXTERN_C_BEGIN @@ -124,6 +123,14 @@ struct TWAnyAddress* _Nonnull TWAnyAddressCreateSS58WithPublicKey(struct TWPubli TW_EXPORT_STATIC_METHOD struct TWAnyAddress* _Nonnull TWAnyAddressCreateWithPublicKeyFilecoinAddressType(struct TWPublicKey* _Nonnull publicKey, enum TWFilecoinAddressType filecoinAddressType); +/// Creates a Firo address from a public key and a given address type. +/// +/// \param publicKey derivates the address from the public key. +/// \param firoAddressType Firo address type. +/// \return TWAnyAddress pointer or nullptr if public key is invalid. +TW_EXPORT_STATIC_METHOD +struct TWAnyAddress* _Nonnull TWAnyAddressCreateWithPublicKeyFiroAddressType(struct TWPublicKey* _Nonnull publicKey, enum TWFiroAddressType firoAddressType); + /// Deletes an address. /// /// \param address address to delete. diff --git a/include/TrustWalletCore/TWAnySigner.h b/include/TrustWalletCore/TWAnySigner.h index 25cebe5c1a0..a50f1246618 100644 --- a/include/TrustWalletCore/TWAnySigner.h +++ b/include/TrustWalletCore/TWAnySigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once #include "TWBase.h" diff --git a/include/TrustWalletCore/TWAsnParser.h b/include/TrustWalletCore/TWAsnParser.h index 67d0c6588cd..b008a0578d8 100644 --- a/include/TrustWalletCore/TWAsnParser.h +++ b/include/TrustWalletCore/TWAsnParser.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBarz.h b/include/TrustWalletCore/TWBarz.h index 3bc08ed31b4..1454e219b5d 100644 --- a/include/TrustWalletCore/TWBarz.h +++ b/include/TrustWalletCore/TWBarz.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once #include "TWBase.h" @@ -41,4 +39,20 @@ TWData *_Nonnull TWBarzGetInitCode(TWString* _Nonnull factory, struct TWPublicKe /// \return Bytes of the formatted signature TW_EXPORT_STATIC_METHOD TWData *_Nonnull TWBarzGetFormattedSignature(TWData* _Nonnull signature, TWData* _Nonnull challenge, TWData* _Nonnull authenticatorData, TWString* _Nonnull clientDataJSON); + +/// Returns the final hash to be signed by Barz for signing messages & typed data +/// +/// \param msgHash Original msgHash +/// \param barzAddress The address of Barz wallet signing the message +/// \param chainId The chainId of the network the verification will happen +/// \return The final hash to be signed +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWBarzGetPrefixedMsgHash(TWData* _Nonnull msgHash, TWString* _Nonnull barzAddress, uint32_t chainId); + +/// Returns the encoded diamondCut function call for Barz contract upgrades +/// +/// \param input The serialized data of DiamondCutInput +/// \return The encoded bytes of diamondCut function call +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWBarzGetDiamondCutCode(TWData *_Nonnull input); TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWBase.h b/include/TrustWalletCore/TWBase.h index f0486654eb7..1e334d18679 100644 --- a/include/TrustWalletCore/TWBase.h +++ b/include/TrustWalletCore/TWBase.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #if !defined(TW_EXTERN_C_BEGIN) #if defined(__cplusplus) diff --git a/include/TrustWalletCore/TWBase32.h b/include/TrustWalletCore/TWBase32.h index 153d348f3a3..a8f09039ec8 100644 --- a/include/TrustWalletCore/TWBase32.h +++ b/include/TrustWalletCore/TWBase32.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBase58.h b/include/TrustWalletCore/TWBase58.h index 87a3cbe70fd..c59ddd2a2a0 100644 --- a/include/TrustWalletCore/TWBase58.h +++ b/include/TrustWalletCore/TWBase58.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBase64.h b/include/TrustWalletCore/TWBase64.h index 7ca0c37492f..5b3cff51a73 100644 --- a/include/TrustWalletCore/TWBase64.h +++ b/include/TrustWalletCore/TWBase64.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBitcoinAddress.h b/include/TrustWalletCore/TWBitcoinAddress.h index 1ef667bf6a4..5fd4d2172a0 100644 --- a/include/TrustWalletCore/TWBitcoinAddress.h +++ b/include/TrustWalletCore/TWBitcoinAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBitcoinFee.h b/include/TrustWalletCore/TWBitcoinFee.h deleted file mode 100644 index 33dc9c6c5ea..00000000000 --- a/include/TrustWalletCore/TWBitcoinFee.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright © 2017-2023 Trust Wallet. -// -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. - -#pragma once - -#include "TWData.h" -#include "TWString.h" - -TW_EXTERN_C_BEGIN - -TW_EXPORT_CLASS -struct TWBitcoinFee; - -/// Calculates the fee of any Bitcoin transaction. -/// -/// \param data: the signed transaction in its final form. -/// \param satVb: the satoshis per vbyte amount. The passed on string is interpreted as a unit64_t. -/// \returns the fee denominated in satoshis or nullptr if the transaction failed to be decoded. -TW_EXPORT_STATIC_METHOD -TWString* _Nullable TWBitcoinFeeCalculateFee(TWData* _Nonnull data, TWString* _Nonnull satVb); - -TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWBitcoinMessageSigner.h b/include/TrustWalletCore/TWBitcoinMessageSigner.h index 6b09918f3a6..4090ff4850f 100644 --- a/include/TrustWalletCore/TWBitcoinMessageSigner.h +++ b/include/TrustWalletCore/TWBitcoinMessageSigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBitcoinScript.h b/include/TrustWalletCore/TWBitcoinScript.h index 94ed5ce020b..b49e65ae1f8 100644 --- a/include/TrustWalletCore/TWBitcoinScript.h +++ b/include/TrustWalletCore/TWBitcoinScript.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -193,26 +191,6 @@ struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessPubkeyHash(TWDa TW_EXPORT_STATIC_METHOD struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessScriptHash(TWData* _Nonnull scriptHash); -/// Builds the Ordinals inscripton for BRC20 transfer. -/// -/// \param ticker ticker of the brc20 -/// \param amount uint64 transfer amount -/// \param pubkey Non-null pointer to a pubkey -/// \note Must be deleted with \TWBitcoinScriptDelete -/// \return A pointer to the built script -TW_EXPORT_STATIC_METHOD -TWData* _Nonnull TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* _Nonnull ticker, TWString* _Nonnull amount, TWData* _Nonnull pubkey); - -/// Builds the Ordinals inscripton for NFT construction. -/// -/// \param mimeType the MIME type of the payload -/// \param payload the payload to inscribe -/// \param pubkey Non-null pointer to a pubkey -/// \note Must be deleted with \TWBitcoinScriptDelete -/// \return A pointer to the built script -TW_EXPORT_STATIC_METHOD -TWData* _Nonnull TWBitcoinScriptBuildOrdinalNftInscription(TWString* _Nonnull mimeType, TWData* _Nonnull payload, TWData* _Nonnull pubkey); - /// Builds a appropriate lock script for the given address.. /// /// \param address Non-null pointer to an address diff --git a/include/TrustWalletCore/TWBitcoinSigHashType.h b/include/TrustWalletCore/TWBitcoinSigHashType.h index c71bde2d480..4d7e9c49a38 100644 --- a/include/TrustWalletCore/TWBitcoinSigHashType.h +++ b/include/TrustWalletCore/TWBitcoinSigHashType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index d715176ed7b..faf4c575c1b 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -64,6 +62,9 @@ enum TWBlockchain { TWBlockchainTheOpenNetwork = 49, TWBlockchainSui = 50, TWBlockchainGreenfield = 51, + TWBlockchainInternetComputer = 52, + TWBlockchainNativeEvmos = 53, // Cosmos + TWBlockchainNativeInjective = 54, // Cosmos }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCardano.h b/include/TrustWalletCore/TWCardano.h index 0892d616183..17bd9ebb61f 100644 --- a/include/TrustWalletCore/TWCardano.h +++ b/include/TrustWalletCore/TWCardano.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index ad78ddfa90d..388bd270334 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -72,7 +70,7 @@ enum TWCoinType { TWCoinTypeTheta = 500, TWCoinTypeThunderCore = 1001, TWCoinTypeNEO = 888, - TWCoinTypeTomoChain = 889, + TWCoinTypeViction = 889, TWCoinTypeTron = 195, TWCoinTypeVeChain = 818, TWCoinTypeViacoin = 14, @@ -127,7 +125,7 @@ enum TWCoinType { TWCoinTypeMoonriver = 10001285, TWCoinTypeMoonbeam = 10001284, TWCoinTypeKavaEvm = 10002222, - TWCoinTypeKlaytn = 10008217, + TWCoinTypeKaia = 10008217, TWCoinTypeMeter = 18000, TWCoinTypeOKXChain = 996, TWCoinTypeStratis = 105105, @@ -162,7 +160,7 @@ enum TWCoinType { TWCoinTypePersistence = 16000118, TWCoinTypeAkash = 17000118, TWCoinTypeNoble = 18000118, - TWCoinTypeScroll = 534353, + TWCoinTypeScroll = 534352, TWCoinTypeRootstock = 137, TWCoinTypeThetaFuel = 361, TWCoinTypeConfluxeSpace = 1030, @@ -177,6 +175,18 @@ enum TWCoinType { TWCoinTypeGreenfield = 5600, TWCoinTypeMantle = 5000, TWCoinTypeZenEON = 7332, + TWCoinTypeInternetComputer = 223, + TWCoinTypeTia = 21000118, + TWCoinTypeMantaPacific = 169, + TWCoinTypeNativeZetaChain = 10007000, + TWCoinTypeZetaEVM = 20007000, + TWCoinTypeDydx = 22000118, + TWCoinTypeMerlin = 4200, + TWCoinTypeLightlink = 1890, + TWCoinTypeBlast = 81457, + TWCoinTypeBounceBit = 6001, + TWCoinTypeZkLinkNova = 810180, + // end_of_tw_coin_type_marker_do_not_modify }; /// Returns the blockchain for a coin type. diff --git a/include/TrustWalletCore/TWCoinTypeConfiguration.h b/include/TrustWalletCore/TWCoinTypeConfiguration.h index c83db7753e0..ca71f4dc56e 100644 --- a/include/TrustWalletCore/TWCoinTypeConfiguration.h +++ b/include/TrustWalletCore/TWCoinTypeConfiguration.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWCryptoBox.h b/include/TrustWalletCore/TWCryptoBox.h new file mode 100644 index 00000000000..abd27ca8191 --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBox.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCryptoBoxPublicKey.h" +#include "TWCryptoBoxSecretKey.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// `crypto_box` encryption algorithms. +TW_EXPORT_STRUCT +struct TWCryptoBox; + +/// Encrypts message using `my_secret` and `other_pubkey`. +/// The output will have a randomly generated nonce prepended to it. +/// The output will be Overhead + 24 bytes longer than the original. +/// +/// \param mySecret *non-null* pointer to my secret key. +/// \param otherPubkey *non-null* pointer to other's public key. +/// \param message *non-null* pointer to the message to be encrypted. +/// \return *nullable* pointer to the encrypted message with randomly generated nonce prepended to it. +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWCryptoBoxEncryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull message); + +/// Decrypts box produced by `TWCryptoBoxEncryptEasy`. +/// We assume a 24-byte nonce is prepended to the encrypted text in box. +/// +/// \param mySecret *non-null* pointer to my secret key. +/// \param otherPubkey *non-null* pointer to other's public key. +/// \param encrypted *non-null* pointer to the encrypted message with nonce prepended to it. +/// \return *nullable* pointer to the decrypted message. +TW_EXPORT_STATIC_METHOD +TWData* _Nullable TWCryptoBoxDecryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull encrypted); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCryptoBoxPublicKey.h b/include/TrustWalletCore/TWCryptoBoxPublicKey.h new file mode 100644 index 00000000000..e46ea72feae --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBoxPublicKey.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Public key used in `crypto_box` cryptography. +TW_EXPORT_CLASS +struct TWCryptoBoxPublicKey; + +/// Determines if the given public key is valid or not. +/// +/// \param data *non-null* byte array. +/// \return true if the public key is valid, false otherwise. +TW_EXPORT_STATIC_METHOD +bool TWCryptoBoxPublicKeyIsValid(TWData* _Nonnull data); + +/// Create a `crypto_box` public key with the given block of data. +/// +/// \param data *non-null* byte array. Expected to have 32 bytes. +/// \note Should be deleted with \tw_crypto_box_public_key_delete. +/// \return Nullable pointer to Public Key. +TW_EXPORT_STATIC_METHOD +struct TWCryptoBoxPublicKey* _Nullable TWCryptoBoxPublicKeyCreateWithData(TWData* _Nonnull data); + +/// Delete the given public key. +/// +/// \param publicKey *non-null* pointer to public key. +TW_EXPORT_METHOD +void TWCryptoBoxPublicKeyDelete(struct TWCryptoBoxPublicKey* _Nonnull publicKey); + +/// Returns the raw data of the given public-key. +/// +/// \param publicKey *non-null* pointer to a public key. +/// \return C-compatible result with a C-compatible byte array. +TW_EXPORT_PROPERTY +TWData* _Nonnull TWCryptoBoxPublicKeyData(struct TWCryptoBoxPublicKey* _Nonnull publicKey); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCryptoBoxSecretKey.h b/include/TrustWalletCore/TWCryptoBoxSecretKey.h new file mode 100644 index 00000000000..f93ad92eb56 --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBoxSecretKey.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCryptoBoxPublicKey.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Secret key used in `crypto_box` cryptography. +TW_EXPORT_CLASS +struct TWCryptoBoxSecretKey; + +/// Determines if the given secret key is valid or not. +/// +/// \param data *non-null* byte array. +/// \return true if the secret key is valid, false otherwise. +TW_EXPORT_STATIC_METHOD +bool TWCryptoBoxSecretKeyIsValid(TWData* _Nonnull data); + +/// Create a random secret key. +/// +/// \note Should be deleted with \tw_crypto_box_secret_key_delete. +/// \return *non-null* pointer to Secret Key. +TW_EXPORT_STATIC_METHOD +struct TWCryptoBoxSecretKey* _Nonnull TWCryptoBoxSecretKeyCreate(); + +/// Create a `crypto_box` secret key with the given block of data. +/// +/// \param data *non-null* byte array. Expected to have 32 bytes. +/// \note Should be deleted with \tw_crypto_box_secret_key_delete. +/// \return Nullable pointer to Secret Key. +TW_EXPORT_STATIC_METHOD +struct TWCryptoBoxSecretKey* _Nullable TWCryptoBoxSecretKeyCreateWithData(TWData* _Nonnull data); + +/// Delete the given secret `key`. +/// +/// \param key *non-null* pointer to secret key. +TW_EXPORT_METHOD +void TWCryptoBoxSecretKeyDelete(struct TWCryptoBoxSecretKey* _Nonnull key); + +/// Returns the public key associated with the given `key`. +/// +/// \param key *non-null* pointer to the private key. +/// \return *non-null* pointer to the corresponding public key. +TW_EXPORT_METHOD +struct TWCryptoBoxPublicKey* _Nonnull TWCryptoBoxSecretKeyGetPublicKey(struct TWCryptoBoxSecretKey* _Nonnull key); + +/// Returns the raw data of the given secret-key. +/// +/// \param secretKey *non-null* pointer to a secret key. +/// \return C-compatible result with a C-compatible byte array. +TW_EXPORT_PROPERTY +TWData* _Nonnull TWCryptoBoxSecretKeyData(struct TWCryptoBoxSecretKey* _Nonnull secretKey); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCurve.h b/include/TrustWalletCore/TWCurve.h index f04256c62c2..3b7f2b003bd 100644 --- a/include/TrustWalletCore/TWCurve.h +++ b/include/TrustWalletCore/TWCurve.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWData.h b/include/TrustWalletCore/TWData.h index 8c498d81cc4..77dc7c625b6 100644 --- a/include/TrustWalletCore/TWData.h +++ b/include/TrustWalletCore/TWData.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWDataVector.h b/include/TrustWalletCore/TWDataVector.h index 3f950635aea..fbfae776ad2 100644 --- a/include/TrustWalletCore/TWDataVector.h +++ b/include/TrustWalletCore/TWDataVector.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWDerivationPath.h b/include/TrustWalletCore/TWDerivationPath.h index 0d116870e93..ccec3051138 100644 --- a/include/TrustWalletCore/TWDerivationPath.h +++ b/include/TrustWalletCore/TWDerivationPath.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWDerivationPathIndex.h b/include/TrustWalletCore/TWDerivationPathIndex.h index fe7d93ae2c0..a015f37b5f5 100644 --- a/include/TrustWalletCore/TWDerivationPathIndex.h +++ b/include/TrustWalletCore/TWDerivationPathIndex.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereum.h b/include/TrustWalletCore/TWEthereum.h index 20007e4aaff..9ff49f208b2 100644 --- a/include/TrustWalletCore/TWEthereum.h +++ b/include/TrustWalletCore/TWEthereum.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereumAbi.h b/include/TrustWalletCore/TWEthereumAbi.h index 33b976dc61a..f2d23a4dec5 100644 --- a/include/TrustWalletCore/TWEthereumAbi.h +++ b/include/TrustWalletCore/TWEthereumAbi.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereumAbiFunction.h b/include/TrustWalletCore/TWEthereumAbiFunction.h index d07199a2844..cb5fbb9407c 100644 --- a/include/TrustWalletCore/TWEthereumAbiFunction.h +++ b/include/TrustWalletCore/TWEthereumAbiFunction.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereumAbiValue.h b/include/TrustWalletCore/TWEthereumAbiValue.h index 0af2f1e22ad..fcbe14de839 100644 --- a/include/TrustWalletCore/TWEthereumAbiValue.h +++ b/include/TrustWalletCore/TWEthereumAbiValue.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereumMessageSigner.h b/include/TrustWalletCore/TWEthereumMessageSigner.h index cf21d8a8ddd..6d73c338f6c 100644 --- a/include/TrustWalletCore/TWEthereumMessageSigner.h +++ b/include/TrustWalletCore/TWEthereumMessageSigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWEthereumRlp.h b/include/TrustWalletCore/TWEthereumRlp.h index 1644c86e1e2..361ac305cbc 100644 --- a/include/TrustWalletCore/TWEthereumRlp.h +++ b/include/TrustWalletCore/TWEthereumRlp.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWFIOAccount.h b/include/TrustWalletCore/TWFIOAccount.h index a8abca53615..e11a65c4b3d 100644 --- a/include/TrustWalletCore/TWFIOAccount.h +++ b/include/TrustWalletCore/TWFIOAccount.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWFilecoinAddressConverter.h b/include/TrustWalletCore/TWFilecoinAddressConverter.h index aa5e37b8000..b6c3689984c 100644 --- a/include/TrustWalletCore/TWFilecoinAddressConverter.h +++ b/include/TrustWalletCore/TWFilecoinAddressConverter.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWFilecoinAddressType.h b/include/TrustWalletCore/TWFilecoinAddressType.h index 98f82450d3d..5bab60774c5 100644 --- a/include/TrustWalletCore/TWFilecoinAddressType.h +++ b/include/TrustWalletCore/TWFilecoinAddressType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWFiroAddressType.h b/include/TrustWalletCore/TWFiroAddressType.h new file mode 100644 index 00000000000..55fa3a84259 --- /dev/null +++ b/include/TrustWalletCore/TWFiroAddressType.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" + +TW_EXTERN_C_BEGIN + +/// Firo address type. +TW_EXPORT_ENUM(uint32_t) +enum TWFiroAddressType { + TWFiroAddressTypeDefault = 0, // default + TWFiroAddressTypeExchange = 1, +}; + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWGroestlcoinAddress.h b/include/TrustWalletCore/TWGroestlcoinAddress.h index e6512db8c0b..52114afd736 100644 --- a/include/TrustWalletCore/TWGroestlcoinAddress.h +++ b/include/TrustWalletCore/TWGroestlcoinAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWHDVersion.h b/include/TrustWalletCore/TWHDVersion.h index 6e726bef840..ceb7733e676 100644 --- a/include/TrustWalletCore/TWHDVersion.h +++ b/include/TrustWalletCore/TWHDVersion.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWHDWallet.h b/include/TrustWalletCore/TWHDWallet.h index e48e9bd7050..9e902a55587 100644 --- a/include/TrustWalletCore/TWHDWallet.h +++ b/include/TrustWalletCore/TWHDWallet.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWHash.h b/include/TrustWalletCore/TWHash.h index f8f4a9743e2..06024bb4e4f 100644 --- a/include/TrustWalletCore/TWHash.h +++ b/include/TrustWalletCore/TWHash.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWLiquidStaking.h b/include/TrustWalletCore/TWLiquidStaking.h index 4829ef3b594..a50f3e2709b 100644 --- a/include/TrustWalletCore/TWLiquidStaking.h +++ b/include/TrustWalletCore/TWLiquidStaking.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once #include "TWBase.h" diff --git a/include/TrustWalletCore/TWMnemonic.h b/include/TrustWalletCore/TWMnemonic.h index 6baa9b1113c..2cfba1dba70 100644 --- a/include/TrustWalletCore/TWMnemonic.h +++ b/include/TrustWalletCore/TWMnemonic.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWNEARAccount.h b/include/TrustWalletCore/TWNEARAccount.h index 8daa5365cf9..cd8fbb4b97a 100644 --- a/include/TrustWalletCore/TWNEARAccount.h +++ b/include/TrustWalletCore/TWNEARAccount.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWNervosAddress.h b/include/TrustWalletCore/TWNervosAddress.h index eedceb86f08..0254c2bf5cf 100644 --- a/include/TrustWalletCore/TWNervosAddress.h +++ b/include/TrustWalletCore/TWNervosAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPBKDF2.h b/include/TrustWalletCore/TWPBKDF2.h index b7070849dbf..86c6cca6801 100644 --- a/include/TrustWalletCore/TWPBKDF2.h +++ b/include/TrustWalletCore/TWPBKDF2.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPrivateKey.h b/include/TrustWalletCore/TWPrivateKey.h index 2e32b19f532..5fdfc61bcde 100644 --- a/include/TrustWalletCore/TWPrivateKey.h +++ b/include/TrustWalletCore/TWPrivateKey.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPrivateKeyType.h b/include/TrustWalletCore/TWPrivateKeyType.h index a51aebae791..ee9255e0893 100644 --- a/include/TrustWalletCore/TWPrivateKeyType.h +++ b/include/TrustWalletCore/TWPrivateKeyType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPublicKey.h b/include/TrustWalletCore/TWPublicKey.h index 326f694d073..7187ac564af 100644 --- a/include/TrustWalletCore/TWPublicKey.h +++ b/include/TrustWalletCore/TWPublicKey.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPublicKeyType.h b/include/TrustWalletCore/TWPublicKeyType.h index 894292ad5f4..f175fc8c471 100644 --- a/include/TrustWalletCore/TWPublicKeyType.h +++ b/include/TrustWalletCore/TWPublicKeyType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWPurpose.h b/include/TrustWalletCore/TWPurpose.h index 142ed4f10c9..8cfa9cd91ae 100644 --- a/include/TrustWalletCore/TWPurpose.h +++ b/include/TrustWalletCore/TWPurpose.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWRippleXAddress.h b/include/TrustWalletCore/TWRippleXAddress.h index ff412711003..3c9256d2613 100644 --- a/include/TrustWalletCore/TWRippleXAddress.h +++ b/include/TrustWalletCore/TWRippleXAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWSS58AddressType.h b/include/TrustWalletCore/TWSS58AddressType.h index 846c9d8c60f..9f9010c670e 100644 --- a/include/TrustWalletCore/TWSS58AddressType.h +++ b/include/TrustWalletCore/TWSS58AddressType.h @@ -1,9 +1,7 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWSegwitAddress.h b/include/TrustWalletCore/TWSegwitAddress.h index 29c81571132..452b4bc2ed0 100644 --- a/include/TrustWalletCore/TWSegwitAddress.h +++ b/include/TrustWalletCore/TWSegwitAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWSolanaAddress.h b/include/TrustWalletCore/TWSolanaAddress.h index 9e9c4ede571..3a21460e759 100644 --- a/include/TrustWalletCore/TWSolanaAddress.h +++ b/include/TrustWalletCore/TWSolanaAddress.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -37,6 +35,14 @@ void TWSolanaAddressDelete(struct TWSolanaAddress* _Nonnull address); TW_EXPORT_METHOD TWString* _Nullable TWSolanaAddressDefaultTokenAddress(struct TWSolanaAddress* _Nonnull address, TWString* _Nonnull tokenMintAddress); +/// Derive token 2022 address for token +/// +/// \param address Non-null pointer to a Solana Address +/// \param tokenMintAddress Non-null pointer to a token mint address as a string +/// \return Null pointer if the token 2022 address for a token is not found, valid pointer otherwise +TW_EXPORT_METHOD +TWString* _Nullable TWSolanaAddressToken2022Address(struct TWSolanaAddress* _Nonnull address, TWString* _Nonnull tokenMintAddress); + /// Returns the address string representation. /// /// \param address Non-null pointer to a Solana Address diff --git a/include/TrustWalletCore/TWSolanaTransaction.h b/include/TrustWalletCore/TWSolanaTransaction.h new file mode 100644 index 00000000000..681313ff248 --- /dev/null +++ b/include/TrustWalletCore/TWSolanaTransaction.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWDataVector.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +TW_EXPORT_STRUCT +struct TWSolanaTransaction; + +/// Decode Solana transaction, update the recent blockhash and re-sign the transaction. +/// +/// # Warning +/// +/// This is a temporary solution. It will be removed when `Solana.proto` supports +/// direct transaction signing. +/// +/// \param encodedTx base64 encoded Solana transaction. +/// \param recentBlockhash base58 encoded recent blockhash. +/// \param privateKeys list of private keys that should be used to re-sign the transaction. +/// \return serialized `Solana::Proto::SigningOutput`. +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWSolanaTransactionUpdateBlockhashAndSign(TWString *_Nonnull encodedTx, + TWString *_Nonnull recentBlockhash, + const struct TWDataVector *_Nonnull privateKeys); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWStarkExMessageSigner.h b/include/TrustWalletCore/TWStarkExMessageSigner.h index 63e0d2eb7cc..d5299f4f026 100644 --- a/include/TrustWalletCore/TWStarkExMessageSigner.h +++ b/include/TrustWalletCore/TWStarkExMessageSigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStarkWare.h b/include/TrustWalletCore/TWStarkWare.h index 1f767ea6d71..1ff02199f71 100644 --- a/include/TrustWalletCore/TWStarkWare.h +++ b/include/TrustWalletCore/TWStarkWare.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStellarMemoType.h b/include/TrustWalletCore/TWStellarMemoType.h index 812edbf4fda..8f6b66eec8f 100644 --- a/include/TrustWalletCore/TWStellarMemoType.h +++ b/include/TrustWalletCore/TWStellarMemoType.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStellarPassphrase.h b/include/TrustWalletCore/TWStellarPassphrase.h index d56187244eb..307afb84865 100644 --- a/include/TrustWalletCore/TWStellarPassphrase.h +++ b/include/TrustWalletCore/TWStellarPassphrase.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStellarVersionByte.h b/include/TrustWalletCore/TWStellarVersionByte.h index f2df0f65215..94f125ab94c 100644 --- a/include/TrustWalletCore/TWStellarVersionByte.h +++ b/include/TrustWalletCore/TWStellarVersionByte.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStoredKey.h b/include/TrustWalletCore/TWStoredKey.h index 01efa96e691..58a07e521c0 100644 --- a/include/TrustWalletCore/TWStoredKey.h +++ b/include/TrustWalletCore/TWStoredKey.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -297,6 +295,16 @@ TWData* _Nullable TWStoredKeyExportJSON(struct TWStoredKey* _Nonnull key); TW_EXPORT_METHOD bool TWStoredKeyFixAddresses(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password); +/// Re-derives address for the account(s) associated with the given coin. +/// This method can be used if address format has been changed. +/// In case of multiple accounts, all of them will be updated. +/// +/// \param key Non-null pointer to a stored key +/// \param coin Account(s) coin type to be updated +/// \return `false` if there are no accounts associated with the given coin, true otherwise +TW_EXPORT_METHOD +bool TWStoredKeyUpdateAddress(struct TWStoredKey* _Nonnull key, enum TWCoinType coin); + /// Retrieve stored key encoding parameters, as JSON string. /// /// \param key Non-null pointer to a stored key diff --git a/include/TrustWalletCore/TWStoredKeyEncryption.h b/include/TrustWalletCore/TWStoredKeyEncryption.h index 856b407157b..ccb0d7cfcac 100644 --- a/include/TrustWalletCore/TWStoredKeyEncryption.h +++ b/include/TrustWalletCore/TWStoredKeyEncryption.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWStoredKeyEncryptionLevel.h b/include/TrustWalletCore/TWStoredKeyEncryptionLevel.h index 071df20c97f..6a7ceb7bcbc 100644 --- a/include/TrustWalletCore/TWStoredKeyEncryptionLevel.h +++ b/include/TrustWalletCore/TWStoredKeyEncryptionLevel.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWString.h b/include/TrustWalletCore/TWString.h index 336b88922f4..9cfc8e77bfc 100644 --- a/include/TrustWalletCore/TWString.h +++ b/include/TrustWalletCore/TWString.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWTHORChainSwap.h b/include/TrustWalletCore/TWTHORChainSwap.h index bcd35a0911e..11844425bbe 100644 --- a/include/TrustWalletCore/TWTHORChainSwap.h +++ b/include/TrustWalletCore/TWTHORChainSwap.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once #include "TWBase.h" diff --git a/include/TrustWalletCore/TWTONAddressConverter.h b/include/TrustWalletCore/TWTONAddressConverter.h new file mode 100644 index 00000000000..39bb4dfed7e --- /dev/null +++ b/include/TrustWalletCore/TWTONAddressConverter.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// TON address operations. +TW_EXPORT_CLASS +struct TWTONAddressConverter; + +/// Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. +/// The function is mostly used to request a Jetton user address via `get_wallet_address` RPC. +/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user +/// +/// \param address Address to be converted into a Bag Of Cells (BoC). +/// \return Pointer to a base64 encoded Bag Of Cells (BoC). Null if invalid address provided. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONAddressConverterToBoc(TWString *_Nonnull address); + +/// Parses a TON address from a Bag of Cells (BoC) with a single root Cell. +/// The function is mostly used to parse a Jetton user address received on `get_wallet_address` RPC. +/// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user +/// +/// \param boc Base64 encoded Bag Of Cells (BoC). +/// \return Pointer to a Jetton address. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONAddressConverterFromBoc(TWString *_Nonnull boc); + +/// Converts any TON address format to user friendly with the given parameters. +/// +/// \param address raw or user-friendly address to be converted. +/// \param bounceable whether the result address should be bounceable. +/// \param testnet whether the result address should be testnet. +/// \return user-friendly address str. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONAddressConverterToUserFriendly(TWString *_Nonnull address, bool bounceable, bool testnet); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWTONMessageSigner.h b/include/TrustWalletCore/TWTONMessageSigner.h new file mode 100644 index 00000000000..c9c73876ee3 --- /dev/null +++ b/include/TrustWalletCore/TWTONMessageSigner.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWPrivateKey.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// TON message signing. +TW_EXPORT_CLASS +struct TWTONMessageSigner; + +/// Signs an arbitrary message to prove ownership of an address for off-chain services. +/// https://github.com/ton-foundation/specs/blob/main/specs/wtf-0002.md +/// +/// \param privateKey: the private key used for signing +/// \param message: A custom message which is input to the signing. +/// \returns the signature, Hex-encoded. On invalid input null is returned. Returned object needs to be deleted after use. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONMessageSignerSignMessage(struct TWPrivateKey *_Nonnull privateKey, TWString* _Nonnull message); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWTONWallet.h b/include/TrustWalletCore/TWTONWallet.h new file mode 100644 index 00000000000..098702faa6a --- /dev/null +++ b/include/TrustWalletCore/TWTONWallet.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWPublicKey.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// TON wallet operations. +TW_EXPORT_CLASS +struct TWTONWallet; + +/// Constructs a TON Wallet V4R2 stateInit encoded as BoC (BagOfCells) for the given `public_key`. +/// +/// \param publicKey wallet's public key. +/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0). +/// \param walletId wallet's ID allows to create multiple wallets for the same private key. +/// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONWalletBuildV4R2StateInit(struct TWPublicKey *_Nonnull publicKey, int32_t workchain, int32_t walletId); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWTezosMessageSigner.h b/include/TrustWalletCore/TWTezosMessageSigner.h index bcb99bf8a85..be1f586f448 100644 --- a/include/TrustWalletCore/TWTezosMessageSigner.h +++ b/include/TrustWalletCore/TWTezosMessageSigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWTransactionCompiler.h b/include/TrustWalletCore/TWTransactionCompiler.h index 5ec06883c40..67dfc4f0124 100644 --- a/include/TrustWalletCore/TWTransactionCompiler.h +++ b/include/TrustWalletCore/TWTransactionCompiler.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once @@ -18,23 +16,6 @@ TW_EXTERN_C_BEGIN TW_EXPORT_STRUCT struct TWTransactionCompiler; -/// Builds a coin-specific SigningInput (proto object) from a simple transaction. -/// -/// \deprecated `TWTransactionCompilerBuildInput` will be removed soon. -/// \param coin coin type. -/// \param from sender of the transaction. -/// \param to receiver of the transaction. -/// \param amount transaction amount in string -/// \param asset optional asset name, like "BNB" -/// \param memo optional memo -/// \param chainId optional chainId to override default -/// \return serialized data of the SigningInput proto object. -TW_EXPORT_STATIC_METHOD -TWData* _Nonnull TWTransactionCompilerBuildInput(enum TWCoinType coinType, TWString* _Nonnull from, - TWString* _Nonnull to, TWString* _Nonnull amount, - TWString* _Nonnull asset, TWString* _Nonnull memo, - TWString* _Nonnull chainId); - /// Obtains pre-signing hashes of a transaction. /// /// We provide a default `PreSigningOutput` in TransactionCompiler.proto. diff --git a/include/TrustWalletCore/TWTransactionDecoder.h b/include/TrustWalletCore/TWTransactionDecoder.h new file mode 100644 index 00000000000..2d1a22cffe4 --- /dev/null +++ b/include/TrustWalletCore/TWTransactionDecoder.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCoinType.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +TW_EXPORT_STRUCT +struct TWTransactionDecoder; + +/// Decodes a transaction from a binary representation. +/// +/// \param coin coin type. +/// \param encodedTx encoded transaction data. +/// \return serialized protobuf message specific for the given coin. +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWTransactionDecoderDecode(enum TWCoinType coinType, TWData *_Nonnull encodedTx); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWTransactionUtil.h b/include/TrustWalletCore/TWTransactionUtil.h new file mode 100644 index 00000000000..55b2a811428 --- /dev/null +++ b/include/TrustWalletCore/TWTransactionUtil.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCoinType.h" +#include "TWData.h" + +TW_EXTERN_C_BEGIN + +TW_EXPORT_STRUCT +struct TWTransactionUtil; + +/// Calculate the TX hash of a transaction. +/// +/// \param coin coin type. +/// \param encodedTx encoded transaction data. +/// \return The TX hash of a transaction, If the input is invalid or the chain is unsupported, null is returned. +TW_EXPORT_STATIC_METHOD +TWString* _Nullable TWTransactionUtilCalcTxHash(enum TWCoinType coinType, TWString* _Nonnull encodedTx); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWTronMessageSigner.h b/include/TrustWalletCore/TWTronMessageSigner.h index 56f326d49a4..d20baba14c1 100644 --- a/include/TrustWalletCore/TWTronMessageSigner.h +++ b/include/TrustWalletCore/TWTronMessageSigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/include/TrustWalletCore/TWWalletConnectRequest.h b/include/TrustWalletCore/TWWalletConnectRequest.h new file mode 100644 index 00000000000..41ba895457c --- /dev/null +++ b/include/TrustWalletCore/TWWalletConnectRequest.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCoinType.h" +#include "TWData.h" + +TW_EXTERN_C_BEGIN + +/// Represents a WalletConnect signing request. +TW_EXPORT_CLASS +struct TWWalletConnectRequest; + +/// Parses the WalletConnect signing request as a `SigningInput`. +/// +/// \param coin The given coin type to plan the transaction for. +/// \param input The serialized data of a `WalletConnect::Proto::ParseRequestInput` proto object. +/// \return The serialized data of `WalletConnect::Proto::ParseRequestOutput` proto object. +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWWalletConnectRequestParse(enum TWCoinType coin, TWData* _Nonnull input); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWWebAuthn.h b/include/TrustWalletCore/TWWebAuthn.h index 4d988daaecf..c3f24b27a2b 100644 --- a/include/TrustWalletCore/TWWebAuthn.h +++ b/include/TrustWalletCore/TWWebAuthn.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once #include "TWBase.h" diff --git a/jni/android/AnySigner.c b/jni/android/AnySigner.c index d9ac054adc4..fd1704562f5 100644 --- a/jni/android/AnySigner.c +++ b/jni/android/AnySigner.c @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include diff --git a/jni/android/AnySigner.h b/jni/android/AnySigner.h index b51323a6554..566d446a7ee 100644 --- a/jni/android/AnySigner.h +++ b/jni/android/AnySigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #ifndef JNI_TW_ANYSIGNER_H #define JNI_TW_ANYSIGNER_H diff --git a/jni/cpp/Random.cpp b/jni/cpp/Random.cpp index 8e583ae5a92..216c61d46cc 100644 --- a/jni/cpp/Random.cpp +++ b/jni/cpp/Random.cpp @@ -1,12 +1,11 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include #include +#include static JavaVM* cachedJVM; @@ -27,31 +26,42 @@ uint32_t random32() { } void random_buffer(uint8_t *buf, size_t len) { - JNIEnv *env; - + if (cachedJVM) + { + JNIEnv *env; + #if defined(__ANDROID__) || defined(ANDROID) cachedJVM->AttachCurrentThread(&env, nullptr); #else cachedJVM->AttachCurrentThread((void **) &env, nullptr); #endif - // SecureRandom random = new SecureRandom(); - jclass secureRandomClass = env->FindClass("java/security/SecureRandom"); - jmethodID constructor = env->GetMethodID(secureRandomClass, "", "()V"); - jobject random = env->NewObject(secureRandomClass, constructor); + // SecureRandom random = new SecureRandom(); + jclass secureRandomClass = env->FindClass("java/security/SecureRandom"); + jmethodID constructor = env->GetMethodID(secureRandomClass, "", "()V"); + jobject random = env->NewObject(secureRandomClass, constructor); - //byte array[] = new byte[len]; - jbyteArray array = env->NewByteArray(static_cast(len)); + //byte array[] = new byte[len]; + jbyteArray array = env->NewByteArray(static_cast(len)); - //random.nextBytes(bytes); - jmethodID nextBytes = env->GetMethodID(secureRandomClass, "nextBytes", "([B)V"); - env->CallVoidMethod(random, nextBytes, array); + //random.nextBytes(bytes); + jmethodID nextBytes = env->GetMethodID(secureRandomClass, "nextBytes", "([B)V"); + env->CallVoidMethod(random, nextBytes, array); - jbyte* bytes = env->GetByteArrayElements(array, nullptr); - memcpy(buf, bytes, len); - env->ReleaseByteArrayElements(array, bytes, JNI_ABORT); + jbyte* bytes = env->GetByteArrayElements(array, nullptr); + memcpy(buf, bytes, len); + env->ReleaseByteArrayElements(array, bytes, JNI_ABORT); - env->DeleteLocalRef(array); - env->DeleteLocalRef(random); - env->DeleteLocalRef(secureRandomClass); + env->DeleteLocalRef(array); + env->DeleteLocalRef(random); + env->DeleteLocalRef(secureRandomClass); + } + else + { + std::ifstream randomData("/dev/urandom", std::ios::in | std::ios::binary); + if (randomData.is_open()) { + randomData.read(reinterpret_cast(buf), len); + randomData.close(); + } + } } diff --git a/jni/cpp/TWJNI.h b/jni/cpp/TWJNI.h index 4fc8d55d71f..86fc962ca29 100644 --- a/jni/cpp/TWJNI.h +++ b/jni/cpp/TWJNI.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/jni/cpp/TWJNIData.cpp b/jni/cpp/TWJNIData.cpp index 47e294f64b0..942505f593f 100644 --- a/jni/cpp/TWJNIData.cpp +++ b/jni/cpp/TWJNIData.cpp @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include diff --git a/jni/cpp/TWJNIData.h b/jni/cpp/TWJNIData.h index 39d3ca47aa9..9d1c6730368 100644 --- a/jni/cpp/TWJNIData.h +++ b/jni/cpp/TWJNIData.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/jni/cpp/TWJNIString.cpp b/jni/cpp/TWJNIString.cpp index 95b017fac2e..996eb0c747c 100644 --- a/jni/cpp/TWJNIString.cpp +++ b/jni/cpp/TWJNIString.cpp @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include diff --git a/jni/cpp/TWJNIString.h b/jni/cpp/TWJNIString.h index e6452df0134..1691fe47abb 100644 --- a/jni/cpp/TWJNIString.h +++ b/jni/cpp/TWJNIString.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #pragma once diff --git a/jni/java/wallet/core/java/AnySigner.java b/jni/java/wallet/core/java/AnySigner.java index 49c7770d3d1..21caaa185d4 100644 --- a/jni/java/wallet/core/java/AnySigner.java +++ b/jni/java/wallet/core/java/AnySigner.java @@ -1,8 +1,6 @@ -// Copyright © 2017-2020 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package wallet.core.java; diff --git a/jni/kotlin/AnySigner.c b/jni/kotlin/AnySigner.c index 57c215eb34c..83211fb3512 100644 --- a/jni/kotlin/AnySigner.c +++ b/jni/kotlin/AnySigner.c @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #include #include diff --git a/jni/kotlin/AnySigner.h b/jni/kotlin/AnySigner.h index 05742d57122..eac2bd9340b 100644 --- a/jni/kotlin/AnySigner.h +++ b/jni/kotlin/AnySigner.h @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. #ifndef JNI_TW_ANYSIGNER_H #define JNI_TW_ANYSIGNER_H diff --git a/kotlin/build-logic/build.gradle.kts b/kotlin/build-logic/build.gradle.kts index 83e8afc4707..1f41e03775b 100644 --- a/kotlin/build-logic/build.gradle.kts +++ b/kotlin/build-logic/build.gradle.kts @@ -6,6 +6,10 @@ plugins { } allprojects { + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } tasks.withType { compilerOptions { allWarningsAsErrors.set(true) diff --git a/kotlin/kotlin-js-store/yarn.lock b/kotlin/kotlin-js-store/yarn.lock index f2fd767d23d..0995b488885 100644 --- a/kotlin/kotlin-js-store/yarn.lock +++ b/kotlin/kotlin-js-store/yarn.lock @@ -2,6 +2,28 @@ # yarn lockfile v1 +"@babel/code-frame@^7.10.4": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -13,44 +35,86 @@ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@jridgewell/gen-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== dependencies: "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@rollup/plugin-commonjs@^21.0.1": + version "21.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz#45576d7b47609af2db87f55a6d4b46e44fc3a553" + integrity sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-node-resolve@^13.1.3": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" + integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + deepmerge "^4.2.2" + is-builtin-module "^3.1.0" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-typescript@^8.3.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515" + integrity sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + +"@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" "@socket.io/component-emitter@~3.1.0": version "3.1.0" @@ -63,172 +127,186 @@ integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== "@types/cors@^2.8.12": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" - integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== dependencies: "@types/node" "*" "@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" "@types/eslint@*": - version "8.4.10" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.10.tgz#19731b9685c19ed1552da7052b6f668ed7eb64bb" - integrity sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw== + version "8.44.9" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.9.tgz#5799663009645637bd1c45b2e1a7c8f4caf89534" + integrity sha512-6yBxcvwnnYoYT1Uk2d+jvIfsuP4mb2EdIxFnrPABj5a/838qe5bGkNLFOiipX4ULQ7XVQvTxOh7jO+BTAiqsEw== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" - integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/json-schema@*", "@types/json-schema@^7.0.8": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*", "@types/node@>=10.0.0": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "20.10.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.4.tgz#b246fd84d55d5b1b71bf51f964bd514409347198" + integrity sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg== + dependencies: + undici-types "~5.26.4" + +"@types/node@^12.12.14": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^1.2.0": @@ -271,15 +349,15 @@ accepts@~1.3.4: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== -acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.7.1, acorn@^8.8.2: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== ajv-keywords@^3.5.2: version "3.5.2" @@ -306,6 +384,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -326,6 +411,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -342,12 +432,12 @@ binary-extensions@^2.0.0: integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== body-parser@^1.19.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -355,7 +445,7 @@ body-parser@^1.19.0: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -387,42 +477,57 @@ browser-stdout@1.3.1: integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserslist@^4.14.5: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + version "4.22.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" + integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" + caniuse-lite "^1.0.30001565" + electron-to-chromium "^1.4.601" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" camelcase@^6.0.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001449: - version "1.0.30001449" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz#a8d11f6a814c75c9ce9d851dc53eb1d1dfbcd657" - integrity sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw== +caniuse-lite@^1.0.30001565: + version "1.0.30001570" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz#b4e5c1fa786f733ab78fc70f592df6b3f23244ca" + integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" chalk@^4.1.0: version "4.1.2" @@ -470,6 +575,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -477,15 +589,20 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colorette@^2.0.14: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== commander@^2.20.0: version "2.20.3" @@ -497,6 +614,11 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -512,7 +634,7 @@ connect@^3.7.0: parseurl "~1.3.3" utils-merge "1.0.1" -content-type@~1.0.4: +content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -568,6 +690,25 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -603,10 +744,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.284: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +electron-to-chromium@^1.4.601: + version "1.4.612" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.612.tgz#350c6fd4201d677307519b931949fa64dae6a5cc" + integrity sha512-dM8BMtXtlH237ecSMnYdYuCkib2QHq0kpWfUnavjdYsyr/6OsAwg5ZGUfnQ9KD1Ga4QgB2sqXlB2NT8zy2GnVg== emoji-regex@^8.0.0: version "8.0.0" @@ -618,15 +759,15 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== -engine.io@~6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f" - integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA== +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -636,13 +777,13 @@ engine.io@~6.2.1: cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.2.3" + engine.io-parser "~5.2.1" + ws "~8.11.0" -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -653,14 +794,14 @@ ent@~2.2.0: integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== envinfo@^7.7.3: - version "7.8.1" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + version "7.11.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.0.tgz#c3793f44284a55ff8c82faf1ffd91bc6478ea01f" + integrity sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" + integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== escalade@^3.1.1: version "3.1.1" @@ -677,6 +818,11 @@ escape-string-regexp@4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -702,6 +848,16 @@ estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -774,16 +930,16 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== follow-redirects@^1.0.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== -format-util@1.0.5: +format-util@1.0.5, format-util@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== @@ -803,28 +959,29 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" glob-parent@~5.1.2: version "5.1.2" @@ -850,7 +1007,7 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3, glob@^7.1.7: +glob@^7.1.3, glob@^7.1.6, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -862,27 +1019,51 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" he@1.2.0: version "1.2.0" @@ -956,12 +1137,24 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-builtin-module@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== dependencies: - has "^1.0.3" + builtin-modules "^3.3.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extglob@^2.1.1: version "2.1.1" @@ -980,6 +1173,11 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -997,11 +1195,25 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -1017,6 +1229,15 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -1026,6 +1247,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1148,15 +1374,22 @@ log-symbols@4.1.0: is-unicode-supported "^0.1.0" log4js@^6.4.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.7.1.tgz#06e12b1ac915dd1067146ffad8215f666f7d2c51" - integrity sha512-lzbd0Eq1HRdWM2abSD7mk6YIVY0AogGJzb/z+lqzRk+8+XJP+M6L1MS5FUSc3jjGru4dbKjEMJmqlsoYYpuivQ== + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== dependencies: date-format "^4.0.14" debug "^4.3.4" flatted "^3.2.7" rfdc "^1.3.0" - streamroller "^3.1.3" + streamroller "^3.1.5" + +magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" media-typer@0.3.0: version "0.3.0" @@ -1200,9 +1433,9 @@ minimatch@^3.0.4, minimatch@^3.1.1: brace-expansion "^1.1.7" minimist@^1.2.3, minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mkdirp@^0.5.5: version "0.5.6" @@ -1269,10 +1502,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-releases@^2.0.8: - version "2.0.9" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.9.tgz#fe66405285382b0c4ac6bcfbfbe7e8a510650b4d" - integrity sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -1285,9 +1518,9 @@ object-assign@^4: integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== on-finished@2.4.1: version "2.4.1" @@ -1373,7 +1606,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -1386,9 +1619,9 @@ pkg-dir@^4.2.0: find-up "^4.0.0" punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== qjobs@^1.2.0: version "1.2.0" @@ -1414,10 +1647,10 @@ range-parser@^1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" @@ -1460,12 +1693,12 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.9.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.17.0, resolve@^1.19.0, resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -1481,6 +1714,31 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup-plugin-sourcemaps@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" + integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== + dependencies: + "@rollup/pluginutils" "^3.0.9" + source-map-resolve "^0.6.0" + +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.68.0: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -1491,10 +1749,10 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" @@ -1507,13 +1765,30 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.0: +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== dependencies: randombytes "^2.1.0" +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -1547,30 +1822,33 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -socket.io-adapter@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" - integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" -socket.io-parser@~4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" - integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" socket.io@^4.4.1: - version "4.5.4" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.4.tgz#a4513f06e87451c17013b8d13fdfaf8da5a86a90" - integrity sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ== + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" + integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== dependencies: accepts "~1.3.4" base64id "~2.0.0" + cors "~2.8.5" debug "~4.3.2" - engine.io "~6.2.1" - socket.io-adapter "~2.4.0" - socket.io-parser "~4.2.1" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" source-map-js@^1.0.2: version "1.0.2" @@ -1586,6 +1864,14 @@ source-map-loader@4.0.0: iconv-lite "^0.6.3" source-map-js "^1.0.2" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -1599,6 +1885,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -1609,10 +1900,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -streamroller@^3.1.3: - version "3.1.4" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.4.tgz#844a18e795d39c1089a8216e66a1cf1151271df0" - integrity sha512-Ha1Ccw2/N5C/IF8Do6zgNe8F3jQo8MPBnMBGvX0QjNv/I97BcNRzK6/mzOpZHHK7DjMLTI3c7Xw7Y1KvdChkvw== +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== dependencies: date-format "^4.0.14" debug "^4.3.4" @@ -1646,7 +1937,14 @@ supports-color@8.1.1, supports-color@^8.0.0: dependencies: has-flag "^4.0.0" -supports-color@^7.1.0: +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -1663,24 +1961,24 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.1.3: - version "5.3.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" - integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== dependencies: - "@jridgewell/trace-mapping" "^0.3.14" + "@jridgewell/trace-mapping" "^0.3.17" jest-worker "^27.4.5" schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - terser "^5.14.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" -terser@^5.14.1: - version "5.16.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.2.tgz#8f495819439e8b5c150e7530fc434a6e70ea18b2" - integrity sha512-JKuM+KvvWVqT7muHVyrwv7FVRPnmHDwF6XwoIxdbF5Witi0vu99RYpxDexpJndXt3jbZZmmWr2/mQa6HvSNdSg== +terser@^5.0.0, terser@^5.16.8: + version "5.26.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" + integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -1703,6 +2001,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tslib@^2.3.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -1711,10 +2014,25 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +typescript@^3.7.2: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + ua-parser-js@^0.7.30: - version "0.7.33" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" - integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== + version "0.7.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" + integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== universalify@^0.1.0: version "0.1.2" @@ -1726,10 +2044,10 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -1790,11 +2108,12 @@ webpack-merge@^4.1.5: lodash "^4.17.15" webpack-merge@^5.7.3: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== dependencies: clone-deep "^4.0.1" + flat "^5.0.2" wildcard "^2.0.0" webpack-sources@^3.2.3: @@ -1802,22 +2121,22 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.74.0: - version "5.74.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" - integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== +webpack@5.89.0: + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" + acorn-import-assertions "^1.9.0" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" @@ -1826,9 +2145,9 @@ webpack@5.74.0: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" + terser-webpack-plugin "^5.3.7" watchpack "^2.4.0" webpack-sources "^3.2.3" @@ -1847,9 +2166,9 @@ which@^2.0.1: isexe "^2.0.0" wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== workerpool@6.2.1: version "6.2.1" @@ -1870,10 +2189,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@~8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== y18n@^5.0.5: version "5.0.8" diff --git a/kotlin/wallet-core-kotlin/build.gradle.kts b/kotlin/wallet-core-kotlin/build.gradle.kts index 3bea97954ca..63d879ca7c3 100644 --- a/kotlin/wallet-core-kotlin/build.gradle.kts +++ b/kotlin/wallet-core-kotlin/build.gradle.kts @@ -80,6 +80,10 @@ kotlin { getByName("jsMain") { kotlin.srcDir(projectDir.resolve("src/jsMain/generated")) + + dependencies { + implementation(npm(name = "webpack", version = "5.89.0")) + } } } diff --git a/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/AnySigner.kt b/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/AnySigner.kt index 374d66218b9..e6b348433c4 100644 --- a/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/AnySigner.kt +++ b/kotlin/wallet-core-kotlin/src/commonAndroidJvmMain/kotlin/com/trustwallet/core/AnySigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/commonMain/kotlin/com/trustwallet/core/AnySigner.kt b/kotlin/wallet-core-kotlin/src/commonMain/kotlin/com/trustwallet/core/AnySigner.kt index 85e195c229e..576aa1d32cb 100644 --- a/kotlin/wallet-core-kotlin/src/commonMain/kotlin/com/trustwallet/core/AnySigner.kt +++ b/kotlin/wallet-core-kotlin/src/commonMain/kotlin/com/trustwallet/core/AnySigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt index dac9e182bf3..c3d3dbf0527 100644 --- a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt +++ b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt @@ -38,8 +38,9 @@ class CoinAddressDerivationTests { Ethereum, SmartChain, Polygon, Optimism, Zksync, Arbitrum, ArbitrumNova, ECOChain, AvalancheCChain, XDai, Fantom, Celo, CronosChain, SmartBitcoinCash, KuCoinCommunityChain, Boba, Metis, - Aurora, Evmos, Moonriver, Moonbeam, KavaEvm, Klaytn, Meter, OKXChain, PolygonzkEVM, Scroll, - ConfluxeSpace, AcalaEVM, OpBNB, Neon, Base, Linea, Greenfield, Mantle, ZenEON, + Aurora, Evmos, Moonriver, Moonbeam, KavaEvm, Kaia, Meter, OKXChain, PolygonzkEVM, Scroll, + ConfluxeSpace, AcalaEVM, OpBNB, Neon, Base, Linea, Greenfield, Mantle, ZenEON, MantaPacific, + ZetaEVM, Merlin, Lightlink, Blast, BounceBit, ZkLinkNova, -> "0x8f348F300873Fd5DA36950B2aC75a26584584feE" Ronin -> "ronin:8f348F300873Fd5DA36950B2aC75a26584584feE" @@ -53,7 +54,7 @@ class CoinAddressDerivationTests { XRP -> "rPwE3gChNKtZ1mhH3Ko8YFGqKmGRWLWXV3" Tezos -> "tz1acnY9VbMagps26Kj3RfoGRWD9nYG5qaRX" ThunderCore -> "0x4b92b3ED6d8b24575Bf5ce4C6a86ED261DA0C8d7" - TomoChain -> "0xC74b6D8897cBa9A4b659d43fEF73C9cA852cE424" + Viction -> "0xC74b6D8897cBa9A4b659d43fEF73C9cA852cE424" Tron -> "TQ5NMqJjhpQGK7YJbESKtNCo86PJ89ujio" VeChain -> "0x1a553275dF34195eAf23942CB7328AcF9d48c160" Wanchain -> "0xD5ca90b928279FE5D06144136a25DeD90127aC15" @@ -114,8 +115,8 @@ class CoinAddressDerivationTests { NativeEvmos -> "evmos13u6g7vqgw074mgmf2ze2cadzvkz9snlwstd20d" Nervos -> "ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdtyq04tvp02wectaumxn0664yw2jd53lqk4mxg3" Everscale -> "0:0c39661089f86ec5926ea7d4ee4223d634ba4ed6dcc2e80c7b6a8e6d59f79b04" - TON -> "EQDgEMqToTacHic7SnvnPFmvceG5auFkCcAw0mSCvzvKUfk9" - Aptos -> "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" + TON -> "UQDgEMqToTacHic7SnvnPFmvceG5auFkCcAw0mSCvzvKUaT4" + Aptos -> "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" Nebl -> "NgDVaXAwNgBwb88xLiFKomfBmPkEh9F2d7" Sui -> "0xada112cfb90b44ba889cc5d39ac2bf46281e4a91f7919c693bcd9b8323e81ed2" Hedera -> "0.0.302a300506032b657003210049eba62f64d0d941045595d9433e65d84ecc46bcdb1421de55e05fcf2d8357d5" @@ -142,5 +143,9 @@ class CoinAddressDerivationTests { Noble -> "noble142j9u5eaduzd7faumygud6ruhdwme98qc8l3wa" Rootstock -> "0xA2D7065F94F838a3aB9C04D67B312056846424Df" Sei -> "sei142j9u5eaduzd7faumygud6ruhdwme98qagm0sj" + InternetComputer -> "6f8e568160a3c8362789848dc0fa52891964473c045cc25208a305fb35b7c4ab" + Tia -> "celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7" + NativeZetaChain -> "zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304" + Dydx -> "dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky" } } diff --git a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/AnySigner.kt b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/AnySigner.kt index fd1cb8ed651..6de0b110bea 100644 --- a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/AnySigner.kt +++ b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/AnySigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/ByteArrayExt.kt b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/ByteArrayExt.kt index 8498cb26061..ec5923272a1 100644 --- a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/ByteArrayExt.kt +++ b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/ByteArrayExt.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/StringExt.kt b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/StringExt.kt index 19f224a22b7..2eb7fdd311e 100644 --- a/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/StringExt.kt +++ b/kotlin/wallet-core-kotlin/src/iosMain/kotlin/com/trustwallet/core/StringExt.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/WalletCore.kt b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/WalletCore.kt index ba7c30845c5..a454889ea72 100644 --- a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/WalletCore.kt +++ b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/WalletCore.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. import com.trustwallet.core.* import kotlin.js.Promise diff --git a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/AnySigner.kt b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/AnySigner.kt index b9e28700f97..d62bf4f81c3 100644 --- a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/AnySigner.kt +++ b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/AnySigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/ByteArray.kt b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/ByteArray.kt index 615d8c38db9..9163d5bdca2 100644 --- a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/ByteArray.kt +++ b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/ByteArray.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsAnySigner.kt b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsAnySigner.kt index f387eccbe7d..75be5fbd8d7 100644 --- a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsAnySigner.kt +++ b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsAnySigner.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. package com.trustwallet.core diff --git a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsWalletCore.kt b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsWalletCore.kt index f2d75c7c361..0865e64e531 100644 --- a/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsWalletCore.kt +++ b/kotlin/wallet-core-kotlin/src/jsMain/kotlin/com/trustwallet/core/JsWalletCore.kt @@ -1,8 +1,6 @@ -// Copyright © 2017-2023 Trust Wallet. +// SPDX-License-Identifier: Apache-2.0 // -// This file is part of Trust. The full Trust copyright notice, including -// terms governing use, modification, and redistribution, is contained in the -// file LICENSE at the root of the source code distribution tree. +// Copyright © 2017 Trust Wallet. @file:Suppress("PropertyName") diff --git a/protobuf-plugin/CMakeLists.txt b/protobuf-plugin/CMakeLists.txt index 57b7ac59d83..237789be7be 100644 --- a/protobuf-plugin/CMakeLists.txt +++ b/protobuf-plugin/CMakeLists.txt @@ -1,33 +1,26 @@ -# Copyright © 2017-2022 Trust Wallet. +# SPDX-License-Identifier: Apache-2.0 # -# This file is part of Trust. The full Trust copyright notice, including -# terms governing use, modification, and redistribution, is contained in the -# file LICENSE at the root of the source code distribution tree. +# Copyright © 2017 Trust Wallet. -cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(TrustWalletCoreProtobufPlugin) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14" CACHE STRING "Minimum OS X deployment version" FORCE) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if ("$ENV{PREFIX}" STREQUAL "") +if("$ENV{PREFIX}" STREQUAL "") set(PREFIX "${CMAKE_SOURCE_DIR}/../build/local") else() set(PREFIX "$ENV{PREFIX}") endif() -include_directories(${PREFIX}/include) -link_directories(${PREFIX}/lib) +find_package(Protobuf CONFIG REQUIRED PATH ${PREFIX}/lib/pkgconfig) -find_package(Protobuf REQUIRED PATH ${PREFIX}/lib/pkgconfig) -include_directories(${Protobuf_INCLUDE_DIRS}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +add_executable(protoc-gen-c-typedef c_typedef.cc) +target_link_libraries(protoc-gen-c-typedef protobuf::libprotobuf protobuf::libprotoc) -add_executable(protoc-gen-c-typedef c_typedef.cc ${PROTO_SRCS} ${PROTO_HDRS}) -target_link_libraries(protoc-gen-c-typedef protobuf -lprotoc -pthread) - -add_executable(protoc-gen-swift-typealias swift_typealias.cc ${PROTO_SRCS} ${PROTO_HDRS}) -target_link_libraries(protoc-gen-swift-typealias protobuf -lprotoc -pthread) +add_executable(protoc-gen-swift-typealias swift_typealias.cc) +target_link_libraries(protoc-gen-swift-typealias protobuf::libprotobuf protobuf::libprotoc) install(TARGETS protoc-gen-c-typedef protoc-gen-swift-typealias DESTINATION bin) diff --git a/protobuf-plugin/c_typedef.cc b/protobuf-plugin/c_typedef.cc index 9cd48a5e798..1352898cb73 100644 --- a/protobuf-plugin/c_typedef.cc +++ b/protobuf-plugin/c_typedef.cc @@ -14,16 +14,14 @@ class Generator : public compiler::CodeGenerator { return "TW" + proto_file.substr(0, index) + "Proto.h"; } - bool Generate(const FileDescriptor* file, const std::string& parameter, compiler::GeneratorContext* generator_context, string* error) const { + bool Generate(const FileDescriptor* file, const std::string& parameter, compiler::GeneratorContext* generator_context, std::string* error) const { std::unique_ptr output(generator_context->Open(GetOutputFilename(file->name()))); io::Printer printer(output.get(), '$'); printer.Print( - "// Copyright © 2017-2020 Trust Wallet.\n" + "// SPDX-License-Identifier: Apache-2.0\n" "//\n" - "// This file is part of Trust. The full Trust copyright notice, including\n" - "// terms governing use, modification, and redistribution, is contained in the\n" - "// file LICENSE at the root of the source code distribution tree.\n" + "// Copyright © 2017 Trust Wallet.\n" "//\n" "// This is a GENERATED FILE, changes made here WILL BE LOST.\n" "\n" diff --git a/protobuf-plugin/swift_typealias.cc b/protobuf-plugin/swift_typealias.cc index 57a169dea03..3d0983c2ca3 100644 --- a/protobuf-plugin/swift_typealias.cc +++ b/protobuf-plugin/swift_typealias.cc @@ -16,16 +16,14 @@ class Generator : public compiler::CodeGenerator { return proto_file.substr(0, index) + "+Proto.swift"; } - bool Generate(const FileDescriptor* file, const std::string& parameter, compiler::GeneratorContext* generator_context, string* error) const { + bool Generate(const FileDescriptor* file, const std::string& parameter, compiler::GeneratorContext* generator_context, std::string* error) const { std::unique_ptr output(generator_context->Open(GetOutputFilename(file->name()))); io::Printer printer(output.get(), '$'); printer.Print( - "// Copyright © 2017-2020 Trust Wallet.\n" + "// SPDX-License-Identifier: Apache-2.0\n" "//\n" - "// This file is part of Trust. The full Trust copyright notice, including\n" - "// terms governing use, modification, and redistribution, is contained in the\n" - "// file LICENSE at the root of the source code distribution tree.\n" + "// Copyright © 2017 Trust Wallet.\n" "\n" ); diff --git a/registry.json b/registry.json index 8fad28fe10c..1bb8123783a 100644 --- a/registry.json +++ b/registry.json @@ -34,9 +34,9 @@ "publicKeyHasher": "sha256ripemd", "base58Hasher": "sha256d", "explorer": { - "url": "https://blockchair.com", - "txPath": "/bitcoin/transaction/", - "accountPath": "/bitcoin/address/", + "url": "https://mempool.space", + "txPath": "/tx/", + "accountPath": "/address/", "sampleTx": "0607f62530b68cfcc91c57a1702841dd399a899d0eecda8e31ecca3f52f01df2", "sampleAccount": "17A16QmavnUfCW11DAApiJxp7ARnxN5pGX" }, @@ -1166,6 +1166,36 @@ "documentation": "https://docs.sei.io/" } }, + { + "id": "tia", + "name": "Tia", + "displayName": "Celestia", + "coinId": 21000118, + "symbol": "TIA", + "decimals": 6, + "blockchain": "Cosmos", + "chainId": "celestia", + "derivation": [ + { + "path": "m/44'/118'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1", + "hrp": "celestia", + "addressHasher": "sha256ripemd", + "explorer": { + "url": "https://www.mintscan.io/celestia", + "txPath": "/txs/", + "accountPath": "/account/", + "sampleTx": "FF370C65D8D67B8236F9D3A8D2B1256337C60C1965092CADD1FA970288FCE99B", + "sampleAccount": "celestia1tt4tv4jrs4twdtzwywxd8u65duxgk8y73wvfu2" + }, + "info": { + "url": "https://celestia.org/", + "documentation": "https://docs.celestia.org/" + } + }, { "id": "coreum", "name": "Coreum", @@ -1468,7 +1498,7 @@ "url": "https://blockchair.com/stellar", "txPath": "/transaction/", "accountPath": "/account/", - "sampleTx": "d9aeabfa9d24df8c5755125f8af243b74cd3ff878656cfa72c566a8824bf6e84", + "sampleTx": "8a7ff7261e8b3f31af7f6ed257c2e9fe7c47afcd9b1ce1be1bfc1bc5f6a3ad9e", "sampleAccount": "GCILJZQ3CKBKBUJWW4TAM6Q37LJA5MQX6GMSFSQN75BPLWIZ33OPRG52" }, "info": { @@ -1529,9 +1559,9 @@ "publicKeyType": "ed25519Blake2b", "url": "https://nano.org", "explorer": { - "url": "https://nanocrawler.cc", - "txPath": "/explorer/block/", - "accountPath": "/explorer/account/", + "url": "https://www.nanolooker.com", + "txPath": "/block/", + "accountPath": "/account/", "sampleTx": "C264DB7BF40738F0CEFF19B606746CB925B713E4B8699A055699E0DC8ABBC70F", "sampleAccount": "nano_1wpj616kwhe1y38y1mspd8aub8i334cwybqco511iyuxm55zx8d67ptf1tsf" }, @@ -1563,7 +1593,7 @@ "publicKeyHasher": "sha256ripemd", "base58Hasher": "sha256d", "explorer": { - "url": "https://ravencoin.network", + "url": "https://blockbook.ravencoin.org", "txPath": "/tx/", "accountPath": "/address/" }, @@ -1750,9 +1780,9 @@ "curve": "ed25519", "publicKeyType": "ed25519", "explorer": { - "url": "https://algoexplorer.io", - "txPath": "/tx/", - "accountPath": "/address/", + "url": "https://app.dappflow.org/explorer", + "txPath": "/transaction/", + "accountPath": "/account/", "sampleTx": "CR7POXFTYDLC7TV3IXHA7AZKWABUJC52BACLHJQNXAKZJGRPQY3A", "sampleAccount": "J4AEINCSSLDA7LNBNWM4ZXFCTLTOZT5LG3F5BLMFPJYGFWVCMU37EZI2AM" }, @@ -1763,7 +1793,6 @@ "documentation": "https://developer.algorand.org/docs/algod-rest-paths" } }, - { "id": "iotex", "name": "IoTeX", @@ -2005,9 +2034,9 @@ "curve": "ed25519", "publicKeyType": "ed25519", "explorer": { - "url": "https://explorer.near.org", - "txPath": "/transactions/", - "accountPath": "/accounts/", + "url": "https://nearblocks.io", + "txPath": "/txns/", + "accountPath": "/address/", "sampleTx": "FPQAMaVnvFHNwNBJWnTttXfdJhp5FvMGGDJEesB8gvbL", "sampleAccount": "test-trust.vlad.near" }, @@ -2268,11 +2297,11 @@ "hrp": "band", "addressHasher": "sha256ripemd", "explorer": { - "url": "https://scan-wenchang-testnet2.bandchain.org/", + "url": "https://www.mintscan.io/band", "txPath": "/tx/", "accountPath": "/account/", - "sampleTx": "473264551D3063A9EC64EC251C61BE92DDDFCF6CC46D026D1E574D83D5447173", - "sampleAccount": "band12nmsm9khdsv0tywge43q3zwj8kkj3hvup9rltp" + "sampleTx": "74AF38C2183B06EB6274DA4AAC0D2334E6E283643D436852F5E088AEA2CD0B17", + "sampleAccount": "band16gpgu994g2gdrzvwp9047le3pcq9wz6mcgtd4w" }, "info": { "url": "https://bandprotocol.com/", @@ -2411,6 +2440,7 @@ ], "curve": "secp256k1", "publicKeyType": "secp256k1", + "addressHasher": "sha256ripemd", "hrp": "bnb", "chainId": "Binance-Chain-Tigris", "explorer": { @@ -2443,6 +2473,7 @@ ], "curve": "secp256k1", "publicKeyType": "secp256k1", + "addressHasher": "sha256ripemd", "hrp": "tbnb", "explorer": { "url": "https://testnet-explorer.binance.org", @@ -2544,10 +2575,10 @@ } }, { - "id": "tomochain", - "name": "TomoChain", + "id": "viction", + "name": "Viction", "coinId": 889, - "symbol": "TOMO", + "symbol": "VIC", "decimals": 18, "blockchain": "Ethereum", "derivation": [ @@ -2560,15 +2591,15 @@ "chainId": "88", "addressHasher": "keccak256", "explorer": { - "url": "https://tomoscan.io", + "url": "https://www.vicscan.xyz", "txPath": "/tx/", "accountPath": "/address/", "sampleTx": "0x35a8d3ab06c94d5b7d27221b7c9a24ba3f1710dd0fcfd75c5d59b3a885fd709b", "sampleAccount": "0x86cCbD9bfb371c355202086882bC644A7D0b024B" }, "info": { - "url": "https://tomochain.com", - "source": "https://github.com/tomochain/tomochain", + "url": "https://www.viction.xyz/", + "source": "https://github.com/BuildOnViction/tomochain", "rpc": "https://rpc.tomochain.com", "documentation": "https://eth.wiki/json-rpc/API" } @@ -2682,7 +2713,7 @@ "url": "https://oasisscan.com", "txPath": "/transactions/", "accountPath": "/accounts/detail/", - "sampleTx": "0b9bd4983f1c88a1c71bf33562b6ba02b3064e01697d15a0de4bfe1922ec74b8", + "sampleTx": "73dc977fdd8596d4a57e6feb891b21f5da3652d26815dc94f15f7420c298e29e", "sampleAccount": "oasis1qrx376dmwuckmruzn9vq64n49clw72lywctvxdf4" }, "info": { @@ -3077,7 +3108,7 @@ "id": "polygon", "name": "Polygon", "coinId": 966, - "symbol": "MATIC", + "symbol": "POL", "decimals": 18, "blockchain": "Ethereum", "derivation": [ @@ -3098,9 +3129,9 @@ }, "info": { "url": "https://polygon.technology", - "source": "https://github.com/maticnetwork/contracts", + "source": "https://github.com/maticnetwork", "rpc": "https://polygon-rpc.com", - "documentation": "https://eth.wiki/json-rpc/API" + "documentation": "https://docs.polygon.technology" } }, { @@ -3148,6 +3179,7 @@ "curve": "secp256k1", "publicKeyType": "secp256k1", "hrp": "thor", + "addressHasher": "sha256ripemd", "chainId": "thorchain-mainnet-v1", "explorer": { "url": "https://viewblock.io/thorchain", @@ -3166,7 +3198,7 @@ { "id": "optimism", "name": "Optimism", - "displayName": "Optimism Ethereum", + "displayName": "OP Mainnet", "coinId": 10000070, "slip44": 60, "symbol": "ETH", @@ -3259,7 +3291,7 @@ "id": "scroll", "name": "Scroll", "displayName": "Scroll", - "coinId": 534353, + "coinId": 534352, "slip44": 60, "symbol": "ETH", "decimals": 18, @@ -3271,19 +3303,19 @@ ], "curve": "secp256k1", "publicKeyType": "secp256k1Extended", - "chainId": "534353", + "chainId": "534352", "addressHasher": "keccak256", "explorer": { - "url": "https://blockscout.scroll.io", + "url": "https://scrollscan.com", "txPath": "/tx/", "accountPath": "/address/", - "sampleTx": "0xee9196d6840c8d31626324d91c886d20e65711c2026c559133fb23741d3b2f9d", - "sampleAccount": "0xFE993660cd35d68D94b6Eba29F4D928d979cd65B" + "sampleTx": "0xa2062a4530b194a438bb9f9e87cdce59f70775a52e8336892364445847c43ca2", + "sampleAccount": "0xf9062b8a30e0d7722960e305049fa50b86ba6253" }, "info": { "url": "https://scroll.io", "source": "https://github.com/scroll-tech", - "rpc": "https://alpha-rpc.scroll.io/l2", + "rpc": "https://rpc.scroll.io", "documentation": "https://guide.scroll.io" } }, @@ -3944,7 +3976,7 @@ "coinId": 20009001, "symbol": "EVMOS", "decimals": 18, - "blockchain": "Cosmos", + "blockchain": "NativeEvmos", "chainId": "evmos_9001-2", "derivation": [ { @@ -4025,8 +4057,8 @@ } }, { - "id": "klaytn", - "name": "Klaytn", + "id": "kaia", + "name": "Kaia", "coinId": 10008217, "symbol": "KLAY", "decimals": 18, @@ -4040,16 +4072,16 @@ "publicKeyType": "secp256k1Extended", "chainId": "8217", "explorer": { - "url": "https://scope.klaytn.com", + "url": "https://kaiascan.io", "txPath": "/tx/", "accountPath": "/account/", "sampleTx": "0x93ea92687845fe7bb6cacd69c76a16a2a3c2bbb85a8a93ff0e032d0098d583d7", "sampleAccount": "0x2ad9656bf5b82caf10847b431012e28e301e83ba" }, "info": { - "url": "https://klaytn.foundation", - "rpc": "https://public-node-api.klaytnapi.com/v1/cypress", - "documentation": "https://docs.klaytn.foundation" + "url": "https://kaia.io", + "rpc": "https://public-en.node.kaia.io", + "documentation": "https://docs.kaia.io" } }, { @@ -4098,7 +4130,7 @@ "publicKeyType": "secp256k1Extended", "addressHasher": "keccak256", "explorer": { - "url": "https://www.oklink.com/en/okc", + "url": "https://www.oklink.com/oktc", "txPath": "/tx/", "accountPath": "/address/", "sampleTx": "0x46C3A947E8248570FBD28E4FE456CC8F80DFD90716533878FB67857B95FA3D37", @@ -4148,7 +4180,7 @@ "coinId": 5600, "symbol": "BNB", "decimals": 18, - "chainId": "9000", + "chainId": "1017", "blockchain": "Greenfield", "derivation": [ { @@ -4211,6 +4243,7 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/44'/105105'/0'/0/0", "xpub": "xpub", "xprv": "xprv" @@ -4329,6 +4362,38 @@ "documentation": "https://docs.agoric.com" } }, + { + "id": "dydx", + "name": "Dydx", + "displayName": "dYdX", + "coinId": 22000118, + "symbol": "DYDX", + "decimals": 18, + "blockchain": "Cosmos", + "derivation": [ + { + "path": "m/44'/118'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1", + "hrp": "dydx", + "chainId": "dydx-mainnet-1", + "addressHasher": "sha256ripemd", + "explorer": { + "url": "https://www.mintscan.io/dydx", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "F236222E4F7C92FA84711FD6451ED22DD56CBDFA319BFDAFB99A21E4E9B9EC2F", + "sampleAccount": "dydx1adl7usw7z2dnysyn7wvrghu0u0q6gr7jqs4gtt" + }, + "info": { + "url": "https://dydx.exchange", + "source": "https://github.com/dydxprotocol", + "rpc": "https://dydx-dao-api.polkachu.com", + "documentation": "https://docs.dydx.exchange" + } + }, { "id": "nativeinjective", "name": "NativeInjective", @@ -4336,7 +4401,7 @@ "coinId": 10000060, "symbol": "INJ", "decimals": 18, - "blockchain": "Cosmos", + "blockchain": "NativeInjective", "derivation": [ { "path": "m/44'/60'/0'/0/0" @@ -4389,13 +4454,71 @@ "documentation": "https://docs.canto.io/" } }, + { + "id": "zetachain", + "name": "NativeZetaChain", + "displayName": "NativeZetaChain", + "coinId": 10007000, + "symbol": "ZETA", + "decimals": 18, + "blockchain": "Cosmos", + "chainId": "zetachain_7000-1", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "hrp": "zeta", + "addressHasher": "keccak256", + "explorer": { + "url": "https://explorer.zetachain.com", + "txPath": "/cosmos/tx/", + "accountPath": "/address/", + "sampleTx": "2DBB071DDD47985F4470A21E5943CE95D371AE4BDE2267E201D3553FB2BDCFDE", + "sampleAccount": "zeta14py36sx57ud82t9yrks9z6hdsrpn5x6kmxs0ne" + }, + "info": { + "url": "https://www.zetachain.com/", + "documentation": "https://www.zetachain.com/docs/" + } + }, + { + "id": "zetaevm", + "name": "Zeta EVM", + "coinId": 20007000, + "symbol": "ZETA", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "7000", + "addressHasher": "keccak256", + "explorer": { + "url": "https://explorer.zetachain.com", + "txPath": "/evm/tx/", + "accountPath": "/address/", + "sampleTx": "0x04cb1201857de29af97b755e51c888454fb96c1f3bb3c1329bb94d5353d5c19e", + "sampleAccount": "0x85539A58F9c88DdDccBaBBfc660968323Fd1e167" + }, + "info": { + "url": "https://www.zetachain.com/", + "documentation": "https://www.zetachain.com/docs/" + } + }, { "id": "ton", "name": "TON", "coinId": 607, "symbol": "TON", "decimals": 9, - "blockchain": "The Open Network", + "blockchain": "TheOpenNetwork", "derivation": [ { "path": "m/44'/607'/0'" @@ -4404,9 +4527,9 @@ "curve": "ed25519", "publicKeyType": "ed25519", "explorer": { - "url": "https://tonscan.org", - "txPath": "/tx/", - "accountPath": "/address/", + "url": "https://tonviewer.com", + "txPath": "/transaction/", + "accountPath": "/", "sampleTx": "fJXfn0EVhV09HFuEgUHu4Cchb24nUQtIMwSzmzk2tLs=", "sampleAccount": "EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N" }, @@ -4446,5 +4569,217 @@ "rpc": "https://neon-proxy-mainnet.solana.p2p.org/", "documentation": "https://docs.neonfoundation.io/docs/quick_start" } + }, + { + "id": "internet_computer", + "name": "Internet Computer", + "coinId": 223, + "symbol": "ICP", + "decimals": 8, + "blockchain": "InternetComputer", + "derivation": [ + { + "path": "m/44'/223'/0'/0/0", + "xpub": "xpub", + "xpriv": "xpriv" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "explorer": { + "url": "https://dashboard.internetcomputer.org", + "txPath": "/transaction/", + "accountPath": "/account/", + "sampleTx": "9e32c54975adf84a1d98f19df41bbc34a752a899c32cc9c0000200b2c4308f85", + "sampleAccount": "529ea51c22e8d66e8302eabd9297b100fdb369109822248bb86939a671fbc55b" + }, + "info": { + "url": "https://internetcomputer.org", + "source": "https://github.com/dfinity/ic", + "rpc": "", + "documentation": "https://internetcomputer.org/docs" + } + }, + { + "id": "manta", + "name": "Manta Pacific", + "coinId": 169, + "symbol": "ETH", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "169", + "addressHasher": "keccak256", + "explorer": { + "url": "https://pacific-explorer.manta.network", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0x2bbd5d85b0ed05d1416e30ce1197a6f0c27d10ce02593a2719e2baf486d2e8c2", + "sampleAccount": "0xF122a1aC569a36a5Cf6d0F828A22254c8A9afF84" + }, + "info": { + "url": "https://pacific.manta.network", + "source": "https://github.com/manta-network", + "rpc": "https://pacific-rpc.manta.network/http", + "documentation": "https://docs.manta.network/docs/Introduction" + } + }, + { + "id": "merlin", + "name": "Merlin", + "coinId": 4200, + "symbol": "BTC", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "4200", + "addressHasher": "keccak256", + "explorer": { + "url": "https://scan.merlinchain.io", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0xca6f2891959b669237738dd0bc2c1051d966778c9de90b94165032ce58364212", + "sampleAccount": "0xf7e017b3f61bD3410A3b03D7DAD7699FD6780584" + }, + "info": { + "url": "https://merlinchain.io", + "source": "https://merlinchain.io", + "rpc": "https://rpc.merlinchain.io", + "documentation": "https://docs.merlinchain.io/merlin-docs" + } + }, + { + "id": "lightlink", + "name": "Lightlink", + "displayName": "Lightlink Phoenix", + "coinId": 1890, + "symbol": "ETH", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "1890", + "addressHasher": "keccak256", + "explorer": { + "url": "https://phoenix.lightlink.io", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0xc65f82445aefc883fd4ffe09149c8ce48f61b670c0734528a49d4a8bd8309bb0", + "sampleAccount": "0x4566ED6c7a7fFc90E2C7cfF7eB9156262afD2fDe" + }, + "info": { + "url": "https://lightlink.io", + "source": "https://github.com/lightlink-network", + "rpc": "https://endpoints.omniatech.io/v1/lightlink/phoenix/public", + "documentation": "https://docs.lightlink.io/lightlink-protocol" + } + }, + { + "id": "blast", + "name": "Blast", + "coinId": 81457, + "symbol": "ETH", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "81457", + "addressHasher": "keccak256", + "explorer": { + "url": "https://blastscan.io", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0x511fc00e8329343b9e953bf1f75e9b0a7b3cc2eb3a8f049d5be41adf4fbd6cac", + "sampleAccount": "0x0d11f2f0ff55c4fcfc3ff86bdc8e78ffa7df99fd" + }, + "info": { + "url": "https://blast.io", + "source": "https://github.com/blast-io", + "rpc": "https://rpc.blast.io", + "documentation": "https://docs.blast.io" + } + }, + { + "id": "bouncebit", + "name": "BounceBit", + "coinId": 6001, + "symbol": "BB", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "6001", + "addressHasher": "keccak256", + "explorer": { + "url": "https://bbscan.io", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0x52558f4143d058d942e3b73414090266ae3ffce1fe8c25fe86896e2c8e5ef932", + "sampleAccount": "0xf4aa7349a9ccca4609943955b5ddc7bd9278c223" + }, + "info": { + "url": "https://bouncebit.io", + "source": "https://github.com/BounceBit-Labs", + "rpc": "https://fullnode-mainnet.bouncebitapi.com", + "documentation": "https://docs.bouncebit.io" + } + }, + { + "id": "zklinknova", + "name": "ZkLinkNova", + "displayName": "zkLink Nova Mainnet", + "coinId": 810180, + "symbol": "ETH", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "810180", + "addressHasher": "keccak256", + "explorer": { + "url": "https://explorer.zklink.io", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0xeb5eb8710369c89115a83f3e744c15c9d388030cfce2fd3a653dbd18f2947400", + "sampleAccount": "0xF95115BaD9a4585B3C5e2bfB50579f17163A45aA" + }, + "info": { + "url": "https://zklink.io", + "source": "https://github.com/zkLinkProtocol", + "rpc": "https://rpc.zklink.io", + "documentation": "https://docs.zklink.io" + } } ] diff --git a/rust/.config/nextest.toml b/rust/.config/nextest.toml deleted file mode 100644 index 76fd74b5d7f..00000000000 --- a/rust/.config/nextest.toml +++ /dev/null @@ -1,2 +0,0 @@ -[profile.ci.junit] -path = "junit.xml" diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7ae598db1fd..108bb84e1e0 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -22,9 +32,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.69" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arbitrary" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db55d72333851e17d572bec876e390cd3b11eb1ef53ae821dd9f3b653d2b4569" [[package]] name = "arbitrary" @@ -136,9 +152,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bcs" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b06b4c1f053002b70e7084ac944c77d58d5d92b2110dbc5e852735e00ad3ccc" +checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" dependencies = [ "serde", "thiserror", @@ -162,6 +178,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitcoin" version = "0.30.1" @@ -196,11 +221,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitreader" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "bitstream-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" + [[package]] name = "bitvec" -version = "1.0.1" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" dependencies = [ "funty", "radium", @@ -221,13 +261,11 @@ dependencies = [ [[package]] name = "blake2" -version = "0.9.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "crypto-mac", - "digest 0.9.0", - "opaque-debug", + "digest 0.10.6", ] [[package]] @@ -254,6 +292,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.37", + "syn_derive", +] + [[package]] name = "bs58" version = "0.4.0" @@ -296,6 +358,50 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "2.34.0" @@ -319,13 +425,28 @@ checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crunchy" version = "0.2.2" @@ -339,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -351,37 +472,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] [[package]] -name = "crypto-mac" -version = "0.8.0" +name = "crypto_box" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +dependencies = [ + "aead", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" dependencies = [ + "aead", + "cipher", "generic-array", + "poly1305", + "salsa20", "subtle", + "zeroize", ] [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.6", + "fiat-crypto", + "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" @@ -393,6 +548,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" @@ -469,7 +630,7 @@ dependencies = [ "group", "hkdf", "pkcs8", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -494,23 +655,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +[[package]] +name = "ethnum" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8ff382b2fa527fb7fb06eeebfc5bbb3f17e3cc6b9d70b006c41daa8824adac" + [[package]] name = "ff" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" + [[package]] name = "fixed-hash" -version = "0.8.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ - "arbitrary", + "arbitrary 0.4.7", "byteorder", "rand", "rustc-hex", @@ -519,32 +692,21 @@ dependencies = [ [[package]] name = "funty" -version = "2.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.9" @@ -554,7 +716,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -574,16 +736,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -634,13 +808,22 @@ dependencies = [ [[package]] name = "impl-codec" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.2" @@ -662,6 +845,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itertools" version = "0.10.5" @@ -696,7 +888,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.6", + "sha2", "signature", ] @@ -717,9 +909,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" @@ -745,16 +937,20 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "move-core-types" version = "0.0.4" -source = "git+https://github.com/move-language/move?rev=f7137eabc2046f76fdad3ded2c51e03a3b1fbd01#f7137eabc2046f76fdad3ded2c51e03a3b1fbd01" +source = "git+https://github.com/move-language/move?rev=ea70797099baea64f05194a918cebd69ed02b285#ea70797099baea64f05194a918cebd69ed02b285" dependencies = [ "anyhow", "bcs", + "ethnum", "hex", + "num", "once_cell", + "primitive-types", "rand", "ref-cast", "serde", "serde_bytes", + "uint", ] [[package]] @@ -767,47 +963,91 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", + "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p256" @@ -818,14 +1058,14 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.6", + "sha2", ] [[package]] name = "parity-scale-codec" -version = "3.5.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ddb756ca205bd108aee3c62c6d3c994e1df84a59b9d6d4a5ea42ee1fd5a9a28" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec", "bitvec", @@ -837,11 +1077,11 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.1.4" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.107", @@ -875,6 +1115,17 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -892,12 +1143,13 @@ dependencies = [ [[package]] name = "primitive-types" -version = "0.12.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" dependencies = [ "fixed-hash", "impl-codec", + "impl-serde", "uint", ] @@ -908,14 +1160,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.11", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -937,18 +1221,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "radium" -version = "0.7.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" [[package]] name = "rand" @@ -958,7 +1242,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -968,16 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -986,7 +1261,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.9", + "getrandom", ] [[package]] @@ -1036,21 +1311,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "ripemd" version = "0.1.3" @@ -1085,12 +1345,27 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "sec1" version = "0.7.1" @@ -1133,38 +1408,38 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1172,27 +1447,25 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.10.5" +name = "serde_repr" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.6", + "proc-macro2", + "quote", + "syn 2.0.37", ] [[package]] -name = "sha2" -version = "0.9.9" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest 0.10.6", ] [[package]] @@ -1223,15 +1496,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ "digest 0.10.6", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spki" version = "0.7.1" @@ -1255,7 +1522,7 @@ dependencies = [ "num-integer", "num-traits", "rfc6979", - "sha2 0.10.6", + "sha2", "starknet-crypto-codegen", "starknet-curve", "starknet-ff", @@ -1270,7 +1537,7 @@ checksum = "e6dc88f1f470d9de1001ffbb90d2344c9dd1a615f5467daf0574e2975dfd9ebd" dependencies = [ "starknet-curve", "starknet-ff", - "syn 2.0.15", + "syn 2.0.37", ] [[package]] @@ -1291,7 +1558,7 @@ dependencies = [ "ark-ff", "bigdecimal", "crypto-bigint", - "getrandom 0.2.9", + "getrandom", "hex", "serde", ] @@ -1308,6 +1575,25 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "subtle" version = "2.4.1" @@ -1327,9 +1613,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -1337,15 +1623,15 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn_derive" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" dependencies = [ + "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.107", - "unicode-xid", + "syn 2.0.37", ] [[package]] @@ -1394,9 +1680,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" @@ -1406,24 +1692,94 @@ checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.4.7", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", ] [[package]] name = "tw_any_coin" version = "0.1.0" dependencies = [ - "tw_any_coin", "tw_coin_entry", "tw_coin_registry", "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", "tw_misc", + "tw_proto", +] + +[[package]] +name = "tw_aptos" +version = "0.1.0" +dependencies = [ + "move-core-types", + "serde", + "serde_bytes", + "serde_json", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", "tw_number", "tw_proto", ] +[[package]] +name = "tw_base58_address" +version = "0.1.0" +dependencies = [ + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", +] + +[[package]] +name = "tw_bech32_address" +version = "0.1.0" +dependencies = [ + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", +] + +[[package]] +name = "tw_binance" +version = "0.1.0" +dependencies = [ + "quick-protobuf", + "serde", + "serde_json", + "serde_repr", + "strum_macros", + "tw_bech32_address", + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_encoding", + "tw_evm", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_proto", +] + [[package]] name = "tw_bitcoin" version = "0.1.0" @@ -1432,21 +1788,27 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "tw_base58_address", + "tw_bech32_address", "tw_coin_entry", "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", "tw_misc", "tw_proto", "tw_utxo", - "wallet-core-rs", ] [[package]] name = "tw_coin_entry" version = "0.1.0" dependencies = [ + "derivation-path", + "serde", "serde_json", + "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", "tw_misc", @@ -1458,27 +1820,78 @@ dependencies = [ name = "tw_coin_registry" version = "0.1.0" dependencies = [ + "itertools", "lazy_static", "serde", "serde_json", + "strum", + "strum_macros", + "tw_aptos", + "tw_binance", "tw_bitcoin", "tw_coin_entry", + "tw_cosmos", "tw_ethereum", "tw_evm", + "tw_greenfield", + "tw_hash", + "tw_internet_computer", "tw_keypair", "tw_memory", "tw_misc", + "tw_native_evmos", + "tw_native_injective", "tw_ronin", + "tw_solana", + "tw_sui", + "tw_thorchain", + "tw_ton", + "tw_utxo", +] + +[[package]] +name = "tw_cosmos" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_keypair", + "tw_proto", +] + +[[package]] +name = "tw_cosmos_sdk" +version = "0.1.0" +dependencies = [ + "pb-rs", + "quick-protobuf", + "serde", + "serde_json", + "tw_bech32_address", + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", ] [[package]] name = "tw_encoding" version = "0.1.0" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", + "bcs", + "bech32", "bs58", + "ciborium", "data-encoding", "hex", + "serde", + "serde_bytes", "tw_memory", ] @@ -1513,11 +1926,29 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_greenfield" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_encoding", + "tw_evm", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", +] + [[package]] name = "tw_hash" version = "0.1.0" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", "blake-hash", "blake2b-ref", "digest 0.10.6", @@ -1527,35 +1958,55 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2 0.10.6", + "sha2", "sha3", "tw_encoding", "tw_memory", "zeroize", ] +[[package]] +name = "tw_internet_computer" +version = "0.1.0" +dependencies = [ + "pb-rs", + "quick-protobuf", + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "tw_keypair" version = "0.1.0" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", + "bitcoin", "blake2", + "crypto_box", "curve25519-dalek", "der", - "digest 0.9.0", + "digest 0.10.6", "ecdsa", "k256", "lazy_static", "p256", + "pkcs8", + "rand_core", "rfc6979", - "ring", + "secp256k1", "serde", "serde_json", - "sha2 0.9.9", + "sha2", "starknet-crypto", "starknet-ff", "tw_encoding", "tw_hash", + "tw_keypair", "tw_memory", "tw_misc", "zeroize", @@ -1569,24 +2020,40 @@ version = "0.1.0" name = "tw_misc" version = "0.1.0" dependencies = [ + "serde", + "serde_json", "zeroize", ] [[package]] -name = "tw_move_parser" +name = "tw_native_evmos" version = "0.1.0" dependencies = [ - "bcs", - "hex", - "move-core-types", + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + +[[package]] +name = "tw_native_injective" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_hash", + "tw_keypair", "tw_memory", + "tw_proto", ] [[package]] name = "tw_number" version = "0.1.0" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", "lazy_static", "primitive-types", "serde", @@ -1599,11 +2066,9 @@ dependencies = [ name = "tw_proto" version = "0.1.0" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", "pb-rs", "quick-protobuf", - "tw_encoding", - "tw_memory", ] [[package]] @@ -1619,16 +2084,127 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_solana" +version = "0.1.0" +dependencies = [ + "bincode", + "borsh", + "lazy_static", + "serde", + "serde_json", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + +[[package]] +name = "tw_sui" +version = "0.1.0" +dependencies = [ + "indexmap", + "move-core-types", + "serde", + "serde_repr", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + +[[package]] +name = "tw_tests" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tw_any_coin", + "tw_coin_entry", + "tw_coin_registry", + "tw_cosmos_sdk", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", + "tw_solana", + "tw_ton", + "tw_ton_sdk", + "tw_utxo", + "wallet-core-rs", +] + +[[package]] +name = "tw_thorchain" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_cosmos_sdk", + "tw_keypair", + "tw_memory", + "tw_proto", +] + +[[package]] +name = "tw_ton" +version = "0.1.0" +dependencies = [ + "lazy_static", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", + "tw_ton_sdk", +] + +[[package]] +name = "tw_ton_sdk" +version = "0.1.0" +dependencies = [ + "bitreader", + "bitstream-io", + "crc", + "lazy_static", + "num-bigint", + "serde", + "serde_json", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", +] + [[package]] name = "tw_utxo" version = "0.1.0" dependencies = [ + "bech32", "bitcoin", + "byteorder", + "itertools", "secp256k1", + "strum_macros", + "tw_base58_address", + "tw_bech32_address", "tw_coin_entry", "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_proto", ] @@ -1644,7 +2220,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ - "arbitrary", + "arbitrary 1.3.0", "byteorder", "crunchy", "hex", @@ -1664,16 +2240,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "unicode-xid" -version = "0.2.4" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] -name = "untrusted" -version = "0.7.1" +name = "uuid" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] [[package]] name = "vec_map" @@ -1691,10 +2274,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "wallet-core-rs" version = "0.1.0" dependencies = [ - "serde_json", + "bitreader", "tw_any_coin", "tw_bitcoin", - "tw_coin_entry", "tw_coin_registry", "tw_encoding", "tw_ethereum", @@ -1702,16 +2284,21 @@ dependencies = [ "tw_keypair", "tw_memory", "tw_misc", - "tw_move_parser", "tw_number", "tw_proto", + "tw_solana", + "tw_ton", + "uuid", ] [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +name = "wallet_core_bin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tw_coin_registry", +] [[package]] name = "wasi" @@ -1773,16 +2360,6 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1824,31 +2401,36 @@ dependencies = [ ] [[package]] -name = "wyz" -version = "0.5.1" +name = "winnow" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ - "tap", + "memchr", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", - "synstructure", + "syn 2.0.37", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b8fdf347fca..ca41fb6099d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,24 +1,45 @@ [workspace] members = [ + "chains/tw_aptos", + "chains/tw_binance", + "chains/tw_bitcoin", + "chains/tw_cosmos", + "chains/tw_ethereum", + "chains/tw_greenfield", + "chains/tw_internet_computer", + "chains/tw_native_evmos", + "chains/tw_native_injective", + "chains/tw_ronin", + "chains/tw_solana", + "chains/tw_sui", + "chains/tw_thorchain", + "chains/tw_ton", + "frameworks/tw_ton_sdk", + "frameworks/tw_utxo", "tw_any_coin", - "tw_bitcoin", + "tw_base58_address", + "tw_bech32_address", "tw_coin_entry", "tw_coin_registry", + "tw_cosmos_sdk", "tw_encoding", - "tw_ethereum", "tw_evm", "tw_hash", "tw_keypair", "tw_memory", "tw_misc", - "tw_move_parser", "tw_number", "tw_proto", - "tw_ronin", - "tw_utxo", + "tw_tests", + "wallet_core_bin", "wallet_core_rs", ] +[profile.release] +strip = true +codegen-units = 1 +panic = "abort" + [profile.wasm-test] inherits = "release" # Fixes an incredibly slow compilation of `curve25519-dalek` package. diff --git a/rust/chains/tw_aptos/Cargo.toml b/rust/chains/tw_aptos/Cargo.toml new file mode 100644 index 00000000000..d78b0e3c644 --- /dev/null +++ b/rust/chains/tw_aptos/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tw_aptos" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde_json = "1.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_keypair = { path = "../../tw_keypair" } +tw_proto = { path = "../../tw_proto" } +tw_number = { path = "../../tw_number" } +tw_hash = { path = "../../tw_hash" } +tw_memory = { path = "../../tw_memory" } +move-core-types = { git = "https://github.com/move-language/move", rev = "ea70797099baea64f05194a918cebd69ed02b285", features = ["address32"] } +serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11.12" + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/chains/tw_aptos/fuzz/.gitignore b/rust/chains/tw_aptos/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_aptos/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_aptos/fuzz/Cargo.toml b/rust/chains/tw_aptos/fuzz/Cargo.toml new file mode 100644 index 00000000000..721a84b3ab9 --- /dev/null +++ b/rust/chains/tw_aptos/fuzz/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tw_aptos-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_proto = { path = "../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_aptos] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_aptos/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_aptos/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..5d55e467d14 --- /dev/null +++ b/rust/chains/tw_aptos/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_aptos::signer::Signer; +use tw_proto::Aptos::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let _ = Signer::sign_proto(input); +}); diff --git a/rust/chains/tw_aptos/src/address.rs b/rust/chains/tw_aptos/src/address.rs new file mode 100644 index 00000000000..3915750c625 --- /dev/null +++ b/rust/chains/tw_aptos/src/address.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use move_core_types::account_address::{AccountAddress, AccountAddressParseError}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_hash::sha3::sha3_256; +use tw_keypair::ed25519; +use tw_memory::Data; + +#[repr(u8)] +pub enum Scheme { + Ed25519 = 0, +} + +#[derive(Clone)] +pub struct Address { + addr: AccountAddress, +} + +impl Address { + pub const LENGTH: usize = AccountAddress::LENGTH; + + /// Initializes an address with a `ed25519` public key. + pub fn with_ed25519_pubkey( + pubkey: &ed25519::sha512::PublicKey, + ) -> Result { + let mut to_hash = pubkey.as_slice().to_vec(); + to_hash.push(Scheme::Ed25519 as u8); + let hashed = sha3_256(to_hash.as_slice()); + let addr = AccountAddress::from_bytes(hashed).map_err(from_account_error)?; + Ok(Address { addr }) + } + + pub fn inner(&self) -> AccountAddress { + self.addr + } +} + +impl Display for Address { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.addr.to_hex_literal()) + } +} + +impl CoinAddress for Address { + #[inline] + fn data(&self) -> Data { + self.addr.to_vec() + } +} + +#[inline] +pub fn from_account_error(_err: AccountAddressParseError) -> AddressError { + AddressError::InvalidInput +} + +impl FromStr for Address { + type Err = AddressError; + + // https://github.com/aptos-labs/aptos-core/blob/261019cbdafe1c514c60c2b74357ea2c77d25e67/types/src/account_address.rs#L44 + fn from_str(s: &str) -> Result { + const NUM_CHARS: usize = AccountAddress::LENGTH * 2; + let mut has_0x = false; + let mut working = s.trim(); + + // Checks if it has a 0x at the beginning, which is okay + if working.starts_with("0x") { + has_0x = true; + working = &working[2..]; + } + + if working.len() > NUM_CHARS || (!has_0x && working.len() < NUM_CHARS) { + return Err(AddressError::InvalidInput); + } + + if !working.chars().all(|c| char::is_ascii_hexdigit(&c)) { + return Err(AddressError::InvalidInput); + } + + let addr = if has_0x { + AccountAddress::from_hex_literal(s.trim()) + } else { + AccountAddress::from_str(s.trim()) + } + .map_err(from_account_error)?; + + Ok(Address { addr }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_keypair::ed25519::sha512::PrivateKey; + + #[test] + fn test_from_public_key() { + let private = PrivateKey::try_from( + "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5", + ) + .unwrap(); + let public = private.public(); + let addr = Address::with_ed25519_pubkey(&public); + assert_eq!( + addr.as_ref().unwrap().to_string(), + "0x9006fa46f038224e8004bdda97f2e7a60c2c3d135bce7cb15541e5c0aae907a4" + ); + assert_eq!(addr.unwrap().data().len(), Address::LENGTH); + } + + #[test] + fn test_from_account_error() { + assert_eq!( + from_account_error(AccountAddressParseError {}), + AddressError::InvalidInput + ); + } +} diff --git a/rust/chains/tw_aptos/src/aptos_move_packages.rs b/rust/chains/tw_aptos/src/aptos_move_packages.rs new file mode 100644 index 00000000000..e90e6bf2a4b --- /dev/null +++ b/rust/chains/tw_aptos/src/aptos_move_packages.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction_payload::{EntryFunction, TransactionPayload}; +use move_core_types::account_address::AccountAddress; +use move_core_types::ident_str; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use serde_json::json; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bcs; + +pub fn aptos_account_transfer( + to: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("aptos_account").to_owned(), + ), + ident_str!("transfer").to_owned(), + vec![], + vec![bcs::encode(&to)?, bcs::encode(&amount)?], + json!([to.to_hex_literal(), amount.to_string()]), + ))) +} + +pub fn aptos_account_create_account(auth_key: AccountAddress) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("aptos_account").to_owned(), + ), + ident_str!("create_account").to_owned(), + vec![], + vec![bcs::encode(&auth_key)?], + json!([auth_key.to_hex_literal()]), + ))) +} + +pub fn coin_transfer( + coin_type: TypeTag, + to: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("coin").to_owned(), + ), + ident_str!("transfer").to_owned(), + vec![coin_type], + vec![bcs::encode(&to)?, bcs::encode(&amount)?], + json!([to.to_hex_literal(), amount.to_string()]), + ))) +} + +pub fn aptos_account_transfer_coins( + coin_type: TypeTag, + to: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("aptos_account").to_owned(), + ), + ident_str!("transfer_coins").to_owned(), + vec![coin_type], + vec![bcs::encode(&to)?, bcs::encode(&amount)?], + json!([to.to_hex_literal(), amount.to_string()]), + ))) +} + +pub fn token_transfers_offer_script( + receiver: AccountAddress, + creator: AccountAddress, + collection: Vec, + name: Vec, + property_version: u64, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 3, + ]), + ident_str!("token_transfers").to_owned(), + ), + ident_str!("offer_script").to_owned(), + vec![], + vec![ + bcs::encode(&receiver)?, + bcs::encode(&creator)?, + bcs::encode(&collection)?, + bcs::encode(&name)?, + bcs::encode(&property_version)?, + bcs::encode(&amount)?, + ], + json!([ + receiver.to_hex_literal(), + creator.to_hex_literal(), + String::from_utf8_lossy(&collection), + String::from_utf8_lossy(&name), + property_version.to_string(), + amount.to_string() + ]), + ))) +} + +pub fn token_transfers_cancel_offer_script( + receiver: AccountAddress, + creator: AccountAddress, + collection: Vec, + name: Vec, + property_version: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 3, + ]), + ident_str!("token_transfers").to_owned(), + ), + ident_str!("cancel_offer_script").to_owned(), + vec![], + vec![ + bcs::encode(&receiver)?, + bcs::encode(&creator)?, + bcs::encode(&collection)?, + bcs::encode(&name)?, + bcs::encode(&property_version)?, + ], + json!([ + receiver.to_hex_literal(), + creator.to_hex_literal(), + String::from_utf8_lossy(&collection), + String::from_utf8_lossy(&name), + property_version.to_string() + ]), + ))) +} + +pub fn token_transfers_claim_script( + sender: AccountAddress, + creator: AccountAddress, + collection: Vec, + name: Vec, + property_version: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 3, + ]), + ident_str!("token_transfers").to_owned(), + ), + ident_str!("claim_script").to_owned(), + vec![], + vec![ + bcs::encode(&sender)?, + bcs::encode(&creator)?, + bcs::encode(&collection)?, + bcs::encode(&name)?, + bcs::encode(&property_version)?, + ], + json!([ + sender.to_hex_literal(), + creator.to_hex_literal(), + String::from_utf8_lossy(&collection), + String::from_utf8_lossy(&name), + property_version.to_string() + ]), + ))) +} + +pub fn managed_coin_register(coin_type: TypeTag) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("managed_coin").to_owned(), + ), + ident_str!("register").to_owned(), + vec![coin_type], + vec![], + json!([]), + )) +} diff --git a/rust/chains/tw_aptos/src/compiler.rs b/rust/chains/tw_aptos/src/compiler.rs new file mode 100644 index 00000000000..53b9df11bd4 --- /dev/null +++ b/rust/chains/tw_aptos/src/compiler.rs @@ -0,0 +1,76 @@ +use crate::address::Address; +use crate::transaction_builder; +use std::str::FromStr; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::Aptos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct Compiler; + +impl Compiler { + #[inline] + pub fn preimage_hashes( + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let builder = transaction_builder::TransactionFactory::new_from_protobuf(input.clone())?; + let sender = Address::from_str(&input.sender) + .into_tw() + .context("Invalid sender address")?; + let signed_tx = builder + .sender(sender.inner()) + .sequence_number(input.sequence_number as u64) + .build()? + .pre_image()?; + Ok(CompilerProto::PreSigningOutput { + data: signed_tx.into(), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let builder = transaction_builder::TransactionFactory::new_from_protobuf(input.clone())?; + let sender = Address::from_str(&input.sender)?; + let signature = signatures + .first() + .or_tw_err(SigningErrorType::Error_signatures_count)?; + let public_key = public_keys + .first() + .or_tw_err(SigningErrorType::Error_signatures_count)?; + + let signed_tx = builder + .sender(sender.inner()) + .sequence_number(input.sequence_number as u64) + .build()? + .compile(signature.to_vec(), public_key.to_vec())?; + Ok(Proto::SigningOutput { + raw_txn: signed_tx.raw_txn_bytes().clone().into(), + encoded: signed_tx.encoded().clone().into(), + authenticator: Some((*signed_tx.authenticator()).clone().into()), + json: signed_tx.to_json().to_string().into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_aptos/src/constants.rs b/rust/chains/tw_aptos/src/constants.rs new file mode 100644 index 00000000000..714c1a2f782 --- /dev/null +++ b/rust/chains/tw_aptos/src/constants.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub const GAS_UNIT_PRICE: u64 = 100; +pub const MAX_GAS_AMOUNT: u64 = 100_000_000; +pub const APTOS_SALT: &[u8] = b"APTOS::RawTransaction"; diff --git a/rust/chains/tw_aptos/src/entry.rs b/rust/chains/tw_aptos/src/entry.rs new file mode 100644 index 00000000000..f9a46c55049 --- /dev/null +++ b/rust/chains/tw_aptos/src/entry.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::Address; +use crate::compiler::Compiler; +use crate::modules::transaction_util::AptosTransactionUtil; +use crate::signer::Signer; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Aptos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct AptosEntry; + +impl CoinEntry for AptosEntry { + type AddressPrefix = NoPrefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = AptosTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let public_key = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Address::with_ed25519_pubkey(public_key) + } + + #[inline] + fn sign(&self, _coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + Signer::sign_proto(input) + } + + #[inline] + fn preimage_hashes( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + Compiler::preimage_hashes(input) + } + + #[inline] + fn compile( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + Compiler::compile(input, signatures, public_keys) + } + + #[inline] + fn json_signer(&self) -> Option { + None + } + + #[inline] + fn message_signer(&self) -> Option { + None + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(AptosTransactionUtil) + } +} diff --git a/rust/chains/tw_aptos/src/lib.rs b/rust/chains/tw_aptos/src/lib.rs new file mode 100644 index 00000000000..5388e03f4e7 --- /dev/null +++ b/rust/chains/tw_aptos/src/lib.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod aptos_move_packages; +pub mod constants; +pub mod entry; +mod serde_helper; + +pub mod nft; + +pub mod compiler; +pub mod liquid_staking; +pub mod modules; +pub mod signer; +pub mod transaction; +pub mod transaction_builder; +pub mod transaction_payload; diff --git a/rust/chains/tw_aptos/src/liquid_staking.rs b/rust/chains/tw_aptos/src/liquid_staking.rs new file mode 100644 index 00000000000..2ce311e4350 --- /dev/null +++ b/rust/chains/tw_aptos/src/liquid_staking.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::from_account_error; +use crate::transaction_payload::{EntryFunction, TransactionPayload}; +use move_core_types::{account_address::AccountAddress, ident_str, language_storage::ModuleId}; +use serde_json::json; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bcs; +use tw_proto::{ + Aptos::Proto::mod_LiquidStaking::OneOfliquid_stake_transaction_payload, + Aptos::Proto::{LiquidStaking, TortugaClaim, TortugaStake, TortugaUnstake}, +}; + +pub fn tortuga_stake( + smart_contract_address: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + smart_contract_address, + ident_str!("stake_router").to_owned(), + ), + ident_str!("stake").to_owned(), + vec![], + vec![bcs::encode(&amount)?], + json!([amount.to_string()]), + ))) +} + +pub fn tortuga_unstake( + smart_contract_address: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + smart_contract_address, + ident_str!("stake_router").to_owned(), + ), + ident_str!("unstake").to_owned(), + vec![], + vec![bcs::encode(&amount)?], + json!([amount.to_string()]), + ))) +} + +pub fn tortuga_claim( + smart_contract_address: AccountAddress, + idx: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + smart_contract_address, + ident_str!("stake_router").to_owned(), + ), + ident_str!("claim").to_owned(), + vec![], + vec![bcs::encode(&idx)?], + json!([idx.to_string()]), + ))) +} + +pub struct Stake { + pub amount: u64, + pub smart_contract_address: AccountAddress, +} + +pub struct Unstake { + pub amount: u64, + pub smart_contract_address: AccountAddress, +} + +pub struct Claim { + pub idx: u64, + pub smart_contract_address: AccountAddress, +} + +pub enum LiquidStakingOperation { + Stake(Stake), + Unstake(Unstake), + Claim(Claim), +} + +impl TryFrom> for LiquidStakingOperation { + type Error = SigningError; + + fn try_from(value: LiquidStaking) -> SigningResult { + match value.liquid_stake_transaction_payload { + OneOfliquid_stake_transaction_payload::stake(stake_msg) => { + let smart_contract_address = + AccountAddress::from_str(&value.smart_contract_address) + .map_err(from_account_error) + .into_tw() + .context("Invalid Smart Contract address")?; + Ok(LiquidStakingOperation::Stake(Stake { + amount: stake_msg.amount, + smart_contract_address, + })) + }, + OneOfliquid_stake_transaction_payload::unstake(unstake_msg) => { + let smart_contract_address = + AccountAddress::from_str(&value.smart_contract_address) + .map_err(from_account_error) + .into_tw() + .context("Invalid Smart Contract address")?; + Ok(LiquidStakingOperation::Unstake(Unstake { + amount: unstake_msg.amount, + smart_contract_address, + })) + }, + OneOfliquid_stake_transaction_payload::claim(claim) => { + let smart_contract_address = + AccountAddress::from_str(&value.smart_contract_address) + .map_err(from_account_error) + .into_tw() + .context("Invalid Smart Contract address")?; + Ok(LiquidStakingOperation::Claim(Claim { + idx: claim.idx, + smart_contract_address, + })) + }, + OneOfliquid_stake_transaction_payload::None => { + SigningError::err(SigningErrorType::Error_invalid_params) + }, + } + } +} + +impl From for LiquidStaking<'_> { + fn from(value: LiquidStakingOperation) -> Self { + match value { + LiquidStakingOperation::Stake(stake) => LiquidStaking { + smart_contract_address: stake.smart_contract_address.to_hex_literal().into(), + liquid_stake_transaction_payload: OneOfliquid_stake_transaction_payload::stake( + TortugaStake { + amount: stake.amount, + }, + ), + }, + LiquidStakingOperation::Unstake(unstake) => LiquidStaking { + smart_contract_address: unstake.smart_contract_address.to_hex_literal().into(), + liquid_stake_transaction_payload: OneOfliquid_stake_transaction_payload::unstake( + TortugaUnstake { + amount: unstake.amount, + }, + ), + }, + LiquidStakingOperation::Claim(claim) => LiquidStaking { + smart_contract_address: claim.smart_contract_address.to_hex_literal().into(), + liquid_stake_transaction_payload: OneOfliquid_stake_transaction_payload::claim( + TortugaClaim { idx: claim.idx }, + ), + }, + } + } +} diff --git a/rust/chains/tw_aptos/src/modules/mod.rs b/rust/chains/tw_aptos/src/modules/mod.rs new file mode 100644 index 00000000000..c083bb0102e --- /dev/null +++ b/rust/chains/tw_aptos/src/modules/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod transaction_util; diff --git a/rust/chains/tw_aptos/src/modules/transaction_util.rs b/rust/chains/tw_aptos/src/modules/transaction_util.rs new file mode 100644 index 00000000000..6b7034477f8 --- /dev/null +++ b/rust/chains/tw_aptos/src/modules/transaction_util.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_encoding::hex; +use tw_hash::sha3::sha3_256; + +pub struct AptosTransactionUtil; + +impl TransactionUtil for AptosTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +impl AptosTransactionUtil { + fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + let txn_bytes = hex::decode(encoded_tx).map_err(|_| SigningErrorType::Error_input_parse)?; + + // See: https://github.com/aptos-labs/aptos-ts-sdk/blob/f54cac824a41e41dea09c7a6916858a8604dc901/src/api/transaction.ts#L118 + let prefix = sha3_256("APTOS::Transaction".as_bytes()); + + let mut hash_message = Vec::new(); + hash_message.extend_from_slice(&prefix); + // 0 is the index of the enum `Transaction`, see: https://github.com/aptos-labs/aptos-core/blob/6a130c1cca274a5cfdb4a65b441cd5fe61b6c15b/types/src/transaction/mod.rs#L1939 + hash_message.push(0); + hash_message.extend_from_slice(&txn_bytes); + + let tx_hash = sha3_256(&hash_message); + Ok(hex::encode(tx_hash, true)) + } +} diff --git a/rust/chains/tw_aptos/src/nft.rs b/rust/chains/tw_aptos/src/nft.rs new file mode 100644 index 00000000000..172308b4ee2 --- /dev/null +++ b/rust/chains/tw_aptos/src/nft.rs @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::from_account_error; +use move_core_types::account_address::AccountAddress; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_proto::Aptos::Proto::mod_NftMessage::OneOfnft_transaction_payload; +use tw_proto::Aptos::Proto::{CancelOfferNftMessage, ClaimNftMessage, NftMessage, OfferNftMessage}; + +pub struct Offer { + pub receiver: AccountAddress, + pub creator: AccountAddress, + pub collection: Vec, + pub name: Vec, + pub property_version: u64, + pub amount: u64, +} + +pub struct Claim { + pub sender: AccountAddress, + pub creator: AccountAddress, + pub collection: Vec, + pub name: Vec, + pub property_version: u64, +} + +pub enum NftOperation { + Claim(Claim), + Offer(Offer), + Cancel(Offer), +} + +impl TryFrom> for NftOperation { + type Error = SigningError; + + fn try_from(value: NftMessage) -> SigningResult { + match value.nft_transaction_payload { + OneOfnft_transaction_payload::offer_nft(msg) => { + Ok(NftOperation::Offer(Offer::try_from(msg)?)) + }, + OneOfnft_transaction_payload::cancel_offer_nft(msg) => { + Ok(NftOperation::Cancel(Offer::try_from(msg)?)) + }, + OneOfnft_transaction_payload::claim_nft(msg) => { + Ok(NftOperation::Claim(Claim::try_from(msg)?)) + }, + OneOfnft_transaction_payload::None => { + SigningError::err(SigningErrorType::Error_invalid_params) + .context("No transaction payload provided") + }, + } + } +} + +impl From for NftMessage<'_> { + fn from(value: NftOperation) -> Self { + match value { + NftOperation::Claim(claim) => NftMessage { + nft_transaction_payload: OneOfnft_transaction_payload::claim_nft(claim.into()), + }, + NftOperation::Offer(offer) => NftMessage { + nft_transaction_payload: OneOfnft_transaction_payload::offer_nft(offer.into()), + }, + NftOperation::Cancel(cancel) => NftMessage { + nft_transaction_payload: OneOfnft_transaction_payload::cancel_offer_nft( + cancel.into(), + ), + }, + } + } +} + +impl TryFrom> for Offer { + type Error = SigningError; + + fn try_from(value: OfferNftMessage) -> SigningResult { + Ok(Offer { + receiver: AccountAddress::from_str(&value.receiver).map_err(from_account_error)?, + creator: AccountAddress::from_str(&value.creator).map_err(from_account_error)?, + collection: value.collectionName.as_bytes().to_vec(), + name: value.name.as_bytes().to_vec(), + property_version: value.property_version, + amount: value.amount, + }) + } +} + +impl From for OfferNftMessage<'_> { + fn from(value: Offer) -> Self { + OfferNftMessage { + receiver: value.receiver.to_hex_literal().into(), + creator: value.creator.to_hex_literal().into(), + collectionName: String::from_utf8_lossy(value.collection.as_slice()) + .to_string() + .into(), + name: String::from_utf8_lossy(&value.name).to_string().into(), + property_version: value.property_version, + amount: value.amount, + } + } +} + +impl TryFrom> for Offer { + type Error = SigningError; + + fn try_from(value: CancelOfferNftMessage) -> SigningResult { + Ok(Offer { + receiver: AccountAddress::from_str(&value.receiver) + .map_err(from_account_error) + .into_tw() + .context("Invalid receiver address")?, + creator: AccountAddress::from_str(&value.creator) + .map_err(from_account_error) + .into_tw() + .context("Invalid creator address")?, + collection: value.collectionName.as_bytes().to_vec(), + name: value.name.as_bytes().to_vec(), + property_version: value.property_version, + amount: 0, + }) + } +} + +impl From for CancelOfferNftMessage<'_> { + fn from(value: Offer) -> Self { + CancelOfferNftMessage { + receiver: value.receiver.to_hex_literal().into(), + creator: value.creator.to_hex_literal().into(), + collectionName: String::from_utf8_lossy(value.collection.as_slice()) + .to_string() + .into(), + name: String::from_utf8_lossy(value.name.as_slice()) + .to_string() + .into(), + property_version: value.property_version, + } + } +} + +impl TryFrom> for Claim { + type Error = SigningError; + + fn try_from(value: ClaimNftMessage) -> SigningResult { + Ok(Claim { + sender: AccountAddress::from_str(&value.sender) + .map_err(from_account_error) + .into_tw() + .context("Invalid sender address")?, + creator: AccountAddress::from_str(&value.creator) + .map_err(from_account_error) + .into_tw() + .context("Invalid creator address")?, + collection: value.collectionName.as_bytes().to_vec(), + name: value.name.as_bytes().to_vec(), + property_version: value.property_version, + }) + } +} + +impl From for ClaimNftMessage<'_> { + fn from(value: Claim) -> Self { + ClaimNftMessage { + sender: value.sender.to_hex_literal().into(), + creator: value.creator.to_hex_literal().into(), + collectionName: String::from_utf8_lossy(value.collection.as_slice()) + .to_string() + .into(), + name: String::from_utf8_lossy(value.name.as_slice()) + .to_string() + .into(), + property_version: value.property_version, + } + } +} diff --git a/rust/chains/tw_aptos/src/serde_helper/mod.rs b/rust/chains/tw_aptos/src/serde_helper/mod.rs new file mode 100644 index 00000000000..876f018225e --- /dev/null +++ b/rust/chains/tw_aptos/src/serde_helper/mod.rs @@ -0,0 +1,5 @@ +// Copyright © Aptos Foundation +// Parts of the project are originally copyright © Meta Platforms, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod vec_bytes; diff --git a/rust/chains/tw_aptos/src/serde_helper/vec_bytes.rs b/rust/chains/tw_aptos/src/serde_helper/vec_bytes.rs new file mode 100644 index 00000000000..f57311d19dd --- /dev/null +++ b/rust/chains/tw_aptos/src/serde_helper/vec_bytes.rs @@ -0,0 +1,30 @@ +// Copyright © Aptos Foundation +// Parts of the project are originally copyright © Meta Platforms, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use serde::{ + de::Deserializer, + ser::{SerializeSeq, Serializer}, + Deserialize, +}; + +pub fn serialize(data: &[Vec], serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(data.len()))?; + for e in data { + seq.serialize_element(serde_bytes::Bytes::new(e.as_slice()))?; + } + seq.end() +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + Ok(>::deserialize(deserializer)? + .into_iter() + .map(serde_bytes::ByteBuf::into_vec) + .collect()) +} diff --git a/rust/chains/tw_aptos/src/signer.rs b/rust/chains/tw_aptos/src/signer.rs new file mode 100644 index 00000000000..8937e821081 --- /dev/null +++ b/rust/chains/tw_aptos/src/signer.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::Address; +use crate::transaction_builder; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519; +use tw_proto::Aptos::Proto; + +pub struct Signer; + +impl Signer { + #[inline] + pub fn sign_proto(input: Proto::SigningInput<'_>) -> Proto::SigningOutput<'static> { + Self::sign_proto_impl(input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_proto_impl( + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let key_pair = ed25519::sha512::KeyPair::try_from(input.private_key.as_ref())?; + let builder = transaction_builder::TransactionFactory::new_from_protobuf(input.clone())?; + let sender = Address::from_str(&input.sender) + .into_tw() + .context("Invalid sender address")?; + let signed_tx = builder + .sender(sender.inner()) + .sequence_number(input.sequence_number as u64) + .build()? + .sign(key_pair)?; + Ok(Proto::SigningOutput { + raw_txn: signed_tx.raw_txn_bytes().clone().into(), + encoded: signed_tx.encoded().clone().into(), + authenticator: Some((*signed_tx.authenticator()).clone().into()), + json: signed_tx.to_json().to_string().into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_aptos/src/transaction.rs b/rust/chains/tw_aptos/src/transaction.rs new file mode 100644 index 00000000000..f4ca4a2c151 --- /dev/null +++ b/rust/chains/tw_aptos/src/transaction.rs @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::constants::APTOS_SALT; +use crate::transaction_payload::TransactionPayload; +use move_core_types::account_address::AccountAddress; +use serde::Serialize; +use serde_json::{json, Value}; +use std::borrow::Cow; +use tw_coin_entry::error::prelude::*; +use tw_encoding::hex::encode; +use tw_encoding::{bcs, EncodingResult}; +use tw_keypair::ed25519::sha512::KeyPair; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_memory::Data; +use tw_proto::Aptos::Proto; + +#[derive(Clone, Serialize)] +pub enum TransactionAuthenticator { + /// Single Ed25519 signature + Ed25519 { + public_key: Vec, + signature: Vec, + }, +} + +impl From for Proto::TransactionAuthenticator<'_> { + fn from(from: TransactionAuthenticator) -> Self { + Proto::TransactionAuthenticator { + signature: Cow::from(from.get_signature()), + public_key: Cow::from(from.get_public_key()), + } + } +} + +impl TransactionAuthenticator { + pub fn get_signature(&self) -> Vec { + match self { + TransactionAuthenticator::Ed25519 { + public_key: _public_key, + signature, + } => signature.clone(), + } + } + + pub fn get_public_key(&self) -> Vec { + match self { + TransactionAuthenticator::Ed25519 { + public_key, + signature: _signature, + } => public_key.clone(), + } + } + + pub fn to_json(&self) -> Value { + match self { + TransactionAuthenticator::Ed25519 { + public_key, + signature, + } => { + json!({"public_key": encode(public_key, true), + "signature": encode(signature, true), + "type": "ed25519_signature"}) + }, + } + } +} + +/// RawTransaction is the portion of a transaction that a client signs. +#[derive(Clone, Serialize)] +pub struct RawTransaction { + /// Sender's address. + sender: AccountAddress, + + /// Sequence number of this transaction. This must match the sequence number + /// stored in the sender's account at the time the transaction executes. + sequence_number: u64, + + /// The transaction payload, e.g., a script to execute. + payload: TransactionPayload, + + /// Maximal total gas to spend for this transaction. + max_gas_amount: u64, + + /// Price to be paid per gas unit. + gas_unit_price: u64, + + /// Expiration timestamp for this transaction, represented + /// as seconds from the Unix Epoch. If the current blockchain timestamp + /// is greater than or equal to this time, then the transaction has + /// expired and will be discarded. This can be set to a large value far + /// in the future to indicate that a transaction does not expire. + expiration_timestamp_secs: u64, + + /// Chain ID of the Aptos network this transaction is intended for. + chain_id: u8, +} + +impl RawTransaction { + /// Create a new `RawTransaction` with a payload. + /// + /// It can be either to publish a module, to execute a script, or to issue a writeset + /// transaction. + pub fn new( + sender: AccountAddress, + sequence_number: u64, + payload: TransactionPayload, + max_gas_amount: u64, + gas_unit_price: u64, + expiration_timestamp_secs: u64, + chain_id: u8, + ) -> Self { + RawTransaction { + sender, + sequence_number, + payload, + max_gas_amount, + gas_unit_price, + expiration_timestamp_secs, + chain_id, + } + } + + /// Create a new `RawTransaction` with an entry function + fn serialize(&self) -> EncodingResult { + bcs::encode(&self) + } + + fn msg_to_sign(&self) -> SigningResult { + let serialized = self + .serialize() + .into_tw() + .context("Error serializing RawTransaction")?; + let mut preimage = tw_hash::sha3::sha3_256(APTOS_SALT); + preimage.extend_from_slice(serialized.as_slice()); + Ok(preimage) + } + + pub fn pre_image(&self) -> SigningResult> { + self.msg_to_sign() + } + + pub fn compile( + &self, + signature: Vec, + public_key: Vec, + ) -> SigningResult { + let serialized = self.serialize()?; + let auth = TransactionAuthenticator::Ed25519 { + public_key, + signature, + }; + let mut encoded = serialized.clone(); + encoded.extend_from_slice(bcs::encode(&auth)?.as_slice()); + Ok(SignedTransaction { + raw_txn: self.clone(), + authenticator: auth, + raw_txn_bytes: serialized.to_vec(), + encoded, + }) + } + + pub fn sign(self, key_pair: KeyPair) -> SigningResult { + let to_sign = self.pre_image()?; + let signature = key_pair.private().sign(to_sign)?.to_bytes().into_vec(); + let pubkey = key_pair.public().as_slice().to_vec(); + self.compile(signature, pubkey) + } + + pub fn to_json(&self) -> Value { + json!({ + "expiration_timestamp_secs": self.expiration_timestamp_secs.to_string(), + "gas_unit_price": self.gas_unit_price.to_string(), + "max_gas_amount": self.max_gas_amount.to_string(), + "payload": self.payload.to_json(), + "sender": self.sender.to_hex_literal(), + "sequence_number": self.sequence_number.to_string() + }) + } +} + +/// A transaction that has been signed. +/// +/// A `SignedTransaction` is a single transaction that can be atomically executed. Clients submit +/// these to validator nodes, and the validator and executor submits these to the VM. +/// +#[derive(Clone, Serialize)] +pub struct SignedTransaction { + /// The raw transaction + raw_txn: RawTransaction, + + /// Public key and signature to authenticate + authenticator: TransactionAuthenticator, + + #[serde(skip_serializing)] + /// Raw txs bytes + raw_txn_bytes: Vec, + + #[serde(skip_serializing)] + /// Encoded bytes to be broadcast + encoded: Vec, +} + +impl SignedTransaction { + pub fn authenticator(&self) -> &TransactionAuthenticator { + &self.authenticator + } + pub fn raw_txn_bytes(&self) -> &Vec { + &self.raw_txn_bytes + } + pub fn encoded(&self) -> &Vec { + &self.encoded + } + + pub fn to_json(&self) -> Value { + let mut json_value = self.raw_txn.to_json(); + json_value["signature"] = self.authenticator.to_json(); + json_value + } +} diff --git a/rust/chains/tw_aptos/src/transaction_builder.rs b/rust/chains/tw_aptos/src/transaction_builder.rs new file mode 100644 index 00000000000..deae84c8008 --- /dev/null +++ b/rust/chains/tw_aptos/src/transaction_builder.rs @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::from_account_error; +use crate::aptos_move_packages::{ + aptos_account_create_account, aptos_account_transfer, aptos_account_transfer_coins, + coin_transfer, managed_coin_register, token_transfers_cancel_offer_script, + token_transfers_claim_script, token_transfers_offer_script, +}; +use crate::constants::{GAS_UNIT_PRICE, MAX_GAS_AMOUNT}; +use crate::liquid_staking::{ + tortuga_claim, tortuga_stake, tortuga_unstake, LiquidStakingOperation, +}; +use crate::nft::NftOperation; +use crate::transaction::RawTransaction; +use crate::transaction_payload::{ + convert_proto_struct_tag_to_type_tag, EntryFunction, TransactionPayload, +}; +use move_core_types::account_address::AccountAddress; +use move_core_types::language_storage::TypeTag; +use serde_json::Value; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_proto::Aptos::Proto::mod_SigningInput::OneOftransaction_payload; +use tw_proto::Aptos::Proto::SigningInput; + +pub struct TransactionBuilder { + sender: Option, + sequence_number: Option, + payload: TransactionPayload, + max_gas_amount: u64, + gas_unit_price: u64, + expiration_timestamp_secs: u64, + chain_id: u8, +} + +impl TransactionBuilder { + pub fn sender(mut self, sender: AccountAddress) -> Self { + self.sender = Some(sender); + self + } + + pub fn sequence_number(mut self, sequence_number: u64) -> Self { + self.sequence_number = Some(sequence_number); + self + } + + pub fn build(self) -> SigningResult { + let sender = self + .sender + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid sender address")?; + let sequence_number = self + .sequence_number + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid sequence number")?; + Ok(RawTransaction::new( + sender, + sequence_number, + self.payload, + self.max_gas_amount, + self.gas_unit_price, + self.expiration_timestamp_secs, + self.chain_id, + )) + } +} + +#[derive(Clone, Debug)] +pub struct TransactionFactory { + max_gas_amount: u64, + gas_unit_price: u64, + transaction_expiration_time: u64, + chain_id: u8, +} + +impl TransactionFactory { + pub fn new(chain_id: u8) -> Self { + Self { + max_gas_amount: MAX_GAS_AMOUNT, + gas_unit_price: GAS_UNIT_PRICE, + transaction_expiration_time: 30, + chain_id, + } + } + + pub fn new_from_protobuf(input: SigningInput) -> SigningResult { + let factory = TransactionFactory::new(input.chain_id as u8) + .with_gas_unit_price(input.gas_unit_price) + .with_max_gas_amount(input.max_gas_amount) + .with_transaction_expiration_time(input.expiration_timestamp_secs); + match input.transaction_payload { + OneOftransaction_payload::transfer(transfer) => factory + .implicitly_create_user_account_and_transfer( + AccountAddress::from_str(&transfer.to) + .map_err(from_account_error) + .into_tw() + .context("Invalid destination address")?, + transfer.amount, + ), + OneOftransaction_payload::token_transfer(token_transfer) => { + let func = token_transfer + .function + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("'TokenTransferMessage::function' is not set")?; + factory.coins_transfer( + AccountAddress::from_str(&token_transfer.to) + .map_err(from_account_error) + .into_tw() + .context("Invalid destination address")?, + token_transfer.amount, + convert_proto_struct_tag_to_type_tag(func)?, + ) + }, + OneOftransaction_payload::create_account(create_account) => { + let address = AccountAddress::from_str(&create_account.auth_key) + .map_err(from_account_error) + .into_tw() + .context("Invalid 'auth_key' address")?; + factory.create_user_account(address) + }, + OneOftransaction_payload::nft_message(nft_message) => { + factory.nft_ops(NftOperation::try_from(nft_message)?) + }, + OneOftransaction_payload::register_token(register_token) => { + let function = register_token + .function + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("'ManagedTokensRegisterMessage::function' is not set")?; + Ok(factory.register_token(convert_proto_struct_tag_to_type_tag(function)?)) + }, + OneOftransaction_payload::liquid_staking_message(msg) => { + factory.liquid_staking_ops(LiquidStakingOperation::try_from(msg)?) + }, + OneOftransaction_payload::token_transfer_coins(token_transfer_coins) => { + let func = token_transfer_coins + .function + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("'TokenTransferCoinsMessage::function' is not set")?; + factory.implicitly_create_user_and_coins_transfer( + AccountAddress::from_str(&token_transfer_coins.to) + .map_err(from_account_error) + .into_tw() + .context("Invalid destination address")?, + token_transfer_coins.amount, + convert_proto_struct_tag_to_type_tag(func)?, + ) + }, + OneOftransaction_payload::None => { + let is_blind_sign = !input.any_encoded.is_empty(); + let v = serde_json::from_str::(&input.any_encoded) + .into_tw() + .context("Error decoding 'SigningInput::any_encoded' as JSON")?; + if is_blind_sign { + let entry_function = EntryFunction::try_from(v)?; + Ok(factory.payload(TransactionPayload::EntryFunction(entry_function))) + } else { + SigningError::err(SigningErrorType::Error_input_parse) + } + }, + } + } + + pub fn with_max_gas_amount(mut self, max_gas_amount: u64) -> Self { + self.max_gas_amount = max_gas_amount; + self + } + + pub fn with_gas_unit_price(mut self, gas_unit_price: u64) -> Self { + self.gas_unit_price = gas_unit_price; + self + } + + pub fn with_transaction_expiration_time(mut self, transaction_expiration_time: u64) -> Self { + self.transaction_expiration_time = transaction_expiration_time; + self + } + + pub fn payload(&self, payload: TransactionPayload) -> TransactionBuilder { + self.transaction_builder(payload) + } + + pub fn create_user_account(&self, to: AccountAddress) -> SigningResult { + Ok(self.payload(aptos_account_create_account(to)?)) + } + + pub fn register_token(&self, coin_type: TypeTag) -> TransactionBuilder { + self.payload(managed_coin_register(coin_type)) + } + + pub fn nft_ops(&self, operation: NftOperation) -> SigningResult { + match operation { + NftOperation::Claim(claim) => Ok(self.payload(token_transfers_claim_script( + claim.sender, + claim.creator, + claim.collection, + claim.name, + claim.property_version, + )?)), + NftOperation::Cancel(offer) => Ok(self.payload(token_transfers_cancel_offer_script( + offer.receiver, + offer.creator, + offer.collection, + offer.name, + offer.property_version, + )?)), + NftOperation::Offer(offer) => Ok(self.payload(token_transfers_offer_script( + offer.receiver, + offer.creator, + offer.collection, + offer.name, + offer.property_version, + offer.amount, + )?)), + } + } + + pub fn liquid_staking_ops( + &self, + operation: LiquidStakingOperation, + ) -> SigningResult { + match operation { + LiquidStakingOperation::Stake(stake) => { + Ok(self.payload(tortuga_stake(stake.smart_contract_address, stake.amount)?)) + }, + LiquidStakingOperation::Unstake(unstake) => Ok(self.payload(tortuga_unstake( + unstake.smart_contract_address, + unstake.amount, + )?)), + LiquidStakingOperation::Claim(claim) => { + Ok(self.payload(tortuga_claim(claim.smart_contract_address, claim.idx)?)) + }, + } + } + + pub fn implicitly_create_user_account_and_transfer( + &self, + to: AccountAddress, + amount: u64, + ) -> SigningResult { + Ok(self.payload(aptos_account_transfer(to, amount)?)) + } + + pub fn coins_transfer( + &self, + to: AccountAddress, + amount: u64, + coin_type: TypeTag, + ) -> SigningResult { + Ok(self.payload(coin_transfer(coin_type, to, amount)?)) + } + + pub fn implicitly_create_user_and_coins_transfer( + &self, + to: AccountAddress, + amount: u64, + coin_type: TypeTag, + ) -> SigningResult { + Ok(self.payload(aptos_account_transfer_coins(coin_type, to, amount)?)) + } + + fn transaction_builder(&self, payload: TransactionPayload) -> TransactionBuilder { + TransactionBuilder { + sender: None, + sequence_number: None, + payload, + max_gas_amount: self.max_gas_amount, + gas_unit_price: self.gas_unit_price, + expiration_timestamp_secs: self.expiration_timestamp(), + chain_id: self.chain_id, + } + } + + fn expiration_timestamp(&self) -> u64 { + self.transaction_expiration_time + } +} diff --git a/rust/chains/tw_aptos/src/transaction_payload.rs b/rust/chains/tw_aptos/src/transaction_payload.rs new file mode 100644 index 00000000000..aa1c74d02da --- /dev/null +++ b/rust/chains/tw_aptos/src/transaction_payload.rs @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::serde_helper::vec_bytes; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, StructTag, TypeTag}; +use move_core_types::parser::parse_transaction_argument; +use move_core_types::transaction_argument::TransactionArgument; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::default::Default; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::{bcs, EncodingError, EncodingResult}; +use tw_memory::Data; +use tw_proto::Aptos; + +pub type EntryFunctionResult = Result; + +#[derive(Debug)] +pub enum EntryFunctionError { + MissingFunctionName, + InvalidFunctionName, + MissingArguments, + InvalidArguments, + EncodingError, + MissingTypeArguments, + InvalidTypeArguments, +} + +impl From for EntryFunctionError { + fn from(_error: EncodingError) -> Self { + EntryFunctionError::EncodingError + } +} + +impl From for SigningError { + fn from(e: EntryFunctionError) -> Self { + SigningError::new(SigningErrorType::Error_invalid_params) + .context(format!("Error decoding EntryFunction: {e:?}")) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct EntryFunction { + module: ModuleId, + function: Identifier, + ty_args: Vec, + #[serde(with = "vec_bytes")] + args: Vec>, + #[serde(skip_serializing)] + json_args: Value, +} + +impl TryFrom for EntryFunction { + type Error = EntryFunctionError; + + fn try_from(value: Value) -> EntryFunctionResult { + let function_str = value["function"] + .as_str() + .ok_or(EntryFunctionError::MissingFunctionName)?; + let tag = StructTag::from_str(function_str) + .map_err(|_| EntryFunctionError::InvalidFunctionName)?; + + let args = value["arguments"] + .as_array() + .ok_or(EntryFunctionError::MissingArguments)? + .iter() + .map(|element| { + let arg_str = element.to_string(); + let arg = parse_transaction_argument( + arg_str.trim_start_matches('"').trim_end_matches('"'), + ) + .map_err(|_| EntryFunctionError::InvalidArguments)?; + serialize_argument(&arg).map_err(EntryFunctionError::from) + }) + .collect::>>()?; + + let ty_args = value["type_arguments"] + .as_array() + .ok_or(EntryFunctionError::MissingTypeArguments)? + .iter() + .map(|element| { + let ty_arg_str = element + .as_str() + .ok_or(EntryFunctionError::InvalidTypeArguments)?; + TypeTag::from_str(ty_arg_str).map_err(|_| EntryFunctionError::InvalidTypeArguments) + }) + .collect::>>()?; + + Ok(EntryFunction { + module: tag.module_id(), + function: tag.name, + ty_args, + args, + json_args: value["arguments"].clone(), + }) + } +} + +fn serialize_argument(arg: &TransactionArgument) -> EncodingResult { + match arg { + TransactionArgument::U8(v) => bcs::encode(v), + TransactionArgument::U16(v) => bcs::encode(v), + TransactionArgument::U32(v) => bcs::encode(v), + TransactionArgument::U64(v) => bcs::encode(v), + TransactionArgument::U128(v) => bcs::encode(v), + TransactionArgument::U256(v) => bcs::encode(v), + TransactionArgument::U8Vector(v) => bcs::encode(v), + TransactionArgument::Bool(v) => bcs::encode(v), + TransactionArgument::Address(v) => { + let serialized_v = bcs::encode(v)?; + bcs::encode(&serialized_v) + }, + } +} + +pub fn convert_proto_struct_tag_to_type_tag( + struct_tag: Aptos::Proto::StructTag, +) -> SigningResult { + TypeTag::from_str(&format!( + "{}::{}::{}", + struct_tag.account_address, struct_tag.module, struct_tag.name + )) + .tw_err(|_| SigningErrorType::Error_invalid_params) +} + +pub fn convert_type_tag_to_struct_tag(type_tag: TypeTag) -> Aptos::Proto::StructTag<'static> { + if let TypeTag::Struct(st) = type_tag { + Aptos::Proto::StructTag { + account_address: st.address.to_hex_literal().into(), + module: st.module.to_string().into(), + name: st.name.to_string().into(), + } + } else { + Aptos::Proto::StructTag::default() + } +} + +impl EntryFunction { + fn to_json(&self) -> Value { + // Create a JSON array from the `ty_args` field by filtering and mapping + // the items that match `TypeTag::Struct` to their string representation. + let type_arguments: Value = self + .ty_args + .iter() + .map(|item| Some(json!(item.to_string()))) + .collect(); + + // Construct the final JSON value + json!({ + "type": "entry_function_payload", + "function": format!("{}::{}", self.module.short_str_lossless(), self.function.clone().into_string()), + "arguments": self.json_args, + "type_arguments": type_arguments + }) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum TransactionPayload { + Script, + ModuleBundle, + /// A transaction that executes an existing entry function published on-chain. + EntryFunction(EntryFunction), +} + +impl TransactionPayload { + pub fn to_json(&self) -> Value { + match self { + TransactionPayload::Script => Value::default(), + TransactionPayload::ModuleBundle => Value::default(), + TransactionPayload::EntryFunction(entry) => entry.to_json(), + } + } +} + +impl EntryFunction { + pub fn new( + module: ModuleId, + function: Identifier, + ty_args: Vec, + args: Vec>, + json_args: Value, + ) -> Self { + EntryFunction { + module, + function, + ty_args, + args, + json_args, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::address::Address; + use move_core_types::account_address::AccountAddress; + use move_core_types::identifier::Identifier; + use move_core_types::language_storage::{ModuleId, TypeTag}; + use serde_json::{json, Value}; + use std::str::FromStr; + use tw_encoding::hex; + + #[test] + fn test_payload_from_json() { + let payload_value: Value = json!({ + "arguments": ["0xc95db29a67a848940829b3df6119b5e67b788ff0248676e4484c7c6f29c0f5e6"], + "function": "0xc23c3b70956ce8d88fb18ad9ed3b463fe873cb045db3f6d2e2fb15b9aab71d50::IFO::release", + "type": "entry_function_payload", + "type_arguments": [ + "0x48e0e3958d42b8d452c9199d4a221d0d1b15d14655787453dbe77208ced90517::coins::BUSD", + "0x48e0e3958d42b8d452c9199d4a221d0d1b15d14655787453dbe77208ced90517::coins::DAI", + "0x9936836587ca33240d3d3f91844651b16cb07802faf5e34514ed6f78580deb0a::uints::U1" + ] + }); + + let v = EntryFunction::try_from(payload_value.clone()).unwrap(); + assert_eq!(payload_value, v.to_json()); + } + + #[test] + fn test_payload_from_json_with_arg_non_str() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0xd11107bdf0d6d7040c6c0bfbdecb6545191fdf13e8d8d259952f53e1713f61b5::ditto_staking::stake_aptos", + "type_arguments":[], + "arguments": [1000000] + }); + + let v = EntryFunction::try_from(payload_value.clone()).unwrap(); + assert_eq!(payload_value, v.to_json()); + } + + #[test] + fn test_basic_payload() { + let addr = + Address::from_str("0xeeff357ea5c1a4e7bc11b2b17ff2dc2dcca69750bfef1e1ebcaccf8c8018175b") + .unwrap() + .inner(); + let amount: i64 = 1000; + let args = vec![bcs::encode(&addr).unwrap(), bcs::encode(&amount).unwrap()]; + let module = ModuleId::new(AccountAddress::ONE, Identifier::from_str("coin").unwrap()); + let function = Identifier::from_str("transfer").unwrap(); + let type_tag = vec![TypeTag::from_str("0x1::aptos_coin::AptosCoin").unwrap()]; + let entry = EntryFunction::new( + module, + function, + type_tag, + args, + json!([addr.to_hex_literal(), amount.to_string()]), + ); + let tp = TransactionPayload::EntryFunction(entry); + let serialized = bcs::encode(&tp).unwrap(); + assert_eq!(hex::encode(serialized, false), "02000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220eeff357ea5c1a4e7bc11b2b17ff2dc2dcca69750bfef1e1ebcaccf8c8018175b08e803000000000000"); + let payload_value: Value = json!({ + "function": "0x1::coin::transfer", + "type": "entry_function_payload", + "arguments": ["0xeeff357ea5c1a4e7bc11b2b17ff2dc2dcca69750bfef1e1ebcaccf8c8018175b", "1000"], + "type_arguments": ["0x1::aptos_coin::AptosCoin"] + }); + assert_eq!(tp.to_json(), payload_value); + } +} diff --git a/rust/chains/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs new file mode 100644 index 00000000000..e69c6894839 --- /dev/null +++ b/rust/chains/tw_aptos/tests/signer.rs @@ -0,0 +1,871 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use move_core_types::account_address::AccountAddress; +use move_core_types::language_storage::TypeTag; +use std::str::FromStr; +use tw_aptos::liquid_staking; +use tw_aptos::liquid_staking::{LiquidStakingOperation, Stake, Unstake}; +use tw_aptos::nft::{Claim, NftOperation, Offer}; +use tw_aptos::signer::Signer; +use tw_aptos::transaction_payload::convert_type_tag_to_struct_tag; +use tw_coin_entry::error::prelude::*; +use tw_encoding::hex; +use tw_proto::Aptos::Proto; +use tw_proto::Aptos::Proto::{SigningInput, SigningOutput}; + +pub struct AccountCreation { + to: String, +} + +pub struct Transfer { + to: String, + amount: u64, +} + +pub struct TokenTransfer { + transfer: Transfer, + tag: TypeTag, +} + +pub struct RegisterToken { + coin_type: TypeTag, +} + +pub enum OpsDetails { + RegisterToken(RegisterToken), + LiquidStakingOps(LiquidStakingOperation), + AccountCreation(AccountCreation), + Transfer(Transfer), + TokenTransfer(TokenTransfer), + ImplicitTokenTransfer(TokenTransfer), + NftOps(NftOperation), +} + +fn setup_proto_transaction<'a>( + sender: &'a str, + keypair_str: &'a str, + transaction_type: &'a str, + sequence_number: i64, + chain_id: u32, + max_gas_amount: u64, + timestamp: u64, + gas_unit_price: u64, + any_encoded: &'a str, + ops_details: Option, +) -> SigningInput<'a> { + let private = hex::decode(keypair_str).unwrap(); + + let payload: Proto::mod_SigningInput::OneOftransaction_payload = match transaction_type { + "transfer" => { + if let OpsDetails::Transfer(transfer) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::transfer( + Proto::TransferMessage { + to: transfer.to.into(), + amount: transfer.amount, + }, + ) + } else { + panic!("Unsupported arguments") + } + }, + "create_account" => { + if let OpsDetails::AccountCreation(account) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::create_account( + Proto::CreateAccountMessage { + auth_key: account.to.into(), + }, + ) + } else { + panic!("Unsupported arguments") + } + }, + "coin_transfer" => { + if let OpsDetails::TokenTransfer(token_transfer) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::token_transfer( + Proto::TokenTransferMessage { + to: token_transfer.transfer.to.into(), + amount: token_transfer.transfer.amount, + function: Some(convert_type_tag_to_struct_tag(token_transfer.tag)), + }, + ) + } else { + panic!("Unsupported arguments") + } + }, + "implicit_coin_transfer" => { + if let OpsDetails::ImplicitTokenTransfer(token_transfer) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::token_transfer_coins( + Proto::TokenTransferCoinsMessage { + to: token_transfer.transfer.to.into(), + amount: token_transfer.transfer.amount, + function: Some(convert_type_tag_to_struct_tag(token_transfer.tag)), + }, + ) + } else { + panic!("Unsupported arguments") + } + }, + "nft_ops" => { + if let OpsDetails::NftOps(nft) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::nft_message(nft.into()) + } else { + panic!("Unsupported arguments") + } + }, + "register_token" => { + if let OpsDetails::RegisterToken(register_token) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::register_token( + Proto::ManagedTokensRegisterMessage { + function: Some(convert_type_tag_to_struct_tag(register_token.coin_type)), + }, + ) + } else { + panic!("Unsupported arguments") + } + }, + "liquid_staking_ops" => { + if let OpsDetails::LiquidStakingOps(liquid_staking_ops) = ops_details.unwrap() { + Proto::mod_SigningInput::OneOftransaction_payload::liquid_staking_message( + liquid_staking_ops.into(), + ) + } else { + panic!("Unsupported arguments") + } + }, + "blind_sign_json" => Proto::mod_SigningInput::OneOftransaction_payload::None, + _ => Proto::mod_SigningInput::OneOftransaction_payload::None, + }; + + let input = SigningInput { + chain_id, + sender: sender.into(), + sequence_number, + max_gas_amount, + gas_unit_price, + expiration_timestamp_secs: timestamp, + private_key: private.into(), + any_encoded: any_encoded.into(), + transaction_payload: payload, + }; + + input +} + +fn test_tx_result( + output: SigningOutput, + expected_raw_txn_bytes_str: &str, + expected_signature_str: &str, + expected_encoded_txn_str: &str, + json_literal: &str, +) { + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + + assert_eq!( + hex::encode(output.raw_txn.to_vec(), false), + expected_raw_txn_bytes_str + ); + assert_eq!( + hex::encode(output.authenticator.unwrap().signature.to_vec(), false), + expected_signature_str + ); + assert_eq!( + hex::encode(output.encoded.to_vec(), false), + expected_encoded_txn_str + ); + + let json_value_expected: serde_json::Value = serde_json::from_str(json_literal).unwrap(); + let json_value: serde_json::Value = serde_json::from_str(output.json.as_ref()).unwrap(); + assert_eq!(json_value, json_value_expected); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0xb4d62afd3862116e060dd6ad9848ccb50c2bc177799819f1d29c059ae2042467?network=devnet +#[test] +fn test_aptos_sign_transaction_transfer() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", + "transfer", + 99, + 33, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::Transfer(Transfer { + to: "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30".to_string(), + amount: 1000, + })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3063000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e7366657200022007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3008e803000000000000fe4d3200000000006400000000000000c2276ada0000000021", + "5707246db31e2335edc4316a7a656a11691d1d1647f6e864d1ab12f43428aaaf806cf02120d0b608cdd89c5c904af7b137432aacdd60cc53f9fad7bd33578e01", + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3063000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e7366657200022007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3008e803000000000000fe4d3200000000006400000000000000c2276ada00000000210020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c405707246db31e2335edc4316a7a656a11691d1d1647f6e864d1ab12f43428aaaf806cf02120d0b608cdd89c5c904af7b137432aacdd60cc53f9fad7bd33578e01", + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": ["0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30","1000"], + "function": "0x1::aptos_account::transfer", + "type": "entry_function_payload", + "type_arguments": [] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "99", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x5707246db31e2335edc4316a7a656a11691d1d1647f6e864d1ab12f43428aaaf806cf02120d0b608cdd89c5c904af7b137432aacdd60cc53f9fad7bd33578e01", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x477141736de6b0936a6c3734e4d6fd018c7d21f1f28f99028ef0bc6881168602?network=Devnet +#[test] +fn test_aptos_sign_create_account() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "create_account", + 0, // Sequence number + 33, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::AccountCreation(AccountCreation { + to: "0x3aa1672641a4e17b3d913b4c0301e805755a80b12756fc729c5878f12344d30e".to_string(), + })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3000000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e6372656174655f6163636f756e740001203aa1672641a4e17b3d913b4c0301e805755a80b12756fc729c5878f12344d30efe4d3200000000006400000000000000c2276ada0000000021", // Expected raw transaction bytes + "fcba3dfbec76721454ef414955f09f159660a13886b4edd8c579e3c779c29073afe7b25efa3fef9b21c2efb1cf16b4247fc0e5c8f63fdcd1c8d87f5d59f44501", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3000000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e6372656174655f6163636f756e740001203aa1672641a4e17b3d913b4c0301e805755a80b12756fc729c5878f12344d30efe4d3200000000006400000000000000c2276ada00000000210020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40fcba3dfbec76721454ef414955f09f159660a13886b4edd8c579e3c779c29073afe7b25efa3fef9b21c2efb1cf16b4247fc0e5c8f63fdcd1c8d87f5d59f44501", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": ["0x3aa1672641a4e17b3d913b4c0301e805755a80b12756fc729c5878f12344d30e"], + "function": "0x1::aptos_account::create_account", + "type": "entry_function_payload", + "type_arguments": [] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "0", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xfcba3dfbec76721454ef414955f09f159660a13886b4edd8c579e3c779c29073afe7b25efa3fef9b21c2efb1cf16b4247fc0e5c8f63fdcd1c8d87f5d59f44501", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0xb5b383a5c7f99b2edb3bed9533f8169a89051b149d65876a82f4c0b9bf78a15b?network=Devnet +#[test] +fn test_aptos_sign_coin_transfer() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "coin_transfer", + 24, // Sequence number + 32, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::TokenTransfer(TokenTransfer { + transfer: Transfer { + to: "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" + .to_string(), + amount: 100000, + }, + tag: TypeTag::from_str( + "0x43417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b9::coins::BTC", + ) + .unwrap(), + })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30180000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e730342544300022007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3008a086010000000000fe4d3200000000006400000000000000c2276ada0000000020", // Expected raw transaction bytes + "7643ec8aae6198bd13ca6ea2962265859cba5a228e7d181131f6c022700dd02a7a04dc0345ad99a0289e5ab80b130b3864e6404079980bc226f1a13aee7d280a", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30180000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010743417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b905636f696e730342544300022007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3008a086010000000000fe4d3200000000006400000000000000c2276ada00000000200020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c407643ec8aae6198bd13ca6ea2962265859cba5a228e7d181131f6c022700dd02a7a04dc0345ad99a0289e5ab80b130b3864e6404079980bc226f1a13aee7d280a", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": ["0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30","100000"], + "function": "0x1::coin::transfer", + "type": "entry_function_payload", + "type_arguments": ["0x43417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b9::coins::BTC"] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "24", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x7643ec8aae6198bd13ca6ea2962265859cba5a228e7d181131f6c022700dd02a7a04dc0345ad99a0289e5ab80b130b3864e6404079980bc226f1a13aee7d280a", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x197d40ea12e2bfc65a0a913b9f4ca3b0b0208fe0c1514d3d55cef3d5bcf25211?network=mainnet +#[test] +fn test_implicit_aptos_sign_coin_transfer() { + let input = setup_proto_transaction("0x1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf25", // Sender's address + "e7f56c77189e03699a75d8ec5c090e41f3d9d4783bc49c33df8a93d915e10de8", // Keypair + "implicit_coin_transfer", + 2, // Sequence number + 1, + 2000, + 3664390082, + 100, + "", + Some(OpsDetails::ImplicitTokenTransfer(TokenTransfer { transfer: Transfer { to: "0xb7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c".to_string(), amount: 10000 }, tag: TypeTag::from_str("0xe9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9::mee_coin::MeeCoin").unwrap() })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf2502000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107e9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9086d65655f636f696e074d6565436f696e000220b7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c081027000000000000d0070000000000006400000000000000c2276ada0000000001", // Expected raw transaction bytes + "30ebd7e95cb464677f411868e2cbfcb22bc01cc63cded36c459dff45e6d2f1354ae4e090e7dfbb509851c0368b343e0e5ecaf6b08e7c1b94c186530b0f7dee0d", // Expected signature + "1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf2502000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107e9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9086d65655f636f696e074d6565436f696e000220b7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c081027000000000000d0070000000000006400000000000000c2276ada0000000001002062e7a6a486553b56a53e89dfae3f780693e537e5b0a7ed33290780e581ca83694030ebd7e95cb464677f411868e2cbfcb22bc01cc63cded36c459dff45e6d2f1354ae4e090e7dfbb509851c0368b343e0e5ecaf6b08e7c1b94c186530b0f7dee0d", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "2000", + "payload": { + "arguments": ["0xb7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c","10000"], + "function": "0x1::aptos_account::transfer_coins", + "type": "entry_function_payload", + "type_arguments": ["0xe9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9::mee_coin::MeeCoin"] + }, + "sender": "0x1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf25", + "sequence_number": "2", + "signature": { + "public_key": "0x62e7a6a486553b56a53e89dfae3f780693e537e5b0a7ed33290780e581ca8369", + "signature": "0x30ebd7e95cb464677f411868e2cbfcb22bc01cc63cded36c459dff45e6d2f1354ae4e090e7dfbb509851c0368b343e0e5ecaf6b08e7c1b94c186530b0f7dee0d", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x514e473618bd3cb89a2b110b7c473db9a2e10532f98eb42d02d86fb31c00525d?network=testnet +#[test] +fn test_aptos_nft_offer() { + let input = setup_proto_transaction( + "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", // Sender's address + "7bebb6d543d17f6fe4e685cfab239fa37896edd594ff859f1df32f244fb707e2", // Keypair + "nft_ops", + 1, // Sequence number + 2, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::NftOps(NftOperation::Offer(Offer { + receiver: AccountAddress::from_str( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + ) + .unwrap(), + creator: AccountAddress::from_str( + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + ) + .unwrap(), + collection: "Topaz Troopers".as_bytes().to_vec(), + name: "Topaz Trooper #20068".as_bytes().to_vec(), + property_version: 0, + amount: 1, + }))), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee01000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572730c6f666665725f73637269707400062007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000080100000000000000fe4d3200000000006400000000000000c2276ada0000000002", // Expected raw transaction bytes + "af5c7357a83c69e3f425beb23eaf232f8bb36dea3b7cad4a7ab8d735cee999c8ec5285005adf69dc85a6c34b042dd0308fe92b76dad5d6ac88c7b9259902c10f", // Expected signature + "783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee01000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572730c6f666665725f73637269707400062007968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000080100000000000000fe4d3200000000006400000000000000c2276ada00000000020020d1d99b67e37b483161a0fa369c46f34a3be4863c20e20fc7cdc669c0826a411340af5c7357a83c69e3f425beb23eaf232f8bb36dea3b7cad4a7ab8d735cee999c8ec5285005adf69dc85a6c34b042dd0308fe92b76dad5d6ac88c7b9259902c10f", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": [ + "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + "Topaz Troopers", "Topaz Trooper #20068", "0", "1"], + "function": "0x3::token_transfers::offer_script", + "type": "entry_function_payload", + "type_arguments": [] + }, + "sender": "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", + "sequence_number": "1", + "signature": { + "public_key": "0xd1d99b67e37b483161a0fa369c46f34a3be4863c20e20fc7cdc669c0826a4113", + "signature": "0xaf5c7357a83c69e3f425beb23eaf232f8bb36dea3b7cad4a7ab8d735cee999c8ec5285005adf69dc85a6c34b042dd0308fe92b76dad5d6ac88c7b9259902c10f", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x0b8c64e6847c368e4c6bd2cce0e9eab378971b0ef2e3bc40cbd292910a80201d?network=testnet +#[test] +fn test_aptos_cancel_nft_offer() { + let input = setup_proto_transaction( + "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "nft_ops", + 21, // Sequence number + 2, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::NftOps(NftOperation::Cancel(Offer { + receiver: AccountAddress::from_str( + "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", + ) + .unwrap(), + creator: AccountAddress::from_str( + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + ) + .unwrap(), + collection: "Topaz Troopers".as_bytes().to_vec(), + name: "Topaz Trooper #20068".as_bytes().to_vec(), + property_version: 0, + amount: 0, + }))), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3015000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572731363616e63656c5f6f666665725f736372697074000520783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000fe4d3200000000006400000000000000c2276ada0000000002", // Expected raw transaction bytes + "826722d374e276f618123e77da3ac024c89a3f97db9e09e19aa8ed06c3cdfc57d4a21c7890137f9a7c0447cc303447ba10ca5b1908e889071e0a68f48c0f260a", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3015000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572731363616e63656c5f6f666665725f736372697074000520783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000fe4d3200000000006400000000000000c2276ada00000000020020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40826722d374e276f618123e77da3ac024c89a3f97db9e09e19aa8ed06c3cdfc57d4a21c7890137f9a7c0447cc303447ba10ca5b1908e889071e0a68f48c0f260a", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": [ + "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + "Topaz Troopers", "Topaz Trooper #20068", "0"], + "function": "0x3::token_transfers::cancel_offer_script", + "type": "entry_function_payload", + "type_arguments": [] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "21", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x826722d374e276f618123e77da3ac024c89a3f97db9e09e19aa8ed06c3cdfc57d4a21c7890137f9a7c0447cc303447ba10ca5b1908e889071e0a68f48c0f260a", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x60b51e15140ec0b7650334e948fb447ce3cb13ae63492260461ebfa9d02e85c4?network=testnet +#[test] +fn test_aptos_nft_claim() { + let input = setup_proto_transaction( + "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "nft_ops", + 19, // Sequence number + 2, + 3296766, + 3664390082, + 100, + "", + Some(OpsDetails::NftOps(NftOperation::Claim(Claim { + sender: AccountAddress::from_str( + "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", + ) + .unwrap(), + creator: AccountAddress::from_str( + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + ) + .unwrap(), + collection: "Topaz Troopers".as_bytes().to_vec(), + name: "Topaz Trooper #20068".as_bytes().to_vec(), + property_version: 0, + }))), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3013000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572730c636c61696d5f736372697074000520783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000fe4d3200000000006400000000000000c2276ada0000000002", // Expected raw transaction bytes + "ede1ffb5f8f663741c2ca9597af44af81c98f7a910261bb4125f758fd0c0ebbf5bacb34f1196ad45153177729eb6d478676b364ab747da17602713f65ca2dd0a", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3013000000000000000200000000000000000000000000000000000000000000000000000000000000030f746f6b656e5f7472616e73666572730c636c61696d5f736372697074000520783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee209125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac0f0e546f70617a2054726f6f706572731514546f70617a2054726f6f70657220233230303638080000000000000000fe4d3200000000006400000000000000c2276ada00000000020020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40ede1ffb5f8f663741c2ca9597af44af81c98f7a910261bb4125f758fd0c0ebbf5bacb34f1196ad45153177729eb6d478676b364ab747da17602713f65ca2dd0a", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "3296766", + "payload": { + "arguments": [ + "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", + "0x9125e4054d884fdc7296b66e12c0d63a7baa0d88c77e8e784987c0a967c670ac", + "Topaz Troopers", "Topaz Trooper #20068", "0"], + "function": "0x3::token_transfers::claim_script", + "type": "entry_function_payload", + "type_arguments": [] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "19", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xede1ffb5f8f663741c2ca9597af44af81c98f7a910261bb4125f758fd0c0ebbf5bacb34f1196ad45153177729eb6d478676b364ab747da17602713f65ca2dd0a", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0xe591252daed785641bfbbcf72a5d17864568cf32e04c0cc9129f3a13834d0e8e?network=testnet +#[test] +fn test_aptos_register_token() { + let input = setup_proto_transaction("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "register_token", + 23, // Sequence number + 2, + 2000000, + 3664390082, + 100, + "", + Some(OpsDetails::RegisterToken(RegisterToken { coin_type: TypeTag::from_str("0xe4497a32bf4a9fd5601b27661aa0b933a923191bf403bd08669ab2468d43b379::move_coin::MoveCoin").unwrap() })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3017000000000000000200000000000000000000000000000000000000000000000000000000000000010c6d616e616765645f636f696e0872656769737465720107e4497a32bf4a9fd5601b27661aa0b933a923191bf403bd08669ab2468d43b379096d6f76655f636f696e084d6f7665436f696e000080841e00000000006400000000000000c2276ada0000000002", // Expected raw transaction bytes + "e230b49f552fb85356dbec9df13f0dc56228eb7a9c29a8af3a99f4ae95b86c72bdcaa4ff1e9beb0bd81c298b967b9d97449856ec8bc672a08e2efef345c37100", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f3017000000000000000200000000000000000000000000000000000000000000000000000000000000010c6d616e616765645f636f696e0872656769737465720107e4497a32bf4a9fd5601b27661aa0b933a923191bf403bd08669ab2468d43b379096d6f76655f636f696e084d6f7665436f696e000080841e00000000006400000000000000c2276ada00000000020020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40e230b49f552fb85356dbec9df13f0dc56228eb7a9c29a8af3a99f4ae95b86c72bdcaa4ff1e9beb0bd81c298b967b9d97449856ec8bc672a08e2efef345c37100", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "2000000", + "payload": { + "arguments": [], + "function": "0x1::managed_coin::register", + "type": "entry_function_payload", + "type_arguments": ["0xe4497a32bf4a9fd5601b27661aa0b933a923191bf403bd08669ab2468d43b379::move_coin::MoveCoin"] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "23", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xe230b49f552fb85356dbec9df13f0dc56228eb7a9c29a8af3a99f4ae95b86c72bdcaa4ff1e9beb0bd81c298b967b9d97449856ec8bc672a08e2efef345c37100", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x25dca849cb4ebacbff223139f7ad5d24c37c225d9506b8b12a925de70429e685/userTxnOverview?network=mainnet +#[test] +fn test_aptos_tortuga_stake() { + let input = setup_proto_transaction( + "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", // Sender's address + "786fc7ceca43b4c1da018fea5d96f35dfdf5605f220b1205ff29c5c6d9eccf05", // Keypair + "liquid_staking_ops", + 19, // Sequence number + 1, + 5554, + 1670240203, + 100, + "", + Some(OpsDetails::LiquidStakingOps(LiquidStakingOperation::Stake( + Stake { + amount: 100000000, + smart_contract_address: AccountAddress::from_str( + "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f", + ) + .unwrap(), + }, + ))), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1300000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f75746572057374616b6500010800e1f50500000000b2150000000000006400000000000000cbd78d630000000001", // Expected raw transaction bytes + "22d3166c3003f9c24a35fd39c71eb27e0d2bb82541be610822165c9283f56fefe5a9d46421b9caf174995bd8f83141e60ea8cff521ecf4741fe19e6ae9a5680d", // Expected signature + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1300000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f75746572057374616b6500010800e1f50500000000b2150000000000006400000000000000cbd78d630000000001002089e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc44022d3166c3003f9c24a35fd39c71eb27e0d2bb82541be610822165c9283f56fefe5a9d46421b9caf174995bd8f83141e60ea8cff521ecf4741fe19e6ae9a5680d", // Expected encoded transaction + r#"{ + "sender": "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", + "sequence_number": "19", + "max_gas_amount": "5554", + "gas_unit_price": "100", + "expiration_timestamp_secs": "1670240203", + "payload": { + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::stake", + "type_arguments": [], + "arguments": [ + "100000000" + ], + "type": "entry_function_payload" + }, + "signature": { + "public_key": "0x89e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc4", + "signature": "0x22d3166c3003f9c24a35fd39c71eb27e0d2bb82541be610822165c9283f56fefe5a9d46421b9caf174995bd8f83141e60ea8cff521ecf4741fe19e6ae9a5680d", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x92edb4f756fe86118e34a0e64746c70260ee02c2ae2cf402b3e39f6a282ce968?network=mainnet +#[test] +fn test_aptos_tortuga_unstake() { + let input = setup_proto_transaction( + "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", // Sender's address + "786fc7ceca43b4c1da018fea5d96f35dfdf5605f220b1205ff29c5c6d9eccf05", // Keypair + "liquid_staking_ops", + 20, // Sequence number + 1, + 2371, + 1670304949, + 120, + "", + Some(OpsDetails::LiquidStakingOps( + LiquidStakingOperation::Unstake(Unstake { + amount: 99178100, + smart_contract_address: AccountAddress::from_str( + "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f", + ) + .unwrap(), + }), + )), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1400000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657207756e7374616b650001087456e9050000000043090000000000007800000000000000b5d48e630000000001", // Expected raw transaction bytes + "6994b917432ad70ae84d2ce1484e6aece589a68aad1b7c6e38c9697f2a012a083a3a755c5e010fd3d0f149a75dd8d257acbd09f10800e890074e5ad384314d0c", // Expected signature + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1400000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657207756e7374616b650001087456e9050000000043090000000000007800000000000000b5d48e630000000001002089e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc4406994b917432ad70ae84d2ce1484e6aece589a68aad1b7c6e38c9697f2a012a083a3a755c5e010fd3d0f149a75dd8d257acbd09f10800e890074e5ad384314d0c", // Expected encoded transaction + r#"{ + "sender": "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", + "sequence_number": "20", + "max_gas_amount": "2371", + "gas_unit_price": "120", + "expiration_timestamp_secs": "1670304949", + "payload": { + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::unstake", + "type_arguments": [], + "arguments": [ + "99178100" + ], + "type": "entry_function_payload" + }, + "signature": { + "public_key": "0x89e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc4", + "signature": "0x6994b917432ad70ae84d2ce1484e6aece589a68aad1b7c6e38c9697f2a012a083a3a755c5e010fd3d0f149a75dd8d257acbd09f10800e890074e5ad384314d0c", + "type": "ed25519_signature" + } + }"#); +} + +// // Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x9fc874de7a7d3e813d9a1658d896023de270a0096a5e258c196005656ace7d54?network=mainnet +#[test] +fn test_aptos_tortuga_claim() { + let input = setup_proto_transaction( + "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", // Sender's address + "786fc7ceca43b4c1da018fea5d96f35dfdf5605f220b1205ff29c5c6d9eccf05", // Keypair + "liquid_staking_ops", + 28, // Sequence number + 1, + 10, + 1682066783, + 148, + "", + Some(OpsDetails::LiquidStakingOps(LiquidStakingOperation::Claim( + liquid_staking::Claim { + idx: 0, + smart_contract_address: AccountAddress::from_str( + "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f", + ) + .unwrap(), + }, + ))), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1c00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657205636c61696d00010800000000000000000a0000000000000094000000000000005f4d42640000000001", // Expected raw transaction bytes + "c936584f89777e1fe2d5dd75cd8d9c514efc445810ba22f462b6fe7229c6ec7fc1c8b25d3e233eafaa8306433b3220235e563498ba647be38cac87ff618e3d03", // Expected signature + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc1c00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657205636c61696d00010800000000000000000a0000000000000094000000000000005f4d42640000000001002089e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc440c936584f89777e1fe2d5dd75cd8d9c514efc445810ba22f462b6fe7229c6ec7fc1c8b25d3e233eafaa8306433b3220235e563498ba647be38cac87ff618e3d03", // Expected encoded transaction + r#"{ + "sender": "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", + "sequence_number": "28", + "max_gas_amount": "10", + "gas_unit_price": "148", + "expiration_timestamp_secs": "1682066783", + "payload": { + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::claim", + "type_arguments": [], + "arguments": [ + "0" + ], + "type": "entry_function_payload" + }, + "signature": { + "public_key": "0x89e0211d7e19c7d3a8e2030fe16c936a690ca9b95569098c5d2bf1031ff44bc4", + "signature": "0xc936584f89777e1fe2d5dd75cd8d9c514efc445810ba22f462b6fe7229c6ec7fc1c8b25d3e233eafaa8306433b3220235e563498ba647be38cac87ff618e3d03", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x7efd69e7f9462774b932ce500ab51c0d0dcc004cf272e09f8ffd5804c2a84e33?network=mainnet +#[test] +fn test_aptos_blind_sign() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "blind_sign_json", + 42, // Sequence number + 1, + 100011, + 3664390082, + 100, + r#"{ + "function": "0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c::AnimeSwapPoolV1::swap_exact_coins_for_coins_3_pair_entry", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin", + "0x881ac202b1f1e6ad4efcff7a1d0579411533f2502417a19211cfc49751ddb5f4::coin::MOJO", + "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDT", + "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDC" + ], + "arguments": [ + "1000000", + "49329" + ], + "type": "entry_function_payload" + }"#, + None, + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f302a000000000000000216fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c0f416e696d6553776170506f6f6c563127737761705f65786163745f636f696e735f666f725f636f696e735f335f706169725f656e747279040700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e0007881ac202b1f1e6ad4efcff7a1d0579411533f2502417a19211cfc49751ddb5f404636f696e044d4f4a4f0007f22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa05617373657404555344540007f22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa056173736574045553444300020840420f000000000008b1c0000000000000ab860100000000006400000000000000c2276ada0000000001", // Expected raw transaction bytes + "42cd67406e85afd1e948e7ad7f5f484fb4c60d82b267c6b6b28a92301e228b983206d2b87cd5487cf9acfb0effbd183ab90123570eb2e047cb152d337152210b", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f302a000000000000000216fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c0f416e696d6553776170506f6f6c563127737761705f65786163745f636f696e735f666f725f636f696e735f335f706169725f656e747279040700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e0007881ac202b1f1e6ad4efcff7a1d0579411533f2502417a19211cfc49751ddb5f404636f696e044d4f4a4f0007f22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa05617373657404555344540007f22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa056173736574045553444300020840420f000000000008b1c0000000000000ab860100000000006400000000000000c2276ada00000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c4042cd67406e85afd1e948e7ad7f5f484fb4c60d82b267c6b6b28a92301e228b983206d2b87cd5487cf9acfb0effbd183ab90123570eb2e047cb152d337152210b", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "100011", + "payload": { + "function": "0x16fe2df00ea7dde4a63409201f7f4e536bde7bb7335526a35d05111e68aa322c::AnimeSwapPoolV1::swap_exact_coins_for_coins_3_pair_entry", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin", + "0x881ac202b1f1e6ad4efcff7a1d0579411533f2502417a19211cfc49751ddb5f4::coin::MOJO", + "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDT", + "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDC" + ], + "arguments": [ + "1000000", + "49329" + ], + "type": "entry_function_payload" + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "42", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x42cd67406e85afd1e948e7ad7f5f484fb4c60d82b267c6b6b28a92301e228b983206d2b87cd5487cf9acfb0effbd183ab90123570eb2e047cb152d337152210b", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x25dca849cb4ebacbff223139f7ad5d24c37c225d9506b8b12a925de70429e685/payload +#[test] +fn test_aptos_blind_sign_staking() { + let input = setup_proto_transaction( + "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "blind_sign_json", + 43, // Sequence number + 1, + 100011, + 3664390082, + 100, + r#"{ + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::stake", + "type_arguments": [], + "arguments": [ + "100000000" + ], + "type": "entry_function_payload" + }"#, + None, + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc2b00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f75746572057374616b6500010800e1f50500000000ab860100000000006400000000000000c2276ada0000000001", // Expected raw transaction bytes + "a41b7440a50f36e8491319508734acb55488abc6d88fbc9cb2b37ba23210f01f5d08c856cb7abf18c414cf9302ee144450bd99495a7e21e61f624764db91eb0b", // Expected signature + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc2b00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f75746572057374616b6500010800e1f50500000000ab860100000000006400000000000000c2276ada00000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40a41b7440a50f36e8491319508734acb55488abc6d88fbc9cb2b37ba23210f01f5d08c856cb7abf18c414cf9302ee144450bd99495a7e21e61f624764db91eb0b", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "100011", + "payload": { + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::stake", + "type_arguments": [], + "arguments": [ + "100000000" + ], + "type": "entry_function_payload" + }, + "sender": "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", + "sequence_number": "43", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xa41b7440a50f36e8491319508734acb55488abc6d88fbc9cb2b37ba23210f01f5d08c856cb7abf18c414cf9302ee144450bd99495a7e21e61f624764db91eb0b", + "type": "ed25519_signature" + } + }"#); +} + +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x92edb4f756fe86118e34a0e64746c70260ee02c2ae2cf402b3e39f6a282ce968/payload +#[test] +fn test_aptos_blind_sign_unstaking() { + let input = setup_proto_transaction( + "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "blind_sign_json", + 44, // Sequence number + 1, + 100011, + 3664390082, + 100, + r#"{ + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::unstake", + "type_arguments": [], + "arguments": [ + "99178100" + ], + "type": "entry_function_payload" + }"#, + None, + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc2c00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657207756e7374616b650001087456e90500000000ab860100000000006400000000000000c2276ada0000000001", // Expected raw transaction bytes + "a58ad5e3331beb8c0212a18a1f932207cb664b78f5aad3cb1fe7435e0e0e053247ce49b38fd67b064bed34ed643eb6a03165d77c681d7d73ac3161ab984a960a", // Expected signature + "f3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc2c00000000000000028f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f0c7374616b655f726f7574657207756e7374616b650001087456e90500000000ab860100000000006400000000000000c2276ada00000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40a58ad5e3331beb8c0212a18a1f932207cb664b78f5aad3cb1fe7435e0e0e053247ce49b38fd67b064bed34ed643eb6a03165d77c681d7d73ac3161ab984a960a", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "100011", + "payload": { + "function": "0x8f396e4246b2ba87b51c0739ef5ea4f26515a98375308c31ac2ec1e42142a57f::stake_router::unstake", + "type_arguments": [], + "arguments": [ + "99178100" + ], + "type": "entry_function_payload" + }, + "sender": "0xf3d7f364dd7705824a5ebda9c7aab6cb3fc7bb5b58718249f12defec240b36cc", + "sequence_number": "44", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xa58ad5e3331beb8c0212a18a1f932207cb664b78f5aad3cb1fe7435e0e0e053247ce49b38fd67b064bed34ed643eb6a03165d77c681d7d73ac3161ab984a960a", + "type": "ed25519_signature" + } + }"#); +} diff --git a/rust/chains/tw_binance/Cargo.toml b/rust/chains/tw_binance/Cargo.toml new file mode 100644 index 00000000000..e5c9a10aa2c --- /dev/null +++ b/rust/chains/tw_binance/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tw_binance" +version = "0.1.0" +edition = "2021" + +[dependencies] +quick-protobuf = "0.8.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" +strum_macros = "0.25" +tw_bech32_address = { path = "../../tw_bech32_address" } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_encoding = { path = "../../tw_encoding" } +tw_evm = { path = "../../tw_evm" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc", features = ["serde"] } +tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_binance/fuzz/.gitignore b/rust/chains/tw_binance/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_binance/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_binance/fuzz/Cargo.toml b/rust/chains/tw_binance/fuzz/Cargo.toml new file mode 100644 index 00000000000..179f9d77239 --- /dev/null +++ b/rust/chains/tw_binance/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_binance-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_binance] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_binance/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_binance/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..482fd1f5844 --- /dev/null +++ b/rust/chains/tw_binance/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::Binance::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::Binance, input); +}); diff --git a/rust/chains/tw_binance/src/address.rs b/rust/chains/tw_binance/src/address.rs new file mode 100644 index 00000000000..45b00e26bb4 --- /dev/null +++ b/rust/chains/tw_binance/src/address.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; +use tw_bech32_address::bech32_prefix::Bech32Prefix; +use tw_bech32_address::Bech32Address; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::address::CosmosAddress; +use tw_keypair::tw::PublicKey; +use tw_memory::Data; + +/// The list of known BNB hrps. +const BNB_KNOWN_HRPS: [&str; 2] = [ + BinanceAddress::VALIDATOR_HRP, // BNB Validator HRP. + "bca", +]; + +#[derive(Deserialize, PartialEq, Serialize)] +pub struct BinanceAddress(Bech32Address); + +impl CoinAddress for BinanceAddress { + #[inline] + fn data(&self) -> Data { + self.0.data() + } +} + +impl CosmosAddress for BinanceAddress {} + +impl BinanceAddress { + pub const VALIDATOR_HRP: &'static str = "bva"; + + pub fn new_validator_addr(key_hash: Data) -> AddressResult { + Bech32Address::new(Self::VALIDATOR_HRP.to_string(), key_hash).map(BinanceAddress) + } + + /// Creates a Binance address with the only `prefix` + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + address_str: String, + prefix: Option, + ) -> AddressResult + where + Self: Sized, + { + let possible_hrps = match prefix { + Some(Bech32Prefix { hrp }) => vec![hrp], + None => { + let coin_hrp = coin.hrp().ok_or(AddressError::InvalidHrp)?; + let other_hrps = BNB_KNOWN_HRPS + .iter() + .map(|another_hrp| another_hrp.to_string()); + std::iter::once(coin_hrp).chain(other_hrps).collect() + }, + }; + Bech32Address::from_str_checked(possible_hrps, address_str).map(BinanceAddress) + } + + pub fn with_public_key_coin_context( + coin: &dyn CoinContext, + public_key: &PublicKey, + prefix: Option, + ) -> AddressResult { + Bech32Address::with_public_key_coin_context(coin, public_key, prefix).map(BinanceAddress) + } + + pub fn from_key_hash_with_coin( + coin: &dyn CoinContext, + key_hash: Data, + ) -> AddressResult { + Bech32Address::from_key_hash_with_coin(coin, key_hash).map(BinanceAddress) + } +} + +impl FromStr for BinanceAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + Bech32Address::from_str(s).map(BinanceAddress) + } +} + +impl fmt::Display for BinanceAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} diff --git a/rust/chains/tw_binance/src/amino.rs b/rust/chains/tw_binance/src/amino.rs new file mode 100644 index 00000000000..6bcc017dd48 --- /dev/null +++ b/rust/chains/tw_binance/src/amino.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use quick_protobuf::MessageWrite; +use tw_encoding::{EncodingError, EncodingResult}; +use tw_memory::Data; +use tw_proto::serialize; + +pub struct AminoEncoder { + /// The Amino content starts with a prefix. + content: Data, +} + +impl AminoEncoder { + pub fn new(prefix: &[u8]) -> AminoEncoder { + AminoEncoder { + content: prefix.to_vec(), + } + } + + pub fn extend_content(mut self, content: &[u8]) -> AminoEncoder { + self.content.extend_from_slice(content); + self + } + + pub fn extend_with_msg(mut self, msg: &M) -> EncodingResult { + let msg_data = serialize(msg).map_err(|_| EncodingError::Internal)?; + self.content.extend_from_slice(&msg_data); + Ok(self) + } + + pub fn encode(self) -> Data { + self.content + } + + pub fn encode_size_prefixed(self) -> EncodingResult { + const CONTENT_SIZE_CAPACITY: usize = 10; + + let content_len = self.content.len(); + let capacity = content_len + CONTENT_SIZE_CAPACITY; + + let mut buffer = Vec::with_capacity(capacity); + + Self::write_varint(&mut buffer, content_len as u64)?; + buffer.extend_from_slice(&self.content); + + Ok(buffer) + } + + /// This method takes `&mut Data` instead of `&mut [u8]` because the given `buffer` can be extended (become longer). + fn write_varint(buffer: &mut Data, num: u64) -> EncodingResult<()> { + let mut writer = quick_protobuf::Writer::new(buffer); + writer + .write_varint(num) + .map_err(|_| EncodingError::Internal) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_encoding::hex::DecodeHex; + + struct TestInput { + prefix: &'static str, + content: &'static str, + content_size_prefixed: bool, + expected: &'static str, + } + + fn amino_encode_impl(input: TestInput) { + let prefix = input.prefix.decode_hex().unwrap(); + let content = input.content.decode_hex().unwrap(); + + let encoder = AminoEncoder::new(&prefix).extend_content(&content); + + let actual = if input.content_size_prefixed { + encoder + .encode_size_prefixed() + .expect("Error on Amino encoding with content size prefix") + } else { + encoder.encode() + }; + + let expected = input.expected.decode_hex().unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_amino_encode() { + let content_size_prefixed = false; + + amino_encode_impl(TestInput { + prefix: "0b0c0d0e", + content: "0102030405060708", + content_size_prefixed, + expected: "0b0c0d0e0102030405060708", + }); + amino_encode_impl(TestInput { + prefix: "0b0c0d0e", + content: "01020304050607080102030405060708010203040506070801020304050607080102030405060708", + content_size_prefixed, + expected: "0b0c0d0e01020304050607080102030405060708010203040506070801020304050607080102030405060708", + }); + } + + #[test] + fn test_amino_encode_with_content_size_prefix() { + let content_size_prefixed = true; + + amino_encode_impl(TestInput { + prefix: "0b0c0d0e", + content: "0102030405060708", + content_size_prefixed, + expected: "0c0b0c0d0e0102030405060708", + }); + amino_encode_impl(TestInput { + prefix: "0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e", + content: "0102030405060708", + content_size_prefixed, + expected: "dc020b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0b0c0d0e0f101112131415161718191a1b1c1d1e0102030405060708", + }); + } +} diff --git a/rust/chains/tw_binance/src/compiler.rs b/rust/chains/tw_binance/src/compiler.rs new file mode 100644 index 00000000000..400c37e36ec --- /dev/null +++ b/rust/chains/tw_binance/src/compiler.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::context::BinanceContext; +use crate::modules::preimager::{JsonPreimager, JsonTxPreimage}; +use crate::modules::serializer::BinanceAminoSerializer; +use crate::modules::tx_builder::TxBuilder; +use crate::signature::BinanceSignature; +use crate::transaction::SignerInfo; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::common::compile_input::SingleSignaturePubkey; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_cosmos_sdk::modules::serializer::json_serializer::JsonSerializer; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_cosmos_sdk::public_key::CosmosPublicKey; +use tw_misc::traits::ToBytesVec; +use tw_proto::Binance::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct BinanceCompiler; + +impl BinanceCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + let JsonTxPreimage { + tx_hash, + encoded_tx, + } = JsonPreimager::preimage_hash(&unsigned_tx)?; + + Ok(CompilerProto::PreSigningOutput { + data_hash: tx_hash.to_vec().into(), + data: encoded_tx.as_bytes().to_vec().into(), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let SingleSignaturePubkey { + signature, + public_key, + } = SingleSignaturePubkey::from_sign_pubkey_list(signatures, public_keys)?; + let signature = BinanceSignature::try_from(signature.as_slice())?; + let public_key_params = None; + let public_key = + Secp256PublicKey::from_bytes(coin, public_key.as_slice(), public_key_params)?; + + let signature_bytes = signature.to_vec(); + let signature_json = JsonSerializer::::serialize_signature( + &public_key, + signature_bytes.clone(), + ); + + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + let signed_tx = unsigned_tx.into_signed(SignerInfo { + public_key, + signature, + }); + + let encoded_tx = BinanceAminoSerializer::serialize_signed_tx(&signed_tx)?; + + let signature_json = + serde_json::to_string(&signature_json).tw_err(|_| SigningErrorType::Error_internal)?; + Ok(Proto::SigningOutput { + encoded: encoded_tx.into(), + signature: signature_bytes.into(), + signature_json: signature_json.into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_binance/src/context.rs b/rust/chains/tw_binance/src/context.rs new file mode 100644 index 00000000000..58952365b63 --- /dev/null +++ b/rust/chains/tw_binance/src/context.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use tw_cosmos_sdk::context::CosmosContext; +use tw_cosmos_sdk::private_key::secp256k1::Secp256PrivateKey; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_cosmos_sdk::signature::secp256k1::Secp256k1Signature; +use tw_hash::hasher::Hasher; + +pub struct BinanceContext; + +impl CosmosContext for BinanceContext { + type Address = BinanceAddress; + type PrivateKey = Secp256PrivateKey; + type PublicKey = Secp256PublicKey; + type Signature = Secp256k1Signature; + + /// Binance Beacon chain uses `sha256` hash. + fn default_tx_hasher() -> Hasher { + Hasher::Sha256 + } +} diff --git a/rust/chains/tw_binance/src/entry.rs b/rust/chains/tw_binance/src/entry.rs new file mode 100644 index 00000000000..55930473d53 --- /dev/null +++ b/rust/chains/tw_binance/src/entry.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::compiler::BinanceCompiler; +use crate::modules::wallet_connect::connector::BinanceWalletConnector; +use crate::signer::BinanceSigner; +use std::str::FromStr; +use tw_bech32_address::bech32_prefix::Bech32Prefix; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_keypair::tw::PublicKey; +use tw_proto::Binance::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct BinanceEntry; + +impl CoinEntry for BinanceEntry { + type AddressPrefix = Bech32Prefix; + type Address = BinanceAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = BinanceWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + BinanceAddress::from_str_with_coin_and_prefix(coin, address.to_string(), prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + BinanceAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + BinanceAddress::with_public_key_coin_context(coin, &public_key, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + BinanceSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + BinanceCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + BinanceCompiler::compile(coin, input, signatures, public_keys) + } + + #[inline] + fn wallet_connector(&self) -> Option { + Some(BinanceWalletConnector) + } +} diff --git a/rust/chains/tw_binance/src/lib.rs b/rust/chains/tw_binance/src/lib.rs new file mode 100644 index 00000000000..6df844a203d --- /dev/null +++ b/rust/chains/tw_binance/src/lib.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod amino; +pub mod compiler; +pub mod context; +pub mod entry; +pub mod modules; +pub mod signature; +pub mod signer; +pub mod transaction; diff --git a/rust/chains/tw_binance/src/modules/mod.rs b/rust/chains/tw_binance/src/modules/mod.rs new file mode 100644 index 00000000000..943cd0f39ce --- /dev/null +++ b/rust/chains/tw_binance/src/modules/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod preimager; +pub mod serializer; +pub mod tx_builder; +pub mod wallet_connect; diff --git a/rust/chains/tw_binance/src/modules/preimager.rs b/rust/chains/tw_binance/src/modules/preimager.rs new file mode 100644 index 00000000000..45e2dacdf53 --- /dev/null +++ b/rust/chains/tw_binance/src/modules/preimager.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::UnsignedTransaction; +use tw_coin_entry::error::prelude::*; +use tw_hash::{sha2, H256}; + +pub struct JsonTxPreimage { + pub encoded_tx: String, + pub tx_hash: H256, +} + +pub struct JsonPreimager; + +impl JsonPreimager { + pub fn preimage_hash(unsigned: &UnsignedTransaction) -> SigningResult { + let encoded_tx = + serde_json::to_string(unsigned).tw_err(|_| SigningErrorType::Error_internal)?; + let tx_hash = sha2::sha256(encoded_tx.as_bytes()); + let tx_hash = H256::try_from(tx_hash.as_slice()).expect("sha256 must return 32 bytes"); + Ok(JsonTxPreimage { + encoded_tx, + tx_hash, + }) + } +} diff --git a/rust/chains/tw_binance/src/modules/serializer.rs b/rust/chains/tw_binance/src/modules/serializer.rs new file mode 100644 index 00000000000..88ec0ad22f3 --- /dev/null +++ b/rust/chains/tw_binance/src/modules/serializer.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::amino::AminoEncoder; +use crate::transaction::SignedTransaction; +use std::borrow::Cow; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::public_key::CosmosPublicKey; +use tw_memory::Data; +use tw_misc::traits::ToBytesVec; +use tw_proto::serialize; +use tw_proto::Binance::Proto; + +/// cbindgen:ignore +pub const TRANSACTION_AMINO_PREFIX: [u8; 4] = [0xF0, 0x62, 0x5D, 0xEE]; +/// cbindgen:ignore +pub const PUBLIC_KEY_PREFIX: [u8; 4] = [0xEB, 0x5A, 0xE9, 0x87]; + +pub struct BinanceAminoSerializer; + +impl BinanceAminoSerializer { + pub fn serialize_signed_tx(tx: &SignedTransaction) -> SigningResult { + let msgs = tx + .unsigned + .msgs + .iter() + .map(|msg| msg.as_ref().to_amino_protobuf().map(Cow::from)) + .collect::>>()?; + + let signature = Self::serialize_signature(tx)?; + let tx = Proto::Transaction { + msgs, + signatures: vec![signature.into()], + memo: tx.unsigned.memo.clone().into(), + source: tx.unsigned.source, + data: tx.unsigned.data.clone().unwrap_or_default().into(), + }; + Ok(AminoEncoder::new(&TRANSACTION_AMINO_PREFIX) + .extend_with_msg(&tx)? + .encode_size_prefixed()?) + } + + pub fn serialize_public_key(public_key: Data) -> Data { + let public_key_len = public_key.len() as u8; + AminoEncoder::new(&PUBLIC_KEY_PREFIX) + // Push the length of the public key. + .extend_content(&[public_key_len]) + .extend_content(public_key.as_slice()) + .encode() + } + + pub fn serialize_signature(signed: &SignedTransaction) -> SigningResult { + let sign_msg = Proto::Signature { + pub_key: Self::serialize_public_key(signed.signer.public_key.to_bytes()).into(), + signature: signed.signer.signature.to_vec().into(), + account_number: signed.unsigned.account_number, + sequence: signed.unsigned.sequence, + }; + // There is no need to use Amino encoding here as the prefix is empty. + serialize(&sign_msg).tw_err(|_| SigningErrorType::Error_internal) + } +} diff --git a/rust/chains/tw_binance/src/modules/tx_builder.rs b/rust/chains/tw_binance/src/modules/tx_builder.rs new file mode 100644 index 00000000000..ce1340d63c0 --- /dev/null +++ b/rust/chains/tw_binance/src/modules/tx_builder.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::message::{BinanceMessageEnum, TWBinanceProto}; +use crate::transaction::UnsignedTransaction; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_proto::Binance::Proto; + +pub struct TxBuilder; + +impl TxBuilder { + pub fn unsigned_tx_from_proto( + coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + let msg = BinanceMessageEnum::from_tw_proto(coin, &input.order_oneof)?; + Ok(UnsignedTransaction { + account_number: input.account_number, + chain_id: input.chain_id.to_string(), + data: None, + memo: input.memo.to_string(), + msgs: vec![msg], + sequence: input.sequence, + source: input.source, + }) + } + + pub fn unsigned_tx_to_proto( + unsigned: &UnsignedTransaction, + ) -> SigningResult> { + if unsigned.msgs.len() != 1 { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Expected exactly one Transaction Message"); + } + let msg = unsigned + .msgs + .first() + .expect("There should be exactly one message") + .to_tw_proto(); + + Ok(Proto::SigningInput { + chain_id: unsigned.chain_id.clone().into(), + account_number: unsigned.account_number, + sequence: unsigned.sequence, + source: unsigned.source, + memo: unsigned.memo.clone().into(), + private_key: Cow::default(), + order_oneof: msg, + }) + } +} diff --git a/rust/chains/tw_binance/src/modules/wallet_connect/connector.rs b/rust/chains/tw_binance/src/modules/wallet_connect/connector.rs new file mode 100644 index 00000000000..e66b4688a2f --- /dev/null +++ b/rust/chains/tw_binance/src/modules/wallet_connect/connector.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::TxBuilder; +use crate::modules::wallet_connect::types::SignAminoRequest; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::wallet_connector::WalletConnector; +use tw_coin_entry::signing_output_error; +use tw_proto::WalletConnect::Proto::{ + self as WCProto, mod_ParseRequestOutput::OneOfsigning_input_oneof as SigningInputEnum, +}; + +pub struct BinanceWalletConnector; + +impl WalletConnector for BinanceWalletConnector { + fn parse_request( + &self, + coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> WCProto::ParseRequestOutput<'static> { + Self::parse_request_impl(coin, request) + .unwrap_or_else(|e| signing_output_error!(WCProto::ParseRequestOutput, e)) + } +} + +impl BinanceWalletConnector { + fn parse_request_impl( + coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> SigningResult> { + match request.method { + WCProto::Method::CosmosSignAmino => Self::parse_sign_amino_request(coin, request), + _ => SigningError::err(SigningErrorType::Error_not_supported) + .context("Unknown WalletConnect method"), + } + } + + pub fn parse_sign_amino_request( + _coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> SigningResult> { + let amino_req: SignAminoRequest = serde_json::from_str(&request.payload) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error deserializing WalletConnect signAmino request as JSON")?; + + // Parse a `SigningInput` from the given `signDoc`. + let signing_input = TxBuilder::unsigned_tx_to_proto(&amino_req.sign_doc)?; + + Ok(WCProto::ParseRequestOutput { + signing_input_oneof: SigningInputEnum::binance(signing_input), + ..WCProto::ParseRequestOutput::default() + }) + } +} diff --git a/rust/chains/tw_binance/src/modules/wallet_connect/mod.rs b/rust/chains/tw_binance/src/modules/wallet_connect/mod.rs new file mode 100644 index 00000000000..1a90ae8a9cf --- /dev/null +++ b/rust/chains/tw_binance/src/modules/wallet_connect/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod connector; +pub mod types; diff --git a/rust/chains/tw_binance/src/modules/wallet_connect/types.rs b/rust/chains/tw_binance/src/modules/wallet_connect/types.rs new file mode 100644 index 00000000000..f5ac124e47e --- /dev/null +++ b/rust/chains/tw_binance/src/modules/wallet_connect/types.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::UnsignedTransaction; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct SignAminoRequest { + #[serde(rename = "signDoc")] + pub sign_doc: UnsignedTransaction, +} diff --git a/rust/chains/tw_binance/src/signature.rs b/rust/chains/tw_binance/src/signature.rs new file mode 100644 index 00000000000..45e1b5c6fca --- /dev/null +++ b/rust/chains/tw_binance/src/signature.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_keypair::ecdsa::secp256k1; + +pub type BinanceSignature = secp256k1::VerifySignature; diff --git a/rust/chains/tw_binance/src/signer.rs b/rust/chains/tw_binance/src/signer.rs new file mode 100644 index 00000000000..86cb5da5b4b --- /dev/null +++ b/rust/chains/tw_binance/src/signer.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::context::BinanceContext; +use crate::modules::preimager::{JsonPreimager, JsonTxPreimage}; +use crate::modules::serializer::BinanceAminoSerializer; +use crate::modules::tx_builder::TxBuilder; +use crate::signature::BinanceSignature; +use crate::transaction::SignerInfo; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_cosmos_sdk::modules::serializer::json_serializer::JsonSerializer; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_misc::traits::ToBytesVec; +use tw_proto::Binance::Proto; + +pub struct BinanceSigner; + +impl BinanceSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + let JsonTxPreimage { tx_hash, .. } = JsonPreimager::preimage_hash(&unsigned_tx)?; + + let key_pair = secp256k1::KeyPair::try_from(input.private_key.as_ref())?; + + let signature = BinanceSignature::from(key_pair.sign(tx_hash)?); + let public_key = + Secp256PublicKey::from_secp256k1_public_key(coin.public_key_type(), key_pair.public())?; + + let signature_bytes = signature.to_vec(); + let signature_json = JsonSerializer::::serialize_signature( + &public_key, + signature_bytes.clone(), + ); + + let signed_tx = unsigned_tx.into_signed(SignerInfo { + public_key, + signature, + }); + let encoded_tx = BinanceAminoSerializer::serialize_signed_tx(&signed_tx)?; + + let signature_json = + serde_json::to_string(&signature_json).tw_err(|_| SigningErrorType::Error_internal)?; + Ok(Proto::SigningOutput { + encoded: encoded_tx.into(), + signature: signature_bytes.into(), + signature_json: signature_json.into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/htlt_order.rs b/rust/chains/tw_binance/src/transaction/message/htlt_order.rs new file mode 100644 index 00000000000..902ae019e82 --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/htlt_order.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto, Token}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_encoding::hex::as_hex; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +#[derive(Deserialize, Serialize)] +pub struct HTLTOrder { + pub amount: Vec, + pub cross_chain: bool, + pub expected_income: String, + pub from: BinanceAddress, + pub height_span: i64, + #[serde(with = "as_hex")] + pub random_number_hash: Data, + pub recipient_other_chain: String, + pub sender_other_chain: String, + pub timestamp: i64, + pub to: BinanceAddress, +} + +impl HTLTOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xB3, 0x3F, 0x9A, 0x24]; +} + +impl BinanceMessage for HTLTOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for HTLTOrder { + type Proto<'a> = Proto::HTLTOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + let to = BinanceAddress::from_key_hash_with_coin(coin, msg.to.to_vec())?; + + let amount = msg.amount.iter().map(Token::from_tw_proto).collect(); + + Ok(HTLTOrder { + from, + to, + recipient_other_chain: msg.recipient_other_chain.to_string(), + sender_other_chain: msg.sender_other_chain.to_string(), + random_number_hash: msg.random_number_hash.to_vec(), + timestamp: msg.timestamp, + amount, + expected_income: msg.expected_income.to_string(), + height_span: msg.height_span, + cross_chain: msg.cross_chain, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::HTLTOrder { + from: self.from.data().into(), + to: self.to.data().into(), + recipient_other_chain: self.recipient_other_chain.clone().into(), + sender_other_chain: self.sender_other_chain.clone().into(), + random_number_hash: self.random_number_hash.clone().into(), + timestamp: self.timestamp, + amount: self.amount.iter().map(Token::to_tw_proto).collect(), + expected_income: self.expected_income.clone().into(), + height_span: self.height_span, + cross_chain: self.cross_chain, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct DepositHTLTOrder { + pub amount: Vec, + pub from: BinanceAddress, + #[serde(with = "as_hex")] + pub swap_id: Data, +} + +impl DepositHTLTOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x63, 0x98, 0x64, 0x96]; +} + +impl BinanceMessage for DepositHTLTOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for DepositHTLTOrder { + type Proto<'a> = Proto::DepositHTLTOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + let amount = msg.amount.iter().map(Token::from_tw_proto).collect(); + + Ok(DepositHTLTOrder { + from, + amount, + swap_id: msg.swap_id.to_vec(), + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::DepositHTLTOrder { + from: self.from.data().into(), + amount: self.amount.iter().map(Token::to_tw_proto).collect(), + swap_id: self.swap_id.clone().into(), + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct ClaimHTLTOrder { + pub from: BinanceAddress, + #[serde(with = "as_hex")] + pub random_number: Data, + #[serde(with = "as_hex")] + pub swap_id: Data, +} + +impl ClaimHTLTOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xC1, 0x66, 0x53, 0x00]; +} + +impl BinanceMessage for ClaimHTLTOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for ClaimHTLTOrder { + type Proto<'a> = Proto::ClaimHTLOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + + Ok(ClaimHTLTOrder { + from, + swap_id: msg.swap_id.to_vec(), + random_number: msg.random_number.to_vec(), + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::ClaimHTLOrder { + from: self.from.data().into(), + swap_id: self.swap_id.clone().into(), + random_number: self.random_number.clone().into(), + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct RefundHTLTOrder { + pub from: BinanceAddress, + #[serde(with = "as_hex")] + pub swap_id: Data, +} + +impl RefundHTLTOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x34, 0x54, 0xA2, 0x7C]; +} + +impl TWBinanceProto for RefundHTLTOrder { + type Proto<'a> = Proto::RefundHTLTOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + let swap_id = msg.swap_id.to_vec(); + + Ok(RefundHTLTOrder { from, swap_id }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::RefundHTLTOrder { + from: self.from.data().into(), + swap_id: self.swap_id.clone().into(), + } + } +} + +impl BinanceMessage for RefundHTLTOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/mod.rs b/rust/chains/tw_binance/src/transaction/message/mod.rs new file mode 100644 index 00000000000..0879bcf5b3e --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/mod.rs @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::{Deserialize, Serialize, Serializer}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; +use tw_proto::Binance::Proto::{self, mod_SigningInput::OneOforder_oneof as BinanceMessageProto}; + +pub mod htlt_order; +pub mod send_order; +pub mod side_chain_delegate; +pub mod time_lock_order; +pub mod token_order; +pub mod trade_order; +pub mod tranfer_out_order; + +pub trait BinanceMessage { + fn to_amino_protobuf(&self) -> SigningResult; +} + +/// A Binance message represented as a Trust Wallet Core Protobuf message. +pub trait TWBinanceProto: Sized { + type Proto<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult; + + fn to_tw_proto(&self) -> Self::Proto<'static>; +} + +/// Please note that some of the fields are typped such as `SideDelegateOrder`. +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +pub enum BinanceMessageEnum { + HTLTOrder(htlt_order::HTLTOrder), + DepositHTLTOrder(htlt_order::DepositHTLTOrder), + ClaimHTLTOrder(htlt_order::ClaimHTLTOrder), + RefundHTLTOrder(htlt_order::RefundHTLTOrder), + SendOrder(send_order::SendOrder), + SideDelegateOrder(side_chain_delegate::SideDelegateOrder), + SideRedelegateOrder(side_chain_delegate::SideRedelegateOrder), + SideUndelegateOrder(side_chain_delegate::SideUndelegateOrder), + StakeMigrationOrder(side_chain_delegate::StakeMigrationOrder), + TimeLockOrder(time_lock_order::TimeLockOrder), + TimeRelockOrder(time_lock_order::TimeRelockOrder), + TimeUnlockOrder(time_lock_order::TimeUnlockOrder), + TokenFreezeOrder(token_order::TokenFreezeOrder), + TokenUnfreezeOrder(token_order::TokenUnfreezeOrder), + TokenIssueOrder(token_order::TokenIssueOrder), + TokenMintOrder(token_order::TokenMintOrder), + TokenBurnOrder(token_order::TokenBurnOrder), + NewTradeOrder(trade_order::NewTradeOrder), + CancelTradeOrder(trade_order::CancelTradeOrder), + TransferOutOrder(tranfer_out_order::TransferOutOrder), +} + +impl TWBinanceProto for BinanceMessageEnum { + type Proto<'a> = BinanceMessageProto<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + match msg { + BinanceMessageProto::trade_order(ref order) => { + trade_order::NewTradeOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::NewTradeOrder) + }, + BinanceMessageProto::cancel_trade_order(ref order) => { + trade_order::CancelTradeOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::CancelTradeOrder) + }, + BinanceMessageProto::send_order(ref order) => { + send_order::SendOrder::from_tw_proto(coin, order).map(BinanceMessageEnum::SendOrder) + }, + BinanceMessageProto::freeze_order(ref order) => { + token_order::TokenFreezeOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TokenFreezeOrder) + }, + BinanceMessageProto::unfreeze_order(ref order) => { + token_order::TokenUnfreezeOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TokenUnfreezeOrder) + }, + BinanceMessageProto::htlt_order(ref order) => { + htlt_order::HTLTOrder::from_tw_proto(coin, order).map(BinanceMessageEnum::HTLTOrder) + }, + BinanceMessageProto::depositHTLT_order(ref order) => { + htlt_order::DepositHTLTOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::DepositHTLTOrder) + }, + BinanceMessageProto::claimHTLT_order(ref order) => { + htlt_order::ClaimHTLTOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::ClaimHTLTOrder) + }, + BinanceMessageProto::refundHTLT_order(ref order) => { + htlt_order::RefundHTLTOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::RefundHTLTOrder) + }, + BinanceMessageProto::issue_order(ref order) => { + token_order::TokenIssueOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TokenIssueOrder) + }, + BinanceMessageProto::mint_order(ref order) => { + token_order::TokenMintOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TokenMintOrder) + }, + BinanceMessageProto::burn_order(ref order) => { + token_order::TokenBurnOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TokenBurnOrder) + }, + BinanceMessageProto::transfer_out_order(ref order) => { + tranfer_out_order::TransferOutOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TransferOutOrder) + }, + BinanceMessageProto::side_delegate_order(ref order) => { + side_chain_delegate::SideDelegateOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::SideDelegateOrder) + }, + BinanceMessageProto::side_redelegate_order(ref order) => { + side_chain_delegate::SideRedelegateOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::SideRedelegateOrder) + }, + BinanceMessageProto::side_undelegate_order(ref order) => { + side_chain_delegate::SideUndelegateOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::SideUndelegateOrder) + }, + BinanceMessageProto::time_lock_order(ref order) => { + time_lock_order::TimeLockOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TimeLockOrder) + }, + BinanceMessageProto::time_relock_order(ref order) => { + time_lock_order::TimeRelockOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TimeRelockOrder) + }, + BinanceMessageProto::time_unlock_order(ref order) => { + time_lock_order::TimeUnlockOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::TimeUnlockOrder) + }, + BinanceMessageProto::side_stake_migration_order(ref order) => { + side_chain_delegate::StakeMigrationOrder::from_tw_proto(coin, order) + .map(BinanceMessageEnum::StakeMigrationOrder) + }, + BinanceMessageProto::None => SigningError::err(SigningErrorType::Error_invalid_params), + } + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + match self { + BinanceMessageEnum::HTLTOrder(m) => BinanceMessageProto::htlt_order(m.to_tw_proto()), + BinanceMessageEnum::DepositHTLTOrder(m) => { + BinanceMessageProto::depositHTLT_order(m.to_tw_proto()) + }, + BinanceMessageEnum::ClaimHTLTOrder(m) => { + BinanceMessageProto::claimHTLT_order(m.to_tw_proto()) + }, + BinanceMessageEnum::RefundHTLTOrder(m) => { + BinanceMessageProto::refundHTLT_order(m.to_tw_proto()) + }, + BinanceMessageEnum::SendOrder(m) => BinanceMessageProto::send_order(m.to_tw_proto()), + BinanceMessageEnum::SideDelegateOrder(m) => { + BinanceMessageProto::side_delegate_order(m.to_tw_proto()) + }, + BinanceMessageEnum::SideRedelegateOrder(m) => { + BinanceMessageProto::side_redelegate_order(m.to_tw_proto()) + }, + BinanceMessageEnum::SideUndelegateOrder(m) => { + BinanceMessageProto::side_undelegate_order(m.to_tw_proto()) + }, + BinanceMessageEnum::StakeMigrationOrder(m) => { + BinanceMessageProto::side_stake_migration_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TimeLockOrder(m) => { + BinanceMessageProto::time_lock_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TimeRelockOrder(m) => { + BinanceMessageProto::time_relock_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TimeUnlockOrder(m) => { + BinanceMessageProto::time_unlock_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TokenFreezeOrder(m) => { + BinanceMessageProto::freeze_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TokenUnfreezeOrder(m) => { + BinanceMessageProto::unfreeze_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TokenIssueOrder(m) => { + BinanceMessageProto::issue_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TokenMintOrder(m) => { + BinanceMessageProto::mint_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TokenBurnOrder(m) => { + BinanceMessageProto::burn_order(m.to_tw_proto()) + }, + BinanceMessageEnum::NewTradeOrder(m) => { + BinanceMessageProto::trade_order(m.to_tw_proto()) + }, + BinanceMessageEnum::CancelTradeOrder(m) => { + BinanceMessageProto::cancel_trade_order(m.to_tw_proto()) + }, + BinanceMessageEnum::TransferOutOrder(m) => { + BinanceMessageProto::transfer_out_order(m.to_tw_proto()) + }, + } + } +} + +impl<'a> AsRef for BinanceMessageEnum { + fn as_ref(&self) -> &(dyn BinanceMessage + 'a) { + match self { + BinanceMessageEnum::HTLTOrder(m) => m, + BinanceMessageEnum::DepositHTLTOrder(m) => m, + BinanceMessageEnum::ClaimHTLTOrder(m) => m, + BinanceMessageEnum::RefundHTLTOrder(m) => m, + BinanceMessageEnum::SendOrder(m) => m, + BinanceMessageEnum::SideDelegateOrder(m) => m, + BinanceMessageEnum::SideRedelegateOrder(m) => m, + BinanceMessageEnum::SideUndelegateOrder(m) => m, + BinanceMessageEnum::StakeMigrationOrder(m) => m, + BinanceMessageEnum::TimeLockOrder(m) => m, + BinanceMessageEnum::TimeRelockOrder(m) => m, + BinanceMessageEnum::TimeUnlockOrder(m) => m, + BinanceMessageEnum::TokenFreezeOrder(m) => m, + BinanceMessageEnum::TokenUnfreezeOrder(m) => m, + BinanceMessageEnum::TokenIssueOrder(m) => m, + BinanceMessageEnum::TokenMintOrder(m) => m, + BinanceMessageEnum::TokenBurnOrder(m) => m, + BinanceMessageEnum::NewTradeOrder(m) => m, + BinanceMessageEnum::CancelTradeOrder(m) => m, + BinanceMessageEnum::TransferOutOrder(m) => m, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct Token { + /// Amount. + pub amount: i64, + /// Token ID. + pub denom: String, +} + +impl Token { + pub fn serialize_with_string_amount(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct TokenWithStringAmount<'a> { + amount: String, + denom: &'a str, + } + + TokenWithStringAmount { + amount: self.amount.to_string(), + denom: &self.denom, + } + .serialize(serializer) + } + + fn from_tw_proto(msg: &Proto::mod_SendOrder::Token<'_>) -> Self { + Token { + denom: msg.denom.to_string(), + amount: msg.amount, + } + } + + fn to_tw_proto(&self) -> Proto::mod_SendOrder::Token<'static> { + Proto::mod_SendOrder::Token { + denom: self.denom.clone().into(), + amount: self.amount, + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/send_order.rs b/rust/chains/tw_binance/src/transaction/message/send_order.rs new file mode 100644 index 00000000000..6ffd7d69be2 --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/send_order.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto, Token}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +/// Either an input or output of a `SendOrder`. +#[derive(Deserialize, Serialize)] +pub struct InOut { + /// Source address. + pub address: BinanceAddress, + + /// Input coin amounts. + pub coins: Vec, +} + +impl InOut { + pub fn to_input_proto(&self) -> Proto::mod_SendOrder::Input<'static> { + Proto::mod_SendOrder::Input { + address: self.address.data().into(), + coins: self.coins.iter().map(Token::to_tw_proto).collect(), + } + } + + pub fn to_output_proto(&self) -> Proto::mod_SendOrder::Output<'static> { + Proto::mod_SendOrder::Output { + address: self.address.data().into(), + coins: self.coins.iter().map(Token::to_tw_proto).collect(), + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct SendOrder { + pub inputs: Vec, + pub outputs: Vec, +} + +impl SendOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x2A, 0x2C, 0x87, 0xFA]; +} + +impl BinanceMessage for SendOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for SendOrder { + type Proto<'a> = Proto::SendOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + fn in_out_from_proto( + coin: &dyn CoinContext, + address_key_hash: &[u8], + coins: &[Proto::mod_SendOrder::Token], + ) -> SigningResult { + let address = BinanceAddress::from_key_hash_with_coin(coin, address_key_hash.to_vec())?; + let coins = coins.iter().map(Token::from_tw_proto).collect(); + + Ok(InOut { address, coins }) + } + + let inputs = msg + .inputs + .iter() + .map(|input| in_out_from_proto(coin, &input.address, &input.coins)) + .collect::>>()?; + + let outputs = msg + .outputs + .iter() + .map(|output| in_out_from_proto(coin, &output.address, &output.coins)) + .collect::>>()?; + + Ok(SendOrder { inputs, outputs }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::SendOrder { + inputs: self.inputs.iter().map(InOut::to_input_proto).collect(), + outputs: self.outputs.iter().map(InOut::to_output_proto).collect(), + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/side_chain_delegate.rs b/rust/chains/tw_binance/src/transaction/message/side_chain_delegate.rs new file mode 100644 index 00000000000..4684056cede --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/side_chain_delegate.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto, Token}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_evm::address::Address as EthereumAddress; +use tw_memory::Data; +use tw_misc::serde::Typed; +use tw_proto::Binance::Proto; + +pub type SideDelegateOrder = Typed; + +/// cosmos-sdk/MsgSideChainDelegate +#[derive(Deserialize, Serialize)] +pub struct SideDelegateOrderValue { + #[serde(serialize_with = "Token::serialize_with_string_amount")] + pub delegation: Token, + pub delegator_addr: BinanceAddress, + pub side_chain_id: String, + pub validator_addr: BinanceAddress, +} + +impl SideDelegateOrderValue { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xE3, 0xA0, 0x7F, 0xD2]; + /// cbindgen:ignore + pub const MESSAGE_TYPE: &'static str = "cosmos-sdk/MsgSideChainDelegate"; +} + +impl BinanceMessage for SideDelegateOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&SideDelegateOrderValue::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for SideDelegateOrder { + type Proto<'a> = Proto::SideChainDelegate<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let delegator_addr = + BinanceAddress::from_key_hash_with_coin(coin, msg.delegator_addr.to_vec())?; + let validator_addr = BinanceAddress::new_validator_addr(msg.validator_addr.to_vec())?; + + let delegation = msg + .delegation + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + + let value = SideDelegateOrderValue { + delegator_addr, + validator_addr, + delegation: Token::from_tw_proto(delegation), + side_chain_id: msg.chain_id.to_string(), + }; + Ok(Typed { + ty: SideDelegateOrderValue::MESSAGE_TYPE.to_string(), + value, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::SideChainDelegate { + delegator_addr: self.value.delegator_addr.data().into(), + validator_addr: self.value.validator_addr.data().into(), + delegation: Some(self.value.delegation.to_tw_proto()), + chain_id: self.value.side_chain_id.clone().into(), + } + } +} + +pub type SideRedelegateOrder = Typed; + +/// cosmos-sdk/MsgSideChainRedelegate +#[derive(Deserialize, Serialize)] +pub struct SideRedelegateOrderValue { + #[serde(serialize_with = "Token::serialize_with_string_amount")] + pub amount: Token, + pub delegator_addr: BinanceAddress, + pub side_chain_id: String, + pub validator_dst_addr: BinanceAddress, + pub validator_src_addr: BinanceAddress, +} + +impl SideRedelegateOrderValue { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xE3, 0xCE, 0xD3, 0x64]; + /// cbindgen:ignore + pub const MESSAGE_TYPE: &'static str = "cosmos-sdk/MsgSideChainRedelegate"; +} + +impl BinanceMessage for SideRedelegateOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&SideRedelegateOrderValue::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for SideRedelegateOrder { + type Proto<'a> = Proto::SideChainRedelegate<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let delegator_addr = + BinanceAddress::from_key_hash_with_coin(coin, msg.delegator_addr.to_vec())?; + let validator_src_addr = + BinanceAddress::new_validator_addr(msg.validator_src_addr.to_vec())?; + let validator_dst_addr = + BinanceAddress::new_validator_addr(msg.validator_dst_addr.to_vec())?; + + let amount = msg + .amount + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + + let value = SideRedelegateOrderValue { + delegator_addr, + validator_src_addr, + validator_dst_addr, + amount: Token::from_tw_proto(amount), + side_chain_id: msg.chain_id.to_string(), + }; + Ok(Typed { + ty: SideRedelegateOrderValue::MESSAGE_TYPE.to_string(), + value, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::SideChainRedelegate { + delegator_addr: self.value.delegator_addr.data().into(), + validator_src_addr: self.value.validator_src_addr.data().into(), + validator_dst_addr: self.value.validator_dst_addr.data().into(), + amount: Some(self.value.amount.to_tw_proto()), + chain_id: self.value.side_chain_id.clone().into(), + } + } +} + +pub type SideUndelegateOrder = Typed; + +/// cosmos-sdk/MsgSideChainUndelegate +#[derive(Deserialize, Serialize)] +pub struct SideUndelegateOrderValue { + #[serde(serialize_with = "Token::serialize_with_string_amount")] + pub amount: Token, + pub delegator_addr: BinanceAddress, + pub side_chain_id: String, + pub validator_addr: BinanceAddress, +} + +impl SideUndelegateOrderValue { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x51, 0x4F, 0x7E, 0x0E]; + /// cbindgen:ignore + pub const MESSAGE_TYPE: &'static str = "cosmos-sdk/MsgSideChainUndelegate"; +} + +impl BinanceMessage for SideUndelegateOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&SideUndelegateOrderValue::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for SideUndelegateOrder { + type Proto<'a> = Proto::SideChainUndelegate<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let delegator_addr = + BinanceAddress::from_key_hash_with_coin(coin, msg.delegator_addr.to_vec())?; + let validator_addr = BinanceAddress::new_validator_addr(msg.validator_addr.to_vec())?; + + let amount = msg + .amount + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + + let value = SideUndelegateOrderValue { + delegator_addr, + validator_addr, + amount: Token::from_tw_proto(amount), + side_chain_id: msg.chain_id.to_string(), + }; + Ok(Typed { + ty: SideUndelegateOrderValue::MESSAGE_TYPE.to_string(), + value, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::SideChainUndelegate { + delegator_addr: self.value.delegator_addr.data().into(), + validator_addr: self.value.validator_addr.data().into(), + amount: Some(self.value.amount.to_tw_proto()), + chain_id: self.value.side_chain_id.clone().into(), + } + } +} + +pub type StakeMigrationOrder = Typed; + +/// https://github.com/bnb-chain/bnc-cosmos-sdk/blob/cf3ab19af300ccd6a6381287c3fae6bf6ac12f5e/x/stake/types/stake_migration.go#L29-L35 +#[derive(Deserialize, Serialize)] +pub struct StakeMigrationOrderValue { + #[serde(serialize_with = "Token::serialize_with_string_amount")] + pub amount: Token, + pub delegator_addr: EthereumAddress, + pub refund_addr: BinanceAddress, + pub validator_dst_addr: EthereumAddress, + pub validator_src_addr: BinanceAddress, +} + +impl StakeMigrationOrderValue { + /// cbindgen:ignore + /// https://github.com/bnb-chain/javascript-sdk/blob/442286ac2923fdfd7cb4fb2299f722ec263c714c/src/types/tx/stdTx.ts#L68 + pub const PREFIX: [u8; 4] = [0x38, 0x58, 0x91, 0x96]; + /// cbindgen:ignore + pub const MESSAGE_TYPE: &'static str = "cosmos-sdk/MsgSideChainStakeMigration"; +} + +impl BinanceMessage for StakeMigrationOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&StakeMigrationOrderValue::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for StakeMigrationOrder { + type Proto<'a> = Proto::SideChainStakeMigration<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let delegator_addr = EthereumAddress::try_from(msg.delegator_addr.as_ref())?; + let refund_addr = BinanceAddress::from_key_hash_with_coin(coin, msg.refund_addr.to_vec())?; + let validator_dst_addr = EthereumAddress::try_from(msg.validator_dst_addr.as_ref())?; + let validator_src_addr = + BinanceAddress::new_validator_addr(msg.validator_src_addr.to_vec())?; + + let amount = msg + .amount + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + + let value = StakeMigrationOrderValue { + amount: Token::from_tw_proto(amount), + delegator_addr, + refund_addr, + validator_dst_addr, + validator_src_addr, + }; + Ok(Typed { + ty: StakeMigrationOrderValue::MESSAGE_TYPE.to_string(), + value, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::SideChainStakeMigration { + delegator_addr: self.value.delegator_addr.data().into(), + validator_src_addr: self.value.validator_src_addr.data().into(), + validator_dst_addr: self.value.validator_dst_addr.data().into(), + refund_addr: self.value.refund_addr.data().into(), + amount: Some(self.value.amount.to_tw_proto()), + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/time_lock_order.rs b/rust/chains/tw_binance/src/transaction/message/time_lock_order.rs new file mode 100644 index 00000000000..9e2e4e21e8c --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/time_lock_order.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto, Token}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +#[derive(Deserialize, Serialize)] +pub struct TimeLockOrder { + pub amount: Vec, + pub description: String, + pub from: BinanceAddress, + pub lock_time: i64, +} + +impl TimeLockOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x07, 0x92, 0x15, 0x31]; +} + +impl BinanceMessage for TimeLockOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TimeLockOrder { + type Proto<'a> = Proto::TimeLockOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from_address.to_vec())?; + let amount = msg.amount.iter().map(Token::from_tw_proto).collect(); + + Ok(TimeLockOrder { + from, + description: msg.description.to_string(), + amount, + lock_time: msg.lock_time, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TimeLockOrder { + from_address: self.from.data().into(), + description: self.description.clone().into(), + amount: self.amount.iter().map(Token::to_tw_proto).collect(), + lock_time: self.lock_time, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TimeRelockOrder { + /// If the amount is empty or omitted, set null to avoid signature verification error. + pub amount: Option>, + pub description: String, + pub from: BinanceAddress, + pub lock_time: i64, + pub time_lock_id: i64, +} + +impl TimeRelockOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x50, 0x47, 0x11, 0xDA]; +} + +impl BinanceMessage for TimeRelockOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TimeRelockOrder { + type Proto<'a> = Proto::TimeRelockOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from_address.to_vec())?; + + let amount = if msg.amount.is_empty() { + None + } else { + Some(msg.amount.iter().map(Token::from_tw_proto).collect()) + }; + + Ok(TimeRelockOrder { + from, + time_lock_id: msg.id, + description: msg.description.to_string(), + amount, + lock_time: msg.lock_time, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + let amount = match self.amount { + Some(ref tokens) => tokens.iter().map(Token::to_tw_proto).collect(), + None => Vec::default(), + }; + + Proto::TimeRelockOrder { + from_address: self.from.data().into(), + id: self.time_lock_id, + description: self.description.clone().into(), + amount, + lock_time: self.lock_time, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TimeUnlockOrder { + pub from: BinanceAddress, + pub time_lock_id: i64, +} + +impl TimeUnlockOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xC4, 0x05, 0x0C, 0x6C]; +} + +impl BinanceMessage for TimeUnlockOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TimeUnlockOrder { + type Proto<'a> = Proto::TimeUnlockOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from_address.to_vec())?; + Ok(TimeUnlockOrder { + from, + time_lock_id: msg.id, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TimeUnlockOrder { + from_address: self.from.data().into(), + id: self.time_lock_id, + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/token_order.rs b/rust/chains/tw_binance/src/transaction/message/token_order.rs new file mode 100644 index 00000000000..ec88ba88e26 --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/token_order.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +#[derive(Deserialize, Serialize)] +pub struct TokenFreezeOrder { + pub amount: i64, + pub from: BinanceAddress, + pub symbol: String, +} + +impl TokenFreezeOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xE7, 0x74, 0xB3, 0x2D]; +} + +impl BinanceMessage for TokenFreezeOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TokenFreezeOrder { + type Proto<'a> = Proto::TokenFreezeOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + Ok(TokenFreezeOrder { + from, + symbol: msg.symbol.to_string(), + amount: msg.amount, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TokenFreezeOrder { + from: self.from.data().into(), + symbol: self.symbol.clone().into(), + amount: self.amount, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TokenUnfreezeOrder { + pub amount: i64, + pub from: BinanceAddress, + pub symbol: String, +} + +impl TokenUnfreezeOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x65, 0x15, 0xFF, 0x0D]; +} + +impl BinanceMessage for TokenUnfreezeOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TokenUnfreezeOrder { + type Proto<'a> = Proto::TokenUnfreezeOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + Ok(TokenUnfreezeOrder { + from, + symbol: msg.symbol.to_string(), + amount: msg.amount, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TokenUnfreezeOrder { + from: self.from.data().into(), + symbol: self.symbol.clone().into(), + amount: self.amount, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TokenIssueOrder { + pub from: BinanceAddress, + pub mintable: bool, + pub name: String, + pub symbol: String, + pub total_supply: i64, +} + +impl TokenIssueOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x17, 0xEF, 0xAB, 0x80]; +} + +impl BinanceMessage for TokenIssueOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TokenIssueOrder { + type Proto<'a> = Proto::TokenIssueOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + Ok(TokenIssueOrder { + from, + name: msg.name.to_string(), + symbol: msg.symbol.to_string(), + total_supply: msg.total_supply, + mintable: msg.mintable, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TokenIssueOrder { + from: self.from.data().into(), + name: self.name.clone().into(), + symbol: self.symbol.clone().into(), + total_supply: self.total_supply, + mintable: self.mintable, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TokenMintOrder { + pub amount: i64, + pub from: BinanceAddress, + pub symbol: String, +} + +impl TokenMintOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x46, 0x7E, 0x08, 0x29]; +} + +impl BinanceMessage for TokenMintOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TokenMintOrder { + type Proto<'a> = Proto::TokenMintOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + Ok(TokenMintOrder { + from, + symbol: msg.symbol.to_string(), + amount: msg.amount, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TokenMintOrder { + from: self.from.data().into(), + symbol: self.symbol.clone().into(), + amount: self.amount, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct TokenBurnOrder { + pub amount: i64, + pub from: BinanceAddress, + pub symbol: String, +} + +impl TokenBurnOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x7E, 0xD2, 0xD2, 0xA0]; +} + +impl BinanceMessage for TokenBurnOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TokenBurnOrder { + type Proto<'a> = Proto::TokenBurnOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + Ok(TokenBurnOrder { + from, + symbol: msg.symbol.to_string(), + amount: msg.amount, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TokenBurnOrder { + from: self.from.data().into(), + symbol: self.symbol.clone().into(), + amount: self.amount, + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/trade_order.rs b/rust/chains/tw_binance/src/transaction/message/trade_order.rs new file mode 100644 index 00000000000..0add4dcd87b --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/trade_order.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +#[repr(i64)] +#[derive( + Clone, Copy, serde_repr::Deserialize_repr, serde_repr::Serialize_repr, strum_macros::FromRepr, +)] +pub enum OrderType { + /// https://github.com/bnb-chain/python-sdk/blob/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/binance_chain/constants.py#L62 + Limit = 2, +} + +#[derive(Deserialize, Serialize)] +pub struct NewTradeOrder { + /// Order id, optional. + pub id: String, + /// Order type. + #[serde(rename = "ordertype")] + pub order_type: OrderType, + /// Price of the order, which is the real price multiplied by 1e8 (10^8) and rounded to integer. + pub price: i64, + /// Quantity of the order, which is the real price multiplied by 1e8 (10^8) and rounded to integer. + pub quantity: i64, + /// Originating address. + pub sender: BinanceAddress, + /// 1 for buy and 2 for sell. + pub side: i64, + /// Symbol for trading pair in full name of the tokens. + pub symbol: String, + /// 1 for Good Till Expire(GTE) order and 3 for Immediate Or Cancel (IOC). + #[serde(rename = "timeinforce")] + pub time_in_force: i64, +} + +impl NewTradeOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0xCE, 0x6D, 0xC0, 0x43]; +} + +impl BinanceMessage for NewTradeOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for NewTradeOrder { + type Proto<'a> = Proto::TradeOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let order_type = OrderType::from_repr(msg.ordertype) + .or_tw_err(SigningErrorType::Error_invalid_params)?; + let sender = BinanceAddress::from_key_hash_with_coin(coin, msg.sender.to_vec())?; + + Ok(NewTradeOrder { + id: msg.id.to_string(), + order_type, + price: msg.price, + quantity: msg.quantity, + sender, + side: msg.side, + symbol: msg.symbol.to_string(), + time_in_force: msg.timeinforce, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TradeOrder { + id: self.id.clone().into(), + ordertype: self.order_type as i64, + price: self.price, + quantity: self.quantity, + sender: self.sender.data().into(), + side: self.side, + symbol: self.symbol.clone().into(), + timeinforce: self.time_in_force, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct CancelTradeOrder { + /// Order id to cancel. + pub refid: String, + /// Originating address. + pub sender: BinanceAddress, + /// Symbol for trading pair in full name of the tokens. + pub symbol: String, +} + +impl CancelTradeOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x16, 0x6E, 0x68, 0x1B]; +} + +impl BinanceMessage for CancelTradeOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for CancelTradeOrder { + type Proto<'a> = Proto::CancelTradeOrder<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let sender = BinanceAddress::from_key_hash_with_coin(coin, msg.sender.to_vec())?; + Ok(CancelTradeOrder { + sender, + symbol: msg.symbol.to_string(), + refid: msg.refid.to_string(), + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::CancelTradeOrder { + sender: self.sender.data().into(), + symbol: self.symbol.clone().into(), + refid: self.refid.clone().into(), + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/message/tranfer_out_order.rs b/rust/chains/tw_binance/src/transaction/message/tranfer_out_order.rs new file mode 100644 index 00000000000..30dfef73f56 --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/message/tranfer_out_order.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BinanceAddress; +use crate::amino::AminoEncoder; +use crate::transaction::message::{BinanceMessage, TWBinanceProto, Token}; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_evm::address::Address as EthereumAddress; +use tw_hash::H160; +use tw_memory::Data; +use tw_proto::Binance::Proto; + +#[derive(Deserialize, Serialize)] +pub struct TransferOutOrder { + pub amount: Token, + pub expire_time: i64, + pub from: BinanceAddress, + pub to: EthereumAddress, +} + +impl TransferOutOrder { + /// cbindgen:ignore + pub const PREFIX: [u8; 4] = [0x80, 0x08, 0x19, 0xC0]; +} + +impl BinanceMessage for TransferOutOrder { + fn to_amino_protobuf(&self) -> SigningResult { + Ok(AminoEncoder::new(&Self::PREFIX) + .extend_with_msg(&self.to_tw_proto())? + .encode()) + } +} + +impl TWBinanceProto for TransferOutOrder { + type Proto<'a> = Proto::TransferOut<'a>; + + fn from_tw_proto(coin: &dyn CoinContext, msg: &Self::Proto<'_>) -> SigningResult { + let from = BinanceAddress::from_key_hash_with_coin(coin, msg.from.to_vec())?; + + let to_bytes = + H160::try_from(msg.to.as_ref()).tw_err(|_| SigningErrorType::Error_invalid_address)?; + let to = EthereumAddress::from_bytes(to_bytes); + + let amount_proto = msg + .amount + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + + Ok(TransferOutOrder { + from, + to, + amount: Token::from_tw_proto(amount_proto), + expire_time: msg.expire_time, + }) + } + + fn to_tw_proto(&self) -> Self::Proto<'static> { + Proto::TransferOut { + from: self.from.data().into(), + to: self.to.data().into(), + amount: Some(self.amount.to_tw_proto()), + expire_time: self.expire_time, + } + } +} diff --git a/rust/chains/tw_binance/src/transaction/mod.rs b/rust/chains/tw_binance/src/transaction/mod.rs new file mode 100644 index 00000000000..fbd24f33ebe --- /dev/null +++ b/rust/chains/tw_binance/src/transaction/mod.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::signature::BinanceSignature; +use crate::transaction::message::BinanceMessageEnum; +use serde::{Deserialize, Serialize}; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_memory::Data; +use tw_misc::serde::as_string; + +pub mod message; + +#[derive(Deserialize, Serialize)] +pub struct UnsignedTransaction { + #[serde(with = "as_string")] + pub account_number: i64, + pub chain_id: String, + pub data: Option, + pub memo: String, + pub msgs: Vec, + #[serde(with = "as_string")] + pub sequence: i64, + #[serde(with = "as_string")] + pub source: i64, +} + +impl UnsignedTransaction { + pub fn into_signed(self, signer: SignerInfo) -> SignedTransaction { + SignedTransaction { + unsigned: self, + signer, + } + } +} + +pub struct SignerInfo { + pub public_key: Secp256PublicKey, + pub signature: BinanceSignature, +} + +pub struct SignedTransaction { + pub unsigned: UnsignedTransaction, + pub signer: SignerInfo, +} diff --git a/rust/chains/tw_bitcoin/Cargo.toml b/rust/chains/tw_bitcoin/Cargo.toml new file mode 100644 index 00000000000..1d5571814b0 --- /dev/null +++ b/rust/chains/tw_bitcoin/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tw_bitcoin" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoin = { version = "0.30.0", features = ["rand-std"] } +secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tw_bech32_address = { path = "../../tw_bech32_address" } +tw_base58_address = { path = "../../tw_base58_address" } +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } +tw_utxo = { path = "../../frameworks/tw_utxo" } diff --git a/rust/chains/tw_bitcoin/src/entry.rs b/rust/chains/tw_bitcoin/src/entry.rs new file mode 100644 index 00000000000..5d6b81508ec --- /dev/null +++ b/rust/chains/tw_bitcoin/src/entry.rs @@ -0,0 +1,99 @@ +use crate::modules::compiler::BitcoinCompiler; +use crate::modules::planner::BitcoinPlanner; +use crate::modules::signer::BitcoinSigner; +use crate::modules::transaction_util::BitcoinTransactionUtil; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_keypair::tw::PublicKey; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::address::standard_bitcoin::{StandardBitcoinAddress, StandardBitcoinPrefix}; + +pub struct BitcoinEntry; + +impl CoinEntry for BitcoinEntry { + type AddressPrefix = StandardBitcoinPrefix; + type Address = StandardBitcoinAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = Proto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = BitcoinPlanner; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = BitcoinTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + StandardBitcoinAddress::from_str_with_coin_and_prefix(coin, address, prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + StandardBitcoinAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: PublicKey, + derivation: Derivation, + prefix: Option, + ) -> AddressResult { + StandardBitcoinAddress::derive_as_tw(coin, &public_key, derivation, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, proto: Self::SigningInput<'_>) -> Self::SigningOutput { + BitcoinSigner::sign(coin, &proto) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + ) -> Self::PreSigningOutput { + BitcoinCompiler::preimage_hashes(coin, proto) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + proto: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + BitcoinCompiler::compile(coin, proto, signatures, public_keys) + } + + #[inline] + fn plan_builder(&self) -> Option { + Some(BitcoinPlanner) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(BitcoinTransactionUtil) + } +} diff --git a/rust/chains/tw_bitcoin/src/lib.rs b/rust/chains/tw_bitcoin/src/lib.rs new file mode 100644 index 00000000000..1b6998ea2c6 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/lib.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod entry; +pub mod modules; diff --git a/rust/chains/tw_bitcoin/src/modules/compiler.rs b/rust/chains/tw_bitcoin/src/modules/compiler.rs new file mode 100644 index 00000000000..398940ec3e7 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/compiler.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::protobuf_builder::ProtobufBuilder; +use crate::modules::psbt_request::PsbtRequest; +use crate::modules::signing_request::SigningRequestBuilder; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::BitcoinV2::Proto; +use tw_proto::BitcoinV2::Proto::mod_PreSigningOutput::{ + SigningMethod as ProtoSigningMethod, TaprootTweak as ProtoTaprootTweak, +}; +use tw_proto::BitcoinV2::Proto::mod_SigningInput::OneOftransaction as TransactionType; +use tw_utxo::modules::sighash_computer::{SighashComputer, TaprootTweak, TxPreimage}; +use tw_utxo::modules::sighash_verifier::SighashVerifier; +use tw_utxo::modules::tx_compiler::TxCompiler; +use tw_utxo::modules::tx_planner::TxPlanner; +use tw_utxo::modules::utxo_selector::SelectResult; +use tw_utxo::signing_mode::SigningMethod; +use tw_utxo::transaction::transaction_interface::TransactionInterface; + +pub struct BitcoinCompiler; + +impl BitcoinCompiler { + /// Please note that [`Proto::SigningInput::public_key`] must be set. + /// If the public key should be derived from a private key, please do it before this method is called. + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let unsigned_tx = match input.transaction { + TransactionType::builder(ref tx_builder) => { + let request = SigningRequestBuilder::build(coin, &input, tx_builder)?; + TxPlanner::plan(request)?.unsigned_tx + }, + TransactionType::psbt(ref psbt) => PsbtRequest::build(&input, psbt)?.unsigned_tx, + TransactionType::None => { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Either `TransactionBuilder` or `Psbt` should be set") + }, + }; + + let TxPreimage { sighashes } = SighashComputer::preimage_tx(&unsigned_tx)?; + + let sighashes: Vec<_> = sighashes + .into_iter() + .map(|sighash| Proto::mod_PreSigningOutput::Sighash { + public_key: Cow::from(sighash.signer_pubkey), + sighash: Cow::from(sighash.sighash.to_vec()), + signing_method: signing_method(sighash.signing_method), + tweak: taproot_tweak(sighash.taproot_tweak), + }) + .collect(); + + Ok(Proto::PreSigningOutput { + sighashes, + ..Proto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + match input.transaction { + TransactionType::builder(ref tx) => { + Self::compile_with_tx_builder(coin, &input, tx, signatures) + }, + TransactionType::psbt(ref psbt) => Self::compile_psbt(coin, &input, psbt, signatures), + TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No transaction type specified"), + } + } + + fn compile_with_tx_builder( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + tx_builder_input: &Proto::TransactionBuilder, + signatures: Vec, + ) -> SigningResult> { + let request = SigningRequestBuilder::build(coin, input, tx_builder_input)?; + let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?; + + SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?; + let signed_tx = TxCompiler::compile(unsigned_tx, &signatures)?; + let tx_proto = ProtobufBuilder::tx_to_proto(&signed_tx); + + Ok(Proto::SigningOutput { + transaction: Some(tx_proto), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + weight: signed_tx.weight() as u64, + // `fee` should haven't been changed since it's a difference between `sum(inputs)` and `sum(outputs)`. + fee: plan.fee_estimate, + ..Proto::SigningOutput::default() + }) + } + + fn compile_psbt( + _coin: &dyn CoinContext, + input: &Proto::SigningInput, + psbt: &Proto::Psbt, + signatures: Vec, + ) -> SigningResult> { + let PsbtRequest { unsigned_tx, .. } = PsbtRequest::build(input, psbt)?; + let fee = unsigned_tx.fee()?; + + SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?; + let signed_tx = TxCompiler::compile(unsigned_tx, &signatures)?; + let tx_proto = ProtobufBuilder::tx_to_proto(&signed_tx); + + Ok(Proto::SigningOutput { + transaction: Some(tx_proto), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + weight: signed_tx.weight() as u64, + fee, + ..Proto::SigningOutput::default() + }) + } +} + +pub fn signing_method(s: SigningMethod) -> ProtoSigningMethod { + match s { + SigningMethod::Legacy => ProtoSigningMethod::Legacy, + SigningMethod::Segwit => ProtoSigningMethod::Segwit, + SigningMethod::Taproot => ProtoSigningMethod::Taproot, + } +} + +pub fn taproot_tweak(tweak: Option) -> Option> { + tweak.map(|tweak| { + let merkle_root = match tweak.merkle_root { + Some(root) => root.to_vec(), + None => Vec::default(), + }; + ProtoTaprootTweak { + merkle_root: Cow::from(merkle_root), + } + }) +} diff --git a/rust/chains/tw_bitcoin/src/modules/mod.rs b/rust/chains/tw_bitcoin/src/modules/mod.rs new file mode 100644 index 00000000000..fc470aafecb --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod compiler; +pub mod planner; +pub mod protobuf_builder; +pub mod psbt; +pub mod psbt_request; +pub mod signer; +pub mod signing_request; +pub mod transaction_util; +pub mod tx_builder; diff --git a/rust/chains/tw_bitcoin/src/modules/planner/mod.rs b/rust/chains/tw_bitcoin/src/modules/planner/mod.rs new file mode 100644 index 00000000000..21d437f8149 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/planner/mod.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::signing_request::SigningRequestBuilder; +use crate::modules::tx_builder::utxo_protobuf::parse_out_point; +use std::borrow::Cow; +use std::collections::HashMap; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::plan_builder::PlanBuilder; +use tw_coin_entry::signing_output_error; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::modules::tx_planner::TxPlanner; +use tw_utxo::modules::utxo_selector::SelectResult; + +pub mod psbt_planner; + +pub struct BitcoinPlanner; + +impl BitcoinPlanner { + pub fn plan_impl<'a>( + coin: &dyn CoinContext, + input: &Proto::SigningInput<'a>, + ) -> SigningResult> { + use Proto::mod_SigningInput::OneOftransaction as TransactionType; + + match input.transaction { + TransactionType::builder(ref tx) => Self::plan_with_tx_builder(coin, input, tx), + TransactionType::psbt(ref psbt) => { + psbt_planner::PsbtPlanner::plan_psbt(coin, input, psbt) + }, + TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("Either `TransactionBuilder` or `Psbt` should be set"), + } + } + + pub fn plan_with_tx_builder<'a>( + coin: &dyn CoinContext, + input: &Proto::SigningInput<'a>, + tx_builder: &Proto::TransactionBuilder<'a>, + ) -> SigningResult> { + let request = SigningRequestBuilder::build(coin, input, tx_builder)?; + let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?; + + // Prepare a map of source Inputs Proto `{ OutPoint -> Input }`. + // It will be used to find a Input Proto by its `OutPoint`. + let mut inputs_map = HashMap::with_capacity(tx_builder.inputs.len()); + for utxo in tx_builder.inputs.iter() { + let key = parse_out_point(&utxo.out_point)?; + if inputs_map.insert(key, utxo).is_some() { + // Found a duplicate UTXO. Return an error. + return SigningError::err(SigningErrorType::Error_invalid_utxo) + .context("Provided duplicate UTXOs with the same OutPoint"); + } + } + + // Fill out the selected Inputs Proto. + let mut selected_inputs_proto = Vec::with_capacity(unsigned_tx.inputs().len()); + for selected_utxo in unsigned_tx.inputs() { + let utxo_proto = inputs_map + .get(&selected_utxo.previous_output) + .or_tw_err(SigningErrorType::Error_internal) + .context("Planned transaction contains an unknown UTXO")?; + selected_inputs_proto.push((*utxo_proto).clone()); + } + + // Fill out the Output Proto. + let mut outputs_proto = Vec::with_capacity(unsigned_tx.transaction().outputs.len()); + for selected_output in unsigned_tx.transaction().outputs.iter() { + // For now, just provide a scriptPubkey as is. + // Later it's probably worth to return the same output builders as in `SigningInput`. + let to_recipient = Proto::mod_Output::OneOfto_recipient::custom_script_pubkey( + Cow::from(selected_output.script_pubkey.to_vec()), + ); + + outputs_proto.push(Proto::Output { + value: selected_output.value, + to_recipient, + }) + } + + Ok(Proto::TransactionPlan { + inputs: selected_inputs_proto, + outputs: outputs_proto, + available_amount: plan.total_spend, + send_amount: plan.total_send, + vsize_estimate: plan.vsize_estimate as u64, + fee_estimate: plan.fee_estimate, + change: plan.change, + ..Proto::TransactionPlan::default() + }) + } +} + +impl PlanBuilder for BitcoinPlanner { + type SigningInput<'a> = Proto::SigningInput<'a>; + type Plan<'a> = Proto::TransactionPlan<'a>; + + fn plan<'a>(&self, coin: &dyn CoinContext, input: &Self::SigningInput<'a>) -> Self::Plan<'a> { + Self::plan_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::TransactionPlan, e)) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs b/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs new file mode 100644 index 00000000000..0967e85b75a --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::psbt_request::PsbtRequest; +use crate::modules::signing_request::SigningRequestBuilder; +use crate::modules::tx_builder::script_parser::StandardScriptParser; +use crate::modules::tx_builder::BitcoinChainInfo; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_proto::BitcoinV2::Proto; +use tw_proto::BitcoinV2::Proto::mod_Input::OneOfclaiming_script as ClaimingScriptProto; +use tw_proto::BitcoinV2::Proto::mod_Output::OneOfto_recipient as ToRecipientProto; +use tw_utxo::transaction::standard_transaction::{TransactionInput, TransactionOutput}; +use tw_utxo::transaction::transaction_interface::TransactionInterface; +use tw_utxo::transaction::UtxoToSign; + +pub struct PsbtPlanner; + +impl PsbtPlanner { + pub fn plan_psbt( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + psbt_input: &Proto::Psbt, + ) -> SigningResult> { + let chain_info = SigningRequestBuilder::chain_info(coin, &input.chain_info)?; + let PsbtRequest { unsigned_tx, .. } = PsbtRequest::build(input, psbt_input)?; + + let total_input = unsigned_tx.total_input()?; + let fee_estimate = unsigned_tx.fee()?; + + let vsize_estimate = unsigned_tx.estimate_transaction().vsize() as u64; + + let inputs: Vec<_> = unsigned_tx + .input_args() + .iter() + .zip(unsigned_tx.inputs()) + .map(|(unsigned_txin, txin)| Self::utxo_to_proto(unsigned_txin, txin, &chain_info)) + .collect::>()?; + + let outputs: Vec<_> = unsigned_tx + .outputs() + .iter() + .map(|txout| Self::output_to_proto(txout, &chain_info)) + .collect::>()?; + + Ok(Proto::TransactionPlan { + inputs, + outputs, + available_amount: total_input, + send_amount: total_input, + vsize_estimate, + fee_estimate, + change: 0, + ..Proto::TransactionPlan::default() + }) + } + + pub fn utxo_to_proto( + unsigned_txin: &UtxoToSign, + txin: &TransactionInput, + chain_info: &BitcoinChainInfo, + ) -> SigningResult> { + let out_point = Proto::OutPoint { + hash: txin.previous_output.hash.to_vec().into(), + vout: txin.previous_output.index, + }; + let sequence = Proto::mod_Input::Sequence { + sequence: txin.sequence, + }; + + let from_address = StandardScriptParser + .parse(&unsigned_txin.prevout_script_pubkey)? + .try_to_address(chain_info)? + .or_tw_err(SigningErrorType::Error_invalid_utxo) + .context("Unexpected UTXO scriptPubkey")? + .to_string(); + + Ok(Proto::Input { + out_point: Some(out_point), + value: unsigned_txin.amount, + sighash_type: unsigned_txin.sighash_ty.raw_sighash(), + sequence: Some(sequence), + claiming_script: ClaimingScriptProto::receiver_address(from_address.into()), + }) + } + + pub fn output_to_proto( + output: &TransactionOutput, + chain_info: &BitcoinChainInfo, + ) -> SigningResult> { + let to_recipient = match StandardScriptParser + .parse(&output.script_pubkey)? + .try_to_address(chain_info)? + { + Some(to_addr) => ToRecipientProto::to_address(to_addr.to_string().into()), + // Cannot convert the output scriptPubkey into an address. Return it as is. + None => ToRecipientProto::custom_script_pubkey(output.script_pubkey.to_vec().into()), + }; + + Ok(Proto::Output { + value: output.value, + to_recipient, + }) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/protobuf_builder/mod.rs b/rust/chains/tw_bitcoin/src/modules/protobuf_builder/mod.rs new file mode 100644 index 00000000000..dc25bc975e5 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/protobuf_builder/mod.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::script::{Script, Witness}; +use tw_utxo::transaction::standard_transaction::{ + Transaction, TransactionInput, TransactionOutput, +}; +use tw_utxo::transaction::transaction_interface::TransactionInterface; + +pub struct ProtobufBuilder; + +impl ProtobufBuilder { + pub fn tx_to_proto(tx: &Transaction) -> Proto::Transaction<'static> { + let inputs = tx.inputs().iter().map(Self::tx_input_to_proto).collect(); + let outputs = tx.outputs().iter().map(Self::tx_output_to_proto).collect(); + + Proto::Transaction { + version: tx.version(), + lock_time: tx.locktime, + inputs, + outputs, + } + } + + fn tx_input_to_proto( + input: &TransactionInput, + ) -> Proto::mod_Transaction::TransactionInput<'static> { + Proto::mod_Transaction::TransactionInput { + out_point: Some(Proto::OutPoint { + hash: Cow::from(input.previous_output.hash.to_vec()), + vout: input.previous_output.index, + }), + sequence: input.sequence, + script_sig: Self::script_data(&input.script_sig), + witness_items: Self::witness_to_proto(&input.witness), + } + } + + fn tx_output_to_proto( + output: &TransactionOutput, + ) -> Proto::mod_Transaction::TransactionOutput<'static> { + Proto::mod_Transaction::TransactionOutput { + script_pubkey: Self::script_data(&output.script_pubkey), + value: output.value, + } + } + + fn witness_to_proto(witness: &Witness) -> Vec> { + witness.as_items().iter().map(Self::script_data).collect() + } + + fn script_data(script: &Script) -> Cow<'static, [u8]> { + Cow::from(script.to_vec()) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt.rs new file mode 100644 index 00000000000..58bb5d54a58 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use bitcoin::psbt::Psbt; +use tw_utxo::transaction::standard_transaction::Transaction; + +/// Finalizes the [Partially Signed Bitcoin Transaction](Psbt) +/// by updating the final `script_sig` and/or `witness`. +pub fn update_psbt_signed(psbt: &mut Psbt, signed_tx: &Transaction) { + for (signed_txin, utxo_psbt) in signed_tx.inputs.iter().zip(psbt.inputs.iter_mut()) { + if !signed_txin.script_sig.is_empty() { + utxo_psbt.final_script_sig = Some(bitcoin::ScriptBuf::from_bytes( + signed_txin.script_sig.to_vec(), + )); + } + + if !signed_txin.witness.is_empty() { + let mut final_witness = bitcoin::Witness::new(); + for witness_item in signed_txin.witness.as_items() { + final_witness.push(bitcoin::ScriptBuf::from_bytes(witness_item.to_vec())); + } + utxo_psbt.final_script_witness = Some(final_witness); + } + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs new file mode 100644 index 00000000000..f3d63e4c6ca --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::psbt_request::output_psbt::OutputPsbt; +use crate::modules::psbt_request::utxo_psbt::UtxoPsbt; +use crate::modules::signing_request::SigningRequestBuilder; +use bitcoin::psbt::Psbt; +use tw_coin_entry::error::prelude::*; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::transaction::standard_transaction::builder::TransactionBuilder; +use tw_utxo::transaction::standard_transaction::Transaction; +use tw_utxo::transaction::unsigned_transaction::UnsignedTransaction; + +pub mod output_psbt; +pub mod utxo_psbt; + +pub struct PsbtRequest { + pub psbt: Psbt, + pub unsigned_tx: UnsignedTransaction, +} + +impl PsbtRequest { + pub fn build(input: &Proto::SigningInput, psbt_input: &Proto::Psbt) -> SigningResult { + let psbt = Psbt::deserialize(&psbt_input.psbt) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error deserializing PSBT")?; + + let version = psbt + .unsigned_tx + .version + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid PSBT transaction version")?; + let lock_time = psbt.unsigned_tx.lock_time.to_consensus_u32(); + + let public_keys = SigningRequestBuilder::get_public_keys(input)?; + + let mut builder = TransactionBuilder::default(); + builder.version(version).lock_time(lock_time); + + // Add all UTXOs to the unsigned transaction builder. + for (txin, txin_psbt) in psbt.unsigned_tx.input.iter().zip(psbt.inputs.iter()) { + let utxo_builder = UtxoPsbt::new(txin, txin_psbt, &public_keys); + + let (utxo, utxo_args) = utxo_builder + .build() + .context("Error creating UTXO from PSBT")?; + builder.push_input(utxo, utxo_args); + } + + // Add all outputs to the unsigned transaction builder. + for txout in psbt.unsigned_tx.output.iter() { + let output = OutputPsbt::new(txout) + .build() + .context("Error creating Output from PSBT")?; + builder.push_output(output); + } + + let unsigned_tx = builder.build()?; + Ok(PsbtRequest { psbt, unsigned_tx }) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs new file mode 100644 index 00000000000..c691744230d --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::*; +use tw_utxo::script::Script; +use tw_utxo::transaction::standard_transaction::TransactionOutput; + +/// Currently, we rely on `bitcoin` crate to build our own [`TransactionOutput`]. +pub struct OutputPsbt<'a> { + output: &'a bitcoin::TxOut, +} + +impl<'a> OutputPsbt<'a> { + pub fn new(output: &'a bitcoin::TxOut) -> Self { + OutputPsbt { output } + } + + pub fn build(self) -> SigningResult { + let value = self + .output + .value + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_utxo_amount) + .context("PSBT Output amount is too large")?; + let script_pubkey = Script::from(self.output.script_pubkey.to_bytes()); + Ok(TransactionOutput { + value, + script_pubkey, + }) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs new file mode 100644 index 00000000000..58d144de377 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::public_keys::PublicKeys; +use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser}; +use secp256k1::ThirtyTwoByteHash; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_utxo::script::Script; +use tw_utxo::sighash::SighashType; +use tw_utxo::transaction::standard_transaction::builder::UtxoBuilder; +use tw_utxo::transaction::standard_transaction::TransactionInput; +use tw_utxo::transaction::UtxoToSign; + +/// Currently, we rely on `bitcoin` crate to build our own [`UtxoToSign`]. +pub struct UtxoPsbt<'a> { + utxo: &'a bitcoin::TxIn, + utxo_psbt: &'a bitcoin::psbt::Input, + public_keys: &'a PublicKeys, +} + +impl<'a> UtxoPsbt<'a> { + pub fn new( + utxo: &'a bitcoin::TxIn, + utxo_psbt: &'a bitcoin::psbt::Input, + public_keys: &'a PublicKeys, + ) -> Self { + UtxoPsbt { + utxo, + utxo_psbt, + public_keys, + } + } + + pub fn build(self) -> SigningResult<(TransactionInput, UtxoToSign)> { + if let Some(ref non_witness_utxo) = self.utxo_psbt.non_witness_utxo { + self.build_non_witness_utxo(non_witness_utxo) + } else if let Some(ref witness_utxo) = self.utxo_psbt.witness_utxo { + self.build_witness_utxo(witness_utxo) + } else { + SigningError::err(SigningErrorType::Error_invalid_params) + .context("Neither 'witness_utxo' nor 'non_witness_utxo' are set in the PSBT") + } + } + + pub fn build_non_witness_utxo( + &self, + non_witness_utxo: &bitcoin::Transaction, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let prev_out_idx = self.utxo.previous_output.vout as usize; + let prev_out = non_witness_utxo + .output + .get(prev_out_idx) + .or_tw_err(SigningErrorType::Error_invalid_utxo) + .with_context(|| { + format!("'Psbt::non_witness_utxo' does not contain '{prev_out_idx}' output") + })?; + + let script = Script::from(prev_out.script_pubkey.to_bytes()); + let builder = self.prepare_builder(prev_out.value)?; + + self.build_utxo_with_script(builder, &script) + } + + pub fn build_witness_utxo( + &self, + witness_utxo: &bitcoin::TxOut, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let script = Script::from(witness_utxo.script_pubkey.to_bytes()); + let builder = self.prepare_builder(witness_utxo.value)?; + self.build_utxo_with_script(builder, &script) + } + + fn build_utxo_with_script( + &self, + builder: UtxoBuilder, + script: &Script, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + match StandardScriptParser.parse(script)? { + StandardScript::P2PK(pubkey) => builder.p2pk(&pubkey), + StandardScript::P2PKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2pkh(&pubkey) + }, + StandardScript::P2WPKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2wpkh(&pubkey) + }, + StandardScript::P2TR(tweaked_pubkey) => { + if self.has_tap_scripts() { + return SigningError::err(SigningErrorType::Error_not_supported) + .context("P2TR script path is not supported for PSBT at the moment"); + } + builder.p2tr_key_path_with_tweaked_pubkey(&tweaked_pubkey) + }, + StandardScript::P2SH(_) | StandardScript::P2WSH(_) => { + SigningError::err(SigningErrorType::Error_not_supported) + .context("P2SH and P2WSH scriptPubkey's are not supported yet") + }, + StandardScript::OpReturn(_) => SigningError::err(SigningErrorType::Error_invalid_utxo) + .context("Cannot spend an OP_RETURN output"), + } + } + + fn prepare_builder(&self, amount: u64) -> SigningResult { + let prevout_hash = H256::from(self.utxo.previous_output.txid.to_raw_hash().into_32()); + let prevout_index = self.utxo.previous_output.vout; + let sequence = self.utxo.sequence.0; + + let sighash_ty = match self.utxo_psbt.sighash_type { + Some(psbt_ty) => SighashType::from_u32(psbt_ty.to_u32())?, + None => SighashType::default(), + }; + + let amount = amount + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_utxo_amount) + .context("PSBT UTXO amount is too large")?; + + Ok(UtxoBuilder::default() + .prev_txid(prevout_hash) + .prev_index(prevout_index) + .sequence(sequence) + .sighash_type(sighash_ty) + .amount(amount)) + } + + fn has_tap_scripts(&self) -> bool { + !self.utxo_psbt.tap_scripts.is_empty() + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/signer.rs b/rust/chains/tw_bitcoin/src/modules/signer.rs new file mode 100644 index 00000000000..1bed884f053 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/signer.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::protobuf_builder::ProtobufBuilder; +use crate::modules::psbt::update_psbt_signed; +use crate::modules::psbt_request::PsbtRequest; +use crate::modules::signing_request::SigningRequestBuilder; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::{ecdsa, schnorr}; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::modules::keys_manager::KeysManager; +use tw_utxo::modules::tx_planner::TxPlanner; +use tw_utxo::modules::tx_signer::TxSigner; +use tw_utxo::modules::utxo_selector::SelectResult; +use tw_utxo::signing_mode::SigningMethod; +use tw_utxo::transaction::standard_transaction::Transaction; +use tw_utxo::transaction::transaction_interface::TransactionInterface; +use tw_utxo::transaction::unsigned_transaction::UnsignedTransaction; + +pub struct BitcoinSigner; + +impl BitcoinSigner { + pub fn sign( + coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + pub fn sign_impl( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + ) -> SigningResult> { + use Proto::mod_SigningInput::OneOftransaction as TransactionType; + + match input.transaction { + TransactionType::builder(ref tx) => Self::sign_with_tx_builder(coin, input, tx), + TransactionType::psbt(ref psbt) => Self::sign_psbt(coin, input, psbt), + TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("Either `TransactionBuilder` or `Psbt` should be set"), + } + } + + pub fn sign_with_tx_builder( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + tx_builder_input: &Proto::TransactionBuilder, + ) -> SigningResult> { + let request = SigningRequestBuilder::build(coin, input, tx_builder_input)?; + let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?; + + let keys_manager = Self::keys_manager_for_tx( + &input.private_keys, + &unsigned_tx, + input.dangerous_use_fixed_schnorr_rng, + )?; + + let signed_tx = + TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; + + Ok(Proto::SigningOutput { + transaction: Some(ProtobufBuilder::tx_to_proto(&signed_tx)), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + // `fee` should haven't been changed since it's a difference between `sum(inputs)` and `sum(outputs)`. + fee: plan.fee_estimate, + weight: signed_tx.weight() as u64, + ..Proto::SigningOutput::default() + }) + } + + pub fn sign_psbt( + _coin: &dyn CoinContext, + input: &Proto::SigningInput, + psbt_input: &Proto::Psbt, + ) -> SigningResult> { + let PsbtRequest { + mut psbt, + unsigned_tx, + } = PsbtRequest::build(input, psbt_input)?; + + let fee = unsigned_tx.fee()?; + + let keys_manager = Self::keys_manager_for_tx( + &input.private_keys, + &unsigned_tx, + input.dangerous_use_fixed_schnorr_rng, + )?; + + let signed_tx = + TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; + + update_psbt_signed(&mut psbt, &signed_tx); + + Ok(Proto::SigningOutput { + transaction: Some(ProtobufBuilder::tx_to_proto(&signed_tx)), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + fee, + weight: signed_tx.weight() as u64, + psbt: Some(Proto::Psbt { + psbt: Cow::from(psbt.serialize()), + }), + ..Proto::SigningOutput::default() + }) + } + + fn keys_manager_for_tx

( + private_keys: &[P], + unsigned_tx: &UnsignedTransaction, + dangerous_use_fixed_schnorr_rng: bool, + ) -> SigningResult + where + P: AsRef<[u8]>, + { + let has_taproot = unsigned_tx + .input_args() + .iter() + .any(|utxo_args| utxo_args.signing_method == SigningMethod::Taproot); + + let mut keys_manager = KeysManager::default(); + + // Parse private keys and put them to the keys manager. + for private in private_keys.iter() { + let ecdsa_private = ecdsa::secp256k1::PrivateKey::try_from(private.as_ref()) + .into_tw() + .context("Invalid ecdsa secp256k1 private key")?; + keys_manager.add_ecdsa_private(ecdsa_private); + + if has_taproot { + let schnorr_private = schnorr::PrivateKey::try_from(private.as_ref()) + .into_tw() + .context("Invalid schnorr private key")?; + + if dangerous_use_fixed_schnorr_rng { + keys_manager.add_schnorr_private(schnorr_private.no_aux_rand()); + } else { + keys_manager.add_schnorr_private(schnorr_private); + } + } + } + + Ok(keys_manager) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs b/rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs new file mode 100644 index 00000000000..bddbcac4b0c --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::output_protobuf::OutputProtobuf; +use crate::modules::tx_builder::public_keys::PublicKeys; +use crate::modules::tx_builder::utxo_protobuf::UtxoProtobuf; +use crate::modules::tx_builder::BitcoinChainInfo; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_misc::traits::OptionalEmpty; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::dust::DustPolicy; +use tw_utxo::modules::tx_planner::{PlanRequest, RequestType}; +use tw_utxo::modules::utxo_selector::InputSelector; +use tw_utxo::transaction::standard_transaction::builder::TransactionBuilder; +use tw_utxo::transaction::standard_transaction::Transaction; +use Proto::mod_TransactionBuilder::OneOfdust_policy as ProtoDustPolicy; + +const DEFAULT_TX_VERSION: u32 = 1; + +pub type StandardSigningRequest = PlanRequest; + +pub struct SigningRequestBuilder; + +impl SigningRequestBuilder { + pub fn build( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + transaction_builder: &Proto::TransactionBuilder, + ) -> SigningResult { + let chain_info = Self::chain_info(coin, &input.chain_info)?; + let dust_policy = Self::dust_policy(&transaction_builder.dust_policy)?; + let fee_per_vbyte = transaction_builder.fee_per_vb; + let version = Self::transaction_version(&transaction_builder.version); + + let public_keys = Self::get_public_keys(input)?; + + let mut builder = TransactionBuilder::default(); + builder + .version(version) + .lock_time(transaction_builder.lock_time); + + // Parse all UTXOs. + for utxo_proto in transaction_builder.inputs.iter() { + let utxo_builder = UtxoProtobuf::new(&chain_info, utxo_proto, &public_keys); + + let (utxo, utxo_args) = utxo_builder + .utxo_from_proto() + .context("Error creating UTXO from Protobuf")?; + builder.push_input(utxo, utxo_args); + } + + // If `max_amount_output` is set, construct a transaction with only one output. + if let Some(max_output_proto) = transaction_builder.max_amount_output.as_ref() { + let output_builder = OutputProtobuf::new(&chain_info, max_output_proto); + + let max_output = output_builder + .output_from_proto() + .context("Error creating Max Output from Protobuf")?; + builder.push_output(max_output); + + let unsigned_tx = builder.build()?; + return Ok(StandardSigningRequest { + ty: RequestType::SendMax { unsigned_tx }, + dust_policy, + fee_per_vbyte, + }); + } + + // `max_amount_output` isn't set, parse all Outputs. + for output_proto in transaction_builder.outputs.iter() { + let output = OutputProtobuf::new(&chain_info, output_proto) + .output_from_proto() + .context("Error creating Output from Proto")?; + builder.push_output(output); + } + + // Parse change output if it was provided. + let change_output = transaction_builder + .change_output + .as_ref() + .map(|change_output_proto| { + OutputProtobuf::new(&chain_info, change_output_proto) + .output_from_proto() + .context("Error creating Change Output from Proto") + }) + .transpose()?; + + let input_selector = Self::input_selector(&transaction_builder.input_selector); + + let unsigned_tx = builder.build()?; + Ok(StandardSigningRequest { + ty: RequestType::SendExact { + unsigned_tx, + change_output, + input_selector, + }, + dust_policy, + fee_per_vbyte, + }) + } + + pub fn get_public_keys(input: &Proto::SigningInput) -> SigningResult { + let mut public_keys = PublicKeys::default(); + + if input.private_keys.is_empty() { + for public in input.public_keys.iter() { + public_keys.add_public_key(public.to_vec()); + } + } else { + for private in input.private_keys.iter() { + public_keys.add_public_with_ecdsa_private(private)?; + } + } + + Ok(public_keys) + } + + fn input_selector(selector: &Proto::InputSelector) -> InputSelector { + match selector { + Proto::InputSelector::SelectAscending => InputSelector::Ascending, + Proto::InputSelector::SelectInOrder => InputSelector::InOrder, + Proto::InputSelector::SelectDescending => InputSelector::Descending, + Proto::InputSelector::UseAll => InputSelector::UseAll, + } + } + + fn dust_policy(proto: &ProtoDustPolicy) -> SigningResult { + match proto { + ProtoDustPolicy::fixed_dust_threshold(fixed) => Ok(DustPolicy::FixedAmount(*fixed)), + ProtoDustPolicy::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No dust policy provided"), + } + } + + fn transaction_version(proto: &Proto::TransactionVersion) -> u32 { + match proto { + Proto::TransactionVersion::UseDefault => DEFAULT_TX_VERSION, + Proto::TransactionVersion::V1 => 1, + Proto::TransactionVersion::V2 => 2, + } + } + + pub fn chain_info( + coin: &dyn CoinContext, + chain_info: &Option, + ) -> SigningResult { + fn prefix_to_u8(prefix: u32, prefix_name: &str) -> SigningResult { + prefix + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .with_context(|| format!("Invalid {prefix_name} prefix. It must fit uint8")) + } + + if let Some(info) = chain_info { + let hrp = info.hrp.to_string().empty_or_some(); + return Ok(BitcoinChainInfo { + p2pkh_prefix: prefix_to_u8(info.p2pkh_prefix, "p2pkh")?, + p2sh_prefix: prefix_to_u8(info.p2sh_prefix, "p2sh")?, + hrp, + }); + } + + // Try to get the chain info from the context. + // Note that not all Bitcoin forks support HRP (segwit addresses). + let hrp = coin.hrp(); + match (coin.p2pkh_prefix(), coin.p2sh_prefix()) { + (Some(p2pkh_prefix), Some(p2sh_prefix)) => Ok(BitcoinChainInfo { + p2pkh_prefix, + p2sh_prefix, + hrp, + }), + _ => SigningError::err(SigningErrorType::Error_invalid_params) + .context("Neither 'SigningInput.chain_info' nor p2pkh/p2sh prefixes specified in the registry.json") + } + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/transaction_util.rs b/rust/chains/tw_bitcoin/src/modules/transaction_util.rs new file mode 100644 index 00000000000..19862095334 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/transaction_util.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use bitcoin::consensus::deserialize; +use bitcoin::Transaction; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_encoding::hex::decode; + +pub struct BitcoinTransactionUtil; + +impl TransactionUtil for BitcoinTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +impl BitcoinTransactionUtil { + fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + let tx = decode(encoded_tx).map_err(|_| SigningErrorType::Error_input_parse)?; + + // Deserialize the transaction + let tx: Transaction = deserialize(&tx).map_err(|_| SigningErrorType::Error_input_parse)?; + + // Calculate the transaction ID + let txid = tx.txid(); + + // Note: to_string() returns the reversed byte order, which is the RPC format + Ok(txid.to_string()) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs new file mode 100644 index 00000000000..37c99c33e53 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod output_protobuf; +pub mod public_keys; +pub mod script_parser; +pub mod utxo_protobuf; + +pub struct BitcoinChainInfo { + pub p2pkh_prefix: u8, + pub p2sh_prefix: u8, + /// Note that not all Bitcoin forks support HRP (segwit addresses). + pub hrp: Option, +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs new file mode 100644 index 00000000000..0e1a6e374b0 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::BitcoinChainInfo; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_hash::hasher::sha256_ripemd; +use tw_hash::sha2::sha256; +use tw_hash::{Hash, H160, H256}; +use tw_keypair::{ecdsa, schnorr}; +use tw_memory::Data; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::address::legacy::LegacyAddress; +use tw_utxo::address::segwit::SegwitAddress; +use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; +use tw_utxo::address::taproot::TaprootAddress; +use tw_utxo::script::Script; +use tw_utxo::transaction::standard_transaction::builder::OutputBuilder; +use tw_utxo::transaction::standard_transaction::TransactionOutput; + +pub struct OutputProtobuf<'a> { + chain_info: &'a BitcoinChainInfo, + output: &'a Proto::Output<'a>, +} + +impl<'a> OutputProtobuf<'a> { + pub fn new(chain_info: &'a BitcoinChainInfo, output: &'a Proto::Output<'a>) -> Self { + OutputProtobuf { chain_info, output } + } + + pub fn output_from_proto(self) -> SigningResult { + use Proto::mod_Output::mod_OutputBuilder::OneOfvariant as BuilderType; + use Proto::mod_Output::OneOfto_recipient as RecipientType; + + match self.output.to_recipient { + RecipientType::builder(ref builder) => match builder.variant { + BuilderType::p2sh(ref redeem) => self.p2sh(redeem), + BuilderType::p2pk(ref pubkey) => self.p2pk(pubkey), + BuilderType::p2pkh(ref pubkey_or_hash) => self.p2pkh(pubkey_or_hash), + BuilderType::p2wsh(ref redeem) => self.p2wsh(redeem), + BuilderType::p2wpkh(ref pubkey_or_hash) => self.p2wpkh(pubkey_or_hash), + BuilderType::p2tr_key_path(ref key_path) => self.p2tr_key_path(key_path), + BuilderType::p2tr_script_path(ref script) => self.p2tr_script_path(script), + BuilderType::p2tr_dangerous_assume_tweaked(ref pubkey) => { + self.p2tr_dangerous_assume_tweaked(pubkey) + }, + BuilderType::brc20_inscribe(ref inscription) => self.brc20_inscribe(inscription), + BuilderType::op_return(ref data) => self.op_return(data), + BuilderType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No Output Builder type provided"), + }, + RecipientType::custom_script_pubkey(ref script) => self.custom_script(script.to_vec()), + RecipientType::to_address(ref address) => self.recipient_address(address), + RecipientType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No Output recipient type provided"), + } + } + + pub fn p2sh( + &self, + redeem: &Proto::mod_Output::RedeemScriptOrHash, + ) -> SigningResult { + let redeem_hash = + Self::redeem_hash_from_proto(redeem, sha256_ripemd).context("P2SH builder")?; + Ok(self.prepare_builder()?.p2sh_from_hash(&redeem_hash)) + } + + pub fn p2pk(&self, pubkey: &[u8]) -> SigningResult { + let pubkey = ecdsa::secp256k1::PublicKey::try_from(pubkey) + .into_tw() + .context("Invalid P2PK public key")?; + Ok(self.prepare_builder()?.p2pk(&pubkey)) + } + + pub fn p2pkh( + &self, + pubkey_or_hash: &Proto::PublicKeyOrHash, + ) -> SigningResult { + let pubkey_hash = Self::pubkey_hash_from_proto(pubkey_or_hash).context("P2PKH builder")?; + Ok(self.prepare_builder()?.p2pkh_from_hash(&pubkey_hash)) + } + + pub fn p2wsh( + &self, + redeem: &Proto::mod_Output::RedeemScriptOrHash, + ) -> SigningResult { + let redeem_hash = Self::redeem_hash_from_proto(redeem, sha256).context("P2WSH builder")?; + Ok(self.prepare_builder()?.p2wsh_from_hash(&redeem_hash)) + } + + pub fn p2wpkh( + &self, + pubkey_or_hash: &Proto::PublicKeyOrHash, + ) -> SigningResult { + let pubkey_hash = Self::pubkey_hash_from_proto(pubkey_or_hash).context("P2WPKH builder")?; + Ok(self.prepare_builder()?.p2wpkh_from_hash(&pubkey_hash)) + } + + pub fn p2tr_key_path(&self, taproot_pubkey: &[u8]) -> SigningResult { + let public_key = schnorr::PublicKey::try_from(taproot_pubkey) + .into_tw() + .context("Invalid P2TR key path. Must be a schnorr public key")?; + Ok(self.prepare_builder()?.p2tr_key_path(&public_key)) + } + + pub fn p2tr_dangerous_assume_tweaked( + &self, + tweaked_pubkey: &[u8], + ) -> SigningResult { + let tweaked_x_only = H256::try_from(tweaked_pubkey) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid P2TR tweaked public key. Expected 32 bytes x-only public key")?; + Ok(self + .prepare_builder()? + .p2tr_dangerous_assume_tweaked(&tweaked_x_only)) + } + + pub fn p2tr_script_path( + &self, + taproot_script_path: &Proto::mod_Output::OutputTaprootScriptPath, + ) -> SigningResult { + let public_key = schnorr::PublicKey::try_from(taproot_script_path.internal_key.as_ref()) + .into_tw() + .context( + "Invalid OutputTaprootScriptPath.internal_key. Must be a schnorr public key", + )?; + + let merkle_root = H256::try_from(taproot_script_path.merkle_root.as_ref()) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid OutputTaprootScriptPath.merkle_root. Must be a 32 byte array")?; + + Ok(self + .prepare_builder()? + .p2tr_script_path(&public_key, merkle_root)) + } + + pub fn brc20_inscribe( + &self, + inscription: &Proto::mod_Output::OutputBrc20Inscription, + ) -> SigningResult { + let public_key = schnorr::PublicKey::try_from(inscription.inscribe_to.as_ref())?; + self.prepare_builder()?.brc20_transfer( + &public_key, + inscription.ticker.to_string(), + inscription.transfer_amount.to_string(), + ) + } + + pub fn custom_script(&self, script_data: Data) -> SigningResult { + let script = Script::from(script_data); + Ok(self.prepare_builder()?.custom_script_pubkey(script)) + } + + pub fn recipient_address(&self, addr: &str) -> SigningResult { + let addr = StandardBitcoinAddress::from_str(addr) + .into_tw() + .context("Invalid recipient address")?; + + match addr { + StandardBitcoinAddress::Legacy(ref legacy) => self.recipient_legacy_address(legacy), + StandardBitcoinAddress::Segwit(ref segwit) => self.recipient_segwit_address(segwit), + StandardBitcoinAddress::Taproot(ref taproot) => self.recipient_taproot_address(taproot), + } + } + + pub fn op_return(&self, op_return_data: &[u8]) -> SigningResult { + self.prepare_builder()?.op_return(op_return_data) + } + + pub fn recipient_legacy_address( + &self, + addr: &LegacyAddress, + ) -> SigningResult { + let p2pkh_prefix = self.chain_info.p2pkh_prefix; + let p2sh_prefix = self.chain_info.p2sh_prefix; + + if addr.prefix() == p2pkh_prefix { + Ok(self.prepare_builder()?.p2pkh_from_hash(&addr.payload())) + } else if addr.prefix() == p2sh_prefix { + Ok(self.prepare_builder()?.p2sh_from_hash(&addr.payload())) + } else { + // Unknown + SigningError::err(SigningErrorType::Error_invalid_address).context(format!( + "The given '{addr}' address has unexpected prefix. Expected p2pkh={p2pkh_prefix} p2sh={p2sh_prefix}", + )) + } + } + + pub fn recipient_segwit_address( + &self, + addr: &SegwitAddress, + ) -> SigningResult { + if let Ok(pubkey_hash) = H160::try_from(addr.witness_program()) { + Ok(self.prepare_builder()?.p2wpkh_from_hash(&pubkey_hash)) + } else if let Ok(script_hash) = H256::try_from(addr.witness_program()) { + Ok(self.prepare_builder()?.p2wsh_from_hash(&script_hash)) + } else { + return SigningError::err(SigningErrorType::Error_invalid_address) + .context(format!("The given '{addr}' Segwit address has unexpected witness program. Expected either 20 or 32 bytes")); + } + } + + pub fn recipient_taproot_address( + &self, + addr: &TaprootAddress, + ) -> SigningResult { + let pubkey_x_only = H256::try_from(addr.witness_program()) + .tw_err(|_| SigningErrorType::Error_invalid_address) + .context(format!("The given '{addr}' Taproot address has unexpected witness program. Expected 32 bytes public key"))?; + + Ok(self + .prepare_builder()? + .p2tr_dangerous_assume_tweaked(&pubkey_x_only)) + } + + /// Tries to convert [`Proto::RedeemScriptOrHash`] to [`Hash`] using a specific `hasher` function. + /// Please note `P2SH` and `P2WSH` use different hashing functions. + pub fn redeem_hash_from_proto( + input: &Proto::mod_Output::RedeemScriptOrHash, + hasher: F, + ) -> SigningResult> + where + F: FnOnce(&[u8]) -> Data, + { + use Proto::mod_Output::mod_RedeemScriptOrHash::OneOfvariant as RedeemOrHashType; + + let hash_data = match input.variant { + RedeemOrHashType::redeem_script(ref redeem) => hasher(redeem.as_ref()), + RedeemOrHashType::hash(ref hash) => hash.to_vec(), + RedeemOrHashType::None => { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Expected a redeem script or its hash") + }, + }; + Hash::::try_from(hash_data.as_slice()) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .with_context(|| format!("Expected exactly {N} bytes redeem script hash")) + } + + pub fn prepare_builder(&self) -> SigningResult { + if self.output.value < 0 { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Transaction Output amount cannot be negative"); + } + Ok(OutputBuilder::new(self.output.value)) + } + + /// Tries to convert [`Proto::PublicKeyOrHash`] to [`Hash`]. + /// Please note `P2PKH` and `P2WPKH` use the same `ripemd(sha256(x))` hash function. + pub fn pubkey_hash_from_proto( + input: &Proto::PublicKeyOrHash, + ) -> SigningResult> { + use Proto::mod_PublicKeyOrHash::OneOfvariant as PublicKeyOrHashType; + + let hash_data = match input.variant { + PublicKeyOrHashType::pubkey(ref pubkey) => sha256_ripemd(pubkey.as_ref()), + PublicKeyOrHashType::hash(ref hash) => hash.to_vec(), + PublicKeyOrHashType::None => { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Expected a public key or its hash") + }, + }; + Hash::::try_from(hash_data.as_slice()) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .with_context(|| format!("Expected exactly {N} bytes public key hash")) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs new file mode 100644 index 00000000000..6c822c686b6 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::collections::HashMap; +use tw_coin_entry::error::prelude::*; +use tw_hash::hasher::sha256_ripemd; +use tw_hash::H160; +use tw_keypair::ecdsa; +use tw_memory::Data; + +/// A transaction builder helper that allows to easily get an access to a public key by using its `ripemd(sha256)` hash. +#[derive(Default)] +pub struct PublicKeys { + /// The map of public keys by their hash, i.e `{ ripemd(sha256(public_key)) -> public_key }`. + public_key_hash_map: HashMap, +} + +impl PublicKeys { + /// Adds a public key to the keys manager. + /// Simply ignore duplicate keys. + pub fn add_public_key(&mut self, pubkey: Data) -> &mut Self { + let pubkey_hash = H160::try_from(sha256_ripemd(&pubkey).as_slice()) + .expect("sha256_ripemd must return exactly 20 bytes"); + self.public_key_hash_map.insert(pubkey_hash, pubkey); + self + } + + /// Adds a public key derived from the ecdsa secp256k1 `private`. + pub fn add_public_with_ecdsa_private(&mut self, private: &[u8]) -> SigningResult<&mut Self> { + let private = ecdsa::secp256k1::PrivateKey::try_from(private) + .into_tw() + .context("Given an invalid ecdsa secp256k1 private key")?; + Ok(self.add_public_key(private.public().compressed().to_vec())) + } + + pub fn get_public_key(&self, pubkey_hash: &H160) -> SigningResult<&[u8]> { + self.public_key_hash_map + .get(pubkey_hash) + .map(|pubkey_bytes|pubkey_bytes.as_slice()) + .or_tw_err(SigningErrorType::Error_missing_private_key) + .with_context(|| format!("Missing either a private or public key corresponding to the pubkey hash: {pubkey_hash}")) + } + + pub fn get_ecdsa_public_key( + &self, + pubkey_hash: &H160, + ) -> SigningResult { + let pubkey_data = self.get_public_key(pubkey_hash)?; + ecdsa::secp256k1::PublicKey::try_from(pubkey_data) + .into_tw() + .context("Expected a valid ecdsa secp256k1 public key") + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs new file mode 100644 index 00000000000..26d8a70e009 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::BitcoinChainInfo; +use tw_coin_entry::error::prelude::*; +use tw_hash::{H160, H256}; +use tw_keypair::{ecdsa, schnorr}; +use tw_memory::Data; +use tw_utxo::address::legacy::LegacyAddress; +use tw_utxo::address::segwit::SegwitAddress; +use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; +use tw_utxo::address::taproot::TaprootAddress; +use tw_utxo::script::standard_script::conditions; +use tw_utxo::script::Script; + +pub enum StandardScript { + /// Compressed or uncompressed public key bytes. + P2PK(ecdsa::secp256k1::PublicKey), + /// Public key hash. + P2PKH(H160), + /// Script hash. + P2SH(H160), + /// Public key hash. + P2WPKH(H160), + /// Script hash. + P2WSH(H256), + /// Tweaked public key. + /// The public key can be tweaked as either key-path or script-path, + P2TR(schnorr::XOnlyPublicKey), + /// OP_RETURN payload. + OpReturn(Data), +} + +impl StandardScript { + pub fn try_to_address( + &self, + chain_info: &BitcoinChainInfo, + ) -> AddressResult> { + let try_hrp = || chain_info.hrp.clone().ok_or(AddressError::MissingPrefix); + + match self { + StandardScript::P2PK(pubkey) => { + // Display P2PK input as P2PKH. + LegacyAddress::p2pkh_with_public_key(chain_info.p2pkh_prefix, pubkey) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2PKH(pubkey_hash) => { + LegacyAddress::new(chain_info.p2pkh_prefix, pubkey_hash.as_slice()) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2SH(script_hash) => { + LegacyAddress::new(chain_info.p2sh_prefix, script_hash.as_slice()) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2WPKH(pubkey_hash) => { + SegwitAddress::new(try_hrp()?, pubkey_hash.to_vec()) + .map(StandardBitcoinAddress::Segwit) + .map(Some) + }, + StandardScript::P2WSH(script_hash) => { + SegwitAddress::new(try_hrp()?, script_hash.to_vec()) + .map(StandardBitcoinAddress::Segwit) + .map(Some) + }, + StandardScript::P2TR(tweaked_pubkey) => { + TaprootAddress::new(try_hrp()?, tweaked_pubkey.bytes().to_vec()) + .map(StandardBitcoinAddress::Taproot) + .map(Some) + }, + StandardScript::OpReturn(_) => Ok(None), + } + } +} + +pub struct StandardScriptParser; + +impl StandardScriptParser { + /// Later, this method can be moved to a trait. + pub fn parse(&self, script: &Script) -> SigningResult { + if let Some(pubkey) = conditions::match_p2pk(script) { + // P2PK + let pubkey = ecdsa::secp256k1::PublicKey::try_from(pubkey) + .into_tw() + .context("P2PK scriptPubkey must contain a valid ecdsa secp256k1 public key")?; + Ok(StandardScript::P2PK(pubkey)) + } else if let Some(pubkey_hash) = conditions::match_p2pkh(script) { + // P2PKH + Ok(StandardScript::P2PKH(pubkey_hash)) + } else if let Some(script_hash) = conditions::match_p2sh(script) { + // P2SH + Ok(StandardScript::P2SH(script_hash)) + } else if let Some(pubkey_hash) = conditions::match_p2wpkh(script) { + // P2WPKH + Ok(StandardScript::P2WPKH(pubkey_hash)) + } else if let Some(script_hash) = conditions::match_p2wsh(script) { + // P2WSH + Ok(StandardScript::P2WSH(script_hash)) + } else if let Some(tweaked_pubkey) = conditions::match_p2tr(script) { + // P2TR + let tweaked_pubkey_x_only = + schnorr::XOnlyPublicKey::try_from(tweaked_pubkey.as_slice()) + .into_tw() + .context("P2TR scriptPubkey must contain a valid tweaked schnorr public key")?; + Ok(StandardScript::P2TR(tweaked_pubkey_x_only)) + } else if let Some(payload) = conditions::match_op_return(script) { + // OP_RETURN + Ok(StandardScript::OpReturn(payload)) + } else { + // Unknown + SigningError::err(SigningErrorType::Error_script_output).context( + "The given custom scriptPubkey is not supported. Consider using a proper Input/Output builder", + ) + } + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs new file mode 100644 index 00000000000..d536915a3c8 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::public_keys::PublicKeys; +use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser}; +use crate::modules::tx_builder::BitcoinChainInfo; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_hash::{H160, H256}; +use tw_keypair::{ecdsa, schnorr}; +use tw_memory::Data; +use tw_misc::traits::ToBytesVec; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::address::legacy::LegacyAddress; +use tw_utxo::address::segwit::SegwitAddress; +use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; +use tw_utxo::address::taproot::TaprootAddress; +use tw_utxo::script::Script; +use tw_utxo::sighash::SighashType; +use tw_utxo::transaction::standard_transaction::builder::UtxoBuilder; +use tw_utxo::transaction::standard_transaction::TransactionInput; +use tw_utxo::transaction::transaction_parts::OutPoint; +use tw_utxo::transaction::UtxoToSign; + +pub struct UtxoProtobuf<'a> { + chain_info: &'a BitcoinChainInfo, + input: &'a Proto::Input<'a>, + public_keys: &'a PublicKeys, +} + +impl<'a> UtxoProtobuf<'a> { + pub fn new( + chain_info: &'a BitcoinChainInfo, + input: &'a Proto::Input<'a>, + public_keys: &'a PublicKeys, + ) -> Self { + UtxoProtobuf { + chain_info, + input, + public_keys, + } + } + + pub fn utxo_from_proto(self) -> SigningResult<(TransactionInput, UtxoToSign)> { + use Proto::mod_Input::mod_InputBuilder::OneOfvariant as BuilderType; + use Proto::mod_Input::OneOfclaiming_script as ScriptType; + + match self.input.claiming_script { + ScriptType::script_builder(ref builder) => match builder.variant { + // BuilderType::p2sh(ref redeem_script) => self.p2sh(redeem_script.to_vec()), + BuilderType::p2pk(ref pubkey) => self.p2pk(pubkey), + BuilderType::p2pkh(ref pubkey_or_hash) => self.p2pkh(pubkey_or_hash), + // BuilderType::p2wsh(ref redeem_script) => self.p2wsh(redeem_script.to_vec()), + BuilderType::p2wpkh(ref pubkey_or_hash) => self.p2wpkh(pubkey_or_hash), + BuilderType::p2tr_key_path(ref key_path) => self.p2tr_key_path(key_path), + // BuilderType::p2tr_script_path(ref script) => self.p2tr_script_path(script), + BuilderType::brc20_inscribe(ref inscription) => self.brc20_inscribe(inscription), + BuilderType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No Input Builder type provided"), + }, + ScriptType::script_data(ref script) => self.custom_script(script.to_vec()), + ScriptType::receiver_address(ref address) => self.recipient_address(address), + ScriptType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No Input claiming script provided"), + } + } + + // TODO next iteration + // pub fn p2sh(&self, redeem_script: Data) -> SigningResult<(TransactionInput, UtxoToSign)> { + // let redeem_script = Script::from(redeem_script); + // self.prepare_builder()?.p2sh(redeem_script) + // } + + pub fn p2pk(&self, pubkey: &[u8]) -> SigningResult<(TransactionInput, UtxoToSign)> { + let pubkey = ecdsa::secp256k1::PublicKey::try_from(pubkey) + .into_tw() + .context("Invalid P2PK public key")?; + + self.prepare_builder()?.p2pk(&pubkey) + } + + pub fn p2pkh( + &self, + pubkey_or_hash: &Proto::PublicKeyOrHash, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let pubkey_hash = self.get_ecdsa_pubkey_from_proto(pubkey_or_hash)?; + self.prepare_builder()?.p2pkh(&pubkey_hash) + } + + // TODO next iteration + // pub fn p2wsh(&self, redeem_script: Data) -> SigningResult<(TransactionInput, UtxoToSign)> { + // let script = Script::from(redeem_script); + // self.prepare_builder()?.p2wsh(script) + // } + + pub fn p2wpkh( + &self, + pubkey_or_hash: &Proto::PublicKeyOrHash, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let pubkey = self.get_ecdsa_pubkey_from_proto(pubkey_or_hash)?; + self.prepare_builder()?.p2wpkh(&pubkey) + } + + pub fn p2tr_key_path(&self, pubkey: &[u8]) -> SigningResult<(TransactionInput, UtxoToSign)> { + let public_key = schnorr::PublicKey::try_from(pubkey)?; + self.prepare_builder()?.p2tr_key_path(&public_key) + } + + // TODO next iteration + // pub fn p2tr_script_path( + // &self, + // taproot_script_path: &Proto::mod_Input::InputTaprootScriptPath, + // ) -> SigningResult<(TransactionInput, UtxoToSign)> { + // let payload = Script::from(taproot_script_path.payload.to_vec()); + // // let x = taproot_script_path. + // self.prepare_builder()? + // .p2tr_script_path(payload, taproot_script_path.control_block.to_vec()) + // } + + pub fn brc20_inscribe( + &self, + inscription: &Proto::mod_Input::InputBrc20Inscription, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let public_key = schnorr::PublicKey::try_from(inscription.inscribe_to.as_ref())?; + self.prepare_builder()?.brc20_transfer( + &public_key, + inscription.ticker.to_string(), + inscription.transfer_amount.to_string(), + ) + } + + pub fn custom_script( + &self, + script_data: Data, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let script = Script::from(script_data); + let builder = self.prepare_builder()?; + + match StandardScriptParser.parse(&script)? { + StandardScript::P2PK(pk) => builder.p2pk(&pk), + StandardScript::P2PKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2pkh(&pubkey) + }, + StandardScript::P2WPKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2wpkh(&pubkey) + }, + StandardScript::P2TR(tweaked_pubkey) => { + builder.p2tr_key_path_with_tweaked_pubkey(&tweaked_pubkey) + }, + StandardScript::P2SH(_) | StandardScript::P2WSH(_) => { + SigningError::err(SigningErrorType::Error_not_supported) + .context("P2SH and P2WSH scriptPubkey's are not supported yet") + }, + StandardScript::OpReturn(_) => SigningError::err(SigningErrorType::Error_invalid_utxo) + .context("Cannot spend an OP_RETURN output"), + } + } + + pub fn recipient_address(&self, addr: &str) -> SigningResult<(TransactionInput, UtxoToSign)> { + let addr = StandardBitcoinAddress::from_str(addr) + .into_tw() + .context("Invalid claiming script recipient address")?; + + match addr { + StandardBitcoinAddress::Legacy(ref legacy) => self.recipient_legacy_address(legacy), + StandardBitcoinAddress::Segwit(ref segwit) => self.recipient_segwit_address(segwit), + StandardBitcoinAddress::Taproot(ref taproot) => self.recipient_taproot_address(taproot), + } + .with_context(|| format!("Error handling {addr} input recipient")) + } + + pub fn recipient_legacy_address( + &self, + addr: &LegacyAddress, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let p2pkh_prefix = self.chain_info.p2pkh_prefix; + let p2sh_prefix = self.chain_info.p2sh_prefix; + + if p2pkh_prefix == addr.prefix() { + // P2PKH + let pubkey = self.public_keys.get_ecdsa_public_key(&addr.payload())?; + self.prepare_builder()?.p2pkh(&pubkey) + } else if p2sh_prefix == addr.prefix() { + // P2SH + SigningError::err(SigningErrorType::Error_script_redeem).context( + "pay-to-script-hash can only be used via 'Input.InputBuilder.p2sh'.\ + That is because P2SH address does not provide redeem script but its hash", + ) + } else { + // Unknown + SigningError::err(SigningErrorType::Error_invalid_address).context(format!( + "The given '{addr}' address has unexpected prefix. Expected p2pkh={p2pkh_prefix} p2sh={p2sh_prefix}", + )) + } + } + + pub fn recipient_segwit_address( + &self, + addr: &SegwitAddress, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let witness_program = addr.witness_program(); + match witness_program.len() { + // P2WPKH + H160::LEN => { + let pubkey_hash = H160::try_from(witness_program) + .expect("'witness_program' length must be checked already"); + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + self.prepare_builder()?.p2wpkh(&pubkey) + }, + // P2WSH + H256::LEN => { + SigningError::err(SigningErrorType::Error_script_redeem).context( + "pay-to-witness-script-hash can only be used via 'Input.InputBuilder.p2wsh'.\ + That is because P2SH address does not provide redeem script but its hash" + ) + }, + // Unknown + _ => SigningError::err(SigningErrorType::Error_invalid_address) + .context(format!("The given '{addr}' segwit address should have 20 or 32 byte length witness program")), + } + } + + pub fn recipient_taproot_address( + &self, + addr: &TaprootAddress, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let tweaked_pubkey = schnorr::XOnlyPublicKey::try_from(addr.witness_program()) + .tw_err(|_| SigningErrorType::Error_invalid_address) + .with_context(|| { + format!("The given '{addr}' taproot address witness program should be a valid tweaked schnorr public key") + })?; + + self.prepare_builder()? + .p2tr_key_path_with_tweaked_pubkey(&tweaked_pubkey) + } + + pub fn prepare_builder(&self) -> SigningResult { + let OutPoint { hash, index } = parse_out_point(&self.input.out_point)?; + let sighash_ty = SighashType::from_u32(self.input.sighash_type)?; + + if self.input.value < 0 { + return SigningError::err(SigningErrorType::Error_invalid_utxo_amount) + .context("UTXO amount cannot be negative"); + } + + let sequence = self + .input + .sequence + .clone() + .map(|seq| seq.sequence) + // Use the default 0xFFFFFFFF sequence value if not specified. + .unwrap_or(u32::MAX); + + Ok(UtxoBuilder::default() + .prev_txid(hash) + .prev_index(index) + .sequence(sequence) + .amount(self.input.value) + .sighash_type(sighash_ty)) + } + + /// Tries to convert [`Proto::PublicKeyOrHash`] to [`Hash`]. + /// Please note `P2PKH` and `P2WPKH` use the same `ripemd(sha256(x))` hash function. + fn get_ecdsa_pubkey_from_proto( + &self, + input: &Proto::PublicKeyOrHash, + ) -> SigningResult { + use Proto::mod_PublicKeyOrHash::OneOfvariant as PublicKeyOrHashType; + + let pubkey_data = match input.variant { + PublicKeyOrHashType::pubkey(ref pubkey) => pubkey.as_ref(), + PublicKeyOrHashType::hash(ref hash) => { + let hash = H160::try_from(hash.as_ref()) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Expected 20 bytes public key hash")?; + self.public_keys.get_public_key(&hash)? + }, + PublicKeyOrHashType::None => { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Expected a public key or its hash") + }, + }; + + ecdsa::secp256k1::PublicKey::try_from(pubkey_data) + .into_tw() + .context("Expected a valid ecdsa secp256k1 public key") + } +} + +pub fn parse_out_point(maybe_out_point: &Option) -> SigningResult { + let out_point = maybe_out_point + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("No OutPoint provided for a UTXO")?; + + let hash = H256::try_from(out_point.hash.as_ref()) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid previous txid")?; + + Ok(OutPoint { + hash, + index: out_point.vout, + }) +} diff --git a/rust/chains/tw_cosmos/Cargo.toml b/rust/chains/tw_cosmos/Cargo.toml new file mode 100644 index 00000000000..cbeae311ec3 --- /dev/null +++ b/rust/chains/tw_cosmos/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tw_cosmos" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_keypair = { path = "../../tw_keypair" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk", features = ["test-utils"] } diff --git a/rust/chains/tw_cosmos/src/entry.rs b/rust/chains/tw_cosmos/src/entry.rs new file mode 100644 index 00000000000..dc2e9de681b --- /dev/null +++ b/rust/chains/tw_cosmos/src/entry.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_cosmos_sdk::address::{Address, Bech32Prefix}; +use tw_cosmos_sdk::context::StandardCosmosContext; +use tw_cosmos_sdk::modules::compiler::tw_compiler::TWTransactionCompiler; +use tw_cosmos_sdk::modules::signer::tw_signer::TWSigner; +use tw_cosmos_sdk::modules::transaction_util::CosmosTransactionUtil; +use tw_keypair::tw; +use tw_proto::Cosmos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct CosmosEntry; + +impl CoinEntry for CosmosEntry { + type AddressPrefix = Bech32Prefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = CosmosTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + Address::from_str_with_coin_and_prefix(coin, address.to_string(), prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: tw::PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + Address::with_public_key_coin_context(coin, &public_key, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + TWSigner::::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + TWTransactionCompiler::::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + TWTransactionCompiler::::compile( + coin, + input, + signatures, + public_keys, + ) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(CosmosTransactionUtil::::default()) + } +} diff --git a/rust/chains/tw_cosmos/src/lib.rs b/rust/chains/tw_cosmos/src/lib.rs new file mode 100644 index 00000000000..c080f3da3cf --- /dev/null +++ b/rust/chains/tw_cosmos/src/lib.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod entry; diff --git a/rust/chains/tw_ethereum/Cargo.toml b/rust/chains/tw_ethereum/Cargo.toml new file mode 100644 index 00000000000..6a3cbe0ebd8 --- /dev/null +++ b/rust/chains/tw_ethereum/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_ethereum" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_evm = { path = "../../tw_evm" } +tw_keypair = { path = "../../tw_keypair" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/chains/tw_ethereum/src/entry.rs b/rust/chains/tw_ethereum/src/entry.rs new file mode 100644 index 00000000000..aad3d6f90b5 --- /dev/null +++ b/rust/chains/tw_ethereum/src/entry.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_evm::address::Address; +use tw_evm::evm_context::StandardEvmContext; +use tw_evm::evm_entry::EvmEntry; +use tw_evm::modules::compiler::Compiler; +use tw_evm::modules::message_signer::EthMessageSigner; +use tw_evm::modules::signer::Signer; +use tw_evm::modules::transaction_util::EvmTransactionUtil; +use tw_keypair::tw::PublicKey; +use tw_proto::Ethereum::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct EthereumEntry; + +impl CoinEntry for EthereumEntry { + type AddressPrefix = NoPrefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = EthMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = EvmTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Ok(Address::with_secp256k1_pubkey(public_key)) + } + + #[inline] + fn sign(&self, _coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + Signer::::sign_proto(input) + } + + #[inline] + fn preimage_hashes( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + Compiler::::preimage_hashes(input) + } + + #[inline] + fn compile( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + Compiler::::compile(input, signatures, public_keys) + } + + #[inline] + fn message_signer(&self) -> Option { + Some(EthMessageSigner) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(EvmTransactionUtil) + } +} + +impl EvmEntry for EthereumEntry { + type Context = StandardEvmContext; +} diff --git a/rust/chains/tw_ethereum/src/lib.rs b/rust/chains/tw_ethereum/src/lib.rs new file mode 100644 index 00000000000..c080f3da3cf --- /dev/null +++ b/rust/chains/tw_ethereum/src/lib.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod entry; diff --git a/rust/chains/tw_ethereum/tests/compiler.rs b/rust/chains/tw_ethereum/tests/compiler.rs new file mode 100644 index 00000000000..7d88e7279e2 --- /dev/null +++ b/rust/chains/tw_ethereum/tests/compiler.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_coin_entry::coin_entry_ext::CoinEntryExt; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_encoding::hex; +use tw_ethereum::entry::EthereumEntry; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::tw; +use tw_number::U256; +use tw_proto::Ethereum::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_proto::{deserialize, serialize}; + +#[test] +fn test_external_signature_sign() { + let coin = TestCoinContext::default(); + + let transfer = Proto::mod_Transaction::Transfer { + amount: U256::encode_be_compact(1_000_000_000_000_000_000), + data: Cow::default(), + }; + let input = Proto::SigningInput { + nonce: U256::encode_be_compact(11), + chain_id: U256::encode_be_compact(1), + gas_price: U256::encode_be_compact(20_000_000_000), + gas_limit: U256::encode_be_compact(21_000), + to_address: "0x3535353535353535353535353535353535353535".into(), + transaction: Some(Proto::Transaction { + transaction_oneof: Proto::mod_Transaction::OneOftransaction_oneof::transfer(transfer), + }), + ..Proto::SigningInput::default() + }; + + // Step 1: Obtain preimage hash + let input_data = serialize(&input).unwrap(); + let preimage_data = EthereumEntry + .preimage_hashes(&coin, &input_data) + .expect("!preimage_hashes"); + let preimage: CompilerProto::PreSigningOutput = + deserialize(&preimage_data).expect("Coin entry returned an invalid output"); + + assert_eq!(preimage.error, SigningErrorType::OK); + assert!(preimage.error_message.is_empty()); + assert_eq!( + hex::encode(&preimage.data_hash, false), + "15e180a6274b2f6a572b9b51823fce25ef39576d10188ecdcd7de44526c47217" + ); + + // Simulate signature, normally obtained from signature server + let public_key = secp256k1::PublicKey::try_from("044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a").unwrap(); + let public_key = tw::PublicKey::Secp256k1Extended(public_key); + let signature = hex::decode("360a84fb41ad07f07c845fedc34cde728421803ebbaae392fc39c116b29fc07b53bd9d1376e15a191d844db458893b928f3efbfee90c9febf51ab84c9796677900").unwrap(); + + // Verify signature (pubkey & hash & signature) + assert!(public_key.verify(&signature, &preimage.data_hash)); + + // Step 2: Compile transaction info + let input_data = serialize(&input).unwrap(); + let output_data = EthereumEntry + .compile( + &coin, + &input_data, + vec![signature], + vec![public_key.to_bytes()], + ) + .expect("!compile"); + let output: Proto::SigningOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + let expected_encoded = "f86c0b8504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a0360a84fb41ad07f07c845fedc34cde728421803ebbaae392fc39c116b29fc07ba053bd9d1376e15a191d844db458893b928f3efbfee90c9febf51ab84c97966779"; + assert_eq!(hex::encode(output.encoded, false), expected_encoded); + + // Double check: check if simple signature process gives the same result. Note that private + // keys were not used anywhere up to this point. + let input = Proto::SigningInput { + private_key: Cow::from( + hex::decode("4646464646464646464646464646464646464646464646464646464646464646") + .unwrap(), + ), + ..input + }; + + let input_data = serialize(&input).unwrap(); + let output_data = EthereumEntry + .sign(&coin, &input_data) + .expect("!output_data"); + let output: Proto::SigningOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + assert_eq!(hex::encode(output.encoded, false), expected_encoded); +} diff --git a/rust/chains/tw_ethereum/tests/signer.rs b/rust/chains/tw_ethereum/tests/signer.rs new file mode 100644 index 00000000000..06b6af14507 --- /dev/null +++ b/rust/chains/tw_ethereum/tests/signer.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_entry_ext::CoinEntryExt; +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_encoding::hex::DecodeHex; +use tw_ethereum::entry::EthereumEntry; + +#[test] +fn test_sign_json() { + let coin = TestCoinContext::default(); + + let input_json = r#"{"chainId":"AQ==","gasPrice":"1pOkAA==","gasLimit":"Ugg=","toAddress":"0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1","transaction":{"transfer":{"amount":"A0i8paFgAA=="}}})"#; + let private_key = "17209af590a86462395d5881e60d11c7fa7d482cfb02b5a01b93c2eeef243543" + .decode_hex() + .unwrap(); + + EthereumEntry + .sign_json(&coin, input_json, private_key) + .expect_err("'EthEntry::sign_json' is not supported yet"); + + // Expected result - "f86a8084d693a400825208947d8bf18c7ce84b3e175b339c4ca93aed1dd166f1870348bca5a160008025a0fe5802b49e04c6b1705088310e133605ed8b549811a18968ad409ea02ad79f21a05bf845646fb1e1b9365f63a7fd5eb5e984094e3ed35c3bed7361aebbcbf41f10" +} diff --git a/rust/chains/tw_greenfield/Cargo.toml b/rust/chains/tw_greenfield/Cargo.toml new file mode 100644 index 00000000000..ab4ee6f224f --- /dev/null +++ b/rust/chains/tw_greenfield/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tw_greenfield" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_encoding = { path = "../../tw_encoding" } +tw_evm = { path = "../../tw_evm" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_number = { path = "../../tw_number" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_misc = { path = "../../tw_misc", features = ["test-utils"] } diff --git a/rust/chains/tw_greenfield/fuzz/.gitignore b/rust/chains/tw_greenfield/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_greenfield/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_greenfield/fuzz/Cargo.toml b/rust/chains/tw_greenfield/fuzz/Cargo.toml new file mode 100644 index 00000000000..fbc0f5790cc --- /dev/null +++ b/rust/chains/tw_greenfield/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_greenfield-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_greenfield] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_greenfield/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_greenfield/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..f27808e5e42 --- /dev/null +++ b/rust/chains/tw_greenfield/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::Greenfield::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::Greenfield, input); +}); diff --git a/rust/chains/tw_greenfield/src/address.rs b/rust/chains/tw_greenfield/src/address.rs new file mode 100644 index 00000000000..94b9565b17a --- /dev/null +++ b/rust/chains/tw_greenfield/src/address.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::Serialize; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::address::CosmosAddress; +use tw_evm::address::Address as EthereumAddress; +use tw_keypair::ecdsa::secp256k1; +use tw_memory::Data; + +#[derive(Clone, Serialize)] +pub struct GreenfieldAddress(EthereumAddress); + +impl GreenfieldAddress { + /// Initializes an address with a `secp256k1` public key. + pub fn with_secp256k1_pubkey(pubkey: &secp256k1::PublicKey) -> GreenfieldAddress { + GreenfieldAddress(EthereumAddress::with_secp256k1_pubkey(pubkey)) + } +} + +impl CosmosAddress for GreenfieldAddress {} + +impl CoinAddress for GreenfieldAddress { + #[inline] + fn data(&self) -> Data { + self.0.data() + } +} + +impl FromStr for GreenfieldAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + EthereumAddress::from_str(s).map(GreenfieldAddress) + } +} + +impl fmt::Display for GreenfieldAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/rust/chains/tw_greenfield/src/compiler.rs b/rust/chains/tw_greenfield/src/compiler.rs new file mode 100644 index 00000000000..e9db4725199 --- /dev/null +++ b/rust/chains/tw_greenfield/src/compiler.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::context::GreenfieldContext; +use crate::modules::eip712_signer::{Eip712Signer, Eip712TxPreimage}; +use crate::modules::tx_builder::TxBuilder; +use crate::public_key::GreenfieldPublicKey; +use crate::signature::GreenfieldSignature; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::common::compile_input::SingleSignaturePubkey; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_cosmos_sdk::modules::broadcast_msg::{BroadcastMode, BroadcastMsg}; +use tw_cosmos_sdk::modules::serializer::json_serializer::JsonSerializer; +use tw_cosmos_sdk::modules::serializer::protobuf_serializer::ProtobufSerializer; +use tw_cosmos_sdk::public_key::CosmosPublicKey; +use tw_misc::traits::ToBytesVec; +use tw_proto::Greenfield::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct GreenfieldCompiler; + +impl GreenfieldCompiler { + /// Please note that [`Proto::SigningInput::public_key`] must be set. + /// If the public key should be derived from a private key, please do it before this method is called. + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let unsigned = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + let Eip712TxPreimage { eip712_tx, tx_hash } = Eip712Signer::preimage_hash(&unsigned)?; + + Ok(CompilerProto::PreSigningOutput { + data: Cow::from(eip712_tx.to_vec()), + data_hash: Cow::from(tx_hash.to_vec()), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + pub(crate) fn compile_impl( + coin: &dyn CoinContext, + mut input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let SingleSignaturePubkey { + signature: raw_signature, + public_key, + } = SingleSignaturePubkey::from_sign_pubkey_list(signatures, public_keys)?; + + let public_key_params = None; + let public_key = GreenfieldPublicKey::from_bytes(coin, &public_key, public_key_params) + .into_tw() + .context("Invalid provided public key")?; + let signature = GreenfieldSignature::try_from(raw_signature.as_slice()) + .into_tw() + .context("Invalid provided signature")?; + let signature_bytes = signature.to_vec(); + + // Set the public key. It will be used to construct a signer info. + input.public_key = Cow::from(public_key.to_bytes()); + let unsigned = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + + let signed_tx = unsigned.into_signed(signature); + let signed_tx_raw = ProtobufSerializer::::build_signed_tx(&signed_tx)?; + + let broadcast_mode = Self::broadcast_mode(input.mode); + let broadcast_tx = BroadcastMsg::raw(broadcast_mode, &signed_tx_raw).to_json_string(); + + let signature_json = JsonSerializer::::serialize_signature( + &public_key, + signature_bytes.clone(), + ); + let signature_json = serde_json::to_string(&[signature_json]) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing signatures as JSON")?; + + Ok(Proto::SigningOutput { + signature: Cow::from(signature_bytes), + signature_json: Cow::from(signature_json), + serialized: Cow::from(broadcast_tx), + ..Proto::SigningOutput::default() + }) + } + + fn broadcast_mode(input: Proto::BroadcastMode) -> BroadcastMode { + match input { + Proto::BroadcastMode::SYNC => BroadcastMode::Sync, + Proto::BroadcastMode::ASYNC => BroadcastMode::Async, + } + } +} diff --git a/rust/chains/tw_greenfield/src/context.rs b/rust/chains/tw_greenfield/src/context.rs new file mode 100644 index 00000000000..888147cde48 --- /dev/null +++ b/rust/chains/tw_greenfield/src/context.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::public_key::GreenfieldPublicKey; +use tw_cosmos_sdk::context::CosmosContext; +use tw_cosmos_sdk::private_key::secp256k1::Secp256PrivateKey; +use tw_cosmos_sdk::signature::secp256k1::Secp256k1Signature; +use tw_hash::hasher::Hasher; + +pub struct GreenfieldContext; + +impl CosmosContext for GreenfieldContext { + type Address = GreenfieldAddress; + type PrivateKey = Secp256PrivateKey; + type PublicKey = GreenfieldPublicKey; + type Signature = Secp256k1Signature; + + /// Greenfield uses EIP712 message signing algorithm built upon `keccak256` hash. + fn default_tx_hasher() -> Hasher { + Hasher::Keccak256 + } +} diff --git a/rust/chains/tw_greenfield/src/eip712_types.rs b/rust/chains/tw_greenfield/src/eip712_types.rs new file mode 100644 index 00000000000..3bf8f103d52 --- /dev/null +++ b/rust/chains/tw_greenfield/src/eip712_types.rs @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::GreenfieldFee; +use core::fmt; +use serde::{Serialize, Serializer}; +use serde_json::Value as Json; +use std::collections::BTreeMap; +use tw_cosmos_sdk::transaction::message::JsonMessage; +use tw_cosmos_sdk::transaction::Coin; +use tw_evm::abi::param_type::constructor::TypeConstructor; +use tw_evm::message::eip712::message_types::MessageTypesBuilder; +use tw_evm::message::eip712::property::PropertyType; +use tw_number::U256; + +const DOMAIN_NAME: &str = "Greenfield Tx"; +const DOMAIN_VERSION: &str = "1.0.0"; +const DOMAIN_VERIFY_CONTRACT: &str = "greenfield"; +const DOMAIN_SALT: &str = "0"; + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct MsgPropertyName(pub usize); + +impl fmt::Display for MsgPropertyName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "msg{}", self.0) + } +} + +pub struct MsgPropertyType(pub usize); + +impl fmt::Display for MsgPropertyType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Msg{}", self.0) + } +} + +impl Serialize for MsgPropertyName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +/// # EIP712 type +/// +/// ```json +/// { +/// "EIP712Domain": [ +/// { +/// "name": "chainId", +/// "type": "uint256" +/// }, +/// { +/// "name": "name", +/// "type": "string" +/// }, +/// { +/// "name": "salt", +/// "type": "string" +/// }, +/// { +/// "name": "verifyingContract", +/// "type": "string" +/// }, +/// { +/// "name": "version", +/// "type": "string" +/// } +/// ] +/// } +/// ``` +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Eip712Domain { + pub name: String, + pub version: String, + #[serde(serialize_with = "U256::as_decimal_str")] + pub chain_id: U256, + pub verifying_contract: String, + pub salt: String, +} + +impl Eip712Domain { + const TYPE_NAME: &'static str = "EIP712Domain"; + + /// Returns a Serializable data of the `EIP712Domain` type. + /// https://github.com/bnb-chain/greenfield-cosmos-sdk/blob/b48770f5e210b28536f92734b6228913666d4da1/x/auth/tx/eip712.go#L35-L40 + pub fn new(chain_id: U256) -> Eip712Domain { + Eip712Domain { + name: DOMAIN_NAME.to_string(), + version: DOMAIN_VERSION.to_string(), + chain_id, + verifying_contract: DOMAIN_VERIFY_CONTRACT.to_string(), + salt: DOMAIN_SALT.to_string(), + } + } + + pub fn declare_eip712_types(&self, builder: &mut MessageTypesBuilder) { + if let Some(mut domain_builder) = builder.add_custom_type(Self::TYPE_NAME.to_string()) { + domain_builder + .add_property("chainId", PropertyType::Uint) + .add_property("name", PropertyType::String) + .add_property("salt", PropertyType::String) + .add_property("verifyingContract", PropertyType::String) + .add_property("version", PropertyType::String); + } + } +} + +/// # EIP712 type +/// +/// ```json +/// { +/// "Coin": [ +/// { +/// "name": "amount", +/// "type": "uint256" +/// }, +/// { +/// "name": "denom", +/// "type": "string" +/// } +/// ] +/// } +/// ``` +pub struct Eip712Coin; + +impl Eip712Coin { + const TYPE_NAME: &'static str = "Coin"; + + pub fn declare_eip712_types(builder: &mut MessageTypesBuilder) { + if let Some(mut coin_builder) = builder.add_custom_type(Self::TYPE_NAME.to_string()) { + coin_builder + .add_property("amount", PropertyType::Uint) + .add_property("denom", PropertyType::String); + } + } +} + +/// # EIP712 type +/// +/// ```json +/// { +/// "Fee": [ +/// { +/// "name": "amount", +/// "type": "Coin[]" +/// }, +/// { +/// "name": "gas_limit", +/// "type": "uint256" +/// }, +/// { +/// "name": "granter", +/// "type": "string" +/// }, +/// { +/// "name": "payer", +/// "type": "string" +/// } +/// ] +/// } +/// ``` +#[derive(Serialize)] +pub struct Eip712Fee { + pub amount: Vec, + #[serde(serialize_with = "U256::as_decimal_str")] + pub gas_limit: U256, + pub payer: String, + pub granter: String, +} + +impl Eip712Fee { + const TYPE_NAME: &'static str = "Fee"; + + pub fn declare_eip712_types(builder: &mut MessageTypesBuilder) { + // `Tx` depends on `Coin` and `Fee` custom types. + Eip712Coin::declare_eip712_types(builder); + + if let Some(mut fee_builder) = builder.add_custom_type(Self::TYPE_NAME.to_string()) { + let amount_type = PropertyType::Custom(Eip712Coin::TYPE_NAME.to_string()); + fee_builder + .add_property("amount", PropertyType::array(amount_type)) + .add_property("gas_limit", PropertyType::Uint) + .add_property("granter", PropertyType::String) + .add_property("payer", PropertyType::String); + } + } +} + +impl From for Eip712Fee { + fn from(fee: GreenfieldFee) -> Self { + let payer = fee + .payer + .as_ref() + .map(|addr| addr.to_string()) + .unwrap_or_default(); + let granter = fee + .granter + .as_ref() + .map(|addr| addr.to_string()) + .unwrap_or_default(); + + Eip712Fee { + amount: fee.amounts.clone(), + gas_limit: U256::from(fee.gas_limit), + payer, + granter, + } + } +} + +#[derive(Clone, Serialize)] +pub struct Eip712TypedMsg { + #[serde(rename = "type")] + pub msg_type: String, + #[serde(flatten)] + pub value: Json, +} + +impl From for Eip712TypedMsg { + fn from(msg: JsonMessage) -> Self { + Eip712TypedMsg { + msg_type: msg.msg_type, + value: msg.value, + } + } +} + +/// # EIP712 type +/// +/// ```json +/// { +/// "Tx": [ +/// { +/// "name": "account_number", +/// "type": "uint256" +/// }, +/// { +/// "name": "chain_id", +/// "type": "uint256" +/// }, +/// { +/// "name": "fee", +/// "type": "Fee" +/// }, +/// { +/// "name": "memo", +/// "type": "string" +/// }, +/// { +/// "name": "msg1", +/// "type": "Msg1" +/// }, +/// { +/// "name": "sequence", +/// "type": "uint256" +/// }, +/// { +/// "name": "timeout_height", +/// "type": "uint256" +/// } +/// ] +/// } +/// ``` +#[derive(Serialize)] +pub struct Eip712Transaction { + #[serde(serialize_with = "U256::as_decimal_str")] + pub account_number: U256, + #[serde(serialize_with = "U256::as_decimal_str")] + pub chain_id: U256, + pub fee: Eip712Fee, + pub memo: String, + /// Will be flatten as `"msg1": { ... }, "msg2": { ... }`. + #[serde(flatten)] + pub msgs: BTreeMap, + #[serde(serialize_with = "U256::as_decimal_str")] + pub sequence: U256, + #[serde(serialize_with = "U256::as_decimal_str")] + pub timeout_height: U256, +} + +impl Eip712Transaction { + /// cbindgen::ignore + pub const TYPE_NAME: &'static str = "Tx"; + + pub fn declare_eip712_types(&self, builder: &mut MessageTypesBuilder) { + Eip712Fee::declare_eip712_types(builder); + + let Some(mut tx_builder) = builder.add_custom_type(Self::TYPE_NAME.to_string()) else { + return; + }; + + tx_builder + .add_property("account_number", PropertyType::Uint) + .add_property("chain_id", PropertyType::Uint) + .add_property( + "fee", + PropertyType::Custom(Eip712Fee::TYPE_NAME.to_string()), + ) + .add_property("memo", PropertyType::String) + .add_property("sequence", PropertyType::Uint) + .add_property("timeout_height", PropertyType::Uint); + + for (msg_property_name, _msg) in self.msgs.iter() { + let msg_property_type = MsgPropertyType(msg_property_name.0).to_string(); + let msg_property_type = PropertyType::Custom(msg_property_type.to_string()); + + let msg_property_name = msg_property_name.to_string(); + tx_builder.add_property(&msg_property_name, msg_property_type); + } + + tx_builder.sort_by_names(); + } +} diff --git a/rust/chains/tw_greenfield/src/entry.rs b/rust/chains/tw_greenfield/src/entry.rs new file mode 100644 index 00000000000..448cbe54f15 --- /dev/null +++ b/rust/chains/tw_greenfield/src/entry.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::compiler::GreenfieldCompiler; +use crate::signer::GreenfieldSigner; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Greenfield::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct GreenfieldEntry; + +impl CoinEntry for GreenfieldEntry { + type AddressPrefix = NoPrefix; + type Address = GreenfieldAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + GreenfieldAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + GreenfieldAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Ok(GreenfieldAddress::with_secp256k1_pubkey(public_key)) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + GreenfieldSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + GreenfieldCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + GreenfieldCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_greenfield/src/lib.rs b/rust/chains/tw_greenfield/src/lib.rs new file mode 100644 index 00000000000..02e2d565253 --- /dev/null +++ b/rust/chains/tw_greenfield/src/lib.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod context; +pub mod eip712_types; +pub mod entry; +pub mod modules; +pub mod public_key; +pub mod signature; +pub mod signer; +pub mod transaction; diff --git a/rust/chains/tw_greenfield/src/modules/eip712_signer.rs b/rust/chains/tw_greenfield/src/modules/eip712_signer.rs new file mode 100644 index 00000000000..c5ead1f19de --- /dev/null +++ b/rust/chains/tw_greenfield/src/modules/eip712_signer.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::eip712_types::{ + Eip712Domain, Eip712Fee, Eip712Transaction, Eip712TypedMsg, MsgPropertyName, +}; +use crate::transaction::GreenfieldUnsignedTransaction; +use std::collections::BTreeMap; +use tw_coin_entry::error::prelude::*; +use tw_evm::message::eip712::eip712_message::Eip712Message; +use tw_evm::message::eip712::message_types::MessageTypesBuilder; +use tw_evm::message::{to_signing, EthMessage}; +use tw_hash::H256; +use tw_number::U256; + +pub struct Eip712TxPreimage { + pub eip712_tx: String, + pub tx_hash: H256, +} + +pub struct Eip712Signer; + +impl Eip712Signer { + pub fn preimage_hash( + unsigned: &GreenfieldUnsignedTransaction, + ) -> SigningResult { + // `types_builder` will be used to declare all custom types like `Tx`, `Fee`, `Msg1` etc. + let mut types_builder = MessageTypesBuilder::default(); + + // Step 1: Convert all [`TxBody::messages`] to a map of `msg1: GreenfieldTypedMsg`, `msg2: GreenfieldTypedMsg`. + // at the same time, declare the message custom types. + let mut msgs = BTreeMap::new(); + for (msg_idx, msg) in unsigned.tx_body.messages.iter().enumerate() { + // Index of the transaction messages starts from 1. + let msg_idx = msg_idx + 1; + + let property_name = MsgPropertyName(msg_idx); + let property_value = Eip712TypedMsg::from(msg.to_json()?); + + msgs.insert(property_name, property_value); + + // Declare message custom types like `Msg1`, `TypeMsg1Amount`, etc. + msg.declare_eip712_type(msg_idx, &mut types_builder); + } + + // Step 2: Generate `Tx` and `Domain` objects - the main parts of the EIP712 message. + let tx_to_sign = Eip712Transaction { + account_number: U256::from(unsigned.account_number), + chain_id: unsigned.eth_chain_id, + fee: Eip712Fee::from(unsigned.fee.clone()), + memo: unsigned.tx_body.memo.clone(), + msgs, + sequence: U256::from(unsigned.signer.sequence), + timeout_height: U256::zero(), + }; + let domain = Eip712Domain::new(unsigned.eth_chain_id); + + // Step 3: Declare `Tx`, `Domain` and all types they depend on. + tx_to_sign.declare_eip712_types(&mut types_builder); + domain.declare_eip712_types(&mut types_builder); + + // Step 4: Generate EIP712 message with all declared custom types, `Domain` and `Tx`, + // and compute the EIP712 message hash. + let msg_to_sign = Eip712Message { + types: types_builder.build(), + domain: serde_json::to_value(domain) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing EIP712Domain as JSON")?, + primary_type: Eip712Transaction::TYPE_NAME.to_string(), + message: serde_json::to_value(tx_to_sign) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing EIP712 message payload as JSON")?, + }; + + let tx_hash = msg_to_sign.hash().map_err(to_signing)?; + let eip712_tx = + serde_json::to_string(&msg_to_sign).tw_err(|_| SigningErrorType::Error_internal)?; + + Ok(Eip712TxPreimage { eip712_tx, tx_hash }) + } +} diff --git a/rust/chains/tw_greenfield/src/modules/mod.rs b/rust/chains/tw_greenfield/src/modules/mod.rs new file mode 100644 index 00000000000..a9eed00576a --- /dev/null +++ b/rust/chains/tw_greenfield/src/modules/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod eip712_signer; +pub mod tx_builder; diff --git a/rust/chains/tw_greenfield/src/modules/tx_builder.rs b/rust/chains/tw_greenfield/src/modules/tx_builder.rs new file mode 100644 index 00000000000..6a12bb2aa6e --- /dev/null +++ b/rust/chains/tw_greenfield/src/modules/tx_builder.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::public_key::GreenfieldPublicKey; +use crate::transaction::message::GreenfieldMessageBox; +use crate::transaction::{ + GreenfieldFee, GreenfieldSignMode, GreenfieldSignerInfo, GreenfieldTxBody, + GreenfieldUnsignedTransaction, +}; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::public_key::CosmosPublicKey; +use tw_cosmos_sdk::transaction::{Coin, SignerInfo}; +use tw_misc::traits::OptionalEmpty; +use tw_number::U256; +use tw_proto::Greenfield::Proto; + +const DEFAULT_TIMEOUT_HEIGHT: u64 = 0; + +/// [`TxBuilder`] is used to build `UnsignedTransaction` +/// from the `TW::Greenfield::Proto::SigningInput` protobuf message. +pub struct TxBuilder; + +impl TxBuilder { + /// Please note that [`Proto::SigningInput::public_key`] must be set. + /// If the public key should be derived from a private key, please do it before this method is called. + pub fn unsigned_tx_from_proto( + coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + let signer = Self::signer_info_from_proto(coin, input)?; + + let fee = input + .fee + .as_ref() + .or_tw_err(SigningErrorType::Error_wrong_fee) + .context("No 'fee' specified")?; + let fee = Self::fee_from_proto(fee, &signer)?; + + let eth_chain_id = U256::from_str(&input.eth_chain_id) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid ETH chain ID")?; + + Ok(GreenfieldUnsignedTransaction { + signer, + fee, + cosmos_chain_id: input.cosmos_chain_id.to_string(), + eth_chain_id, + account_number: input.account_number, + tx_body: Self::tx_body_from_proto(input)?, + }) + } + + pub fn signer_info_from_proto( + coin: &dyn CoinContext, + input: &Proto::SigningInput, + ) -> SigningResult { + let public_key_params = None; + let public_key = + GreenfieldPublicKey::from_bytes(coin, &input.public_key, public_key_params)?; + let sign_mode = match input.signing_mode { + Proto::SigningMode::Eip712 => GreenfieldSignMode::Eip712, + }; + Ok(SignerInfo { + public_key, + sequence: input.sequence, + sign_mode: sign_mode.into(), + }) + } + + fn fee_from_proto( + input: &Proto::Fee, + signer: &GreenfieldSignerInfo, + ) -> SigningResult { + let payer = GreenfieldAddress::with_secp256k1_pubkey(&signer.public_key.0); + + let amounts = input + .amounts + .iter() + .map(Self::coin_from_proto) + .collect::>()?; + Ok(GreenfieldFee { + amounts, + gas_limit: input.gas, + payer: Some(payer), + granter: None, + }) + } + + fn coin_from_proto(input: &Proto::Amount<'_>) -> SigningResult { + let amount = U256::from_str(&input.amount) + .into_tw() + .context("Invalid amount: expected uint256 decimal-string")?; + Ok(Coin { + amount, + denom: input.denom.to_string(), + }) + } + + fn tx_body_from_proto(input: &Proto::SigningInput<'_>) -> SigningResult { + if input.messages.is_empty() { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("No transaction messages provided"); + } + + let messages = input + .messages + .iter() + .map(Self::tx_message_from_proto) + .collect::>()?; + + Ok(GreenfieldTxBody { + messages, + memo: input.memo.to_string(), + timeout_height: DEFAULT_TIMEOUT_HEIGHT, + }) + } + + pub fn tx_message_from_proto(input: &Proto::Message) -> SigningResult { + use Proto::mod_Message::OneOfmessage_oneof as MessageEnum; + + match input.message_oneof { + MessageEnum::send_coins_message(ref send) => Self::send_msg_from_proto(send), + MessageEnum::bridge_transfer_out(ref transfer_out) => { + Self::bridge_transfer_out_from_proto(transfer_out) + }, + MessageEnum::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No message type provided"), + } + } + + pub fn send_msg_from_proto( + send: &Proto::mod_Message::Send<'_>, + ) -> SigningResult { + use crate::transaction::message::send_order::GreenfieldSendMessage; + use tw_cosmos_sdk::transaction::message::cosmos_bank_message::SendMessage; + + let amounts = send + .amounts + .iter() + .map(Self::coin_from_proto) + .collect::>()?; + let msg = SendMessage { + custom_type_prefix: send.type_prefix.to_string().empty_or_some(), + from_address: GreenfieldAddress::from_str(&send.from_address) + .into_tw() + .context("Invalid sender address")?, + to_address: GreenfieldAddress::from_str(&send.to_address) + .into_tw() + .context("Invalid recipient address")?, + amount: amounts, + }; + Ok(Box::new(GreenfieldSendMessage(msg))) + } + + pub fn bridge_transfer_out_from_proto( + transfer_out: &Proto::mod_Message::BridgeTransferOut<'_>, + ) -> SigningResult { + use crate::transaction::message::transfer_out::GreenfieldTransferOut; + + let amount = transfer_out + .amount + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("No transfer amount specified")?; + + let msg = GreenfieldTransferOut { + custom_type_prefix: transfer_out.type_prefix.to_string().empty_or_some(), + amount: Self::coin_from_proto(amount)?, + from: GreenfieldAddress::from_str(&transfer_out.from_address) + .into_tw() + .context("Invalid sender address")?, + to: GreenfieldAddress::from_str(&transfer_out.to_address) + .into_tw() + .context("Invalid recipient address")?, + }; + Ok(Box::new(msg)) + } +} diff --git a/rust/chains/tw_greenfield/src/public_key.rs b/rust/chains/tw_greenfield/src/public_key.rs new file mode 100644 index 00000000000..41f4a3cced6 --- /dev/null +++ b/rust/chains/tw_greenfield/src/public_key.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_cosmos_sdk::public_key::{ + CosmosPublicKey, JsonPublicKey, ProtobufPublicKey, PublicKeyParams, +}; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::{tw, KeyPairError, KeyPairResult}; +use tw_memory::Data; +use tw_proto::{google, to_any}; + +pub struct GreenfieldPublicKey(pub secp256k1::PublicKey); + +impl JsonPublicKey for GreenfieldPublicKey { + fn public_key_type(&self) -> String { + "/cosmos.crypto.eth.ethsecp256k1.PubKey".to_string() + } +} + +impl ProtobufPublicKey for GreenfieldPublicKey { + fn to_proto(&self) -> google::protobuf::Any { + let proto = tw_cosmos_sdk::proto::cosmos::crypto::eth::ethsecp256k1::PubKey { + key: self.0.compressed().to_vec(), + }; + to_any(&proto) + } +} + +impl CosmosPublicKey for GreenfieldPublicKey { + fn from_private_key( + _coin: &dyn CoinContext, + private_key: &tw::PrivateKey, + // Ignore custom public key parameters. + _params: Option, + ) -> KeyPairResult { + let public_key = private_key + .get_public_key_by_type(tw::PublicKeyType::Secp256k1)? + .to_secp256k1() + .ok_or(KeyPairError::InvalidPublicKey)? + .clone(); + Ok(GreenfieldPublicKey(public_key)) + } + + fn from_bytes( + _coin: &dyn CoinContext, + public_key_bytes: &[u8], + // Ignore custom public key parameters. + _params: Option, + ) -> KeyPairResult { + secp256k1::PublicKey::try_from(public_key_bytes).map(GreenfieldPublicKey) + } + + fn to_bytes(&self) -> Data { + self.0.compressed().to_vec() + } +} diff --git a/rust/chains/tw_greenfield/src/signature.rs b/rust/chains/tw_greenfield/src/signature.rs new file mode 100644 index 00000000000..bc2094edb67 --- /dev/null +++ b/rust/chains/tw_greenfield/src/signature.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_cosmos_sdk::signature::CosmosSignature; +use tw_evm::message::signature::{MessageSignature, SignatureType}; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::KeyPairError; +use tw_misc::traits::ToBytesVec; + +/// secp256k1 signature with the legacy ETH replay protection. +#[derive(Clone)] +pub struct GreenfieldSignature(MessageSignature); + +impl TryFrom for GreenfieldSignature { + type Error = KeyPairError; + + fn try_from(sign: secp256k1::Signature) -> Result { + MessageSignature::prepared(sign, SignatureType::Legacy).map(GreenfieldSignature) + } +} + +impl CosmosSignature for GreenfieldSignature {} + +/// [`GreenfieldSignature::try_from`] tries to parse a standard secp256k1 signature from the given bytes, +/// and applies the legacy ETH replay protection. +impl<'a> TryFrom<&'a [u8]> for GreenfieldSignature { + type Error = KeyPairError; + + fn try_from(bytes: &'a [u8]) -> Result { + let standard_sign = secp256k1::Signature::try_from(bytes)?; + GreenfieldSignature::try_from(standard_sign) + } +} + +impl ToBytesVec for GreenfieldSignature { + fn to_vec(&self) -> Vec { + self.0.to_bytes().to_vec() + } +} diff --git a/rust/chains/tw_greenfield/src/signer.rs b/rust/chains/tw_greenfield/src/signer.rs new file mode 100644 index 00000000000..afde1ed2747 --- /dev/null +++ b/rust/chains/tw_greenfield/src/signer.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::compiler::GreenfieldCompiler; +use crate::modules::eip712_signer::{Eip712Signer, Eip712TxPreimage}; +use crate::modules::tx_builder::TxBuilder; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_misc::traits::ToBytesVec; +use tw_proto::Greenfield::Proto; + +pub struct GreenfieldSigner; + +impl GreenfieldSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + coin: &dyn CoinContext, + mut input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let key_pair = secp256k1::KeyPair::try_from(input.private_key.as_ref())?; + let public_key = key_pair.public().compressed().to_vec(); + + // Set the public key. It will be used to construct a signer info. + input.public_key = Cow::from(public_key.clone()); + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(coin, &input)?; + + let Eip712TxPreimage { tx_hash, .. } = Eip712Signer::preimage_hash(&unsigned_tx)?; + // Get the standard secp256k1 signature. It will be EIP155 protected at the `GreenfieldCompiler::compile_impl`. + let signature = key_pair.sign(tx_hash)?; + + let signatures = vec![signature.to_vec()]; + let public_keys = vec![public_key]; + + GreenfieldCompiler::compile_impl(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_greenfield/src/transaction/message/mod.rs b/rust/chains/tw_greenfield/src/transaction/message/mod.rs new file mode 100644 index 00000000000..7921931c696 --- /dev/null +++ b/rust/chains/tw_greenfield/src/transaction/message/mod.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::eip712_types::MsgPropertyType; +use tw_cosmos_sdk::transaction::message::{CosmosMessage, CosmosMessageBox}; +use tw_evm::message::eip712::message_types::MessageTypesBuilder; + +pub mod send_order; +pub mod transfer_out; +pub mod type_msg_amount; + +pub type GreenfieldMessageBox = Box; + +pub trait GreenfieldMessage: CosmosMessage { + fn eip712_type(&self, msg_idx: usize) -> String { + MsgPropertyType(msg_idx).to_string() + } + + fn declare_eip712_type(&self, msg_idx: usize, message_types: &mut MessageTypesBuilder); + + fn to_cosmos_message(&self) -> CosmosMessageBox; +} diff --git a/rust/chains/tw_greenfield/src/transaction/message/send_order.rs b/rust/chains/tw_greenfield/src/transaction/message/send_order.rs new file mode 100644 index 00000000000..7733629ba9d --- /dev/null +++ b/rust/chains/tw_greenfield/src/transaction/message/send_order.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::transaction::message::type_msg_amount::TypeMsgAmount; +use crate::transaction::message::GreenfieldMessage; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::proto::cosmos as CosmosProto; +use tw_cosmos_sdk::transaction::message::cosmos_bank_message::SendMessage; +use tw_cosmos_sdk::transaction::message::{ + message_to_json, CosmosMessage, CosmosMessageBox, JsonMessage, ProtobufMessage, +}; +use tw_evm::abi::param_type::constructor::TypeConstructor; +use tw_evm::message::eip712::message_types::MessageTypesBuilder; +use tw_evm::message::eip712::property::PropertyType; +use tw_proto::type_url; + +/// cosmos.bank.v1beta1.MsgSend +/// +/// # EIP712 custom types +/// +/// ```json +/// { +/// "TypeMsgAmount": [ +/// { +/// "name": "amount", +/// "type": "string" +/// }, +/// { +/// "name": "denom", +/// "type": "string" +/// } +/// ], +/// "Msg": [ +/// { +/// "name": "amount", +/// "type": "TypeMsgAmount[]" +/// }, +/// { +/// "name": "from_address", +/// "type": "string" +/// }, +/// { +/// "name": "to_address", +/// "type": "string" +/// }, +/// { +/// "name": "type", +/// "type": "string" +/// } +/// ] +/// } +/// ``` +#[derive(Clone)] +pub struct GreenfieldSendMessage(pub SendMessage); + +impl CosmosMessage for GreenfieldSendMessage { + fn to_proto(&self) -> SigningResult { + self.0.to_proto() + } + + /// [`GreenfieldSendMessage::to_json`] implementation differs from the original [`SendMessage::to_json`]: + /// * [`JsonMessage::msg_type`] should be "cosmos.bank.v1beta1.MsgSend" if other is not specified. + /// * [`JsonMessage::value`] is the same. + fn to_json(&self) -> SigningResult { + let msg_type = self + .0 + .custom_type_prefix + .clone() + .unwrap_or_else(type_url::); + message_to_json(&msg_type, &self.0) + } +} + +impl GreenfieldMessage for GreenfieldSendMessage { + fn declare_eip712_type(&self, msg_idx: usize, message_types: &mut MessageTypesBuilder) { + let this_msg_type_name = self.eip712_type(msg_idx); + + TypeMsgAmount::declare_eip712_type(msg_idx, message_types); + + if let Some(mut builder) = message_types.add_custom_type(this_msg_type_name) { + let amount_msg_type = PropertyType::Custom(TypeMsgAmount::eip712_type(msg_idx)); + builder + .add_property("amount", PropertyType::array(amount_msg_type)) + .add_property("from_address", PropertyType::String) + .add_property("to_address", PropertyType::String) + .add_property("type", PropertyType::String); + } + } + + fn to_cosmos_message(&self) -> CosmosMessageBox { + Box::new(self.clone()) + } +} diff --git a/rust/chains/tw_greenfield/src/transaction/message/transfer_out.rs b/rust/chains/tw_greenfield/src/transaction/message/transfer_out.rs new file mode 100644 index 00000000000..00a01116db5 --- /dev/null +++ b/rust/chains/tw_greenfield/src/transaction/message/transfer_out.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::transaction::message::type_msg_amount::TypeMsgAmount; +use crate::transaction::message::GreenfieldMessage; +use serde::Serialize; +use tw_coin_entry::error::prelude::*; +use tw_cosmos_sdk::modules::serializer::protobuf_serializer::build_coin; +use tw_cosmos_sdk::proto::greenfield as GreenfieldProto; +use tw_cosmos_sdk::transaction::message::{ + message_to_json, CosmosMessage, CosmosMessageBox, JsonMessage, ProtobufMessage, +}; +use tw_cosmos_sdk::transaction::Coin; +use tw_evm::message::eip712::message_types::MessageTypesBuilder; +use tw_evm::message::eip712::property::PropertyType; +use tw_proto::{to_any, type_url}; + +/// greenfield.bridge.MsgTransferOut +/// +/// # EIP712 custom types +/// +/// ```json +/// { +/// "TypeMsgAmount": [ +/// { +/// "name": "amount", +/// "type": "string" +/// }, +/// { +/// "name": "denom", +/// "type": "string" +/// } +/// ], +/// "Msg": [ +/// { +/// "name": "amount", +/// "type": "TypeMsgAmount" +/// }, +/// { +/// "name": "from", +/// "type": "string" +/// }, +/// { +/// "name": "to", +/// "type": "string" +/// }, +/// { +/// "name": "type", +/// "type": "string" +/// } +/// ] +/// } +/// ``` +#[derive(Clone, Serialize)] +pub struct GreenfieldTransferOut { + #[serde(skip)] + pub custom_type_prefix: Option, + pub amount: Coin, + pub from: GreenfieldAddress, + pub to: GreenfieldAddress, +} + +impl CosmosMessage for GreenfieldTransferOut { + fn to_proto(&self) -> SigningResult { + let msg = GreenfieldProto::bridge::MsgTransferOut { + from: self.from.to_string(), + to: self.to.to_string(), + amount: Some(build_coin(&self.amount)), + }; + Ok(to_any(&msg)) + } + + fn to_json(&self) -> SigningResult { + let msg_type = self + .custom_type_prefix + .clone() + .unwrap_or_else(type_url::); + message_to_json(&msg_type, self) + } +} + +impl GreenfieldMessage for GreenfieldTransferOut { + fn declare_eip712_type(&self, msg_idx: usize, message_types: &mut MessageTypesBuilder) { + let this_msg_type_name = self.eip712_type(msg_idx); + + TypeMsgAmount::declare_eip712_type(msg_idx, message_types); + + if let Some(mut builder) = message_types.add_custom_type(this_msg_type_name) { + let amount_msg_type = PropertyType::Custom(TypeMsgAmount::eip712_type(msg_idx)); + builder + .add_property("amount", amount_msg_type) + .add_property("from", PropertyType::String) + .add_property("to", PropertyType::String) + .add_property("type", PropertyType::String); + } + } + + fn to_cosmos_message(&self) -> CosmosMessageBox { + Box::new(self.clone()) + } +} diff --git a/rust/chains/tw_greenfield/src/transaction/message/type_msg_amount.rs b/rust/chains/tw_greenfield/src/transaction/message/type_msg_amount.rs new file mode 100644 index 00000000000..e635367220b --- /dev/null +++ b/rust/chains/tw_greenfield/src/transaction/message/type_msg_amount.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_evm::message::eip712::message_types::MessageTypesBuilder; +use tw_evm::message::eip712::property::PropertyType; + +/// Represents an amount type that belongs to a particular order. +pub struct TypeMsgAmount; + +impl TypeMsgAmount { + pub fn eip712_type(msg_idx: usize) -> String { + format!("TypeMsg{msg_idx}Amount") + } + + pub fn declare_eip712_type(msg_idx: usize, message_types: &mut MessageTypesBuilder) { + let type_msg_amount_type = Self::eip712_type(msg_idx); + if let Some(mut builder) = message_types.add_custom_type(type_msg_amount_type) { + builder.add_property("amount", PropertyType::String); + builder.add_property("denom", PropertyType::String); + } + } +} diff --git a/rust/chains/tw_greenfield/src/transaction/mod.rs b/rust/chains/tw_greenfield/src/transaction/mod.rs new file mode 100644 index 00000000000..80f08f72da3 --- /dev/null +++ b/rust/chains/tw_greenfield/src/transaction/mod.rs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GreenfieldAddress; +use crate::context::GreenfieldContext; +use crate::public_key::GreenfieldPublicKey; +use crate::signature::GreenfieldSignature; +use crate::transaction::message::GreenfieldMessageBox; +use tw_cosmos_sdk::transaction::{ + Fee, SignMode, SignedTransaction, SignerInfo, TxBody, UnsignedTransaction, +}; +use tw_misc::traits::ToBytesVec; +use tw_number::U256; + +pub type GreenfieldSignerInfo = SignerInfo; +pub type GreenfieldFee = Fee; +pub type GreenfieldSignedTransaction = SignedTransaction; + +pub mod message; + +pub enum GreenfieldSignMode { + Eip712, +} + +impl From for SignMode { + fn from(mode: GreenfieldSignMode) -> Self { + use tw_cosmos_sdk::proto::cosmos::signing::v1beta1 as signing_proto; + + match mode { + GreenfieldSignMode::Eip712 => { + SignMode::Other(signing_proto::SignMode::SIGN_MODE_EIP_712 as i32) + }, + } + } +} + +pub struct GreenfieldTxBody { + pub messages: Vec, + pub memo: String, + pub timeout_height: u64, +} + +impl GreenfieldTxBody { + fn into_cosmos_tx_body(self) -> TxBody { + TxBody { + messages: self + .messages + .into_iter() + .map(|greenfield_msg| greenfield_msg.to_cosmos_message()) + .collect(), + memo: self.memo, + timeout_height: self.timeout_height, + } + } +} + +pub struct GreenfieldUnsignedTransaction { + pub signer: GreenfieldSignerInfo, + pub fee: GreenfieldFee, + pub cosmos_chain_id: String, + pub eth_chain_id: U256, + pub account_number: u64, + pub tx_body: GreenfieldTxBody, +} + +impl GreenfieldUnsignedTransaction { + pub fn into_signed(self, signature: GreenfieldSignature) -> GreenfieldSignedTransaction { + self.into_cosmos_unsigned().into_signed(signature.to_vec()) + } + + fn into_cosmos_unsigned(self) -> UnsignedTransaction { + UnsignedTransaction { + signer: self.signer, + fee: self.fee, + chain_id: self.cosmos_chain_id, + account_number: self.account_number, + tx_body: self.tx_body.into_cosmos_tx_body(), + } + } +} diff --git a/rust/chains/tw_greenfield/tests/data/send_order_eip712.json b/rust/chains/tw_greenfield/tests/data/send_order_eip712.json new file mode 100644 index 00000000000..01f861be82b --- /dev/null +++ b/rust/chains/tw_greenfield/tests/data/send_order_eip712.json @@ -0,0 +1,149 @@ +{ + "types": { + "Coin": [ + { + "name": "amount", + "type": "uint256" + }, + { + "name": "denom", + "type": "string" + } + ], + "EIP712Domain": [ + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "salt", + "type": "string" + }, + { + "name": "verifyingContract", + "type": "string" + }, + { + "name": "version", + "type": "string" + } + ], + "Fee": [ + { + "name": "amount", + "type": "Coin[]" + }, + { + "name": "gas_limit", + "type": "uint256" + }, + { + "name": "granter", + "type": "string" + }, + { + "name": "payer", + "type": "string" + } + ], + "Msg1": [ + { + "name": "amount", + "type": "TypeMsg1Amount[]" + }, + { + "name": "from_address", + "type": "string" + }, + { + "name": "to_address", + "type": "string" + }, + { + "name": "type", + "type": "string" + } + ], + "Tx": [ + { + "name": "account_number", + "type": "uint256" + }, + { + "name": "chain_id", + "type": "uint256" + }, + { + "name": "fee", + "type": "Fee" + }, + { + "name": "memo", + "type": "string" + }, + { + "name": "msg1", + "type": "Msg1" + }, + { + "name": "sequence", + "type": "uint256" + }, + { + "name": "timeout_height", + "type": "uint256" + } + ], + "TypeMsg1Amount": [ + { + "name": "amount", + "type": "string" + }, + { + "name": "denom", + "type": "string" + } + ] + }, + "primaryType": "Tx", + "domain": { + "name": "Greenfield Tx", + "version": "1.0.0", + "chainId": "5600", + "verifyingContract": "greenfield", + "salt": "0" + }, + "message": { + "account_number": "15560", + "chain_id": "5600", + "fee": { + "amount": [ + { + "amount": "2000000000000000", + "denom": "BNB" + } + ], + "gas_limit": "200000", + "granter": "", + "payer": "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1" + }, + "memo": "", + "msg1": { + "amount": [ + { + "amount": "1000000000000000", + "denom": "BNB" + } + ], + "from_address": "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1", + "to_address": "0x280b27f3676db1C4475EE10F75D510Eb527fd155", + "type": "/cosmos.bank.v1beta1.MsgSend" + }, + "sequence": "2", + "timeout_height": "0" + } +} diff --git a/rust/chains/tw_greenfield/tests/data/transfer_out_eip712.json b/rust/chains/tw_greenfield/tests/data/transfer_out_eip712.json new file mode 100644 index 00000000000..c17ecd1bea2 --- /dev/null +++ b/rust/chains/tw_greenfield/tests/data/transfer_out_eip712.json @@ -0,0 +1,147 @@ +{ + "types": { + "Coin": [ + { + "name": "amount", + "type": "uint256" + }, + { + "name": "denom", + "type": "string" + } + ], + "EIP712Domain": [ + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "salt", + "type": "string" + }, + { + "name": "verifyingContract", + "type": "string" + }, + { + "name": "version", + "type": "string" + } + ], + "Fee": [ + { + "name": "amount", + "type": "Coin[]" + }, + { + "name": "gas_limit", + "type": "uint256" + }, + { + "name": "granter", + "type": "string" + }, + { + "name": "payer", + "type": "string" + } + ], + "Msg1": [ + { + "name": "amount", + "type": "TypeMsg1Amount" + }, + { + "name": "from", + "type": "string" + }, + { + "name": "to", + "type": "string" + }, + { + "name": "type", + "type": "string" + } + ], + "Tx": [ + { + "name": "account_number", + "type": "uint256" + }, + { + "name": "chain_id", + "type": "uint256" + }, + { + "name": "fee", + "type": "Fee" + }, + { + "name": "memo", + "type": "string" + }, + { + "name": "msg1", + "type": "Msg1" + }, + { + "name": "sequence", + "type": "uint256" + }, + { + "name": "timeout_height", + "type": "uint256" + } + ], + "TypeMsg1Amount": [ + { + "name": "amount", + "type": "string" + }, + { + "name": "denom", + "type": "string" + } + ] + }, + "primaryType": "Tx", + "domain": { + "name": "Greenfield Tx", + "version": "1.0.0", + "chainId": "5600", + "verifyingContract": "greenfield", + "salt": "0" + }, + "message": { + "account_number": "15560", + "chain_id": "5600", + "fee": { + "amount": [ + { + "amount": "2000000000000000", + "denom": "BNB" + } + ], + "gas_limit": "200000", + "granter": "", + "payer": "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1" + }, + "memo": "", + "msg1": { + "amount": { + "amount": "1000000000000000", + "denom": "BNB" + }, + "from": "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1", + "to": "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1", + "type": "/greenfield.bridge.MsgTransferOut" + }, + "sequence": "2", + "timeout_height": "0" + } +} \ No newline at end of file diff --git a/rust/chains/tw_greenfield/tests/eip712_signer.rs b/rust/chains/tw_greenfield/tests/eip712_signer.rs new file mode 100644 index 00000000000..e20ab3b48fb --- /dev/null +++ b/rust/chains/tw_greenfield/tests/eip712_signer.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_greenfield::modules::eip712_signer::{Eip712Signer, Eip712TxPreimage}; +use tw_greenfield::modules::tx_builder::TxBuilder; +use tw_misc::assert_eq_json; +use tw_proto::Greenfield::Proto; +use Proto::mod_Message::OneOfmessage_oneof as MessageEnum; + +const SEND_ORDER_EIP712: &str = include_str!("data/send_order_eip712.json"); +const TRANSFER_OUT_EIP712: &str = include_str!("data/transfer_out_eip712.json"); + +const PUBLIC_KEY_15560: &str = "0279ef34064da10db0463c70480616ba020703ec3a45026def7bebd2082f5d6fc8"; + +fn make_amount<'a>(denom: &'a str, amount: &'a str) -> Proto::Amount<'a> { + Proto::Amount { + denom: denom.into(), + amount: amount.into(), + } +} + +/// Testnet transaction: +/// https://greenfieldscan.com/tx/0x9f895cf2dd64fb1f428cefcf2a6585a813c3540fc9fe1ef42db1da2cb1df55ab +#[test] +fn test_eip712_signer_encode_send() { + let coin = TestCoinContext::default(); + + let send_order = Proto::mod_Message::Send { + from_address: "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1".into(), + to_address: "0x280b27f3676db1C4475EE10F75D510Eb527fd155".into(), + amounts: vec![make_amount("BNB", "1000000000000000")], + ..Proto::mod_Message::Send::default() + }; + + let input = Proto::SigningInput { + signing_mode: Proto::SigningMode::Eip712, + account_number: 15560, + eth_chain_id: "5600".into(), + cosmos_chain_id: "greenfield_5600-1".into(), + fee: Some(Proto::Fee { + amounts: vec![make_amount("BNB", "2000000000000000")], + gas: 200000, + }), + sequence: 2, + messages: vec![Proto::Message { + message_oneof: MessageEnum::send_coins_message(send_order), + }], + public_key: PUBLIC_KEY_15560.decode_hex().unwrap().into(), + ..Proto::SigningInput::default() + }; + + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(&coin, &input) + .expect("Error on generating an unsigned transaction"); + + let Eip712TxPreimage { eip712_tx, tx_hash } = + Eip712Signer::preimage_hash(&unsigned_tx).expect("Error on TX preimage"); + + assert_eq_json!(eip712_tx, SEND_ORDER_EIP712); + assert_eq!( + tx_hash.to_hex(), + "b8c62654582ca96b37ca94966199682bf70ed934e740d2f874ff54675a0ac344" + ); +} + +#[test] +fn test_eip712_signer_encode_transfer_out() { + let coin = TestCoinContext::default(); + + let send_order = Proto::mod_Message::BridgeTransferOut { + from_address: "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1".into(), + to_address: "0x9d1d97aDFcd324Bbd603D3872BD78e04098510b1".into(), + amount: Some(make_amount("BNB", "1000000000000000")), + ..Proto::mod_Message::BridgeTransferOut::default() + }; + + let input = Proto::SigningInput { + signing_mode: Proto::SigningMode::Eip712, + account_number: 15560, + eth_chain_id: "5600".into(), + cosmos_chain_id: "greenfield_5600-1".into(), + fee: Some(Proto::Fee { + amounts: vec![make_amount("BNB", "2000000000000000")], + gas: 200000, + }), + sequence: 2, + messages: vec![Proto::Message { + message_oneof: MessageEnum::bridge_transfer_out(send_order), + }], + public_key: PUBLIC_KEY_15560.decode_hex().unwrap().into(), + ..Proto::SigningInput::default() + }; + + let unsigned_tx = TxBuilder::unsigned_tx_from_proto(&coin, &input) + .expect("Error on generating an unsigned transaction"); + + let Eip712TxPreimage { eip712_tx, tx_hash } = + Eip712Signer::preimage_hash(&unsigned_tx).expect("Error on TX preimage"); + + assert_eq_json!(eip712_tx, TRANSFER_OUT_EIP712); + assert_eq!( + tx_hash.to_hex(), + "ea7731461041f5f652ab424bb767c670e484cfe1f4a85179deba8a6596873af4" + ); +} diff --git a/rust/chains/tw_internet_computer/Cargo.toml b/rust/chains/tw_internet_computer/Cargo.toml new file mode 100644 index 00000000000..fda55e71f69 --- /dev/null +++ b/rust/chains/tw_internet_computer/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tw_internet_computer" +version = "0.1.0" +edition = "2021" + +[dependencies] +quick-protobuf = "0.8.1" +serde = { version = "1.0", features = ["derive"] } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[build-dependencies] +pb-rs = "0.10.0" diff --git a/rust/chains/tw_internet_computer/build.rs b/rust/chains/tw_internet_computer/build.rs new file mode 100644 index 00000000000..1b8ee954b23 --- /dev/null +++ b/rust/chains/tw_internet_computer/build.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use pb_rs::types::FileDescriptor; +use pb_rs::ConfigBuilder; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +fn main() { + let proto_ext = Some(Path::new("proto").as_os_str()); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("proto"); + + let proto_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src") + .join("transactions") + .join("proto"); + let proto_dir_str = proto_dir.to_str().expect("Invalid proto directory path"); + // Re-run this build.rs if the `proto` directory has been changed (i.e. a new file is added). + println!("cargo:rerun-if-changed={}", proto_dir_str); + + let protos: Vec<_> = fs::read_dir(&proto_dir) + .expect("Expected a valid directory with proto files") + .filter_map(|file| { + let file = file.ok()?; + if file.path().extension() != proto_ext { + return None; + } + + let path = file.path(); + let path_str = path.to_str().expect("Invalid Proto file name"); + println!("cargo:rerun-if-changed={}", path_str); + Some(path) + }) + .collect(); + + // Delete all old generated files before re-generating new ones + if out_dir.exists() { + fs::remove_dir_all(&out_dir).expect("Error removing out directory"); + } + fs::DirBuilder::new() + .create(&out_dir) + .expect("Error creating out directory"); + + let out_protos = ConfigBuilder::new(&protos, None, Some(&out_dir), &[proto_dir]) + .expect("Error configuring pb-rs builder") + .dont_use_cow(true) + .owned(true) + .build(); + FileDescriptor::run(&out_protos).expect("Error generating proto files"); +} diff --git a/rust/chains/tw_internet_computer/fuzz/.gitignore b/rust/chains/tw_internet_computer/fuzz/.gitignore new file mode 100644 index 00000000000..1a45eee7760 --- /dev/null +++ b/rust/chains/tw_internet_computer/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/rust/chains/tw_internet_computer/fuzz/Cargo.lock b/rust/chains/tw_internet_computer/fuzz/Cargo.lock new file mode 100644 index 00000000000..887d622e7ea --- /dev/null +++ b/rust/chains/tw_internet_computer/fuzz/Cargo.lock @@ -0,0 +1,1595 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d1988118c887f61418940e322d574e8a2dd67165f1f1556eaae22e4019c6af" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "ppv-lite86", +] + +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "blake2b-ref" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294d17c72e0ba59fad763caa112368d0672083779cdebbb97164f4bb4c1e339a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "groestl" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343cfc165f92a988fd60292f7a0bfde4352a5a0beff9fbec29251ca4e9676e4d" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.7", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.7", +] + +[[package]] +name = "parity-scale-codec" +version = "3.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d640b40e81625f53b59f9c5812d3fb9e7ce11971d3fb34268eb7a83adf98051" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac464815d51fff2f64d690bf6ea4442d365e53495d50737685cfecfa3dd3f62" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pb-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "354a34df9c65b596152598001c0fe3393379ec2db03ae30b9985659422e2607e" +dependencies = [ + "clap", + "env_logger", + "log", + "nom", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primeorder" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "starknet-crypto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693e6362f150f9276e429a910481fb7f3bcb8d6aa643743f587cfece0b374874" +dependencies = [ + "crypto-bigint", + "hex", + "hmac", + "num-bigint", + "num-integer", + "num-traits", + "rfc6979", + "sha2 0.10.7", + "starknet-crypto-codegen", + "starknet-curve 0.3.0", + "starknet-ff", + "zeroize", +] + +[[package]] +name = "starknet-crypto-codegen" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6527b845423542c8a16e060ea1bc43f67229848e7cd4c4d80be994a84220ce" +dependencies = [ + "starknet-curve 0.4.0", + "starknet-ff", + "syn 2.0.31", +] + +[[package]] +name = "starknet-curve" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252610baff59e4c4332ce3569f7469c5d3f9b415a2240d698fb238b2b4fc0942" +dependencies = [ + "starknet-ff", +] + +[[package]] +name = "starknet-curve" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68a0d87ae56572abf83ddbfd44259a7c90dbeeee1629a1ffe223e7f9a8f3052" +dependencies = [ + "starknet-ff", +] + +[[package]] +name = "starknet-ff" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2cb1d9c0a50380cddab99cb202c6bfb3332728a2769bd0ca2ee80b0b390dd4" +dependencies = [ + "ark-ff", + "bigdecimal", + "crypto-bigint", + "getrandom 0.2.10", + "hex", + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tw_coin_entry" +version = "0.1.0" +dependencies = [ + "serde_json", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", +] + +[[package]] +name = "tw_encoding" +version = "0.1.0" +dependencies = [ + "bs58", + "ciborium", + "data-encoding", + "hex", + "serde", + "tw_memory", +] + +[[package]] +name = "tw_hash" +version = "0.1.0" +dependencies = [ + "blake-hash", + "blake2b-ref", + "digest 0.10.7", + "groestl", + "hmac", + "ripemd", + "serde", + "sha1", + "sha2 0.10.7", + "sha3", + "tw_encoding", + "tw_memory", + "zeroize", +] + +[[package]] +name = "tw_internet_computer" +version = "0.1.0" +dependencies = [ + "pb-rs", + "quick-protobuf", + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + +[[package]] +name = "tw_internet_computer-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "tw_internet_computer", + "tw_keypair", +] + +[[package]] +name = "tw_keypair" +version = "0.1.0" +dependencies = [ + "blake2", + "curve25519-dalek", + "der", + "digest 0.9.0", + "ecdsa", + "k256", + "lazy_static", + "p256", + "pkcs8", + "rfc6979", + "serde", + "sha2 0.9.9", + "starknet-crypto", + "starknet-ff", + "tw_encoding", + "tw_hash", + "tw_memory", + "tw_misc", + "zeroize", +] + +[[package]] +name = "tw_memory" +version = "0.1.0" + +[[package]] +name = "tw_misc" +version = "0.1.0" +dependencies = [ + "zeroize", +] + +[[package]] +name = "tw_number" +version = "0.1.0" +dependencies = [ + "primitive-types", + "tw_hash", + "tw_memory", +] + +[[package]] +name = "tw_proto" +version = "0.1.0" +dependencies = [ + "pb-rs", + "quick-protobuf", + "tw_encoding", + "tw_memory", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.31", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] diff --git a/rust/chains/tw_internet_computer/fuzz/Cargo.toml b/rust/chains/tw_internet_computer/fuzz/Cargo.toml new file mode 100644 index 00000000000..a31848778a8 --- /dev/null +++ b/rust/chains/tw_internet_computer/fuzz/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tw_internet_computer-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } + +[dependencies.tw_internet_computer] +path = ".." + + +[dependencies.tw_keypair] +path = "../../tw_keypair" + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "tw_internet_computer_transfer" +path = "fuzz_targets/tw_internet_computer_transfer.rs" +test = false +doc = false diff --git a/rust/chains/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs b/rust/chains/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs new file mode 100644 index 00000000000..568b15610b2 --- /dev/null +++ b/rust/chains/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs @@ -0,0 +1,71 @@ +#![no_main] + +use libfuzzer_sys::{arbitrary, fuzz_target}; +use tw_internet_computer::{ + address::AccountIdentifier, + protocol::principal::Principal, + transactions::transfer::{transfer, TransferArgs}, +}; +use tw_keypair::ecdsa::secp256k1; + +#[derive(Debug, arbitrary::Arbitrary)] +struct ArbitraryTransferArgs { + memo: u64, + amount: u64, + max_fee: Option, + #[arbitrary(with = arbitrary_to_field)] + to: String, + current_timestamp_nanos: u64, + permitted_drift: Option, +} + +fn arbitrary_to_field(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + let mut buf = [0; 29]; + u.fill_buffer(&mut buf)?; + let principal = Principal::from_slice(&buf); + let account_identifier = AccountIdentifier::new(&principal); + Ok(account_identifier.to_hex()) +} + +impl From<&ArbitraryTransferArgs> for TransferArgs { + fn from(prev: &ArbitraryTransferArgs) -> TransferArgs { + TransferArgs { + memo: prev.memo, + amount: prev.amount, + max_fee: prev.max_fee, + to: prev.to.clone(), + current_timestamp_nanos: prev.current_timestamp_nanos, + } + } +} + +#[derive(Debug, arbitrary::Arbitrary)] +struct TWInternetComputerTransactionsTransferInput { + #[arbitrary(with = arbitrary_private_key)] + private: Vec, + #[arbitrary(with = arbitrary_canister_id)] + canister_id: Principal, + args: ArbitraryTransferArgs, +} + +fn arbitrary_private_key(u: &mut arbitrary::Unstructured) -> arbitrary::Result> { + let mut buf = [0; 32]; + u.fill_buffer(&mut buf)?; + Ok(Vec::from(buf.as_slice())) +} + +fn arbitrary_canister_id(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + let mut buf = [0; 29]; + u.fill_buffer(&mut buf)?; + let principal = Principal::from_slice(&buf); + Ok(principal) +} + +fuzz_target!(|input: TWInternetComputerTransactionsTransferInput| { + let Ok(private_key) = secp256k1::PrivateKey::try_from(input.private.as_slice()) else { + return; + }; + + let args = TransferArgs::from(&input.args); + transfer(private_key, input.canister_id, args).ok(); +}); diff --git a/rust/chains/tw_internet_computer/src/address.rs b/rust/chains/tw_internet_computer/src/address.rs new file mode 100644 index 00000000000..e439b573a9d --- /dev/null +++ b/rust/chains/tw_internet_computer/src/address.rs @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::{coin_entry::CoinAddress, error::prelude::*}; +use tw_encoding::hex; +use tw_hash::{crc32::crc32, sha2::sha224, H256}; +use tw_keypair::ecdsa::secp256k1::PublicKey; + +use crate::protocol::principal::Principal; + +pub trait IcpAddress: std::str::FromStr + Into { + fn from_str_optional(s: &str) -> AddressResult> { + if s.is_empty() { + return Ok(None); + } + + Self::from_str(s).map(Some) + } +} + +/// The ICP ledger keeps track of accounts using account identifiers. +/// An account identifier is created by `SHA-224` hashing: +/// * \x0Aaccount-id +/// * the owner's principal ID +/// * subaccount (32-bytes) +/// +/// Then, +/// * CRC32 the SHA-224 hash +/// * Prepend the CRC32 to the SHA-224. +/// +/// https://internetcomputer.org/docs/current/references/ledger/#_accounts +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountIdentifier { + bytes: H256, +} + +impl AccountIdentifier { + /// Create a default account identifier from the given principal owner. + pub fn new(owner: &Principal) -> Self { + let mut input = vec![]; + input.extend_from_slice(b"\x0Aaccount-id"); + input.extend_from_slice(owner.as_slice()); + input.extend_from_slice(&H256::new()[..]); + + let hash = sha224(&input); + let crc32_bytes = crc32(&hash).to_be_bytes(); + + let mut bytes = H256::new(); + bytes[0..4].copy_from_slice(&crc32_bytes); + bytes[4..32].copy_from_slice(&hash); + Self { bytes } + } + + /// Return the textual-encoded account identifier. + pub fn to_hex(&self) -> String { + hex::encode(self.bytes, false) + } + + /// Instantiate an account identifier from a hex-encoded string. + pub fn from_hex(hex_str: &str) -> AddressResult { + if hex_str.len() != 64 { + return Err(AddressError::FromHexError); + } + + let hex = H256::try_from( + hex::decode(hex_str) + .map_err(|_| AddressError::FromHexError)? + .as_slice(), + ) + .map_err(|_| AddressError::FromHexError)?; + + if !is_check_sum_valid(hex) { + return Err(AddressError::FromHexError); + } + + Ok(Self { bytes: hex }) + } +} + +impl From<&PublicKey> for AccountIdentifier { + /// Takes a Secp256k1 public key, DER-encodes the public key, + /// and creates a principal from the encoding. + fn from(public_key: &PublicKey) -> Self { + let principal = Principal::from(public_key); + AccountIdentifier::new(&principal) + } +} + +impl std::str::FromStr for AccountIdentifier { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + AccountIdentifier::from_hex(s).map_err(|_| AddressError::FromHexError) + } +} + +impl std::fmt::Display for AccountIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl CoinAddress for AccountIdentifier { + fn data(&self) -> tw_memory::Data { + self.bytes.into_vec() + } +} + +impl IcpAddress for AccountIdentifier {} + +impl AsRef<[u8]> for AccountIdentifier { + fn as_ref(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +fn is_check_sum_valid(hash: H256) -> bool { + let found_checksum = &hash[0..4]; + let expected_checksum = crc32(&hash[4..]).to_be_bytes(); + found_checksum == expected_checksum +} + +#[cfg(test)] +mod test { + use tw_keypair::ecdsa::secp256k1::PublicKey; + + use super::*; + + const VALID_ADDRESSES: [&str; 10] = [ + "fb257577279ecac604d4780214af95aa6adc3a814f6f3d6d7ac844d1deca500a", + "e90c48d54847f4758f1d6b589a1db2500757a49a6722d4f775e050107b4b752d", + "a7c5baf393aed527ef6fb3869fbf84dd4e562edf9b04bd8f9bfbd6b8e6a22776", + "4cb2ca5cfcfa1d952f8cd7f0ec46c96e1023ab057b83a2c7ce236b9e71ccca0b", + "a93fff2708a6305e8946a0a06cbf559da01a758da58a615d404037b08ea96181", + "6e66c78a45cec01bcd0efd6dd142a82dc63b1a591c4ccb3c5877cd4d667747b4", + "c4ca697b46bb89ebf19eef3ad7b5e7bfa73315c0433a68a12a67f60fe017b9ad", + "7c513ec0de7347555b75cfefe29e56689de36636321fb0c8addf24a4f934ff0b", + "f61e15cdcaf0325bbaeb9a23a9f49d5447b33e6feee9763c2fdfe3a986142912", + "bb3357cba483f268d044d4bbd4639f82c16028a76eebdf62c51bc11fc918d278", + ]; + + const TOO_SHORT_ADDRESS: &str = + "3357cba483f268d044d4bbd4639f82c16028a76eebdf62c51bc11fc918d278b"; + const TOO_LONG_ADDRESS: &str = + "3357cba483f268d044d4bbd4639f82c16028a76eebdf62c51bc11fc918d278bce"; + const INVALID_CHECKSUM_ADDRESS: &str = + "553357cba483f268d044d4bbd4639f82c16028a76eebdf62c51bc11fc918d278"; + + const PUBLIC_KEY_HEX: &str = "048542e6fb4b17d6dfcac3948fe412c00d626728815ee7cc70509603f1bc92128a6e7548f3432d6248bc49ff44a1e50f6389238468d17f7d7024de5be9b181dbc8"; + + #[test] + fn from_hex() { + assert!(VALID_ADDRESSES + .iter() + .all(|s| AccountIdentifier::from_hex(s).is_ok())); + + assert!( + AccountIdentifier::from_hex(TOO_SHORT_ADDRESS).is_err(), + "Address is too short" + ); + assert!( + AccountIdentifier::from_hex(TOO_LONG_ADDRESS).is_err(), + "Address is too long" + ); + assert!( + AccountIdentifier::from_hex(INVALID_CHECKSUM_ADDRESS).is_err(), + "Invalid checksum" + ); + } + + #[test] + fn from_public_key() { + let public_key = PublicKey::try_from(PUBLIC_KEY_HEX).expect("Failed to populate key"); + let address = AccountIdentifier::from(&public_key); + assert_eq!( + address.to_hex(), + "2f25874478d06cf68b9833524a6390d0ba69c566b02f46626979a3d6a4153211" + ); + } +} diff --git a/rust/chains/tw_internet_computer/src/context.rs b/rust/chains/tw_internet_computer/src/context.rs new file mode 100644 index 00000000000..41cee32fb86 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/context.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::{ + address::{AccountIdentifier, IcpAddress}, + protocol::principal::Principal, +}; + +pub trait InternetComputerContext { + type Address: IcpAddress; + + fn get_canister_id() -> Principal; +} + +#[derive(Default)] +pub struct StandardInternetComputerContext; + +impl InternetComputerContext for StandardInternetComputerContext { + type Address = AccountIdentifier; + + fn get_canister_id() -> Principal { + // ICP Ledger Canister + Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap() + } +} + +#[cfg(test)] +mod test { + + use std::marker::PhantomData; + + use tw_coin_entry::error::prelude::*; + + use super::*; + + const TEST_OWNER_PRINCIPAL_ID: &str = + "t4u4z-y3dur-j63pk-nw4rv-yxdbt-agtt6-nygn7-ywh6y-zm2f4-sdzle-3qe"; + const TEST_TEXTUAL_ICP_ADDRESS: &str = + "943d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826a"; + const TEXTUAL_ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; + + pub struct ContextTest { + _phantom: PhantomData, + } + + impl ContextTest { + fn get_canister_id() -> Principal { + Context::get_canister_id() + } + + fn account_identifier_optional(s: &str) -> AddressResult> { + Context::Address::from_str_optional(s) + } + } + + #[test] + fn standard_internet_computer_context_canister_address() { + let owner = Principal::from_text(TEST_OWNER_PRINCIPAL_ID).unwrap(); + let expected_account_identifier = AccountIdentifier::new(&owner); + let account_identifier = + ContextTest::::account_identifier_optional( + TEST_TEXTUAL_ICP_ADDRESS, + ) + .unwrap() + .unwrap(); + assert_eq!(expected_account_identifier, account_identifier); + } + + #[test] + fn standard_internet_computer_context_canister_type() { + let ledger_canister_id = ContextTest::::get_canister_id(); + assert_eq!(ledger_canister_id.to_text(), TEXTUAL_ICP_LEDGER_CANISTER_ID); + } +} diff --git a/rust/chains/tw_internet_computer/src/entry.rs b/rust/chains/tw_internet_computer/src/entry.rs new file mode 100644 index 00000000000..3e9be8f75cb --- /dev/null +++ b/rust/chains/tw_internet_computer/src/entry.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; + +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::{ + coin_context::CoinContext, + coin_entry::CoinEntry, + error::prelude::*, + modules::{ + json_signer::NoJsonSigner, message_signer::NoMessageSigner, plan_builder::NoPlanBuilder, + wallet_connector::NoWalletConnector, + }, + prefix::NoPrefix, + signing_output_error, +}; +use tw_proto::{ + Common::Proto::SigningError as CommonError, InternetComputer::Proto, + TxCompiler::Proto as CompilerProto, +}; + +use crate::{address::AccountIdentifier, context::StandardInternetComputerContext, signer::Signer}; + +pub struct InternetComputerEntry; + +impl CoinEntry for InternetComputerEntry { + type AddressPrefix = NoPrefix; + type Address = AccountIdentifier; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + Self::Address::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Self::Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + public_key: tw_keypair::tw::PublicKey, + _derivation: tw_coin_entry::derivation::Derivation, + _prefix: Option, + ) -> AddressResult { + let secp256k1_public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Ok(Self::Address::from(secp256k1_public_key)) + } + + #[inline] + fn sign( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::SigningOutput { + Signer::::sign_proto(input) + } + + fn preimage_hashes( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + _input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + signing_output_error!( + CompilerProto::PreSigningOutput, + SigningError::new(CommonError::Error_not_supported) + ) + } + + fn compile( + &self, + _coin: &dyn tw_coin_entry::coin_context::CoinContext, + _input: Self::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> Self::SigningOutput { + signing_output_error!( + Proto::SigningOutput, + SigningError::new(CommonError::Error_not_supported) + ) + } +} diff --git a/rust/chains/tw_internet_computer/src/lib.rs b/rust/chains/tw_internet_computer/src/lib.rs new file mode 100644 index 00000000000..acef6a64209 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/lib.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod context; +pub mod entry; +pub mod protocol; +pub mod signer; +pub mod transactions; diff --git a/rust/chains/tw_internet_computer/src/protocol/envelope.rs b/rust/chains/tw_internet_computer/src/protocol/envelope.rs new file mode 100644 index 00000000000..c9627d53e1b --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/envelope.rs @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::collections::BTreeMap; + +use serde::{Serialize, Serializer}; + +use super::{ + principal::Principal, + request_id::{hash_of_map, RawHttpRequestVal, RequestId}, +}; + +pub trait RepresentationHashable { + fn request_id(&self) -> RequestId; +} + +#[derive(Debug, Clone, Serialize)] +pub struct Envelope { + pub content: C, + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_pubkey: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_sig: Option>, +} + +/// A replicated call to a canister method, whether update or query. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "request_type", rename = "call")] +pub struct EnvelopeCallContent { + /// A random series of bytes to uniquely identify this message. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_nonce" + )] + pub nonce: Option>, + /// A nanosecond timestamp after which this request is no longer valid. + pub ingress_expiry: u64, + /// The principal that is sending this request. + pub sender: Principal, + /// The ID of the canister to be called. + pub canister_id: Principal, + /// The name of the canister method to be called. + pub method_name: String, + /// The argument to pass to the canister method. + #[serde(serialize_with = "serialize_arg")] + pub arg: Vec, +} + +impl RepresentationHashable for EnvelopeCallContent { + fn request_id(&self) -> RequestId { + let mut map = vec![ + ( + "request_type".to_string(), + RawHttpRequestVal::String("call".to_string()), + ), + ( + "canister_id".to_string(), + RawHttpRequestVal::Bytes(self.canister_id.as_slice().to_vec()), + ), + ( + "method_name".to_string(), + RawHttpRequestVal::String(self.method_name.to_string()), + ), + ( + "arg".to_string(), + RawHttpRequestVal::Bytes(self.arg.clone()), + ), + ( + "ingress_expiry".to_string(), + RawHttpRequestVal::U64(self.ingress_expiry), + ), + ( + "sender".to_string(), + RawHttpRequestVal::Bytes(self.sender.as_slice().to_vec()), + ), + ] + .into_iter() + .collect::>(); + + if let Some(some_nonce) = &self.nonce { + map.insert( + "nonce".to_string(), + RawHttpRequestVal::Bytes(some_nonce.clone()), + ); + } + RequestId(hash_of_map(&map)) + } +} + +/// A request for information from the [IC state tree](https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree). +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "request_type", rename = "read_state")] +pub struct EnvelopeReadStateContent { + /// A nanosecond timestamp after which this request is no longer valid. + pub ingress_expiry: u64, + /// The principal that is sending this request. + pub sender: Principal, + /// A list of paths within the state tree to fetch. + pub paths: Vec>, +} + +impl RepresentationHashable for EnvelopeReadStateContent { + fn request_id(&self) -> RequestId { + let map = vec![ + ( + "request_type".to_string(), + RawHttpRequestVal::String("read_state".to_string()), + ), + ( + "ingress_expiry".to_string(), + RawHttpRequestVal::U64(self.ingress_expiry), + ), + ( + "paths".to_string(), + RawHttpRequestVal::Array( + self.paths + .iter() + .map(|p| { + RawHttpRequestVal::Array( + p.iter() + .map(|b| RawHttpRequestVal::Bytes(b.as_slice().to_vec())) + .collect(), + ) + }) + .collect(), + ), + ), + ( + "sender".to_string(), + RawHttpRequestVal::Bytes(self.sender.as_slice().to_vec()), + ), + ] + .into_iter() + .collect::>(); + + RequestId(hash_of_map(&map)) + } +} + +fn serialize_arg(arg: &[u8], s: S) -> Result +where + S: Serializer, +{ + s.serialize_bytes(arg) +} + +fn serialize_nonce(nonce: &Option>, s: S) -> Result +where + S: Serializer, +{ + match nonce { + Some(nonce) => s.serialize_bytes(nonce), + None => s.serialize_none(), + } +} + +#[derive(Debug, Clone)] +pub struct Label(Vec); + +impl Label { + #[inline] + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl From<&str> for Label { + fn from(value: &str) -> Self { + Label(value.as_bytes().to_vec()) + } +} + +impl From for Label { + fn from(value: RequestId) -> Self { + Label(value.0.as_slice().to_vec()) + } +} + +// Serialization +impl serde::Serialize for Label { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_bytes(self.0.as_slice()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn representation_independent_hash_call_or_query() { + let content = EnvelopeCallContent { + ingress_expiry: 1685570400000000000, + sender: Principal::anonymous(), + canister_id: Principal::from_slice(&[0, 0, 0, 0, 0, 0, 4, 210]), + method_name: "hello".to_string(), + arg: b"DIDL\x00\xFD*".to_vec(), + nonce: None, + }; + + assert_eq!( + tw_encoding::hex::encode(content.request_id().0, false), + "1d1091364d6bb8a6c16b203ee75467d59ead468f523eb058880ae8ec80e2b101" + ); + } + + #[test] + fn representation_independent_hash_read_state() { + let content = EnvelopeCallContent { + ingress_expiry: 1685570400000000000, + sender: Principal::anonymous(), + canister_id: Principal::from_slice(&[0, 0, 0, 0, 0, 0, 4, 210]), + method_name: "hello".to_string(), + arg: b"DIDL\x00\xFD*".to_vec(), + nonce: None, + }; + let update_request_id = content.request_id(); + + let content = EnvelopeReadStateContent { + ingress_expiry: 1685570400000000000, + sender: Principal::anonymous(), + paths: vec![vec![ + Label::from("request_status"), + Label::from(update_request_id), + ]], + }; + let request_id = content.request_id(); + assert_eq!( + tw_encoding::hex::encode(request_id.0, false), + "3cde0f14a953c3afbe1335f22e861bb62389f1449beca02707ab197e6829c2a3" + ); + } +} diff --git a/rust/chains/tw_internet_computer/src/protocol/identity.rs b/rust/chains/tw_internet_computer/src/protocol/identity.rs new file mode 100644 index 00000000000..b97e0308ec2 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/identity.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H256; +use tw_keypair::{ecdsa::secp256k1::PrivateKey, traits::SigningKeyTrait, KeyPairError}; + +use super::principal::Principal; + +#[derive(Debug)] +pub enum SigningError { + Failed(KeyPairError), +} + +/// Contains the signature and the associated DER-encoded public key from a call to +/// [Identity::sign]. +pub struct IdentitySignature { + pub signature: Vec, + pub public_key: Vec, +} + +/// An identity is a simple way to abstract away signing for envelopes. +/// When creating a request to the IC, the sender and signature are required +/// for authentication purposes. The sender is derived from a DER-encode public key +/// and the signature is created using the private key. +pub struct Identity { + private_key: PrivateKey, + der_encoded_public_key: Vec, +} + +impl Identity { + /// Gets the public key and DER-encodes it and returns an Identity. + pub fn new(private_key: PrivateKey) -> Self { + let public_key = private_key.public(); + let der_encoded_public_key = public_key.der_encoded(); + + Self { + private_key, + der_encoded_public_key, + } + } + + /// Returns the principal of the private key. + /// Sender represents who is sending the request. + pub fn sender(&self) -> Principal { + Principal::self_authenticating(&self.der_encoded_public_key) + } + + /// Signs the given content with the private key. + /// The signatures are encoded as the concatenation of the 32-byte big endian + /// encodings of the two values r and s. + /// + /// See: https://internetcomputer.org/docs/current/references/ic-interface-spec#ecdsa + pub fn sign(&self, content: H256) -> Result { + let signature = self + .private_key + .sign(content) + .map_err(SigningError::Failed)?; + + let r = signature.r(); + let s = signature.s(); + let mut bytes = [0u8; 64]; + bytes[..32].clone_from_slice(r.as_slice()); + bytes[32..].clone_from_slice(s.as_slice()); + + let signature = IdentitySignature { + public_key: self.der_encoded_public_key.clone(), + signature: bytes.to_vec(), //Signature bytes + }; + + Ok(signature) + } +} + +#[cfg(test)] +mod test { + + use tw_encoding::hex; + + use super::*; + + /// Test that the sender is derived from the private key. + #[test] + fn sender() { + let private_key = PrivateKey::try_from( + "227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be", + ) + .unwrap(); + let identity = Identity::new(private_key); + let sender = identity.sender(); + assert_eq!( + sender.to_text(), + "hpikg-6exdt-jn33w-ndty3-fc7jc-tl2lr-buih3-cs3y7-tftkp-sfp62-gqe" + ) + } + + /// Test signing with the identity. + #[test] + fn sign() { + let private_key = PrivateKey::try_from( + "227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be", + ) + .unwrap(); + let der_encoded_public_key = private_key.public().der_encoded(); + let identity = Identity::new(private_key); + let content = H256::new(); + let signature = identity.sign(content).unwrap(); + assert_eq!( + hex::encode(signature.signature, false), + "17c0974ee2ae621099389a5e4d0f960925d2e0e23658df03069308fb8edcb7bb120a338ada3e2ede7f41f6ed2f424a8a4f2c8fb68260f27d4f1bf96d19094b9f" + ); + assert_eq!(der_encoded_public_key, signature.public_key); + } +} diff --git a/rust/chains/tw_internet_computer/src/protocol/mod.rs b/rust/chains/tw_internet_computer/src/protocol/mod.rs new file mode 100644 index 00000000000..5d1f0380c38 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/mod.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod envelope; +pub mod identity; +pub mod principal; +pub mod request_id; +pub mod rosetta; + +use std::time::Duration; + +/// This constant defines the maximum amount of time an ingress message can wait +/// to start executing after submission before it is expired. Hence, if an +/// ingress message is submitted at time `t` and it has not been scheduled for +/// execution till time `t+MAX_INGRESS_TTL`, it will be expired. +/// +/// At the time of writing, this constant is also used to control how long the +/// status of a completed ingress message (IngressStatus ∈ [Completed, Failed]) +/// is maintained by the IC before it is deleted from the ingress history. +const MAX_INGRESS_TTL: Duration = Duration::from_secs(5 * 60); + +/// An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01. +pub fn get_ingress_expiry( + current_timestamp_duration: Duration, + permitted_drift_in_seconds: Option, +) -> u64 { + let permitted_drift = permitted_drift_in_seconds + .map(Duration::from_secs) + .unwrap_or(Duration::from_secs(60)); + + current_timestamp_duration + .saturating_add(MAX_INGRESS_TTL) + .saturating_sub(permitted_drift) + .as_nanos() as u64 +} diff --git a/rust/chains/tw_internet_computer/src/protocol/principal.rs b/rust/chains/tw_internet_computer/src/protocol/principal.rs new file mode 100644 index 00000000000..905739a86b8 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/principal.rs @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +/// Taken from candid crate and modified to rely upon built-in crc32 and SHA224 functionality. +use std::fmt::Write; + +use tw_hash::{crc32::crc32, sha2::sha224}; +use tw_keypair::ecdsa::secp256k1::PublicKey; + +/// An error happened while encoding, decoding or serializing a [`Principal`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PrincipalError { + BytesTooLong, + InvalidBase32, + TextTooShort, + TextTooLong, + CheckSequenceNotMatch, + AbnormalGrouped(Principal), +} + +/// Generic ID on Internet Computer. +/// +/// Principals are generic identifiers for canisters, users +/// and possibly other concepts in the future. +/// As far as most uses of the IC are concerned they are +/// opaque binary blobs with a length between 0 and 29 bytes, +/// and there is intentionally no mechanism to tell canister ids and user ids apart. +/// +/// Note a principal is not necessarily tied with a public key-pair, +/// yet we need at least a key-pair of a related principal to sign +/// requests. +/// +/// A Principal can be serialized to a byte array ([`Vec`]) or a text +/// representation, but the inner structure of the byte representation +/// is kept private. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Principal { + /// Length. + len: u8, + + /// The content buffer. When returning slices this should always be sized according to + /// `len`. + bytes: [u8; Self::MAX_LENGTH_IN_BYTES], +} + +impl Principal { + const MAX_LENGTH_IN_BYTES: usize = 29; + const CRC_LENGTH_IN_BYTES: usize = 4; + + const SELF_AUTHENTICATING_TAG: u8 = 2; + const ANONYMOUS_TAG: u8 = 4; + + /// Construct a [`Principal`] of the IC management canister + pub const fn management_canister() -> Self { + Self { + len: 0, + bytes: [0; Self::MAX_LENGTH_IN_BYTES], + } + } + + /// Construct a self-authenticating ID from public key + pub fn self_authenticating>(public_key: P) -> Self { + let public_key = public_key.as_ref(); + let hash = sha224(public_key); + let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; + bytes[..Self::MAX_LENGTH_IN_BYTES - 1].copy_from_slice(hash.as_slice()); + bytes[Self::MAX_LENGTH_IN_BYTES - 1] = Self::SELF_AUTHENTICATING_TAG; + + Self { + len: Self::MAX_LENGTH_IN_BYTES as u8, + bytes, + } + } + + /// Construct an anonymous ID. + pub const fn anonymous() -> Self { + let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; + bytes[0] = Self::ANONYMOUS_TAG; + Self { len: 1, bytes } + } + + /// Construct a [`Principal`] from a slice of bytes. + /// + /// # Panics + /// + /// Panics if the slice is longer than 29 bytes. + pub fn from_slice(slice: &[u8]) -> Self { + match Self::try_from_slice(slice) { + Ok(v) => v, + _ => panic!("slice length exceeds capacity"), + } + } + + /// Construct a [`Principal`] from a slice of bytes. + pub fn try_from_slice(slice: &[u8]) -> Result { + const MAX_LENGTH_IN_BYTES: usize = Principal::MAX_LENGTH_IN_BYTES; + if slice.len() > MAX_LENGTH_IN_BYTES { + return Err(PrincipalError::BytesTooLong); + } + + let mut bytes = [0; MAX_LENGTH_IN_BYTES]; + bytes[0..slice.len()].copy_from_slice(slice); + + Ok(Self { + len: slice.len() as u8, + bytes, + }) + } + + /// Parse a [`Principal`] from text representation. + pub fn from_text>(text: S) -> Result { + // Strategy: Parse very liberally, then pretty-print and compare output + // This is both simpler and yields better error messages + + let mut s = text.as_ref().to_string(); + s.make_ascii_uppercase(); + s.retain(|c| c != '-'); + + let bytes = tw_encoding::base32::decode(&s, None, false) + .map_err(|_| PrincipalError::InvalidBase32)?; + + if bytes.len() < Self::CRC_LENGTH_IN_BYTES { + return Err(PrincipalError::TextTooShort); + } + + let crc_bytes = &bytes[..Self::CRC_LENGTH_IN_BYTES]; + let data_bytes = &bytes[Self::CRC_LENGTH_IN_BYTES..]; + if data_bytes.len() > Self::MAX_LENGTH_IN_BYTES { + return Err(PrincipalError::TextTooLong); + } + + if crc32(data_bytes).to_be_bytes() != crc_bytes { + return Err(PrincipalError::CheckSequenceNotMatch); + } + + // Already checked data_bytes.len() <= MAX_LENGTH_IN_BYTES + // safe to unwrap here + let result = Self::try_from_slice(data_bytes).unwrap(); + let expected = format!("{result}"); + + // In the Spec: + // The textual representation is conventionally printed with lower case letters, + // but parsed case-insensitively. + if text.as_ref().to_ascii_lowercase() != expected { + return Err(PrincipalError::AbnormalGrouped(result)); + } + Ok(result) + } + + /// Convert [`Principal`] to text representation. + pub fn to_text(&self) -> String { + format!("{self}") + } + + /// Return the [`Principal`]'s underlying slice of bytes. + #[inline] + pub fn as_slice(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } +} + +impl std::str::FromStr for Principal { + type Err = PrincipalError; + + fn from_str(s: &str) -> Result { + Principal::from_text(s) + } +} + +impl From<&PublicKey> for Principal { + /// Takes a Secp256k1 public key, DER-encodes the public key, + /// and creates a principal from the encoding. + fn from(public_key: &PublicKey) -> Self { + Self::self_authenticating(public_key.der_encoded()) + } +} + +impl std::fmt::Display for Principal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let blob: &[u8] = self.as_slice(); + + // calc checksum + let checksum = crc32(blob); + + // combine blobs + let mut bytes = vec![]; + bytes.extend_from_slice(&checksum.to_be_bytes()); + bytes.extend_from_slice(blob); + + // base32 + let mut s = + tw_encoding::base32::encode(&bytes, None, false).map_err(|_| std::fmt::Error)?; + s.make_ascii_lowercase(); + + // write out string with dashes + let mut s = s.as_str(); + while s.len() > 5 { + f.write_str(&s[..5])?; + f.write_char('-')?; + s = &s[5..]; + } + f.write_str(s) + } +} + +impl TryFrom<&str> for Principal { + type Error = PrincipalError; + + fn try_from(s: &str) -> Result { + Principal::from_text(s) + } +} + +impl TryFrom> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: Vec) -> Result { + Self::try_from(bytes.as_slice()) + } +} + +impl TryFrom<&Vec> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: &Vec) -> Result { + Self::try_from(bytes.as_slice()) + } +} + +impl TryFrom<&[u8]> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: &[u8]) -> Result { + Self::try_from_slice(bytes) + } +} + +impl AsRef<[u8]> for Principal { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +// Serialization +impl serde::Serialize for Principal { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + self.to_text().serialize(serializer) + } else { + serializer.serialize_bytes(self.as_slice()) + } + } +} diff --git a/rust/chains/tw_internet_computer/src/protocol/request_id.rs b/rust/chains/tw_internet_computer/src/protocol/request_id.rs new file mode 100644 index 00000000000..ecc48a5a17f --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/request_id.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use tw_hash::{sha2::sha256, H256}; + +const DOMAIN_IC_REQUEST: &[u8; 11] = b"\x0Aic-request"; + +/// When signing requests or querying the status of a request +/// (see Request status) in the state tree, the user identifies +/// the request using a request id, which is the +/// representation-independent hash of the content map of the +/// original request. A request id must have length of 32 bytes. +pub struct RequestId(pub(crate) H256); + +impl RequestId { + /// Create the prehash from the request ID. + /// See: https://internetcomputer.org/docs/current/references/ic-interface-spec#envelope-authentication + pub fn sig_data(&self) -> H256 { + let mut sig_data = vec![]; + sig_data.extend_from_slice(DOMAIN_IC_REQUEST); + sig_data.extend_from_slice(self.0.as_slice()); + H256::try_from(sha256(&sig_data).as_slice()).unwrap_or_else(|_| H256::new()) + } +} + +/// The different types of values supported in `RawHttpRequest`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum RawHttpRequestVal { + Bytes(#[serde(serialize_with = "serialize_bytes")] Vec), + String(String), + U64(u64), + Array(Vec), +} + +fn serialize_bytes(bytes: &[u8], s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_bytes(bytes) +} + +fn hash_string(value: String) -> Vec { + sha256(value.as_bytes()) +} + +fn hash_bytes(value: Vec) -> Vec { + sha256(value.as_slice()) +} + +fn hash_u64(value: u64) -> Vec { + // We need at most ⌈ 64 / 7 ⌉ = 10 bytes to encode a 64 bit + // integer in LEB128. + let mut buf = [0u8; 10]; + let mut n = value; + let mut i = 0; + + loop { + let byte = (n & 0x7f) as u8; + n >>= 7; + + if n == 0 { + buf[i] = byte; + break; + } else { + buf[i] = byte | 0x80; + i += 1; + } + } + + hash_bytes(buf[..=i].to_vec()) +} + +// arrays, encoded as the concatenation of the hashes of the encodings of the +// array elements. +fn hash_array(elements: Vec) -> Vec { + let mut buffer = vec![]; + elements + .into_iter() + // Hash the encoding of all the array elements. + .for_each(|e| { + let mut hashed_val = hash_val(e); + buffer.append(&mut hashed_val); + }); + sha256(&buffer) +} + +fn hash_val(val: RawHttpRequestVal) -> Vec { + match val { + RawHttpRequestVal::String(string) => hash_string(string), + RawHttpRequestVal::Bytes(bytes) => hash_bytes(bytes), + RawHttpRequestVal::U64(integer) => hash_u64(integer), + RawHttpRequestVal::Array(elements) => hash_array(elements), + } +} + +fn hash_key_val(key: String, val: RawHttpRequestVal) -> Vec { + let mut key_hash = hash_string(key); + let mut val_hash = hash_val(val); + key_hash.append(&mut val_hash); + key_hash +} + +/// Describes `hash_of_map` as specified in the public spec. +/// See: https://internetcomputer.org/docs/current/references/ic-interface-spec#hash-of-map +pub fn hash_of_map(map: &BTreeMap) -> H256 { + let mut hashes: Vec> = Vec::new(); + for (key, val) in map.iter() { + hashes.push(hash_key_val(key.to_string(), val.clone())); + } + + // Computes hash by first sorting by "field name" hash, which is the + // same as sorting by concatenation of H(field name) · H(field value) + // (although in practice it's actually more stable in the presence of + // duplicated field names). Then concatenate all the hashes. + hashes.sort(); + + let buffer = hashes.into_iter().flatten().collect::>(); + let hash = sha256(&buffer); + + H256::try_from(hash.as_slice()).unwrap_or_else(|_| H256::new()) +} diff --git a/rust/chains/tw_internet_computer/src/protocol/rosetta.rs b/rust/chains/tw_internet_computer/src/protocol/rosetta.rs new file mode 100644 index 00000000000..a929a4f5f15 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/protocol/rosetta.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use super::envelope::{Envelope, EnvelopeCallContent, EnvelopeReadStateContent}; +use serde::Serialize; + +/// The types of requests that are available from the Rosetta node. +/// This enum is truncated to include support only for the +/// operations that this crate can currently perform. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum RequestType { + // Aliases for backwards compatibility + #[serde(rename = "TRANSACTION")] + #[serde(alias = "Send")] + Send, +} + +/// The type (encoded as CBOR) returned by the Rosetta node's +/// /construction/combine endpoint. It contains the +/// IC calls to submit the transaction and to check the result. +pub type SignedTransaction = Vec; + +/// A vector of update/read-state calls for different ingress windows +/// of the same call. +pub type Request = (RequestType, Vec); + +#[derive(Debug, Clone)] +pub enum EnvelopePairError { + InvalidUpdateEnvelope, + InvalidReadStateEnvelope, +} + +/// A signed IC update call and the corresponding read-state call for +/// a particular ingress window. +#[derive(Debug, Clone, Serialize)] +pub struct EnvelopePair { + update: Envelope, + read_state: Envelope, +} + +impl EnvelopePair { + pub fn new( + update_envelope: Envelope, + read_state_envelope: Envelope, + ) -> Result { + Ok(Self { + update: update_envelope, + read_state: read_state_envelope, + }) + } +} diff --git a/rust/chains/tw_internet_computer/src/signer.rs b/rust/chains/tw_internet_computer/src/signer.rs new file mode 100644 index 00000000000..ee4a39a76de --- /dev/null +++ b/rust/chains/tw_internet_computer/src/signer.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::marker::PhantomData; + +use tw_coin_entry::{error::prelude::*, signing_output_error}; +use tw_keypair::ecdsa::secp256k1; +use tw_proto::{Common::Proto::SigningError as CommonError, InternetComputer::Proto}; + +use crate::{ + context::InternetComputerContext, + protocol::identity, + transactions::{self, sign_transaction}, +}; + +impl From for SigningError { + fn from(error: transactions::SignTransactionError) -> Self { + match error { + transactions::SignTransactionError::InvalidArguments => { + SigningError::new(CommonError::Error_invalid_params) + }, + transactions::SignTransactionError::Identity(identity_error) => match identity_error { + identity::SigningError::Failed(_) => SigningError::new(CommonError::Error_signing), + }, + transactions::SignTransactionError::InvalidEnvelopePair + | transactions::SignTransactionError::EncodingArgsFailed => { + SigningError::new(CommonError::Error_internal) + }, + transactions::SignTransactionError::InvalidToAccountIdentifier => { + SigningError::new(CommonError::Error_invalid_address) + }, + transactions::SignTransactionError::InvalidAmount => { + SigningError::new(CommonError::Error_invalid_requested_token_amount) + }, + } + } +} + +pub struct Signer { + _phantom: PhantomData, +} + +impl Signer { + #[inline] + pub fn sign_proto(input: Proto::SigningInput<'_>) -> Proto::SigningOutput<'static> { + Self::sign_proto_impl(input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_proto_impl( + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let private_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref())?; + + let Some(ref transaction) = input.transaction else { + return SigningError::err(CommonError::Error_invalid_params); + }; + + let canister_id = Context::get_canister_id(); + let signed_transaction = + sign_transaction(private_key, canister_id, &transaction.transaction_oneof)?; + + let cbor_encoded_signed_transaction = tw_encoding::cbor::encode(&signed_transaction) + .tw_err(|_| CommonError::Error_internal)?; + + Ok(Proto::SigningOutput { + signed_transaction: cbor_encoded_signed_transaction.into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_internet_computer/src/transactions/mod.rs b/rust/chains/tw_internet_computer/src/transactions/mod.rs new file mode 100644 index 00000000000..e0c21697c1d --- /dev/null +++ b/rust/chains/tw_internet_computer/src/transactions/mod.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod transfer; + +pub mod proto { + include!(concat!(env!("OUT_DIR"), "/proto/mod.rs")); +} + +use tw_keypair::ecdsa::secp256k1::PrivateKey; +use tw_proto::InternetComputer::Proto::mod_Transaction::OneOftransaction_oneof as Tx; + +use crate::protocol::{identity, principal::Principal, rosetta}; + +#[derive(Debug)] +pub enum SignTransactionError { + InvalidAmount, + InvalidArguments, + Identity(identity::SigningError), + EncodingArgsFailed, + InvalidToAccountIdentifier, + InvalidEnvelopePair, +} + +pub fn sign_transaction( + private_key: PrivateKey, + canister_id: Principal, + transaction: &Tx, +) -> Result { + match transaction { + Tx::transfer(transfer_args) => transfer::transfer( + private_key, + canister_id, + transfer::TransferArgs { + memo: transfer_args.memo, + amount: transfer_args.amount, + max_fee: None, + to: transfer_args.to_account_identifier.to_string(), + current_timestamp_nanos: transfer_args.current_timestamp_nanos, + permitted_drift: if transfer_args.permitted_drift > 0 { + Some(transfer_args.permitted_drift) + } else { + None + }, + }, + ), + Tx::None => Err(SignTransactionError::InvalidArguments), + } +} diff --git a/rust/chains/tw_internet_computer/src/transactions/proto/ledger.proto b/rust/chains/tw_internet_computer/src/transactions/proto/ledger.proto new file mode 100644 index 00000000000..59858bbf2ea --- /dev/null +++ b/rust/chains/tw_internet_computer/src/transactions/proto/ledger.proto @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +// -*- c-basic-offset: 2 -*- +// Source: https://github.com/dfinity/ic/blob/master/rs/rosetta-api/icp_ledger/proto/ic_ledger/pb/v1/types.proto +// Commit Hash: 703eb96fea44ad2c82740e4360a526a2a127a960 +// Striped annotations related to the use of hardware wallets. + +syntax = "proto3"; + +package ic_ledger.pb.v1; + +import "./types.proto"; + +// Annotations related to the use of hardware wallets. The annotated messages are +// parsed on hardware wallets and marked fields are displayed in a trusted user +// interface (TUI). We must not, for instance, add fields that would change the +// semantics of the message such that old hardware wallets would not display +// appropriate information to users. + +// ** LEDGER CANISTER ENDPOINTS + +// Initialise the ledger canister +message LedgerInit { + AccountIdentifier minting_account = 1; + repeated Account initial_values = 2; + ic_base_types.pb.v1.PrincipalId archive_canister = 3; + uint32 max_message_size_bytes = 4; +} + +// The format of values serialized to/from the stable memory during and upgrade +message LedgerUpgrade {} + +// Make a payment +message SendRequest { + Memo memo = 1; + Payment payment = 2; + Tokens max_fee = 3; + Subaccount from_subaccount = 4; + AccountIdentifier to = 5; + BlockIndex created_at = 6; + TimeStamp created_at_time = 7; +} + +message SendResponse { + BlockIndex resulting_height = 1; +} + +// Notify a canister that it has received a payment +message NotifyRequest { + BlockIndex block_height = 1; + Tokens max_fee = 2; + Subaccount from_subaccount = 3; + ic_base_types.pb.v1.PrincipalId to_canister = 4; + Subaccount to_subaccount = 5; +} + +message NotifyResponse {} + +message TransactionNotificationRequest { + ic_base_types.pb.v1.PrincipalId from = 1; + Subaccount from_subaccount = 2; + ic_base_types.pb.v1.PrincipalId to = 3; + Subaccount to_subaccount = 4; + BlockIndex block_height = 5; + Tokens amount = 6; + Memo memo = 7; +} + +message TransactionNotificationResponse { + bytes response = 1; +} + +message CyclesNotificationResponse { + oneof response { + ic_base_types.pb.v1.PrincipalId created_canister_id = 1; + Refund refund = 2; + ToppedUp topped_up = 3; + } +} + +// Get the balance of an account +message AccountBalanceRequest { + AccountIdentifier account = 1; +} + +message AccountBalanceResponse { + Tokens balance = 1; +} + +// Get the length of the chain with a certification +message TipOfChainRequest {} + +message TipOfChainResponse { + Certification certification = 1; + BlockIndex chain_length = 2; +} + +// How many Tokens are there not in the minting account +message TotalSupplyRequest {} + +message TotalSupplyResponse { + Tokens total_supply = 1; +} + +// Archive any blocks older than this +message LedgerArchiveRequest { + TimeStamp timestamp = 1; +} + +// * Shared Endpoints * + +// Get a single block +message BlockRequest { + uint64 block_height = 1; +} + +message EncodedBlock { + bytes block = 1; +} + +message BlockResponse { + oneof block_content { + EncodedBlock block = 1; + ic_base_types.pb.v1.PrincipalId canister_id = 2; + } +} + +// Get a set of blocks +message GetBlocksRequest { + uint64 start = 1; + uint64 length = 2; +} + +message Refund { + BlockIndex refund = 2; + string error = 3; +} + +message ToppedUp {} + +message EncodedBlocks { + repeated EncodedBlock blocks = 1; +} + +message GetBlocksResponse { + oneof get_blocks_content { + EncodedBlocks blocks = 1; + string error = 2; + } +} + +// Iterate through blocks +message IterBlocksRequest { + uint64 start = 1; + uint64 length = 2; +} + +message IterBlocksResponse { + repeated EncodedBlock blocks = 1; +} + +message ArchiveIndexEntry { + uint64 height_from = 1; + uint64 height_to = 2; + ic_base_types.pb.v1.PrincipalId canister_id = 3; +} + +message ArchiveIndexResponse { + repeated ArchiveIndexEntry entries = 1; +} + +// ** ARCHIVE CANISTER ENDPOINTS ** + +// * Archive canister * +// Init the archive canister +message ArchiveInit { + uint32 node_max_memory_size_bytes = 1; + uint32 max_message_size_bytes = 2; +} + +// Add blocks to the archive canister +message ArchiveAddRequest { + repeated Block blocks = 1; +} + +message ArchiveAddResponse {} + +// Fetch a list of all of the archive nodes +message GetNodesRequest {} + +message GetNodesResponse { + repeated ic_base_types.pb.v1.PrincipalId nodes = 1; +} + +// ** BASIC TYPES ** +message Tokens { + uint64 e8s = 1; +} + +message Payment { + Tokens receiver_gets = 1; +} + +message BlockIndex { + uint64 height = 1; +} + +// This is the +message Block { + Hash parent_hash = 1; + TimeStamp timestamp = 2; + Transaction transaction = 3; +} + +message Hash { + bytes hash = 1; +} + +message Account { + AccountIdentifier identifier = 1; + Tokens balance = 2; +} + +message Transaction { + oneof transfer { + Burn burn = 1; + Mint mint = 2; + Send send = 3; + } + Memo memo = 4; + Icrc1Memo icrc1_memo = 7; + BlockIndex created_at = 5; // obsolete + TimeStamp created_at_time = 6; +} + +message Send { + // The meaning of the [from] field depends on the transaction type: + // - Transfer: [from] is the source account. + // - TransferFrom: [from] is the approver. + // - Approve: [from] is the approver. + AccountIdentifier from = 1; + // The meaning of the [to] field depends on the transaction type: + // - Transfer: [to] is the destination account. + // - TransferFrom: [to] is the destination account. + // - Approve: [to] is the default account id of the approved principal. + AccountIdentifier to = 2; + // If the transaction type is Approve, the amount must be zero. + Tokens amount = 3; + Tokens max_fee = 4; + + // We represent metadata of new operation types as submessages for + // backward compatibility with old clients. + oneof extension { + Approve approve = 5; + TransferFrom transfer_from = 6; + } +} + +message TransferFrom { + // The default account id of the principal who sent the transaction. + AccountIdentifier spender = 1; +} + +message Approve { + Tokens allowance = 1; + TimeStamp expires_at = 2; + Tokens expected_allowance = 3; +} + +message Mint { + AccountIdentifier to = 2; + Tokens amount = 3; +} + +message Burn { + AccountIdentifier from = 1; + Tokens amount = 3; +} + +message AccountIdentifier { + // Can contain either: + // * the 32 byte identifier (4 byte checksum + 28 byte hash) + // * the 28 byte hash + bytes hash = 1; +} + +message Subaccount { + bytes sub_account = 1; +} + +message Memo { + uint64 memo = 1; +} + +message Icrc1Memo { + bytes memo = 1; +} + +message TimeStamp { + uint64 timestamp_nanos = 1; +} + +message Certification { + bytes certification = 1; +} + +message TransferFeeRequest {} + +message TransferFeeResponse { + Tokens transfer_fee = 1; +} \ No newline at end of file diff --git a/rust/chains/tw_internet_computer/src/transactions/proto/types.proto b/rust/chains/tw_internet_computer/src/transactions/proto/types.proto new file mode 100644 index 00000000000..f869c11ccc3 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/transactions/proto/types.proto @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +// Source: https://github.com/dfinity/ic/blob/master/rs/rosetta-api/icp_ledger/proto/ic_ledger/pb/v1/types.proto +// Commit Hash: 703eb96fea44ad2c82740e4360a526a2a127a960 +// Striped annotations related to the use of hardware wallets. + +syntax = "proto3"; + +package ic_base_types.pb.v1; + +// A PB container for a PrincipalId, which uniquely identifies +// a principal. +message PrincipalId { + bytes serialized_id = 1; +} \ No newline at end of file diff --git a/rust/chains/tw_internet_computer/src/transactions/transfer.rs b/rust/chains/tw_internet_computer/src/transactions/transfer.rs new file mode 100644 index 00000000000..7cc846f3b53 --- /dev/null +++ b/rust/chains/tw_internet_computer/src/transactions/transfer.rs @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::time::Duration; + +use tw_keypair::ecdsa::secp256k1::PrivateKey; + +use crate::{ + address::AccountIdentifier, + protocol::{ + envelope::{ + Envelope, EnvelopeCallContent, EnvelopeReadStateContent, Label, RepresentationHashable, + }, + get_ingress_expiry, + identity::Identity, + principal::Principal, + request_id::RequestId, + rosetta, + }, + transactions::proto::ic_ledger::pb::v1::{ + AccountIdentifier as ProtoAccountIdentifier, Memo, Payment, SendRequest, TimeStamp, Tokens, + }, +}; + +use super::SignTransactionError; + +/// Arguments to be used with [transfer] to create a signed transaction enveloper pair. +#[derive(Clone, Debug)] +pub struct TransferArgs { + /// The memo field is used as a method to help identify the transaction. + pub memo: u64, + /// The amount of ICP to send as e8s. + pub amount: u64, + /// The maximum fee will to be paid to complete the transfer. + /// If not provided, the minimum fee will be applied to the transaction. Currently 10_000 e8s (0.00010000 ICP). + pub max_fee: Option, + /// The address to send the amount to. + pub to: String, + /// The current timestamp in nanoseconds. + pub current_timestamp_nanos: u64, + /// The duration to tune up ingress expiry in seconds. + pub permitted_drift: Option, +} + +impl TryFrom for SendRequest { + type Error = SignTransactionError; + + fn try_from(args: TransferArgs) -> Result { + let current_timestamp_duration = Duration::from_nanos(args.current_timestamp_nanos); + let timestamp_nanos = current_timestamp_duration.as_nanos() as u64; + + let to_account_identifier = AccountIdentifier::from_hex(&args.to) + .map_err(|_| SignTransactionError::InvalidToAccountIdentifier)?; + let to_hash = to_account_identifier.as_ref().to_vec(); + + let request = Self { + memo: Some(Memo { memo: args.memo }), + payment: Some(Payment { + receiver_gets: Some(Tokens { e8s: args.amount }), + }), + max_fee: args.max_fee.map(|fee| Tokens { e8s: fee }), + from_subaccount: None, + to: Some(ProtoAccountIdentifier { hash: to_hash }), + created_at: None, + created_at_time: Some(TimeStamp { timestamp_nanos }), + }; + Ok(request) + } +} + +/// The endpoint on the ledger canister that is used to make transfers. +const METHOD_NAME: &str = "send_pb"; + +/// Given a secp256k1 private key, the canister ID of an ICP-based ledger canister, and the actual transfer args, +/// this function creates a signed transaction to be sent to a Rosetta API node. +pub fn transfer( + private_key: PrivateKey, + canister_id: Principal, + args: TransferArgs, +) -> Result { + if args.amount < 1 { + return Err(SignTransactionError::InvalidAmount); + } + + let current_timestamp_duration = Duration::from_nanos(args.current_timestamp_nanos); + let ingress_expiry = get_ingress_expiry(current_timestamp_duration, args.permitted_drift); + let identity = Identity::new(private_key); + + // Encode the arguments for the ledger `send_pb` endpoint. + let send_request = SendRequest::try_from(args)?; + let arg = + tw_proto::serialize(&send_request).map_err(|_| SignTransactionError::EncodingArgsFailed)?; + // Create the update envelope. + let (request_id, update_envelope) = + create_update_envelope(&identity, canister_id, arg, ingress_expiry)?; + + // Create the read state envelope. + let (_, read_state_envelope) = + create_read_state_envelope(&identity, request_id, ingress_expiry)?; + + // Create a new EnvelopePair with the update call and read_state envelopes. + let envelope_pair = rosetta::EnvelopePair::new(update_envelope, read_state_envelope) + .map_err(|_| SignTransactionError::InvalidEnvelopePair)?; + + // Create a signed transaction containing the envelope pair. + let request: rosetta::Request = (rosetta::RequestType::Send, vec![envelope_pair]); + Ok(vec![request]) +} + +#[inline] +fn create_update_envelope( + identity: &Identity, + canister_id: Principal, + arg: Vec, + ingress_expiry: u64, +) -> Result<(RequestId, Envelope), SignTransactionError> { + let sender = identity.sender(); + let content = EnvelopeCallContent { + nonce: None, + ingress_expiry, + sender, + canister_id, + method_name: METHOD_NAME.to_string(), + arg, + }; + + let request_id = content.request_id(); + let signature = identity + .sign(request_id.sig_data()) + .map_err(SignTransactionError::Identity)?; + + let env = Envelope { + content, + sender_pubkey: Some(signature.public_key), + sender_sig: Some(signature.signature), + }; + Ok((request_id, env)) +} + +#[inline] +fn create_read_state_envelope( + identity: &Identity, + update_request_id: RequestId, + ingress_expiry: u64, +) -> Result<(RequestId, Envelope), SignTransactionError> { + let sender = identity.sender(); + + let content = EnvelopeReadStateContent { + ingress_expiry, + sender, + paths: vec![vec![ + Label::from("request_status"), + Label::from(update_request_id), + ]], + }; + + let request_id = content.request_id(); + let signature = identity + .sign(request_id.sig_data()) + .map_err(SignTransactionError::Identity)?; + + let env = Envelope { + content, + sender_pubkey: Some(signature.public_key), + sender_sig: Some(signature.signature), + }; + Ok((request_id, env)) +} + +#[cfg(test)] +mod test { + use tw_encoding::hex; + + use crate::address::AccountIdentifier; + + use super::*; + + pub const SIGNED_TRANSACTION: &str = "81826b5452414e53414354494f4e81a266757064617465a367636f6e74656e74a66c726571756573745f747970656463616c6c6e696e67726573735f6578706972791b177a297215cfe8006673656e646572581d971cd2ddeecd1cf1b28be914d7a5c43441f6296f1f9966a7c8aff68d026b63616e69737465725f69644a000000000000000201016b6d6574686f645f6e616d656773656e645f706263617267583b0a0012070a050880c2d72f2a220a20943d12e762f43806782f524b8f90297298a6d79e4749b41b585ec427409c826a3a0a088090caa5a3a78abd176d73656e6465725f7075626b65799858183018561830100607182a1886184818ce183d02010605182b188104000a0318420004183d18ab183a182118a81838184d184c187e1852188a187e18dc18d8184418ea18cd18c5189518ac188518b518bc181d188515186318bc18e618ab18d2184318d3187c184f18cd18f018de189b18b5181918dd18ef1889187218e71518c40418d4189718881843187218c611182e18cc18e6186b182118630218356a73656e6465725f736967984013186f18b9181c189818b318a8186518b2186118d418971618b1187d18eb185818e01826182f1873183b185018cb185d18ef18d81839186418b3183218da1824182f184e18a01880182718c0189018c918a018fd18c418d9189e189818b318ef1874183b185118e118a51864185918e718ed18c71889186c1822182318ca6a726561645f7374617465a367636f6e74656e74a46c726571756573745f747970656a726561645f73746174656e696e67726573735f6578706972791b177a297215cfe8006673656e646572581d971cd2ddeecd1cf1b28be914d7a5c43441f6296f1f9966a7c8aff68d0265706174687381824e726571756573745f7374617475735820e8fbc2d5b0bf837b3a369249143e50d4476faafb2dd620e4e982586a51ebcf1e6d73656e6465725f7075626b65799858183018561830100607182a1886184818ce183d02010605182b188104000a0318420004183d18ab183a182118a81838184d184c187e1852188a187e18dc18d8184418ea18cd18c5189518ac188518b518bc181d188515186318bc18e618ab18d2184318d3187c184f18cd18f018de189b18b5181918dd18ef1889187218e71518c40418d4189718881843187218c611182e18cc18e6186b182118630218356a73656e6465725f7369679840182d182718201888188618ce187f0c182a187a18d718e818df18fb18d318d41118a5186a184b18341842185318d718e618e8187a1828186c186a183618461418f3183318bd18a618a718bc18d918c818b7189d186e1865188418ff18fd18e418e9187f181b18d705184818b21872187818d6181c161833184318a2"; + + fn make_transfer_args() -> TransferArgs { + let current_timestamp_nanos = Duration::from_secs(1_691_709_940).as_nanos() as u64; + let owner = + Principal::from_text("t4u4z-y3dur-j63pk-nw4rv-yxdbt-agtt6-nygn7-ywh6y-zm2f4-sdzle-3qe") + .unwrap(); + let to_account_identifier = AccountIdentifier::new(&owner); + + TransferArgs { + memo: 0, + amount: 100_000_000, + max_fee: None, + to: to_account_identifier.to_hex(), + current_timestamp_nanos, + permitted_drift: None, + } + } + + #[test] + fn transfer_successful() { + let private_key = PrivateKey::try_from( + "227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be", + ) + .unwrap(); + let canister_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); + let transfer_args = make_transfer_args(); + + let signed_transaction = transfer(private_key, canister_id, transfer_args).unwrap(); + // Encode the signed transaction. + let cbor_encoded_signed_transaction = + tw_encoding::cbor::encode(&signed_transaction).unwrap(); + let hex_encoded_signed_transaction = hex::encode(&cbor_encoded_signed_transaction, false); + assert_eq!(hex_encoded_signed_transaction, SIGNED_TRANSACTION); + } + + #[test] + fn transfer_invalid_amount() { + let private_key = PrivateKey::try_from( + "227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be", + ) + .unwrap(); + let canister_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); + let mut transfer_args = make_transfer_args(); + transfer_args.amount = 0; + + let signed_transaction = transfer(private_key, canister_id, transfer_args); + assert!(matches!( + signed_transaction, + Err(SignTransactionError::InvalidAmount) + )); + } + + #[test] + fn transfer_invalid_to_account_identifier() { + let private_key = PrivateKey::try_from( + "227102911bb99ce7285a55f952800912b7d22ebeeeee59d77fc33a5d7c7080be", + ) + .unwrap(); + let canister_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); + let mut transfer_args = make_transfer_args(); + transfer_args.to = "invalid".to_string(); + + let signed_transaction = transfer(private_key, canister_id, transfer_args); + assert!(matches!( + signed_transaction, + Err(SignTransactionError::InvalidToAccountIdentifier) + )); + } +} diff --git a/rust/chains/tw_native_evmos/Cargo.toml b/rust/chains/tw_native_evmos/Cargo.toml new file mode 100644 index 00000000000..87137a252cc --- /dev/null +++ b/rust/chains/tw_native_evmos/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_native_evmos" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk", features = ["test-utils"] } diff --git a/rust/chains/tw_native_evmos/src/context.rs b/rust/chains/tw_native_evmos/src/context.rs new file mode 100644 index 00000000000..abebb7cd8f9 --- /dev/null +++ b/rust/chains/tw_native_evmos/src/context.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::ethermint_public_key::EthermintEthSecp256PublicKey; +use tw_cosmos_sdk::address::Address; +use tw_cosmos_sdk::context::CosmosContext; +use tw_cosmos_sdk::private_key::secp256k1::Secp256PrivateKey; +use tw_cosmos_sdk::signature::secp256k1::Secp256k1Signature; +use tw_hash::hasher::Hasher; + +pub struct NativeEvmosContext; + +impl CosmosContext for NativeEvmosContext { + type Address = Address; + type PrivateKey = Secp256PrivateKey; + type PublicKey = EthermintEthSecp256PublicKey; + type Signature = Secp256k1Signature; + + fn default_tx_hasher() -> Hasher { + Hasher::Keccak256 + } +} diff --git a/rust/chains/tw_native_evmos/src/entry.rs b/rust/chains/tw_native_evmos/src/entry.rs new file mode 100644 index 00000000000..e3964e6454c --- /dev/null +++ b/rust/chains/tw_native_evmos/src/entry.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::context::NativeEvmosContext; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_cosmos_sdk::address::{Address, Bech32Prefix}; +use tw_cosmos_sdk::modules::compiler::tw_compiler::TWTransactionCompiler; +use tw_cosmos_sdk::modules::signer::tw_signer::TWSigner; +use tw_keypair::tw; +use tw_proto::Cosmos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct NativeEvmosEntry; + +impl CoinEntry for NativeEvmosEntry { + type AddressPrefix = Bech32Prefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + Address::from_str_with_coin_and_prefix(coin, address.to_string(), prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: tw::PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + Address::with_public_key_coin_context(coin, &public_key, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + TWSigner::::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + TWTransactionCompiler::::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + TWTransactionCompiler::::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_native_evmos/src/ethermint_public_key.rs b/rust/chains/tw_native_evmos/src/ethermint_public_key.rs new file mode 100644 index 00000000000..c2bf2d8f0ad --- /dev/null +++ b/rust/chains/tw_native_evmos/src/ethermint_public_key.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_cosmos_sdk::proto::ethermint; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_cosmos_sdk::public_key::{ + CosmosPublicKey, JsonPublicKey, ProtobufPublicKey, PublicKeyParams, +}; +use tw_keypair::{tw, KeyPairResult}; +use tw_memory::Data; +use tw_proto::{google, type_url}; + +const ETHERMINT_SECP256K1_PUBLIC_KEY_TYPE: &str = "ethermint/PubKeyEthSecp256k1"; + +pub struct EthermintEthSecp256PublicKey(Secp256PublicKey); + +impl EthermintEthSecp256PublicKey { + fn default_public_key_params() -> PublicKeyParams { + PublicKeyParams { + // `NativeEvmos` requires the public key to be compressed, + // however the uncompressed public key is used to generate an address. + public_key_type: tw::PublicKeyType::Secp256k1, + json_type: ETHERMINT_SECP256K1_PUBLIC_KEY_TYPE.to_string(), + protobuf_type_url: type_url::(), + } + } +} + +impl CosmosPublicKey for EthermintEthSecp256PublicKey { + fn from_bytes( + coin: &dyn CoinContext, + public_key_bytes: &[u8], + maybe_params: Option, + ) -> KeyPairResult { + // Use default Ethermint public key parameters if otherwise is not specified, + // however the uncompressed public key is used to generate an address. + let params = maybe_params.unwrap_or_else(Self::default_public_key_params); + Secp256PublicKey::from_bytes(coin, public_key_bytes, Some(params)) + .map(EthermintEthSecp256PublicKey) + } + + fn to_bytes(&self) -> Data { + self.0.to_bytes() + } +} + +impl JsonPublicKey for EthermintEthSecp256PublicKey { + fn public_key_type(&self) -> String { + self.0.public_key_type() + } +} + +impl ProtobufPublicKey for EthermintEthSecp256PublicKey { + fn to_proto(&self) -> google::protobuf::Any { + self.0.to_proto() + } +} diff --git a/rust/chains/tw_native_evmos/src/lib.rs b/rust/chains/tw_native_evmos/src/lib.rs new file mode 100644 index 00000000000..0c7068cba6f --- /dev/null +++ b/rust/chains/tw_native_evmos/src/lib.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod context; +pub mod entry; +pub mod ethermint_public_key; diff --git a/rust/chains/tw_native_injective/Cargo.toml b/rust/chains/tw_native_injective/Cargo.toml new file mode 100644 index 00000000000..b8d8a0bcb50 --- /dev/null +++ b/rust/chains/tw_native_injective/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_native_injective" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk", features = ["test-utils"] } diff --git a/rust/chains/tw_native_injective/src/context.rs b/rust/chains/tw_native_injective/src/context.rs new file mode 100644 index 00000000000..c50dff8c44a --- /dev/null +++ b/rust/chains/tw_native_injective/src/context.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::injective_public_key::InjectiveEthSecp256PublicKey; +use tw_cosmos_sdk::address::Address; +use tw_cosmos_sdk::context::CosmosContext; +use tw_cosmos_sdk::private_key::secp256k1::Secp256PrivateKey; +use tw_cosmos_sdk::signature::secp256k1::Secp256k1Signature; +use tw_hash::hasher::Hasher; + +pub struct NativeInjectiveContext; + +impl CosmosContext for NativeInjectiveContext { + type Address = Address; + type PrivateKey = Secp256PrivateKey; + type PublicKey = InjectiveEthSecp256PublicKey; + type Signature = Secp256k1Signature; + + fn default_tx_hasher() -> Hasher { + Hasher::Keccak256 + } +} diff --git a/rust/chains/tw_native_injective/src/entry.rs b/rust/chains/tw_native_injective/src/entry.rs new file mode 100644 index 00000000000..bca5b9759dd --- /dev/null +++ b/rust/chains/tw_native_injective/src/entry.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::context::NativeInjectiveContext; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_cosmos_sdk::address::{Address, Bech32Prefix}; +use tw_cosmos_sdk::modules::compiler::tw_compiler::TWTransactionCompiler; +use tw_cosmos_sdk::modules::signer::tw_signer::TWSigner; +use tw_keypair::tw; +use tw_proto::Cosmos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct NativeInjectiveEntry; + +impl CoinEntry for NativeInjectiveEntry { + type AddressPrefix = Bech32Prefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + Address::from_str_with_coin_and_prefix(coin, address.to_string(), prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: tw::PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + Address::with_public_key_coin_context(coin, &public_key, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + TWSigner::::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + TWTransactionCompiler::::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + TWTransactionCompiler::::compile( + coin, + input, + signatures, + public_keys, + ) + } +} diff --git a/rust/chains/tw_native_injective/src/injective_public_key.rs b/rust/chains/tw_native_injective/src/injective_public_key.rs new file mode 100644 index 00000000000..c405867f6e8 --- /dev/null +++ b/rust/chains/tw_native_injective/src/injective_public_key.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_cosmos_sdk::proto::injective; +use tw_cosmos_sdk::public_key::secp256k1::Secp256PublicKey; +use tw_cosmos_sdk::public_key::{ + CosmosPublicKey, JsonPublicKey, ProtobufPublicKey, PublicKeyParams, +}; +use tw_keypair::KeyPairResult; +use tw_memory::Data; +use tw_proto::{google, type_url}; + +/// https://github.com/cosmostation/cosmostation-chrome-extension/blob/e2fd27d71a17993f8eef07ce30f7a04a32e52788/src/constants/cosmos.ts#L4 +const INJECTIVE_SECP256K1_PUBLIC_KEY_TYPE: &str = "injective/PubKeyEthSecp256k1"; + +pub struct InjectiveEthSecp256PublicKey(Secp256PublicKey); + +impl InjectiveEthSecp256PublicKey { + fn default_public_key_params(coin: &dyn CoinContext) -> PublicKeyParams { + PublicKeyParams { + // `NativeInjective` uses the same public key type as specified in `registry.json`. + public_key_type: coin.public_key_type(), + json_type: INJECTIVE_SECP256K1_PUBLIC_KEY_TYPE.to_string(), + protobuf_type_url: type_url::(), + } + } +} + +impl CosmosPublicKey for InjectiveEthSecp256PublicKey { + fn from_bytes( + coin: &dyn CoinContext, + public_key_bytes: &[u8], + maybe_params: Option, + ) -> KeyPairResult { + // Use default Ethermint public key parameters if otherwise is not specified, + // however the uncompressed public key is used to generate an address. + let params = maybe_params.unwrap_or_else(|| Self::default_public_key_params(coin)); + Secp256PublicKey::from_bytes(coin, public_key_bytes, Some(params)) + .map(InjectiveEthSecp256PublicKey) + } + + fn to_bytes(&self) -> Data { + self.0.to_bytes() + } +} + +impl JsonPublicKey for InjectiveEthSecp256PublicKey { + fn public_key_type(&self) -> String { + self.0.public_key_type() + } +} + +impl ProtobufPublicKey for InjectiveEthSecp256PublicKey { + fn to_proto(&self) -> google::protobuf::Any { + self.0.to_proto() + } +} diff --git a/rust/chains/tw_native_injective/src/lib.rs b/rust/chains/tw_native_injective/src/lib.rs new file mode 100644 index 00000000000..fb550eba23b --- /dev/null +++ b/rust/chains/tw_native_injective/src/lib.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod context; +pub mod entry; +pub mod injective_public_key; diff --git a/rust/chains/tw_ronin/Cargo.toml b/rust/chains/tw_ronin/Cargo.toml new file mode 100644 index 00000000000..fadcf48ebab --- /dev/null +++ b/rust/chains/tw_ronin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_ronin" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_evm = { path = "../../tw_evm" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/chains/tw_ronin/src/address.rs b/rust/chains/tw_ronin/src/address.rs new file mode 100644 index 00000000000..73bdaf91624 --- /dev/null +++ b/rust/chains/tw_ronin/src/address.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_evm::address::{Address as EthAddress, EvmAddress}; +use tw_keypair::ecdsa::secp256k1; +use tw_memory::Data; + +/// cbindgen:ignore +const RONIN_PREFIX: &str = "ronin:"; + +#[derive(Debug)] +pub struct Address(EthAddress); + +impl Address { + /// Initializes an address with a `secp256k1` public key. + #[inline] + pub fn with_secp256k1_pubkey(pubkey: &secp256k1::PublicKey) -> Address { + Address(EthAddress::with_secp256k1_pubkey(pubkey)) + } +} + +impl EvmAddress for Address {} + +impl From

for EthAddress { + #[inline] + fn from(addr: Address) -> Self { + addr.0 + } +} + +impl FromStr for Address { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + // Accept both Ronin and standard addresses. + let standard = match s.strip_prefix(RONIN_PREFIX) { + Some(ronin_no_prefix) => format!("0x{ronin_no_prefix}"), + None => s.to_string(), + }; + EthAddress::from_str(&standard).map(Address) + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let standard_prefixed = self.0.to_string(); + // Strip the `0x` prefix. + let standard_no_prefix = standard_prefixed + .strip_prefix("0x") + .unwrap_or(&standard_prefixed); + write!(f, "{RONIN_PREFIX}{standard_no_prefix}") + } +} + +impl CoinAddress for Address { + #[inline] + fn data(&self) -> Data { + self.0.data() + } +} diff --git a/rust/chains/tw_ronin/src/entry.rs b/rust/chains/tw_ronin/src/entry.rs new file mode 100644 index 00000000000..49a21c71c0c --- /dev/null +++ b/rust/chains/tw_ronin/src/entry.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::Address; +use crate::ronin_context::RoninContext; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_evm::evm_entry::EvmEntry; +use tw_evm::modules::compiler::Compiler; +use tw_evm::modules::message_signer::EthMessageSigner; +use tw_evm::modules::signer::Signer; +use tw_keypair::tw::PublicKey; +use tw_proto::Ethereum::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct RoninEntry; + +impl CoinEntry for RoninEntry { + type AddressPrefix = NoPrefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = EthMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + Ok(Address::with_secp256k1_pubkey(public_key)) + } + + #[inline] + fn sign(&self, _coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + Signer::::sign_proto(input) + } + + #[inline] + fn preimage_hashes( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + Compiler::::preimage_hashes(input) + } + + #[inline] + fn compile( + &self, + _coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + Compiler::::compile(input, signatures, public_keys) + } + + #[inline] + fn message_signer(&self) -> Option { + Some(EthMessageSigner) + } +} + +impl EvmEntry for RoninEntry { + type Context = RoninContext; +} diff --git a/rust/chains/tw_ronin/src/lib.rs b/rust/chains/tw_ronin/src/lib.rs new file mode 100644 index 00000000000..615f3566671 --- /dev/null +++ b/rust/chains/tw_ronin/src/lib.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod entry; +pub mod ronin_context; diff --git a/rust/chains/tw_ronin/src/ronin_context.rs b/rust/chains/tw_ronin/src/ronin_context.rs new file mode 100644 index 00000000000..e30475a9f1f --- /dev/null +++ b/rust/chains/tw_ronin/src/ronin_context.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::Address; +use tw_evm::evm_context::EvmContext; + +#[derive(Default)] +pub struct RoninContext; + +impl EvmContext for RoninContext { + type Address = Address; +} diff --git a/rust/chains/tw_ronin/tests/address.rs b/rust/chains/tw_ronin/tests/address.rs new file mode 100644 index 00000000000..4a97ee560cc --- /dev/null +++ b/rust/chains/tw_ronin/tests/address.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_ronin::address::Address; + +#[test] +fn test_ronin_address_valid() { + let normalized = "ronin:EC49280228b0D05Aa8e8b756503254e1eE7835ab"; + let valid = [ + normalized, + "ronin:ec49280228b0d05aa8e8b756503254e1ee7835ab", + "0xEC49280228b0D05Aa8e8b756503254e1eE7835ab", + "ronin:0xEC49280228b0D05Aa8e8b756503254e1eE7835ab", + ]; + + for test in valid { + let addr = Address::from_str(test).unwrap(); + assert_eq!(addr.to_string(), normalized); + } +} + +#[test] +fn test_ronin_address_invalid() { + let invalid = [ + "EC49280228b0D05Aa8e8b756503254e1eE7835ab", // no prefix + "ec49280228b0d05aa8e8b756503254e1ee7835ab", // no prefix + "roni:EC49280228b0D05Aa8e8b756503254e1eE7835ab", // wrong prefix + "ronin=EC49280228b0D05Aa8e8b756503254e1eE7835ab", // wrong prefix + "0xronin:EC49280228b0D05Aa8e8b756503254e1eE7835ab", // wrong prefix + "EC49280228b0D05Aa8e8b756503254e1eE7835", // too short + "ronin:EC49280228b0D05Aa8e8b756503254e1eE7835", // too short + "ronin:ec49280228b0d05aa8e8b756503254e1ee7835", // too short + "", // empty + ]; + + for test in invalid { + Address::from_str(test).unwrap_err(); + } +} diff --git a/rust/chains/tw_ronin/tests/compiler.rs b/rust/chains/tw_ronin/tests/compiler.rs new file mode 100644 index 00000000000..18d55a3fd92 --- /dev/null +++ b/rust/chains/tw_ronin/tests/compiler.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_coin_entry::coin_entry_ext::CoinEntryExt; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_keypair::ecdsa::secp256k1; +use tw_keypair::tw; +use tw_number::U256; +use tw_proto::Ethereum::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_proto::{deserialize, serialize}; +use tw_ronin::entry::RoninEntry; + +#[test] +fn test_ronin_preimage_hashes_and_compile() { + let coin = TestCoinContext::default(); + + let transfer = Proto::mod_Transaction::Transfer { + amount: U256::encode_be_compact(1_000_000_000_000_000_000), + data: Cow::default(), + }; + let input = Proto::SigningInput { + nonce: U256::encode_be_compact(11), + chain_id: U256::encode_be_compact(1), + gas_price: U256::encode_be_compact(20_000_000_000), + gas_limit: U256::encode_be_compact(21_000), + to_address: "ronin:3535353535353535353535353535353535353535".into(), + transaction: Some(Proto::Transaction { + transaction_oneof: Proto::mod_Transaction::OneOftransaction_oneof::transfer(transfer), + }), + ..Proto::SigningInput::default() + }; + + let input_data = serialize(&input).unwrap(); + + let res = RoninEntry + .preimage_hashes(&coin, &input_data) + .expect("!preimage_hashes"); + let preimage: CompilerProto::PreSigningOutput = + deserialize(res.as_slice()).expect("Coin entry returned an invalid output"); + + assert_eq!(preimage.error, SigningErrorType::OK); + assert!(preimage.error_message.is_empty()); + assert_eq!( + preimage.data_hash.to_hex(), + "15e180a6274b2f6a572b9b51823fce25ef39576d10188ecdcd7de44526c47217" + ); + + // Simulate signature, normally obtained from signature server + let public_key = secp256k1::PublicKey::try_from("044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a").unwrap(); + let public_key = tw::PublicKey::Secp256k1Extended(public_key); + let signature = "360a84fb41ad07f07c845fedc34cde728421803ebbaae392fc39c116b29fc07b53bd9d1376e15a191d844db458893b928f3efbfee90c9febf51ab84c9796677900".decode_hex().unwrap(); + + // Verify signature (pubkey & hash & signature) + assert!(public_key.verify(&signature, &preimage.data_hash)); + + // Step 3: Compile transaction info + let output_data = RoninEntry + .compile( + &coin, + &input_data, + vec![signature], + vec![public_key.to_bytes()], + ) + .expect("!compile"); + let output: Proto::SigningOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + let expected_encoded = "f86c0b8504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a0360a84fb41ad07f07c845fedc34cde728421803ebbaae392fc39c116b29fc07ba053bd9d1376e15a191d844db458893b928f3efbfee90c9febf51ab84c97966779"; + assert_eq!(output.encoded.to_hex(), expected_encoded); +} diff --git a/rust/chains/tw_ronin/tests/rlp.rs b/rust/chains/tw_ronin/tests/rlp.rs new file mode 100644 index 00000000000..d3d9548fd7b --- /dev/null +++ b/rust/chains/tw_ronin/tests/rlp.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_coin_entry::error::prelude::*; +use tw_encoding::hex::ToHex; +use tw_evm::evm_entry::EvmEntryExt; +use tw_proto::EthereumRlp::Proto as RlpProto; +use tw_proto::{deserialize, serialize}; +use tw_ronin::entry::RoninEntry; +use RlpProto::mod_RlpItem::OneOfitem as Item; + +#[test] +fn test_rlp_encode_ronin_address() { + let ronin_addr = "ronin:6b175474e89094c44da98b954eedeac495271d0f"; + let input = RlpProto::EncodingInput { + item: Some(RlpProto::RlpItem { + item: Item::address(Cow::from(ronin_addr)), + }), + }; + let input_data = serialize(&input).unwrap(); + let output_data = RoninEntry.encode_rlp(&input_data).unwrap(); + let output: RlpProto::EncodingOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!( + output.encoded.to_hex(), + "946b175474e89094c44da98b954eedeac495271d0f" + ); +} diff --git a/rust/chains/tw_ronin/tests/signer.rs b/rust/chains/tw_ronin/tests/signer.rs new file mode 100644 index 00000000000..309592e9278 --- /dev/null +++ b/rust/chains/tw_ronin/tests/signer.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_coin_entry::coin_entry_ext::CoinEntryExt; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_number::U256; +use tw_proto::Ethereum::Proto; +use tw_proto::{deserialize, serialize}; +use tw_ronin::entry::RoninEntry; + +/// https://explorer.roninchain.com/tx/0xf13a2c4421700f8782ca73eaf16bb8baf82bcf093e23570a1ff062cdd8dbf6c3 +#[test] +fn test_ronin_signing() { + let coin = TestCoinContext::default(); + + let private = "0x4646464646464646464646464646464646464646464646464646464646464646" + .decode_hex() + .unwrap(); + + let transfer = Proto::mod_Transaction::Transfer { + amount: U256::encode_be_compact(276_447), + data: Cow::default(), + }; + + let input = Proto::SigningInput { + chain_id: U256::encode_be_compact(2020), + nonce: U256::encode_be_compact(0), + gas_price: U256::encode_be_compact(1_000_000_000), + gas_limit: U256::encode_be_compact(21_000), + to_address: "ronin:c36edf48e21cf395b206352a1819de658fd7f988".into(), + transaction: Some(Proto::Transaction { + transaction_oneof: Proto::mod_Transaction::OneOftransaction_oneof::transfer(transfer), + }), + private_key: private.into(), + ..Proto::SigningInput::default() + }; + let input_data = serialize(&input).unwrap(); + + let output_data = RoninEntry.sign(&coin, &input_data).expect("!sign"); + let output: Proto::SigningOutput = + deserialize(&output_data).expect("Coin entry returned an invalid output"); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + + let expected = "f86880843b9aca0082520894c36edf48e21cf395b206352a1819de658fd7f988830437df80820feca0442aa06b0d0465bfecf84b28e2ce614a32a1ccc12735dc03a5799517d6659d7aa004e1bf2efa30743f1b6d49dbec2671e9fb5ead1e7da15e352ca1df6fb86a8ba7"; + assert_eq!(output.encoded.to_hex(), expected); +} + +#[test] +fn test_sign_json() { + let coin = TestCoinContext::default(); + + let input_json = r#"{"chainId":"B+Q=","nonce":"AA==","gasPrice":"O5rKAA==","gasLimit":"Ugg=","toAddress":"ronin:c36edf48e21cf395b206352a1819de658fd7f988","privateKey":"RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkY=","transaction":{"transfer":{"amount":"BDff"}}}"#; + let private_key = "0x4646464646464646464646464646464646464646464646464646464646464646" + .decode_hex() + .unwrap(); + + RoninEntry + .sign_json(&coin, input_json, private_key) + .expect_err("'EthEntry::sign_json' is not supported yet"); + + // Expected result - "f86880843b9aca0082520894c36edf48e21cf395b206352a1819de658fd7f988830437df80820feca0442aa06b0d0465bfecf84b28e2ce614a32a1ccc12735dc03a5799517d6659d7aa004e1bf2efa30743f1b6d49dbec2671e9fb5ead1e7da15e352ca1df6fb86a8ba7" +} diff --git a/rust/chains/tw_solana/Cargo.toml b/rust/chains/tw_solana/Cargo.toml new file mode 100644 index 00000000000..99c893b081e --- /dev/null +++ b/rust/chains/tw_solana/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tw_solana" +version = "0.1.0" +edition = "2021" + +[dependencies] +bincode = "1.3.3" +borsh = { version = "1.3.1", features = ["derive"] } +lazy_static = "1.4.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_solana/src/address.rs b/rust/chains/tw_solana/src/address.rs new file mode 100644 index 00000000000..cb2051be84c --- /dev/null +++ b/rust/chains/tw_solana/src/address.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::SOLANA_ALPHABET; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base58; +use tw_hash::{as_byte_sequence, sha2, H256}; +use tw_keypair::{ed25519, tw}; +use tw_memory::Data; + +pub const MAX_SEEDS: usize = 16; +pub const MAX_SEED_LEN: usize = H256::LEN; +const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress"; + +#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct SolanaAddress { + bytes: H256, +} + +impl SolanaAddress { + pub fn with_public_key(public_key: &tw::PublicKey) -> AddressResult { + let bytes = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .to_bytes(); + Ok(SolanaAddress { bytes }) + } + + pub fn with_public_key_ed25519(public_key: &ed25519::sha512::PublicKey) -> SolanaAddress { + SolanaAddress { + bytes: public_key.to_bytes(), + } + } + + pub fn with_public_key_bytes(bytes: H256) -> SolanaAddress { + SolanaAddress { bytes } + } + + pub fn bytes(&self) -> H256 { + self.bytes + } + + /// Find a valid [program derived address][pda] and its corresponding bump seed. + /// + /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses + /// + /// Program derived addresses (PDAs) are account keys that only the program, + /// `program_id`, has the authority to sign. The address is of the same form + /// as a Solana `Pubkey`, except they are ensured to not be on the ed25519 + /// curve and thus have no associated private key. When performing + /// cross-program invocations the program can "sign" for the key by calling + /// [`invoke_signed`] and passing the same seeds used to generate the + /// address, along with the calculated _bump seed_, which this function + /// returns as the second tuple element. The runtime will verify that the + /// program associated with this address is the caller and thus authorized + /// to be the signer. + pub fn find_program_address( + seeds: &[&[u8]], + program_id: SolanaAddress, + ) -> Option { + let mut bump_seed = [u8::MAX]; + for _ in 0..u8::MAX { + let mut seeds_with_bump = seeds.to_vec(); + seeds_with_bump.push(&bump_seed); + match Self::create_program_address(&seeds_with_bump, program_id) { + Ok(Some(address)) => return Some(address), + // Try to re-compute the program address with a different seed. + Ok(None) => (), + Err(_) => return None, + } + // Try to re-compute the program address with a different seed. + bump_seed[0] -= 1; + } + None + } + + /// Create a valid [program derived address][pda] without searching for a bump seed. + /// + /// [pda]: https://solana.com/docs/core/cpi#program-derived-addresses + /// + /// Because this function does not create a bump seed, it may unpredictably + /// return an error for any given set of seeds and is not generally suitable + /// for creating program derived addresses. + /// + /// However, it can be used for efficiently verifying that a set of seeds plus + /// bump seed generated by [`find_program_address`] derives a particular + /// address as expected. See the example for details. + /// + /// See the documentation for [`find_program_address`] for a full description + /// of program derived addresses and bump seeds. + /// + /// [`find_program_address`]: Pubkey::find_program_address + pub fn create_program_address( + seeds: &[&[u8]], + program_id: SolanaAddress, + ) -> AddressResult> { + if seeds.len() > MAX_SEEDS { + return Err(AddressError::Internal); + } + if seeds.iter().any(|seed| seed.len() > MAX_SEED_LEN) { + return Err(AddressError::Internal); + } + + let mut data_to_hash = Vec::new(); + + // concatenate seeds + for seed in seeds { + data_to_hash.extend_from_slice(seed); + } + // Append `program_id`. + data_to_hash.extend_from_slice(program_id.bytes.as_slice()); + data_to_hash.extend_from_slice(PDA_MARKER); + + let hash = H256::try_from(sha2::sha256(&data_to_hash).as_slice()) + .expect("sha256 must return 32 bytes"); + + // The given hash (aka new public key) must not be on the ed25519 elliptic curve. + match ed25519::sha512::PublicKey::try_from(hash.as_slice()) { + Ok(_) => Ok(None), + Err(_) => Ok(Some(SolanaAddress::with_public_key_bytes(hash))), + } + } +} + +impl CoinAddress for SolanaAddress { + #[inline] + fn data(&self) -> Data { + self.bytes.to_vec() + } +} + +impl FromStr for SolanaAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let bytes = + base58::decode(s, SOLANA_ALPHABET).map_err(|_| AddressError::FromBase58Error)?; + let bytes = H256::try_from(bytes.as_slice()).map_err(|_| AddressError::InvalidInput)?; + Ok(SolanaAddress { bytes }) + } +} + +impl From<&'static str> for SolanaAddress { + fn from(s: &'static str) -> Self { + SolanaAddress::from_str(s).unwrap() + } +} + +impl fmt::Debug for SolanaAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl fmt::Display for SolanaAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let encoded = base58::encode(self.bytes.as_slice(), SOLANA_ALPHABET); + write!(f, "{}", encoded) + } +} + +impl Serialize for SolanaAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + return self.to_string().serialize(serializer); + } + as_byte_sequence::serialize(&self.bytes(), serializer) + } +} + +impl<'de> Deserialize<'de> for SolanaAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + let addr_str = String::deserialize(deserializer)?; + return SolanaAddress::from_str(&addr_str) + .map_err(|e| DeError::custom(format!("{e:?}"))); + } + let bytes = as_byte_sequence::deserialize(deserializer)?; + Ok(SolanaAddress { bytes }) + } +} diff --git a/rust/chains/tw_solana/src/blockhash.rs b/rust/chains/tw_solana/src/blockhash.rs new file mode 100644 index 00000000000..1cfe0a92fa5 --- /dev/null +++ b/rust/chains/tw_solana/src/blockhash.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::SOLANA_ALPHABET; +use serde::Deserialize; +use std::fmt; +use std::str::FromStr; +use tw_encoding::base58::{self, as_base58_bitcoin}; +use tw_encoding::EncodingError; +use tw_hash::H256; + +#[derive(Clone, Copy, Default, Deserialize)] +pub struct Blockhash(#[serde(with = "as_base58_bitcoin")] H256); + +impl Blockhash { + pub fn with_bytes(bytes: H256) -> Blockhash { + Blockhash(bytes) + } + + pub fn to_bytes(&self) -> H256 { + self.0 + } +} + +impl fmt::Display for Blockhash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", base58::encode(self.0.as_slice(), SOLANA_ALPHABET)) + } +} + +impl FromStr for Blockhash { + type Err = EncodingError; + + fn from_str(s: &str) -> Result { + let bytes = base58::decode(s, SOLANA_ALPHABET)?; + let bytes = H256::try_from(bytes.as_slice()).map_err(|_| EncodingError::InvalidInput)?; + Ok(Blockhash(bytes)) + } +} diff --git a/rust/chains/tw_solana/src/compiler.rs b/rust/chains/tw_solana/src/compiler.rs new file mode 100644 index 00000000000..818f04a8ce9 --- /dev/null +++ b/rust/chains/tw_solana/src/compiler.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::modules::message_builder::MessageBuilder; +use crate::modules::proto_builder::ProtoBuilder; +use crate::modules::tx_signer::TxSigner; +use crate::SOLANA_ALPHABET; +use std::borrow::Cow; +use std::collections::HashMap; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; +use tw_keypair::ed25519; +use tw_keypair::traits::VerifyingKeyTrait; +use tw_proto::Solana::Proto; + +pub struct SolanaCompiler; + +impl SolanaCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let builder = MessageBuilder::new(input); + let unsigned_msg = builder.build()?; + let data_to_sign = TxSigner::preimage_versioned(&unsigned_msg)?; + + let signers: Vec<_> = unsigned_msg + .signers() + .map(|addr| Cow::from(addr.to_string().into_bytes())) + .collect(); + + Ok(Proto::PreSigningOutput { + signers, + data: Cow::from(data_to_sign), + ..Proto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let encode = move |data| match input.tx_encoding { + Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), + Proto::Encoding::Base64 => base64::encode(data, STANDARD), + }; + + if signatures.len() != public_keys.len() { + return SigningError::err(SigningErrorType::Error_signatures_count) + .context("Expected the same number of signatures and public keys"); + } + + let builder = MessageBuilder::new(input); + let unsigned_msg = builder.build()?; + let data_to_sign = TxSigner::preimage_versioned(&unsigned_msg)?; + + // Verify the given signatures and collect the key-signature map. + let mut key_signs = HashMap::default(); + for (sign, pubkey) in signatures.iter().zip(public_keys.iter()) { + let signature = ed25519::Signature::try_from(sign.as_slice())?; + let pubkey = ed25519::sha512::PublicKey::try_from(pubkey.as_slice())?; + + if !pubkey.verify(signature.clone(), data_to_sign.clone()) + && !signature.to_bytes().is_zero() + { + return SigningError::err(SigningErrorType::Error_signing) + .context("Error verifying the given signature"); + } + + key_signs.insert(SolanaAddress::with_public_key_ed25519(&pubkey), signature); + } + + let signed_tx = TxSigner::compile_versioned(unsigned_msg, key_signs)?; + + let signed_encoded = bincode::serialize(&signed_tx) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing signed transaction")?; + let signed_encoded = encode(&signed_encoded); + let unsigned_encoded = encode(&data_to_sign); + + Ok(Proto::SigningOutput { + encoded: Cow::from(signed_encoded), + unsigned_tx: Cow::from(unsigned_encoded), + signatures: ProtoBuilder::build_signatures(&signed_tx), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/defined_addresses.rs b/rust/chains/tw_solana/src/defined_addresses.rs new file mode 100644 index 00000000000..f7e663ef1f4 --- /dev/null +++ b/rust/chains/tw_solana/src/defined_addresses.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use lazy_static::lazy_static; + +macro_rules! define { + ($name:ident = $s:literal) => { + lazy_static! { + pub static ref $name: SolanaAddress = SolanaAddress::from($s); + } + }; +} + +define!(SYSTEM_PROGRAM_ID_ADDRESS = "11111111111111111111111111111111"); +define!(STAKE_PROGRAM_ID_ADDRESS = "Stake11111111111111111111111111111111111111"); +define!(TOKEN_PROGRAM_ID_ADDRESS = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +define!(TOKEN_2022_PROGRAM_ID_ADDRESS = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); +define!(ASSOCIATED_TOKEN_PROGRAM_ID_ADDRESS = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); +define!(SYSVAR_RENT_ID_ADDRESS = "SysvarRent111111111111111111111111111111111"); +define!(SYSVAR_CLOCK_ID_ADDRESS = "SysvarC1ock11111111111111111111111111111111"); +define!(STAKE_CONFIG_ID_ADDRESS = "StakeConfig11111111111111111111111111111111"); +define!(NULL_ID_ADDRESS = "11111111111111111111111111111111"); +define!(SYSVAR_STAKE_HISTORY_ID_ADDRESS = "SysvarStakeHistory1111111111111111111111111"); +define!(MEMO_PROGRAM_ID_ADDRESS = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); +define!(SYSVAR_RECENT_BLOCKHASHS_ADDRESS = "SysvarRecentB1ockHashes11111111111111111111"); +define!(COMPUTE_BUDGET_ADDRESS = "ComputeBudget111111111111111111111111111111"); diff --git a/rust/chains/tw_solana/src/entry.rs b/rust/chains/tw_solana/src/entry.rs new file mode 100644 index 00000000000..5fd330739fe --- /dev/null +++ b/rust/chains/tw_solana/src/entry.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::compiler::SolanaCompiler; +use crate::modules::transaction_decoder::SolanaTransactionDecoder; +use crate::modules::transaction_util::SolanaTransactionUtil; +use crate::modules::wallet_connect::connector::SolanaWalletConnector; +use crate::signer::SolanaSigner; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Solana::Proto; + +pub struct SolanaEntry; + +impl CoinEntry for SolanaEntry { + type AddressPrefix = NoPrefix; + type Address = SolanaAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = Proto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = SolanaWalletConnector; + type TransactionDecoder = SolanaTransactionDecoder; + type TransactionUtil = SolanaTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + SolanaAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + SolanaAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + SolanaAddress::with_public_key(&public_key) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + SolanaSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + SolanaCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + SolanaCompiler::compile(coin, input, signatures, public_keys) + } + + #[inline] + fn wallet_connector(&self) -> Option { + Some(SolanaWalletConnector) + } + + #[inline] + fn transaction_decoder(&self) -> Option { + Some(SolanaTransactionDecoder) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(SolanaTransactionUtil) + } +} diff --git a/rust/chains/tw_solana/src/instruction.rs b/rust/chains/tw_solana/src/instruction.rs new file mode 100644 index 00000000000..fee1928163b --- /dev/null +++ b/rust/chains/tw_solana/src/instruction.rs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use borsh::BorshSerialize; +use serde::{Deserialize, Serialize}; +use tw_memory::Data; + +pub struct Instruction { + /// Pubkey of the program that executes this instruction. + pub program_id: SolanaAddress, + /// Metadata describing accounts that should be passed to the program. + pub accounts: Vec, + /// Opaque data passed to the program for its own interpretation. + pub data: Data, +} + +impl Instruction { + /// Create a new instruction from a value, encoded with [`bincode`]. + /// + /// [`bincode`]: https://docs.rs/bincode/latest/bincode/ + /// + /// `program_id` is the address of the program that will execute the instruction. + /// `accounts` contains a description of all accounts that may be accessed by the program. + pub fn new_with_bincode( + program_id: SolanaAddress, + data: T, + accounts: Vec, + ) -> Self { + let data = bincode::serialize(&data).expect("Error serializing bincode"); + Self { + program_id, + accounts, + data, + } + } + + /// Create a new instruction from a value, encoded with [`borsh`]. + /// + /// [`borsh`]: https://docs.rs/borsh/latest/borsh/ + /// + /// `program_id` is the address of the program that will execute the instruction. + /// `accounts` contains a description of all accounts that may be accessed by the program. + pub fn new_with_borsh( + program_id: SolanaAddress, + data: &T, + accounts: Vec, + ) -> Self { + let data = borsh::to_vec(data).expect("Error serializing borsh"); + Self { + program_id, + accounts, + data, + } + } + + pub fn new(program_id: SolanaAddress, data: Data, accounts: Vec) -> Self { + Self { + program_id, + accounts, + data, + } + } + + pub fn with_references>(mut self, references: I) -> Self { + for reference in references { + self.accounts.push(AccountMeta::readonly(reference, false)); + } + self + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct AccountMeta { + /// An account's public key. + pub pubkey: SolanaAddress, + /// True if an `Instruction` requires a `Transaction` signature matching `pubkey`. + pub is_signer: bool, + /// True if the account data or metadata may be mutated during program execution. + pub is_writable: bool, +} + +impl AccountMeta { + /// Construct metadata for a writable account. + pub fn new(pubkey: SolanaAddress, is_signer: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable: true, + } + } + + /// Construct metadata for a read-only account. + pub fn readonly(pubkey: SolanaAddress, is_signer: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable: false, + } + } +} diff --git a/rust/chains/tw_solana/src/lib.rs b/rust/chains/tw_solana/src/lib.rs new file mode 100644 index 00000000000..5bf3b779386 --- /dev/null +++ b/rust/chains/tw_solana/src/lib.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::base58::Alphabet; + +pub mod address; +pub mod blockhash; +pub mod compiler; +pub mod defined_addresses; +pub mod entry; +pub mod instruction; +pub mod modules; +pub mod program; +pub mod signer; +pub mod transaction; + +// cbindgen:ignore +pub const SOLANA_ALPHABET: Alphabet = Alphabet::Bitcoin; diff --git a/rust/chains/tw_solana/src/modules/compiled_instructions.rs b/rust/chains/tw_solana/src/modules/compiled_instructions.rs new file mode 100644 index 00000000000..28b0f30cd6b --- /dev/null +++ b/rust/chains/tw_solana/src/modules/compiled_instructions.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::instruction::Instruction; +use crate::transaction::CompiledInstruction; +use tw_coin_entry::error::prelude::*; + +pub fn compile_instructions( + ixs: &[Instruction], + keys: &[SolanaAddress], +) -> SigningResult> { + ixs.iter().map(|ix| compile_instruction(ix, keys)).collect() +} + +fn position(keys: &[SolanaAddress], key: &SolanaAddress) -> SigningResult { + keys.iter() + .position(|k| k == key) + .map(|k| k as u8) + .or_tw_err(SigningErrorType::Error_internal) +} + +/// https://github.com/solana-labs/solana/blob/4b65cc8eef6ef79cb9b9cbc534a99b4900e58cf7/sdk/program/src/message/legacy.rs#L72-L84 +pub(crate) fn compile_instruction( + ix: &Instruction, + keys: &[SolanaAddress], +) -> SigningResult { + let accounts = ix + .accounts + .iter() + .map(|account_meta| position(keys, &account_meta.pubkey)) + .collect::>>() + .context("Cannot build account metas")?; + + Ok(CompiledInstruction { + program_id_index: position(keys, &ix.program_id) + .context("Program ID account is not provided")?, + data: ix.data.clone(), + accounts, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::instruction::AccountMeta; + use crate::SOLANA_ALPHABET; + use std::str::FromStr; + use tw_encoding::base58; + use tw_keypair::ed25519; + + #[test] + fn test_compile_instruction() { + let public_0 = base58::decode( + "GymAh18wHuFTytfSJWi8eYTA9x5S3sNb9CJSGBWoPRE3", + SOLANA_ALPHABET, + ) + .unwrap(); + let public_0 = ed25519::sha512::PublicKey::try_from(public_0.as_slice()).unwrap(); + let address_0 = SolanaAddress::with_public_key_ed25519(&public_0); + + let public_1 = base58::decode( + "2oKoYSAHgveX91917v4DUEuN8BNKXDg8KJWpaGyEay9V", + SOLANA_ALPHABET, + ) + .unwrap(); + let public_1 = ed25519::sha512::PublicKey::try_from(public_1.as_slice()).unwrap(); + let address_1 = SolanaAddress::with_public_key_ed25519(&public_1); + + let program_id = SolanaAddress::from_str("11111111111111111111111111111111").unwrap(); + + let addresses = vec![address_0, address_1, program_id]; + let ixs_addresses = vec![ + AccountMeta::new(address_1, false), + AccountMeta::new(address_0, false), + AccountMeta::new(program_id, false), + AccountMeta::new(address_1, false), + AccountMeta::new(address_0, false), + ]; + let data = vec![0_u8, 1, 2, 4]; + let instruction = Instruction::new(program_id, data.clone(), ixs_addresses); + + let compiled_ix = compile_instruction(&instruction, &addresses).unwrap(); + let expected = CompiledInstruction { + program_id_index: 2, + accounts: vec![1, 0, 2, 1, 0], + data, + }; + assert_eq!(compiled_ix, expected); + } +} diff --git a/rust/chains/tw_solana/src/modules/compiled_keys.rs b/rust/chains/tw_solana/src/modules/compiled_keys.rs new file mode 100644 index 00000000000..e138c1d9b29 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/compiled_keys.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/solana-labs/solana/blob/4b65cc8eef6ef79cb9b9cbc534a99b4900e58cf7/sdk/program/src/message/compiled_keys.rs + +use crate::address::SolanaAddress; +use crate::instruction::Instruction; +use crate::transaction::MessageHeader; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use tw_coin_entry::error::prelude::*; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +struct CompiledKeyMeta { + is_signer: bool, + is_writable: bool, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct CompiledKeys { + ordered_keys: Vec, + key_meta_map: HashMap, +} + +impl CompiledKeys { + pub fn with_fee_payer(fee_payer: SolanaAddress) -> Self { + let mut selfi = Self::default(); + + selfi.key_meta_map.insert( + fee_payer, + CompiledKeyMeta { + is_signer: true, + is_writable: true, + }, + ); + // Fee payer must be the first account in the keys list. + selfi.ordered_keys.push(fee_payer); + + selfi + } + + pub fn compile(mut self, instructions: &[Instruction]) -> Self { + for ix in instructions { + for account_meta in &ix.accounts { + let meta_entry = self.key_meta_map.entry(account_meta.pubkey); + if matches!(meta_entry, Entry::Vacant(_)) { + self.ordered_keys.push(account_meta.pubkey); + } + + let meta = meta_entry.or_default(); + meta.is_signer |= account_meta.is_signer; + meta.is_writable |= account_meta.is_writable; + } + } + + // add programIds (read-only, at end) + for ix in instructions { + let meta_entry = self.key_meta_map.entry(ix.program_id); + if matches!(meta_entry, Entry::Vacant(_)) { + self.ordered_keys.push(ix.program_id); + } + meta_entry.or_default(); + } + + self + } + + pub fn try_into_message_components(self) -> SigningResult<(MessageHeader, Vec)> { + let try_into_u8 = |num: usize| -> SigningResult { + u8::try_from(num).tw_err(|_| SigningErrorType::Error_tx_too_big) + }; + + let Self { + ordered_keys, + key_meta_map, + } = self; + + let filter = |account, is_signer: bool, is_writable: bool| -> Option { + let meta = key_meta_map.get(account).copied().unwrap_or_default(); + (meta.is_signer == is_signer && meta.is_writable == is_writable).then_some(*account) + }; + + let writable_signer_keys: Vec = ordered_keys + .iter() + .filter_map(|key| filter(key, true, true)) + .collect(); + let readonly_signer_keys: Vec = ordered_keys + .iter() + .filter_map(|key| filter(key, true, false)) + .collect(); + let writable_non_signer_keys: Vec = ordered_keys + .iter() + .filter_map(|key| filter(key, false, true)) + .collect(); + let readonly_non_signer_keys: Vec = ordered_keys + .iter() + .filter_map(|key| filter(key, false, false)) + .collect(); + + let signers_len = writable_signer_keys + .len() + .saturating_add(readonly_signer_keys.len()); + + let header = MessageHeader { + num_required_signatures: try_into_u8(signers_len) + .context("Too many signatures required")?, + num_readonly_signed_accounts: try_into_u8(readonly_signer_keys.len()) + .context("Too many accounts in the transaction")?, + num_readonly_unsigned_accounts: try_into_u8(readonly_non_signer_keys.len()) + .context("Too many accounts in the transaction")?, + }; + + let static_account_keys: Vec<_> = std::iter::empty() + .chain(writable_signer_keys) + .chain(readonly_signer_keys) + .chain(writable_non_signer_keys) + .chain(readonly_non_signer_keys) + .collect(); + + Ok((header, static_account_keys)) + } +} diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/compute_budget_instruction.rs b/rust/chains/tw_solana/src/modules/instruction_builder/compute_budget_instruction.rs new file mode 100644 index 00000000000..a95218e4365 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/instruction_builder/compute_budget_instruction.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::defined_addresses::COMPUTE_BUDGET_ADDRESS; +use crate::instruction::Instruction; +use borsh::{BorshDeserialize, BorshSerialize}; + +pub type UnitLimit = u32; +pub type UnitPrice = u64; + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum ComputeBudgetInstruction { + Unused, + RequestHeapFrame(u32), + SetComputeUnitLimit(UnitLimit), + SetComputeUnitPrice(UnitPrice), + SetLoadedAccountsDataSizeLimit(u32), +} + +pub struct ComputeBudgetInstructionBuilder; + +impl ComputeBudgetInstructionBuilder { + /// Create a `ComputeBudgetInstruction::SetComputeUnitLimit` `Instruction` + pub fn set_compute_unit_limit(units: UnitLimit) -> Instruction { + let account_metas = vec![]; + Instruction::new_with_borsh( + *COMPUTE_BUDGET_ADDRESS, + &ComputeBudgetInstruction::SetComputeUnitLimit(units), + account_metas, + ) + } + + /// Create a `ComputeBudgetInstruction::SetComputeUnitPrice` `Instruction` + pub fn set_compute_unit_price(micro_lamports: UnitPrice) -> Instruction { + let account_metas = vec![]; + Instruction::new_with_borsh( + *COMPUTE_BUDGET_ADDRESS, + &ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports), + account_metas, + ) + } +} diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs b/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs new file mode 100644 index 00000000000..9a4da04d11d --- /dev/null +++ b/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::instruction::Instruction; + +pub mod compute_budget_instruction; +pub mod stake_instruction; +pub mod system_instruction; +pub mod token_instruction; + +use compute_budget_instruction::ComputeBudgetInstructionBuilder; +use system_instruction::SystemInstructionBuilder; + +#[derive(Default)] +pub struct InstructionBuilder { + instructions: Vec, +} + +impl InstructionBuilder { + /// Adds an `advance_nonce` instruction if the nonce account provided. + pub fn maybe_advance_nonce( + &mut self, + nonce_account: Option, + authorized_account: SolanaAddress, + ) -> &mut Self { + if let Some(nonce_account) = nonce_account { + self.instructions + .push(SystemInstructionBuilder::advance_nonce_account( + nonce_account, + authorized_account, + )); + } + self + } + + /// Adds a `memo` instruction if it is not empty. + pub fn maybe_memo(&mut self, memo: &str) -> &mut Self { + if !memo.is_empty() { + self.instructions.push(SystemInstructionBuilder::memo(memo)); + } + self + } + + /// Adds a `priority_fee` instruction if it is provided. + pub fn maybe_priority_fee_price(&mut self, micro_lamports: Option) -> &mut Self { + if let Some(micro_lamports) = micro_lamports { + let ix = ComputeBudgetInstructionBuilder::set_compute_unit_price(micro_lamports); + self.instructions.push(ix); + } + self + } + + /// Adds a `fee_limit` instruction if it is provided. + pub fn maybe_priority_fee_limit(&mut self, units: Option) -> &mut Self { + if let Some(units) = units { + let ix = ComputeBudgetInstructionBuilder::set_compute_unit_limit(units); + self.instructions.push(ix); + } + self + } + + pub fn add_instruction(&mut self, instruction: Instruction) -> &mut Self { + self.instructions.push(instruction); + self + } + + pub fn add_instructions(&mut self, instructions: I) -> &mut Self + where + I: IntoIterator, + { + self.instructions.extend(instructions); + self + } + + pub fn output(self) -> Vec { + self.instructions + } +} diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/stake_instruction.rs b/rust/chains/tw_solana/src/modules/instruction_builder/stake_instruction.rs new file mode 100644 index 00000000000..ad02414ec72 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/instruction_builder/stake_instruction.rs @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::blockhash::Blockhash; +use crate::defined_addresses::*; +use crate::instruction::{AccountMeta, Instruction}; +use crate::modules::instruction_builder::system_instruction::SystemInstructionBuilder; +use crate::program::stake_program::StakeProgram; +use serde::{Deserialize, Serialize}; + +type UnixTimestamp = i64; +type Epoch = u64; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Authorized { + pub staker: SolanaAddress, + pub withdrawer: SolanaAddress, +} + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)] +pub struct Lockup { + /// UnixTimestamp at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub unix_timestamp: UnixTimestamp, + /// epoch height at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub epoch: Epoch, + /// custodian signature on a transaction exempts the operation from + /// lockup constraints + pub custodian: SolanaAddress, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum StakeAuthorize { + Staker, + Withdrawer, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct LockupArgs { + pub unix_timestamp: Option, + pub epoch: Option, + pub custodian: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum StakeInstruction { + /// Initialize a stake with lockup and authorization information + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized stake account + /// 1. `[]` Rent sysvar + /// + /// Authorized carries pubkeys that must sign staker transactions + /// and withdrawer transactions. + /// Lockup carries information about withdrawal restrictions + Initialize(Authorized, Lockup), + + /// Authorize a key to manage stake or withdrawal + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` The stake or withdraw authority + /// 3. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + Authorize(SolanaAddress, StakeAuthorize), + + /// Delegate a stake to a particular vote account + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account to be delegated + /// 1. `[]` Vote account to which this stake will be delegated + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[]` Address of config account that carries stake config + /// 5. `[SIGNER]` Stake authority + /// + /// The entire balance of the staking account is staked. DelegateStake + /// can be called multiple times, but re-delegation is delayed + /// by one epoch + DelegateStake, + + /// Split u64 tokens and stake off a stake account into another stake account. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be split; must be in the Initialized or Stake state + /// 1. `[WRITE]` Uninitialized stake account that will take the split-off amount + /// 2. `[SIGNER]` Stake authority + Split(u64), + + /// Withdraw unstaked lamports from the stake account + /// + /// # Account references + /// 0. `[WRITE]` Stake account from which to withdraw + /// 1. `[WRITE]` Recipient account + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[SIGNER]` Withdraw authority + /// 5. Optional: `[SIGNER]` Lockup authority, if before lockup expiration + /// + /// The u64 is the portion of the stake account balance to be withdrawn, + /// must be `<= StakeAccount.lamports - staked_lamports`. + Withdraw(u64), + + /// Deactivates the stake in the account + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` Stake authority + Deactivate, + + /// Set stake lockup + /// + /// If a lockup is not active, the withdraw authority may set a new lockup + /// If a lockup is active, the lockup custodian may update the lockup parameters + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account + /// 1. `[SIGNER]` Lockup authority or withdraw authority + SetLockup { + unix_timestamp: Option, + epoch: Option, + custodian: Option, + }, + + /// Merge two stake accounts. + /// + /// Both accounts must have identical lockup and authority keys. A merge + /// is possible between two stakes in the following states with no additional + /// conditions: + /// + /// * two deactivated stakes + /// * an inactive stake into an activating stake during its activation epoch + /// + /// For the following cases, the voter pubkey and vote credits observed must match: + /// + /// * two activated stakes + /// * two activating accounts that share an activation epoch, during the activation epoch + /// + /// All other combinations of stake states will fail to merge, including all + /// "transient" states, where a stake is activating or deactivating with a + /// non-zero effective stake. + /// + /// # Account references + /// 0. `[WRITE]` Destination stake account for the merge + /// 1. `[WRITE]` Source stake account for to merge. This account will be drained + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[SIGNER]` Stake authority + Merge, + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[SIGNER]` Base key of stake or withdraw authority + /// 2. `[]` Clock sysvar + /// 3. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeWithSeed { + new_authorized_pubkey: SolanaAddress, + stake_authorize: StakeAuthorize, + authority_seed: String, + authority_owner: SolanaAddress, + }, + + /// Initialize a stake with authorization information + /// + /// This instruction is similar to `Initialize` except that the withdraw authority + /// must be a signer, and no lockup is applied to the account. + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized stake account + /// 1. `[]` Rent sysvar + /// 2. `[]` The stake authority + /// 3. `[SIGNER]` The withdraw authority + /// + InitializeChecked, + + /// Authorize a key to manage stake or withdrawal + /// + /// This instruction behaves like `Authorize` with the additional requirement that the new + /// stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` The stake or withdraw authority + /// 3. `[SIGNER]` The new stake or withdraw authority + /// 4. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeChecked(StakeAuthorize), + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// This instruction behaves like `AuthorizeWithSeed` with the additional requirement that + /// the new stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[SIGNER]` Base key of stake or withdraw authority + /// 2. `[]` Clock sysvar + /// 3. `[SIGNER]` The new stake or withdraw authority + /// 4. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeCheckedWithSeed { + stake_authorize: StakeAuthorize, + authority_seed: String, + authority_owner: SolanaAddress, + }, + + /// Set stake lockup + /// + /// This instruction behaves like `SetLockup` with the additional requirement that + /// the new lockup authority also be a signer. + /// + /// If a lockup is not active, the withdraw authority may set a new lockup + /// If a lockup is active, the lockup custodian may update the lockup parameters + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account + /// 1. `[SIGNER]` Lockup authority or withdraw authority + /// 2. Optional: `[SIGNER]` New lockup authority + SetLockupChecked { + unix_timestamp: Option, + epoch: Option, + }, + + /// Get the minimum stake delegation, in lamports + /// + /// # Account references + /// None + /// + /// Returns the minimum delegation as a little-endian encoded u64 value. + /// Programs can use the [`get_minimum_delegation()`] helper function to invoke and + /// retrieve the return value for this instruction. + /// + /// [`get_minimum_delegation()`]: super::tools::get_minimum_delegation + GetMinimumDelegation, + + /// Deactivate stake delegated to a vote account that has been delinquent for at least + /// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs. + /// + /// No signer is required for this instruction as it is a common good to deactivate abandoned + /// stake. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account + /// 1. `[]` Delinquent vote account for the delegated stake account + /// 2. `[]` Reference vote account that has voted at least once in the last + /// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs + DeactivateDelinquent, + + /// Redelegate activated stake to another vote account. + /// + /// Upon success: + /// * the balance of the delegated stake account will be reduced to the undelegated amount in + /// the account (rent exempt minimum and any additional lamports not part of the delegation), + /// and scheduled for deactivation. + /// * the provided uninitialized stake account will receive the original balance of the + /// delegated stake account, minus the rent exempt minimum, and scheduled for activation to + /// the provided vote account. Any existing lamports in the uninitialized stake account + /// will also be included in the re-delegation. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account to be redelegated. The account must be fully + /// activated and carry a balance greater than or equal to the minimum delegation amount + /// plus rent exempt minimum + /// 1. `[WRITE]` Uninitialized stake account that will hold the redelegated stake + /// 2. `[]` Vote account to which this stake will be re-delegated + /// 3. `[]` Address of config account that carries stake config + /// 4. `[SIGNER]` Stake authority + /// + Redelegate, +} + +pub struct DepositStakeArgs { + pub sender: SolanaAddress, + pub validator: SolanaAddress, + pub stake_account: Option, + pub recent_blockhash: Blockhash, + pub lamports: u64, + pub space: u64, +} + +pub struct StakeInstructionBuilder; + +impl StakeInstructionBuilder { + /// Creates an Initialize Stake instruction. + pub fn stake_initialize( + stake_pubkey: SolanaAddress, + authorized: Authorized, + lockup: Lockup, + ) -> Instruction { + Instruction::new_with_bincode( + *STAKE_PROGRAM_ID_ADDRESS, + StakeInstruction::Initialize(authorized, lockup), + vec![ + AccountMeta::new(stake_pubkey, false), + AccountMeta::readonly(*SYSVAR_RENT_ID_ADDRESS, false), + ], + ) + } + + pub fn withdraw( + stake_pubkey: SolanaAddress, + withdrawer_pubkey: SolanaAddress, + to_pubkey: SolanaAddress, + lamports: u64, + custodian_pubkey: Option, + ) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(stake_pubkey, false), + AccountMeta::new(to_pubkey, false), + AccountMeta::readonly(*SYSVAR_CLOCK_ID_ADDRESS, false), + AccountMeta::readonly(*SYSVAR_STAKE_HISTORY_ID_ADDRESS, false), + AccountMeta::readonly(withdrawer_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::readonly(custodian_pubkey, true)); + } + + Instruction::new_with_bincode( + *STAKE_PROGRAM_ID_ADDRESS, + StakeInstruction::Withdraw(lamports), + account_metas, + ) + } + + pub fn delegate( + stake_pubkey: SolanaAddress, + vote_pubkey: SolanaAddress, + authorized_pubkey: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(stake_pubkey, false), + AccountMeta::readonly(vote_pubkey, false), + AccountMeta::readonly(*SYSVAR_CLOCK_ID_ADDRESS, false), + AccountMeta::readonly(*SYSVAR_STAKE_HISTORY_ID_ADDRESS, false), + AccountMeta::readonly(*STAKE_CONFIG_ID_ADDRESS, false), + AccountMeta::readonly(authorized_pubkey, true), + ]; + Instruction::new_with_bincode( + *STAKE_PROGRAM_ID_ADDRESS, + StakeInstruction::DelegateStake, + account_metas, + ) + } + + /// 0. `[WRITE]` Delegated stake account + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` Stake authority + pub fn deactivate( + stake_pubkey: SolanaAddress, + authorized_pubkey: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(stake_pubkey, false), + AccountMeta::readonly(*SYSVAR_CLOCK_ID_ADDRESS, false), + AccountMeta::new(authorized_pubkey, true), + ]; + Instruction::new_with_bincode( + *STAKE_PROGRAM_ID_ADDRESS, + StakeInstruction::Deactivate, + account_metas, + ) + } + + /// The function represents "stake delegation" operation that consists of several small instructions. + pub fn deposit_stake(args: DepositStakeArgs) -> Vec { + let stake_addr = args.stake_account.unwrap_or_else(|| { + // no stake address specified, generate a new unique + StakeProgram::address_from_recent_blockhash(&args.sender, &args.recent_blockhash) + }); + let seed = StakeProgram::recent_blockhash_as_seed(&args.recent_blockhash); + + let authorized = Authorized { + staker: args.sender, + withdrawer: args.sender, + }; + let lockup = Lockup::default(); + + vec![ + SystemInstructionBuilder::create_account_with_seed( + args.sender, + stake_addr, + args.sender, + seed, + args.lamports, + args.space, + ), + StakeInstructionBuilder::stake_initialize(stake_addr, authorized, lockup), + StakeInstructionBuilder::delegate(stake_addr, args.validator, args.sender), + ] + } +} diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/system_instruction.rs b/rust/chains/tw_solana/src/modules/instruction_builder/system_instruction.rs new file mode 100644 index 00000000000..ee7e0ab3f59 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/instruction_builder/system_instruction.rs @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::defined_addresses::*; +use crate::instruction::{AccountMeta, Instruction}; +use serde::{Deserialize, Serialize}; + +/// An instruction to the system program. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum SystemInstruction { + /// Create a new account + /// + /// # Account references + /// 0. `[WRITE, SIGNER]` Funding account + /// 1. `[WRITE, SIGNER]` New account + CreateAccount { + /// Number of lamports to transfer to the new account + lamports: u64, + + /// Number of bytes of memory to allocate + space: u64, + + /// Address of program that will own the new account + owner: SolanaAddress, + }, + + /// Assign account to a program + /// + /// # Account references + /// 0. `[WRITE, SIGNER]` Assigned account public key + Assign { + /// Owner program account + owner: SolanaAddress, + }, + + /// Transfer lamports + /// + /// # Account references + /// 0. `[WRITE, SIGNER]` Funding account + /// 1. `[WRITE]` Recipient account + Transfer { lamports: u64 }, + + /// Create a new account at an address derived from a base pubkey and a seed + /// + /// # Account references + /// 0. `[WRITE, SIGNER]` Funding account + /// 1. `[WRITE]` Created account + /// 2. `[SIGNER]` (optional) Base account; the account matching the base SolanaAddress below must be + /// provided as a signer, but may be the same as the funding account + /// and provided as account 0 + CreateAccountWithSeed { + /// Base public key + base: SolanaAddress, + + /// String of ASCII chars, no longer than `SolanaAddress::MAX_SEED_LEN` + seed: String, + + /// Number of lamports to transfer to the new account + lamports: u64, + + /// Number of bytes of memory to allocate + space: u64, + + /// Owner program account address + owner: SolanaAddress, + }, + + /// Consumes a stored nonce, replacing it with a successor + /// + /// # Account references + /// 0. `[WRITE]` Nonce account + /// 1. `[]` RecentBlockhashes sysvar + /// 2. `[SIGNER]` Nonce authority + AdvanceNonceAccount, + + /// Withdraw funds from a nonce account + /// + /// # Account references + /// 0. `[WRITE]` Nonce account + /// 1. `[WRITE]` Recipient account + /// 2. `[]` RecentBlockhashes sysvar + /// 3. `[]` Rent sysvar + /// 4. `[SIGNER]` Nonce authority + /// + /// The `u64` parameter is the lamports to withdraw, which must leave the + /// account balance above the rent exempt reserve or at zero. + WithdrawNonceAccount(u64), + + /// Drive state of Uninitialized nonce account to Initialized, setting the nonce value + /// + /// # Account references + /// 0. `[WRITE]` Nonce account + /// 1. `[]` RecentBlockhashes sysvar + /// 2. `[]` Rent sysvar + /// + /// The `SolanaAddress` parameter specifies the entity authorized to execute nonce + /// instruction on the account + /// + /// No signatures are required to execute this instruction, enabling derived + /// nonce account addresses + InitializeNonceAccount(SolanaAddress), + + /// Change the entity authorized to execute nonce instructions on the account + /// + /// # Account references + /// 0. `[WRITE]` Nonce account + /// 1. `[SIGNER]` Nonce authority + /// + /// The `SolanaAddress` parameter identifies the entity to authorize + AuthorizeNonceAccount(SolanaAddress), + + /// Allocate space in a (possibly new) account without funding + /// + /// # Account references + /// 0. `[WRITE, SIGNER]` New account + Allocate { + /// Number of bytes of memory to allocate + space: u64, + }, + + /// Allocate space for and assign an account at an address + /// derived from a base public key and a seed + /// + /// # Account references + /// 0. `[WRITE]` Allocated account + /// 1. `[SIGNER]` Base account + AllocateWithSeed { + /// Base public key + base: SolanaAddress, + + /// String of ASCII chars, no longer than `pubkey::MAX_SEED_LEN` + seed: String, + + /// Number of bytes of memory to allocate + space: u64, + + /// Owner program account + owner: SolanaAddress, + }, + + /// Assign account to a program based on a seed + /// + /// # Account references + /// 0. `[WRITE]` Assigned account + /// 1. `[SIGNER]` Base account + AssignWithSeed { + /// Base public key + base: SolanaAddress, + + /// String of ASCII chars, no longer than `pubkey::MAX_SEED_LEN` + seed: String, + + /// Owner program account + owner: SolanaAddress, + }, + + /// Transfer lamports from a derived address + /// + /// # Account references + /// 0. `[WRITE]` Funding account + /// 1. `[SIGNER]` Base for funding account + /// 2. `[WRITE]` Recipient account + TransferWithSeed { + /// Amount to transfer + lamports: u64, + + /// Seed to use to derive the funding account address + from_seed: String, + + /// Owner to use to derive the funding account address + from_owner: SolanaAddress, + }, + + /// One-time idempotent upgrade of legacy nonce versions in order to bump + /// them out of chain blockhash domain. + /// + /// # Account references + /// 0. `[WRITE]` Nonce account + UpgradeNonceAccount, +} + +pub struct SystemInstructionBuilder; + +impl SystemInstructionBuilder { + pub fn memo(memo: &str) -> Instruction { + let account_metas = Vec::default(); + let data = memo.as_bytes().to_vec(); + Instruction::new(*MEMO_PROGRAM_ID_ADDRESS, data, account_metas) + } + + pub fn create_account( + from_pubkey: SolanaAddress, + to_pubkey: SolanaAddress, + lamports: u64, + space: u64, + owner: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(from_pubkey, true), + AccountMeta::new(to_pubkey, true), + ]; + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::CreateAccount { + lamports, + space, + owner, + }, + account_metas, + ) + } + + pub fn transfer( + from_pubkey: SolanaAddress, + to_pubkey: SolanaAddress, + lamports: u64, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(from_pubkey, true), + AccountMeta::new(to_pubkey, false), + ]; + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::Transfer { lamports }, + account_metas, + ) + } + + /// Please note `to_pubkey` must match `create_with_seed(base, seed, owner)`. + pub fn create_account_with_seed( + from_pubkey: SolanaAddress, + to_pubkey: SolanaAddress, + base: SolanaAddress, + seed: String, + lamports: u64, + space: u64, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(from_pubkey, true), + AccountMeta::new(to_pubkey, false), + AccountMeta::readonly(base, true), + ]; + + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::CreateAccountWithSeed { + base, + seed, + lamports, + space, + owner: *STAKE_PROGRAM_ID_ADDRESS, + }, + account_metas, + ) + } + + /// Advance the value of a durable transaction nonce. + /// + /// # Required signers + /// + /// The `authorized_pubkey` signer must sign the transaction. + pub fn advance_nonce_account( + nonce_pubkey: SolanaAddress, + authorized_pubkey: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(nonce_pubkey, false), + AccountMeta::readonly(*SYSVAR_RECENT_BLOCKHASHS_ADDRESS, false), + AccountMeta::readonly(authorized_pubkey, true), + ]; + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::AdvanceNonceAccount, + account_metas, + ) + } + + /// Create an account containing a durable transaction nonce. + pub fn init_nonce_account( + nonce_pubkey: SolanaAddress, + authority: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(nonce_pubkey, false), + AccountMeta::readonly(*SYSVAR_RECENT_BLOCKHASHS_ADDRESS, false), + AccountMeta::readonly(*SYSVAR_RENT_ID_ADDRESS, false), + ]; + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::InitializeNonceAccount(authority), + account_metas, + ) + } + + /// Withdraw lamports from a durable transaction nonce account. + pub fn withdraw_nonce_account( + nonce_pubkey: SolanaAddress, + authorized_pubkey: SolanaAddress, + to_pubkey: SolanaAddress, + lamports: u64, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(nonce_pubkey, false), + AccountMeta::new(to_pubkey, false), + AccountMeta::readonly(*SYSVAR_RECENT_BLOCKHASHS_ADDRESS, false), + AccountMeta::readonly(*SYSVAR_RENT_ID_ADDRESS, false), + AccountMeta::readonly(authorized_pubkey, true), + ]; + Instruction::new_with_bincode( + *SYSTEM_PROGRAM_ID_ADDRESS, + SystemInstruction::WithdrawNonceAccount(lamports), + account_metas, + ) + } + + /// The function represents "create nonce account" operation that consists of several small instructions. + pub fn create_nonce_account( + signer: SolanaAddress, + new_nonce_account: SolanaAddress, + rent: u64, + space: u64, + ) -> Vec { + let program_id = *SYSTEM_PROGRAM_ID_ADDRESS; + vec![ + SystemInstructionBuilder::create_account( + signer, + new_nonce_account, + rent, + space, + program_id, + ), + SystemInstructionBuilder::init_nonce_account(new_nonce_account, signer), + ] + } +} diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/token_instruction.rs b/rust/chains/tw_solana/src/modules/instruction_builder/token_instruction.rs new file mode 100644 index 00000000000..cc5a47957e3 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/instruction_builder/token_instruction.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::defined_addresses::*; +use crate::instruction::{AccountMeta, Instruction}; +use std::mem::size_of; +use tw_memory::Data; + +/// Instructions supported by the token program. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenInstruction { + /// Transfers tokens from one account to another either directly or via a + /// delegate. If this account is associated with the native mint then equal + /// amounts of SOL and Tokens will be transferred to the destination + /// account. + /// + /// This instruction differs from Transfer in that the token mint and + /// decimals value is checked by the caller. This may be useful when + /// creating transactions offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[signer]` The source account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[]` The source account's multisignature owner/delegate. + /// 4. ..4+M `[signer]` M signer accounts. + TransferChecked { + /// The amount of tokens to transfer. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, +} + +impl TokenInstruction { + /// Packs a [TokenInstruction](enum.TokenInstruction.html) into a byte + /// buffer. + /// + /// # Important + /// + /// This serialization method differs from [`bincode::serialize`]: + /// It needs to serialize enum `type` as u8 (1 byte), but [`bincode::serialize`] serializes the type as u32 (4 bytes). + pub fn pack(&self) -> Vec { + let mut buf = Vec::with_capacity(size_of::()); + match self { + &Self::TransferChecked { amount, decimals } => { + // https://github.com/solana-labs/solana-program-library/blob/5418cf9b90d5c9ff5bff9f55fd17651f66c98902/token/program-2022/src/instruction.rs#L334-L339 + // https://github.com/trustwallet/wallet-core/blob/cd5a27481d2181e63362cb57e2b2160506cce163/src/Solana/Instruction.h#L37 + buf.push(12); + buf.extend_from_slice(&amount.to_le_bytes()); + buf.push(decimals); + }, + }; + buf + } +} + +pub struct TokenInstructionBuilder; + +impl TokenInstructionBuilder { + // `create_associated_token_account()` solana-program-library/associated-token-account/program/src/lib.rs + pub fn create_account( + funding_pubkey: SolanaAddress, + other_main_pubkey: SolanaAddress, + token_mint_pubkey: SolanaAddress, + token_pubkey: SolanaAddress, + token_program_id: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(funding_pubkey, true), + AccountMeta::new(token_pubkey, false), + AccountMeta::readonly(other_main_pubkey, false), + AccountMeta::readonly(token_mint_pubkey, false), + AccountMeta::readonly(*SYSTEM_PROGRAM_ID_ADDRESS, false), + AccountMeta::readonly(token_program_id, false), + AccountMeta::readonly(*SYSVAR_RENT_ID_ADDRESS, false), + ]; + let data = Data::default(); + Instruction::new(*ASSOCIATED_TOKEN_PROGRAM_ID_ADDRESS, data, account_metas) + } + + /// transfer_checked() solana-program-library/token/program/src/instruction.rs + pub fn transfer_checked( + sender_token_pubkey: SolanaAddress, + token_mint_pubkey: SolanaAddress, + recipient_token_pubkey: SolanaAddress, + signer: SolanaAddress, + amount: u64, + decimals: u8, + token_program_id: SolanaAddress, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(sender_token_pubkey, false), + AccountMeta::readonly(token_mint_pubkey, false), + AccountMeta::new(recipient_token_pubkey, false), + AccountMeta::new(signer, true), + ]; + + let data = TokenInstruction::TransferChecked { amount, decimals }.pack(); + Instruction::new(token_program_id, data, account_metas) + } +} diff --git a/rust/chains/tw_solana/src/modules/message_builder.rs b/rust/chains/tw_solana/src/modules/message_builder.rs new file mode 100644 index 00000000000..c4852154b1f --- /dev/null +++ b/rust/chains/tw_solana/src/modules/message_builder.rs @@ -0,0 +1,782 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::blockhash::Blockhash; +use crate::defined_addresses::{TOKEN_2022_PROGRAM_ID_ADDRESS, TOKEN_PROGRAM_ID_ADDRESS}; +use crate::instruction::Instruction; +use crate::modules::compiled_instructions::compile_instructions; +use crate::modules::compiled_keys::CompiledKeys; +use crate::modules::instruction_builder::compute_budget_instruction::{UnitLimit, UnitPrice}; +use crate::modules::instruction_builder::stake_instruction::{ + DepositStakeArgs, StakeInstructionBuilder, +}; +use crate::modules::instruction_builder::system_instruction::SystemInstructionBuilder; +use crate::modules::instruction_builder::token_instruction::TokenInstructionBuilder; +use crate::modules::instruction_builder::InstructionBuilder; +use crate::modules::PubkeySignatureMap; +use crate::transaction::versioned::VersionedMessage; +use crate::transaction::{legacy, v0, CompiledInstruction, MessageHeader, Signature}; +use std::borrow::Cow; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_keypair::ed25519; +use tw_keypair::traits::KeyPairTrait; +use tw_proto::Solana::Proto; +use Proto::mod_SigningInput::OneOftransaction_type as ProtoTransactionType; + +const DEFAULT_SPACE: u64 = 200; +const DEFAULT_CREATE_NONCE_SPACE: u64 = 80; + +pub struct MessageBuilder<'a> { + input: Proto::SigningInput<'a>, +} + +impl<'a> MessageBuilder<'a> { + pub fn new(input: Proto::SigningInput<'a>) -> Self { + MessageBuilder { input } + } + + pub fn signing_keys(&self) -> SigningResult> { + let mut signing_keys = Vec::default(); + if !self.input.fee_payer_private_key.is_empty() { + let fee_payer_private_key = + ed25519::sha512::KeyPair::try_from(self.input.fee_payer_private_key.as_ref()) + .into_tw() + .context("Invalid fee payer private key")?; + signing_keys.push(fee_payer_private_key); + } + + signing_keys.push(self.signer_key()?); + + // Consider matching other transaction types if they may contain other private keys. + if let ProtoTransactionType::create_nonce_account(ref nonce) = self.input.transaction_type { + let nonce_private_key = + ed25519::sha512::KeyPair::try_from(nonce.nonce_account_private_key.as_ref()) + .into_tw() + .context("Invalid nonce account private key")?; + signing_keys.push(nonce_private_key); + } + + Ok(signing_keys) + } + + pub fn external_signatures(&self) -> SigningResult { + match self.input.raw_message { + Some(ref raw_message) => RawMessageBuilder::external_signatures(raw_message), + None => Ok(PubkeySignatureMap::default()), + } + } + + pub fn build(self) -> SigningResult { + if let Some(ref raw_message) = self.input.raw_message { + return RawMessageBuilder::build(raw_message); + } + + let instructions = self.build_instructions()?; + + // Please note the fee payer can be different from the actual signer. + let (message_header, account_keys) = CompiledKeys::with_fee_payer(self.fee_payer()?) + .compile(&instructions) + .try_into_message_components()?; + + let compiled_instructions = compile_instructions(&instructions, &account_keys)?; + + if self.input.v0_msg { + Ok(VersionedMessage::V0(v0::Message { + header: message_header, + account_keys, + recent_blockhash: self.recent_blockhash()?.to_bytes(), + instructions: compiled_instructions, + address_table_lookups: Vec::default(), + })) + } else { + Ok(VersionedMessage::Legacy(legacy::Message { + header: message_header, + account_keys, + recent_blockhash: self.recent_blockhash()?.to_bytes(), + instructions: compiled_instructions, + })) + } + } + + /// Please note that this method can add [`MessageBuilder::signer_keys`] if necessary. + fn build_instructions(&self) -> SigningResult> { + match self.input.transaction_type { + ProtoTransactionType::transfer_transaction(ref transfer) => { + self.transfer_from_proto(transfer) + }, + ProtoTransactionType::delegate_stake_transaction(ref delegate) => { + self.delegate_stake_from_proto(delegate) + }, + ProtoTransactionType::deactivate_stake_transaction(ref deactivate) => { + self.deactivate_stake_from_proto(deactivate) + }, + ProtoTransactionType::deactivate_all_stake_transaction(ref deactivate_all) => { + self.deactivate_all_stake_from_proto(deactivate_all) + }, + ProtoTransactionType::withdraw_transaction(ref withdraw) => { + self.withdraw_from_proto(withdraw) + }, + ProtoTransactionType::withdraw_all_transaction(ref withdraw_all) => { + self.withdraw_all_from_proto(withdraw_all) + }, + ProtoTransactionType::create_token_account_transaction(ref create_token_acc) => { + self.create_token_account_from_proto(create_token_acc) + }, + ProtoTransactionType::token_transfer_transaction(ref token_transfer) => { + self.token_transfer_from_proto(token_transfer) + }, + ProtoTransactionType::create_and_transfer_token_transaction( + ref create_and_transfer, + ) => self.create_and_transfer_token_from_proto(create_and_transfer), + ProtoTransactionType::create_nonce_account(ref create_nonce) => { + self.create_nonce_account_from_proto(create_nonce) + }, + ProtoTransactionType::withdraw_nonce_account(ref withdraw_nonce) => { + self.withdraw_nonce_from_proto(withdraw_nonce) + }, + ProtoTransactionType::advance_nonce_account(ref advance_nonce) => { + self.advance_nonce_from_proto(advance_nonce) + }, + ProtoTransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params) + .context("No transaction type specified"), + } + } + + fn transfer_from_proto( + &self, + transfer: &Proto::Transfer<'_>, + ) -> SigningResult> { + let from = self.signer_address()?; + let to = SolanaAddress::from_str(&transfer.recipient) + .into_tw() + .context("Invalid recipient address")?; + + let references = Self::parse_references(&transfer.references)?; + + let transfer_ix = SystemInstructionBuilder::transfer(from, to, transfer.value) + .with_references(references); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, from) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .maybe_memo(transfer.memo.as_ref()) + .add_instruction(transfer_ix); + Ok(builder.output()) + } + + fn delegate_stake_from_proto( + &self, + delegate: &Proto::DelegateStake, + ) -> SigningResult> { + let sender = self.signer_address()?; + let validator = SolanaAddress::from_str(delegate.validator_pubkey.as_ref()) + .into_tw() + .context("Invalid validator address")?; + + let stake_account = if delegate.stake_account.is_empty() { + None + } else { + let stake_account = SolanaAddress::from_str(&delegate.stake_account) + .into_tw() + .context("Invalid stake account")?; + Some(stake_account) + }; + + let deposit_ixs = StakeInstructionBuilder::deposit_stake(DepositStakeArgs { + sender, + validator, + stake_account, + recent_blockhash: self.recent_blockhash()?, + lamports: delegate.value, + space: DEFAULT_SPACE, + }); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, sender) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instructions(deposit_ixs); + Ok(builder.output()) + } + + fn deactivate_stake_from_proto( + &self, + deactivate: &Proto::DeactivateStake, + ) -> SigningResult> { + let sender = self.signer_address()?; + let stake_account = SolanaAddress::from_str(&deactivate.stake_account) + .into_tw() + .context("Invalid stake account")?; + + let deactivate_ix = StakeInstructionBuilder::deactivate(stake_account, sender); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, sender) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instruction(deactivate_ix); + Ok(builder.output()) + } + + fn deactivate_all_stake_from_proto( + &self, + deactivate_all: &Proto::DeactivateAllStake, + ) -> SigningResult> { + let sender = self.signer_address()?; + let deactivate_ixs = deactivate_all + .stake_accounts + .iter() + .map(|stake_account| { + let stake_account = SolanaAddress::from_str(stake_account.as_ref())?; + Ok(StakeInstructionBuilder::deactivate(stake_account, sender)) + }) + .collect::>>() + .context("Invalid stake account(s)")?; + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, sender) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instructions(deactivate_ixs); + Ok(builder.output()) + } + + fn withdraw_from_proto( + &self, + withdraw: &Proto::WithdrawStake, + ) -> SigningResult> { + let sender = self.signer_address()?; + let stake_account = SolanaAddress::from_str(withdraw.stake_account.as_ref()) + .into_tw() + .context("Invalid stake account")?; + + let custodian_account = None; + + let withdraw_ix = StakeInstructionBuilder::withdraw( + stake_account, + sender, + sender, + withdraw.value, + custodian_account, + ); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, sender) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instruction(withdraw_ix); + Ok(builder.output()) + } + + fn withdraw_all_from_proto( + &self, + withdraw_all: &Proto::WithdrawAllStake, + ) -> SigningResult> { + let sender = self.signer_address()?; + + let withdraw_ixs = withdraw_all + .stake_accounts + .iter() + .map(|withdraw| { + let stake_account = SolanaAddress::from_str(withdraw.stake_account.as_ref()) + .into_tw() + .context("Invalid stake account")?; + + let custodian_account = None; + Ok(StakeInstructionBuilder::withdraw( + stake_account, + sender, + sender, + withdraw.value, + custodian_account, + )) + }) + .collect::>>()?; + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, sender) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instructions(withdraw_ixs); + Ok(builder.output()) + } + + fn create_token_account_from_proto( + &self, + create_token_acc: &Proto::CreateTokenAccount, + ) -> SigningResult> { + let funding_account = self.signer_address()?; + let other_main_address = SolanaAddress::from_str(create_token_acc.main_address.as_ref()) + .into_tw() + .context("Invalid main address")?; + + let token_mint_address = + SolanaAddress::from_str(create_token_acc.token_mint_address.as_ref()) + .into_tw() + .context("Invalid token mint address")?; + + let token_address = SolanaAddress::from_str(create_token_acc.token_address.as_ref()) + .into_tw() + .context("Invalid token address to be created")?; + + let instruction = TokenInstructionBuilder::create_account( + funding_account, + other_main_address, + token_mint_address, + token_address, + match_program_id(create_token_acc.token_program_id), + ); + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, funding_account) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instruction(instruction); + Ok(builder.output()) + } + + fn token_transfer_from_proto( + &self, + token_transfer: &Proto::TokenTransfer, + ) -> SigningResult> { + let signer = self.signer_address()?; + let sender_token_address = + SolanaAddress::from_str(token_transfer.sender_token_address.as_ref()) + .into_tw() + .context("Invalid sender token address")?; + + let token_mint_address = + SolanaAddress::from_str(token_transfer.token_mint_address.as_ref()) + .into_tw() + .context("Invalid token mint address")?; + + let recipient_token_address = + SolanaAddress::from_str(token_transfer.recipient_token_address.as_ref()) + .into_tw() + .context("Invalid recipient token address")?; + + let decimals = token_transfer + .decimals + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid token decimals. Expected lower than 256")?; + + let references = Self::parse_references(&token_transfer.references)?; + let transfer_instruction = TokenInstructionBuilder::transfer_checked( + sender_token_address, + token_mint_address, + recipient_token_address, + signer, + token_transfer.amount, + decimals, + match_program_id(token_transfer.token_program_id), + ) + .with_references(references); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, signer) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .maybe_memo(token_transfer.memo.as_ref()) + .add_instruction(transfer_instruction); + Ok(builder.output()) + } + + fn create_and_transfer_token_from_proto( + &self, + create_and_transfer: &Proto::CreateAndTransferToken, + ) -> SigningResult> { + let signer = self.signer_address()?; + let fee_payer = self.fee_payer()?; + + let sender_token_address = + SolanaAddress::from_str(create_and_transfer.sender_token_address.as_ref()) + .into_tw() + .context("Invalid sender token address")?; + + let token_mint_address = + SolanaAddress::from_str(create_and_transfer.token_mint_address.as_ref()) + .into_tw() + .context("Invalid token mint address")?; + + let recipient_main_address = + SolanaAddress::from_str(create_and_transfer.recipient_main_address.as_ref()) + .into_tw() + .context("Invalid recipient main address")?; + + let recipient_token_address = + SolanaAddress::from_str(create_and_transfer.recipient_token_address.as_ref()) + .into_tw() + .context("Invalid recipient token address")?; + + let references = Self::parse_references(&create_and_transfer.references)?; + + let decimals = create_and_transfer + .decimals + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid token decimals. Expected lower than 256")?; + + let create_account_instruction = TokenInstructionBuilder::create_account( + // Can be different from the actual signer. + fee_payer, + recipient_main_address, + token_mint_address, + recipient_token_address, + match_program_id(create_and_transfer.token_program_id), + ); + let transfer_instruction = TokenInstructionBuilder::transfer_checked( + sender_token_address, + token_mint_address, + recipient_token_address, + signer, + create_and_transfer.amount, + decimals, + match_program_id(create_and_transfer.token_program_id), + ) + .with_references(references); + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, signer) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instruction(create_account_instruction) + // Optional memo. Order: before transfer, as per documentation. + .maybe_memo(create_and_transfer.memo.as_ref()) + .add_instruction(transfer_instruction); + Ok(builder.output()) + } + + fn create_nonce_account_from_proto( + &self, + create_nonce: &Proto::CreateNonceAccount, + ) -> SigningResult> { + let signer = self.signer_address()?; + let prev_nonce_account = self.nonce_account()?; + + let new_nonce_account = if create_nonce.nonce_account.is_empty() { + let nonce_key = + ed25519::sha512::KeyPair::try_from(create_nonce.nonce_account_private_key.as_ref()) + .into_tw() + .context("Invalid nonce account private key")?; + SolanaAddress::with_public_key_ed25519(nonce_key.public()) + } else { + SolanaAddress::from_str(create_nonce.nonce_account.as_ref()) + .into_tw() + .context("Invalid nonce account")? + }; + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(prev_nonce_account, signer) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instructions(SystemInstructionBuilder::create_nonce_account( + signer, + new_nonce_account, + create_nonce.rent, + DEFAULT_CREATE_NONCE_SPACE, + )); + Ok(builder.output()) + } + + fn withdraw_nonce_from_proto( + &self, + withdraw_nonce: &Proto::WithdrawNonceAccount, + ) -> SigningResult> { + let signer = self.signer_address()?; + let withdraw_from_nonce = SolanaAddress::from_str(withdraw_nonce.nonce_account.as_ref()) + .into_tw() + .context("Invalid nonce account")?; + + let recipient = SolanaAddress::from_str(withdraw_nonce.recipient.as_ref()) + .into_tw() + .context("Invalid recipient")?; + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(self.nonce_account()?, signer) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()) + .add_instruction(SystemInstructionBuilder::withdraw_nonce_account( + withdraw_from_nonce, + signer, + recipient, + withdraw_nonce.value, + )); + Ok(builder.output()) + } + + fn advance_nonce_from_proto( + &self, + advance_nonce: &Proto::AdvanceNonceAccount, + ) -> SigningResult> { + let signer = self.signer_address()?; + let nonce_account = SolanaAddress::from_str(advance_nonce.nonce_account.as_ref()) + .into_tw() + .context("Invalid nonce account")?; + + let mut builder = InstructionBuilder::default(); + builder + .maybe_advance_nonce(Some(nonce_account), signer) + .maybe_priority_fee_price(self.priority_fee_price()) + .maybe_priority_fee_limit(self.priority_fee_limit()); + Ok(builder.output()) + } + + fn nonce_account(&self) -> SigningResult> { + if self.input.nonce_account.is_empty() { + Ok(None) + } else { + SolanaAddress::from_str(&self.input.nonce_account) + .map(Some) + .map_err(SigningError::from) + .context("Invalid nonce account") + } + } + + fn signer_address(&self) -> SigningResult { + if self.input.private_key.is_empty() { + SolanaAddress::from_str(&self.input.sender) + .map_err(SigningError::from) + .context("Sender address is either not set or invalid") + } else { + Ok(SolanaAddress::with_public_key_ed25519( + self.signer_key()?.public(), + )) + } + } + + /// Returns a fee payer of the transaction. + /// Please note it can be different the transaction signer if [`Proto::SigningInput::fee_payer`] is set. + pub fn fee_payer(&self) -> SigningResult { + if !self.input.fee_payer_private_key.is_empty() { + let fee_payer_key = + ed25519::sha512::KeyPair::try_from(self.input.fee_payer_private_key.as_ref()) + .into_tw() + .context("Invalid fee payer private key")?; + return Ok(SolanaAddress::with_public_key_ed25519( + fee_payer_key.public(), + )); + } + if !self.input.fee_payer.is_empty() { + return SolanaAddress::from_str(self.input.fee_payer.as_ref()) + .map_err(SigningError::from) + .context("Invalid fee payer address"); + } + self.signer_address() + } + + fn recent_blockhash(&self) -> SigningResult { + Blockhash::from_str(&self.input.recent_blockhash) + .map_err(SigningError::from) + .context("Invalid recent blockhash") + } + + fn signer_key(&self) -> SigningResult { + ed25519::sha512::KeyPair::try_from(self.input.private_key.as_ref()) + .map_err(SigningError::from) + .context("Invalid signer key") + } + + fn priority_fee_price(&self) -> Option { + self.input + .priority_fee_price + .as_ref() + .map(|proto| proto.price) + } + + fn priority_fee_limit(&self) -> Option { + self.input + .priority_fee_limit + .as_ref() + .map(|proto| proto.limit) + } + + fn parse_references(refs: &[Cow<'_, str>]) -> SigningResult> { + refs.iter() + .map(|addr| SolanaAddress::from_str(addr).map_err(SigningError::from)) + .collect::>>() + .context("Invalid transaction reference(s)") + } +} + +pub struct RawMessageBuilder; + +impl RawMessageBuilder { + pub fn build(raw_message: &Proto::RawMessage) -> SigningResult { + use Proto::mod_RawMessage::OneOfmessage as RawMessageType; + + match raw_message.message { + RawMessageType::legacy(ref legacy) => Self::build_legacy(legacy), + RawMessageType::v0(ref v0) => Self::build_v0(v0), + RawMessageType::None => SigningError::err(SigningErrorType::Error_invalid_params), + } + } + + pub fn external_signatures( + raw_message: &Proto::RawMessage, + ) -> SigningResult { + let mut key_signs = PubkeySignatureMap::with_capacity(raw_message.signatures.len()); + for entry in raw_message.signatures.iter() { + let pubkey = SolanaAddress::from_str(entry.pubkey.as_ref()) + .into_tw() + .context("Invalid 'PubkeySignature::public'")?; + + let signature = Signature::from_str(entry.signature.as_ref()) + .context("Invalid 'PubkeySignature::signature'")?; + + let ed25519_signature = ed25519::Signature::try_from(signature.0.as_slice()) + .into_tw() + .context("Invalid 'PubkeySignature::signature'")?; + + key_signs.insert(pubkey, ed25519_signature); + } + Ok(key_signs) + } + + fn build_legacy( + legacy: &Proto::mod_RawMessage::MessageLegacy, + ) -> SigningResult { + let header = Self::build_message_header(&legacy.header)?; + let account_keys = Self::build_account_keys(&legacy.account_keys)?; + let recent_blockhash = Blockhash::from_str(legacy.recent_blockhash.as_ref()) + .into_tw() + .context("Invalid recent blockhash")?; + + let instructions: Vec<_> = Self::build_instructions(&legacy.instructions)?; + + Ok(VersionedMessage::Legacy(legacy::Message { + header, + account_keys, + recent_blockhash: recent_blockhash.to_bytes(), + instructions, + })) + } + + fn build_v0(v0: &Proto::mod_RawMessage::MessageV0) -> SigningResult { + let header = Self::build_message_header(&v0.header)?; + let account_keys = Self::build_account_keys(&v0.account_keys)?; + let recent_blockhash = Blockhash::from_str(v0.recent_blockhash.as_ref()) + .into_tw() + .context("Invalid recent blockhash")?; + + let instructions: Vec<_> = Self::build_instructions(&v0.instructions)?; + let address_table_lookups = v0 + .address_table_lookups + .iter() + .map(Self::build_address_lookup_table) + .collect::>>()?; + + Ok(VersionedMessage::V0(v0::Message { + header, + account_keys, + recent_blockhash: recent_blockhash.to_bytes(), + instructions, + address_table_lookups, + })) + } + + fn build_message_header( + raw_header: &Option, + ) -> SigningResult { + let raw_header = raw_header + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params) + .context("No message header provided")?; + + Ok(MessageHeader { + num_required_signatures: try_into_u8(raw_header.num_required_signatures)?, + num_readonly_signed_accounts: try_into_u8(raw_header.num_readonly_signed_accounts)?, + num_readonly_unsigned_accounts: try_into_u8(raw_header.num_readonly_unsigned_accounts)?, + }) + } + + fn build_account_keys(raw_keys: &[Cow<'_, str>]) -> SigningResult> { + raw_keys + .iter() + .map(|s| SolanaAddress::from_str(s.as_ref())) + .collect::>>() + .map_err(SigningError::from) + .context("Cannot build account keys") + } + + fn build_instructions( + ixs: &[Proto::mod_RawMessage::Instruction], + ) -> SigningResult> { + ixs.iter().map(Self::build_instruction).collect() + } + + fn build_instruction( + ix: &Proto::mod_RawMessage::Instruction, + ) -> SigningResult { + let accounts = ix + .accounts + .iter() + .map(|idx| try_into_u8(*idx)) + .collect::>>() + .context("Cannot build account metas")?; + + Ok(CompiledInstruction { + program_id_index: try_into_u8(ix.program_id).context("Invalid program ID")?, + accounts, + data: ix.program_data.to_vec(), + }) + } + + fn build_address_lookup_table( + lookup: &Proto::mod_RawMessage::MessageAddressTableLookup, + ) -> SigningResult { + let account_key = SolanaAddress::from_str(lookup.account_key.as_ref()) + .into_tw() + .context("Invalid lookup's account key")?; + + let writable_indexes = lookup + .writable_indexes + .iter() + .copied() + .map(try_into_u8) + .collect::>>() + .context("Invalid writable index(s)")?; + + let readonly_indexes = lookup + .readonly_indexes + .iter() + .copied() + .map(try_into_u8) + .collect::>>() + .context("Invalid readonly index(s)")?; + + Ok(v0::MessageAddressTableLookup { + account_key, + writable_indexes, + readonly_indexes, + }) + } +} + +fn try_into_u8(num: T) -> SigningResult +where + u8: TryFrom, +{ + u8::try_from(num).tw_err(|_| SigningErrorType::Error_tx_too_big) +} + +fn match_program_id(program_id: Proto::TokenProgramId) -> SolanaAddress { + match program_id { + Proto::TokenProgramId::TokenProgram => *TOKEN_PROGRAM_ID_ADDRESS, + Proto::TokenProgramId::Token2022Program => *TOKEN_2022_PROGRAM_ID_ADDRESS, + } +} diff --git a/rust/chains/tw_solana/src/modules/mod.rs b/rust/chains/tw_solana/src/modules/mod.rs new file mode 100644 index 00000000000..074e5cbc8a7 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/mod.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use std::collections::HashMap; +use tw_keypair::ed25519; + +pub mod compiled_instructions; +pub mod compiled_keys; +pub mod instruction_builder; +pub mod message_builder; +pub mod proto_builder; +pub mod transaction_decoder; +pub mod transaction_util; +pub mod tx_signer; +pub mod utils; +pub mod wallet_connect; + +pub type PubkeySignatureMap = HashMap; diff --git a/rust/chains/tw_solana/src/modules/proto_builder.rs b/rust/chains/tw_solana/src/modules/proto_builder.rs new file mode 100644 index 00000000000..d40ff7ca664 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/proto_builder.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::v0; +use crate::transaction::versioned::{VersionedMessage, VersionedTransaction}; +use std::borrow::Cow; +use tw_proto::Solana::Proto::{self, mod_RawMessage::OneOfmessage as ProtoMessageType}; + +pub struct ProtoBuilder; + +impl ProtoBuilder { + pub fn build_from_tx(tx: &VersionedTransaction) -> Proto::RawMessage<'static> { + let message_header = Proto::mod_RawMessage::MessageHeader { + num_required_signatures: tx.message.header().num_required_signatures as u32, + num_readonly_signed_accounts: tx.message.header().num_readonly_signed_accounts as u32, + num_readonly_unsigned_accounts: tx.message.header().num_readonly_unsigned_accounts + as u32, + }; + + let account_keys: Vec<_> = tx + .message + .account_keys() + .iter() + .map(|acc| Cow::from(acc.to_string())) + .collect(); + + let recent_blockhash = Cow::from(tx.message.recent_blockhash().to_string()); + + let instructions = tx + .message + .instructions() + .iter() + .map(|ix| Proto::mod_RawMessage::Instruction { + program_id: ix.program_id_index as u32, + accounts: vec_u8_to_u32(&ix.accounts), + program_data: Cow::from(ix.data.clone()), + }) + .collect(); + + let message = match tx.message { + VersionedMessage::Legacy(_) => { + ProtoMessageType::legacy(Proto::mod_RawMessage::MessageLegacy { + header: Some(message_header), + account_keys, + recent_blockhash, + instructions, + }) + }, + VersionedMessage::V0(ref v0) => { + let address_table_lookups = + Self::build_address_table_lookups(&v0.address_table_lookups); + ProtoMessageType::v0(Proto::mod_RawMessage::MessageV0 { + header: Some(message_header), + account_keys, + recent_blockhash, + instructions, + address_table_lookups, + }) + }, + }; + + Proto::RawMessage { + signatures: Self::build_signatures(tx), + message, + } + } + + fn build_address_table_lookups( + lookups: &[v0::MessageAddressTableLookup], + ) -> Vec> { + lookups + .iter() + .map(|lookup| Proto::mod_RawMessage::MessageAddressTableLookup { + account_key: Cow::from(lookup.account_key.to_string()), + writable_indexes: vec_u8_to_u32(&lookup.writable_indexes), + readonly_indexes: vec_u8_to_u32(&lookup.readonly_indexes), + }) + .collect() + } + + pub fn build_signatures(tx: &VersionedTransaction) -> Vec> { + tx.message + .account_keys() + .iter() + .zip(tx.signatures.iter()) + .map(|(pubkey, signature)| Proto::PubkeySignature { + pubkey: Cow::from(pubkey.to_string()), + signature: Cow::from(signature.to_string()), + }) + .collect() + } +} + +fn vec_u8_to_u32(v: &[u8]) -> Vec { + v.iter().map(|num| *num as u32).collect() +} diff --git a/rust/chains/tw_solana/src/modules/transaction_decoder.rs b/rust/chains/tw_solana/src/modules/transaction_decoder.rs new file mode 100644 index 00000000000..02a441a464a --- /dev/null +++ b/rust/chains/tw_solana/src/modules/transaction_decoder.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::proto_builder::ProtoBuilder; +use crate::transaction::versioned::VersionedTransaction; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_decoder::TransactionDecoder; +use tw_coin_entry::signing_output_error; +use tw_proto::Solana::Proto; + +pub struct SolanaTransactionDecoder; + +impl TransactionDecoder for SolanaTransactionDecoder { + type Output = Proto::DecodingTransactionOutput<'static>; + + fn decode_transaction(&self, coin: &dyn CoinContext, tx: &[u8]) -> Self::Output { + Self::decode_transaction_impl(coin, tx) + .unwrap_or_else(|e| signing_output_error!(Proto::DecodingTransactionOutput, e)) + } +} + +impl SolanaTransactionDecoder { + pub(crate) fn decode_transaction_impl( + _coin: &dyn CoinContext, + tx: &[u8], + ) -> SigningResult> { + let decoded_tx: VersionedTransaction = bincode::deserialize(tx) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error decoding transaction as 'bincode'")?; + let transaction = ProtoBuilder::build_from_tx(&decoded_tx); + + Ok(Proto::DecodingTransactionOutput { + transaction: Some(transaction), + ..Proto::DecodingTransactionOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/modules/transaction_util.rs b/rust/chains/tw_solana/src/modules/transaction_util.rs new file mode 100644 index 00000000000..fdde5d6952c --- /dev/null +++ b/rust/chains/tw_solana/src/modules/transaction_util.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::transaction_decoder::SolanaTransactionDecoder; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_encoding::base64; +use tw_encoding::base64::STANDARD; + +pub struct SolanaTransactionUtil; + +impl TransactionUtil for SolanaTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +impl SolanaTransactionUtil { + fn calc_tx_hash_impl(coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + // Solana signed transactions can be encoded in either base64 or base58. For more information, see: https://solana.com/docs/rpc/http/sendtransaction + // Currently, this function only accepts base64 encoding. + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; + let decoded_tx_output = SolanaTransactionDecoder::decode_transaction_impl(coin, &tx_bytes)?; + + let first_sig = decoded_tx_output + .transaction + .as_ref() + .and_then(|tx| tx.signatures.first()) + .or_tw_err(SigningErrorType::Error_input_parse) + .context("There is no transaction signatures. Looks like it hasn't been signed yet")?; + + // Tx hash is the first signature + Ok(first_sig.signature.to_string()) + } +} diff --git a/rust/chains/tw_solana/src/modules/tx_signer.rs b/rust/chains/tw_solana/src/modules/tx_signer.rs new file mode 100644 index 00000000000..5fb421e6876 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/tx_signer.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::modules::PubkeySignatureMap; +use crate::transaction::{versioned, Signature}; +use std::collections::HashMap; +use tw_coin_entry::error::prelude::*; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_memory::Data; + +pub struct TxSigner; + +impl TxSigner { + pub fn sign_versioned( + unsigned_msg: versioned::VersionedMessage, + keys: &[ed25519::sha512::KeyPair], + external_signatures: &PubkeySignatureMap, + ) -> SigningResult { + let mut key_signs = HashMap::default(); + + let message_encoded = Self::preimage_versioned(&unsigned_msg)?; + + // Add external signatures first, so they can be overriden if corresponding private keys are specified. + key_signs.extend(external_signatures.clone()); + + // Sign the message with all given private keys. + for private_key in keys { + let signing_pubkey = + SolanaAddress::with_public_key_bytes(private_key.public().to_bytes()); + let ed25519_signature = private_key.sign(message_encoded.clone())?; + + key_signs.insert(signing_pubkey, ed25519_signature); + } + + Self::compile_versioned(unsigned_msg, key_signs) + } + + pub fn compile_versioned( + unsigned_msg: versioned::VersionedMessage, + key_signs: HashMap, + ) -> SigningResult { + let mut tx = versioned::VersionedTransaction::unsigned(unsigned_msg); + + let actual_signatures = key_signs.len(); + let expected_signatures = tx.message.num_required_signatures(); + if actual_signatures != expected_signatures { + return SigningError::err(SigningErrorType::Error_signatures_count) + .with_context(|| format!("Expected '{expected_signatures}' signatures, provided '{actual_signatures}'")); + } + + for (signing_pubkey, ed25519_signature) in key_signs { + // Find an index of the corresponding account. + let account_index = tx + .message + .get_account_index(signing_pubkey) + .or_tw_err(SigningErrorType::Error_missing_private_key) + .with_context(|| { + format!("Provided a signature for an unexpected account: {signing_pubkey}") + })?; + let signature_to_reassign = tx + .signatures + .get_mut(account_index) + .or_tw_err(SigningErrorType::Error_signatures_count) + .context("Internal error: invalid number of Tx.signatures[]")?; + *signature_to_reassign = Signature(ed25519_signature.to_bytes()); + } + + Ok(tx) + } + + pub fn preimage_versioned(msg: &versioned::VersionedMessage) -> SigningResult { + bincode::serialize(&msg) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing Solana Message as 'bincode'") + } +} diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs new file mode 100644 index 00000000000..85b662784b8 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::proto_builder::ProtoBuilder; +use crate::modules::tx_signer::TxSigner; +use crate::modules::PubkeySignatureMap; +use crate::transaction::versioned::VersionedTransaction; +use crate::SOLANA_ALPHABET; +use std::borrow::Cow; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; +use tw_hash::H256; +use tw_keypair::{ed25519, KeyPairResult}; +use tw_memory::Data; +use tw_proto::Solana::Proto; + +pub struct SolanaTransaction; + +impl SolanaTransaction { + pub fn update_blockhash_and_sign( + encoded_tx: &str, + recent_blockhash: &str, + private_keys: &[Data], + ) -> Proto::SigningOutput<'static> { + Self::update_blockhash_and_sign_impl(encoded_tx, recent_blockhash, private_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn update_blockhash_and_sign_impl( + encoded_tx: &str, + recent_blockhash: &str, + private_keys: &[Data], + ) -> SigningResult> { + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; + + let tx_to_sign: VersionedTransaction = + bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; + let mut msg_to_sign = tx_to_sign.message; + + let new_blockchain_hash = base58::decode(recent_blockhash, SOLANA_ALPHABET)?; + let new_blockchain_hash = H256::try_from(new_blockchain_hash.as_slice()) + .tw_err(|_| SigningErrorType::Error_invalid_params)?; + + // Update the transaction's blockhash and re-sign it. + msg_to_sign.set_recent_blockhash(new_blockchain_hash); + + let unsigned_encoded = TxSigner::preimage_versioned(&msg_to_sign)?; + + // Do not sign the transaction if there is no private keys, but set zeroed signatures. + // It's needed to estimate the transaction fee with an updated blockhash without using real private keys. + let signed_tx = if private_keys.is_empty() { + VersionedTransaction::unsigned(msg_to_sign) + } else { + let private_keys = private_keys + .iter() + .map(|pk| ed25519::sha512::KeyPair::try_from(pk.as_slice())) + .collect::>>()?; + + let external_signatures = PubkeySignatureMap::default(); + TxSigner::sign_versioned(msg_to_sign, &private_keys, &external_signatures)? + }; + + let unsigned_encoded = base64::encode(&unsigned_encoded, STANDARD); + let signed_encoded = + bincode::serialize(&signed_tx).tw_err(|_| SigningErrorType::Error_internal)?; + let signed_encoded = base64::encode(&signed_encoded, STANDARD); + + Ok(Proto::SigningOutput { + encoded: Cow::from(signed_encoded), + unsigned_tx: Cow::from(unsigned_encoded), + signatures: ProtoBuilder::build_signatures(&signed_tx), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/modules/wallet_connect/connector.rs b/rust/chains/tw_solana/src/modules/wallet_connect/connector.rs new file mode 100644 index 00000000000..041d50012ac --- /dev/null +++ b/rust/chains/tw_solana/src/modules/wallet_connect/connector.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::proto_builder::ProtoBuilder; +use crate::modules::wallet_connect::request::SignTransactionRequest; +use crate::transaction::versioned::VersionedTransaction; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::wallet_connector::WalletConnector; +use tw_coin_entry::signing_output_error; +use tw_proto::Solana::Proto; +use tw_proto::WalletConnect::Proto::{ + self as WCProto, mod_ParseRequestOutput::OneOfsigning_input_oneof as SigningInputEnum, +}; + +pub struct SolanaWalletConnector; + +impl WalletConnector for SolanaWalletConnector { + fn parse_request( + &self, + coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> WCProto::ParseRequestOutput<'static> { + Self::parse_request_impl(coin, request) + .unwrap_or_else(|e| signing_output_error!(WCProto::ParseRequestOutput, e)) + } +} + +impl SolanaWalletConnector { + fn parse_request_impl( + coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> SigningResult> { + match request.method { + WCProto::Method::SolanaSignTransaction => { + Self::parse_sign_transaction_request(coin, request) + }, + _ => SigningError::err(SigningErrorType::Error_not_supported) + .context("Unknown WalletConnect method"), + } + } + + pub fn parse_sign_transaction_request( + _coin: &dyn CoinContext, + request: WCProto::ParseRequestInput<'_>, + ) -> SigningResult> { + let sign_req: SignTransactionRequest = serde_json::from_str(&request.payload) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error parsing WalletConnect signing request as JSON")?; + + let transaction: VersionedTransaction = bincode::deserialize(&sign_req.transaction.0) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error deserializing Solana transaction as 'bincode'")?; + + let signing_input = Proto::SigningInput { + raw_message: Some(ProtoBuilder::build_from_tx(&transaction)), + ..Proto::SigningInput::default() + }; + + Ok(WCProto::ParseRequestOutput { + signing_input_oneof: SigningInputEnum::solana(signing_input), + ..WCProto::ParseRequestOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/modules/wallet_connect/mod.rs b/rust/chains/tw_solana/src/modules/wallet_connect/mod.rs new file mode 100644 index 00000000000..6a72d9528b8 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/wallet_connect/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod connector; +pub mod request; diff --git a/rust/chains/tw_solana/src/modules/wallet_connect/request.rs b/rust/chains/tw_solana/src/modules/wallet_connect/request.rs new file mode 100644 index 00000000000..603de8e1045 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/wallet_connect/request.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::Deserialize; +use tw_encoding::base64::Base64Encoded; + +/// `solana_signTransaction` request payload without legacy fields. +/// https://docs.walletconnect.com/advanced/rpc-reference/solana-rpc#solana_signtransaction +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignTransactionRequest { + pub transaction: Base64Encoded, +} diff --git a/rust/chains/tw_solana/src/program/mod.rs b/rust/chains/tw_solana/src/program/mod.rs new file mode 100644 index 00000000000..bd0451b533b --- /dev/null +++ b/rust/chains/tw_solana/src/program/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod stake_program; diff --git a/rust/chains/tw_solana/src/program/stake_program.rs b/rust/chains/tw_solana/src/program/stake_program.rs new file mode 100644 index 00000000000..e70a9a2fe3b --- /dev/null +++ b/rust/chains/tw_solana/src/program/stake_program.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::blockhash::Blockhash; +use crate::defined_addresses::*; +use tw_coin_entry::error::prelude::*; +use tw_hash::sha2::sha256; +use tw_hash::H256; + +pub const MAX_SEED_LEN: usize = 32; + +pub struct StakeProgram; + +impl StakeProgram { + pub fn recent_blockhash_as_seed(recent_blockhash: &Blockhash) -> String { + recent_blockhash + .to_string() + .chars() + .take(MAX_SEED_LEN) + .collect() + } + + pub fn address_from_recent_blockhash( + from: &SolanaAddress, + recent_blockhash: &Blockhash, + ) -> SolanaAddress { + let mut seed = from.bytes().to_vec(); + seed.extend_from_slice(Self::recent_blockhash_as_seed(recent_blockhash).as_bytes()); + seed.extend_from_slice(STAKE_PROGRAM_ID_ADDRESS.bytes().as_slice()); + let bytes = + H256::try_from(sha256(&seed).as_slice()).expect("sha256 expected to return 32 bytes"); + SolanaAddress::with_public_key_bytes(bytes) + } + + /// https://github.com/solana-labs/solana-program-library/blob/master/associated-token-account/program/src/lib.rs#L35 + pub fn get_associated_token_address( + main_address: SolanaAddress, + token_program_id: SolanaAddress, + token_mint_address: SolanaAddress, + ) -> AddressResult { + SolanaAddress::find_program_address( + &[ + main_address.bytes().as_slice(), + token_program_id.bytes().as_slice(), + token_mint_address.bytes().as_slice(), + ], + *ASSOCIATED_TOKEN_PROGRAM_ID_ADDRESS, + ) + .ok_or(AddressError::InvalidInput) + } +} diff --git a/rust/chains/tw_solana/src/signer.rs b/rust/chains/tw_solana/src/signer.rs new file mode 100644 index 00000000000..caa552aa4da --- /dev/null +++ b/rust/chains/tw_solana/src/signer.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::message_builder::MessageBuilder; +use crate::modules::proto_builder::ProtoBuilder; +use crate::modules::tx_signer::TxSigner; +use crate::SOLANA_ALPHABET; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_encoding::base58; +use tw_encoding::base64::{self, STANDARD}; +use tw_proto::Solana::Proto; + +pub struct SolanaSigner; + +impl SolanaSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let encode = move |data| match input.tx_encoding { + Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), + Proto::Encoding::Base64 => base64::encode(data, STANDARD), + }; + + let builder = MessageBuilder::new(input); + let signing_keys = builder.signing_keys()?; + let external_signatures = builder.external_signatures()?; + let unsigned_msg = builder.build()?; + + let encoded_unsigned = bincode::serialize(&unsigned_msg) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing Solana Message as 'bincode'")?; + let encoded_unsigned = encode(&encoded_unsigned); + + let signed_tx = + TxSigner::sign_versioned(unsigned_msg, &signing_keys, &external_signatures)?; + + let encoded_tx = bincode::serialize(&signed_tx) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing Solana Transaction as 'bincode'")?; + let encoded_tx = encode(&encoded_tx); + + Ok(Proto::SigningOutput { + encoded: Cow::from(encoded_tx), + unsigned_tx: Cow::from(encoded_unsigned), + signatures: ProtoBuilder::build_signatures(&signed_tx), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/transaction/legacy.rs b/rust/chains/tw_solana/src/transaction/legacy.rs new file mode 100644 index 00000000000..dae3bf13949 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/legacy.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::transaction::{short_vec, CompiledInstruction, MessageHeader, Signature}; +use serde::{Deserialize, Serialize}; +use tw_hash::{as_byte_sequence, H256}; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Message { + /// The message header, identifying signed and read-only `account_keys`. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + pub header: MessageHeader, + + /// All the account keys used by this transaction. + #[serde(with = "short_vec")] + pub account_keys: Vec, + + /// The id of a recent ledger entry. + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + + /// Programs that will be executed in sequence and committed in one atomic transaction if all + /// succeed. + #[serde(with = "short_vec")] + pub instructions: Vec, +} + +#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize)] +pub struct Transaction { + /// A set of signatures of a serialized [`Message`], signed by the first + /// keys of the `Message`'s [`account_keys`], where the number of signatures + /// is equal to [`num_required_signatures`] of the `Message`'s + /// [`MessageHeader`]. + /// + /// [`account_keys`]: Message::account_keys + /// [`MessageHeader`]: crate::message::MessageHeader + /// [`num_required_signatures`]: crate::message::MessageHeader::num_required_signatures + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + #[serde(with = "short_vec")] + pub signatures: Vec, + + /// The message to sign. + pub message: Message, +} diff --git a/rust/chains/tw_solana/src/transaction/mod.rs b/rust/chains/tw_solana/src/transaction/mod.rs new file mode 100644 index 00000000000..a37cdc75963 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/mod.rs @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::SOLANA_ALPHABET; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base58; +use tw_hash::{as_byte_sequence, H512}; + +pub mod legacy; +pub mod short_vec; +pub mod v0; +pub mod versioned; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub struct MessageHeader { + /// The number of signatures required for this message to be considered + /// valid. The signers of those signatures must match the first + /// `num_required_signatures` of [`Message::account_keys`]. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + pub num_required_signatures: u8, + + /// The last `num_readonly_signed_accounts` of the signed keys are read-only + /// accounts. + pub num_readonly_signed_accounts: u8, + + /// The last `num_readonly_unsigned_accounts` of the unsigned keys are + /// read-only accounts. + pub num_readonly_unsigned_accounts: u8, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CompiledInstruction { + /// Index into the transaction keys array indicating the program account that executes this instruction. + pub program_id_index: u8, + /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. + #[serde(with = "short_vec")] + pub accounts: Vec, + /// The program input data. + #[serde(with = "short_vec")] + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Signature(#[serde(with = "as_byte_sequence")] pub H512); + +impl FromStr for Signature { + type Err = SigningError; + + fn from_str(s: &str) -> Result { + let data = base58::decode(s, SOLANA_ALPHABET) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error decoding Solana Signature from base58")?; + H512::try_from(data.as_slice()) + .map(Signature) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Solana Signature must be 64 byte length") + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", base58::encode(self.0.as_slice(), SOLANA_ALPHABET)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::address::SolanaAddress; + use crate::transaction::v0::MessageAddressTableLookup; + use crate::transaction::versioned::{VersionedMessage, VersionedTransaction}; + use crate::SOLANA_ALPHABET; + use tw_encoding::base58; + use tw_encoding::base64::{self, STANDARD}; + use tw_encoding::hex::ToHex; + use tw_hash::H256; + use tw_memory::Data; + + fn base58_decode(s: &'static str) -> Data { + base58::decode(s, SOLANA_ALPHABET).unwrap() + } + + fn base58_decode_h256(s: &'static str) -> H256 { + let bytes = base58::decode(s, SOLANA_ALPHABET).unwrap(); + H256::try_from(bytes.as_slice()).unwrap() + } + + #[test] + fn test_rango_transaction_ser_de() { + let serialized = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", STANDARD).unwrap(); + let actual: VersionedTransaction = bincode::deserialize(&serialized).unwrap(); + + let expected = VersionedTransaction { + signatures: vec![Signature(H512::default())], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 7, + }, + account_keys: vec![ + SolanaAddress::from("AHy6YZA8BsHgQfVkk7MbwpAN94iyN7Nf1zN4nPqUN32Q"), + SolanaAddress::from("g7dD1FHSemkUQrX1Eak37wzvDjscgBW2pFCENwjLdMX"), + SolanaAddress::from("7m57LBTxtzhWn6WdFxKtnoJLBQXyNERLYebebXLVaKy3"), + SolanaAddress::from("AEBCPtV8FFkWFAKxrz7mbYvobpkZuWaRWQCyJVRaheUD"), + SolanaAddress::from("BND2ehwWVeHVA5EtMm2b7Vu51AT8f2PNWusS9KQX5moy"), + SolanaAddress::from("DVCeozFGbe6ew3eWTnZByjHeYqTq1cvbrB7JJhkLxaRJ"), + SolanaAddress::from("GvgWmk8iPACw1AEMt47WzkuTkKoSGbn4Xk3aLM8vdbJD"), + SolanaAddress::from("HkphEpUqnFBxBuCPEq5j1HA9L8EwmsmRT6UcFKziptM1"), + SolanaAddress::from("Hzxx6b5a7dmmJeDXLQzr4dTrc2HGK9ar5YRakZgr3ZZ7"), + SolanaAddress::from("11111111111111111111111111111111"), + SolanaAddress::from("ComputeBudget111111111111111111111111111111"), + SolanaAddress::from("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"), + SolanaAddress::from("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + SolanaAddress::from("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + SolanaAddress::from("D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"), + SolanaAddress::from("GGztQqQ6pCPaJQnNpXBgELr5cs3WwDakRbh1iEMzjgSJ"), + ], + recent_blockhash: base58_decode_h256( + "DiSimxK2z1cRa6yD4goqte3rDMmghJAD8WDUZEab2CzD", + ), + instructions: vec![ + CompiledInstruction { + program_id_index: 10, + accounts: vec![], + data: base58_decode("K1FDJ7"), + }, + CompiledInstruction { + program_id_index: 10, + accounts: vec![], + data: base58_decode("3E9ErJ5MrzbZ"), + }, + CompiledInstruction { + program_id_index: 13, + accounts: vec![0, 6, 0, 35, 9, 12], + data: base58_decode("2"), + }, + CompiledInstruction { + program_id_index: 9, + accounts: vec![0, 6], + data: base58_decode("3Bxs3zzLZLuLQEYX"), + }, + CompiledInstruction { + program_id_index: 12, + accounts: vec![6], + data: base58_decode("J"), + }, + CompiledInstruction { + program_id_index: 11, + accounts: vec![ + 12, 15, 0, 6, 1, 5, 2, 35, 33, 11, 11, 14, 11, 29, 12, 15, 26, 1, 25, + 7, 24, 4, 8, 3, 36, 27, 28, 12, 30, 15, 7, 5, 16, 18, 17, 31, 21, 31, + 20, 23, 7, 5, 33, 34, 19, 31, 15, 12, 12, 32, 31, 22, 11, + ], + data: base58_decode( + "5n9zLuyvSGkuf4iDD6PfDvzvzehUkDghmApUkZSXSx57jF9RGSH5Y23tzFJDG3", + ), + }, + CompiledInstruction { + program_id_index: 12, + accounts: vec![6, 0, 0], + data: base58_decode("A"), + }, + ], + address_table_lookups: vec![ + MessageAddressTableLookup { + account_key: SolanaAddress::from( + "FeXRmSWmwChZbB2EC7Qjw9XKk28yBrPj3k3nzT1DKfak", + ), + writable_indexes: vec![202, 200, 201], + readonly_indexes: vec![196, 197, 36, 199], + }, + MessageAddressTableLookup { + account_key: SolanaAddress::from( + "5cFsmTCEfmvpBUBHqsWZnf9n5vTWLYH2LT8X7HdShwxP", + ), + writable_indexes: vec![160, 245, 248, 159, 157], + readonly_indexes: vec![156, 244, 246, 247], + }, + MessageAddressTableLookup { + account_key: SolanaAddress::from( + "HJ5StCvsDU4JsvK39VcsHjaoTRTtQU749MQ9qUsJaG1m", + ), + writable_indexes: vec![122, 121, 125], + readonly_indexes: vec![110, 126], + }, + ], + }), + }; + + assert_eq!(actual, expected); + + let serialized_again = bincode::serialize(&actual).unwrap(); + assert_eq!(serialized_again.to_hex(), serialized.to_hex()); + } +} diff --git a/rust/chains/tw_solana/src/transaction/short_vec.rs b/rust/chains/tw_solana/src/transaction/short_vec.rs new file mode 100644 index 00000000000..5001a942902 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/short_vec.rs @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Compact serde-encoding of vectors with small length. +//! Source code: https://github.com/solana-labs/solana/blob/a16f982169eb197fad0eb8c58c307fb069f69d8f/sdk/program/src/short_vec.rs + +#![allow(clippy::arithmetic_side_effects)] +use serde::{ + de::{self, Deserializer, SeqAccess, Visitor}, + ser::{self, SerializeTuple, Serializer}, + Deserialize, Serialize, +}; +use std::{convert::TryFrom, fmt, marker::PhantomData}; + +/// Same as u16, but serialized with 1 to 3 bytes. If the value is above +/// 0x7f, the top bit is set and the remaining value is stored in the next +/// bytes. Each byte follows the same pattern until the 3rd byte. The 3rd +/// byte, if needed, uses all 8 bits to store the last byte of the original +/// value. +pub struct ShortU16(pub u16); + +impl Serialize for ShortU16 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Pass a non-zero value to serialize_tuple() so that serde_json will + // generate an open bracket. + let mut seq = serializer.serialize_tuple(1)?; + + let mut rem_val = self.0; + loop { + let mut elem = (rem_val & 0x7f) as u8; + rem_val >>= 7; + if rem_val == 0 { + seq.serialize_element(&elem)?; + break; + } else { + elem |= 0x80; + seq.serialize_element(&elem)?; + } + } + seq.end() + } +} + +enum VisitStatus { + Done(u16), + More(u16), +} + +#[derive(Debug)] +enum VisitError { + TooLong(usize), + TooShort(usize), + Overflow(u32), + Alias, + ByteThreeContinues, +} + +impl VisitError { + fn into_de_error<'de, A>(self) -> A::Error + where + A: SeqAccess<'de>, + { + match self { + VisitError::TooLong(len) => de::Error::invalid_length(len, &"three or fewer bytes"), + VisitError::TooShort(len) => de::Error::invalid_length(len, &"more bytes"), + VisitError::Overflow(val) => de::Error::invalid_value( + de::Unexpected::Unsigned(val as u64), + &"a value in the range [0, 65535]", + ), + VisitError::Alias => de::Error::invalid_value( + de::Unexpected::Other("alias encoding"), + &"strict form encoding", + ), + VisitError::ByteThreeContinues => de::Error::invalid_value( + de::Unexpected::Other("continue signal on byte-three"), + &"a terminal signal on or before byte-three", + ), + } + } +} + +type VisitResult = Result; + +const MAX_ENCODING_LENGTH: usize = 3; +fn visit_byte(elem: u8, val: u16, nth_byte: usize) -> VisitResult { + if elem == 0 && nth_byte != 0 { + return Err(VisitError::Alias); + } + + let val = u32::from(val); + let elem = u32::from(elem); + let elem_val = elem & 0x7f; + let elem_done = (elem & 0x80) == 0; + + if nth_byte >= MAX_ENCODING_LENGTH { + return Err(VisitError::TooLong(nth_byte.saturating_add(1))); + } else if nth_byte == MAX_ENCODING_LENGTH.saturating_sub(1) && !elem_done { + return Err(VisitError::ByteThreeContinues); + } + + let shift = u32::try_from(nth_byte) + .unwrap_or(u32::MAX) + .saturating_mul(7); + let elem_val = elem_val.checked_shl(shift).unwrap_or(u32::MAX); + + let new_val = val | elem_val; + let val = u16::try_from(new_val).map_err(|_| VisitError::Overflow(new_val))?; + + if elem_done { + Ok(VisitStatus::Done(val)) + } else { + Ok(VisitStatus::More(val)) + } +} + +struct ShortU16Visitor; + +impl<'de> Visitor<'de> for ShortU16Visitor { + type Value = ShortU16; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a ShortU16") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + // Decodes an unsigned 16 bit integer one-to-one encoded as follows: + // 1 byte : 0xxxxxxx => 00000000 0xxxxxxx : 0 - 127 + // 2 bytes : 1xxxxxxx 0yyyyyyy => 00yyyyyy yxxxxxxx : 128 - 16,383 + // 3 bytes : 1xxxxxxx 1yyyyyyy 000000zz => zzyyyyyy yxxxxxxx : 16,384 - 65,535 + let mut val: u16 = 0; + for nth_byte in 0..MAX_ENCODING_LENGTH { + let elem: u8 = seq.next_element()?.ok_or_else(|| { + VisitError::TooShort(nth_byte.saturating_add(1)).into_de_error::() + })?; + match visit_byte(elem, val, nth_byte).map_err(|e| e.into_de_error::())? { + VisitStatus::Done(new_val) => return Ok(ShortU16(new_val)), + VisitStatus::More(new_val) => val = new_val, + } + } + + Err(VisitError::ByteThreeContinues.into_de_error::()) + } +} + +impl<'de> Deserialize<'de> for ShortU16 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_tuple(3, ShortU16Visitor) + } +} + +/// If you don't want to use the ShortVec newtype, you can do ShortVec +/// serialization on an ordinary vector with the following field annotation: +/// +/// #[serde(with = "short_vec")] +/// +pub fn serialize( + elements: &[T], + serializer: S, +) -> Result { + // Pass a non-zero value to serialize_tuple() so that serde_json will + // generate an open bracket. + let mut seq = serializer.serialize_tuple(1)?; + + let len = elements.len(); + if len > u16::MAX as usize { + return Err(ser::Error::custom("length larger than u16")); + } + let short_len = ShortU16(len as u16); + seq.serialize_element(&short_len)?; + + for element in elements { + seq.serialize_element(element)?; + } + seq.end() +} + +struct ShortVecVisitor { + _t: PhantomData, +} + +impl<'de, T> Visitor<'de> for ShortVecVisitor +where + T: Deserialize<'de>, +{ + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a Vec with a multi-byte length") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: SeqAccess<'de>, + { + let short_len: ShortU16 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let len = short_len.0 as usize; + + let mut result = Vec::with_capacity(len); + for i in 0..len { + let elem = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &self))?; + result.push(elem); + } + Ok(result) + } +} + +/// If you don't want to use the ShortVec newtype, you can do ShortVec +/// deserialization on an ordinary vector with the following field annotation: +/// +/// #[serde(with = "short_vec")] +/// +pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let visitor = ShortVecVisitor { _t: PhantomData }; + deserializer.deserialize_tuple(usize::MAX, visitor) +} + +pub struct ShortVec(pub Vec); + +impl Serialize for ShortVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize(&self.0, serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for ShortVec { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserialize(deserializer).map(ShortVec) + } +} + +/// Return the decoded value and how many bytes it consumed. +#[allow(clippy::result_unit_err)] +pub fn decode_shortu16_len(bytes: &[u8]) -> Result<(usize, usize), ()> { + let mut val = 0; + for (nth_byte, byte) in bytes.iter().take(MAX_ENCODING_LENGTH).enumerate() { + match visit_byte(*byte, val, nth_byte).map_err(|_| ())? { + VisitStatus::More(new_val) => val = new_val, + VisitStatus::Done(new_val) => { + return Ok((usize::from(new_val), nth_byte.saturating_add(1))); + }, + } + } + Err(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use bincode::{deserialize, serialize}; + + /// Return the serialized length. + fn encode_len(len: u16) -> Vec { + bincode::serialize(&ShortU16(len)).unwrap() + } + + fn assert_len_encoding(len: u16, bytes: &[u8]) { + assert_eq!(encode_len(len), bytes, "unexpected usize encoding"); + assert_eq!( + decode_shortu16_len(bytes).unwrap(), + (usize::from(len), bytes.len()), + "unexpected usize decoding" + ); + } + + #[test] + fn test_short_vec_encode_len() { + assert_len_encoding(0x0, &[0x0]); + assert_len_encoding(0x7f, &[0x7f]); + assert_len_encoding(0x80, &[0x80, 0x01]); + assert_len_encoding(0xff, &[0xff, 0x01]); + assert_len_encoding(0x100, &[0x80, 0x02]); + assert_len_encoding(0x7fff, &[0xff, 0xff, 0x01]); + assert_len_encoding(0xffff, &[0xff, 0xff, 0x03]); + } + + fn assert_good_deserialized_value(value: u16, bytes: &[u8]) { + assert_eq!(value, deserialize::(bytes).unwrap().0); + } + + fn assert_bad_deserialized_value(bytes: &[u8]) { + assert!(deserialize::(bytes).is_err()); + } + + #[test] + fn test_deserialize() { + assert_good_deserialized_value(0x0000, &[0x00]); + assert_good_deserialized_value(0x007f, &[0x7f]); + assert_good_deserialized_value(0x0080, &[0x80, 0x01]); + assert_good_deserialized_value(0x00ff, &[0xff, 0x01]); + assert_good_deserialized_value(0x0100, &[0x80, 0x02]); + assert_good_deserialized_value(0x07ff, &[0xff, 0x0f]); + assert_good_deserialized_value(0x3fff, &[0xff, 0x7f]); + assert_good_deserialized_value(0x4000, &[0x80, 0x80, 0x01]); + assert_good_deserialized_value(0xffff, &[0xff, 0xff, 0x03]); + + // aliases + // 0x0000 + assert_bad_deserialized_value(&[0x80, 0x00]); + assert_bad_deserialized_value(&[0x80, 0x80, 0x00]); + // 0x007f + assert_bad_deserialized_value(&[0xff, 0x00]); + assert_bad_deserialized_value(&[0xff, 0x80, 0x00]); + // 0x0080 + assert_bad_deserialized_value(&[0x80, 0x81, 0x00]); + // 0x00ff + assert_bad_deserialized_value(&[0xff, 0x81, 0x00]); + // 0x0100 + assert_bad_deserialized_value(&[0x80, 0x82, 0x00]); + // 0x07ff + assert_bad_deserialized_value(&[0xff, 0x8f, 0x00]); + // 0x3fff + assert_bad_deserialized_value(&[0xff, 0xff, 0x00]); + + // too short + assert_bad_deserialized_value(&[]); + assert_bad_deserialized_value(&[0x80]); + + // too long + assert_bad_deserialized_value(&[0x80, 0x80, 0x80, 0x00]); + + // too large + // 0x0001_0000 + assert_bad_deserialized_value(&[0x80, 0x80, 0x04]); + // 0x0001_8000 + assert_bad_deserialized_value(&[0x80, 0x80, 0x06]); + } + + #[test] + fn test_short_vec_u8() { + let vec = ShortVec(vec![4u8; 32]); + let bytes = serialize(&vec).unwrap(); + assert_eq!(bytes.len(), vec.0.len() + 1); + + let vec1: ShortVec = deserialize(&bytes).unwrap(); + assert_eq!(vec.0, vec1.0); + } + + #[test] + fn test_short_vec_u8_too_long() { + let vec = ShortVec(vec![4u8; u16::MAX as usize]); + assert!(matches!(serialize(&vec), Ok(_))); + + let vec = ShortVec(vec![4u8; u16::MAX as usize + 1]); + assert!(matches!(serialize(&vec), Err(_))); + } + + #[test] + fn test_short_vec_json() { + let vec = ShortVec(vec![0, 1, 2]); + let s = serde_json::to_string(&vec).unwrap(); + assert_eq!(s, "[[3],0,1,2]"); + } + + #[test] + fn test_short_vec_aliased_length() { + let bytes = [ + 0x81, 0x80, 0x00, // 3-byte alias of 1 + 0x00, + ]; + assert!(deserialize::>(&bytes).is_err()); + } +} diff --git a/rust/chains/tw_solana/src/transaction/v0.rs b/rust/chains/tw_solana/src/transaction/v0.rs new file mode 100644 index 00000000000..8b1a67d0cc9 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/v0.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::transaction::{short_vec, CompiledInstruction, MessageHeader}; +use serde::{Deserialize, Serialize}; +use tw_hash::{as_byte_sequence, H256}; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MessageAddressTableLookup { + /// Address lookup table account key + pub account_key: SolanaAddress, + /// List of indexes used to load writable account addresses + #[serde(with = "short_vec")] + pub writable_indexes: Vec, + /// List of indexes used to load readonly account addresses + #[serde(with = "short_vec")] + pub readonly_indexes: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Message { + /// The message header, identifying signed and read-only `account_keys`. + /// Header values only describe static `account_keys`, they do not describe + /// any additional account keys loaded via address table lookups. + pub header: MessageHeader, + + /// List of accounts loaded by this transaction. + #[serde(with = "short_vec")] + pub account_keys: Vec, + + /// The blockhash of a recent block. + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + + /// Instructions that invoke a designated program, are executed in sequence, + /// and committed in one atomic transaction if all succeed. + /// + /// # Notes + /// + /// Program indexes must index into the list of message `account_keys` because + /// program id's cannot be dynamically loaded from a lookup table. + /// + /// Account indexes must index into the list of addresses + /// constructed from the concatenation of three key lists: + /// 1) message `account_keys` + /// 2) ordered list of keys loaded from `writable` lookup table indexes + /// 3) ordered list of keys loaded from `readable` lookup table indexes + #[serde(with = "short_vec")] + pub instructions: Vec, + + /// List of address table lookups used to load additional accounts + /// for this transaction. + #[serde(with = "short_vec")] + pub address_table_lookups: Vec, +} diff --git a/rust/chains/tw_solana/src/transaction/versioned.rs b/rust/chains/tw_solana/src/transaction/versioned.rs new file mode 100644 index 00000000000..ade097c4b7d --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/versioned.rs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Source code: https://github.com/solana-labs/solana/blob/a16f982169eb197fad0eb8c58c307fb069f69d8f/sdk/program/src/message/versions/mod.rs + +use crate::address::SolanaAddress; +use crate::blockhash::Blockhash; +use crate::transaction::{legacy, short_vec, v0, CompiledInstruction, MessageHeader, Signature}; +use serde::de::{SeqAccess, Unexpected, Visitor}; +use serde::ser::SerializeTuple; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use tw_hash::{as_byte_sequence, H256}; + +/// Bit mask that indicates whether a serialized message is versioned. +pub const MESSAGE_VERSION_PREFIX: u8 = 0x80; + +// NOTE: Serialization-related changes must be paired with the direct read at sigverify. +/// An atomic transaction +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct VersionedTransaction { + /// List of signatures + #[serde(with = "short_vec")] + pub signatures: Vec, + /// Message to sign. + pub message: VersionedMessage, +} + +impl VersionedTransaction { + pub fn unsigned(message: VersionedMessage) -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default(); message.num_required_signatures()], + message, + } + } +} + +/// Either a legacy message or a v0 message. +/// +/// # Serialization +/// +/// If the first bit is set, the remaining 7 bits will be used to determine +/// which message version is serialized starting from version `0`. If the first +/// is bit is not set, all bytes are used to encode the legacy `Message` +/// format. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum VersionedMessage { + Legacy(legacy::Message), + V0(v0::Message), +} + +impl VersionedMessage { + pub fn header(&self) -> &MessageHeader { + match self { + VersionedMessage::Legacy(legacy) => &legacy.header, + VersionedMessage::V0(v0) => &v0.header, + } + } + + pub fn account_keys(&self) -> &[SolanaAddress] { + match self { + VersionedMessage::Legacy(legacy) => &legacy.account_keys, + VersionedMessage::V0(v0) => &v0.account_keys, + } + } + + pub fn signers(&self) -> impl Iterator { + let signatures_count = self.num_required_signatures(); + match self { + VersionedMessage::Legacy(legacy) => &legacy.account_keys, + VersionedMessage::V0(v0) => &v0.account_keys, + } + .iter() + .take(signatures_count) + } + + pub fn recent_blockhash(&self) -> Blockhash { + match self { + VersionedMessage::Legacy(legacy) => Blockhash::with_bytes(legacy.recent_blockhash), + VersionedMessage::V0(v0) => Blockhash::with_bytes(v0.recent_blockhash), + } + } + + pub fn instructions(&self) -> &[CompiledInstruction] { + match self { + VersionedMessage::Legacy(legacy) => &legacy.instructions, + VersionedMessage::V0(v0) => &v0.instructions, + } + } + + pub fn num_required_signatures(&self) -> usize { + match self { + VersionedMessage::Legacy(legacy) => legacy.header.num_required_signatures as usize, + VersionedMessage::V0(v0) => v0.header.num_required_signatures as usize, + } + } + + pub fn get_account_index(&self, account: SolanaAddress) -> Option { + let account_keys = match self { + VersionedMessage::Legacy(legacy) => &legacy.account_keys, + VersionedMessage::V0(v0) => &v0.account_keys, + }; + account_keys.iter().position(|pk| *pk == account) + } + + pub fn set_recent_blockhash(&mut self, recent_blockhash: H256) { + match self { + VersionedMessage::Legacy(legacy) => legacy.recent_blockhash = recent_blockhash, + VersionedMessage::V0(v0) => v0.recent_blockhash = recent_blockhash, + } + } +} + +impl Serialize for VersionedMessage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Legacy(message) => { + let mut seq = serializer.serialize_tuple(1)?; + seq.serialize_element(message)?; + seq.end() + }, + Self::V0(message) => { + let mut seq = serializer.serialize_tuple(2)?; + seq.serialize_element(&MESSAGE_VERSION_PREFIX)?; + seq.serialize_element(message)?; + seq.end() + }, + } + } +} + +enum MessagePrefix { + Legacy(u8), + Versioned(u8), +} + +impl<'de> Deserialize<'de> for MessagePrefix { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PrefixVisitor; + + impl<'de> Visitor<'de> for PrefixVisitor { + type Value = MessagePrefix; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("message prefix byte") + } + + // Serde's integer visitors bubble up to u64 so check the prefix + // with this function instead of visit_u8. This approach is + // necessary because serde_json directly calls visit_u64 for + // unsigned integers. + fn visit_u64(self, value: u64) -> Result { + if value > u8::MAX as u64 { + Err(de::Error::invalid_type(Unexpected::Unsigned(value), &self))?; + } + + let byte = value as u8; + if byte & MESSAGE_VERSION_PREFIX != 0 { + Ok(MessagePrefix::Versioned(byte & !MESSAGE_VERSION_PREFIX)) + } else { + Ok(MessagePrefix::Legacy(byte)) + } + } + } + + deserializer.deserialize_u8(PrefixVisitor) + } +} + +impl<'de> Deserialize<'de> for VersionedMessage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MessageVisitor; + + impl<'de> Visitor<'de> for MessageVisitor { + type Value = VersionedMessage; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("message bytes") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let prefix: MessagePrefix = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match prefix { + MessagePrefix::Legacy(num_required_signatures) => { + // The remaining fields of the legacy Message struct after the first byte. + #[derive(Serialize, Deserialize)] + struct RemainingLegacyMessage { + pub num_readonly_signed_accounts: u8, + pub num_readonly_unsigned_accounts: u8, + #[serde(with = "short_vec")] + pub account_keys: Vec, + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + #[serde(with = "short_vec")] + pub instructions: Vec, + } + + let message: RemainingLegacyMessage = + seq.next_element()?.ok_or_else(|| { + // will never happen since tuple length is always 2 + de::Error::invalid_length(1, &self) + })?; + + Ok(VersionedMessage::Legacy(legacy::Message { + header: MessageHeader { + num_required_signatures, + num_readonly_signed_accounts: message.num_readonly_signed_accounts, + num_readonly_unsigned_accounts: message + .num_readonly_unsigned_accounts, + }, + account_keys: message.account_keys, + recent_blockhash: message.recent_blockhash, + instructions: message.instructions, + })) + }, + MessagePrefix::Versioned(version) => { + match version { + 0 => { + Ok(VersionedMessage::V0(seq.next_element()?.ok_or_else( + || { + // will never happen since tuple length is always 2 + de::Error::invalid_length(1, &self) + }, + )?)) + }, + 127 => { + // 0xff is used as the first byte of the off-chain messages + // which corresponds to version 127 of the versioned messages. + // This explicit check is added to prevent the usage of version 127 + // in the runtime as a valid transaction. + Err(de::Error::custom("off-chain messages are not accepted")) + }, + _ => Err(de::Error::invalid_value( + de::Unexpected::Unsigned(version as u64), + &"a valid transaction message version", + )), + } + }, + } + } + } + + deserializer.deserialize_tuple(2, MessageVisitor) + } +} diff --git a/rust/chains/tw_solana/tests/get_default_token_address.rs b/rust/chains/tw_solana/tests/get_default_token_address.rs new file mode 100644 index 00000000000..4f075dde885 --- /dev/null +++ b/rust/chains/tw_solana/tests/get_default_token_address.rs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_solana::address::SolanaAddress; +use tw_solana::blockhash::Blockhash; +use tw_solana::defined_addresses::TOKEN_PROGRAM_ID_ADDRESS; +use tw_solana::program::stake_program::StakeProgram; + +fn test_get_default_token_address_impl( + main_address: &str, + token_mint_address: &str, + expected: &str, +) { + let main_address = SolanaAddress::from_str(main_address).unwrap(); + let token_mint_address = SolanaAddress::from_str(token_mint_address).unwrap(); + let expected = SolanaAddress::from_str(expected).unwrap(); + + let actual = StakeProgram::get_associated_token_address( + main_address, + *TOKEN_PROGRAM_ID_ADDRESS, + token_mint_address, + ) + .expect("!get_associated_token_address"); + assert_eq!(actual, expected); +} + +#[test] +fn test_get_default_token_address() { + test_get_default_token_address_impl( + "B1iGmDJdvmxyUiYM8UEo2Uw2D58EmUrw4KyLYMmrhf8V", + "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "EDNd1ycsydWYwVmrYZvqYazFqwk1QjBgAUKFjBoz1jKP", + ); + test_get_default_token_address_impl( + "FtsZbVDYGBUw4R5Rcy8p58RAdYRFUJcAiBRdQAona7t1", + "FQYWEccPpAVR5Q16FKoTFUa6z8BWzdK5eh3D586fdQbM", + "2XGUJePx3CJSYQvAQcSgLf7xpjCdMUgx8LNaSCUhE2LS", + ); + test_get_default_token_address_impl( + "74nsHsFivzUPLEygULuZLs193MnDNZfnKrSEkgA4qkY7", + "FQYWEccPpAVR5Q16FKoTFUa6z8BWzdK5eh3D586fdQbM", + "823YRj6GozU23rwSyd1f78vPopVoQPCeK79ppWCu5SYQ", + ); + test_get_default_token_address_impl( + "HBYC51YrGFAZ8rM7Sj8e9uqKggpSrDYrinQDZzvMtqQp", + "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "6X4X1Ae24mkoWeCEpktevySVG9jzeCufut5vtUW3wFrD", + ); + test_get_default_token_address_impl( + "Eg5jqooyG6ySaXKbQUu4Lpvu2SqUPZrNkM4zXs9iUDLJ", + "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "ANVCrmRw7Ww7rTFfMbrjApSPXEEcZpBa6YEiBdf98pAf", + ); +} + +fn test_address_from_recent_blockhash_impl(main_address: &str, blockhash: &str, expected: &str) { + let main_address = SolanaAddress::from_str(main_address).unwrap(); + let expected = SolanaAddress::from_str(expected).unwrap(); + let blockhash = Blockhash::from_str(blockhash).unwrap(); + + let actual = StakeProgram::address_from_recent_blockhash(&main_address, &blockhash); + assert_eq!(actual, expected); +} + +#[test] +fn test_address_from_recent_blockhash() { + test_address_from_recent_blockhash_impl( + "zVSpQnbBZ7dyUWzXhrUQRsTYYNzoAdJWHsHSqhPj3Xu", + "11111111111111111111111111111111", + "GQDDc5EVGJZFC7AvpEJ8eoCQ75Yy4gr7eu17frCjvQRQ", + ); + test_address_from_recent_blockhash_impl( + "zVSpQnbBZ7dyUWzXhrUQRsTYYNzoAdJWHsHSqhPj3Xu", + "9ipJh5xfyoyDaiq8trtrdqQeAhQbQkWy2eANizKvx75K", + "2Kos1xJRBq3Ae1GnVNBx7HgJhq8KvdUe2bXE4QGdNaXb", + ); + test_address_from_recent_blockhash_impl( + "B1iGmDJdvmxyUiYM8UEo2Uw2D58EmUrw4KyLYMmrhf8V", + "AfzzEC8NVXoxKoHdjXLDVzqwqvvZmgPuqyJqjuHiPY1D", + "Fxhhm82PZVuXEwycT28vGqknUEnVeoHh4UWEnJNQUDbv", + ); +} diff --git a/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs new file mode 100644 index 00000000000..5521e7069af --- /dev/null +++ b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::*; +use tw_encoding::base58; +use tw_solana::modules::utils::SolanaTransaction; +use tw_solana::SOLANA_ALPHABET; + +#[test] +fn test_update_recent_blockhash_and_sign() { + // base64 encoded + let encoded_tx = "AQPWaOi7dMdmQpXi8HyQQKwiqIftrg1igGQxGtZeT50ksn4wAnyH4DtDrkkuE0fqgx80LTp4LwNN9a440SrmoA8BAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA="; + // base58 encoded + let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; + let private_key = base58::decode( + "A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr", + SOLANA_ALPHABET, + ) + .unwrap(); + + let output = + SolanaTransaction::update_blockhash_and_sign(encoded_tx, new_blockhash, &[private_key]); + assert_eq!(output.error, SigningErrorType::OK); + let expected = "AdQl49kO1FxfkAnAuK9KSQEGLzxHNYLqBrYGFN711q7aT/qyrzYMn/7/IdFBy6yMhjOA1CkwZsgmqmbu+XKvVAUBAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAseKSLpOE0fdq67Jk9Ckme2c3SYD//nqcykr/oci67zEBAgIAAQwCAAAAKgAAAAAAAAA="; + assert_eq!(output.encoded, expected); +} diff --git a/rust/chains/tw_sui/Cargo.toml b/rust/chains/tw_sui/Cargo.toml new file mode 100644 index 00000000000..1aa3d7cabb3 --- /dev/null +++ b/rust/chains/tw_sui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_sui" +version = "0.1.0" +edition = "2021" + +[dependencies] +indexmap = "2.0" +move-core-types = { git = "https://github.com/move-language/move", rev = "ea70797099baea64f05194a918cebd69ed02b285", features = ["address32"] } +serde = { version = "1.0", features = ["derive"] } +serde_repr = "0.1" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_sui/fuzz/.gitignore b/rust/chains/tw_sui/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_sui/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_sui/fuzz/Cargo.toml b/rust/chains/tw_sui/fuzz/Cargo.toml new file mode 100644 index 00000000000..32ce151dfaa --- /dev/null +++ b/rust/chains/tw_sui/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_sui-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_sui] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..bf0158d275f --- /dev/null +++ b/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::Sui::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::Sui, input); +}); diff --git a/rust/chains/tw_sui/src/address.rs b/rust/chains/tw_sui/src/address.rs new file mode 100644 index 00000000000..063418906dc --- /dev/null +++ b/rust/chains/tw_sui/src/address.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use move_core_types::account_address::AccountAddress; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_encoding::hex; +use tw_hash::blake2::blake2_b; +use tw_keypair::ed25519; +use tw_memory::Data; + +#[repr(u8)] +pub enum Scheme { + Ed25519 = 0, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct SuiAddress(AccountAddress); + +impl SuiAddress { + pub const LENGTH: usize = AccountAddress::LENGTH; + + /// Initializes an address with a `ed25519` public key. + pub fn with_ed25519_pubkey(pubkey: &ed25519::sha512::PublicKey) -> AddressResult { + const CAPACITY: usize = ed25519::sha512::PublicKey::LEN + 1; + + let mut to_hash = Vec::with_capacity(CAPACITY); + to_hash.push(Scheme::Ed25519 as u8); + to_hash.extend_from_slice(pubkey.as_slice()); + let hashed = + blake2_b(to_hash.as_slice(), SuiAddress::LENGTH).map_err(|_| AddressError::Internal)?; + + AccountAddress::from_bytes(hashed) + .map(SuiAddress) + .map_err(|_| AddressError::Internal) + } + + pub fn into_inner(self) -> AccountAddress { + self.0 + } +} + +impl FromStr for SuiAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s).map_err(|_| AddressError::FromHexError)?; + AccountAddress::from_bytes(bytes) + .map(SuiAddress) + .map_err(|_| AddressError::InvalidInput) + } +} + +impl fmt::Display for SuiAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prefixed = true; + write!(f, "{}", hex::encode(self.0.as_ref(), prefixed)) + } +} + +impl CoinAddress for SuiAddress { + #[inline] + fn data(&self) -> Data { + self.0.to_vec() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_keypair::ed25519::sha512::PrivateKey; + + #[test] + fn test_from_public_key() { + let private = PrivateKey::try_from( + "088baa019f081d6eab8dff5c447f9ce2f83c1babf3d03686299eaf6a1e89156e", + ) + .unwrap(); + let public = private.public(); + let addr = SuiAddress::with_ed25519_pubkey(&public).unwrap(); + assert_eq!( + addr.to_string(), + "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015" + ); + } + + /// https://github.com/trustwallet/wallet-core/issues/3837 + #[test] + fn test_sui_address_str_with_leading_zero() { + let s = "0x0cf10169225a251113b3198dc81d15ba72286f73353a8212f03bad10bd0f0a99"; + let addr = SuiAddress::from_str(s).unwrap(); + assert_eq!(addr.to_string(), s); + } +} diff --git a/rust/chains/tw_sui/src/compiler.rs b/rust/chains/tw_sui/src/compiler.rs new file mode 100644 index 00000000000..1da7236a8f3 --- /dev/null +++ b/rust/chains/tw_sui/src/compiler.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::{TWTransaction, TWTransactionBuilder}; +use crate::modules::tx_signer::{TransactionPreimage, TxSigner}; +use crate::signature::SuiSignatureInfo; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::common::compile_input::SingleSignaturePubkey; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_encoding::base64::{self, STANDARD}; +use tw_keypair::ed25519; +use tw_proto::Sui::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SuiCompiler; + +impl SuiCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let builder = TWTransactionBuilder::new(input); + let tx_to_sign = builder.build()?; + + let TransactionPreimage { + tx_data_to_sign, + tx_hash_to_sign, + .. + } = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::preimage(&tx)?, + TWTransaction::SignDirect(tx_data) => TxSigner::preimage_direct(tx_data)?, + }; + + Ok(CompilerProto::PreSigningOutput { + data: Cow::from(tx_data_to_sign), + data_hash: Cow::from(tx_hash_to_sign.to_vec()), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult> { + let builder = TWTransactionBuilder::new(input); + let tx_to_sign = builder.build()?; + + let TransactionPreimage { + unsigned_tx_data, .. + } = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::preimage(&tx), + TWTransaction::SignDirect(tx_data) => TxSigner::preimage_direct(tx_data), + }?; + + let SingleSignaturePubkey { + signature: raw_signature, + public_key: public_key_bytes, + } = SingleSignaturePubkey::from_sign_pubkey_list(signatures, public_keys)?; + + let signature = ed25519::Signature::try_from(raw_signature.as_slice())?; + let public_key = ed25519::sha512::PublicKey::try_from(public_key_bytes.as_slice())?; + + let signature_info = SuiSignatureInfo::ed25519(&signature, &public_key); + + let unsigned_tx = base64::encode(&unsigned_tx_data, STANDARD); + Ok(Proto::SigningOutput { + unsigned_tx: Cow::from(unsigned_tx), + signature: Cow::from(signature_info.to_base64()), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_sui/src/constants.rs b/rust/chains/tw_sui/src/constants.rs new file mode 100644 index 00000000000..35c5d525f3e --- /dev/null +++ b/rust/chains/tw_sui/src/constants.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::sui_types::{ObjectID, SequenceNumber}; +use move_core_types::account_address::AccountAddress; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; + +pub const OBJECT_START_VERSION: SequenceNumber = SequenceNumber(1); + +/// 0x5: hardcoded object ID for the singleton sui system state object. +pub const SUI_SYSTEM_STATE_ADDRESS: AccountAddress = address_from_single_byte(5); +pub const SUI_SYSTEM_STATE_OBJECT_ID: ObjectID = ObjectID(SUI_SYSTEM_STATE_ADDRESS); +pub const SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION: SequenceNumber = OBJECT_START_VERSION; + +/// 0x3-- account address where sui system modules are stored +/// Same as the ObjectID +pub const SUI_SYSTEM_ADDRESS: AccountAddress = address_from_single_byte(3); +pub const SUI_SYSTEM_PACKAGE_ID: ObjectID = ObjectID(SUI_SYSTEM_ADDRESS); + +pub const SUI_SYSTEM_MODULE_NAME: &IdentStr = ident_str!("sui_system"); +pub const ADD_STAKE_MUL_COIN_FUN_NAME: &IdentStr = ident_str!("request_add_stake_mul_coin"); +pub const WITHDRAW_STAKE_FUN_NAME: &IdentStr = ident_str!("request_withdraw_stake"); + +const fn address_from_single_byte(b: u8) -> AccountAddress { + let mut addr = [0u8; AccountAddress::LENGTH]; + addr[AccountAddress::LENGTH - 1] = b; + AccountAddress::new(addr) +} diff --git a/rust/chains/tw_sui/src/entry.rs b/rust/chains/tw_sui/src/entry.rs new file mode 100644 index 00000000000..63bd4e92df7 --- /dev/null +++ b/rust/chains/tw_sui/src/entry.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::compiler::SuiCompiler; +use crate::modules::transaction_util::SuiTransactionUtil; +use crate::signer::SuiSigner; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Sui::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SuiEntry; + +impl CoinEntry for SuiEntry { + type AddressPrefix = NoPrefix; + type Address = SuiAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = SuiTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + SuiAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + SuiAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let ed25519_public = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + SuiAddress::with_ed25519_pubkey(ed25519_public) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + SuiSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + SuiCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + SuiCompiler::compile(coin, input, signatures, public_keys) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(SuiTransactionUtil) + } +} diff --git a/rust/chains/tw_sui/src/lib.rs b/rust/chains/tw_sui/src/lib.rs new file mode 100644 index 00000000000..9a2411b8058 --- /dev/null +++ b/rust/chains/tw_sui/src/lib.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod constants; +pub mod entry; +pub mod modules; +pub mod signature; +pub mod signer; +pub mod transaction; diff --git a/rust/chains/tw_sui/src/modules/mod.rs b/rust/chains/tw_sui/src/modules/mod.rs new file mode 100644 index 00000000000..32f928c8ec3 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod transaction_util; +pub mod tx_builder; +pub mod tx_signer; diff --git a/rust/chains/tw_sui/src/modules/transaction_util.rs b/rust/chains/tw_sui/src/modules/transaction_util.rs new file mode 100644 index 00000000000..f2ad4ff585f --- /dev/null +++ b/rust/chains/tw_sui/src/modules/transaction_util.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_encoding::base58::{self, Alphabet}; +use tw_encoding::base64::{self, STANDARD}; +use tw_hash::blake2::blake2_b; +use tw_hash::H256; + +pub struct SuiTransactionUtil; + +impl TransactionUtil for SuiTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +// See: https://github.com/mofalabs/sui/blob/74908b3ad8b82e5e401d5017fed4fa7dc2361569/lib/builder/hash.dart#L7 +fn hash_typed_data(type_tag: &str, data: &[u8]) -> Result, tw_hash::Error> { + let type_tag_bytes: Vec = format!("{}::", type_tag).into_bytes(); + + let mut data_with_tag = Vec::with_capacity(type_tag_bytes.len() + data.len()); + data_with_tag.extend_from_slice(&type_tag_bytes); + data_with_tag.extend_from_slice(data); + + blake2_b(&data_with_tag, H256::LEN) +} + +impl SuiTransactionUtil { + fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + let tx = base64::decode(encoded_tx, STANDARD)?; + let tx_hash = hash_typed_data("TransactionData", &tx) + .map_err(|_| SigningErrorType::Error_input_parse)?; + + Ok(base58::encode(&tx_hash, Alphabet::Bitcoin)) + } +} diff --git a/rust/chains/tw_sui/src/modules/tx_builder.rs b/rust/chains/tw_sui/src/modules/tx_builder.rs new file mode 100644 index 00000000000..301f9f3cde4 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/tx_builder.rs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::sui_types::{ObjectDigest, ObjectID, ObjectRef, SequenceNumber}; +use crate::transaction::transaction_builder::TransactionBuilder; +use crate::transaction::transaction_data::TransactionData; +use std::borrow::Cow; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base64::{self, STANDARD}; +use tw_keypair::ed25519; +use tw_keypair::traits::KeyPairTrait; +use tw_memory::Data; +use tw_proto::Sui::Proto; +use tw_proto::Sui::Proto::mod_SigningInput::OneOftransaction_payload as TransactionType; + +pub enum TWTransaction { + Transaction(TransactionData), + SignDirect(Data), +} + +pub struct TWTransactionBuilder<'a> { + input: Proto::SigningInput<'a>, +} + +impl<'a> TWTransactionBuilder<'a> { + pub fn new(input: Proto::SigningInput<'a>) -> Self { + TWTransactionBuilder { input } + } + + pub fn signer_key(&self) -> SigningResult { + ed25519::sha512::KeyPair::try_from(self.input.private_key.as_ref()) + .map_err(SigningError::from) + } + + pub fn build(self) -> SigningResult { + let tx_data = match self.input.transaction_payload { + TransactionType::sign_direct_message(ref direct) => { + let raw_data = self.sign_direct_from_proto(direct)?; + return Ok(TWTransaction::SignDirect(raw_data)); + }, + TransactionType::pay_sui(ref pay_sui) => self.pay_sui_from_proto(pay_sui), + TransactionType::pay_all_sui(ref pay_all_sui) => { + self.pay_all_sui_from_proto(pay_all_sui) + }, + TransactionType::pay(ref pay) => self.pay_from_proto(pay), + TransactionType::request_add_stake(ref stake) => self.stake_from_proto(stake), + TransactionType::request_withdraw_stake(ref withdraw) => { + self.withdraw_from_proto(withdraw) + }, + TransactionType::transfer_object(ref transfer_obj) => { + self.transfer_object_from_proto(transfer_obj) + }, + TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params), + }?; + Ok(TWTransaction::Transaction(tx_data)) + } + + fn sign_direct_from_proto(&self, sign_direct: &Proto::SignDirect<'_>) -> SigningResult { + base64::decode(&sign_direct.unsigned_tx_msg, STANDARD) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error parsing Raw Unsigned TX message as base64") + } + + fn pay_sui_from_proto(&self, pay_sui: &Proto::PaySui<'_>) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay_sui.input_coins)?; + let recipients = Self::parse_addresses(&pay_sui.recipients) + .context("Invalid one of the recipients addresses")?; + + TransactionBuilder::pay_sui( + signer, + input_coins, + recipients, + pay_sui.amounts.clone(), + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn pay_all_sui_from_proto( + &self, + pay_all_sui: &Proto::PayAllSui<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay_all_sui.input_coins)?; + let recipient = SuiAddress::from_str(&pay_all_sui.recipient) + .into_tw() + .context("Invalid recipient address")?; + + TransactionBuilder::pay_all_sui( + signer, + input_coins, + recipient, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn pay_from_proto(&self, pay: &Proto::Pay<'_>) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay.input_coins)?; + let recipients = Self::parse_addresses(&pay.recipients) + .context("Invalid one of the recipients addresses")?; + let gas = Self::require_coin(&pay.gas).context("No 'gas' coin specified")?; + + TransactionBuilder::pay( + signer, + input_coins, + recipients, + pay.amounts.clone(), + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn stake_from_proto( + &self, + stake: &Proto::RequestAddStake<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let input_coins = Self::build_coins(&stake.coins)?; + let amount = stake.amount.as_ref().map(|a| a.amount); + let validator = SuiAddress::from_str(stake.validator.as_ref()) + .into_tw() + .context("Invalid validator address")?; + let gas = Self::require_coin(&stake.gas).context("No 'gas' coin specified")?; + + TransactionBuilder::request_add_stake( + signer, + input_coins, + amount, + validator, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn withdraw_from_proto( + &self, + withdraw: &Proto::RequestWithdrawStake<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let staked_sui = + Self::require_coin(&withdraw.staked_sui).context("No 'staked_sui' coin specified")?; + let gas = Self::require_coin(&withdraw.gas).context("No 'gas' coin specified")?; + + TransactionBuilder::request_withdraw_stake( + signer, + staked_sui, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn transfer_object_from_proto( + &self, + transfer_obj: &Proto::TransferObject<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let recipient = SuiAddress::from_str(&transfer_obj.recipient) + .into_tw() + .context("Invalid recipient address")?; + let object = Self::require_coin(&transfer_obj.object).context("No 'object' specified")?; + let gas = Self::require_coin(&transfer_obj.gas).context("No 'gas' coin specified")?; + + TransactionBuilder::transfer_object( + signer, + object, + recipient, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn signer_address(&self) -> SigningResult { + if self.input.private_key.is_empty() { + SuiAddress::from_str(&self.input.signer) + .into_tw() + .context("Invalid signer address") + } else { + let keypair = self.signer_key()?; + SuiAddress::with_ed25519_pubkey(keypair.public()).map_err(SigningError::from) + } + } + + fn build_coins(coins: &[Proto::ObjectRef]) -> SigningResult> { + coins.iter().map(Self::build_coin).collect() + } + + fn require_coin(maybe_coin: &Option) -> SigningResult { + let coin = maybe_coin + .as_ref() + .or_tw_err(SigningErrorType::Error_invalid_params)?; + Self::build_coin(coin) + } + + fn build_coin(coin: &Proto::ObjectRef) -> SigningResult { + let object_id = ObjectID::from_str(coin.object_id.as_ref()).context("Invalid Object ID")?; + let version = SequenceNumber(coin.version); + let object_digest = ObjectDigest::from_str(coin.object_digest.as_ref())?; + + Ok((object_id, version, object_digest)) + } + + fn parse_addresses(addresses: &[Cow<'_, str>]) -> SigningResult> { + let mut res = Vec::with_capacity(addresses.len()); + for addr in addresses { + res.push(SuiAddress::from_str(addr.as_ref())?); + } + Ok(res) + } +} diff --git a/rust/chains/tw_sui/src/modules/tx_signer.rs b/rust/chains/tw_sui/src/modules/tx_signer.rs new file mode 100644 index 00000000000..34eb476f48b --- /dev/null +++ b/rust/chains/tw_sui/src/modules/tx_signer.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::signature::SuiSignatureInfo; +use crate::transaction::transaction_data::TransactionData; +use serde::Serialize; +use serde_repr::Serialize_repr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bcs; +use tw_hash::blake2::blake2_b; +use tw_hash::H256; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_memory::Data; + +/// This enums specifies the intent scope. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentScope { + /// Used for a user signature on a transaction data. + TransactionData = 0, +} + +/// The version here is to distinguish between signing different versions of the struct +/// or enum. Serialized output between two different versions of the same struct/enum +/// might accidentally (or maliciously on purpose) match. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentVersion { + V0 = 0, +} + +/// This enums specifies the application ID. Two intents in two different applications +/// (i.e., Narwhal, Sui, Ethereum etc) should never collide, so that even when a signing +/// key is reused, nobody can take a signature designated for app_1 and present it as a +/// valid signature for an (any) intent in app_2. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum AppId { + Sui = 0, +} + +/// An intent is a compact struct serves as the domain separator for a message that a signature commits to. +/// It consists of three parts: [enum IntentScope] (what the type of the message is), +/// [enum IntentVersion], [enum AppId] (what application that the signature refers to). +/// It is used to construct [struct IntentMessage] that what a signature commits to. +/// +/// The serialization of an Intent is a 3-byte array where each field is represented by a byte. +#[derive(Serialize)] +pub struct Intent { + pub scope: IntentScope, + pub version: IntentVersion, + pub app_id: AppId, +} + +/// Intent Message is a wrapper around a message with its intent. The message can +/// be any type that implements [trait Serialize]. *ALL* signatures in Sui must commits +/// to the intent message, not the message itself. This guarantees any intent +/// message signed in the system cannot collide with another since they are domain +/// separated by intent. +/// +/// The serialization of an IntentMessage is compact: it only appends three bytes +/// to the message itself. +#[derive(Serialize)] +pub struct IntentMessage { + pub intent: Intent, + pub value: T, +} + +pub struct TransactionPreimage { + /// Transaction `bcs` encoded representation. + pub unsigned_tx_data: Data, + /// [`TransactionPreimage::unsigned_tx_data`] extended with the `IntentMessage`. + pub tx_data_to_sign: Data, + /// Hash of the [`TransactionPreimage::tx_data_to_sign`]. + pub tx_hash_to_sign: H256, +} + +pub struct TxSigner; + +impl TxSigner { + pub fn sign( + tx: &TransactionData, + signer_key: &ed25519::sha512::KeyPair, + ) -> SigningResult<(TransactionPreimage, SuiSignatureInfo)> { + let public_key = signer_key.public(); + let signer_address = SuiAddress::with_ed25519_pubkey(public_key)?; + if signer_address != tx.sender() { + return SigningError::err(SigningErrorType::Error_missing_private_key) + .context("Given private key does not belong to the sender address"); + } + + let unsigned_tx_data = bcs::encode(tx) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing TransactionData")?; + Self::sign_direct(unsigned_tx_data, signer_key) + } + + pub fn sign_direct( + unsigned_tx_data: Data, + signer_key: &ed25519::sha512::KeyPair, + ) -> SigningResult<(TransactionPreimage, SuiSignatureInfo)> { + let preimage = Self::preimage_direct(unsigned_tx_data)?; + let signature = signer_key.sign(preimage.tx_hash_to_sign.into_vec())?; + let signature_info = SuiSignatureInfo::ed25519(&signature, signer_key.public()); + Ok((preimage, signature_info)) + } + + pub fn preimage(tx: &TransactionData) -> SigningResult { + let unsigned_tx_data = bcs::encode(tx) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing TransactionData")?; + Self::preimage_direct(unsigned_tx_data) + } + + pub fn preimage_direct(unsigned_tx_data: Data) -> SigningResult { + let intent = Intent { + scope: IntentScope::TransactionData, + version: IntentVersion::V0, + app_id: AppId::Sui, + }; + let intent_data = bcs::encode(&intent) + .tw_err(|_| SigningErrorType::Error_internal) + .context("Error serializing Intent message")?; + + let tx_data_to_sign: Data = intent_data + .into_iter() + .chain(unsigned_tx_data.iter().copied()) + .collect(); + let tx_hash_to_sign = blake2_b(&tx_data_to_sign, H256::LEN) + .and_then(|hash| H256::try_from(hash.as_slice())) + .tw_err(|_| SigningErrorType::Error_internal)?; + + Ok(TransactionPreimage { + unsigned_tx_data, + tx_data_to_sign, + tx_hash_to_sign, + }) + } +} diff --git a/rust/chains/tw_sui/src/signature.rs b/rust/chains/tw_sui/src/signature.rs new file mode 100644 index 00000000000..e03aacad05f --- /dev/null +++ b/rust/chains/tw_sui/src/signature.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::base64::{self, STANDARD}; +use tw_hash::{H256, H512}; +use tw_keypair::ed25519; +use tw_memory::Data; + +#[derive(Clone, Copy)] +#[repr(u8)] +pub enum SignatureScheme { + ED25519 = 0, +} + +pub struct SuiSignatureInfo { + scheme: SignatureScheme, + signature: H512, + public_key: H256, +} + +impl SuiSignatureInfo { + pub fn ed25519( + signature: &ed25519::Signature, + public_key: &ed25519::sha512::PublicKey, + ) -> SuiSignatureInfo { + SuiSignatureInfo { + scheme: SignatureScheme::ED25519, + signature: signature.to_bytes(), + public_key: public_key.to_bytes(), + } + } + + pub fn to_vec(&self) -> Data { + let mut scheme: Data = Vec::with_capacity(H512::LEN + H256::LEN + 1); + scheme.push(self.scheme as u8); + scheme.extend_from_slice(self.signature.as_slice()); + scheme.extend_from_slice(self.public_key.as_slice()); + scheme + } + + pub fn to_base64(&self) -> String { + base64::encode(&self.to_vec(), STANDARD) + } +} diff --git a/rust/chains/tw_sui/src/signer.rs b/rust/chains/tw_sui/src/signer.rs new file mode 100644 index 00000000000..4cf4df45514 --- /dev/null +++ b/rust/chains/tw_sui/src/signer.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::{TWTransaction, TWTransactionBuilder}; +use crate::modules::tx_signer::TxSigner; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_encoding::base64::{self, STANDARD}; +use tw_proto::Sui::Proto; + +pub struct SuiSigner; + +impl SuiSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let builder = TWTransactionBuilder::new(input); + let signer_key = builder.signer_key()?; + let tx_to_sign = builder.build()?; + + let (preimage, signature) = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::sign(&tx, &signer_key)?, + TWTransaction::SignDirect(tx_data) => TxSigner::sign_direct(tx_data, &signer_key)?, + }; + + let unsigned_tx = base64::encode(&preimage.unsigned_tx_data, STANDARD); + Ok(Proto::SigningOutput { + unsigned_tx: Cow::from(unsigned_tx), + signature: Cow::from(signature.to_base64()), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_sui/src/transaction/command.rs b/rust/chains/tw_sui/src/transaction/command.rs new file mode 100644 index 00000000000..533c0be3df2 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/command.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::sui_types::ObjectID; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; + +/// A single command in a programmable transaction. +#[derive(Debug, Deserialize, Serialize)] +pub enum Command { + /// A call to either an entry or a public Move function + MoveCall(Box), + /// `(Vec, address)` + /// It sends n-objects to the specified address. These objects must have store + /// (public transfer) and either the previous owner must be an address or the object must + /// be newly created. + TransferObjects(Vec, Argument), + /// `(&mut Coin, Vec)` -> `Vec>` + /// It splits off some amounts into a new coins with those amounts + SplitCoins(Argument, Vec), + /// `(&mut Coin, Vec>)` + /// It merges n-coins into the first coin + MergeCoins(Argument, Vec), + /// Publishes a Move package. It takes the package bytes and a list of the package's transitive + /// dependencies to link against on-chain. + Publish(Vec>, Vec), + /// `forall T: Vec -> vector` + /// Given n-values of the same type, it constructs a vector. For non objects or an empty vector, + /// the type tag must be specified. + MakeMoveVec(Option, Vec), +} + +impl Command { + pub fn move_call( + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + arguments: Vec, + ) -> Self { + Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module, + function, + type_arguments, + arguments, + })) + } +} + +/// The command for calling a Move function, either an entry function or a public +/// function (which cannot return references). +#[derive(Debug, Deserialize, Serialize)] +pub struct ProgrammableMoveCall { + /// The package containing the module and function. + pub package: ObjectID, + /// The specific module in the package containing the function. + pub module: Identifier, + /// The function to be called. + pub function: Identifier, + /// The type arguments to the function. + pub type_arguments: Vec, + /// The arguments to the function. + pub arguments: Vec, +} + +/// An argument to a programmable transaction command +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum Argument { + /// The gas coin. The gas coin can only be used by-ref, except for with + /// `TransferObjects`, which can use it by-value. + GasCoin, + /// One of the input objects or primitive values (from + /// `ProgrammableTransaction` inputs) + Input(u16), + /// The result of another command (from `ProgrammableTransaction` commands) + Result(u16), + /// Like a `Result` but it accesses a nested result. Currently, the only usage + /// of this is to access a value from a Move call with multiple return values. + NestedResult(u16, u16), +} diff --git a/rust/chains/tw_sui/src/transaction/mod.rs b/rust/chains/tw_sui/src/transaction/mod.rs new file mode 100644 index 00000000000..46b7c4a5cf0 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod command; +pub mod programmable_transaction; +pub mod sui_types; +pub mod transaction_builder; +pub mod transaction_data; diff --git a/rust/chains/tw_sui/src/transaction/programmable_transaction.rs b/rust/chains/tw_sui/src/transaction/programmable_transaction.rs new file mode 100644 index 00000000000..82445cae86e --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/programmable_transaction.rs @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::command::{Argument, Command}; +use crate::transaction::sui_types::{CallArg, ObjectArg, ObjectID, ObjectRef}; +use indexmap::IndexMap; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bcs; + +/// A series of commands where the results of one command can be used in future +/// commands +#[derive(Debug, Deserialize, Serialize)] +pub struct ProgrammableTransaction { + /// Input objects or primitive values + pub inputs: Vec, + /// The commands to be executed sequentially. A failure in any command will + /// result in the failure of the entire transaction. + pub commands: Vec, +} + +#[derive(Eq, Hash, PartialEq)] +enum BuilderArg { + Object(ObjectID), + Pure(Vec), + ForcedNonUniquePure(usize), +} + +#[derive(Default)] +pub struct ProgrammableTransactionBuilder { + inputs: IndexMap, + commands: Vec, +} + +impl ProgrammableTransactionBuilder { + pub fn finish(self) -> ProgrammableTransaction { + let Self { inputs, commands } = self; + let inputs = inputs.into_values().collect(); + ProgrammableTransaction { inputs, commands } + } + + /// Will fail to generate if recipients and amounts do not have the same lengths. + /// Or if coins is empty + pub fn pay( + &mut self, + coins: Vec, + recipients: Vec, + amounts: Vec, + ) -> SigningResult<()> { + let mut coins = coins.into_iter(); + let Some(coin) = coins.next() else { + // coins vector is empty + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("No coins provided"); + }; + let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin))?; + let merge_args: Vec<_> = coins + .map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c))) + .collect::>()?; + if !merge_args.is_empty() { + self.command(Command::MergeCoins(coin_arg, merge_args)); + } + self.pay_impl(recipients, amounts, coin_arg) + } + + /// Will fail to generate if recipients and amounts do not have the same lengths + pub fn pay_sui(&mut self, recipients: Vec, amounts: Vec) -> SigningResult<()> { + self.pay_impl(recipients, amounts, Argument::GasCoin) + } + + pub fn pay_all_sui(&mut self, recipient: SuiAddress) { + let rec_arg = self.pure(recipient).unwrap(); + self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg)); + } + + pub fn transfer_object( + &mut self, + recipient: SuiAddress, + object_ref: ObjectRef, + ) -> SigningResult<()> { + let rec_arg = self.pure(recipient).unwrap(); + let obj_arg = self.obj(ObjectArg::ImmOrOwnedObject(object_ref)); + self.commands + .push(Command::TransferObjects(vec![obj_arg?], rec_arg)); + Ok(()) + } + + /// Will fail to generate if given an empty ObjVec + pub fn move_call( + &mut self, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + call_args: Vec, + ) -> SigningResult<()> { + let arguments = call_args + .into_iter() + .map(|a| self.input(a)) + .collect::>()?; + self.command(Command::move_call( + package, + module, + function, + type_arguments, + arguments, + )); + Ok(()) + } + + pub fn input(&mut self, call_arg: CallArg) -> SigningResult { + match call_arg { + CallArg::Pure(bytes) => { + let force_separate = false; + Ok(self.pure_bytes(bytes, force_separate)) + }, + CallArg::Object(obj) => self.obj(obj), + } + } + + pub fn pure_bytes(&mut self, bytes: Vec, force_separate: bool) -> Argument { + let arg = if force_separate { + BuilderArg::ForcedNonUniquePure(self.inputs.len()) + } else { + BuilderArg::Pure(bytes.clone()) + }; + let (i, _) = self.inputs.insert_full(arg, CallArg::Pure(bytes)); + Argument::Input(i as u16) + } + + pub fn pure(&mut self, value: T) -> SigningResult { + let force_separate = false; + Ok(self.pure_bytes(bcs::encode(&value)?, force_separate)) + } + + pub fn obj(&mut self, obj_arg: ObjectArg) -> SigningResult { + let id = obj_arg.id(); + let obj_arg = if let Some(old_value) = self.inputs.get(&BuilderArg::Object(id)) { + let old_obj_arg = match old_value { + CallArg::Pure(_) => { + return SigningError::err(SigningErrorType::Error_internal) + .context("Expected Object, found Pure") + }, + CallArg::Object(arg) => arg, + }; + match (old_obj_arg, obj_arg) { + ( + ObjectArg::SharedObject { + id: id1, + initial_shared_version: v1, + mutable: mut1, + }, + ObjectArg::SharedObject { + id: id2, + initial_shared_version: v2, + mutable: mut2, + }, + ) if v1 == &v2 => { + if id1 != &id2 || id != id2 { + // "invariant violation! object has id does not match call arg" + return SigningError::err(SigningErrorType::Error_internal) + .context("invariant violation! object has id does not match call arg"); + } + ObjectArg::SharedObject { + id, + initial_shared_version: v2, + mutable: *mut1 || mut2, + } + }, + (old_obj_arg, obj_arg) => { + if old_obj_arg != &obj_arg { + return SigningError::err(SigningErrorType::Error_internal) + .with_context(|| format!("Mismatched Object argument kind for object {id:?}. {old_value:?} is not compatible with {obj_arg:?}")); + } + obj_arg + }, + } + } else { + obj_arg + }; + let (i, _) = self + .inputs + .insert_full(BuilderArg::Object(id), CallArg::Object(obj_arg)); + Ok(Argument::Input(i as u16)) + } + + pub fn make_obj_vec( + &mut self, + objs: impl IntoIterator, + ) -> SigningResult { + let make_vec_args = objs + .into_iter() + .map(|obj| self.obj(obj)) + .collect::>()?; + Ok(self.command(Command::MakeMoveVec(None, make_vec_args))) + } + + pub fn command(&mut self, command: Command) -> Argument { + let i = self.commands.len(); + self.commands.push(command); + Argument::Result(i as u16) + } + + fn pay_impl( + &mut self, + recipients: Vec, + amounts: Vec, + coin: Argument, + ) -> SigningResult<()> { + if recipients.len() != amounts.len() { + // "Recipients and amounts mismatch. Got {} recipients but {} amounts" + return SigningError::err(SigningErrorType::Error_invalid_params).with_context(|| { + let recipients_num = recipients.len(); + let amounts_num = amounts.len(); + format!("Recipients and amounts mismatch. Got {recipients_num} recipients but {amounts_num} amounts") + }); + } + if amounts.is_empty() { + return Ok(()); + } + + // collect recipients in the case where they are non-unique in order + // to minimize the number of transfers that must be performed + let mut recipient_map: IndexMap> = IndexMap::new(); + let mut amt_args = vec![]; + for (i, (recipient, amount)) in recipients.into_iter().zip(amounts).enumerate() { + recipient_map.entry(recipient).or_default().push(i); + amt_args.push(self.pure(amount)?); + } + let Argument::Result(split_primary) = self.command(Command::SplitCoins(coin, amt_args)) + else { + return SigningError::err(SigningErrorType::Error_internal) + .context("self.command should always give an Argument::Result"); + }; + for (recipient, split_secondaries) in recipient_map { + let rec_arg = self.pure(recipient).unwrap(); + let coins = split_secondaries + .into_iter() + .map(|j| Argument::NestedResult(split_primary, j as u16)) + .collect(); + self.command(Command::TransferObjects(coins, rec_arg)); + } + Ok(()) + } +} diff --git a/rust/chains/tw_sui/src/transaction/sui_types.rs b/rust/chains/tw_sui/src/transaction/sui_types.rs new file mode 100644 index 00000000000..1948789adf9 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/sui_types.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::constants::{SUI_SYSTEM_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION}; +use move_core_types::account_address::AccountAddress; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base58::{self, Alphabet}; +use tw_hash::{as_bytes, H256}; +use tw_memory::Data; + +pub type ObjectRef = (ObjectID, SequenceNumber, ObjectDigest); +pub type EpochId = u64; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct SequenceNumber(pub u64); + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ObjectID(pub AccountAddress); + +impl FromStr for ObjectID { + type Err = SigningError; + + fn from_str(s: &str) -> Result { + let addr = SuiAddress::from_str(s) + .into_tw() + .context("Invalid Object ID")?; + Ok(ObjectID(addr.into_inner())) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ObjectDigest(#[serde(with = "as_bytes")] pub H256); + +impl FromStr for ObjectDigest { + type Err = SigningError; + + fn from_str(s: &str) -> Result { + let bytes = base58::decode(s, Alphabet::Bitcoin) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid Object Digest: expected valid base58 string")?; + H256::try_from(bytes.as_slice()) + .map(ObjectDigest) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid Object Digest: expected exactly 32 bytes") + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum CallArg { + // contains no structs or objects + Pure(Data), + // an object + Object(ObjectArg), +} + +impl CallArg { + pub const SUI_SYSTEM_MUT: Self = Self::Object(ObjectArg::SUI_SYSTEM_MUT); +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub enum ObjectArg { + // A Move object, either immutable, or owned mutable. + ImmOrOwnedObject(ObjectRef), + // A Move object that's shared. + // SharedObject::mutable controls whether caller asks for a mutable reference to shared object. + SharedObject { + id: ObjectID, + initial_shared_version: SequenceNumber, + mutable: bool, + }, +} + +impl ObjectArg { + pub const SUI_SYSTEM_MUT: Self = Self::SharedObject { + id: SUI_SYSTEM_STATE_OBJECT_ID, + initial_shared_version: SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION, + mutable: true, + }; + + pub fn id(&self) -> ObjectID { + match self { + ObjectArg::ImmOrOwnedObject((id, _, _)) | ObjectArg::SharedObject { id, .. } => *id, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GasData { + pub payment: Vec, + pub owner: SuiAddress, + pub price: u64, + pub budget: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionExpiration { + /// The transaction has no expiration + None, + /// Validators wont sign a transaction unless the expiration Epoch + /// is greater than or equal to the current epoch + Epoch(EpochId), +} diff --git a/rust/chains/tw_sui/src/transaction/transaction_builder.rs b/rust/chains/tw_sui/src/transaction/transaction_builder.rs new file mode 100644 index 00000000000..3c7309013fb --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/transaction_builder.rs @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::constants::{ + ADD_STAKE_MUL_COIN_FUN_NAME, SUI_SYSTEM_MODULE_NAME, SUI_SYSTEM_PACKAGE_ID, + WITHDRAW_STAKE_FUN_NAME, +}; +use crate::transaction::command::Command; +use crate::transaction::programmable_transaction::ProgrammableTransactionBuilder; +use crate::transaction::sui_types::{CallArg, ObjectArg, ObjectRef}; +use crate::transaction::transaction_data::{TransactionData, TransactionKind}; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bcs; + +pub struct TransactionBuilder; + +impl TransactionBuilder { + pub fn request_add_stake( + signer: SuiAddress, + coins: Vec, + amount: Option, + validator: SuiAddress, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let obj_vec: Vec<_> = coins.into_iter().map(ObjectArg::ImmOrOwnedObject).collect(); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + let arguments = vec![ + builder.input(CallArg::SUI_SYSTEM_MUT).unwrap(), + builder.make_obj_vec(obj_vec)?, + builder.input(CallArg::Pure(bcs::encode(&amount)?)).unwrap(), + builder + .input(CallArg::Pure(bcs::encode(&validator)?)) + .unwrap(), + ]; + builder.command(Command::move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(), + vec![], + arguments, + )); + builder.finish() + }; + Ok(TransactionData::new_programmable( + signer, + vec![gas], + pt, + gas_budget, + gas_price, + )) + } + + pub fn request_withdraw_stake( + signer: SuiAddress, + staked_sui: ObjectRef, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + TransactionData::new_move_call( + signer, + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + WITHDRAW_STAKE_FUN_NAME.to_owned(), + vec![], + gas, + vec![ + CallArg::SUI_SYSTEM_MUT, + CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_sui)), + ], + gas_budget, + gas_price, + ) + } + + /// Send `Coin` to a list of addresses, where T can be any coin type, following a list of amounts. + /// The object specified in the gas field will be used to pay the gas fee for the transaction. + /// The gas object can not appear in input_coins. + /// https://docs.sui.io/sui-api-ref#unsafe_pay + #[allow(clippy::too_many_arguments)] + pub fn pay( + signer: SuiAddress, + input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.iter().any(|coin| coin.0 == gas.0) { + return SigningError::err(SigningErrorType::Error_invalid_params).context( + "Gas coin is in input coins of Pay transaction, use PaySui transaction instead!", + ); + } + + TransactionData::new_pay( + signer, + input_coins, + recipients, + amounts, + gas, + gas_budget, + gas_price, + ) + } + + /// Send SUI coins to a list of addresses, following a list of amounts. + /// This is for SUI coin only and does not require a separate gas coin object. + /// https://docs.sui.io/sui-api-ref#unsafe_paysui + pub fn pay_sui( + signer: SuiAddress, + mut input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.is_empty() { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Empty input coins for Pay related transaction"); + } + + let gas_object_ref = input_coins.remove(0); + TransactionData::new_pay_sui( + signer, + input_coins, + recipients, + amounts, + gas_object_ref, + gas_budget, + gas_price, + ) + } + + /// Send all SUI coins to one recipient. + /// This is for SUI coin only and does not require a separate gas coin object. + /// https://docs.sui.io/sui-api-ref#unsafe_payallsui + pub fn pay_all_sui( + signer: SuiAddress, + mut input_coins: Vec, + recipient: SuiAddress, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.is_empty() { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("Empty input coins for Pay related transaction"); + } + + let gas_object_ref = input_coins.remove(0); + Ok(TransactionData::new_pay_all_sui( + signer, + input_coins, + recipient, + gas_object_ref, + gas_budget, + gas_price, + )) + } + + pub fn transfer_object( + signer: SuiAddress, + object: ObjectRef, + recipient: SuiAddress, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.transfer_object(recipient, object)?; + + Ok(TransactionData::new( + TransactionKind::ProgrammableTransaction(builder.finish()), + signer, + gas, + gas_budget, + gas_price, + )) + } +} diff --git a/rust/chains/tw_sui/src/transaction/transaction_data.rs b/rust/chains/tw_sui/src/transaction/transaction_data.rs new file mode 100644 index 00000000000..bfe18fa4e3a --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/transaction_data.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::programmable_transaction::{ + ProgrammableTransaction, ProgrammableTransactionBuilder, +}; +use crate::transaction::sui_types::{CallArg, GasData, ObjectID, ObjectRef, TransactionExpiration}; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::error::prelude::*; + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionData { + V1(TransactionDataV1), +} + +impl TransactionData { + #[inline] + pub fn new( + kind: TransactionKind, + sender: SuiAddress, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> Self { + TransactionData::V1(TransactionDataV1 { + kind, + sender, + gas_data: GasData { + price: gas_price, + owner: sender, + payment: vec![gas_payment], + budget: gas_budget, + }, + expiration: TransactionExpiration::None, + }) + } + + #[inline] + pub fn new_programmable( + sender: SuiAddress, + gas_payment: Vec, + pt: ProgrammableTransaction, + gas_budget: u64, + gas_price: u64, + ) -> Self { + Self::new_programmable_allow_sponsor(sender, gas_payment, pt, gas_budget, gas_price, sender) + } + + #[inline] + pub fn new_programmable_allow_sponsor( + sender: SuiAddress, + gas_payment: Vec, + pt: ProgrammableTransaction, + gas_budget: u64, + gas_price: u64, + sponsor: SuiAddress, + ) -> Self { + let kind = TransactionKind::ProgrammableTransaction(pt); + Self::new_with_gas_coins_allow_sponsor( + kind, + sender, + gas_payment, + gas_budget, + gas_price, + sponsor, + ) + } + + #[inline] + pub fn new_with_gas_coins_allow_sponsor( + kind: TransactionKind, + sender: SuiAddress, + gas_payment: Vec, + gas_budget: u64, + gas_price: u64, + gas_sponsor: SuiAddress, + ) -> Self { + TransactionData::V1(TransactionDataV1 { + kind, + sender, + gas_data: GasData { + price: gas_price, + owner: gas_sponsor, + payment: gas_payment, + budget: gas_budget, + }, + expiration: TransactionExpiration::None, + }) + } + + pub fn new_pay( + sender: SuiAddress, + coins: Vec, + recipients: Vec, + amounts: Vec, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay(coins, recipients, amounts)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, + vec![gas_payment], + pt, + gas_budget, + gas_price, + )) + } + + pub fn new_pay_sui( + sender: SuiAddress, + mut coins: Vec, + recipients: Vec, + amounts: Vec, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + coins.insert(0, gas_payment); + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay_sui(recipients, amounts)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, coins, pt, gas_budget, gas_price, + )) + } + + pub fn new_pay_all_sui( + sender: SuiAddress, + mut coins: Vec, + recipient: SuiAddress, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> Self { + coins.insert(0, gas_payment); + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay_all_sui(recipient); + builder.finish() + }; + Self::new_programmable(sender, coins, pt, gas_budget, gas_price) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_move_call( + sender: SuiAddress, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + gas_payment: ObjectRef, + arguments: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + Self::new_move_call_with_gas_coins( + sender, + package, + module, + function, + type_arguments, + vec![gas_payment], + arguments, + gas_budget, + gas_price, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_move_call_with_gas_coins( + sender: SuiAddress, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + gas_payment: Vec, + arguments: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.move_call(package, module, function, type_arguments, arguments)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, + gas_payment, + pt, + gas_budget, + gas_price, + )) + } + + pub fn sender(&self) -> SuiAddress { + match self { + TransactionData::V1(v1) => v1.sender, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TransactionDataV1 { + pub kind: TransactionKind, + pub sender: SuiAddress, + pub gas_data: GasData, + pub expiration: TransactionExpiration, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionKind { + /// A transaction that allows the interleaving of native commands and Move calls + ProgrammableTransaction(ProgrammableTransaction), +} diff --git a/rust/chains/tw_sui/tests/decode_transaction.rs b/rust/chains/tw_sui/tests/decode_transaction.rs new file mode 100644 index 00000000000..4b2c8c4a370 --- /dev/null +++ b/rust/chains/tw_sui/tests/decode_transaction.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_encoding::base64::{self, STANDARD}; +use tw_encoding::bcs; +use tw_encoding::hex::DecodeHex; +use tw_sui::address::SuiAddress; +use tw_sui::transaction::command::{Argument, Command}; +use tw_sui::transaction::programmable_transaction::ProgrammableTransaction; +use tw_sui::transaction::sui_types::{ + CallArg, GasData, ObjectDigest, ObjectID, SequenceNumber, TransactionExpiration, +}; +use tw_sui::transaction::transaction_data::{TransactionData, TransactionDataV1, TransactionKind}; + +#[test] +fn test_decode_transfer_tx() { + let programmable = ProgrammableTransaction { + inputs: vec![ + CallArg::Pure("1027000000000000".decode_hex().unwrap()), + CallArg::Pure( + "259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015" + .decode_hex() + .unwrap(), + ), + ], + commands: vec![ + Command::SplitCoins(Argument::GasCoin, vec![Argument::Input(0)]), + Command::TransferObjects(vec![Argument::NestedResult(0, 0)], Argument::Input(1)), + ], + }; + + let v1 = TransactionDataV1 { + kind: TransactionKind::ProgrammableTransaction(programmable), + sender: SuiAddress::from_str( + "0xd575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d50", + ) + .unwrap(), + gas_data: GasData { + payment: vec![( + ObjectID( + SuiAddress::from_str( + "0x06f2c2c8c1d8964df1019d6616e9705719bebabd931da2755cb948ceb7e68964", + ) + .unwrap() + .into_inner(), + ), + SequenceNumber(748), + ObjectDigest::from_str("7UoYeVzREVT17ZyYbRTsKzRCec5xJWm6FMh8AKaDPdDx").unwrap(), + )], + owner: SuiAddress::from_str( + "0xd575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d50", + ) + .unwrap(), + price: 1, + budget: 2000, + }, + expiration: TransactionExpiration::None, + }; + let data = TransactionData::V1(v1); + + let bytes = base64::encode(&bcs::encode(&data).unwrap(), STANDARD); + // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet + assert_eq!(bytes, "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"); +} diff --git a/rust/chains/tw_thorchain/Cargo.toml b/rust/chains/tw_thorchain/Cargo.toml new file mode 100644 index 00000000000..a39bc4d7c80 --- /dev/null +++ b/rust/chains/tw_thorchain/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tw_thorchain" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_cosmos_sdk = { path = "../../tw_cosmos_sdk", features = ["test-utils"] } diff --git a/rust/chains/tw_thorchain/src/compiler.rs b/rust/chains/tw_thorchain/src/compiler.rs new file mode 100644 index 00000000000..7e3e81ad090 --- /dev/null +++ b/rust/chains/tw_thorchain/src/compiler.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::signing_input::ThorchainSigningInput; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_cosmos_sdk::context::StandardCosmosContext; +use tw_cosmos_sdk::modules::compiler::tw_compiler::TWTransactionCompiler; +use tw_proto::Cosmos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct ThorchainCompiler; + +impl ThorchainCompiler { + pub fn preimage_hashes( + coin: &dyn CoinContext, + mut input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + ThorchainSigningInput::prepare_signing_input(&mut input); + TWTransactionCompiler::::preimage_hashes(coin, input) + } + + pub fn compile( + coin: &dyn CoinContext, + mut input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + ThorchainSigningInput::prepare_signing_input(&mut input); + TWTransactionCompiler::::compile( + coin, + input, + signatures, + public_keys, + ) + } +} diff --git a/rust/chains/tw_thorchain/src/entry.rs b/rust/chains/tw_thorchain/src/entry.rs new file mode 100644 index 00000000000..9f93134601f --- /dev/null +++ b/rust/chains/tw_thorchain/src/entry.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::compiler::ThorchainCompiler; +use crate::signer::ThorchainSigner; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_cosmos_sdk::address::{Address, Bech32Prefix}; +use tw_keypair::tw; +use tw_proto::Cosmos::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct ThorchainEntry; + +impl CoinEntry for ThorchainEntry { + type AddressPrefix = Bech32Prefix; + type Address = Address; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + Address::from_str_with_coin_and_prefix(coin, address.to_string(), prefix) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + Address::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: tw::PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + Address::with_public_key_coin_context(coin, &public_key, prefix) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + ThorchainSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + ThorchainCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + ThorchainCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_thorchain/src/lib.rs b/rust/chains/tw_thorchain/src/lib.rs new file mode 100644 index 00000000000..9abacf4dfac --- /dev/null +++ b/rust/chains/tw_thorchain/src/lib.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod compiler; +pub mod entry; +pub mod signer; +pub mod signing_input; diff --git a/rust/chains/tw_thorchain/src/signer.rs b/rust/chains/tw_thorchain/src/signer.rs new file mode 100644 index 00000000000..7346be163af --- /dev/null +++ b/rust/chains/tw_thorchain/src/signer.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::signing_input::ThorchainSigningInput; +use tw_coin_entry::coin_context::CoinContext; +use tw_cosmos_sdk::context::StandardCosmosContext; +use tw_cosmos_sdk::modules::signer::tw_signer::TWSigner; +use tw_proto::Cosmos::Proto; + +pub struct ThorchainSigner; + +impl ThorchainSigner { + pub fn sign( + coin: &dyn CoinContext, + mut input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + ThorchainSigningInput::prepare_signing_input(&mut input); + TWSigner::::sign(coin, input) + } +} diff --git a/rust/chains/tw_thorchain/src/signing_input.rs b/rust/chains/tw_thorchain/src/signing_input.rs new file mode 100644 index 00000000000..429a62a6434 --- /dev/null +++ b/rust/chains/tw_thorchain/src/signing_input.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::borrow::Cow; +use tw_proto::Cosmos::Proto; +use tw_proto::Cosmos::Proto::mod_Message::OneOfmessage_oneof as MessageEnum; + +const THORCHAIN_PREFIX_MSG_SEND: &str = "thorchain/MsgSend"; + +pub struct ThorchainSigningInput; + +impl ThorchainSigningInput { + pub fn prepare_signing_input(input: &mut Proto::SigningInput) { + for message in input.messages.iter_mut() { + if let MessageEnum::send_coins_message(ref mut msg_send) = message.message_oneof { + msg_send.type_prefix = Cow::from(THORCHAIN_PREFIX_MSG_SEND); + } + } + } +} diff --git a/rust/chains/tw_ton/Cargo.toml b/rust/chains/tw_ton/Cargo.toml new file mode 100644 index 00000000000..076a7d84f61 --- /dev/null +++ b/rust/chains/tw_ton/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_ton" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = "1.4.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_number = { path = "../../tw_number" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } +tw_ton_sdk = { path = "../../frameworks/tw_ton_sdk" } diff --git a/rust/chains/tw_ton/fuzz/.gitignore b/rust/chains/tw_ton/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_ton/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_ton/fuzz/Cargo.toml b/rust/chains/tw_ton/fuzz/Cargo.toml new file mode 100644 index 00000000000..671fc399a02 --- /dev/null +++ b/rust/chains/tw_ton/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_ton-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_ton] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..4c2a190be6a --- /dev/null +++ b/rust/chains/tw_ton/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::TheOpenNetwork::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::TON, input); +}); diff --git a/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code b/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code new file mode 100644 index 00000000000..e1d04cde08a --- /dev/null +++ b/rust/chains/tw_ton/resources/wallet/wallet_v4r2.code @@ -0,0 +1 @@ +te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVGliJeU= \ No newline at end of file diff --git a/rust/chains/tw_ton/resources/wallet/wallet_v5r1.code b/rust/chains/tw_ton/resources/wallet/wallet_v5r1.code new file mode 100644 index 00000000000..586cfc39e88 --- /dev/null +++ b/rust/chains/tw_ton/resources/wallet/wallet_v5r1.code @@ -0,0 +1 @@ +te6cckECFAEAAoEAART/APSkE/S88sgLAQIBIAINAgFIAwQC3NAg10nBIJFbj2Mg1wsfIIIQZXh0br0hghBzaW50vbCSXwPgghBleHRuuo60gCDXIQHQdNch+kAw+kT4KPpEMFi9kVvg7UTQgQFB1yH0BYMH9A5voTGRMOGAQNchcH/bPOAxINdJgQKAuZEw4HDiEA8CASAFDAIBIAYJAgFuBwgAGa3OdqJoQCDrkOuF/8AAGa8d9qJoQBDrkOuFj8ACAUgKCwAXsyX7UTQcdch1wsfgABGyYvtRNDXCgCAAGb5fD2omhAgKDrkPoCwBAvIOAR4g1wsfghBzaWduuvLgin8PAeaO8O2i7fshgwjXIgKDCNcjIIAg1yHTH9Mf0x/tRNDSANMfINMf0//XCgAK+QFAzPkQmiiUXwrbMeHywIffArNQB7Dy0IRRJbry4IVQNrry4Ib4I7vy0IgikvgA3gGkf8jKAMsfAc8Wye1UIJL4D95w2zzYEAP27aLt+wL0BCFukmwhjkwCIdc5MHCUIccAs44tAdcoIHYeQ2wg10nACPLgkyDXSsAC8uCTINcdBscSwgBSMLDy0InXTNc5MAGk6GwShAe78uCT10rAAPLgk+1V4tIAAcAAkVvg69csCBQgkXCWAdcsCBwS4lIQseMPINdKERITAJYB+kAB+kT4KPpEMFi68uCR7UTQgQFB1xj0BQSdf8jKAEAEgwf0U/Lgi44UA4MH9Fvy4Iwi1woAIW4Bs7Dy0JDiyFADzxYS9ADJ7VQAcjDXLAgkji0h8uCS0gDtRNDSAFETuvLQj1RQMJExnAGBAUDXIdcKAPLgjuLIygBYzxbJ7VST8sCN4gAQk1vbMeHXTNC01sNe \ No newline at end of file diff --git a/rust/chains/tw_ton/src/address.rs b/rust/chains/tw_ton/src/address.rs new file mode 100644 index 00000000000..3175a4cab0b --- /dev/null +++ b/rust/chains/tw_ton/src/address.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::resources::{BASE_WORKCHAIN, MASTER_WORKCHAIN}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_memory::Data; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::raw_address::RawAddress; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; + +pub const DEFAULT_BOUNCEABLE: bool = false; +pub const DEFAULT_TESTNET: bool = false; + +/// User-friendly, base64 URL-safe **by default** encoded TON address. +/// Please note it also supports raw (hex) and user-friendly base64 standard representations as well. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TonAddress(UserFriendlyAddress); + +impl TonAddress { + pub const NULL: TonAddress = TonAddress::null(); + + pub const fn null() -> TonAddress { + TonAddress(UserFriendlyAddress::with_flags( + AddressData::null(), + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + pub fn new(workchain: i32, hash_part: H256) -> Self { + let data = AddressData::new(workchain, hash_part); + TonAddress(UserFriendlyAddress::with_flags( + data, + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + pub fn from_hex_str(s: &str) -> AddressResult { + let raw_address = RawAddress::from_str(s)?; + let user_friendly_address = UserFriendlyAddress::with_flags( + raw_address.into_data(), + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + ); + Ok(TonAddress(user_friendly_address)) + } + + #[inline] + pub fn from_base64_url(s: &str) -> AddressResult { + UserFriendlyAddress::from_base64_url(s).map(TonAddress) + } + + #[inline] + pub fn from_base64_std(s: &str) -> AddressResult { + UserFriendlyAddress::from_base64_std(s).map(TonAddress) + } + + #[inline] + pub fn with_address_data(data: AddressData) -> TonAddress { + TonAddress(UserFriendlyAddress::with_flags( + data, + DEFAULT_BOUNCEABLE, + DEFAULT_TESTNET, + )) + } + + /// Normalizes the TON address according to the best wallet practice: + /// https://docs.ton.org/learn/overviews/addresses#bounceable-vs-non-bounceable-addresses + /// + /// Returns error if the address workchain is unexpected. + #[inline] + pub fn normalize(self) -> AddressResult { + let workchain = self.0.as_ref().workchain; + if workchain != MASTER_WORKCHAIN && workchain != BASE_WORKCHAIN { + return Err(AddressError::UnexpectedAddressPrefix); + } + + Ok(self + .set_bounceable(DEFAULT_BOUNCEABLE) + .set_testnet(DEFAULT_TESTNET)) + } + + #[inline] + pub fn set_bounceable(self, bounceable: bool) -> Self { + TonAddress(self.0.set_bounceable(bounceable)) + } + + #[inline] + pub fn set_testnet(self, testnet: bool) -> Self { + TonAddress(self.0.set_testnet(testnet)) + } + + #[inline] + pub fn bounceable(&self) -> bool { + self.0.bounceable() + } +} + +impl AsRef for TonAddress { + fn as_ref(&self) -> &AddressData { + self.0.as_ref() + } +} + +impl CoinAddress for TonAddress { + #[inline] + fn data(&self) -> Data { + self.0.as_ref().hash_part.to_vec() + } +} + +impl FromStr for TonAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if s.len() != 48 { + return TonAddress::from_hex_str(s); + } + + // Some form of base64 address, check which one + if s.contains('-') || s.contains('_') { + TonAddress::from_base64_url(s) + } else { + TonAddress::from_base64_std(s) + } + } +} + +impl fmt::Display for TonAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_base64_url()) + } +} diff --git a/rust/chains/tw_ton/src/compiler.rs b/rust/chains/tw_ton/src/compiler.rs new file mode 100644 index 00000000000..2bcea271351 --- /dev/null +++ b/rust/chains/tw_ton/src/compiler.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::signing_request::builder::SigningRequestBuilder; +use crate::signing_request::cell_creator::ExternalMessageCreator; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519::Signature; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::error::cell_to_signing_error; + +pub(crate) const HAS_CRC32: bool = true; + +pub struct TheOpenNetworkCompiler; + +impl TheOpenNetworkCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let signing_request = SigningRequestBuilder::build(&input)?; + + let external_message = + ExternalMessageCreator::create_external_message_to_sign(&signing_request) + .map_err(cell_to_signing_error)?; + + Ok(CompilerProto::PreSigningOutput { + data: Cow::from(external_message.cell_hash().to_vec()), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + if signatures.len() != 1 { + return TWError::err(SigningErrorType::Error_signatures_count) + .context("Expected exactly one signature"); + } + let signature = Signature::try_from(signatures[0].as_slice())?; + + let signing_request = SigningRequestBuilder::build(&input)?; + + let external_message = + ExternalMessageCreator::create_external_message_to_sign(&signing_request) + .map_err(cell_to_signing_error)?; + + let signed_external_message = signing_request + .wallet + .compile_signed_external_message(external_message, signature)?; + + // Whether to add 'StateInit' reference. + let state_init = signing_request.seqno == 0; + let signed_tx = signing_request + .wallet + .compile_transaction(signed_external_message, state_init) + .context("Error compiling an external message")? + .build() + .context("Error generating signed message cell") + .map_err(cell_to_signing_error)?; + + let signed_tx_hash = signed_tx.cell_hash(); + let signed_tx_encoded = BagOfCells::from_root(signed_tx) + .to_base64(HAS_CRC32) + .context("Error serializing signed transaction as BoC") + .map_err(cell_to_signing_error)?; + + Ok(Proto::SigningOutput { + encoded: signed_tx_encoded.into(), + hash: signed_tx_hash.to_vec().into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_ton/src/entry.rs b/rust/chains/tw_ton/src/entry.rs new file mode 100644 index 00000000000..a2bd5aadc2d --- /dev/null +++ b/rust/chains/tw_ton/src/entry.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::compiler::TheOpenNetworkCompiler; +use crate::modules::transaction_util::TonTransactionUtil; +use crate::signer::TheOpenNetworkSigner; +use crate::wallet::{wallet_v4, VersionedTonWallet}; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::TheOpenNetwork::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct TheOpenNetworkEntry; + +impl CoinEntry for TheOpenNetworkEntry { + type AddressPrefix = NoPrefix; + type Address = TonAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = TonTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + // TODO consider checking whether the transaction is on testnet. + TonAddress::from_str(address).and_then(TonAddress::normalize) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + TonAddress::from_str(address).and_then(TonAddress::normalize) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let ed25519_pubkey = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + // Currently, we use the V4R2 wallet + let wallet = VersionedTonWallet::V4R2( + wallet_v4::WalletV4R2::std_with_public_key(ed25519_pubkey.clone()) + .map_err(|_| AddressError::Internal)?, + ); + Ok(wallet.address().clone()) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + TheOpenNetworkSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + TheOpenNetworkCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + TheOpenNetworkCompiler::compile(coin, input, signatures, public_keys) + } + + #[inline] + fn transaction_util(&self) -> Option { + Some(TonTransactionUtil) + } +} diff --git a/rust/chains/tw_ton/src/lib.rs b/rust/chains/tw_ton/src/lib.rs new file mode 100644 index 00000000000..7e35d3bc165 --- /dev/null +++ b/rust/chains/tw_ton/src/lib.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod entry; +pub mod message; +pub mod modules; +pub mod resources; +pub mod signer; +pub mod signing_request; +pub mod transaction; +pub mod wallet; diff --git a/rust/chains/tw_ton/src/message/external_message/mod.rs b/rust/chains/tw_ton/src/message/external_message/mod.rs new file mode 100644 index 00000000000..88c077ae30c --- /dev/null +++ b/rust/chains/tw_ton/src/message/external_message/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod wallet_v4; +pub mod wallet_v5; diff --git a/rust/chains/tw_ton/src/message/external_message/wallet_v4.rs b/rust/chains/tw_ton/src/message/external_message/wallet_v4.rs new file mode 100644 index 00000000000..d23d0090de9 --- /dev/null +++ b/rust/chains/tw_ton/src/message/external_message/wallet_v4.rs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::internal_message::InternalMessage; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +pub struct ExternalMessageWalletV4 { + pub wallet_id: i32, + pub expire_at: u32, + pub seqno: u32, + pub internal_messages: Vec, +} + +impl ExternalMessageWalletV4 { + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder + .store_i32(32, self.wallet_id)? + .store_u32(32, self.expire_at)? + .store_u32(32, self.seqno)?; + builder.store_u8(8, 0)?; // has op + for internal_message in self.internal_messages.iter() { + builder.store_u8(8, internal_message.mode)?; + builder.store_reference(&internal_message.message)?; + } + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/external_message/wallet_v5.rs b/rust/chains/tw_ton/src/message/external_message/wallet_v5.rs new file mode 100644 index 00000000000..af4a2863323 --- /dev/null +++ b/rust/chains/tw_ton/src/message/external_message/wallet_v5.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::out_list::build_out_list; +use crate::message::out_list::out_action::OutAction; +use tw_coin_entry::error::prelude::ResultContext; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::{CellError, CellErrorType, CellResult}; + +const SEND_MODE_IGNORE_ACTION_PHASE_ERRORS: u8 = 0x02; + +pub enum V5R1OpCode { + // Currently, only AuthSignedExternal is supported. AuthSignedInternal/AuthExtension are not supported. + AuthSignedExternal, +} + +impl V5R1OpCode { + // See https://github.com/ton-blockchain/wallet-contract-v5/blob/88557ebc33047a95207f6e47ac8aadb102dff744/contracts/wallet_v5.fc#L33 + pub fn to_ser_tag(&self) -> u32 { + match self { + V5R1OpCode::AuthSignedExternal => 0x7369676e, + } + } +} + +pub struct ExternalMessageWalletV5 { + pub opcode: V5R1OpCode, + pub wallet_id: i32, + pub expire_at: u32, + pub seqno: u32, + /// Currently, only basic actions are supported. Extended actions are not supported. + pub basic_actions: Vec, +} + +impl ExternalMessageWalletV5 { + /// Build the external message for wallet v5. + pub fn build(&self) -> CellResult { + // Check the number of basic actions + if self.basic_actions.len() > 255 { + return CellError::err(CellErrorType::InternalError) + .context("Maximum number of actions in a single request is 255"); + } + + let mut builder = CellBuilder::new(); + match self.opcode { + V5R1OpCode::AuthSignedExternal => { + builder.store_u32(32, self.opcode.to_ser_tag())?; + + // Make sure +2 flag (ignore errors send mode) is set for all external send messages + // See https://github.com/ton-blockchain/wallet-contract-v5/blob/88557ebc33047a95207f6e47ac8aadb102dff744/contracts/wallet_v5.fc#L82 + for action in &self.basic_actions { + if (action.mode & SEND_MODE_IGNORE_ACTION_PHASE_ERRORS) == 0 { + return CellError::err(CellErrorType::InternalError) + .context("External send message must have ignore errors send mode"); + } + } + }, + } + builder + .store_i32(32, self.wallet_id)? + .store_u32(32, self.expire_at)? + .store_u32(32, self.seqno)?; + builder.store_bit(true)?; // true means basic actions is stored in reference + + let mut basic_actions = self.basic_actions.clone(); + basic_actions.reverse(); + + builder.store_child(build_out_list(&basic_actions)?)?; + builder.store_bit(false)?; // false means no extended actions + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/internal_message/mod.rs b/rust/chains/tw_ton/src/message/internal_message/mod.rs new file mode 100644 index 00000000000..87516a8f068 --- /dev/null +++ b/rust/chains/tw_ton/src/message/internal_message/mod.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::{Cell, CellArc}; + +pub mod transfer; + +pub struct InternalMessage { + /// https://docs.ton.org/develop/smart-contracts/messages#message-modes + pub mode: u8, + pub message: CellArc, +} + +impl InternalMessage { + pub fn new(mode: u8, message: Cell) -> InternalMessage { + InternalMessage { + mode, + message: message.into_arc(), + } + } +} diff --git a/rust/chains/tw_ton/src/message/internal_message/transfer.rs b/rust/chains/tw_ton/src/message/internal_message/transfer.rs new file mode 100644 index 00000000000..3da99cc0f09 --- /dev/null +++ b/rust/chains/tw_ton/src/message/internal_message/transfer.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::CellResult; + +const BIT_0: bool = false; +const IHR_DISABLED: bool = true; +const BOUNCED: bool = false; +const CURRENCY_COLLECTIONS: bool = false; +const CREATED_LT: u64 = 0; +const CREATED_AT: u32 = 0; +const IHR_FEES: U256 = U256::ZERO; +const FWD_FEES: U256 = U256::ZERO; + +/// Standard Internal message - transfer TON. +pub struct TransferInternalMessage { + pub dest: TonAddress, + pub value: U256, + /// Deploy a smart contract different from the sender's wallet contract. + /// For example, to deploy a chatbot Doge: https://github.com/LaDoger/doge.fc + /// + /// Note consider using [`ExternalMessage::state_init`] to deploy the sender's wallet contract. + pub state_init: Option, + pub data: Option, +} + +impl TransferInternalMessage { + pub fn new(dest: TonAddress, value: U256) -> Self { + TransferInternalMessage { + dest, + value, + state_init: None, + data: None, + } + } + + pub fn with_data(&mut self, data: CellArc) -> &mut Self { + self.data = Some(data); + self + } + + pub fn with_state_init(&mut self, state_init: CellArc) -> &mut Self { + self.state_init = Some(state_init); + self + } + + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder.store_bit(BIT_0)?; // bit0 + builder.store_bit(IHR_DISABLED)?; // ihr_disabled + builder.store_bit(self.dest.bounceable())?; // bounce + builder.store_bit(BOUNCED)?; // bounced + builder.store_address(&TonAddress::NULL)?; // src_addr + builder.store_address(&self.dest)?; // dest_addr + builder.store_coins(&self.value)?; // value + builder.store_bit(CURRENCY_COLLECTIONS)?; // currency_collections + builder.store_coins(&IHR_FEES)?; // ihr_fees + builder.store_coins(&FWD_FEES)?; // fwd_fees + builder.store_u64(64, CREATED_LT)?; // created_lt + builder.store_u32(32, CREATED_AT)?; // created_at + + // (Maybe (Either StateInit ^StateInit)) + builder.store_bit(self.state_init.is_some())?; // state_init? + if let Some(state_init) = self.state_init.as_ref() { + builder.store_bit(true)?; // store state_init as a reference, not inline + builder.store_reference(state_init)?; + } + + // (Either X ^X) = Message X + builder.store_bit(self.data.is_some())?; // data? + if let Some(data) = self.data.as_ref() { + builder.store_reference(data)?; + } + + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/mod.rs b/rust/chains/tw_ton/src/message/mod.rs new file mode 100644 index 00000000000..0996f7b8276 --- /dev/null +++ b/rust/chains/tw_ton/src/message/mod.rs @@ -0,0 +1,5 @@ +pub mod external_message; +pub mod internal_message; +pub mod out_list; +pub mod payload; +pub mod signed_message; diff --git a/rust/chains/tw_ton/src/message/out_list/mod.rs b/rust/chains/tw_ton/src/message/out_list/mod.rs new file mode 100644 index 00000000000..85f6df623d8 --- /dev/null +++ b/rust/chains/tw_ton/src/message/out_list/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::out_list::out_action::OutAction; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +pub mod out_action; + +pub fn build_out_list(actions: &[OutAction]) -> CellResult { + let cell = actions + .iter() + .fold(CellBuilder::new().build(), |acc, action| { + let mut builder = CellBuilder::new(); + builder.store_child(acc?)?; + builder.store_cell(&action.build()?)?; + builder.build() + }); + + let mut builder = CellBuilder::new(); + builder.store_cell(&cell?)?; + builder.build() +} diff --git a/rust/chains/tw_ton/src/message/out_list/out_action.rs b/rust/chains/tw_ton/src/message/out_list/out_action.rs new file mode 100644 index 00000000000..bf270c307b2 --- /dev/null +++ b/rust/chains/tw_ton/src/message/out_list/out_action.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::CellResult; + +#[derive(Clone)] +pub enum OutActionType { + // Currently, only SendMsg is supported. SetCode is not supported. + SendMsg, +} + +impl OutActionType { + // See https://ton.org/tblkch.pdf 4.4.11. Serialization of output actions + pub fn to_ser_tag(&self) -> u32 { + match self { + OutActionType::SendMsg => 0x0ec3c86d, + } + } +} + +#[derive(Clone)] +pub struct OutAction { + pub typ: OutActionType, + pub mode: u8, + pub data: CellArc, // out msg (SendMsg) or new code (SetCode) +} + +impl OutAction { + pub fn new(typ: OutActionType, mode: u8, out_msg: CellArc) -> Self { + OutAction { + typ, + mode, + data: out_msg, + } + } + + pub fn build(&self) -> CellResult { + match self.typ { + OutActionType::SendMsg => self.build_out_action_send_msg(), + } + } + + fn build_out_action_send_msg(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder + .store_u32(32, self.typ.to_ser_tag())? + .store_u8(8, self.mode)? + .store_reference(&self.data)?; + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/comment.rs b/rust/chains/tw_ton/src/message/payload/comment.rs new file mode 100644 index 00000000000..64176ae1d15 --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/comment.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +/// Transaction payload the consists of an arbitrary comment only. +pub struct CommentPayload { + comment: String, +} + +impl CommentPayload { + pub fn new(comment: String) -> Self { + CommentPayload { comment } + } + + pub fn build(&self) -> CellResult { + let mut builder = CellBuilder::new(); + builder.store_u32(32, 0)?.store_string(&self.comment)?; + builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/empty.rs b/rust/chains/tw_ton/src/message/payload/empty.rs new file mode 100644 index 00000000000..724cad0592e --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/empty.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +/// Empty transaction payload. +pub struct EmptyPayload; + +impl EmptyPayload { + pub fn build(&self) -> CellResult { + CellBuilder::new().build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs b/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs new file mode 100644 index 00000000000..b6e687551ae --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/jetton_transfer.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_coin_entry::error::prelude::ResultContext; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::{CellError, CellErrorType, CellResult}; + +pub const JETTON_TRANSFER: u32 = 0x0f8a7ea5; + +/// Jetton transfer message payload with an optional comment. +#[derive(Debug)] +pub struct JettonTransferPayload { + /// Arbitrary request number. + query_id: u64, + /// Amount of transferred jettons in elementary units. + jetton_amount: U256, + /// Address of the new owner of the jettons. + destination: TonAddress, + /// Address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins. + response_destination: TonAddress, + /// Optional custom data (which is used by either sender or receiver jetton wallet for inner logic). + /// At WalletCore, we do not use `custom_payload` at the moment. + #[allow(dead_code)] + custom_payload: Option, + /// Amount of nanotons to be sent to the destination address. + forward_ton_amount: U256, + /// Optional custom data that should be sent to the destination address. + /// At WalletCore, we do not use `forward_payload` at the moment. + #[allow(dead_code)] + forward_payload: Option, + /// Optional transfer comment. + comment: Option, +} + +impl JettonTransferPayload { + pub fn new(destination: TonAddress, jetton_amount: U256) -> Self { + JettonTransferPayload { + query_id: 0, + jetton_amount, + destination, + response_destination: TonAddress::null(), + custom_payload: None, + forward_ton_amount: U256::zero(), + forward_payload: None, + comment: None, + } + } + + pub fn with_query_id(&mut self, query_id: u64) -> &mut Self { + self.query_id = query_id; + self + } + + pub fn with_response_destination(&mut self, response_destination: TonAddress) -> &mut Self { + self.response_destination = response_destination; + self + } + + pub fn with_comment(&mut self, comment: String) -> &mut Self { + self.comment = Some(comment); + self + } + + pub fn with_forward_ton_amount(&mut self, forward_ton_amount: U256) -> &mut Self { + self.forward_ton_amount = forward_ton_amount; + self + } + + pub fn build(&self) -> CellResult { + if self.forward_ton_amount.is_zero() && self.forward_payload.is_some() { + return CellError::err(CellErrorType::CellBuilderError) + .context("Forward_ton_amount must be positive when specifying forward_payload"); + } + + let mut message = CellBuilder::new(); + message.store_u32(32, JETTON_TRANSFER)?; + message.store_u64(64, self.query_id)?; + message.store_coins(&self.jetton_amount)?; + message.store_address(&self.destination)?; + message.store_address(&self.response_destination)?; + + if let Some(ref cp) = self.custom_payload { + message.store_bit(true)?; + message.store_reference(cp)?; + } else { + message.store_bit(false)?; + } + + message.store_coins(&self.forward_ton_amount)?; + + if let Some(ref fp) = self.forward_payload { + message.store_bit(true)?; + message.store_reference(fp)?; + } else { + message.store_bit(false)?; + } + + if let Some(ref comment) = self.comment { + message.store_u32(32, 0)?; + message.store_string(comment)?; + } + + message.build() + } +} diff --git a/rust/chains/tw_ton/src/message/payload/mod.rs b/rust/chains/tw_ton/src/message/payload/mod.rs new file mode 100644 index 00000000000..8b50715cd12 --- /dev/null +++ b/rust/chains/tw_ton/src/message/payload/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod comment; +pub mod empty; +pub mod jetton_transfer; diff --git a/rust/chains/tw_ton/src/message/signed_message/mod.rs b/rust/chains/tw_ton/src/message/signed_message/mod.rs new file mode 100644 index 00000000000..cfe3788fe74 --- /dev/null +++ b/rust/chains/tw_ton/src/message/signed_message/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod signed_message_v4; +pub mod signed_message_v5; diff --git a/rust/chains/tw_ton/src/message/signed_message/signed_message_v4.rs b/rust/chains/tw_ton/src/message/signed_message/signed_message_v4.rs new file mode 100644 index 00000000000..4b624b247f0 --- /dev/null +++ b/rust/chains/tw_ton/src/message/signed_message/signed_message_v4.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H512; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::*; + +pub struct SignedMessageV4 { + pub signature: H512, + pub external_message: Cell, +} + +impl SignedMessageV4 { + pub fn build(&self) -> CellResult { + let mut body_builder = CellBuilder::new(); + + // In the case of WALLET_V4_R2, the signature is stored before the external message. + body_builder.store_slice(self.signature.as_slice())?; + body_builder.store_cell(&self.external_message)?; + + body_builder.build() + } +} diff --git a/rust/chains/tw_ton/src/message/signed_message/signed_message_v5.rs b/rust/chains/tw_ton/src/message/signed_message/signed_message_v5.rs new file mode 100644 index 00000000000..932fafa9894 --- /dev/null +++ b/rust/chains/tw_ton/src/message/signed_message/signed_message_v5.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H512; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::*; + +pub struct SignedMessageV5 { + pub signature: H512, + pub external_message: Cell, +} + +impl SignedMessageV5 { + pub fn build(&self) -> CellResult { + let mut body_builder = CellBuilder::new(); + + // In the case of WALLET_V5_R1, the signature is stored after the external message. + body_builder.store_cell(&self.external_message)?; + body_builder.store_slice(self.signature.as_slice())?; + + body_builder.build() + } +} diff --git a/rust/chains/tw_ton/src/modules/address_converter.rs b/rust/chains/tw_ton/src/modules/address_converter.rs new file mode 100644 index 00000000000..733524140af --- /dev/null +++ b/rust/chains/tw_ton/src/modules/address_converter.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_coin_entry::error::prelude::ResultContext; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; + +const HAS_CRC32: bool = true; + +pub struct AddressConverter; + +impl AddressConverter { + /// Converts a TON user address into a single root Cell. + pub fn convert_to_cell(addr: &TonAddress) -> CellResult { + let mut builder = CellBuilder::new(); + builder + .store_address(addr) + .context("Error storing given address to CellBuilder")?; + builder.build() + } + + /// Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. + /// The function is mostly used to request a Jetton user address via `get_wallet_address` RPC. + /// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user + pub fn convert_to_boc(addr: &TonAddress) -> CellResult { + Self::convert_to_cell(addr).map(BagOfCells::from_root) + } + + /// Converts a TON user address into a Bag of Cells (BoC) with a single root Cell. + /// The function is mostly used to request a Jetton user address via `get_wallet_address` RPC. + /// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user + pub fn convert_to_boc_base64(addr: &TonAddress) -> CellResult { + Self::convert_to_boc(addr).and_then(|boc| boc.to_base64(HAS_CRC32)) + } + + /// Parses a TON address from a single root Cell. + pub fn parse_from_cell(cell: &Cell) -> CellResult { + cell.parse_fully(|parser| parser.load_address()) + .map(TonAddress::with_address_data) + } + + /// Parses a TON address from a Bag of Cells (BoC) with a single root Cell. + /// The function is mostly used to parse a Jetton user address received on `get_wallet_address` RPC. + /// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user + pub fn parse_from_boc(boc: &BagOfCells) -> CellResult { + boc.single_root() + .and_then(|cell| Self::parse_from_cell(cell)) + } + + /// Parses a TON address from a Bag of Cells (BoC) with a single root Cell. + /// The function is mostly used to parse a Jetton user address received on `get_wallet_address` RPC. + /// https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user + pub fn parse_from_boc_base64(boc: &str) -> CellResult { + BagOfCells::parse_base64(boc).and_then(|boc| Self::parse_from_boc(&boc)) + } +} diff --git a/rust/chains/tw_ton/src/modules/mod.rs b/rust/chains/tw_ton/src/modules/mod.rs new file mode 100644 index 00000000000..95d32dc4847 --- /dev/null +++ b/rust/chains/tw_ton/src/modules/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address_converter; +pub mod personal_message_signer; +pub mod transaction_util; +pub mod wallet_provider; diff --git a/rust/chains/tw_ton/src/modules/personal_message_signer.rs b/rust/chains/tw_ton/src/modules/personal_message_signer.rs new file mode 100644 index 00000000000..fad7a7a023d --- /dev/null +++ b/rust/chains/tw_ton/src/modules/personal_message_signer.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::sha2::sha512; +use tw_keypair::ed25519::sha512::PrivateKey; +use tw_keypair::ed25519::Signature; +use tw_keypair::traits::SigningKeyTrait; +use tw_keypair::KeyPairResult; + +pub const TON_PERSONAL_MESSAGE_PREFIX: &str = "ton-safe-sign-magic"; + +pub struct PersonalMessageSigner; + +impl PersonalMessageSigner { + /// Signs an arbitrary message. + /// https://www.openmask.app/docs/api-reference/rpc-api#ton_personalsign + /// https://github.com/OpenProduct/openmask-extension/blob/7566ceb2772fed7a3a27d2a67bd34bf89e862557/src/view/screen/notifications/sign/api.ts#L21-L48 + pub fn sign(private_key: &PrivateKey, msg: &str) -> KeyPairResult { + let msg_hash = sha512(msg.as_bytes()); + + let mut msg_to_sign = vec![0xff_u8, 0xff]; + msg_to_sign.extend_from_slice(TON_PERSONAL_MESSAGE_PREFIX.as_bytes()); + msg_to_sign.extend_from_slice(msg_hash.as_slice()); + + private_key.sign(msg_to_sign) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_hash::H512; + + #[test] + fn test_sign_personal_message() { + // The private key has been derived by using [ton-mnemonic](https://www.npmjs.com/package/tonweb-mnemonic/v/0.0.2) + // from the following mnemonic: + // document shield addict crime broom point story depend suit satisfy test chicken valid tail speak fortune sound drill seek cube cheap body music recipe + let private_key = PrivateKey::try_from( + "112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18", + ) + .unwrap(); + + let signature = PersonalMessageSigner::sign(&private_key, "Hello world").unwrap(); + // The following signature has been computed by calling `window.ton.send("ton_personalSign", { data: "Hello world" });`. + let expected_sig = "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504"; + assert_eq!(signature.to_bytes(), H512::from(expected_sig)); + } +} diff --git a/rust/chains/tw_ton/src/modules/transaction_util.rs b/rust/chains/tw_ton/src/modules/transaction_util.rs new file mode 100644 index 00000000000..47200e82187 --- /dev/null +++ b/rust/chains/tw_ton/src/modules/transaction_util.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::transaction_util::TransactionUtil; +use tw_ton_sdk::boc::BagOfCells; + +pub struct TonTransactionUtil; + +impl TransactionUtil for TonTransactionUtil { + fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + Self::calc_tx_hash_impl(coin, encoded_tx) + } +} + +impl TonTransactionUtil { + // In the TON blockchain, there are both message hashes and transaction hashes. + // Strictly speaking, this function returns the message hash, not the transaction hash, + // because we often use the TON message hash to track transaction status. + // The transaction hash is unknown until the transaction is included in a block. + fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult { + let boc = BagOfCells::parse_base64(encoded_tx) + .map_err(|_| SigningErrorType::Error_input_parse)?; + let root_cell_hash = boc + .roots + .first() + .ok_or(SigningErrorType::Error_input_parse)? + .cell_hash(); + + // The message hash in TON can be encoded in base64, base64url, or hex. + // Here, we return the message hash in hex encoding. + Ok(root_cell_hash.to_string()) + } +} diff --git a/rust/chains/tw_ton/src/modules/wallet_provider.rs b/rust/chains/tw_ton/src/modules/wallet_provider.rs new file mode 100644 index 00000000000..ed09e7f93df --- /dev/null +++ b/rust/chains/tw_ton/src/modules/wallet_provider.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::wallet::{wallet_v4, wallet_v5}; +use tw_keypair::ed25519::sha512::PublicKey; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::error::CellResult; + +const HAS_CRC32: bool = true; + +/// TON Wallet common functionality. +pub struct WalletProvider; + +impl WalletProvider { + /// Constructs a TON Wallet V4R2 stateInit encoded as BoC (BagOfCells) for the given `public_key`. + pub fn v4r2_state_init( + public_key: PublicKey, + workchain: i32, + wallet_id: i32, + ) -> CellResult { + let state_init = wallet_v4::WalletV4R2::with_public_key(workchain, public_key, wallet_id)? + .state_init()? + .to_cell()?; + BagOfCells::from_root(state_init).to_base64(HAS_CRC32) + } + + /// Constructs a TON Wallet V5R1 stateInit encoded as BoC (BagOfCells) for the given `public_key`. + pub fn v5r1_state_init( + public_key: PublicKey, + workchain: i32, + wallet_id: i32, + ) -> CellResult { + let state_init = wallet_v5::WalletV5R1::with_public_key(workchain, public_key, wallet_id)? + .state_init()? + .to_cell()?; + BagOfCells::from_root(state_init).to_base64(HAS_CRC32) + } +} diff --git a/rust/chains/tw_ton/src/resources.rs b/rust/chains/tw_ton/src/resources.rs new file mode 100644 index 00000000000..04a1ad37c81 --- /dev/null +++ b/rust/chains/tw_ton/src/resources.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use lazy_static::lazy_static; +use tw_ton_sdk::boc::BagOfCells; + +pub const DEFAULT_WALLET_ID: i32 = 0x29a9a317; +/// The wallet id 2147483409 comes from: https://github.com/ton-org/ton/blob/f9842909ac0e7d6f66d055dd18a4c41ec3416c02/src/wallets/v5r1/WalletV5R1WalletId.ts#L21C22-L21C32 +/// The V5R1 wallet id differs between the mainnet and testnet. We support V5R1 only on the mainnet. +pub const WALLET_ID_V5R1_TON_MAINNET: i32 = 2147483409; +/// https://docs.ton.org/develop/howto/step-by-step#1-smart-contract-addresses +pub const BASE_WORKCHAIN: i32 = 0; +pub const MASTER_WORKCHAIN: i32 = -1; + +lazy_static! { + pub static ref WALLET_V4R2_CODE: BagOfCells = { + let code = include_str!("../resources/wallet/wallet_v4r2.code"); + BagOfCells::parse_base64(code).expect("Cannot decode wallet_v4r2.code") + }; + pub static ref WALLET_V5R1_CODE: BagOfCells = { + let code = include_str!("../resources/wallet/wallet_v5r1.code"); + BagOfCells::parse_base64(code).expect("Cannot decode wallet_v5r1.code") + }; +} diff --git a/rust/chains/tw_ton/src/signer.rs b/rust/chains/tw_ton/src/signer.rs new file mode 100644 index 00000000000..44306434b73 --- /dev/null +++ b/rust/chains/tw_ton/src/signer.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::compiler::HAS_CRC32; +use crate::signing_request::builder::SigningRequestBuilder; +use crate::signing_request::cell_creator::ExternalMessageCreator; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_misc::traits::ToBytesVec; +use tw_proto::TheOpenNetwork::Proto; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::error::cell_to_signing_error; + +pub struct TheOpenNetworkSigner; + +impl TheOpenNetworkSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { + let signing_request = SigningRequestBuilder::build(&input)?; + + let external_message = + ExternalMessageCreator::create_external_message_to_sign(&signing_request) + .map_err(cell_to_signing_error)?; + + // Whether to add 'StateInit' reference. + let state_init = signing_request.seqno == 0; + let signed_tx = signing_request + .wallet + .sign_transaction(external_message, state_init) + .context("Error signing/wrapping an external message")? + .build() + .context("Error generating signed message cell") + .map_err(cell_to_signing_error)?; + + let signed_tx_hash = signed_tx.cell_hash(); + let signed_tx_encoded = BagOfCells::from_root(signed_tx) + .to_base64(HAS_CRC32) + .context("Error serializing signed transaction as BoC") + .map_err(cell_to_signing_error)?; + + Ok(Proto::SigningOutput { + encoded: signed_tx_encoded.into(), + hash: signed_tx_hash.to_vec().into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_ton/src/signing_request/builder.rs b/rust/chains/tw_ton/src/signing_request/builder.rs new file mode 100644 index 00000000000..5324546deb4 --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/builder.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::signing_request::{ + JettonTransferRequest, SigningRequest, TransferCustomRequest, TransferPayload, TransferRequest, +}; +use crate::wallet::{wallet_v4, wallet_v5, VersionedTonWallet}; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +use tw_number::U256; +use tw_proto::TheOpenNetwork::Proto; +use tw_ton_sdk::error::cell_to_signing_error; +use Proto::mod_Transfer::OneOfpayload as PayloadType; + +const STATE_INIT_EXPIRE_AT: u32 = 0xffffffff; + +pub struct SigningRequestBuilder; + +impl SigningRequestBuilder { + pub fn build(input: &Proto::SigningInput) -> SigningResult { + let wallet = Self::wallet(input)?; + + let messages = input + .messages + .iter() + .map(Self::transfer_request) + .collect::>>()?; + + let expire_at = if input.sequence_number == 0 { + STATE_INIT_EXPIRE_AT + } else if input.expire_at == 0 { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("'expire_at' must be set"); + } else { + input.expire_at + }; + + Ok(SigningRequest { + wallet, + messages, + expire_at, + seqno: input.sequence_number, + }) + } + + /// Currently, V4R2 and V5R1 wallets supported. + fn wallet(input: &Proto::SigningInput) -> SigningResult { + if !input.private_key.is_empty() { + let key_pair = KeyPair::try_from(input.private_key.as_ref()) + .into_tw() + .context("Invalid private key")?; + + return match input.wallet_version { + Proto::WalletVersion::WALLET_V4_R2 => Ok(VersionedTonWallet::V4R2( + wallet_v4::WalletV4R2::std_with_key_pair(&key_pair) + .map_err(cell_to_signing_error)?, + )), + Proto::WalletVersion::WALLET_V5_R1 => Ok(VersionedTonWallet::V5R1( + wallet_v5::WalletV5R1::std_with_key_pair(&key_pair) + .map_err(cell_to_signing_error)?, + )), + _ => SigningError::err(SigningErrorType::Error_not_supported) + .context("Wallet version not supported"), + }; + } + + let public_key = PublicKey::try_from(input.public_key.as_ref()) + .into_tw() + .context("Expected either 'private_key' or 'public_key' to be set")?; + + match input.wallet_version { + Proto::WalletVersion::WALLET_V4_R2 => Ok(VersionedTonWallet::V4R2( + wallet_v4::WalletV4R2::std_with_public_key(public_key) + .map_err(cell_to_signing_error)?, + )), + Proto::WalletVersion::WALLET_V5_R1 => Ok(VersionedTonWallet::V5R1( + wallet_v5::WalletV5R1::std_with_public_key(public_key) + .map_err(cell_to_signing_error)?, + )), + _ => SigningError::err(SigningErrorType::Error_not_supported) + .context("Wallet version not supported"), + } + } + + fn transfer_request(input: &Proto::Transfer) -> SigningResult { + let dest = TonAddress::from_str(input.dest.as_ref()) + .into_tw() + .context("Invalid 'dest' address")? + // Set the 'bounceable' flag explicitly as specified in the Protobuf. + .set_bounceable(input.bounceable); + + let comment = if input.comment.is_empty() { + None + } else { + Some(input.comment.to_string()) + }; + + let mode = u8::try_from(input.mode) + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("'mode' must fit uint8")?; + + let payload = match input.payload { + PayloadType::jetton_transfer(ref jetton) => { + Some(Self::jetton_transfer_request(jetton)?) + }, + PayloadType::custom_payload(ref custom) => Some(Self::custom_request(custom)?), + PayloadType::None => None, + }; + + Ok(TransferRequest { + dest, + ton_amount: U256::from(input.amount), + mode, + comment, + payload, + }) + } + + fn jetton_transfer_request(input: &Proto::JettonTransfer) -> SigningResult { + let dest = TonAddress::from_str(input.to_owner.as_ref()) + .into_tw() + .context("Invalid 'dest' address")?; + + let response_address = TonAddress::from_str(input.response_address.as_ref()) + .into_tw() + .context("Invalid 'response_address' address")?; + + let jetton_payload = JettonTransferRequest { + query_id: input.query_id, + jetton_amount: U256::from(input.jetton_amount), + dest, + response_address, + forward_ton_amount: U256::from(input.forward_amount), + }; + + Ok(TransferPayload::JettonTransfer(jetton_payload)) + } + + fn custom_request(input: &Proto::CustomPayload) -> SigningResult { + let state_init = if input.state_init.is_empty() { + None + } else { + Some(input.state_init.to_string()) + }; + + let payload = if input.payload.is_empty() { + None + } else { + Some(input.payload.to_string()) + }; + + Ok(TransferPayload::Custom(TransferCustomRequest { + state_init, + payload, + })) + } +} diff --git a/rust/chains/tw_ton/src/signing_request/cell_creator.rs b/rust/chains/tw_ton/src/signing_request/cell_creator.rs new file mode 100644 index 00000000000..f2295848d90 --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/cell_creator.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::message::internal_message::transfer::TransferInternalMessage; +use crate::message::internal_message::InternalMessage; +use crate::message::payload::comment::CommentPayload; +use crate::message::payload::empty::EmptyPayload; +use crate::message::payload::jetton_transfer::JettonTransferPayload; +use crate::signing_request::{ + JettonTransferRequest, SigningRequest, TransferCustomRequest, TransferPayload, TransferRequest, +}; +use std::sync::Arc; +use tw_coin_entry::error::prelude::ResultContext; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::{Cell, CellArc}; +use tw_ton_sdk::error::{CellError, CellErrorType, CellResult}; + +pub struct InternalMessageCreator; + +impl InternalMessageCreator { + pub fn create_internal_message( + transfer_request: &TransferRequest, + ) -> CellResult { + let mut transfer_message = TransferInternalMessage::new( + transfer_request.dest.clone(), + transfer_request.ton_amount, + ); + + // Store a custom contract StateInit Cell if it's provided. + if let Some(state_init) = Self::maybe_custom_state_init(transfer_request)? { + transfer_message.with_state_init(state_init); + } + // In WalletCore, we always store the transfer data even if it's an empty Cell. + transfer_message.with_data(Self::transfer_payload(transfer_request)?); + + let transfer_message_cell = transfer_message + .build() + .context("Error generating 'Transfer' internal message cell")?; + + Ok(InternalMessage::new( + transfer_request.mode, + transfer_message_cell, + )) + } + + fn transfer_payload(transfer_request: &TransferRequest) -> CellResult { + match transfer_request.payload { + Some(TransferPayload::JettonTransfer(ref jetton)) => { + Self::jetton_transfer_payload(jetton, transfer_request.comment.clone()) + }, + Some(TransferPayload::Custom(ref custom)) => Self::custom_payload(custom), + // Otherwise, this is an ordinary TON transfer with an optional comment. + None => Self::maybe_comment_payload(transfer_request.comment.clone()), + } + } + + fn maybe_comment_payload(comment: Option) -> CellResult { + match comment { + Some(comment) => CommentPayload::new(comment) + .build() + .context("Error generating Transfer's comment payload"), + None => EmptyPayload + .build() + .context("Error generating Transfer's empty payload"), + } + .map(Cell::into_arc) + } + + fn jetton_transfer_payload( + jetton: &JettonTransferRequest, + comment: Option, + ) -> CellResult { + let mut payload = JettonTransferPayload::new(jetton.dest.clone(), jetton.jetton_amount); + payload + .with_query_id(jetton.query_id) + .with_response_destination(jetton.response_address.clone()) + .with_forward_ton_amount(jetton.forward_ton_amount); + + if let Some(comment) = comment { + payload.with_comment(comment); + } + + payload + .build() + .map(Cell::into_arc) + .context("Error generating Jetton Transfer payload") + } + + fn custom_payload(custom: &TransferCustomRequest) -> CellResult { + match custom.payload { + Some(ref payload) => BagOfCells::parse_base64(payload) + .context("Error parsing custom Transfer payload")? + .single_root() + .map(Arc::clone) + .context("Custom Transfer payload must contain only one single root"), + // Create an empty Cell payload. + None => EmptyPayload + .build() + .map(Cell::into_arc) + .context("Error generating Transfer's empty payload"), + } + } + + fn maybe_custom_state_init(request: &TransferRequest) -> CellResult> { + let Some(TransferPayload::Custom(ref custom)) = request.payload else { + return Ok(None); + }; + + let Some(ref state_init) = custom.state_init else { + return Ok(None); + }; + + let state_init_cell = BagOfCells::parse_base64(state_init) + .context("Error parsing Transfer stateInit")? + .single_root() + .map(Arc::clone) + .context("stateInit must contain only one single root")?; + Ok(Some(state_init_cell)) + } +} + +pub struct ExternalMessageCreator; + +impl ExternalMessageCreator { + pub fn create_external_message_to_sign(request: &SigningRequest) -> CellResult { + if request.messages.is_empty() { + return CellError::err(CellErrorType::CellBuilderError) + .context("There must be at least one Transfer message"); + } + + let internal_messages = request + .messages + .iter() + .map(InternalMessageCreator::create_internal_message) + .collect::>>()?; + + request + .wallet + .create_external_body(request.expire_at, request.seqno, internal_messages) + .context("Error generating an external message cell") + } +} diff --git a/rust/chains/tw_ton/src/signing_request/mod.rs b/rust/chains/tw_ton/src/signing_request/mod.rs new file mode 100644 index 00000000000..d0d0e3b05bf --- /dev/null +++ b/rust/chains/tw_ton/src/signing_request/mod.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::wallet::VersionedTonWallet; +use tw_number::U256; + +pub mod builder; +pub mod cell_creator; + +pub enum TransferPayload { + /// Jetton Transfer message payload. + JettonTransfer(JettonTransferRequest), + /// Custom Transfer message payload. + Custom(TransferCustomRequest), +} + +pub struct TransferRequest { + /// TON recipient address. + /// Also determines whether the transaction is bounceable or not. + pub dest: TonAddress, + /// Amount to send in nanotons. + pub ton_amount: U256, + /// Send mode. + /// https://ton.org/docs/develop/func/stdlib#send_raw_message + pub mode: u8, + /// Transfer comment message. + pub comment: Option, + /// Transfer payload. + pub payload: Option, +} + +pub struct JettonTransferRequest { + /// Arbitrary request number. + pub query_id: u64, + /// Amount of transferred jettons in elementary integer units. + /// The real value transferred is `jetton_amount` multiplied by ten to the power of token decimal precision. + pub jetton_amount: U256, + /// Address of the new owner of the jettons. + pub dest: TonAddress, + /// Address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins. + /// Usually the sender should get back their toncoins. + pub response_address: TonAddress, + /// Amount in nanotons to forward to recipient. Basically minimum amount - 1 nanoton should be used. + pub forward_ton_amount: U256, +} + +pub struct TransferCustomRequest { + /// (string base64, optional): raw one-cell BoC encoded in Base64. + /// Can be used to deploy a smart contract. + pub state_init: Option, + /// (string base64, optional): raw one-cell BoC encoded in Base64. + pub payload: Option, +} + +pub struct SigningRequest { + /// Wallet initialized with the user's key-pair or public key. + pub wallet: VersionedTonWallet, + pub messages: Vec, + /// External message counter. + /// https://ton.org/docs/develop/smart-contracts/guidelines/external-messages + pub seqno: u32, + /// Expiration UNIX timestamp. + pub expire_at: u32, +} diff --git a/rust/chains/tw_ton/src/transaction.rs b/rust/chains/tw_ton/src/transaction.rs new file mode 100644 index 00000000000..1fdf8e8d925 --- /dev/null +++ b/rust/chains/tw_ton/src/transaction.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use tw_number::U256; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::*; +use tw_ton_sdk::message::state_init::StateInit; + +/// See an example at https://docs.ton.org/develop/smart-contracts/tutorials/wallet#contract-deployment-via-wallet +pub const INCOMING_EXTERNAL_TRANSACTION: u8 = 0b10; + +pub struct SignedTransaction { + pub src_address: TonAddress, + pub dest_address: TonAddress, + pub import_fee: U256, + /// Created via `StateInit`. + pub state_init: Option, + pub signed_body: Cell, +} + +impl SignedTransaction { + pub fn build(&self) -> CellResult { + let mut wrap_builder = CellBuilder::new(); + wrap_builder + .store_u8(2, INCOMING_EXTERNAL_TRANSACTION)? // incoming external transaction + .store_address(&self.src_address)? // src + .store_address(&self.dest_address)? // dest + .store_coins(&self.import_fee)?; // import fee + + if let Some(ref state_init) = self.state_init { + wrap_builder.store_bit(true)?; // state init present + wrap_builder.store_bit(true)?; // state init in ref + wrap_builder.store_child(state_init.to_cell()?)?; // state init + } else { + wrap_builder.store_bit(false)?; // state init absent + } + + wrap_builder.store_bit(true)?; // signed_body is always defined + wrap_builder.store_child(self.signed_body.clone())?; // Signed body + + wrap_builder.build() + } +} diff --git a/rust/chains/tw_ton/src/wallet/mod.rs b/rust/chains/tw_ton/src/wallet/mod.rs new file mode 100644 index 00000000000..8b4a01311b7 --- /dev/null +++ b/rust/chains/tw_ton/src/wallet/mod.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::message::internal_message::InternalMessage; +use crate::message::signed_message::signed_message_v4::SignedMessageV4; +use crate::message::signed_message::signed_message_v5::SignedMessageV5; +use crate::transaction::SignedTransaction; +use tw_coin_entry::error::prelude::*; +use tw_keypair::ed25519::Signature; +use tw_keypair::traits::SigningKeyTrait; +use tw_number::U256; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::{cell_to_signing_error, CellResult}; +use tw_ton_sdk::message::state_init::StateInit; + +pub mod wallet_v4; +pub mod wallet_v5; + +/// Notes: Rust specialization for generic code is not completed. See https://github.com/rust-lang/rust/issues/31844 +/// Currently, we use a workaround to implement the versioned TonWallet struct. +/// After the Rust specialization feature is finished, maybe we can remove this workaround. +pub enum VersionedTonWallet { + V4R2(wallet_v4::WalletV4R2), + V5R1(wallet_v5::WalletV5R1), +} + +impl VersionedTonWallet { + pub fn address(&self) -> &TonAddress { + match self { + Self::V4R2(wallet_v4r2) => &wallet_v4r2.address, + Self::V5R1(wallet_v5r1) => &wallet_v5r1.address, + } + } + + pub fn state_init(&self) -> CellResult { + match self { + Self::V4R2(wallet_v4r2) => wallet_v4r2.state_init(), + Self::V5R1(wallet_v5r1) => wallet_v5r1.state_init(), + } + } + + pub fn create_external_body( + &self, + expire_at: u32, + seqno: u32, + internal_messages: Vec, + ) -> CellResult { + match self { + Self::V4R2(wallet_v4r2) => { + wallet_v4r2.create_external_body(expire_at, seqno, internal_messages) + }, + Self::V5R1(wallet_v5r1) => { + wallet_v5r1.create_external_body(expire_at, seqno, internal_messages) + }, + } + } + + pub fn sign_external_message(&self, external_message: Cell) -> SigningResult { + let message_hash = external_message.cell_hash(); + let sig = match self { + Self::V4R2(wallet_v4r2) => wallet_v4r2.private_key.as_ref(), + Self::V5R1(wallet_v5r1) => wallet_v5r1.private_key.as_ref(), + } + .or_tw_err(SigningErrorType::Error_internal) + .context("'TonWallet' should be initialized with a key-pair to be able to sign a message")? + .sign(message_hash.to_vec())?; + + self.compile_signed_external_message(external_message, sig) + } + + pub fn compile_signed_external_message( + &self, + external_message: Cell, + sig: Signature, + ) -> SigningResult { + match self { + Self::V4R2(_) => Ok(SignedMessageV4 { + signature: sig.to_bytes(), + external_message, + } + .build() + .map_err(cell_to_signing_error)?), + + Self::V5R1(_) => Ok(SignedMessageV5 { + signature: sig.to_bytes(), + external_message, + } + .build() + .map_err(cell_to_signing_error)?), + } + } + + pub fn sign_transaction( + &self, + external_message: Cell, + state_init: bool, + ) -> SigningResult { + let signed_external_message = self.sign_external_message(external_message.clone())?; + self.compile_transaction(signed_external_message, state_init) + } + + pub fn compile_transaction( + &self, + signed_external_message: Cell, + state_init: bool, + ) -> SigningResult { + let state_init = if state_init { + let state_init = self.state_init().map_err(cell_to_signing_error)?; + Some(state_init) + } else { + None + }; + + Ok(SignedTransaction { + src_address: TonAddress::null(), + // The wallet contract address. + dest_address: self.address().clone(), + import_fee: U256::zero(), + state_init, + signed_body: signed_external_message, + }) + } +} diff --git a/rust/chains/tw_ton/src/wallet/wallet_v4.rs b/rust/chains/tw_ton/src/wallet/wallet_v4.rs new file mode 100644 index 00000000000..2ecb279a1e1 --- /dev/null +++ b/rust/chains/tw_ton/src/wallet/wallet_v4.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::message::external_message::wallet_v4::ExternalMessageWalletV4; +use crate::message::internal_message::InternalMessage; +use crate::resources::{BASE_WORKCHAIN, DEFAULT_WALLET_ID, WALLET_V4R2_CODE}; +use std::sync::Arc; +use tw_keypair::ed25519::sha512::{KeyPair, PrivateKey, PublicKey}; +use tw_keypair::traits::KeyPairTrait; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; +use tw_ton_sdk::message::state_init::StateInit; + +pub struct WalletV4R2 { + pub public_key: PublicKey, + pub(crate) private_key: Option, + /// TON address derived from the [`TonWallet::public_key`]. + pub address: TonAddress, + pub wallet_id: i32, +} + +impl WalletV4R2 { + /// Creates a standard TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub fn std_with_public_key(public_key: PublicKey) -> CellResult { + let wallet_id = DEFAULT_WALLET_ID; + Self::with_public_key(BASE_WORKCHAIN, public_key, wallet_id) + } + + /// Creates a standard TON wallet from the given key-pair. + pub fn std_with_key_pair(key_pair: &KeyPair) -> CellResult { + let wallet_id = DEFAULT_WALLET_ID; + Self::with_key_pair(BASE_WORKCHAIN, key_pair, wallet_id) + } + + /// Creates a TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub(crate) fn with_public_key( + workchain: i32, + public_key: PublicKey, + wallet_id: i32, + ) -> CellResult { + Self::new(workchain, public_key, None, wallet_id) + } + + /// Creates a TON wallet from the given key-pair. + fn with_key_pair(workchain: i32, key_pair: &KeyPair, wallet_id: i32) -> CellResult { + let public = key_pair.public().clone(); + let private = key_pair.private().clone(); + Self::new(workchain, public, Some(private), wallet_id) + } + + /// Private function to create the VersionedTonWallet with the given public and optional private keys. + /// Do not make it public as the function caller can provide unrelated keys. + fn new( + workchain: i32, + public_key: PublicKey, + private_key: Option, + wallet_id: i32, + ) -> CellResult { + let state_init_hash = Self::state_init_impl(&public_key, wallet_id)?.create_account_id()?; + let address = TonAddress::new(workchain, state_init_hash); + + Ok(Self { + public_key, + private_key, + address, + wallet_id, + }) + } + + /// Return the stateInit for the wallet. + pub fn state_init(&self) -> CellResult { + Self::state_init_impl(&self.public_key, self.wallet_id) + } + + fn state_init_impl(public_key: &PublicKey, wallet_id: i32) -> CellResult { + let seqno = 0; + + let mut builder = CellBuilder::new(); + builder + .store_u32(32, seqno)? + .store_i32(32, wallet_id)? + .store_slice(public_key.as_slice())? + // empty plugin dict + .store_bit(false)?; + + let initial_data = builder.build()?.into_arc(); + let code = WALLET_V4R2_CODE.single_root().map(Arc::clone)?; + + Ok(StateInit::default().set_code(code).set_data(initial_data)) + } + + pub(crate) fn create_external_body( + &self, + expire_at: u32, + seqno: u32, + internal_messages: Vec, + ) -> CellResult { + ExternalMessageWalletV4 { + wallet_id: self.wallet_id, + expire_at, + seqno, + internal_messages, + } + .build() + } +} diff --git a/rust/chains/tw_ton/src/wallet/wallet_v5.rs b/rust/chains/tw_ton/src/wallet/wallet_v5.rs new file mode 100644 index 00000000000..98a45b6a94e --- /dev/null +++ b/rust/chains/tw_ton/src/wallet/wallet_v5.rs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::TonAddress; +use crate::message::external_message::wallet_v5::{ExternalMessageWalletV5, V5R1OpCode}; +use crate::message::internal_message::InternalMessage; +use crate::message::out_list::out_action::{OutAction, OutActionType}; +use crate::resources::{BASE_WORKCHAIN, WALLET_ID_V5R1_TON_MAINNET, WALLET_V5R1_CODE}; +use std::sync::Arc; +use tw_keypair::ed25519::sha512::{KeyPair, PrivateKey, PublicKey}; +use tw_keypair::traits::KeyPairTrait; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; +use tw_ton_sdk::error::CellResult; +use tw_ton_sdk::message::state_init::StateInit; + +pub struct WalletV5R1 { + pub public_key: PublicKey, + pub(crate) private_key: Option, + /// TON address derived from the [`TonWallet::public_key`]. + pub address: TonAddress, + pub wallet_id: i32, +} + +impl WalletV5R1 { + /// Creates a standard TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub fn std_with_public_key(public_key: PublicKey) -> CellResult { + let wallet_id = WALLET_ID_V5R1_TON_MAINNET; + Self::with_public_key(BASE_WORKCHAIN, public_key, wallet_id) + } + + /// Creates a standard TON wallet from the given key-pair. + pub fn std_with_key_pair(key_pair: &KeyPair) -> CellResult { + let wallet_id = WALLET_ID_V5R1_TON_MAINNET; + Self::with_key_pair(BASE_WORKCHAIN, key_pair, wallet_id) + } + + /// Creates a TON wallet from the given public key. + /// Please note when created with public key only, wallet cannot be used to sign messages. + pub(crate) fn with_public_key( + workchain: i32, + public_key: PublicKey, + wallet_id: i32, + ) -> CellResult { + Self::new(workchain, public_key, None, wallet_id) + } + + /// Creates a TON wallet from the given key-pair. + fn with_key_pair(workchain: i32, key_pair: &KeyPair, wallet_id: i32) -> CellResult { + let public = key_pair.public().clone(); + let private = key_pair.private().clone(); + Self::new(workchain, public, Some(private), wallet_id) + } + + /// Private function to create the VersionedTonWallet with the given public and optional private keys. + /// Do not make it public as the function caller can provide unrelated keys. + fn new( + workchain: i32, + public_key: PublicKey, + private_key: Option, + wallet_id: i32, + ) -> CellResult { + let state_init_hash = Self::state_init_impl(&public_key, wallet_id)?.create_account_id()?; + let address = TonAddress::new(workchain, state_init_hash); + + Ok(Self { + public_key, + private_key, + address, + wallet_id, + }) + } + + /// Return the stateInit for the wallet. + pub(crate) fn state_init(&self) -> CellResult { + Self::state_init_impl(&self.public_key, self.wallet_id) + } + + fn state_init_impl(public_key: &PublicKey, wallet_id: i32) -> CellResult { + let seqno = 0; + + let mut builder = CellBuilder::new(); + builder + .store_bit(true)? // signature auth allowed + .store_u32(32, seqno)? + .store_i32(32, wallet_id)? + .store_slice(public_key.as_slice())? + // empty plugin dict + .store_bit(false)?; + + let initial_data = builder.build()?.into_arc(); + let code = WALLET_V5R1_CODE.single_root().map(Arc::clone)?; + + Ok(StateInit::default().set_code(code).set_data(initial_data)) + } + + pub(crate) fn create_external_body( + &self, + expire_at: u32, + seqno: u32, + internal_messages: Vec, + ) -> CellResult { + // Convert internal_messages to basic_actions + let basic_actions: Vec = internal_messages + .into_iter() + .map(|msg| OutAction { + typ: OutActionType::SendMsg, + mode: msg.mode, + data: msg.message, + }) + .collect(); + + ExternalMessageWalletV5 { + opcode: V5R1OpCode::AuthSignedExternal, + wallet_id: self.wallet_id, + expire_at, + seqno, + basic_actions, + } + .build() + } +} diff --git a/rust/chains/tw_ton/tests/address.rs b/rust/chains/tw_ton/tests/address.rs new file mode 100644 index 00000000000..fb99cd0c008 --- /dev/null +++ b/rust/chains/tw_ton/tests/address.rs @@ -0,0 +1,39 @@ +use tw_keypair::ed25519::sha512::KeyPair; +use tw_keypair::traits::KeyPairTrait; +use tw_ton::wallet::wallet_v5; + +/// Tests for TON V5R1 address. +/// +/// Note: These tests should move to rust/tw_any_coin/tests/chains/ton/ton_address.rs after we define +/// a new Derivation for the TON V5R1 address. +#[test] +fn test_ton_v5r1_address_derive() { + let test_cases = [( + "3570e35f54cfb843f2cfaf2b8cae7ceeb7b32225d7dbbd86f611056d74d9073e", // private key + "UQAU3o5-Sp1MYRpw3U7b_wmARxqI49LxiFhEoVCxpUKjTYXk", // V5R1 address on the mainnet + )]; + + for test_case in test_cases.iter() { + let private_key_hex = test_case.0; + let expected_address = test_case.1; + + // Decode the private key from hex + let private_key_bytes = + tw_encoding::hex::decode(private_key_hex).expect("Invalid hex string"); + + // Create a KeyPair from the private key bytes + let key_pair = + KeyPair::try_from(private_key_bytes.as_slice()).expect("Failed to create key pair"); + + // Extract the public key from the KeyPair + let public_key = key_pair.public().clone(); + + // Create the VersionedTonWallet using the public key + let wallet = wallet_v5::WalletV5R1::std_with_public_key(public_key) + .expect("Failed to create wallet"); + + let actual_address = wallet.address.to_string(); + + assert_eq!(actual_address, expected_address); + } +} diff --git a/rust/coverage.stats b/rust/coverage.stats index 7d7ab43dc7c..fd6ae261616 100644 --- a/rust/coverage.stats +++ b/rust/coverage.stats @@ -1 +1 @@ -92.0 \ No newline at end of file +94.0 \ No newline at end of file diff --git a/rust/frameworks/tw_ton_sdk/Cargo.toml b/rust/frameworks/tw_ton_sdk/Cargo.toml new file mode 100644 index 00000000000..c8544bef84b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tw_ton_sdk" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitreader = "0.3.8" +bitstream-io = "2.5.0" +crc = "3" +lazy_static = "1.4.0" +num-bigint = "0.4" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_keypair = { path = "../../tw_keypair" } +tw_hash = { path = "../../tw_hash" } +tw_memory = { path = "../../tw_memory" } +tw_number = { path = "../../tw_number" } + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/rust/frameworks/tw_ton_sdk/fuzz/.gitignore b/rust/frameworks/tw_ton_sdk/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml b/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml new file mode 100644 index 00000000000..2536b143a70 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tw_ton_sdk-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.tw_ton_sdk] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "boc_encode" +path = "fuzz_targets/boc_encode.rs" +test = false +doc = false diff --git a/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs b/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs new file mode 100644 index 00000000000..fd178943d78 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/fuzz/fuzz_targets/boc_encode.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_ton_sdk::boc::BagOfCells; + +fuzz_target!(|data: &[u8]| { + let _ = BagOfCells::parse(data); +}); diff --git a/rust/frameworks/tw_ton_sdk/src/address/address_data.rs b/rust/frameworks/tw_ton_sdk/src/address/address_data.rs new file mode 100644 index 00000000000..73784543467 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/address_data.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::H256; + +const WORKCHAIN_MASK: i32 = 0xff; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AddressData { + pub workchain: i32, + pub hash_part: H256, +} + +impl AddressData { + pub const NULL: AddressData = AddressData::null(); + + pub const fn null() -> AddressData { + AddressData { + workchain: 0, + hash_part: H256::new(), + } + } + + pub fn new(workchain: i32, hash_part: H256) -> AddressData { + AddressData { + workchain, + hash_part, + } + } + + pub fn workchain_byte(&self) -> u8 { + (self.workchain & WORKCHAIN_MASK) as u8 + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/address/mod.rs b/rust/frameworks/tw_ton_sdk/src/address/mod.rs new file mode 100644 index 00000000000..0a9b80e7d60 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address_data; +pub mod raw_address; +pub mod user_friendly_address; diff --git a/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs b/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs new file mode 100644 index 00000000000..37c18357c4d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/raw_address.rs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::address_data::AddressData; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::error::prelude::AddressError; +use tw_encoding::hex; +use tw_hash::H256; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAddress(AddressData); + +impl RawAddress { + #[inline] + pub fn into_data(self) -> AddressData { + self.0 + } +} + +impl AsRef for RawAddress { + #[inline] + fn as_ref(&self) -> &AddressData { + &self.0 + } +} + +impl From for RawAddress { + #[inline] + fn from(value: AddressData) -> Self { + RawAddress(value) + } +} + +impl FromStr for RawAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let mut it = s.split(':'); + + let workchain = it + .next() + .ok_or(AddressError::MissingPrefix)? + .parse::() + .map_err(|_| AddressError::InvalidInput)?; + + let hash_hex = it.next().ok_or(AddressError::InvalidInput)?; + let decoded_hash_part = hex::decode(hash_hex).map_err(|_| AddressError::FromHexError)?; + + let hash_part = + H256::try_from(decoded_hash_part.as_slice()).map_err(|_| AddressError::InvalidInput)?; + + // Expected only 2 parts of the hex-encoded address. + if it.next().is_some() { + return Err(AddressError::InvalidInput); + } + + Ok(RawAddress(AddressData::new(workchain, hash_part))) + } +} + +impl fmt::Display for RawAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prefixed = false; + write!( + f, + "{}:{}", + self.0.workchain, + hex::encode(self.0.hash_part, prefixed) + ) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs b/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs new file mode 100644 index 00000000000..b1723e108d1 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/address/user_friendly_address.rs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::address_data::AddressData; +use crate::crc::CRC_16_XMODEM; +use tw_coin_entry::error::prelude::{AddressError, AddressResult}; +use tw_encoding::base64; +use tw_encoding::base64::{NO_PAD, URL_NO_PAD}; +use tw_hash::{H256, H288}; + +const BASE64_ADDRESS_LEN: usize = 48; +const CHECKSUM_MASK: u16 = 0xff; + +const BOUNCEABLE: u8 = 0x11; +const NON_BOUNCEABLE: u8 = 0x51; +const BOUNCEABLE_TESTNET: u8 = 0x91; +const NON_BOUNCEABLE_TESTNET: u8 = 0xD1; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserFriendlyAddress { + data: AddressData, + bounceable: bool, + testnet: bool, +} + +impl UserFriendlyAddress { + #[inline] + pub const fn with_flags(data: AddressData, bounceable: bool, testnet: bool) -> Self { + UserFriendlyAddress { + data, + bounceable, + testnet, + } + } + + #[inline] + pub fn into_data(self) -> AddressData { + self.data + } + + #[inline] + pub fn bounceable(&self) -> bool { + self.bounceable + } + + #[inline] + pub fn testnet(&self) -> bool { + self.testnet + } + + #[inline] + pub fn set_bounceable(self, bounceable: bool) -> Self { + UserFriendlyAddress { bounceable, ..self } + } + + #[inline] + pub fn set_testnet(self, testnet: bool) -> Self { + UserFriendlyAddress { testnet, ..self } + } + + /// Parses url-safe base64 representation of an address + /// + /// # Returns + /// the address, non-bounceable flag, non-production flag. + pub fn from_base64_url(s: &str) -> AddressResult { + Self::from_base64_with_config(s, URL_NO_PAD) + } + + /// Parses standard base64 representation of an address + /// + /// # Returns + /// the address, bounceable flag, testnet flag. + pub fn from_base64_std(s: &str) -> AddressResult { + Self::from_base64_with_config(s, NO_PAD) + } + + /// Parses base64 representation of an address with encoding config. + /// + /// # Returns + /// the address, non-bounceable flag, non-production flag. + fn from_base64_with_config(s: &str, config: base64::Config) -> AddressResult { + if s.len() != BASE64_ADDRESS_LEN { + return Err(AddressError::InvalidInput); + } + let bytes = base64::decode(s, config).map_err(|_| AddressError::FromBase64Error)?; + // Address length has been checked already. + let slice = H288::try_from(bytes.as_slice()).map_err(|_| AddressError::Internal)?; + Self::from_base64_bytes(slice) + } + + /// Parses decoded base64 representation of an address + /// + /// # Returns + /// the address, bounceable flag, testnet flag. + fn from_base64_bytes(bytes: H288) -> AddressResult { + let (bounceable, testnet) = match bytes[0] { + BOUNCEABLE => (true, false), + NON_BOUNCEABLE => (false, false), + BOUNCEABLE_TESTNET => (true, true), + NON_BOUNCEABLE_TESTNET => (false, true), + _ => return Err(AddressError::InvalidInput), + }; + + let workchain = bytes[1] as i8 as i32; + + let calc_crc = CRC_16_XMODEM.checksum(&bytes[0..34]); + let addr_crc = ((bytes[34] as u16) << 8) | bytes[35] as u16; + if calc_crc != addr_crc { + return Err(AddressError::InvalidChecksum); + } + + let hash_part = H256::try_from(&bytes[2..34]).expect("Expected exactly 32 bytes"); + let data = AddressData::new(workchain, hash_part); + Ok(UserFriendlyAddress { + data, + bounceable, + testnet, + }) + } + + pub fn to_base64_url(&self) -> String { + self.to_base64_with_config(URL_NO_PAD) + } + + pub fn to_base64_std(&self) -> String { + self.to_base64_with_config(NO_PAD) + } + + fn to_base64_with_config(&self, config: base64::Config) -> String { + let bytes = self.to_base64_bytes(); + base64::encode(bytes.as_slice(), config) + } + + fn to_base64_bytes(&self) -> H288 { + let mut bytes = H288::default(); + let tag: u8 = match (self.bounceable, self.testnet) { + (false, false) => NON_BOUNCEABLE, + (true, false) => BOUNCEABLE, + (false, true) => NON_BOUNCEABLE_TESTNET, + (true, true) => BOUNCEABLE_TESTNET, + }; + bytes[0] = tag; + bytes[1] = self.data.workchain_byte(); + bytes[2..34].clone_from_slice(self.data.hash_part.as_slice()); + let crc = CRC_16_XMODEM.checksum(&bytes[0..34]); + bytes[34] = ((crc >> 8) & CHECKSUM_MASK) as u8; + bytes[35] = (crc & CHECKSUM_MASK) as u8; + + bytes + } +} + +impl AsRef for UserFriendlyAddress { + #[inline] + fn as_ref(&self) -> &AddressData { + &self.data + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs b/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs new file mode 100644 index 00000000000..5ae28069164 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/binary_reader.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::error::{CellErrorType, CellResult}; +use bitstream_io::{BigEndian, ByteRead, ByteReader}; +use std::io::Cursor; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +pub struct BinaryReader<'a> { + reader: ByteReader, BigEndian>, +} + +#[allow(dead_code)] +impl<'a> BinaryReader<'a> { + pub fn new(data: &'a [u8]) -> Self { + let cursor = Cursor::new(data); + BinaryReader { + reader: ByteReader::new(cursor), + } + } + + pub fn read_u8(&mut self) -> CellResult { + self.reader + .read::() + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_u32(&mut self) -> CellResult { + self.reader + .read::() + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_bytes(&mut self, buf: &mut [u8]) -> CellResult<()> { + self.reader + .read_bytes(buf) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_to_vec(&mut self, num_bytes: usize) -> CellResult { + self.reader + .read_to_vec(num_bytes) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } + + pub fn read_var_size(&mut self, num_bytes: usize) -> CellResult { + let mut bytes = vec![0; num_bytes]; + self.read_bytes(&mut bytes)?; + + let mut result = 0; + for &byte in &bytes { + result <<= 8; + result |= usize::from(byte); + } + Ok(result) + } + + pub fn skip(&mut self, num_bytes: u32) -> CellResult<()> { + self.reader + .skip(num_bytes) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs b/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs new file mode 100644 index 00000000000..688d71f4e18 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/binary_writer.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::error::{CellErrorType, CellResult}; +use bitstream_io::{BigEndian, BitWrite, BitWriter, Numeric}; +use tw_coin_entry::error::prelude::{MapTWError, OrTWError, ResultContext}; +use tw_memory::Data; + +pub struct BinaryWriter { + writer: BitWriter, +} + +impl BinaryWriter { + pub fn with_capacity(capacity: usize) -> BinaryWriter { + BinaryWriter { + writer: BitWriter::new(Vec::with_capacity(capacity)), + } + } + + pub fn write_bit(&mut self, bit: bool) -> CellResult<&mut Self> { + self.writer + .write_bit(bit) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + pub fn write(&mut self, bits: u32, val: V) -> CellResult<&mut Self> + where + V: Numeric, + { + self.writer + .write(bits, val) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + pub fn write_bytes(&mut self, bytes: &[u8]) -> CellResult<&mut Self> { + self.writer + .write_bytes(bytes) + .tw_err(|_| CellErrorType::BagOfCellsSerializationError)?; + Ok(self) + } + + /// TODO the function doesn't count `bit_len / 8` count of bytes. + /// Original code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell.rs#L507-L526 + pub(crate) fn write_bits(&mut self, data: &[u8], bit_len: usize) -> CellResult<()> { + let data_len = data.len(); + let rest_bits = bit_len % 8; + let full_bytes = rest_bits == 0; + + if full_bytes { + self.write_bytes(data)?; + } else { + self.write_bytes(&data[..data_len - 1])?; + let last_byte = data[data_len - 1]; + let l = last_byte | 1 << (8 - rest_bits - 1); + self.write(8, l)?; + } + + Ok(()) + } + + pub fn bytes_if_aligned(&mut self) -> CellResult<&[u8]> { + self.writer + .writer() + .map(|vec| vec.as_slice()) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .context("Stream is not byte-aligned") + } + + /// Pads the stream with 0 bits until it is aligned at a whole byte. + /// Does nothing if the stream is already aligned. + /// Returns the number of trailing zero bits required to align the Cell. + pub fn align(&mut self) -> CellResult { + let mut trailing_zeros = 0; + while !self.writer.byte_aligned() { + self.write_bit(false)?; + trailing_zeros += 1; + } + Ok(trailing_zeros) + } + + pub fn finish(mut self) -> CellResult { + self.bytes_if_aligned().map(|slice| slice.to_vec()) + } +} + +impl Default for BinaryWriter { + fn default() -> Self { + BinaryWriter { + writer: BitWriter::new(Vec::default()), + } + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs b/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs new file mode 100644 index 00000000000..c3f4e3d60e0 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/boc_to_raw_boc.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/raw_boc_from_boc.rs + +use crate::boc::raw::{RawBagOfCells, RawCell}; +use crate::boc::BagOfCells; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellErrorType, CellResult}; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{OrTWError, ResultContext}; +use tw_hash::H256; + +type IndexedCellRef = RefCell; +type CellsByHash = BTreeMap; + +#[derive(Debug, Clone)] +struct IndexedCell { + index: usize, + cell: CellArc, +} + +pub(crate) fn convert_to_raw_boc(boc: &BagOfCells) -> CellResult { + let cells_by_hash = build_and_verify_index(&boc.roots); + + // Sort indexed cells by their index value. + let mut index_slice: Vec<_> = cells_by_hash.values().collect(); + index_slice.sort_unstable_by(|a, b| a.borrow().index.cmp(&b.borrow().index)); + + // Remove gaps in indices. + index_slice + .iter() + .enumerate() + .for_each(|(real_index, indexed_cell)| indexed_cell.borrow_mut().index = real_index); + + let cells_iter = index_slice + .into_iter() + .map(|indexed_cell| indexed_cell.borrow().cell.clone()); + let raw_cells = raw_cells_from_cells(cells_iter, &cells_by_hash)?; + let root_indices = root_indices(&boc.roots, &cells_by_hash)?; + + Ok(RawBagOfCells { + cells: raw_cells, + roots: root_indices, + }) +} + +fn build_and_verify_index(roots: &[CellArc]) -> CellsByHash { + let mut current_cells: Vec<_> = roots.iter().map(Arc::clone).collect(); + let mut new_hash_index = 0; + + // The Bag of Cells serialization process is not deterministic, + // and these uncertainties make it difficult to write test cases. + // Therefore, we use a BTreeMap instead of a HashMap to remove the uncertainty. + let mut cells_by_hash = BTreeMap::new(); + + // Process cells to build the initial index. + while !current_cells.is_empty() { + let mut next_cells = Vec::with_capacity(current_cells.len() * 4); + for cell in current_cells.iter() { + let hash = cell.cell_hash(); + + if cells_by_hash.contains_key(&hash) { + continue; // Skip if already indexed. + } + + cells_by_hash.insert( + hash, + RefCell::new(IndexedCell { + cell: Arc::clone(cell), + index: new_hash_index, + }), + ); + + new_hash_index += 1; + next_cells.extend(cell.references().iter().map(Arc::clone)); // Add referenced cells for the next iteration. + } + + current_cells = next_cells; + } + + // Ensure indices are in the correct order based on cell references. + let mut verify_order = true; + while verify_order { + verify_order = false; + + for index_cell in cells_by_hash.values() { + for reference in index_cell.borrow().cell.references().iter() { + let ref_hash = reference.cell_hash(); + if let Some(id_ref) = cells_by_hash.get(&ref_hash) { + if id_ref.borrow().index < index_cell.borrow().index { + id_ref.borrow_mut().index = new_hash_index; + new_hash_index += 1; + verify_order = true; // Reverify if an index was updated. + } + } + } + } + } + + cells_by_hash +} + +fn root_indices(roots: &[CellArc], cells_dict: &CellsByHash) -> CellResult> { + roots + .iter() + .map(|root_cell| root_cell.cell_hash()) + .map(|root_cell_hash| { + cells_dict + .get(&root_cell_hash) + .map(|index_record| index_record.borrow().index) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .with_context(|| { + format!( + "Couldn't find cell with hash {root_cell_hash} while searching for roots" + ) + }) + }) + .collect() +} + +fn raw_cells_from_cells( + cells: impl Iterator, + cells_by_hash: &CellsByHash, +) -> CellResult> { + cells + .map(|cell| raw_cell_from_cell(&cell, cells_by_hash)) + .collect() +} + +fn raw_cell_from_cell(cell: &Cell, cells_by_hash: &CellsByHash) -> CellResult { + raw_cell_reference_indices(cell, cells_by_hash).map(|reference_indices| { + RawCell::new( + cell.data().to_vec(), + cell.bit_len(), + reference_indices, + cell.get_level_mask(), + cell.is_exotic(), + ) + }) +} + +fn raw_cell_reference_indices(cell: &Cell, cells_by_hash: &CellsByHash) -> CellResult> { + cell.references() + .iter() + .map(|cell| { + cells_by_hash + .get(&cell.cell_hash()) + .or_tw_err(CellErrorType::BagOfCellsSerializationError) + .with_context(|| { + format!( + "Couldn't find cell with hash {:?} while searching for references", + cell.cell_hash() + ) + }) + .map(|cell| cell.borrow().index) + }) + .collect() +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/mod.rs b/rust/frameworks/tw_ton_sdk/src/boc/mod.rs new file mode 100644 index 00000000000..b0d4a9f2be1 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/mod.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/bag_of_cells.rs + +use crate::boc::raw::RawBagOfCells; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use std::sync::Arc; +use tw_coin_entry::error::prelude::*; +use tw_encoding::base64::{self, STANDARD}; +use tw_memory::Data; + +pub mod binary_reader; +pub mod binary_writer; +pub mod boc_to_raw_boc; +pub mod raw; + +use boc_to_raw_boc::convert_to_raw_boc; + +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub struct BagOfCells { + pub roots: Vec, +} + +impl BagOfCells { + pub fn from_root(root: Cell) -> BagOfCells { + BagOfCells { + roots: vec![root.into_arc()], + } + } + + pub fn single_root(&self) -> CellResult<&CellArc> { + let root_count = self.roots.len(); + if root_count == 1 { + Ok(&self.roots[0]) + } else { + CellError::err(CellErrorType::CellParserError) + .context(format!("Single root expected, got {root_count}")) + } + } + + pub fn parse(serial: &[u8]) -> CellResult { + let raw = RawBagOfCells::parse(serial)?; + let num_cells = raw.cells.len(); + let mut cells: Vec = Vec::with_capacity(num_cells); + + for (cell_index, raw_cell) in raw.cells.into_iter().enumerate().rev() { + let mut references = Vec::with_capacity(raw_cell.references.len()); + for ref_index in &raw_cell.references { + if *ref_index <= cell_index { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("References to previous cells are not supported"); + } + let cell_ref_idx = (num_cells - 1) + .checked_sub(*ref_index) + .or_tw_err(CellErrorType::BagOfCellsDeserializationError) + .context("Cell references to an out-of-bound cell")?; + references.push(cells[cell_ref_idx].clone()); + } + + let cell = Cell::new( + raw_cell.data, + raw_cell.bit_len, + references, + raw_cell.is_exotic, + ) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError)?; + cells.push(cell.into_arc()); + } + + if num_cells < raw.roots.len() { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("BagOfCells contains more roots than cells"); + } + let roots = raw + .roots + .into_iter() + .map(|r| { + let cell_idx = (num_cells - 1) + .checked_sub(r) + .or_tw_err(CellErrorType::BagOfCellsDeserializationError) + .context("Root index doesn't correspond to a Cell")?; + Ok(Arc::clone(&cells[cell_idx])) + }) + .collect::>>()?; + + Ok(BagOfCells { roots }) + } + + pub fn parse_base64(base64: &str) -> CellResult { + let bin = base64::decode(base64, STANDARD) + .tw_err(|_| CellErrorType::BagOfCellsDeserializationError) + .context("Expected base64 encoded BagOfCells")?; + Self::parse(&bin) + } + + pub fn serialize(&self, has_crc32: bool) -> CellResult { + let raw = convert_to_raw_boc(self)?; + raw.serialize(has_crc32) + } + + pub fn to_base64(&self, has_crc32: bool) -> CellResult { + let encoded = self.serialize(has_crc32)?; + Ok(base64::encode(&encoded, STANDARD)) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/boc/raw.rs b/rust/frameworks/tw_ton_sdk/src/boc/raw.rs new file mode 100644 index 00000000000..3b02a02d415 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/boc/raw.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/raw.rs + +use crate::boc::binary_reader::BinaryReader; +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::level_mask::LevelMask; +use crate::crc::CRC_32_ISCSI; +use crate::error::{CellError, CellErrorType, CellResult}; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +const GENERIC_BOC_MAGIC: u32 = 0xb5ee9c72; +/// The max number of cells in a BoC. +const MAX_CELLS: usize = 4096; + +/// Raw representation of Cell. +/// +/// References are stored as indices in BagOfCells. +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub(crate) struct RawCell { + pub(crate) data: Data, + pub(crate) bit_len: usize, + pub(crate) references: Vec, + pub(crate) is_exotic: bool, + level_mask: u32, +} + +impl RawCell { + pub(crate) fn new( + data: Vec, + bit_len: usize, + references: Vec, + level_mask: u32, + is_exotic: bool, + ) -> Self { + Self { + data, + bit_len, + references, + level_mask: level_mask & 7, + is_exotic, + } + } +} + +/// Raw representation of BagOfCells. +/// +/// `cells` must be topologically sorted. +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub(crate) struct RawBagOfCells { + pub(crate) cells: Vec, + pub(crate) roots: Vec, +} + +impl RawBagOfCells { + pub(crate) fn parse(serial: &[u8]) -> CellResult { + let mut reader = BinaryReader::new(serial); + + // serialized_boc#b5ee9c72 + let magic = reader.read_u32()?; + + let (has_idx, has_crc32c, _has_cache_bits, size) = match magic { + GENERIC_BOC_MAGIC => { + // has_idx:(## 1) has_crc32c:(## 1) has_cache_bits:(## 1) flags:(## 2) { flags = 0 } + let header = reader.read_u8()?; + let has_idx = (header >> 7) & 1 == 1; + let has_crc32c = (header >> 6) & 1 == 1; + let has_cache_bits = (header >> 5) & 1 == 1; + // size:(## 3) { size <= 4 } + let size = header & 0b0000_0111; + + (has_idx, has_crc32c, has_cache_bits, size) + }, + magic => { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context(format!("Unsupported cell magic number: {:#}", magic)); + }, + }; + // off_bytes:(## 8) { off_bytes <= 8 } + let off_bytes = reader.read_u8()?; + //cells:(##(size * 8)) + let cells = reader.read_var_size(size as usize)?; + if cells > MAX_CELLS { + return CellError::err(CellErrorType::BagOfCellsDeserializationError).context(format!( + "Max number of cells is '{MAX_CELLS}', but given '{cells}' Cells" + )); + } + + // roots:(##(size * 8)) { roots >= 1 } + let roots = reader.read_var_size(size as usize)?; + if roots > MAX_CELLS { + return CellError::err(CellErrorType::BagOfCellsDeserializationError).context(format!( + "Max number of cells is '{MAX_CELLS}', but given '{roots}' root Cells" + )); + } + + // absent:(##(size * 8)) { roots + absent <= cells } + let _absent = reader.read_var_size(size as usize)?; + // tot_cells_size:(##(off_bytes * 8)) + let _tot_cells_size = reader.read_var_size(off_bytes as usize)?; + // root_list:(roots * ##(size * 8)) + let mut root_list = vec![]; + for _ in 0..roots { + root_list.push(reader.read_var_size(size as usize)?) + } + // index:has_idx?(cells * ##(off_bytes * 8)) + let mut index = vec![]; + if has_idx { + for _ in 0..cells { + index.push(reader.read_var_size(off_bytes as usize)?) + } + } + // cell_data:(tot_cells_size * [ uint8 ]) + let mut cell_vec = Vec::with_capacity(cells); + + for _ in 0..cells { + let cell = read_cell(&mut reader, size)?; + cell_vec.push(cell); + } + // crc32c:has_crc32c?uint32 + let _crc32c = if has_crc32c { reader.read_u32()? } else { 0 }; + // TODO: Check crc32 + + Ok(RawBagOfCells { + cells: cell_vec, + roots: root_list, + }) + } + + pub(crate) fn serialize(&self, has_crc32: bool) -> CellResult { + // Based on https://github.com/toncenter/tonweb/blob/c2d5d0fc23d2aec55a0412940ce6e580344a288c/src/boc/Cell.js#L198 + + let root_count = self.roots.len(); + let num_ref_bits = 32 - (self.cells.len() as u32).leading_zeros(); + let num_ref_bytes = (num_ref_bits + 7) / 8; + let has_idx = false; + + let mut full_size = 0u32; + + for cell in &self.cells { + full_size += raw_cell_size(cell, num_ref_bytes); + } + + let num_offset_bits = 32 - full_size.leading_zeros(); + let num_offset_bytes = (num_offset_bits + 7) / 8; + + let total_size = 4 + // magic + 1 + // flags and s_bytes + 1 + // offset_bytes + 3 * num_ref_bytes + // cells_num, roots, complete + num_offset_bytes + // full_size + num_ref_bytes + // root_idx + (if has_idx { self.cells.len() as u32 * num_offset_bytes } else { 0 }) + + full_size + + (if has_crc32 { 4 } else { 0 }); + + let mut writer = BinaryWriter::with_capacity(total_size as usize); + + writer.write(32, GENERIC_BOC_MAGIC)?; + + //write flags byte + let has_cache_bits = false; + let flags: u8 = 0; + writer.write_bit(has_idx)?; + writer.write_bit(has_crc32)?; + writer.write_bit(has_cache_bits)?; + writer.write(2, flags)?; + writer.write(3, num_ref_bytes)?; + writer.write(8, num_offset_bytes)?; + writer.write(8 * num_ref_bytes, self.cells.len() as u32)?; + writer.write(8 * num_ref_bytes, root_count as u32)?; + writer.write(8 * num_ref_bytes, 0)?; // Complete BOCs only + writer.write(8 * num_offset_bytes, full_size)?; + for &root in &self.roots { + writer.write(8 * num_ref_bytes, root as u32)?; + } + + for cell in &self.cells { + write_raw_cell(&mut writer, cell, num_ref_bytes)?; + } + + if has_crc32 { + let bytes = writer.bytes_if_aligned()?; + let cs = CRC_32_ISCSI.checksum(bytes); + writer.write_bytes(cs.to_le_bytes().as_slice())?; + } + writer.align()?; + writer.finish() + } +} + +fn read_cell(reader: &mut BinaryReader, size: u8) -> CellResult { + let d1 = reader.read_u8()?; + let d2 = reader.read_u8()?; + + let ref_num = d1 & 0b111; + let is_exotic = (d1 & 0b1000) != 0; + let has_hashes = (d1 & 0b10000) != 0; + let level_mask = (d1 >> 5) as u32; + let data_size = ((d2 >> 1) + (d2 & 1)).into(); + let full_bytes = (d2 & 0x01) == 0; + + if has_hashes { + let hash_count = LevelMask::new(level_mask).hash_count(); + let skip_size = hash_count * (32 + 2); + + // TODO: check depth and hashes + reader.skip(skip_size as u32)?; + } + + let mut data = reader.read_to_vec(data_size)?; + + let data_len = data.len(); + let padding_len = if data_len > 0 && !full_bytes { + // Fix last byte, + // see https://github.com/toncenter/tonweb/blob/c2d5d0fc23d2aec55a0412940ce6e580344a288c/src/boc/BitString.js#L302 + let num_zeros = data[data_len - 1].trailing_zeros(); + if num_zeros >= 8 { + return CellError::err(CellErrorType::BagOfCellsDeserializationError) + .context("Last byte of binary must not be zero if full_byte flag is not set"); + } + data[data_len - 1] &= !(1 << num_zeros); + num_zeros + 1 + } else { + 0 + }; + let bit_len = data.len() * 8 - padding_len as usize; + let mut references: Vec = Vec::new(); + for _ in 0..ref_num { + references.push(reader.read_var_size(size as usize)?); + } + let cell = RawCell::new(data, bit_len, references, level_mask, is_exotic); + Ok(cell) +} + +fn raw_cell_size(cell: &RawCell, ref_size_bytes: u32) -> u32 { + let data_len = (cell.bit_len + 7) / 8; + 2 + data_len as u32 + cell.references.len() as u32 * ref_size_bytes +} + +fn write_raw_cell( + writer: &mut BinaryWriter, + cell: &RawCell, + ref_size_bytes: u32, +) -> CellResult<()> { + let level = cell.level_mask; + let is_exotic = cell.is_exotic as u32; + let num_refs = cell.references.len() as u32; + let d1 = num_refs + is_exotic * 8 + level * 32; + + let padding_bits = cell.bit_len % 8; + let full_bytes = padding_bits == 0; + let data = cell.data.as_slice(); + let data_len_bytes = (cell.bit_len + 7) / 8; + // data_len_bytes <= 128 by spec, but d2 must be u8 by spec as well + let d2 = (data_len_bytes * 2 - if full_bytes { 0 } else { 1 }) as u8; //subtract 1 if the last byte is not full + + writer.write(8, d1)?; + writer.write(8, d2)?; + if !full_bytes { + writer.write_bytes(&data[..data_len_bytes - 1])?; + let last_byte = data[data_len_bytes - 1]; + let l = last_byte | 1 << (8 - padding_bits - 1); + writer.write(8, l)?; + } else { + writer.write_bytes(data)?; + } + + for r in cell.references.as_slice() { + writer.write(8 * ref_size_bytes, *r as u32)?; + } + + Ok(()) +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs new file mode 100644 index 00000000000..99d394ca169 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_builder.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/builder.rs + +use crate::address::address_data::AddressData; +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::cell_parser::CellParser; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitstream_io::Numeric; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{MapTWError, ResultContext}; +use tw_number::U256; + +const MAX_CELL_BITS: usize = 1023; +const MAX_CELL_REFERENCES: usize = 4; + +#[derive(Default)] +pub struct CellBuilder { + bit_writer: BinaryWriter, + references: Vec, + is_cell_exotic: bool, +} + +impl CellBuilder { + pub fn new() -> CellBuilder { + CellBuilder::default() + } + + pub fn store_bit(&mut self, val: bool) -> CellResult<&mut Self> { + self.bit_writer.write_bit(val)?; + Ok(self) + } + + pub fn store_byte(&mut self, val: u8) -> CellResult<&mut Self> { + self.store_u8(8, val) + } + + pub fn store_u8(&mut self, bit_len: usize, val: u8) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_u32(&mut self, bit_len: usize, val: u32) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_i32(&mut self, bit_len: usize, val: i32) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_u64(&mut self, bit_len: usize, val: u64) -> CellResult<&mut Self> { + self.store_numeric(bit_len, val) + } + + pub fn store_uint(&mut self, bit_len: usize, val: &U256) -> CellResult<&mut Self> { + if val.bits() > bit_len { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Value {val} doesn't fit in {bit_len} bits (takes {} bits)", + val.bits() + )); + } + // example: bit_len=13, val=5. 5 = 00000101, we must store 0000000000101 + // leading_zeros_bits = 10 + // leading_zeros_bytes = 10 / 8 = 1 + let leading_zero_bits = bit_len - val.bits(); + let leading_zeros_bytes = leading_zero_bits / 8; + for _ in 0..leading_zeros_bytes { + self.store_byte(0)?; + } + // we must align high byte of val to specified bit_len, 00101 in our case + let extra_zeros = leading_zero_bits % 8; + for _ in 0..extra_zeros { + self.store_bit(false)?; + } + // and then store val's high byte in minimum number of bits + let val_bytes = val.to_big_endian_compact(); + let high_bits_cnt = { + let cnt = val.bits() % 8; + if cnt == 0 { + 8 + } else { + cnt + } + }; + let high_byte = val_bytes[0]; + for i in 0..high_bits_cnt { + self.store_bit(high_byte & (1 << (high_bits_cnt - i - 1)) != 0)?; + } + // store the rest of val + for byte in val_bytes.iter().skip(1) { + self.store_byte(*byte)?; + } + Ok(self) + } + + pub fn store_slice(&mut self, slice: &[u8]) -> CellResult<&mut Self> { + for val in slice { + self.store_byte(*val)?; + } + Ok(self) + } + + pub fn store_string(&mut self, val: &str) -> CellResult<&mut Self> { + self.store_slice(val.as_bytes()) + } + + pub fn store_coins(&mut self, val: &U256) -> CellResult<&mut Self> { + if val.is_zero() { + self.store_u8(4, 0) + } else { + let num_bytes = (val.bits() + 7) / 8; + self.store_u8(4, num_bytes as u8)?; + self.store_uint(num_bytes * 8, val) + } + } + + /// Stores address without optimizing hole address. + pub fn store_raw_address(&mut self, val: A) -> CellResult<&mut Self> + where + A: AsRef, + { + let val = val.as_ref(); + + self.store_u8(2, 0b10_u8)?; + self.store_bit(false)?; + self.store_u8(8, val.workchain_byte())?; + self.store_slice(val.hash_part.as_slice())?; + Ok(self) + } + + /// Stores address optimizing hole address two to bits + pub fn store_address(&mut self, val: A) -> CellResult<&mut Self> + where + A: AsRef, + { + if val.as_ref() == &AddressData::NULL { + self.store_u8(2, 0)?; + } else { + self.store_raw_address(val)?; + } + Ok(self) + } + + /// Adds reference to an existing `Cell`. + /// + /// The reference is passed as `ArcCell` so it might be references from other cells. + pub fn store_reference(&mut self, cell: &CellArc) -> CellResult<&mut Self> { + let ref_count = self.references.len() + 1; + if ref_count > MAX_CELL_REFERENCES { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most 4 references, got {ref_count}" + )); + } + self.references.push(Arc::clone(cell)); + Ok(self) + } + + pub fn store_references(&mut self, refs: &[CellArc]) -> CellResult<&mut Self> { + for r in refs { + self.store_reference(r)?; + } + Ok(self) + } + + /// Adds a reference to a newly constructed `Cell`. + /// + /// The cell is wrapped it the `Arc`. + pub fn store_child(&mut self, cell: Cell) -> CellResult<&mut Self> { + self.store_reference(&cell.into_arc()) + } + + pub fn store_remaining_bits(&mut self, parser: &mut CellParser) -> CellResult<&mut Self> { + let num_full_bytes = parser.remaining_bits() / 8; + let bytes = parser.load_bytes(num_full_bytes)?; + self.store_slice(bytes.as_slice())?; + let num_bits = parser.remaining_bits() % 8; + let tail = parser.load_u8(num_bits)?; + self.store_u8(num_bits, tail)?; + Ok(self) + } + + pub fn store_cell_data(&mut self, cell: &Cell) -> CellResult<&mut Self> { + let mut parser = cell.parser(); + self.store_remaining_bits(&mut parser)?; + Ok(self) + } + + pub fn store_cell(&mut self, cell: &Cell) -> CellResult<&mut Self> { + self.store_cell_data(cell)?; + self.store_references(cell.references.as_slice())?; + Ok(self) + } + + pub fn build(mut self) -> CellResult { + let trailing_zeros = self.bit_writer.align()?; + + let vec = self + .bit_writer + .finish() + .tw_err(|_| CellErrorType::InternalError) + .context("Stream must be byte-aligned already")?; + + let bit_len = vec.len() * 8 - trailing_zeros; + if bit_len > MAX_CELL_BITS { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most {MAX_CELL_BITS} bits, got {bit_len}", + )); + } + let ref_count = self.references.len(); + if ref_count > MAX_CELL_REFERENCES { + return CellError::err(CellErrorType::CellBuilderError).context(format!( + "Cell must contain at most 4 references, got {ref_count}", + )); + } + + Cell::new(vec, bit_len, self.references.clone(), self.is_cell_exotic) + } + + fn store_numeric(&mut self, bit_len: usize, val: V) -> CellResult<&mut Self> { + self.bit_writer.write(bit_len as u32, val)?; + Ok(self) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs new file mode 100644 index 00000000000..e7143ded0a3 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_parser.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/parser.rs + +use crate::address::address_data::AddressData; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitreader::BitReader; +use num_bigint::BigUint; +use tw_coin_entry::error::prelude::{MapTWError, ResultContext}; +use tw_hash::H256; +use tw_memory::Data; +use tw_number::U256; + +pub struct CellParser<'a> { + bit_reader: BitReader<'a>, +} + +impl<'a> CellParser<'a> { + pub fn new(data: &'a [u8], bit_len: usize) -> Self { + CellParser { + bit_reader: BitReader::new(data).relative_reader_atmost(bit_len as u64), + } + } + + pub fn remaining_bits(&self) -> usize { + self.bit_reader.remaining() as usize + } + + pub fn load_bit(&mut self) -> CellResult { + self.bit_reader + .read_bool() + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u8(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u8(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u32(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u32(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_u64(&mut self, bit_len: usize) -> CellResult { + self.bit_reader + .read_u64(bit_len as u8) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_uint(&mut self, bit_len: usize) -> CellResult { + let num_words = (bit_len + 31) / 32; + let high_word_bits = if bit_len % 32 == 0 { 32 } else { bit_len % 32 }; + let mut words: Vec = vec![0; num_words]; + let high_word = self.load_u32(high_word_bits)?; + words[num_words - 1] = high_word; + for i in (0..num_words - 1).rev() { + let word = self.load_u32(32)?; + words[i] = word; + } + let big_uint = BigUint::new(words); + let uint = U256::from_big_endian_slice(&big_uint.to_bytes_be()) + .tw_err(|_| CellErrorType::CellParserError) + .context("Expected up to 32 bytes of uint")?; + Ok(uint) + } + + pub fn load_slice(&mut self, slice: &mut [u8]) -> CellResult<()> { + self.bit_reader + .read_u8_slice(slice) + .tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_bytes(&mut self, num_bytes: usize) -> CellResult { + let mut res = vec![0; num_bytes]; + self.load_slice(res.as_mut_slice())?; + Ok(res) + } + + pub fn load_string(&mut self, num_bytes: usize) -> CellResult { + let bytes = self.load_bytes(num_bytes)?; + String::from_utf8(bytes).tw_err(|_| CellErrorType::CellParserError) + } + + pub fn load_coins(&mut self) -> CellResult { + let num_bytes = self.load_u8(4)?; + if num_bytes == 0 { + Ok(U256::zero()) + } else { + self.load_uint((num_bytes * 8) as usize) + } + } + + pub fn load_address(&mut self) -> CellResult { + let tp = self.load_u8(2)?; + match tp { + 0 => Ok(AddressData::null()), + 2 => { + let _res1 = self.load_u8(1)?; + let wc = self.load_u8(8)?; + let mut hash_part = H256::default(); + self.load_slice(hash_part.as_mut_slice())?; + Ok(AddressData::new(wc as i32, hash_part)) + }, + _ => CellError::err(CellErrorType::CellParserError) + .context(format!("Invalid address type: {tp}")), + } + } + + pub fn ensure_empty(&self) -> CellResult<()> { + let remaining = self.remaining_bits(); + if remaining == 0 { + Ok(()) + } else { + CellError::err(CellErrorType::CellParserError) + .context(format!("{remaining} unread bits left")) + } + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs b/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs new file mode 100644 index 00000000000..3effee51a1d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/cell_type.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/cell_type.rs + +use crate::cell::level_mask::LevelMask; +use crate::cell::{Cell, CellArc}; +use crate::error::{CellError, CellErrorType, CellResult}; +use bitstream_io::{BigEndian, ByteRead, ByteReader}; +use std::io::Cursor; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; + +struct Pruned { + hash: H256, + depth: u16, +} + +pub(crate) struct HashesAndDepths { + pub hashes: [H256; 4], + pub depths: [u16; 4], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum CellType { + Ordinary, + PrunedBranch, + Library, + MerkleProof, + MerkleUpdate, +} + +impl CellType { + pub(crate) fn determine_exotic_cell_type(data: &[u8]) -> CellResult { + let Some(type_byte) = data.first() else { + return CellError::err(CellErrorType::InvalidExoticCell) + .context("Not enough data to determine exotic cell type"); + }; + + let cell_type = match type_byte { + 1 => CellType::PrunedBranch, + 2 => CellType::Library, + 3 => CellType::MerkleProof, + 4 => CellType::MerkleUpdate, + cell_type => { + return CellError::err(CellErrorType::InvalidExoticCell).context(format!( + "Invalid first byte in exotic cell data: {cell_type}" + )); + }, + }; + Ok(cell_type) + } + + pub(crate) fn validate( + &self, + _data: &[u8], + _bit_len: usize, + _references: impl AsRef<[CellArc]>, + ) -> CellResult<()> { + // TODO consider implementing data validation according to the cell type. + // match self { + // CellType::Ordinary => Ok(()), + // CellType::PrunedBranch => self.validate_exotic_pruned(data, bit_len, references), + // CellType::Library => self.validate_library(bit_len), + // CellType::MerkleProof => self.validate_merkle_proof(data, bit_len, references), + // CellType::MerkleUpdate => self.validate_merkle_update(data, bit_len, references), + // } + Ok(()) + } + + pub(crate) fn level_mask( + &self, + cell_data: &[u8], + cell_data_bit_len: usize, + references: &[CellArc], + ) -> CellResult { + let ensure_ref_at_least = |at_least_count: usize| { + if references.len() < at_least_count { + return CellError::err(CellErrorType::CellParserError) + .context("Invalid number of Cell references to get level_mask"); + } + Ok(()) + }; + + let result = match self { + CellType::Ordinary => references + .iter() + .fold(LevelMask::new(0), |level_mask, reference| { + level_mask.apply_or(reference.level_mask) + }), + CellType::PrunedBranch => self.pruned_level_mask(cell_data, cell_data_bit_len)?, + CellType::Library => LevelMask::new(0), + CellType::MerkleProof => { + ensure_ref_at_least(1)?; + references[0].level_mask.shift_right() + }, + CellType::MerkleUpdate => { + ensure_ref_at_least(2)?; + references[0] + .level_mask + .apply_or(references[1].level_mask) + .shift_right() + }, + }; + + Ok(result) + } + + pub(crate) fn child_depth(&self, child: &Cell, level: u8) -> u16 { + if matches!(self, CellType::MerkleProof | CellType::MerkleUpdate) { + child.get_depth(level + 1) + } else { + child.get_depth(level) + } + } + + pub(crate) fn resolve_hashes_and_depths( + &self, + hashes: Vec, + depths: Vec, + data: &[u8], + bit_len: usize, + level_mask: LevelMask, + ) -> CellResult { + let mut resolved_hashes = [H256::default(); 4]; + let mut resolved_depths = [0; 4]; + + for i in 0..4 { + let hash_index = level_mask.apply(i).hash_index(); + + let (hash, depth) = if self == &CellType::PrunedBranch { + let this_hash_index = level_mask.hash_index(); + if hash_index != this_hash_index { + let pruned = self.pruned(data, bit_len, level_mask)?; + (pruned[hash_index].hash, pruned[hash_index].depth) + } else { + (hashes[0], depths[0]) + } + } else { + (hashes[hash_index], depths[hash_index]) + }; + + resolved_hashes[i as usize] = hash; + resolved_depths[i as usize] = depth; + } + + Ok(HashesAndDepths { + hashes: resolved_hashes, + depths: resolved_depths, + }) + } + + fn pruned_level_mask(&self, data: &[u8], bit_len: usize) -> CellResult { + if data.len() < 5 { + return CellError::err(CellErrorType::InvalidExoticCell).context(format!( + "Pruned Branch cell date can't be shorter than 5 bytes, got {}", + data.len() + )); + } + + let level_mask = if self.is_config_proof(bit_len) { + LevelMask::new(1) + } else { + let mask_byte = data[1]; + LevelMask::new(mask_byte as u32) + }; + + Ok(level_mask) + } + + fn pruned( + &self, + data: &[u8], + bit_len: usize, + level_mask: LevelMask, + ) -> CellResult> { + type RawCellHash = [u8; H256::LEN]; + + let current_index = if self.is_config_proof(bit_len) { 1 } else { 2 }; + + let cursor = Cursor::new(&data[current_index..]); + let mut reader = ByteReader::endian(cursor, BigEndian); + + let level = level_mask.level() as usize; + let hashes = (0..level) + .map(|_| reader.read::().map(H256::from)) + .collect::, _>>() + .tw_err(|_| CellErrorType::CellBuilderError)?; + let depths = (0..level) + .map(|_| reader.read::()) + .collect::, _>>() + .tw_err(|_| CellErrorType::CellBuilderError)?; + + let result = hashes + .into_iter() + .zip(depths) + .map(|(hash, depth)| Pruned { hash, depth }) + .collect(); + + Ok(result) + } + + /// Special case for config proof + /// This test proof is generated in the moment of voting for a slashing + /// it seems that tools generate it incorrectly and therefore doesn't have mask in it + /// so we need to hardcode it equal to 1 in this case + fn is_config_proof(&self, bit_len: usize) -> bool { + self == &CellType::PrunedBranch && bit_len == 280 + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs b/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs new file mode 100644 index 00000000000..18075a49368 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/level_mask.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell/level_mask.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LevelMask { + mask: u32, +} + +impl LevelMask { + pub fn new(new_mask: u32) -> Self { + Self { mask: new_mask } + } + + pub fn mask(&self) -> u32 { + self.mask + } + + pub fn level(&self) -> u8 { + 32 - self.mask.leading_zeros() as u8 + } + + pub fn hash_index(&self) -> usize { + self.mask.count_ones() as usize + } + + pub fn hash_count(&self) -> usize { + self.hash_index() + 1 + } + + pub fn apply(&self, level: u8) -> Self { + LevelMask { + mask: self.mask & ((1u32 << level) - 1), + } + } + + pub fn apply_or(&self, other: Self) -> Self { + LevelMask { + mask: self.mask | other.mask, + } + } + + pub fn shift_right(&self) -> Self { + LevelMask { + mask: self.mask >> 1, + } + } + + pub fn is_significant(&self, level: u8) -> bool { + level == 0 || ((self.mask >> (level - 1)) % 2 != 0) + } +} diff --git a/rust/frameworks/tw_ton_sdk/src/cell/mod.rs b/rust/frameworks/tw_ton_sdk/src/cell/mod.rs new file mode 100644 index 00000000000..c6edaf7adfb --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/cell/mod.rs @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Original source code: https://github.com/ston-fi/tonlib-rs/blob/b96a5252df583261ed755656292930af46c2039a/src/cell.rs + +use crate::boc::binary_writer::BinaryWriter; +use crate::cell::cell_parser::CellParser; +use std::fmt; +use std::sync::Arc; +use tw_coin_entry::error::prelude::{MapTWError, OrTWError, ResultContext}; +use tw_encoding::base64::{self, URL_NO_PAD}; +use tw_encoding::hex::ToHex; +use tw_hash::sha2::sha256; +use tw_hash::H256; +use tw_memory::Data; + +pub mod cell_builder; +pub mod cell_parser; +pub mod cell_type; +pub mod level_mask; + +use crate::cell::cell_type::{CellType, HashesAndDepths}; +use crate::cell::level_mask::LevelMask; +use crate::error::{CellError, CellErrorType, CellResult}; + +const MAX_LEVEL: u8 = 3; + +pub type CellArc = Arc; + +#[derive(PartialEq, Eq, Clone, Hash)] +pub struct Cell { + data: Data, + bit_len: usize, + references: Vec, + cell_type: CellType, + level_mask: LevelMask, + hashes: [H256; 4], + depths: [u16; 4], +} + +impl Cell { + pub fn new( + data: Data, + bit_len: usize, + references: Vec, + is_exotic: bool, + ) -> CellResult { + let cell_type = if is_exotic { + CellType::determine_exotic_cell_type(&data)? + } else { + CellType::Ordinary + }; + + cell_type.validate(&data, bit_len, &references)?; + let level_mask = cell_type.level_mask(&data, bit_len, &references)?; + let HashesAndDepths { hashes, depths } = + calculate_hashes_and_depths(cell_type, &data, bit_len, &references, level_mask)?; + + let result = Self { + data, + bit_len, + references, + level_mask, + cell_type, + hashes, + depths, + }; + + Ok(result) + } + + pub fn into_arc(self) -> CellArc { + Arc::new(self) + } + + pub fn data(&self) -> &[u8] { + self.data.as_slice() + } + + pub fn bit_len(&self) -> usize { + self.bit_len + } + + pub fn references(&self) -> &[CellArc] { + &self.references + } + + pub(crate) fn get_level_mask(&self) -> u32 { + self.level_mask.mask() + } + + pub fn is_exotic(&self) -> bool { + self.cell_type != CellType::Ordinary + } + + pub fn cell_hash(&self) -> H256 { + self.get_hash(MAX_LEVEL) + } + + pub fn cell_hash_base64(&self) -> String { + base64::encode(self.cell_hash().as_slice(), URL_NO_PAD) + } + + pub fn get_hash(&self, level: u8) -> H256 { + self.hashes[level.min(3) as usize] + } + + pub fn get_depth(&self, level: u8) -> u16 { + self.depths[level.min(3) as usize] + } + + pub fn parser(&self) -> CellParser { + CellParser::new(&self.data, self.bit_len) + } + + pub fn parse_fully(&self, parse: F) -> Result + where + F: FnOnce(&mut CellParser) -> CellResult, + { + let mut reader = self.parser(); + let res = parse(&mut reader); + reader.ensure_empty()?; + res + } + + fn fmt_debug(&self, f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result { + for _ in 0..depth { + write!(f, "\t")?; + } + // Append Cell IDX. + if depth == 0 { + write!(f, "Cell(root)")?; + } else { + write!(f, "-> Cell(ref {depth})")?; + } + + let maybe_exotic_type = if matches!(self.cell_type, CellType::Ordinary) { + "".to_string() + } else { + format!("exotic={:?} ", self.cell_type) + }; + + write!( + f, + " {{ data={}, bit_len={} {maybe_exotic_type}}}", + self.data.to_hex(), + self.bit_len, + )?; + + writeln!(f)?; + let ref_depth = depth + 1; + for reference in self.references() { + reference.fmt_debug(f, ref_depth)?; + } + Ok(()) + } +} + +impl fmt::Debug for Cell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let root_depth = 0; + self.fmt_debug(f, root_depth) + } +} + +fn get_repr_for_data( + (original_data, original_data_bit_len): (&[u8], usize), + (data, data_bit_len): (&[u8], usize), + refs: &[CellArc], + level_mask: LevelMask, + level: u8, + cell_type: CellType, +) -> CellResult { + // Allocate + let data_len = data.len(); + // descriptors + data + (hash + depth) * refs_count + let buffer_len = 2 + data_len + (32 + 2) * refs.len(); + + let mut writer = BinaryWriter::with_capacity(buffer_len); + let d1 = get_refs_descriptor(cell_type, refs, level_mask.apply(level).mask())?; + let d2 = get_bits_descriptor(original_data, original_data_bit_len)?; + + // Write descriptors + writer.write(8, d1)?; + writer.write(8, d2)?; + // Write main data + writer.write_bits(data, data_bit_len)?; + // Write ref data + write_ref_depths(&mut writer, refs, cell_type, level)?; + write_ref_hashes(&mut writer, refs, cell_type, level)?; + + writer.bytes_if_aligned().map(|b| b.to_vec()) +} + +/// This function replicates unknown logic of resolving cell data +/// https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/crypto/vm/cells/DataCell.cpp#L214 +fn calculate_hashes_and_depths( + cell_type: CellType, + data: &[u8], + bit_len: usize, + references: &[CellArc], + level_mask: LevelMask, +) -> CellResult { + let hash_count = if cell_type == CellType::PrunedBranch { + 1 + } else { + level_mask.hash_count() + }; + + let total_hash_count = level_mask.hash_count(); + let hash_i_offset = total_hash_count - hash_count; + + let mut depths: Vec = Vec::with_capacity(hash_count); + let mut hashes: Vec = Vec::with_capacity(hash_count); + + // Iterate through significant levels + for (hash_i, level_i) in (0..=level_mask.level()) + .filter(|&i| level_mask.is_significant(i)) + .enumerate() + { + if hash_i < hash_i_offset { + continue; + } + + let (current_data, current_bit_len) = if hash_i == hash_i_offset { + (data, bit_len) + } else { + let previous_hash = hashes + .get(hash_i - hash_i_offset - 1) + .or_tw_err(CellErrorType::InternalError) + .context("Can't get right hash")?; + (previous_hash.as_slice(), 256) + }; + + // Calculate Depth + let depth = if references.is_empty() { + 0 + } else { + let max_ref_depth = references.iter().fold(0, |max_depth, reference| { + let child_depth = cell_type.child_depth(reference, level_i); + max_depth.max(child_depth) + }); + + max_ref_depth + .checked_add(1) + .or_tw_err(CellErrorType::CellParserError) + .with_context(|| format!("max_ref_depth is too large: {max_ref_depth}"))? + }; + + // Calculate Hash + let repr = get_repr_for_data( + (data, bit_len), + (current_data, current_bit_len), + references, + level_mask, + level_i, + cell_type, + )?; + let hash = sha256(&repr); + let hash = H256::try_from(hash.as_slice()).expect("Expected 32 bytes hash"); + + depths.push(depth); + hashes.push(hash); + } + + cell_type.resolve_hashes_and_depths(hashes, depths, data, bit_len, level_mask) +} + +/// `references.len() as u8 + 8 * cell_type_var + level_mask as u8 * 32` +fn get_refs_descriptor( + cell_type: CellType, + references: &[CellArc], + level_mask: u32, +) -> CellResult { + let cell_type_var = (cell_type != CellType::Ordinary) as u8; + let references_len: u8 = references + .len() + .try_into() + .tw_err(|_| CellErrorType::CellParserError) + .with_context(|| format!("Got too much Cell references: {}", references.len()))?; + let level_mask_u8: u8 = level_mask + .try_into() + .tw_err(|_| CellErrorType::CellParserError) + .with_context(|| format!("Cell level_mask is too large: {level_mask}"))?; + + level_mask_u8 + .checked_mul(32) + .and_then(|v| v.checked_add(8 * cell_type_var)) + .and_then(|v| v.checked_add(references_len)) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_refs_descriptor") +} + +fn get_bits_descriptor(data: &[u8], bit_len: usize) -> CellResult { + let rest_bits = bit_len % 8; + let full_bytes = rest_bits == 0; + + let double_len = data + .len() + .try_into() + .ok() + .and_then(|len: u8| len.checked_mul(2)) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_bits_descriptor()")?; + + let inverted_full_bytes = !full_bytes as u8; + + // subtract 1 if the last byte is not full + double_len + .checked_sub(inverted_full_bytes) + .or_tw_err(CellErrorType::CellParserError) + .context("!get_bits_descriptor()") +} + +fn write_ref_depths( + writer: &mut BinaryWriter, + refs: &[CellArc], + parent_cell_type: CellType, + level: u8, +) -> CellResult<()> { + for reference in refs { + let child_depth = if matches!( + parent_cell_type, + CellType::MerkleProof | CellType::MerkleUpdate + ) { + reference.get_depth(level + 1) + } else { + reference.get_depth(level) + }; + + writer.write(8, child_depth / 256)?; + writer.write(8, child_depth % 256)?; + } + + Ok(()) +} + +fn write_ref_hashes( + writer: &mut BinaryWriter, + refs: &[CellArc], + parent_cell_type: CellType, + level: u8, +) -> CellResult<()> { + for reference in refs { + let child_hash = if matches!( + parent_cell_type, + CellType::MerkleProof | CellType::MerkleUpdate + ) { + reference.get_hash(level + 1) + } else { + reference.get_hash(level) + }; + + writer.write_bytes(child_hash.as_slice())?; + } + + Ok(()) +} diff --git a/rust/frameworks/tw_ton_sdk/src/crc.rs b/rust/frameworks/tw_ton_sdk/src/crc.rs new file mode 100644 index 00000000000..fcd860b6b0e --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/crc.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crc::Crc; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref CRC_32_ISCSI: Crc = Crc::::new(&crc::CRC_32_ISCSI); + pub static ref CRC_16_XMODEM: Crc = Crc::::new(&crc::CRC_16_XMODEM); +} diff --git a/rust/frameworks/tw_ton_sdk/src/error.rs b/rust/frameworks/tw_ton_sdk/src/error.rs new file mode 100644 index 00000000000..0994ba2a614 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/error.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::*; + +pub type CellResult = Result; +pub type CellError = TWError; + +#[derive(Debug)] +pub enum CellErrorType { + BagOfCellsDeserializationError, + BagOfCellsSerializationError, + CellBuilderError, + CellParserError, + InvalidAddressType, + InvalidExoticCell, + NonEmptyReader, + InternalError, +} + +pub fn cell_to_signing_error(cell_err: CellError) -> SigningError { + cell_err.map_err(|cell_ty| match cell_ty { + CellErrorType::BagOfCellsDeserializationError + | CellErrorType::CellParserError + | CellErrorType::InvalidExoticCell + | CellErrorType::NonEmptyReader => SigningErrorType::Error_input_parse, + CellErrorType::BagOfCellsSerializationError | CellErrorType::CellBuilderError => { + SigningErrorType::Error_internal + }, + CellErrorType::InvalidAddressType => SigningErrorType::Error_invalid_address, + CellErrorType::InternalError => SigningErrorType::Error_internal, + }) +} diff --git a/rust/frameworks/tw_ton_sdk/src/lib.rs b/rust/frameworks/tw_ton_sdk/src/lib.rs new file mode 100644 index 00000000000..7224847dd6b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/lib.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod boc; +pub mod cell; +pub mod crc; +pub mod error; +pub mod message; diff --git a/rust/frameworks/tw_ton_sdk/src/message/mod.rs b/rust/frameworks/tw_ton_sdk/src/message/mod.rs new file mode 100644 index 00000000000..2558269f38c --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/message/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod state_init; diff --git a/rust/frameworks/tw_ton_sdk/src/message/state_init.rs b/rust/frameworks/tw_ton_sdk/src/message/state_init.rs new file mode 100644 index 00000000000..a0d8a9d1630 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/src/message/state_init.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::cell::cell_builder::CellBuilder; +use crate::cell::{Cell, CellArc}; +use crate::error::CellResult; +use tw_hash::H256; + +#[derive(Default)] +pub struct StateInit { + code: Option, + data: Option, +} + +impl StateInit { + pub fn set_code(mut self, code: CellArc) -> Self { + self.code = Some(code); + self + } + + pub fn set_data(mut self, data: CellArc) -> Self { + self.data = Some(data); + self + } + + pub fn create_account_id(&self) -> CellResult { + Ok(self.to_cell()?.cell_hash()) + } + + pub fn to_cell(&self) -> CellResult { + let split_depth = false; + let tick_tock = false; + let library = false; + + let mut builder = CellBuilder::new(); + builder + .store_bit(split_depth)? + .store_bit(tick_tock)? + .store_bit(self.code.is_some())? + .store_bit(self.data.is_some())? + .store_bit(library)?; + if let Some(ref code) = self.code { + builder.store_reference(code)?; + } + if let Some(ref data) = self.data { + builder.store_reference(data)?; + } + builder.build() + } +} diff --git a/rust/frameworks/tw_ton_sdk/tests/address.rs b/rust/frameworks/tw_ton_sdk/tests/address.rs new file mode 100644 index 00000000000..8e84bf301b8 --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/address.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_hash::H256; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::raw_address::RawAddress; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; + +const WORKCHAIN: i32 = 0; +const ADDRESS_BYTES: &str = "e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76"; + +const RAW_ADDRESS: &str = "0:e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76"; +const BOUNCEABLE_URL_ADDRESS: &str = "EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR"; +const BOUNCEABLE_ADDRESS: &str = "EQDk2VTvn04SUKJrW7rXahzdF8/Qi6utb0wj43InCu9vdjrR"; + +fn addr_data() -> AddressData { + let bytes = H256::from_str(ADDRESS_BYTES).unwrap(); + AddressData::new(WORKCHAIN, bytes) +} + +#[test] +fn test_raw_address_from_to_string() { + let actual = RawAddress::from_str(RAW_ADDRESS).unwrap(); + let expected = RawAddress::from(addr_data()); + assert_eq!(actual, expected); + let actual_encoded = actual.to_string(); + assert_eq!(actual_encoded, RAW_ADDRESS); +} + +#[test] +fn test_user_friendly_address_from_to_url_string() { + let actual = UserFriendlyAddress::from_base64_url(BOUNCEABLE_URL_ADDRESS).unwrap(); + let expected = UserFriendlyAddress::with_flags(addr_data(), true, false); + assert_eq!(actual, expected); + let actual_encoded = actual.to_base64_url(); + assert_eq!(actual_encoded, BOUNCEABLE_URL_ADDRESS); +} + +#[test] +fn test_user_friendly_address_from_to_std_string() { + let actual = UserFriendlyAddress::from_base64_std(BOUNCEABLE_ADDRESS).unwrap(); + let expected = UserFriendlyAddress::with_flags(addr_data(), true, false); + assert_eq!(actual, expected); + let actual_encoded = actual.to_base64_std(); + assert_eq!(actual_encoded, BOUNCEABLE_ADDRESS); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs b/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs new file mode 100644 index 00000000000..6c4a9dcf2ae --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/boc_encode.rs @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::Deserialize; +use serde_json::{json, Value as Json}; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_ton_sdk::boc::BagOfCells; +use tw_ton_sdk::cell::cell_builder::CellBuilder; +use tw_ton_sdk::cell::Cell; + +#[derive(Deserialize)] +struct ExpectedCell { + data: String, + bit_len: usize, + references: Vec, +} + +struct TestCase { + input_encoded: &'static str, + expected: Json, + expected_hash: &'static str, + /// Expected encoded can be different from the original [`TestCase::input_encoded`] + /// since same Cell can be BoC encoded differently. + expected_encoded: &'static str, +} + +/// Takes a JSON object that will be deserialized as `ExpectedCell`. +/// It's done to allow the function caller to use `json!` macro for readability. +#[track_caller] +fn test_boc_encode_decode(input: TestCase) { + let boc_decoded = BagOfCells::parse_base64(input.input_encoded).unwrap(); + let root_cell = boc_decoded + .single_root() + .expect("Expected single root Cell"); + + let expected: ExpectedCell = serde_json::from_value(input.expected) + .expect("Error deserializing `ExpectedCell` from JSON"); + assert_eq_cell(root_cell, expected); + + let actual_hash = root_cell.cell_hash_base64(); + assert_eq!(actual_hash, input.expected_hash); + + // Wrap the Cell to the BoC again. + let boc_encoded = BagOfCells::from_root(root_cell.as_ref().clone()) + .to_base64(true) + .unwrap(); + assert_eq!(boc_encoded, input.expected_encoded); +} + +#[track_caller] +fn assert_eq_cell(cell: &Cell, expected: ExpectedCell) { + assert_eq!(cell.data().to_hex(), expected.data, "Invalid Cell.data"); + assert_eq!(cell.bit_len(), expected.bit_len, "Invalid Cell.bit_len"); + for (cell_ref, expected_ref) in cell.references().iter().zip(expected.references) { + assert_eq_cell(cell_ref, expected_ref); + } +} + +#[test] +fn test_boc_encode_jetton_transfer_tx() { + let expected = json!({ + "data": "8800b4510655c8136d4ff5be8ea40a9e161ab0f88321d4969cd828d453f22c7c4b2a08", + "bit_len": 277, + "references": [{ + "data": "688595a2c8b55e7bde026a06f72d3f98f78d52a52eec663bea675b44069578d78338f0e58793370446f6ea491ce97a180de915eb4a3688ea1b37e4c64bbea30529a9a3176a8e07f6000000010003", + "bit_len": 624, + "references": [{ + "data": "620031341f879da9b83ede2949836e1a9fb5ae1c75431117aeb6531a77cf3aae83f3202faf080000000000000000000000000001", + "bit_len": 416, + "references": [{ + "data": "0f8a7ea5000000000000000041dcd65008000b8196730b5e1d033e99c42857e699fa6c827758d8a9489ac210b3bb131d133900168a20cab9026da9feb7d1d48153c2c3561f10643a92d39b051a8a7e458f89654202000000007465737420636f6d6d656e74", + "bit_len": 808, + "references": [] + }] + }] + }] + }); + + // The same Cell can be BoC encoded differently. + // Try to decode the original encoded transaction: https://testnet.tonviewer.com/transaction/12bfe84f947740aec3faa54f04a50690900e3aae9ac9596cfa6804a61a48f429 + test_boc_encode_decode(TestCase { + input_encoded: "te6ccgICAAQAAQAAARgAAAFFiAC0UQZVyBNtT/W+jqQKnhYasPiDIdSWnNgo1FPyLHxLKgwAAQGcaIWVosi1XnveAmoG9y0/mPeNUqUu7GY76mdbRAaVeNeDOPDlh5M3BEb26kkc6XoYDekV60o2iOobN+TGS76jBSmpoxdqjgf2AAAAAQADAAIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEAAwDKD4p+pQAAAAAAAAAAQdzWUAgAC4GWcwteHQM+mcQoV+aZ+myCd1jYqUiawhCzuxMdEzkAFoogyrkCban+t9HUgVPCw1YfEGQ6ktObBRqKfkWPiWVCAgAAAAB0ZXN0IGNvbW1lbnQ=", + expected: expected.clone(), + expected_hash: "yYwgXI3TfZpqtdthYvW503zvoGfeJKdlFUpet6NZ8i8", + expected_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + }); + + // Try to decode with same encoded data. + test_boc_encode_decode(TestCase { + input_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + expected, + expected_hash: "yYwgXI3TfZpqtdthYvW503zvoGfeJKdlFUpet6NZ8i8", + expected_encoded: "te6cckECBAEAARUAAUWIALRRBlXIE21P9b6OpAqeFhqw+IMh1Jac2CjUU/IsfEsqDAEBnGiFlaLItV573gJqBvctP5j3jVKlLuxmO+pnW0QGlXjXgzjw5YeTNwRG9upJHOl6GA3pFetKNojqGzfkxku+owUpqaMXao4H9gAAAAEAAwIBaGIAMTQfh52puD7eKUmDbhqfta4cdUMRF662Uxp3zzqug/MgL68IAAAAAAAAAAAAAAAAAAEDAMoPin6lAAAAAAAAAABB3NZQCAALgZZzC14dAz6ZxChX5pn6bIJ3WNipSJrCELO7Ex0TOQAWiiDKuQJtqf630dSBU8LDVh8QZDqS05sFGop+RY+JZUICAAAAAHRlc3QgY29tbWVudG/bd5c=", + }); +} + +#[test] +fn test_wallet_code_hashes() { + let wallet_v3r1_code = BagOfCells::parse_base64("te6cckEBAQEAYgAAwP8AIN0gggFMl7qXMO1E0NcLH+Ck8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVD++buA=").unwrap(); + assert_eq!( + wallet_v3r1_code.single_root().unwrap().cell_hash_base64(), + "thBBpYp5gLlG6PueGY48kE0keZ_6NldOpCUcQaVm9YE" + ); + + let wallet_v3r2_code = BagOfCells::parse_base64("te6cckEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVBC9ba0=").unwrap(); + assert_eq!( + wallet_v3r2_code.single_root().unwrap().cell_hash_base64(), + "hNr6RJ-Ypph3ibojI1gHK8D3bcRSQAKl0JGLmnXS1Zk" + ); + + let wallet_v4r2_code = BagOfCells::parse_base64("te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVGliJeU=").unwrap(); + assert_eq!( + wallet_v4r2_code.single_root().unwrap().cell_hash_base64(), + "_rX_aCDi_w2Ug-fg1iyBfYRniftK5YDIeIZtlZ2r1cA" + ); +} + +#[test] +fn test_boc_encode_cell_builder() { + let leaf = { + let mut builder = CellBuilder::new(); + builder.store_byte(10).unwrap(); + builder.build().unwrap() + }; + let inter = { + let mut builder = CellBuilder::new(); + builder.store_byte(20).unwrap().store_child(leaf).unwrap(); + builder.build().unwrap() + }; + let root = { + let mut builder = CellBuilder::new(); + builder.store_byte(30).unwrap().store_child(inter).unwrap(); + builder.build().unwrap() + }; + + let boc = BagOfCells::from_root(root); + assert_eq!( + boc.to_base64(true).unwrap(), + "te6cckEBAwEACwABAh4BAQIUAgACCjHga8U=" + ); +} + +fn typical_boc_test(boc_base64: &str, expected_hash: &str) { + let boc = BagOfCells::parse_base64(boc_base64).unwrap(); + let root_cell = boc.single_root().unwrap(); + let hash = root_cell.cell_hash(); + assert_eq!(hash.to_string(), expected_hash); + + let boc_base64_again = boc.to_base64(false).unwrap(); + assert_eq!(boc_base64_again, boc_base64); +} + +#[test] +fn test_boc_encode_pruned_block() { + let boc = "te6ccgEBBAEArwAJRgPIr248LcbQSSCsDD5Rb27WLhRGYiTEGG+uChgAAXoNHAAIASJxwAtrH/x8t+GjDO5/X/f1fk4Rw3oYx+9S1gRE8vya04qzwiyFkEMdYglgAAAaNN8fbBluIJfFw9NAAgMoSAEB/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cAAByhIAQEg0z54hgTX/ohMEnHs6qluCydagWgxQoxSyLwK8qfAOQAA"; + let hash = "a6f4b8afa43a9ee61f6d89050d665d164c94c5eca658ddb6c2ab34b4118ab34c"; + typical_boc_test(boc, hash); +} + +/// Checks whether BoC encoding doesn't panic because of invalid input. +#[test] +fn test_boc_decode_invalid() { + #[track_caller] + fn test_invalid(input_hex: &str) { + let input = input_hex.decode_hex().unwrap(); + BagOfCells::parse(&input).unwrap_err(); + } + + test_invalid("b5ee9c725e0000030000000000000000000000000000000000005e"); + + // Errors in `BagOfCells::parse()`. + test_invalid("b5ee9c72c9000001000000000000100000000000000000ff20d1fffe20000052180000001926"); + test_invalid("b5ee9c7201000001000056600000000c000c0cff5e0000005eb5ee9c72ca0c0c0c0c0c0c00"); + test_invalid("b5ee9c72ca0000010000560c0c130c0c0c0c0c0c0c0c000c0c0c5e5e0c0c00b5ee0c5e5e"); + + // Errors in `cell::get_bit_descriptor()`. + test_invalid("b5ee9c72ca0000010000560c0c130c0c0c0c0c0c0c0c000c0c0c5e5e0c0c00b5ee0c5e5e"); + test_invalid("b5ee9c72ca0000230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000c000c0cffffffffffff0000000000000000000000000000000000000000000600080c"); + + // Error in `cell::get_refs_descriptor()`. + test_invalid("b5ee9c72d1000c0c0c0c20260cba5e0900002a2600000000000000090909090909090909090909090909090909090909091f1f1f1f090909090909090909090971ee31310909090909090909090200000900090909090901680909090909090909090909090909090909090909090000000000000000000000000c88f3"); + // Errors in `CellType::level_mask()`. + test_invalid("b5ee9c72ca0000180000250125000000000000000b0b0b0b0b0b0404040404040404030404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040408080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808040404040c04040404040404040404040404040404040404040404040404040404040404040404040404270404040404040404040404040404040404040400005204040404040404040404000404040404040404040404040404040403fb04040404040404040404040404040404040404040404040400002501250b4b0b0800ca00250c00000c000c100c0c0c26"); + // Error in `cell::calculate_hashes_and_depths()`. + test_invalid("b5ee9c72d1000a000000000000000008860101ff041cffff000100000000000010081c01000000000000000000000000000000000000b5ee00000000ff9c72d1000a0000000000000000000000ac0000000006060606060606060606060606000008d60104ff031cff530000002e0000080000000000000000b0504f4f4ab0b0b0b0b0b0b0b0b00f00b00500000f0000000000030053a900002f00000000000000feffffffff0000000000009ce4ee6100000000000000000000000000000886fc00ff041cffff00000000000063000000000000eeee9c72069c720606060000060600"); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/cell.rs b/rust/frameworks/tw_ton_sdk/tests/cell.rs new file mode 100644 index 00000000000..0baf948a31b --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/cell.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_ton_sdk::boc::BagOfCells; + +#[test] +fn test_cell_format() { + let boc = BagOfCells::parse_base64( + "te6ccgEBAgEALQABDv8AiNDtHtgBCEIC5wowbAAnJ5YkP1ac4Mko6kz8nxtlxbAGbjghWfXoDfU=", + ) + .unwrap(); + let cell = boc.single_root().unwrap(); + + let actual_fmt = format!("{cell:?}"); + let expected = +"Cell(root) { data=ff0088d0ed1ed8, bit_len=56 } + -> Cell(ref 1) { data=02e70a306c00272796243f569ce0c928ea4cfc9f1b65c5b0066e382159f5e80df5, bit_len=264 exotic=Library } +"; + assert_eq!(actual_fmt, expected); +} diff --git a/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs b/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs new file mode 100644 index 00000000000..445e3842a3d --- /dev/null +++ b/rust/frameworks/tw_ton_sdk/tests/cell_parser.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_number::U256; +use tw_ton_sdk::address::address_data::AddressData; +use tw_ton_sdk::address::user_friendly_address::UserFriendlyAddress; +use tw_ton_sdk::boc::BagOfCells; + +/// In this test we parse a TON internal transfer message encoded as BoC. +#[test] +fn test_cell_parse_internal_transfer_message() { + let boc = BagOfCells::parse_base64( + "te6cckEBAQEAOgAAcGIARUMTww0u7LZO2ecEA9iRbS9qOcmmc6tFLaWOGw5dlFdEAAAAAAAAAAAAAAAAAAAAAAAAAAAAfRXk0w==", + ) + .unwrap(); + let cell = boc.single_root().unwrap(); + let mut parser = cell.parser(); + + let bit_0 = parser.load_bit().unwrap(); + let ihr_disabled = parser.load_bit().unwrap(); + let bounce = parser.load_bit().unwrap(); + let bounced = parser.load_bit().unwrap(); + let src_addr = parser.load_address().unwrap(); + let dest_addr = parser.load_address().unwrap(); + let value = parser.load_coins().unwrap(); + let currency_collections = parser.load_bit().unwrap(); + let ihr_fees = parser.load_coins().unwrap(); + let fwd_fees = parser.load_coins().unwrap(); + let created_lt = parser.load_u64(64).unwrap(); + let created_at = parser.load_u32(32).unwrap(); + let contains_state_init = parser.load_bit().unwrap(); + let contains_data = parser.load_bit().unwrap(); + + parser.ensure_empty().expect("Must be read fully"); + + let expected_dest_addr = + UserFriendlyAddress::from_base64_url("EQCKhieGGl3ZbJ2zzggHsSLaXtRzk0znVopbSxw2HLsorkdl") + .unwrap(); + let expected_value = U256::from(9_223_372_036_854_775_808_u64); + + assert_eq!(bit_0, false); + assert_eq!(ihr_disabled, true); + assert_eq!(bounce, true); + assert_eq!(bounced, false); + assert_eq!(src_addr, AddressData::NULL); + assert_eq!(dest_addr, expected_dest_addr.into_data()); + assert_eq!(value, expected_value); + assert_eq!(currency_collections, false); + assert_eq!(ihr_fees, U256::ZERO); + assert_eq!(fwd_fees, U256::ZERO); + assert_eq!(created_lt, 0); + assert_eq!(created_at, 0); + assert_eq!(contains_state_init, false); + assert_eq!(contains_data, false); +} diff --git a/rust/frameworks/tw_utxo/Cargo.toml b/rust/frameworks/tw_utxo/Cargo.toml new file mode 100644 index 00000000000..08009365f7f --- /dev/null +++ b/rust/frameworks/tw_utxo/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tw_utxo" +version = "0.1.0" +edition = "2021" + +[dependencies] +bech32 = "0.9.1" +bitcoin = { version = "0.30.0", features = ["rand-std"] } +byteorder = "1.4" +itertools = "0.10.5" +secp256k1 = { version = "0.27.0", features = ["rand-std"] } +strum_macros = "0.25" +tw_base58_address = { path = "../../tw_base58_address" } +tw_bech32_address = { path = "../../tw_bech32_address" } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } diff --git a/rust/frameworks/tw_utxo/src/address/derivation.rs b/rust/frameworks/tw_utxo/src/address/derivation.rs new file mode 100644 index 00000000000..05ced642e02 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/derivation.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::derivation::{ChildIndex, Derivation}; + +pub const SEGWIT_DERIVATION_PATH_TYPE: ChildIndex = ChildIndex::Hardened(84); + +pub enum BitcoinDerivation { + Legacy, + Segwit, +} + +impl BitcoinDerivation { + /// TrustWallet derivation inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L67 + pub fn tw_derivation(coin: &dyn CoinContext, derivation: Derivation) -> BitcoinDerivation { + match derivation { + // In case of a default derivation specified by the function caller, + // we should check the default derivation in the `coin`'s context. + // Please note that testnet derivation is no longer supported. Instead, use address prefix. + Derivation::Default | Derivation::Testnet => (), + Derivation::Segwit => return BitcoinDerivation::Segwit, + Derivation::Legacy => return BitcoinDerivation::Legacy, + } + + let Some(default_derivation) = coin.derivations().first() else { + return BitcoinDerivation::Legacy; + }; + let derivation_path_type = default_derivation.path.path().first().copied(); + + match default_derivation.name { + Derivation::Segwit => BitcoinDerivation::Segwit, + Derivation::Default if derivation_path_type == Some(SEGWIT_DERIVATION_PATH_TYPE) => { + BitcoinDerivation::Segwit + }, + Derivation::Default | Derivation::Legacy | Derivation::Testnet => { + BitcoinDerivation::Legacy + }, + } + } + + /// TrustWallet behaviour inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L14 + pub fn tw_supports_segwit(coin: &dyn CoinContext) -> bool { + coin.derivations().iter().any(|der| { + der.name == Derivation::Segwit + || der.path.path().first().copied() == Some(SEGWIT_DERIVATION_PATH_TYPE) + }) + } +} diff --git a/rust/frameworks/tw_utxo/src/address/legacy.rs b/rust/frameworks/tw_utxo/src/address/legacy.rs new file mode 100644 index 00000000000..22def1d3750 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/legacy.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::script::Script; +use std::fmt; +use std::str::FromStr; +use tw_base58_address::Base58Address; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::prefix::BitcoinBase58Prefix; +use tw_encoding::base58::Alphabet; +use tw_hash::hasher::{sha256_ripemd, Hasher}; +use tw_hash::H160; +use tw_keypair::{ecdsa, tw}; + +pub const BITCOIN_ADDRESS_SIZE: usize = 21; +pub const BITCOIN_ADDRESS_CHECKSUM_SIZE: usize = 4; + +type BitcoinBase58Address = Base58Address; + +#[derive(Debug, Eq, PartialEq)] +pub struct LegacyAddress(BitcoinBase58Address); + +impl LegacyAddress { + pub fn new(prefix: u8, data: &[u8]) -> AddressResult { + let mut bytes = Vec::with_capacity(data.len() + 1); + // Insert the prefix to the beginning of the address bytes array. + bytes.push(prefix); + bytes.extend_from_slice(data); + + BitcoinBase58Address::new(&bytes, Alphabet::Bitcoin, Hasher::Sha256d).map(LegacyAddress) + } + + pub fn p2pkh_with_public_key( + p2pkh_prefix: u8, + public_key: &ecdsa::secp256k1::PublicKey, + ) -> AddressResult { + let public_key_hash = sha256_ripemd(public_key.compressed().as_slice()); + LegacyAddress::new(p2pkh_prefix, &public_key_hash) + } + + /// Tries to parse a `LegacyAddress` and check if + pub fn p2pkh_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + ) -> AddressResult { + let p2pkh_prefix = match prefix { + Some(prefix) => prefix.p2pkh, + None => coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?, + }; + + let ecdsa_public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + + LegacyAddress::p2pkh_with_public_key(p2pkh_prefix, ecdsa_public_key) + } + + pub fn p2sh_with_prefix_byte( + redeem_script: &Script, + p2sh_prefix: u8, + ) -> AddressResult { + let script_hash = sha256_ripemd(redeem_script.as_slice()); + LegacyAddress::new(p2sh_prefix, &script_hash) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let base58_prefix = match prefix { + Some(base58_prefix) => base58_prefix, + None => { + let p2pkh = coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?; + let p2sh = coin.p2sh_prefix().ok_or(AddressError::InvalidRegistry)?; + BitcoinBase58Prefix { p2pkh, p2sh } + }, + }; + + LegacyAddress::from_str_checked(s, base58_prefix.p2pkh, base58_prefix.p2sh) + } + + pub fn from_str_checked( + s: &str, + p2pkh_prefix: u8, + p2sh_prefix: u8, + ) -> AddressResult { + let addr = LegacyAddress::from_str(s)?; + if addr.prefix() == p2pkh_prefix || addr.prefix() == p2sh_prefix { + Ok(addr) + } else { + Err(AddressError::UnexpectedAddressPrefix) + } + } + + pub fn prefix(&self) -> u8 { + self.0.as_ref()[0] + } + + /// Address bytes excluding the prefix (skip first byte). + pub fn bytes(&self) -> &[u8] { + &self.0.as_ref()[1..] + } + + pub fn payload(&self) -> H160 { + debug_assert_eq!(self.bytes().len(), H160::LEN); + H160::try_from(self.bytes()).expect("Legacy address must be exactly 20 bytes") + } +} + +impl FromStr for LegacyAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + BitcoinBase58Address::from_str_with_alphabet(s, Alphabet::Bitcoin, Hasher::Sha256d) + .map(LegacyAddress) + } +} + +impl<'a> TryFrom<&'a [u8]> for LegacyAddress { + type Error = AddressError; + + fn try_from(bytes: &'a [u8]) -> Result { + BitcoinBase58Address::new(bytes, Alphabet::Bitcoin, Hasher::Sha256d).map(LegacyAddress) + } +} + +impl fmt::Display for LegacyAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/rust/frameworks/tw_utxo/src/address/mod.rs b/rust/frameworks/tw_utxo/src/address/mod.rs new file mode 100644 index 00000000000..c7c4539b421 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod derivation; +pub mod legacy; +pub mod segwit; +pub mod standard_bitcoin; +pub mod taproot; +pub mod witness_program; + +type Bech32Prefix = tw_bech32_address::bech32_prefix::Bech32Prefix; diff --git a/rust/frameworks/tw_utxo/src/address/segwit.rs b/rust/frameworks/tw_utxo/src/address/segwit.rs new file mode 100644 index 00000000000..8098fc4ad93 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/segwit.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use super::Bech32Prefix; +use crate::address::witness_program::{WitnessProgram, WITNESS_V0}; +use crate::script::Script; +use core::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_hash::hasher::sha256_ripemd; +use tw_hash::sha2::sha256; +use tw_hash::{H160, H256}; +use tw_keypair::tw; +use tw_memory::Data; + +/// Witness program sizes valid for V0 (Segwit). +const WITNESS_V0_VALID_PROGRAM_SIZES: [usize; 2] = [H160::LEN, H256::LEN]; + +#[derive(Debug, Eq, PartialEq)] +pub struct SegwitAddress { + inner: WitnessProgram, +} + +impl SegwitAddress { + pub fn new(hrp: String, witness_program: Data) -> AddressResult { + // Specific segwit v0 check. These addresses can never spend funds sent to them. + if !WITNESS_V0_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let inner = WitnessProgram::new(hrp, WITNESS_V0, witness_program, bech32::Variant::Bech32)?; + Ok(SegwitAddress { inner }) + } + + pub fn p2wpkh_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + + let public_key_bytes = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .compressed(); + let public_key_hash = sha256_ripemd(public_key_bytes.as_slice()); + + Self::new(hrp, public_key_hash.to_vec()) + } + + pub fn p2wsh_with_hrp(redeem_script: &Script, hrp: String) -> AddressResult { + let script_hash = sha256(redeem_script.as_slice()); + Self::new(hrp, script_hash) + } + + pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult { + let address = Self::from_str(s)?; + if address.inner.hrp() != expected_hrp { + return Err(AddressError::InvalidHrp); + } + Ok(address) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + SegwitAddress::from_str_checked(s, &hrp) + } + + pub fn witness_program(&self) -> &[u8] { + self.inner.witness_program() + } +} + +impl FromStr for SegwitAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let inner = WitnessProgram::from_str_checked( + s, + WITNESS_V0, + bech32::Variant::Bech32, + &WITNESS_V0_VALID_PROGRAM_SIZES, + )?; + Ok(SegwitAddress { inner }) + } +} + +impl fmt::Display for SegwitAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_encoding::hex::DecodeHex; + + struct TestInputValid { + str: &'static str, + normalized: &'static str, + expected: SegwitAddress, + } + + #[track_caller] + fn segwit_addr(hrp: &str, program: &str) -> SegwitAddress { + SegwitAddress::new(hrp.to_string(), program.decode_hex().unwrap()) + .expect("Cannot construct a SegwitAddress from the input") + } + + /// Tests if the given `s` string representation is converted from and to `expected` segwit address. + #[track_caller] + fn test_to_from_str_valid(input: TestInputValid) { + let actual = SegwitAddress::from_str(input.str).expect("Expected a valid address"); + assert_eq!(actual, input.expected, "String -> SegwitAddress"); + + let actual_str = actual.to_string(); + assert_eq!(actual_str, input.normalized, "SegwitAddress -> String"); + } + + #[track_caller] + fn test_from_str_invalid(str: &str) { + let _ = SegwitAddress::from_str(str).expect_err("Expected an invalid Segwit address"); + } + + #[test] + fn test_segwit_address_to_from_str() { + test_to_from_str_valid(TestInputValid { + str: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + normalized: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + expected: segwit_addr("bc", "751e76e8199196d454941c45d1b3a323f1433bd6"), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + normalized: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + expected: segwit_addr( + "tb", + "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + normalized: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + expected: segwit_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1qpjult34k9spjfym8hss2jrwjgf0xjf40ze0pp8", + normalized: "bc1qpjult34k9spjfym8hss2jrwjgf0xjf40ze0pp8", + expected: segwit_addr("bc", "0cb9f5c6b62c03249367bc20a90dd2425e6926af"), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1qm9jzmujvdqjj6y28hptk859zs3yyv78hpqqjfj", + normalized: "bc1qm9jzmujvdqjj6y28hptk859zs3yyv78hpqqjfj", + expected: segwit_addr("bc", "d9642df24c68252d1147b85763d0a284484678f7"), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + normalized: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + expected: segwit_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + } + + #[test] + fn test_segwit_address_from_str_invalid() { + // witness program size 38 + test_from_str_invalid( + "bc1q0xlxvlhemja6c4dqv22uapctqupfhlxm0xlxvlhemja6c4dqv22uapctqupfkpvgusg", + ); + + // version 1 + test_from_str_invalid("bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf"); + + // version 1 + test_from_str_invalid( + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + ); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 16 + test_from_str_invalid("BC1SW50QGDZ25J"); + } +} diff --git a/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs new file mode 100644 index 00000000000..6f582b0976b --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! This module contains a standard Bitcoin address enumeration each of these exist on the mainnet network. +//! TODO consider moving the file to `tw_bitcoin`. + +use crate::address::derivation::BitcoinDerivation; +use crate::address::legacy::LegacyAddress; +use crate::address::segwit::SegwitAddress; +use crate::address::taproot::TaprootAddress; +use crate::address::Bech32Prefix; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::prefix::{AddressPrefix, BitcoinBase58Prefix}; +use tw_keypair::tw; +use tw_memory::Data; + +/// A standard set of Bitcoin address prefixes. +/// The set of address prefixes can differ for Bitcoin forks. +/// TODO add `TaprootBech32` enum variant. +pub enum StandardBitcoinPrefix { + Base58(BitcoinBase58Prefix), + Bech32(Bech32Prefix), +} + +impl TryFrom for StandardBitcoinPrefix { + type Error = AddressError; + + fn try_from(prefix: AddressPrefix) -> Result { + match prefix { + AddressPrefix::BitcoinBase58(base58) => Ok(StandardBitcoinPrefix::Base58(base58)), + AddressPrefix::Hrp(hrp) => Ok(StandardBitcoinPrefix::Bech32(Bech32Prefix { hrp })), + } + } +} + +/// A standard set of Bitcoin address types. +/// +/// The set of address types can differ for Bitcoin forks. +/// For example, Zcash does not support segwit addresses. +#[derive(Debug, Eq, PartialEq)] +pub enum StandardBitcoinAddress { + Legacy(LegacyAddress), + Segwit(SegwitAddress), + Taproot(TaprootAddress), +} + +impl StandardBitcoinAddress { + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address matches the given `prefix` address or belongs to the `coin` network. + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + match prefix { + Some(StandardBitcoinPrefix::Base58(base58)) => { + LegacyAddress::from_str_with_coin_and_prefix(coin, s, Some(base58)) + .map(StandardBitcoinAddress::Legacy) + }, + Some(StandardBitcoinPrefix::Bech32(bech32)) => { + SegwitAddress::from_str_with_coin_and_prefix(coin, s, Some(bech32)) + .map(StandardBitcoinAddress::Segwit) + }, + None => StandardBitcoinAddress::from_str_checked(coin, s), + } + } + + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address belongs to the `coin` network. + pub fn from_str_checked( + coin: &dyn CoinContext, + s: &str, + ) -> AddressResult { + // Try to parse a Segwit address if the coin supports it. + if BitcoinDerivation::tw_supports_segwit(coin) { + if let Ok(segwit) = SegwitAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Segwit(segwit)); + } + + // TODO use `BitcoinDerivation::tw_supports_taproot` based on `registry.json`. + if let Ok(taproot) = TaprootAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Taproot(taproot)); + } + } + + // Otherwise, try to parse a Legacy address. + if let Ok(legacy) = LegacyAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Legacy(legacy)); + } + + Err(AddressError::InvalidInput) + } + + /// TrustWallet derivation inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L67 + pub fn derive_as_tw( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + derivation: Derivation, + maybe_prefix: Option, + ) -> AddressResult { + match maybe_prefix { + Some(StandardBitcoinPrefix::Base58(prefix)) => { + return LegacyAddress::p2pkh_with_coin_and_prefix(coin, public_key, Some(prefix)) + .map(StandardBitcoinAddress::Legacy); + }, + Some(StandardBitcoinPrefix::Bech32(prefix)) => { + return SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, Some(prefix)) + .map(StandardBitcoinAddress::Segwit); + }, + // Derive an address as declared in registry.json. + None => (), + } + + match BitcoinDerivation::tw_derivation(coin, derivation) { + BitcoinDerivation::Legacy => { + LegacyAddress::p2pkh_with_coin_and_prefix(coin, public_key, None) + .map(StandardBitcoinAddress::Legacy) + }, + BitcoinDerivation::Segwit => { + SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, None) + .map(StandardBitcoinAddress::Segwit) + }, + } + } +} + +impl FromStr for StandardBitcoinAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if let Ok(legacy) = LegacyAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Legacy(legacy)); + } + if let Ok(segwit) = SegwitAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Segwit(segwit)); + } + if let Ok(taproot) = TaprootAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Taproot(taproot)); + } + Err(AddressError::InvalidInput) + } +} + +impl fmt::Display for StandardBitcoinAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StandardBitcoinAddress::Legacy(legacy) => write!(f, "{legacy}"), + StandardBitcoinAddress::Segwit(segwit) => write!(f, "{segwit}"), + StandardBitcoinAddress::Taproot(taproot) => write!(f, "{taproot}"), + } + } +} + +impl CoinAddress for StandardBitcoinAddress { + fn data(&self) -> Data { + match self { + StandardBitcoinAddress::Legacy(legacy) => legacy.bytes().to_vec(), + StandardBitcoinAddress::Segwit(segwit) => segwit.witness_program().to_vec(), + StandardBitcoinAddress::Taproot(taproot) => taproot.witness_program().to_vec(), + } + } +} diff --git a/rust/frameworks/tw_utxo/src/address/taproot.rs b/rust/frameworks/tw_utxo/src/address/taproot.rs new file mode 100644 index 00000000000..d0f18810a20 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/taproot.rs @@ -0,0 +1,250 @@ +use super::Bech32Prefix; +use crate::address::witness_program::WitnessProgram; +use bitcoin::key::TapTweak; +use core::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_hash::{H256, H264}; +use tw_keypair::tw; +use tw_memory::Data; +use tw_misc::traits::ToBytesVec; + +/// cbindgen:ignore +pub const WITNESS_V1: u8 = 1; +/// Witness program sizes valid for V1 (Taproot). +/// cbindgen:ignore +pub const WITNESS_V1_VALID_PROGRAM_SIZES: [usize; 1] = [H256::LEN]; + +#[derive(Debug, Eq, PartialEq)] +pub struct TaprootAddress { + inner: WitnessProgram, +} + +impl TaprootAddress { + pub fn new(hrp: String, witness_program: Data) -> AddressResult { + // Specific Taproot V1 check. These addresses can never spend funds sent to them. + if !WITNESS_V1_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let inner = + WitnessProgram::new(hrp, WITNESS_V1, witness_program, bech32::Variant::Bech32m)?; + Ok(TaprootAddress { inner }) + } + + /// Create a Taproot address from a public key and an optional merkle root. + /// Taproot transactions come in two variants: + /// + /// * P2TR key-path: which is used for "normal" balance transfers and is + /// internally _tweaked_ with an empty (None) merkle root. + /// * P2TR script-path: which is used for complex scripts, such as + /// Ordinals/BRC20, and is internally _tweaked_ with a merkle root of all + /// possible spending conditions. + pub fn p2tr_with_public_key( + hrp: String, + internal_pubkey: &H264, + merkle_root: Option<&H256>, + ) -> AddressResult { + // We're relying on the `bitcoin` crate to generate anything Taproot related. + + // Convert the native `H256` to `TapNodeHash` from the `bitcoin` crate. + let merkle_root = merkle_root.map(|hash| { + let tap_hash = + as bitcoin::hashes::Hash>::from_slice( + hash.as_slice(), + ) + .expect("merkle_root length is 32 bytes"); + + bitcoin::taproot::TapNodeHash::from_raw_hash(tap_hash) + }); + + // Tweak the public key with the (empty) merkle root. + let pubkey = bitcoin::PublicKey::from_slice(internal_pubkey.as_slice()).unwrap(); + let internal_key = bitcoin::secp256k1::XOnlyPublicKey::from(pubkey.inner); + let (output_key, _parity) = + internal_key.tap_tweak(&bitcoin::secp256k1::Secp256k1::new(), merkle_root); + + Self::new(hrp, output_key.serialize().to_vec()) + } + + /// Create a Taproot address from a public key and an optional merkle root. + /// Taproot transactions come in two variants: + /// + /// * P2TR key-path: which is used for "normal" balance transfers and is + /// internally _tweaked_ with an empty (None) merkle root. + /// * P2TR script-path: which is used for complex scripts, such as + /// Ordinals/BRC20, and is internally _tweaked_ with a merkle root of all + /// possible spending conditions. + pub fn p2tr_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + merkle_root: Option<&H256>, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + + let public_key_bytes = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .compressed(); + + Self::p2tr_with_public_key(hrp, &public_key_bytes, merkle_root) + } + + pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult { + let address = Self::from_str(s)?; + if address.inner.hrp() != expected_hrp { + return Err(AddressError::InvalidHrp); + } + Ok(address) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + TaprootAddress::from_str_checked(s, &hrp) + } + + pub fn witness_program(&self) -> &[u8] { + self.inner.witness_program() + } +} + +impl FromStr for TaprootAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let inner = WitnessProgram::from_str_checked( + s, + WITNESS_V1, + bech32::Variant::Bech32m, + &WITNESS_V1_VALID_PROGRAM_SIZES, + )?; + Ok(TaprootAddress { inner }) + } +} + +impl fmt::Display for TaprootAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_coin_entry::test_utils::test_context::TestCoinContext; + use tw_encoding::hex::DecodeHex; + + struct TestInputValid { + str: &'static str, + normalized: &'static str, + expected: TaprootAddress, + } + + #[track_caller] + fn taproot_addr(hrp: &str, program: &str) -> TaprootAddress { + TaprootAddress::new(hrp.to_string(), program.decode_hex().unwrap()) + .expect("Cannot construct a TaprootAddress from the input") + } + + /// Tests if the given `s` string representation is converted from and to `expected` segwit address. + #[track_caller] + fn test_to_from_str_valid(input: TestInputValid) { + let actual = TaprootAddress::from_str(input.str).expect("Expected a valid address"); + assert_eq!(actual, input.expected, "String -> TaprootAddress"); + + let actual_str = actual.to_string(); + assert_eq!(actual_str, input.normalized, "TaprootAddress -> String"); + } + + #[track_caller] + fn test_from_str_invalid(str: &str) { + let _ = TaprootAddress::from_str(str).expect_err("Expected an invalid Taproot address"); + } + + #[test] + fn test_segwit_address_to_from_str() { + test_to_from_str_valid(TestInputValid { + str: "bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf", + normalized: "bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf", + expected: taproot_addr( + "bc", + "5ee16f6144e2d46edea1427e1222ea879377e029b0c5d2e252517aee85948ec7", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + normalized: "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + expected: taproot_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + normalized: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + expected: taproot_addr( + "bc", + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ), + }); + } + + #[test] + fn test_taproot_address_from_str_invalid() { + // version 0 + test_from_str_invalid("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 16 + test_from_str_invalid("BC1SW50QGDZ25J"); + + // program size 40 + test_from_str_invalid( + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + ); + } + + #[test] + fn test_taproot_address_create_with_coin_and_prefix() { + let coin = TestCoinContext::default(); + let hrp = "bc".to_string(); + let merkle_root = None; + + let public_key_bytes = "03cdf7e208a0146c3a35c181944a96a15b2a58256be69adad640a9a97d408b9b44" + .decode_hex() + .unwrap(); + let public_key = + tw::PublicKey::new(public_key_bytes.clone(), tw::PublicKeyType::Secp256k1).unwrap(); + + let addr = TaprootAddress::p2tr_with_coin_and_prefix( + &coin, + &public_key, + Some(Bech32Prefix { hrp }), + merkle_root, + ) + .unwrap(); + assert_eq!( + addr.to_string(), + "bc1purekytqrqzfzdulufmll8a335jhvw9x4glhzp8fv76yxlsxeyptsfylq9h" + ); + } +} diff --git a/rust/frameworks/tw_utxo/src/address/witness_program.rs b/rust/frameworks/tw_utxo/src/address/witness_program.rs new file mode 100644 index 00000000000..0edb5ea65be --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/witness_program.rs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use bech32::FromBase32; +use std::fmt; +use std::ops::RangeInclusive; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +/// cbindgen:ignore +pub const WITNESS_V0: u8 = 0; +/// cbindgen:ignore +pub const MAX_WITNESS_VERSION: u8 = 16; +/// cbindgen:ignore +pub const WITNESS_VERSIONS: RangeInclusive = WITNESS_V0..=MAX_WITNESS_VERSION; +/// Witness program sizes valid for most of the witness versions. +/// Please note that V0 is more constraint. +/// cbindgen:ignore +pub const WITNESS_VALID_PROGRAM_SIZES: RangeInclusive = 2..=40; + +/// A segwit address implementation that supports various program versions. +/// For example: +/// * witness V0 is Segwit address +/// * witness V1 is Taproot address +#[derive(Debug, Eq, PartialEq)] +pub struct WitnessProgram { + hrp: String, + witness_version: u8, + witness_program: Data, + /// An address string created from this `hrp`, `witness_version` and `witness_program`. + address_str: String, + bech32_variant: bech32::Variant, +} + +impl WitnessProgram { + pub fn new( + hrp: String, + witness_version: u8, + witness_program: Data, + bech32_variant: bech32::Variant, + ) -> AddressResult { + if !WITNESS_VERSIONS.contains(&witness_version) { + return Err(AddressError::Unsupported); + } + + if !WITNESS_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let address_str = + Self::fmt_internal(&hrp, witness_version, &witness_program, bech32_variant)?; + Ok(WitnessProgram { + hrp, + witness_version, + witness_program, + address_str, + bech32_variant, + }) + } + + pub fn witness_version(&self) -> u8 { + self.witness_version + } + + pub fn witness_program(&self) -> &[u8] { + &self.witness_program + } + + pub fn hrp(&self) -> &str { + &self.hrp + } + + pub fn from_str_checked( + s: &str, + expected_version: u8, + expected_checksum_type: bech32::Variant, + valid_program_sizes: &[usize], + ) -> AddressResult { + let (hrp, payload_u5, checksum_variant) = + bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?; + + if payload_u5.is_empty() { + return Err(AddressError::InvalidInput); + } + + // Get the script version and program (converted from 5-bit to 8-bit) + let (version, program) = payload_u5.split_at(1); + let version = version[0].to_u8(); + let program = Data::from_base32(program).map_err(|_| AddressError::FromBech32Error)?; + + // Check witness version. + if version != expected_version { + return Err(AddressError::Unsupported); + } + + // Check encoding. + if checksum_variant != expected_checksum_type { + return Err(AddressError::InvalidInput); + } + + // Check witness program sizes. + if !valid_program_sizes.contains(&program.len()) { + return Err(AddressError::InvalidInput); + } + + WitnessProgram::new(hrp, version, program, checksum_variant) + } + + fn fmt_internal( + hrp: &str, + witness_version: u8, + witness_program: &[u8], + bech32_variant: bech32::Variant, + ) -> AddressResult { + const STRING_CAPACITY: usize = 100; + + let mut result_addr = String::with_capacity(STRING_CAPACITY); + + let version_u5 = + bech32::u5::try_from_u8(witness_version).expect("WitnessVersion must be 0..=16"); + + { + let mut bech32_writer = + bech32::Bech32Writer::new(hrp, bech32_variant, &mut result_addr) + .map_err(|_| AddressError::FromBech32Error)?; + bech32::WriteBase32::write_u5(&mut bech32_writer, version_u5) + .map_err(|_| AddressError::FromBech32Error)?; + bech32::ToBase32::write_base32(&witness_program, &mut bech32_writer) + .map_err(|_| AddressError::FromBech32Error)?; + } + + Ok(result_addr) + } +} + +impl fmt::Display for WitnessProgram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address_str) + } +} diff --git a/rust/frameworks/tw_utxo/src/constants.rs b/rust/frameworks/tw_utxo/src/constants.rs new file mode 100644 index 00000000000..0a99f519fac --- /dev/null +++ b/rust/frameworks/tw_utxo/src/constants.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +/// A standard transaction is limited to 400k weight units (WU). +/// https://bitcoin.stackexchange.com/questions/35570/what-is-the-maximum-number-of-inputs-outputs-a-transaction-can-have +pub const MAX_TRANSACTION_WEIGHT: usize = 400_000; diff --git a/rust/frameworks/tw_utxo/src/dust/dust_filter.rs b/rust/frameworks/tw_utxo/src/dust/dust_filter.rs new file mode 100644 index 00000000000..0d68ee106dc --- /dev/null +++ b/rust/frameworks/tw_utxo/src/dust/dust_filter.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::dust::DustPolicy; +use crate::script::standard_script::conditions; +use crate::transaction::transaction_interface::{TransactionInterface, TxOutputInterface}; +use crate::transaction::unsigned_transaction::UnsignedTransaction; +use std::marker::PhantomData; +use tw_coin_entry::error::prelude::*; + +pub struct DustFilter { + dust_policy: DustPolicy, + _phantom: PhantomData, +} + +impl DustFilter { + pub fn new(dust_policy: DustPolicy) -> Self { + DustFilter { + dust_policy, + _phantom: PhantomData, + } + } + + /// Filter dust UTXOs out. + /// Returns an error if there are no valid UTXOs. + pub fn filter_inputs( + &self, + mut transaction: UnsignedTransaction, + ) -> SigningResult> { + let dust_threshold = self.dust_policy.dust_threshold(); + + transaction.retain_inputs(|_utxo, utxo_args| utxo_args.amount >= dust_threshold)?; + + Ok(transaction) + } + + /// Checks if all transaction output amounts are greater or equal to a dust threshold. + pub fn check_outputs( + &self, + transaction: &UnsignedTransaction, + ) -> SigningResult<()> { + let dust_threshold = self.dust_policy.dust_threshold(); + + let has_dust_output = transaction.transaction().outputs().iter().any(|output| { + if conditions::is_op_return(output.script_pubkey()) { + // Ignore the OP_RETURN output value. It can (or even should) be 0. + return false; + } + output.value() < dust_threshold + }); + + if has_dust_output { + return SigningError::err(SigningErrorType::Error_dust_amount_requested); + } + Ok(()) + } +} diff --git a/rust/frameworks/tw_utxo/src/dust/mod.rs b/rust/frameworks/tw_utxo/src/dust/mod.rs new file mode 100644 index 00000000000..5e605ae241f --- /dev/null +++ b/rust/frameworks/tw_utxo/src/dust/mod.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::transaction_parts::Amount; + +pub mod dust_filter; + +/// Transaction dust amount calculator. +/// Later, we plan to add support for `DynamicDust` policy with a `min_relay_fee` amount. +#[derive(Clone, Copy)] +pub enum DustPolicy { + FixedAmount(Amount), +} + +impl DustPolicy { + pub fn dust_threshold(&self) -> Amount { + match self { + DustPolicy::FixedAmount(amount) => *amount, + } + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs new file mode 100644 index 00000000000..4a95a378699 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::stream::Stream; +use crate::encode::Encodable; +use std::ops::RangeInclusive; + +const ONE_BYTE_RANGE: RangeInclusive = 0..=0xFC; +const TWO_BYTES_RANGE: RangeInclusive = 0xFD..=0xFFFF; +const FOUR_BYTES_RANGE: RangeInclusive = 0x10000..=0xFFFF_FFFF; + +const TWO_BYTES_FLAG: u8 = 0xFD_u8; +const FOUR_BYTES_FLAG: u8 = 0xFE_u8; +const EIGHT_BYTES_FLAG: u8 = 0xFF_u8; + +/// A type of variable-length integer commonly used in the Bitcoin P2P protocol and Bitcoin serialized data structures. +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct CompactInteger(u64); + +impl From for CompactInteger { + fn from(value: usize) -> Self { + CompactInteger(value as u64) + } +} + +impl Encodable for CompactInteger { + fn encode(&self, stream: &mut Stream) { + let v = self.0; + + if ONE_BYTE_RANGE.contains(&v) { + stream.append(&(v as u8)); + } else if TWO_BYTES_RANGE.contains(&v) { + stream.append(&TWO_BYTES_FLAG).append(&(v as u16)); + } else if FOUR_BYTES_RANGE.contains(&v) { + stream.append(&FOUR_BYTES_FLAG).append(&(v as u32)); + } else { + stream.append(&EIGHT_BYTES_FLAG).append(&v); + } + } + + fn encoded_size(&self) -> usize { + const BYTE_FLAG: usize = 1; + + let v = self.0; + if ONE_BYTE_RANGE.contains(&v) { + BYTE_FLAG + } else if TWO_BYTES_RANGE.contains(&v) { + BYTE_FLAG + 2 + } else if FOUR_BYTES_RANGE.contains(&v) { + BYTE_FLAG + 4 + } else { + BYTE_FLAG + 8 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compact_integer_stream() { + let mut stream = Stream::default(); + + stream + .append(&CompactInteger::from(0_usize)) + .append(&CompactInteger::from(0xfc_usize)) + .append(&CompactInteger::from(0xfd_usize)) + .append(&CompactInteger::from(0xffff_usize)) + .append(&CompactInteger::from(0x10000_usize)) + .append(&CompactInteger::from(0xffff_ffff_usize)) + .append(&CompactInteger(0x1_0000_0000_u64)); + + let expected = vec![ + 0_u8, 0xfc, 0xfd, 0xfd, 0x00, 0xfd, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x01, 0x00, 0xfe, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + ]; + + assert_eq!(stream.out(), expected); + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/impls.rs b/rust/frameworks/tw_utxo/src/encode/impls.rs new file mode 100644 index 00000000000..b21b1567cc1 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/impls.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::compact_integer::CompactInteger; +use crate::encode::stream::Stream; +use crate::encode::Encodable; +use byteorder::{LittleEndian, WriteBytesExt}; +use tw_hash::Hash; +use tw_memory::Data; + +impl Encodable for Data { + fn encode(&self, stream: &mut Stream) { + stream + .append(&CompactInteger::from(self.len())) + .append_raw_slice(self.as_slice()); + } + + fn encoded_size(&self) -> usize { + CompactInteger::from(self.len()).encoded_size() + self.len() + } +} + +impl Encodable for Hash { + #[inline] + fn encode(&self, stream: &mut Stream) { + stream.append_raw_slice(self.as_slice()); + } + + #[inline] + fn encoded_size(&self) -> usize { + N + } +} + +impl Encodable for u8 { + #[inline] + fn encode(&self, s: &mut Stream) { + s.write_u8(*self).unwrap(); + } + + #[inline] + fn encoded_size(&self) -> usize { + 1 + } +} + +macro_rules! impl_encodable_for_int { + ($int:ty, $size:literal, $write_fn:tt) => { + impl Encodable for $int { + #[inline] + fn encode(&self, s: &mut Stream) { + s.$write_fn::(*self).unwrap(); + } + + #[inline] + fn encoded_size(&self) -> usize { + $size + } + } + }; +} + +impl_encodable_for_int!(i32, 4, write_i32); +impl_encodable_for_int!(i64, 8, write_i64); +impl_encodable_for_int!(u16, 2, write_u16); +impl_encodable_for_int!(u32, 4, write_u32); +impl_encodable_for_int!(u64, 8, write_u64); + +#[cfg(test)] +mod tests { + use super::*; + use crate::encode::encode; + use tw_encoding::hex::{DecodeHex, ToHex}; + + #[test] + fn test_stream_append() { + let mut stream = Stream::default(); + + stream + .append(&1u8) + .append(&2u16) + .append(&3u32) + .append(&4u64); + + let expected = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(stream.out(), expected); + } + + #[test] + fn test_bytes_serialize() { + let expected = "020145".decode_hex().unwrap(); + let bytes = "0145".decode_hex().unwrap(); + assert_eq!(expected, encode(&bytes)); + } + + #[test] + fn test_steam_append_slice() { + let mut slice = [0u8; 4]; + slice[0] = 0x64; + let mut stream = Stream::default(); + stream.append_raw_slice(&slice); + assert_eq!(stream.out().to_hex(), "64000000"); + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/mod.rs b/rust/frameworks/tw_utxo/src/encode/mod.rs new file mode 100644 index 00000000000..14eaad21574 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/mod.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::stream::Stream; +use tw_memory::Data; + +pub mod compact_integer; +pub mod impls; +pub mod stream; + +pub fn encode(t: &T) -> Data +where + T: Encodable, +{ + let mut stream = Stream::default(); + stream.append(t); + stream.out() +} + +pub trait Encodable { + /// Serialize the struct and appends it to the end of stream. + fn encode(&self, stream: &mut Stream); + + /// Hint about the size of serialized struct. + fn encoded_size(&self) -> usize; +} diff --git a/rust/frameworks/tw_utxo/src/encode/stream.rs b/rust/frameworks/tw_utxo/src/encode/stream.rs new file mode 100644 index 00000000000..566debfd62b --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/stream.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::compact_integer::CompactInteger; +use crate::encode::Encodable; +use std::io; +use std::io::Write; +use tw_memory::Data; + +/// Stream used for serialization of Bitcoin structures +#[derive(Default)] +pub struct Stream { + buffer: Data, +} + +impl Stream { + /// New stream + pub fn new() -> Self { + Stream { + buffer: Data::default(), + } + } + + /// Serializes the struct and appends it to the end of stream. + pub fn append(&mut self, t: &T) -> &mut Self + where + T: Encodable, + { + t.encode(self); + self + } + + /// Appends raw bytes to the end of the stream. + pub fn append_raw_slice(&mut self, bytes: &[u8]) -> &mut Self { + // discard error for now, since we write to simple vector + self.buffer.write_all(bytes).unwrap(); + self + } + + /// Appends a list of serializable structs to the end of the stream. + pub fn append_list(&mut self, t: &[T]) -> &mut Self { + CompactInteger::from(t.len()).encode(self); + for i in t { + i.encode(self); + } + self + } + + /// Full stream. + pub fn out(self) -> Data { + self.buffer + } +} + +impl Write for Stream { + #[inline] + fn write(&mut self, buf: &[u8]) -> Result { + self.buffer.write(buf) + } + + #[inline] + fn flush(&mut self) -> Result<(), io::Error> { + self.buffer.flush() + } +} diff --git a/rust/frameworks/tw_utxo/src/lib.rs b/rust/frameworks/tw_utxo/src/lib.rs new file mode 100644 index 00000000000..84748580ded --- /dev/null +++ b/rust/frameworks/tw_utxo/src/lib.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod constants; +pub mod dust; +pub mod encode; +pub mod modules; +pub mod script; +pub mod sighash; +pub mod signature; +pub mod signing_mode; +pub mod spending_data; +pub mod transaction; diff --git a/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs b/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs new file mode 100644 index 00000000000..f6fc0242ffb --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::transaction_interface::TransactionInterface; +use crate::transaction::transaction_parts::Amount; +use std::marker::PhantomData; +use tw_coin_entry::error::prelude::*; + +pub struct FeeEstimator { + _phantom: PhantomData, +} + +impl FeeEstimator { + pub fn estimate_fee(tx: &Transaction, fee_rate: Amount) -> SigningResult { + let vsize = tx.vsize(); + Amount::try_from(vsize) + .ok() + .and_then(|vsize| vsize.checked_mul(fee_rate)) + .or_tw_err(SigningErrorType::Error_wrong_fee) + .with_context(|| format!("feePerVByte is too large: '{vsize} * {fee_rate}' overflow")) + } +} diff --git a/rust/frameworks/tw_utxo/src/modules/keys_manager.rs b/rust/frameworks/tw_utxo/src/modules/keys_manager.rs new file mode 100644 index 00000000000..cb272d37529 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/keys_manager.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::sighash_computer::TaprootTweak; +use std::collections::HashMap; +use tw_coin_entry::error::prelude::*; +use tw_hash::H264; +use tw_keypair::{ecdsa, schnorr}; + +/// Standard Bitcoin keys manager. +/// Supports ecdsa and schnorr private keys. +#[derive(Default)] +pub struct KeysManager { + /// Ecdsa public to private keys. + ecdsa_public_private_map: HashMap, + /// Schnorr private keys. + schnorr_private_keys: Vec, +} + +impl KeysManager { + pub fn add_ecdsa_private(&mut self, private: ecdsa::secp256k1::PrivateKey) -> &mut Self { + self.ecdsa_public_private_map + .insert(private.public().compressed(), private); + self + } + + pub fn add_schnorr_private(&mut self, private: schnorr::PrivateKey) -> &mut Self { + self.schnorr_private_keys.push(private); + self + } + + pub fn get_ecdsa_private( + &self, + public: &ecdsa::secp256k1::PublicKey, + ) -> SigningResult<&ecdsa::secp256k1::PrivateKey> { + let pubkey_bytes = public.compressed(); + + self.ecdsa_public_private_map + .get(&pubkey_bytes) + .or_tw_err(SigningErrorType::Error_missing_private_key) + .with_context(|| format!("Cannot find a private key corresponding to the ecdsa public key: {pubkey_bytes}")) + } + + /// Gets a schnorr private key by an either tweaked or untweaked x-only public key. + /// The function iterates over the private keys, tweaks them if specified in `taproot_tweak`, + /// and returns `Ok(schnorr::PrivateKey)` if found. + pub fn get_schnorr_private( + &self, + public: &schnorr::XOnlyPublicKey, + taproot_tweak: &Option, + ) -> SigningResult { + let pubkey_bytes = public.bytes(); + + for private_key in self.schnorr_private_keys.iter() { + match taproot_tweak { + Some(ref tweak) => { + let tweaked_private = private_key.clone().tweak(tweak.merkle_root); + if tweaked_private.public().x_only().bytes() == pubkey_bytes { + return Ok(tweaked_private); + } + // Otherwise, continue searching for a private key. + }, + None => { + if private_key.public().x_only().bytes() == pubkey_bytes { + return Ok(private_key.clone()); + } + // Otherwise, continue searching for a private key. + }, + } + } + + SigningError::err(SigningErrorType::Error_missing_private_key) + .context(format!("Cannot find a private key corresponding to the x-only schnorr public key: {pubkey_bytes}")) + } +} diff --git a/rust/frameworks/tw_utxo/src/modules/mod.rs b/rust/frameworks/tw_utxo/src/modules/mod.rs new file mode 100644 index 00000000000..9d59f9124d1 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod fee_estimator; +pub mod keys_manager; +pub mod sighash_computer; +pub mod sighash_verifier; +pub mod tx_compiler; +pub mod tx_planner; +pub mod tx_signer; +pub mod utxo_selector; diff --git a/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs b/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs new file mode 100644 index 00000000000..0bcfb626960 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::script::Script; +use crate::signing_mode::SigningMethod; +use crate::transaction::transaction_interface::TransactionInterface; +use crate::transaction::transaction_parts::Amount; +use crate::transaction::unsigned_transaction::UnsignedTransaction; +use crate::transaction::{ + TransactionPreimage, UtxoPreimageArgs, UtxoTaprootPreimageArgs, UtxoToSign, +}; +use std::marker::PhantomData; +use tw_coin_entry::coin_entry::PublicKeyBytes; +use tw_coin_entry::error::prelude::SigningResult; +use tw_hash::H256; + +#[derive(Debug, Clone)] +pub struct TxPreimage { + /// Transaction signatures in the same order as the transaction UTXOs. + pub sighashes: Vec, +} + +#[derive(Debug, Clone)] +pub struct UtxoSighash { + /// The signing method needs to be used for this sighash. + pub signing_method: SigningMethod, + pub sighash: H256, + pub signer_pubkey: PublicKeyBytes, + /// Taproot tweak if [`SigningMethod::Taproot`] signing method is used. + /// Empty if there is no need to tweak the private to sign the sighash. + pub taproot_tweak: Option, +} + +#[derive(Debug, Clone)] +pub struct TaprootTweak { + /// 32 bytes merkle root of the script tree. + /// `None` if there are no scripts, and the private key should be tweaked without a merkle root. + pub merkle_root: Option, +} + +/// Sighash Computer with a standard Bitcoin behaviour. +/// +/// # Important +/// +/// If needed to implement a custom logic, consider adding a different Sighash Computer. +pub struct SighashComputer { + _phantom: PhantomData, +} + +impl SighashComputer +where + Transaction: TransactionPreimage + TransactionInterface, +{ + /// Computes sighashes of [`SighashComputer::transaction`]. + pub fn preimage_tx( + unsigned_tx: &UnsignedTransaction, + ) -> SigningResult { + unsigned_tx + .input_args() + .iter() + .enumerate() + .map(|(input_index, utxo)| { + let signing_method = utxo.signing_method; + + let utxo_args = UtxoPreimageArgs { + input_index, + script_pubkey: utxo.script_pubkey.clone(), + amount: utxo.amount, + // TODO move `leaf_hash_code_separator` to `UtxoTaprootPreimageArgs`. + leaf_hash_code_separator: utxo.leaf_hash_code_separator, + sighash_ty: utxo.sighash_ty, + tx_hasher: utxo.tx_hasher, + signing_method, + }; + + let (sighash, taproot_tweak) = match signing_method { + SigningMethod::Legacy | SigningMethod::Segwit => { + let sighash = unsigned_tx.transaction().preimage_tx(&utxo_args)?; + (sighash, None) + }, + SigningMethod::Taproot => { + // TODO Move `tr_spent_amounts` and `tr_spent_script_pubkeys` logic to `Transaction::preimage_taproot_tx()`. + let tr_spent_amounts: Vec = unsigned_tx + .input_args() + .iter() + .map(|utxo| utxo.amount) + .collect(); + + let tr_spent_script_pubkeys: Vec