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 index 0a37390cdd5..84ab0914391 100644 --- 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 @@ -22,7 +22,7 @@ class TestSuiSigner { } @Test - fun SuiTransactionSigning() { + fun testSuiDirectSigning() { // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet val txBytes = """ AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA @@ -37,4 +37,37 @@ class TestSuiSigner { assertEquals(result.unsignedTx, txBytes); assertEquals(result.signature, expectedSignature) } + + @Test + fun testSuiTransfer() { + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + val txBytes = """ + AAAEAAjoAwAAAAAAAAAIUMMAAAAAAAAAIKcXWr3V7ZLr4605DbNmxqcGR4zfUXzebPmGMAZc2jd6ACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgMCAAIBAAABAQABAQMAAAAAAQIAAQEDAAABAAEDAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayAWNgILOn3HsRw6pvQZsX+KnBLn95ox0b3S3mcLTt1jAFeHEaBQAAAAAgGGuNnxrqusosgjP3gQ3jBjnhapGNBlcU0yTaupXpa0BU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAwMYtAAAAAAAA + """.trimIndent() + val key = + "7e6682f7bf479ef0f627823cffd4e1a940a7af33e5fb39d9e0f631d2ecc5daff".toHexBytesInByteString() + + val paySui = Sui.PaySui.newBuilder() + .addInputCoins(Sui.ObjectRef.newBuilder().apply { + objectId = "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005" + version = 85619064 + objectDigest = "2eKuWbZSVfpFVfg8FXY9wP6W5AFXnTchSoUdp7obyYZ5" + }) + .addRecipients("0xa7175abdd5ed92ebe3ad390db366c6a706478cdf517cde6cf98630065cda377a") + .addRecipients("0x54e80d76d790c277f5a44f3ce92f53d26f5894892bf395dee6375988876be6b2") + .addAmounts(1000) + .addAmounts(50000) + + val signingInput = Sui.SigningInput.newBuilder() + .setPaySui(paySui) + .setPrivateKey(key) + .setGasBudget(3000000) + .setReferenceGasPrice(750) + .build() + + val result = AnySigner.sign(signingInput, CoinType.SUI, Sui.SigningOutput.parser()) + val expectedSignature = "AEh44B7iGArEHF1wOLAQJMLNgGnaIwn3gKPC92vtDJqITDETAM5z9plaxio1xomt6/cZReQ5FZaQsMC6l7E0BwmF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g==" + assertEquals(result.unsignedTx, txBytes); + assertEquals(result.signature, expectedSignature) + } } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 231e9ff182b..3d7c0c29603 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1754,6 +1754,7 @@ dependencies = [ "tw_native_injective", "tw_ronin", "tw_solana", + "tw_sui", "tw_thorchain", ] @@ -2005,6 +2006,22 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_sui" +version = "0.1.0" +dependencies = [ + "indexmap", + "move-core-types", + "serde", + "serde_repr", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "tw_thorchain" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index bcd400d3e1f..183f29e20a6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,30 +1,31 @@ [workspace] members = [ + "chains/tw_aptos", "chains/tw_binance", "chains/tw_cosmos", + "chains/tw_ethereum", + "chains/tw_internet_computer", "chains/tw_greenfield", "chains/tw_native_evmos", "chains/tw_native_injective", + "chains/tw_ronin", "chains/tw_solana", + "chains/tw_sui", "chains/tw_thorchain", "tw_any_coin", - "tw_aptos", "tw_bech32_address", "tw_bitcoin", "tw_coin_entry", "tw_coin_registry", "tw_cosmos_sdk", "tw_encoding", - "tw_ethereum", "tw_evm", "tw_hash", - "tw_internet_computer", "tw_keypair", "tw_memory", "tw_misc", "tw_number", "tw_proto", - "tw_ronin", "tw_utxo", "wallet_core_rs", ] diff --git a/rust/chains/tw_aptos/Cargo.toml b/rust/chains/tw_aptos/Cargo.toml new file mode 100644 index 00000000000..d78b0e3c644 --- /dev/null +++ b/rust/chains/tw_aptos/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tw_aptos" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde_json = "1.0" +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_keypair = { path = "../../tw_keypair" } +tw_proto = { path = "../../tw_proto" } +tw_number = { path = "../../tw_number" } +tw_hash = { path = "../../tw_hash" } +tw_memory = { path = "../../tw_memory" } +move-core-types = { git = "https://github.com/move-language/move", rev = "ea70797099baea64f05194a918cebd69ed02b285", features = ["address32"] } +serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11.12" + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/tw_aptos/fuzz/.gitignore b/rust/chains/tw_aptos/fuzz/.gitignore similarity index 100% rename from rust/tw_aptos/fuzz/.gitignore rename to rust/chains/tw_aptos/fuzz/.gitignore diff --git a/rust/tw_aptos/fuzz/Cargo.toml b/rust/chains/tw_aptos/fuzz/Cargo.toml similarity index 100% rename from rust/tw_aptos/fuzz/Cargo.toml rename to rust/chains/tw_aptos/fuzz/Cargo.toml diff --git a/rust/tw_aptos/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_aptos/fuzz/fuzz_targets/sign.rs similarity index 100% rename from rust/tw_aptos/fuzz/fuzz_targets/sign.rs rename to rust/chains/tw_aptos/fuzz/fuzz_targets/sign.rs diff --git a/rust/tw_aptos/src/address.rs b/rust/chains/tw_aptos/src/address.rs similarity index 87% rename from rust/tw_aptos/src/address.rs rename to rust/chains/tw_aptos/src/address.rs index 619ae3d4317..520166c9798 100644 --- a/rust/tw_aptos/src/address.rs +++ b/rust/chains/tw_aptos/src/address.rs @@ -6,26 +6,11 @@ use move_core_types::account_address::{AccountAddress, AccountAddressParseError} use std::fmt::{Display, Formatter}; use std::str::FromStr; use tw_coin_entry::coin_entry::CoinAddress; -use tw_coin_entry::error::{AddressError, AddressResult}; +use tw_coin_entry::error::AddressError; use tw_hash::sha3::sha3_256; use tw_keypair::ed25519; use tw_memory::Data; -pub trait AptosAddress: FromStr + Into
{ - /// Tries to parse an address from the string representation. - /// Returns `Ok(None)` if the given `s` string is empty. - #[inline] - fn from_str_optional(s: &str) -> AddressResult> { - if s.is_empty() { - return Ok(None); - } - - Self::from_str(s).map(Some) - } -} - -impl AptosAddress for Address {} - #[repr(u8)] pub enum Scheme { Ed25519 = 0, @@ -38,8 +23,8 @@ pub struct Address { impl Address { pub const LENGTH: usize = AccountAddress::LENGTH; - /// Initializes an address with a `ed25519` public key. + /// Initializes an address with a `ed25519` public key. pub fn with_ed25519_pubkey( pubkey: &ed25519::sha512::PublicKey, ) -> Result { diff --git a/rust/tw_aptos/src/aptos_move_packages.rs b/rust/chains/tw_aptos/src/aptos_move_packages.rs similarity index 100% rename from rust/tw_aptos/src/aptos_move_packages.rs rename to rust/chains/tw_aptos/src/aptos_move_packages.rs diff --git a/rust/tw_aptos/src/bcs_encoding.rs b/rust/chains/tw_aptos/src/bcs_encoding.rs similarity index 100% rename from rust/tw_aptos/src/bcs_encoding.rs rename to rust/chains/tw_aptos/src/bcs_encoding.rs diff --git a/rust/tw_aptos/src/compiler.rs b/rust/chains/tw_aptos/src/compiler.rs similarity index 100% rename from rust/tw_aptos/src/compiler.rs rename to rust/chains/tw_aptos/src/compiler.rs diff --git a/rust/tw_aptos/src/constants.rs b/rust/chains/tw_aptos/src/constants.rs similarity index 100% rename from rust/tw_aptos/src/constants.rs rename to rust/chains/tw_aptos/src/constants.rs diff --git a/rust/tw_aptos/src/entry.rs b/rust/chains/tw_aptos/src/entry.rs similarity index 100% rename from rust/tw_aptos/src/entry.rs rename to rust/chains/tw_aptos/src/entry.rs diff --git a/rust/tw_aptos/src/lib.rs b/rust/chains/tw_aptos/src/lib.rs similarity index 100% rename from rust/tw_aptos/src/lib.rs rename to rust/chains/tw_aptos/src/lib.rs diff --git a/rust/tw_aptos/src/liquid_staking.rs b/rust/chains/tw_aptos/src/liquid_staking.rs similarity index 100% rename from rust/tw_aptos/src/liquid_staking.rs rename to rust/chains/tw_aptos/src/liquid_staking.rs diff --git a/rust/tw_aptos/src/nft.rs b/rust/chains/tw_aptos/src/nft.rs similarity index 100% rename from rust/tw_aptos/src/nft.rs rename to rust/chains/tw_aptos/src/nft.rs diff --git a/rust/tw_aptos/src/serde_helper/mod.rs b/rust/chains/tw_aptos/src/serde_helper/mod.rs similarity index 100% rename from rust/tw_aptos/src/serde_helper/mod.rs rename to rust/chains/tw_aptos/src/serde_helper/mod.rs diff --git a/rust/tw_aptos/src/serde_helper/vec_bytes.rs b/rust/chains/tw_aptos/src/serde_helper/vec_bytes.rs similarity index 100% rename from rust/tw_aptos/src/serde_helper/vec_bytes.rs rename to rust/chains/tw_aptos/src/serde_helper/vec_bytes.rs diff --git a/rust/tw_aptos/src/signer.rs b/rust/chains/tw_aptos/src/signer.rs similarity index 100% rename from rust/tw_aptos/src/signer.rs rename to rust/chains/tw_aptos/src/signer.rs diff --git a/rust/tw_aptos/src/transaction.rs b/rust/chains/tw_aptos/src/transaction.rs similarity index 100% rename from rust/tw_aptos/src/transaction.rs rename to rust/chains/tw_aptos/src/transaction.rs diff --git a/rust/tw_aptos/src/transaction_builder.rs b/rust/chains/tw_aptos/src/transaction_builder.rs similarity index 100% rename from rust/tw_aptos/src/transaction_builder.rs rename to rust/chains/tw_aptos/src/transaction_builder.rs diff --git a/rust/tw_aptos/src/transaction_payload.rs b/rust/chains/tw_aptos/src/transaction_payload.rs similarity index 100% rename from rust/tw_aptos/src/transaction_payload.rs rename to rust/chains/tw_aptos/src/transaction_payload.rs diff --git a/rust/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs similarity index 100% rename from rust/tw_aptos/tests/signer.rs rename to rust/chains/tw_aptos/tests/signer.rs diff --git a/rust/chains/tw_binance/src/transaction/message/htlt_order.rs b/rust/chains/tw_binance/src/transaction/message/htlt_order.rs index f677a0dfc7e..e55e3e4b3e5 100644 --- a/rust/chains/tw_binance/src/transaction/message/htlt_order.rs +++ b/rust/chains/tw_binance/src/transaction/message/htlt_order.rs @@ -20,7 +20,7 @@ pub struct HTLTOrder { pub expected_income: String, pub from: BinanceAddress, pub height_span: i64, - #[serde(serialize_with = "as_hex")] + #[serde(with = "as_hex")] pub random_number_hash: Data, pub recipient_other_chain: String, pub sender_other_chain: String, @@ -84,7 +84,7 @@ impl TWBinanceProto for HTLTOrder { pub struct DepositHTLTOrder { pub amount: Vec, pub from: BinanceAddress, - #[serde(serialize_with = "as_hex")] + #[serde(with = "as_hex")] pub swap_id: Data, } @@ -127,9 +127,9 @@ impl TWBinanceProto for DepositHTLTOrder { #[derive(Deserialize, Serialize)] pub struct ClaimHTLTOrder { pub from: BinanceAddress, - #[serde(serialize_with = "as_hex")] + #[serde(with = "as_hex")] pub random_number: Data, - #[serde(serialize_with = "as_hex")] + #[serde(with = "as_hex")] pub swap_id: Data, } @@ -171,7 +171,7 @@ impl TWBinanceProto for ClaimHTLTOrder { #[derive(Deserialize, Serialize)] pub struct RefundHTLTOrder { pub from: BinanceAddress, - #[serde(serialize_with = "as_hex")] + #[serde(with = "as_hex")] pub swap_id: Data, } diff --git a/rust/chains/tw_ethereum/Cargo.toml b/rust/chains/tw_ethereum/Cargo.toml new file mode 100644 index 00000000000..6a3cbe0ebd8 --- /dev/null +++ b/rust/chains/tw_ethereum/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_ethereum" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_evm = { path = "../../tw_evm" } +tw_keypair = { path = "../../tw_keypair" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/tw_ethereum/src/entry.rs b/rust/chains/tw_ethereum/src/entry.rs similarity index 100% rename from rust/tw_ethereum/src/entry.rs rename to rust/chains/tw_ethereum/src/entry.rs diff --git a/rust/tw_ethereum/src/lib.rs b/rust/chains/tw_ethereum/src/lib.rs similarity index 100% rename from rust/tw_ethereum/src/lib.rs rename to rust/chains/tw_ethereum/src/lib.rs diff --git a/rust/tw_ethereum/tests/compiler.rs b/rust/chains/tw_ethereum/tests/compiler.rs similarity index 100% rename from rust/tw_ethereum/tests/compiler.rs rename to rust/chains/tw_ethereum/tests/compiler.rs diff --git a/rust/tw_ethereum/tests/signer.rs b/rust/chains/tw_ethereum/tests/signer.rs similarity index 100% rename from rust/tw_ethereum/tests/signer.rs rename to rust/chains/tw_ethereum/tests/signer.rs diff --git a/rust/chains/tw_internet_computer/Cargo.toml b/rust/chains/tw_internet_computer/Cargo.toml new file mode 100644 index 00000000000..fda55e71f69 --- /dev/null +++ b/rust/chains/tw_internet_computer/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tw_internet_computer" +version = "0.1.0" +edition = "2021" + +[dependencies] +quick-protobuf = "0.8.1" +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" } + +[build-dependencies] +pb-rs = "0.10.0" diff --git a/rust/tw_internet_computer/build.rs b/rust/chains/tw_internet_computer/build.rs similarity index 100% rename from rust/tw_internet_computer/build.rs rename to rust/chains/tw_internet_computer/build.rs diff --git a/rust/tw_internet_computer/fuzz/.gitignore b/rust/chains/tw_internet_computer/fuzz/.gitignore similarity index 100% rename from rust/tw_internet_computer/fuzz/.gitignore rename to rust/chains/tw_internet_computer/fuzz/.gitignore diff --git a/rust/tw_internet_computer/fuzz/Cargo.lock b/rust/chains/tw_internet_computer/fuzz/Cargo.lock similarity index 100% rename from rust/tw_internet_computer/fuzz/Cargo.lock rename to rust/chains/tw_internet_computer/fuzz/Cargo.lock diff --git a/rust/tw_internet_computer/fuzz/Cargo.toml b/rust/chains/tw_internet_computer/fuzz/Cargo.toml similarity index 100% rename from rust/tw_internet_computer/fuzz/Cargo.toml rename to rust/chains/tw_internet_computer/fuzz/Cargo.toml diff --git a/rust/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs b/rust/chains/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs similarity index 100% rename from rust/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs rename to rust/chains/tw_internet_computer/fuzz/fuzz_targets/tw_internet_computer_transfer.rs diff --git a/rust/tw_internet_computer/src/address.rs b/rust/chains/tw_internet_computer/src/address.rs similarity index 100% rename from rust/tw_internet_computer/src/address.rs rename to rust/chains/tw_internet_computer/src/address.rs diff --git a/rust/tw_internet_computer/src/context.rs b/rust/chains/tw_internet_computer/src/context.rs similarity index 100% rename from rust/tw_internet_computer/src/context.rs rename to rust/chains/tw_internet_computer/src/context.rs diff --git a/rust/tw_internet_computer/src/entry.rs b/rust/chains/tw_internet_computer/src/entry.rs similarity index 100% rename from rust/tw_internet_computer/src/entry.rs rename to rust/chains/tw_internet_computer/src/entry.rs diff --git a/rust/tw_internet_computer/src/lib.rs b/rust/chains/tw_internet_computer/src/lib.rs similarity index 100% rename from rust/tw_internet_computer/src/lib.rs rename to rust/chains/tw_internet_computer/src/lib.rs diff --git a/rust/tw_internet_computer/src/protocol/envelope.rs b/rust/chains/tw_internet_computer/src/protocol/envelope.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/envelope.rs rename to rust/chains/tw_internet_computer/src/protocol/envelope.rs diff --git a/rust/tw_internet_computer/src/protocol/identity.rs b/rust/chains/tw_internet_computer/src/protocol/identity.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/identity.rs rename to rust/chains/tw_internet_computer/src/protocol/identity.rs diff --git a/rust/tw_internet_computer/src/protocol/mod.rs b/rust/chains/tw_internet_computer/src/protocol/mod.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/mod.rs rename to rust/chains/tw_internet_computer/src/protocol/mod.rs diff --git a/rust/tw_internet_computer/src/protocol/principal.rs b/rust/chains/tw_internet_computer/src/protocol/principal.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/principal.rs rename to rust/chains/tw_internet_computer/src/protocol/principal.rs diff --git a/rust/tw_internet_computer/src/protocol/request_id.rs b/rust/chains/tw_internet_computer/src/protocol/request_id.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/request_id.rs rename to rust/chains/tw_internet_computer/src/protocol/request_id.rs diff --git a/rust/tw_internet_computer/src/protocol/rosetta.rs b/rust/chains/tw_internet_computer/src/protocol/rosetta.rs similarity index 100% rename from rust/tw_internet_computer/src/protocol/rosetta.rs rename to rust/chains/tw_internet_computer/src/protocol/rosetta.rs diff --git a/rust/tw_internet_computer/src/signer.rs b/rust/chains/tw_internet_computer/src/signer.rs similarity index 100% rename from rust/tw_internet_computer/src/signer.rs rename to rust/chains/tw_internet_computer/src/signer.rs diff --git a/rust/tw_internet_computer/src/transactions/mod.rs b/rust/chains/tw_internet_computer/src/transactions/mod.rs similarity index 100% rename from rust/tw_internet_computer/src/transactions/mod.rs rename to rust/chains/tw_internet_computer/src/transactions/mod.rs diff --git a/rust/tw_internet_computer/src/transactions/proto/ledger.proto b/rust/chains/tw_internet_computer/src/transactions/proto/ledger.proto similarity index 100% rename from rust/tw_internet_computer/src/transactions/proto/ledger.proto rename to rust/chains/tw_internet_computer/src/transactions/proto/ledger.proto diff --git a/rust/tw_internet_computer/src/transactions/proto/types.proto b/rust/chains/tw_internet_computer/src/transactions/proto/types.proto similarity index 100% rename from rust/tw_internet_computer/src/transactions/proto/types.proto rename to rust/chains/tw_internet_computer/src/transactions/proto/types.proto diff --git a/rust/tw_internet_computer/src/transactions/transfer.rs b/rust/chains/tw_internet_computer/src/transactions/transfer.rs similarity index 100% rename from rust/tw_internet_computer/src/transactions/transfer.rs rename to rust/chains/tw_internet_computer/src/transactions/transfer.rs diff --git a/rust/chains/tw_ronin/Cargo.toml b/rust/chains/tw_ronin/Cargo.toml new file mode 100644 index 00000000000..fadcf48ebab --- /dev/null +++ b/rust/chains/tw_ronin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_ronin" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_evm = { path = "../../tw_evm" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_number = { path = "../../tw_number", features = ["helpers"] } diff --git a/rust/tw_ronin/src/address.rs b/rust/chains/tw_ronin/src/address.rs similarity index 100% rename from rust/tw_ronin/src/address.rs rename to rust/chains/tw_ronin/src/address.rs diff --git a/rust/tw_ronin/src/entry.rs b/rust/chains/tw_ronin/src/entry.rs similarity index 100% rename from rust/tw_ronin/src/entry.rs rename to rust/chains/tw_ronin/src/entry.rs diff --git a/rust/tw_ronin/src/lib.rs b/rust/chains/tw_ronin/src/lib.rs similarity index 100% rename from rust/tw_ronin/src/lib.rs rename to rust/chains/tw_ronin/src/lib.rs diff --git a/rust/tw_ronin/src/ronin_context.rs b/rust/chains/tw_ronin/src/ronin_context.rs similarity index 100% rename from rust/tw_ronin/src/ronin_context.rs rename to rust/chains/tw_ronin/src/ronin_context.rs diff --git a/rust/tw_ronin/tests/address.rs b/rust/chains/tw_ronin/tests/address.rs similarity index 100% rename from rust/tw_ronin/tests/address.rs rename to rust/chains/tw_ronin/tests/address.rs diff --git a/rust/tw_ronin/tests/compiler.rs b/rust/chains/tw_ronin/tests/compiler.rs similarity index 100% rename from rust/tw_ronin/tests/compiler.rs rename to rust/chains/tw_ronin/tests/compiler.rs diff --git a/rust/tw_ronin/tests/rlp.rs b/rust/chains/tw_ronin/tests/rlp.rs similarity index 100% rename from rust/tw_ronin/tests/rlp.rs rename to rust/chains/tw_ronin/tests/rlp.rs diff --git a/rust/tw_ronin/tests/signer.rs b/rust/chains/tw_ronin/tests/signer.rs similarity index 100% rename from rust/tw_ronin/tests/signer.rs rename to rust/chains/tw_ronin/tests/signer.rs diff --git a/rust/chains/tw_sui/Cargo.toml b/rust/chains/tw_sui/Cargo.toml new file mode 100644 index 00000000000..1aa3d7cabb3 --- /dev/null +++ b/rust/chains/tw_sui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_sui" +version = "0.1.0" +edition = "2021" + +[dependencies] +indexmap = "2.0" +move-core-types = { git = "https://github.com/move-language/move", rev = "ea70797099baea64f05194a918cebd69ed02b285", features = ["address32"] } +serde = { version = "1.0", features = ["derive"] } +serde_repr = "0.1" +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" } diff --git a/rust/chains/tw_sui/fuzz/.gitignore b/rust/chains/tw_sui/fuzz/.gitignore new file mode 100644 index 00000000000..5c404b9583f --- /dev/null +++ b/rust/chains/tw_sui/fuzz/.gitignore @@ -0,0 +1,5 @@ +target +corpus +artifacts +coverage +Cargo.lock diff --git a/rust/chains/tw_sui/fuzz/Cargo.toml b/rust/chains/tw_sui/fuzz/Cargo.toml new file mode 100644 index 00000000000..32ce151dfaa --- /dev/null +++ b/rust/chains/tw_sui/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tw_sui-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tw_any_coin = { path = "../../../tw_any_coin", features = ["test-utils"] } +tw_coin_registry = { path = "../../../tw_coin_registry" } +tw_proto = { path = "../../../tw_proto", features = ["fuzz"] } + +[dependencies.tw_sui] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sign" +path = "fuzz_targets/sign.rs" +test = false +doc = false diff --git a/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs b/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs new file mode 100644 index 00000000000..bf0158d275f --- /dev/null +++ b/rust/chains/tw_sui/fuzz/fuzz_targets/sign.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_proto::Sui::Proto; + +fuzz_target!(|input: Proto::SigningInput<'_>| { + let mut signer = AnySignerHelper::::default(); + let _ = signer.sign(CoinType::Sui, input); +}); diff --git a/rust/chains/tw_sui/src/address.rs b/rust/chains/tw_sui/src/address.rs new file mode 100644 index 00000000000..76c359561ec --- /dev/null +++ b/rust/chains/tw_sui/src/address.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use move_core_types::account_address::AccountAddress; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::{AddressError, AddressResult}; +use tw_encoding::hex; +use tw_hash::blake2::blake2_b; +use tw_keypair::ed25519; +use tw_memory::Data; + +#[repr(u8)] +pub enum Scheme { + Ed25519 = 0, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct SuiAddress(AccountAddress); + +impl SuiAddress { + pub const LENGTH: usize = AccountAddress::LENGTH; + + /// Initializes an address with a `ed25519` public key. + pub fn with_ed25519_pubkey(pubkey: &ed25519::sha512::PublicKey) -> AddressResult { + const CAPACITY: usize = ed25519::sha512::PublicKey::LEN + 1; + + let mut to_hash = Vec::with_capacity(CAPACITY); + to_hash.push(Scheme::Ed25519 as u8); + to_hash.extend_from_slice(pubkey.as_slice()); + let hashed = + blake2_b(to_hash.as_slice(), SuiAddress::LENGTH).map_err(|_| AddressError::Internal)?; + + AccountAddress::from_bytes(hashed) + .map(SuiAddress) + .map_err(|_| AddressError::Internal) + } + + pub fn into_inner(self) -> AccountAddress { + self.0 + } +} + +impl FromStr for SuiAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s).map_err(|_| AddressError::FromHexError)?; + AccountAddress::from_bytes(bytes) + .map(SuiAddress) + .map_err(|_| AddressError::InvalidInput) + } +} + +impl fmt::Display for SuiAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_hex_literal()) + } +} + +impl CoinAddress for SuiAddress { + #[inline] + fn data(&self) -> Data { + self.0.to_vec() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_keypair::ed25519::sha512::PrivateKey; + + #[test] + fn test_from_public_key() { + let private = PrivateKey::try_from( + "088baa019f081d6eab8dff5c447f9ce2f83c1babf3d03686299eaf6a1e89156e", + ) + .unwrap(); + let public = private.public(); + let addr = SuiAddress::with_ed25519_pubkey(&public).unwrap(); + assert_eq!( + addr.to_string(), + "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015" + ); + } +} diff --git a/rust/chains/tw_sui/src/compiler.rs b/rust/chains/tw_sui/src/compiler.rs new file mode 100644 index 00000000000..adff5bd13d6 --- /dev/null +++ b/rust/chains/tw_sui/src/compiler.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::{TWTransaction, TWTransactionBuilder}; +use crate::modules::tx_signer::{TransactionPreimage, TxSigner}; +use crate::signature::SuiSignatureInfo; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::common::compile_input::SingleSignaturePubkey; +use tw_coin_entry::error::SigningResult; +use tw_coin_entry::signing_output_error; +use tw_encoding::base64; +use tw_keypair::ed25519; +use tw_proto::Sui::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SuiCompiler; + +impl SuiCompiler { + #[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> { + let builder = TWTransactionBuilder::new(input); + let tx_to_sign = builder.build()?; + + let TransactionPreimage { + tx_data_to_sign, + tx_hash_to_sign, + .. + } = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::preimage(&tx)?, + TWTransaction::SignDirect(tx_data) => TxSigner::preimage_direct(tx_data)?, + }; + + Ok(CompilerProto::PreSigningOutput { + data: Cow::from(tx_data_to_sign), + data_hash: Cow::from(tx_hash_to_sign.to_vec()), + ..CompilerProto::PreSigningOutput::default() + }) + } + + #[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> { + let builder = TWTransactionBuilder::new(input); + let tx_to_sign = builder.build()?; + + let TransactionPreimage { + unsigned_tx_data, .. + } = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::preimage(&tx), + TWTransaction::SignDirect(tx_data) => TxSigner::preimage_direct(tx_data), + }?; + + let SingleSignaturePubkey { + signature: raw_signature, + public_key: public_key_bytes, + } = SingleSignaturePubkey::from_sign_pubkey_list(signatures, public_keys)?; + + let signature = ed25519::Signature::try_from(raw_signature.as_slice())?; + let public_key = ed25519::sha512::PublicKey::try_from(public_key_bytes.as_slice())?; + + let signature_info = SuiSignatureInfo::ed25519(&signature, &public_key); + + let is_url = false; + let unsigned_tx = base64::encode(&unsigned_tx_data, is_url); + Ok(Proto::SigningOutput { + unsigned_tx: Cow::from(unsigned_tx), + signature: Cow::from(signature_info.to_base64()), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_sui/src/constants.rs b/rust/chains/tw_sui/src/constants.rs new file mode 100644 index 00000000000..35c5d525f3e --- /dev/null +++ b/rust/chains/tw_sui/src/constants.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::sui_types::{ObjectID, SequenceNumber}; +use move_core_types::account_address::AccountAddress; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; + +pub const OBJECT_START_VERSION: SequenceNumber = SequenceNumber(1); + +/// 0x5: hardcoded object ID for the singleton sui system state object. +pub const SUI_SYSTEM_STATE_ADDRESS: AccountAddress = address_from_single_byte(5); +pub const SUI_SYSTEM_STATE_OBJECT_ID: ObjectID = ObjectID(SUI_SYSTEM_STATE_ADDRESS); +pub const SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION: SequenceNumber = OBJECT_START_VERSION; + +/// 0x3-- account address where sui system modules are stored +/// Same as the ObjectID +pub const SUI_SYSTEM_ADDRESS: AccountAddress = address_from_single_byte(3); +pub const SUI_SYSTEM_PACKAGE_ID: ObjectID = ObjectID(SUI_SYSTEM_ADDRESS); + +pub const SUI_SYSTEM_MODULE_NAME: &IdentStr = ident_str!("sui_system"); +pub const ADD_STAKE_MUL_COIN_FUN_NAME: &IdentStr = ident_str!("request_add_stake_mul_coin"); +pub const WITHDRAW_STAKE_FUN_NAME: &IdentStr = ident_str!("request_withdraw_stake"); + +const fn address_from_single_byte(b: u8) -> AccountAddress { + let mut addr = [0u8; AccountAddress::LENGTH]; + addr[AccountAddress::LENGTH - 1] = b; + AccountAddress::new(addr) +} diff --git a/rust/chains/tw_sui/src/entry.rs b/rust/chains/tw_sui/src/entry.rs new file mode 100644 index 00000000000..70682041a43 --- /dev/null +++ b/rust/chains/tw_sui/src/entry.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::compiler::SuiCompiler; +use crate::signer::SuiSigner; +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::{AddressError, AddressResult}; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Sui::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SuiEntry; + +impl CoinEntry for SuiEntry { + type AddressPrefix = NoPrefix; + type Address = SuiAddress; + 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; + type TransactionDecoder = NoTransactionDecoder; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + SuiAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + SuiAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let ed25519_public = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + SuiAddress::with_ed25519_pubkey(ed25519_public) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + SuiSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + SuiCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + SuiCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_sui/src/lib.rs b/rust/chains/tw_sui/src/lib.rs new file mode 100644 index 00000000000..9a2411b8058 --- /dev/null +++ b/rust/chains/tw_sui/src/lib.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod constants; +pub mod entry; +pub mod modules; +pub mod signature; +pub mod signer; +pub mod transaction; diff --git a/rust/chains/tw_sui/src/modules/mod.rs b/rust/chains/tw_sui/src/modules/mod.rs new file mode 100644 index 00000000000..7398efd0e9b --- /dev/null +++ b/rust/chains/tw_sui/src/modules/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod tx_builder; +pub mod tx_signer; diff --git a/rust/chains/tw_sui/src/modules/tx_builder.rs b/rust/chains/tw_sui/src/modules/tx_builder.rs new file mode 100644 index 00000000000..58ec6dff986 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/tx_builder.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::sui_types::{ObjectDigest, ObjectID, ObjectRef, SequenceNumber}; +use crate::transaction::transaction_builder::TransactionBuilder; +use crate::transaction::transaction_data::TransactionData; +use std::borrow::Cow; +use std::str::FromStr; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_encoding::base64; +use tw_keypair::ed25519; +use tw_keypair::traits::KeyPairTrait; +use tw_memory::Data; +use tw_proto::Sui::Proto; +use tw_proto::Sui::Proto::mod_SigningInput::OneOftransaction_payload as TransactionType; + +pub enum TWTransaction { + Transaction(TransactionData), + SignDirect(Data), +} + +pub struct TWTransactionBuilder<'a> { + input: Proto::SigningInput<'a>, +} + +impl<'a> TWTransactionBuilder<'a> { + pub fn new(input: Proto::SigningInput<'a>) -> Self { + TWTransactionBuilder { input } + } + + pub fn signer_key(&self) -> SigningResult { + ed25519::sha512::KeyPair::try_from(self.input.private_key.as_ref()) + .map_err(SigningError::from) + } + + pub fn build(self) -> SigningResult { + let tx_data = match self.input.transaction_payload { + TransactionType::sign_direct_message(ref direct) => { + let raw_data = self.sign_direct_from_proto(direct)?; + return Ok(TWTransaction::SignDirect(raw_data)); + }, + TransactionType::pay_sui(ref pay_sui) => self.pay_sui_from_proto(pay_sui), + TransactionType::pay_all_sui(ref pay_all_sui) => { + self.pay_all_sui_from_proto(pay_all_sui) + }, + TransactionType::pay(ref pay) => self.pay_from_proto(pay), + TransactionType::request_add_stake(ref stake) => self.stake_from_proto(stake), + TransactionType::request_withdraw_stake(ref withdraw) => { + self.withdraw_from_proto(withdraw) + }, + TransactionType::transfer_object(ref transfer_obj) => { + self.transfer_object_from_proto(transfer_obj) + }, + TransactionType::None => Err(SigningError(SigningErrorType::Error_invalid_params)), + }?; + Ok(TWTransaction::Transaction(tx_data)) + } + + fn sign_direct_from_proto(&self, sign_direct: &Proto::SignDirect<'_>) -> SigningResult { + let url = false; + base64::decode(&sign_direct.unsigned_tx_msg, url) + .map_err(|_| SigningError(SigningErrorType::Error_input_parse)) + } + + fn pay_sui_from_proto(&self, pay_sui: &Proto::PaySui<'_>) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay_sui.input_coins)?; + let recipients = Self::parse_addresses(&pay_sui.recipients)?; + + TransactionBuilder::pay_sui( + signer, + input_coins, + recipients, + pay_sui.amounts.clone(), + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn pay_all_sui_from_proto( + &self, + pay_all_sui: &Proto::PayAllSui<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay_all_sui.input_coins)?; + let recipient = SuiAddress::from_str(&pay_all_sui.recipient)?; + + TransactionBuilder::pay_all_sui( + signer, + input_coins, + recipient, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn pay_from_proto(&self, pay: &Proto::Pay<'_>) -> SigningResult { + let signer = self.signer_address()?; + let input_coins = Self::build_coins(&pay.input_coins)?; + let recipients = Self::parse_addresses(&pay.recipients)?; + let gas = Self::require_coin(&pay.gas)?; + + TransactionBuilder::pay( + signer, + input_coins, + recipients, + pay.amounts.clone(), + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn stake_from_proto( + &self, + stake: &Proto::RequestAddStake<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let input_coins = Self::build_coins(&stake.coins)?; + let amount = stake.amount.as_ref().map(|a| a.amount); + let validator = SuiAddress::from_str(stake.validator.as_ref())?; + let gas = Self::require_coin(&stake.gas)?; + + TransactionBuilder::request_add_stake( + signer, + input_coins, + amount, + validator, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn withdraw_from_proto( + &self, + withdraw: &Proto::RequestWithdrawStake<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let staked_sui = Self::require_coin(&withdraw.staked_sui)?; + let gas = Self::require_coin(&withdraw.gas)?; + + TransactionBuilder::request_withdraw_stake( + signer, + staked_sui, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn transfer_object_from_proto( + &self, + transfer_obj: &Proto::TransferObject<'_>, + ) -> SigningResult { + let signer = self.signer_address()?; + + let recipient = SuiAddress::from_str(&transfer_obj.recipient)?; + let object = Self::require_coin(&transfer_obj.object)?; + let gas = Self::require_coin(&transfer_obj.gas)?; + + TransactionBuilder::transfer_object( + signer, + object, + recipient, + gas, + self.input.gas_budget, + self.input.reference_gas_price, + ) + } + + fn signer_address(&self) -> SigningResult { + if self.input.private_key.is_empty() { + SuiAddress::from_str(&self.input.signer).map_err(SigningError::from) + } else { + let keypair = self.signer_key()?; + SuiAddress::with_ed25519_pubkey(keypair.public()).map_err(SigningError::from) + } + } + + fn build_coins(coins: &[Proto::ObjectRef]) -> SigningResult> { + coins.iter().map(Self::build_coin).collect() + } + + fn require_coin(maybe_coin: &Option) -> SigningResult { + let coin = maybe_coin + .as_ref() + .ok_or(SigningError(SigningErrorType::Error_invalid_params))?; + Self::build_coin(coin) + } + + fn build_coin(coin: &Proto::ObjectRef) -> SigningResult { + let object_id = ObjectID::from_str(coin.object_id.as_ref())?; + let version = SequenceNumber(coin.version); + let object_digest = ObjectDigest::from_str(coin.object_digest.as_ref())?; + + Ok((object_id, version, object_digest)) + } + + fn parse_addresses(addresses: &[Cow<'_, str>]) -> SigningResult> { + let mut res = Vec::with_capacity(addresses.len()); + for addr in addresses { + res.push(SuiAddress::from_str(addr.as_ref())?); + } + Ok(res) + } +} diff --git a/rust/chains/tw_sui/src/modules/tx_signer.rs b/rust/chains/tw_sui/src/modules/tx_signer.rs new file mode 100644 index 00000000000..e7364feac56 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/tx_signer.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::signature::SuiSignatureInfo; +use crate::transaction::transaction_data::TransactionData; +use serde::Serialize; +use serde_repr::Serialize_repr; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_encoding::bcs; +use tw_hash::blake2::blake2_b; +use tw_hash::H256; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_memory::Data; + +/// This enums specifies the intent scope. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentScope { + /// Used for a user signature on a transaction data. + TransactionData = 0, +} + +/// The version here is to distinguish between signing different versions of the struct +/// or enum. Serialized output between two different versions of the same struct/enum +/// might accidentally (or maliciously on purpose) match. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentVersion { + V0 = 0, +} + +/// This enums specifies the application ID. Two intents in two different applications +/// (i.e., Narwhal, Sui, Ethereum etc) should never collide, so that even when a signing +/// key is reused, nobody can take a signature designated for app_1 and present it as a +/// valid signature for an (any) intent in app_2. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum AppId { + Sui = 0, +} + +/// An intent is a compact struct serves as the domain separator for a message that a signature commits to. +/// It consists of three parts: [enum IntentScope] (what the type of the message is), +/// [enum IntentVersion], [enum AppId] (what application that the signature refers to). +/// It is used to construct [struct IntentMessage] that what a signature commits to. +/// +/// The serialization of an Intent is a 3-byte array where each field is represented by a byte. +#[derive(Serialize)] +pub struct Intent { + pub scope: IntentScope, + pub version: IntentVersion, + pub app_id: AppId, +} + +/// Intent Message is a wrapper around a message with its intent. The message can +/// be any type that implements [trait Serialize]. *ALL* signatures in Sui must commits +/// to the intent message, not the message itself. This guarantees any intent +/// message signed in the system cannot collide with another since they are domain +/// separated by intent. +/// +/// The serialization of an IntentMessage is compact: it only appends three bytes +/// to the message itself. +#[derive(Serialize)] +pub struct IntentMessage { + pub intent: Intent, + pub value: T, +} + +pub struct TransactionPreimage { + /// Transaction `bcs` encoded representation. + pub unsigned_tx_data: Data, + /// [`TransactionPreimage::unsigned_tx_data`] extended with the `IntentMessage`. + pub tx_data_to_sign: Data, + /// Hash of the [`TransactionPreimage::tx_data_to_sign`]. + pub tx_hash_to_sign: H256, +} + +pub struct TxSigner; + +impl TxSigner { + pub fn sign( + tx: &TransactionData, + signer_key: &ed25519::sha512::KeyPair, + ) -> SigningResult<(TransactionPreimage, SuiSignatureInfo)> { + let public_key = signer_key.public(); + let signer_address = SuiAddress::with_ed25519_pubkey(public_key)?; + if signer_address != tx.sender() { + return Err(SigningError(SigningErrorType::Error_missing_private_key)); + } + + let unsigned_tx_data = + bcs::encode(tx).map_err(|_| SigningError(SigningErrorType::Error_internal))?; + Self::sign_direct(unsigned_tx_data, signer_key) + } + + pub fn sign_direct( + unsigned_tx_data: Data, + signer_key: &ed25519::sha512::KeyPair, + ) -> SigningResult<(TransactionPreimage, SuiSignatureInfo)> { + let preimage = Self::preimage_direct(unsigned_tx_data)?; + let signature = signer_key.sign(preimage.tx_hash_to_sign.into_vec())?; + let signature_info = SuiSignatureInfo::ed25519(&signature, signer_key.public()); + Ok((preimage, signature_info)) + } + + pub fn preimage(tx: &TransactionData) -> SigningResult { + let unsigned_tx_data = + bcs::encode(tx).map_err(|_| SigningError(SigningErrorType::Error_internal))?; + Self::preimage_direct(unsigned_tx_data) + } + + pub fn preimage_direct(unsigned_tx_data: Data) -> SigningResult { + let intent = Intent { + scope: IntentScope::TransactionData, + version: IntentVersion::V0, + app_id: AppId::Sui, + }; + let intent_data = + bcs::encode(&intent).map_err(|_| SigningError(SigningErrorType::Error_internal))?; + + let tx_data_to_sign: Data = intent_data + .into_iter() + .chain(unsigned_tx_data.iter().copied()) + .collect(); + let tx_hash_to_sign = blake2_b(&tx_data_to_sign, H256::LEN) + .and_then(|hash| H256::try_from(hash.as_slice())) + .map_err(|_| SigningError(SigningErrorType::Error_internal))?; + + Ok(TransactionPreimage { + unsigned_tx_data, + tx_data_to_sign, + tx_hash_to_sign, + }) + } +} diff --git a/rust/chains/tw_sui/src/signature.rs b/rust/chains/tw_sui/src/signature.rs new file mode 100644 index 00000000000..4a35bf808d0 --- /dev/null +++ b/rust/chains/tw_sui/src/signature.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::base64; +use tw_hash::{H256, H512}; +use tw_keypair::ed25519; +use tw_memory::Data; + +#[derive(Clone, Copy)] +#[repr(u8)] +pub enum SignatureScheme { + ED25519 = 0, +} + +pub struct SuiSignatureInfo { + scheme: SignatureScheme, + signature: H512, + public_key: H256, +} + +impl SuiSignatureInfo { + pub fn ed25519( + signature: &ed25519::Signature, + public_key: &ed25519::sha512::PublicKey, + ) -> SuiSignatureInfo { + SuiSignatureInfo { + scheme: SignatureScheme::ED25519, + signature: signature.to_bytes(), + public_key: public_key.to_bytes(), + } + } + + pub fn to_vec(&self) -> Data { + let mut scheme: Data = Vec::with_capacity(H512::LEN + H256::LEN + 1); + scheme.push(self.scheme as u8); + scheme.extend_from_slice(self.signature.as_slice()); + scheme.extend_from_slice(self.public_key.as_slice()); + scheme + } + + pub fn to_base64(&self) -> String { + let is_url = false; + base64::encode(&self.to_vec(), is_url) + } +} diff --git a/rust/chains/tw_sui/src/signer.rs b/rust/chains/tw_sui/src/signer.rs new file mode 100644 index 00000000000..bfb59285678 --- /dev/null +++ b/rust/chains/tw_sui/src/signer.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::{TWTransaction, TWTransactionBuilder}; +use crate::modules::tx_signer::TxSigner; +use std::borrow::Cow; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::SigningResult; +use tw_coin_entry::signing_output_error; +use tw_encoding::base64; +use tw_proto::Sui::Proto; + +pub struct SuiSigner; + +impl SuiSigner { + 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> { + let builder = TWTransactionBuilder::new(input); + let signer_key = builder.signer_key()?; + let tx_to_sign = builder.build()?; + + let (preimage, signature) = match tx_to_sign { + TWTransaction::Transaction(tx) => TxSigner::sign(&tx, &signer_key)?, + TWTransaction::SignDirect(tx_data) => TxSigner::sign_direct(tx_data, &signer_key)?, + }; + + let is_url = false; + let unsigned_tx = base64::encode(&preimage.unsigned_tx_data, is_url); + Ok(Proto::SigningOutput { + unsigned_tx: Cow::from(unsigned_tx), + signature: Cow::from(signature.to_base64()), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/chains/tw_sui/src/transaction/command.rs b/rust/chains/tw_sui/src/transaction/command.rs new file mode 100644 index 00000000000..533c0be3df2 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/command.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::sui_types::ObjectID; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; + +/// A single command in a programmable transaction. +#[derive(Debug, Deserialize, Serialize)] +pub enum Command { + /// A call to either an entry or a public Move function + MoveCall(Box), + /// `(Vec, address)` + /// It sends n-objects to the specified address. These objects must have store + /// (public transfer) and either the previous owner must be an address or the object must + /// be newly created. + TransferObjects(Vec, Argument), + /// `(&mut Coin, Vec)` -> `Vec>` + /// It splits off some amounts into a new coins with those amounts + SplitCoins(Argument, Vec), + /// `(&mut Coin, Vec>)` + /// It merges n-coins into the first coin + MergeCoins(Argument, Vec), + /// Publishes a Move package. It takes the package bytes and a list of the package's transitive + /// dependencies to link against on-chain. + Publish(Vec>, Vec), + /// `forall T: Vec -> vector` + /// Given n-values of the same type, it constructs a vector. For non objects or an empty vector, + /// the type tag must be specified. + MakeMoveVec(Option, Vec), +} + +impl Command { + pub fn move_call( + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + arguments: Vec, + ) -> Self { + Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module, + function, + type_arguments, + arguments, + })) + } +} + +/// The command for calling a Move function, either an entry function or a public +/// function (which cannot return references). +#[derive(Debug, Deserialize, Serialize)] +pub struct ProgrammableMoveCall { + /// The package containing the module and function. + pub package: ObjectID, + /// The specific module in the package containing the function. + pub module: Identifier, + /// The function to be called. + pub function: Identifier, + /// The type arguments to the function. + pub type_arguments: Vec, + /// The arguments to the function. + pub arguments: Vec, +} + +/// An argument to a programmable transaction command +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum Argument { + /// The gas coin. The gas coin can only be used by-ref, except for with + /// `TransferObjects`, which can use it by-value. + GasCoin, + /// One of the input objects or primitive values (from + /// `ProgrammableTransaction` inputs) + Input(u16), + /// The result of another command (from `ProgrammableTransaction` commands) + Result(u16), + /// Like a `Result` but it accesses a nested result. Currently, the only usage + /// of this is to access a value from a Move call with multiple return values. + NestedResult(u16, u16), +} diff --git a/rust/chains/tw_sui/src/transaction/mod.rs b/rust/chains/tw_sui/src/transaction/mod.rs new file mode 100644 index 00000000000..46b7c4a5cf0 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod command; +pub mod programmable_transaction; +pub mod sui_types; +pub mod transaction_builder; +pub mod transaction_data; diff --git a/rust/chains/tw_sui/src/transaction/programmable_transaction.rs b/rust/chains/tw_sui/src/transaction/programmable_transaction.rs new file mode 100644 index 00000000000..6a3529299b5 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/programmable_transaction.rs @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::command::{Argument, Command}; +use crate::transaction::sui_types::{CallArg, ObjectArg, ObjectID, ObjectRef}; +use indexmap::IndexMap; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_encoding::bcs; + +/// A series of commands where the results of one command can be used in future +/// commands +#[derive(Debug, Deserialize, Serialize)] +pub struct ProgrammableTransaction { + /// Input objects or primitive values + pub inputs: Vec, + /// The commands to be executed sequentially. A failure in any command will + /// result in the failure of the entire transaction. + pub commands: Vec, +} + +#[derive(Eq, Hash, PartialEq)] +enum BuilderArg { + Object(ObjectID), + Pure(Vec), + ForcedNonUniquePure(usize), +} + +#[derive(Default)] +pub struct ProgrammableTransactionBuilder { + inputs: IndexMap, + commands: Vec, +} + +impl ProgrammableTransactionBuilder { + pub fn finish(self) -> ProgrammableTransaction { + let Self { inputs, commands } = self; + let inputs = inputs.into_values().collect(); + ProgrammableTransaction { inputs, commands } + } + + /// Will fail to generate if recipients and amounts do not have the same lengths. + /// Or if coins is empty + pub fn pay( + &mut self, + coins: Vec, + recipients: Vec, + amounts: Vec, + ) -> SigningResult<()> { + let mut coins = coins.into_iter(); + let Some(coin) = coins.next() else { + // coins vector is empty + return Err(SigningError(SigningErrorType::Error_invalid_params)); + }; + let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin))?; + let merge_args: Vec<_> = coins + .map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c))) + .collect::>()?; + if !merge_args.is_empty() { + self.command(Command::MergeCoins(coin_arg, merge_args)); + } + self.pay_impl(recipients, amounts, coin_arg) + } + + /// Will fail to generate if recipients and amounts do not have the same lengths + pub fn pay_sui(&mut self, recipients: Vec, amounts: Vec) -> SigningResult<()> { + self.pay_impl(recipients, amounts, Argument::GasCoin) + } + + pub fn pay_all_sui(&mut self, recipient: SuiAddress) { + let rec_arg = self.pure(recipient).unwrap(); + self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg)); + } + + pub fn transfer_object( + &mut self, + recipient: SuiAddress, + object_ref: ObjectRef, + ) -> SigningResult<()> { + let rec_arg = self.pure(recipient).unwrap(); + let obj_arg = self.obj(ObjectArg::ImmOrOwnedObject(object_ref)); + self.commands + .push(Command::TransferObjects(vec![obj_arg?], rec_arg)); + Ok(()) + } + + /// Will fail to generate if given an empty ObjVec + pub fn move_call( + &mut self, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + call_args: Vec, + ) -> SigningResult<()> { + let arguments = call_args + .into_iter() + .map(|a| self.input(a)) + .collect::>()?; + self.command(Command::move_call( + package, + module, + function, + type_arguments, + arguments, + )); + Ok(()) + } + + pub fn input(&mut self, call_arg: CallArg) -> SigningResult { + match call_arg { + CallArg::Pure(bytes) => { + let force_separate = false; + Ok(self.pure_bytes(bytes, force_separate)) + }, + CallArg::Object(obj) => self.obj(obj), + } + } + + pub fn pure_bytes(&mut self, bytes: Vec, force_separate: bool) -> Argument { + let arg = if force_separate { + BuilderArg::ForcedNonUniquePure(self.inputs.len()) + } else { + BuilderArg::Pure(bytes.clone()) + }; + let (i, _) = self.inputs.insert_full(arg, CallArg::Pure(bytes)); + Argument::Input(i as u16) + } + + pub fn pure(&mut self, value: T) -> SigningResult { + let force_separate = false; + Ok(self.pure_bytes(bcs::encode(&value)?, force_separate)) + } + + pub fn obj(&mut self, obj_arg: ObjectArg) -> SigningResult { + let id = obj_arg.id(); + let obj_arg = if let Some(old_value) = self.inputs.get(&BuilderArg::Object(id)) { + let old_obj_arg = match old_value { + CallArg::Pure(_) => return Err(SigningError(SigningErrorType::Error_internal)), + CallArg::Object(arg) => arg, + }; + match (old_obj_arg, obj_arg) { + ( + ObjectArg::SharedObject { + id: id1, + initial_shared_version: v1, + mutable: mut1, + }, + ObjectArg::SharedObject { + id: id2, + initial_shared_version: v2, + mutable: mut2, + }, + ) if v1 == &v2 => { + if id1 != &id2 || id != id2 { + // "invariant violation! object has id does not match call arg" + return Err(SigningError(SigningErrorType::Error_internal)); + } + ObjectArg::SharedObject { + id, + initial_shared_version: v2, + mutable: *mut1 || mut2, + } + }, + (old_obj_arg, obj_arg) => { + if old_obj_arg != &obj_arg { + // "Mismatched Object argument kind for object {id}. {old_value:?} is not compatible with {obj_arg:?}" + return Err(SigningError(SigningErrorType::Error_internal)); + } + obj_arg + }, + } + } else { + obj_arg + }; + let (i, _) = self + .inputs + .insert_full(BuilderArg::Object(id), CallArg::Object(obj_arg)); + Ok(Argument::Input(i as u16)) + } + + pub fn make_obj_vec( + &mut self, + objs: impl IntoIterator, + ) -> SigningResult { + let make_vec_args = objs + .into_iter() + .map(|obj| self.obj(obj)) + .collect::>()?; + Ok(self.command(Command::MakeMoveVec(None, make_vec_args))) + } + + pub fn command(&mut self, command: Command) -> Argument { + let i = self.commands.len(); + self.commands.push(command); + Argument::Result(i as u16) + } + + fn pay_impl( + &mut self, + recipients: Vec, + amounts: Vec, + coin: Argument, + ) -> SigningResult<()> { + if recipients.len() != amounts.len() { + // "Recipients and amounts mismatch. Got {} recipients but {} amounts" + return Err(SigningError(SigningErrorType::Error_invalid_params)); + } + if amounts.is_empty() { + return Ok(()); + } + + // collect recipients in the case where they are non-unique in order + // to minimize the number of transfers that must be performed + let mut recipient_map: IndexMap> = IndexMap::new(); + let mut amt_args = vec![]; + for (i, (recipient, amount)) in recipients.into_iter().zip(amounts).enumerate() { + recipient_map.entry(recipient).or_default().push(i); + amt_args.push(self.pure(amount)?); + } + let Argument::Result(split_primary) = self.command(Command::SplitCoins(coin, amt_args)) + else { + panic!("self.command should always give a Argument::Result") + }; + for (recipient, split_secondaries) in recipient_map { + let rec_arg = self.pure(recipient).unwrap(); + let coins = split_secondaries + .into_iter() + .map(|j| Argument::NestedResult(split_primary, j as u16)) + .collect(); + self.command(Command::TransferObjects(coins, rec_arg)); + } + Ok(()) + } +} diff --git a/rust/chains/tw_sui/src/transaction/sui_types.rs b/rust/chains/tw_sui/src/transaction/sui_types.rs new file mode 100644 index 00000000000..b35df1b26d8 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/sui_types.rs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::constants::{SUI_SYSTEM_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION}; +use move_core_types::account_address::AccountAddress; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tw_coin_entry::error::{AddressError, SigningError, SigningErrorType}; +use tw_encoding::base58::{self, Alphabet}; +use tw_hash::{as_bytes, H256}; +use tw_memory::Data; + +pub type ObjectRef = (ObjectID, SequenceNumber, ObjectDigest); +pub type EpochId = u64; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct SequenceNumber(pub u64); + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ObjectID(pub AccountAddress); + +impl FromStr for ObjectID { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let addr = SuiAddress::from_str(s)?; + Ok(ObjectID(addr.into_inner())) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ObjectDigest(#[serde(with = "as_bytes")] pub H256); + +impl FromStr for ObjectDigest { + type Err = SigningError; + + fn from_str(s: &str) -> Result { + let bytes = base58::decode(s, Alphabet::BITCOIN) + .map_err(|_| SigningError(SigningErrorType::Error_invalid_params))?; + H256::try_from(bytes.as_slice()) + .map(ObjectDigest) + .map_err(|_| SigningError(SigningErrorType::Error_invalid_params)) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum CallArg { + // contains no structs or objects + Pure(Data), + // an object + Object(ObjectArg), +} + +impl CallArg { + pub const SUI_SYSTEM_MUT: Self = Self::Object(ObjectArg::SUI_SYSTEM_MUT); +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub enum ObjectArg { + // A Move object, either immutable, or owned mutable. + ImmOrOwnedObject(ObjectRef), + // A Move object that's shared. + // SharedObject::mutable controls whether caller asks for a mutable reference to shared object. + SharedObject { + id: ObjectID, + initial_shared_version: SequenceNumber, + mutable: bool, + }, +} + +impl ObjectArg { + pub const SUI_SYSTEM_MUT: Self = Self::SharedObject { + id: SUI_SYSTEM_STATE_OBJECT_ID, + initial_shared_version: SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION, + mutable: true, + }; + + pub fn id(&self) -> ObjectID { + match self { + ObjectArg::ImmOrOwnedObject((id, _, _)) | ObjectArg::SharedObject { id, .. } => *id, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GasData { + pub payment: Vec, + pub owner: SuiAddress, + pub price: u64, + pub budget: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionExpiration { + /// The transaction has no expiration + None, + /// Validators wont sign a transaction unless the expiration Epoch + /// is greater than or equal to the current epoch + Epoch(EpochId), +} diff --git a/rust/chains/tw_sui/src/transaction/transaction_builder.rs b/rust/chains/tw_sui/src/transaction/transaction_builder.rs new file mode 100644 index 00000000000..93ad29060f0 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/transaction_builder.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::constants::{ + ADD_STAKE_MUL_COIN_FUN_NAME, SUI_SYSTEM_MODULE_NAME, SUI_SYSTEM_PACKAGE_ID, + WITHDRAW_STAKE_FUN_NAME, +}; +use crate::transaction::command::Command; +use crate::transaction::programmable_transaction::ProgrammableTransactionBuilder; +use crate::transaction::sui_types::{CallArg, ObjectArg, ObjectRef}; +use crate::transaction::transaction_data::{TransactionData, TransactionKind}; +use tw_coin_entry::error::{SigningError, SigningErrorType, SigningResult}; +use tw_encoding::bcs; + +pub struct TransactionBuilder; + +impl TransactionBuilder { + pub fn request_add_stake( + signer: SuiAddress, + coins: Vec, + amount: Option, + validator: SuiAddress, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let obj_vec: Vec<_> = coins.into_iter().map(ObjectArg::ImmOrOwnedObject).collect(); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + let arguments = vec![ + builder.input(CallArg::SUI_SYSTEM_MUT).unwrap(), + builder.make_obj_vec(obj_vec)?, + builder.input(CallArg::Pure(bcs::encode(&amount)?)).unwrap(), + builder + .input(CallArg::Pure(bcs::encode(&validator)?)) + .unwrap(), + ]; + builder.command(Command::move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(), + vec![], + arguments, + )); + builder.finish() + }; + Ok(TransactionData::new_programmable( + signer, + vec![gas], + pt, + gas_budget, + gas_price, + )) + } + + pub fn request_withdraw_stake( + signer: SuiAddress, + staked_sui: ObjectRef, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + TransactionData::new_move_call( + signer, + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + WITHDRAW_STAKE_FUN_NAME.to_owned(), + vec![], + gas, + vec![ + CallArg::SUI_SYSTEM_MUT, + CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_sui)), + ], + gas_budget, + gas_price, + ) + } + + /// Send `Coin` to a list of addresses, where T can be any coin type, following a list of amounts. + /// The object specified in the gas field will be used to pay the gas fee for the transaction. + /// The gas object can not appear in input_coins. + /// https://docs.sui.io/sui-api-ref#unsafe_pay + #[allow(clippy::too_many_arguments)] + pub fn pay( + signer: SuiAddress, + input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.iter().any(|coin| coin.0 == gas.0) { + // Gas coin is in input coins of Pay transaction, use PaySui transaction instead!. + return Err(SigningError(SigningErrorType::Error_invalid_params)); + } + + TransactionData::new_pay( + signer, + input_coins, + recipients, + amounts, + gas, + gas_budget, + gas_price, + ) + } + + /// Send SUI coins to a list of addresses, following a list of amounts. + /// This is for SUI coin only and does not require a separate gas coin object. + /// https://docs.sui.io/sui-api-ref#unsafe_paysui + pub fn pay_sui( + signer: SuiAddress, + mut input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.is_empty() { + // "Empty input coins for Pay related transaction" + return Err(SigningError(SigningErrorType::Error_invalid_params)); + } + + let gas_object_ref = input_coins.remove(0); + TransactionData::new_pay_sui( + signer, + input_coins, + recipients, + amounts, + gas_object_ref, + gas_budget, + gas_price, + ) + } + + /// Send all SUI coins to one recipient. + /// This is for SUI coin only and does not require a separate gas coin object. + /// https://docs.sui.io/sui-api-ref#unsafe_payallsui + pub fn pay_all_sui( + signer: SuiAddress, + mut input_coins: Vec, + recipient: SuiAddress, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + if input_coins.is_empty() { + // "Empty input coins for Pay related transaction" + return Err(SigningError(SigningErrorType::Error_invalid_params)); + } + + let gas_object_ref = input_coins.remove(0); + Ok(TransactionData::new_pay_all_sui( + signer, + input_coins, + recipient, + gas_object_ref, + gas_budget, + gas_price, + )) + } + + pub fn transfer_object( + signer: SuiAddress, + object: ObjectRef, + recipient: SuiAddress, + gas: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.transfer_object(recipient, object)?; + + Ok(TransactionData::new( + TransactionKind::ProgrammableTransaction(builder.finish()), + signer, + gas, + gas_budget, + gas_price, + )) + } +} diff --git a/rust/chains/tw_sui/src/transaction/transaction_data.rs b/rust/chains/tw_sui/src/transaction/transaction_data.rs new file mode 100644 index 00000000000..6f3329cc419 --- /dev/null +++ b/rust/chains/tw_sui/src/transaction/transaction_data.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::SuiAddress; +use crate::transaction::programmable_transaction::{ + ProgrammableTransaction, ProgrammableTransactionBuilder, +}; +use crate::transaction::sui_types::{CallArg, GasData, ObjectID, ObjectRef, TransactionExpiration}; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::TypeTag; +use serde::{Deserialize, Serialize}; +use tw_coin_entry::error::SigningResult; + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionData { + V1(TransactionDataV1), +} + +impl TransactionData { + #[inline] + pub fn new( + kind: TransactionKind, + sender: SuiAddress, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> Self { + TransactionData::V1(TransactionDataV1 { + kind, + sender, + gas_data: GasData { + price: gas_price, + owner: sender, + payment: vec![gas_payment], + budget: gas_budget, + }, + expiration: TransactionExpiration::None, + }) + } + + #[inline] + pub fn new_programmable( + sender: SuiAddress, + gas_payment: Vec, + pt: ProgrammableTransaction, + gas_budget: u64, + gas_price: u64, + ) -> Self { + Self::new_programmable_allow_sponsor(sender, gas_payment, pt, gas_budget, gas_price, sender) + } + + #[inline] + pub fn new_programmable_allow_sponsor( + sender: SuiAddress, + gas_payment: Vec, + pt: ProgrammableTransaction, + gas_budget: u64, + gas_price: u64, + sponsor: SuiAddress, + ) -> Self { + let kind = TransactionKind::ProgrammableTransaction(pt); + Self::new_with_gas_coins_allow_sponsor( + kind, + sender, + gas_payment, + gas_budget, + gas_price, + sponsor, + ) + } + + #[inline] + pub fn new_with_gas_coins_allow_sponsor( + kind: TransactionKind, + sender: SuiAddress, + gas_payment: Vec, + gas_budget: u64, + gas_price: u64, + gas_sponsor: SuiAddress, + ) -> Self { + TransactionData::V1(TransactionDataV1 { + kind, + sender, + gas_data: GasData { + price: gas_price, + owner: gas_sponsor, + payment: gas_payment, + budget: gas_budget, + }, + expiration: TransactionExpiration::None, + }) + } + + pub fn new_pay( + sender: SuiAddress, + coins: Vec, + recipients: Vec, + amounts: Vec, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay(coins, recipients, amounts)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, + vec![gas_payment], + pt, + gas_budget, + gas_price, + )) + } + + pub fn new_pay_sui( + sender: SuiAddress, + mut coins: Vec, + recipients: Vec, + amounts: Vec, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + coins.insert(0, gas_payment); + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay_sui(recipients, amounts)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, coins, pt, gas_budget, gas_price, + )) + } + + pub fn new_pay_all_sui( + sender: SuiAddress, + mut coins: Vec, + recipient: SuiAddress, + gas_payment: ObjectRef, + gas_budget: u64, + gas_price: u64, + ) -> Self { + coins.insert(0, gas_payment); + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.pay_all_sui(recipient); + builder.finish() + }; + Self::new_programmable(sender, coins, pt, gas_budget, gas_price) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_move_call( + sender: SuiAddress, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + gas_payment: ObjectRef, + arguments: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + Self::new_move_call_with_gas_coins( + sender, + package, + module, + function, + type_arguments, + vec![gas_payment], + arguments, + gas_budget, + gas_price, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_move_call_with_gas_coins( + sender: SuiAddress, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + gas_payment: Vec, + arguments: Vec, + gas_budget: u64, + gas_price: u64, + ) -> SigningResult { + let pt = { + let mut builder = ProgrammableTransactionBuilder::default(); + builder.move_call(package, module, function, type_arguments, arguments)?; + builder.finish() + }; + Ok(Self::new_programmable( + sender, + gas_payment, + pt, + gas_budget, + gas_price, + )) + } + + pub fn sender(&self) -> SuiAddress { + match self { + TransactionData::V1(v1) => v1.sender, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TransactionDataV1 { + pub kind: TransactionKind, + pub sender: SuiAddress, + pub gas_data: GasData, + pub expiration: TransactionExpiration, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum TransactionKind { + /// A transaction that allows the interleaving of native commands and Move calls + ProgrammableTransaction(ProgrammableTransaction), +} diff --git a/rust/chains/tw_sui/tests/decode_transaction.rs b/rust/chains/tw_sui/tests/decode_transaction.rs new file mode 100644 index 00000000000..746516874b1 --- /dev/null +++ b/rust/chains/tw_sui/tests/decode_transaction.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::str::FromStr; +use tw_encoding::hex::DecodeHex; +use tw_encoding::{base64, bcs}; +use tw_sui::address::SuiAddress; +use tw_sui::transaction::command::{Argument, Command}; +use tw_sui::transaction::programmable_transaction::ProgrammableTransaction; +use tw_sui::transaction::sui_types::{ + CallArg, GasData, ObjectDigest, ObjectID, SequenceNumber, TransactionExpiration, +}; +use tw_sui::transaction::transaction_data::{TransactionData, TransactionDataV1, TransactionKind}; + +#[test] +fn test_decode_transfer_tx() { + let programmable = ProgrammableTransaction { + inputs: vec![ + CallArg::Pure("1027000000000000".decode_hex().unwrap()), + CallArg::Pure( + "259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015" + .decode_hex() + .unwrap(), + ), + ], + commands: vec![ + Command::SplitCoins(Argument::GasCoin, vec![Argument::Input(0)]), + Command::TransferObjects(vec![Argument::NestedResult(0, 0)], Argument::Input(1)), + ], + }; + + let v1 = TransactionDataV1 { + kind: TransactionKind::ProgrammableTransaction(programmable), + sender: SuiAddress::from_str( + "0xd575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d50", + ) + .unwrap(), + gas_data: GasData { + payment: vec![( + ObjectID( + SuiAddress::from_str( + "0x06f2c2c8c1d8964df1019d6616e9705719bebabd931da2755cb948ceb7e68964", + ) + .unwrap() + .into_inner(), + ), + SequenceNumber(748), + ObjectDigest::from_str("7UoYeVzREVT17ZyYbRTsKzRCec5xJWm6FMh8AKaDPdDx").unwrap(), + )], + owner: SuiAddress::from_str( + "0xd575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d50", + ) + .unwrap(), + price: 1, + budget: 2000, + }, + expiration: TransactionExpiration::None, + }; + let data = TransactionData::V1(v1); + + let is_url = false; + let bytes = base64::encode(&bcs::encode(&data).unwrap(), is_url); + // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet + assert_eq!(bytes, "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"); +} diff --git a/rust/coverage.stats b/rust/coverage.stats index f2511044fdd..fd6ae261616 100644 --- a/rust/coverage.stats +++ b/rust/coverage.stats @@ -1 +1 @@ -93.0 \ No newline at end of file +94.0 \ No newline at end of file diff --git a/rust/tw_any_coin/tests/chains/mod.rs b/rust/tw_any_coin/tests/chains/mod.rs index cd580c22dde..c4e5020a1dd 100644 --- a/rust/tw_any_coin/tests/chains/mod.rs +++ b/rust/tw_any_coin/tests/chains/mod.rs @@ -13,6 +13,7 @@ mod internet_computer; mod native_evmos; mod native_injective; mod solana; +mod sui; mod tbinance; mod thorchain; mod zetachain; diff --git a/rust/tw_any_coin/tests/chains/solana/solana_sign.rs b/rust/tw_any_coin/tests/chains/solana/solana_sign.rs index 186105c5fda..b667a3c588c 100644 --- a/rust/tw_any_coin/tests/chains/solana/solana_sign.rs +++ b/rust/tw_any_coin/tests/chains/solana/solana_sign.rs @@ -219,6 +219,36 @@ fn test_solana_sign_delegate_stake_no_stake_account() { assert_eq!(output.encoded, "j24mVM9Zgu5vDZhPLGGuCRXQnP9djNtxdHh4txN3S7dwJsNNL5fbhzGpPgSUAcLGoMVCfF9TuqTYfpfJnb4sJFe1ahM8yPL5HwuKL6py5AZJFi8SWx9fvaVB699dCPo1GT3JoEBLPCZ9o2jQtnwzLkzTYJnKv2axqhKWFE2sz6TBA5J39eZcjMFUYgyxz6Q5S4MWqYQCb8UET2NAEZoKcfy7j8N25WXL6Gj4j3hBZjpHQQNaGaNEprEqyma3ZuVhpGiCALSsuzVLX3wZVo4icXwe952deMFA4tH3BK1jcSQCgfmcKDJ9nd7bdrnUUs4BoMdF1uDZB5LxE2UH8QiqtYvaUcorF4SJ3gPxM5ykbyPsNK1cSYZF9NMpW2GofyC17eELwnHQTQB2kqphxJZu7BahvkwiDPPeeydiXAkBspJ3nc3PCBujv6WJw22ZHw5j6zAP8ZGnCW44pqtWD5qifF9tTKhySKdANNiWifs3tSCCPQqjfJXu14drNinR6VG8rJxS1qgmRYiRQUa7m1vtoaZFRN5qKUeAfoFKkAVaNnMdwgsNqNH4dqBodTCJFs1LkYwhgRZdZGbwXTn1j7vpR3DSnv4g72i2H556srzK53jdUmdv6yfxt516XDSshqZtHnKZ1tudxKjBXwsqT3imDiZFVka9wKWUAYMCi4XZ79CY6Xpsd9c18U2e9TCngQmgkTATFgrqysfraokNffgqWxvsPMugksbvbPjJs3iCzByvphkC9p7hCf6LwbeF8XnVB91EAgRDA4VLE1f9wkcq5zjy879YWJ4r516h3PQszTz1EaJXNAXdbk5Em7eyuuabGP1Q3nijFTL2yhMDsXpgrjAuEAABNxFMd4J1JRMaic615mHrhwociksrsfQK"); } +#[test] +fn test_solana_sign_delegate_stake_with_priority_fee() { + let delegate = Proto::DelegateStake { + validator_pubkey: "BWkvytz3MAiLkUbMuYK5yV1VYThbBYYQYG3gdef8NLw5".into(), + // 0.01 + value: 10000000, + ..Proto::DelegateStake::default() + }; + + let input = Proto::SigningInput { + // Corresponding Solana address: GEjr7dsDVHZMzTvnz1AgDW76LJ2GC7DATnHHMUVoF4p6. + private_key: "43c0aeb43038c582b1abac52e5bdc2c20c83a1995fbfde5936ccd2e6392e9d8b" + .decode_hex() + .unwrap() + .into(), + recent_blockhash: "7uKCh5WRoLdib5LfcX4NETFcJroQPjHVsekdXniyaoFZ".into(), + transaction_type: TransactionType::delegate_stake_transaction(delegate), + priority_fee_price: Some(Proto::PriorityFeePrice { price: 1000 }), + priority_fee_limit: Some(Proto::PriorityFeeLimit { limit: 10_000 }), + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Solana, input); + + assert_eq!(output.error, SigningError::OK); + // // https://solscan.io/tx/2DRUt4Q3BxYPB1Whd4MyekByh4pXWgrfQAybZ3obeNReR96sKYX1pZgZV12NUkLXJRb1c6ffAcEx1Eu7kYmA6zqH + assert_eq!(output.encoded, "XUMBKTDMfK4i8N3RBJtzzDsA3KnKZ4R4HexjXxiwsKr3j4DhENHcpUFWA41xwU94dE4av4hqP9Z5nvES9FU7GuECsDE5CuRagt5b1EEH2SPW8AjdeGbTs7JfT4nm3nd6gh2uJHXVMCjTYEhTKSKgLehgCa2JqwodiKEoPUdPwRoq7YoznzSBzvwvmSZrySC2eA8pp1PaBNDCG9rMAg1mDAzAu4VNmH7Nhh9bssGPcxgKZfoWmrhpvNcx6bDV4xBJBtvsYoGasRRSTXwF82VjA1L5aaQWJjAbBBZgsiws2Z5CqeWWdt6XF7sUHovAZihUdxXzJXPjRpEgX4HqHdriqTnPuaNwmVMEcNKzmZWJ543LTz4yUhyLRbYbN9hxWMtpiKBnjZe5KX783NPiuSHioyxAvmfkdKbDyKUf6a9G9HocHZMjKT3Yz1yeanHo69V2mAfKGZCPT7hRhRae6QoXu16QjHDHNBDLUtX8Db49vNJLdV3cYWPuyZ5eHLfDbw6r9jzCF34nmXJ4kFZHJHatp3wGKGbTtyeoxZnJVxLFmv7byoKVZ1hQdxGYg3q2jfjc2NsSj2Expmz6qLGZWMyVy6cVYjfqcQz9bwT89zC8823mAnFEPKRs8whZevCxifGiUfT6H99zZzNKfuJAhwJ14rP5VpZHCiDSuDRHzjydzwFxHDKamnAQXYuadEWJB89gJWA6wPoBQE1q4kRFTrYjU32mmnfwCs6Ji18uzNfrP2W6xPMs93vz2Pejpgzyru6anf7cYhKWVtou6bM9uNdVV5Qj5zr3eY4FqbNXXMz4WkiWmKQb5nUeyvp4e424WS1QUkwNYVW5vmehM2AuzMM9VEA5F2VMyrDihE74ZFA9yctaYZ4ovtH2TvSVgCGsS7ujqHoETVtDiyrjmgJvm68NtjcTioSKpAPgEvMciwxVwsUmYC6NGC6iCP2oVc2S6QCzixF"); +} + #[test] fn test_solana_sign_delegate_stake_no_stake_account_5zrqgk1() { // Corresponding Solana address: GcQQ1qx822KK14zyfTkMLQMLEZ4a9d88HnKepaA2XbmW. diff --git a/rust/tw_any_coin/tests/chains/sui/mod.rs b/rust/tw_any_coin/tests/chains/sui/mod.rs new file mode 100644 index 00000000000..53876af861f --- /dev/null +++ b/rust/tw_any_coin/tests/chains/sui/mod.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_proto::Sui::Proto; + +mod sui_address; +mod sui_compile; +mod sui_sign; +mod test_cases; + +fn object_ref(id: &'static str, version: u64, digest: &'static str) -> Proto::ObjectRef<'static> { + Proto::ObjectRef { + object_id: id.into(), + version, + object_digest: digest.into(), + } +} diff --git a/rust/tw_any_coin/tests/chains/sui/sui_address.rs b/rust/tw_any_coin/tests/chains/sui/sui_address.rs new file mode 100644 index 00000000000..c2a484ed332 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/sui/sui_address.rs @@ -0,0 +1,55 @@ +// 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_sui_address_normalization() { + test_address_normalization( + CoinType::Sui, + "259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + ); +} + +#[test] +fn test_sui_address_is_valid() { + test_address_valid( + CoinType::Sui, + "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + ); +} + +#[test] +fn test_sui_address_invalid() { + // Address 20 are invalid in SUI. + test_address_invalid(CoinType::Sui, "0xb1dc06bd64d4e179a482b97bb68243f6c02c1b92"); + test_address_invalid(CoinType::Sui, "b1dc06bd64d4e179a482b97bb68243f6c02c1b92"); + // Too long. + test_address_invalid( + CoinType::Sui, + "d575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d502", + ); + // Too short. + test_address_invalid(CoinType::Sui, "b1dc06bd64d4e179a482b97bb68243f6c02c1b9"); + // Invalid short address. + test_address_invalid(CoinType::Sui, "0x11"); + // Invalid Hex + test_address_invalid( + CoinType::Sui, + "0xS59ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + ); +} + +#[test] +fn test_sui_address_get_data() { + test_address_get_data( + CoinType::Sui, + "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + "259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015", + ); +} diff --git a/rust/tw_any_coin/tests/chains/sui/sui_compile.rs b/rust/tw_any_coin/tests/chains/sui/sui_compile.rs new file mode 100644 index 00000000000..67f84472525 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/sui/sui_compile.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::sui::test_cases::{transfer_d4ay9tdb, PRIVATE_KEY_54E80D76, SENDER_54E80D76}; +use tw_any_coin::test_utils::sign_utils::{CompilerHelper, PreImageHelper}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; +use tw_misc::traits::ToBytesVec; +use tw_proto::Common::Proto::SigningError; +use tw_proto::Sui::Proto::{self, mod_SigningInput::OneOftransaction_payload as TransactionType}; +use tw_proto::TxCompiler::Proto as CompilerProto; + +struct SuiCompileArgs<'a> { + input: Proto::SigningInput<'a>, + private_key: &'a str, + tx_hash: &'a str, + unsigned_tx_data: &'a str, + signature: &'a str, +} + +fn test_sui_compile_impl(args: SuiCompileArgs) { + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Sui, &args.input); + + assert_eq!(preimage_output.error, SigningError::OK); + assert_eq!(preimage_output.data_hash.to_hex(), args.tx_hash); + + // Step 3: Compile transaction info + + // Simulate external signing. + let key_pair = ed25519::sha512::KeyPair::try_from(args.private_key).unwrap(); + let public_key_bytes = key_pair.public().to_vec(); + let signature_bytes = key_pair + .sign(preimage_output.data_hash.to_vec()) + .unwrap() + .to_vec(); + + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::Sui, + &args.input, + vec![signature_bytes], + vec![public_key_bytes], + ); + + assert_eq!(output.error, SigningError::OK); + assert_eq!(output.unsigned_tx, args.unsigned_tx_data); + assert_eq!(output.signature, args.signature); +} + +#[test] +fn test_sui_compile_transfer() { + let input = Proto::SigningInput { + signer: SENDER_54E80D76.into(), + ..transfer_d4ay9tdb::sui_transfer_input() + }; + + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + test_sui_compile_impl(SuiCompileArgs { + input, + private_key: PRIVATE_KEY_54E80D76, + tx_hash: transfer_d4ay9tdb::TX_HASH, + unsigned_tx_data: transfer_d4ay9tdb::UNSIGNED_TX, + signature: transfer_d4ay9tdb::SIGNATURE, + }); +} + +#[test] +fn test_sui_compile_direct_transfer() { + let direct = Proto::SignDirect { + unsigned_tx_msg: transfer_d4ay9tdb::UNSIGNED_TX.into(), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::sign_direct_message(direct), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + ..Proto::SigningInput::default() + }; + + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + test_sui_compile_impl(SuiCompileArgs { + input, + private_key: PRIVATE_KEY_54E80D76, + tx_hash: transfer_d4ay9tdb::TX_HASH, + unsigned_tx_data: transfer_d4ay9tdb::UNSIGNED_TX, + signature: transfer_d4ay9tdb::SIGNATURE, + }); +} diff --git a/rust/tw_any_coin/tests/chains/sui/sui_sign.rs b/rust/tw_any_coin/tests/chains/sui/sui_sign.rs new file mode 100644 index 00000000000..bbc3f297e34 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/sui/sui_sign.rs @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::sui::object_ref; +use crate::chains::sui::test_cases::{transfer_d4ay9tdb, PRIVATE_KEY_54E80D76, SENDER_54E80D76}; +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_proto::Common::Proto::SigningError; +use tw_proto::Sui::Proto::{self, mod_SigningInput::OneOftransaction_payload as TransactionType}; + +fn test_sign_direct_impl(unsigned_tx: &str, private_key: &str, expected_signature: &str) { + let direct = Proto::SignDirect { + unsigned_tx_msg: unsigned_tx.into(), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::sign_direct_message(direct), + private_key: private_key.decode_hex().unwrap().into(), + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + assert_eq!(output.unsigned_tx, unsigned_tx); + assert_eq!(output.signature, expected_signature); +} + +#[test] +fn test_sui_sign_direct_transfer() { + let unsigned_tx = "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"; + let private_key = "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266"; + let expected_signature = "APxPduNVvHj2CcRcHOtiP2aBR9qP3vO2Cb0g12PI64QofDB6ks33oqe/i/iCTLcop2rBrkczwrayZuJOdi7gvwNqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="; + + test_sign_direct_impl(unsigned_tx, private_key, expected_signature); +} + +#[test] +fn test_sui_sign_direct_transfer_nft() { + let unsigned_tx = "AAAv0f6HrJCZ/1cuDVuxh1BL12XMeHxkKeZ7Js9grhcB0u8xtTvoOepOHAAAAAAAAAAgJvcpOSvKhM+tHPgGAnp5Pmc8l3wjZhVxK4/BrLu4YAgttQCskZzd41GsNuNxHYMsbbl2aSEnoKw8oAGf/LobCM7RxGurtPZtHAAAAAAAAAAgwk74iUAH9S+cGVXQxAydItvltZ3UK2L0vg1TYgDMPfABAAAAAAAAAOgDAAAAAAAA"; + let private_key = "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266"; + let expected_signature = "AI+KRy820ucibONQXbaVm53ixNWqRcqp16/aG0hvX7Mt3dOMqTDKRYoRBRvbMDsyPFmpS+n5iYvs5vuGdqjUvgBqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="; + + test_sign_direct_impl(unsigned_tx, private_key, expected_signature); +} + +#[test] +fn test_sui_sign_direct_move_call() { + let unsigned_tx = "AAIAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAAAAAAAAINaXMihjlCd4CQVFRPjcNb7QfYP4wGgQyl1xbplvEKUCA3N1aQh0cmFuc2ZlcgACAQCdB6Mav5rHiXD0rAWTCxS+ENwxMBsAAAAAAAAAINqDfrJUZebPjUi7xcyR3QcQSA9tOLwxThgYaZ1vMfgfABQU2gJ3ToaOYd1F/R6mXryOZdvpRi21AKyRnN3jUaw243EdgyxtuXZppM+mSjYYEQWDcV/7hFRrAE0VtRwbAAAAAAAAACC5nJxYaYJfa9rfbxSikaEFVmHGuXyCIZoZbMpxMwLebAEAAAAAAAAA0AcAAAAAAAA="; + let private_key = "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266"; + let expected_signature = "AHoX1/mzUS8WQ+tNr0gXtfI7KFXjSbDlUxbGG2gkEh6L8FngU2KrsXsR1N8MzCXyJIz7+YvTfl5+Dh6AWSZC5wVqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="; + + test_sign_direct_impl(unsigned_tx, private_key, expected_signature); +} + +#[test] +fn test_sui_sign_direct_add_delegation() { + let unsigned_tx = "AAIAAAAAAAAAAAAAAAAAAAAAAAAAAgEAAAAAAAAAIEt/p6rXSTjdKP6wJOXyx0c2xsgJ4MJtfxe7qHC34u4UCnN1aV9zeXN0ZW0fcmVxdWVzdF9hZGRfZGVsZWdhdGlvbl9tdWxfY29pbgAEAQEAAAAAAAAAAAAAAAAAAAAAAAAABQEAAAAAAAAAAgIAGSkMV9AFc419O9dL1kez9tzVIOiXzAEAAAAAACAfIePlHHP/+iv++FWQW9ofkVm4S2sFwupGikSq8bNjYwAH1A26NKDn7pJfn9zWaDi1nbntMJfMAQAAAAAAIKfMZAZktdmw36jwg/jcK1TDmrHmSZ/fdkeInO3BSjfWAAkB0AcAAAAAAAAAFAej1I8mhjmcpQTQtiX2J2HZ7y2xLbUArJGc3eNRrDbjcR2DLG25dmlY2RBfTU/P+GL1qpxE8NQrwiLw/JfMAQAAAAAAICs2NJlowCmCnpJ+hja2VwZE5K6yGM/qw0MSRnn9tW2cbgAAAAAAAACghgEAAAAAAA=="; + let private_key = "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266"; + let expected_signature = "AMn4XpOcE9pX/VWCcue/tMkk+TxRQprGas53TT9W4beLkj6XuQdSNLSdjp9AmbqQPHKh0yJZ9i7Q2i6aax8NdQZqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="; + + test_sign_direct_impl(unsigned_tx, private_key, expected_signature); +} + +#[test] +fn test_sui_sign_transfer_sui() { + let input = Proto::SigningInput { + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + ..transfer_d4ay9tdb::sui_transfer_input() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + assert_eq!(output.unsigned_tx, transfer_d4ay9tdb::UNSIGNED_TX); + assert_eq!(output.signature, transfer_d4ay9tdb::SIGNATURE); +} + +#[test] +fn test_sui_sign_split_sui() { + let pay_sui = Proto::PaySui { + input_coins: vec![object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85887685, + "GnzkqXxoowwtz1W33JrjwaW63FpnXmVo8DoVVWUwARyx", + )], + recipients: vec![ + SENDER_54E80D76.into(), + SENDER_54E80D76.into(), + SENDER_54E80D76.into(), + ], + amounts: vec![150_000, 200_000, 100_000], + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay_sui(pay_sui), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.007 SUI + gas_budget: 7000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/GNoQj54Ra8qGbzbvD25KXEYTsRDKTH5SSjLtHftGNwBM + assert_eq!(output.unsigned_tx, "AAAEAAjwSQIAAAAAAAAIQA0DAAAAAAAACKCGAQAAAAAAACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgICAAMBAAABAQABAgABAwMAAAAAAwAAAQADAAACAAEDAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayAWNgILOn3HsRw6pvQZsX+KnBLn95ox0b3S3mcLTt1jAFxYoeBQAAAAAg6qe+uHxDnn7q4cupb3Z1reQK3m4sh6efYtcz8fWA6C9U6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAwM9qAAAAAAAA"); + assert_eq!(output.signature, "AAN/lP/bRRsgdDS/QCSl45D5gHdKv4Aow0Hmkcot6w+84vd2X+nvOgxyYo2BMInBIbsCqlOtnn8t9zo2+dNSegGF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_merge_sui() { + // Coin type: `0x2::sui::SUI`. + let primary_coin = object_ref( + "0x102054b7676a46b1bae724134dc962db729f3389acf79d3d6f3c27ba018a0404", + 85887686, + "J3ZVMi8NUj2cbwB94dYFxTLoDJiBbLj156D7DmBUnVb2", + ); + let primary_coin_balance = 150000; + + // Coin type: `0x2::sui::SUI`. + let coin_to_merge = object_ref( + "0xf3b55a88fe631fdc778621c334941dd82453736fa02c7f6effd4441c38a805cc", + 85887686, + "AT7MeU611cvSpM7B2cQFd53Uiw7WtVwuoQcUcZjWbxou", + ); + let coin_to_merge_balance = 100000; + + let pay = Proto::Pay { + input_coins: vec![primary_coin, coin_to_merge], + recipients: vec![SENDER_54E80D76.into()], + gas: Some(object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85887691, + "JAdejLaC6f59Ko7SugLSuwVn55wVKYE2ukQae4nRJcm5", + )), + amounts: vec![primary_coin_balance + coin_to_merge_balance], + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay(pay), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.004 SUI + gas_budget: 4000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/68wBKsZyYXmCUydDmabQ71kTcFWTfDG7tFmTLk1HgNdN + assert_eq!(output.unsigned_tx, "AAAEAQAQIFS3Z2pGsbrnJBNNyWLbcp8ziaz3nT1vPCe6AYoEBMaKHgUAAAAAIP0+kx97Pe9YDREgUkz6oiMWshB9Lmh378kj8zPFQKydAQDztVqI/mMf3HeGIcM0lB3YJFNzb6Asf27/1EQcOKgFzMaKHgUAAAAAIIxpeFeM16YQBcGe5g1g/yPrBg49nG7O3ONFnBMQpao8AAiQ0AMAAAAAAAAgVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rIDAwEAAAEBAQACAQAAAQECAAEBAwEAAAABAwBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgFjYCCzp9x7EcOqb0GbF/ipwS5/eaMdG90t5nC07dYwBcuKHgUAAAAAIP8OWIzz7zyhJZG6luM+fwC+wc/3IWtHtGWeD/6h5YNwVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rLuAgAAAAAAAAAJPQAAAAAAAA=="); + assert_eq!(output.signature, "AAjKOQKQuLYdWN798F50O0dtLtRWsAa6bl/C4xJHnJaEIpRbYdhlxRXXfcSDpB6/YI14YU5P+auk6KsFGOBZmg2F69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_transfer_all_sui() { + // Coin type: `0x2::sui::SUI`. + let pay_all_sui = Proto::PayAllSui { + input_coins: vec![ + object_ref( + "0x102054b7676a46b1bae724134dc962db729f3389acf79d3d6f3c27ba018a0404", + 85887692, + "AHRJoaqPDuBd9fUcJApoiGF94LAKWzXeUd4M6R3qQdCh", + ), + object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85887692, + "8Q8GsKYTwhUDVMEAuv98P52Wj6HMoQdZdrZNfgsVPStU", + ), + object_ref( + "0xf424e836c9158c53e7361d24ed81875ef06b988465cb75561e035f8f42f00d33", + 85887692, + "74iZSGGCqC61JtERAkXFbPAvWLDwT2fHSQFJbebMtcEJ", + ), + ], + recipient: "0xf887e7077017554511e736d43424363da946d8aa748225f6b054630a0b1c0ae5".into(), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay_all_sui(pay_all_sui), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.004 SUI + gas_budget: 5000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/3yNCCsiEFMyoNcsCniCcSQ9AFZY2WVoQWGa56fcd1nvh + assert_eq!(output.unsigned_tx, "AAABACD4h+cHcBdVRRHnNtQ0JDY9qUbYqnSCJfawVGMKCxwK5QEBAQABAABU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgMQIFS3Z2pGsbrnJBNNyWLbcp8ziaz3nT1vPCe6AYoEBMyKHgUAAAAAIInt9ZC5H/D+LXQVrm5FMVRbXYYja9DzMY7xtj9fmTgOY2Ags6fcexHDqm9Bmxf4qcEuf3mjHRvdLeZwtO3WMAXMih4FAAAAACBt7mQv2i7T+meqMktoDf8lCK0rhyCHnv7MB+dzIwEWXfQk6DbJFYxT5zYdJO2Bh17wa5iEZct1Vh4DX49C8A0zzIoeBQAAAAAgWhna69UsKB/zNdrxzcL1x4N3cD4QnQllvgrYmNa0dqdU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAQEtMAAAAAAAA"); + assert_eq!(output.signature, "AC+cq5DVVb97CpvtgbPer5tC1TpyItXPuvZsC7mQySyrVks/eymaovfZL62zCjtyjM2gpVGt2Hy8xDLIIb5YiAaF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_transfer_token() { + let pay = Proto::Pay { + input_coins: vec![ + // Coin type: `0x1d58e26e85fbf9ee8596872686da75544342487f95b1773be3c9a49ab1061b19::suia_token::SUIA_TOKEN` + object_ref( + "0x69e007e40d4b64528c3d9e519e3d1f3e5c9c962870748171f7b94c168c868161", + 85619063, + "HwJaXWsUXZnd1G8cQVtRmWvBvPKGAZMoTryoJqazNoNK", + ), + ], + recipients: vec![ + "0xa7175abdd5ed92ebe3ad390db366c6a706478cdf517cde6cf98630065cda377a".into(), + ], + amounts: vec![123000], + // Coin type: `0x2::sui::SUI`. + gas: Some(object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85619065, + "9zB7kRVtKQcxCfi47z22u6vQGcUtnYFBXeiuaDfHUudr", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay(pay), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.003 SUI + gas_budget: 4000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/DYRXibkwsy84d9qdKktePE3gj9zN4yFpVx2ehv1uYMCo + assert_eq!(output.unsigned_tx, "AAADAQBp4AfkDUtkUow9nlGePR8+XJyWKHB0gXH3uUwWjIaBYXdxGgUAAAAAIPukOuVHEHFrj96jMXgsE8RmAU/Es2ejPAhD41bNXslEAAh44AEAAAAAAAAgpxdavdXtkuvjrTkNs2bGpwZHjN9RfN5s+YYwBlzaN3oCAgEAAAEBAQABAQMAAAAAAQIAVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rIBY2Ags6fcexHDqm9Bmxf4qcEuf3mjHRvdLeZwtO3WMAV5cRoFAAAAACCFgwpF3YPy9Px5R/K+WLDjMSiO7AsEa/4cNWn5P/nkIVToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ay7gIAAAAAAAAACT0AAAAAAAA="); + assert_eq!(output.signature, "AAXK0so7TwO285ZhWKKRYs2MyFumsFSlOe4boampQKmrqhIZKhNnJKCTFEkarJTq5lIIvyTtgIu5S93gOsxN4guF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_split_tokens() { + let pay = Proto::Pay { + input_coins: vec![ + // Coin type: `0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2::buck::BUCK` + object_ref( + "0xcc94319baba4c4a2f1988805952f1cf0edf9690a150976396491ba72f7ea06f4", + 85887684, + "5ErzJWYsjecyvjBSYg2CXPB76oqqJHCRarkYxsSYEf7c", + ), + ], + recipients: vec![ + SENDER_54E80D76.into(), + SENDER_54E80D76.into(), + SENDER_54E80D76.into(), + ], + amounts: vec![10000000, 7000000, 123], + // Coin type: `0x2::sui::SUI`. + gas: Some(object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85887686, + "HE58PPRBBwksCbB7DMG6RRUcHkR4uJEh5yq5Eax5g546", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay(pay), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.007 SUI + gas_budget: 7000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/6yp2AfESB3od1AMmS7Q6KDbLPJgjNrcTBYR4YSx9nYTR + assert_eq!(output.unsigned_tx, "AAAFAQDMlDGbq6TEovGYiAWVLxzw7flpChUJdjlkkbpy9+oG9MSKHgUAAAAAID770d4E8lYGVpgqgH7V6wveP0upByVv6GPKEis8+F1vAAiAlpgAAAAAAAAIwM9qAAAAAAAACHsAAAAAAAAAACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgICAQAAAwEBAAECAAEDAAEDAwAAAAADAAABAAMAAAIAAQQAVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rIBY2Ags6fcexHDqm9Bmxf4qcEuf3mjHRvdLeZwtO3WMAXGih4FAAAAACDxFDVI2G12qWGuVXlDH+ENYODgVgpT6lVcFhrw27vlu1ToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ay7gIAAAAAAADAz2oAAAAAAAA="); + assert_eq!(output.signature, "AGrbddwztZ+spZCG39obT6Qp+Yv35hXfIPOExVNXzkUpdr7NDMjEMS19BLT6rc811EFiMMVi65G4RmetXGOeUgWF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +/// Merge `primary_coin_id`, `coin_to_merge1` and `coin_to_merge2` coins into one coin. +/// Read the migration guide: https://blog.sui.io/sui-payment-transaction-types/ +#[test] +fn test_sui_sign_merge_tokens() { + // Coin type: `0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2::buck::BUCK` + let primary_coin = object_ref( + "0x7c91902ea14bc1e1a27358d7aa44f7ab9f10890642ae97d03b4e8a4c804662cd", + 85887687, + "DWJeDBNn5Uyb69E6xxoMZL2wupH9VaGxY3Pf7asJfRCQ", + ); + let primary_coin_balance = 10000000; + + // Coin type: `0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2::buck::BUCK` + let coin_to_merge1 = object_ref( + "0x3a5edd52deb7535dadb6cf92b9ed5e0d0eb959a6ce19ea075a3f7e1a8fe29070", + 85887687, + "79CmNvfmneL651e4ND2Kqje13ZJ2sbpGk6oXsa87TkQv", + ); + let coin_to_merge_balance1 = 7000000; + + // Coin type: `0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2::buck::BUCK` + let coin_to_merge2 = object_ref( + "0xcc94319baba4c4a2f1988805952f1cf0edf9690a150976396491ba72f7ea06f4", + 85887687, + "CZbcgfFMyUFmTErb9jewVVcThKPeLqEHk5SvoRtnihHD", + ); + let coin_to_merge_balance2 = 135116; + + let pay = Proto::Pay { + input_coins: vec![primary_coin, coin_to_merge1, coin_to_merge2], + recipients: vec![SENDER_54E80D76.into()], + amounts: vec![primary_coin_balance + coin_to_merge_balance1 + coin_to_merge_balance2], + // Coin type: `0x2::sui::SUI`. + gas: Some(object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85887687, + "GvBAtzvLkhaj2mpC4LnmTH7796zYfHLof4U5dpRStSbn", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::pay(pay), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.007 SUI + gas_budget: 7000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/EadSFJmRbfcJXjWsTwDZ1jUDonN6uWfjEgs79SuG2NCj + assert_eq!(output.unsigned_tx, "AAAFAQB8kZAuoUvB4aJzWNeqRPernxCJBkKul9A7TopMgEZizceKHgUAAAAAILnOCLChQW+Ka6TYqDCKxTKXk7bbxxfROgRAn9cAqdZ1AQA6Xt1S3rdTXa22z5K57V4NDrlZps4Z6gdaP34aj+KQcMeKHgUAAAAAIFtAEid8uKSWbaEakB3Qld75NYy9NE0uUtVG7z2Oj+mHAQDMlDGbq6TEovGYiAWVLxzw7flpChUJdjlkkbpy9+oG9MeKHgUAAAAAIKvKSBp+aB45dA6ogOr30c+9zJ0SI3c+/OczUGBIHJxkAAgMdgUBAAAAAAAgVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rIDAwEAAAIBAQABAgACAQAAAQEDAAEBAwEAAAABBABU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgFjYCCzp9x7EcOqb0GbF/ipwS5/eaMdG90t5nC07dYwBceKHgUAAAAAIOx+ljTbUQkwMHYczKT1iN+DkOpvQYNfaLWlTcnznhGtVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rLuAgAAAAAAAMDPagAAAAAAAA=="); + assert_eq!(output.signature, "AIPDD+jY6aYK0bc2XhtvdSyygk9HKha9WdZjTcRxasDkAi6vq1/a43s9jTV7WuZ7otAXk21eu8zevJ+HB0MABwWF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_delegate_sui() { + let add_stake = Proto::RequestAddStake { + // Coin type: `0x2::sui::SUI`. + coins: vec![ + object_ref( + "0xff1af62d35654956964437882b33d3256aad20214f18a234c62b5e258ca163ee", + 83160977, + "FAugxdfWPQrMu57mMc9FmgNSjkt613pixR6V5M9nashw", + ), + object_ref( + "0x5ef77d20c7d6745d3d9b5f69e7825aae733fa5c8a3f82f7192749e3169791c8c", + 85887695, + "F3JgSqdQJgzBsNnzJiYkr2XkjTEXmq7NEybixjEYrSf4", + ), + ], + // Do not specify the amount. + amount: Some(Proto::Amount { + // 1.00095 + amount: 1_000_000_000 + 1_000_000 - 50_000, + }), + // https://suiscan.xyz/mainnet/validator/0x61953ea72709eed72f4441dd944eec49a11b4acabfc8e04015e89c63be81b6ab/delegators + validator: "0x61953ea72709eed72f4441dd944eec49a11b4acabfc8e04015e89c63be81b6ab".into(), + // Coin type: `0x2::sui::SUI`. + gas: Some(object_ref( + "0x102054b7676a46b1bae724134dc962db729f3389acf79d3d6f3c27ba018a0404", + 85989207, + "GXGhEVNJGNBsvaTiLi85bGask5PbVXTZUKyN6CLR3N7D", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::request_add_stake(add_stake), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.009 SUI + gas_budget: 9000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/9CHdn8h68pnC7pKxFN7ABCCiufFkYQQ6EwFEQEPiz6bp + assert_eq!(output.unsigned_tx, "AAAFAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQEAAAAAAAAAAQEA/xr2LTVlSVaWRDeIKzPTJWqtICFPGKI0xiteJYyhY+6R7/QEAAAAACDSjWt6fM4gT8LU9OmUKUD0oeVAN3195wXyRgLAAkj/RgEAXvd9IMfWdF09m19p54JarnM/pcij+C9xknSeMWl5HIzPih4FAAAAACDQmsUAK2qhMxauQja6zUchci2O+VpXNpKHQPa5uzG92wAJAfBIqTsAAAAAACBhlT6nJwnu1y9EQd2UTuxJoRtKyr/I4EAV6JxjvoG2qwIFAAIBAQABAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMKc3VpX3N5c3RlbRpyZXF1ZXN0X2FkZF9zdGFrZV9tdWxfY29pbgAEAQAAAgAAAQMAAQQAVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rIBECBUt2dqRrG65yQTTcli23KfM4ms9509bzwnugGKBARXFyAFAAAAACDmoHkZ4Q2u0tMpkkJOmnK9WHxAXfwVxtKnoGoU3ZecTFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ay7gIAAAAAAABAVIkAAAAAAAA="); + assert_eq!(output.signature, "AF7oDeTkRQT23xGuW1WsILvm2FQIycaP6bvbTA8oQ8QJU75VQcJDTgEscfxfg8GAN60uzSLKVAJKXKOu8O6vugmF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_undelegate_sui() { + let add_stake = Proto::RequestWithdrawStake { + // Coin type: `0x2::sui::SUI`. + staked_sui: Some(object_ref( + "0x4d3211b1569a24226d672b7de4edb08fa9e19b4f02dab89c9236e4fc8c4ab12d", + 86012336, + "559WJM2RXnQvPyLzRJpUj2bH9ZuZNxd48YJKwNXxtxMn", + )), + // Coin type: `0x2::sui::SUI`. + gas: Some(object_ref( + "0x102054b7676a46b1bae724134dc962db729f3389acf79d3d6f3c27ba018a0404", + 86012337, + "AhhZeVzEF8uGG8rRKv4h4J2MyeUXZvvDYEb1C64pyip7", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::request_withdraw_stake(add_stake), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.009 SUI + gas_budget: 9000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: AwZgU1EoWoo2Zn72U119KRvdjkvUz8QXN3fedLnGXa4n + assert_eq!(output.unsigned_tx, "AAACAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQEAAAAAAAAAAQEATTIRsVaaJCJtZyt95O2wj6nhm08C2rickjbk/IxKsS2wcSAFAAAAACA8frAQitBlYHSw54BYKrEOpjPNXZtUQcp8CBCgeteO2QEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMKc3VpX3N5c3RlbRZyZXF1ZXN0X3dpdGhkcmF3X3N0YWtlAAIBAAABAQBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgEQIFS3Z2pGsbrnJBNNyWLbcp8ziaz3nT1vPCe6AYoEBLFxIAUAAAAAIJAmR388UsDK20u66hpL0Yo017timzGO1w9bTx3rP9fAVOgNdteQwnf1pE886S9T0m9YlIkr85Xe5jdZiIdr5rLuAgAAAAAAAEBUiQAAAAAAAA=="); + assert_eq!(output.signature, "ADbHKQ6TSvyrjll8YwIl+0/BnPR3YedjbLllydFcYL2gCt5AdX2wXZxbwfFgpLCMUPe2tGXzj09MkWsjxEM3XwqF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} + +#[test] +fn test_sui_sign_transfer_nft() { + let transfer_obj = Proto::TransferObject { + // NFT https://suiscan.xyz/mainnet/object/0x2e2355a7e5f857a67c237d27e5a2184f9c683f4275d54bf90dcc70f6117f4a03 + object: Some(object_ref( + "0x2e2355a7e5f857a67c237d27e5a2184f9c683f4275d54bf90dcc70f6117f4a03", + 86012337, + "2XTKVJGNZm7i6ZYGQ6ikZowFu855TpTUTip8JJ1jf1ch", + )), + recipient: "0xf887e7077017554511e736d43424363da946d8aa748225f6b054630a0b1c0ae5".into(), + gas: Some(object_ref( + "0x102054b7676a46b1bae724134dc962db729f3389acf79d3d6f3c27ba018a0404", + 87121030, + "5eWAHWYnidUinZFf3CWNCLSxsUr3c56VVVVofAgaP6bu", + )), + }; + + let input = Proto::SigningInput { + transaction_payload: TransactionType::transfer_object(transfer_obj), + private_key: PRIVATE_KEY_54E80D76.decode_hex().unwrap().into(), + // 0.004 SUI + gas_budget: 4000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::Sui, input); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/zJdcR77RiaMTzq1rURePQdcEFLEKBMUXiiUG2PyGWzR + assert_eq!(output.unsigned_tx, "AAACACD4h+cHcBdVRRHnNtQ0JDY9qUbYqnSCJfawVGMKCxwK5QEALiNVp+X4V6Z8I30n5aIYT5xoP0J11Uv5Dcxw9hF/SgOxcSAFAAAAACAWqN6yiNss1A1yjjz0hYuYwWdS3Dui2QSHjdKsQz08ZgEBAQEBAAEAAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayARAgVLdnakaxuuckE03JYttynzOJrPedPW88J7oBigQEhlwxBQAAAAAgRQo1hDoAiMbl2lgicyjy67PmKIWT5wccUlQMAfu84LxU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAAAk9AAAAAAAA"); + assert_eq!(output.signature, "AIbNoo74XJ9EvfVCBVwM2YMht5qsPHSu4Cb61uzKq6g2tgh4dlhKpY9Shhw/hHjlNGcg590+PvXm4nlj/IWy6wGF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="); +} diff --git a/rust/tw_any_coin/tests/chains/sui/test_cases.rs b/rust/tw_any_coin/tests/chains/sui/test_cases.rs new file mode 100644 index 00000000000..04b824f3261 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/sui/test_cases.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::sui::object_ref; +use tw_proto::Sui::Proto::{self, mod_SigningInput::OneOftransaction_payload as TransactionType}; + +pub(super) const PRIVATE_KEY_54E80D76: &str = + "7e6682f7bf479ef0f627823cffd4e1a940a7af33e5fb39d9e0f631d2ecc5daff"; +pub(super) const SENDER_54E80D76: &str = + "0x54e80d76d790c277f5a44f3ce92f53d26f5894892bf395dee6375988876be6b2"; + +/// Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm +pub(super) mod transfer_d4ay9tdb { + use super::*; + + pub const UNSIGNED_TX: &str = "AAAEAAjoAwAAAAAAAAAIUMMAAAAAAAAAIKcXWr3V7ZLr4605DbNmxqcGR4zfUXzebPmGMAZc2jd6ACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgMCAAIBAAABAQABAQMAAAAAAQIAAQEDAAABAAEDAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayAWNgILOn3HsRw6pvQZsX+KnBLn95ox0b3S3mcLTt1jAFeHEaBQAAAAAgGGuNnxrqusosgjP3gQ3jBjnhapGNBlcU0yTaupXpa0BU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAwMYtAAAAAAAA"; + pub const SIGNATURE: &str = "AEh44B7iGArEHF1wOLAQJMLNgGnaIwn3gKPC92vtDJqITDETAM5z9plaxio1xomt6/cZReQ5FZaQsMC6l7E0BwmF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g=="; + pub const TX_HASH: &str = "4c171457873befef70077461909ae40ac67bdad476b832b9c09b589cd698578f"; + + pub fn sui_transfer_input() -> Proto::SigningInput<'static> { + let pay_sui = Proto::PaySui { + input_coins: vec![object_ref( + "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005", + 85619064, + "2eKuWbZSVfpFVfg8FXY9wP6W5AFXnTchSoUdp7obyYZ5", + )], + recipients: vec![ + "0xa7175abdd5ed92ebe3ad390db366c6a706478cdf517cde6cf98630065cda377a".into(), + // Send some amount to self. + SENDER_54E80D76.into(), + ], + amounts: vec![1000, 50_000], + }; + + Proto::SigningInput { + transaction_payload: TransactionType::pay_sui(pay_sui), + // 0.003 SUI + gas_budget: 3000000, + reference_gas_price: 750, + ..Proto::SigningInput::default() + } + } +} 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 a0929cbec72..4eeb9ed7f01 100644 --- a/rust/tw_any_coin/tests/coin_address_derivation_test.rs +++ b/rust/tw_any_coin/tests/coin_address_derivation_test.rs @@ -152,6 +152,7 @@ fn test_coin_address_derivation() { CoinType::NativeZetaChain => "zeta14s0vgnj0pjnazu4hsqlksdk7slah9vcfcwctsr", CoinType::Dydx => "dydx1ten42eesehw0ktddcp0fws7d3ycsqez3kaamq3", CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC", + CoinType::Sui => "0x1a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/rust/tw_aptos/Cargo.toml b/rust/tw_aptos/Cargo.toml deleted file mode 100644 index 38b0270b8f1..00000000000 --- a/rust/tw_aptos/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "tw_aptos" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde_json = "1.0" -tw_coin_entry = { path = "../tw_coin_entry" } -tw_encoding = { path = "../tw_encoding" } -tw_keypair = { path = "../tw_keypair" } -tw_proto = { path = "../tw_proto" } -tw_number = { path = "../tw_number" } -tw_hash = { path = "../tw_hash" } -tw_memory = { path = "../tw_memory" } -move-core-types = { git = "https://github.com/move-language/move", rev = "ea70797099baea64f05194a918cebd69ed02b285", features = ["address32"] } -serde = { version = "1.0", features = ["derive"] } -serde_bytes = "0.11.12" - -[dev-dependencies] -tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } -tw_encoding = { path = "../tw_encoding" } -tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index 2359e4d6329..dfbbc4b7b0d 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -9,23 +9,24 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = "0.25" strum_macros = "0.25" -tw_aptos = { path = "../tw_aptos" } +tw_aptos = { path = "../chains/tw_aptos" } tw_binance = { path = "../chains/tw_binance" } tw_bitcoin = { path = "../tw_bitcoin" } tw_coin_entry = { path = "../tw_coin_entry" } tw_cosmos = { path = "../chains/tw_cosmos" } -tw_ethereum = { path = "../tw_ethereum" } +tw_ethereum = { path = "../chains/tw_ethereum" } tw_evm = { path = "../tw_evm" } tw_greenfield = { path = "../chains/tw_greenfield" } tw_hash = { path = "../tw_hash" } -tw_internet_computer = { path = "../tw_internet_computer" } +tw_internet_computer = { path = "../chains/tw_internet_computer" } tw_keypair = { path = "../tw_keypair" } tw_memory = { path = "../tw_memory" } 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_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } +tw_sui = { path = "../chains/tw_sui" } 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 19b7198ed90..3aff7a6d250 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -20,6 +20,7 @@ pub enum BlockchainType { NativeInjective, Ronin, Solana, + Sui, 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 c978e17f5c0..5a335fa5c3b 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -20,6 +20,7 @@ use tw_native_evmos::entry::NativeEvmosEntry; use tw_native_injective::entry::NativeInjectiveEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; +use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; @@ -37,6 +38,7 @@ const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; +const SUI: SuiEntry = SuiEntry; const THORCHAIN: ThorchainEntry = ThorchainEntry; // end_of_blockchain_entries - USED TO GENERATE CODE @@ -54,6 +56,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&NATIVE_INJECTIVE), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), + BlockchainType::Sui => Ok(&SUI), BlockchainType::Thorchain => Ok(&THORCHAIN), // end_of_blockchain_dispatcher - USED TO GENERATE CODE BlockchainType::Unsupported => Err(RegistryError::Unsupported), diff --git a/rust/tw_encoding/src/bcs.rs b/rust/tw_encoding/src/bcs.rs index 5f15345bd66..8ed8257e43c 100644 --- a/rust/tw_encoding/src/bcs.rs +++ b/rust/tw_encoding/src/bcs.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::{EncodingError, EncodingResult}; +use serde::de::DeserializeOwned; use serde::Serialize; use tw_memory::Data; @@ -12,3 +13,10 @@ where { bcs::to_bytes(value).map_err(|_| EncodingError::InvalidInput) } + +pub fn decode(bytes: &[u8]) -> EncodingResult +where + T: DeserializeOwned, +{ + bcs::from_bytes(bytes).map_err(|_| EncodingError::InvalidInput) +} diff --git a/rust/tw_encoding/src/hex.rs b/rust/tw_encoding/src/hex.rs index aeed9dd751e..5db6e8038a3 100644 --- a/rust/tw_encoding/src/hex.rs +++ b/rust/tw_encoding/src/hex.rs @@ -3,7 +3,6 @@ // Copyright © 2017 Trust Wallet. pub use hex::FromHexError; -use serde::{Serialize, Serializer}; use tw_memory::Data; pub type FromHexResult = Result; @@ -64,13 +63,31 @@ pub fn encode>(data: T, prefixed: bool) -> String { encoded } -/// Serializes the `value` as a hex. -pub fn as_hex(value: &T, serializer: S) -> Result -where - T: ToHex, - S: Serializer, -{ - value.to_hex().serialize(serializer) +pub mod as_hex { + use super::*; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt; + + /// Serializes the `value` as a hex. + pub fn serialize(value: &T, serializer: S) -> Result + where + T: ToHex, + S: Serializer, + { + value.to_hex().serialize(serializer) + } + + pub fn deserialize<'de, D, T, E>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: for<'a> TryFrom<&'a [u8], Error = E>, + E: fmt::Debug, + { + let s = String::deserialize(deserializer)?; + let data = decode(&s).map_err(|e| Error::custom(format!("{e:?}")))?; + T::try_from(&data).map_err(|e| Error::custom(format!("Error parsing from bytes: {e:?}"))) + } } #[cfg(test)] diff --git a/rust/tw_ethereum/Cargo.toml b/rust/tw_ethereum/Cargo.toml deleted file mode 100644 index 2b8f6554a62..00000000000 --- a/rust/tw_ethereum/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "tw_ethereum" -version = "0.1.0" -edition = "2021" - -[dependencies] -tw_coin_entry = { path = "../tw_coin_entry" } -tw_evm = { path = "../tw_evm" } -tw_keypair = { path = "../tw_keypair" } -tw_proto = { path = "../tw_proto" } - -[dev-dependencies] -tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } -tw_encoding = { path = "../tw_encoding" } -tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/tw_evm/src/modules/abi_encoder.rs b/rust/tw_evm/src/modules/abi_encoder.rs index 1e2b746897d..de70367d618 100644 --- a/rust/tw_evm/src/modules/abi_encoder.rs +++ b/rust/tw_evm/src/modules/abi_encoder.rs @@ -18,6 +18,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::marker::PhantomData; use std::str::FromStr; +use tw_encoding::hex::as_hex; use tw_hash::H32; use tw_misc::traits::ToBytesVec; use tw_number::{I256, U256}; @@ -88,7 +89,7 @@ impl AbiEncoder { let function = abi_json .map - .get_mut(&short_signature) + .get_mut(&ContractCallSignature(short_signature)) .ok_or(AbiError(AbiErrorKind::Error_abi_mismatch))?; let decoded_tokens = function.decode_input(encoded_data)?; @@ -451,9 +452,12 @@ impl AbiEncoder { #[derive(Deserialize)] struct SmartContractCallAbiJson { #[serde(flatten)] - map: HashMap, + map: HashMap, } +#[derive(Eq, Deserialize, Hash, PartialEq, Serialize)] +struct ContractCallSignature(#[serde(with = "as_hex")] H32); + #[derive(Serialize)] struct SmartContractCallDecodedInputJson<'a> { function: String, diff --git a/rust/tw_hash/src/hash_array.rs b/rust/tw_hash/src/hash_array.rs index 5732f56f8ff..80e2c1877e3 100644 --- a/rust/tw_hash/src/hash_array.rs +++ b/rust/tw_hash/src/hash_array.rs @@ -177,28 +177,25 @@ impl fmt::Display for Hash { } #[cfg(feature = "serde")] -mod impl_serde { +pub mod as_bytes { use super::Hash; use serde::de::Error; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde::{Deserialize, Deserializer, Serializer}; + use tw_memory::Data; - impl<'de, const N: usize> Deserialize<'de> for Hash { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let hex = String::deserialize(deserializer)?; - hex.parse().map_err(|e| Error::custom(format!("{e:?}"))) - } + pub fn deserialize<'de, const N: usize, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let bytes = Data::deserialize(deserializer)?; + Hash::::try_from(bytes.as_slice()).map_err(|e| Error::custom(format!("{e:?}"))) } - impl Serialize for Hash { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.to_string().serialize(serializer) - } + pub fn serialize(hash: &Hash, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&hash.0) } } @@ -335,7 +332,9 @@ mod tests { #[cfg(all(test, feature = "serde"))] mod serde_tests { use super::*; + use serde::{Deserialize, Serialize}; use serde_json::json; + use tw_encoding::hex::as_hex; const BYTES_32: [u8; 32] = [ 175u8, 238, 252, 167, 77, 154, 50, 92, 241, 214, 182, 145, 29, 97, 166, 92, 50, 175, 168, @@ -343,27 +342,60 @@ mod serde_tests { ]; const HEX_32: &str = "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5"; + #[derive(Debug, Deserialize, Serialize)] + struct TestAsHex { + #[serde(with = "as_hex")] + data: Hash<32>, + } + + #[derive(Debug, Deserialize, Serialize)] + struct TestAsByteSequence { + #[serde(with = "as_byte_sequence")] + data: Hash<32>, + } + + #[test] + fn test_hash_deserialize_as_byte_sequence() { + let res: TestAsByteSequence = serde_json::from_value(json!({"data": BYTES_32})).unwrap(); + assert_eq!(res.data.0, BYTES_32); + } + #[test] - fn test_hash_deserialize() { - let unprefixed: Hash<32> = serde_json::from_value(json!(HEX_32)).unwrap(); - assert_eq!(unprefixed.0, BYTES_32); + fn test_hash_serialize_as_byte_sequence() { + let res = TestAsByteSequence { + data: Hash::<32>::from(BYTES_32), + }; + assert_eq!( + serde_json::to_value(&res).unwrap(), + json!({"data": BYTES_32}) + ); + } + + #[test] + fn test_hash_deserialize_as_hex() { + let unprefixed: TestAsHex = serde_json::from_value(json!({"data": HEX_32})).unwrap(); + + assert_eq!(unprefixed.data.0, BYTES_32); - let prefixed: Hash<32> = serde_json::from_value(json!(HEX_32)).unwrap(); - assert_eq!(prefixed.0, BYTES_32); + let prefixed: TestAsHex = + serde_json::from_value(json!({"data": format!("0x{HEX_32}")})).unwrap(); + assert_eq!(prefixed.data.0, BYTES_32); } #[test] - fn test_hash_deserialize_error() { - serde_json::from_value::>(json!( - "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45" - )) + fn test_hash_deserialize_as_hex_error() { + serde_json::from_value::( + json!({"data": "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45"}), + ) .unwrap_err(); } #[test] - fn test_hash_serialize() { - let hash = Hash::<32>::from(HEX_32); - let actual = serde_json::to_value(&hash).unwrap(); - assert_eq!(actual, json!(HEX_32)); + fn test_hash_serialize_as_hex() { + let test = TestAsHex { + data: Hash::<32>::from(HEX_32), + }; + let actual = serde_json::to_value(&test).unwrap(); + assert_eq!(actual, json!({"data": HEX_32})); } } diff --git a/rust/tw_hash/src/lib.rs b/rust/tw_hash/src/lib.rs index 48838e77569..0e83ce463b6 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::{as_byte_sequence, concat, Hash, H160, H256, H264, H32, H512, H520}; +pub use hash_array::{as_byte_sequence, as_bytes, concat, Hash, H160, H256, H264, H32, H512, H520}; use tw_encoding::hex::FromHexError; diff --git a/rust/tw_internet_computer/Cargo.toml b/rust/tw_internet_computer/Cargo.toml deleted file mode 100644 index e40261494e7..00000000000 --- a/rust/tw_internet_computer/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "tw_internet_computer" -version = "0.1.0" -edition = "2021" - -[dependencies] -quick-protobuf = "0.8.1" -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" } - -[build-dependencies] -pb-rs = "0.10.0" diff --git a/rust/tw_keypair/src/ed25519/signature.rs b/rust/tw_keypair/src/ed25519/signature.rs index 43051c4325e..55592e50a6a 100644 --- a/rust/tw_keypair/src/ed25519/signature.rs +++ b/rust/tw_keypair/src/ed25519/signature.rs @@ -37,6 +37,9 @@ pub struct Signature { } impl Signature { + /// cbindgen:ignore + pub const LEN: usize = H512::LEN; + /// Returns the signature data (64 bytes). pub fn to_bytes(&self) -> H512 { let left = H256::from(self.R.to_bytes()); diff --git a/rust/tw_keypair/tests/ed25519_blake2b_tests.rs b/rust/tw_keypair/tests/ed25519_blake2b_tests.rs index 53e8e718d84..a8f4cacb02e 100644 --- a/rust/tw_keypair/tests/ed25519_blake2b_tests.rs +++ b/rust/tw_keypair/tests/ed25519_blake2b_tests.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; -use tw_encoding::hex; +use tw_encoding::hex::{self, as_hex}; use tw_hash::{H256, H512}; use tw_keypair::ed25519::blake2b::KeyPair; use tw_keypair::traits::{SigningKeyTrait, VerifyingKeyTrait}; @@ -13,8 +13,10 @@ const ED25519_BLAKE2B_SIGN: &str = include_str!("ed25519_blake2b_sign.json"); #[derive(Deserialize)] struct Ed255191SignTest { + #[serde(with = "as_hex")] secret: H256, msg: String, + #[serde(with = "as_hex")] signature: H512, } diff --git a/rust/tw_keypair/tests/ed25519_extended_cardano_tests.rs b/rust/tw_keypair/tests/ed25519_extended_cardano_tests.rs index 42f2b17bca7..ed2e021b961 100644 --- a/rust/tw_keypair/tests/ed25519_extended_cardano_tests.rs +++ b/rust/tw_keypair/tests/ed25519_extended_cardano_tests.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; -use tw_encoding::hex; +use tw_encoding::hex::{self, as_hex}; use tw_hash::H512; use tw_keypair::ed25519::cardano::ExtendedKeyPair; use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait, VerifyingKeyTrait}; @@ -18,6 +18,7 @@ const ED25519_EXTENDED_CARDANO_PRIV_TO_PUB: &str = struct Ed255191ExtendedCardanoSignTest { secret: String, msg: String, + #[serde(with = "as_hex")] signature: H512, } diff --git a/rust/tw_keypair/tests/ed25519_tests.rs b/rust/tw_keypair/tests/ed25519_tests.rs index 4b76afb5613..57dbe81c2e4 100644 --- a/rust/tw_keypair/tests/ed25519_tests.rs +++ b/rust/tw_keypair/tests/ed25519_tests.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; -use tw_encoding::hex; +use tw_encoding::hex::{self, as_hex}; use tw_hash::{H256, H512}; use tw_keypair::ed25519::sha512::KeyPair; use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait, VerifyingKeyTrait}; @@ -15,14 +15,18 @@ const ED25519_PRIV_TO_PUB: &str = include_str!("ed25519_priv_to_pub.json"); #[derive(Deserialize)] struct Ed255191SignTest { + #[serde(with = "as_hex")] secret: H256, msg: String, + #[serde(with = "as_hex")] signature: H512, } #[derive(Deserialize)] struct Ed255191PrivToPubTest { + #[serde(with = "as_hex")] secret: H256, + #[serde(with = "as_hex")] public: H256, } diff --git a/rust/tw_keypair/tests/ed25519_waves_tests.rs b/rust/tw_keypair/tests/ed25519_waves_tests.rs index 7b39d226383..c231ee362b1 100644 --- a/rust/tw_keypair/tests/ed25519_waves_tests.rs +++ b/rust/tw_keypair/tests/ed25519_waves_tests.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; -use tw_encoding::hex; +use tw_encoding::hex::{self, as_hex}; use tw_hash::{H256, H512}; use tw_keypair::ed25519::waves::KeyPair; use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait, VerifyingKeyTrait}; @@ -15,14 +15,18 @@ const ED25519_WAVES_PRIV_TO_PUB: &str = include_str!("ed25519_waves_priv_to_pub. #[derive(Deserialize)] struct Ed255191WavesSignTest { + #[serde(with = "as_hex")] secret: H256, msg: String, + #[serde(with = "as_hex")] signature: H512, } #[derive(Deserialize)] struct Ed255191WavesPrivToPubTest { + #[serde(with = "as_hex")] secret: H256, + #[serde(with = "as_hex")] public: H256, } diff --git a/rust/tw_keypair/tests/nist256p1_tests.rs b/rust/tw_keypair/tests/nist256p1_tests.rs index 8e60832de80..a3a603f9bd3 100644 --- a/rust/tw_keypair/tests/nist256p1_tests.rs +++ b/rust/tw_keypair/tests/nist256p1_tests.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; +use tw_encoding::hex::as_hex; use tw_hash::{H256, H264, H520}; use tw_keypair::ecdsa::nist256p1::{PrivateKey, PublicKey, VerifySignature}; use tw_keypair::traits::VerifyingKeyTrait; @@ -14,14 +15,19 @@ const NIST256P1_PRIV_TO_PUB_COMPRESSED: &str = #[derive(Deserialize)] struct Nist256p1VerifyTest { + #[serde(with = "as_hex")] public: H264, + #[serde(with = "as_hex")] msg: H256, + #[serde(with = "as_hex")] signature: H520, } #[derive(Deserialize)] struct Nist256p1PrivToPubCompressedTest { + #[serde(with = "as_hex")] secret: H256, + #[serde(with = "as_hex")] public: H264, } diff --git a/rust/tw_keypair/tests/secp256k1_tests.rs b/rust/tw_keypair/tests/secp256k1_tests.rs index f320cdcc32c..9e56c024862 100644 --- a/rust/tw_keypair/tests/secp256k1_tests.rs +++ b/rust/tw_keypair/tests/secp256k1_tests.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use serde::Deserialize; +use tw_encoding::hex::as_hex; use tw_hash::{H256, H520}; use tw_keypair::ecdsa::secp256k1::{KeyPair, VerifySignature}; use tw_keypair::traits::{SigningKeyTrait, VerifyingKeyTrait}; @@ -12,8 +13,11 @@ const SECP256K1_SIGN: &str = include_str!("secp256k1_sign.json"); #[derive(Deserialize)] struct Secp256k1SignTest { + #[serde(with = "as_hex")] secret: H256, + #[serde(with = "as_hex")] hash: H256, + #[serde(with = "as_hex")] signature: H520, } diff --git a/rust/tw_ronin/Cargo.toml b/rust/tw_ronin/Cargo.toml deleted file mode 100644 index 98a13847890..00000000000 --- a/rust/tw_ronin/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "tw_ronin" -version = "0.1.0" -edition = "2021" - -[dependencies] -tw_coin_entry = { path = "../tw_coin_entry" } -tw_evm = { path = "../tw_evm" } -tw_keypair = { path = "../tw_keypair" } -tw_memory = { path = "../tw_memory" } -tw_proto = { path = "../tw_proto" } - -[dev-dependencies] -tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } -tw_encoding = { path = "../tw_encoding" } -tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index d0c43170fdf..eb148ecab18 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -35,7 +35,7 @@ tw_any_coin = { path = "../tw_any_coin", optional = true } tw_bitcoin = { path = "../tw_bitcoin", optional = true } tw_coin_registry = { path = "../tw_coin_registry", optional = true } tw_encoding = { path = "../tw_encoding", optional = true } -tw_ethereum = { path = "../tw_ethereum", optional = true } +tw_ethereum = { path = "../chains/tw_ethereum", optional = true } tw_hash = { path = "../tw_hash", optional = true } tw_keypair = { path = "../tw_keypair", optional = true } tw_memory = { path = "../tw_memory", optional = true } diff --git a/src/Sui/Address.cpp b/src/Sui/Address.cpp deleted file mode 100644 index bc432805e94..00000000000 --- a/src/Sui/Address.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#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, TW::Hash::HasherBlake2b) { -} - -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 deleted file mode 100644 index 74d5fd6ad27..00000000000 --- a/src/Sui/Address.h +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#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 deleted file mode 100644 index cc537615924..00000000000 --- a/src/Sui/Entry.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Entry.h" - -#include "Address.h" -#include "Signer.h" - -#include "proto/TransactionCompiler.pb.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); -} - -Data Entry::preImageHashes([[maybe_unused]] TWCoinType coin, const Data& txInputData) const { - return txCompilerTemplate( - txInputData, [](const auto& input, auto& output) { - output = Signer::preImageHashes(input); - }); -} - -void Entry::compile([[maybe_unused]] TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const { - dataOut = txCompilerSingleTemplate( - txInputData, signatures, publicKeys, - [](const auto& input, auto& output, const auto& signature, const auto& publicKey) { - auto txSignatureScheme = Signer::signatureScheme(signature, publicKey); - output.set_unsigned_tx(input.sign_direct_message().unsigned_tx_msg()); - output.set_signature(txSignatureScheme); - }); -} - -} // namespace TW::Sui diff --git a/src/Sui/Entry.h b/src/Sui/Entry.h index 160520b6db7..86106198b80 100644 --- a/src/Sui/Entry.h +++ b/src/Sui/Entry.h @@ -4,17 +4,11 @@ #pragma once -#include "CoinEntry.h" +#include "rust/RustCoinEntry.h" namespace TW::Sui { -class Entry final : public CoinEntry { -public: - bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const override; - std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const override; - void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const override; - Data preImageHashes(TWCoinType coin, const Data& txInputData) const override; - void compile(TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const override; +class Entry final : public Rust::RustCoinEntry { }; } // namespace TW::Sui diff --git a/src/Sui/Signer.cpp b/src/Sui/Signer.cpp deleted file mode 100644 index 29984779b65..00000000000 --- a/src/Sui/Signer.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#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) { - auto protoOutput = Proto::SigningOutput(); - auto privateKey = PrivateKey(Data(input.private_key().begin(), input.private_key().end())); - - auto toSign = transactionPreimage(input); - auto signature = privateKey.sign(TW::Hash::blake2b(toSign, 32), TWCurveED25519); - auto publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519); - auto txSignatureScheme = signatureScheme(signature, publicKey); - - auto unsignedTx = input.sign_direct_message().unsigned_tx_msg(); - protoOutput.set_unsigned_tx(unsignedTx); - protoOutput.set_signature(txSignatureScheme); - return protoOutput; -} - -TxCompiler::Proto::PreSigningOutput Signer::preImageHashes(const Proto::SigningInput& input) { - TxCompiler::Proto::PreSigningOutput output; - auto preImage = Signer::transactionPreimage(input); - auto preImageHash = TW::Hash::blake2b(preImage, 32); - output.set_data(preImage.data(), preImage.size()); - output.set_data_hash(preImageHash.data(), preImageHash.size()); - return output; -} - -Data Signer::transactionPreimage(const Proto::SigningInput& input) { - auto unsignedTx = input.sign_direct_message().unsigned_tx_msg(); - auto unsignedTxData = TW::Base64::decode(unsignedTx); - Data toSign{TransactionData, V0, IntentAppId::Sui}; - append(toSign, unsignedTxData); - return toSign; -} - -std::string Signer::signatureScheme(const Data& signature, const PublicKey& publicKey) { - Data signatureScheme{0x00}; - append(signatureScheme, signature); - append(signatureScheme, publicKey.bytes); - return TW::Base64::encode(signatureScheme); -} - -// Data - -} // namespace TW::Sui diff --git a/src/Sui/Signer.h b/src/Sui/Signer.h deleted file mode 100644 index a23f59454f9..00000000000 --- a/src/Sui/Signer.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "PrivateKey.h" -#include "proto/Sui.pb.h" -#include "proto/TransactionCompiler.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); - - static TxCompiler::Proto::PreSigningOutput preImageHashes(const Proto::SigningInput& input); - - /// Get transaction data to be signed (with a type tag). - static Data transactionPreimage(const Proto::SigningInput& input); - - static std::string signatureScheme(const Data& signature, const PublicKey& publicKey); -}; - -} // namespace TW::Sui diff --git a/src/proto/Sui.proto b/src/proto/Sui.proto index 5b11812db41..1efb8798b8c 100644 --- a/src/proto/Sui.proto +++ b/src/proto/Sui.proto @@ -9,20 +9,133 @@ option java_package = "wallet.core.jni.proto"; import "Common.proto"; +// Object info (including Coins). +message ObjectRef { + // Hex string representing the object ID. + string object_id = 1; + // Object version. + uint64 version = 2; + // Base58 string representing the object digest. + string object_digest = 3; +} + +// Optional amount. +message Amount { + uint64 amount = 1; +} + // Base64 encoded msg to sign (string) message SignDirect { // Obtain by calling any write RpcJson on SUI string unsigned_tx_msg = 1; } +// Send `Coin` to a list of addresses, where T can be any coin type, following a list of amounts. +// The object specified in the gas field will be used to pay the gas fee for the transaction. +// The gas object can not appear in input_coins. +// https://docs.sui.io/sui-api-ref#unsafe_pay +message Pay { + // The Sui coins to be used in this transaction, including the coin for gas payment. + repeated ObjectRef input_coins = 1; + + // The recipients' addresses, the length of this vector must be the same as amounts. + repeated string recipients = 2; + + // The amounts to be transferred to recipients, following the same order. + repeated uint64 amounts = 3; + + // Gas object to be used in this transaction. + ObjectRef gas = 4; +} + +// Send SUI coins to a list of addresses, following a list of amounts. +// This is for SUI coin only and does not require a separate gas coin object. +// https://docs.sui.io/sui-api-ref#unsafe_paysui +message PaySui { + // The Sui coins to be used in this transaction, including the coin for gas payment. + repeated ObjectRef input_coins = 1; + + // The recipients' addresses, the length of this vector must be the same as amounts. + repeated string recipients = 2; + + // The amounts to be transferred to recipients, following the same order. + repeated uint64 amounts = 3; +} + +// Send all SUI coins to one recipient. +// This is for SUI coin only and does not require a separate gas coin object. +// https://docs.sui.io/sui-api-ref#unsafe_payallsui +message PayAllSui { + // The Sui coins to be used in this transaction, including the coin for gas payment. + repeated ObjectRef input_coins = 1; + + // The recipient address. + string recipient = 2; +} + +// Add stake to a validator's staking pool using multiple coins and amount. +// https://docs.sui.io/sui-api-ref#unsafe_requestaddstake +message RequestAddStake { + // Coin objects to stake. + repeated ObjectRef coins = 1; + + // Optional stake amount. + Amount amount = 2; + + // The validator's Sui address. + string validator = 3; + + // Gas object to be used in this transaction. + ObjectRef gas = 4; +} + +// Withdraw stake from a validator's staking pool. +// https://docs.sui.io/sui-api-ref#unsafe_requestwithdrawstake +message RequestWithdrawStake { + // StakedSui object ID. + ObjectRef staked_sui = 1; + + // Gas object to be used in this transaction. + ObjectRef gas = 2; +} + +/// Transfer an object from one address to another. The object's type must allow public transfers. +/// https://docs.sui.io/sui-api-ref#unsafe_transferobject +message TransferObject { + // Object ID to be transferred. + ObjectRef object = 1; + + // The recipient address. + string recipient = 2; + + // Gas object to be used in this transaction. + ObjectRef gas = 3; +} + // Input data necessary to create a signed transaction. message SigningInput { - // Private key to sign the transaction (bytes) + // Private key to sign the transaction (bytes). bytes private_key = 1; + // Optional transaction signer. + // Needs to be set if no private key provided at `TransactionCompiler` module. + string signer = 2; + oneof transaction_payload { - SignDirect sign_direct_message = 2; + SignDirect sign_direct_message = 3; + Pay pay = 4; + PaySui pay_sui = 5; + PayAllSui pay_all_sui = 6; + RequestAddStake request_add_stake = 7; + RequestWithdrawStake request_withdraw_stake = 8; + TransferObject transfer_object = 9; } + + // The gas budget, the transaction will fail if the gas cost exceed the budget. + uint64 gas_budget = 12; + + // Reference gas price. + uint64 reference_gas_price = 13; } // Transaction signing output. diff --git a/swift/Tests/Blockchains/SuiTests.swift b/swift/Tests/Blockchains/SuiTests.swift index 7e8092cd805..868cb6947ec 100644 --- a/swift/Tests/Blockchains/SuiTests.swift +++ b/swift/Tests/Blockchains/SuiTests.swift @@ -18,7 +18,38 @@ class SuiTests: XCTestCase { XCTAssertFalse(AnyAddress.isValid(string: invalid, coin: .sui)) } - func testSign() { + func testSignDirect() { + // Successfully broadcasted: https://suiscan.xyz/mainnet/tx/D4Ay9TdBJjXkGmrZSstZakpEWskEQHaWURP6xWPRXbAm + let privateKeyData = Data(hexString: "7e6682f7bf479ef0f627823cffd4e1a940a7af33e5fb39d9e0f631d2ecc5daff")! + let txBytes = """ +AAAEAAjoAwAAAAAAAAAIUMMAAAAAAAAAIKcXWr3V7ZLr4605DbNmxqcGR4zfUXzebPmGMAZc2jd6ACBU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsgMCAAIBAAABAQABAQMAAAAAAQIAAQEDAAABAAEDAFToDXbXkMJ39aRPPOkvU9JvWJSJK/OV3uY3WYiHa+ayAWNgILOn3HsRw6pvQZsX+KnBLn95ox0b3S3mcLTt1jAFeHEaBQAAAAAgGGuNnxrqusosgjP3gQ3jBjnhapGNBlcU0yTaupXpa0BU6A1215DCd/WkTzzpL1PSb1iUiSvzld7mN1mIh2vmsu4CAAAAAAAAwMYtAAAAAAAA +""" + + let input = SuiSigningInput.with { + $0.paySui = SuiPaySui.with { + $0.inputCoins = [SuiObjectRef.with { + $0.objectID = "0x636020b3a7dc7b11c3aa6f419b17f8a9c12e7f79a31d1bdd2de670b4edd63005" + $0.version = 85619064 + $0.objectDigest = "2eKuWbZSVfpFVfg8FXY9wP6W5AFXnTchSoUdp7obyYZ5" + }] + $0.recipients = [ + "0xa7175abdd5ed92ebe3ad390db366c6a706478cdf517cde6cf98630065cda377a", + "0x54e80d76d790c277f5a44f3ce92f53d26f5894892bf395dee6375988876be6b2" + ] + $0.amounts = [1000, 50000] + } + $0.privateKey = privateKeyData + // 0.003 SUI + $0.gasBudget = 3000000 + $0.referenceGasPrice = 750 + } + let output: SuiSigningOutput = AnySigner.sign(input: input, coin: .sui) + XCTAssertEqual(output.unsignedTx, txBytes) + let expectedSignature = "AEh44B7iGArEHF1wOLAQJMLNgGnaIwn3gKPC92vtDJqITDETAM5z9plaxio1xomt6/cZReQ5FZaQsMC6l7E0BwmF69FEH+T5VPvl3GB3vwCOEZpeJpKXxvcIPQAdKsh2/g==" + XCTAssertEqual(output.signature, expectedSignature) + } + + func testTransferSui() { // Successfully broadcasted https://explorer.sui.io/txblock/HkPo6rYPyDY53x1MBszvSZVZyixVN7CHvCJGX381czAh?network=devnet let privateKeyData = Data(hexString: "3823dce5288ab55dd1c00d97e91933c613417fdb282a0b8b01a7f5f5a533b266")! let txBytes = """ diff --git a/tests/chains/Sui/AddressTests.cpp b/tests/chains/Sui/AddressTests.cpp deleted file mode 100644 index e42f6a5d55d..00000000000 --- a/tests/chains/Sui/AddressTests.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#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 32 are valid in SUI - ASSERT_TRUE(Address::isValid("0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015")); - ASSERT_TRUE(Address::isValid("259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015")); -} - -TEST(SuiAddress, Invalid) { - // Address 20 are invalid in SUI - ASSERT_FALSE(Address::isValid("0xb1dc06bd64d4e179a482b97bb68243f6c02c1b92")); - ASSERT_FALSE(Address::isValid("b1dc06bd64d4e179a482b97bb68243f6c02c1b92")); - // Too long - ASSERT_FALSE(Address::isValid("d575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d502")); - // Too short - ASSERT_FALSE(Address::isValid("b1dc06bd64d4e179a482b97bb68243f6c02c1b9")); - // Invalid short address - ASSERT_FALSE(Address::isValid("0x11")); - // Invalid Hex - ASSERT_FALSE(Address::isValid("0xS59ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015")); -} - -TEST(SuiAddress, FromString) { - auto address = Address("259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015"); - ASSERT_EQ(address.string(), "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015"); -} - -TEST(SuiAddress, FromPrivateKey) { - auto privateKey = PrivateKey(parse_hex("088baa019f081d6eab8dff5c447f9ce2f83c1babf3d03686299eaf6a1e89156e")); - auto address = Address(privateKey.getPublicKey(TWPublicKeyTypeED25519)); - ASSERT_EQ(address.string(), "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015"); -} - -TEST(SuiAddress, FromPublicKey) { - auto publicKey = PublicKey(parse_hex("ad0e293a56c9fc648d1872a00521d97e6b65724519a2676c2c47cb95d131cf5a"), TWPublicKeyTypeED25519); - auto address = Address(publicKey); - ASSERT_EQ(address.string(), "0x259ff8074ab425cbb489f236e18e08f03f1a7856bdf7c7a2877bd64f738b5015"); -} - -} // namespace TW::Sui::tests diff --git a/tests/chains/Sui/CompilerTests.cpp b/tests/chains/Sui/CompilerTests.cpp index cbe887f78d5..b496546ff43 100644 --- a/tests/chains/Sui/CompilerTests.cpp +++ b/tests/chains/Sui/CompilerTests.cpp @@ -2,9 +2,10 @@ // // Copyright © 2017 Trust Wallet. -#include "Sui/Signer.h" #include "HexCoding.h" #include "PrivateKey.h" +#include "proto/Sui.pb.h" +#include "proto/TransactionCompiler.pb.h" #include "PublicKey.h" #include "TransactionCompiler.h" diff --git a/tests/chains/Sui/SignerTests.cpp b/tests/chains/Sui/SignerTests.cpp index eb61ce26ef0..ce801ca9113 100644 --- a/tests/chains/Sui/SignerTests.cpp +++ b/tests/chains/Sui/SignerTests.cpp @@ -2,11 +2,11 @@ // // Copyright © 2017 Trust Wallet. -#include "Sui/Signer.h" -#include "Sui/Address.h" #include "HexCoding.h" #include "PrivateKey.h" +#include "proto/Sui.pb.h" #include "PublicKey.h" +#include "TestUtilities.h" #include @@ -19,45 +19,11 @@ TEST(SuiSigner, Transfer) { 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(), "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"); - ASSERT_EQ(result.signature(), "APxPduNVvHj2CcRcHOtiP2aBR9qP3vO2Cb0g12PI64QofDB6ks33oqe/i/iCTLcop2rBrkczwrayZuJOdi7gvwNqfN7sFqdcD/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(), "AI+KRy820ucibONQXbaVm53ixNWqRcqp16/aG0hvX7Mt3dOMqTDKRYoRBRvbMDsyPFmpS+n5iYvs5vuGdqjUvgBqfN7sFqdcD/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(), "AHoX1/mzUS8WQ+tNr0gXtfI7KFXjSbDlUxbGG2gkEh6L8FngU2KrsXsR1N8MzCXyJIz7+YvTfl5+Dh6AWSZC5wVqfN7sFqdcD/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(), "AMn4XpOcE9pX/VWCcue/tMkk+TxRQprGas53TT9W4beLkj6XuQdSNLSdjp9AmbqQPHKh0yJZ9i7Q2i6aax8NdQZqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); + Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypeSui); + ASSERT_EQ(output.unsigned_tx(), "AAACAAgQJwAAAAAAAAAgJZ/4B0q0Jcu0ifI24Y4I8D8aeFa998eih3vWT3OLUBUCAgABAQAAAQEDAAAAAAEBANV1rX8Y6UhGKlz2mPVk7zlKdSpx/sYkk6+KBVwBLA1QAQbywsjB2JZN8QGdZhbpcFcZvrq9kx2idVy5SM635olk7AIAAAAAAAAgYEVuxmf1zRBGdoDr+VDtMpIFF12s2Ua7I2ru1XyGF8/Vda1/GOlIRipc9pj1ZO85SnUqcf7GJJOvigVcASwNUAEAAAAAAAAA0AcAAAAAAAAA"); + ASSERT_EQ(output.signature(), "APxPduNVvHj2CcRcHOtiP2aBR9qP3vO2Cb0g12PI64QofDB6ks33oqe/i/iCTLcop2rBrkczwrayZuJOdi7gvwNqfN7sFqdcD/Z4e8I1YQlGkDMCK7EOgmydRDqfH8C9jg=="); } } // namespace TW::Sui::tests diff --git a/tests/common/HDWallet/HDWalletTests.cpp b/tests/common/HDWallet/HDWalletTests.cpp index 32d68f6b006..355036071c6 100644 --- a/tests/common/HDWallet/HDWalletTests.cpp +++ b/tests/common/HDWallet/HDWalletTests.cpp @@ -8,7 +8,6 @@ #include "Bitcoin/SegwitAddress.h" #include "IoTeX/Address.h" #include "Cosmos/Address.h" -#include "Sui/Address.h" #include "Coin.h" #include "Ethereum/Address.h" #include "Ethereum/EIP2645.h" @@ -440,18 +439,6 @@ 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(), "0xd575ad7f18e948462a5cf698f564ef394a752a71fec62493af8a055c012c0d50"); - } -} - TEST(HDWallet, HederaKey) { // https://github.com/hashgraph/hedera-sdk-js/blob/e0cd39c84ab189d59a6bcedcf16e4102d7bb8beb/packages/cryptography/test/unit/Mnemonic.js#L47 { diff --git a/tests/common/TestUtilities.h b/tests/common/TestUtilities.h index 35949c7166d..28ccc30584d 100644 --- a/tests/common/TestUtilities.h +++ b/tests/common/TestUtilities.h @@ -4,6 +4,8 @@ #pragma once +#include +#include #include #include