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
}