Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a75afa8

Browse files
authoredApr 11, 2025··
feat(pactus): support testnet address derivation (#4330)
* feat(pactus): support testnet address derivation * test(pactus): fix KT broken tests * test(pactus): fix KT broken tests * feat(pactus): support validator string for testnet * chore: derivation name in YAML file in camelCase
1 parent 4eaaa60 commit a75afa8

File tree

21 files changed

+408
-49
lines changed

21 files changed

+408
-49
lines changed
 

‎android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,23 @@ class TestCoinType {
3636
assertEquals(CoinType.TEZOS.value(), 1729)
3737
assertEquals(CoinType.QTUM.value(), 2301)
3838
assertEquals(CoinType.NEBULAS.value(), 2718)
39+
assertEquals(CoinType.PACTUS.value(), 21888)
3940
}
4041

4142
@Test
4243
fun testCoinPurpose() {
4344
assertEquals(Purpose.BIP84, CoinType.BITCOIN.purpose())
45+
assertEquals(Purpose.BIP44, CoinType.PACTUS.purpose())
4446
}
4547

4648
@Test
4749
fun testCoinCurve() {
4850
assertEquals(Curve.SECP256K1, CoinType.BITCOIN.curve())
51+
assertEquals(Curve.ED25519, CoinType.PACTUS.curve())
4952
}
5053

5154
@Test
52-
fun testDerivationPath() {
55+
fun testDerivationPathBitcoin() {
5356
var res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPath().toString()
5457
assertEquals(res, "m/84'/0'/0'/0/0")
5558
res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINLEGACY).toString()
@@ -61,10 +64,31 @@ class TestCoinType {
6164
}
6265

6366
@Test
64-
fun testDeriveAddressFromPublicKeyAndDerivation() {
67+
fun testDeriveAddressFromPublicKeyAndDerivationBitcoin() {
6568
val publicKey = PublicKey("0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798".toHexByteArray(), PublicKeyType.SECP256K1)
6669

6770
val address = CoinType.BITCOIN.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.BITCOINSEGWIT)
6871
assertEquals(address, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
6972
}
73+
74+
@Test
75+
fun testDerivationPathPactus() {
76+
var res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPath().toString()
77+
assertEquals(res, "m/44'/21888'/3'/0'")
78+
res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPathWithDerivation(Derivation.PACTUSMAINNET).toString()
79+
assertEquals(res, "m/44'/21888'/3'/0'")
80+
res = CoinType.createFromValue(CoinType.PACTUS.value()).derivationPathWithDerivation(Derivation.PACTUSTESTNET).toString()
81+
assertEquals(res, "m/44'/21777'/3'/0'")
82+
}
83+
84+
@Test
85+
fun testDeriveAddressFromPublicKeyAndDerivationPactus() {
86+
val publicKey = PublicKey("95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa".toHexByteArray(), PublicKeyType.ED25519)
87+
88+
val mainnet_address = CoinType.PACTUS.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.PACTUSMAINNET)
89+
assertEquals(mainnet_address, "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr")
90+
91+
val testnet_address = CoinType.PACTUS.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.PACTUSTESTNET)
92+
assertEquals(testnet_address, "tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg")
93+
}
7094
}

‎android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class TestPactusAddress {
1717
}
1818

1919
@Test
20-
fun testAddress() {
20+
fun testMainnetAddress() {
2121
val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray())
2222
val pubkey = key.publicKeyEd25519
2323
val address = AnyAddress(pubkey, CoinType.PACTUS)
@@ -26,4 +26,15 @@ class TestPactusAddress {
2626
assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa")
2727
assertEquals(address.description(), expected.description())
2828
}
29+
30+
@Test
31+
fun testTestnetAddress() {
32+
val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray())
33+
val pubkey = key.publicKeyEd25519
34+
val address = AnyAddress(pubkey, CoinType.PACTUS, Derivation.PACTUSTESTNET)
35+
val expected = AnyAddress("tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg", CoinType.PACTUS)
36+
37+
assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa")
38+
assertEquals(address.description(), expected.description())
39+
}
2940
}

‎android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class TestHDWallet {
9999
}
100100

