From 7772467b2f72cfe32d421c52ddd83ac853259b0f Mon Sep 17 00:00:00 2001 From: Sztergbaum Roman Date: Fri, 27 Jan 2023 12:29:56 +0100 Subject: [PATCH] [Sui]: Add SUI Implementation (#2883) --- .../blockchains/CoinAddressDerivationTests.kt | 1 + .../app/blockchains/sui/TestSuiAddress.kt | 35 ++++++ .../core/app/blockchains/sui/TestSuiSigner.kt | 42 +++++++ docs/registry.md | 1 + include/TrustWalletCore/TWBlockchain.h | 1 + include/TrustWalletCore/TWCoinType.h | 1 + registry.json | 28 +++++ rust/Cargo.lock | 9 +- rust/Cargo.toml | 1 + rust/cbindgen.toml | 3 + rust/src/encoding/mod.rs | 115 ++++++++++++++++++ rust/src/lib.rs | 1 + src/Aptos/Address.cpp | 66 +--------- src/Aptos/Address.h | 38 ++---- src/Base64.cpp | 91 +++++--------- src/Base64.h | 2 +- src/Coin.cpp | 5 +- src/Move/Address.h | 106 ++++++++++++++++ src/Sui/Address.cpp | 24 ++++ src/Sui/Address.h | 39 ++++++ src/Sui/Entry.cpp | 26 ++++ src/Sui/Entry.h | 20 +++ src/Sui/Signer.cpp | 45 +++++++ src/Sui/Signer.h | 25 ++++ src/proto/Sui.proto | 34 ++++++ swift/Tests/Blockchains/SuiTests.swift | 40 ++++++ swift/Tests/CoinAddressDerivationTests.swift | 3 + tests/chains/Sui/AddressTests.cpp | 53 ++++++++ tests/chains/Sui/SignerTests.cpp | 65 ++++++++++ tests/chains/Sui/TWCoinTypeTests.cpp | 33 +++++ tests/common/Base64Tests.cpp | 9 ++ tests/common/CoinAddressDerivationTests.cpp | 3 + tests/common/HDWallet/HDWalletTests.cpp | 13 ++ .../common/rust/bindgen/WalletCoreRsTests.cpp | 1 - tools/install-wasm-dependencies | 2 +- walletconsole/lib/Util.cpp | 9 +- wasm/CMakeLists.txt | 2 +- wasm/tests/Blockchain/Sui.test.ts | 28 +++++ 38 files changed, 858 insertions(+), 162 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt create mode 100644 rust/cbindgen.toml create mode 100644 rust/src/encoding/mod.rs create mode 100644 src/Move/Address.h create mode 100644 src/Sui/Address.cpp create mode 100644 src/Sui/Address.h create mode 100644 src/Sui/Entry.cpp create mode 100644 src/Sui/Entry.h create mode 100644 src/Sui/Signer.cpp create mode 100644 src/Sui/Signer.h create mode 100644 src/proto/Sui.proto create mode 100644 swift/Tests/Blockchains/SuiTests.swift create mode 100644 tests/chains/Sui/AddressTests.cpp create mode 100644 tests/chains/Sui/SignerTests.cpp create mode 100644 tests/chains/Sui/TWCoinTypeTests.cpp create mode 100644 wasm/tests/Blockchain/Sui.test.ts diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt index dd3b7491a5c..84ed86b221f 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt @@ -108,6 +108,7 @@ class CoinAddressDerivationTests { EVERSCALE -> assertEquals("0:0c39661089f86ec5926ea7d4ee4223d634ba4ed6dcc2e80c7b6a8e6d59f79b04", address) TON -> assertEquals("EQDgEMqToTacHic7SnvnPFmvceG5auFkCcAw0mSCvzvKUfk9", address) APTOS -> assertEquals("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", address) + SUI -> assertEquals("0x061ce2b2100a71bb7aa0da98998887ad82597948", address) HEDERA -> assertEquals("0.0.302a300506032b657003210049eba62f64d0d941045595d9433e65d84ecc46bcdb1421de55e05fcf2d8357d5", address) SECRET -> assertEquals("secret1f69sk5033zcdr2p2yf3xjehn7xvgdeq09d2llh", address) NATIVEINJECTIVE -> assertEquals("inj13u6g7vqgw074mgmf2ze2cadzvkz9snlwcrtq8a", address) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt new file mode 100644 index 00000000000..87ae5c400e0 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiAddress.kt @@ -0,0 +1,35 @@ +// Copyright © 2017-2022 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +package com.trustwallet.core.app.blockchains.sui + +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestSuiAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + val any = AnyAddress("0x061ce2b2100a71bb7aa0da98998887ad82597948", CoinType.SUI) + assertEquals(any.coin(), CoinType.SUI) + assertEquals(any.description(), "0x061ce2b2100a71bb7aa0da98998887ad82597948") + + Assert.assertFalse( + AnyAddress.isValid( + "0xMQqpqMQgCBuiPkoXfgZZsJvuzCeI1zc00z6vHJj4", + CoinType.SUI + ) + ) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt new file mode 100644 index 00000000000..9a5782eebd0 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/sui/TestSuiSigner.kt @@ -0,0 +1,42 @@ +// Copyright © 2017-2022 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +package com.trustwallet.core.app.blockchains.sui + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +import com.trustwallet.core.app.utils.toHexByteArray +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHexBytesInByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.CoinType +import wallet.core.jni.proto.Sui + +class TestSuiSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun SuiTransactionSigning() { + // Successfully broadcasted https://explorer.sui.io/transaction/rxLgxcAqgMg8gphp6eCsSGQcdZnwFYx2SRdwEhnAUC4 + val txBytes = """ + AAUCLiNiMy/EzosKCk5EZr5QQZmMVLnvAAAAAAAAACDqj/OT+1+qyLZKV4YLw8kpK3/bTZKspTUmh1pBuUfHPLb0crwkV1LQcBARaxER8XhTNJmK7wAAAAAAAAAgaQEguOdXa+m16IM536nsveakQ4u/GYJAc1fpYGGKEvgBQUP35yxF+cEL5qm153kw18dVeuYB6AMAAAAAAAAttQCskZzd41GsNuNxHYMsbbl2aS4jYjMvxM6LCgpORGa+UEGZjFS57wAAAAAAAAAg6o/zk/tfqsi2SleGC8PJKSt/202SrKU1JodaQblHxzwBAAAAAAAAAOgDAAAAAAAA + """.trimIndent() + val key = + "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266".toHexBytesInByteString() + val signDirect = Sui.SignDirect.newBuilder().setUnsignedTxMsg(txBytes).build() + val signingInput = + Sui.SigningInput.newBuilder().setSignDirectMessage(signDirect).setPrivateKey(key).build() + val result = AnySigner.sign(signingInput, CoinType.SUI, Sui.SigningOutput.parser()) + val expectedSignature = "AIYRmHDpQesfAx3iWBCMwInf3MZ56ZQGnPWNtECFjcSq0ssAgjRW6GLnFCX24tfDNjSm9gjYgoLmn1No15iFJAtqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg==" + assertEquals(result.unsignedTx, txBytes); + assertEquals(result.signature, expectedSignature) + } +} diff --git a/docs/registry.md b/docs/registry.md index 0f0784f6e60..65da87087a0 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -55,6 +55,7 @@ This list is generated from [./registry.json](../registry.json) | 607 | TON | TON | | | | 637 | Aptos | APT | | | | 714 | BNB Beacon Chain | BNB | | | +| 784 | Sui | SUI | | | | 818 | VeChain | VET | | | | 820 | Callisto | CLO | | | | 888 | NEO | NEO | | | diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index 4231010137d..c93bab5e2b9 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -57,6 +57,7 @@ enum TWBlockchain { TWBlockchainAptos = 43, // Aptos TWBlockchainHedera = 44, // Hedera TWBlockchainTheOpenNetwork = 45, + TWBlockchainSui = 46, }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index 39b3c9bdeb0..22a3540c265 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -130,6 +130,7 @@ enum TWCoinType { TWCoinTypeNativeInjective = 10000060, TWCoinTypeAgoric = 564, TWCoinTypeTON = 607, + TWCoinTypeSui = 784, }; /// Returns the blockchain for a coin type. diff --git a/registry.json b/registry.json index 2bfa2e3ad3c..07081485815 100644 --- a/registry.json +++ b/registry.json @@ -429,6 +429,34 @@ "documentation": "https://fullnode.mainnet.aptoslabs.com/v1/spec#/" } }, + { + "id": "sui", + "name": "Sui", + "coinId": 784, + "symbol": "SUI", + "decimals": 9, + "blockchain": "Sui", + "derivation": [ + { + "path": "m/44'/784'/0'/0'/0'" + } + ], + "curve": "ed25519", + "publicKeyType": "ed25519", + "explorer": { + "url": "https://explorer.sui.io/", + "txPath": "/transaction/", + "accountPath": "/address/", + "sampleTx": "SWRW1RoMHxnD9NeobgBoC4cXGwp2Hc511CnfWUoTBmo", + "sampleAccount": "0x62107e1afefccc7b2267ab74e332c146f5c2ca15" + }, + "info": { + "url": "https://sui.io/", + "source": "https://github.com/MystenLabs/sui", + "rpc": "https://fullnode.testnet.sui.io", + "documentation": "https://docs.sui.io/" + } + }, { "id": "cosmos", "name": "Cosmos", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 00747873f1b..3efb0c41ea4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -97,6 +97,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "bcs" version = "0.1.4" @@ -806,7 +812,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95540acef038bfdf3c91da323cedf0fd335f73899152cabdf407033fc7560713" dependencies = [ - "base64", + "base64 0.13.1", "ethereum-types", "hex", "serde", @@ -994,6 +1000,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "wallet-core-rs" version = "0.1.0" dependencies = [ + "base64 0.21.0", "bcs", "hex", "move-core-types", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b3a7546634b..7752a93202d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,5 +16,6 @@ starknet-ff = "0.1.0" starknet-signers = "0.1.0" bcs = "0.1.4" hex = "0.4.3" +base64 = "0.21.0" [dev-dependencies] diff --git a/rust/cbindgen.toml b/rust/cbindgen.toml new file mode 100644 index 00000000000..0ccb2ad1451 --- /dev/null +++ b/rust/cbindgen.toml @@ -0,0 +1,3 @@ +# Whether to add a `#pragma once` guard +# default: doesn't emit a `#pragma once` +pragma_once = true diff --git a/rust/src/encoding/mod.rs b/rust/src/encoding/mod.rs new file mode 100644 index 00000000000..82bff00e40b --- /dev/null +++ b/rust/src/encoding/mod.rs @@ -0,0 +1,115 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +use base64::{Engine as _, engine::{general_purpose}}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +#[no_mangle] +pub extern "C" fn encode_base64(data: *const u8, len: usize, is_url: bool) -> *mut c_char { + let data = unsafe { std::slice::from_raw_parts(data, len) }; + let encoded = if is_url { + general_purpose::URL_SAFE.encode(data) + } else { + general_purpose::STANDARD.encode(data) + }; + CString::new(encoded).unwrap().into_raw() +} + +#[repr(C)] +pub struct CByteArray { + data: *mut u8, + size: usize, +} + +#[no_mangle] +pub extern "C" fn decode_base64(data: *const c_char, is_url: bool) -> CByteArray { + if data.is_null() { + return CByteArray { data: std::ptr::null_mut(), size: 0 }; + } + let c_str = unsafe { CStr::from_ptr(data) }; + let str_slice = c_str.to_str().unwrap(); + let decoded = if is_url { + general_purpose::URL_SAFE + .decode(str_slice) + } else { + general_purpose::STANDARD + .decode(str_slice) + }; + let decoded = match decoded { + Ok(decoded) => decoded, + Err(_) => return CByteArray { data: std::ptr::null_mut(), size: 0 } + }; + let size = decoded.len(); + let mut decoded_vec = decoded.to_vec(); + let ptr = decoded_vec.as_mut_ptr(); + std::mem::forget(decoded_vec); + CByteArray { data: ptr, size } +} + + +#[cfg(test)] +mod tests { + use std::ffi::CString; + use crate::encoding::{decode_base64, encode_base64}; + + #[test] + fn test_encode_base64_ffi() { + let data = b"hello world"; + let encoded = unsafe { + std::ffi::CStr::from_ptr(encode_base64(data.as_ptr(), data.len(), false)) + }; + let expected = "aGVsbG8gd29ybGQ="; + assert_eq!(encoded.to_str().unwrap(), expected); + } + + #[test] + fn test_encode_base64_url_ffi() { + let data = b"+'?ab"; + let encoded = unsafe { + std::ffi::CStr::from_ptr(encode_base64(data.as_ptr(), data.len(), true)) + }; + let expected = "Kyc_YWI="; + assert_eq!(encoded.to_str().unwrap(), expected); + } + + #[test] + fn test_decode_base64_url() { + let encoded = "Kyc_YWI="; + let expected = b"+'?ab"; + + let encoded_c_str = CString::new(encoded).unwrap(); + let encoded_ptr = encoded_c_str.as_ptr(); + + let decoded_ptr = decode_base64(encoded_ptr, true); + let decoded_slice = unsafe { std::slice::from_raw_parts(decoded_ptr.data, decoded_ptr.size) }; + + assert_eq!(decoded_slice, expected); + } + + #[test] + fn test_decode_base64() { + let encoded = "aGVsbG8gd29ybGQh"; + let expected = b"hello world!"; + + let encoded_c_str = CString::new(encoded).unwrap(); + let encoded_ptr = encoded_c_str.as_ptr(); + + let decoded_ptr = decode_base64(encoded_ptr, false); + let decoded_slice = unsafe { std::slice::from_raw_parts(decoded_ptr.data, decoded_ptr.size) }; + + assert_eq!(decoded_slice, expected); + } + + #[test] + fn test_decode_base64_invalid() { + let invalid_encoded = "_This_is_an_invalid_base64_"; + let encoded_c_str = CString::new(invalid_encoded).unwrap(); + let encoded_ptr = encoded_c_str.as_ptr(); + let decoded_ptr = decode_base64(encoded_ptr, false); + assert_eq!(decoded_ptr.data.is_null(), true); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 01bae1bc0ae..e0efd13f8a2 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -7,3 +7,4 @@ pub mod move_parser; pub mod memory; pub mod starknet; +pub mod encoding; diff --git a/src/Aptos/Address.cpp b/src/Aptos/Address.cpp index 30c1b587872..2688e58c279 100644 --- a/src/Aptos/Address.cpp +++ b/src/Aptos/Address.cpp @@ -8,76 +8,18 @@ #include "Address.h" #include "HexCoding.h" -namespace { - -std::string normalize(const std::string& string, std::size_t hexLen) { - std::string hexStr((TW::Aptos::Address::size * 2) - hexLen, '0'); - hexStr.append(string); - return hexStr; -} - -} // namespace - namespace TW::Aptos { -bool Address::isValid(const std::string& string) { - auto address = string; - if (address.starts_with("0x")) { - address = address.substr(2); - if (std::size_t hexLen = address.size(); hexLen < Address::size * 2) { - address = normalize(address, hexLen); - } - } - if (address.size() != 2 * Address::size) { - return false; - } - const auto data = parse_hex(address); - return isValid(data); +Address::Address(const std::string& string) : Address::AptosAddress(string) { } -Address::Address(const std::string& string) { - if (!isValid(string)) { - throw std::invalid_argument("Invalid address string"); - } - auto hexFunctor = [&string]() { - if (std::size_t hexLen = string.size() - 2; string.starts_with("0x") && hexLen < Address::size * 2) { - //! We have specific address like 0x1, padding it. - return parse_hex(normalize(string.substr(2), hexLen)); - } else { - return parse_hex(string); - } - }; - - const auto data = hexFunctor(); - std::copy(data.begin(), data.end(), bytes.begin()); +Address::Address(const PublicKey& publicKey): Address::AptosAddress(publicKey) { } -Address::Address(const Data& data) { - if (!isValid(data)) { - throw std::invalid_argument("Invalid address data"); - } - std::copy(data.begin(), data.end(), bytes.begin()); -} - -Address::Address(const PublicKey& publicKey) { - if (publicKey.type != TWPublicKeyTypeED25519) { - throw std::invalid_argument("Invalid public key type"); - } +Data Address::getDigest(const PublicKey& publicKey) { auto key_data = publicKey.bytes; append(key_data, 0x00); - const auto data = Hash::sha3_256(key_data); - std::copy(data.begin(), data.end(), bytes.begin()); -} - -std::string Address::string(bool withPrefix) const { - std::string output = withPrefix ? "0x" : ""; - return output + hex(bytes); -} - -std::string Address::shortString() const { - std::string s = hex(bytes); - s.erase(0, s.find_first_not_of('0')); - return s; + return key_data; } BCS::Serializer& operator<<(BCS::Serializer& stream, Address addr) noexcept { diff --git a/src/Aptos/Address.h b/src/Aptos/Address.h index 328c387edf4..0e372a88e93 100644 --- a/src/Aptos/Address.h +++ b/src/Aptos/Address.h @@ -9,53 +9,29 @@ #include "BCS.h" #include "Data.h" -#include "../PublicKey.h" +#include "Move/Address.h" +#include "PublicKey.h" #include namespace TW::Aptos { -class Address { +class Address : public Move::Address { public: - static constexpr size_t size = 32; - - std::array bytes; - - /// Determines whether a collection of bytes makes a valid address. - static bool isValid(const Data& data) { return data.size() == size; } - - /// Determines whether a string makes a valid address. - static bool isValid(const std::string& string); - - static Address zero() { - return Address("0x0"); - } - - static Address one() { - return Address("0x1"); - } - - static Address three() { - return Address("0x3"); - } + using AptosAddress = Move::Address; + using AptosAddress::size; + using AptosAddress::bytes; /// Initializes an Aptos address with a string representation. explicit Address(const std::string& string); - /// Initializes an Aptos address with a collection of bytes - explicit Address(const Data& data); - /// Initializes an Aptos address with a public key. explicit Address(const PublicKey& publicKey); /// Constructor that allow factory programming; Address() noexcept = default; - /// Returns a string representation of the address. - [[nodiscard]] std::string string(bool withPrefix = true) const; - - /// Returns a short string representation of the address. E.G 0x1; - [[nodiscard]] std::string shortString() const; + Data getDigest(const PublicKey& publicKey); }; constexpr inline bool operator==(const Address& lhs, const Address& rhs) noexcept { diff --git a/src/Base64.cpp b/src/Base64.cpp index edb5db03998..a09f399e3c7 100644 --- a/src/Base64.cpp +++ b/src/Base64.cpp @@ -1,15 +1,35 @@ -// Copyright © 2017-2020 Trust Wallet. +// Copyright © 2017-2023 Trust Wallet. // // This file is part of Trust. The full Trust copyright notice, including // terms governing use, modification, and redistribution, is contained in the // file LICENSE at the root of the source code distribution tree. #include "Base64.h" +#include "rust/bindgen/WalletCoreRSBindgen.h" -#include -#include -#include -#include +namespace TW::Base64::internal { + +std::string encode(const Data& val, bool is_url) { + char* encoded = encode_base64(val.data(), val.size(), is_url); + std::string encoded_str(encoded); + free_string(encoded); + return encoded_str; +} + +Data decode(const std::string& val, bool is_url) { + if (val.empty()) { + return Data(); + } + auto decoded = decode_base64(val.c_str(), is_url); + if (decoded.data == nullptr) { + return Data(); + } + std::vector decoded_vec(&decoded.data[0], &decoded.data[decoded.size]); + std::free(decoded.data); + return decoded_vec; +} + +} namespace TW::Base64 { @@ -36,65 +56,20 @@ bool isBase64orBase64Url(const string& val) { return isBase64Any(val, base64_chars) || isBase64Any(val, base64_url_chars); } -Data decode(const string& val) { - using namespace boost::archive::iterators; - using It = transform_width, 8, 6>; - return boost::algorithm::trim_right_copy_if(Data(It(begin(val)), It(end(val))), - [](char c) { return c == '\0'; }); -} - -string encode(const Data& val) { - using namespace boost::archive::iterators; - using It = base64_from_binary>; - auto encoded = string(It(begin(val)), It(end(val))); - return encoded.append((3 - val.size() % 3) % 3, '='); -} - -/// Convert from Base64Url format to regular -void convertFromBase64Url(string& b) { - // '-' and '_' (Base64URL format) are changed to '+' and '/' - // in-place replace - size_t n = b.length(); - char* start = b.data(); - char* end = start + n; - for (auto* p = start; p < end; ++p) { - if (*p == '-') { - *p = '+'; - } else if (*p == '_') { - *p = '/'; - } - } +Data decodeBase64Url(const string& val) { + return internal::decode(val, true); } -/// Convert from regular format to Base64Url -void convertToBase64Url(string& b) { - // '+' and '/' are changed to '-' and '_' (Base64URL format) - // in-place replace - size_t n = b.length(); - char* start = b.data(); - char* end = start + n; - for (auto* p = start; p < end; ++p) { - if (*p == '+') { - *p = '-'; - } else if (*p == '/') { - *p = '_'; - } - } +string encodeBase64Url(const Data& val) { + return internal::encode(val, true); } -Data decodeBase64Url(const string& val) { - string base64Url = val; - convertFromBase64Url(base64Url); - return decode(base64Url); +std::string encode(const Data& val) { + return internal::encode(val, false); } -string encodeBase64Url(const Data& val) { - using namespace boost::archive::iterators; - using It = base64_from_binary>; - auto encoded = string(It(begin(val)), It(end(val))); - encoded.append((3 - val.size() % 3) % 3, '='); - convertToBase64Url(encoded); - return encoded; +Data decode(const string& val) { + return internal::decode(val, false); } } // namespace TW::Base64 diff --git a/src/Base64.h b/src/Base64.h index 8ea6f74356b..eacd9e147a5 100644 --- a/src/Base64.h +++ b/src/Base64.h @@ -17,7 +17,7 @@ bool isBase64orBase64Url(const std::string& val); Data decode(const std::string& val); // Encode bytes into Base64 string -std::string encode(const Data& val); +std::string encode(const TW::Data& val); // Decode a Base64Url-format or a regular Base64 string. // Base64Url format uses '-' and '_' as the two special characters, Base64 uses '+'and '/'. diff --git a/src/Coin.cpp b/src/Coin.cpp index 521db9fb0a8..167fb0026b2 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -1,4 +1,4 @@ -// Copyright © 2017-2022 Trust Wallet. +// Copyright © 2017-2023 Trust Wallet. // // This file is part of Trust. The full Trust copyright notice, including // terms governing use, modification, and redistribution, is contained in the @@ -57,6 +57,7 @@ #include "Zilliqa/Entry.h" #include "Hedera/Entry.h" #include "TheOpenNetwork/Entry.h" +#include "Sui/Entry.h" // end_of_coin_includes_marker_do_not_modify using namespace TW; @@ -107,6 +108,7 @@ Nervos::Entry NervosDP; Everscale::Entry EverscaleDP; Hedera::Entry HederaDP; TheOpenNetwork::Entry tonDP; +Sui::Entry SuiDP; // end_of_coin_dipatcher_declarations_marker_do_not_modify CoinEntry* coinDispatcher(TWCoinType coinType) { @@ -159,6 +161,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) { case TWBlockchainAptos: entry = &AptosDP; break; case TWBlockchainHedera: entry = &HederaDP; break; case TWBlockchainTheOpenNetwork: entry = &tonDP; break; + case TWBlockchainSui: entry = &SuiDP; break; // end_of_coin_dipatcher_switch_marker_do_not_modify default: entry = nullptr; break; diff --git a/src/Move/Address.h b/src/Move/Address.h new file mode 100644 index 00000000000..216f1d209d6 --- /dev/null +++ b/src/Move/Address.h @@ -0,0 +1,106 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#pragma once + +#include "Data.h" +#include "HexCoding.h" +#include "PublicKey.h" +#include + +namespace TW::Move { +template +class Address { +private: + static std::string normalize(const std::string& string, std::size_t hexLen) { + std::string hexStr((size * 2) - hexLen, '0'); + hexStr.append(string); + return hexStr; + } + + /// Determines whether a collection of bytes makes a valid address. + static bool isValid(const Data& data) { return data.size() == size; } +public: + static constexpr int size = N; + std::array bytes; + + /// Determines whether a string makes a valid address. + static bool isValid(const std::string& string) { + auto address = string; + if (address.starts_with("0x")) { + address = address.substr(2); + if (std::size_t hexLen = address.size(); hexLen < Address::size * 2) { + address = normalize(address, hexLen); + } + } + if (address.size() != 2 * Address::size) { + return false; + } + const auto data = parse_hex(address); + return isValid(data); + }; + + Address() noexcept = default; + + Address(const std::string& string) { + if (!isValid(string)) { + throw std::invalid_argument("Invalid address string"); + } + auto hexFunctor = [&string]() { + if (std::size_t hexLen = string.size() - 2; string.starts_with("0x") && hexLen < size * 2) { + //! We have specific address like 0x1, padding it. + return parse_hex(normalize(string.substr(2), hexLen)); + } else { + return parse_hex(string); + } + }; + + const auto data = hexFunctor(); + std::copy(data.begin(), data.end(), bytes.begin()); + } + + Address(const Data& data) { + if (!isValid(data)) { + throw std::invalid_argument("Invalid address data"); + } + std::copy_n(data.begin(), size, bytes.begin()); + } + + Address(const PublicKey& publicKey) { + if (publicKey.type != TWPublicKeyTypeED25519) { + throw std::invalid_argument("Invalid public key type"); + } + auto digest = static_cast(this)->getDigest(publicKey); + const auto data = Hash::sha3_256(digest); + std::copy_n(data.begin(), Address::size, bytes.begin()); + } + + static Derived zero() { + return Derived("0x0"); + } + + static Derived one() { + return Derived("0x1"); + } + + static Derived three() { + return Derived("0x3"); + } + + /// Returns a string representation of the address. + [[nodiscard]] std::string string(bool withPrefix = true) const { + std::string output = withPrefix ? "0x" : ""; + return output + hex(bytes); + }; + + /// Returns a short string representation of the address. E.G 0x1; + [[nodiscard]] std::string shortString() const { + std::string s = hex(bytes); + s.erase(0, s.find_first_not_of('0')); + return s; + }; +}; +} // namespace TW::Move diff --git a/src/Sui/Address.cpp b/src/Sui/Address.cpp new file mode 100644 index 00000000000..63bba970c76 --- /dev/null +++ b/src/Sui/Address.cpp @@ -0,0 +1,24 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#include "Address.h" +#include "HexCoding.h" + +namespace TW::Sui { + +Address::Address(const std::string& string) : Address::SuiAddress(string) { +} + +Address::Address(const PublicKey& publicKey): Address::SuiAddress(publicKey) { +} + +Data Address::getDigest(const PublicKey& publicKey) { + auto key_data = Data{0x00}; + append(key_data, publicKey.bytes); + return key_data; +} + +} // namespace TW::Sui diff --git a/src/Sui/Address.h b/src/Sui/Address.h new file mode 100644 index 00000000000..ccdf055a1c1 --- /dev/null +++ b/src/Sui/Address.h @@ -0,0 +1,39 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#pragma once + +#include "Data.h" +#include "PublicKey.h" +#include "Move/Address.h" + +#include + +namespace TW::Sui { + +class Address : public Move::Address { +public: + using SuiAddress = Move::Address; + using SuiAddress::size; + using SuiAddress::bytes; + + /// Initializes an Sui address with a string representation. + explicit Address(const std::string& string); + + /// Initializes an Sui address with a public key. + explicit Address(const PublicKey& publicKey); + + /// Constructor that allow factory programming; + Address() noexcept = default; + + Data getDigest(const PublicKey& publicKey); +}; + +constexpr inline bool operator==(const Address& lhs, const Address& rhs) noexcept { + return lhs.bytes == rhs.bytes; +} + +} // namespace TW::Sui diff --git a/src/Sui/Entry.cpp b/src/Sui/Entry.cpp new file mode 100644 index 00000000000..8df448495ca --- /dev/null +++ b/src/Sui/Entry.cpp @@ -0,0 +1,26 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#include "Entry.h" + +#include "Address.h" +#include "Signer.h" + +namespace TW::Sui { + +bool Entry::validateAddress([[maybe_unused]] TWCoinType coin, const std::string& address, [[maybe_unused]] const PrefixVariant& addressPrefix) const { + return Address::isValid(address); +} + +std::string Entry::deriveAddress([[maybe_unused]] TWCoinType coin, const PublicKey& publicKey, [[maybe_unused]] TWDerivation derivation, [[maybe_unused]] const PrefixVariant& addressPrefix) const { + return Address(publicKey).string(); +} + +void Entry::sign([[maybe_unused]] TWCoinType coin, const TW::Data& dataIn, TW::Data& dataOut) const { + signTemplate(dataIn, dataOut); +} + +} // namespace TW::Sui diff --git a/src/Sui/Entry.h b/src/Sui/Entry.h new file mode 100644 index 00000000000..75cfde95062 --- /dev/null +++ b/src/Sui/Entry.h @@ -0,0 +1,20 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#pragma once + +#include "CoinEntry.h" + +namespace TW::Sui { + +class Entry final : public CoinEntry { +public: + bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const; + std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const; + void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const; +}; + +} // namespace TW::Sui diff --git a/src/Sui/Signer.cpp b/src/Sui/Signer.cpp new file mode 100644 index 00000000000..49e9b247f63 --- /dev/null +++ b/src/Sui/Signer.cpp @@ -0,0 +1,45 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#include "Signer.h" +#include "Address.h" +#include "Base64.h" +#include "PublicKey.h" + +namespace { + +enum IntentScope : int { + TransactionData = 0, +}; + +enum IntentVersion : int { + V0 = 0, +}; + +enum IntentAppId { + Sui = 0 +}; + +} // namespace + +namespace TW::Sui { + +Proto::SigningOutput Signer::sign(const Proto::SigningInput& input) noexcept { + auto protoOutput = Proto::SigningOutput(); + auto unsignedTx = input.sign_direct_message().unsigned_tx_msg(); + auto unsignedTxData = TW::Base64::decode(unsignedTx); + Data toSign{TransactionData, V0, IntentAppId::Sui}; + append(toSign, unsignedTxData); + auto privateKey = PrivateKey(Data(input.private_key().begin(), input.private_key().end())); + Data signatureScheme{0x00}; + append(signatureScheme, privateKey.sign(toSign, TWCurveED25519)); + append(signatureScheme, privateKey.getPublicKey(TWPublicKeyTypeED25519).bytes); + protoOutput.set_unsigned_tx(unsignedTx); + protoOutput.set_signature(TW::Base64::encode(signatureScheme)); + return protoOutput; +} + +} // namespace TW::Sui diff --git a/src/Sui/Signer.h b/src/Sui/Signer.h new file mode 100644 index 00000000000..2b158464c88 --- /dev/null +++ b/src/Sui/Signer.h @@ -0,0 +1,25 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#pragma once + +#include "Data.h" +#include "PrivateKey.h" +#include "proto/Sui.pb.h" + +namespace TW::Sui { + +/// Helper class that performs Sui transaction signing. +class Signer { +public: + /// Hide default constructor + Signer() = delete; + + /// Signs a Proto::SigningInput transaction + static Proto::SigningOutput sign(const Proto::SigningInput& input) noexcept; +}; + +} // namespace TW::Sui diff --git a/src/proto/Sui.proto b/src/proto/Sui.proto new file mode 100644 index 00000000000..ae68ccf2367 --- /dev/null +++ b/src/proto/Sui.proto @@ -0,0 +1,34 @@ +// Copyright © 2017-2022 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +syntax = "proto3"; + +package TW.Sui.Proto; +option java_package = "wallet.core.jni.proto"; + +// Base64 encoded msg to sign (string) +message SignDirect { + // Obtain by calling any write RpcJson on SUI + string unsigned_tx_msg = 1; +} + +// Input data necessary to create a signed transaction. +message SigningInput { + // Private key to sign the transaction (bytes) + bytes private_key = 1; + + oneof transaction_payload { + SignDirect sign_direct_message = 2; + } +} + +// Transaction signing output. +message SigningOutput { + /// The raw transaction without indent in base64 + string unsigned_tx = 1; + /// The signature encoded in base64 + string signature = 2; +} diff --git a/swift/Tests/Blockchains/SuiTests.swift b/swift/Tests/Blockchains/SuiTests.swift new file mode 100644 index 00000000000..a8a64e8892c --- /dev/null +++ b/swift/Tests/Blockchains/SuiTests.swift @@ -0,0 +1,40 @@ +// Copyright © 2017-2022 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +import WalletCore +import XCTest + +class SuiTests: XCTestCase { + func testAddress() { + let anyAddress = AnyAddress(string: "0x061ce2b2100a71bb7aa0da98998887ad82597948", coin: .sui) + + XCTAssertEqual(anyAddress?.description, "0x061ce2b2100a71bb7aa0da98998887ad82597948") + XCTAssertEqual(anyAddress?.coin, .sui) + + let invalid = "MQqpqMQgCBuiPkoXfgZZsJvuzCeI1zc00z6vHJj4" + XCTAssertNil(Data(hexString: invalid)) + XCTAssertNil(AnyAddress(string: invalid, coin: .sui)) + XCTAssertFalse(AnyAddress.isValid(string: invalid, coin: .sui)) + } + + func testSign() { + // Successfully broadcasted https://explorer.sui.io/transaction/rxLgxcAqgMg8gphp6eCsSGQcdZnwFYx2SRdwEhnAUC4 + let privateKeyData = Data(hexString: "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")! + let txBytes = """ +AAUCLiNiMy/EzosKCk5EZr5QQZmMVLnvAAAAAAAAACDqj/OT+1+qyLZKV4YLw8kpK3/bTZKspTUmh1pBuUfHPLb0crwkV1LQcBARaxER8XhTNJmK7wAAAAAAAAAgaQEguOdXa+m16IM536nsveakQ4u/GYJAc1fpYGGKEvgBQUP35yxF+cEL5qm153kw18dVeuYB6AMAAAAAAAAttQCskZzd41GsNuNxHYMsbbl2aS4jYjMvxM6LCgpORGa+UEGZjFS57wAAAAAAAAAg6o/zk/tfqsi2SleGC8PJKSt/202SrKU1JodaQblHxzwBAAAAAAAAAOgDAAAAAAAA +""" + let input = SuiSigningInput.with { + $0.signDirectMessage = SuiSignDirect.with { + $0.unsignedTxMsg = txBytes + } + $0.privateKey = privateKeyData + } + let output: SuiSigningOutput = AnySigner.sign(input: input, coin: .sui) + XCTAssertEqual(output.unsignedTx, txBytes) + let expectedSignature = "AIYRmHDpQesfAx3iWBCMwInf3MZ56ZQGnPWNtECFjcSq0ssAgjRW6GLnFCX24tfDNjSm9gjYgoLmn1No15iFJAtqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg==" + XCTAssertEqual(output.signature, expectedSignature) + } +} diff --git a/swift/Tests/CoinAddressDerivationTests.swift b/swift/Tests/CoinAddressDerivationTests.swift index 1863b13d3b5..eced318a140 100644 --- a/swift/Tests/CoinAddressDerivationTests.swift +++ b/swift/Tests/CoinAddressDerivationTests.swift @@ -259,6 +259,9 @@ class CoinAddressDerivationTests: XCTestCase { case .aptos: let expectedResult = "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30"; assertCoinDerivation(coin, expectedResult, derivedAddress, address) + case .sui: + let expectedResult = "0x061ce2b2100a71bb7aa0da98998887ad82597948"; + assertCoinDerivation(coin, expectedResult, derivedAddress, address) case .hedera: let expectedResult = "0.0.302a300506032b657003210049eba62f64d0d941045595d9433e65d84ecc46bcdb1421de55e05fcf2d8357d5"; assertCoinDerivation(coin, expectedResult, derivedAddress, address) diff --git a/tests/chains/Sui/AddressTests.cpp b/tests/chains/Sui/AddressTests.cpp new file mode 100644 index 00000000000..c28c9c4253e --- /dev/null +++ b/tests/chains/Sui/AddressTests.cpp @@ -0,0 +1,53 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#include "HexCoding.h" +#include "Sui/Address.h" +#include "PublicKey.h" +#include "PrivateKey.h" +#include +#include + +namespace TW::Sui::tests { + +TEST(SuiAddress, Valid) { + ASSERT_TRUE(Address::isValid("0x1")); + // Address 20 are valid in SUI + ASSERT_TRUE(Address::isValid("0xb1dc06bd64d4e179a482b97bb68243f6c02c1b92")); + ASSERT_TRUE(Address::isValid("b1dc06bd64d4e179a482b97bb68243f6c02c1b92")); +} + +TEST(SuiAddress, Invalid) { + // Address 32 are invalid in SUI + ASSERT_FALSE(Address::isValid("0xeeff357ea5c1a4e7bc11b2b17ff2dc2dcca69750bfef1e1ebcaccf8c8018175b")); + ASSERT_FALSE(Address::isValid("eeff357ea5c1a4e7bc11b2b17ff2dc2dcca69750bfef1e1ebcaccf8c8018175b")); + ASSERT_FALSE(Address::isValid("19aadeca9388e009d136245b9a67423f3eee242b03142849eb4f81a4a409e59c")); + // Too long + ASSERT_FALSE(Address::isValid("b1dc06bd64d4e179a482b97bb68243f6c02c1b921")); + // Too short + ASSERT_FALSE(Address::isValid("b1dc06bd64d4e179a482b97bb68243f6c02c1b9")); + // Invalid Hex + ASSERT_FALSE(Address::isValid("0xS1dc06bd64d4e179a482b97bb68243f6c02c1b92")); +} + +TEST(SuiAddress, FromString) { + auto address = Address("b1dc06bd64d4e179a482b97bb68243f6c02c1b92"); + ASSERT_EQ(address.string(), "0xb1dc06bd64d4e179a482b97bb68243f6c02c1b92"); +} + +TEST(SuiAddress, FromPrivateKey) { + auto privateKey = PrivateKey(parse_hex("088baa019f081d6eab8dff5c447f9ce2f83c1babf3d03686299eaf6a1e89156e")); + auto address = Address(privateKey.getPublicKey(TWPublicKeyTypeED25519)); + ASSERT_EQ(address.string(), "0xb638d15fa81d301a9756259d0c7b2ca27a00a531"); +} + +TEST(SuiAddress, FromPublicKey) { + auto publicKey = PublicKey(parse_hex("ad0e293a56c9fc648d1872a00521d97e6b65724519a2676c2c47cb95d131cf5a"), TWPublicKeyTypeED25519); + auto address = Address(publicKey); + ASSERT_EQ(address.string(), "0xb638d15fa81d301a9756259d0c7b2ca27a00a531"); +} + +} // namespace TW::Sui::tests diff --git a/tests/chains/Sui/SignerTests.cpp b/tests/chains/Sui/SignerTests.cpp new file mode 100644 index 00000000000..29c640dfbff --- /dev/null +++ b/tests/chains/Sui/SignerTests.cpp @@ -0,0 +1,65 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#include "Sui/Signer.h" +#include "Sui/Address.h" +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" + +#include + +namespace TW::Sui::tests { + +TEST(SuiSigner, Transfer) { + // Successfully broadcasted https://explorer.sui.io/transaction/rxLgxcAqgMg8gphp6eCsSGQcdZnwFYx2SRdwEhnAUC4 + Proto::SigningInput input; + auto txMsg = "AAUCLiNiMy/EzosKCk5EZr5QQZmMVLnvAAAAAAAAACDqj/OT+1+qyLZKV4YLw8kpK3/bTZKspTUmh1pBuUfHPLb0crwkV1LQcBARaxER8XhTNJmK7wAAAAAAAAAgaQEguOdXa+m16IM536nsveakQ4u/GYJAc1fpYGGKEvgBQUP35yxF+cEL5qm153kw18dVeuYB6AMAAAAAAAAttQCskZzd41GsNuNxHYMsbbl2aS4jYjMvxM6LCgpORGa+UEGZjFS57wAAAAAAAAAg6o/zk/tfqsi2SleGC8PJKSt/202SrKU1JodaQblHxzwBAAAAAAAAAOgDAAAAAAAA"; + input.mutable_sign_direct_message()->set_unsigned_tx_msg(txMsg); + auto privateKey = PrivateKey(parse_hex("3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + auto result = Signer::sign(input); + ASSERT_EQ(result.unsigned_tx(), "AAUCLiNiMy/EzosKCk5EZr5QQZmMVLnvAAAAAAAAACDqj/OT+1+qyLZKV4YLw8kpK3/bTZKspTUmh1pBuUfHPLb0crwkV1LQcBARaxER8XhTNJmK7wAAAAAAAAAgaQEguOdXa+m16IM536nsveakQ4u/GYJAc1fpYGGKEvgBQUP35yxF+cEL5qm153kw18dVeuYB6AMAAAAAAAAttQCskZzd41GsNuNxHYMsbbl2aS4jYjMvxM6LCgpORGa+UEGZjFS57wAAAAAAAAAg6o/zk/tfqsi2SleGC8PJKSt/202SrKU1JodaQblHxzwBAAAAAAAAAOgDAAAAAAAA"); + ASSERT_EQ(result.signature(), "AIYRmHDpQesfAx3iWBCMwInf3MZ56ZQGnPWNtECFjcSq0ssAgjRW6GLnFCX24tfDNjSm9gjYgoLmn1No15iFJAtqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); +} + +TEST(SuiSigner, TransferNFT) { + // Successfully broadcasted https://explorer.sui.io/transaction/EmnhP9swuoijxYwHMywnXDGCXfFs1QxErsYoyWy9Y15J + Proto::SigningInput input; + std::string unsigned_tx = R"(AAAv0f6HrJCZ/1cuDVuxh1BL12XMeHxkKeZ7Js9grhcB0u8xtTvoOepOHAAAAAAAAAAgJvcpOSvKhM+tHPgGAnp5Pmc8l3wjZhVxK4/BrLu4YAgttQCskZzd41GsNuNxHYMsbbl2aSEnoKw8oAGf/LobCM7RxGurtPZtHAAAAAAAAAAgwk74iUAH9S+cGVXQxAydItvltZ3UK2L0vg1TYgDMPfABAAAAAAAAAOgDAAAAAAAA)"; + input.mutable_sign_direct_message()->set_unsigned_tx_msg(unsigned_tx); + auto privateKey = PrivateKey(parse_hex("3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + auto result = Signer::sign(input); + ASSERT_EQ(result.unsigned_tx(), unsigned_tx); + ASSERT_EQ(result.signature(), "AIjbyuyg9YX0f8/DXB5XZBnUCOqhUrPDbU9/E/FlzwGtDS57cOL/gZwN3vTV1KiOuN0cr0kxypgJpVLKlhd8hgdqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); +} + +TEST(SuiSigner, MoveCall) { + // Successfully broadcasted on: https://explorer.sui.io/transaction/3Gg8AcEfokDnA8m7W58ANmeCr8vkSaPWjXMp9sLMScTj + Proto::SigningInput input; + std::string unsigned_tx = R"(AAIAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAAAAAAAAINaXMihjlCd4CQVFRPjcNb7QfYP4wGgQyl1xbplvEKUCA3N1aQh0cmFuc2ZlcgACAQCdB6Mav5rHiXD0rAWTCxS+ENwxMBsAAAAAAAAAINqDfrJUZebPjUi7xcyR3QcQSA9tOLwxThgYaZ1vMfgfABQU2gJ3ToaOYd1F/R6mXryOZdvpRi21AKyRnN3jUaw243EdgyxtuXZppM+mSjYYEQWDcV/7hFRrAE0VtRwbAAAAAAAAACC5nJxYaYJfa9rfbxSikaEFVmHGuXyCIZoZbMpxMwLebAEAAAAAAAAA0AcAAAAAAAA=)"; + input.mutable_sign_direct_message()->set_unsigned_tx_msg(unsigned_tx); + auto privateKey = PrivateKey(parse_hex("3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + auto result = Signer::sign(input); + ASSERT_EQ(result.unsigned_tx(), unsigned_tx); + ASSERT_EQ(result.signature(), "AE8394w/+KOodhLjnKgu21iW0xZur6MA4ajPh31f2xaOI7vs6JHLAHLk5ED3bfJfc5ZehmC6D4DMyrH4F0dA3A1qfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); +} + +TEST(SuiSigner, AddDelegation) { + // Successfully broadcasted on: https://explorer.sui.io/transaction/3Gg8AcEfokDnA8m7W58ANmeCr8vkSaPWjXMp9sLMScTj + Proto::SigningInput input; + std::string unsigned_tx = R"(AAIAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAAAAAAAAIEt/p6rXSTjdKP6wJOXyx0c2xsgJ4MJtfxe7qHC34u4UCnN1aV9zeXN0ZW0fcmVxdWVzdF9hZGRfZGVsZWdhdGlvbl9tdWxfY29pbgAEAQEAAAAAAAAAAAAAAAAAAAAAAAAABQEAAAAAAAAAAgIAGSkMV9AFc419O9dL1kez9tzVIOiXzAEAAAAAACAfIePlHHP/+iv++FWQW9ofkVm4S2sFwupGikSq8bNjYwAH1A26NKDn7pJfn9zWaDi1nbntMJfMAQAAAAAAIKfMZAZktdmw36jwg/jcK1TDmrHmSZ/fdkeInO3BSjfWAAkB0AcAAAAAAAAAFAej1I8mhjmcpQTQtiX2J2HZ7y2xLbUArJGc3eNRrDbjcR2DLG25dmlY2RBfTU/P+GL1qpxE8NQrwiLw/JfMAQAAAAAAICs2NJlowCmCnpJ+hja2VwZE5K6yGM/qw0MSRnn9tW2cbgAAAAAAAACghgEAAAAAAA==)"; + input.mutable_sign_direct_message()->set_unsigned_tx_msg(unsigned_tx); + auto privateKey = PrivateKey(parse_hex("3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + auto result = Signer::sign(input); + ASSERT_EQ(result.unsigned_tx(), unsigned_tx); + ASSERT_EQ(result.signature(), "AKSbUoc+F4JGG9i+A8yVYyzcD8BXNV88iSaWpoS5KXUG7ao2pxjyvfUJEYyhWXTxgQazNDnIM1xGhD7zu1GU1wRqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); +} + +} // namespace TW::Sui::tests diff --git a/tests/chains/Sui/TWCoinTypeTests.cpp b/tests/chains/Sui/TWCoinTypeTests.cpp new file mode 100644 index 00000000000..9489907728d --- /dev/null +++ b/tests/chains/Sui/TWCoinTypeTests.cpp @@ -0,0 +1,33 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. +// +// This is a GENERATED FILE, changes made here MAY BE LOST. +// Generated one-time (codegen/bin/cointests) +// + +#include "TestUtilities.h" +#include +#include + + +TEST(TWSuiCoinType, TWCoinType) { + const auto coin = TWCoinTypeSui; + const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin)); + const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin)); + const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin)); + const auto txId = WRAPS(TWStringCreateWithUTF8Bytes("SWRW1RoMHxnD9NeobgBoC4cXGwp2Hc511CnfWUoTBmo")); + const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get())); + const auto accId = WRAPS(TWStringCreateWithUTF8Bytes("0x62107e1afefccc7b2267ab74e332c146f5c2ca15")); + const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get())); + + assertStringsEqual(id, "sui"); + assertStringsEqual(name, "Sui"); + assertStringsEqual(symbol, "SUI"); + ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), 9); + ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchainSui); + assertStringsEqual(txUrl, "https://explorer.sui.io//transaction/SWRW1RoMHxnD9NeobgBoC4cXGwp2Hc511CnfWUoTBmo"); + assertStringsEqual(accUrl, "https://explorer.sui.io//address/0x62107e1afefccc7b2267ab74e332c146f5c2ca15"); +} diff --git a/tests/common/Base64Tests.cpp b/tests/common/Base64Tests.cpp index 1d61102f7ff..339c4859175 100644 --- a/tests/common/Base64Tests.cpp +++ b/tests/common/Base64Tests.cpp @@ -46,6 +46,15 @@ TEST(Base64, decode) { EXPECT_EQ("11ff8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d291b", hex(decoded)); } + + +TEST(Base64, EncodeDecodeSui) { + auto v = "AAIAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAAAAAAAAINaXMihjlCd4CQVFRPjcNb7QfYP4wGgQyl1xbplvEKUCA3N1aQh0cmFuc2ZlcgACAQCDlY9/fBVEt0yclyDF8RrjSRBfRRsAAAAAAAAAIJttZrU/26Bim7ku4dwY8d3fdabngn0B6dY/hLKgb6+xABQv0f6HrJCZ/1cuDVuxh1BL12XMeC21AKyRnN3jUaw243EdgyxtuXZpG62iKzFvYdk6RMGXxnoWd8RcfwkUAQAAAAAAACDi9GYNIZ0FXpPPi+zdDUuzHfs6MDoxzPuXGPZJq8ZfOAEAAAAAAAAA0AcAAAAAAAA="; + auto decoded = decode(v); + auto encoded = encode(decoded); + ASSERT_EQ(encoded, v); +} + TEST(Base64, UrlFormat) { const std::string const1 = "11003faa8556289975ec991ac9994dfb613abec4ea000d5094e6379080f594e559b330b8"; diff --git a/tests/common/CoinAddressDerivationTests.cpp b/tests/common/CoinAddressDerivationTests.cpp index 34b27fce10e..7f54228c9a1 100644 --- a/tests/common/CoinAddressDerivationTests.cpp +++ b/tests/common/CoinAddressDerivationTests.cpp @@ -256,6 +256,9 @@ TEST(Coin, DeriveAddress) { case TWCoinTypeAptos: EXPECT_EQ(address, "0xce2fd04ac9efa74f17595e5785e847a2399d7e637f5e8179244f76191f653276"); break; + case TWCoinTypeSui: + EXPECT_EQ(address, "0xfc93395679dec6ca84d9766be2014b6bc1473f2e"); + break; case TWCoinTypeHedera: EXPECT_EQ(address, "0.0.302a300506032b6570032100ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e67148"); break; diff --git a/tests/common/HDWallet/HDWalletTests.cpp b/tests/common/HDWallet/HDWalletTests.cpp index c5f858bbc04..44441d5b113 100644 --- a/tests/common/HDWallet/HDWalletTests.cpp +++ b/tests/common/HDWallet/HDWalletTests.cpp @@ -8,6 +8,7 @@ #include "Bitcoin/Address.h" #include "Bitcoin/CashAddress.h" #include "Bitcoin/SegwitAddress.h" +#include "Sui/Address.h" #include "Coin.h" #include "Ethereum/Address.h" #include "Ethereum/EIP2645.h" @@ -440,6 +441,18 @@ TEST(HDWallet, AptosKey) { } } +TEST(HDWallet, SuiKey) { + const auto derivPath = "m/44'/784'/0'/0'/0'"; + HDWallet wallet = HDWallet("cost add execute system fault long raccoon stone paddle column ketchup smile debate wood marble please jar can goddess magnet axis celery rough gold", ""); + { + const auto privateKey = wallet.getKey(TWCoinTypeSui, DerivationPath(derivPath)); + EXPECT_EQ(hex(privateKey.bytes), "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266"); + auto pubkey = privateKey.getPublicKey(TWPublicKeyTypeED25519); + EXPECT_EQ(hex(pubkey.bytes), "6a7cdeec16a75c0ff6787bc2356109469033022bb10e826c9d443a9f1fc0bd8e"); + EXPECT_EQ(TW::Sui::Address(pubkey).string(), "0x2db500ac919cdde351ac36e3711d832c6db97669"); + } +} + TEST(HDWallet, HederaKey) { // https://github.com/hashgraph/hedera-sdk-js/blob/e0cd39c84ab189d59a6bcedcf16e4102d7bb8beb/packages/cryptography/test/unit/Mnemonic.js#L47 { diff --git a/tests/common/rust/bindgen/WalletCoreRsTests.cpp b/tests/common/rust/bindgen/WalletCoreRsTests.cpp index ce99263e0c1..4293863ec56 100644 --- a/tests/common/rust/bindgen/WalletCoreRsTests.cpp +++ b/tests/common/rust/bindgen/WalletCoreRsTests.cpp @@ -5,7 +5,6 @@ // file LICENSE at the root of the source code distribution tree. #include "rust/bindgen/WalletCoreRSBindgen.h" - #include "gtest/gtest.h" TEST(RustBindgen, MoveParseFunctionArgument) { diff --git a/tools/install-wasm-dependencies b/tools/install-wasm-dependencies index dcf17caa686..31fc852b850 100755 --- a/tools/install-wasm-dependencies +++ b/tools/install-wasm-dependencies @@ -2,7 +2,7 @@ set -e -emsdk_version=3.1.22 +emsdk_version=3.1.30 git clone https://github.com/emscripten-core/emsdk.git diff --git a/walletconsole/lib/Util.cpp b/walletconsole/lib/Util.cpp index 86dd938dc84..a0312d05261 100644 --- a/walletconsole/lib/Util.cpp +++ b/walletconsole/lib/Util.cpp @@ -38,14 +38,13 @@ bool Util::base64Encode(const string& p, string& res) { } bool Util::base64Decode(const string& p, string& res) { - try { - auto dec = Base64::decode(p); - res = TW::hex(dec); - return true; - } catch (exception& ex) { + auto dec = Base64::decode(p); + if (dec.empty()) { _out << "Error while Base64 decode" << endl; return false; } + res = TW::hex(dec); + return true; } bool Util::fileW(const string& fileName, const string& data, [[maybe_unused]] string& res) { diff --git a/wasm/CMakeLists.txt b/wasm/CMakeLists.txt index e7ea9f4ca0e..592e636d94c 100644 --- a/wasm/CMakeLists.txt +++ b/wasm/CMakeLists.txt @@ -40,5 +40,5 @@ set_target_properties(${TARGET_NAME} set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_FLAGS "-O2 -sSTRICT -sUSE_BOOST_HEADERS=1" - LINK_FLAGS "--bind --no-entry --closure 1 -O2 -sSTRICT -sASSERTIONS -sMODULARIZE=1 -sALLOW_MEMORY_GROWTH=1 -sDYNAMIC_EXECUTION=0" + LINK_FLAGS "--bind --no-entry --closure 1 -O2 -sSTRICT -sASSERTIONS -sMODULARIZE=1 -sALLOW_MEMORY_GROWTH=1 -sDYNAMIC_EXECUTION=0 -s EXPORTED_FUNCTIONS=['_setThrew']" ) diff --git a/wasm/tests/Blockchain/Sui.test.ts b/wasm/tests/Blockchain/Sui.test.ts new file mode 100644 index 00000000000..49baaea6c84 --- /dev/null +++ b/wasm/tests/Blockchain/Sui.test.ts @@ -0,0 +1,28 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +import "mocha"; +import { assert } from "chai"; +import { Buffer } from "buffer"; +import { TW } from "../../dist"; + +describe("Sui", () => { + it("test sign Sui", () => { + const { PrivateKey, HexCoding, AnySigner, AnyAddress, CoinType } = globalThis.core; + const txDataInput = TW.Sui.Proto.SigningInput.create({ + signDirectMessage: TW.Sui.Proto.SignDirect.create({ + unsignedTxMsg: "AAUCLiNiMy/EzosKCk5EZr5QQZmMVLnvAAAAAAAAACDqj/OT+1+qyLZKV4YLw8kpK3/bTZKspTUmh1pBuUfHPLb0crwkV1LQcBARaxER8XhTNJmK7wAAAAAAAAAgaQEguOdXa+m16IM536nsveakQ4u/GYJAc1fpYGGKEvgBQUP35yxF+cEL5qm153kw18dVeuYB6AMAAAAAAAAttQCskZzd41GsNuNxHYMsbbl2aS4jYjMvxM6LCgpORGa+UEGZjFS57wAAAAAAAAAg6o/zk/tfqsi2SleGC8PJKSt/202SrKU1JodaQblHxzwBAAAAAAAAAOgDAAAAAAAA" + }), + privateKey: HexCoding.decode( + "0x3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266", + ) + }); + const input = TW.Sui.Proto.SigningInput.encode(txDataInput).finish(); + const outputData = AnySigner.sign(input, CoinType.sui); + const output = TW.Sui.Proto.SigningOutput.decode(outputData); + assert.equal(output.signature, "AIYRmHDpQesfAx3iWBCMwInf3MZ56ZQGnPWNtECFjcSq0ssAgjRW6GLnFCX24tfDNjSm9gjYgoLmn1No15iFJAtqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg==") + }); +});