diff --git a/.github/workflows/codegen-v2.yml b/.github/workflows/codegen-v2.yml index 8438d788d56..d7bde064e0d 100644 --- a/.github/workflows/codegen-v2.yml +++ b/.github/workflows/codegen-v2.yml @@ -21,7 +21,7 @@ jobs: tools/install-sys-dependencies-linux - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.8 - name: Install Rust dependencies run: | diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml new file mode 100644 index 00000000000..e044e2d7b77 --- /dev/null +++ b/.github/workflows/flutter-ci.yml @@ -0,0 +1,70 @@ +name: Flutter CI + +on: + push: + branches: [ dev, master ] + pull_request: + branches: [ dev, master ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-24.04 + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v3 + - 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 }}-${{ runner.arch }}-${{ hashFiles('tools/install-sys-dependencies-linux') }}-internal-${{ hashFiles('tools/install-dependencies') }}-${{ hashFiles('tools/dependencies-version') }} + - name: Install internal dependencies + run: | + tools/install-dependencies + env: + CC: /usr/bin/clang + CXX: /usr/bin/clang++ + if: steps.internal_cache.outputs.cache-hit != 'true' + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Code generation + run: | + tools/generate-files native + env: + CC: /usr/bin/clang + CXX: /usr/bin/clang++ + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: '3.8.1' + cache: true + cache-key: dart-3.8.1 + cache-path: ${{ github.workspace }}/.pub-cache + + - name: Install Dart dependencies + run: | + cd flutter + dart pub get + dart pub upgrade + + - name: Flutter build + run: | + tools/flutter-build + env: + CC: /usr/bin/clang + CXX: /usr/bin/clang++ + diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index ac5f55c258b..2a18b249f43 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: build: - runs-on: macos-latest-xlarge + runs-on: macos-15-xlarge if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/kotlin-sample-ci.yml b/.github/workflows/kotlin-sample-ci.yml index f96b2a4b280..aaa8070d8ce 100644 --- a/.github/workflows/kotlin-sample-ci.yml +++ b/.github/workflows/kotlin-sample-ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: build: - runs-on: macos-latest-xlarge + runs-on: macos-14-xlarge if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/linux-ci-sonarcloud.yml b/.github/workflows/linux-ci-sonarcloud.yml index 32522f7b7c6..f7e4643f343 100644 --- a/.github/workflows/linux-ci-sonarcloud.yml +++ b/.github/workflows/linux-ci-sonarcloud.yml @@ -12,7 +12,9 @@ concurrency: jobs: build: - if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false + # if: github.event.pull_request.draft == false + # Temporarily disabled due to issues with SonarCloud account. + if: false runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/linux-ci-rust.yml b/.github/workflows/rust.yml similarity index 58% rename from .github/workflows/linux-ci-rust.yml rename to .github/workflows/rust.yml index 377c60ee23d..cf0b7dfe462 100644 --- a/.github/workflows/linux-ci-rust.yml +++ b/.github/workflows/rust.yml @@ -1,4 +1,4 @@ -name: Linux CI Rust +name: Rust CI on: push: @@ -16,7 +16,7 @@ concurrency: jobs: # Check formatting, clippy warnings, run tests and check code coverage. - build-and-test: + rust-lints: permissions: contents: read checks: write @@ -29,7 +29,7 @@ jobs: tools/install-sys-dependencies-linux - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.8 - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -51,18 +51,6 @@ jobs: cargo clippy -- -D warnings working-directory: rust - - name: Run tests - run: | - tools/rust-coverage - - - name: Gather and check Rust code coverage - run: | - tools/check-coverage rust/coverage.stats rust/coverage.info - - - name: Run Doc tests - run: | - tools/rust-test doc - # Run Rust tests in WASM. test-wasm: runs-on: ubuntu-24.04 @@ -74,7 +62,7 @@ jobs: tools/install-sys-dependencies-linux - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.8 - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -105,7 +93,7 @@ jobs: tools/install-sys-dependencies-mac - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.8 - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -163,3 +151,113 @@ jobs: comment-author: 'github-actions[bot]' edit-mode: replace body-path: 'report-diff.md' + + memory-profiler: + runs-on: ubuntu-24.04 + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.8 + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install llvm + run: | + # to get the symbolizer for debug symbol resolution + sudo apt install llvm + + - name: Install nightly + uses: dtolnay/rust-toolchain@nightly + + - name: Enable debug symbols + run: | + cd rust + # to fix buggy leak analyzer: + # https://github.com/japaric/rust-san#unrealiable-leaksanitizer + # ensure there's a profile.dev section + if ! grep -qE '^[ \t]*[profile.dev]' Cargo.toml; then + echo >> Cargo.toml + echo '[profile.dev]' >> Cargo.toml + fi + # remove pre-existing opt-levels in profile.dev + sed -i '/^\s*\[profile.dev\]/,/^\s*\[/ {/^\s*opt-level/d}' Cargo.toml + # now set opt-level to 1 + sed -i '/^\s*\[profile.dev\]/a opt-level = 1' Cargo.toml + cat Cargo.toml + + - name: cargo test -Zsanitizer=address + # only --lib --tests b/c of https://github.com/rust-lang/rust/issues/53945 + run: | + cd rust + cargo test --lib --tests --all-features --target x86_64-unknown-linux-gnu + env: + ASAN_OPTIONS: "detect_odr_violation=0:detect_leaks=0" + RUSTFLAGS: "-Z sanitizer=address" + + - name: cargo test -Zsanitizer=leak + if: always() + run: | + cd rust + cargo test --all-features --target x86_64-unknown-linux-gnu + env: + RUSTFLAGS: "-Z sanitizer=leak" + + coverage: + runs-on: ubuntu-24.04 + 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.8 + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + rust + + - name: Install Rust dependencies + run: | + tools/install-rust-dependencies dev + + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: | + cd rust + cargo generate-lockfile + + - name: Run tests + run: | + tools/rust-coverage + + - name: Run Doc tests + run: | + tools/rust-test doc + + - name: Record Rust version + run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" + + # TODO: Uncomment this when we have a codecov token + # - name: Upload to codecov.io + # uses: codecov/codecov-action@v5 + # with: + # fail_ci_if_error: true + # token: ${{ secrets.CODECOV_TOKEN }} + # env_vars: OS,RUST + + - name: Gather and check Rust code coverage + run: | + tools/check-coverage rust/coverage.stats rust/lcov.info diff --git a/.gitignore b/.gitignore index 9c41a67c770..1a9598980ee 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ swift/Sources/Generated swift/wallet-core/ codegen-v2/bindings/ +src/Generated/*.h src/Generated/*.cpp include/TrustWalletCore/TWHRP.h include/TrustWalletCore/TW*Proto.h @@ -48,12 +49,20 @@ include/TrustWalletCore/TWTONMessageSigner.h include/TrustWalletCore/TWMessageSigner.h include/TrustWalletCore/TWWalletConnectRequest.h include/TrustWalletCore/TWSolanaTransaction.h +include/TrustWalletCore/TWCryptoBoxPublicKey.h +include/TrustWalletCore/TWCryptoBoxSecretKey.h +include/TrustWalletCore/TWEthereum.h +include/TrustWalletCore/TWBarz.h +include/TrustWalletCore/TWBiz.h +include/TrustWalletCore/TWEip7702.h +include/TrustWalletCore/TWWebAuthnSolidity.h # Wasm emsdk/ wasm-build/ # Code coverage files +lcov.info coverage.info coverage/ swift/test_output/ @@ -77,3 +86,6 @@ samples/cpp/sample # Rust target build **/target/ + +# Kotlin +kotlin/.kotlin diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fec5513326..34a75490016 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,11 @@ elseif (${TW_COMPILE_JAVA}) find_package(JNI REQUIRED) target_include_directories(TrustWalletCore PRIVATE ${JNI_INCLUDE_DIRS}) target_link_libraries(TrustWalletCore PUBLIC ${WALLET_CORE_BINDGEN} ${PROJECT_NAME}_INTERFACE PRIVATE TrezorCrypto protobuf Boost::boost) +elseif (${FLUTTER}) + message("Configuring for Flutter") + file(GLOB_RECURSE sources src/*.c src/*.cc src/*.cpp src/*.h) + add_library(TrustWalletCore SHARED ${sources} ${PROTO_SRCS} ${PROTO_HDRS}) + target_link_libraries(TrustWalletCore PUBLIC ${WALLET_CORE_BINDGEN} ${PROJECT_NAME}_INTERFACE PRIVATE TrezorCrypto protobuf Boost::boost) else () message("Configuring standalone") file(GLOB_RECURSE sources src/*.c src/*.cc src/*.cpp src/*.h) diff --git a/Dockerfile b/Dockerfile index 082d5fd1969..8111b5813ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,16 @@ RUN apt-get update \ ENV CC=/usr/bin/clang-14 ENV CXX=/usr/bin/clang++-14 +# CMake will not use the ENV if CMAKE_C_COMPILER is explicitly defined +RUN ln -s /usr/bin/clang-14 /usr/bin/clang +RUN ln -s /usr/bin/clang++-14 /usr/bin/clang++ + # Install rust 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 \ +RUN cargo install --force cbindgen --locked \ && rustup target add wasm32-unknown-emscripten # ↑ Setup build environment diff --git a/Package.swift b/Package.swift index 16f845ca81f..3278a4e87dd 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ let package = Package( platforms: [.iOS(.v13)], products: [ .library(name: "WalletCore", targets: ["WalletCore"]), - .library(name: "SwiftProtobuf", targets: ["SwiftProtobuf"]) + .library(name: "WalletCoreSwiftProtobuf", targets: ["WalletCoreSwiftProtobuf"]) ], dependencies: [], targets: [ @@ -16,7 +16,7 @@ let package = Package( checksum: "651894a9418fdd33ae5374367a6a64a57fa92b6e6ffb2d6723c319da97472cb4" ), .binaryTarget( - name: "SwiftProtobuf", + name: "WalletCoreSwiftProtobuf", url: "https://github.com/trustwallet/wallet-core/releases/download/4.2.9/SwiftProtobuf.xcframework.zip", checksum: "946efd4b0132b92208335902e0b65e0aba2d11b9dd6f6d79cc8318e2530c9ae0" ) diff --git a/README.md b/README.md index 7456046acbd..06ce58f529f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Then add libraries to target's `dependencies`: ```swift .product(name: "WalletCore", package: "WalletCore"), -.product(name: "SwiftProtobuf", package: "WalletCore"), +.product(name: "WalletCoreSwiftProtobuf", package: "WalletCore"), ``` ### CocoaPods diff --git a/WalletCore.podspec b/WalletCore.podspec index 42fe7431537..ee960c3a1eb 100644 --- a/WalletCore.podspec +++ b/WalletCore.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| end s.subspec 'Core' do |ss| - protobuf_source_dir = 'build/local/src/protobuf/protobuf-3.19.2' + protobuf_source_dir = 'build/local/src/protobuf/protobuf-3.20.3' include_dir = 'build/local/include' ss.source_files = 'src/**/*.{c,cc,cpp,h}', @@ -45,6 +45,7 @@ Pod::Spec.new do |s| "#{protobuf_source_dir}/src/google/protobuf/api.pb.cc", "#{protobuf_source_dir}/src/google/protobuf/arena.cc", "#{protobuf_source_dir}/src/google/protobuf/arenastring.cc", + "#{protobuf_source_dir}/src/google/protobuf/arenaz_sampler.cc", "#{protobuf_source_dir}/src/google/protobuf/compiler/importer.cc", "#{protobuf_source_dir}/src/google/protobuf/compiler/parser.cc", "#{protobuf_source_dir}/src/google/protobuf/descriptor.cc", @@ -59,8 +60,6 @@ Pod::Spec.new do |s| "#{protobuf_source_dir}/src/google/protobuf/generated_enum_util.cc", "#{protobuf_source_dir}/src/google/protobuf/generated_message_bases.cc", "#{protobuf_source_dir}/src/google/protobuf/generated_message_reflection.cc", - "#{protobuf_source_dir}/src/google/protobuf/generated_message_table_driven.cc", - "#{protobuf_source_dir}/src/google/protobuf/generated_message_table_driven_lite.cc", "#{protobuf_source_dir}/src/google/protobuf/generated_message_tctable_full.cc", "#{protobuf_source_dir}/src/google/protobuf/generated_message_tctable_lite.cc", "#{protobuf_source_dir}/src/google/protobuf/generated_message_util.cc", diff --git a/WalletCoreSwiftProtobuf.podspec b/WalletCoreSwiftProtobuf.podspec new file mode 100644 index 00000000000..135ef9092be --- /dev/null +++ b/WalletCoreSwiftProtobuf.podspec @@ -0,0 +1,22 @@ +# Original podspec: +# https://github.com/apple/swift-protobuf/blob/main/SwiftProtobuf.podspec +Pod::Spec.new do |s| + s.name = 'WalletCoreSwiftProtobuf' + s.version = '1.29.0' + s.license = { :type => 'Apache 2.0', :file => 'LICENSE.txt' } + s.summary = 'Swift Protobuf Runtime Library' + s.homepage = 'https://github.com/apple/swift-protobuf' + s.author = 'Apple Inc.' + s.source = { :git => 'https://github.com/apple/swift-protobuf.git', :tag => s.version } + + s.requires_arc = true + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.13' + + s.cocoapods_version = '>= 1.13.0' + + s.source_files = 'Sources/SwiftProtobuf/**/*.swift' + s.resource_bundle = {'WalletCoreSwiftProtobuf' => ['Sources/SwiftProtobuf/PrivacyInfo.xcprivacy']} + + s.swift_versions = ['5.0'] +end 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 94adc9f0ad5..b1b407dfc87 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 @@ -47,7 +47,7 @@ class CoinAddressDerivationTests { FANTOM, CELO, CRONOSCHAIN, SMARTBITCOINCASH, KUCOINCOMMUNITYCHAIN, BOBA, METIS, 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, SONIC, + ZETAEVM, MERLIN, LIGHTLINK, BLAST, BOUNCEBIT, ZKLINKNOVA, SONIC, PLASMA, MONAD, -> assertEquals("0x8f348F300873Fd5DA36950B2aC75a26584584feE", address) RONIN -> assertEquals("ronin:8f348F300873Fd5DA36950B2aC75a26584584feE", address) @@ -102,6 +102,7 @@ class CoinAddressDerivationTests { ACALA -> assertEquals("25GGezx3LWFQj6HZpYzoWoVzLsHojGtybef3vthC9nd19ms3", address) KUSAMA -> assertEquals("G9xV2EatmrjRC1FLPexc3ddqNRRzCsAdURU8RFiAAJX6ppY", address) POLKADOT -> assertEquals("13nN6BGAoJwd7Nw1XxeBCx5YcBXuYnL94Mh7i3xBprqVSsFk", address) + POLYMESH -> assertEquals("2DHK8VhBpacs9quk78AVP9TmmcG5iXi2oKtZqneSNsVXxCKw", address) PIVX -> assertEquals("D81AqC8zKma3Cht4TbVuh4jyVVyLkZULCm", address) KAVA -> assertEquals("kava1drpa0x9ptz0fql3frv562rcrhj2nstuz3pas87", address) CARDANO -> assertEquals("addr1qyr8jjfnypp95eq74aqzn7ss687ehxclgj7mu6gratmg3mul2040vt35dypp042awzsjk5xm3zr3zm5qh7454uwdv08s84ray2", 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 abbc87b7c2f..98f46092a88 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 @@ -36,20 +36,23 @@ class TestCoinType { assertEquals(CoinType.TEZOS.value(), 1729) assertEquals(CoinType.QTUM.value(), 2301) assertEquals(CoinType.NEBULAS.value(), 2718) + assertEquals(CoinType.PACTUS.value(), 21888) } @Test fun testCoinPurpose() { assertEquals(Purpose.BIP84, CoinType.BITCOIN.purpose()) + assertEquals(Purpose.BIP44, CoinType.PACTUS.purpose()) } @Test fun testCoinCurve() { assertEquals(Curve.SECP256K1, CoinType.BITCOIN.curve()) + assertEquals(Curve.ED25519, CoinType.PACTUS.curve()) } @Test - fun testDerivationPath() { + fun testDerivationPathBitcoin() { var res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPath().toString() assertEquals(res, "m/84'/0'/0'/0/0") res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINLEGACY).toString() @@ -61,10 +64,31 @@ class TestCoinType { } @Test - fun testDeriveAddressFromPublicKeyAndDerivation() { + fun testDeriveAddressFromPublicKeyAndDerivationBitcoin() { val publicKey = PublicKey("0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798".toHexByteArray(), PublicKeyType.SECP256K1) val address = CoinType.BITCOIN.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.BITCOINSEGWIT) assertEquals(address, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") } + + @Test + fun testDerivationPathPactus() { + var res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPath().toString() + assertEquals(res, "m/44'/21888'/3'/0'") + res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPathWithDerivation(Derivation.PACTUSMAINNET).toString() + assertEquals(res, "m/44'/21888'/3'/0'") + res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPathWithDerivation(Derivation.PACTUSTESTNET).toString() + assertEquals(res, "m/44'/21777'/3'/0'") + } + + @Test + fun testDeriveAddressFromPublicKeyAndDerivationPactus() { + val publicKey = PublicKey("95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa".toHexByteArray(), PublicKeyType.ED25519) + + val mainnet_address = CoinType.PACTUS.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.PACTUSMAINNET) + assertEquals(mainnet_address, "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr") + + val testnet_address = CoinType.PACTUS.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.PACTUSTESTNET) + assertEquals(testnet_address, "tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg") + } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBarz.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBarz.kt index 235cd442b73..47097ce42dd 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBarz.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBarz.kt @@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals import org.junit.Test import wallet.core.jni.* import wallet.core.java.AnySigner -import wallet.core.jni.CoinType import wallet.core.jni.CoinType.ETHEREUM import wallet.core.jni.proto.Ethereum import wallet.core.jni.EthereumAbi @@ -17,7 +16,6 @@ import wallet.core.jni.PrivateKey import wallet.core.jni.PublicKey import wallet.core.jni.PublicKeyType import com.trustwallet.core.app.utils.Numeric -import org.junit.Assert.assertArrayEquals import wallet.core.jni.proto.Barz import wallet.core.jni.Barz as WCBarz @@ -117,8 +115,12 @@ class TestBarz { }.build() transaction = Ethereum.Transaction.newBuilder().apply { - transfer = Ethereum.Transaction.Transfer.newBuilder().apply { - amount = ByteString.copyFrom("0x2386f26fc10000".toHexByteArray()) + scwExecute = Ethereum.Transaction.SCWalletExecute.newBuilder().apply { + transaction = Ethereum.Transaction.newBuilder().apply { + transfer = Ethereum.Transaction.Transfer.newBuilder().apply { + amount = ByteString.copyFrom("0x2386f26fc10000".toHexByteArray()) + }.build() + }.build() }.build() }.build() } @@ -159,8 +161,12 @@ class TestBarz { }.build() transaction = Ethereum.Transaction.newBuilder().apply { - transfer = Ethereum.Transaction.Transfer.newBuilder().apply { - amount = ByteString.copyFrom("0x2386f26fc10000".toHexByteArray()) + scwExecute = Ethereum.Transaction.SCWalletExecute.newBuilder().apply { + transaction = Ethereum.Transaction.newBuilder().apply { + transfer = Ethereum.Transaction.Transfer.newBuilder().apply { + amount = ByteString.copyFrom("0x2386f26fc10000".toHexByteArray()) + }.build() + }.build() }.build() }.build() } @@ -204,14 +210,14 @@ class TestBarz { }.build() transaction = Ethereum.Transaction.newBuilder().apply { - batch = Ethereum.Transaction.Batch.newBuilder().apply { + scwBatch = Ethereum.Transaction.SCWalletBatch.newBuilder().apply { addAllCalls(listOf( - Ethereum.Transaction.Batch.BatchedCall.newBuilder().apply { + Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { address = "0x03bBb5660B8687C2aa453A0e42dCb6e0732b1266" amount = ByteString.copyFrom("0x00".toHexByteArray()) payload = ByteString.copyFrom(approveCall) }.build(), - Ethereum.Transaction.Batch.BatchedCall.newBuilder().apply { + Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { address = "0x03bBb5660B8687C2aa453A0e42dCb6e0732b1266" amount = ByteString.copyFrom("0x00".toHexByteArray()) payload = ByteString.copyFrom(transferCall) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBiz.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBiz.kt new file mode 100644 index 00000000000..0966dc81d71 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBiz.kt @@ -0,0 +1,365 @@ +package com.trustwallet.core.app.blockchains.ethereum + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +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.* +import wallet.core.jni.CoinType.ETHEREUM +import wallet.core.jni.proto.Ethereum +import wallet.core.jni.proto.Ethereum.SigningOutput +import wallet.core.jni.proto.Ethereum.TransactionMode +import wallet.core.jni.Biz as WCBiz + +class TestBiz { + + init { + System.loadLibrary("TrustWalletCore") + } + + // https://bscscan.com/tx/0x425eb17a8e1dee2fcee8352a772d83cbb069c2e03f2c5d9d00da3b3ef66ce48b + @Test + fun testSignEip7702EoaBatched() { + val transferFunc1 = EthereumAbiFunction("transfer") + transferFunc1.addParamAddress("0x2EF648D7C03412B832726fd4683E2625deA047Ba".toHexByteArray(), false) + // 100_000_000_000_000 + transferFunc1.addParamUInt256("0x5af3107a4000".toHexByteArray(), false) + val transferPayload1 = EthereumAbi.encode(transferFunc1) + + val transferFunc2 = EthereumAbiFunction("transfer") + transferFunc2.addParamAddress("0x95dc01ebd10b6dccf1cc329af1a3f73806117c2e".toHexByteArray(), false) + // 500_000_000_000_000 + transferFunc2.addParamUInt256("0x1c6bf52634000".toHexByteArray(), false) + val transferPayload2 = EthereumAbi.encode(transferFunc2) + + val signingInput = Ethereum.SigningInput.newBuilder() + signingInput.apply { + privateKey = ByteString.copyFrom(PrivateKey("0xe148e40f06ee3ba316cdb2571f33486cf879c0ffd2b279ce9f9a88c41ce962e7".toHexByteArray()).data()) + chainId = ByteString.copyFrom("0x38".toHexByteArray()) + nonce = ByteString.copyFrom("0x12".toHexByteArray()) + txMode = TransactionMode.SetCode + + gasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) + maxFeePerGas = ByteString.copyFrom("0x3b9aca00".toHexByteArray()) + maxInclusionFeePerGas = ByteString.copyFrom("0x3b9aca00".toHexByteArray()) + + transaction = Ethereum.Transaction.newBuilder().apply { + scwBatch = Ethereum.Transaction.SCWalletBatch.newBuilder().apply { + walletType = Ethereum.SCWalletType.Biz + addAllCalls(listOf( + Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { + // TWT + address = "0x4B0F1812e5Df2A09796481Ff14017e6005508003" + amount = ByteString.copyFrom("0x00".toHexByteArray()) + payload = ByteString.copyFrom(transferPayload1) + }.build(), + Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { + // TWT + address = "0x4B0F1812e5Df2A09796481Ff14017e6005508003" + amount = ByteString.copyFrom("0x00".toHexByteArray()) + payload = ByteString.copyFrom(transferPayload2) + }.build() + )) + }.build() + }.build() + + eip7702Authorization = Ethereum.Authorization.newBuilder().apply { + address = "0x117BC8454756456A0f83dbd130Bb94D793D3F3F7" + }.build() + } + + val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser()) + + assertEquals(Numeric.toHexString(output.preHash.toByteArray()), "0x00b2d13719df301927ddcbdad5b6bc6214f2007c6408df883c9ea483b45e6f44") + assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x04f9030f3812843b9aca00843b9aca00830186a0945132829820b44dc3e8586cec926a16fca0a5608480b9024434fcd5be00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000004b0f1812e5df2a09796481ff14017e6005508003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000002ef648d7c03412b832726fd4683e2625dea047ba00000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000004b0f1812e5df2a09796481ff14017e6005508003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000095dc01ebd10b6dccf1cc329af1a3f73806117c2e0000000000000000000000000000000000000000000000000001c6bf5263400000000000000000000000000000000000000000000000000000000000c0f85cf85a3894117bc8454756456a0f83dbd130bb94d793d3f3f71380a0073afc661c158a2dccf4183f87e1e4d62b4d406af418cfd69959368ec9bec2a6a064292fd61d4d16b840470a86fc4f7a89413f9126d897f2268eb76a1d887c6d7a01a0e8bcbd96323c9d3e67b74366b2f43299100996d9e8874a6fd87186ac8f580d4ca07c25b4f0619af77fb953e8f0e4372bfbee62616ad419697516108eeb9bcebb28") + } + + // https://bscscan.com/tx/0x6f8b2c8d50e8bb543d7124703b75d9e495832116a1a61afabf40b9b0ac43c980 + @Test + fun testSignEnvelopedBiz() { + val signingInput = Ethereum.SigningInput.newBuilder() + signingInput.apply { + privateKey = ByteString.copyFrom(PrivateKey("0xe762e91cc4889a9fce79b2d2ffc079f86c48331f57b2cd16a33bee060fe448e1".toHexByteArray()).data()) + chainId = ByteString.copyFrom("0x38".toHexByteArray()) + nonce = ByteString.copyFrom("0x02".toHexByteArray()) + txMode = TransactionMode.Enveloped + + gasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) + maxFeePerGas = ByteString.copyFrom("0x3b9aca00".toHexByteArray()) + maxInclusionFeePerGas = ByteString.copyFrom("0x3b9aca00".toHexByteArray()) + + transaction = Ethereum.Transaction.newBuilder().apply { + scwExecute = Ethereum.Transaction.SCWalletExecute.newBuilder().apply { + walletType = Ethereum.SCWalletType.Biz + transaction = Ethereum.Transaction.newBuilder().apply { + erc20Transfer = Ethereum.Transaction.ERC20Transfer.newBuilder().apply { + to = "0x95dc01ebd10b6dccf1cc329af1a3f73806117c2e" + amount = ByteString.copyFrom("0xb5e620f48000".toHexByteArray()) + }.build() + }.build() + }.build() + }.build() + + // TWT token. + toAddress = "0x4B0F1812e5Df2A09796481Ff14017e6005508003" + } + + val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser()) + + assertEquals(Numeric.toHexString(output.preHash.toByteArray()), "0x60260356568ae70838bd80085b971e1e4ebe42046688fd8511a268986e522121") + assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02f901503802843b9aca00843b9aca00830186a0946e860086bba8fdeafb553815af0f09a854cc887a80b8e4b61d27f60000000000000000000000004b0f1812e5df2a09796481ff14017e6005508003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000095dc01ebd10b6dccf1cc329af1a3f73806117c2e0000000000000000000000000000000000000000000000000000b5e620f4800000000000000000000000000000000000000000000000000000000000c080a0fb45762a262f4c32090576e9de087482d25cd00b6ea2522eb7d5a40f435acdbaa0151dbd48a4f4bf06080313775fe32ececd68869d721518a92bf292e4a84322f9") + } + + @Test + fun testBizTransferAccountDeployedV07() { + val chainIdByteArray = "0x7A69".toHexByteArray() // 31337 + val wallet = "0x174a240e5147D02dE4d7724D5D3E1c1bF11cE029" + + val transfer = Ethereum.Transaction.Transfer.newBuilder().apply { + amount = ByteString.copyFrom("0x2386f26fc10000".toHexByteArray()) + data = ByteString.EMPTY + }.build() + + val userOpV07 = Ethereum.UserOperationV0_7.newBuilder().apply { + entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + sender = wallet + preVerificationGas = ByteString.copyFrom("0xF4240".toHexByteArray()) // 1000000 + verificationGasLimit = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000 + factory = "0xf471789937856d80e589f5996cf8b0511ddd9de4" + factoryData = ByteString.copyFrom("0xf471789937856d80e589f5996cf8b0511ddd9de4".toHexByteArray()) + paymaster = "0xf62849f9a0b5bf2913b396098f7c7019b51a820a" + paymasterVerificationGasLimit = ByteString.copyFrom("0x1869F".toHexByteArray()) // 99999 + paymasterPostOpGasLimit = ByteString.copyFrom("0x15B38".toHexByteArray()) // 88888 + paymasterData = ByteString.copyFrom( + "0x00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b" + .toHexByteArray() + ) + }.build() + + // Create signing input + val signingInput = Ethereum.SigningInput.newBuilder().apply { + privateKey = ByteString.copyFrom(PrivateKey("3c90badc15c4d35733769093d3733501e92e7f16e101df284cee9a310d36c483".toHexByteArray()).data()) + chainId = ByteString.copyFrom(chainIdByteArray) // 31337 + nonce = ByteString.copyFrom("0x00".toHexByteArray()) + txMode = Ethereum.TransactionMode.UserOp + gasLimit = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000 + maxFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000 + maxInclusionFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) + toAddress = "0x61061fCAE11fD5461535e134EfF67A98CFFF44E9" + + transaction = Ethereum.Transaction.newBuilder().apply { + scwExecute = Ethereum.Transaction.SCWalletExecute.newBuilder().apply { + transaction = Ethereum.Transaction.newBuilder().apply { + this.transfer = transfer + }.build() + }.build() + }.build() + + userOperationV07 = userOpV07 + }.build() + + val output = AnySigner.sign(signingInput, CoinType.ETHEREUM, Ethereum.SigningOutput.parser()) + + assertEquals( + "0xf177858c1c500e51f38ffe937bed7e4d3a8678725900be4682d3ce04d97071eb", + Numeric.toHexString(output.preHash.toByteArray()) + ) + + val codeAddress = "0x2e234DAe75C793f67A35089C9d99245E1C58470b" + val codeName = "Biz" + val codeVersion = "v1.0.0" + val typeHash = "0x4f51e7a567f083a31264743067875fc6a7ae45c32c5bd71f6a998c4625b13867" + val domainSeparatorHash = "0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472" + val hash = "0xf177858c1c500e51f38ffe937bed7e4d3a8678725900be4682d3ce04d97071eb" + + val encodedHash = WCBiz.getEncodedHash( + chainIdByteArray, + codeAddress, + codeName, + codeVersion, + typeHash, + domainSeparatorHash, + wallet, + hash + ) + assertEquals( + "0xc63891abc38f7a991f89ad7cb6d7e53543627b0536c3f5e545b736756c971635", + Numeric.toHexString(encodedHash) + ) + + val privateKey = "0x947dd69af402e7f48da1b845dfc1df6be593d01a0d8274bd03ec56712e7164e8" + val signedHash = WCBiz.getSignedHash( + "0xc63891abc38f7a991f89ad7cb6d7e53543627b0536c3f5e545b736756c971635", + privateKey + ) + assertEquals( + "0xa29e460720e4b539f593d1a407827d9608cccc2c18b7af7b3689094dca8a016755bca072ffe39bc62285b65aff8f271f20798a421acf18bb2a7be8dbe0eb05f81c", + Numeric.toHexString(signedHash) + ) + } + + @Test + fun testBizEncodeRegisterSessionCall() { + val publicKey = PublicKey( + "0x041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d".toHexByteArray(), + PublicKeyType.NIST256P1EXTENDED + ) + val validUntil = "0x15181" // 86_401 + val encoded = WCBiz.encodeRegisterSessionCall(publicKey, Numeric.hexStringToByteArray(validUntil)) + assertEquals( + "0x826491fb000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000151810000000000000000000000000000000000000000000000000000000000000041041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d00000000000000000000000000000000000000000000000000000000000000", + Numeric.toHexString(encoded) + ) + } + + @Test + fun testBizEncodeRemoveSessionCall() { + val publicKey = PublicKey( + "0x041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d".toHexByteArray(), + PublicKeyType.NIST256P1EXTENDED + ) + val encoded = WCBiz.encodeRemoveSessionCall(publicKey) + assertEquals( + "0xe1c06abd00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000041041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d00000000000000000000000000000000000000000000000000000000000000", + Numeric.toHexString(encoded) + ) + } + + @Test + fun testBizEncodePasskeyNonce() { + val nonce = "0x7b" // 123 + val passkeyNonce = WCBiz.encodePasskeySessionNonce(Numeric.hexStringToByteArray(nonce)) + assertEquals( + "0x00000000000000000000000000000000050041d6a66939a8000000000000007b", + Numeric.toHexString(passkeyNonce) + ) + } + + @Test + fun testSignUserOperationV7WithPasskeySession() { + val chainIdByteArray = "0x7A69".toHexByteArray() // 31337 + val wallet = "0x336Cd992a83242D91f556C1F7e855AcA366193e0" + val bizPasskeySessionAccount = "0xa0Cb889707d426A7A386870A03bc70d1b0697598" + val bizPasskeySessionCodeName = "PasskeySession" + val codeVersion = "v1.0.0" + // keccak("PasskeySession(bytes32 userOpHash)") + val typeHash = "0x3463fe66e4d03af5b942aebde2b191eff52d291c0a2c8cc302d786854f34bfc9" + // keccak("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") + val domainSeparatorHash = "0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472" + + // Step 1. Generate UserOperation and its hash. + + val batch = Ethereum.Transaction.SCWalletBatch.newBuilder() + .setWalletType(Ethereum.SCWalletType.Biz4337) + .addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { + address = "0x0000000000000000000000000000000000000001" + amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray()) + payload = ByteString.EMPTY + }) + .addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { + address = "0x0000000000000000000000000000000000000002" + amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray()) + payload = ByteString.EMPTY + }) + .addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply { + address = "0x0000000000000000000000000000000000000003" + amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray()) + payload = ByteString.EMPTY + }) + .build() + + val actualNonce = Numeric.hexStringToByteArray("0x01") + val passkeyNonce = WCBiz.encodePasskeySessionNonce(actualNonce) + + val userOpV07 = Ethereum.UserOperationV0_7.newBuilder().apply { + entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + sender = wallet + preVerificationGas = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000 + verificationGasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000 + paymaster = "0xf62849f9a0b5bf2913b396098f7c7019b51a820a" + paymasterVerificationGasLimit = ByteString.copyFrom("0x1869F".toHexByteArray()) // 99999 + paymasterPostOpGasLimit = ByteString.copyFrom("0x15B38".toHexByteArray()) // 88888 + paymasterData = ByteString.copyFrom( + "0x00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b" + .toHexByteArray() + ) + }.build() + + // Create signing input + val signingInput = Ethereum.SigningInput.newBuilder().apply { + // Dummy private key. + privateKey = ByteString.copyFrom("0x03d99692017473e2d631945a812607b23269d85721e0f370b8d3e7d29a874fd2".toHexByteArray()) + chainId = ByteString.copyFrom(chainIdByteArray) // 31337 + nonce = ByteString.copyFrom(passkeyNonce) + txMode = Ethereum.TransactionMode.UserOp + gasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000 + maxFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000 + maxInclusionFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000 + toAddress = "0x61061fCAE11fD5461535e134EfF67A98CFFF44E9" + + transaction = Ethereum.Transaction.newBuilder().apply { + scwBatch = batch + }.build() + + userOperationV07 = userOpV07 + }.build() + + val output = AnySigner.sign(signingInput, CoinType.ETHEREUM, Ethereum.SigningOutput.parser()) + + val userOpHash = output.preHash.toByteArray() + assertEquals( + "0x7762e85586107f2bca787a9163b71f0584eabd84258a93cca0e896589a193571", + Numeric.toHexString(userOpHash) + ) + + // Step 2. Encode UserOperation hash in Biz format. + + val encodedUserOpHash = WCBiz.getEncodedHash( + chainIdByteArray, + bizPasskeySessionAccount, + bizPasskeySessionCodeName, + codeVersion, + typeHash, + domainSeparatorHash, + wallet, + Numeric.toHexString(userOpHash) + ) + assertEquals( + "0x7d130331f16bb3d2bc3d72db02879d0745d4452592e56723de8b27cf1ee006c7", + Numeric.toHexString(encodedUserOpHash) + ) + + // Step 3. Generate a WebAuthn with the given challenge (encodedUserOpHash) and passkey signature. + + val clientJsonFirstPart = "{\"type\":\"webauthn.get\",\"challenge\":\"" + val challengeBase64 = Base64.encodeUrl(encodedUserOpHash) + val challengeBase64NoPad = challengeBase64.trimEnd('=') + val clientJsonLastPart = "\",\"origin\":\"https://sign.coinbase.com/",\"crossOrigin\":false}" + val clientJson = clientJsonFirstPart + challengeBase64NoPad + clientJsonLastPart + assertEquals(clientJson, "{\"type\":\"webauthn.get\",\"challenge\":\"fRMDMfFrs9K8PXLbAoedB0XURSWS5Wcj3osnzx7gBsc\",\"origin\":\"https://sign.coinbase.com/",\"crossOrigin\":false}") + + // Authenticator data for Chrome Profile touchID signature + val authenticatorData = "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000" + // Signature computed on a device using passkey. + val derSignature = "0x3045022073f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be022100e091f452b74519a2894a96d142bdd1888ac6513f5dff76e48c0312144ef9b382".toHexByteArray() + + val passkeySignature = WebAuthnSolidity.getFormattedSignature(authenticatorData, clientJson, derSignature) + assertEquals( + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000017000000000000000000000000000000000000000000000000000000000000000173f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be1f6e0bac48bae65e76b5692ebd422e773220a96e491827a067b6b8aead6971cf000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2266524d444d66467273394b3850584c62416f656442305855525357533557636a336f736e7a783767427363222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000", + Numeric.toHexString(passkeySignature) + ) + + // Step 4. Final step. Biz-specific adjustments. + + // `passkeyIndex` can be gotten by using `Biz.getPasskeySessionIndexForValidation()` view function. + // https://github.com/trustwallet/7702-passkey-session/blob/b5c85a5c9a72c19195d1d60a1c90b3908a6a0371/src/BizPasskeySession.sol#L412 + val passkeyIndex: Byte = 0x00 + val passkeyIndexAttachedSignature = byteArrayOf(passkeyIndex) + passkeySignature + assertEquals( + "0x00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000017000000000000000000000000000000000000000000000000000000000000000173f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be1f6e0bac48bae65e76b5692ebd422e773220a96e491827a067b6b8aead6971cf000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2266524d444d66467273394b3850584c62416f656442305855525357533557636a336f736e7a783767427363222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000", + Numeric.toHexString(passkeyIndexAttachedSignature) + ) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEip7702.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEip7702.kt new file mode 100644 index 00000000000..7f9bb514b3f --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEip7702.kt @@ -0,0 +1,43 @@ +package com.trustwallet.core.app.blockchains.ethereum + +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import com.trustwallet.core.app.utils.Numeric +import wallet.core.jni.Eip7702 + +class TestEip7702 { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAuthorizationHash() { + val chainId = "0x01".toHexByteArray() + val contractAddress = "0xB91aaa96B138A1B1D94c9df4628187132c5F2bf1" + val nonce = "0x01".toHexByteArray() + + val authorizationHash = Eip7702.getAuthorizationHash(chainId, contractAddress, nonce) + assertEquals(Numeric.toHexString(authorizationHash), "0x3ae543b2fa103a39a6985d964a67caed05f6b9bb2430ad6d498cda743fe911d9") // Verified with viem + } + + @Test + fun testSignAuthorization() { + val chainId = "0x01".toHexByteArray() + val contractAddress = "0xB91aaa96B138A1B1D94c9df4628187132c5F2bf1" + val nonce = "0x01".toHexByteArray() + val privateKey = "0x947dd69af402e7f48da1b845dfc1df6be593d01a0d8274bd03ec56712e7164e8" + + val signedAuthorization = Eip7702.signAuthorization(chainId, contractAddress, nonce, privateKey) + val json = org.json.JSONObject(signedAuthorization) + + // Verified with viem + assertEquals(Numeric.toHexString(chainId), json.getString("chainId")) + assertEquals(contractAddress, json.getString("address")) + assertEquals(Numeric.toHexString(nonce), json.getString("nonce")) + assertEquals("0x01", json.getString("yParity")) + assertEquals("0x2c39f2f64441dd38c364ee175dc6b9a87f34ca330bce968f6c8e22811e3bb710", json.getString("r")) + assertEquals("0x5f1bcde93dee4b214e60bc0e63babcc13e4fecb8a23c4098fd89844762aa012c", json.getString("s")) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt index b58b068b859..96e7217fc70 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt @@ -17,7 +17,7 @@ class TestPactusAddress { } @Test - fun testAddress() { + fun testMainnetAddress() { val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray()) val pubkey = key.publicKeyEd25519 val address = AnyAddress(pubkey, CoinType.PACTUS) @@ -26,4 +26,15 @@ class TestPactusAddress { assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa") assertEquals(address.description(), expected.description()) } + + @Test + fun testTestnetAddress() { + val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray()) + val pubkey = key.publicKeyEd25519 + val address = AnyAddress(pubkey, CoinType.PACTUS, Derivation.PACTUSTESTNET) + val expected = AnyAddress("tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg", CoinType.PACTUS) + + assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa") + assertEquals(address.description(), expected.description()) + } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt new file mode 100644 index 00000000000..a7e77d9a3e0 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.polymesh + +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 TestPolymeshAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + + val key = PrivateKey("0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4".toHexByteArray()) + val pubkey = key.publicKeyEd25519 + val address = AnyAddress(pubkey, CoinType.POLYMESH) + val expected = AnyAddress("2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys", CoinType.POLYMESH) + + assertEquals(pubkey.data().toHex(), "0x4bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c") + assertEquals(address.description(), expected.description()) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt new file mode 100644 index 00000000000..116c5b76d40 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.polymesh + +import com.trustwallet.core.app.utils.Numeric +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.CoinType.POLYMESH +import wallet.core.jni.proto.Polkadot +import wallet.core.jni.proto.Polymesh +import wallet.core.jni.proto.Polymesh.SigningOutput + +class TestPolymeshSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + val genesisHashStr = "0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063".toHexBytesInByteString() + // Private key for testing. DO NOT USE, since this is public. + val TestKey1 = "0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4".toHexBytesInByteString() + + @Test + fun PolymeshTransactionSigning() { + // https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04 + + // Step 1: Prepare input. + val blockHashStr = "77d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d".toHexBytesInByteString() + + var call = Polymesh.Balance.Transfer.newBuilder().apply { + toAddress = "2CpqFh8VnwJAjenw4xSUWCaaJ2QwGdhnCikoSEczMhjgqyj7" + value = "0x0F4240".toHexBytesInByteString() + } + + val input = Polymesh.SigningInput.newBuilder().apply { + genesisHash = genesisHashStr + blockHash = blockHashStr + era = Polkadot.Era.newBuilder().apply { + blockNumber = 16_102_106 + period = 64 + }.build() + network = POLYMESH.ss58Prefix() + nonce = 1 + specVersion = 7000005 + transactionVersion = 7 + privateKey = TestKey1 + runtimeCall = Polymesh.RuntimeCall.newBuilder().apply { + balanceCall = Polymesh.Balance.newBuilder().apply { + transfer = call.build() + }.build() + }.build() + } + + val output = AnySigner.sign(input.build(), POLYMESH, SigningOutput.parser()) + val encoded = Numeric.toHexString(output.encoded.toByteArray()) + + val expected = "0x390284004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210ba501040005000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00" + assertEquals(encoded, expected) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tezos/TestTezosSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tezos/TestTezosSigner.kt index f14a6a6c689..3b31512c560 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tezos/TestTezosSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tezos/TestTezosSigner.kt @@ -161,7 +161,7 @@ class TestTezosTransactionSigner { assertTrue(AnySigner.supportsJSON(TEZOS.value())) assertEquals( result, - "3756ef37b1be849e3114643f0aa5847cabf9a896d3bfe4dd51448de68e91da016b0081faa75f741ef614b0e35fcc8c90dfa3b0b95721f80992f001f44e810200429a986c8072a40a1f3a3e2ab5a5819bb1b2fb69993c5004837815b9dc55923e6c0081faa75f741ef614b0e35fcc8c90dfa3b0b95721f80993f001f44e810201000081faa75f741ef614b0e35fcc8c90dfa3b0b957210001b86398d5b9be737dca8e4106ea18d70e69b75e92f892fb283546a99152b8d7794b919c0fbf1c31de386069a60014491c0e7505adef5781cead1cfe6608030b" + "3756ef37b1be849e3114643f0aa5847cabf9a896d3bfe4dd51448de68e91da016b0081faa75f741ef614b0e35fcc8c90dfa3b0b95721f80992f001f44e810200429a986c8072a40a1f3a3e2ab5a5819bb1b2fb69993c5004837815b9dc55923e006c0081faa75f741ef614b0e35fcc8c90dfa3b0b95721f80993f001f44e810201000081faa75f741ef614b0e35fcc8c90dfa3b0b957210097e828f0ec91b827995ef3ee81aac244f39f9961f2b1479626e228a14b80e860ae1dd3818648222c4b6aebcd3c57de8acebd1640f35c498e900e61876b7ecf0a" ) } } 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 69dd91abada..635d615734a 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 @@ -26,7 +26,7 @@ class TestTheOpenNetworkSigner { val transfer = TheOpenNetwork.Transfer.newBuilder() .setDest("EQBm--PFwDv1yCeS-QTJ-L8oiUpqo9IT1BwgVptlSq3ts90Q") - .setAmount(10) + .setAmount(ByteString.copyFrom("0A".toHexByteArray())) // 10 .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) .setBounceable(true) .build() @@ -52,15 +52,15 @@ class TestTheOpenNetworkSigner { val privateKey = PrivateKey("c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee".toHexByteArray()) val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder() - .setJettonAmount(500 * 1000 * 1000) + .setJettonAmount(ByteString.copyFrom("1DCD6500".toHexByteArray())) // 500 * 1000 * 1000 .setToOwner("EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8") .setResponseAddress("EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk") - .setForwardAmount(1) + .setForwardAmount(ByteString.copyFrom("01".toHexByteArray())) // 1 .build() val transfer = TheOpenNetwork.Transfer.newBuilder() .setDest("EQBiaD8PO1NwfbxSkwbcNT9rXDjqhiIvXWymNO-edV0H5lja") - .setAmount(100 * 1000 * 1000) + .setAmount(ByteString.copyFrom("05F5E100".toHexByteArray())) // 100 * 1000 * 1000 .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) .setComment("test comment") .setBounceable(true) @@ -100,7 +100,7 @@ class TestTheOpenNetworkSigner { val transfer = TheOpenNetwork.Transfer.newBuilder() .setDest(dogeChatbotDeployingAddress) // 0.069 TON - .setAmount(69_000_000) + .setAmount(ByteString.copyFrom("041CDB40".toHexByteArray())) // 69_000_000 .setMode(TheOpenNetwork.SendMode.PAY_FEES_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE) .setBounceable(false) .setStateInit(dogeChatbotStateInit) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tron/TestTronMessageSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tron/TestTronMessageSigner.kt index 23cfb2302d3..7730a59f9ca 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tron/TestTronMessageSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/tron/TestTronMessageSigner.kt @@ -20,7 +20,7 @@ class TestTronMessageSigner { val privateKey = PrivateKey(data) val msg = "Hello World" val signature = TronMessageSigner.signMessage(privateKey, msg) - assertEquals("9bb6d11ec8a6a3fb686a8f55b123e7ec4e9746a26157f6f9e854dd72f5683b450397a7b0a9653865658de8f9243f877539882891bad30c7286c3bf5622b900471b", signature) + assertEquals("bc0753c070cc55693097df11bc11e1a7c4bd5e1a40b9dc94c75568e59bcc9d6b50a7873ef25b469e494490a54de37327b4bc7fc825c81a377b555e34fb7261ba1c", signature) val pubKey = privateKey.getPublicKey(CoinType.TRON) assertTrue(TronMessageSigner.verifyMessage(pubKey, msg, signature)) } 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 9d165d95142..b4f3d147fb6 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 @@ -99,7 +99,7 @@ class TestHDWallet { } @Test - fun testGetKeyForCoin() { + fun testGetKeyForCoinBitcoin() { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) val key = wallet.getKeyForCoin(coin) @@ -109,7 +109,7 @@ class TestHDWallet { } @Test - fun testGetKeyDerivation() { + fun testGetKeyDerivationBitcoin() { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) @@ -127,7 +127,7 @@ class TestHDWallet { } @Test - fun testGetAddressForCoin() { + fun testGetAddressForCoinBitcoin() { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) @@ -136,7 +136,7 @@ class TestHDWallet { } @Test - fun testGetAddressDerivation() { + fun testGetAddressDerivationBitcoin() { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) @@ -153,6 +153,49 @@ class TestHDWallet { assertEquals(address4, "bc1pgqks0cynn93ymve4x0jq3u7hne77908nlysp289hc44yc4cmy0hslyckrz") } + @Test + fun testGetKeyForCoinPactus() { + val coin = CoinType.PACTUS + val wallet = HDWallet(words, password) + val key = wallet.getKeyForCoin(coin) + + val address = coin.deriveAddress(key) + assertEquals(address, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff") + } + + @Test + fun testGetKeyDerivationPactus() { + val coin = CoinType.PACTUS + val wallet = HDWallet(words, password) + + val key1 = wallet.getKeyDerivation(coin, Derivation.PACTUSMAINNET) + assertEquals(key1.data().toHex(), "0x153fefb8168f246f9f77c60ea10765c1c39828329e87284ddd316770717f3a5e") + + val key2 = wallet.getKeyDerivation(coin, Derivation.PACTUSTESTNET) + assertEquals(key2.data().toHex(), "0x54f3c54dd6af5794bea1f86de05b8b9f164215e8deee896f604919046399e54d") + } + + @Test + fun testGetAddressForCoinPactus() { + val coin = CoinType.PACTUS + val wallet = HDWallet(words, password) + + val address = wallet.getAddressForCoin(coin) + assertEquals(address, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff") + } + + @Test + fun testGetAddressDerivationPactus() { + val coin = CoinType.PACTUS + val wallet = HDWallet(words, password) + + val address1 = wallet.getAddressDerivation(coin, Derivation.PACTUSMAINNET) + assertEquals(address1, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff") + + val address2 = wallet.getAddressDerivation(coin, Derivation.PACTUSTESTNET) + assertEquals(address2, "tpc1rjtamyqp203j4367q4plkp4qt32d7sv34kfmj5e") + } + @Test fun testDerive() { val wallet = HDWallet(words, password) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt index 599d3369216..9b00f5adbfa 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt @@ -91,4 +91,36 @@ class TestKeyStore { val privateKey = newKeyStore.decryptPrivateKey("".toByteArray()) assertNull(privateKey) } + + @Test + fun testImportKeyEncodedEthereum() { + val privateKeyHex = "9cdb5cab19aec3bd0fcd614c5f185e7a1d97634d4225730eba22497dc89a716c" + val password = "password".toByteArray() + val key = StoredKey.importPrivateKeyEncoded(privateKeyHex, "name", password, CoinType.ETHEREUM) + val json = key.exportJSON() + + val keyStore = StoredKey.importJSON(json) + val storedEncoded = keyStore.decryptPrivateKeyEncoded(password) + + assertTrue(keyStore.hasPrivateKeyEncoded()) + assertNotNull(keyStore) + assertNotNull(storedEncoded) + assertEquals(privateKeyHex, storedEncoded) + } + + @Test + fun testImportKeyEncodedSolana() { + val privateKeyBase58 = "A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr" + val password = "password".toByteArray() + val key = StoredKey.importPrivateKeyEncoded(privateKeyBase58, "name", password, CoinType.SOLANA) + val json = key.exportJSON() + + val keyStore = StoredKey.importJSON(json) + val storedEncoded = keyStore.decryptPrivateKeyEncoded(password) + + assertTrue(keyStore.hasPrivateKeyEncoded()) + assertNotNull(keyStore) + assertNotNull(storedEncoded) + assertEquals(privateKeyBase58, storedEncoded) + } } diff --git a/android/gradlew b/android/gradlew index aeb74cbb43e..d0054d28acf 100755 --- a/android/gradlew +++ b/android/gradlew @@ -104,7 +104,7 @@ cygwin=false msys=false darwin=false nonstop=false -case "$( uname )" in #( +case "$( uname -s )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( diff --git a/bootstrap.sh b/bootstrap.sh index 1cc2636b987..10e5812aa06 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -15,7 +15,7 @@ if isHelp; then fi echo "#### Installing system dependencies ... ####" -if [[ $(uname) == "Darwin" ]]; then +if [[ $(uname -s) == "Darwin" ]]; then tools/install-sys-dependencies-mac else tools/install-sys-dependencies-linux diff --git a/cmake/Protobuf.cmake b/cmake/Protobuf.cmake index 8e0a93251d2..6228eed7bfc 100644 --- a/cmake/Protobuf.cmake +++ b/cmake/Protobuf.cmake @@ -2,8 +2,8 @@ # # 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) +set(protobuf_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../build/local/src/protobuf/protobuf-3.20.3) +set(protobuf_source_dir ${CMAKE_CURRENT_LIST_DIR}/../build/local/src/protobuf/protobuf-3.20.3) # sort + uniq -u # https://github.com/protocolbuffers/protobuf/blob/master/cmake/libprotobuf.cmake @@ -16,6 +16,7 @@ set(protobuf_SOURCE_FILES ${protobuf_source_dir}/src/google/protobuf/api.pb.cc ${protobuf_source_dir}/src/google/protobuf/arena.cc ${protobuf_source_dir}/src/google/protobuf/arenastring.cc + ${protobuf_source_dir}/src/google/protobuf/arenaz_sampler.cc ${protobuf_source_dir}/src/google/protobuf/compiler/importer.cc ${protobuf_source_dir}/src/google/protobuf/compiler/parser.cc ${protobuf_source_dir}/src/google/protobuf/descriptor.cc @@ -30,8 +31,6 @@ set(protobuf_SOURCE_FILES ${protobuf_source_dir}/src/google/protobuf/generated_enum_util.cc ${protobuf_source_dir}/src/google/protobuf/generated_message_bases.cc ${protobuf_source_dir}/src/google/protobuf/generated_message_reflection.cc - ${protobuf_source_dir}/src/google/protobuf/generated_message_table_driven.cc - ${protobuf_source_dir}/src/google/protobuf/generated_message_table_driven_lite.cc ${protobuf_source_dir}/src/google/protobuf/generated_message_tctable_full.cc ${protobuf_source_dir}/src/google/protobuf/generated_message_tctable_lite.cc ${protobuf_source_dir}/src/google/protobuf/generated_message_util.cc @@ -51,6 +50,7 @@ set(protobuf_SOURCE_FILES ${protobuf_source_dir}/src/google/protobuf/message.cc ${protobuf_source_dir}/src/google/protobuf/message_lite.cc ${protobuf_source_dir}/src/google/protobuf/parse_context.cc + ${protobuf_source_dir}/src/google/protobuf/reflection_internal.h ${protobuf_source_dir}/src/google/protobuf/reflection_ops.cc ${protobuf_source_dir}/src/google/protobuf/repeated_field.cc ${protobuf_source_dir}/src/google/protobuf/repeated_ptr_field.cc @@ -104,6 +104,7 @@ set(protobuf_HEADER_FILES ${protobuf_source_dir}/src/google/protobuf/arena.h ${protobuf_source_dir}/src/google/protobuf/arena_impl.h ${protobuf_source_dir}/src/google/protobuf/arenastring.h + ${protobuf_source_dir}/src/google/protobuf/arenaz_sampler.h ${protobuf_source_dir}/src/google/protobuf/compiler/importer.h ${protobuf_source_dir}/src/google/protobuf/compiler/parser.h ${protobuf_source_dir}/src/google/protobuf/descriptor.h @@ -121,11 +122,8 @@ set(protobuf_HEADER_FILES ${protobuf_source_dir}/src/google/protobuf/generated_enum_util.h ${protobuf_source_dir}/src/google/protobuf/generated_message_bases.h ${protobuf_source_dir}/src/google/protobuf/generated_message_reflection.h - ${protobuf_source_dir}/src/google/protobuf/generated_message_table_driven.h - ${protobuf_source_dir}/src/google/protobuf/generated_message_table_driven_lite.h ${protobuf_source_dir}/src/google/protobuf/generated_message_tctable_decl.h ${protobuf_source_dir}/src/google/protobuf/generated_message_tctable_impl.h - ${protobuf_source_dir}/src/google/protobuf/generated_message_tctable_impl.inc ${protobuf_source_dir}/src/google/protobuf/generated_message_util.h ${protobuf_source_dir}/src/google/protobuf/has_bits.h ${protobuf_source_dir}/src/google/protobuf/implicit_weak_message.h diff --git a/cmake/StandardSettings.cmake b/cmake/StandardSettings.cmake index bef5a651b52..14a781a120d 100644 --- a/cmake/StandardSettings.cmake +++ b/cmake/StandardSettings.cmake @@ -1,7 +1,9 @@ # # Default settings # -set(CMAKE_CXX_VISIBILITY_PRESET hidden) +if (NOT FLUTTER) + set(CMAKE_CXX_VISIBILITY_PRESET hidden) +endif () set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS 1) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version" FORCE) @@ -66,7 +68,7 @@ endif () option(TW_UNIT_TESTS "Enable the unit tests of the project" ON) option(TW_BUILD_EXAMPLES "Enable the examples builds of the project" ON) -if (ANDROID OR IOS_PLATFORM OR TW_COMPILE_WASM OR TW_COMPILE_JAVA) +if (ANDROID OR IOS_PLATFORM OR TW_COMPILE_WASM OR TW_COMPILE_JAVA OR FLUTTER) set(TW_UNIT_TESTS OFF) set(TW_BUILD_EXAMPLES OFF) endif() diff --git a/codegen-v2/manifest/TWDerivation.yaml b/codegen-v2/manifest/TWDerivation.yaml index c2143b80431..b8d1cb6f0b9 100644 --- a/codegen-v2/manifest/TWDerivation.yaml +++ b/codegen-v2/manifest/TWDerivation.yaml @@ -19,3 +19,11 @@ enums: value: 5 - name: solanaSolana value: 6 + - name: stratisSegwit + value: 7 + - name: bitcoinTaproot + value: 8 + - name: pactusMainnet + value: 9 + - name: pactusTestnet + value: 10 diff --git a/codegen-v2/src/codegen/cpp/code_gen.rs b/codegen-v2/src/codegen/cpp/code_gen.rs index 03ebc852c97..96af8f7c525 100644 --- a/codegen-v2/src/codegen/cpp/code_gen.rs +++ b/codegen-v2/src/codegen/cpp/code_gen.rs @@ -1,10 +1,9 @@ use heck::ToLowerCamelCase; -use regex::Regex; -use serde::{Deserialize, Serialize}; use std::fmt::Write as _; use std::fs; use std::io::Write; +use super::code_gen_types::*; use crate::Error::BadFormat; use crate::Result; @@ -12,74 +11,6 @@ static IN_DIR: &str = "../rust/bindings/"; static HEADER_OUT_DIR: &str = "../include/TrustWalletCore/"; static SOURCE_OUT_DIR: &str = "../src/Generated/"; -#[derive(Deserialize, Serialize, Debug)] -pub struct TWConfig { - pub class: String, - pub static_functions: Vec, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct TWStaticFunction { - pub name: String, - pub rust_name: String, - pub args: Vec, - pub return_type: String, - pub docs: Vec, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct TWArg { - pub name: String, - pub ty: String, -} - -fn convert_standard_type_to_cpp(ty: &str) -> String { - match ty { - "TWPrivateKey" => "struct TWPrivateKey".to_string(), - "TWPublicKey" => "struct TWPublicKey".to_string(), - "TWDataVector" => "struct TWDataVector".to_string(), - _ => ty.to_string(), - } -} - -fn convert_rust_type_to_cpp(ty: &str) -> String { - let trimmed = ty.replace(" ", ""); - if let Some(captures) = Regex::new(r"^Nonnull<(.+)>$") - .expect("Failed to create regex") - .captures(&trimmed) - { - format!("{} *_Nonnull", convert_standard_type_to_cpp(&captures[1])) - } else if let Some(captures) = Regex::new(r"^NonnullMut<(.+)>$") - .expect("Failed to create regex") - .captures(&trimmed) - { - format!("{} *_Nonnull", convert_standard_type_to_cpp(&captures[1])) - } else if let Some(captures) = Regex::new(r"^Nullable<(.+)>$") - .expect("Failed to create regex") - .captures(&trimmed) - { - format!("{} *_Nullable", convert_standard_type_to_cpp(&captures[1])) - } else if let Some(captures) = Regex::new(r"^NullableMut<(.+)>$") - .expect("Failed to create regex") - .captures(&trimmed) - { - format!("{} *_Nullable", convert_standard_type_to_cpp(&captures[1])) - } else { - match ty { - "u8" => "uint8_t".to_string(), - "u16" => "uint16_t".to_string(), - "u32" => "uint32_t".to_string(), - "u64" => "uint64_t".to_string(), - "i8" => "int8_t".to_string(), - "i16" => "int16_t".to_string(), - "i32" => "int32_t".to_string(), - "i64" => "int64_t".to_string(), - "TWFFICoinType" => "enum TWCoinType".to_string(), - _ => ty.to_string(), - } - } -} - fn generate_license(file: &mut std::fs::File) -> Result<()> { writeln!(file, "// SPDX-License-Identifier: Apache-2.0")?; writeln!(file, "//")?; @@ -97,27 +28,27 @@ fn generate_header_includes(file: &mut std::fs::File, info: &TWConfig) -> Result // Include headers based on argument types let mut included_headers = std::collections::HashSet::new(); - for func in &info.static_functions { - for arg in &func.args { - if arg.ty.contains("TWString") && included_headers.insert("TWString.h") { - writeln!(file, "#include \"TWString.h\"")?; - } - if arg.ty.contains("TWData") && included_headers.insert("TWData.h") { - writeln!(file, "#include \"TWData.h\"")?; - } - if arg.ty.contains("TWPrivateKey") && included_headers.insert("TWPrivateKey.h") { - writeln!(file, "#include \"TWPrivateKey.h\"")?; - } - if arg.ty.contains("TWPublicKey") && included_headers.insert("TWPublicKey.h") { - writeln!(file, "#include \"TWPublicKey.h\"")?; + for (_, func) in info.functions(true) { + for ty in func.types() { + let tw_type = TWType::from(ty); + match tw_type { + TWType::Pointer(_, header_name) => { + if header_name == info.class { + continue; + } + if included_headers.insert(header_name.clone()) { + writeln!(file, "#include \"{}.h\"", header_name)?; + } + } + TWType::Standard(ty) => { + if ty.contains("TWFFICoinType") + && included_headers.insert("TWCoinType.h".to_string()) + { + // Need to handle this case separately because it's not a pointer type + writeln!(file, "#include \"TWCoinType.h\"")?; + } + } } - if arg.ty.contains("TWDataVector") && included_headers.insert("TWDataVector.h") { - writeln!(file, "#include \"TWDataVector.h\"")?; - } - if arg.ty.contains("TWFFICoinType") && included_headers.insert("TWCoinType.h") { - writeln!(file, "#include \"TWCoinType.h\"")?; - } - // Additional type checks can be added here in the future } } @@ -131,12 +62,19 @@ fn generate_class_declaration(file: &mut std::fs::File, info: &TWConfig) -> Resu fn generate_function_signature( class_name: &str, - func: &TWStaticFunction, + func_type: TWFunctionType, + func: &TWFunction, is_declaration: bool, ) -> Result { - let return_type = convert_rust_type_to_cpp(&func.return_type); + let return_type = TWType::from(func.return_type.clone()).cpp_type(); let whether_export = if is_declaration { - "TW_EXPORT_STATIC_METHOD " + match func_type { + TWFunctionType::StaticFunction | TWFunctionType::Constructor => { + "TW_EXPORT_STATIC_METHOD " + } + TWFunctionType::Method | TWFunctionType::Destructor => "TW_EXPORT_METHOD ", + TWFunctionType::Property => "TW_EXPORT_PROPERTY ", + } } else { "" }; @@ -146,7 +84,7 @@ fn generate_function_signature( write!( &mut signature, "{} {}", - convert_rust_type_to_cpp(&arg.ty), + TWType::from(arg.ty.clone()).cpp_type(), arg.name.to_lower_camel_case() ) .map_err(|e| BadFormat(e.to_string()))?; @@ -161,9 +99,10 @@ fn generate_function_signature( fn generate_function_declaration( file: &mut std::fs::File, class_name: &str, - func: &TWStaticFunction, + func_type: TWFunctionType, + func: &TWFunction, ) -> Result<()> { - let func_dec = generate_function_signature(class_name, func, true)?; + let func_dec = generate_function_signature(class_name, func_type, func, true)?; for doc in &func.docs { writeln!(file, "/// {}", doc)?; } @@ -182,8 +121,8 @@ pub fn generate_header(info: &TWConfig) -> Result<()> { writeln!(file, "\nTW_EXTERN_C_BEGIN\n")?; generate_class_declaration(&mut file, info)?; - for func in &info.static_functions { - generate_function_declaration(&mut file, &info.class, func)?; + for (func_type, func) in info.functions(true) { + generate_function_declaration(&mut file, &info.class, func_type, func)?; } writeln!(file, "TW_EXTERN_C_END")?; @@ -193,24 +132,74 @@ pub fn generate_header(info: &TWConfig) -> Result<()> { Ok(()) } +fn generate_wrapper_header(info: &TWConfig) -> Result<()> { + let class_name = &info.class; + let wrapper_class_name = class_name.replace("TW", ""); + let file_path = format!("{SOURCE_OUT_DIR}/{}.h", wrapper_class_name); + let mut file = std::fs::File::create(&file_path)?; + + generate_license(&mut file)?; + generate_header_guard(&mut file)?; + + writeln!(file, "#include \"rust/Wrapper.h\"\n")?; + + writeln!( + file, + "using {wrapper_class_name}Ptr = std::shared_ptr;\n", + )?; + + writeln!(file, "struct {} {{", wrapper_class_name)?; + + let Some(destructor) = &info.destructor else { + panic!("No destructor found for {}", wrapper_class_name); + }; + let destructor_name = &destructor.rust_name; + writeln!( + file, + "\texplicit {wrapper_class_name}(TW::Rust::{class_name}* raw_ptr): ptr(raw_ptr, TW::Rust::{destructor_name}) {{}}\n", + )?; + + writeln!(file, "\t{wrapper_class_name}Ptr ptr;")?; + writeln!(file, "}};\n")?; + + writeln!(file, "struct {} {{", class_name)?; + writeln!(file, "\t{wrapper_class_name} impl;")?; + writeln!(file, "}};\n")?; + + Ok(()) +} + fn generate_source_includes(file: &mut std::fs::File, info: &TWConfig) -> Result<()> { writeln!(file, "#include ", info.class)?; writeln!(file, "#include \"rust/Wrapper.h\"")?; // Include headers based on argument types let mut included_headers = std::collections::HashSet::new(); - for func in &info.static_functions { - for arg in &func.args { - if arg.ty.contains("TWPrivateKey") && included_headers.insert("TWPrivateKey.h") { + for (_, func) in info.functions(true) { + for ty in func.types() { + let tw_type = TWType::from(ty); + let TWType::Pointer(_, header_name) = tw_type else { + continue; + }; + if header_name.contains("TWPrivateKey") + && included_headers.insert("TWPrivateKey.h".to_string()) + { writeln!(file, "#include \"../PrivateKey.h\"")?; - } - if arg.ty.contains("TWPublicKey") && included_headers.insert("TWPublicKey.h") { + } else if header_name.contains("TWPublicKey") + && included_headers.insert("TWPublicKey.h".to_string()) + { writeln!(file, "#include \"../PublicKey.h\"")?; - } - if arg.ty.contains("TWDataVector") && included_headers.insert("TWDataVector.h") { + } else if header_name.contains("TWDataVector") + && included_headers.insert("TWDataVector.h".to_string()) + { writeln!(file, "#include \"../DataVector.h\"")?; + } else if !header_name.contains("TWString") && !header_name.contains("TWData") { + // Do not need wrapper headers for these types + let wrapper_header_name = header_name.replace("TW", ""); + if included_headers.insert(wrapper_header_name.clone()) { + writeln!(file, "#include \"{}.h\"", wrapper_header_name)?; + } } - // Additional type checks can be added here in the future } } @@ -229,223 +218,281 @@ fn generate_function_call(args: &Vec) -> Result { Ok(func_call) } -fn generate_return_type(func: &TWStaticFunction, converted_args: &Vec) -> Result { +fn generate_return_type(func: &TWFunction, converted_args: &Vec) -> Result { let mut return_string = String::new(); - match func.return_type.replace(" ", "").as_str() { - "NullableMut" | "Nullable" => { - write!( - &mut return_string, - "\tconst Rust::TWStringWrapper result = Rust::{}{}\n\ - \tif (!result) {{ return nullptr; }}\n\ - \treturn TWStringCreateWithUTF8Bytes(result.c_str());\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } - "NullableMut" | "Nullable" => { - write!( - &mut return_string, - "\tconst Rust::TWDataWrapper result = Rust::{}{}\n\ - \tif (!result.ptr) {{ return nullptr; }}\n\ - \tconst auto resultData = result.toDataOrDefault();\n\ - \treturn TWDataCreateWithBytes(resultData.data(), resultData.size());\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } - "Nonnull" | "NonnullMut" => { - write!( - &mut return_string, - "\tconst Rust::TWDataWrapper result = Rust::{}{}\n\ - \tconst auto resultData = result.toDataOrDefault();\n\ - \treturn TWDataCreateWithBytes(resultData.data(), resultData.size());\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } - "NullableMut" | "Nullable" => { - write!( - &mut return_string, - "\tconst auto result = Rust::{}{}\n\ - \tconst auto resultRustPrivateKey = Rust::wrapTWPrivateKey(result);\n\ - \tif (!resultRustPrivateKey.get()) {{ return nullptr; }}\n\ - \tconst auto resultData = Rust::tw_private_key_bytes(resultRustPrivateKey.get());\n\ - \tconst auto resultSize = Rust::tw_private_key_size(resultRustPrivateKey.get());\n\ - \tconst Data out(resultData, resultData + resultSize);\n\ - \treturn new TWPrivateKey {{ PrivateKey(out) }};\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } - "NullableMut" | "Nullable" => { - write!( - &mut return_string, - "\tconst auto result = Rust::{}{}\n\ - \tconst auto resultRustPublicKey = Rust::wrapTWPublicKey(result);\n\ - \tif (!resultRustPublicKey.get()) {{ return nullptr; }}\n\ - \tconst auto resultData = Rust::tw_public_key_data(resultRustPublicKey.get());\n\ - \tconst Data out(resultData.data, resultData.data + resultData.size);\n\ - \treturn new TWPublicKey {{ PublicKey(out, a->impl.type) }};\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } - ty if ty.contains("Nonnull") => { - panic!("Nonnull types are not supported in C++ except for TWData"); - } - _ => { - write!( - &mut return_string, - "\treturn Rust::{}{}\n", - func.rust_name, - generate_function_call(&converted_args)?.as_str() - ) - .map_err(|e| BadFormat(e.to_string()))?; - } + let tw_type = TWType::from(func.return_type.replace(" ", "").to_string()); + match tw_type { + TWType::Pointer(pointer_type, ty) => match (pointer_type, ty.as_str()) { + (TWPointerType::Nullable, "TWString") | (TWPointerType::NullableMut, "TWString") => { + write!( + &mut return_string, + "\tconst Rust::TWStringWrapper result = Rust::{}{}\n\ + \tif (!result) {{ return nullptr; }}\n\ + \treturn TWStringCreateWithUTF8Bytes(result.c_str());\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NonnullMut, "TWString") | (TWPointerType::Nonnull, "TWString") => { + write!( + &mut return_string, + "\tconst Rust::TWStringWrapper result = Rust::{}{}\n\ + \tconst auto resultString = result.toStringOrDefault();\n\ + \treturn TWStringCreateWithUTF8Bytes(resultString.c_str());\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NullableMut, "TWData") | (TWPointerType::Nullable, "TWData") => { + write!( + &mut return_string, + "\tconst Rust::TWDataWrapper result = Rust::{}{}\n\ + \tif (!result.ptr) {{ return nullptr; }}\n\ + \tconst auto resultData = result.toDataOrDefault();\n\ + \treturn TWDataCreateWithBytes(resultData.data(), resultData.size());\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NonnullMut, "TWData") | (TWPointerType::Nonnull, "TWData") => { + write!( + &mut return_string, + "\tconst Rust::TWDataWrapper result = Rust::{}{}\n\ + \tconst auto resultData = result.toDataOrDefault();\n\ + \treturn TWDataCreateWithBytes(resultData.data(), resultData.size());\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NullableMut, "TWPrivateKey") + | (TWPointerType::Nullable, "TWPrivateKey") => { + write!( + &mut return_string, + "\tconst auto result = Rust::{}{}\n\ + \tconst auto resultRustPrivateKey = Rust::wrapTWPrivateKey(result);\n\ + \tif (!resultRustPrivateKey.get()) {{ return nullptr; }}\n\ + \tconst auto resultData = Rust::tw_private_key_bytes(resultRustPrivateKey.get());\n\ + \tconst auto resultSize = Rust::tw_private_key_size(resultRustPrivateKey.get());\n\ + \tconst Data out(resultData, resultData + resultSize);\n\ + \treturn new TWPrivateKey {{ PrivateKey(out) }};\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NonnullMut, "TWPrivateKey") + | (TWPointerType::Nonnull, "TWPrivateKey") => { + panic!("Nonnull TWPrivateKey is not supported"); + } + (TWPointerType::NullableMut, "TWPublicKey") + | (TWPointerType::Nullable, "TWPublicKey") => { + write!( + &mut return_string, + "\tconst auto result = Rust::{}{}\n\ + \tconst auto resultRustPublicKey = Rust::wrapTWPublicKey(result);\n\ + \tif (!resultRustPublicKey.get()) {{ return nullptr; }}\n\ + \tconst auto resultData = Rust::tw_public_key_data(resultRustPublicKey.get());\n\ + \tconst Data out(resultData.data, resultData.data + resultData.size);\n\ + \treturn new TWPublicKey {{ PublicKey(out, a->impl.type) }};\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + (TWPointerType::NonnullMut, "TWPublicKey") + | (TWPointerType::Nonnull, "TWPublicKey") => { + panic!("Nonnull TWPublicKey is not supported"); + } + (pointer_type, class_name) => { + let wrapper_class_name = class_name.replace("TW", ""); + let null_return = match pointer_type { + TWPointerType::Nullable | TWPointerType::NullableMut => { + "if (!resultRaw) {{ return nullptr; }}\n" + } + _ => "", + }; + write!( + &mut return_string, + "\tauto* resultRaw = Rust::{}{}\n\ + {null_return}\ + \tconst {wrapper_class_name} resultWrapped(resultRaw);\n\ + \treturn new {class_name} {{ resultWrapped }};\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + }, + TWType::Standard(ty) => match ty.as_str() { + "void" => { + write!( + &mut return_string, + "\tRust::{}{}\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + _ => { + write!( + &mut return_string, + "\treturn Rust::{}{}\n", + func.rust_name, + generate_function_call(&converted_args)?.as_str() + ) + .map_err(|e| BadFormat(e.to_string()))?; + } + }, } Ok(return_string) } -fn generate_conversion_code_with_var_name(ty: &str, name: &str) -> Result<(String, String)> { - match ty { - "TWString *_Nonnull" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tauto& {name}String = *reinterpret_cast({name});\n\ - \tconst Rust::TWStringWrapper {name}RustStr = {name}String;" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustStr.get()", name))) - } - "TWString *_Nullable" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tRust::TWStringWrapper {name}RustStr;\n\ - \tif ({name} != nullptr) {{\n\ +fn generate_conversion_code_with_var_name(tw_type: TWType, name: &str) -> Result<(String, String)> { + match tw_type { + TWType::Pointer(ref pointer_type, ref ty) => match (pointer_type, ty.as_str()) { + (TWPointerType::Nonnull, "TWString") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tauto& {name}String = *reinterpret_cast({name});\n\ + \tconst Rust::TWStringWrapper {name}RustStr = {name}String;" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustStr.get()", name))) + } + (TWPointerType::Nullable, "TWString") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tRust::TWStringWrapper {name}RustStr;\n\ + \tif ({name} != nullptr) {{\n\ \t\t{name}RustStr = *reinterpret_cast({name});\n\ - \t}}" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustStr.get()", name))) - } - "TWData *_Nonnull" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tauto& {name}Data = *reinterpret_cast({name});\n\ - \tconst Rust::TWDataWrapper {name}RustData = {name}Data;" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustData.get()", name))) - } - "TWData *_Nullable" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tRust::TWDataWrapper {name}RustData;\n\ - \tif ({name} != nullptr) {{\n\ - \t\t{name}RustData = *reinterpret_cast({name});\n\ - \t}}" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustData.get()", name))) - } - "struct TWPrivateKey *_Nonnull" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tauto &{name}PrivateKey = *reinterpret_cast({name});\n\ - \tauto* {name}RustRaw = Rust::tw_private_key_create_with_data({name}PrivateKey.bytes.data(), {name}PrivateKey.bytes.size());\n\ - \tconst auto {name}RustPrivateKey = Rust::wrapTWPrivateKey({name}RustRaw);" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustPrivateKey.get()", name))) - } - "struct TWPrivateKey *_Nullable" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tstd::shared_ptr {name}RustPrivateKey;\n\ - \tif ({name} != nullptr) {{\n\ + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustStr.get()", name))) + } + (TWPointerType::Nonnull, "TWData") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tauto& {name}Data = *reinterpret_cast({name});\n\ + \tconst Rust::TWDataWrapper {name}RustData = {name}Data;" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustData.get()", name))) + } + (TWPointerType::Nullable, "TWData") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tRust::TWDataWrapper {name}RustData;\n\ + \tif ({name} != nullptr) {{\n\ + \t\t{name}RustData = *reinterpret_cast({name});\n\ + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustData.get()", name))) + } + (TWPointerType::Nonnull, "TWPrivateKey") | (TWPointerType::NonnullMut, "TWPrivateKey") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tauto &{name}PrivateKey = *reinterpret_cast({name});\n\ + \tauto* {name}RustRaw = Rust::tw_private_key_create_with_data({name}PrivateKey.bytes.data(), {name}PrivateKey.bytes.size());\n\ + \tconst auto {name}RustPrivateKey = Rust::wrapTWPrivateKey({name}RustRaw);" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustPrivateKey.get()", name))) + } + (TWPointerType::Nullable, "TWPrivateKey") | (TWPointerType::NullableMut, "TWPrivateKey") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tstd::shared_ptr {name}RustPrivateKey;\n\ + \tif ({name} != nullptr) {{\n\ \t\tconst auto& {name}PrivateKey = {name};\n\ \t\tauto* {name}RustRaw = Rust::tw_private_key_create_with_data({name}PrivateKey->impl.bytes.data(), {name}PrivateKey->impl.bytes.size());\n\ \t\t{name}RustPrivateKey = Rust::wrapTWPrivateKey({name}RustRaw);\n\ - \t}}" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustPrivateKey.get()", name))) - } - "struct TWPublicKey *_Nonnull" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tauto &{name}PublicKey = *reinterpret_cast({name});\n\ - \tconst auto {name}PublicKeyType = static_cast({name}PublicKey.type);\n\ - \tauto* {name}RustRaw = Rust::tw_public_key_create_with_data({name}PublicKey.bytes.data(), {name}PublicKey.bytes.size(), {name}PublicKeyType);\n\ - \tconst auto {name}RustPublicKey = Rust::wrapTWPublicKey({name}RustRaw);" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustPublicKey.get()", name))) - } - "struct TWPublicKey *_Nullable" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tstd::shared_ptr {name}RustPublicKey;\n\ - \tif ({name} != nullptr) {{\n\ + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustPrivateKey.get()", name))) + } + (TWPointerType::Nonnull, "TWPublicKey") | (TWPointerType::NonnullMut, "TWPublicKey") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tauto &{name}PublicKey = *reinterpret_cast({name});\n\ + \tconst auto {name}PublicKeyType = static_cast({name}PublicKey.type);\n\ + \tauto* {name}RustRaw = Rust::tw_public_key_create_with_data({name}PublicKey.bytes.data(), {name}PublicKey.bytes.size(), {name}PublicKeyType);\n\ + \tconst auto {name}RustPublicKey = Rust::wrapTWPublicKey({name}RustRaw);" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustPublicKey.get()", name))) + } + (TWPointerType::Nullable, "TWPublicKey") | (TWPointerType::NullableMut, "TWPublicKey") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tstd::shared_ptr {name}RustPublicKey;\n\ + \tif ({name} != nullptr) {{\n\ \t\tconst auto& {name}PublicKey = {name};\n\ \t\tconst auto {name}PublicKeyType = static_cast({name}PublicKey->impl.type);\n\ \t\tauto* {name}RustRaw = Rust::tw_public_key_create_with_data({name}PublicKey->impl.bytes.data(), {name}PublicKey->impl.bytes.size(), {name}PublicKeyType);\n\ \t\t{name}RustPublicKey = Rust::wrapTWPublicKey({name}RustRaw);\n\ - \t}}" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustPublicKey.get()", name))) - } - "struct TWDataVector *_Nonnull" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tconst Rust::TWDataVectorWrapper {name}RustDataVector = createFromTWDataVector({name});" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustDataVector.get()", name))) - } - "struct TWDataVector *_Nullable" => { - let mut conversion_code = String::new(); - writeln!( - &mut conversion_code, - "\tstd::shared_ptr {name}RustDataVector;\n\ - \tif ({name} != nullptr) {{\n\ + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustPublicKey.get()", name))) + } + (TWPointerType::Nonnull, "TWDataVector") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tconst Rust::TWDataVectorWrapper {name}RustDataVector = createFromTWDataVector({name});" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustDataVector.get()", name))) + } + (TWPointerType::Nullable, "TWDataVector") => { + let mut conversion_code = String::new(); + writeln!( + &mut conversion_code, + "\tstd::shared_ptr {name}RustDataVector;\n\ + \tif ({name} != nullptr) {{\n\ \t\t{name}RustDataVector = createFromTWDataVector({name});\n\ - \t}}" - ) - .map_err(|e| BadFormat(e.to_string()))?; - Ok((conversion_code, format!("{}RustDataVector.get()", name))) - } - _ => Ok(("".to_string(), name.to_string())), + \t}}" + ) + .map_err(|e| BadFormat(e.to_string()))?; + Ok((conversion_code, format!("{}RustDataVector.get()", name))) + } + _ if tw_type.cpp_type().starts_with("struct ") => { + Ok(("".to_string(), format!("{name}->impl.ptr.get()"))) + } + _ => panic!("Unsupported type: {}", tw_type.cpp_type()), + }, + TWType::Standard(_) => Ok(("".to_string(), name.to_string())), } } fn generate_function_definition( file: &mut std::fs::File, info: &TWConfig, - func: &TWStaticFunction, + func_type: TWFunctionType, + func: &TWFunction, ) -> Result<()> { - let mut func_def = generate_function_signature(&info.class, func, false)?; + let mut func_def = generate_function_signature(&info.class, func_type, func, false)?; func_def += " {\n"; let mut converted_args = vec![]; for arg in func.args.iter() { - let func_type = convert_rust_type_to_cpp(&arg.ty); + let func_type = TWType::from(arg.ty.clone()); let (conversion_code, converted_arg) = - generate_conversion_code_with_var_name(&func_type, &arg.name.to_lower_camel_case())?; + generate_conversion_code_with_var_name(func_type, &arg.name.to_lower_camel_case())?; func_def += conversion_code.as_str(); converted_args.push(converted_arg); } @@ -456,6 +503,27 @@ fn generate_function_definition( Ok(()) } +fn generate_destructor_definition( + file: &mut std::fs::File, + info: &TWConfig, + destructor: &TWFunction, +) -> Result<()> { + let function_signature = + generate_function_signature(&info.class, TWFunctionType::Destructor, destructor, false)?; + assert!( + destructor.args.len() == 1, + "Destructor must have exactly one argument" + ); + let arg_name = &destructor.args[0].name.to_lower_camel_case(); + writeln!( + file, + "{function_signature}{{\n\ + \tdelete {arg_name};\n\ + }}" + )?; + Ok(()) +} + fn generate_source(info: &TWConfig) -> Result<()> { let file_path = format!("{SOURCE_OUT_DIR}/{}.cpp", info.class); let mut file = std::fs::File::create(&file_path)?; @@ -465,8 +533,11 @@ fn generate_source(info: &TWConfig) -> Result<()> { writeln!(file, "\nusing namespace TW;\n")?; - for func in &info.static_functions { - generate_function_definition(&mut file, info, func)?; + for (func_type, func) in info.functions(false) { + generate_function_definition(&mut file, info, func_type, func)?; + } + if let Some(destructor) = &info.destructor { + generate_destructor_definition(&mut file, info, destructor)?; } file.flush()?; @@ -490,6 +561,10 @@ pub fn generate_cpp_bindings() -> Result<()> { let info: TWConfig = serde_yaml::from_str(&file_contents).expect("Failed to parse YAML file"); + if info.is_wrapped() { + generate_wrapper_header(&info)?; + } + generate_header(&info)?; generate_source(&info)?; } diff --git a/codegen-v2/src/codegen/cpp/code_gen_types.rs b/codegen-v2/src/codegen/cpp/code_gen_types.rs new file mode 100644 index 00000000000..555fa8b8e88 --- /dev/null +++ b/codegen-v2/src/codegen/cpp/code_gen_types.rs @@ -0,0 +1,152 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; + +pub enum TWFunctionType { + StaticFunction, + Constructor, + Destructor, + Method, + Property, +} + +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct TWConfig { + pub class: String, + pub static_functions: Vec, + pub constructors: Option>, + pub destructor: Option, + pub methods: Option>, + pub properties: Option>, +} + +impl TWConfig { + pub fn functions(&self, include_destructor: bool) -> Vec<(TWFunctionType, &TWFunction)> { + let mut functions = self + .static_functions + .iter() + .map(|f| (TWFunctionType::StaticFunction, f)) + .collect::>(); + if let Some(constructors) = &self.constructors { + functions.extend( + constructors + .iter() + .map(|f| (TWFunctionType::Constructor, f)), + ); + } + if let Some(methods) = &self.methods { + functions.extend(methods.iter().map(|f| (TWFunctionType::Method, f))); + } + if let Some(properties) = &self.properties { + functions.extend(properties.iter().map(|f| (TWFunctionType::Property, f))); + } + if include_destructor { + if let Some(destructor) = &self.destructor { + functions.push((TWFunctionType::Destructor, destructor)); + } + } + functions + } + + pub fn is_wrapped(&self) -> bool { + self.constructors.is_some() && self.destructor.is_some() + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TWFunction { + pub name: String, + pub rust_name: String, + pub args: Vec, + pub return_type: String, + pub docs: Vec, +} + +impl TWFunction { + pub fn types(&self) -> Vec { + self.args + .iter() + .map(|arg| arg.ty.clone()) + .chain(std::iter::once(self.return_type.clone())) + .collect() + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct TWArg { + pub name: String, + pub ty: String, +} + +#[derive(Debug)] +pub enum TWPointerType { + Nonnull, + NonnullMut, + Nullable, + NullableMut, +} + +#[derive(Debug)] +pub enum TWType { + Pointer(TWPointerType, String), + Standard(String), +} + +impl From for TWType { + fn from(ty: String) -> Self { + let trimmed = ty.replace(" ", ""); + if let Some(captures) = Regex::new(r"^Nonnull<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + TWType::Pointer(TWPointerType::Nonnull, captures[1].to_string()) + } else if let Some(captures) = Regex::new(r"^NonnullMut<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + TWType::Pointer(TWPointerType::NonnullMut, captures[1].to_string()) + } else if let Some(captures) = Regex::new(r"^Nullable<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + TWType::Pointer(TWPointerType::Nullable, captures[1].to_string()) + } else if let Some(captures) = Regex::new(r"^NullableMut<(.+)>$") + .expect("Failed to create regex") + .captures(&trimmed) + { + TWType::Pointer(TWPointerType::NullableMut, captures[1].to_string()) + } else { + TWType::Standard(trimmed) + } + } +} + +impl TWType { + pub fn cpp_type(&self) -> String { + match self { + TWType::Standard(ty) => match ty.as_str() { + "u8" => "uint8_t".to_string(), + "u16" => "uint16_t".to_string(), + "u32" => "uint32_t".to_string(), + "u64" => "uint64_t".to_string(), + "i8" => "int8_t".to_string(), + "i16" => "int16_t".to_string(), + "i32" => "int32_t".to_string(), + "i64" => "int64_t".to_string(), + "TWFFICoinType" => "enum TWCoinType".to_string(), + _ => ty.to_string(), + }, + TWType::Pointer(pointer_type, ty) => { + let ty = match ty.as_str() { + "TWString" | "TWData" => ty.to_string(), + _ => format!("struct {}", ty), + }; + match pointer_type { + TWPointerType::Nonnull => format!("{} *_Nonnull", ty), + TWPointerType::NonnullMut => format!("{} *_Nonnull", ty), + TWPointerType::Nullable => format!("{} *_Nullable", ty), + TWPointerType::NullableMut => format!("{} *_Nullable", ty), + } + } + } + } +} diff --git a/codegen-v2/src/codegen/cpp/mod.rs b/codegen-v2/src/codegen/cpp/mod.rs index 147960dd30c..d066709dbaf 100644 --- a/codegen-v2/src/codegen/cpp/mod.rs +++ b/codegen-v2/src/codegen/cpp/mod.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; pub mod blockchain_dispatcher_generator; pub mod code_gen; +pub mod code_gen_types; pub mod entry_generator; pub mod new_blockchain; pub mod new_cosmos_chain; diff --git a/codegen-v2/src/codegen/rust/coin_crate.rs b/codegen-v2/src/codegen/rust/coin_crate.rs index 0e1b133f171..c546f4cbcf2 100644 --- a/codegen-v2/src/codegen/rust/coin_crate.rs +++ b/codegen-v2/src/codegen/rust/coin_crate.rs @@ -13,7 +13,8 @@ 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_MANIFEST_TEMPLATE: &str = + include_str!("templates/blockchain_crate/Cargo.toml.hbs"); const BLOCKCHAIN_LIB_TEMPLATE: &str = include_str!("templates/blockchain_crate/lib.rs"); const BLOCKCHAIN_SIGNER_TEMPLATE: &str = include_str!("templates/blockchain_crate/signer.rs"); diff --git a/codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml b/codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml.hbs similarity index 100% rename from codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml rename to codegen-v2/src/codegen/rust/templates/blockchain_crate/Cargo.toml.hbs diff --git a/codegen-v2/src/codegen/swift/functions.rs b/codegen-v2/src/codegen/swift/functions.rs index 1f7e4aa8d39..ec7888eca4f 100644 --- a/codegen-v2/src/codegen/swift/functions.rs +++ b/codegen-v2/src/codegen/swift/functions.rs @@ -28,7 +28,7 @@ pub(super) fn process_methods( let mut ops = vec![]; - // Initalize the 'self' type, which is then passed on to the underlying + // Initialize the 'self' type, which is then passed on to the underlying // C FFI function, assuming the function is not static. // // E.g: diff --git a/codegen-v2/src/codegen/swift/properties.rs b/codegen-v2/src/codegen/swift/properties.rs index 61d73e7171f..44ba77ee0a9 100644 --- a/codegen-v2/src/codegen/swift/properties.rs +++ b/codegen-v2/src/codegen/swift/properties.rs @@ -28,7 +28,7 @@ pub(super) fn process_properties( let mut ops = vec![]; - // Initalize the 'self' type, which is then passed on to the underlying + // Initialize the 'self' type, which is then passed on to the underlying // C FFI function. ops.push(match object { // E.g. `let obj = self.rawValue` diff --git a/codegen/bin/coins b/codegen/bin/coins index c5a3690c1a4..3433d9d07ff 100755 --- a/codegen/bin/coins +++ b/codegen/bin/coins @@ -26,6 +26,10 @@ def self.camel_case(id) id[0].upcase + id[1..].downcase end +def self.upper_camel_case(id) + id.split('_').map { |w| w.capitalize }.join +end + def self.coin_img(coin) "" end diff --git a/codegen/lib/derivation.rb b/codegen/lib/derivation.rb index 1b9b8233e78..1a680fe073c 100644 --- a/codegen/lib/derivation.rb +++ b/codegen/lib/derivation.rb @@ -14,7 +14,7 @@ def derivation_name(deriv) # Returns a string of `` if derivation's name is specified, otherwise returns `Default`. def derivation_enum_name_no_prefix(deriv, coin) return "Default" if deriv['name'].nil? - format_name(coin['name']) + camel_case(deriv['name']) + format_name(coin['name']) + upper_camel_case(deriv['name']) end # Returns a string of `TWDerivation` if derivation's name is specified, otherwise returns `TWDerivationDefault`. diff --git a/codegen/lib/templates/jni/parameter_access.erb b/codegen/lib/templates/jni/parameter_access.erb index 4c3ceb9f2fa..ea9e3c13c5d 100644 --- a/codegen/lib/templates/jni/parameter_access.erb +++ b/codegen/lib/templates/jni/parameter_access.erb @@ -6,8 +6,24 @@ parameters = method.parameters.drop(1) end - parameters.each do |param| - if param.type.name == :data -%> + parameters.each do |param| -%> +<% if !param.type.is_nullable && (JNIHelper.type(param.type) == 'jobject' || JNIHelper.type(param.type) == 'jstring' || JNIHelper.type(param.type) == 'jbyteArray') + # In case of constructor (starts with Create), it always returns jlong type. + if method.name.start_with?('Create') -%> + JNI_CHECK_NULL_AND_RETURN_ZERO(env, <%= param.name %>, "<%= param.name %>"); +<% elsif JNIHelper.type(method.return_type) == 'void' -%> + JNI_CHECK_NULL_AND_RETURN_VOID(env, <%= param.name %>, "<%= param.name %>"); +<% elsif JNIHelper.type(method.return_type) == 'jbyteArray' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% elsif JNIHelper.type(method.return_type) == 'jstring' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% elsif JNIHelper.type(method.return_type) == 'jobject' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% else -%> + JNI_CHECK_NULL_AND_RETURN_ZERO(env, <%= param.name %>, "<%= param.name %>"); +<% end -%> +<% end -%> +<% if param.type.name == :data -%> TWData *<%= param.name %>Data = TWDataCreateWithJByteArray(env, <%= param.name %>); <% elsif param.type.name == :string -%> TWString *<%= param.name %>String = TWStringCreateWithJString(env, <%= param.name %>); diff --git a/codegen/lib/templates/kotlin_jni/parameter_access.erb b/codegen/lib/templates/kotlin_jni/parameter_access.erb index a5dcae13aa1..790b3115d87 100644 --- a/codegen/lib/templates/kotlin_jni/parameter_access.erb +++ b/codegen/lib/templates/kotlin_jni/parameter_access.erb @@ -6,8 +6,24 @@ parameters = method.parameters.drop(1) end - parameters.each do |param| - if param.type.name == :data -%> + parameters.each do |param| -%> +<% if !param.type.is_nullable && (KotlinJniHelper.type(param.type) == 'jobject' || KotlinJniHelper.type(param.type) == 'jstring' || KotlinJniHelper.type(param.type) == 'jbyteArray') + # In case of constructor (starts with Create), it always returns jlong type. + if method.name.start_with?('Create') -%> + JNI_CHECK_NULL_AND_RETURN_ZERO(env, <%= param.name %>, "<%= param.name %>"); +<% elsif KotlinJniHelper.type(method.return_type) == 'void' -%> + JNI_CHECK_NULL_AND_RETURN_VOID(env, <%= param.name %>, "<%= param.name %>"); +<% elsif KotlinJniHelper.type(method.return_type) == 'jbyteArray' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% elsif KotlinJniHelper.type(method.return_type) == 'jstring' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% elsif KotlinJniHelper.type(method.return_type) == 'jobject' -%> + JNI_CHECK_NULL_AND_RETURN_NULL(env, <%= param.name %>, "<%= param.name %>"); +<% else -%> + JNI_CHECK_NULL_AND_RETURN_ZERO(env, <%= param.name %>, "<%= param.name %>"); +<% end -%> +<% end -%> +<% if param.type.name == :data -%> TWData *<%= param.name %>Data = TWDataCreateWithJByteArray(env, <%= param.name %>); <% elsif param.type.name == :string -%> TWString *<%= param.name %>String = TWStringCreateWithJString(env, <%= param.name %>); diff --git a/docs/registry.md b/docs/registry.md index 2c0bce5d0bc..acb153ce4f6 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -64,6 +64,7 @@ This list is generated from [./registry.json](../registry.json) | 508 | MultiversX | eGLD | | | | 529 | Secret | SCRT | | | | 564 | Agoric | BLD | | | +| 595 | Polymesh | POLYX | | | | 607 | TON | TON | | | | 637 | Aptos | APT | | | | 714 | BNB Beacon Chain | BNB | | | @@ -96,6 +97,8 @@ This list is generated from [./registry.json](../registry.json) | 7332 | Zen EON | ZEN | | | | 8453 | Base | ETH | | | | 8964 | NULS | NULS | | | +| 9745 | Plasma Mainnet | XPL | | | +| 10143 | Monad | MON | | | | 14001 | WAX | WAXP | | | | 18000 | Meter | MTR | | | | 19167 | Flux | FLUX | | | @@ -132,7 +135,7 @@ This list is generated from [./registry.json](../registry.json) | 10004689 | IoTeX EVM | IOTX | | | | 10007000 | NativeZetaChain | ZETA | | | | 10007700 | NativeCanto | CANTO | | | -| 10008217 | Kaia | KAIA | | | +| 10008217 | Kaia | KAIA | | | | 10009000 | Avalanche C-Chain | AVAX | | | | 10009001 | Evmos | EVMOS | | | | 10042170 | Arbitrum Nova | ETH | | | diff --git a/flutter/.gitignore b/flutter/.gitignore new file mode 100644 index 00000000000..c21255d8683 --- /dev/null +++ b/flutter/.gitignore @@ -0,0 +1,5 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ +*.dylib +wallet_core_bindings.dart diff --git a/flutter/CHANGELOG.md b/flutter/CHANGELOG.md new file mode 100644 index 00000000000..effe43c82c8 --- /dev/null +++ b/flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 00000000000..60f642dfe32 --- /dev/null +++ b/flutter/README.md @@ -0,0 +1,24 @@ +Wallet Core Bindings for Flutter + +## Installation + +1. Install Dart SDK: + - Visit [Dart SDK installation page](https://dart.dev/get-dart) + - Follow the instructions for your operating system + +2. Install dependencies: + ```bash + dart pub get + ``` + +## Usage + +### Running the App +```bash +dart run +``` + +### Test +```bash +dart test +``` diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml new file mode 100644 index 00000000000..dee8927aafe --- /dev/null +++ b/flutter/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/flutter/bin/flutter.dart b/flutter/bin/flutter.dart new file mode 100644 index 00000000000..deac69f6fe1 --- /dev/null +++ b/flutter/bin/flutter.dart @@ -0,0 +1,139 @@ +import 'package:flutter/wallet_core.dart'; +import 'package:flutter/wallet_core_bindings.dart'; + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +void main(List arguments) { + final walletCore = load(); + + final passwordTwString = walletCore.TWStringCreateWithUTF8Bytes( + "".toNativeUtf8().cast(), + ); + + print("Creating a new HD wallet ... "); + final walletNew = walletCore.TWHDWalletCreate(128, passwordTwString); + print("done."); + print( + "Secret mnemonic for new wallet: '${walletCore.TWStringUTF8Bytes(walletCore.TWHDWalletMnemonic(walletNew)).cast().toDartString()}'.", + ); + walletCore.TWHDWalletDelete(walletNew); + + // Alternative: Import wallet with existing recovery phrase (mnemonic) + print("Importing an HD wallet from earlier ... "); + final secretMnemonic = walletCore.TWStringCreateWithUTF8Bytes( + "ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn fatal" + .toNativeUtf8() + .cast(), + ); + final walletImp = walletCore.TWHDWalletCreateWithMnemonic( + secretMnemonic, + walletCore.TWStringCreateWithUTF8Bytes("".toNativeUtf8().cast()), + ); + walletCore.TWStringDelete(secretMnemonic); + print("done."); + print( + "Secret mnemonic for imported wallet: '${walletCore.TWStringUTF8Bytes(walletCore.TWHDWalletMnemonic(walletImp)).cast().toDartString()}'.", + ); + + // coin type: we use Ethereum + const coinType = TWCoinType.TWCoinTypeEthereum; + print( + "Working with coin: ${walletCore.TWStringUTF8Bytes(walletCore.TWCoinTypeConfigurationGetName(coinType)).cast().toDartString()} ${walletCore.TWStringUTF8Bytes(walletCore.TWCoinTypeConfigurationGetSymbol(coinType)).cast().toDartString()}", + ); + + // Derive default address + print("Obtaining default address ... "); + final address = walletCore.TWStringUTF8Bytes( + walletCore.TWHDWalletGetAddressForCoin(walletImp, coinType), + ).cast().toDartString(); + print("done."); + print("Default address: '$address'"); + + // Alternative: Derive address using default derivation path + // Done in 2 steps: derive private key, then address from private key + // Note that private key is passed around between the two calls by the wallet -- be always cautious when handling secrets, avoid the risk of leaking secrets + print( + "Default derivation path: ${walletCore.TWStringUTF8Bytes(walletCore.TWCoinTypeDerivationPath(coinType)).cast().toDartString()}", + ); + final secretPrivateKeyDefault = walletCore.TWHDWalletGetKeyForCoin( + walletImp, + coinType, + ); + final addressDefault = walletCore.TWStringUTF8Bytes( + walletCore.TWCoinTypeDeriveAddress(coinType, secretPrivateKeyDefault), + ).cast().toDartString(); + print("Address from default key: '$addressDefault'"); + + // Alternative: Derive address using custom derivation path + final customDerivationPath = walletCore.TWStringCreateWithUTF8Bytes( + "m/44'/60'/1'/0/0".toNativeUtf8().cast(), + ); + final secretPrivateKeyCustom = walletCore.TWHDWalletGetKey( + walletImp, + coinType, + customDerivationPath, + ); + walletCore.TWStringDelete(customDerivationPath); + final addressCustom = walletCore.TWStringUTF8Bytes( + walletCore.TWCoinTypeDeriveAddress(coinType, secretPrivateKeyCustom), + ).cast().toDartString(); + print("Custom-derived address: '$addressCustom'"); + print(""); + + print( + "RECEIVE funds: Perform send from somewhere else to this address: $address", + ); + print(""); + + // Steps for sending: + // 1. put together a send message (contains sender and receiver address, amount, gas price, etc.) + // 2. sign this message + // 3. broadcast this message to the P2P network -- not done in this sample + print("SEND funds:"); + const dummyReceiverAddress = "0xC37054b3b48C3317082E7ba872d7753D13da4986"; + final secretPrivKey = walletCore.TWPrivateKeyData(secretPrivateKeyDefault); + + print("preparing transaction (using AnySigner) ... "); + const chainIdB64 = "AQ=="; // base64(parse_hex("01")) + const gasPriceB64 = + "1pOkAA=="; // base64(parse_hex("d693a4")) decimal 3600000000 + const gasLimitB64 = "Ugg="; // base64(parse_hex("5208")) decimal 21000 + const amountB64 = + "A0i8paFgAA=="; // base64(parse_hex("0348bca5a160")) 924400000000000 + final transaction = + "{" + "\"chainId\":\"$chainIdB64" + "\",\"gasPrice\":\"$gasPriceB64" + "\",\"gasLimit\":\"$gasLimitB64" + "\",\"toAddress\":\"$dummyReceiverAddress" + "\",\"transaction\":{\"transfer\":{\"amount\":\"$amountB64" + "\"}}}"; + print("transaction: $transaction"); + + print("signing transaction ... "); + final json = walletCore.TWStringCreateWithUTF8Bytes( + transaction.toNativeUtf8().cast(), + ); + final result = walletCore.TWAnySignerSignJSON( + json, + secretPrivKey, + TWCoinType.TWCoinTypeEthereum, + ); + final signedTransaction = walletCore.TWStringUTF8Bytes( + result, + ).cast().toDartString(); + print("done"); + print( + "Signed transaction data (to be broadcast to network): (len ${signedTransaction.length}) '$signedTransaction'", + ); + // see e.g. https://github.com/flightwallet/decode-eth-tx for checking binary output content + print(""); + walletCore.TWStringDelete(json); + walletCore.TWStringDelete(result); + + print("Bye!"); + walletCore.TWHDWalletDelete(walletImp); + + walletCore.TWStringDelete(passwordTwString); +} diff --git a/flutter/config.yaml b/flutter/config.yaml new file mode 100644 index 00000000000..49f89948405 --- /dev/null +++ b/flutter/config.yaml @@ -0,0 +1,7 @@ +output: 'lib/wallet_core_bindings.dart' +headers: + entry-points: + - 'include/**.h' +name: 'WalletCore' +description: 'Bindings to WalletCore' +silence-enum-warning: true diff --git a/flutter/include b/flutter/include new file mode 120000 index 00000000000..f5030fe8899 --- /dev/null +++ b/flutter/include @@ -0,0 +1 @@ +../include \ No newline at end of file diff --git a/flutter/lib/wallet_core.dart b/flutter/lib/wallet_core.dart new file mode 100644 index 00000000000..e4f25a6bbf3 --- /dev/null +++ b/flutter/lib/wallet_core.dart @@ -0,0 +1,31 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'package:path/path.dart' as path; + +import 'package:flutter/wallet_core_bindings.dart'; + +WalletCore load() { + var libraryPath = path.join( + Directory.current.path, + 'lib', + 'libTrustWalletCore.so', + ); + + if (Platform.isMacOS) { + libraryPath = path.join( + Directory.current.path, + 'lib', + 'libTrustWalletCore.dylib', + ); + } + + if (Platform.isWindows) { + libraryPath = path.join( + Directory.current.path, + 'lib', + 'libTrustWalletCore.dll', + ); + } + + return WalletCore(DynamicLibrary.open(libraryPath)); +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock new file mode 100644 index 00000000000..6a3de8ad0dd --- /dev/null +++ b/flutter/pubspec.lock @@ -0,0 +1,469 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f + url: "https://pub.dev/" + source: hosted + version: "82.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" + url: "https://pub.dev/" + source: hosted + version: "7.4.5" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev/" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev/" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev/" + source: hosted + version: "2.1.2" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev/" + source: hosted + version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev/" + source: hosted + version: "0.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev/" + source: hosted + version: "0.4.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev/" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev/" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "4b8701e48a58f7712492c9b1f7ba0bb9d525644dd66d023b62e1fc8cdb560c8a" + url: "https://pub.dev/" + source: hosted + version: "1.14.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev/" + source: hosted + version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + url: "https://pub.dev/" + source: hosted + version: "3.1.0" + ffi: + dependency: "direct main" + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev/" + source: hosted + version: "2.1.4" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: cb3edbfb68ac5283102a2deb7057913d3a1fb16552dacda0c07eb144497e4891 + url: "https://pub.dev/" + source: hosted + version: "19.0.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev/" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev/" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev/" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev/" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev/" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev/" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev/" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev/" + source: hosted + version: "4.9.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev/" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev/" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev/" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev/" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev/" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev/" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev/" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev/" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev/" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev/" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev/" + source: hosted + version: "1.5.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev/" + source: hosted + version: "3.2.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev/" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev/" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev/" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev/" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev/" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev/" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev/" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev/" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev/" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev/" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev/" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + url: "https://pub.dev/" + source: hosted + version: "1.26.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev/" + source: hosted + version: "0.7.6" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + url: "https://pub.dev/" + source: hosted + version: "0.6.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev/" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "6f82e9ee8e7339f5d8b699317f6f3afc17c80a68ebef1bc0d6f52a678c14b1e6" + url: "https://pub.dev/" + source: hosted + version: "15.0.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev/" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev/" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev/" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev/" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev/" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev/" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 + url: "https://pub.dev/" + source: hosted + version: "2.2.2" +sdks: + dart: ">=3.8.1 <4.0.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml new file mode 100644 index 00000000000..b0ca629c361 --- /dev/null +++ b/flutter/pubspec.yaml @@ -0,0 +1,18 @@ +name: flutter +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.8.1 + +# Add regular dependencies here. +dependencies: + ffi: ^2.1.4 + path: ^1.9.1 + # path: ^1.8.0 + +dev_dependencies: + ffigen: ^19.0.0 + lints: ^5.0.0 + test: ^1.24.0 diff --git a/flutter/test/coin_address_derivation_test.dart b/flutter/test/coin_address_derivation_test.dart new file mode 100644 index 00000000000..88e5ac20d99 --- /dev/null +++ b/flutter/test/coin_address_derivation_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/wallet_core.dart'; +import 'package:flutter/wallet_core_bindings.dart'; +import 'package:test/test.dart'; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +void main() { + test('derive addresses from phrase', () { + final walletCore = load(); + final mnemonic = + 'shoot island position soft burden budget tooth cruel issue economy destroy above'; + + final mnemonicTwString = walletCore.TWStringCreateWithUTF8Bytes( + mnemonic.toNativeUtf8().cast(), + ); + final passwordTwString = walletCore.TWStringCreateWithUTF8Bytes( + "".toNativeUtf8().cast(), + ); + + final wallet = walletCore.TWHDWalletCreateWithMnemonic( + mnemonicTwString, + passwordTwString, + ); + + // Test a few key coins + final coinsAndAddresses = [ + ( + TWCoinType.TWCoinTypeBitcoin, + 'bc1quvuarfksewfeuevuc6tn0kfyptgjvwsvrprk9d', + ), + ( + TWCoinType.TWCoinTypeEthereum, + '0x8f348F300873Fd5DA36950B2aC75a26584584feE', + ), + ( + TWCoinType.TWCoinTypeBinance, + 'bnb12vtaxl9952zm6rwf7v8jerq74pvaf77fcmvzhw', + ), + ( + TWCoinType.TWCoinTypeCosmos, + 'cosmos142j9u5eaduzd7faumygud6ruhdwme98qsy2ekn', + ), + ]; + + for (final coinAndAddress in coinsAndAddresses) { + final coin = coinAndAddress.$1; + final expectedAddress = coinAndAddress.$2; + + final address = walletCore.TWHDWalletGetAddressForCoin(wallet, coin); + final addressStr = walletCore.TWStringUTF8Bytes( + address, + ).cast().toDartString(); + + expect(addressStr, expectedAddress); + + walletCore.TWStringDelete(address); + } + + walletCore.TWHDWalletDelete(wallet); + walletCore.TWStringDelete(mnemonicTwString); + walletCore.TWStringDelete(passwordTwString); + }); +} diff --git a/flutter/test/tw_string_test.dart b/flutter/test/tw_string_test.dart new file mode 100644 index 00000000000..79a84a2976a --- /dev/null +++ b/flutter/test/tw_string_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/wallet_core.dart'; +import 'package:test/test.dart'; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +void main() { + test('twstring', () { + final walletCore = load(); + + final twString = walletCore.TWStringCreateWithUTF8Bytes( + 'Hello, World!'.toNativeUtf8().cast(), + ); + final bytes = walletCore.TWStringUTF8Bytes(twString); + expect(bytes.cast().toDartString(), 'Hello, World!'); + + final size = walletCore.TWStringSize(twString); + expect(size, 13); + + walletCore.TWStringDelete(twString); + }); +} diff --git a/include/TrustWalletCore/TWBarz.h b/include/TrustWalletCore/TWBarz.h deleted file mode 100644 index 1454e219b5d..00000000000 --- a/include/TrustWalletCore/TWBarz.h +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. -#pragma once - -#include "TWBase.h" -#include "TWData.h" -#include "TWString.h" -#include "TWPublicKey.h" - -TW_EXTERN_C_BEGIN - -/// Barz functions -TW_EXPORT_STRUCT -struct TWBarz; - -/// Calculate a counterfactual address for the smart contract wallet -/// -/// \param input The serialized data of ContractAddressInput. -/// \return The address. -TW_EXPORT_STATIC_METHOD -TWString *_Nonnull TWBarzGetCounterfactualAddress(TWData *_Nonnull input); - -/// Returns the init code parameter of ERC-4337 User Operation -/// -/// \param factory Wallet factory address (BarzFactory) -/// \param publicKey Public key for the verification facet -/// \param verificationFacet Verification facet address -/// \return The address. -TW_EXPORT_STATIC_METHOD -TWData *_Nonnull TWBarzGetInitCode(TWString* _Nonnull factory, struct TWPublicKey* _Nonnull publicKey, TWString* _Nonnull verificationFacet, uint32_t salt); - -/// Converts the original ASN-encoded signature from webauthn to the format accepted by Barz -/// -/// \param signature Original signature -/// \param challenge The original challenge that was signed -/// \param authenticatorData Returned from Webauthn API -/// \param clientDataJSON Returned from Webauthn API -/// \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/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index 355134bda05..519cf9c4429 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -68,6 +68,7 @@ enum TWBlockchain { TWBlockchainBitcoinCash = 55, TWBlockchainPactus = 56, TWBlockchainKomodo = 57, + TWBlockchainPolymesh = 58, // Substrate }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index da009c8f1ff..3ee41d4b1a9 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -188,6 +188,9 @@ enum TWCoinType { TWCoinTypeZkLinkNova = 810180, TWCoinTypePactus = 21888, TWCoinTypeSonic = 10000146, + TWCoinTypePolymesh = 595, + TWCoinTypePlasma = 9745, + TWCoinTypeMonad = 10143, // end_of_tw_coin_type_marker_do_not_modify }; diff --git a/include/TrustWalletCore/TWCryptoBoxPublicKey.h b/include/TrustWalletCore/TWCryptoBoxPublicKey.h deleted file mode 100644 index e46ea72feae..00000000000 --- a/include/TrustWalletCore/TWCryptoBoxPublicKey.h +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index f93ad92eb56..00000000000 --- a/include/TrustWalletCore/TWCryptoBoxSecretKey.h +++ /dev/null @@ -1,60 +0,0 @@ -// 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/TWDerivation.h b/include/TrustWalletCore/TWDerivation.h index 3437313e934..41eebac3494 100644 --- a/include/TrustWalletCore/TWDerivation.h +++ b/include/TrustWalletCore/TWDerivation.h @@ -25,6 +25,9 @@ enum TWDerivation { TWDerivationSolanaSolana = 6, TWDerivationStratisSegwit = 7, TWDerivationBitcoinTaproot = 8, + TWDerivationPactusMainnet = 9, + TWDerivationPactusTestnet = 10, + TWDerivationSmartChainStableAccount = 11, // end_of_derivation_enum - USED TO GENERATE CODE }; diff --git a/include/TrustWalletCore/TWEthereum.h b/include/TrustWalletCore/TWEthereum.h deleted file mode 100644 index 9ff49f208b2..00000000000 --- a/include/TrustWalletCore/TWEthereum.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "TWBase.h" -#include "TWString.h" - -TW_EXTERN_C_BEGIN - -TW_EXPORT_STRUCT -struct TWEthereum; - -/// Generate a layer 2 eip2645 derivation path from eth address, layer, application and given index. -/// -/// \param wallet non-null TWHDWallet -/// \param ethAddress non-null Ethereum address -/// \param layer non-null layer 2 name (E.G starkex) -/// \param application non-null layer 2 application (E.G immutablex) -/// \param index non-null layer 2 index (E.G 1) -/// \return a valid eip2645 layer 2 derivation path as a string -TW_EXPORT_STATIC_METHOD -TWString* _Nonnull TWEthereumEip2645GetPath(TWString* _Nonnull ethAddress, TWString* _Nonnull layer, TWString* _Nonnull application, TWString* _Nonnull index); - -TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWStoredKey.h b/include/TrustWalletCore/TWStoredKey.h index 58a07e521c0..20615e776d4 100644 --- a/include/TrustWalletCore/TWStoredKey.h +++ b/include/TrustWalletCore/TWStoredKey.h @@ -51,6 +51,52 @@ struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKey(TWData* _Nonnull priva TW_EXPORT_STATIC_METHOD struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKeyWithEncryption(TWData* _Nonnull privateKey, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption); +/// Imports a private key. +/// +/// \param privateKey Non-null Block of data private key +/// \param name The name of the stored key to import as a non-null string +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \param encryption cipher encryption mode +/// \param derivation derivation of the given coin type +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKeyWithEncryptionAndDerivation(TWData* _Nonnull privateKey, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption, enum TWDerivation derivation); + +/// Imports an encoded private key. +/// +/// \param privateKey Non-null encoded private key +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \note Returned object needs to be deleted with \TWStoredKeyDelete +/// \return Nullptr if the key can't be imported, the stored key otherwise +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKeyEncoded(TWString* _Nonnull privateKey, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin); + +/// Imports an encoded private key. +/// +/// \param privateKey Non-null encoded private key +/// \param name The name of the stored key to import as a non-null string +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \param encryption cipher encryption mode +/// \note Returned object needs to be deleted with \TWStoredKeyDelete +/// \return Nullptr if the key can't be imported, the stored key otherwise +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKeyEncodedWithEncryption(TWString* _Nonnull privateKey, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption); + +/// Imports an encoded private key. +/// +/// \param privateKey Non-null encoded private key +/// \param name The name of the stored key to import as a non-null string +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \param encryption cipher encryption mode +/// \param derivation derivation of the given coin type +/// \note Returned object needs to be deleted with \TWStoredKeyDelete +/// \return Nullptr if the key can't be imported, the stored key otherwise +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportPrivateKeyEncodedWithEncryptionAndDerivation(TWString* _Nonnull privateKey, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption, enum TWDerivation derivation); + /// Imports an HD wallet. /// /// \param mnemonic Non-null bip39 mnemonic @@ -253,6 +299,21 @@ bool TWStoredKeyStore(struct TWStoredKey* _Nonnull key, TWString* _Nonnull path) TW_EXPORT_METHOD TWData* _Nullable TWStoredKeyDecryptPrivateKey(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password); +/// Decrypts the encoded private key. +/// +/// \param key Non-null pointer to a stored key +/// \param password Non-null block of data, password of the stored key +/// \return Decrypted encoded private key as a string if success, null pointer otherwise +TW_EXPORT_METHOD +TWString* _Nullable TWStoredKeyDecryptPrivateKeyEncoded(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password); + +/// Whether the private key is encoded. +/// +/// \param key Non-null pointer to a stored key +/// \return true if the private key is encoded, false otherwise +TW_EXPORT_PROPERTY +bool TWStoredKeyHasPrivateKeyEncoded(struct TWStoredKey* _Nonnull key); + /// Decrypts the mnemonic phrase. /// /// \param key Non-null pointer to a stored key diff --git a/include/TrustWalletCore/TWWebAuthn.h b/include/TrustWalletCore/TWWebAuthn.h index c3f24b27a2b..c104c346790 100644 --- a/include/TrustWalletCore/TWWebAuthn.h +++ b/include/TrustWalletCore/TWWebAuthn.h @@ -24,7 +24,7 @@ struct TWPublicKey *_Nullable TWWebAuthnGetPublicKey(TWData *_Nonnull attestatio /// \param signature ASN encoded webauthn signature: https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types /// \return Concatenated r and s values. TW_EXPORT_STATIC_METHOD -TWData *_Nonnull TWWebAuthnGetRSValues(TWData *_Nonnull signature); +TWData *_Nullable TWWebAuthnGetRSValues(TWData *_Nonnull signature); /// Reconstructs the original message that was signed via P256 curve. Can be used for signature validation. /// diff --git a/jni/android/AnySigner.c b/jni/android/AnySigner.c index fd1704562f5..b02c28c9941 100644 --- a/jni/android/AnySigner.c +++ b/jni/android/AnySigner.c @@ -10,6 +10,7 @@ #include "TWJNI.h" jbyteArray JNICALL Java_wallet_core_java_AnySigner_nativeSign(JNIEnv *env, jclass thisClass, jbyteArray input, jint coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, input, "input"); TWData *inputData = TWDataCreateWithJByteArray(env, input); TWData *outputData = TWAnySignerSign(inputData, coin); jbyteArray resultData = TWDataJByteArray(outputData, env); @@ -22,7 +23,9 @@ jboolean JNICALL Java_wallet_core_java_AnySigner_supportsJSON(JNIEnv *env, jclas } jstring JNICALL Java_wallet_core_java_AnySigner_signJSON(JNIEnv *env, jclass thisClass, jstring json, jbyteArray key, jint coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, json, "json"); TWString *jsonString = TWStringCreateWithJString(env, json); + JNI_CHECK_NULL_AND_RETURN_NULL(env, key, "key"); TWData *keyData = TWDataCreateWithJByteArray(env, key); TWString *result = TWAnySignerSignJSON(jsonString, keyData, coin); TWDataDelete(keyData); @@ -31,6 +34,7 @@ jstring JNICALL Java_wallet_core_java_AnySigner_signJSON(JNIEnv *env, jclass thi } jbyteArray JNICALL Java_wallet_core_java_AnySigner_nativePlan(JNIEnv *env, jclass thisClass, jbyteArray input, jint coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, input, "input"); TWData *inputData = TWDataCreateWithJByteArray(env, input); TWData *outputData = TWAnySignerPlan(inputData, coin); jbyteArray resultData = TWDataJByteArray(outputData, env); diff --git a/jni/cpp/TWJNI.h b/jni/cpp/TWJNI.h index 86fc962ca29..938b691c5b3 100644 --- a/jni/cpp/TWJNI.h +++ b/jni/cpp/TWJNI.h @@ -19,3 +19,36 @@ #include #include "TWJNIData.h" #include "TWJNIString.h" + +#define JNI_CHECK_NULL_AND_RETURN_VOID(env, param, paramName) \ + do { \ + if (param == NULL) { \ + jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); \ + if (exceptionClass != NULL) { \ + (*env)->ThrowNew(env, exceptionClass, paramName " parameter cannot be null"); \ + } \ + return; \ + } \ + } while(0) + +#define JNI_CHECK_NULL_AND_RETURN_ZERO(env, param, paramName) \ + do { \ + if (param == NULL) { \ + jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); \ + if (exceptionClass != NULL) { \ + (*env)->ThrowNew(env, exceptionClass, paramName " parameter cannot be null"); \ + } \ + return 0; \ + } \ + } while(0) + +#define JNI_CHECK_NULL_AND_RETURN_NULL(env, param, paramName) \ + do { \ + if (param == NULL) { \ + jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); \ + if (exceptionClass != NULL) { \ + (*env)->ThrowNew(env, exceptionClass, paramName " parameter cannot be null"); \ + } \ + return NULL; \ + } \ + } while(0) diff --git a/jni/kotlin/AnySigner.c b/jni/kotlin/AnySigner.c index 83211fb3512..6d64fa4ebf4 100644 --- a/jni/kotlin/AnySigner.c +++ b/jni/kotlin/AnySigner.c @@ -10,10 +10,12 @@ #include "TWJNI.h" jbyteArray JNICALL Java_com_trustwallet_core_AnySigner_sign(JNIEnv *env, jclass thisClass, jbyteArray input, jobject coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, coin, "coin"); jclass coinClass = (*env)->GetObjectClass(env, coin); jmethodID coinValueMethodID = (*env)->GetMethodID(env, coinClass, "value", "()I"); uint32_t coinValue = (*env)->CallIntMethod(env, coin, coinValueMethodID); + JNI_CHECK_NULL_AND_RETURN_NULL(env, input, "input"); TWData *inputData = TWDataCreateWithJByteArray(env, input); TWData *outputData = TWAnySignerSign(inputData, coinValue); jbyteArray resultData = TWDataJByteArray(outputData, env); @@ -22,6 +24,7 @@ jbyteArray JNICALL Java_com_trustwallet_core_AnySigner_sign(JNIEnv *env, jclass } jboolean JNICALL Java_com_trustwallet_core_AnySigner_supportsJson(JNIEnv *env, jclass thisClass, jobject coin) { + JNI_CHECK_NULL_AND_RETURN_ZERO(env, coin, "coin"); jclass coinClass = (*env)->GetObjectClass(env, coin); jmethodID coinValueMethodID = (*env)->GetMethodID(env, coinClass, "value", "()I"); uint32_t coinValue = (*env)->CallIntMethod(env, coin, coinValueMethodID); @@ -29,11 +32,15 @@ jboolean JNICALL Java_com_trustwallet_core_AnySigner_supportsJson(JNIEnv *env, j } jstring JNICALL Java_com_trustwallet_core_AnySigner_signJson(JNIEnv *env, jclass thisClass, jstring json, jbyteArray key, jobject coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, coin, "coin"); jclass coinClass = (*env)->GetObjectClass(env, coin); jmethodID coinValueMethodID = (*env)->GetMethodID(env, coinClass, "value", "()I"); uint32_t coinValue = (*env)->CallIntMethod(env, coin, coinValueMethodID); + JNI_CHECK_NULL_AND_RETURN_NULL(env, json, "json"); TWString *jsonString = TWStringCreateWithJString(env, json); + + JNI_CHECK_NULL_AND_RETURN_NULL(env, key, "key"); TWData *keyData = TWDataCreateWithJByteArray(env, key); TWString *result = TWAnySignerSignJSON(jsonString, keyData, coinValue); TWDataDelete(keyData); @@ -42,10 +49,12 @@ jstring JNICALL Java_com_trustwallet_core_AnySigner_signJson(JNIEnv *env, jclass } jbyteArray JNICALL Java_com_trustwallet_core_AnySigner_plan(JNIEnv *env, jclass thisClass, jbyteArray input, jobject coin) { + JNI_CHECK_NULL_AND_RETURN_NULL(env, coin, "coin"); jclass coinClass = (*env)->GetObjectClass(env, coin); jmethodID coinValueMethodID = (*env)->GetMethodID(env, coinClass, "value", "()I"); uint32_t coinValue = (*env)->CallIntMethod(env, coin, coinValueMethodID); + JNI_CHECK_NULL_AND_RETURN_NULL(env, input, "input"); TWData *inputData = TWDataCreateWithJByteArray(env, input); TWData *outputData = TWAnySignerPlan(inputData, coinValue); jbyteArray resultData = TWDataJByteArray(outputData, env); diff --git a/kotlin/gradlew b/kotlin/gradlew index f5feea6d6b1..46695364f36 100755 --- a/kotlin/gradlew +++ b/kotlin/gradlew @@ -108,7 +108,7 @@ cygwin=false msys=false darwin=false nonstop=false -case "$( uname )" in #( +case "$( uname -s )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( 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 77417a3bc42..8765207cf4b 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 @@ -40,7 +40,7 @@ class CoinAddressDerivationTests { Fantom, Celo, CronosChain, SmartBitcoinCash, KuCoinCommunityChain, Boba, Metis, 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, Sonic, + ZetaEVM, Merlin, Lightlink, Blast, BounceBit, ZkLinkNova, Sonic, Plasma, Monad, -> "0x8f348F300873Fd5DA36950B2aC75a26584584feE" Ronin -> "ronin:8f348F300873Fd5DA36950B2aC75a26584584feE" @@ -95,6 +95,7 @@ class CoinAddressDerivationTests { Acala -> "25GGezx3LWFQj6HZpYzoWoVzLsHojGtybef3vthC9nd19ms3" Kusama -> "G9xV2EatmrjRC1FLPexc3ddqNRRzCsAdURU8RFiAAJX6ppY" Polkadot -> "13nN6BGAoJwd7Nw1XxeBCx5YcBXuYnL94Mh7i3xBprqVSsFk" + Polymesh -> "2DHK8VhBpacs9quk78AVP9TmmcG5iXi2oKtZqneSNsVXxCKw" Pivx -> "D81AqC8zKma3Cht4TbVuh4jyVVyLkZULCm" Kava -> "kava1drpa0x9ptz0fql3frv562rcrhj2nstuz3pas87" Cardano -> "addr1qyr8jjfnypp95eq74aqzn7ss687ehxclgj7mu6gratmg3mul2040vt35dypp042awzsjk5xm3zr3zm5qh7454uwdv08s84ray2" diff --git a/registry.json b/registry.json index c4815092bb7..8d0ed91e1bd 100644 --- a/registry.json +++ b/registry.json @@ -1343,6 +1343,7 @@ "p2shPrefix": 189, "publicKeyHasher": "sha256ripemd", "base58Hasher": "sha256d", + "hrp": "tex", "explorer": { "url": "https://blockchair.com/zcash", "txPath": "/transaction/", @@ -3089,6 +3090,11 @@ "derivation": [ { "path": "m/44'/60'/0'/0/0" + }, + { + "name": "stable_account", + "path": "m/44'/60'/9172'/3604/9999", + "description": "TrustWallet's specific derivation path for Stable Account wallets" } ], "curve": "secp256k1", @@ -4827,7 +4833,12 @@ "blockchain": "Pactus", "derivation": [ { + "name": "mainnet", "path": "m/44'/21888'/3'/0'" + }, + { + "name": "testnet", + "path": "m/44'/21777'/3'/0'" } ], "curve": "ed25519", @@ -4844,5 +4855,95 @@ "rpc": "https://docs.pactus.org/api/http", "documentation": "https://docs.pactus.org/" } + }, + { + "id": "polymesh", + "name": "Polymesh", + "coinId": 595, + "symbol": "POLYX", + "decimals": 6, + "blockchain": "Polymesh", + "derivation": [ + { + "path": "m/44'/595'/0'/0'/0'" + } + ], + "curve": "ed25519", + "publicKeyType": "ed25519", + "addressHasher": "keccak256", + "ss58Prefix": 12, + "explorer": { + "url": "https://polymesh.subscan.io/", + "txPath": "/extrinsic/", + "accountPath": "/account/", + "sampleTx": "0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04", + "sampleAccount": "2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv" + }, + "info": { + "url": "https://polymesh.network/", + "source": "https://github.com/PolymeshAssociation/Polymesh", + "rpc": "wss://rpc.polymesh.network/", + "documentation": "https://developers.polymesh.network/" + } + }, + { + "id": "plasma", + "name": "Plasma", + "displayName": "Plasma Mainnet", + "coinId": 9745, + "symbol": "XPL", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "9745", + "addressHasher": "keccak256", + "explorer": { + "url": "https://plasmascan.to/", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0x3700212ec535b4c804363be87ba8a5f5668de6314ed41978f6ad71c5340d4d77", + "sampleAccount": "0x30A3E1F27B60c095E2c87bce1e2ffB24f381C8cd" + }, + "info": { + "url": "https://plasma.to/", + "rpc": "https://rpc.plasma.to/", + "documentation": "https://plasmascan.to/documentation" + } + }, + { + "id": "monad", + "name": "Monad", + "displayName": "Monad", + "coinId": 10143, + "symbol": "MON", + "decimals": 18, + "blockchain": "Ethereum", + "derivation": [ + { + "path": "m/44'/60'/0'/0/0" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1Extended", + "chainId": "143", + "addressHasher": "keccak256", + "explorer": { + "url": "https://monvision.io/", + "txPath": "/tx/", + "accountPath": "/address/", + "sampleTx": "0x8394f9f01bc2ae2cc93e19170bf80c303210f6f4198e5ec3cc99b0cba04962b6", + "sampleAccount": "0x6ab69B482987b0BA1f1c96BDbDC192a80CB09132" + }, + "info": { + "url": "https://www.monad.xyz/", + "rpc": "https://rpc.monad.xyz/", + "documentation": "https://docs.monad.xyz/" + } } ] diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml deleted file mode 100644 index 47684146c24..00000000000 --- a/rust/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[env] -CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9bb86fb9dd8..111c30e594c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -21,6 +21,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -144,6 +153,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -157,7 +172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" dependencies = [ "serde", - "thiserror", + "thiserror 1.0.38", ] [[package]] @@ -366,6 +381,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.1" @@ -413,18 +440,33 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "const-oid" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -534,6 +576,41 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.96", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.96", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -550,6 +627,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -610,6 +697,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.6" @@ -649,6 +742,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "env_logger" version = "0.7.1" @@ -703,6 +808,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "funty" version = "1.1.0" @@ -759,6 +870,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.2" @@ -819,6 +936,36 @@ dependencies = [ "quick-error", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "impl-codec" version = "0.5.1" @@ -848,6 +995,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.7.0" @@ -855,7 +1013,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1010,6 +1169,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1140,6 +1305,24 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1280,22 +1463,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.14" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c78fb8c9293bcd48ef6fce7b4ca950ceaf21210de6e105a883ee280c0f7b9ed" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.14" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9c0c92af03644e4806106281fe2e068ac5bc0ae74a707266d06ea27bccee5f" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.96", ] [[package]] @@ -1380,6 +1563,30 @@ dependencies = [ "cipher", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "sec1" version = "0.7.1" @@ -1423,10 +1630,11 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -1439,11 +1647,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1452,13 +1669,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", + "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1472,13 +1691,44 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.7.0", "itoa", "ryu", "serde", @@ -1603,6 +1853,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -1692,7 +1948,16 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.38", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1706,6 +1971,48 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "toml_datetime" version = "0.6.5" @@ -1718,7 +2025,7 @@ version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.7.0", "toml_datetime", "winnow 0.4.7", ] @@ -1729,7 +2036,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] @@ -1893,6 +2200,7 @@ dependencies = [ "tw_native_injective", "tw_pactus", "tw_polkadot", + "tw_polymesh", "tw_ripple", "tw_ronin", "tw_solana", @@ -1990,6 +2298,7 @@ dependencies = [ "tw_encoding", "tw_hash", "tw_keypair", + "tw_macros", "tw_memory", "tw_misc", "tw_number", @@ -2092,6 +2401,7 @@ dependencies = [ "tw_encoding", "tw_hash", "tw_keypair", + "tw_macros", "tw_memory", "tw_misc", "zeroize", @@ -2201,6 +2511,22 @@ dependencies = [ "tw_substrate", ] +[[package]] +name = "tw_polymesh" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", + "tw_proto", + "tw_scale", + "tw_ss58_address", + "tw_substrate", +] + [[package]] name = "tw_proto" version = "0.1.0" @@ -2266,6 +2592,7 @@ dependencies = [ "tw_keypair", "tw_memory", "tw_misc", + "tw_number", "tw_proto", ] @@ -2301,7 +2628,7 @@ dependencies = [ name = "tw_sui" version = "0.1.0" dependencies = [ - "indexmap", + "indexmap 2.7.0", "move-core-types", "serde", "serde_json", @@ -2410,7 +2737,12 @@ dependencies = [ name = "tw_zcash" version = "0.1.0" dependencies = [ + "bech32", + "postcard", + "serde", + "serde_with", "tw_base58_address", + "tw_bech32_address", "tw_bitcoin", "tw_coin_entry", "tw_encoding", @@ -2499,6 +2831,7 @@ dependencies = [ "tw_coin_registry", "tw_encoding", "tw_ethereum", + "tw_evm", "tw_hash", "tw_keypair", "tw_macros", @@ -2534,6 +2867,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] @@ -2614,6 +2948,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "winnow" version = "0.4.7" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 87f80d4f1f4..09ab546a720 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,6 +16,7 @@ members = [ "chains/tw_native_injective", "chains/tw_pactus", "chains/tw_polkadot", + "chains/tw_polymesh", "chains/tw_ripple", "chains/tw_ronin", "chains/tw_solana", diff --git a/rust/chains/tw_aptos/src/aptos_move_packages.rs b/rust/chains/tw_aptos/src/aptos_move_packages.rs index 6362875763e..913b3393b9c 100644 --- a/rust/chains/tw_aptos/src/aptos_move_packages.rs +++ b/rust/chains/tw_aptos/src/aptos_move_packages.rs @@ -192,22 +192,6 @@ pub fn token_transfers_claim_script( ))) } -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!([]), - )) -} - pub fn fungible_asset_transfer( metadata_address: AccountAddress, to: AccountAddress, diff --git a/rust/chains/tw_aptos/src/transaction_builder.rs b/rust/chains/tw_aptos/src/transaction_builder.rs index 5449f86608d..7fc0fb96eb5 100644 --- a/rust/chains/tw_aptos/src/transaction_builder.rs +++ b/rust/chains/tw_aptos/src/transaction_builder.rs @@ -5,9 +5,8 @@ use crate::address::from_account_error; use crate::aptos_move_packages::{ aptos_account_create_account, aptos_account_transfer, aptos_account_transfer_coins, - coin_transfer, fungible_asset_transfer, managed_coin_register, - token_transfers_cancel_offer_script, token_transfers_claim_script, - token_transfers_offer_script, + coin_transfer, fungible_asset_transfer, 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::{ @@ -124,13 +123,6 @@ impl TransactionFactory { 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)?) }, @@ -200,10 +192,6 @@ impl TransactionFactory { 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( diff --git a/rust/chains/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs index eb4f41c8232..93a7c0de474 100644 --- a/rust/chains/tw_aptos/tests/signer.rs +++ b/rust/chains/tw_aptos/tests/signer.rs @@ -35,9 +35,7 @@ pub struct FungibleAssetTransfer { amount: u64, } -pub struct RegisterToken { - coin_type: TypeTag, -} +pub struct RegisterToken; pub enum OpsDetails { RegisterToken(RegisterToken), @@ -122,17 +120,6 @@ fn setup_proto_transaction<'a>( 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( @@ -598,46 +585,6 @@ fn test_aptos_nft_claim() { }"#); } -// 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() { diff --git a/rust/chains/tw_binance/src/transaction/message/mod.rs b/rust/chains/tw_binance/src/transaction/message/mod.rs index 0879bcf5b3e..4917a852ca1 100644 --- a/rust/chains/tw_binance/src/transaction/message/mod.rs +++ b/rust/chains/tw_binance/src/transaction/message/mod.rs @@ -14,7 +14,7 @@ pub mod side_chain_delegate; pub mod time_lock_order; pub mod token_order; pub mod trade_order; -pub mod tranfer_out_order; +pub mod transfer_out_order; pub trait BinanceMessage { fn to_amino_protobuf(&self) -> SigningResult; @@ -29,7 +29,7 @@ pub trait TWBinanceProto: Sized { fn to_tw_proto(&self) -> Self::Proto<'static>; } -/// Please note that some of the fields are typped such as `SideDelegateOrder`. +/// Please note that some of the fields are typed such as `SideDelegateOrder`. #[derive(Deserialize, Serialize)] #[serde(untagged)] pub enum BinanceMessageEnum { @@ -52,7 +52,7 @@ pub enum BinanceMessageEnum { TokenBurnOrder(token_order::TokenBurnOrder), NewTradeOrder(trade_order::NewTradeOrder), CancelTradeOrder(trade_order::CancelTradeOrder), - TransferOutOrder(tranfer_out_order::TransferOutOrder), + TransferOutOrder(transfer_out_order::TransferOutOrder), } impl TWBinanceProto for BinanceMessageEnum { @@ -107,7 +107,7 @@ impl TWBinanceProto for BinanceMessageEnum { .map(BinanceMessageEnum::TokenBurnOrder) }, BinanceMessageProto::transfer_out_order(ref order) => { - tranfer_out_order::TransferOutOrder::from_tw_proto(coin, order) + transfer_out_order::TransferOutOrder::from_tw_proto(coin, order) .map(BinanceMessageEnum::TransferOutOrder) }, BinanceMessageProto::side_delegate_order(ref order) => { diff --git a/rust/chains/tw_binance/src/transaction/message/tranfer_out_order.rs b/rust/chains/tw_binance/src/transaction/message/transfer_out_order.rs similarity index 100% rename from rust/chains/tw_binance/src/transaction/message/tranfer_out_order.rs rename to rust/chains/tw_binance/src/transaction/message/transfer_out_order.rs diff --git a/rust/chains/tw_bitcoin/src/context.rs b/rust/chains/tw_bitcoin/src/context.rs index 5b65c0d0281..2da57d60edc 100644 --- a/rust/chains/tw_bitcoin/src/context.rs +++ b/rust/chains/tw_bitcoin/src/context.rs @@ -4,8 +4,8 @@ use crate::modules::protobuf_builder::standard_protobuf_builder::StandardProtobufBuilder; use crate::modules::protobuf_builder::ProtobufBuilder; -use crate::modules::psbt_request::standard_psbt_request_builder::StandardPsbtRequestBuilder; -use crate::modules::psbt_request::PsbtRequestBuilder; +use crate::modules::psbt_request::standard_psbt_request_handler::StandardPsbtRequestHandler; +use crate::modules::psbt_request::PsbtRequestHandler; use crate::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder; use crate::modules::signing_request::SigningRequestBuilder; use tw_coin_entry::error::prelude::SigningResult; @@ -21,7 +21,7 @@ use tw_utxo::transaction::standard_transaction::Transaction; pub trait BitcoinSigningContext: UtxoContext + Sized { type SigningRequestBuilder: SigningRequestBuilder; type ProtobufBuilder: ProtobufBuilder; - type PsbtRequestBuilder: PsbtRequestBuilder; + type PsbtRequestHandler: PsbtRequestHandler; } #[derive(Default)] @@ -31,6 +31,7 @@ impl UtxoContext for StandardBitcoinContext { type Address = StandardBitcoinAddress; type Transaction = Transaction; type FeeEstimator = StandardFeeEstimator; + type Psbt = bitcoin::psbt::Psbt; fn addr_to_script_pubkey( addr: &Self::Address, @@ -49,5 +50,5 @@ impl UtxoContext for StandardBitcoinContext { impl BitcoinSigningContext for StandardBitcoinContext { type SigningRequestBuilder = StandardSigningRequestBuilder; type ProtobufBuilder = StandardProtobufBuilder; - type PsbtRequestBuilder = StandardPsbtRequestBuilder; + type PsbtRequestHandler = StandardPsbtRequestHandler; } diff --git a/rust/chains/tw_bitcoin/src/modules/compiler.rs b/rust/chains/tw_bitcoin/src/modules/compiler.rs index c12ed068257..f6e4a5fa938 100644 --- a/rust/chains/tw_bitcoin/src/modules/compiler.rs +++ b/rust/chains/tw_bitcoin/src/modules/compiler.rs @@ -4,7 +4,7 @@ use crate::context::BitcoinSigningContext; use crate::modules::protobuf_builder::ProtobufBuilder; -use crate::modules::psbt_request::{PsbtRequest, PsbtRequestBuilder}; +use crate::modules::psbt_request::{PsbtRequest, PsbtRequestHandler}; use crate::modules::signing_request::SigningRequestBuilder; use std::borrow::Cow; use std::marker::PhantomData; @@ -53,7 +53,7 @@ impl BitcoinCompiler { TxPlanner::plan(request)?.unsigned_tx }, TransactionType::psbt(ref psbt) => { - Context::PsbtRequestBuilder::build(&input, psbt)?.unsigned_tx + Context::PsbtRequestHandler::parse_request(&input, psbt)?.unsigned_tx }, TransactionType::None => { return SigningError::err(SigningErrorType::Error_invalid_params) @@ -137,7 +137,8 @@ impl BitcoinCompiler { psbt: &Proto::Psbt, signatures: Vec, ) -> SigningResult> { - let PsbtRequest { unsigned_tx, .. } = Context::PsbtRequestBuilder::build(input, psbt)?; + let PsbtRequest { unsigned_tx, .. } = + Context::PsbtRequestHandler::parse_request(input, psbt)?; let fee = unsigned_tx.fee()?; SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?; diff --git a/rust/chains/tw_bitcoin/src/modules/mod.rs b/rust/chains/tw_bitcoin/src/modules/mod.rs index fc470aafecb..9363ec45e0b 100644 --- a/rust/chains/tw_bitcoin/src/modules/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/mod.rs @@ -5,7 +5,6 @@ pub mod compiler; pub mod planner; pub mod protobuf_builder; -pub mod psbt; pub mod psbt_request; pub mod signer; pub mod signing_request; diff --git a/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs b/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs index 3543a5b1fd8..8aa40f58588 100644 --- a/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs +++ b/rust/chains/tw_bitcoin/src/modules/planner/psbt_planner.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::context::BitcoinSigningContext; -use crate::modules::psbt_request::{PsbtRequest, PsbtRequestBuilder}; +use crate::modules::psbt_request::{PsbtRequest, PsbtRequestHandler}; use crate::modules::signing_request::standard_signing_request::chain_info; use crate::modules::tx_builder::script_parser::StandardScriptParser; use crate::modules::tx_builder::BitcoinChainInfo; @@ -32,7 +32,7 @@ impl PsbtPlanner { ) -> SigningResult> { let chain_info = chain_info(coin, &input.chain_info)?; let PsbtRequest { unsigned_tx, .. } = - Context::PsbtRequestBuilder::build(input, psbt_input)?; + Context::PsbtRequestHandler::parse_request(input, psbt_input)?; let total_input = unsigned_tx.total_input()?; let fee_estimate = unsigned_tx.fee()?; diff --git a/rust/chains/tw_bitcoin/src/modules/psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt.rs deleted file mode 100644 index 41582de3a8c..00000000000 --- a/rust/chains/tw_bitcoin/src/modules/psbt.rs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -use bitcoin::psbt::Psbt; -use tw_utxo::transaction::transaction_interface::{TransactionInterface, TxInputInterface}; - -/// 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.has_script_sig() { - utxo_psbt.final_script_sig = Some(bitcoin::ScriptBuf::from_bytes( - signed_txin.script_sig().to_vec(), - )); - } - - if let Some(witness) = signed_txin.witness() { - if witness.is_empty() { - continue; - } - - let mut final_witness = bitcoin::Witness::new(); - for witness_item in 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 index 1964243f55a..4b216140c5c 100644 --- a/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs @@ -2,38 +2,62 @@ // // Copyright © 2017 Trust Wallet. -use bitcoin::psbt::Psbt; use std::marker::PhantomData; use tw_coin_entry::error::prelude::*; +use tw_memory::Data; use tw_proto::BitcoinV2::Proto; use tw_utxo::context::UtxoContext; use tw_utxo::transaction::unsigned_transaction::UnsignedTransaction; pub mod output_psbt; -pub mod standard_psbt_request_builder; +pub mod standard_psbt_request_handler; pub mod utxo_psbt; -pub trait PsbtRequestBuilder { - fn build( +pub trait PsbtRequestHandler { + /// Parses a PSBT request from Protobuf. + fn parse_request( input: &Proto::SigningInput, psbt_input: &Proto::Psbt, ) -> SigningResult>; + + /// Finalizes the [Partially Signed Bitcoin Transaction](Psbt) + /// by updating the final `script_sig` and/or `witness`. + fn update_signed( + psbt: &mut Context::Psbt, + signed_tx: &Context::Transaction, + ) -> SigningResult<()>; + + /// Serializes the PSBT into bytes. + fn serialize_psbt(psbt: &Context::Psbt) -> SigningResult; } pub struct PsbtRequest { - pub psbt: Psbt, + pub psbt: Context::Psbt, pub unsigned_tx: UnsignedTransaction, - _phantom: PhantomData, + pub _phantom: PhantomData, } pub struct NoPsbtRequestBuilder; -impl PsbtRequestBuilder for NoPsbtRequestBuilder { - fn build( +impl PsbtRequestHandler for NoPsbtRequestBuilder { + fn parse_request( _input: &Proto::SigningInput, _psbt_input: &Proto::Psbt, ) -> SigningResult> { SigningError::err(SigningErrorType::Error_not_supported) .context("PSBT signing is not supported") } + + fn update_signed( + _psbt: &mut Context::Psbt, + _signed_tx: &Context::Transaction, + ) -> SigningResult<()> { + SigningError::err(SigningErrorType::Error_not_supported) + .context("PSBT signing is not supported") + } + + fn serialize_psbt(_psbt: &Context::Psbt) -> SigningResult { + SigningError::err(SigningErrorType::Error_not_supported) + .context("PSBT signing is not supported") + } } diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_builder.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_handler.rs similarity index 60% rename from rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_builder.rs rename to rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_handler.rs index ce0636511fd..99f24d029eb 100644 --- a/rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_builder.rs +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/standard_psbt_request_handler.rs @@ -4,23 +4,26 @@ use crate::modules::psbt_request::output_psbt::OutputPsbt; use crate::modules::psbt_request::utxo_psbt::UtxoPsbt; -use crate::modules::psbt_request::{PsbtRequest, PsbtRequestBuilder}; +use crate::modules::psbt_request::{PsbtRequest, PsbtRequestHandler}; use crate::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder; -use bitcoin::psbt::Psbt; use std::marker::PhantomData; use tw_coin_entry::error::prelude::*; +use tw_memory::Data; use tw_proto::BitcoinV2::Proto; use tw_utxo::context::UtxoContext; use tw_utxo::transaction::standard_transaction::builder::TransactionBuilder; use tw_utxo::transaction::standard_transaction::Transaction; +use tw_utxo::transaction::transaction_interface::{TransactionInterface, TxInputInterface}; -pub struct StandardPsbtRequestBuilder; +pub use bitcoin::psbt::Psbt; -impl PsbtRequestBuilder for StandardPsbtRequestBuilder +pub struct StandardPsbtRequestHandler; + +impl PsbtRequestHandler for StandardPsbtRequestHandler where - Context: UtxoContext, + Context: UtxoContext, { - fn build( + fn parse_request( input: &Proto::SigningInput, psbt_input: &Proto::Psbt, ) -> SigningResult> { @@ -66,4 +69,34 @@ where _phantom: PhantomData, }) } + + fn update_signed( + psbt: &mut Context::Psbt, + signed_tx: &Context::Transaction, + ) -> SigningResult<()> { + for (signed_txin, utxo_psbt) in signed_tx.inputs().iter().zip(psbt.inputs.iter_mut()) { + if signed_txin.has_script_sig() { + utxo_psbt.final_script_sig = Some(bitcoin::ScriptBuf::from_bytes( + signed_txin.script_sig().to_vec(), + )); + } + + if let Some(witness) = signed_txin.witness() { + if witness.is_empty() { + continue; + } + + let mut final_witness = bitcoin::Witness::new(); + for witness_item in witness.as_items() { + final_witness.push(bitcoin::ScriptBuf::from_bytes(witness_item.to_vec())); + } + utxo_psbt.final_script_witness = Some(final_witness); + } + } + Ok(()) + } + + fn serialize_psbt(psbt: &Context::Psbt) -> SigningResult { + Ok(psbt.serialize()) + } } 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 index 57e2d387de7..bdce5d6dc52 100644 --- a/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs @@ -96,7 +96,7 @@ impl<'a> UtxoPsbt<'a> { }, StandardScript::P2SH(_) | StandardScript::P2WSH(_) => { SigningError::err(SigningErrorType::Error_not_supported) - .context("P2SH and P2WSH scriptPubkey's are not supported yet") + .context("P2SH and P2WSH scriptPubkeys are not supported yet") }, StandardScript::OpReturn(_) => SigningError::err(SigningErrorType::Error_invalid_utxo) .context("Cannot spend an OP_RETURN output"), diff --git a/rust/chains/tw_bitcoin/src/modules/signer.rs b/rust/chains/tw_bitcoin/src/modules/signer.rs index 1a3a7dd7bdc..9ee87096906 100644 --- a/rust/chains/tw_bitcoin/src/modules/signer.rs +++ b/rust/chains/tw_bitcoin/src/modules/signer.rs @@ -4,8 +4,7 @@ use crate::context::BitcoinSigningContext; use crate::modules::protobuf_builder::ProtobufBuilder; -use crate::modules::psbt::update_psbt_signed; -use crate::modules::psbt_request::{PsbtRequest, PsbtRequestBuilder}; +use crate::modules::psbt_request::{PsbtRequest, PsbtRequestHandler}; use crate::modules::signing_request::SigningRequestBuilder; use std::borrow::Cow; use std::marker::PhantomData; @@ -89,7 +88,7 @@ impl BitcoinSigner { mut psbt, unsigned_tx, .. - } = Context::PsbtRequestBuilder::build(input, psbt_input)?; + } = Context::PsbtRequestHandler::parse_request(input, psbt_input)?; let fee = unsigned_tx.fee()?; @@ -102,7 +101,7 @@ impl BitcoinSigner { let signed_tx = TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; - update_psbt_signed(&mut psbt, &signed_tx); + Context::PsbtRequestHandler::update_signed(&mut psbt, &signed_tx)?; Ok(Proto::SigningOutput { transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx), @@ -113,7 +112,7 @@ impl BitcoinSigner { fee, weight: signed_tx.weight() as u64, psbt: Some(Proto::Psbt { - psbt: Cow::from(psbt.serialize()), + psbt: Cow::from(Context::PsbtRequestHandler::serialize_psbt(&psbt)?), }), ..Proto::SigningOutput::default() }) 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 index c08cd45bcbc..0a729ef4364 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs @@ -169,7 +169,7 @@ impl<'a, Context: UtxoContext> UtxoProtobuf<'a, Context> { }, StandardScript::P2SH(_) | StandardScript::P2WSH(_) => { SigningError::err(SigningErrorType::Error_not_supported) - .context("P2SH and P2WSH scriptPubkey's are not supported yet") + .context("P2SH and P2WSH scriptPubkeys are not supported yet") }, StandardScript::OpReturn(_) => SigningError::err(SigningErrorType::Error_invalid_utxo) .context("Cannot spend an OP_RETURN output"), diff --git a/rust/chains/tw_bitcoin/tests/babylon_staking.rs b/rust/chains/tw_bitcoin/tests/babylon_staking.rs index 801d7cfc4fe..5ff5a947ae6 100644 --- a/rust/chains/tw_bitcoin/tests/babylon_staking.rs +++ b/rust/chains/tw_bitcoin/tests/babylon_staking.rs @@ -35,12 +35,12 @@ fn test_babylon_scripts() { let expected = &test["expected"]; let covenant_public_keys = parse_pks(¶ms["covenant_public_keys"]); - let convenant_quorum = params["covenant_quorum"].as_u64().unwrap() as u32; + let covenant_quorum = params["covenant_quorum"].as_u64().unwrap() as u32; let finality_provider_public_keys = parse_pks(¶ms["finality_provider_public_keys"]); let staker_public_key = parse_pk(¶ms["staker_public_key"]); let staker_time = params["staking_time"].as_u64().unwrap() as u16; - let covenants = MultiSigOrderedKeys::new(covenant_public_keys, convenant_quorum).unwrap(); + let covenants = MultiSigOrderedKeys::new(covenant_public_keys, covenant_quorum).unwrap(); let finality_providers = MultiSigOrderedKeys::new(finality_provider_public_keys, 1).unwrap(); diff --git a/rust/chains/tw_bitcoin/tests/data/babylon_staking_transactions.json b/rust/chains/tw_bitcoin/tests/data/babylon_staking_transactions.json index eab9db9f383..79672e38438 100644 --- a/rust/chains/tw_bitcoin/tests/data/babylon_staking_transactions.json +++ b/rust/chains/tw_bitcoin/tests/data/babylon_staking_transactions.json @@ -33,7 +33,7 @@ } }, { - "name": "1 finality key, 3/5 covenant committe, 1 staker key with op_return", + "name": "1 finality key, 3/5 covenant committee, 1 staker key with op_return", "parameters": { "covenant_public_keys": [ "02cc5c77da065c490a320834fdcf2c3da70ecd442054c90f874a1edb4669607b83", @@ -70,7 +70,7 @@ } }, { - "name": "3 finality keys, 3/5 covenant committe, 1 staker key with no op_return", + "name": "3 finality keys, 3/5 covenant committee, 1 staker key with no op_return", "parameters": { "covenant_public_keys": [ "02042916c9cd52cfa146118c37b6b118f082bb50fb91da1c51b76dfc2100e66f00", @@ -109,7 +109,7 @@ } }, { - "name": "1 finality keys, 7/9 covenant committe, 1 staker key with op_return", + "name": "1 finality keys, 7/9 covenant committee, 1 staker key with op_return", "parameters": { "covenant_public_keys": [ "0287ed5bb2d036baf209eb49520327f6bd05285dabd30c97f239c3a69ff419950b", @@ -150,7 +150,7 @@ } }, { - "name": "10 finality keys, 18/20 covenant committe, 1 staker key with no op_return", + "name": "10 finality keys, 18/20 covenant committee, 1 staker key with no op_return", "parameters": { "covenant_public_keys": [ "02e3803a6ecff76daf35709c8484f382783d211970f22397d7a258f40ca3b46304", diff --git a/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs b/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs index dd9fbf6c13a..afc87af1ae1 100644 --- a/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs +++ b/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs @@ -19,7 +19,7 @@ pub fn calculate_checksum(prefix: &str, payload: &[u8]) -> u64 { poly_mod(&raw_data) } -pub fn cacl_and_append_checksum(prefix: &str, payload: &[u8]) -> Data { +pub fn calc_and_append_checksum(prefix: &str, payload: &[u8]) -> Data { // The checksum sits in the last eight bytes. // Append the phantom checksum to calculate an actual value. let mut payload_with_checksum: Vec<_> = diff --git a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs index 96bc91e0815..d2211724bc3 100644 --- a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs +++ b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs @@ -100,7 +100,7 @@ impl CashAddress { bech32::convert_bits(&payload, from, to, pad).map_err(|_| AddressError::InvalidInput)? }; - let payload_with_checksum = checksum::cacl_and_append_checksum(hrp, &payload_u5); + let payload_with_checksum = checksum::calc_and_append_checksum(hrp, &payload_u5); let encoded_payload = cash_base32::encode(&payload_with_checksum).map_err(|_| AddressError::InvalidInput)?; Ok(format!("{hrp}:{encoded_payload}")) diff --git a/rust/chains/tw_bitcoincash/src/context.rs b/rust/chains/tw_bitcoincash/src/context.rs index 201d25da009..3a3424f9738 100644 --- a/rust/chains/tw_bitcoincash/src/context.rs +++ b/rust/chains/tw_bitcoincash/src/context.rs @@ -5,7 +5,9 @@ use crate::address::Address; use tw_bitcoin::context::BitcoinSigningContext; use tw_bitcoin::modules::protobuf_builder::standard_protobuf_builder::StandardProtobufBuilder; -use tw_bitcoin::modules::psbt_request::standard_psbt_request_builder::StandardPsbtRequestBuilder; +use tw_bitcoin::modules::psbt_request::standard_psbt_request_handler::{ + Psbt, StandardPsbtRequestHandler, +}; use tw_bitcoin::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder; use tw_coin_entry::error::prelude::*; use tw_utxo::context::{AddressPrefixes, UtxoContext}; @@ -20,6 +22,7 @@ impl UtxoContext for BitcoinCashContext { type Address = Address; type Transaction = Transaction; type FeeEstimator = StandardFeeEstimator; + type Psbt = Psbt; fn addr_to_script_pubkey( addr: &Self::Address, @@ -41,5 +44,5 @@ impl UtxoContext for BitcoinCashContext { impl BitcoinSigningContext for BitcoinCashContext { type SigningRequestBuilder = StandardSigningRequestBuilder; type ProtobufBuilder = StandardProtobufBuilder; - type PsbtRequestBuilder = StandardPsbtRequestBuilder; + type PsbtRequestHandler = StandardPsbtRequestHandler; } diff --git a/rust/chains/tw_decred/src/context.rs b/rust/chains/tw_decred/src/context.rs index f46d43747ee..f25255aea63 100644 --- a/rust/chains/tw_decred/src/context.rs +++ b/rust/chains/tw_decred/src/context.rs @@ -21,6 +21,7 @@ impl UtxoContext for DecredContext { type Address = DecredAddress; type Transaction = DecredTransaction; type FeeEstimator = StandardFeeEstimator; + type Psbt = (); const PUBLIC_KEY_HASHER: Hasher = Hasher::Blake256ripemd; const TX_HASHER: Hasher = Hasher::Blake256; @@ -36,5 +37,5 @@ impl UtxoContext for DecredContext { impl BitcoinSigningContext for DecredContext { type SigningRequestBuilder = DecredSigningRequestBuilder; type ProtobufBuilder = DecredProtobufBuilder; - type PsbtRequestBuilder = NoPsbtRequestBuilder; + type PsbtRequestHandler = NoPsbtRequestBuilder; } diff --git a/rust/chains/tw_decred/src/modules/decred_sighash.rs b/rust/chains/tw_decred/src/modules/decred_sighash.rs index 315bfbb7646..5682dfbc204 100644 --- a/rust/chains/tw_decred/src/modules/decred_sighash.rs +++ b/rust/chains/tw_decred/src/modules/decred_sighash.rs @@ -19,10 +19,10 @@ impl DecredSighash { let mut tx_preimage = tx.clone(); let inputs_to_preimage = LegacySighash::inputs_for_preimage(&tx_preimage, args)?; - let outpus_to_preimage = LegacySighash::outputs_for_preimage(&tx_preimage, args); + let outputs_to_preimage = LegacySighash::outputs_for_preimage(&tx_preimage, args); tx_preimage.replace_inputs(inputs_to_preimage); - tx_preimage.replace_outputs(outpus_to_preimage); + tx_preimage.replace_outputs(outputs_to_preimage); let prefix_hash = Self::tx_prefix_hash(&tx_preimage); let witness_hash = Self::tx_witness_hash(&tx_preimage); diff --git a/rust/chains/tw_groestlcoin/src/context.rs b/rust/chains/tw_groestlcoin/src/context.rs index 6885fa9db04..704164b1629 100644 --- a/rust/chains/tw_groestlcoin/src/context.rs +++ b/rust/chains/tw_groestlcoin/src/context.rs @@ -5,7 +5,9 @@ use crate::address::GroestlAddress; use tw_bitcoin::context::BitcoinSigningContext; use tw_bitcoin::modules::protobuf_builder::standard_protobuf_builder::StandardProtobufBuilder; -use tw_bitcoin::modules::psbt_request::standard_psbt_request_builder::StandardPsbtRequestBuilder; +use tw_bitcoin::modules::psbt_request::standard_psbt_request_handler::{ + Psbt, StandardPsbtRequestHandler, +}; use tw_bitcoin::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder; use tw_coin_entry::error::prelude::SigningResult; use tw_hash::hasher::Hasher; @@ -22,6 +24,7 @@ impl UtxoContext for GroestlContext { type Address = GroestlAddress; type Transaction = Transaction; type FeeEstimator = StandardFeeEstimator; + type Psbt = Psbt; /// Groestlcoin uses a different hash algorithm. const TX_HASHER: Hasher = Sha256; @@ -42,5 +45,5 @@ impl UtxoContext for GroestlContext { impl BitcoinSigningContext for GroestlContext { type SigningRequestBuilder = StandardSigningRequestBuilder; type ProtobufBuilder = StandardProtobufBuilder; - type PsbtRequestBuilder = StandardPsbtRequestBuilder; + type PsbtRequestHandler = StandardPsbtRequestHandler; } diff --git a/rust/chains/tw_komodo/src/context.rs b/rust/chains/tw_komodo/src/context.rs index 4b0cb2ec818..f838e6bf575 100644 --- a/rust/chains/tw_komodo/src/context.rs +++ b/rust/chains/tw_komodo/src/context.rs @@ -21,6 +21,7 @@ impl UtxoContext for KomodoContext { type Address = LegacyAddress; type Transaction = ZcashTransaction; type FeeEstimator = ZcashFeeEstimator; + type Psbt = (); fn addr_to_script_pubkey( addr: &Self::Address, @@ -33,5 +34,5 @@ impl UtxoContext for KomodoContext { impl BitcoinSigningContext for KomodoContext { type SigningRequestBuilder = ZcashSigningRequestBuilder; type ProtobufBuilder = ZcashProtobufBuilder; - type PsbtRequestBuilder = NoPsbtRequestBuilder; + type PsbtRequestHandler = NoPsbtRequestBuilder; } diff --git a/rust/chains/tw_pactus/src/entry.rs b/rust/chains/tw_pactus/src/entry.rs index e9d12f3ac8c..1e0344221a7 100644 --- a/rust/chains/tw_pactus/src/entry.rs +++ b/rust/chains/tw_pactus/src/entry.rs @@ -21,6 +21,7 @@ use tw_proto::TxCompiler::Proto as CompilerProto; use crate::compiler::PactusCompiler; use crate::modules::transaction_util::PactusTransactionUtil; use crate::signer::PactusSigner; +use crate::types::network::Network; use crate::types::Address; pub struct PactusEntry; @@ -60,13 +61,18 @@ impl CoinEntry for PactusEntry { &self, _coin: &dyn CoinContext, public_key: PublicKey, - _derivation: Derivation, + derivation: Derivation, _prefix: Option, ) -> AddressResult { let public_key = public_key .to_ed25519() .ok_or(AddressError::PublicKeyTypeMismatch)?; - Address::from_public_key(public_key) + + match derivation { + Derivation::Default => Address::from_public_key(public_key, Network::Mainnet), + Derivation::Testnet => Address::from_public_key(public_key, Network::Testnet), + _ => AddressResult::Err(AddressError::Unsupported), + } } #[inline] diff --git a/rust/chains/tw_pactus/src/types/address.rs b/rust/chains/tw_pactus/src/types/address.rs index 785e82c6639..aee9b1a6b7e 100644 --- a/rust/chains/tw_pactus/src/types/address.rs +++ b/rust/chains/tw_pactus/src/types/address.rs @@ -17,7 +17,8 @@ use tw_memory::Data; use crate::encoder::error::Error; use crate::encoder::{Decodable, Encodable}; -const ADDRESS_HRP: &str = "pc"; +use super::network::Network; + const TREASURY_ADDRESS_STRING: &str = "000000000000000000000000000000000000000000"; /// Enum for Pactus address types. @@ -66,18 +67,20 @@ impl Decodable for AddressType { /// The hash is computed as RIPEMD160(Blake2b(public key)). #[derive(Debug, Clone, PartialEq)] pub struct Address { + network: Network, addr_type: AddressType, pub_hash: H160, } impl Address { - pub fn from_public_key(public_key: &PublicKey) -> Result { + pub fn from_public_key(public_key: &PublicKey, network: Network) -> Result { let pud_data = public_key.to_bytes(); let pub_hash_data = ripemd_160(&blake2_b(pud_data.as_ref(), 32).map_err(|_| AddressError::Internal)?); let pub_hash = Address::vec_to_pub_hash(pub_hash_data)?; Ok(Address { + network, addr_type: AddressType::Ed25519Account, pub_hash, }) @@ -110,12 +113,12 @@ impl fmt::Display for Address { return f.write_str(TREASURY_ADDRESS_STRING); } + let hrp = self.network.address_hrp().map_err(|_| fmt::Error)?; let mut b32 = Vec::with_capacity(33); b32.push(bech32::u5::try_from_u8(self.addr_type.clone() as u8).map_err(|_| fmt::Error)?); b32.extend_from_slice(&self.pub_hash.to_vec().to_base32()); - bech32::encode_to_fmt(f, ADDRESS_HRP, &b32, bech32::Variant::Bech32m) - .map_err(|_| fmt::Error)? + bech32::encode_to_fmt(f, hrp, &b32, bech32::Variant::Bech32m).map_err(|_| fmt::Error)? } } @@ -146,6 +149,7 @@ impl Decodable for Address { let addr_type = AddressType::decode(r)?; if addr_type == AddressType::Treasury { return Ok(Address { + network: Network::Unknown, addr_type, pub_hash: H160::new(), }); @@ -153,6 +157,7 @@ impl Decodable for Address { let pub_hash = H160::decode(r)?; Ok(Address { + network: Network::Unknown, addr_type, pub_hash, }) @@ -165,16 +170,14 @@ impl FromStr for Address { fn from_str(s: &str) -> Result { if s == TREASURY_ADDRESS_STRING { return Ok(Address { + network: Network::Unknown, addr_type: AddressType::Treasury, pub_hash: H160::new(), }); } let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?; - - if hrp != ADDRESS_HRP { - return Err(AddressError::InvalidHrp); - } + let network = Network::try_from_hrp(&hrp)?; if b32.len() != 33 { return Err(AddressError::InvalidInput); @@ -185,6 +188,7 @@ impl FromStr for Address { let pub_hash = Address::vec_to_pub_hash(b8)?; Ok(Address { + network, addr_type, pub_hash, }) @@ -241,12 +245,20 @@ mod test { .decode_hex() .unwrap(); - let addr = deserialize::
(&data).unwrap(); + let mut addr = deserialize::
(&data).unwrap(); assert!(!addr.is_treasury()); + + addr.network = Network::Mainnet; assert_eq!( addr.to_string(), "pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra" ); + + addr.network = Network::Testnet; + assert_eq!( + addr.to_string(), + "tpc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsrtuyllk" + ); } #[test] @@ -289,6 +301,7 @@ mod test { for case in test_cases { let pub_hash_data = case.pub_hash.decode_hex().unwrap(); let addr = Address { + network: Network::Mainnet, addr_type: case.addr_type, pub_hash: Address::vec_to_pub_hash(pub_hash_data).unwrap(), }; @@ -307,7 +320,7 @@ mod test { "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5", ) .unwrap(); - let address = Address::from_public_key(&private_key.public()).unwrap(); + let address = Address::from_public_key(&private_key.public(), Network::Mainnet).unwrap(); let mut w = Vec::new(); address.encode(&mut w).unwrap(); @@ -323,13 +336,16 @@ mod test { .unwrap(); let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap(); let public_key = private_key.public(); - let address = Address::from_public_key(&public_key).unwrap(); + let mainnet_address = Address::from_public_key(&public_key, Network::Mainnet).unwrap(); + let testnet_address = Address::from_public_key(&public_key, Network::Testnet).unwrap(); let expected_public_key = "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa"; - let expected_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"; + let expected_mainnet_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"; + let expected_testnet_address = "tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg"; assert_eq!(public_key.to_bytes().to_hex(), expected_public_key); - assert_eq!(address.to_string(), expected_address); + assert_eq!(mainnet_address.to_string(), expected_mainnet_address); + assert_eq!(testnet_address.to_string(), expected_testnet_address); } } diff --git a/rust/chains/tw_pactus/src/types/amount.rs b/rust/chains/tw_pactus/src/types/amount.rs index 84c9e156e6a..4efb03b7e7a 100644 --- a/rust/chains/tw_pactus/src/types/amount.rs +++ b/rust/chains/tw_pactus/src/types/amount.rs @@ -5,7 +5,15 @@ pub struct Amount(pub i64); impl Encodable for Amount { fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> { - VarInt::from(self.0 as usize).encode(w) + let amount: usize = match self.0.try_into() { + Ok(amount) => amount, + Err(_) => { + return Err(Error::IoError(std::io::Error::from( + std::io::ErrorKind::InvalidInput, + ))); + }, + }; + VarInt::from(amount).encode(w) } fn encoded_size(&self) -> usize { diff --git a/rust/chains/tw_pactus/src/types/mod.rs b/rust/chains/tw_pactus/src/types/mod.rs index ff66b91c4f4..d7ea9b78918 100644 --- a/rust/chains/tw_pactus/src/types/mod.rs +++ b/rust/chains/tw_pactus/src/types/mod.rs @@ -1,5 +1,6 @@ pub mod address; pub mod amount; +pub mod network; pub mod validator_public_key; pub use address::Address; diff --git a/rust/chains/tw_pactus/src/types/network.rs b/rust/chains/tw_pactus/src/types/network.rs new file mode 100644 index 00000000000..230d531efe7 --- /dev/null +++ b/rust/chains/tw_pactus/src/types/network.rs @@ -0,0 +1,64 @@ +use tw_coin_entry::error::prelude::*; + +/// Represents the type of network (e.g., Mainnet or Testnet). +/// +/// The `CoinType` for Mainnet is defined as `21888`, and for Testnet, it is `21777`. +/// +/// The network type does not affect the decoding or encoding of addresses or transactions. +/// Instead, it is primarily used to facilitate the conversion of an address or public key +/// into its string representation (using bech32m). +/// +/// Note: TrustWallet Core does not provide an API for converting a public key directly +/// to its string representation; it only converts it to a hex representation. +/// However, it provides the API to convert an address to its string representation. +#[derive(Debug, Clone, PartialEq)] +pub enum Network { + /// The network type is either unknown or not explicitly set. + /// + /// Address raw bytes do not inherently carry the network type, + /// but the address string carries the network using an HRP (Human-Readable Part). + /// - Mainnet addresses start with `pc1...`. + /// - Testnet addresses start with `tpc1...`. + /// + /// When deriving an address from a string, the network type is inferred from the HRP. + /// When decoding an address from a public key or raw data, the network type must be explicitly set. + Unknown = 0, + + /// Represents the Mainnet network. + Mainnet = 1, + + /// Represents the Testnet network. + Testnet = 2, +} + +const MAINNET_ADDRESS_HRP: &str = "pc"; +const TESTNET_ADDRESS_HRP: &str = "tpc"; + +pub const MAINNET_PUBLIC_KEY_HRP: &str = "public"; +pub const TESTNET_PUBLIC_KEY_HRP: &str = "tpublic"; + +impl Network { + pub fn try_from_hrp(hrp: &str) -> Result { + match hrp { + MAINNET_ADDRESS_HRP => Ok(Network::Mainnet), + TESTNET_ADDRESS_HRP => Ok(Network::Testnet), + _ => Err(AddressError::InvalidHrp), + } + } + + pub fn address_hrp(&self) -> Result<&'static str, AddressError> { + match &self { + Network::Mainnet => Ok(MAINNET_ADDRESS_HRP), + Network::Testnet => Ok(TESTNET_ADDRESS_HRP), + Network::Unknown => Err(AddressError::InvalidHrp), + } + } + + pub fn public_key_hrp(&self) -> Result<&'static str, AddressError> { + match &self { + Network::Mainnet => Ok(MAINNET_PUBLIC_KEY_HRP), + Network::Testnet => Ok(TESTNET_PUBLIC_KEY_HRP), + Network::Unknown => Err(AddressError::InvalidHrp), + } + } +} diff --git a/rust/chains/tw_pactus/src/types/validator_public_key.rs b/rust/chains/tw_pactus/src/types/validator_public_key.rs index 84425774143..5818d72d56d 100644 --- a/rust/chains/tw_pactus/src/types/validator_public_key.rs +++ b/rust/chains/tw_pactus/src/types/validator_public_key.rs @@ -5,8 +5,9 @@ use bech32::FromBase32; use std::str::FromStr; use tw_keypair::KeyPairError; +use super::network::{MAINNET_PUBLIC_KEY_HRP, TESTNET_PUBLIC_KEY_HRP}; + pub const BLS_PUBLIC_KEY_SIZE: usize = 96; -pub const PUBLIC_KEY_HRP: &str = "public"; #[derive(Debug)] pub struct ValidatorPublicKey(pub [u8; BLS_PUBLIC_KEY_SIZE]); @@ -34,7 +35,7 @@ impl FromStr for ValidatorPublicKey { fn from_str(s: &str) -> Result { let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| KeyPairError::InvalidPublicKey)?; - if hrp != PUBLIC_KEY_HRP { + if hrp != MAINNET_PUBLIC_KEY_HRP && hrp != TESTNET_PUBLIC_KEY_HRP { return Err(KeyPairError::InvalidPublicKey); } @@ -83,6 +84,11 @@ mod test { pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a", pub_key_data: "af0f74917f5065af94727ae9541b0ddcfb5b828a9e016b02498f477ed37fb44d5d882495afb6fd4f9773e4ea9deee436030c4d61c6e3a1151585e1d838cae1444a438d089ce77e10c492a55f6908125c5be9b236a246e4082d08de564e111e65", }, + TestCase { + name: "OK", + pub_key_str: "tpublic1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5fmv7tx", + pub_key_data: "af0f74917f5065af94727ae9541b0ddcfb5b828a9e016b02498f477ed37fb44d5d882495afb6fd4f9773e4ea9deee436030c4d61c6e3a1151585e1d838cae1444a438d089ce77e10c492a55f6908125c5be9b236a246e4082d08de564e111e65", + }, ]; for case in test_cases { diff --git a/rust/chains/tw_polkadot/src/call_encoder/mod.rs b/rust/chains/tw_polkadot/src/call_encoder/mod.rs index db5cb90c2b7..afe4768152e 100644 --- a/rust/chains/tw_polkadot/src/call_encoder/mod.rs +++ b/rust/chains/tw_polkadot/src/call_encoder/mod.rs @@ -1,11 +1,11 @@ -use crate::{ctx_from_tw, KUSAMA, POLKADOT, POLYMESH}; +use crate::{ctx_from_tw, KUSAMA, POLKADOT}; use tw_proto::Polkadot::Proto::{ self, mod_Balance::{BatchAssetTransfer, BatchTransfer, OneOfmessage_oneof as BalanceVariant}, mod_CallIndices::OneOfvariant as CallIndicesVariant, mod_SigningInput::OneOfmessage_oneof as SigningVariant, mod_Staking::{ - Bond, BondAndNominate, Chill, ChillAndUnbond, Nominate, + Bond, BondAndNominate, BondExtra, BondExtraAndNominate, Chill, ChillAndUnbond, Nominate, OneOfmessage_oneof as StakingVariant, Unbond, }, Balance, CallIndices, Staking, @@ -19,9 +19,6 @@ use generic::*; pub mod polkadot; use polkadot::*; -pub mod polymesh; -use polymesh::*; - pub fn validate_call_index(call_index: &Option) -> EncodeResult { let index = match call_index { Some(CallIndices { @@ -56,7 +53,6 @@ impl CallEncoder { let encoder = match ctx.network { POLKADOT => PolkadotCallEncoder::new_boxed(ctx), KUSAMA => KusamaCallEncoder::new_boxed(ctx), - POLYMESH => PolymeshCallEncoder::new_boxed(ctx), _ => PolkadotCallEncoder::new_boxed(ctx), }; Ok(Self { encoder }) @@ -135,6 +131,30 @@ impl CallEncoder { self.encode_batch(vec![first, second], &ban.call_indices) } + fn encode_staking_bond_extra_and_nominate( + &self, + bean: &BondExtraAndNominate, + ) -> EncodeResult { + // Encode a bond call + let first = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::bond_extra(BondExtra { + value: bean.value.clone(), + call_indices: bean.bond_extra_call_indices.clone(), + }), + }))?; + + // Encode a nominate call + let second = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::nominate(Nominate { + nominators: bean.nominators.clone(), + call_indices: bean.nominate_call_indices.clone(), + }), + }))?; + + // Encode both calls as batched + self.encode_batch(vec![first, second], &bean.call_indices) + } + fn encode_staking_chill_and_unbond(&self, cau: &ChillAndUnbond) -> EncodeResult { let first = self.encode_call(&SigningVariant::staking_call(Proto::Staking { message_oneof: StakingVariant::chill(Chill { @@ -159,6 +179,10 @@ impl CallEncoder { let batch = self.encode_staking_bond_and_nominate(ban)?; Ok(Some(batch)) }, + StakingVariant::bond_extra_and_nominate(bean) => { + let batch = self.encode_staking_bond_extra_and_nominate(bean)?; + Ok(Some(batch)) + }, StakingVariant::chill_and_unbond(cau) => { let batch = self.encode_staking_chill_and_unbond(cau)?; Ok(Some(batch)) diff --git a/rust/chains/tw_polkadot/src/entry.rs b/rust/chains/tw_polkadot/src/entry.rs index fc7f469e3b9..04fc068d8b7 100644 --- a/rust/chains/tw_polkadot/src/entry.rs +++ b/rust/chains/tw_polkadot/src/entry.rs @@ -56,7 +56,7 @@ impl PolkadotEntry { builder.extension(CheckEra { era, current_hash }); builder.extension(CheckNonce::new(input.nonce as u32)); if let Some(fee_asset_id) = ctx.fee_asset_id { - builder.extension(ChargeAssetTxPayment::new(tip, fee_asset_id)); + builder.extension(ChargeAssetTxPayment::new(tip, fee_asset_id.as_u32())); } else { builder.extension(ChargeTransactionPayment::new(tip)); } diff --git a/rust/chains/tw_polkadot/src/lib.rs b/rust/chains/tw_polkadot/src/lib.rs index 651203de009..cf91f6c4616 100644 --- a/rust/chains/tw_polkadot/src/lib.rs +++ b/rust/chains/tw_polkadot/src/lib.rs @@ -25,15 +25,22 @@ pub fn network_id_from_tw(input: &'_ Proto::SigningInput<'_>) -> EncodeResult) -> Option { +pub fn fee_asset_id_from_tw(input: &'_ Proto::SigningInput<'_>) -> Option { // Special case for batches. - match &input.message_oneof { - SigningVariant::balance_call(b) => match &b.message_oneof { - BalanceVariant::asset_transfer(at) => Some(at.fee_asset_id), - BalanceVariant::batch_asset_transfer(bat) => Some(bat.fee_asset_id), - _ => None, - }, - _ => None, + if let SigningVariant::balance_call(b) = &input.message_oneof { + match &b.message_oneof { + BalanceVariant::asset_transfer(at) => return Some(FeeAssetId::Asset(at.fee_asset_id)), + BalanceVariant::batch_asset_transfer(bat) => { + return Some(FeeAssetId::Asset(bat.fee_asset_id)) + }, + _ => {}, + } + } + + if input.charge_native_as_asset_tx_payment { + Some(FeeAssetId::Native) + } else { + None } } diff --git a/rust/chains/tw_polkadot/tests/extrinsic.rs b/rust/chains/tw_polkadot/tests/extrinsic.rs index f576982c449..1a379ff1d4d 100644 --- a/rust/chains/tw_polkadot/tests/extrinsic.rs +++ b/rust/chains/tw_polkadot/tests/extrinsic.rs @@ -5,7 +5,6 @@ use tw_encoding::hex::ToHex; use tw_number::U256; use tw_proto::Polkadot::Proto; use tw_proto::Polkadot::Proto::mod_Balance::{AssetTransfer, BatchAssetTransfer, Transfer}; -use tw_proto::Polkadot::Proto::mod_Identity::mod_AddAuthorization::{AuthData, Data}; use tw_proto::Polkadot::Proto::mod_Staking::{ Bond, BondExtra, Chill, Nominate, Rebond, Unbond, WithdrawUnbonded, }; @@ -27,37 +26,6 @@ fn custom_call_indices(module: u8, method: u8) -> Option { }) } -fn polymesh_identity_call( - call: Proto::mod_Identity::OneOfmessage_oneof, -) -> Proto::mod_SigningInput::OneOfmessage_oneof { - Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call(Proto::PolymeshCall { - message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( - Proto::Identity { - message_oneof: call, - }, - ), - }) -} - -fn polymesh_add_auth_call( - add_auth: Proto::mod_Identity::AddAuthorization, -) -> Proto::mod_SigningInput::OneOfmessage_oneof { - polymesh_identity_call(Proto::mod_Identity::OneOfmessage_oneof::add_authorization( - add_auth, - )) -} - -fn polymesh_join_identity(auth_id: u64) -> Proto::mod_SigningInput::OneOfmessage_oneof<'static> { - polymesh_identity_call( - Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key( - Proto::mod_Identity::JoinIdentityAsKey { - call_indices: None, - auth_id, - }, - ), - ) -} - fn balance_call( call: Proto::mod_Balance::OneOfmessage_oneof, ) -> Proto::mod_SigningInput::OneOfmessage_oneof { @@ -75,122 +43,25 @@ fn staking_call( } #[test] -fn polymesh_encode_transfer_with_memo() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000 - +fn polkadot_encode_transfer() { let input = Proto::SigningInput { - network: 12, + network: 0, multi_address: true, message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { - to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + to_address: "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), - memo: "MEMO PADDED WITH SPACES".into(), - call_indices: custom_call_indices(0x05, 0x01), - })), - ..Default::default() - }; - - let encoded = encode_input(&input).expect("error encoding call"); - assert_eq!( - encoded.to_hex(), - "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000" - ); -} - -#[test] -fn polymesh_encode_authorization_join_identity() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 - - let input = Proto::SigningInput { - network: 12, - multi_address: true, - message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { - target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), ..Default::default() - }), - ..Default::default() - }; - - let encoded = encode_input(&input).expect("error encoding call"); - assert_eq!( - encoded.to_hex(), - "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000" - ); -} - -#[test] -fn polymesh_encode_authorization_join_identity_with_zero_data() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000 - - let input = Proto::SigningInput { - network: 12, - multi_address: true, - message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { - target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), - data: Some(AuthData { - asset: Some(Data { - data: (&[0x00]).into(), - }), - extrinsic: Some(Data { - data: (&[0x00]).into(), - }), - portfolio: Some(Data { - data: (&[0x00]).into(), - }), - }), - ..Default::default() - }), - ..Default::default() - }; - - let encoded = encode_input(&input).expect("error encoding call"); - assert_eq!( - encoded.to_hex(), - "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000" - ); -} - -#[test] -fn polymesh_encode_authorization_join_identity_allowing_everything() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 - - let input = Proto::SigningInput { - network: 12, - multi_address: true, - message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { - target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), - data: Some(AuthData { - asset: None, - extrinsic: None, - portfolio: None, - }), - ..Default::default() - }), + })), ..Default::default() }; let encoded = encode_input(&input).expect("error encoding call"); assert_eq!( encoded.to_hex(), - "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000" + "050000a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d04" ); } -#[test] -fn polymesh_encode_identity() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x07040b13000000000000 - - let input = Proto::SigningInput { - network: 12, - multi_address: true, - message_oneof: polymesh_join_identity(4875), - ..Default::default() - }; - - let encoded = encode_input(&input).expect("error encoding call"); - assert_eq!(encoded.to_hex(), "07040b13000000000000"); -} - #[test] fn statemint_encode_asset_transfer() { // tx on mainnet @@ -318,7 +189,7 @@ fn encode_staking_chill() { #[test] fn encode_staking_bond_with_controller() { let input = Proto::SigningInput { - network: 12, + network: 0, multi_address: true, message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { controller: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), @@ -332,7 +203,7 @@ fn encode_staking_bond_with_controller() { let encoded = encode_input(&input).expect("error encoding call"); assert_eq!( encoded.to_hex(), - "11000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" + "07000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" ); } diff --git a/rust/chains/tw_polymesh/Cargo.toml b/rust/chains/tw_polymesh/Cargo.toml new file mode 100644 index 00000000000..1b9e2cfefd9 --- /dev/null +++ b/rust/chains/tw_polymesh/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_polymesh" +version = "0.1.0" +edition = "2021" + +[dependencies] +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_proto = { path = "../../tw_proto" } +tw_scale = { path = "../../tw_scale" } +tw_ss58_address = { path = "../../tw_ss58_address" } +tw_substrate = { path = "../../frameworks/tw_substrate" } diff --git a/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs b/rust/chains/tw_polymesh/src/call_encoder.rs similarity index 60% rename from rust/chains/tw_polkadot/src/call_encoder/polymesh.rs rename to rust/chains/tw_polymesh/src/call_encoder.rs index dde4a047308..848e86b58f7 100644 --- a/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs +++ b/rust/chains/tw_polymesh/src/call_encoder.rs @@ -1,45 +1,40 @@ use std::str::FromStr; +use crate::ctx_from_tw; +use crate::types::*; use tw_coin_entry::error::prelude::*; -use tw_hash::H256; use tw_number::U256; -use tw_proto::Polkadot::Proto::{ +use tw_proto::Polkadot::Proto::{mod_CallIndices::OneOfvariant as CallIndicesVariant, CallIndices}; +use tw_proto::Polymesh::Proto::{ + self, mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, - mod_Identity::{AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant}, - mod_PolymeshCall::OneOfmessage_oneof as PolymeshVariant, + mod_Identity::{ + mod_AddAuthorization::mod_Authorization::OneOfauth_oneof as AuthVariant, AddAuthorization, + JoinIdentityAsKey, LeaveIdentityAsKey, OneOfmessage_oneof as IdentityVariant, + }, + mod_RuntimeCall::OneOfpallet_oneof as RuntimeCallVariant, mod_Staking::{ Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, WithdrawUnbonded, }, - Balance, Identity, Staking, + mod_Utility::{BatchKind, OneOfmessage_oneof as UtilityVariant}, + Balance, Identity, Staking, Utility, }; -use tw_scale::{impl_enum_scale, impl_struct_scale, Compact, RawOwned, ToScale}; +use tw_scale::{impl_enum_scale, Compact, RawOwned, ToScale}; use tw_ss58_address::SS58Address; use tw_substrate::address::SubstrateAddress; - -use super::*; - -impl_struct_scale!( - #[derive(Clone, Debug)] - pub struct Memo(H256); -); - -impl Memo { - pub fn new(memo: &str) -> Self { - let memo = memo.as_bytes(); - let mut bytes = [0; 32]; - let len = memo.len().min(32); - bytes[0..len].copy_from_slice(&memo[0..len]); - - Self(bytes.into()) - } +use tw_substrate::*; + +fn validate_call_index(call_index: &Option) -> EncodeResult { + let index = match call_index { + Some(CallIndices { + variant: CallIndicesVariant::custom(c), + }) => Some((c.module_index, c.method_index)), + _ => None, + }; + CallIndex::from_tw(index) } -impl_struct_scale!( - #[derive(Clone, Debug)] - pub struct IdentityId(H256); -); - impl_enum_scale!( #[derive(Clone, Debug)] pub enum PolymeshBalances { @@ -89,21 +84,10 @@ impl PolymeshBalances { } } -impl_enum_scale!( - #[derive(Clone, Debug)] - pub enum Signatory { - Identity(IdentityId) = 0x00, - Account(AccountId) = 0x01, - } -); - impl_enum_scale!( #[derive(Clone, Debug)] pub enum AuthorizationData { - JoinIdentity { - // TODO: Polymesh permissions. - permissions: RawOwned, - } = 0x05, + JoinIdentity { permissions: Permissions } = 0x05, } ); @@ -113,6 +97,7 @@ impl_enum_scale!( JoinIdentity { auth_id: u64, } = 0x04, + LeaveIdentity = 0x05, AddAuthorization { target: Signatory, data: AuthorizationData, @@ -122,52 +107,43 @@ impl_enum_scale!( ); impl PolymeshIdentity { - fn encode_join_identity(join: &JoinIdentityAsKey) -> WithCallIndexResult { - let ci = validate_call_index(&join.call_indices)?; + fn encode_join_identity(msg: &JoinIdentityAsKey) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; Ok(ci.wrap(Self::JoinIdentity { - auth_id: join.auth_id, + auth_id: msg.auth_id, })) } - fn encode_add_authorization(auth: &AddAuthorization) -> WithCallIndexResult { - let ci = validate_call_index(&auth.call_indices)?; - let target = - SS58Address::from_str(&auth.target).map_err(|_| EncodeError::InvalidAddress)?; - let mut data = Vec::new(); - if let Some(auth_data) = &auth.data { - if let Some(asset) = &auth_data.asset { - data.push(0x01); - data.extend_from_slice(&asset.data); - } else { - data.push(0x00); - } - - if let Some(extrinsic) = &auth_data.extrinsic { - data.push(0x01); - data.extend_from_slice(&extrinsic.data); - } else { - data.push(0x00); - } + fn encode_leave_identity(msg: &LeaveIdentityAsKey) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; + Ok(ci.wrap(Self::LeaveIdentity)) + } - if let Some(portfolio) = &auth_data.portfolio { - data.push(0x01); - data.extend_from_slice(&portfolio.data); - } else { - data.push(0x00); + fn encode_add_authorization(msg: &AddAuthorization) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; + let target = SS58Address::from_str(&msg.target).map_err(|_| EncodeError::InvalidAddress)?; + let data = if let Some(auth) = &msg.authorization { + match &auth.auth_oneof { + AuthVariant::join_identity(perms) => AuthorizationData::JoinIdentity { + permissions: perms.try_into().map_err(|_| EncodeError::InvalidValue)?, + }, + AuthVariant::None => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported Authorization"); + }, } } else { - // Mark everything as authorized (asset, extrinsic, portfolio) - data.push(0x00); - data.push(0x00); - data.push(0x00); - } + return Err(EncodeError::NotSupported) + .into_tw() + .context("Missing Authorization"); + }; + Ok(ci.wrap(Self::AddAuthorization { target: Signatory::Account(SubstrateAddress(target)), - data: AuthorizationData::JoinIdentity { - permissions: RawOwned(data), - }, - expiry: if auth.expiry > 0 { - Some(auth.expiry) + data, + expiry: if msg.expiry > 0 { + Some(msg.expiry) } else { None }, @@ -177,6 +153,7 @@ impl PolymeshIdentity { pub fn encode_call(ident: &Identity) -> WithCallIndexResult { match &ident.message_oneof { IdentityVariant::join_identity_as_key(t) => Self::encode_join_identity(t), + IdentityVariant::leave_identity_as_key(t) => Self::encode_leave_identity(t), IdentityVariant::add_authorization(a) => Self::encode_add_authorization(a), _ => Err(EncodeError::NotSupported) .into_tw() @@ -208,7 +185,7 @@ impl_enum_scale!( Chill = 0x06, Rebond { value: Compact, - } = 0x18, + } = 0x13, } ); @@ -307,54 +284,97 @@ impl PolymeshStaking { } } +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshUtility { + Batch { calls: Vec } = 0x00, + BatchAll { calls: Vec } = 0x02, + ForceBatch { calls: Vec } = 0x04, + } +); + +impl PolymeshUtility { + pub fn encode_call(encoder: &mut CallEncoder, u: &Utility) -> WithCallIndexResult { + if encoder.batch_depth > 0 { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Nested batch calls not allowed"); + } + encoder.batch_depth += 1; + match &u.message_oneof { + UtilityVariant::batch(b) => { + let ci = validate_call_index(&b.call_indices)?; + let calls = b + .calls + .iter() + .map(|call| encoder.encode_runtime_call(call)) + .collect::>>()?; + encoder.batch_depth -= 1; + let batch = match b.kind { + BatchKind::StopOnError => Self::Batch { calls }, + BatchKind::Atomic => Self::BatchAll { calls }, + BatchKind::Optimistic => Self::ForceBatch { calls }, + }; + Ok(ci.wrap(batch)) + }, + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported utility call"), + } + } +} + impl_enum_scale!( #[derive(Clone, Debug)] pub enum PolymeshCall { Balances(PolymeshBalances) = 0x05, Identity(PolymeshIdentity) = 0x07, Staking(PolymeshStaking) = 0x11, - Utility(GenericUtility) = 0x29, + Utility(PolymeshUtility) = 0x29, } ); -pub struct PolymeshCallEncoder; +pub struct CallEncoder { + pub batch_depth: u32, +} + +impl CallEncoder { + pub fn from_ctx(_ctx: &SubstrateContext) -> Self { + Self { batch_depth: 0 } + } -impl PolymeshCallEncoder { - pub fn new_boxed(_ctx: &SubstrateContext) -> Box { - Box::new(Self) + pub fn encode_input(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let mut encoder = Self::from_ctx(&ctx); + let call = input + .runtime_call + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing runtime call")?; + encoder.encode_runtime_call(call) } -} -impl TWPolkadotCallEncoder for PolymeshCallEncoder { - fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult { - let call = match msg { - SigningVariant::balance_call(b) => { - PolymeshBalances::encode_call(b)?.map(PolymeshCall::Balances) + pub fn encode_runtime_call(&mut self, call: &Proto::RuntimeCall) -> EncodeResult { + let call = match &call.pallet_oneof { + RuntimeCallVariant::balance_call(msg) => { + PolymeshBalances::encode_call(msg)?.map(PolymeshCall::Balances) }, - SigningVariant::polymesh_call(msg) => match &msg.message_oneof { - PolymeshVariant::identity_call(msg) => { - PolymeshIdentity::encode_call(msg)?.map(PolymeshCall::Identity) - }, - PolymeshVariant::None => { - return Err(EncodeError::NotSupported) - .into_tw() - .context("Polymesh call variant is None"); - }, + RuntimeCallVariant::identity_call(msg) => { + PolymeshIdentity::encode_call(msg)?.map(PolymeshCall::Identity) + }, + RuntimeCallVariant::staking_call(msg) => { + PolymeshStaking::encode_call(msg)?.map(PolymeshCall::Staking) }, - SigningVariant::staking_call(s) => { - PolymeshStaking::encode_call(s)?.map(PolymeshCall::Staking) + RuntimeCallVariant::utility_call(msg) => { + PolymeshUtility::encode_call(self, msg)?.map(PolymeshCall::Utility) }, - SigningVariant::None => { + RuntimeCallVariant::None => { return Err(EncodeError::NotSupported) .into_tw() - .context("Staking call variant is None"); + .context("Runtime call variant is None"); }, }; Ok(RawOwned(call.to_scale())) } - - fn encode_batch(&self, calls: Vec) -> EncodeResult { - let call = PolymeshCall::Utility(GenericUtility::BatchAll { calls }); - Ok(RawOwned(call.to_scale())) - } } diff --git a/rust/chains/tw_polymesh/src/entry.rs b/rust/chains/tw_polymesh/src/entry.rs new file mode 100644 index 00000000000..843a18b52b6 --- /dev/null +++ b/rust/chains/tw_polymesh/src/entry.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::ctx_from_tw; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +use tw_number::U256; +use tw_proto::Polymesh::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_scale::{RawOwned, ToScale}; +use tw_ss58_address::SS58Address; +use tw_substrate::*; + +use crate::call_encoder::CallEncoder; + +pub struct PolymeshEntry; + +impl PolymeshEntry { + #[inline] + fn get_keypair_impl( + &self, + _coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + Ok(KeyPair::try_from(input.private_key.as_ref())?) + } + + #[inline] + fn build_transaction_impl( + &self, + _coin: &dyn CoinContext, + public_key: Option, + input: &Proto::SigningInput<'_>, + ) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let mut encoder = CallEncoder::from_ctx(&ctx); + let call = input + .runtime_call + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing runtime call")?; + let call = encoder.encode_runtime_call(call)?; + let era = match &input.era { + Some(era) => Era::mortal(era.period, era.block_number), + None => Era::immortal(), + }; + let genesis_hash = input.genesis_hash.as_ref().try_into().unwrap_or_default(); + let current_hash = input.block_hash.as_ref().try_into().unwrap_or_default(); + let tip = U256::from_big_endian_slice(&input.tip) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + let mut builder = TransactionBuilder::new(true, call); + // Add chain extensions. + builder.extension(CheckVersion(input.spec_version)); + builder.extension(CheckVersion(input.transaction_version)); + builder.extension(CheckGenesis(genesis_hash)); + builder.extension(CheckEra { era, current_hash }); + builder.extension(CheckNonce::new(input.nonce as u32)); + builder.extension(ChargeTransactionPayment::new(tip)); + if let Some(public_key) = public_key { + let account = SubstrateAddress( + SS58Address::from_public_key(&public_key, ctx.network).map_err(|e| { + TWError::new(EncodeError::InvalidAddress).context(format!("{e:?}")) + })?, + ); + builder.set_account(account); + } + Ok(builder) + } + + #[inline] + fn signing_output_impl( + &self, + _coin: &dyn CoinContext, + result: SigningResult, + ) -> SigningResult> { + let encoded = result?.to_scale(); + Ok(Proto::SigningOutput { + encoded: encoded.into(), + ..Default::default() + }) + } + + #[inline] + fn presigning_output_impl( + &self, + _coin: &dyn CoinContext, + result: SigningResult, + ) -> SigningResult> { + let pre_image = result?.to_scale(); + Ok(CompilerProto::PreSigningOutput { + // `pre_image` is already hashed if it is larger then 256 bytes. + data_hash: pre_image.clone().into(), + data: pre_image.into(), + ..Default::default() + }) + } +} + +impl SubstrateCoinEntry for PolymeshEntry { + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + #[inline] + fn get_keypair( + &self, + coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + self.get_keypair_impl(coin, input) + } + + #[inline] + fn build_transaction( + &self, + coin: &dyn CoinContext, + public_key: Option, + input: &Self::SigningInput<'_>, + ) -> SigningResult { + self.build_transaction_impl(coin, public_key, input) + .map_err(|e| e.map_err(SigningErrorType::from)) + } + + #[inline] + fn signing_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::SigningOutput { + self.signing_output_impl(coin, result) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + #[inline] + fn presigning_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::PreSigningOutput { + self.presigning_output_impl(coin, result) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } +} diff --git a/rust/chains/tw_polymesh/src/lib.rs b/rust/chains/tw_polymesh/src/lib.rs new file mode 100644 index 00000000000..cc814de9bbd --- /dev/null +++ b/rust/chains/tw_polymesh/src/lib.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_proto::Polymesh::Proto; +use tw_ss58_address::NetworkId; +use tw_substrate::*; + +pub mod call_encoder; +pub mod entry; +pub mod types; + +pub const POLYMESH_PREFIX: u16 = 12; +pub const POLYMESH: NetworkId = NetworkId::new_unchecked(POLYMESH_PREFIX); + +pub fn ctx_from_tw(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + let network = + NetworkId::try_from(input.network as u16).map_err(|_| EncodeError::InvalidNetworkId)?; + let spec_version = input.spec_version; + + Ok(SubstrateContext { + multi_address: true, + network, + spec_version, + transaction_version: input.transaction_version, + fee_asset_id: None, + check_metadata: false, + }) +} diff --git a/rust/chains/tw_polymesh/src/types.rs b/rust/chains/tw_polymesh/src/types.rs new file mode 100644 index 00000000000..6778d08dc26 --- /dev/null +++ b/rust/chains/tw_polymesh/src/types.rs @@ -0,0 +1,312 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use tw_coin_entry::error::prelude::*; +use tw_hash::{Hash, H256}; +use tw_proto::Polkadot::Proto::RewardDestination as TWRewardDestination; +use tw_proto::Polymesh::Proto::{ + mod_SecondaryKeyPermissions::{ + AssetPermissions as TWAssetPermissions, ExtrinsicPermissions as TWExtrinsicPermissions, + PalletPermissions as TWPalletPermissions, PortfolioPermissions as TWPortfolioPermissions, + RestrictionKind as TWRestrictionKind, + }, + AssetId as TWAssetId, IdentityId as TWIdentityId, PortfolioId as TWPortfolioId, + SecondaryKeyPermissions, +}; +use tw_scale::{impl_enum_scale, impl_struct_scale, ToScale}; + +use super::*; + +impl_struct_scale!( + #[derive(Clone, Debug)] + pub struct Memo(H256); +); + +impl Memo { + pub fn new(memo: &str) -> Self { + let memo = memo.as_bytes(); + let mut bytes = [0; 32]; + let len = memo.len().min(32); + bytes[0..len].copy_from_slice(&memo[0..len]); + + Self(bytes.into()) + } +} + +pub type H128 = Hash<16>; + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct AssetId(H128); +); + +impl TryFrom<&TWAssetId<'_>> for AssetId { + type Error = TWError; + + fn try_from(id: &TWAssetId) -> Result { + let did = H128::try_from(id.id.as_ref()) + .map_err(|_| EncodeError::InvalidValue) + .into_tw() + .context("Expected 16 byte AssetId")?; + Ok(Self(did)) + } +} + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct IdentityId(H256); +); + +impl TryFrom<&TWIdentityId<'_>> for IdentityId { + type Error = TWError; + + fn try_from(id: &TWIdentityId) -> Result { + let did = H256::try_from(id.id.as_ref()) + .map_err(|_| EncodeError::InvalidValue) + .into_tw() + .context("Expected 32 byte IdentityId")?; + Ok(Self(did)) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub enum PortfolioKind { + #[default] + Default = 0x00, + User(u64) = 0x01, + } +); + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct PortfolioId { + did: IdentityId, + kind: PortfolioKind, + } +); + +impl TryFrom<&TWPortfolioId<'_>> for PortfolioId { + type Error = TWError; + + fn try_from(portfolio: &TWPortfolioId) -> Result { + Ok(Self { + did: portfolio + .identity + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing portfolio identity")? + .try_into()?, + kind: if portfolio.default { + PortfolioKind::Default + } else { + PortfolioKind::User(portfolio.user) + }, + }) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum Signatory { + Identity(IdentityId) = 0x00, + Account(AccountId) = 0x01, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub enum RestrictionKind { + #[default] + Whole = 0x00, + These = 0x01, + Except = 0x02, + } +); + +impl From for RestrictionKind { + fn from(kind: TWRestrictionKind) -> Self { + match kind { + TWRestrictionKind::Whole => Self::Whole, + TWRestrictionKind::These => Self::These, + TWRestrictionKind::Except => Self::Except, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct AssetPermissions { + kind: RestrictionKind, + assets: BTreeSet, +} + +impl ToScale for AssetPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.assets.to_scale_into(data); + } + } +} + +impl TryFrom<&TWAssetPermissions<'_>> for AssetPermissions { + type Error = TWError; + + fn try_from(perms: &TWAssetPermissions) -> Result { + Ok(Self { + kind: perms.kind.into(), + assets: perms + .assets + .iter() + .map(|asset| asset.try_into()) + .collect::>>()?, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct PortfolioPermissions { + kind: RestrictionKind, + portfolios: BTreeSet, +} + +impl ToScale for PortfolioPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.portfolios.to_scale_into(data); + } + } +} + +impl TryFrom<&TWPortfolioPermissions<'_>> for PortfolioPermissions { + type Error = TWError; + + fn try_from(perms: &TWPortfolioPermissions) -> Result { + Ok(Self { + kind: perms.kind.into(), + portfolios: perms + .portfolios + .iter() + .map(|portfolio| portfolio.try_into()) + .collect::>>()?, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct PalletPermissions { + kind: RestrictionKind, + extrinsic_names: BTreeSet, +} + +impl ToScale for PalletPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.extrinsic_names.to_scale_into(data); + } + } +} + +impl From<&TWPalletPermissions<'_>> for PalletPermissions { + fn from(perms: &TWPalletPermissions) -> Self { + Self { + kind: perms.kind.into(), + extrinsic_names: perms + .extrinsic_names + .iter() + .map(|name| name.to_string()) + .collect(), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ExtrinsicPermissions { + kind: RestrictionKind, + pallets: BTreeMap, +} + +impl ToScale for ExtrinsicPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.pallets.to_scale_into(data); + } + } +} + +impl From<&TWExtrinsicPermissions<'_>> for ExtrinsicPermissions { + fn from(perms: &TWExtrinsicPermissions) -> Self { + Self { + kind: perms.kind.into(), + pallets: perms + .pallets + .iter() + .map(|pallet| (pallet.pallet_name.to_string(), pallet.into())) + .collect(), + } + } +} + +impl_struct_scale!( + #[derive(Clone, Debug, Default)] + pub struct Permissions { + asset: AssetPermissions, + extrinsic: ExtrinsicPermissions, + portfolio: PortfolioPermissions, + } +); + +impl TryFrom<&SecondaryKeyPermissions<'_>> for Permissions { + type Error = TWError; + + fn try_from(perms: &SecondaryKeyPermissions) -> Result { + Ok(Self { + asset: if let Some(perms) = &perms.asset { + perms.try_into()? + } else { + AssetPermissions::default() + }, + extrinsic: if let Some(perms) = &perms.extrinsic { + perms.into() + } else { + ExtrinsicPermissions::default() + }, + portfolio: if let Some(perms) = &perms.portfolio { + perms.try_into()? + } else { + PortfolioPermissions::default() + }, + }) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum AuthorizationData { + JoinIdentity { permissions: Permissions } = 0x05, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum RewardDestination { + Staked = 0x00, + Stash = 0x01, + Controller = 0x02, + Account(AccountId) = 0x03, + None = 0x04, + } +); + +impl RewardDestination { + pub fn from_tw(dest: &TWRewardDestination) -> EncodeResult { + match dest { + TWRewardDestination::STAKED => Ok(Self::Staked), + TWRewardDestination::STASH => Ok(Self::Stash), + TWRewardDestination::CONTROLLER => Ok(Self::Controller), + } + } +} diff --git a/rust/chains/tw_polymesh/tests/extrinsic.rs b/rust/chains/tw_polymesh/tests/extrinsic.rs new file mode 100644 index 00000000000..777efbedce7 --- /dev/null +++ b/rust/chains/tw_polymesh/tests/extrinsic.rs @@ -0,0 +1,732 @@ +use std::borrow::Cow; +use std::default::Default; + +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_number::U256; +use tw_proto::Polkadot::Proto::RewardDestination; +use tw_proto::Polymesh::Proto::{ + self, + mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, + mod_Identity::{ + mod_AddAuthorization::{mod_Authorization::OneOfauth_oneof as AuthVariant, Authorization}, + AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant, + }, + mod_RuntimeCall::OneOfpallet_oneof as CallVariant, + mod_SecondaryKeyPermissions::{ + AssetPermissions, ExtrinsicPermissions, PalletPermissions, PortfolioPermissions, + RestrictionKind, + }, + mod_Staking::{ + Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, + WithdrawUnbonded, + }, + mod_Utility::{Batch, BatchKind, OneOfmessage_oneof as UtilityVariant}, + AssetId, Balance, Identity, IdentityId, PortfolioId, RuntimeCall, SecondaryKeyPermissions, + Staking, Utility, +}; + +use tw_polymesh::call_encoder::CallEncoder; +use tw_substrate::EncodeError; + +fn expect_encoded(input: &Proto::SigningInput<'_>, expected_value: &str) { + let encoded = CallEncoder::encode_input(input).expect("error encoding call"); + assert_eq!(encoded.0.to_hex(), expected_value); +} + +fn polymesh_identity_call(call: IdentityVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::identity_call(Identity { + message_oneof: call, + }), + } +} + +fn polymesh_add_auth_call(add_auth: AddAuthorization) -> RuntimeCall<'_> { + polymesh_identity_call(IdentityVariant::add_authorization(add_auth)) +} + +fn polymesh_join_identity(auth_id: u64) -> RuntimeCall<'static> { + polymesh_identity_call(IdentityVariant::join_identity_as_key(JoinIdentityAsKey { + call_indices: None, + auth_id, + })) +} + +fn balance_call(call: BalanceVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::balance_call(Balance { + message_oneof: call, + }), + } +} + +fn staking_call(call: StakingVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::staking_call(Staking { + message_oneof: call, + }), + } +} + +fn batch_calls(kind: BatchKind, calls: Vec>) -> RuntimeCall<'static> { + RuntimeCall { + pallet_oneof: CallVariant::utility_call(Utility { + message_oneof: UtilityVariant::batch(Batch { + kind, + calls, + ..Default::default() + }), + }), + } +} + +fn build_input(runtime_call: RuntimeCall<'_>) -> Proto::SigningInput<'_> { + Proto::SigningInput { + network: 12, + transaction_version: 7, + runtime_call: Some(runtime_call), + ..Default::default() + } +} + +/// Test POLYX transfer with memo. +#[test] +fn polymesh_encode_transfer_with_memo() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000 + + let input = build_input(balance_call( + Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + }), + )); + + expect_encoded(&input, "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000"); +} + +/// Test add authorization to join identity with default permissions (`Whole` meaning all permissions). +#[test] +fn polymesh_encode_authorization_join_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions::default()), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000", + ); +} + +/// Test add authorization with expiry. +#[test] +fn polymesh_encode_authorization_with_expiry() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c13205000000012a00000000000000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions::default()), + }), + expiry: 42, + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c13205000000012a00000000000000" + ); +} + +/// Test add authorization to join identity with no permissions. +#[test] +fn polymesh_encode_authorization_join_identity_with_zero_data() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // No asset permissions. + asset: Some(AssetPermissions { + kind: RestrictionKind::These, + assets: vec![], + }), + // No extrinsic permissions. + extrinsic: Some(ExtrinsicPermissions { + kind: RestrictionKind::These, + pallets: vec![], + }), + // No portfolio permissions. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::These, + portfolios: vec![], + }), + }), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000", + ); +} + +/// Test add authorization to join identity with custom permissions +#[test] +fn polymesh_encode_authorization_join_identity_with_custom_permissions() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c132050104cadc557ef87f4410b6a4bf446901930e010414417373657401045872656769737465725f756e697175655f7469636b65720108010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001010000000000000000 + + // asset + let asset = "0xcadc557ef87f4410b6a4bf446901930e" + .decode_hex() + .expect("valid asset hex"); + // Identity + let did = "0x0100000000000000000000000000000000000000000000000000000000000000" + .decode_hex() + .expect("valid did hex"); + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // Allow one asset permission. + asset: Some(AssetPermissions { + kind: RestrictionKind::These, + assets: vec![AssetId { id: asset.into() }], + }), + // Allow one extrinsic permissions. + extrinsic: Some(ExtrinsicPermissions { + kind: RestrictionKind::These, + pallets: vec![PalletPermissions { + pallet_name: "Asset".into(), + kind: RestrictionKind::These, + extrinsic_names: vec!["register_unique_ticker".into()], + }], + }), + // Allow some portfolios permissions. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::These, + portfolios: vec![ + // Default portfolio. + PortfolioId { + identity: Some(IdentityId { + id: did.clone().into(), + }), + default: true, + user: 0, + }, + // User portfolio 1. + PortfolioId { + identity: Some(IdentityId { id: did.into() }), + default: false, + user: 1, + }, + ], + }), + }), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c132050104cadc557ef87f4410b6a4bf446901930e010414417373657401045872656769737465725f756e697175655f7469636b65720108010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001010000000000000000" + ); +} + +/// Test add authorization to join identity with all permissions. Each permission is set to `None`, which defaults to `Whole`. +#[test] +fn polymesh_encode_authorization_join_identity_allowing_everything() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // All asset permissions. + asset: None, + // All extrinsic permissions. + extrinsic: None, + // All portfolio permissions. + portfolio: None, + }), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000", + ); +} + +/// Test accepting a join identity authorization. +#[test] +fn polymesh_encode_identity_join_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x07040b13000000000000 + + let input = build_input(polymesh_join_identity(4875)); + + expect_encoded(&input, "07040b13000000000000"); +} + +/// Test staking nominate. +#[test] +fn encode_staking_nominate() { + let input = build_input(staking_call(StakingVariant::nominate(Nominate { + nominators: vec![ + "2DxgKKS53wsAeETAZXhmT5A1bTt7h1aV4bKdtkMDwwSzSMXm".into(), + "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + ], + call_indices: None, + }))); + + expect_encoded(&input, "1105080042ef301451c7f596f974daec8ca1234f66809905d13e16c18e23896b0c57e53e00ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e67148"); +} + +/// Test staking chill. +#[test] +fn encode_staking_chill() { + let input = build_input(staking_call(StakingVariant::chill(Chill { + call_indices: None, + }))); + + expect_encoded(&input, "1106"); +} + +/// Test staking bond. +#[test] +fn encode_staking_bond() { + let input = build_input(staking_call(StakingVariant::bond(Bond { + controller: "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: RewardDestination::STAKED, + call_indices: None, + }))); + + expect_encoded( + &input, + "110000ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e671484652310000", + ); +} + +/// Test staking bond extra. +#[test] +fn encode_staking_bond_extra() { + let input = build_input(staking_call(StakingVariant::bond_extra(BondExtra { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "110146523100"); +} + +/// Test staking rebond. +#[test] +fn encode_staking_rebond() { + let input = build_input(staking_call(StakingVariant::rebond(Rebond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "111346523100"); +} + +/// Test staking unbond. +#[test] +fn encode_staking_unbond() { + let input = build_input(staking_call(StakingVariant::unbond(Unbond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "110246523100"); +} + +/// Test staking withdraw unbonded. +#[test] +fn encode_staking_withdraw_unbonded() { + let input = build_input(staking_call(StakingVariant::withdraw_unbonded( + WithdrawUnbonded { + slashing_spans: 84, + call_indices: None, + }, + ))); + + expect_encoded(&input, "110354000000"); +} + +/// Test atomic batching staking calls bond and nominate. +#[test] +fn encode_staking_batch_bond_and_nominate() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::bond(Bond { + controller: "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: RewardDestination::STAKED, + call_indices: None, + })), + staking_call(StakingVariant::nominate(Nominate { + nominators: vec![ + "2DxgKKS53wsAeETAZXhmT5A1bTt7h1aV4bKdtkMDwwSzSMXm".into(), + "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + ], + call_indices: None, + })), + ], + )); + + expect_encoded( + &input, + "290208110000ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e6714846523100001105080042ef301451c7f596f974daec8ca1234f66809905d13e16c18e23896b0c57e53e00ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e67148" + ); +} + +/// Test atomic batching of staking calls chill and unbond. +#[test] +fn encode_staking_batch_chill_and_unbond() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::chill(Chill { call_indices: None })), + staking_call(StakingVariant::unbond(Unbond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + })), + ], + )); + + expect_encoded(&input, "2902081106110246523100"); +} + +/// Test optimistic batch of POLYX transfers. +#[test] +fn encode_batch_transfers() { + let input = build_input(batch_calls( + BatchKind::Optimistic, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + })), + ], + )); + + expect_encoded( + &input, + "2904080500004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e02040501004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c08014d454d4f20504144444544205749544820535041434553000000000000000000", + ); +} + +/// Test stop on first error batch of POLYX transfers. +#[test] +fn encode_batch_transfers_stop_on_first_error() { + let input = build_input(batch_calls( + BatchKind::StopOnError, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + })), + ], + )); + + expect_encoded( + &input, + "2900080500004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e02040501004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c08014d454d4f20504144444544205749544820535041434553000000000000000000", + ); +} + +/// Test that nesting of batch calls is not allowed. +#[test] +fn encode_nested_batch_calls() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + batch_calls( + BatchKind::Atomic, + vec![balance_call( + Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + }), + )], + ), + ], + )); + + let tw_err = + CallEncoder::encode_input(&input).expect_err("nested batch calls should not be allowed"); + assert_eq!(tw_err.error_type(), &EncodeError::NotSupported); + // Ensure the error message contains the expected context. + let context = format!("{:?}", tw_err); + assert!(context.contains("Nested batch calls not allowed")); +} + +fn expect_encode_err(call: RuntimeCall<'_>, err: EncodeError) { + let input = build_input(call); + let res = CallEncoder::encode_input(&input).expect_err("The call should not be supported"); + assert_eq!(res.error_type(), &err); +} + +/// Test "Unsupported X call" errors. +#[test] +fn unsupported_calls() { + // Invalid runtime call. + expect_encode_err( + RuntimeCall { + pallet_oneof: CallVariant::None, + }, + EncodeError::NotSupported, + ); + + // Invalid balance call. + expect_encode_err( + balance_call(Proto::mod_Balance::OneOfmessage_oneof::None), + EncodeError::NotSupported, + ); + + // Invalid staking call. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::None), + EncodeError::NotSupported, + ); + + // Invalid Utility call. + expect_encode_err( + RuntimeCall { + pallet_oneof: CallVariant::utility_call(Utility { + message_oneof: UtilityVariant::None, + }), + }, + EncodeError::NotSupported, + ); + + // Invalid Identity call. + expect_encode_err( + polymesh_identity_call(Proto::mod_Identity::OneOfmessage_oneof::None), + EncodeError::NotSupported, + ); + + // Invalid Polymesh add authorization type. + expect_encode_err( + polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::None, + }), + ..Default::default() + }), + EncodeError::NotSupported, + ); + + // Invalid Polymesh add authorization (no authorization) + expect_encode_err( + polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: None, + ..Default::default() + }), + EncodeError::NotSupported, + ); + + // Invalid asset id in permissions. + expect_encode_err( + polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // One invalid asset permission. + asset: Some(AssetPermissions { + kind: RestrictionKind::Except, + assets: vec![AssetId { + id: vec![0u8].into(), + }], + }), + ..Default::default() + }), + }), + ..Default::default() + }), + EncodeError::InvalidValue, + ); + + // Invalid identity id in portfolio permissions. + expect_encode_err( + polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // One invalid portfolio permission. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::Except, + portfolios: vec![PortfolioId { + identity: Some(IdentityId { id: vec![0].into() }), + default: true, + user: 0, + }], + }), + ..Default::default() + }), + }), + ..Default::default() + }), + EncodeError::InvalidValue, + ); +} + +/// Test invalid address errors. +#[test] +fn invalid_address() { + // Invalid account in POLYX transfer + expect_encode_err( + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "BAD".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + EncodeError::InvalidAddress, + ); + + // Invalid Polymesh add authorization target. + expect_encode_err( + polymesh_add_auth_call(AddAuthorization { + target: "BAD".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions::default()), + }), + ..Default::default() + }), + EncodeError::InvalidAddress, + ); + + // Invalid controller address in bond. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: "BAD".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: RewardDestination::STAKED, + call_indices: None, + })), + EncodeError::InvalidAddress, + ); + + // Invalid address in nomination. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::nominate(Nominate { + nominators: vec!["BAD".into()], + call_indices: None, + })), + EncodeError::InvalidAddress, + ); +} + +fn test_invalid_value(value: Vec) { + // Invalid balance in POLYX transfer. + expect_encode_err( + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + // value is too long. + value: value.clone().into(), + ..Default::default() + })), + EncodeError::InvalidValue, + ); + + // Invalid bond amount. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: value.clone().into(), + reward_destination: RewardDestination::STAKED, + call_indices: None, + })), + EncodeError::InvalidValue, + ); + + // Invalid bond extra amount. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond_extra( + BondExtra { + value: value.clone().into(), + call_indices: None, + }, + )), + EncodeError::InvalidValue, + ); + + // Invalid unbond amount. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::unbond(Unbond { + value: value.clone().into(), + call_indices: None, + })), + EncodeError::InvalidValue, + ); + + // Invalid rebond amount. + expect_encode_err( + staking_call(Proto::mod_Staking::OneOfmessage_oneof::rebond(Rebond { + value: value.clone().into(), + call_indices: None, + })), + EncodeError::InvalidValue, + ); +} + +/// Test invalid value errors. +#[test] +fn invalid_value() { + // Test with value is not a valid `U256`. + test_invalid_value(vec![0u8; 33]); + + // Invalid balance in POLYX transfer (value is too larger for `u128`) + test_invalid_value(U256::MAX.to_big_endian().to_vec()); +} + +/// Test invalid network id. +#[test] +fn invalid_network_id() { + let input = Proto::SigningInput { + network: 0xFFFF, + ..Default::default() + }; + let res = CallEncoder::encode_input(&input).expect_err("The call should not be supported"); + assert_eq!(res.error_type(), &EncodeError::InvalidNetworkId); +} diff --git a/rust/chains/tw_ripple/src/definitions.rs b/rust/chains/tw_ripple/src/definitions.rs index 16b7cbfd594..eba1a3d9c50 100644 --- a/rust/chains/tw_ripple/src/definitions.rs +++ b/rust/chains/tw_ripple/src/definitions.rs @@ -5,7 +5,7 @@ use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use tw_misc::serde::hashmap_as_tupple_list; +use tw_misc::serde::hashmap_as_tuple_list; pub const DEFINITIONS_JSON: &str = include_str!("../definitions/definitions.json"); @@ -39,7 +39,7 @@ pub struct FieldInfo { #[serde(rename_all = "UPPERCASE")] pub struct Definitions { pub types: HashMap, - #[serde(with = "hashmap_as_tupple_list")] + #[serde(with = "hashmap_as_tuple_list")] pub fields: HashMap, pub transaction_types: HashMap, } diff --git a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs index e5749695f60..90d7ed94248 100644 --- a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs +++ b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs @@ -152,7 +152,9 @@ impl<'a> ProtobufBuilder<'a> { .to_classic_address() .into_tw() .context("Error converting 'Payment.destination' to a Classic address")?; - let destination_tag = payment.destination_tag.zero_or_some(); + let destination_tag = payment + .destination_tag + .try_into_u32_optional("destinationTag")?; self.prepare_builder()? .payment(amount, destination, destination_tag) @@ -200,9 +202,15 @@ impl<'a> ProtobufBuilder<'a> { .escrow_create( native_amount, destination, - escrow_create.destination_tag.zero_or_some(), - escrow_create.cancel_after.zero_or_some(), - escrow_create.finish_after.zero_or_some(), + escrow_create + .destination_tag + .try_into_u32_optional("destinationTag")?, + escrow_create + .cancel_after + .try_into_u32_optional("cancelAfter")?, + escrow_create + .finish_after + .try_into_u32_optional("finishAfter")?, condition, ) .map(TransactionType::EscrowCreate) @@ -323,13 +331,13 @@ impl<'a> ProtobufBuilder<'a> { let mut builder = TransactionBuilder::default(); builder .fee(fee) - .flags(self.input.flags) + .flags(self.input.flags.try_into_u32("inputFlags")?) .sequence(self.input.sequence) .last_ledger_sequence(self.input.last_ledger_sequence) .account_str(self.input.account.as_ref())? .signing_pub_key(&signing_public_key); if self.input.source_tag != 0 { - builder.source_tag(self.input.source_tag); + builder.source_tag(self.input.source_tag.try_into_u32("sourceTag")?); } Ok(builder) } @@ -369,3 +377,19 @@ impl<'a> ProtobufBuilder<'a> { }) } } + +trait AsUint32: TryInto { + fn try_into_u32(self, param: &str) -> SigningResult { + self.try_into() + .map_err(|_| SigningError::new(SigningErrorType::Error_invalid_params)) + .with_context(|| format!("'{param}' must fit u32")) + } + + /// Tries to convert `self` as `u32`. + /// Returns error if `self` doesn't fit `u32` type, or returns `None` if `self` is 0. + fn try_into_u32_optional(self, param: &str) -> SigningResult> { + self.try_into_u32(param).map(u32::zero_or_some) + } +} + +impl AsUint32 for T where T: TryInto {} diff --git a/rust/chains/tw_solana/Cargo.toml b/rust/chains/tw_solana/Cargo.toml index dde71aa2cce..15d9178994b 100644 --- a/rust/chains/tw_solana/Cargo.toml +++ b/rust/chains/tw_solana/Cargo.toml @@ -15,4 +15,5 @@ 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" } diff --git a/rust/chains/tw_solana/src/instruction.rs b/rust/chains/tw_solana/src/instruction.rs index fee1928163b..ce3ff4e7b19 100644 --- a/rust/chains/tw_solana/src/instruction.rs +++ b/rust/chains/tw_solana/src/instruction.rs @@ -5,15 +5,19 @@ use crate::address::SolanaAddress; use borsh::BorshSerialize; use serde::{Deserialize, Serialize}; +use tw_encoding::base58::as_base58_bitcoin; use tw_memory::Data; +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] 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, + #[serde(with = "as_base58_bitcoin")] + pub data: Data, // Rpc getTransaction uses base58 encoding, we use the same encoding for consistency } impl Instruction { @@ -72,6 +76,7 @@ impl Instruction { } #[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AccountMeta { /// An account's public key. pub pubkey: SolanaAddress, diff --git a/rust/chains/tw_solana/src/modules/insert_instruction.rs b/rust/chains/tw_solana/src/modules/insert_instruction.rs index 36c1c583de3..23077a1fdc3 100644 --- a/rust/chains/tw_solana/src/modules/insert_instruction.rs +++ b/rust/chains/tw_solana/src/modules/insert_instruction.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::address::SolanaAddress; +use crate::instruction::AccountMeta; use crate::modules::compiled_keys::try_into_u8; use crate::transaction::v0::MessageAddressTableLookup; use crate::transaction::{CompiledInstruction, MessageHeader}; @@ -11,6 +12,59 @@ use tw_coin_entry::error::prelude::*; use tw_memory::Data; pub trait InsertInstruction { + /// Pushes an instruction + fn push_instruction( + &mut self, + program_id: SolanaAddress, + accounts: Vec, + data: Data, + ) -> SigningResult<()> { + let insert_at = self.instructions_mut().len(); + self.insert_instruction(insert_at, program_id, accounts, data) + } + + /// Inserts an instruction at the given `insert_at` index. + fn insert_instruction( + &mut self, + insert_at: usize, + program_id: SolanaAddress, + accounts: Vec, + data: Data, + ) -> SigningResult<()> { + if insert_at > self.instructions_mut().len() { + return SigningError::err(SigningErrorType::Error_internal) + .context(format!("Unable to add '{program_id}' instruction at the '{insert_at}' index. Number of existing instructions: {}", self.instructions_mut().len())); + } + + // Step 1 - add the `account` in the accounts list. + let accounts: Vec = accounts + .iter() + .map(|account_meta| self.push_account(account_meta)) + .collect::, _>>()?; + + // Step 2 - find or add the `program_id` in the accounts list. + let program_id_index = match self + .account_keys_mut() + .iter() + .position(|acc| *acc == program_id) + { + Some(pos) => try_into_u8(pos)?, + None => self.push_readonly_unsigned_account(program_id)?, + }; + + // Step 3 - Create a `CompiledInstruction` based on the `program_id` index and instruction `accounts` and `data`. + let new_compiled_ix = CompiledInstruction { + program_id_index, + accounts, + data, + }; + + // Step 4 - Insert the created instruction at the given `insert_at` index. + self.instructions_mut().insert(insert_at, new_compiled_ix); + + Ok(()) + } + /// Pushes a simple instruction that doesn't have accounts. fn push_simple_instruction( &mut self, @@ -56,6 +110,121 @@ pub trait InsertInstruction { Ok(()) } + /// Pushes an account to the message. + /// If the account already exists, it must match the `is_signer` and `is_writable` attributes + /// Returns the index of the account in the account list. + fn push_account(&mut self, account: &AccountMeta) -> SigningResult { + // The layout of the account keys is as follows: + // +-------------------------------------+ + // | Writable and required signature | \ + // +-------------------------------------+ |-> num_required_signatures + // | Readonly and required signature | --> num_readonly_signed_accounts / + // +-------------------------------------+ + // | Writable and not required signature | + // +-------------------------------------+ + // | Readonly and not required signature | --> num_readonly_unsigned_accounts + // +-------------------------------------+ + + // Check if the account already exists in `account_keys`, + // if it does, validate `is_signer` and `is_writable` match + if let Some(existing_index) = self + .account_keys_mut() + .iter() + .position(|key| *key == account.pubkey) + { + let existing_is_signer = + existing_index < self.message_header_mut().num_required_signatures as usize; + + let existing_is_writable = if existing_is_signer { + existing_index + < (self.message_header_mut().num_required_signatures + - self.message_header_mut().num_readonly_signed_accounts) + as usize + } else { + existing_index + < (self.account_keys_mut().len() + - self.message_header_mut().num_readonly_unsigned_accounts as usize) + }; + + match (existing_is_signer, account.is_signer) { + // If new account requires weaker or the same signing permissions, it's ok. + (true, false) | (false, false) | (true, true) => (), + // If new account requires stronger signing permissions than we have already, then would need to reorder accounts. + // TODO: Implement reordering accounts if needed. + (false, true) => { + return SigningError::err(SigningErrorType::Error_internal).context( + "Account already exists but the `is_signer` attribute does not match", + ); + }, + } + + match (existing_is_writable, account.is_writable) { + // If new account requires weaker or the same writable permissions, it's ok. + (true, false) | (false, false) | (true, true) => (), + // If new account requires stronger writable permissions than we have already, then would need to reorder accounts. + // TODO: Implement reordering accounts if needed. + (false, true) => { + return SigningError::err(SigningErrorType::Error_internal).context( + "Account already exists but the `is_writable` attribute does not match", + ); + }, + } + + // Return the existing index if validation passes + return try_into_u8(existing_index); + } + + // Determine the insertion position based on is_signer and is_writable + let insert_at = match (account.is_signer, account.is_writable) { + (true, true) => { + self.message_header_mut().num_required_signatures += 1; + // The account is added at the end of the writable and signer accounts + (self.message_header_mut().num_required_signatures + - self.message_header_mut().num_readonly_signed_accounts) + as usize + - 1 + }, + (true, false) => { + self.message_header_mut().num_required_signatures += 1; + self.message_header_mut().num_readonly_signed_accounts += 1; + // The account is added at the end of the read-only and signer accounts + self.message_header_mut().num_required_signatures as usize - 1 + }, + (false, true) => { + // The account is added at the end of the writable and non-signer accounts + self.account_keys_mut().len() + - self.message_header_mut().num_readonly_unsigned_accounts as usize + }, + (false, false) => { + self.message_header_mut().num_readonly_unsigned_accounts += 1; + // The account is added at the end of the list + self.account_keys_mut().len() + }, + }; + + // Insert the account at the determined position + self.account_keys_mut().insert(insert_at, account.pubkey); + + let account_added_at = try_into_u8(insert_at)?; + + // Update program ID and account indexes if the new account was added before its position + let instructions = self.instructions_mut(); + instructions.iter_mut().for_each(|ix| { + // Update program ID index + if ix.program_id_index >= account_added_at { + ix.program_id_index += 1; + } + + // Update account indexes + ix.accounts + .iter_mut() + .filter(|ix_account_id| **ix_account_id >= account_added_at) + .for_each(|ix_account_id| *ix_account_id += 1); + }); + + Ok(account_added_at) + } + fn push_readonly_unsigned_account(&mut self, account: SolanaAddress) -> SigningResult { debug_assert!( !self.account_keys_mut().contains(&account), diff --git a/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs b/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs index 9a4da04d11d..670d381dd88 100644 --- a/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs +++ b/rust/chains/tw_solana/src/modules/instruction_builder/mod.rs @@ -66,6 +66,13 @@ impl InstructionBuilder { self } + pub fn maybe_add_instruction(&mut self, instruction: Option) -> &mut Self { + if let Some(instruction) = instruction { + self.instructions.push(instruction); + } + self + } + pub fn add_instructions(&mut self, instructions: I) -> &mut Self where I: IntoIterator, diff --git a/rust/chains/tw_solana/src/modules/message_builder.rs b/rust/chains/tw_solana/src/modules/message_builder.rs index 8346d67557c..67b0552324a 100644 --- a/rust/chains/tw_solana/src/modules/message_builder.rs +++ b/rust/chains/tw_solana/src/modules/message_builder.rs @@ -160,13 +160,14 @@ impl<'a> MessageBuilder<'a> { let transfer_ix = SystemInstructionBuilder::transfer(from, to, transfer.value) .with_references(references); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instruction(transfer_ix) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -197,12 +198,13 @@ impl<'a> MessageBuilder<'a> { space: DEFAULT_SPACE, }); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instructions(deposit_ixs) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -217,12 +219,13 @@ impl<'a> MessageBuilder<'a> { let deactivate_ix = StakeInstructionBuilder::deactivate(stake_account, sender); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instruction(deactivate_ix) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -241,12 +244,13 @@ impl<'a> MessageBuilder<'a> { .collect::>>() .context("Invalid stake account(s)")?; - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instructions(deactivate_ixs) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -269,12 +273,13 @@ impl<'a> MessageBuilder<'a> { custodian_account, ); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instruction(withdraw_ix) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -303,12 +308,13 @@ impl<'a> MessageBuilder<'a> { }) .collect::>>()?; - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instructions(withdraw_ixs) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -337,12 +343,13 @@ impl<'a> MessageBuilder<'a> { token_address, match_program_id(create_token_acc.token_program_id), ); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instruction(instruction) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -384,13 +391,14 @@ impl<'a> MessageBuilder<'a> { ) .with_references(references); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; 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); + .add_instruction(transfer_instruction) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -448,7 +456,7 @@ impl<'a> MessageBuilder<'a> { ) .with_references(references); - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; builder .maybe_advance_nonce(self.nonce_account()?, signer) .maybe_priority_fee_price(self.priority_fee_price()) @@ -456,7 +464,8 @@ impl<'a> MessageBuilder<'a> { .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); + .add_instruction(transfer_instruction) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -479,7 +488,7 @@ impl<'a> MessageBuilder<'a> { .context("Invalid nonce account")? }; - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; builder .maybe_advance_nonce(prev_nonce_account, signer) .maybe_priority_fee_price(self.priority_fee_price()) @@ -489,7 +498,8 @@ impl<'a> MessageBuilder<'a> { new_nonce_account, create_nonce.rent, DEFAULT_CREATE_NONCE_SPACE, - )); + )) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -506,7 +516,7 @@ impl<'a> MessageBuilder<'a> { .into_tw() .context("Invalid recipient")?; - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; builder .maybe_advance_nonce(self.nonce_account()?, signer) .maybe_priority_fee_price(self.priority_fee_price()) @@ -516,7 +526,8 @@ impl<'a> MessageBuilder<'a> { signer, recipient, withdraw_nonce.value, - )); + )) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -529,11 +540,12 @@ impl<'a> MessageBuilder<'a> { .into_tw() .context("Invalid nonce account")?; - let mut builder = InstructionBuilder::default(); + let mut builder = self.builder_with_token_transfer_to_fee_payer_if_applicable()?; builder .maybe_advance_nonce(Some(nonce_account), signer) .maybe_priority_fee_price(self.priority_fee_price()) - .maybe_priority_fee_limit(self.priority_fee_limit()); + .maybe_priority_fee_limit(self.priority_fee_limit()) + .maybe_add_instruction(self.transfer_to_fee_payer()?); Ok(builder.output()) } @@ -580,6 +592,51 @@ impl<'a> MessageBuilder<'a> { self.signer_address() } + fn builder_with_token_transfer_to_fee_payer_if_applicable( + &self, + ) -> SigningResult { + let Some(sponsored_transfer_token) = self.input.token_transfer_to_fee_payer.as_ref() else { + return Ok(InstructionBuilder::default()); + }; + let signer = self.signer_address()?; + + let fee_mint_address = + SolanaAddress::from_str(sponsored_transfer_token.fee_token_mint_address.as_ref()) + .into_tw() + .context("Invalid fee mint address")?; + + let fee_recipient_token_address = SolanaAddress::from_str( + sponsored_transfer_token + .fee_recipient_token_address + .as_ref(), + ) + .into_tw() + .context("Invalid sponsor token address")?; + + let fee_sender_token_address = + SolanaAddress::from_str(sponsored_transfer_token.fee_sender_token_address.as_ref()) + .into_tw() + .context("Invalid fee sender token address")?; + + let fee_decimals = sponsored_transfer_token + .fee_decimals + .try_into() + .tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid fee decimals. Expected lower than 256")?; + + let mut builder = InstructionBuilder::default(); + builder.add_instruction(TokenInstructionBuilder::transfer_checked( + fee_sender_token_address, + fee_mint_address, + fee_recipient_token_address, + signer, + sponsored_transfer_token.fee_amount, + fee_decimals, + match_program_id(sponsored_transfer_token.fee_token_program_id), + )); + Ok(builder) + } + fn recent_blockhash(&self) -> SigningResult { Blockhash::from_str(&self.input.recent_blockhash) .map_err(SigningError::from) @@ -606,6 +663,24 @@ impl<'a> MessageBuilder<'a> { .map(|proto| proto.limit) } + fn transfer_to_fee_payer(&self) -> SigningResult> { + let Some(ref transfer_to_fee_payer) = self.input.transfer_to_fee_payer else { + return Ok(None); + }; + + let from = self.signer_address()?; + let to = SolanaAddress::from_str(&transfer_to_fee_payer.recipient) + .into_tw() + .context("Invalid 'transfer_to_fee_payer.recipient' address")?; + + let references = Self::parse_references(&transfer_to_fee_payer.references)?; + + Ok(Some( + SystemInstructionBuilder::transfer(from, to, transfer_to_fee_payer.value) + .with_references(references), + )) + } + fn parse_references(refs: &[Cow<'_, str>]) -> SigningResult> { refs.iter() .map(|addr| SolanaAddress::from_str(addr).map_err(SigningError::from)) diff --git a/rust/chains/tw_solana/src/modules/tx_signer.rs b/rust/chains/tw_solana/src/modules/tx_signer.rs index 4358161c0d5..2ca63f928a2 100644 --- a/rust/chains/tw_solana/src/modules/tx_signer.rs +++ b/rust/chains/tw_solana/src/modules/tx_signer.rs @@ -23,7 +23,7 @@ impl TxSigner { let message_encoded = Self::preimage_versioned(&unsigned_msg)?; - // Add external signatures first, so they can be overriden if corresponding private keys are specified. + // Add external signatures first, so they can be overridden if corresponding private keys are specified. key_signs.extend(external_signatures.clone()); // Sign the message with all given private keys. diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs index 6ce3f5c77f6..8e5da814a3a 100644 --- a/rust/chains/tw_solana/src/modules/utils.rs +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -4,11 +4,14 @@ use crate::address::SolanaAddress; use crate::defined_addresses::{COMPUTE_BUDGET_ADDRESS, SYSTEM_PROGRAM_ID_ADDRESS}; +use crate::instruction::Instruction; use crate::modules::insert_instruction::InsertInstruction; use crate::modules::instruction_builder::compute_budget_instruction::{ ComputeBudgetInstruction, ComputeBudgetInstructionBuilder, UnitLimit, UnitPrice, }; -use crate::modules::instruction_builder::system_instruction::SystemInstruction; +use crate::modules::instruction_builder::system_instruction::{ + SystemInstruction, SystemInstructionBuilder, +}; use crate::modules::message_decompiler::{InstructionWithoutAccounts, MessageDecompiler}; use crate::modules::proto_builder::ProtoBuilder; use crate::modules::tx_signer::TxSigner; @@ -16,6 +19,7 @@ use crate::modules::PubkeySignatureMap; use crate::transaction::versioned::VersionedTransaction; use crate::SOLANA_ALPHABET; use std::borrow::Cow; +use std::str::FromStr; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; use tw_encoding::base58; @@ -23,6 +27,7 @@ use tw_encoding::base64::{self, STANDARD}; use tw_hash::H256; use tw_keypair::{ed25519, KeyPairResult}; use tw_memory::Data; +use tw_number::U256; use tw_proto::Solana::Proto; pub struct SolanaTransaction; @@ -173,6 +178,89 @@ impl SolanaTransaction { .to_base64() .tw_err(SigningErrorType::Error_internal) } + + pub fn insert_instruction( + encoded_tx: &str, + insert_at: i32, + instruction: &str, + ) -> SigningResult { + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; + let mut tx: VersionedTransaction = + bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; + + let instruction: Instruction = + serde_json::from_str(instruction).map_err(|_| SigningErrorType::Error_input_parse)?; + + if insert_at >= 0 && insert_at as usize > tx.message.instructions().len() { + return Err(SigningError::from(SigningErrorType::Error_invalid_params)); + } + + let final_insert_at = if insert_at < 0 { + tx.message.instructions().len() // Append to the end if negative + } else { + insert_at as usize // Use the specified position + }; + + tx.message.insert_instruction( + final_insert_at, + instruction.program_id, + instruction.accounts, + instruction.data, + )?; + + // Set the correct number of zero signatures + let unsigned_tx = VersionedTransaction::unsigned(tx.message); + unsigned_tx + .to_base64() + .tw_err(SigningErrorType::Error_internal) + } + + /// Inserts a SOL transfer instruction to the given transaction at the specified position, returning the updated transaction. + /// Please note that compute price and limit instructions should always be the first instructions if they are present in the transaction. + /// If you don't care about the position, use -1. + pub fn insert_transfer_instruction( + encoded_tx: &str, + insert_at: i32, + from: &str, + to: &str, + lamports: &str, + ) -> SigningResult { + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; + let from = + SolanaAddress::from_str(from).map_err(|_| SigningErrorType::Error_input_parse)?; + let to = SolanaAddress::from_str(to).map_err(|_| SigningErrorType::Error_input_parse)?; + let lamports = U256::from_str(lamports) + .and_then(u64::try_from) + .map_err(|_| SigningErrorType::Error_input_parse)?; + + let mut tx: VersionedTransaction = + bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; + + if insert_at >= 0 && insert_at as usize > tx.message.instructions().len() { + return Err(SigningError::from(SigningErrorType::Error_invalid_params)); + } + + let final_insert_at = if insert_at < 0 { + tx.message.instructions().len() // Append to the end if negative + } else { + insert_at as usize // Use the specified position + }; + + // Create transfer instruction and insert it at the specified position. + let transfer_ix = SystemInstructionBuilder::transfer(from, to, lamports); + tx.message.insert_instruction( + final_insert_at, + transfer_ix.program_id, + transfer_ix.accounts, + transfer_ix.data, + )?; + + // Set the correct number of zero signatures + let unsigned_tx = VersionedTransaction::unsigned(tx.message); + unsigned_tx + .to_base64() + .tw_err(SigningErrorType::Error_internal) + } } fn try_instruction_as_compute_budget( diff --git a/rust/chains/tw_ton/src/signing_request/builder.rs b/rust/chains/tw_ton/src/signing_request/builder.rs index 167fd290895..59acc26baf3 100644 --- a/rust/chains/tw_ton/src/signing_request/builder.rs +++ b/rust/chains/tw_ton/src/signing_request/builder.rs @@ -116,9 +116,13 @@ impl SigningRequestBuilder { PayloadType::None => None, }; + let ton_amount = U256::from_big_endian_slice(input.amount.as_ref()) + .tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid 'amount'")?; + Ok(TransferRequest { dest, - ton_amount: U256::from(input.amount), + ton_amount, mode, comment, state_init, @@ -141,13 +145,21 @@ impl SigningRequestBuilder { Some(input.custom_payload.to_string()) }; + let jetton_amount = U256::from_big_endian_slice(input.jetton_amount.as_ref()) + .tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid 'jetton_amount'")?; + + let forward_ton_amount = U256::from_big_endian_slice(input.forward_amount.as_ref()) + .tw_err(SigningErrorType::Error_invalid_params) + .context("Invalid 'forward_amount'")?; + let jetton_payload = JettonTransferRequest { query_id: input.query_id, - jetton_amount: U256::from(input.jetton_amount), + jetton_amount, dest, response_address, custom_payload, - forward_ton_amount: U256::from(input.forward_amount), + forward_ton_amount, }; Ok(TransferPayload::JettonTransfer(jetton_payload)) diff --git a/rust/chains/tw_zcash/Cargo.toml b/rust/chains/tw_zcash/Cargo.toml index 9cc7ed926b9..c6da655bed9 100644 --- a/rust/chains/tw_zcash/Cargo.toml +++ b/rust/chains/tw_zcash/Cargo.toml @@ -4,7 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +bech32 = "0.9.1" +postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } +serde = { version = "1.0", features = ["derive"] } +serde_with = "3.16" tw_base58_address = { path = "../../tw_base58_address" } +tw_bech32_address = { path = "../../tw_bech32_address" } tw_bitcoin = { path = "../../chains/tw_bitcoin" } tw_coin_entry = { path = "../../tw_coin_entry" } tw_encoding = { path = "../../tw_encoding" } diff --git a/rust/chains/tw_zcash/src/address.rs b/rust/chains/tw_zcash/src/address.rs new file mode 100644 index 00000000000..40caf70c6c1 --- /dev/null +++ b/rust/chains/tw_zcash/src/address.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::t_address::TAddress; +use crate::tex_address::TexAddress; +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::error::prelude::{AddressError, AddressResult}; +use tw_keypair::ecdsa; +use tw_memory::Data; +use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix; + +pub enum ZcashAddress { + T(TAddress), + Tex(TexAddress), +} + +impl ZcashAddress { + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + address_str: &str, + prefix: Option, + ) -> AddressResult { + // Check whether the prefix is set and specifies the legacy address prefixes. + match prefix { + Some(StandardBitcoinPrefix::Base58(prefix)) => { + return TAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix)) + .map(ZcashAddress::T); + }, + Some(StandardBitcoinPrefix::Bech32(prefix)) => { + return TexAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix)) + .map(ZcashAddress::Tex); + }, + None => (), + } + + // Otherwise, try to parse address as either Cash or Legacy. + if let Ok(t) = TAddress::from_str_with_coin_and_prefix(coin, address_str, None) { + return Ok(ZcashAddress::T(t)); + } + if let Ok(tex) = TexAddress::from_str_with_coin_and_prefix(coin, address_str, None) { + return Ok(ZcashAddress::Tex(tex)); + } + Err(AddressError::InvalidInput) + } + + pub fn address_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &ecdsa::secp256k1::PublicKey, + prefix: Option, + ) -> AddressResult { + match prefix { + // Check whether the prefix is set and specifies the legacy address prefixes. + Some(StandardBitcoinPrefix::Base58(prefix)) => { + TAddress::p2pkh_with_public_key(prefix.p2pkh, public_key).map(ZcashAddress::T) + }, + Some(StandardBitcoinPrefix::Bech32(prefix)) => { + TexAddress::with_public_key(prefix.hrp, public_key).map(ZcashAddress::Tex) + }, + None => { + let p2pkh_prefix = coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?; + TAddress::p2pkh_with_public_key(p2pkh_prefix, public_key).map(ZcashAddress::T) + }, + } + } +} + +impl FromStr for ZcashAddress { + type Err = AddressError; + + fn from_str(address_str: &str) -> Result { + if let Ok(t) = TAddress::from_str(address_str) { + return Ok(ZcashAddress::T(t)); + } + if let Ok(tex) = TexAddress::from_str(address_str) { + return Ok(ZcashAddress::Tex(tex)); + } + Err(AddressError::InvalidInput) + } +} + +impl CoinAddress for ZcashAddress { + fn data(&self) -> Data { + match self { + ZcashAddress::T(t) => t.data(), + ZcashAddress::Tex(tex) => tex.data(), + } + } +} + +impl fmt::Display for ZcashAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ZcashAddress::T(t) => write!(f, "{t}"), + ZcashAddress::Tex(tex) => write!(f, "{tex}"), + } + } +} diff --git a/rust/chains/tw_zcash/src/context.rs b/rust/chains/tw_zcash/src/context.rs index 001fc40b18b..7990a96d79b 100644 --- a/rust/chains/tw_zcash/src/context.rs +++ b/rust/chains/tw_zcash/src/context.rs @@ -2,13 +2,14 @@ // // Copyright © 2017 Trust Wallet. +use crate::address::ZcashAddress; +use crate::modules::pczt; +use crate::modules::pczt_request::zcash_pczt_request_handler::PcztRequestHandler; use crate::modules::protobuf_builder::ZcashProtobufBuilder; use crate::modules::signing_request::ZcashSigningRequestBuilder; use crate::modules::zcash_fee_estimator::ZcashFeeEstimator; -use crate::t_address::TAddress; use crate::transaction::ZcashTransaction; use tw_bitcoin::context::BitcoinSigningContext; -use tw_bitcoin::modules::psbt_request::NoPsbtRequestBuilder; use tw_coin_entry::error::prelude::SigningResult; use tw_utxo::context::{AddressPrefixes, UtxoContext}; use tw_utxo::script::Script; @@ -17,20 +18,24 @@ use tw_utxo::script::Script; pub struct ZcashContext; impl UtxoContext for ZcashContext { - type Address = TAddress; + type Address = ZcashAddress; type Transaction = ZcashTransaction; type FeeEstimator = ZcashFeeEstimator; + type Psbt = pczt::Pczt; fn addr_to_script_pubkey( addr: &Self::Address, prefixes: AddressPrefixes, ) -> SigningResult