101101
@Test
102-
fun testGetKeyForCoin() {
102+
fun testGetKeyForCoinBitcoin() {
103103
val coin = CoinType.BITCOIN
104104
val wallet = HDWallet(words, password)
105105
val key = wallet.getKeyForCoin(coin)
@@ -109,7 +109,7 @@ class TestHDWallet {
109109
}
110110

111111
@Test
112-
fun testGetKeyDerivation() {
112+
fun testGetKeyDerivationBitcoin() {
113113
val coin = CoinType.BITCOIN
114114
val wallet = HDWallet(words, password)
115115

@@ -127,7 +127,7 @@ class TestHDWallet {
127127
}
128128

129129
@Test
130-
fun testGetAddressForCoin() {
130+
fun testGetAddressForCoinBitcoin() {
131131
val coin = CoinType.BITCOIN
132132
val wallet = HDWallet(words, password)
133133

@@ -136,7 +136,7 @@ class TestHDWallet {
136136
}
137137

138138
@Test
139-
fun testGetAddressDerivation() {
139+
fun testGetAddressDerivationBitcoin() {
140140
val coin = CoinType.BITCOIN
141141
val wallet = HDWallet(words, password)
142142

@@ -153,6 +153,49 @@ class TestHDWallet {
153153
assertEquals(address4, "bc1pgqks0cynn93ymve4x0jq3u7hne77908nlysp289hc44yc4cmy0hslyckrz")
154154
}
155155

156+
@Test
157+
fun testGetKeyForCoinPactus() {
158+
val coin = CoinType.PACTUS
159+
val wallet = HDWallet(words, password)
160+
val key = wallet.getKeyForCoin(coin)
161+
162+
val address = coin.deriveAddress(key)
163+
assertEquals(address, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff")
164+
}
165+
166+
@Test
167+
fun testGetKeyDerivationPactus() {
168+
val coin = CoinType.PACTUS
169+
val wallet = HDWallet(words, password)
170+
171+
val key1 = wallet.getKeyDerivation(coin, Derivation.PACTUSMAINNET)
172+
assertEquals(key1.data().toHex(), "0x153fefb8168f246f9f77c60ea10765c1c39828329e87284ddd316770717f3a5e")
173+
174+
val key2 = wallet.getKeyDerivation(coin, Derivation.PACTUSTESTNET)
175+
assertEquals(key2.data().toHex(), "0x54f3c54dd6af5794bea1f86de05b8b9f164215e8deee896f604919046399e54d")
176+
}
177+
178+
@Test
179+
fun testGetAddressForCoinPactus() {
180+
val coin = CoinType.PACTUS
181+
val wallet = HDWallet(words, password)
182+
183+
val address = wallet.getAddressForCoin(coin)
184+
assertEquals(address, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff")
185+
}
186+
187+
@Test
188+
fun testGetAddressDerivationPactus() {
189+
val coin = CoinType.PACTUS
190+
val wallet = HDWallet(words, password)
191+
192+
val address1 = wallet.getAddressDerivation(coin, Derivation.PACTUSMAINNET)
193+
assertEquals(address1, "pc1rjkzc23l7qkkenx6xwy04srwppzfk6m5t7q46ff")
194+
195+
val address2 = wallet.getAddressDerivation(coin, Derivation.PACTUSTESTNET)
196+
assertEquals(address2, "tpc1rjtamyqp203j4367q4plkp4qt32d7sv34kfmj5e")
197+
}
198+
156199
@Test
157200
fun testDerive() {
158201
val wallet = HDWallet(words, password)

‎codegen-v2/manifest/TWDerivation.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ enums:
1919
value: 5
2020
- name: solanaSolana
2121
value: 6
22+
- name: stratisSegwit
23+
value: 7
24+
- name: bitcoinTaproot
25+
value: 8
26+
- name: pactusMainnet
27+
value: 9
28+
- name: pactusTestnet
29+
value: 10

‎include/TrustWalletCore/TWDerivation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ enum TWDerivation {
2525
TWDerivationSolanaSolana = 6,
2626
TWDerivationStratisSegwit = 7,
2727
TWDerivationBitcoinTaproot = 8,
28+
TWDerivationPactusMainnet = 9,
29+
TWDerivationPactusTestnet = 10,
2830
// end_of_derivation_enum - USED TO GENERATE CODE
2931
};
3032

‎registry.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4827,7 +4827,12 @@
48274827
"blockchain": "Pactus",
48284828
"derivation": [
48294829
{
4830+
"name": "mainnet",
48304831
"path": "m/44'/21888'/3'/0'"
4832+
},
4833+
{
4834+
"name": "testnet",
4835+
"path": "m/44'/21777'/3'/0'"
48314836
}
48324837
],
48334838
"curve": "ed25519",

‎rust/chains/tw_pactus/src/entry.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use tw_proto::TxCompiler::Proto as CompilerProto;
2121
use crate::compiler::PactusCompiler;
2222
use crate::modules::transaction_util::PactusTransactionUtil;
2323
use crate::signer::PactusSigner;
24+
use crate::types::network::Network;
2425
use crate::types::Address;
2526

2627
pub struct PactusEntry;
@@ -60,13 +61,18 @@ impl CoinEntry for PactusEntry {
6061
&self,
6162
_coin: &dyn CoinContext,
6263
public_key: PublicKey,
63-
_derivation: Derivation,
64+
derivation: Derivation,
6465
_prefix: Option<Self::AddressPrefix>,
6566
) -> AddressResult<Self::Address> {
6667
let public_key = public_key
6768
.to_ed25519()
6869
.ok_or(AddressError::PublicKeyTypeMismatch)?;
69-
Address::from_public_key(public_key)
70+
71+
match derivation {
72+
Derivation::Default => Address::from_public_key(public_key, Network::Mainnet),
73+
Derivation::Testnet => Address::from_public_key(public_key, Network::Testnet),
74+
_ => AddressResult::Err(AddressError::Unsupported),
75+
}
7076
}
7177

7278
#[inline]

‎rust/chains/tw_pactus/src/types/address.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use tw_memory::Data;
1717
use crate::encoder::error::Error;
1818
use crate::encoder::{Decodable, Encodable};
1919

20-
const ADDRESS_HRP: &str = "pc";
20+
use super::network::Network;
21+
2122
const TREASURY_ADDRESS_STRING: &str = "000000000000000000000000000000000000000000";
2223

2324
/// Enum for Pactus address types.
@@ -66,18 +67,20 @@ impl Decodable for AddressType {
6667
/// The hash is computed as RIPEMD160(Blake2b(public key)).
6768
#[derive(Debug, Clone, PartialEq)]
6869
pub struct Address {
70+
network: Network,
6971
addr_type: AddressType,
7072
pub_hash: H160,
7173
}
7274

7375
impl Address {
74-
pub fn from_public_key(public_key: &PublicKey) -> Result<Self, AddressError> {
76+
pub fn from_public_key(public_key: &PublicKey, network: Network) -> Result<Self, AddressError> {
7577
let pud_data = public_key.to_bytes();
7678
let pub_hash_data =
7779
ripemd_160(&blake2_b(pud_data.as_ref(), 32).map_err(|_| AddressError::Internal)?);
7880
let pub_hash = Address::vec_to_pub_hash(pub_hash_data)?;
7981

8082
Ok(Address {
83+
network,
8184
addr_type: AddressType::Ed25519Account,
8285
pub_hash,
8386
})
@@ -110,12 +113,12 @@ impl fmt::Display for Address {
110113
return f.write_str(TREASURY_ADDRESS_STRING);
111114
}
112115

116+
let hrp = self.network.address_hrp().map_err(|_| fmt::Error)?;
113117
let mut b32 = Vec::with_capacity(33);
114118

115119
b32.push(bech32::u5::try_from_u8(self.addr_type.clone() as u8).map_err(|_| fmt::Error)?);
116120
b32.extend_from_slice(&self.pub_hash.to_vec().to_base32());
117-
bech32::encode_to_fmt(f, ADDRESS_HRP, &b32, bech32::Variant::Bech32m)
118-
.map_err(|_| fmt::Error)?
121+
bech32::encode_to_fmt(f, hrp, &b32, bech32::Variant::Bech32m).map_err(|_| fmt::Error)?
119122
}
120123
}
121124

@@ -146,13 +149,15 @@ impl Decodable for Address {
146149
let addr_type = AddressType::decode(r)?;
147150
if addr_type == AddressType::Treasury {
148151
return Ok(Address {
152+
network: Network::Unknown,
149153
addr_type,
150154
pub_hash: H160::new(),
151155
});
152156
}
153157

154158
let pub_hash = H160::decode(r)?;
155159
Ok(Address {
160+
network: Network::Unknown,
156161
addr_type,
157162
pub_hash,
158163
})
@@ -165,16 +170,14 @@ impl FromStr for Address {
165170
fn from_str(s: &str) -> Result<Self, AddressError> {
166171
if s == TREASURY_ADDRESS_STRING {
167172
return Ok(Address {
173+
network: Network::Unknown,
168174
addr_type: AddressType::Treasury,
169175
pub_hash: H160::new(),
170176
});
171177
}
172178

173179
let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?;
174-
175-
if hrp != ADDRESS_HRP {
176-
return Err(AddressError::InvalidHrp);
177-
}
180+
let network = Network::try_from_hrp(&hrp)?;
178181

179182
if b32.len() != 33 {
180183
return Err(AddressError::InvalidInput);
@@ -185,6 +188,7 @@ impl FromStr for Address {
185188
let pub_hash = Address::vec_to_pub_hash(b8)?;
186189

187190
Ok(Address {
191+
network,
188192
addr_type,
189193
pub_hash,
190194
})
@@ -241,12 +245,20 @@ mod test {
241245
.decode_hex()
242246
.unwrap();
243247

244-
let addr = deserialize::<Address>(&data).unwrap();
248+
let mut addr = deserialize::<Address>(&data).unwrap();
245249
assert!(!addr.is_treasury());
250+
251+
addr.network = Network::Mainnet;
246252
assert_eq!(
247253
addr.to_string(),
248254
"pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra"
249255
);
256+
257+
addr.network = Network::Testnet;
258+
assert_eq!(
259+
addr.to_string(),
260+
"tpc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsrtuyllk"
261+
);
250262
}
251263

252264
#[test]
@@ -289,6 +301,7 @@ mod test {
289301
for case in test_cases {
290302
let pub_hash_data = case.pub_hash.decode_hex().unwrap();
291303
let addr = Address {
304+
network: Network::Mainnet,
292305
addr_type: case.addr_type,
293306
pub_hash: Address::vec_to_pub_hash(pub_hash_data).unwrap(),
294307
};
@@ -307,7 +320,7 @@ mod test {
307320
"afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5",
308321
)
309322
.unwrap();
310-
let address = Address::from_public_key(&private_key.public()).unwrap();
323+
let address = Address::from_public_key(&private_key.public(), Network::Mainnet).unwrap();
311324
let mut w = Vec::new();
312325

313326
address.encode(&mut w).unwrap();
@@ -323,13 +336,16 @@ mod test {
323336
.unwrap();
324337
let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap();
325338
let public_key = private_key.public();
326-
let address = Address::from_public_key(&public_key).unwrap();
339+
let mainnet_address = Address::from_public_key(&public_key, Network::Mainnet).unwrap();
340+
let testnet_address = Address::from_public_key(&public_key, Network::Testnet).unwrap();
327341

328342
let expected_public_key =
329343
"95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa";
330-
let expected_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr";
344+
let expected_mainnet_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr";
345+
let expected_testnet_address = "tpc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymzqkcrg";
331346

332347
assert_eq!(public_key.to_bytes().to_hex(), expected_public_key);
333-
assert_eq!(address.to_string(), expected_address);
348+
assert_eq!(mainnet_address.to_string(), expected_mainnet_address);
349+
assert_eq!(testnet_address.to_string(), expected_testnet_address);
334350
}
335351
}

‎rust/chains/tw_pactus/src/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod address;
22
pub mod amount;
3+
pub mod network;
34
pub mod validator_public_key;
45

56
pub use address::Address;

0 commit comments

Comments
 (0)