diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaMessageSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaMessageSigner.kt new file mode 100644 index 00000000000..b4eb3752fc7 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaMessageSigner.kt @@ -0,0 +1,44 @@ +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.MessageSigner +import wallet.core.jni.proto.Common.SigningError +import wallet.core.jni.proto.Solana + +class TestSolanaMessageSigner { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testMessageSign() { + val signingInput = Solana.MessageSigningInput.newBuilder().apply { + privateKey = ByteString.copyFrom("44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d".toHexByteArray()) + message = "Hello world" + }.build() + + val outputData = MessageSigner.sign(SOLANA, signingInput.toByteArray()) + val output = Solana.MessageSigningOutput.parseFrom(outputData) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.signature, "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG") + } + + @Test + fun testMessageVerify() { + val verifyingInput = Solana.MessageVerifyingInput.newBuilder().apply { + publicKey = ByteString.copyFrom("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9".toHexByteArray()) + signature = "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG" + message = "Hello world" + }.build() + + assert(MessageSigner.verify(SOLANA, verifyingInput.toByteArray())) + } +} \ No newline at end of file diff --git a/include/TrustWalletCore/TWMessageSigner.h b/include/TrustWalletCore/TWMessageSigner.h new file mode 100644 index 00000000000..c7f3c189edf --- /dev/null +++ b/include/TrustWalletCore/TWMessageSigner.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWPrivateKey.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Represents a message signer to sign custom messages for any blockchain. +TW_EXPORT_CLASS +struct TWMessageSigner; + +/// Signs an arbitrary message to prove ownership of an address for off-chain services. +/// +/// \param coin The given coin type to sign the message for. +/// \param input The serialized data of a `MessageSigningInput` proto object, (e.g. `TW.Solana.Proto.MessageSigningInput`). +/// \return The serialized data of a `MessageSigningOutput` proto object, (e.g. `TW.Solana.Proto.MessageSigningOutput`). +TW_EXPORT_STATIC_METHOD +TWData* _Nullable TWMessageSignerSign(enum TWCoinType coin, TWData* _Nonnull input); + +/// Verifies a signature for a message. +/// +/// \param coin The given coin type to sign the message for. +/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput). +/// \return whether the signature is valid. +TW_EXPORT_STATIC_METHOD +bool TWMessageSignerVerify(enum TWCoinType coin, TWData* _Nonnull input); + +TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 377e7836768..55201c9282e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2155,6 +2155,7 @@ dependencies = [ "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_proto", ] diff --git a/rust/chains/tw_solana/Cargo.toml b/rust/chains/tw_solana/Cargo.toml index 99c893b081e..dde71aa2cce 100644 --- a/rust/chains/tw_solana/Cargo.toml +++ b/rust/chains/tw_solana/Cargo.toml @@ -14,4 +14,5 @@ tw_encoding = { path = "../../tw_encoding" } tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_solana/src/entry.rs b/rust/chains/tw_solana/src/entry.rs index c2a6b46f24f..c340b3e01fe 100644 --- a/rust/chains/tw_solana/src/entry.rs +++ b/rust/chains/tw_solana/src/entry.rs @@ -4,6 +4,7 @@ use crate::address::SolanaAddress; use crate::compiler::SolanaCompiler; +use crate::modules::offchain_message_signer::OffchainMessageSigner; use crate::modules::transaction_decoder::SolanaTransactionDecoder; use crate::modules::transaction_util::SolanaTransactionUtil; use crate::modules::wallet_connect::connector::SolanaWalletConnector; @@ -14,7 +15,6 @@ use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; use tw_coin_entry::derivation::Derivation; use tw_coin_entry::error::prelude::*; use tw_coin_entry::modules::json_signer::NoJsonSigner; -use tw_coin_entry::modules::message_signer::NoMessageSigner; use tw_coin_entry::modules::plan_builder::NoPlanBuilder; use tw_coin_entry::prefix::NoPrefix; use tw_keypair::tw::PublicKey; @@ -32,7 +32,7 @@ impl CoinEntry for SolanaEntry { // Optional modules: type JsonSigner = NoJsonSigner; type PlanBuilder = NoPlanBuilder; - type MessageSigner = NoMessageSigner; + type MessageSigner = OffchainMessageSigner; type WalletConnector = SolanaWalletConnector; type TransactionDecoder = SolanaTransactionDecoder; type TransactionUtil = SolanaTransactionUtil; @@ -88,6 +88,11 @@ impl CoinEntry for SolanaEntry { SolanaCompiler::compile(coin, input, signatures, public_keys) } + #[inline] + fn message_signer(&self) -> Option { + Some(OffchainMessageSigner) + } + #[inline] fn wallet_connector(&self) -> Option { Some(SolanaWalletConnector) diff --git a/rust/chains/tw_solana/src/modules/mod.rs b/rust/chains/tw_solana/src/modules/mod.rs index 8a8d8c61a74..e1432102b17 100644 --- a/rust/chains/tw_solana/src/modules/mod.rs +++ b/rust/chains/tw_solana/src/modules/mod.rs @@ -12,6 +12,7 @@ pub mod insert_instruction; pub mod instruction_builder; pub mod message_builder; pub mod message_decompiler; +pub mod offchain_message_signer; pub mod proto_builder; pub mod transaction_decoder; pub mod transaction_util; diff --git a/rust/chains/tw_solana/src/modules/offchain_message_signer.rs b/rust/chains/tw_solana/src/modules/offchain_message_signer.rs new file mode 100644 index 00000000000..e0139328e2e --- /dev/null +++ b/rust/chains/tw_solana/src/modules/offchain_message_signer.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::SOLANA_ALPHABET; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::message_signer::MessageSigner; +use tw_coin_entry::signing_output_error; +use tw_encoding::base58; +use tw_keypair::ed25519; +use tw_keypair::traits::{SigningKeyTrait, VerifyingKeyTrait}; +use tw_misc::try_or_false; +use tw_proto::Solana::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +/// Currently, supports https://solana.com/developers/cookbook/wallets/sign-message only. +pub struct OffchainMessageSigner; + +impl OffchainMessageSigner { + pub fn sign_message_impl( + _coin: &dyn CoinContext, + input: Proto::MessageSigningInput, + ) -> SigningResult> { + let private_key = ed25519::sha512::PrivateKey::try_from(input.private_key.as_ref())?; + let sign = private_key.sign(input.message.as_bytes().to_vec())?; + let base58_sign = base58::encode(sign.to_bytes().as_slice(), SOLANA_ALPHABET); + Ok(Proto::MessageSigningOutput { + signature: base58_sign.into(), + ..Proto::MessageSigningOutput::default() + }) + } +} + +impl MessageSigner for OffchainMessageSigner { + type MessageSigningInput<'a> = Proto::MessageSigningInput<'a>; + type MessagePreSigningOutput = CompilerProto::PreSigningOutput<'static>; + type MessageSigningOutput = Proto::MessageSigningOutput<'static>; + type MessageVerifyingInput<'a> = Proto::MessageVerifyingInput<'a>; + + fn message_preimage_hashes( + &self, + _coin: &dyn CoinContext, + input: Self::MessageSigningInput<'_>, + ) -> Self::MessagePreSigningOutput { + CompilerProto::PreSigningOutput { + data: input.message.as_bytes().to_vec().into(), + ..CompilerProto::PreSigningOutput::default() + } + } + + fn sign_message( + &self, + coin: &dyn CoinContext, + input: Self::MessageSigningInput<'_>, + ) -> Self::MessageSigningOutput { + Self::sign_message_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::MessageSigningOutput, e)) + } + + fn verify_message( + &self, + _coin: &dyn CoinContext, + input: Self::MessageVerifyingInput<'_>, + ) -> bool { + let sign = try_or_false!(base58::decode(&input.signature, SOLANA_ALPHABET)); + let sign = try_or_false!(ed25519::Signature::try_from(sign.as_slice())); + let public_key = try_or_false!(ed25519::sha512::PublicKey::try_from( + input.public_key.as_ref() + )); + let message_utf8 = input.message.as_bytes().to_vec(); + public_key.verify(sign, message_utf8) + } +} diff --git a/rust/tw_any_coin/src/ffi/tw_message_signer.rs b/rust/tw_any_coin/src/ffi/tw_message_signer.rs index 1bc3d4227d8..6eff9bc66e6 100644 --- a/rust/tw_any_coin/src/ffi/tw_message_signer.rs +++ b/rust/tw_any_coin/src/ffi/tw_message_signer.rs @@ -10,13 +10,13 @@ use tw_memory::ffi::tw_data::TWData; use tw_memory::ffi::RawPtrTrait; use tw_misc::{try_or_else, try_or_false}; -/// Signs a message for the given blockchain. +/// Signs an arbitrary message to prove ownership of an address for off-chain services. /// +/// \param coin The given coin type to sign the message for. /// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput). -/// \param coin The given coin type to sign the transaction for. /// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput). #[no_mangle] -pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32) -> *mut TWData { +pub unsafe extern "C" fn tw_message_signer_sign(coin: u32, input: *const TWData) -> *mut TWData { let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut); let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); @@ -27,11 +27,11 @@ pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32) /// Verifies a signature for a message. /// -/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput). -/// \param coin The given coin type to sign the transaction for. -/// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput). +/// \param coin The given coin type to sign the message for. +/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput). +/// \return whether the signature is valid. #[no_mangle] -pub unsafe extern "C" fn tw_message_signer_verify(input: *const TWData, coin: u32) -> bool { +pub unsafe extern "C" fn tw_message_signer_verify(coin: u32, input: *const TWData) -> bool { let input = try_or_false!(TWData::from_ptr_as_ref(input)); let coin = try_or_false!(CoinType::try_from(coin)); MessageSigner::verify_message(input.as_slice(), coin).unwrap_or_default() @@ -39,13 +39,13 @@ pub unsafe extern "C" fn tw_message_signer_verify(input: *const TWData, coin: u3 /// Computes preimage hashes of a message. /// +/// \param coin The given coin type to sign the message for. /// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput). -/// \param coin The given coin type to sign the transaction for. /// \return The serialized data of TW.TxCompiler.PreSigningOutput. #[no_mangle] pub unsafe extern "C" fn tw_message_signer_pre_image_hashes( - input: *const TWData, coin: u32, + input: *const TWData, ) -> *mut TWData { let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut); let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); diff --git a/rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs index 8379148b394..b3feaecb07a 100644 --- a/rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs +++ b/rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs @@ -25,7 +25,7 @@ fn test_tw_message_signer_sign() { let input_data = TWDataHelper::create(serialize(&input).unwrap()); let output = TWDataHelper::wrap(unsafe { - tw_message_signer_sign(input_data.ptr(), CoinType::Ethereum as u32) + tw_message_signer_sign(CoinType::Ethereum as u32, input_data.ptr()) }) .to_vec() .expect("!tw_message_signer_sign returned nullptr"); @@ -45,7 +45,7 @@ fn test_tw_message_signer_verify() { }; let input_data = TWDataHelper::create(serialize(&input).unwrap()); - let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) }; + let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) }; assert!(verified); } @@ -58,7 +58,7 @@ fn test_tw_message_signer_verify_invalid() { }; let input_data = TWDataHelper::create(serialize(&input).unwrap()); - let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) }; + let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) }; assert!(!verified); } @@ -76,7 +76,7 @@ fn test_tw_message_signer_pre_image_hashes() { let input_data = TWDataHelper::create(serialize(&input).unwrap()); let output = TWDataHelper::wrap(unsafe { - tw_message_signer_pre_image_hashes(input_data.ptr(), CoinType::Ethereum as u32) + tw_message_signer_pre_image_hashes(CoinType::Ethereum as u32, input_data.ptr()) }) .to_vec() .expect("!tw_message_signer_sign returned nullptr"); diff --git a/rust/tw_tests/tests/chains/solana/mod.rs b/rust/tw_tests/tests/chains/solana/mod.rs index 01af3fc52b9..75e43059ad9 100644 --- a/rust/tw_tests/tests/chains/solana/mod.rs +++ b/rust/tw_tests/tests/chains/solana/mod.rs @@ -5,6 +5,7 @@ mod solana_address; mod solana_address_ffi; mod solana_compile; +mod solana_message_sign; mod solana_sign; mod solana_transaction; mod solana_transaction_ffi; diff --git a/rust/tw_tests/tests/chains/solana/solana_message_sign.rs b/rust/tw_tests/tests/chains/solana/solana_message_sign.rs new file mode 100644 index 00000000000..5bae82dd191 --- /dev/null +++ b/rust/tw_tests/tests/chains/solana/solana_message_sign.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::ffi::tw_message_signer::{ + tw_message_signer_pre_image_hashes, tw_message_signer_sign, tw_message_signer_verify, +}; +use tw_coin_entry::error::prelude::SigningErrorType; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_proto::{deserialize, serialize, Solana, TxCompiler}; + +#[test] +fn test_solana_message_signer_sign() { + let input = Solana::Proto::MessageSigningInput { + private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d" + .decode_hex() + .unwrap() + .into(), + message: "Hello world".into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let output = TWDataHelper::wrap(unsafe { + tw_message_signer_sign(CoinType::Solana as u32, input_data.ptr()) + }) + .to_vec() + .expect("!tw_message_signer_sign returned nullptr"); + + let output: Solana::Proto::MessageSigningOutput = deserialize(&output).unwrap(); + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!( + output.signature, + "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG" + ); +} + +#[test] +fn test_solana_message_signer_verify() { + let input = Solana::Proto::MessageVerifyingInput { + public_key: "ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9" + .decode_hex() + .unwrap() + .into(), + message: "Hello world".into(), + signature: "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG".into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let verified = unsafe { tw_message_signer_verify(CoinType::Solana as u32, input_data.ptr()) }; + assert!(verified); +} + +#[test] +fn test_solana_message_signer_pre_image_hashes() { + let message = "Hello world"; + + let input = Solana::Proto::MessageSigningInput { + private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d" + .decode_hex() + .unwrap() + .into(), + message: message.into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let output = TWDataHelper::wrap(unsafe { + tw_message_signer_pre_image_hashes(CoinType::Solana as u32, input_data.ptr()) + }) + .to_vec() + .expect("!tw_message_signer_sign returned nullptr"); + + let output: TxCompiler::Proto::PreSigningOutput = deserialize(&output).unwrap(); + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + let actual_message = String::from_utf8(output.data.to_vec()).unwrap(); + assert_eq!(actual_message, message); +} diff --git a/src/Ethereum/MessageSigner.cpp b/src/Ethereum/MessageSigner.cpp index a50863c9d15..c65e5a894e8 100644 --- a/src/Ethereum/MessageSigner.cpp +++ b/src/Ethereum/MessageSigner.cpp @@ -21,7 +21,7 @@ std::string signMessageRust(const PrivateKey& privateKey, const std::string& mes } Rust::TWDataWrapper inputData(data(input.SerializeAsString())); - Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_sign(inputData.get(), TWCoinTypeEthereum); + Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_sign(TWCoinTypeEthereum, inputData.get()); auto outputData = outputPtr.toDataOrDefault(); if (outputData.empty()) { @@ -43,7 +43,7 @@ Data messagePreImageHashRust(const std::string& message, Proto::MessageType msgT input.set_message_type(msgType); Rust::TWDataWrapper inputData(data(input.SerializeAsString())); - Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_pre_image_hashes(inputData.get(), TWCoinTypeEthereum); + Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_pre_image_hashes(TWCoinTypeEthereum, inputData.get()); auto outputData = outputPtr.toDataOrDefault(); if (outputData.empty()) { @@ -66,7 +66,7 @@ bool verifyMessageRust(const PublicKey& publicKey, const std::string& message, c input.set_signature(signature); Rust::TWDataWrapper inputData(data(input.SerializeAsString())); - return Rust::tw_message_signer_verify(inputData.get(), TWCoinTypeEthereum); + return Rust::tw_message_signer_verify(TWCoinTypeEthereum, inputData.get()); } std::string MessageSigner::signMessage(const PrivateKey& privateKey, const std::string& message, MessageType msgType, MaybeChainId chainId) { diff --git a/src/interface/TWMessageSigner.cpp b/src/interface/TWMessageSigner.cpp new file mode 100644 index 00000000000..805c3928e92 --- /dev/null +++ b/src/interface/TWMessageSigner.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWMessageSigner.h" +#include "rust/Wrapper.h" + +using namespace TW; + +TWData* _Nullable TWMessageSignerSign(enum TWCoinType coin, TWData* _Nonnull input) { + const Data& dataIn = *(reinterpret_cast(input)); + + Rust::TWDataWrapper inputData(dataIn); + Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_sign(coin, inputData.get()); + + auto outputData = outputPtr.toDataOrDefault(); + if (outputData.empty()) { + return nullptr; + } + return TWDataCreateWithBytes(outputData.data(), outputData.size()); +} + +bool TWMessageSignerVerify(enum TWCoinType coin, TWData* _Nonnull input) { + const Data& dataIn = *(reinterpret_cast(input)); + + Rust::TWDataWrapper inputData(dataIn); + return Rust::tw_message_signer_verify(coin, inputData.get()); +} diff --git a/src/proto/Solana.proto b/src/proto/Solana.proto index 0649cf72671..c759769d2b1 100644 --- a/src/proto/Solana.proto +++ b/src/proto/Solana.proto @@ -320,3 +320,33 @@ message PreSigningOutput { // Error code description string error_message = 4; } + +message MessageSigningInput { + // The secret private key used for signing (32 bytes). + bytes private_key = 1; + + // A UTF-8 regular message to sign. + string message = 2; +} + +message MessageSigningOutput { + // The signature, Base58-encoded. + string signature = 1; + + // error code, 0 is ok, other codes will be treated as errors + Common.Proto.SigningError error = 2; + + // error code description + string error_message = 3; +} + +message MessageVerifyingInput { + // The message signed. + string message = 1; + + // Public key that will verify and recover the message from the signature. + bytes public_key = 2; + + // The signature, Base58-encoded. + string signature = 3; +} diff --git a/swift/Tests/Blockchains/SolanaTests.swift b/swift/Tests/Blockchains/SolanaTests.swift index 9c6287ddef3..bb8f996363c 100644 --- a/swift/Tests/Blockchains/SolanaTests.swift +++ b/swift/Tests/Blockchains/SolanaTests.swift @@ -329,4 +329,17 @@ class SolanaTests: XCTestCase { // https://explorer.solana.com/tx/2ho7wZUXbDNz12xGfsXg2kcNMqkBAQjv7YNXNcVcuCmbC4p9FZe9ELeM2gMjq9MKQPpmE3nBW5pbdgwVCfNLr1h8 XCTAssertEqual(output.encoded, "AVUye82Mv+/aWeU2G+B6Nes365mUU2m8iqcGZn/8kFJvw4wY6AgKGG+vJHaknHlCDwE1yi1SIMVUUtNCOm3kHg8BAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA") } + + func testSignUserMessage() throws { + let privateKey = Data(hexString: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d")! + + let input = SolanaMessageSigningInput.with { + $0.privateKey = privateKey + $0.message = "Hello world" + } + let outputData = MessageSigner.sign(coin: .solana, input: try input.serializedData())! + let output = try SolanaMessageSigningOutput(serializedData: outputData) + XCTAssertEqual(output.error, .ok) + XCTAssertEqual(output.signature, "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG") + } } diff --git a/tests/chains/Solana/SolanaMessageSigner.cpp b/tests/chains/Solana/SolanaMessageSigner.cpp new file mode 100644 index 00000000000..a335ed7109e --- /dev/null +++ b/tests/chains/Solana/SolanaMessageSigner.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include +#include + +#include "Data.h" +#include "HexCoding.h" +#include "proto/Solana.pb.h" + +#include "TestUtilities.h" +#include +#include + +namespace TW::Solana { + +TEST(SolanaMessageSigner, Sign) { + const auto privateKey = parse_hex("44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d"); + const auto message = "Hello world"; + + Proto::MessageSigningInput input; + input.set_private_key(privateKey.data(), privateKey.size()); + input.set_message(message); + + const auto inputData = data(input.SerializeAsString()); + const auto inputDataPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + Proto::MessageSigningOutput output; + const auto outputDataPtr = WRAPD(TWMessageSignerSign(TWCoinTypeSolana, inputDataPtr.get())); + output.ParseFromArray(TWDataBytes(outputDataPtr.get()), static_cast(TWDataSize(outputDataPtr.get()))); + + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + EXPECT_EQ(output.signature(), "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"); +} + +TEST(SolanaMessageSigner, Verify) { + const auto publicKey = parse_hex("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9"); + const auto message = "Hello world"; + const auto signature = "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"; + + Proto::MessageVerifyingInput input; + input.set_public_key(publicKey.data(), publicKey.size()); + input.set_message(message); + input.set_signature(signature); + + const auto inputData = data(input.SerializeAsString()); + const auto inputDataPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + EXPECT_TRUE(TWMessageSignerVerify(TWCoinTypeSolana, inputDataPtr.get())); +} + +} // namespace TW::Solana