Skip to content

Commit

Permalink
[Solana]: Move Solana blockchain to Rust (#3691)
Browse files Browse the repository at this point in the history
* [Solana]: Add `MessageRaw` to `SigningInput`

* Replace `Pubkey` with `SolanaAddress`

* [Solana]: Add instruction builders

* [Solana]: WIP Signer

* [Solana]: Move instruction builders to dedicated modules

* [Bitcoin]: Fix the order of compiled account keys

* [Solana]: Fix delegate staking

* [Solana]: Add deactivate staking transaction

* [Solana]: Add deactivate all stake transaction

* [Solana]: Add withdraw stake transaction

* [Solana]: Add withdraw all stake transaction

* [Solana]: Add create token account transaction

* [Solana]: Add TokenTransfer transaction

* Refactor `InstructionBuilder` to allow easily append references, add memo and nonce instructions

* [Solana]: Minor changes

* [Solana]: Add CreateAndTransferToken transaction

* Add support for fee payer

* [Solana]: Add CreateNonceAccount and WithdrawNonceAccount transactions

* [Solana]: Add AdvanceNonceAccount transaction

* [Solana]: Add RawMessage transaction

* [Solana]: Add support for preimage hashing and compiling

* [Solana]: Replace C++ implementation

* [Solana]: Add more compile tests

* [Solana]: Add tw_solana_address_default_token_address

* [Solana]: Add "WithDurableNonce" tests

* [Solana]: Remove C++ implementation

* Identify a bug in `Address::findProgramAddress` function

* [CI] Trigger CI

* [Solana]: Remove extra C++ tests

* [Solana]: Fix clippy warnings

* [Solana]: Fix `tw_solana_address_default_token_address`

* Replace C++'s implementation with `tw_solana_address_default_token_address`

* [Solana]: Remove fix Boost path at CMakeLists.txt

* [Solana]: Add `TransactionDecoder` module (#3698)

* [Solana]: Add external signatures to the `RawMessage`

* [Solana]: Add `ProtoBuilder` to convert `VersionedTransaction` to a `Proto::RawMessage`

* [Solana]: Add `TransactionDecoder` module

* [Solana]: Fix signature overriding, add more tests

* [Solana]: Refactor tests

* [Solana]: Add and implement `TWTransactionDecoderDecode` FFI in C++

* [Solana]: Add Kotlin test

* [Solana]: Remove fix Boost path at CMakeLists.txt

* [Solana]: Add Swift test

* [WalletConnect/Solana]: Add support for WalletConnect signing requests (#3705)

* [Solana]: Add SolanaWalletConnector

* [Solana]: Add pubkey signatures to the `Proto::SigningOutput`

* [Solana]: Add `TWSolanaWalletConnect` test

* [Solana]: Add Kotlin, Swift tests

* [Solana]: Fix C++ test

* [Solana]: Fix iOS test

* [Solana]: Add support for priority fee (#3710)

* [Solana]: Add priority fee price and limit

* [Solana]: Add priority fee tests

* [CI]: Split linux-ci-rust.yml into two jobs

* Move `new-blockchain-rust` step into codegen-v2.yml

* [CI]: Fix linux-ci-rust.yml
  • Loading branch information
satoshiotomakan authored Mar 5, 2024
1 parent 7c71119 commit c0cf03f
Show file tree
Hide file tree
Showing 116 changed files with 5,311 additions and 4,379 deletions.
42 changes: 34 additions & 8 deletions .github/workflows/codegen-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,42 @@ on:
paths:
- 'codegen-v2/**'

env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"

jobs:
test:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path codegen-v2/Cargo.toml
- uses: actions/checkout@v3
- name: Install system dependencies
run: |
tools/install-sys-dependencies-linux
- name: Run sccache-cache
uses: mozilla-actions/[email protected]

- name: Install Rust dependencies
run: |
tools/install-rust-dependencies
- name: Run codegen-v2 tests
run: |
cargo test --all
working-directory: codegen-v2

# Generate files for a blockchain.
# Please note the blockchain should not be implemented in Rust at the moment of running this step,
# otherwise consider either generating files for another blockchain or removing this step at all.
- name: Test codegen-v2 new-blockchain-rust
run: |
cargo run -- new-blockchain-rust iotex
working-directory: codegen-v2

# Check if `new-blockchain-rust` command has generated files that do not break project compilation.
- name: Check Rust compiles
run: |
cargo check --tests
working-directory: rust
45 changes: 28 additions & 17 deletions .github/workflows/linux-ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ concurrency:
cancel-in-progress: true

jobs:
build:
# Check formatting, clippy warnings, run tests and check code coverage.
build-and-test:
permissions:
contents: read
checks: write
Expand All @@ -33,16 +34,14 @@ jobs:
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
key: "build-and-test"
workspaces: |
rust
- name: Install Rust dependencies
run: |
tools/install-rust-dependencies dev
- name: Install emsdk
run: tools/install-wasm-dependencies

- name: Check code formatting
run: |
cargo fmt --check
Expand All @@ -58,9 +57,6 @@ jobs:
cargo llvm-cov nextest --profile ci --no-fail-fast --lcov --output-path coverage.info
working-directory: rust

- name: Run tests in WASM
run: tools/rust-test wasm

- name: Rust Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
Expand All @@ -75,17 +71,32 @@ jobs:
run: |
tools/check-coverage rust/coverage.stats rust/coverage.info
# Generate files for a blockchain in the end of the pipeline.
# Please note the blockchain should not be implemented in Rust at the moment of running this step,
# otherwise consider either generate files for another blockchain or remove this step at all.
- name: Test codegen-v2 new-blockchain-rust
# Run Rust tests in WASM.
test-wasm:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- name: Install system dependencies
run: |
cargo run -- new-blockchain-rust iotex
working-directory: codegen-v2
tools/install-sys-dependencies-linux
- name: Run sccache-cache
uses: mozilla-actions/[email protected]

- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
key: "test-wasm"
workspaces: |
rust
# Check if `new-blockchain-rust` command has generated files that do not break project compilation.
- name: Check Rust compiles
- name: Install Rust dependencies
run: |
cargo check --tests
working-directory: rust
tools/install-rust-dependencies
- name: Install emsdk
run: tools/install-wasm-dependencies

- name: Run tests in WASM
run: tools/rust-test wasm
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.Base58
import wallet.core.java.AnySigner
import wallet.core.jni.Base64
import wallet.core.jni.CoinType
import wallet.core.jni.CoinType.SOLANA
import wallet.core.jni.SolanaTransaction
import wallet.core.jni.DataVector
import wallet.core.jni.TransactionDecoder
import wallet.core.jni.proto.Common.SigningError
import wallet.core.jni.proto.Solana
import wallet.core.jni.proto.Solana.DecodingTransactionOutput
import wallet.core.jni.proto.Solana.SigningInput
import wallet.core.jni.proto.Solana.SigningOutput

class TestSolanaTransaction {
Expand Down Expand Up @@ -39,4 +44,41 @@ class TestSolanaTransaction {
val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"
assertEquals(output.encoded, expectedString)
}

@Test
fun testDecodeUpdateBlockhashAndSign() {
// https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet
val encodedTx = Base64.decode("AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG")
val newBlockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"

val senderPrivateKeyData = "7f0932159226ddec9e1a4b0b8fe7cdc135049f9e549a867d722aa720dd64f32e".toHexByteArray()
val feePayerPrivateKeyData = "4b9d6f57d28b06cbfa1d4cc710953e62d653caf853415c56ffd9d150acdeb7f7".toHexByteArray()

// Step 1: Decode the transaction.

val decodedData = TransactionDecoder.decode(SOLANA, encodedTx)
val decodedOutput = DecodingTransactionOutput.parseFrom(decodedData)

assertEquals(decodedOutput.error, SigningError.OK)
assert(decodedOutput.transaction.hasLegacy())

// Step 2: Update recent blockhash.

val rawTx = decodedOutput.transaction.toBuilder().apply {
legacy = decodedOutput.transaction.legacy.toBuilder().setRecentBlockhash(newBlockhash).build()
}.build()

// Step 3: Re-sign the updated transaction.

val signingInput = SigningInput.newBuilder().apply {
rawMessage = rawTx
privateKey = ByteString.copyFrom(senderPrivateKeyData)
feePayerPrivateKey = ByteString.copyFrom(feePayerPrivateKeyData)
txEncoding = Solana.Encoding.Base64
}.build()

val output = AnySigner.sign(signingInput, SOLANA, SigningOutput.parser())
val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"
assertEquals(output.encoded, expectedString)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.trustwallet.core.app.blockchains.solana

import com.google.protobuf.ByteString
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.java.AnySigner
import wallet.core.jni.Base58
import wallet.core.jni.CoinType.SOLANA
import wallet.core.jni.WalletConnectRequest
import wallet.core.jni.proto.Common.SigningError
import wallet.core.jni.proto.Solana.Encoding
import wallet.core.jni.proto.Solana.SigningOutput
import wallet.core.jni.proto.WalletConnect

class TestSolanaWalletConnectSigning {

init {
System.loadLibrary("TrustWalletCore")
}

@Test
fun testSignSolanaTransactionFromWalletConnectRequest() {
// Step 1: Parse a signing request received through WalletConnect.

val parsingInput = WalletConnect.ParseRequestInput.newBuilder().apply {
method = WalletConnect.Method.SolanaSignTransaction
payload = "{\"transaction\":\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA=\"}"
}.build()

val parsingOutputBytes = WalletConnectRequest.parse(SOLANA, parsingInput.toByteArray())
val parsingOutput = WalletConnect.ParseRequestOutput.parseFrom(parsingOutputBytes)

assertEquals(parsingOutput.error, SigningError.OK)

// Step 2: Set missing fields.

val signingInput = parsingOutput.solana.toBuilder().apply {
privateKey = ByteString.copyFrom(Base58.decodeNoCheck("A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr"))
txEncoding = Encoding.Base64
}.build()

// Step 3: Sign the transaction.

val output = AnySigner.sign(signingInput, SOLANA, SigningOutput.parser())

assertEquals(output.error, SigningError.OK)
assertEquals(output.encoded, "AQPWaOi7dMdmQpXi8HyQQKwiqIftrg1igGQxGtZeT50ksn4wAnyH4DtDrkkuE0fqgx80LTp4LwNN9a440SrmoA8BAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA=")

assertEquals(output.getSignatures(0).pubkey, "7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q")
assertEquals(output.getSignatures(0).signature, "5T6uZBHnHFd8uWErDBTFRVkbKuhbcm94K5MJ2beTYDruzqv4FjS7EMKvC94ZfxNAiWUXZ6bZxS3WXUbhJwYNPWn")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use tw_coin_entry::error::AddressResult;
use tw_coin_entry::modules::json_signer::NoJsonSigner;
use tw_coin_entry::modules::message_signer::NoMessageSigner;
use tw_coin_entry::modules::plan_builder::NoPlanBuilder;
use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder;
use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
use tw_coin_entry::prefix::NoPrefix;
use tw_keypair::tw::PublicKey;
Expand All @@ -33,6 +34,7 @@ impl CoinEntry for {BLOCKCHAIN}Entry {
type PlanBuilder = NoPlanBuilder;
type MessageSigner = NoMessageSigner;
type WalletConnector = NoWalletConnector;
type TransactionDecoder = NoTransactionDecoder;

#[inline]
fn parse_address(
Expand Down
25 changes: 25 additions & 0 deletions include/TrustWalletCore/TWTransactionDecoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

#pragma once

#include "TWBase.h"
#include "TWCoinType.h"
#include "TWData.h"
#include "TWString.h"

TW_EXTERN_C_BEGIN

TW_EXPORT_STRUCT
struct TWTransactionDecoder;

/// Decodes a transaction from a binary representation.
///
/// \param coin coin type.
/// \param encodedTx encoded transaction data.
/// \return serialized protobuf message specific for the given coin.
TW_EXPORT_STATIC_METHOD
TWData *_Nonnull TWTransactionDecoderDecode(enum TWCoinType coinType, TWData *_Nonnull encodedTx);

TW_EXTERN_C_END
Loading

0 comments on commit c0cf03f

Please sign in to comment.