From 1d7795376afe740bbb5dc6f36a54b0b7895741ee Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:33:18 +0700 Subject: [PATCH 1/4] [Solana]: Update blockhash and re-sign transaction (#3678) * [Solana]: Add Solana skeleton * [Solana]: Implement `SolanaAddress` * [Solana]: Add transaction serialization/deserialization * [Solana]: Versioned transaction signing * [Solana]: Add `tw_solana_transaction_update_blockhash_and_sign` in Rust * [Solana]: Add TWSolanaTransactionUpdateBlockhashAndSign * [Solana]: Add Kotlin test * [CI] Trigger CI * [Solana]: Do not sign a transaction if there is no private keys provided * Add `SigningOutput::message_encoded` that needs to be sent to a fee estimation service * [Solana]: Remove extra comments --- .../solana/TestSolanaTransaction.kt | 42 ++ include/TrustWalletCore/TWSolanaTransaction.h | 33 ++ rust/Cargo.lock | 26 ++ rust/Cargo.toml | 1 + rust/chains/tw_solana/Cargo.toml | 17 + rust/chains/tw_solana/src/address.rs | 82 ++++ rust/chains/tw_solana/src/compiler.rs | 50 +++ rust/chains/tw_solana/src/entry.rs | 91 +++++ rust/chains/tw_solana/src/lib.rs | 15 + rust/chains/tw_solana/src/modules/mod.rs | 6 + .../chains/tw_solana/src/modules/tx_signer.rs | 49 +++ rust/chains/tw_solana/src/modules/utils.rs | 78 ++++ rust/chains/tw_solana/src/signer.rs | 27 ++ .../tw_solana/src/transaction/legacy.rs | 55 +++ rust/chains/tw_solana/src/transaction/mod.rs | 177 ++++++++ .../tw_solana/src/transaction/short_vec.rs | 386 ++++++++++++++++++ rust/chains/tw_solana/src/transaction/v0.rs | 58 +++ .../tw_solana/src/transaction/versioned.rs | 222 ++++++++++ .../tests/update_blockhash_and_sign.rs | 27 ++ rust/tw_any_coin/tests/chains/mod.rs | 1 + rust/tw_any_coin/tests/chains/solana/mod.rs | 7 + .../tests/chains/solana/solana_address.rs | 48 +++ .../tests/chains/solana/solana_compile.rs | 9 + .../tests/chains/solana/solana_sign.rs | 9 + .../tests/coin_address_derivation_test.rs | 1 + rust/tw_coin_entry/src/error.rs | 5 + rust/tw_coin_registry/Cargo.toml | 1 + rust/tw_coin_registry/src/blockchain_type.rs | 1 + rust/tw_coin_registry/src/dispatcher.rs | 3 + rust/tw_hash/src/hash_array.rs | 55 +++ rust/tw_hash/src/lib.rs | 2 +- .../src/test_utils/tw_data_vector_helper.rs | 6 + rust/wallet_core_rs/Cargo.toml | 4 +- rust/wallet_core_rs/src/ffi/mod.rs | 1 + rust/wallet_core_rs/src/ffi/solana/mod.rs | 6 + .../src/ffi/solana/transaction.rs | 51 +++ .../tests/solana_transaction.rs | 115 ++++++ src/DataVector.h | 25 ++ src/interface/TWSolanaTransaction.cpp | 28 ++ src/interface/TWTransactionCompiler.cpp | 16 +- src/proto/Solana.proto | 4 + tests/chains/Solana/TWSolanaTransaction.cpp | 36 ++ 42 files changed, 1859 insertions(+), 17 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt create mode 100644 include/TrustWalletCore/TWSolanaTransaction.h create mode 100644 rust/chains/tw_solana/Cargo.toml create mode 100644 rust/chains/tw_solana/src/address.rs create mode 100644 rust/chains/tw_solana/src/compiler.rs create mode 100644 rust/chains/tw_solana/src/entry.rs create mode 100644 rust/chains/tw_solana/src/lib.rs create mode 100644 rust/chains/tw_solana/src/modules/mod.rs create mode 100644 rust/chains/tw_solana/src/modules/tx_signer.rs create mode 100644 rust/chains/tw_solana/src/modules/utils.rs create mode 100644 rust/chains/tw_solana/src/signer.rs create mode 100644 rust/chains/tw_solana/src/transaction/legacy.rs create mode 100644 rust/chains/tw_solana/src/transaction/mod.rs create mode 100644 rust/chains/tw_solana/src/transaction/short_vec.rs create mode 100644 rust/chains/tw_solana/src/transaction/v0.rs create mode 100644 rust/chains/tw_solana/src/transaction/versioned.rs create mode 100644 rust/chains/tw_solana/tests/update_blockhash_and_sign.rs create mode 100644 rust/tw_any_coin/tests/chains/solana/mod.rs create mode 100644 rust/tw_any_coin/tests/chains/solana/solana_address.rs create mode 100644 rust/tw_any_coin/tests/chains/solana/solana_compile.rs create mode 100644 rust/tw_any_coin/tests/chains/solana/solana_sign.rs create mode 100644 rust/wallet_core_rs/src/ffi/solana/mod.rs create mode 100644 rust/wallet_core_rs/src/ffi/solana/transaction.rs create mode 100644 rust/wallet_core_rs/tests/solana_transaction.rs create mode 100644 src/DataVector.h create mode 100644 src/interface/TWSolanaTransaction.cpp create mode 100644 tests/chains/Solana/TWSolanaTransaction.cpp diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt new file mode 100644 index 00000000000..5f26065966f --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt @@ -0,0 +1,42 @@ +package com.trustwallet.core.app.blockchains.solana + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.Base58 +import wallet.core.java.AnySigner +import wallet.core.jni.CoinType.SOLANA +import wallet.core.jni.SolanaTransaction +import wallet.core.jni.DataVector +import wallet.core.jni.proto.Common.SigningError +import wallet.core.jni.proto.Solana +import wallet.core.jni.proto.Solana.SigningOutput + +class TestSolanaTransaction { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testUpdateBlockhashAndSign() { + val encodedTx = "AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG" + val newBlockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg" + + val myPrivateKey = "7f0932159226ddec9e1a4b0b8fe7cdc135049f9e549a867d722aa720dd64f32e".toHexByteArray() + val feePayerPrivateKey = "4b9d6f57d28b06cbfa1d4cc710953e62d653caf853415c56ffd9d150acdeb7f7".toHexByteArray() + + val privateKeys = DataVector() + privateKeys.add(myPrivateKey) + privateKeys.add(feePayerPrivateKey) + + val outputData = SolanaTransaction.updateBlockhashAndSign(encodedTx, newBlockhash, privateKeys) + val output = SigningOutput.parseFrom(outputData) + + assertEquals(output.error, SigningError.OK) + val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG" + assertEquals(output.encoded, expectedString) + } +} \ No newline at end of file diff --git a/include/TrustWalletCore/TWSolanaTransaction.h b/include/TrustWalletCore/TWSolanaTransaction.h new file mode 100644 index 00000000000..681313ff248 --- /dev/null +++ b/include/TrustWalletCore/TWSolanaTransaction.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWDataVector.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +TW_EXPORT_STRUCT +struct TWSolanaTransaction; + +/// Decode Solana transaction, update the recent blockhash and re-sign the transaction. +/// +/// # Warning +/// +/// This is a temporary solution. It will be removed when `Solana.proto` supports +/// direct transaction signing. +/// +/// \param encodedTx base64 encoded Solana transaction. +/// \param recentBlockhash base58 encoded recent blockhash. +/// \param privateKeys list of private keys that should be used to re-sign the transaction. +/// \return serialized `Solana::Proto::SigningOutput`. +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWSolanaTransactionUpdateBlockhashAndSign(TWString *_Nonnull encodedTx, + TWString *_Nonnull recentBlockhash, + const struct TWDataVector *_Nonnull privateKeys); + +TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2f08868d5b9..033fa1c04c2 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -162,6 +162,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitcoin" version = "0.30.1" @@ -1759,6 +1768,7 @@ dependencies = [ "tw_native_evmos", "tw_native_injective", "tw_ronin", + "tw_solana", "tw_thorchain", ] @@ -1995,6 +2005,21 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_solana" +version = "0.1.0" +dependencies = [ + "bincode", + "serde", + "serde_json", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "tw_thorchain" version = "0.1.0" @@ -2092,6 +2117,7 @@ dependencies = [ "tw_misc", "tw_number", "tw_proto", + "tw_solana", ] [[package]] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5896e9dcb5e..a98ff23da18 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -5,6 +5,7 @@ members = [ "chains/tw_greenfield", "chains/tw_native_evmos", "chains/tw_native_injective", + "chains/tw_solana", "chains/tw_thorchain", "tw_any_coin", "tw_aptos", diff --git a/rust/chains/tw_solana/Cargo.toml b/rust/chains/tw_solana/Cargo.toml new file mode 100644 index 00000000000..1d3e0611345 --- /dev/null +++ b/rust/chains/tw_solana/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tw_solana" +version = "0.1.0" +edition = "2021" + +[dependencies] +bincode = "1.3.3" +serde = { version = "1.0", features = ["derive"] } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +serde_json = "1.0" diff --git a/rust/chains/tw_solana/src/address.rs b/rust/chains/tw_solana/src/address.rs new file mode 100644 index 00000000000..57c7808892b --- /dev/null +++ b/rust/chains/tw_solana/src/address.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::SOLANA_ALPHABET; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::{AddressError, AddressResult}; +use tw_encoding::base58; +use tw_hash::H256; +use tw_keypair::tw; +use tw_memory::Data; + +#[derive(Clone, Copy)] +pub struct SolanaAddress { + bytes: H256, +} + +impl SolanaAddress { + pub fn with_public_key(public_key: &tw::PublicKey) -> AddressResult { + let bytes = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .to_bytes(); + Ok(SolanaAddress { bytes }) + } + + pub fn with_public_key_bytes(bytes: H256) -> SolanaAddress { + SolanaAddress { bytes } + } + + pub fn bytes(&self) -> H256 { + self.bytes + } +} + +impl CoinAddress for SolanaAddress { + #[inline] + fn data(&self) -> Data { + self.bytes.to_vec() + } +} + +impl FromStr for SolanaAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let bytes = + base58::decode(s, &SOLANA_ALPHABET).map_err(|_| AddressError::FromBase58Error)?; + let bytes = H256::try_from(bytes.as_slice()).map_err(|_| AddressError::InvalidInput)?; + Ok(SolanaAddress { bytes }) + } +} + +impl fmt::Display for SolanaAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let encoded = base58::encode(self.bytes.as_slice(), &SOLANA_ALPHABET); + write!(f, "{}", encoded) + } +} + +impl Serialize for SolanaAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for SolanaAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let addr_str = String::deserialize(deserializer)?; + SolanaAddress::from_str(&addr_str).map_err(|e| DeError::custom(format!("{e:?}"))) + } +} diff --git a/rust/chains/tw_solana/src/compiler.rs b/rust/chains/tw_solana/src/compiler.rs new file mode 100644 index 00000000000..5f2aabe50db --- /dev/null +++ b/rust/chains/tw_solana/src/compiler.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::SigningResult; +use tw_coin_entry::signing_output_error; +use tw_proto::Solana::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SolanaCompiler; + +impl SolanaCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + todo!() + } +} diff --git a/rust/chains/tw_solana/src/entry.rs b/rust/chains/tw_solana/src/entry.rs new file mode 100644 index 00000000000..9d9f0f7b49a --- /dev/null +++ b/rust/chains/tw_solana/src/entry.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SolanaAddress; +use crate::compiler::SolanaCompiler; +use crate::signer::SolanaSigner; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::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::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Solana::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SolanaEntry; + +impl CoinEntry for SolanaEntry { + type AddressPrefix = NoPrefix; + type Address = SolanaAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + SolanaAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + SolanaAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + SolanaAddress::with_public_key(&public_key) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + SolanaSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + SolanaCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + SolanaCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_solana/src/lib.rs b/rust/chains/tw_solana/src/lib.rs new file mode 100644 index 00000000000..8283647ad9b --- /dev/null +++ b/rust/chains/tw_solana/src/lib.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::base58::Alphabet; + +pub mod address; +pub mod compiler; +pub mod entry; +pub mod modules; +pub mod signer; +pub mod transaction; + +// cbindgen:ignore +pub const SOLANA_ALPHABET: Alphabet = *Alphabet::BITCOIN; diff --git a/rust/chains/tw_solana/src/modules/mod.rs b/rust/chains/tw_solana/src/modules/mod.rs new file mode 100644 index 00000000000..576e5ccc106 --- /dev/null +++ b/rust/chains/tw_solana/src/modules/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod tx_signer; +pub mod utils; diff --git a/rust/chains/tw_solana/src/modules/tx_signer.rs b/rust/chains/tw_solana/src/modules/tx_signer.rs new file mode 100644 index 00000000000..763f037149f --- /dev/null +++ b/rust/chains/tw_solana/src/modules/tx_signer.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::{versioned, Pubkey, Signature}; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_keypair::ed25519; +use tw_keypair::traits::SigningKeyTrait; +use tw_memory::Data; + +pub struct TxSigner; + +impl TxSigner { + pub fn sign_versioned( + mut tx: versioned::VersionedTransaction, + keys: &[ed25519::sha512::PrivateKey], + ) -> SigningResult { + if keys.len() != tx.message.num_required_signatures() { + return Err(SigningError(SigningErrorType::Error_signatures_count)); + } + + tx.zeroize_signatures(); + let message_data = bincode::serialize(&tx.message) + .map_err(|_| SigningError(SigningErrorType::Error_invalid_params))?; + + for private_key in keys { + let signing_pubkey = Pubkey(private_key.public().to_bytes()); + // Find an index of the corresponding account. + let account_index = tx + .message + .get_account_index(signing_pubkey) + .ok_or(SigningError(SigningErrorType::Error_missing_private_key))?; + let signature_to_reassign = tx + .signatures + .get_mut(account_index) + .ok_or(SigningError(SigningErrorType::Error_signatures_count))?; + + let ed25519_signature = private_key.sign(message_data.clone())?; + *signature_to_reassign = Signature(ed25519_signature.to_bytes()); + } + + Ok(tx) + } + + pub fn preimage_versioned(tx: &versioned::VersionedTransaction) -> SigningResult { + bincode::serialize(&tx.message) + .map_err(|_| SigningError(SigningErrorType::Error_invalid_params)) + } +} diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs new file mode 100644 index 00000000000..99f9f69806c --- /dev/null +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_signer::TxSigner; +use crate::transaction::versioned::VersionedTransaction; +use crate::SOLANA_ALPHABET; +use std::borrow::Cow; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_coin_entry::signing_output_error; +use tw_encoding::{base58, base64}; +use tw_hash::H256; +use tw_keypair::{ed25519, KeyPairResult}; +use tw_memory::Data; +use tw_proto::Solana::Proto; + +pub struct SolanaTransaction; + +impl SolanaTransaction { + pub fn update_blockhash_and_sign( + encoded_tx: &str, + recent_blockhash: &str, + private_keys: &[Data], + ) -> Proto::SigningOutput<'static> { + Self::update_blockhash_and_sign_impl(encoded_tx, recent_blockhash, private_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn update_blockhash_and_sign_impl( + encoded_tx: &str, + recent_blockhash: &str, + private_keys: &[Data], + ) -> SigningResult> { + let is_url = false; + let tx_bytes = base64::decode(encoded_tx, is_url)?; + + let mut tx_to_sign: VersionedTransaction = bincode::deserialize(&tx_bytes) + .map_err(|_| SigningError(SigningErrorType::Error_input_parse))?; + + let new_blockchain_hash = base58::decode(recent_blockhash, &SOLANA_ALPHABET)?; + let new_blockchain_hash = H256::try_from(new_blockchain_hash.as_slice()) + .map_err(|_| SigningError(SigningErrorType::Error_invalid_params))?; + + // Update the transaction's blockhash and re-sign it. + tx_to_sign.message.set_recent_blockhash(new_blockchain_hash); + + let unsigned_encoded = TxSigner::preimage_versioned(&tx_to_sign)?; + + // Do not sign the transaction if there is no private keys, but set zeroed signatures. + // It's needed to estimate the transaction fee with an updated blockhash without using real private keys. + let signed_tx = if private_keys.is_empty() { + tx_to_sign.zeroize_signatures(); + tx_to_sign + } else { + let private_keys = private_keys + .iter() + .map(|pk| ed25519::sha512::PrivateKey::try_from(pk.as_slice())) + .collect::>>()?; + + TxSigner::sign_versioned(tx_to_sign, &private_keys)? + }; + + let unsigned_encoded = base64::encode(&unsigned_encoded, is_url); + let signed_encoded = bincode::serialize(&signed_tx) + .map_err(|_| SigningError(SigningErrorType::Error_internal))?; + let signed_encoded = base64::encode(&signed_encoded, is_url); + let message_encoded = bincode::serialize(&signed_tx.message) + .map_err(|_| SigningError(SigningErrorType::Error_internal))?; + let message_encoded = base64::encode(&message_encoded, is_url); + + Ok(Proto::SigningOutput { + encoded: Cow::from(signed_encoded), + unsigned_tx: Cow::from(unsigned_encoded), + message_encoded: Cow::from(message_encoded), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_solana/src/signer.rs b/rust/chains/tw_solana/src/signer.rs new file mode 100644 index 00000000000..27633d26f26 --- /dev/null +++ b/rust/chains/tw_solana/src/signer.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::SigningResult; +use tw_coin_entry::signing_output_error; +use tw_proto::Solana::Proto; + +pub struct SolanaSigner; + +impl SolanaSigner { + pub fn sign( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> Proto::SigningOutput<'static> { + Self::sign_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn sign_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } +} diff --git a/rust/chains/tw_solana/src/transaction/legacy.rs b/rust/chains/tw_solana/src/transaction/legacy.rs new file mode 100644 index 00000000000..94be08e1bc7 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/legacy.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::{short_vec, CompiledInstruction, MessageHeader, Pubkey, Signature}; +use serde::{Deserialize, Serialize}; +use tw_hash::{as_byte_sequence, H256}; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Message { + /// The message header, identifying signed and read-only `account_keys`. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + pub header: MessageHeader, + + /// All the account keys used by this transaction. + #[serde(with = "short_vec")] + pub account_keys: Vec, + + /// The id of a recent ledger entry. + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + + /// Programs that will be executed in sequence and committed in one atomic transaction if all + /// succeed. + #[serde(with = "short_vec")] + pub instructions: Vec, +} + +#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize)] +pub struct Transaction { + /// A set of signatures of a serialized [`Message`], signed by the first + /// keys of the `Message`'s [`account_keys`], where the number of signatures + /// is equal to [`num_required_signatures`] of the `Message`'s + /// [`MessageHeader`]. + /// + /// [`account_keys`]: Message::account_keys + /// [`MessageHeader`]: crate::message::MessageHeader + /// [`num_required_signatures`]: crate::message::MessageHeader::num_required_signatures + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + #[serde(with = "short_vec")] + pub signatures: Vec, + + /// The message to sign. + pub message: Message, +} + +impl Transaction { + /// Zeroize signatures before re-signing... + pub fn zeroize_signatures(&mut self) { + self.signatures + .iter_mut() + .for_each(|signature| *signature = Signature::default()); + } +} diff --git a/rust/chains/tw_solana/src/transaction/mod.rs b/rust/chains/tw_solana/src/transaction/mod.rs new file mode 100644 index 00000000000..5da19f1c730 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/mod.rs @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::{Deserialize, Serialize}; +use tw_hash::{as_byte_sequence, H256, H512}; + +pub mod legacy; +pub mod short_vec; +pub mod v0; +pub mod versioned; + +#[derive(Clone, Copy, Default, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Pubkey(#[serde(with = "as_byte_sequence")] pub(crate) H256); + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub struct MessageHeader { + /// The number of signatures required for this message to be considered + /// valid. The signers of those signatures must match the first + /// `num_required_signatures` of [`Message::account_keys`]. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + pub num_required_signatures: u8, + + /// The last `num_readonly_signed_accounts` of the signed keys are read-only + /// accounts. + pub num_readonly_signed_accounts: u8, + + /// The last `num_readonly_unsigned_accounts` of the unsigned keys are + /// read-only accounts. + pub num_readonly_unsigned_accounts: u8, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CompiledInstruction { + /// Index into the transaction keys array indicating the program account that executes this instruction. + pub program_id_index: u8, + /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. + #[serde(with = "short_vec")] + pub accounts: Vec, + /// The program input data. + #[serde(with = "short_vec")] + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Signature(#[serde(with = "as_byte_sequence")] pub(crate) H512); + +#[cfg(test)] +mod tests { + use super::*; + use crate::address::SolanaAddress; + use crate::transaction::v0::MessageAddressTableLookup; + use crate::transaction::versioned::{VersionedMessage, VersionedTransaction}; + use crate::SOLANA_ALPHABET; + use std::str::FromStr; + use tw_encoding::hex::ToHex; + use tw_encoding::{base58, base64}; + use tw_memory::Data; + + fn address_pubkey(addr: &'static str) -> Pubkey { + Pubkey(SolanaAddress::from_str(addr).unwrap().bytes()) + } + + fn base58_decode(s: &'static str) -> Data { + base58::decode(s, &SOLANA_ALPHABET).unwrap() + } + + fn base58_decode_h256(s: &'static str) -> H256 { + let bytes = base58::decode(s, &SOLANA_ALPHABET).unwrap(); + H256::try_from(bytes.as_slice()).unwrap() + } + + #[test] + fn test_rango_transaction_ser_de() { + let serialized = base64::decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4=", false).unwrap(); + let actual: VersionedTransaction = bincode::deserialize(&serialized).unwrap(); + + let expected = VersionedTransaction { + signatures: vec![Signature(H512::default())], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 7, + }, + account_keys: vec![ + address_pubkey("AHy6YZA8BsHgQfVkk7MbwpAN94iyN7Nf1zN4nPqUN32Q"), + address_pubkey("g7dD1FHSemkUQrX1Eak37wzvDjscgBW2pFCENwjLdMX"), + address_pubkey("7m57LBTxtzhWn6WdFxKtnoJLBQXyNERLYebebXLVaKy3"), + address_pubkey("AEBCPtV8FFkWFAKxrz7mbYvobpkZuWaRWQCyJVRaheUD"), + address_pubkey("BND2ehwWVeHVA5EtMm2b7Vu51AT8f2PNWusS9KQX5moy"), + address_pubkey("DVCeozFGbe6ew3eWTnZByjHeYqTq1cvbrB7JJhkLxaRJ"), + address_pubkey("GvgWmk8iPACw1AEMt47WzkuTkKoSGbn4Xk3aLM8vdbJD"), + address_pubkey("HkphEpUqnFBxBuCPEq5j1HA9L8EwmsmRT6UcFKziptM1"), + address_pubkey("Hzxx6b5a7dmmJeDXLQzr4dTrc2HGK9ar5YRakZgr3ZZ7"), + address_pubkey("11111111111111111111111111111111"), + address_pubkey("ComputeBudget111111111111111111111111111111"), + address_pubkey("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"), + address_pubkey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + address_pubkey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + address_pubkey("D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"), + address_pubkey("GGztQqQ6pCPaJQnNpXBgELr5cs3WwDakRbh1iEMzjgSJ"), + ], + recent_blockhash: base58_decode_h256( + "DiSimxK2z1cRa6yD4goqte3rDMmghJAD8WDUZEab2CzD", + ), + instructions: vec![ + CompiledInstruction { + program_id_index: 10, + accounts: vec![], + data: base58_decode("K1FDJ7"), + }, + CompiledInstruction { + program_id_index: 10, + accounts: vec![], + data: base58_decode("3E9ErJ5MrzbZ"), + }, + CompiledInstruction { + program_id_index: 13, + accounts: vec![0, 6, 0, 35, 9, 12], + data: base58_decode("2"), + }, + CompiledInstruction { + program_id_index: 9, + accounts: vec![0, 6], + data: base58_decode("3Bxs3zzLZLuLQEYX"), + }, + CompiledInstruction { + program_id_index: 12, + accounts: vec![6], + data: base58_decode("J"), + }, + CompiledInstruction { + program_id_index: 11, + accounts: vec![ + 12, 15, 0, 6, 1, 5, 2, 35, 33, 11, 11, 14, 11, 29, 12, 15, 26, 1, 25, + 7, 24, 4, 8, 3, 36, 27, 28, 12, 30, 15, 7, 5, 16, 18, 17, 31, 21, 31, + 20, 23, 7, 5, 33, 34, 19, 31, 15, 12, 12, 32, 31, 22, 11, + ], + data: base58_decode( + "5n9zLuyvSGkuf4iDD6PfDvzvzehUkDghmApUkZSXSx57jF9RGSH5Y23tzFJDG3", + ), + }, + CompiledInstruction { + program_id_index: 12, + accounts: vec![6, 0, 0], + data: base58_decode("A"), + }, + ], + address_table_lookups: vec![ + MessageAddressTableLookup { + account_key: address_pubkey("FeXRmSWmwChZbB2EC7Qjw9XKk28yBrPj3k3nzT1DKfak"), + writable_indexes: vec![202, 200, 201], + readonly_indexes: vec![196, 197, 36, 199], + }, + MessageAddressTableLookup { + account_key: address_pubkey("5cFsmTCEfmvpBUBHqsWZnf9n5vTWLYH2LT8X7HdShwxP"), + writable_indexes: vec![160, 245, 248, 159, 157], + readonly_indexes: vec![156, 244, 246, 247], + }, + MessageAddressTableLookup { + account_key: address_pubkey("HJ5StCvsDU4JsvK39VcsHjaoTRTtQU749MQ9qUsJaG1m"), + writable_indexes: vec![122, 121, 125], + readonly_indexes: vec![110, 126], + }, + ], + }), + }; + + assert_eq!(actual, expected); + + let serialized_again = bincode::serialize(&actual).unwrap(); + assert_eq!(serialized_again.to_hex(), serialized.to_hex()); + } +} diff --git a/rust/chains/tw_solana/src/transaction/short_vec.rs b/rust/chains/tw_solana/src/transaction/short_vec.rs new file mode 100644 index 00000000000..9b89f34277d --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/short_vec.rs @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Compact serde-encoding of vectors with small length. +//! Source code: https://github.com/solana-labs/solana/blob/a16f982169eb197fad0eb8c58c307fb069f69d8f/sdk/program/src/short_vec.rs + +#![allow(clippy::arithmetic_side_effects)] +use serde::{ + de::{self, Deserializer, SeqAccess, Visitor}, + ser::{self, SerializeTuple, Serializer}, + Deserialize, Serialize, +}; +use std::{convert::TryFrom, fmt, marker::PhantomData}; + +/// Same as u16, but serialized with 1 to 3 bytes. If the value is above +/// 0x7f, the top bit is set and the remaining value is stored in the next +/// bytes. Each byte follows the same pattern until the 3rd byte. The 3rd +/// byte, if needed, uses all 8 bits to store the last byte of the original +/// value. +pub struct ShortU16(pub u16); + +impl Serialize for ShortU16 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Pass a non-zero value to serialize_tuple() so that serde_json will + // generate an open bracket. + let mut seq = serializer.serialize_tuple(1)?; + + let mut rem_val = self.0; + loop { + let mut elem = (rem_val & 0x7f) as u8; + rem_val >>= 7; + if rem_val == 0 { + seq.serialize_element(&elem)?; + break; + } else { + elem |= 0x80; + seq.serialize_element(&elem)?; + } + } + seq.end() + } +} + +enum VisitStatus { + Done(u16), + More(u16), +} + +#[derive(Debug)] +enum VisitError { + TooLong(usize), + TooShort(usize), + Overflow(u32), + Alias, + ByteThreeContinues, +} + +impl VisitError { + fn into_de_error<'de, A>(self) -> A::Error + where + A: SeqAccess<'de>, + { + match self { + VisitError::TooLong(len) => de::Error::invalid_length(len, &"three or fewer bytes"), + VisitError::TooShort(len) => de::Error::invalid_length(len, &"more bytes"), + VisitError::Overflow(val) => de::Error::invalid_value( + de::Unexpected::Unsigned(val as u64), + &"a value in the range [0, 65535]", + ), + VisitError::Alias => de::Error::invalid_value( + de::Unexpected::Other("alias encoding"), + &"strict form encoding", + ), + VisitError::ByteThreeContinues => de::Error::invalid_value( + de::Unexpected::Other("continue signal on byte-three"), + &"a terminal signal on or before byte-three", + ), + } + } +} + +type VisitResult = Result; + +const MAX_ENCODING_LENGTH: usize = 3; +fn visit_byte(elem: u8, val: u16, nth_byte: usize) -> VisitResult { + if elem == 0 && nth_byte != 0 { + return Err(VisitError::Alias); + } + + let val = u32::from(val); + let elem = u32::from(elem); + let elem_val = elem & 0x7f; + let elem_done = (elem & 0x80) == 0; + + if nth_byte >= MAX_ENCODING_LENGTH { + return Err(VisitError::TooLong(nth_byte.saturating_add(1))); + } else if nth_byte == MAX_ENCODING_LENGTH.saturating_sub(1) && !elem_done { + return Err(VisitError::ByteThreeContinues); + } + + let shift = u32::try_from(nth_byte) + .unwrap_or(std::u32::MAX) + .saturating_mul(7); + let elem_val = elem_val.checked_shl(shift).unwrap_or(std::u32::MAX); + + let new_val = val | elem_val; + let val = u16::try_from(new_val).map_err(|_| VisitError::Overflow(new_val))?; + + if elem_done { + Ok(VisitStatus::Done(val)) + } else { + Ok(VisitStatus::More(val)) + } +} + +struct ShortU16Visitor; + +impl<'de> Visitor<'de> for ShortU16Visitor { + type Value = ShortU16; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a ShortU16") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + // Decodes an unsigned 16 bit integer one-to-one encoded as follows: + // 1 byte : 0xxxxxxx => 00000000 0xxxxxxx : 0 - 127 + // 2 bytes : 1xxxxxxx 0yyyyyyy => 00yyyyyy yxxxxxxx : 128 - 16,383 + // 3 bytes : 1xxxxxxx 1yyyyyyy 000000zz => zzyyyyyy yxxxxxxx : 16,384 - 65,535 + let mut val: u16 = 0; + for nth_byte in 0..MAX_ENCODING_LENGTH { + let elem: u8 = seq.next_element()?.ok_or_else(|| { + VisitError::TooShort(nth_byte.saturating_add(1)).into_de_error::() + })?; + match visit_byte(elem, val, nth_byte).map_err(|e| e.into_de_error::())? { + VisitStatus::Done(new_val) => return Ok(ShortU16(new_val)), + VisitStatus::More(new_val) => val = new_val, + } + } + + Err(VisitError::ByteThreeContinues.into_de_error::()) + } +} + +impl<'de> Deserialize<'de> for ShortU16 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_tuple(3, ShortU16Visitor) + } +} + +/// If you don't want to use the ShortVec newtype, you can do ShortVec +/// serialization on an ordinary vector with the following field annotation: +/// +/// #[serde(with = "short_vec")] +/// +pub fn serialize( + elements: &[T], + serializer: S, +) -> Result { + // Pass a non-zero value to serialize_tuple() so that serde_json will + // generate an open bracket. + let mut seq = serializer.serialize_tuple(1)?; + + let len = elements.len(); + if len > std::u16::MAX as usize { + return Err(ser::Error::custom("length larger than u16")); + } + let short_len = ShortU16(len as u16); + seq.serialize_element(&short_len)?; + + for element in elements { + seq.serialize_element(element)?; + } + seq.end() +} + +struct ShortVecVisitor { + _t: PhantomData, +} + +impl<'de, T> Visitor<'de> for ShortVecVisitor +where + T: Deserialize<'de>, +{ + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a Vec with a multi-byte length") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: SeqAccess<'de>, + { + let short_len: ShortU16 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let len = short_len.0 as usize; + + let mut result = Vec::with_capacity(len); + for i in 0..len { + let elem = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &self))?; + result.push(elem); + } + Ok(result) + } +} + +/// If you don't want to use the ShortVec newtype, you can do ShortVec +/// deserialization on an ordinary vector with the following field annotation: +/// +/// #[serde(with = "short_vec")] +/// +pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let visitor = ShortVecVisitor { _t: PhantomData }; + deserializer.deserialize_tuple(std::usize::MAX, visitor) +} + +pub struct ShortVec(pub Vec); + +impl Serialize for ShortVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize(&self.0, serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for ShortVec { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserialize(deserializer).map(ShortVec) + } +} + +/// Return the decoded value and how many bytes it consumed. +#[allow(clippy::result_unit_err)] +pub fn decode_shortu16_len(bytes: &[u8]) -> Result<(usize, usize), ()> { + let mut val = 0; + for (nth_byte, byte) in bytes.iter().take(MAX_ENCODING_LENGTH).enumerate() { + match visit_byte(*byte, val, nth_byte).map_err(|_| ())? { + VisitStatus::More(new_val) => val = new_val, + VisitStatus::Done(new_val) => { + return Ok((usize::from(new_val), nth_byte.saturating_add(1))); + }, + } + } + Err(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use bincode::{deserialize, serialize}; + + /// Return the serialized length. + fn encode_len(len: u16) -> Vec { + bincode::serialize(&ShortU16(len)).unwrap() + } + + fn assert_len_encoding(len: u16, bytes: &[u8]) { + assert_eq!(encode_len(len), bytes, "unexpected usize encoding"); + assert_eq!( + decode_shortu16_len(bytes).unwrap(), + (usize::from(len), bytes.len()), + "unexpected usize decoding" + ); + } + + #[test] + fn test_short_vec_encode_len() { + assert_len_encoding(0x0, &[0x0]); + assert_len_encoding(0x7f, &[0x7f]); + assert_len_encoding(0x80, &[0x80, 0x01]); + assert_len_encoding(0xff, &[0xff, 0x01]); + assert_len_encoding(0x100, &[0x80, 0x02]); + assert_len_encoding(0x7fff, &[0xff, 0xff, 0x01]); + assert_len_encoding(0xffff, &[0xff, 0xff, 0x03]); + } + + fn assert_good_deserialized_value(value: u16, bytes: &[u8]) { + assert_eq!(value, deserialize::(bytes).unwrap().0); + } + + fn assert_bad_deserialized_value(bytes: &[u8]) { + assert!(deserialize::(bytes).is_err()); + } + + #[test] + fn test_deserialize() { + assert_good_deserialized_value(0x0000, &[0x00]); + assert_good_deserialized_value(0x007f, &[0x7f]); + assert_good_deserialized_value(0x0080, &[0x80, 0x01]); + assert_good_deserialized_value(0x00ff, &[0xff, 0x01]); + assert_good_deserialized_value(0x0100, &[0x80, 0x02]); + assert_good_deserialized_value(0x07ff, &[0xff, 0x0f]); + assert_good_deserialized_value(0x3fff, &[0xff, 0x7f]); + assert_good_deserialized_value(0x4000, &[0x80, 0x80, 0x01]); + assert_good_deserialized_value(0xffff, &[0xff, 0xff, 0x03]); + + // aliases + // 0x0000 + assert_bad_deserialized_value(&[0x80, 0x00]); + assert_bad_deserialized_value(&[0x80, 0x80, 0x00]); + // 0x007f + assert_bad_deserialized_value(&[0xff, 0x00]); + assert_bad_deserialized_value(&[0xff, 0x80, 0x00]); + // 0x0080 + assert_bad_deserialized_value(&[0x80, 0x81, 0x00]); + // 0x00ff + assert_bad_deserialized_value(&[0xff, 0x81, 0x00]); + // 0x0100 + assert_bad_deserialized_value(&[0x80, 0x82, 0x00]); + // 0x07ff + assert_bad_deserialized_value(&[0xff, 0x8f, 0x00]); + // 0x3fff + assert_bad_deserialized_value(&[0xff, 0xff, 0x00]); + + // too short + assert_bad_deserialized_value(&[]); + assert_bad_deserialized_value(&[0x80]); + + // too long + assert_bad_deserialized_value(&[0x80, 0x80, 0x80, 0x00]); + + // too large + // 0x0001_0000 + assert_bad_deserialized_value(&[0x80, 0x80, 0x04]); + // 0x0001_8000 + assert_bad_deserialized_value(&[0x80, 0x80, 0x06]); + } + + #[test] + fn test_short_vec_u8() { + let vec = ShortVec(vec![4u8; 32]); + let bytes = serialize(&vec).unwrap(); + assert_eq!(bytes.len(), vec.0.len() + 1); + + let vec1: ShortVec = deserialize(&bytes).unwrap(); + assert_eq!(vec.0, vec1.0); + } + + #[test] + fn test_short_vec_u8_too_long() { + let vec = ShortVec(vec![4u8; std::u16::MAX as usize]); + assert!(matches!(serialize(&vec), Ok(_))); + + let vec = ShortVec(vec![4u8; std::u16::MAX as usize + 1]); + assert!(matches!(serialize(&vec), Err(_))); + } + + #[test] + fn test_short_vec_json() { + let vec = ShortVec(vec![0, 1, 2]); + let s = serde_json::to_string(&vec).unwrap(); + assert_eq!(s, "[[3],0,1,2]"); + } + + #[test] + fn test_short_vec_aliased_length() { + let bytes = [ + 0x81, 0x80, 0x00, // 3-byte alias of 1 + 0x00, + ]; + assert!(deserialize::>(&bytes).is_err()); + } +} diff --git a/rust/chains/tw_solana/src/transaction/v0.rs b/rust/chains/tw_solana/src/transaction/v0.rs new file mode 100644 index 00000000000..18f70a40ef4 --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/v0.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::{short_vec, CompiledInstruction, MessageHeader, Pubkey}; +use serde::{Deserialize, Serialize}; +use tw_hash::{as_byte_sequence, H256}; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MessageAddressTableLookup { + /// Address lookup table account key + pub account_key: Pubkey, + /// List of indexes used to load writable account addresses + #[serde(with = "short_vec")] + pub writable_indexes: Vec, + /// List of indexes used to load readonly account addresses + #[serde(with = "short_vec")] + pub readonly_indexes: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Message { + /// The message header, identifying signed and read-only `account_keys`. + /// Header values only describe static `account_keys`, they do not describe + /// any additional account keys loaded via address table lookups. + pub header: MessageHeader, + + /// List of accounts loaded by this transaction. + #[serde(with = "short_vec")] + pub account_keys: Vec, + + /// The blockhash of a recent block. + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + + /// Instructions that invoke a designated program, are executed in sequence, + /// and committed in one atomic transaction if all succeed. + /// + /// # Notes + /// + /// Program indexes must index into the list of message `account_keys` because + /// program id's cannot be dynamically loaded from a lookup table. + /// + /// Account indexes must index into the list of addresses + /// constructed from the concatenation of three key lists: + /// 1) message `account_keys` + /// 2) ordered list of keys loaded from `writable` lookup table indexes + /// 3) ordered list of keys loaded from `readable` lookup table indexes + #[serde(with = "short_vec")] + pub instructions: Vec, + + /// List of address table lookups used to load additional accounts + /// for this transaction. + #[serde(with = "short_vec")] + pub address_table_lookups: Vec, +} diff --git a/rust/chains/tw_solana/src/transaction/versioned.rs b/rust/chains/tw_solana/src/transaction/versioned.rs new file mode 100644 index 00000000000..ffe94c9d76d --- /dev/null +++ b/rust/chains/tw_solana/src/transaction/versioned.rs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Source code: https://github.com/solana-labs/solana/blob/a16f982169eb197fad0eb8c58c307fb069f69d8f/sdk/program/src/message/versions/mod.rs + +use crate::transaction::{ + legacy, short_vec, v0, CompiledInstruction, MessageHeader, Pubkey, Signature, +}; +use serde::de::{SeqAccess, Unexpected, Visitor}; +use serde::ser::SerializeTuple; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use tw_hash::{as_byte_sequence, H256}; + +/// Bit mask that indicates whether a serialized message is versioned. +pub const MESSAGE_VERSION_PREFIX: u8 = 0x80; + +// NOTE: Serialization-related changes must be paired with the direct read at sigverify. +/// An atomic transaction +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct VersionedTransaction { + /// List of signatures + #[serde(with = "short_vec")] + pub signatures: Vec, + /// Message to sign. + pub message: VersionedMessage, +} + +impl VersionedTransaction { + /// Fill the signatures up with zeroed signatures + /// (same number of signatures as [`VersionedTransaction::num_required_signatures`]). + pub fn zeroize_signatures(&mut self) { + self.signatures = vec![Signature::default(); self.message.num_required_signatures()]; + } +} + +/// Either a legacy message or a v0 message. +/// +/// # Serialization +/// +/// If the first bit is set, the remaining 7 bits will be used to determine +/// which message version is serialized starting from version `0`. If the first +/// is bit is not set, all bytes are used to encode the legacy `Message` +/// format. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum VersionedMessage { + Legacy(legacy::Message), + V0(v0::Message), +} + +impl VersionedMessage { + pub fn num_required_signatures(&self) -> usize { + match self { + VersionedMessage::Legacy(legacy) => legacy.header.num_required_signatures as usize, + VersionedMessage::V0(v0) => v0.header.num_required_signatures as usize, + } + } + + pub fn get_account_index(&self, account_pubkey: Pubkey) -> Option { + let account_keys = match self { + VersionedMessage::Legacy(legacy) => &legacy.account_keys, + VersionedMessage::V0(v0) => &v0.account_keys, + }; + account_keys.iter().position(|pk| *pk == account_pubkey) + } + + pub fn set_recent_blockhash(&mut self, recent_blockhash: H256) { + match self { + VersionedMessage::Legacy(legacy) => legacy.recent_blockhash = recent_blockhash, + VersionedMessage::V0(v0) => v0.recent_blockhash = recent_blockhash, + } + } +} + +impl Serialize for VersionedMessage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Legacy(message) => { + let mut seq = serializer.serialize_tuple(1)?; + seq.serialize_element(message)?; + seq.end() + }, + Self::V0(message) => { + let mut seq = serializer.serialize_tuple(2)?; + seq.serialize_element(&MESSAGE_VERSION_PREFIX)?; + seq.serialize_element(message)?; + seq.end() + }, + } + } +} + +enum MessagePrefix { + Legacy(u8), + Versioned(u8), +} + +impl<'de> Deserialize<'de> for MessagePrefix { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PrefixVisitor; + + impl<'de> Visitor<'de> for PrefixVisitor { + type Value = MessagePrefix; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("message prefix byte") + } + + // Serde's integer visitors bubble up to u64 so check the prefix + // with this function instead of visit_u8. This approach is + // necessary because serde_json directly calls visit_u64 for + // unsigned integers. + fn visit_u64(self, value: u64) -> Result { + if value > u8::MAX as u64 { + Err(de::Error::invalid_type(Unexpected::Unsigned(value), &self))?; + } + + let byte = value as u8; + if byte & MESSAGE_VERSION_PREFIX != 0 { + Ok(MessagePrefix::Versioned(byte & !MESSAGE_VERSION_PREFIX)) + } else { + Ok(MessagePrefix::Legacy(byte)) + } + } + } + + deserializer.deserialize_u8(PrefixVisitor) + } +} + +impl<'de> Deserialize<'de> for VersionedMessage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MessageVisitor; + + impl<'de> Visitor<'de> for MessageVisitor { + type Value = VersionedMessage; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("message bytes") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let prefix: MessagePrefix = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match prefix { + MessagePrefix::Legacy(num_required_signatures) => { + // The remaining fields of the legacy Message struct after the first byte. + #[derive(Serialize, Deserialize)] + struct RemainingLegacyMessage { + pub num_readonly_signed_accounts: u8, + pub num_readonly_unsigned_accounts: u8, + #[serde(with = "short_vec")] + pub account_keys: Vec, + #[serde(with = "as_byte_sequence")] + pub recent_blockhash: H256, + #[serde(with = "short_vec")] + pub instructions: Vec, + } + + let message: RemainingLegacyMessage = + seq.next_element()?.ok_or_else(|| { + // will never happen since tuple length is always 2 + de::Error::invalid_length(1, &self) + })?; + + Ok(VersionedMessage::Legacy(legacy::Message { + header: MessageHeader { + num_required_signatures, + num_readonly_signed_accounts: message.num_readonly_signed_accounts, + num_readonly_unsigned_accounts: message + .num_readonly_unsigned_accounts, + }, + account_keys: message.account_keys, + recent_blockhash: message.recent_blockhash, + instructions: message.instructions, + })) + }, + MessagePrefix::Versioned(version) => { + match version { + 0 => { + Ok(VersionedMessage::V0(seq.next_element()?.ok_or_else( + || { + // will never happen since tuple length is always 2 + de::Error::invalid_length(1, &self) + }, + )?)) + }, + 127 => { + // 0xff is used as the first byte of the off-chain messages + // which corresponds to version 127 of the versioned messages. + // This explicit check is added to prevent the usage of version 127 + // in the runtime as a valid transaction. + Err(de::Error::custom("off-chain messages are not accepted")) + }, + _ => Err(de::Error::invalid_value( + de::Unexpected::Unsigned(version as u64), + &"a valid transaction message version", + )), + } + }, + } + } + } + + deserializer.deserialize_tuple(2, MessageVisitor) + } +} diff --git a/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs new file mode 100644 index 00000000000..a4a4cd1c08d --- /dev/null +++ b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::SigningErrorType; +use tw_encoding::base58; +use tw_solana::modules::utils::SolanaTransaction; +use tw_solana::SOLANA_ALPHABET; + +#[test] +fn test_update_recent_blockhash_and_sign() { + // base64 encoded + let encoded_tx = "AQPWaOi7dMdmQpXi8HyQQKwiqIftrg1igGQxGtZeT50ksn4wAnyH4DtDrkkuE0fqgx80LTp4LwNN9a440SrmoA8BAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAKgAAAAAAAAA="; + // base58 encoded + let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; + let private_key = base58::decode( + "A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr", + &SOLANA_ALPHABET, + ) + .unwrap(); + + let output = + SolanaTransaction::update_blockhash_and_sign(encoded_tx, new_blockhash, &[private_key]); + assert_eq!(output.error, SigningErrorType::OK); + let expected = "AdQl49kO1FxfkAnAuK9KSQEGLzxHNYLqBrYGFN711q7aT/qyrzYMn/7/IdFBy6yMhjOA1CkwZsgmqmbu+XKvVAUBAAEDZsL1CMnFVcrMn7JtiOiN1U4hC7WovOVof2DX51xM0H/GizyJTHgrBanCf8bGbrFNTn0x3pCGq30hKbywSTr6AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAseKSLpOE0fdq67Jk9Ckme2c3SYD//nqcykr/oci67zEBAgIAAQwCAAAAKgAAAAAAAAA="; + assert_eq!(output.encoded, expected); +} diff --git a/rust/tw_any_coin/tests/chains/mod.rs b/rust/tw_any_coin/tests/chains/mod.rs index cefb0e37ae4..cd580c22dde 100644 --- a/rust/tw_any_coin/tests/chains/mod.rs +++ b/rust/tw_any_coin/tests/chains/mod.rs @@ -12,6 +12,7 @@ mod greenfield; mod internet_computer; mod native_evmos; mod native_injective; +mod solana; mod tbinance; mod thorchain; mod zetachain; diff --git a/rust/tw_any_coin/tests/chains/solana/mod.rs b/rust/tw_any_coin/tests/chains/solana/mod.rs new file mode 100644 index 00000000000..2b199e6a807 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/solana/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod solana_address; +mod solana_compile; +mod solana_sign; diff --git a/rust/tw_any_coin/tests/chains/solana/solana_address.rs b/rust/tw_any_coin/tests/chains/solana/solana_address.rs new file mode 100644 index 00000000000..9ba8aa31d4d --- /dev/null +++ b/rust/tw_any_coin/tests/chains/solana/solana_address.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_get_data, test_address_invalid, test_address_normalization, test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_solana_address_normalization() { + test_address_normalization( + CoinType::Solana, + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST", + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST", + ); +} + +#[test] +fn test_solana_address_is_valid() { + test_address_valid( + CoinType::Solana, + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST", + ); +} + +#[test] +fn test_solana_address_invalid() { + // Contains invalid base-58 character + test_address_invalid( + CoinType::Solana, + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdSl", + ); + // Is invalid length + test_address_invalid( + CoinType::Solana, + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpd", + ); +} + +#[test] +fn test_solana_address_get_data() { + test_address_get_data( + CoinType::Solana, + "2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST", + "18f9d8d877393bbbe8d697a8a2e52879cc7e84f467656d1cce6bab5a8d2637ec", + ); +} diff --git a/rust/tw_any_coin/tests/chains/solana/solana_compile.rs b/rust/tw_any_coin/tests/chains/solana/solana_compile.rs new file mode 100644 index 00000000000..84b9a412183 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/solana/solana_compile.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +#[ignore] +fn test_solana_compile() { + todo!() +} diff --git a/rust/tw_any_coin/tests/chains/solana/solana_sign.rs b/rust/tw_any_coin/tests/chains/solana/solana_sign.rs new file mode 100644 index 00000000000..194cad133fe --- /dev/null +++ b/rust/tw_any_coin/tests/chains/solana/solana_sign.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +#[ignore] +fn test_solana_sign() { + todo!() +} diff --git a/rust/tw_any_coin/tests/coin_address_derivation_test.rs b/rust/tw_any_coin/tests/coin_address_derivation_test.rs index 9cf10ad244a..b7abadbb806 100644 --- a/rust/tw_any_coin/tests/coin_address_derivation_test.rs +++ b/rust/tw_any_coin/tests/coin_address_derivation_test.rs @@ -148,6 +148,7 @@ fn test_coin_address_derivation() { CoinType::TBinance => "tbnb1ten42eesehw0ktddcp0fws7d3ycsqez3n49hpe", CoinType::NativeZetaChain => "zeta14s0vgnj0pjnazu4hsqlksdk7slah9vcfcwctsr", CoinType::Dydx => "dydx1ten42eesehw0ktddcp0fws7d3ycsqez3kaamq3", + CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/rust/tw_coin_entry/src/error.rs b/rust/tw_coin_entry/src/error.rs index bea816c30d0..e8ceb589066 100644 --- a/rust/tw_coin_entry/src/error.rs +++ b/rust/tw_coin_entry/src/error.rs @@ -28,13 +28,18 @@ pub type AddressResult = Result; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum AddressError { UnknownCoinType, + Unsupported, MissingPrefix, FromHexError, + FromBase58Error, + FromBech32Error, PublicKeyTypeMismatch, UnexpectedAddressPrefix, UnexpectedHasher, InvalidHrp, + InvalidRegistry, InvalidInput, + InvalidChecksum, } pub type SigningResult = Result; diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index 35dd638a29d..2359e4d6329 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -25,6 +25,7 @@ tw_misc = { path = "../tw_misc" } tw_native_evmos = { path = "../chains/tw_native_evmos" } tw_native_injective = { path = "../chains/tw_native_injective" } tw_ronin = { path = "../tw_ronin" } +tw_solana = { path = "../chains/tw_solana" } tw_thorchain = { path = "../chains/tw_thorchain" } [build-dependencies] diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index 87261ec0d95..19b7198ed90 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -19,6 +19,7 @@ pub enum BlockchainType { NativeEvmos, NativeInjective, Ronin, + Solana, Thorchain, // end_of_blockchain_type - USED TO GENERATE CODE #[serde(other)] diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index e6cfebcf7bb..c978e17f5c0 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -19,6 +19,7 @@ use tw_internet_computer::entry::InternetComputerEntry; use tw_native_evmos::entry::NativeEvmosEntry; use tw_native_injective::entry::NativeInjectiveEntry; use tw_ronin::entry::RoninEntry; +use tw_solana::entry::SolanaEntry; use tw_thorchain::entry::ThorchainEntry; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; @@ -35,6 +36,7 @@ const INTERNET_COMPUTER: InternetComputerEntry = InternetComputerEntry; const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; const RONIN: RoninEntry = RoninEntry; +const SOLANA: SolanaEntry = SolanaEntry; const THORCHAIN: ThorchainEntry = ThorchainEntry; // end_of_blockchain_entries - USED TO GENERATE CODE @@ -51,6 +53,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&NATIVE_EVMOS), BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE), BlockchainType::Ronin => Ok(&RONIN), + BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Thorchain => Ok(&THORCHAIN), // end_of_blockchain_dispatcher - USED TO GENERATE CODE BlockchainType::Unsupported => Err(RegistryError::Unsupported), diff --git a/rust/tw_hash/src/hash_array.rs b/rust/tw_hash/src/hash_array.rs index 0c4d893b064..f76893b5349 100644 --- a/rust/tw_hash/src/hash_array.rs +++ b/rust/tw_hash/src/hash_array.rs @@ -198,6 +198,61 @@ mod impl_serde { } } +#[cfg(feature = "serde")] +pub mod as_byte_sequence { + use super::Hash; + use serde::de::{Error, SeqAccess, Visitor}; + use serde::ser::SerializeTuple; + use serde::{Deserializer, Serializer}; + use std::fmt; + use std::marker::PhantomData; + + struct ByteArrayVisitor { + _n: PhantomData<[u8; N]>, + } + + impl<'de, const N: usize> Visitor<'de> for ByteArrayVisitor { + type Value = Hash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Hash") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: SeqAccess<'de>, + { + let mut result = Hash::::default(); + for i in 0..N { + result[i] = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(i, &self))?; + } + Ok(result) + } + } + + pub fn deserialize<'de, const N: usize, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let visitor = ByteArrayVisitor:: { _n: PhantomData }; + deserializer.deserialize_tuple(N, visitor) + } + + pub fn serialize(hash: &Hash, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(N)?; + for el in hash.0 { + tup.serialize_element(&el)?; + } + + tup.end() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/tw_hash/src/lib.rs b/rust/tw_hash/src/lib.rs index 3c62256bbf7..48838e77569 100644 --- a/rust/tw_hash/src/lib.rs +++ b/rust/tw_hash/src/lib.rs @@ -17,7 +17,7 @@ pub mod sha3; mod hash_array; mod hash_wrapper; -pub use hash_array::{concat, Hash, H160, H256, H264, H32, H512, H520}; +pub use hash_array::{as_byte_sequence, concat, Hash, H160, H256, H264, H32, H512, H520}; use tw_encoding::hex::FromHexError; diff --git a/rust/tw_memory/src/test_utils/tw_data_vector_helper.rs b/rust/tw_memory/src/test_utils/tw_data_vector_helper.rs index 6c0c185ed81..63f6287c137 100644 --- a/rust/tw_memory/src/test_utils/tw_data_vector_helper.rs +++ b/rust/tw_memory/src/test_utils/tw_data_vector_helper.rs @@ -32,6 +32,12 @@ impl TWDataVectorHelper { } } +impl Default for TWDataVectorHelper { + fn default() -> Self { + TWDataVectorHelper::create([]) + } +} + impl Drop for TWDataVectorHelper { fn drop(&mut self) { if self.ptr.is_null() { diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index a72042ac6cb..9f9fe5c9ea4 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -8,10 +8,11 @@ name = "wallet_core_rs" crate-type = ["staticlib", "rlib"] # Creates static lib [features] -default = ["bitcoin-legacy", "ethereum-abi", "ethereum-rlp"] +default = ["bitcoin-legacy", "ethereum-abi", "ethereum-rlp", "solana-transaction"] bitcoin-legacy = [] ethereum-abi = [] ethereum-rlp = [] +solana-transaction = [] [dependencies] tw_any_coin = { path = "../tw_any_coin" } @@ -26,6 +27,7 @@ tw_keypair = { path = "../tw_keypair" } tw_memory = { path = "../tw_memory" } tw_misc = { path = "../tw_misc" } tw_proto = { path = "../tw_proto" } +tw_solana = { path = "../chains/tw_solana" } [dev-dependencies] serde_json = "1.0" diff --git a/rust/wallet_core_rs/src/ffi/mod.rs b/rust/wallet_core_rs/src/ffi/mod.rs index 7df4e1ad4eb..4b64452c2e7 100644 --- a/rust/wallet_core_rs/src/ffi/mod.rs +++ b/rust/wallet_core_rs/src/ffi/mod.rs @@ -4,3 +4,4 @@ pub mod bitcoin; pub mod ethereum; +pub mod solana; diff --git a/rust/wallet_core_rs/src/ffi/solana/mod.rs b/rust/wallet_core_rs/src/ffi/solana/mod.rs new file mode 100644 index 00000000000..073788657fa --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/solana/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[cfg(feature = "solana-transaction")] +pub mod transaction; diff --git a/rust/wallet_core_rs/src/ffi/solana/transaction.rs b/rust/wallet_core_rs/src/ffi/solana/transaction.rs new file mode 100644 index 00000000000..a4ae7400a05 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/solana/transaction.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use tw_memory::ffi::tw_data::TWData; +use tw_memory::ffi::tw_data_vector::TWDataVector; +use tw_memory::ffi::tw_string::TWString; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; +use tw_solana::modules::utils::SolanaTransaction; + +/// Decode Solana transaction, update the recent blockhash and re-sign the transaction. +/// +/// # Warning +/// +/// This is a temporary solution. It will be removed when `Solana.proto` supports +/// direct transaction signing. +/// +/// \param encoded_tx base64 encoded Solana transaction. +/// \param recent_blockhash base58 encoded recent blockhash. +/// \param private_keys list of private keys that should be used to re-sign the transaction. +/// \return serialized `Solana::Proto::SigningOutput`. +#[no_mangle] +pub unsafe extern "C" fn tw_solana_transaction_update_blockhash_and_sign( + encoded_tx: *const TWString, + recent_blockhash: *const TWString, + private_keys: *const TWDataVector, +) -> *mut TWData { + let encoded_tx = try_or_else!(TWString::from_ptr_as_ref(encoded_tx), std::ptr::null_mut); + let encoded_tx = try_or_else!(encoded_tx.as_str(), std::ptr::null_mut); + + let recent_blockhash = try_or_else!( + TWString::from_ptr_as_ref(recent_blockhash), + std::ptr::null_mut + ); + let recent_blockhash = try_or_else!(recent_blockhash.as_str(), std::ptr::null_mut); + + let private_keys = try_or_else!( + TWDataVector::from_ptr_as_ref(private_keys), + std::ptr::null_mut + ) + .to_data_vec(); + + let output = + SolanaTransaction::update_blockhash_and_sign(encoded_tx, recent_blockhash, &private_keys); + let output_proto = try_or_else!(tw_proto::serialize(&output), std::ptr::null_mut); + + TWData::from(output_proto).into_ptr() +} diff --git a/rust/wallet_core_rs/tests/solana_transaction.rs b/rust/wallet_core_rs/tests/solana_transaction.rs new file mode 100644 index 00000000000..66b10315eb3 --- /dev/null +++ b/rust/wallet_core_rs/tests/solana_transaction.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::base58; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper; +use tw_memory::test_utils::tw_string_helper::TWStringHelper; +use tw_proto::Common::Proto::SigningError; +use tw_proto::Solana::Proto; +use tw_solana::SOLANA_ALPHABET; +use wallet_core_rs::ffi::solana::transaction::tw_solana_transaction_update_blockhash_and_sign; + +#[test] +fn test_solana_transaction_update_blockhash_and_sign_token_transfer_with_external_fee_payer() { + // base64 encoded + // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet + let encoded_tx = "AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG"; + let encoded_tx = TWStringHelper::create(encoded_tx); + + // base58 encoded + let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; + let new_blockhash = TWStringHelper::create(new_blockhash); + + let my_private_key = base58::decode( + "9YtuoD4sH4h88CVM8DSnkfoAaLY7YeGC2TarDJ8eyMS5", + &SOLANA_ALPHABET, + ) + .unwrap(); + let fee_payer_private_key = base58::decode( + "66ApBuKpo2uSzpjGBraHq7HP8UZMUJzp3um8FdEjkC9c", + &SOLANA_ALPHABET, + ) + .unwrap(); + let private_keys = TWDataVectorHelper::create([fee_payer_private_key, my_private_key]); + + let output_data = unsafe { + TWDataHelper::wrap(tw_solana_transaction_update_blockhash_and_sign( + encoded_tx.ptr(), + new_blockhash.ptr(), + private_keys.ptr(), + )) + .to_vec() + .expect("Expected a non-null output data") + }; + let output: Proto::SigningOutput = tw_proto::deserialize(&output_data).unwrap(); + assert_eq!(output.error, SigningError::OK); + + let expected = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"; + assert_eq!(output.encoded, expected); +} + +#[test] +fn test_solana_transaction_update_blockhash_and_sign_no_matching_pubkey() { + // base64 encoded + let encoded_tx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHEIoR5xuWyrvjIW4xU7CWlPOfyFAiy8B295hGo6tNjBmRCgUkQaFYTleMcAX2p74eBXQZd1dwDyQZAPJfSv2KGc5kcFLJj5qd2BVMaSNGVPfVBm74GbLwUq5/U1Ccdqc2gokZQxRDpMq7aeToP3nRaWIP4RXMxN+LJetccXMPq/QumgOqt7kkqk07cyPCKgYoQ4fQtOqqZn5sEqjWHYj3CDS5ha48uggePWu090s1ff4yoCjAvULeZ+cqYFn+Adk5Teyfw71W3u/F6VTnLQEPW96gJr5Kcm3bGi08n224JyF++PTko52VL0CIM2xtl0WkvNslD6Wawxr7yd9HYllN4Lz8lFwXilWGgyJdOq1qqBuZbE49glHeCO/sJHNnIHC0BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fm0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6OL4d+g9rsaIj0Orta57MRu3jDSWCJf85ae4LBbiD/GXvOojZjsHekJrpRUuPggLJr943hDVD5UareeEucjCvaoHCgAFAsBcFQAKAAkDBBcBAAAAAAANBgAGACMJDAEBCQIABgwCAAAAAMqaOwAAAAAMAQYBEQs1DA8ABgEFAiMhCwsOCx0MDxoBGQcYBAgDJBscDB4PBwUQEhEfFR8UFwcFISITHw8MDCAfFgstwSCbM0HWnIEAAwAAABEBZAABCh0BAyZHAQMAypo7AAAAAJaWFAYAAAAAMgAADAMGAAABCQPZoILFk7gfE2y5bt3AC+g/4OwNzdiHKBhIbdeYvYFEjQPKyMkExMUkx0R25UNa/g5KsG0vfUwdUJ8e8HecK/Jkd3qm9XefBOB0BaD1+J+dBJz09vfyGuRYZH09HfdE/kL8v6Ql+H03+tO+9lMmmVg8O1c6gAN6eX0Cbn4="; + let encoded_tx = TWStringHelper::create(encoded_tx); + + // base58 encoded + let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; + let new_blockhash = TWStringHelper::create(new_blockhash); + + // there is no matching pubkey in the transaction account keys. + let private_key = base58::decode( + "A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr", + &SOLANA_ALPHABET, + ) + .unwrap(); + let private_keys = TWDataVectorHelper::create([private_key]); + + let output_data = unsafe { + TWDataHelper::wrap(tw_solana_transaction_update_blockhash_and_sign( + encoded_tx.ptr(), + new_blockhash.ptr(), + private_keys.ptr(), + )) + .to_vec() + .expect("Expected a non-null output data") + }; + let output: Proto::SigningOutput = tw_proto::deserialize(&output_data).unwrap(); + assert_eq!(output.error, SigningError::Error_missing_private_key); +} + +#[test] +fn test_solana_transaction_update_blockhash_and_sign_empty_private_keys() { + // base64 encoded + // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet + let encoded_tx = "AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG"; + let encoded_tx = TWStringHelper::create(encoded_tx); + + // base58 encoded + let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; + let new_blockhash = TWStringHelper::create(new_blockhash); + + // empty private keys + let private_keys = TWDataVectorHelper::default(); + + let output_data = unsafe { + TWDataHelper::wrap(tw_solana_transaction_update_blockhash_and_sign( + encoded_tx.ptr(), + new_blockhash.ptr(), + private_keys.ptr(), + )) + .to_vec() + .expect("Expected a non-null output data") + }; + let output: Proto::SigningOutput = tw_proto::deserialize(&output_data).unwrap(); + assert_eq!(output.error, SigningError::OK); + + let expected = "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"; + assert_eq!(output.encoded, expected); + + let expected_message = "AgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"; + assert_eq!(output.message_encoded, expected_message); +} diff --git a/src/DataVector.h b/src/DataVector.h new file mode 100644 index 00000000000..3e3a071807a --- /dev/null +++ b/src/DataVector.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TrustWalletCore/TWDataVector.h" +#include "Data.h" + +namespace TW { + +static std::vector createFromTWDataVector(const struct TWDataVector* _Nonnull dataVector) { + std::vector ret; + const auto n = TWDataVectorSize(dataVector); + for (auto i = 0uL; i < n; ++i) { + const auto* const elem = TWDataVectorGet(dataVector, i); + if (const auto* const data = reinterpret_cast(elem); data) { + ret.emplace_back(*data); + TWDataDelete(elem); + } + } + return ret; +} + +} // namespace TW diff --git a/src/interface/TWSolanaTransaction.cpp b/src/interface/TWSolanaTransaction.cpp new file mode 100644 index 00000000000..a585c9f7508 --- /dev/null +++ b/src/interface/TWSolanaTransaction.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWSolanaTransaction.h" +#include "DataVector.h" +#include "rust/Wrapper.h" + +using namespace TW; + +TWData *_Nonnull TWSolanaTransactionUpdateBlockhashAndSign(TWString *_Nonnull encodedTx, + TWString *_Nonnull recentBlockhash, + const struct TWDataVector *_Nonnull privateKeys) { + + auto& encodedTxRef = *reinterpret_cast(encodedTx); + auto& recentBlockhashRef = *reinterpret_cast(recentBlockhash); + + Rust::TWStringWrapper encodedTxStr = encodedTxRef; + Rust::TWStringWrapper recentBlockhashStr = recentBlockhashRef; + Rust::TWDataVectorWrapper privateKeysVec = createFromTWDataVector(privateKeys); + + Rust::TWDataWrapper output = Rust::tw_solana_transaction_update_blockhash_and_sign(encodedTxStr.get(), + recentBlockhashStr.get(), + privateKeysVec.get()); + + auto outputData = output.toDataOrDefault(); + return TWDataCreateWithBytes(outputData.data(), outputData.size()); +} diff --git a/src/interface/TWTransactionCompiler.cpp b/src/interface/TWTransactionCompiler.cpp index 2a55cf129dc..5db643063ee 100644 --- a/src/interface/TWTransactionCompiler.cpp +++ b/src/interface/TWTransactionCompiler.cpp @@ -5,26 +5,12 @@ #include #include "TransactionCompiler.h" -#include "Data.h" -#include "uint256.h" +#include "DataVector.h" #include using namespace TW; -static std::vector createFromTWDataVector(const struct TWDataVector* _Nonnull dataVector) { - std::vector ret; - const auto n = TWDataVectorSize(dataVector); - for (auto i = 0uL; i < n; ++i) { - const auto* const elem = TWDataVectorGet(dataVector, i); - if (const auto* const data = reinterpret_cast(elem); data) { - ret.emplace_back(*data); - TWDataDelete(elem); - } - } - return ret; -} - TWData *_Nonnull TWTransactionCompilerPreImageHashes(enum TWCoinType coinType, TWData *_Nonnull txInputData) { Data result; try { diff --git a/src/proto/Solana.proto b/src/proto/Solana.proto index 7ee3e0cf244..45d4e858f7f 100644 --- a/src/proto/Solana.proto +++ b/src/proto/Solana.proto @@ -198,6 +198,10 @@ message SigningOutput { // The unsigned transaction string unsigned_tx = 4; + + // The encoded message. Can be used to estimate a transaction fee required to execute the message. + // Please note that this is set only on `SolanaTransaction.updateBlockhashAndSign`, but no on `AnySigner.sign`. + string message_encoded = 5; } /// Transaction pre-signing output diff --git a/tests/chains/Solana/TWSolanaTransaction.cpp b/tests/chains/Solana/TWSolanaTransaction.cpp new file mode 100644 index 00000000000..54f579931e5 --- /dev/null +++ b/tests/chains/Solana/TWSolanaTransaction.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "PrivateKey.h" +#include "TrustWalletCore/TWSolanaTransaction.h" +#include "proto/Solana.pb.h" +#include "TestUtilities.h" + +#include + +using namespace TW; +namespace TW::Solana::tests { + +TEST(TWSolanaTransaction, UpdateBlockhashAndSign) { + // base64 encoded + // https://explorer.solana.com/tx/3KbvREZUat76wgWMtnJfWbJL74Vzh4U2eabVJa3Z3bb2fPtW8AREP5pbmRwUrxZCESbTomWpL41PeKDcPGbojsej?cluster=devnet + auto encodedTx = STRING("AnQTYwZpkm3fs4SdLxnV6gQj3hSLsyacpxDdLMALYWObm722f79IfYFTbZeFK9xHtMumiDOWAM2hHQP4r/GtbARpncaXgOVFv7OgbRLMbuCEJHO1qwcdCbtH72VzyzU8yw9sqqHIAaCUE8xaQTgT6Z5IyZfeyMe2QGJIfOjz65UPAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqe6sdLXiXSDILEtzckCjkjchiSf6zVGpMYiAE5BE2IqHAQUEAgQDAQoMoA8AAAAAAAAG"); + // base58 encoded + auto newBlockhash = STRING("CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"); + + auto myPrivateKey = DATA("7f0932159226ddec9e1a4b0b8fe7cdc135049f9e549a867d722aa720dd64f32e"); + auto feePayerPrivateKey = DATA("4b9d6f57d28b06cbfa1d4cc710953e62d653caf853415c56ffd9d150acdeb7f7"); + auto privateKeysVec = WRAP(TWDataVector, TWDataVectorCreateWithData(myPrivateKey.get())); + TWDataVectorAdd(privateKeysVec.get(), feePayerPrivateKey.get()); + + auto outputData = WRAPD(TWSolanaTransactionUpdateBlockhashAndSign(encodedTx.get(), newBlockhash.get(), privateKeysVec.get())); + + Proto::SigningOutput output; + output.ParseFromArray(TWDataBytes(outputData.get()), static_cast(TWDataSize(outputData.get()))); + + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + EXPECT_EQ(output.encoded(), "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"); +} + +} // TW::Solana::tests From 2c423d27900077394e7165db50555dc56d0ba4be Mon Sep 17 00:00:00 2001 From: Sztergbaum Roman Date: Thu, 8 Feb 2024 08:21:06 +0100 Subject: [PATCH 2/4] feat(ios): fix ios ci (#3681) * feat(ios): fix ios ci * feat(kotlin): fix kotlin ci * feat(kotlin): fix kotlin ci --- samples/kmp/gradle.properties | 5 ++++- samples/kmp/shared/build.gradle.kts | 2 +- tools/ios-test | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/kmp/gradle.properties b/samples/kmp/gradle.properties index dc2c082de2b..8f3835a8705 100644 --- a/samples/kmp/gradle.properties +++ b/samples/kmp/gradle.properties @@ -10,4 +10,7 @@ android.nonTransitiveRClass=true #MPP kotlin.mpp.enableCInteropCommonization=true -kotlin.mpp.androidSourceSetLayoutVersion=2 \ No newline at end of file +kotlin.mpp.androidSourceSetLayoutVersion=2 + +#Native +kotlin.native.cacheKind.iosArm64=none diff --git a/samples/kmp/shared/build.gradle.kts b/samples/kmp/shared/build.gradle.kts index 8de33b05a29..ca1ad047e3d 100644 --- a/samples/kmp/shared/build.gradle.kts +++ b/samples/kmp/shared/build.gradle.kts @@ -35,7 +35,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("com.trustwallet:wallet-core-kotlin:4.0.16") + implementation("com.trustwallet:wallet-core-kotlin:4.0.22") } } val commonTest by getting { diff --git a/tools/ios-test b/tools/ios-test index 38014c0723d..639c605a8d6 100755 --- a/tools/ios-test +++ b/tools/ios-test @@ -11,7 +11,7 @@ xcodegen && pod install xcodebuild -workspace TrustWalletCore.xcworkspace \ -scheme WalletCore \ -sdk iphonesimulator \ - -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ + -destination "platform=iOS Simulator,name=iPhone 15,OS=17.2" \ test | xcbeautify popd From c08a40707ab12812fd6ed3e30698e5c81763bf7c Mon Sep 17 00:00:00 2001 From: David Kim <63450340+PowerStream3604@users.noreply.github.com> Date: Thu, 8 Feb 2024 18:29:21 +0900 Subject: [PATCH 3/4] [Barz] Add prefixedMsgHash and diamondCut Encoder (#3680) * Add declaration to Barz header * Add Bytes32 type to ETH ABI Proto * Add DiamondCut input to Barz Proto * Add functions for Barz * Add Barz interface * Add Barz test * Update comments * Update header comments * Remove comments * Update hardcode text to constants * Update initData encoding --------- Co-authored-by: Sztergbaum Roman --- include/TrustWalletCore/TWBarz.h | 16 ++++ src/Ethereum/ABI/ProtoParam.h | 20 +++++ src/Ethereum/Barz.cpp | 133 ++++++++++++++++++++++++++++ src/Ethereum/Barz.h | 2 + src/interface/TWBarz.cpp | 21 +++++ src/proto/Barz.proto | 22 +++++ tests/chains/Ethereum/BarzTests.cpp | 102 +++++++++++++++++++++ 7 files changed, 316 insertions(+) diff --git a/include/TrustWalletCore/TWBarz.h b/include/TrustWalletCore/TWBarz.h index bc2d0615ba0..1454e219b5d 100644 --- a/include/TrustWalletCore/TWBarz.h +++ b/include/TrustWalletCore/TWBarz.h @@ -39,4 +39,20 @@ TWData *_Nonnull TWBarzGetInitCode(TWString* _Nonnull factory, struct TWPublicKe /// \return Bytes of the formatted signature TW_EXPORT_STATIC_METHOD TWData *_Nonnull TWBarzGetFormattedSignature(TWData* _Nonnull signature, TWData* _Nonnull challenge, TWData* _Nonnull authenticatorData, TWString* _Nonnull clientDataJSON); + +/// Returns the final hash to be signed by Barz for signing messages & typed data +/// +/// \param msgHash Original msgHash +/// \param barzAddress The address of Barz wallet signing the message +/// \param chainId The chainId of the network the verification will happen +/// \return The final hash to be signed +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWBarzGetPrefixedMsgHash(TWData* _Nonnull msgHash, TWString* _Nonnull barzAddress, uint32_t chainId); + +/// Returns the encoded diamondCut function call for Barz contract upgrades +/// +/// \param input The serialized data of DiamondCutInput +/// \return The encoded bytes of diamondCut function call +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWBarzGetDiamondCutCode(TWData *_Nonnull input); TW_EXTERN_C_END diff --git a/src/Ethereum/ABI/ProtoParam.h b/src/Ethereum/ABI/ProtoParam.h index c9d6c2f1c80..8391ee94a26 100644 --- a/src/Ethereum/ABI/ProtoParam.h +++ b/src/Ethereum/ABI/ProtoParam.h @@ -74,6 +74,26 @@ class ProtoByteArray final: public BaseProtoParam { Data m_data; }; +class ProtoBytes32 final: public BaseProtoParam { +public: + explicit ProtoBytes32(const Data& data): m_data(data) { + if (data.size() != 32) { + throw std::invalid_argument("Data must be exactly 32 bytes long"); + } + } + + ~ProtoBytes32() override = default; + + AbiProto::Token toToken() const override { + AbiProto::Token proto; + proto.set_byte_array_fix(m_data.data(), m_data.size()); + return proto; + } + +private: + Data m_data; +}; + class ProtoString final: public BaseProtoParam { public: explicit ProtoString(std::string str): m_string(std::move(str)) { diff --git a/src/Ethereum/Barz.cpp b/src/Ethereum/Barz.cpp index 2cd9a0da071..6002af3692b 100644 --- a/src/Ethereum/Barz.cpp +++ b/src/Ethereum/Barz.cpp @@ -2,6 +2,8 @@ // // Copyright © 2017 Trust Wallet. +#include +#include "ABI/ValueEncoder.h" #include "ABI/Function.h" #include "AddressChecksum.h" #include "EIP1014.h" @@ -85,4 +87,135 @@ Data getFormattedSignature(const Data& signature, const Data challenge, const Da return {}; } +Data getPrefixedMsgHash(const Data msgHash, const std::string& barzAddress, const uint32_t chainId) { + // keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); + const Data& domainSeparatorTypeHashData = parse_hex("0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218"); + // keccak256("BarzMessage(bytes message)") + const Data& barzMsgHashData = parse_hex("0xb1bcb804a4a3a1af3ee7920d949bdfd417ea1b736c3552c8d6563a229a619100"); + const auto signedDataPrefix = "0x1901"; + + auto encodedDomainSeparatorData = Ethereum::ABI::Function::encodeParams(Ethereum::ABI::BaseParams { + std::make_shared(domainSeparatorTypeHashData), + std::make_shared(chainId), + std::make_shared(barzAddress) + }); + if (!encodedDomainSeparatorData.has_value()) { + return {}; + } + + Data domainSeparator = encodedDomainSeparatorData.value(); + const Data domainSeparatorHash = Hash::keccak256(domainSeparator); + + auto encodedRawMessageData = Ethereum::ABI::Function::encodeParams(Ethereum::ABI::BaseParams { + std::make_shared(barzMsgHashData), + std::make_shared(Hash::keccak256(msgHash)), + }); + + Data rawMessageData = encodedRawMessageData.value(); + + auto encodedMsg = Ethereum::ABI::Function::encodeParams(Ethereum::ABI::BaseParams { + std::make_shared(domainSeparatorHash), + std::make_shared(Hash::keccak256(rawMessageData)) + }); + auto encodedMsgData = signedDataPrefix + hex(encodedMsg.value()); + + Data finalEncodedMsgData = parse_hex(encodedMsgData); + + const Data encodedMsgHash = Hash::keccak256(finalEncodedMsgData); + + Data envelope; + append(envelope, encodedMsgHash); + + return envelope; +} + +// Function to encode the diamondCut function call using protobuf message as input +Data getDiamondCutCode(const Proto::DiamondCutInput& input) { + const auto diamondCutSelector = "1f931c1c"; + const auto dataLocationChunk = "60"; + const char defaultPadding = '0'; + Data encoded; + + // function diamondCut( + // FacetCut[] calldata diamondCut, + // address init, + // bytes calldata _calldata // Note that Barz does not use the _calldata for initialization. + // ) + Data encodedSignature = parse_hex(diamondCutSelector); // diamondCut() function selector + encoded.insert(encoded.end(), encodedSignature.begin(), encodedSignature.end()); + + // First argument Data Location `diamondCut` + Data dataLocation = parse_hex(dataLocationChunk); + pad_left(dataLocation, 32); + append(encoded, dataLocation); + + // Encode second Parameter `init` + Data initAddress = parse_hex(input.init_address()); + pad_left(initAddress, 32); + append(encoded, initAddress); + + // Third Argument Data location `_calldata` + auto callDataDataLocation = int(hex(encoded).size()) / 2; + + Ethereum::ABI::ValueEncoder::encodeUInt256(input.facet_cuts_size(), encoded); + + // Prepend the function selector for the diamondCut function + int instructChunk = 0; + int totalInstructChunk = 0; + int prevDataPosition = 0; + const auto encodingChunk = 32; + const auto bytesChunkLine = 5; + int chunkLocation; + Data dataPosition; + // Encode each FacetCut from the input + for (const auto& facetCut : input.facet_cuts()) { + if (instructChunk == 0) { + prevDataPosition = input.facet_cuts_size() * encodingChunk; + Ethereum::ABI::ValueEncoder::encodeUInt256(prevDataPosition, encoded); + chunkLocation = int(hex(encoded).size()) / 2; + } else { + prevDataPosition = prevDataPosition + (instructChunk * encodingChunk); + Ethereum::ABI::ValueEncoder::encodeUInt256(prevDataPosition, dataPosition); + instructChunk = 0; + + encoded.insert(encoded.begin() + chunkLocation, dataPosition.begin(), dataPosition.end()); + ++instructChunk; + } + Ethereum::ABI::ValueEncoder::encodeAddress(parse_hex(facetCut.facet_address()), encoded); // facet address + ++instructChunk; + Ethereum::ABI::ValueEncoder::encodeUInt256(facetCut.action(), encoded); // FacetAction enum + ++instructChunk; + append(encoded, dataLocation); // adding 0x60 DataStorage position + ++instructChunk; + Ethereum::ABI::ValueEncoder::encodeUInt256(facetCut.function_selectors_size(), encoded); // Number of FacetSelector + ++instructChunk; + // Encode and append function selectors + for (const auto& selector : facetCut.function_selectors()) { + Ethereum::ABI::ValueEncoder::encodeBytes(parse_hex(hex(selector)), encoded); + ++instructChunk; + } + totalInstructChunk += instructChunk; + } + + Data calldataLength; + Ethereum::ABI::ValueEncoder::encodeUInt256((totalInstructChunk * encodingChunk) + (bytesChunkLine * encodingChunk), calldataLength); + + encoded.insert(encoded.begin() + callDataDataLocation, calldataLength.begin(), calldataLength.end()); + + auto initDataSize = int(hex(parse_hex(input.init_data())).size()); + if (initDataSize == 0 || initDataSize % 2 != 0) + return {}; + + auto initDataLength = initDataSize / 2; // 1 byte is encoded into 2 char + Ethereum::ABI::ValueEncoder::encodeUInt256(initDataLength, encoded); + + append(encoded, parse_hex(input.init_data())); + + const int paddingLength = (encodingChunk * 2) - (initDataSize % (encodingChunk * 2)); + const std::string padding(paddingLength, defaultPadding); + append(encoded, parse_hex(padding)); + + return encoded; +} + } // namespace TW::Barz diff --git a/src/Ethereum/Barz.h b/src/Ethereum/Barz.h index 9ea1d0c0ccd..f29afc4eeed 100644 --- a/src/Ethereum/Barz.h +++ b/src/Ethereum/Barz.h @@ -14,5 +14,7 @@ namespace TW::Barz { std::string getCounterfactualAddress(const Proto::ContractAddressInput input); Data getInitCode(const std::string& factoryAddress, const PublicKey& publicKey, const std::string& verificationFacet, const uint32_t salt); Data getFormattedSignature(const Data& signature, const Data challenge, const Data& authenticatorData, const std::string& clientDataJSON); +Data getPrefixedMsgHash(const Data msgHash, const std::string& barzAddress, const uint32_t chainId); +Data getDiamondCutCode(const Proto::DiamondCutInput& input); // action should be one of 0, 1, 2. 0 = Add, 1 = Remove, 2 = Replace } diff --git a/src/interface/TWBarz.cpp b/src/interface/TWBarz.cpp index cbfdea41700..60da9690279 100644 --- a/src/interface/TWBarz.cpp +++ b/src/interface/TWBarz.cpp @@ -36,4 +36,25 @@ TWData *_Nonnull TWBarzGetFormattedSignature(TWData* _Nonnull signature, TWData* const auto initCode = TW::Barz::getFormattedSignature(signatureData, challengeData, authenticatorDataConverted, clientDataJSONStr); return TWDataCreateWithData(&initCode); +} + +TWData *_Nonnull TWBarzGetPrefixedMsgHash(TWData* _Nonnull msgHash, TWString* _Nonnull barzAddress, uint32_t chainId) { + const auto& msgHashData = *reinterpret_cast(msgHash); + const auto& barzAddressData = *reinterpret_cast(barzAddress); + + const auto prefixedMsgHash = TW::Barz::getPrefixedMsgHash(msgHashData, barzAddressData, chainId); + return TWDataCreateWithData(&prefixedMsgHash); +} + +TWData *_Nonnull TWBarzGetDiamondCutCode(TWData *_Nonnull input) { + TW::Barz::Proto::DiamondCutInput inputProto; + + const auto bytes = TWDataBytes(input); + const auto size = static_cast(TWDataSize(input)); + if (!inputProto.ParseFromArray(bytes, size)) { + return ""; + } + + const auto diamondCutCode = TW::Barz::getDiamondCutCode(inputProto); + return TWDataCreateWithData(&diamondCutCode); } \ No newline at end of file diff --git a/src/proto/Barz.proto b/src/proto/Barz.proto index b0be650c0f3..6c8342054e7 100644 --- a/src/proto/Barz.proto +++ b/src/proto/Barz.proto @@ -28,3 +28,25 @@ message ContractAddressInput { // Salt is used to derive multiple account from the same public key uint32 salt = 9; } + +// FacetCutAction represents the action to be performed for a FacetCut +enum FacetCutAction { + ADD = 0; + REPLACE = 1; + REMOVE = 2; +} + +// FacetCut represents a single operation to be performed on a facet +message FacetCut { + string facet_address = 1; // The address of the facet + FacetCutAction action = 2; // The action to perform + repeated bytes function_selectors = 3; // List of function selectors, each is bytes4 +} + +// DiamondCutInput represents the input parameters for a diamondCut operation +message DiamondCutInput { + repeated FacetCut facet_cuts = 1; // List of facet cuts to apply + string init_address = 2; // Address to call with `init` data after applying cuts + bytes init_data = 3; // Data to pass to `init` function call +} + diff --git a/tests/chains/Ethereum/BarzTests.cpp b/tests/chains/Ethereum/BarzTests.cpp index 6d4bc6dfc45..479726d7393 100644 --- a/tests/chains/Ethereum/BarzTests.cpp +++ b/tests/chains/Ethereum/BarzTests.cpp @@ -313,4 +313,106 @@ TEST(Barz, SignR1BatchedTransferAccountDeployed) { TWEthereumAbiFunctionDelete(transferFunc); } +TEST(Barz, GetPrefixedMsgHash) { + { + const Data& msgHash = parse_hex("0xa6ebe22d8c1ec7edbd7f5776e49a161f67ab97161d7b8c648d80abf365765cf2"); + const std::string& barzAddress = "0x913233BfC283ffe89a5E70ADC39c0926d240bbD9"; + const auto chainId = 3604; + + const auto& prefixedMsgHash = Barz::getPrefixedMsgHash(msgHash, barzAddress, chainId); + ASSERT_EQ(hexEncoded(prefixedMsgHash), "0x0488fb3e4fdaa890bf55532fc9840fb9edef9c38244f431c9430a78a86d89157"); + } +} + +TEST(Barz, GetPrefixedMsgHashWithZeroChainId) { + { + const Data& msgHash = parse_hex("0xcf267a78c5adaf96f341a696eb576824284c572f3e61be619694d539db1925f9"); + const std::string& barzAddress = "0xB91aaa96B138A1B1D94c9df4628187132c5F2bf1"; + const auto chainId = 0; + + const auto& prefixedMsgHash = Barz::getPrefixedMsgHash(msgHash, barzAddress, chainId); + ASSERT_EQ(hexEncoded(prefixedMsgHash), "0xc74e78634261222af51530703048f98a1b7b995a606a624f0a008e7aaba7a21b"); + } +} + +TEST(Barz, GetDiamondCutCode) { + { + TW::Barz::Proto::DiamondCutInput input; + + input.set_init_address("0x0000000000000000000000000000000000000000"); + input.set_init_data("0x00"); + + auto* facetCutAdd = input.add_facet_cuts(); + facetCutAdd->set_facet_address("0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"); + facetCutAdd->set_action(TW::Barz::Proto::FacetCutAction::ADD); + + auto functionSelectorAdd = parse_hex("0xfdd8a83c"); + facetCutAdd->add_function_selectors(functionSelectorAdd.data(), functionSelectorAdd.size()); + + const auto& diamondCutCode = Barz::getDiamondCutCode(input); + ASSERT_EQ(hexEncoded(diamondCutCode), "0x1f931c1c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001fdd8a83c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"); + } } + +TEST(Barz, GetDiamondCutCodeWithMultipleCut) { + { + TW::Barz::Proto::DiamondCutInput input; + + input.set_init_address("0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"); + input.set_init_data("0x12341234"); + + auto* facetCutMigrationFacet = input.add_facet_cuts(); + facetCutMigrationFacet->set_facet_address("0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"); + facetCutMigrationFacet->set_action(TW::Barz::Proto::FacetCutAction::ADD); + + auto migrationSignature = parse_hex("0xfdd8a83c"); + + facetCutMigrationFacet->add_function_selectors(migrationSignature.data(), migrationSignature.size()); + facetCutMigrationFacet->add_function_selectors(migrationSignature.data(), migrationSignature.size()); + facetCutMigrationFacet->add_function_selectors(migrationSignature.data(), migrationSignature.size()); + + auto* facetCutTestFacet = input.add_facet_cuts(); + facetCutTestFacet->set_facet_address("0x6e3c94d74af6227aEeF75b54a679e969189a6aEC"); + facetCutTestFacet->set_action(TW::Barz::Proto::FacetCutAction::ADD); + + auto testSignature = parse_hex("0x12345678"); + facetCutTestFacet->add_function_selectors(testSignature.data(), testSignature.size()); + + + const auto& diamondCutCode = Barz::getDiamondCutCode(input); + ASSERT_EQ(hexEncoded(diamondCutCode), "0x1f931c1c00000000000000000000000000000000000000000000000000000000000000600000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003fdd8a83c00000000000000000000000000000000000000000000000000000000fdd8a83c00000000000000000000000000000000000000000000000000000000fdd8a83c000000000000000000000000000000000000000000000000000000000000000000000000000000006e3c94d74af6227aeef75b54a679e969189a6aec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001123456780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041234123400000000000000000000000000000000000000000000000000000000"); + } +} + +TEST(Barz, GetDiamondCutCodeWithZeroSelector) { + { + TW::Barz::Proto::DiamondCutInput input; + + input.set_init_address("0x0000000000000000000000000000000000000000"); + input.set_init_data("0x00"); + auto* facetCutAdd = input.add_facet_cuts(); + facetCutAdd->set_facet_address("0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"); + facetCutAdd->set_action(TW::Barz::Proto::FacetCutAction::ADD); + + const auto& diamondCutCode = Barz::getDiamondCutCode(input); + ASSERT_EQ(hexEncoded(diamondCutCode), "0x1f931c1c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"); + } +} + +TEST(Barz, GetDiamondCutCodeWithLongInitData) { + { + TW::Barz::Proto::DiamondCutInput input; + + input.set_init_address("0x0000000000000000000000000000000000000000"); + input.set_init_data("0xb61d27f6000000000000000000000000c2ce171d25837cd43e496719f5355a847edc679b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024a526d83b00000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b90600000000000000000000000000000000000000000000000000000000"); + auto* facetCutAdd = input.add_facet_cuts(); + facetCutAdd->set_facet_address("0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"); + facetCutAdd->set_action(TW::Barz::Proto::FacetCutAction::ADD); + + const auto& diamondCutCode = Barz::getDiamondCutCode(input); + ASSERT_EQ(hexEncoded(diamondCutCode), "0x1f931c1c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c4b61d27f6000000000000000000000000c2ce171d25837cd43e496719f5355a847edc679b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024a526d83b00000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b9060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + } +} + +} + From 7523c9805ce094639dfbda6d643e439f5ef1705e Mon Sep 17 00:00:00 2001 From: Sztergbaum Roman Date: Fri, 9 Feb 2024 12:19:54 +0100 Subject: [PATCH 4/4] [WIP]: support timeout_height from proto (#3684) --- .../src/modules/serializer/json_serializer.rs | 22 +++++++ rust/tw_cosmos_sdk/src/modules/tx_builder.rs | 4 +- rust/tw_cosmos_sdk/tests/sign.rs | 64 +++++++++++++++++++ src/proto/Cosmos.proto | 3 + 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/rust/tw_cosmos_sdk/src/modules/serializer/json_serializer.rs b/rust/tw_cosmos_sdk/src/modules/serializer/json_serializer.rs index 0b2c81195ad..1bf3dff3649 100644 --- a/rust/tw_cosmos_sdk/src/modules/serializer/json_serializer.rs +++ b/rust/tw_cosmos_sdk/src/modules/serializer/json_serializer.rs @@ -18,6 +18,9 @@ pub struct SignedTxJson { pub memo: String, pub msg: Vec>, pub signatures: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub timeout_height: Option, } #[derive(Serialize)] @@ -28,6 +31,9 @@ pub struct UnsignedTxJson { pub memo: String, pub msgs: Vec>, pub sequence: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub timeout_height: Option, } #[derive(Serialize)] @@ -69,11 +75,19 @@ where let signature = Self::serialize_signature(&signed.signer.public_key, signed.signature.clone()); + let convert = |value: u64| { + if value == 0 { + None + } else { + Some(value.to_string()) + } + }; Ok(SignedTxJson { fee: Self::build_fee(&signed.fee), memo: signed.tx_body.memo.clone(), msg, signatures: vec![signature], + timeout_height: convert(signed.tx_body.timeout_height), }) } @@ -87,6 +101,13 @@ where .map(|msg| msg.to_json()) .collect::>()?; + let convert = |value: u64| { + if value == 0 { + None + } else { + Some(value.to_string()) + } + }; Ok(UnsignedTxJson { account_number: unsigned.account_number.to_string(), chain_id: unsigned.chain_id.clone(), @@ -94,6 +115,7 @@ where memo: unsigned.tx_body.memo.clone(), msgs, sequence: unsigned.signer.sequence.to_string(), + timeout_height: convert(unsigned.tx_body.timeout_height), }) } diff --git a/rust/tw_cosmos_sdk/src/modules/tx_builder.rs b/rust/tw_cosmos_sdk/src/modules/tx_builder.rs index c39d13f8c70..8bd97cafbb1 100644 --- a/rust/tw_cosmos_sdk/src/modules/tx_builder.rs +++ b/rust/tw_cosmos_sdk/src/modules/tx_builder.rs @@ -20,8 +20,6 @@ use tw_number::U256; use tw_proto::Cosmos::Proto; use tw_proto::{google, serialize}; -const DEFAULT_TIMEOUT_HEIGHT: u64 = 0; - pub struct TxBuilder { _phantom: PhantomData, } @@ -126,7 +124,7 @@ where Ok(TxBody { messages, memo: input.memo.to_string(), - timeout_height: DEFAULT_TIMEOUT_HEIGHT, + timeout_height: input.timeout_height, }) } diff --git a/rust/tw_cosmos_sdk/tests/sign.rs b/rust/tw_cosmos_sdk/tests/sign.rs index 37436b4947f..8b703393ff3 100644 --- a/rust/tw_cosmos_sdk/tests/sign.rs +++ b/rust/tw_cosmos_sdk/tests/sign.rs @@ -135,6 +135,70 @@ fn test_sign_raw_json() { }); } +#[test] +fn test_sign_raw_json_with_timeout() { + let coin = TestCoinContext::default() + .with_public_key_type(PublicKeyType::Secp256k1) + .with_hrp("cosmos"); + + let raw_json_msg = Proto::mod_Message::RawJSON { + type_pb: "osmosis/poolmanager/split-amount-in".into(), + value: r#"{ + "routes": [ + { + "pools": [ + { + "pool_id": "463", + "token_out_denom": "ibc/1DC495FCEFDA068A3820F903EDBD78B942FBD204D7E93D3BA2B432E9669D1A59" + }, + { + "pool_id": "916", + "token_out_denom": "ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580" + } + ], + "token_in_amount": "70000" + }, + { + "pools": [ + { + "pool_id": "907", + "token_out_denom": "ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580" + } + ], + "token_in_amount": "30000" + } + ], + "sender": "osmo1qr7dhmvcqm4fnleaqel3gel4u20nk5rp9rwsae", + "token_in_denom": "uosmo", + "token_out_min_amount": "885297" + }"#.into(), + }; + let input = Proto::SigningInput { + account_number: 24139, + chain_id: "osmosis-1".into(), + sequence: 191, + fee: Some(make_fee(617438, make_amount("uosmo", "1853"))), + private_key: account_1037_private_key(), + messages: vec![make_message(MessageEnum::raw_json_message(raw_json_msg))], + timeout_height: 13692007, + ..Proto::SigningInput::default() + }; + + // `RawJSON` doesn't support Protobuf serialization and signing. + test_sign_protobuf_error::(TestErrorInput { + coin: &coin, + input: input.clone(), + error: SigningError::Error_not_supported, + }); + test_sign_json::(TestInput { + coin: &coin, + input, + tx: r#"{"mode":"block","tx":{"fee":{"amount":[{"amount":"1853","denom":"uosmo"}],"gas":"617438"},"memo":"","msg":[{"type":"osmosis/poolmanager/split-amount-in","value":{"routes":[{"pools":[{"pool_id":"463","token_out_denom":"ibc/1DC495FCEFDA068A3820F903EDBD78B942FBD204D7E93D3BA2B432E9669D1A59"},{"pool_id":"916","token_out_denom":"ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580"}],"token_in_amount":"70000"},{"pools":[{"pool_id":"907","token_out_denom":"ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580"}],"token_in_amount":"30000"}],"sender":"osmo1qr7dhmvcqm4fnleaqel3gel4u20nk5rp9rwsae","token_in_denom":"uosmo","token_out_min_amount":"885297"}}],"signatures":[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AlcobsPzfTNVe7uqAAsndErJAjqplnyudaGB0f+R+p3F"},"signature":"7gMxXwqzZDe5+h1i16q7A7CgGUtLl2+Q8/YaUZCeYvp8kISbBwD2SlNTpJtz1RLskzF2uNcDebo61HbcVn9dAw=="}],"timeout_height":"13692007"}}"#, + signature: "ee03315f0ab36437b9fa1d62d7aabb03b0a0194b4b976f90f3f61a51909e62fa7c90849b0700f64a5353a49b73d512ec933176b8d70379ba3ad476dc567f5d03", + signature_json: r#"[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AlcobsPzfTNVe7uqAAsndErJAjqplnyudaGB0f+R+p3F"},"signature":"7gMxXwqzZDe5+h1i16q7A7CgGUtLl2+Q8/YaUZCeYvp8kISbBwD2SlNTpJtz1RLskzF2uNcDebo61HbcVn9dAw=="}]"#, + }); +} + #[test] fn test_sign_ibc_transfer() { let coin = TestCoinContext::default() diff --git a/src/proto/Cosmos.proto b/src/proto/Cosmos.proto index 6ac55b56570..04384442e93 100644 --- a/src/proto/Cosmos.proto +++ b/src/proto/Cosmos.proto @@ -425,6 +425,9 @@ message SigningInput { // Optional. If set, use a different Signer info when signing the transaction. SignerInfo signer_info = 12; + + // Optional timeout_height + uint64 timeout_height = 13; } // Result containing the signed and encoded transaction.