diff --git a/docs/registry.md b/docs/registry.md index acb153ce4f6..af3fedbfe62 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -92,6 +92,7 @@ This list is generated from [./registry.json](../registry.json) | 4200 | Merlin | BTC | | | | 5000 | Mantle | MNT | | | | 5600 | BNB Greenfield | BNB | | | +| 5757 | Stacks | STX | | | | 6001 | BounceBit | BB | | | | 6060 | GoChain | GO | | | | 7332 | Zen EON | ZEN | | | diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index 519cf9c4429..2f4029587c3 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -69,6 +69,7 @@ enum TWBlockchain { TWBlockchainPactus = 56, TWBlockchainKomodo = 57, TWBlockchainPolymesh = 58, // Substrate + TWBlockchainStacks = 59, }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index 3ee41d4b1a9..3a7974e7629 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -191,6 +191,7 @@ enum TWCoinType { TWCoinTypePolymesh = 595, TWCoinTypePlasma = 9745, TWCoinTypeMonad = 10143, + TWCoinTypeStacks = 5757, // end_of_tw_coin_type_marker_do_not_modify }; diff --git a/include/TrustWalletCore/TWDerivation.h b/include/TrustWalletCore/TWDerivation.h index 41eebac3494..30934d10ad2 100644 --- a/include/TrustWalletCore/TWDerivation.h +++ b/include/TrustWalletCore/TWDerivation.h @@ -28,6 +28,8 @@ enum TWDerivation { TWDerivationPactusMainnet = 9, TWDerivationPactusTestnet = 10, TWDerivationSmartChainStableAccount = 11, + TWDerivationStacksMainnet = 12, + TWDerivationStacksTestnet = 13, // end_of_derivation_enum - USED TO GENERATE CODE }; diff --git a/registry.json b/registry.json index 8d0ed91e1bd..2a58fb69ccf 100644 --- a/registry.json +++ b/registry.json @@ -4945,5 +4945,44 @@ "rpc": "https://rpc.monad.xyz", "documentation": "https://docs.monad.xyz" } + }, + { + "id": "stacks", + "name": "Stacks", + "coinId": 5757, + "symbol": "STX", + "decimals": 8, + "blockchain": "Stacks", + "derivation": [ + { + "name": "mainnet", + "path": "m/44'/5757'/0'/0/0", + "xpub": "xpub", + "xprv": "xprv" + }, + { + "name": "testnet", + "path": "m/44'/5757/0'/0/0", + "xpub": "xpub", + "xprv": "xprv" + } + ], + "curve": "secp256k1", + "publicKeyType": "secp256k1", + "publicKeyHasher": "sha256ripemd", + "base58Hasher": "sha256d", + "explorer": { + "url": "https://explorer.hiro.so", + "txPath": "/txid/", + "accountPath": "/address/", + "sampleTx": "0x2aeb646c13a0261ebc02003877d2db6c839d3bcbeea6ba7ede877f484f6ce70c", + "sampleAccount": "SP3XXK8BG5X7CRH7W07RRJK3JZJXJ799WX3Y0SMCR" + }, + "info": { + "url": "https://stacks.org", + "source": "https://github.com/stacks-network/stacks-core", + "rpc": "", + "documentation": "https://docs.hiro.so/en" + } } ] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6442582cac2..60837f0878f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -71,7 +71,7 @@ dependencies = [ "ark-serialize", "ark-std", "derivative", - "digest 0.10.6", + "digest 0.10.7", "itertools", "num-bigint", "num-traits", @@ -110,7 +110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-std", - "digest 0.10.6", + "digest 0.10.7", "num-bigint", ] @@ -172,7 +172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" dependencies = [ "serde", - "thiserror 1.0.38", + "thiserror 1.0.69", ] [[package]] @@ -282,7 +282,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -558,7 +558,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.6", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -687,9 +687,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.3", "const-oid", @@ -710,7 +710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a48e5d537b8a30c0b023116d981b16334be1485af7ca68db3a2b7024cbc957fd" dependencies = [ "der", - "digest 0.10.6", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -730,7 +730,7 @@ checksum = "75c71eaa367f2e5d556414a8eea812bc62985c879748d6403edabd9cb03f16e7" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -850,7 +850,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343cfc165f92a988fd60292f7a0bfde4352a5a0beff9fbec29251ca4e9676e4d" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -924,7 +924,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1218,9 +1218,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -1483,9 +1483,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -1494,9 +1494,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rfc6979" @@ -1514,7 +1514,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1743,18 +1743,18 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1763,7 +1763,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -1773,7 +1773,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core", ] @@ -1944,11 +1944,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.38", + "thiserror-impl 1.0.69", ] [[package]] @@ -1962,13 +1962,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.96", ] [[package]] @@ -2204,6 +2204,7 @@ dependencies = [ "tw_ripple", "tw_ronin", "tw_solana", + "tw_stacks", "tw_substrate", "tw_sui", "tw_thorchain", @@ -2345,7 +2346,7 @@ dependencies = [ "arbitrary 1.3.0", "blake-hash", "blake2b-ref", - "digest 0.10.6", + "digest 0.10.7", "groestl", "hmac", "ripemd", @@ -2384,7 +2385,7 @@ dependencies = [ "crypto_box", "curve25519-dalek", "der", - "digest 0.10.6", + "digest 0.10.7", "ecdsa", "k256", "lazy_static", @@ -2609,6 +2610,20 @@ dependencies = [ "tw_scale", ] +[[package]] +name = "tw_stacks" +version = "0.1.0" +dependencies = [ + "once_cell", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_proto", +] + [[package]] name = "tw_substrate" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 09ab546a720..ce1e30bd02e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,6 +20,7 @@ members = [ "chains/tw_ripple", "chains/tw_ronin", "chains/tw_solana", + "chains/tw_stacks", "chains/tw_sui", "chains/tw_thorchain", "chains/tw_ton", diff --git a/rust/chains/tw_stacks/Cargo.toml b/rust/chains/tw_stacks/Cargo.toml new file mode 100644 index 00000000000..f0401a002a3 --- /dev/null +++ b/rust/chains/tw_stacks/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tw_stacks" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } +tw_hash = { path = "../../tw_hash" } +once_cell = "1.21.3" \ No newline at end of file diff --git a/rust/chains/tw_stacks/src/address.rs b/rust/chains/tw_stacks/src/address.rs new file mode 100644 index 00000000000..9c130da9a39 --- /dev/null +++ b/rust/chains/tw_stacks/src/address.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::c32; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_hash::{hasher::StatefulHasher, ripemd::Sha256Ripemd, sha2, H160}; +use tw_keypair::tw::PublicKey; +use tw_memory::Data; + +pub struct StacksAddress { + pub(crate) version: u8, + bytes: H160, + check: [u8; 4], +} + +impl StacksAddress { + pub fn new(version: u8, public_key: &PublicKey) -> Self { + let hasher = Sha256Ripemd; + let hash_data = public_key.to_bytes(); + let mut hash_bytes = hasher.hash(&hash_data); + let bytes = H160::try_from(hash_bytes.as_slice()).unwrap(); // Sha256Ripemd will always return correct number of bytes + + let mut check_data = Vec::new(); + check_data.push(version); + check_data.append(&mut hash_bytes); + + let check_bytes = sha2::sha256(&sha2::sha256(&check_data))[0..4].to_vec(); + + let check: [u8; 4] = check_bytes.try_into().unwrap(); + + StacksAddress { + version, + bytes, + check, + } + } +} + +impl CoinAddress for StacksAddress { + #[inline] + fn data(&self) -> Data { + self.bytes.to_vec() + } +} + +impl FromStr for StacksAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + // first, normalize the following characters: + // O, o => 0 + // I, i => 1 + // L, l => 1 + let s = s.replace("O", "0"); + let s = s.replace("o", "0"); + let s = s.replace("I", "1"); + let s = s.replace("i", "1"); + let s = s.replace("L", "1"); + let s = s.replace("l", "1"); + let s = s.as_str(); + + if s.len() < 2 { + return Err(AddressError::MissingPrefix); + } + + if &s[0..1] != "S" { + return Err(AddressError::MissingPrefix); + } + + let version = match &s[1..2] { + "P" => 22, + "M" => 20, + "T" => 26, + "N" => 21, + prefix => { + eprintln!("unsupported address prefix {prefix}"); + return Err(AddressError::UnexpectedAddressPrefix); + }, + }; + + let payload = c32::decode(&s[2..]).map_err(|e| { + eprintln!("c32::decode error {e:?} on str {}", &s[2..]); + AddressError::Unsupported + })?; + + if payload.len() < 24 { + eprintln!("from_str Unsupported payload.len < 24"); + return Err(AddressError::Unsupported); + } + + let hash_bytes = payload[0..20].to_vec(); + let bytes = H160::try_from(hash_bytes.as_slice()).unwrap(); // Sha256Ripemd will always return correct number of bytes + + let mut check_data = Vec::new(); + check_data.push(version); + check_data.extend_from_slice(&payload[0..20]); + + let check_bytes = sha2::sha256(&sha2::sha256(&check_data))[0..4].to_vec(); // we can always grab 4 bytes from the beginning of a SHA256 hash + + if check_bytes != payload[20..].to_vec() { + return Err(AddressError::InvalidChecksum); + } + + let check: [u8; 4] = check_bytes.try_into().unwrap(); // we grabbed exactly 4 bytes above + + Ok(StacksAddress { + version, + bytes, + check, + }) + } +} + +impl fmt::Display for StacksAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "S")?; + match self.version { + 22 => write!(f, "P")?, + 20 => write!(f, "M")?, + 26 => write!(f, "T")?, + 21 => write!(f, "N")?, + _ => panic!("bad version {}", self.version), + } + + let mut bytes = Vec::new(); + bytes.append(&mut self.bytes.into_vec()); + bytes.append(&mut self.check.to_vec()); + + let encoded = c32::encode(&bytes); + + write!(f, "{}", encoded)?; + + Ok(()) + } +} diff --git a/rust/chains/tw_stacks/src/c32.rs b/rust/chains/tw_stacks/src/c32.rs new file mode 100644 index 00000000000..de1a056de5c --- /dev/null +++ b/rust/chains/tw_stacks/src/c32.rs @@ -0,0 +1,169 @@ +use once_cell::sync::Lazy; + +const C32_ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +static C32_BYTE_MAP: Lazy<[Option; 128]> = Lazy::new(|| { + let mut table: [Option; 128] = [None; 128]; + + let alphabet: [char; 32] = C32_ALPHABET + .iter() + .map(|byte| *byte as char) + .collect::>() + .try_into() + .unwrap(); + + alphabet.iter().enumerate().for_each(|(i, x)| { + table[*x as usize] = Some(i as u8); + }); + + alphabet + .iter() + .map(|c| c.to_ascii_lowercase()) + .enumerate() + .for_each(|(i, x)| { + table[x as usize] = Some(i as u8); + }); + + [('O', '0'), ('L', '1'), ('I', '1')] + .into_iter() + .for_each(|special_pair| { + let i = alphabet + .iter() + .enumerate() + .find(|(_, a)| **a == special_pair.1) + .unwrap() + .0; + + table[special_pair.0 as usize] = Some(i as u8); + table[special_pair.0.to_ascii_lowercase() as usize] = Some(i as u8); + }); + + table +}); + +fn encode_overhead(len: usize) -> usize { + (len * 8 + 4) / 5 +} + +fn decode_underhead(len: usize) -> usize { + len / (8f64 / 5f64).ceil() as usize +} + +#[derive(Clone, Debug, Eq, PartialEq)] +/// C32 error type +pub enum C32Error { + /// Invalid C32 string. + InvalidC32, + /// Invalid character. + InvalidChar(char), + /// Conversion error, from utf8. + FromUtf8Error(std::string::FromUtf8Error), + /// Integer conversion error. + IntConversionError(std::num::TryFromIntError), +} + +impl From for C32Error { + fn from(e: std::string::FromUtf8Error) -> Self { + Self::FromUtf8Error(e) + } +} + +impl From for C32Error { + fn from(e: std::num::TryFromIntError) -> Self { + Self::IntConversionError(e) + } +} + +/// C32 encode the given data +pub fn encode(data: impl AsRef<[u8]>) -> String { + let data = data.as_ref(); + + let mut encoded = Vec::with_capacity(encode_overhead(data.len())); + let mut buffer = 0u32; + let mut bits = 0; + + for byte in data.iter().rev() { + buffer |= (*byte as u32) << bits; + bits += 8; + + while bits >= 5 { + encoded.push(C32_ALPHABET[(buffer & 0x1F) as usize]); + buffer >>= 5; + bits -= 5; + } + } + + if bits > 0 { + encoded.push(C32_ALPHABET[(buffer & 0x1F) as usize]); + } + + while let Some(i) = encoded.pop() { + if i != C32_ALPHABET[0] { + encoded.push(i); + break; + } + } + + for i in data { + if *i == 0 { + encoded.push(C32_ALPHABET[0]); + } else { + break; + } + } + + encoded.reverse(); + + String::from_utf8(encoded).unwrap() +} + +/// C32 decode the given data +pub fn decode(input: impl AsRef) -> Result, C32Error> { + let input = input.as_ref().as_bytes(); + + if !input.is_ascii() { + return Err(C32Error::InvalidC32); + } + + let mut decoded = Vec::with_capacity(decode_underhead(input.len())); + let mut carry = 0u16; + let mut carry_bits = 0; + + for byte in input.iter().rev() { + let Some(bits) = C32_BYTE_MAP.get(*byte as usize).unwrap() else { + return Err(C32Error::InvalidChar(*byte as char)); + }; + + carry |= (u16::from(*bits)) << carry_bits; + carry_bits += 5; + + if carry_bits >= 8 { + decoded.push((carry & 0xFF) as u8); + carry >>= 8; + carry_bits -= 8; + } + } + + if carry_bits > 0 { + decoded.push(u8::try_from(carry)?); + } + + while let Some(i) = decoded.pop() { + if i != 0 { + decoded.push(i); + break; + } + } + + for byte in input.iter() { + if *byte == b'0' { + decoded.push(0); + } else { + break; + } + } + + decoded.reverse(); + + Ok(decoded) +} diff --git a/rust/chains/tw_stacks/src/compiler.rs b/rust/chains/tw_stacks/src/compiler.rs new file mode 100644 index 00000000000..94e6eb8cda9 --- /dev/null +++ b/rust/chains/tw_stacks/src/compiler.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::Stacks::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct StacksCompiler; + +impl StacksCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + todo!() + } +} diff --git a/rust/chains/tw_stacks/src/entry.rs b/rust/chains/tw_stacks/src/entry.rs new file mode 100644 index 00000000000..b584ec70449 --- /dev/null +++ b/rust/chains/tw_stacks/src/entry.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::StacksAddress; +use crate::compiler::StacksCompiler; +use crate::signer::StacksSigner; +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::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::Stacks::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct StacksEntry; + +impl CoinEntry for StacksEntry { + type AddressPrefix = NoPrefix; + type Address = StacksAddress; + 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; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + address: &str, + _prefix: Option, + ) -> AddressResult { + StacksAddress::from_str(address) + } + + #[inline] + fn parse_address_unchecked(&self, address: &str) -> AddressResult { + StacksAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + public_key: PublicKey, + derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + let version = match derivation { + Derivation::Default => 22, + Derivation::Testnet => 26, + _ => return Err(AddressError::FromHexError), + }; + + Ok(StacksAddress::new(version, &public_key)) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + StacksSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + StacksCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + StacksCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_stacks/src/lib.rs b/rust/chains/tw_stacks/src/lib.rs new file mode 100644 index 00000000000..bf26c773ed2 --- /dev/null +++ b/rust/chains/tw_stacks/src/lib.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +mod c32; +pub mod compiler; +pub mod entry; +pub mod signer; diff --git a/rust/chains/tw_stacks/src/signer.rs b/rust/chains/tw_stacks/src/signer.rs new file mode 100644 index 00000000000..3dc6ca7d737 --- /dev/null +++ b/rust/chains/tw_stacks/src/signer.rs @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::StacksAddress; + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +//use tw_encoding::hex; +use tw_hash::{hasher::StatefulHasher, ripemd::Sha256Ripemd, sha2, H160, H256}; +use tw_keypair::{ecdsa::secp256k1::PrivateKey, traits::SigningKeyTrait}; +use tw_misc::traits::ToBytesVec; +use tw_proto::Stacks::Proto; +use tw_proto::Stacks::Proto::mod_SigningInput::OneOfmessage_oneof as SigningInputMessage; + +use std::str::FromStr; + +pub struct StacksSigner; + +impl StacksSigner { + 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 signed_tx = match input.message_oneof { + SigningInputMessage::transfer(xfer) => { + let rcpt_type: u8 = 0x05; // rcpt address + let rcpt_addr = StacksAddress::from_str(&xfer.to) + .map_err(|_| SigningErrorType::Error_invalid_address)?; + let amount: u64 = xfer + .amount + .try_into() + .map_err(|_| SigningErrorType::Error_invalid_params)?; // microSTX + let fee: u64 = xfer + .fee + .try_into() + .map_err(|_| SigningErrorType::Error_invalid_params)?; // microSTX + let nonce: u64 = xfer + .nonce + .try_into() + .map_err(|_| SigningErrorType::Error_invalid_params)?; + if xfer.memo.as_bytes().len() > 34 { + return Err(SigningErrorType::Error_invalid_params.into()); + } + let memo_bytes: [u8; 34] = { + let msg = xfer.memo.as_bytes(); + let mut b = [0u8; 34]; + b[0..msg.len()].copy_from_slice(msg); + b + }; + + let version: u8 = 0x00; // Mainnet + let chain_id: u32 = 0x00000001; // Mainnet + let anchor_mode: u8 = 0x03; // Any + let post_condition_mode: u8 = 0x01; // Allow + let hash_mode: u8 = 0x00; // P2PKH single-sig + + // Parse private key + let private_key_bytes = input.private_key; + let secret_key = PrivateKey::try_from(&private_key_bytes[..]) + .map_err(|_| SigningErrorType::Error_invalid_private_key)?; + + // Compute public key (compressed) + let public_key = secret_key.public(); + let pubkey_bytes = public_key.to_vec(); + + // Compute signer hash160: ripemd160(sha256(pubkey_bytes)) + let hasher = Sha256Ripemd; + let signer_hash160: [u8; 20] = hasher.hash(&pubkey_bytes).try_into().unwrap(); // will always give 20 bytes + + // Rcpt hash160 + let rcpt_hash160_bytes = rcpt_addr.data(); + let rcpt_hash160 = H160::try_from(&rcpt_hash160_bytes[..]).unwrap(); // is always 20 bytes + + // Build payload: 0x00 + rcpt principal (version + hash160) + amount BE + memo + let mut payload = vec![0x00]; + payload.push(rcpt_type); + payload.push(rcpt_addr.version); + payload.extend_from_slice(&rcpt_hash160.as_slice()); + payload.extend_from_slice(&amount.to_be_bytes()); + payload.extend_from_slice(&memo_bytes); + + // Build unsigned auth: 0x04 + hash_mode + signer + nonce0 + fee0 + cleared signature + let mut auth_unsigned = vec![0x04, hash_mode]; + auth_unsigned.extend_from_slice(&signer_hash160); + auth_unsigned.extend_from_slice(&0u64.to_be_bytes()); + auth_unsigned.extend_from_slice(&0u64.to_be_bytes()); + auth_unsigned.extend_from_slice(&[0u8; 66]); + + // Build post conditions: u32 0 + let post_conditions = [0u8; 4]; + + // Serialize unsigned tx for initial sighash + let mut unsigned_tx = vec![version]; + unsigned_tx.extend_from_slice(&chain_id.to_be_bytes()); + unsigned_tx.extend_from_slice(&auth_unsigned); + unsigned_tx.push(anchor_mode); + unsigned_tx.push(post_condition_mode); + unsigned_tx.extend_from_slice(&post_conditions); + unsigned_tx.extend_from_slice(&payload); + + // Compute initial sighash = SHA512/256(unsigned_tx) + let initial_sighash = sha2::sha512_256(&unsigned_tx); + + //let serialized_initial_sighash = hex::encode(&initial_sighash, false); + //println!("initial sighash (hex): {}", serialized_initial_sighash); + + // Auth flag for standard origin: 0x04 + let auth_flag: u8 = 0x04; + + // Compute pre_sign_hash = SHA512/256(initial_sighash || auth_flag || fee BE || nonce BE) + let mut pre_sign_data = vec![]; + pre_sign_data.extend_from_slice(&initial_sighash); + pre_sign_data.push(auth_flag); + pre_sign_data.extend_from_slice(&fee.to_be_bytes()); + pre_sign_data.extend_from_slice(&nonce.to_be_bytes()); + let pre_sign_hash = sha2::sha512_256(&pre_sign_data); + + // Sign the pre_sign_hash with ECDSA secp256k1 + let message = H256::try_from(&pre_sign_hash[..]).unwrap(); // is always 32 bytes + let rsig = secret_key + .sign(message) + .map_err(|_| SigningErrorType::Error_signing)?; + + // Serialize signature to 65 bytes Bitcoin-style + let compressed = true; // since we used compressed pubkey + let header_byte = rsig.v(); + let mut signature = [0u8; 65]; + signature[0] = header_byte; + signature[1..33].copy_from_slice(&rsig.r().as_slice()); + signature[33..].copy_from_slice(&rsig.s().as_slice()); + + // Key encoding: 0x00 compressed, 0x01 uncompressed + let key_encoding: u8 = if compressed { 0x00 } else { 0x01 }; + + // Now build the signed auth: 0x04 + hash_mode + signer + nonce BE + fee BE + key_encoding + signature + let mut auth_signed = vec![0x04, hash_mode]; + auth_signed.extend_from_slice(&signer_hash160); + auth_signed.extend_from_slice(&nonce.to_be_bytes()); + auth_signed.extend_from_slice(&fee.to_be_bytes()); + auth_signed.push(key_encoding); + auth_signed.extend_from_slice(&signature); + + // Serialize signed tx + let mut signed_tx = vec![version]; + signed_tx.extend_from_slice(&chain_id.to_be_bytes()); + signed_tx.extend_from_slice(&auth_signed); + signed_tx.push(anchor_mode); + signed_tx.push(post_condition_mode); + signed_tx.extend_from_slice(&post_conditions); + signed_tx.extend_from_slice(&payload); + + signed_tx + }, + _ => todo!(), + }; + + //let serialized_hex = hex::encode(&signed_tx, false); + //println!("Signed transaction (hex): {}", serialized_hex); + + Ok(Proto::SigningOutput { + encoded: signed_tx.into(), + ..Proto::SigningOutput::default() + }) + } +} diff --git a/rust/tw_any_coin/src/test_utils/address_utils.rs b/rust/tw_any_coin/src/test_utils/address_utils.rs index 7d7b9cff373..3b4c42f92ba 100644 --- a/rust/tw_any_coin/src/test_utils/address_utils.rs +++ b/rust/tw_any_coin/src/test_utils/address_utils.rs @@ -30,6 +30,7 @@ impl WithDestructor for TWAnyAddress { } } +#[derive(Debug)] pub enum KeyType { PrivateKey(&'static str), PublicKey(&'static str), diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index d3617a4c475..02c5371c243 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -34,6 +34,7 @@ tw_polymesh = { path = "../chains/tw_polymesh" } tw_ripple = { path = "../chains/tw_ripple" } tw_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } +tw_stacks = { path = "../chains/tw_stacks" } tw_substrate = { path = "../frameworks/tw_substrate" } tw_sui = { path = "../chains/tw_sui" } tw_thorchain = { path = "../chains/tw_thorchain" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index 146743ec386..6cc2e383d93 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -29,6 +29,7 @@ pub enum BlockchainType { Ripple, Ronin, Solana, + Stacks, Sui, TheOpenNetwork, Thorchain, diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index 419be6b9eb2..74a7bda66a3 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -28,6 +28,7 @@ use tw_polymesh::entry::PolymeshEntry; use tw_ripple::entry::RippleEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; +use tw_stacks::entry::StacksEntry; use tw_substrate::entry::SubstrateEntry; use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; @@ -57,6 +58,7 @@ const POLYMESH: SubstrateEntry = SubstrateEntry(PolymeshEntry); const RIPPLE: RippleEntry = RippleEntry; const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; +const STACKS: StacksEntry = StacksEntry; const SUI: SuiEntry = SuiEntry; const THE_OPEN_NETWORK: TheOpenNetworkEntry = TheOpenNetworkEntry; const THORCHAIN: ThorchainEntry = ThorchainEntry; @@ -86,6 +88,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&RIPPLE), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), + BlockchainType::Stacks => Ok(&STACKS), BlockchainType::Sui => Ok(&SUI), BlockchainType::TheOpenNetwork => Ok(&THE_OPEN_NETWORK), BlockchainType::Thorchain => Ok(&THORCHAIN), diff --git a/rust/tw_coin_registry/src/tw_derivation.rs b/rust/tw_coin_registry/src/tw_derivation.rs index ae6b1b61e6b..39b7af1e4d6 100644 --- a/rust/tw_coin_registry/src/tw_derivation.rs +++ b/rust/tw_coin_registry/src/tw_derivation.rs @@ -21,6 +21,8 @@ pub enum TWDerivation { PactusMainnet = 9, PactusTestnet = 10, SmartChainStableAccount = 11, + StacksMainnet = 12, + StacksTestnet = 13, // end_of_derivation_enum - USED TO GENERATE CODE #[default] Default = 0, @@ -38,6 +40,8 @@ impl From for Derivation { TWDerivation::PactusMainnet => Derivation::Default, TWDerivation::PactusTestnet => Derivation::Testnet, TWDerivation::SmartChainStableAccount => Derivation::Default, + TWDerivation::StacksMainnet => Derivation::Default, + TWDerivation::StacksTestnet => Derivation::Testnet, } } } diff --git a/rust/tw_tests/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs index 885e1b26e0f..6def57418e2 100644 --- a/rust/tw_tests/tests/chains/mod.rs +++ b/rust/tw_tests/tests/chains/mod.rs @@ -24,6 +24,7 @@ mod polkadot; mod polymesh; mod ripple; mod solana; +mod stacks; mod sui; mod tbinance; mod thorchain; diff --git a/rust/tw_tests/tests/chains/stacks/mod.rs b/rust/tw_tests/tests/chains/stacks/mod.rs new file mode 100644 index 00000000000..1906b8732c3 --- /dev/null +++ b/rust/tw_tests/tests/chains/stacks/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod stacks_address; +mod stacks_compile; +mod stacks_sign; diff --git a/rust/tw_tests/tests/chains/stacks/stacks_address.rs b/rust/tw_tests/tests/chains/stacks/stacks_address.rs new file mode 100644 index 00000000000..d8fd43d7523 --- /dev/null +++ b/rust/tw_tests/tests/chains/stacks/stacks_address.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, KeyType, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_stacks_address_derive() { + let private_key_hexstr = "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5"; + test_address_derive( + CoinType::Stacks, + KeyType::PrivateKey(private_key_hexstr), + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); + + let private_key_hexstr = "4646464646464646464646464646464646464646464646464646464646464646"; + test_address_derive( + CoinType::Stacks, + KeyType::PrivateKey(private_key_hexstr), + "SP2YS424BPZM2TR8TKEAFQDTA1441AAVR9W6EVSKY", + ); + + let private_key_hexstr = "5ee478841eee47c23c164530d3df3469168431af52569c3dcc5d715666a7321b"; + test_address_derive( + CoinType::Stacks, + KeyType::PrivateKey(private_key_hexstr), + "SP3R2M51AC00TKNMF08XE9WSJDZ3WT4QGQNEQPDQA", + ); + + let private_key_hexstr = "0000000000000000000000000000000000000000000000000000000000000001"; + test_address_derive( + CoinType::Stacks, + KeyType::PrivateKey(private_key_hexstr), + "SP1THWXQ8368SDN2MJGE4BMDKMCHZ2GSVTS1X0BPM", + ); + + let private_key_hexstr = "1111111111111111111111111111111111111111111111111111111111111111"; + test_address_derive( + CoinType::Stacks, + KeyType::PrivateKey(private_key_hexstr), + "SP3Y74M5227FDVHREWPH773F5Y1W1ED8WXY3RAVG4", + ); +} + +#[test] +fn test_stacks_address_normalization() { + test_address_normalization( + CoinType::Stacks, + "SP1F6ENB7636XSYSDNQO5X5T3SP4K2o34A6D1G43K", + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); + test_address_normalization( + CoinType::Stacks, + "SPIF6ENB7636XSYSDNQ05X5T3SP4K2034A6DiG43K", + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); + test_address_normalization( + CoinType::Stacks, + "SPLF6ENB7636XSYSDNQ05X5T3SP4K2034A6DlG43K", + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); +} + +#[test] +fn test_stacks_address_is_valid() { + test_address_valid( + CoinType::Stacks, + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); +} + +#[test] +fn test_stacks_address_invalid() { + test_address_invalid( + CoinType::Stacks, + "ZP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); + test_address_invalid( + CoinType::Stacks, + "SX1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", + ); + test_address_invalid( + CoinType::Stacks, + "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43Z", + ); +} + +#[test] +fn test_stacks_address_get_data() { + test_address_get_data( + CoinType::Stacks, + "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", + "0xa46ff88886c2ef9762d970b4d2c63678835bd39d", + ); + test_address_get_data( + CoinType::Stacks, + "SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", + "0xa46ff88886c2ef9762d970b4d2c63678835bd39d", + ); + test_address_get_data( + CoinType::Stacks, + "ST2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQYAC0RQ", + "0xa46ff88886c2ef9762d970b4d2c63678835bd39d", + ); + test_address_get_data( + CoinType::Stacks, + "SN2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKP6D2ZK9", + "0xa46ff88886c2ef9762d970b4d2c63678835bd39d", + ); +} diff --git a/rust/tw_tests/tests/chains/stacks/stacks_compile.rs b/rust/tw_tests/tests/chains/stacks/stacks_compile.rs new file mode 100644 index 00000000000..e208ec06698 --- /dev/null +++ b/rust/tw_tests/tests/chains/stacks/stacks_compile.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_stacks_compile() { + // compilation is not supported in the stacks blockchain +} diff --git a/rust/tw_tests/tests/chains/stacks/stacks_sign.rs b/rust/tw_tests/tests/chains/stacks/stacks_sign.rs new file mode 100644 index 00000000000..0f466ad3de8 --- /dev/null +++ b/rust/tw_tests/tests/chains/stacks/stacks_sign.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::ffi::tw_any_signer::tw_any_signer_sign; +use tw_coin_entry::error::prelude::*; +use tw_coin_registry::coin_type::CoinType; +use tw_coin_registry::registry::get_coin_item; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +//use tw_misc::assert_eq_json; +use tw_proto::Stacks::Proto; +use tw_proto::Stacks::Proto::mod_SigningInput::OneOfmessage_oneof as SigningInputMessage; +use tw_proto::{deserialize, serialize}; + +// generated using stacks.js +const ENCODED: &str = "0000000001040015c31b8c1c11c515e244b75806bac48d1399c775000000000000000500000000000000020001e91464423f0de32fff11efc8136b019e2919502134e573bce8eb97dc7d11ad667b44886f31a314c60392f05c5452721b5e8f50c475df6fc19ca9c1864f5a0616030100000000000516df0ba3e79792be7be5e50a370289accfc8c9e03200000000000186a06d656d6f20286e6f7420696e636c7564656400000000000000000000000000000000"; + +#[test] +fn test_stacks_sign() { + let coin = CoinType::Stacks; + let _coin_item = get_coin_item(coin).unwrap(); + + // Configuration for valid transfer + let private_key_hex = "edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc"; + let to = "SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159"; + let amount: i64 = 100_000; // microSTX + let fee: i64 = 2; // microSTX + let nonce: i64 = 5; + let memo = "memo (not included"; + + let valid_transfer = Proto::TransferMessage { + amount, + fee, + to: std::borrow::Cow::Borrowed(to), + memo: std::borrow::Cow::Borrowed(memo), + nonce, + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(valid_transfer.clone()), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!(output.encoded.to_hex(), ENCODED); + + // Invalid rcpt + let to = "SZ3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159"; + let transfer = Proto::TransferMessage { + to: std::borrow::Cow::Borrowed(to), + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + assert_eq!(output.error, SigningErrorType::Error_invalid_address); + + // Invalid amount + let transfer = Proto::TransferMessage { + amount: -valid_transfer.amount, + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::Error_invalid_params); + + // Invalid fee + let transfer = Proto::TransferMessage { + fee: -valid_transfer.fee, + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::Error_invalid_params); + + // Invalid nonce + let transfer = Proto::TransferMessage { + nonce: -valid_transfer.nonce, + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::Error_invalid_params); + + // Invalid memo + let memo: &str = &String::from_utf8(vec![b'X'; 35]).unwrap(); + let transfer = Proto::TransferMessage { + memo: std::borrow::Cow::Borrowed(memo), + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::Error_invalid_params); + + // Invalid private key + let private_key_hex_ex = "edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bcab"; + let transfer = Proto::TransferMessage { + ..valid_transfer.clone() + }; + let input = Proto::SigningInput { + private_key: private_key_hex_ex.decode_hex().unwrap().into(), + message_oneof: SigningInputMessage::transfer(transfer), + }; + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + + let output = TWDataHelper::wrap(unsafe { tw_any_signer_sign(input_data.ptr(), coin as u32) }) + .to_vec() + .expect("!tw_any_signer_sign returned nullptr"); + let output: Proto::SigningOutput = deserialize(&output).unwrap(); + + assert_eq!(output.error, SigningErrorType::Error_invalid_private_key); +} diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs index c2f386ef844..881682bcb53 100644 --- a/rust/tw_tests/tests/coin_address_derivation_test.rs +++ b/rust/tw_tests/tests/coin_address_derivation_test.rs @@ -167,6 +167,7 @@ fn test_coin_address_derivation() { CoinType::XRP => "r9cwJ8hM13jodBBGtioB44FUZ5HwWGwqfX", CoinType::Groestlcoin => "grs1qten42eesehw0ktddcp0fws7d3ycsqez35034a2", CoinType::Decred => "DsbEmWV6ZZBsUJY2vVi5u7H62GUfBFPBfoF", + CoinType::Stacks => "SP1F6ENB7636XSYSDNQ05X5T3SP4K2034A6D1G43K", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/src/Coin.cpp b/src/Coin.cpp index 3aa2d14d4ac..34a8ab73950 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -70,6 +70,7 @@ #include "Pactus/Entry.h" #include "Komodo/Entry.h" #include "Polymesh/Entry.h" +#include "Stacks/Entry.h" // end_of_coin_includes_marker_do_not_modify using namespace TW; @@ -133,6 +134,7 @@ BitcoinCash::Entry BitcoinCashDP; Pactus::Entry PactusDP; Komodo::Entry KomodoDP; Polymesh::Entry PolymeshDP; +Stacks::Entry StacksDP; // end_of_coin_dipatcher_declarations_marker_do_not_modify CoinEntry* coinDispatcher(TWCoinType coinType) { @@ -198,6 +200,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) { case TWBlockchainPactus: entry = &PactusDP; break; case TWBlockchainKomodo: entry = &KomodoDP; break; case TWBlockchainPolymesh: entry = &PolymeshDP; break; + case TWBlockchainStacks: entry = &StacksDP; break; // end_of_coin_dipatcher_switch_marker_do_not_modify default: entry = nullptr; break; diff --git a/src/Stacks/Entry.h b/src/Stacks/Entry.h new file mode 100644 index 00000000000..72762627ff3 --- /dev/null +++ b/src/Stacks/Entry.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/RustCoinEntry.h" + +namespace TW::Stacks { + +/// Entry point for Stacks coin. +/// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file +class Entry : public Rust::RustCoinEntry { +}; + +} // namespace TW::Stacks + diff --git a/src/proto/Stacks.proto b/src/proto/Stacks.proto new file mode 100644 index 00000000000..dcc6090141a --- /dev/null +++ b/src/proto/Stacks.proto @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +syntax = "proto3"; + +package TW.Stacks.Proto; +option java_package = "wallet.core.jni.proto"; + +import "Common.proto"; + +// TODO: typical balance transfer, add more fields needed to sign +message TransferMessage { + int64 amount = 1; + int64 fee = 2; + string to = 3; + string memo = 4; + int64 nonce = 5; +} + +// TODO: Input data necessary to create a signed transaction. +message SigningInput { + bytes private_key = 1; + + oneof message_oneof { + TransferMessage transfer = 2; + } +} + +// Transaction signing output. +message SigningOutput { + // Signed and encoded transaction bytes. + bytes encoded = 1; + + // A possible error, `OK` if none. + Common.Proto.SigningError error = 2; + + string error_message = 3; +} diff --git a/tests/chains/Stacks/TWAnyAddressTests.cpp b/tests/chains/Stacks/TWAnyAddressTests.cpp new file mode 100644 index 00000000000..5292c489894 --- /dev/null +++ b/tests/chains/Stacks/TWAnyAddressTests.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include +#include "HexCoding.h" + +#include "TestUtilities.h" +#include + +using namespace TW; + +// TODO: Finalize tests + +TEST(TWStacks, Address) { + auto string = STRING("SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypeStacks)); + auto string2 = WRAPS(TWAnyAddressDescription(addr.get())); + EXPECT_TRUE(TWStringEqual(string.get(), string2.get())); + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "a46ff88886c2ef9762d970b4d2c63678835bd39d"); +} diff --git a/tests/chains/Stacks/TWAnySignerTests.cpp b/tests/chains/Stacks/TWAnySignerTests.cpp new file mode 100644 index 00000000000..5f89e46444a --- /dev/null +++ b/tests/chains/Stacks/TWAnySignerTests.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include + +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "proto/Stacks.pb.h" +#include "TestUtilities.h" + +#include + +using namespace TW; + +// TODO: Finalize tests + +TEST(TWStacks, Sign) { + Stacks::Proto::SigningInput input; + auto& tf = *input.mutable_transfer(); + tf.set_to("SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"); + tf.set_amount(100000); + tf.set_fee(10000); + tf.set_memo("hello"); + auto privateKey = PrivateKey(parse_hex("a1b2c3d4e5f60000000000000000000000000000000000000000000000000001")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + Stacks::Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypeStacks); + //ASSERT_EQ(hex(output.raw_txn()), "07968dab936c1bad187c60ce4082f307d03} +} diff --git a/tests/chains/Stacks/TWCoinTypeTests.cpp b/tests/chains/Stacks/TWCoinTypeTests.cpp new file mode 100644 index 00000000000..81bdf961b66 --- /dev/null +++ b/tests/chains/Stacks/TWCoinTypeTests.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include +#include + +TEST(TWStacksCoinType, TWCoinType) { + const auto coin = TWCoinTypeStacks; + const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin)); + const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin)); + const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin)); + const auto txId = WRAPS(TWStringCreateWithUTF8Bytes("0x2aeb646c13a0261ebc02003877d2db6c839d3bcbeea6ba7ede877f484f6ce70c")); + const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get())); + const auto accId = WRAPS(TWStringCreateWithUTF8Bytes("SP3XXK8BG5X7CRH7W07RRJK3JZJXJ799WX3Y0SMCR")); + const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get())); + + assertStringsEqual(id, "stacks"); + assertStringsEqual(name, "Stacks"); + assertStringsEqual(symbol, "STX"); + ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), 8); + ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchainStacks); + ASSERT_EQ(TWCoinTypeStaticPrefix(coin), 0); + assertStringsEqual(txUrl, "https://explorer.hiro.so/txid/0x2aeb646c13a0261ebc02003877d2db6c839d3bcbeea6ba7ede877f484f6ce70c"); + assertStringsEqual(accUrl, "https://explorer.hiro.so/address/SP3XXK8BG5X7CRH7W07RRJK3JZJXJ799WX3Y0SMCR"); +} diff --git a/tests/common/CoinAddressDerivationTests.cpp b/tests/common/CoinAddressDerivationTests.cpp index 1dafcf3d775..301a938b1c2 100644 --- a/tests/common/CoinAddressDerivationTests.cpp +++ b/tests/common/CoinAddressDerivationTests.cpp @@ -405,6 +405,9 @@ TEST(Coin, DeriveAddress) { case TWCoinTypePolymesh: EXPECT_EQ(address, "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F"); break; + case TWCoinTypeStacks: + EXPECT_EQ(address, "SP2YS424BPZM2TR8TKEAFQDTA1441AAVR9W6EVSKY"); + break; // end_of_coin_address_derivation_tests_marker_do_not_modify // no default branch here, intentionally, to better notice any missing coins }