diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt
index 9064a873e7c..f99db4e987c 100644
--- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt
+++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt
@@ -154,5 +154,6 @@ class CoinAddressDerivationTests {
TIA -> assertEquals("celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7", address)
NATIVEZETACHAIN -> assertEquals("zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304", address)
DYDX -> assertEquals("dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky", address)
+ PACTUS -> assertEquals("pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg", address)
}
}
diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt
new file mode 100644
index 00000000000..b58b068b859
--- /dev/null
+++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusAddress.kt
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+package com.trustwallet.core.app.blockchains.pactus
+
+import com.trustwallet.core.app.utils.toHex
+import com.trustwallet.core.app.utils.toHexByteArray
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import wallet.core.jni.*
+
+class TestPactusAddress {
+
+ init {
+ System.loadLibrary("TrustWalletCore")
+ }
+
+ @Test
+ fun testAddress() {
+ val key = PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6".toHexByteArray())
+ val pubkey = key.publicKeyEd25519
+ val address = AnyAddress(pubkey, CoinType.PACTUS)
+ val expected = AnyAddress("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr", CoinType.PACTUS)
+
+ assertEquals(pubkey.data().toHex(), "0x95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa")
+ assertEquals(address.description(), expected.description())
+ }
+}
diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt
new file mode 100644
index 00000000000..81bbe9bf532
--- /dev/null
+++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/pactus/TestPactusSigner.kt
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+package com.trustwallet.core.app.blockchains.pactus
+
+import com.google.protobuf.ByteString
+import com.trustwallet.core.app.utils.toHexByteArray
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import wallet.core.jni.PrivateKey
+import wallet.core.java.AnySigner
+import wallet.core.jni.CoinType
+import wallet.core.jni.CoinType.PACTUS
+import wallet.core.jni.proto.Pactus
+import wallet.core.jni.proto.Pactus.SigningOutput
+import com.trustwallet.core.app.utils.Numeric
+import org.junit.Assert.assertArrayEquals
+
+class TestPactusSigner {
+
+ init {
+ System.loadLibrary("TrustWalletCore")
+ }
+
+ @Test
+ fun testPactusTransferSigning() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f
+ //
+ val signingInput = Pactus.SigningInput.newBuilder()
+ signingInput.apply {
+ privateKey = ByteString.copyFrom(
+ PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"
+ .toHexByteArray()).data()
+ )
+ transaction = Pactus.TransactionMessage.newBuilder().apply {
+ lockTime = 2335524
+ fee = 10000000
+ memo = "wallet-core"
+ transfer = Pactus.TransferPayload.newBuilder().apply {
+ sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ receiver = "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra"
+ amount = 200000000
+ }.build()
+ }.build()
+ }
+
+ val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser())
+
+ assertEquals(
+ "0x1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f",
+ Numeric.toHexString(output.transactionId.toByteArray())
+ )
+
+ assertEquals(
+ "0x4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506",
+ Numeric.toHexString(output.signature.toByteArray())
+ )
+
+ assertEquals(
+ "0x000124a3230080ade2040b77616c6c65742d636f726501037098338e0b6808119dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa",
+ Numeric.toHexString(output.signedTransactionData.toByteArray())
+ )
+ }
+
+ @Test
+ fun testPactusBondWithPublicKeySigning() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f
+ //
+ val signingInput = Pactus.SigningInput.newBuilder()
+ signingInput.apply {
+ privateKey = ByteString.copyFrom(
+ PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"
+ .toHexByteArray()).data()
+ )
+ transaction = Pactus.TransactionMessage.newBuilder().apply {
+ lockTime = 2339009
+ fee = 10000000
+ memo = "wallet-core"
+ bond = Pactus.BondPayload.newBuilder().apply {
+ sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ receiver = "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl"
+ stake = 1000000000
+ publicKey = "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn"
+ }.build()
+ }.build()
+ }
+
+ val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser())
+
+ assertEquals(
+ "0xd194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f",
+ Numeric.toHexString(output.transactionId.toByteArray())
+ )
+
+ assertEquals(
+ "0x0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300",
+ Numeric.toHexString(output.signature.toByteArray())
+ )
+
+ assertEquals(
+ "0x0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b6808119dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f451823d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa",
+ Numeric.toHexString(output.signedTransactionData.toByteArray())
+ )
+ }
+
+ @Test
+ fun testPactusBondWithoutPublicKeySigning() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80
+ //
+ val signingInput = Pactus.SigningInput.newBuilder()
+ signingInput.apply {
+ privateKey = ByteString.copyFrom(
+ PrivateKey("4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"
+ .toHexByteArray()).data()
+ )
+ transaction = Pactus.TransactionMessage.newBuilder().apply {
+ lockTime = 2335580
+ fee = 10000000
+ memo = "wallet-core"
+ bond = Pactus.BondPayload.newBuilder().apply {
+ sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ receiver = "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd"
+ stake = 1000000000
+ }.build()
+ }.build()
+ }
+
+ val output = AnySigner.sign(signingInput.build(), PACTUS, SigningOutput.parser())
+
+ assertEquals(
+ "0xf83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80",
+ Numeric.toHexString(output.transactionId.toByteArray())
+ )
+
+ assertEquals(
+ "0x9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d",
+ Numeric.toHexString(output.signature.toByteArray())
+ )
+
+ assertEquals(
+ "0x00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b6808119dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa",
+ Numeric.toHexString(output.signedTransactionData.toByteArray())
+ )
+ }
+}
diff --git a/docs/registry.md b/docs/registry.md
index 7606e14a45b..a5045889d19 100644
--- a/docs/registry.md
+++ b/docs/registry.md
@@ -99,6 +99,7 @@ This list is generated from [./registry.json](../registry.json)
| 14001 | WAX | WAXP | | |
| 18000 | Meter | MTR | | |
| 19167 | Flux | FLUX | | |
+| 21888 | Pactus | PAC | | |
| 52752 | Celo | CELO | | |
| 59144 | Linea | ETH | | |
| 81457 | Blast | ETH | | |
diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h
index 84cac17e3b4..eeb2f744474 100644
--- a/include/TrustWalletCore/TWBlockchain.h
+++ b/include/TrustWalletCore/TWBlockchain.h
@@ -66,6 +66,7 @@ enum TWBlockchain {
TWBlockchainNativeEvmos = 53, // Cosmos
TWBlockchainNativeInjective = 54, // Cosmos
TWBlockchainBitcoinCash = 55,
+ TWBlockchainPactus = 56,
};
TW_EXTERN_C_END
diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h
index 388bd270334..9d2f10e36c9 100644
--- a/include/TrustWalletCore/TWCoinType.h
+++ b/include/TrustWalletCore/TWCoinType.h
@@ -186,6 +186,7 @@ enum TWCoinType {
TWCoinTypeBlast = 81457,
TWCoinTypeBounceBit = 6001,
TWCoinTypeZkLinkNova = 810180,
+ TWCoinTypePactus = 21888,
// end_of_tw_coin_type_marker_do_not_modify
};
diff --git a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt
index c3d3dbf0527..959fd4821eb 100644
--- a/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt
+++ b/kotlin/wallet-core-kotlin/src/commonTest/kotlin/com/trustwallet/core/test/CoinAddressDerivationTests.kt
@@ -147,5 +147,6 @@ class CoinAddressDerivationTests {
Tia -> "celestia142j9u5eaduzd7faumygud6ruhdwme98qpwmfv7"
NativeZetaChain -> "zeta13u6g7vqgw074mgmf2ze2cadzvkz9snlwywj304"
Dydx -> "dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky"
+ Pactus -> "pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg"
}
}
diff --git a/registry.json b/registry.json
index abaf52b05bd..d88798aff9c 100644
--- a/registry.json
+++ b/registry.json
@@ -4787,5 +4787,32 @@
"rpc": "https://rpc.zklink.io",
"documentation": "https://docs.zklink.io"
}
+ },
+ {
+ "id": "pactus",
+ "name": "Pactus",
+ "coinId": 21888,
+ "symbol": "PAC",
+ "decimals": 9,
+ "blockchain": "Pactus",
+ "derivation": [
+ {
+ "path": "m/44'/21888'/3'/0'"
+ }
+ ],
+ "curve": "ed25519",
+ "publicKeyType": "ed25519",
+ "hrp": "pc",
+ "explorer": {
+ "url": "https://pacviewer.com",
+ "txPath": "/transaction/",
+ "accountPath": "/address/"
+ },
+ "info": {
+ "url": "https://pactus.org",
+ "source": "https://github.com/pactus-project/pactus",
+ "rpc": "https://docs.pactus.org/api/http",
+ "documentation": "https://docs.pactus.org"
+ }
}
]
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index dcb26f709ab..e12cf3ea375 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -1857,6 +1857,7 @@ dependencies = [
"tw_misc",
"tw_native_evmos",
"tw_native_injective",
+ "tw_pactus",
"tw_ronin",
"tw_solana",
"tw_sui",
@@ -2078,6 +2079,20 @@ dependencies = [
"tw_memory",
]
+[[package]]
+name = "tw_pactus"
+version = "0.1.0"
+dependencies = [
+ "bech32",
+ "byteorder",
+ "tw_coin_entry",
+ "tw_encoding",
+ "tw_hash",
+ "tw_keypair",
+ "tw_memory",
+ "tw_proto",
+]
+
[[package]]
name = "tw_proto"
version = "0.1.0"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index f6bd5522ea7..692beec2692 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -10,6 +10,7 @@ members = [
"chains/tw_internet_computer",
"chains/tw_native_evmos",
"chains/tw_native_injective",
+ "chains/tw_pactus",
"chains/tw_ronin",
"chains/tw_solana",
"chains/tw_sui",
diff --git a/rust/chains/tw_pactus/Cargo.toml b/rust/chains/tw_pactus/Cargo.toml
new file mode 100644
index 00000000000..105b0aa7679
--- /dev/null
+++ b/rust/chains/tw_pactus/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "tw_pactus"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+bech32 = "0.9.1"
+byteorder = "1.4"
+tw_coin_entry = { path = "../../tw_coin_entry" }
+tw_keypair = { path = "../../tw_keypair" }
+tw_memory = { path = "../../tw_memory" }
+tw_proto = { path = "../../tw_proto" }
+tw_hash = { path = "../../tw_hash" }
+tw_encoding = { path = "../../tw_encoding" }
+
+[dev-dependencies]
+tw_encoding = { path = "../../tw_encoding" }
+
diff --git a/rust/chains/tw_pactus/src/compiler.rs b/rust/chains/tw_pactus/src/compiler.rs
new file mode 100644
index 00000000000..3433b77d6e2
--- /dev/null
+++ b/rust/chains/tw_pactus/src/compiler.rs
@@ -0,0 +1,84 @@
+// 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_keypair::ed25519;
+use tw_proto::Pactus::Proto;
+use tw_proto::TxCompiler::Proto as CompilerProto;
+
+use crate::modules::tx_builder::TxBuilder;
+
+pub struct PactusCompiler;
+
+impl PactusCompiler {
+ #[inline]
+ pub fn preimage_hashes(
+ coin: &dyn CoinContext,
+ input: Proto::SigningInput<'_>,
+ ) -> CompilerProto::PreSigningOutput<'static> {
+ Self::preimage_hashes_impl(coin, input)
+ .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e))
+ }
+
+ fn preimage_hashes_impl(
+ _coin: &dyn CoinContext,
+ input: Proto::SigningInput<'_>,
+ ) -> SigningResult> {
+ let trx = TxBuilder::from_proto(&input)?;
+ let sign_bytes = trx.sign_bytes()?;
+
+ let output = CompilerProto::PreSigningOutput {
+ data_hash: trx.id().into(),
+ data: sign_bytes.into(),
+ ..CompilerProto::PreSigningOutput::default()
+ };
+
+ Ok(output)
+ }
+
+ #[inline]
+ pub fn compile(
+ coin: &dyn CoinContext,
+ input: Proto::SigningInput<'_>,
+ signatures: Vec,
+ public_keys: Vec,
+ ) -> Proto::SigningOutput<'static> {
+ Self::compile_impl(coin, input, signatures, public_keys)
+ .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e))
+ }
+
+ fn compile_impl(
+ _coin: &dyn CoinContext,
+ input: Proto::SigningInput<'_>,
+ signatures: Vec,
+ public_keys: Vec,
+ ) -> SigningResult> {
+ let signature_bytes = signatures
+ .first()
+ .or_tw_err(SigningErrorType::Error_signatures_count)?;
+ let public_key_bytes = public_keys
+ .first()
+ .or_tw_err(SigningErrorType::Error_signatures_count)?;
+
+ let public_key = ed25519::sha512::PublicKey::try_from(public_key_bytes.as_slice())?;
+ let signature = ed25519::Signature::try_from(signature_bytes.as_slice())?;
+
+ let mut trx = TxBuilder::from_proto(&input)?;
+ trx.set_signatory(public_key.to_owned(), signature.to_owned());
+
+ let data = trx.to_bytes()?;
+
+ let output = Proto::SigningOutput {
+ transaction_id: trx.id().into(),
+ signed_transaction_data: data.into(),
+ signature: signature.to_bytes().to_vec().into(),
+ ..Proto::SigningOutput::default()
+ };
+
+ Ok(output)
+ }
+}
diff --git a/rust/chains/tw_pactus/src/encoder/decode.rs b/rust/chains/tw_pactus/src/encoder/decode.rs
new file mode 100644
index 00000000000..f9e2ef80edf
--- /dev/null
+++ b/rust/chains/tw_pactus/src/encoder/decode.rs
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use tw_hash::Hash;
+use tw_keypair::ed25519::{sha512::PublicKey, Signature};
+
+use super::error::Error;
+use crate::encoder::var_int::VarInt;
+use crate::encoder::Decodable;
+
+pub(crate) fn decode_var_slice(r: &mut dyn std::io::Read) -> Result, Error> {
+ let len = *VarInt::decode(r)?;
+ let mut buf = vec![0; len as usize];
+ r.read_exact(&mut buf)?;
+
+ Ok(buf)
+}
+
+pub(crate) fn decode_fix_slice(
+ r: &mut dyn std::io::Read,
+) -> Result<[u8; N], Error> {
+ let mut buf: [u8; N] = [0; N];
+ r.read_exact(&mut buf)?;
+
+ Ok(buf)
+}
+
+impl Decodable for Vec {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ decode_var_slice(r)
+ }
+}
+
+impl Decodable for String {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let data = decode_var_slice(r)?;
+ String::from_utf8(data).map_err(|_| self::Error::ParseFailed("Invalid String"))
+ }
+}
+
+impl Decodable for PublicKey {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let data = decode_fix_slice::<{ PublicKey::LEN }>(r)?;
+ PublicKey::try_from(data.as_slice())
+ .map_err(|_| self::Error::ParseFailed("Invalid Public Key"))
+ }
+}
+
+impl Decodable for Signature {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let data = decode_fix_slice::<{ Signature::LEN }>(r)?;
+ Signature::try_from(data.as_slice())
+ .map_err(|_| self::Error::ParseFailed("Invalid Signature"))
+ }
+}
+
+impl Decodable for Hash {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let data = decode_fix_slice::(r)?;
+ Hash::try_from(data.as_slice()).map_err(|_| self::Error::ParseFailed("Invalid Hash"))
+ }
+}
+
+macro_rules! impl_decodable_for_int {
+ ($int:ty, $size:literal) => {
+ impl Decodable for $int {
+ #[inline]
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let mut buf = [0; $size];
+ r.read_exact(&mut buf[..])?;
+ Ok(<$int>::from_le_bytes(buf))
+ }
+ }
+ };
+}
+
+impl_decodable_for_int!(u8, 1);
+impl_decodable_for_int!(i32, 4);
+impl_decodable_for_int!(i64, 8);
+impl_decodable_for_int!(u16, 2);
+impl_decodable_for_int!(u32, 4);
+impl_decodable_for_int!(u64, 8);
+
+#[cfg(test)]
+mod tests {
+ use std::io::Cursor;
+
+ use tw_encoding::hex::DecodeHex;
+
+ use super::*;
+ use crate::encoder::deserialize;
+
+ #[test]
+ fn test_decode_var_slice() {
+ let expected = vec![1, 2, 3, 4];
+ let mut cursor = Cursor::new("0401020304".decode_hex().unwrap());
+ let slice = decode_var_slice(&mut cursor).unwrap();
+
+ assert_eq!(expected, slice);
+ }
+
+ #[test]
+ fn test_decode_fix_slice() {
+ let expected = vec![1, 2, 3, 4];
+ let mut cursor = Cursor::new("01020304".decode_hex().unwrap());
+ let slice = decode_fix_slice::<4>(&mut cursor).unwrap();
+
+ assert_eq!(expected, slice);
+ }
+
+ #[test]
+ fn test_encode_numbers() {
+ let data = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0];
+ let mut cursor = Cursor::new(data);
+
+ assert_eq!(1u8, u8::decode(&mut cursor).unwrap());
+ assert_eq!(2u16, u16::decode(&mut cursor).unwrap());
+ assert_eq!(3u32, u32::decode(&mut cursor).unwrap());
+ assert_eq!(4u64, u64::decode(&mut cursor).unwrap());
+ }
+
+ #[test]
+ fn test_decode_bytes() {
+ let expected = "0145".decode_hex().unwrap();
+ let bytes = "020145".decode_hex().unwrap();
+
+ assert_eq!(expected, deserialize::>(&bytes).unwrap());
+ }
+
+ #[test]
+ fn test_encode_string() {
+ let expected = "hello".to_string();
+ let bytes = "0568656c6c6f056844656c6c6e".decode_hex().unwrap();
+
+ assert_eq!(expected, deserialize::(&bytes).unwrap());
+ }
+}
diff --git a/rust/chains/tw_pactus/src/encoder/encode.rs b/rust/chains/tw_pactus/src/encoder/encode.rs
new file mode 100644
index 00000000000..9840c1873a8
--- /dev/null
+++ b/rust/chains/tw_pactus/src/encoder/encode.rs
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use byteorder::{LittleEndian, WriteBytesExt};
+use tw_hash::Hash;
+use tw_keypair::ed25519::{sha512::PublicKey, Signature};
+
+use super::error::Error;
+use crate::encoder::var_int::VarInt;
+use crate::encoder::Encodable;
+
+pub(crate) fn encode_var_slice(data: &[u8], w: &mut dyn std::io::Write) -> Result<(), Error> {
+ VarInt::from(data.len()).encode(w)?;
+ w.write_all(data)?;
+
+ Ok(())
+}
+
+pub(crate) fn encode_fix_slice(data: &[u8], w: &mut dyn std::io::Write) -> Result<(), Error> {
+ w.write_all(data)?;
+
+ Ok(())
+}
+
+impl Encodable for Vec {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_var_slice(self, w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ VarInt::from(self.len()).encoded_size() + self.len()
+ }
+}
+
+impl Encodable for String {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_var_slice(self.as_bytes(), w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ VarInt::from(self.len()).encoded_size() + self.len()
+ }
+}
+
+impl Encodable for PublicKey {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_fix_slice(self.as_slice(), w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ PublicKey::LEN
+ }
+}
+
+impl Encodable for Signature {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_fix_slice(self.to_bytes().as_slice(), w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ Signature::LEN
+ }
+}
+
+impl Encodable for Hash {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_fix_slice(self.as_slice(), w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ N
+ }
+}
+
+impl Encodable for u8 {
+ #[inline]
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ w.write_u8(*self)?;
+
+ Ok(())
+ }
+
+ #[inline]
+ fn encoded_size(&self) -> usize {
+ 1
+ }
+}
+
+macro_rules! impl_encodable_for_int {
+ ($int:ty, $size:literal, $write_fn:tt) => {
+ impl Encodable for $int {
+ #[inline]
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ w.$write_fn::(*self)?;
+
+ Ok(())
+ }
+
+ #[inline]
+ fn encoded_size(&self) -> usize {
+ $size
+ }
+ }
+ };
+}
+
+impl_encodable_for_int!(i32, 4, write_i32);
+impl_encodable_for_int!(i64, 8, write_i64);
+impl_encodable_for_int!(u16, 2, write_u16);
+impl_encodable_for_int!(u32, 4, write_u32);
+impl_encodable_for_int!(u64, 8, write_u64);
+
+#[cfg(test)]
+mod tests {
+ use tw_encoding::hex::DecodeHex;
+
+ use super::*;
+ use crate::encoder::serialize;
+
+ #[test]
+ fn test_encode_var_slice() {
+ let expected = "0401020304".decode_hex().unwrap();
+ let slice = vec![1, 2, 3, 4];
+ let mut w = Vec::new();
+ encode_var_slice(&slice, &mut w).unwrap();
+
+ assert_eq!(expected, w.to_vec());
+ }
+
+ #[test]
+ fn test_encode_fix_slice() {
+ let expected = "01020304".decode_hex().unwrap();
+ let slice = vec![1, 2, 3, 4];
+ let mut w = Vec::new();
+ encode_fix_slice(&slice, &mut w).unwrap();
+
+ assert_eq!(expected, w.to_vec());
+ }
+
+ #[test]
+ fn test_encode_numbers() {
+ let mut w = Vec::new();
+
+ 1u8.encode(&mut w).unwrap();
+ 2u16.encode(&mut w).unwrap();
+ 3u32.encode(&mut w).unwrap();
+ 4u64.encode(&mut w).unwrap();
+
+ let expected = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0];
+ assert_eq!(w.to_vec(), expected);
+ }
+
+ #[test]
+ fn test_encode_bytes() {
+ let expected = "020145".decode_hex().unwrap();
+ let bytes = "0145".decode_hex().unwrap();
+
+ assert_eq!(expected, serialize(&bytes).unwrap());
+ assert_eq!(expected.len(), bytes.encoded_size());
+ }
+
+ #[test]
+ fn test_encode_string() {
+ let expected = "0568656c6c6f".decode_hex().unwrap();
+ let msg = "hello".to_string();
+
+ assert_eq!(expected, serialize(&msg).unwrap());
+ assert_eq!(expected.len(), msg.encoded_size());
+ }
+}
diff --git a/rust/chains/tw_pactus/src/encoder/error.rs b/rust/chains/tw_pactus/src/encoder/error.rs
new file mode 100644
index 00000000000..b3643854a90
--- /dev/null
+++ b/rust/chains/tw_pactus/src/encoder/error.rs
@@ -0,0 +1,20 @@
+use tw_coin_entry::error::prelude::{SigningError, SigningErrorType};
+
+/// Errors encountered when encoding or decoding data.
+#[derive(Debug)]
+pub enum Error {
+ IoError(std::io::Error),
+ ParseFailed(&'static str),
+}
+
+impl From for Error {
+ fn from(err: std::io::Error) -> Self {
+ Error::IoError(err)
+ }
+}
+
+impl From for SigningError {
+ fn from(_: Error) -> Self {
+ SigningError::new(SigningErrorType::Error_input_parse)
+ }
+}
diff --git a/rust/chains/tw_pactus/src/encoder/mod.rs b/rust/chains/tw_pactus/src/encoder/mod.rs
new file mode 100644
index 00000000000..506c3baec67
--- /dev/null
+++ b/rust/chains/tw_pactus/src/encoder/mod.rs
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use std::io::Cursor;
+
+use error::Error;
+
+pub mod decode;
+pub mod encode;
+pub mod error;
+pub mod var_int;
+
+pub fn serialize(t: &T) -> Result, Error> {
+ let mut writer = Vec::with_capacity(t.encoded_size());
+ t.encode(&mut writer)?;
+
+ Ok(writer.to_vec())
+}
+
+pub fn deserialize(data: &[u8]) -> Result {
+ let mut cursor = Cursor::new(data);
+ T::decode(&mut cursor)
+}
+
+/// Trait for encoding an object into a consistent byte sequence.
+pub trait Encodable {
+ /// Encode the object in consistent and deterministic way.
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error>;
+
+ /// Determine the size of serialized object.
+ fn encoded_size(&self) -> usize;
+}
+
+/// Trait for decoding an object from a byte sequence.
+pub trait Decodable: Sized {
+ /// Decode the object in consistent and deterministic way.
+ fn decode(r: &mut dyn std::io::Read) -> Result;
+}
diff --git a/rust/chains/tw_pactus/src/encoder/var_int.rs b/rust/chains/tw_pactus/src/encoder/var_int.rs
new file mode 100644
index 00000000000..aba98f95f4f
--- /dev/null
+++ b/rust/chains/tw_pactus/src/encoder/var_int.rs
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use std::ops::Deref;
+
+use byteorder::ReadBytesExt;
+
+use super::{error::Error, Decodable};
+use crate::encoder::Encodable;
+
+/// A type of variable-length integer used in the Pactus blockchain to serialize a variable-length integer.
+#[derive(Default, Debug, Clone, Copy, PartialEq)]
+pub struct VarInt(u64);
+
+impl From for VarInt {
+ fn from(value: usize) -> Self {
+ VarInt(value as u64)
+ }
+}
+
+impl Deref for VarInt {
+ type Target = u64;
+
+ fn deref(&self) -> &u64 {
+ &self.0
+ }
+}
+
+impl Encodable for VarInt {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ let mut val = self.0;
+ // Make sure that there is one after this
+ while val >= 0x80 {
+ let n = (val as u8 & 0x7f) | 0x80;
+ w.write_all(&[n])?;
+ val >>= 7; // It should be in multiples of 7, this should just get the next part
+ }
+
+ w.write_all(&[val as u8])?;
+
+ Ok(())
+ }
+
+ fn encoded_size(&self) -> usize {
+ match self.0 {
+ val if val >= 0x8000000000000000 => 10,
+ val if val >= 0x100000000000000 => 9,
+ val if val >= 0x2000000000000 => 8,
+ val if val >= 0x40000000000 => 7,
+ val if val >= 0x800000000 => 6,
+ val if val >= 0x10000000 => 5,
+ val if val >= 0x200000 => 4,
+ val if val >= 0x4000 => 3,
+ val if val >= 0x80 => 2,
+ _ => 1,
+ }
+ }
+}
+
+impl Decodable for VarInt {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let mut res: Vec = vec![];
+ loop {
+ let n = r.read_u8()?;
+ // Zero in any position other than the first is invalid
+ // since it is not the shortest encoding.
+ if n == 0 && !res.is_empty() {
+ return Err(Error::ParseFailed("VarInt has a zero in a position other than the first. This is not the shortest encoding."));
+ }
+ res.push(n & 0b0111_1111);
+ if n & 0b1000_0000 == 0 {
+ break;
+ }
+ }
+ let mut int = 0u64;
+ res.reverse();
+ let (last, arr) = res.split_last().unwrap();
+ for bits in arr {
+ int |= *bits as u64;
+ int = if int.leading_zeros() >= 7 {
+ int << 7
+ } else {
+ return Err(Error::ParseFailed("VarInt overflows u64"));
+ };
+ }
+ int |= *last as u64;
+ Ok(VarInt(int))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use crate::encoder::deserialize;
+
+ #[test]
+ fn test_var_int_encode_data() {
+ let mut w = Vec::new();
+
+ VarInt::from(0x00_usize).encode(&mut w).unwrap();
+ VarInt::from(0xfc_usize).encode(&mut w).unwrap();
+ VarInt::from(0xfd_usize).encode(&mut w).unwrap();
+ VarInt::from(0xffff_usize).encode(&mut w).unwrap();
+ VarInt::from(0x01_0000_usize).encode(&mut w).unwrap();
+ VarInt::from(0xffff_ffff_usize).encode(&mut w).unwrap();
+ VarInt(0x01_0000_0000_u64).encode(&mut w).unwrap();
+
+ let expected = vec![
+ 0x00, // 0x00
+ 0xfc, 0x01, // 0xfc
+ 0xfd, 0x01, // 0xfd
+ 0xff, 0xff, 0x03, // 0xffff
+ 0x80, 0x80, 0x04, // 0x01_0000
+ 0xff, 0xff, 0xff, 0xff, 0x0f, // 0xffff_ffff
+ 0x80, 0x80, 0x80, 0x80, 0x10, // 0x01_0000_0000
+ ];
+
+ assert_eq!(w.to_vec(), expected);
+ }
+
+ // Define the common test cases as a constant
+ const VARINT_TEST_CASES: &[(u64, &[u8])] = &[
+ (0x0u64, &[0x00u8]),
+ (0xffu64, &[0xff, 0x01]),
+ (0x7fffu64, &[0xff, 0xff, 0x01]),
+ (0x3fffffu64, &[0xff, 0xff, 0xff, 0x01]),
+ (0x1fffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x01]),
+ (0xfffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0xff, 0x01]),
+ (
+ 0x7ffffffffffu64,
+ &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01],
+ ),
+ (
+ 0x3ffffffffffffu64,
+ &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01],
+ ),
+ (
+ 0x1ffffffffffffffu64,
+ &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01],
+ ),
+ (
+ 0xffffffffffffffffu64,
+ &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01],
+ ),
+ (0x200u64, &[0x80, 0x04]),
+ (0x027fu64, &[0xff, 0x04]),
+ (0xff00000000u64, &[0x80, 0x80, 0x80, 0x80, 0xf0, 0x1f]),
+ (0xffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x0f]),
+ (0x100000000u64, &[0x80, 0x80, 0x80, 0x80, 0x10]),
+ (0x7ffffffffu64, &[0xff, 0xff, 0xff, 0xff, 0x7f]),
+ (0x800000000u64, &[0x80, 0x80, 0x80, 0x80, 0x80, 0x01]),
+ ];
+
+ #[test]
+ fn test_var_int_encode() {
+ for (i, (value, encoded)) in VARINT_TEST_CASES.iter().enumerate() {
+ let mut w = Vec::new();
+ let var_int = VarInt(*value);
+ let encoded_size = var_int.encoded_size();
+ var_int.encode(&mut w).unwrap();
+ let out = w.as_slice();
+
+ assert_eq!(out, *encoded, "Test {i} failed: data mismatch");
+ assert_eq!(
+ encoded_size,
+ out.len(),
+ "Test {i} failed: encoded size mismatch"
+ );
+ }
+ }
+
+ #[test]
+ fn test_var_int_decode() {
+ for (i, (value, data)) in VARINT_TEST_CASES.iter().enumerate() {
+ let var_int = deserialize::(data).unwrap();
+
+ assert_eq!(*value, *var_int, "Test {i} failed: value mismatch");
+ }
+ }
+
+ #[test]
+ fn test_var_int_parse_error() {
+ // varint must be shortest encoding
+ let res = deserialize::(&[0x98, 0]);
+ assert!(matches!(res.unwrap_err(), Error::ParseFailed(_)));
+
+ // If the last number is not a 0, it will error with an IO error (UnexpectedEof)
+ let res = deserialize::(&[0xff; 1]);
+ assert!(matches!(res.unwrap_err(), Error::IoError(_)));
+
+ let res = deserialize::(&[
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 1u8,
+ ]);
+ assert!(matches!(res.unwrap_err(), Error::ParseFailed(_)));
+ }
+}
diff --git a/rust/chains/tw_pactus/src/entry.rs b/rust/chains/tw_pactus/src/entry.rs
new file mode 100644
index 00000000000..e9d12f3ac8c
--- /dev/null
+++ b/rust/chains/tw_pactus/src/entry.rs
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+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::wallet_connector::NoWalletConnector;
+use tw_coin_entry::prefix::NoPrefix;
+use tw_keypair::tw::PublicKey;
+use tw_proto::Pactus::Proto;
+use tw_proto::TxCompiler::Proto as CompilerProto;
+
+use crate::compiler::PactusCompiler;
+use crate::modules::transaction_util::PactusTransactionUtil;
+use crate::signer::PactusSigner;
+use crate::types::Address;
+
+pub struct PactusEntry;
+
+impl CoinEntry for PactusEntry {
+ type AddressPrefix = NoPrefix;
+ type Address = Address;
+ type SigningInput<'a> = Proto::SigningInput<'a>;
+ type SigningOutput = Proto::SigningOutput<'static>;
+ type PreSigningOutput = CompilerProto::PreSigningOutput<'static>;
+ type TransactionUtil = PactusTransactionUtil;
+
+ // Optional modules:
+ type JsonSigner = NoJsonSigner;
+ type PlanBuilder = NoPlanBuilder;
+ type MessageSigner = NoMessageSigner;
+ type WalletConnector = NoWalletConnector;
+ type TransactionDecoder = NoTransactionDecoder;
+
+ #[inline]
+ fn parse_address(
+ &self,
+ _coin: &dyn CoinContext,
+ address: &str,
+ _prefix: Option,
+ ) -> AddressResult {
+ Address::from_str(address)
+ }
+
+ #[inline]
+ fn parse_address_unchecked(&self, address: &str) -> AddressResult {
+ Address::from_str(address)
+ }
+
+ #[inline]
+ fn derive_address(
+ &self,
+ _coin: &dyn CoinContext,
+ public_key: PublicKey,
+ _derivation: Derivation,
+ _prefix: Option,
+ ) -> AddressResult {
+ let public_key = public_key
+ .to_ed25519()
+ .ok_or(AddressError::PublicKeyTypeMismatch)?;
+ Address::from_public_key(public_key)
+ }
+
+ #[inline]
+ fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput {
+ PactusSigner::sign(coin, input)
+ }
+
+ #[inline]
+ fn preimage_hashes(
+ &self,
+ coin: &dyn CoinContext,
+ input: Self::SigningInput<'_>,
+ ) -> Self::PreSigningOutput {
+ PactusCompiler::preimage_hashes(coin, input)
+ }
+
+ #[inline]
+ fn compile(
+ &self,
+ coin: &dyn CoinContext,
+ input: Self::SigningInput<'_>,
+ signatures: Vec,
+ public_keys: Vec,
+ ) -> Self::SigningOutput {
+ PactusCompiler::compile(coin, input, signatures, public_keys)
+ }
+
+ #[inline]
+ fn transaction_util(&self) -> Option {
+ Some(PactusTransactionUtil)
+ }
+}
diff --git a/rust/chains/tw_pactus/src/lib.rs b/rust/chains/tw_pactus/src/lib.rs
new file mode 100644
index 00000000000..938d9bba25a
--- /dev/null
+++ b/rust/chains/tw_pactus/src/lib.rs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+pub mod compiler;
+pub mod encoder;
+pub mod entry;
+pub mod modules;
+pub mod signer;
+pub mod transaction;
+pub mod types;
diff --git a/rust/chains/tw_pactus/src/modules/mod.rs b/rust/chains/tw_pactus/src/modules/mod.rs
new file mode 100644
index 00000000000..b09e92443b1
--- /dev/null
+++ b/rust/chains/tw_pactus/src/modules/mod.rs
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+pub mod transaction_util;
+pub mod tx_builder;
diff --git a/rust/chains/tw_pactus/src/modules/transaction_util.rs b/rust/chains/tw_pactus/src/modules/transaction_util.rs
new file mode 100644
index 00000000000..a28e72b8f35
--- /dev/null
+++ b/rust/chains/tw_pactus/src/modules/transaction_util.rs
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use tw_coin_entry::coin_context::CoinContext;
+use tw_coin_entry::error::prelude::*;
+use tw_coin_entry::modules::transaction_util::TransactionUtil;
+use tw_encoding::hex;
+
+use crate::encoder::deserialize;
+use crate::transaction::Transaction;
+
+pub struct PactusTransactionUtil;
+
+impl TransactionUtil for PactusTransactionUtil {
+ fn calc_tx_hash(&self, coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult {
+ Self::calc_tx_hash_impl(coin, encoded_tx)
+ }
+}
+
+impl PactusTransactionUtil {
+ fn calc_tx_hash_impl(_coin: &dyn CoinContext, encoded_tx: &str) -> SigningResult {
+ let trx_bytes = hex::decode(encoded_tx).map_err(|_| SigningErrorType::Error_input_parse)?;
+
+ let trx = deserialize::(&trx_bytes)?;
+
+ Ok(hex::encode(trx.id(), false))
+ }
+}
diff --git a/rust/chains/tw_pactus/src/modules/tx_builder.rs b/rust/chains/tw_pactus/src/modules/tx_builder.rs
new file mode 100644
index 00000000000..e9e2449be96
--- /dev/null
+++ b/rust/chains/tw_pactus/src/modules/tx_builder.rs
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use crate::transaction::payload::{BondPayload, Payload, TransferPayload};
+use crate::transaction::Transaction;
+use crate::types::{Address, Amount, ValidatorPublicKey};
+use std::str::FromStr;
+use tw_coin_entry::error::prelude::*;
+use tw_proto::Pactus;
+
+pub struct TxBuilder;
+
+impl TxBuilder {
+ pub fn from_proto(input: &Pactus::Proto::SigningInput) -> SigningResult {
+ match &input.transaction {
+ None => SigningError::err(SigningErrorType::Error_invalid_params),
+ Some(trx) => {
+ let payload: Box = match &trx.payload {
+ Pactus::Proto::mod_TransactionMessage::OneOfpayload::transfer(pld) => {
+ let sender = Address::from_str(&pld.sender)?;
+ let receiver = Address::from_str(&pld.receiver)?;
+ Box::new(TransferPayload::new(sender, receiver, Amount(pld.amount)))
+ },
+ Pactus::Proto::mod_TransactionMessage::OneOfpayload::bond(pld) => {
+ let sender = Address::from_str(&pld.sender)?;
+ let receiver = Address::from_str(&pld.receiver)?;
+ let public_key = if !pld.public_key.is_empty() {
+ Some(ValidatorPublicKey::from_str(&pld.public_key)?)
+ } else {
+ None
+ };
+
+ Box::new(BondPayload::new(
+ sender,
+ receiver,
+ Amount(pld.stake),
+ public_key,
+ ))
+ },
+ Pactus::Proto::mod_TransactionMessage::OneOfpayload::None => {
+ return SigningError::err(SigningErrorType::Error_invalid_params)
+ },
+ };
+
+ Ok(Transaction::new(
+ trx.lock_time,
+ Amount(trx.fee),
+ trx.memo.to_string(),
+ payload,
+ ))
+ },
+ }
+ }
+}
diff --git a/rust/chains/tw_pactus/src/signer.rs b/rust/chains/tw_pactus/src/signer.rs
new file mode 100644
index 00000000000..baf4f0a068b
--- /dev/null
+++ b/rust/chains/tw_pactus/src/signer.rs
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use tw_coin_entry::coin_context::CoinContext;
+use tw_coin_entry::error::prelude::*;
+use tw_coin_entry::signing_output_error;
+use tw_keypair::ed25519;
+use tw_keypair::traits::KeyPairTrait;
+use tw_proto::Pactus::Proto;
+
+use crate::modules::tx_builder::TxBuilder;
+
+pub struct PactusSigner;
+
+impl PactusSigner {
+ 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 mut trx = TxBuilder::from_proto(&input)?;
+ let key_pair = ed25519::sha512::KeyPair::try_from(input.private_key.as_ref())?;
+ let signature = trx.sign(key_pair.private())?;
+
+ let data = trx.to_bytes()?;
+
+ let output = Proto::SigningOutput {
+ transaction_id: trx.id().into(),
+ signed_transaction_data: data.into(),
+ signature: signature.to_bytes().to_vec().into(),
+ ..Proto::SigningOutput::default()
+ };
+
+ Ok(output)
+ }
+}
diff --git a/rust/chains/tw_pactus/src/transaction/mod.rs b/rust/chains/tw_pactus/src/transaction/mod.rs
new file mode 100644
index 00000000000..6e00ff37699
--- /dev/null
+++ b/rust/chains/tw_pactus/src/transaction/mod.rs
@@ -0,0 +1,274 @@
+pub mod payload;
+
+use std::fmt::Debug;
+
+use payload::{BondPayload, Payload, PayloadType, TransferPayload};
+use tw_coin_entry::error::prelude::SigningResult;
+use tw_hash::blake2::blake2_b;
+use tw_keypair::ed25519::sha512::{PrivateKey, PublicKey};
+use tw_keypair::ed25519::Signature;
+use tw_keypair::traits::SigningKeyTrait;
+
+use crate::encoder::error::Error as EncoderError;
+use crate::encoder::{deserialize, Decodable, Encodable};
+use crate::types::Amount;
+
+const VERSION_LATEST: u8 = 1;
+const FLAG_NOT_SIGNED: u8 = 0x02;
+
+#[derive(Debug)]
+pub struct Transaction {
+ flags: u8,
+ version: u8,
+ lock_time: u32,
+ fee: Amount,
+ memo: String,
+ payload: Box,
+ signature: Option,
+ public_key: Option,
+}
+
+impl Transaction {
+ pub fn new(lock_time: u32, fee: Amount, memo: String, payload: Box) -> Self {
+ Transaction {
+ flags: FLAG_NOT_SIGNED,
+ version: VERSION_LATEST,
+ lock_time,
+ fee,
+ memo,
+ payload,
+ public_key: None,
+ signature: None,
+ }
+ }
+
+ pub fn from_bytes(input: &[u8]) -> SigningResult {
+ Ok(deserialize::(input)?)
+ }
+
+ pub fn sign(&mut self, private_key: &PrivateKey) -> SigningResult {
+ let sign_bytes = self.sign_bytes()?;
+ let signature = private_key.sign(sign_bytes)?;
+
+ self.set_signatory(private_key.public(), signature.clone());
+
+ Ok(signature)
+ }
+
+ pub fn set_signatory(&mut self, public_key: PublicKey, signature: Signature) {
+ // Unset "Not Signed" flag
+ self.flags &= !FLAG_NOT_SIGNED;
+
+ self.public_key = Some(public_key);
+ self.signature = Some(signature);
+ }
+
+ pub fn id(&self) -> Vec {
+ blake2_b(&self.sign_bytes().unwrap_or_default(), 32).unwrap_or_default()
+ }
+
+ pub fn to_bytes(&self) -> SigningResult> {
+ let mut w = Vec::with_capacity(self.encoded_size());
+
+ self.encode(&mut w)?;
+
+ Ok(w.to_vec())
+ }
+
+ pub fn sign_bytes(&self) -> SigningResult> {
+ let mut w = Vec::new();
+ self.encode_with_no_signatory(&mut w)?;
+ let mut sign_bytes = w.to_vec();
+ sign_bytes.remove(0); // Remove flags
+
+ Ok(sign_bytes)
+ }
+
+ fn encode_with_no_signatory(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> {
+ self.flags.encode(w)?;
+ self.version.encode(w)?;
+ self.lock_time.encode(w)?;
+ self.fee.encode(w)?;
+ self.memo.encode(w)?;
+ self.payload.payload_type().encode(w)?;
+ self.payload.encode(w)?;
+
+ Ok(())
+ }
+
+ fn is_signed(&self) -> bool {
+ self.flags & FLAG_NOT_SIGNED == 0
+ }
+}
+
+impl Encodable for Transaction {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> {
+ self.encode_with_no_signatory(w)?;
+
+ if let Some(sig) = &self.signature {
+ sig.encode(w)?;
+ }
+
+ if let Some(pub_key) = &self.public_key {
+ pub_key.encode(w)?;
+ }
+
+ Ok(())
+ }
+
+ fn encoded_size(&self) -> usize {
+ let mut len = self.flags.encoded_size()
+ + self.version.encoded_size()
+ + self.lock_time.encoded_size()
+ + self.payload.payload_type().encoded_size()
+ + self.fee.encoded_size()
+ + self.memo.encoded_size()
+ + self.payload.encoded_size();
+
+ if let Some(sig) = &self.signature {
+ len += sig.encoded_size();
+ }
+
+ if let Some(pub_key) = &self.public_key {
+ len += pub_key.encoded_size();
+ }
+
+ len
+ }
+}
+
+impl Decodable for Transaction {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let flags = u8::decode(r)?;
+ let version = u8::decode(r)?;
+ let lock_time = u32::decode(r)?;
+ let fee = Amount::decode(r)?;
+ let memo = String::decode(r)?;
+ let payload_type = PayloadType::decode(r)?;
+ let payload: Box = match payload_type {
+ PayloadType::Transfer => Box::new(TransferPayload::decode(r)?),
+ PayloadType::Bond => Box::new(BondPayload::decode(r)?),
+ _ => return Err(EncoderError::ParseFailed("Unsupported payload")),
+ };
+
+ let mut trx = Transaction {
+ flags,
+ version,
+ lock_time,
+ fee,
+ memo,
+ payload,
+ public_key: None,
+ signature: None,
+ };
+
+ if !trx.is_signed() {
+ return Ok(trx);
+ }
+
+ let signature = Signature::decode(r)?;
+ let public_key = PublicKey::decode(r)?;
+
+ trx.signature = Some(signature);
+ trx.public_key = Some(public_key);
+
+ Ok(trx)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use tw_encoding::hex::DecodeHex;
+
+ use crate::types::Address;
+
+ use super::*;
+
+ #[test]
+ fn test_payload_type_encoding() {
+ let mut stream = Vec::new();
+
+ let payload = PayloadType::Unbond;
+ payload.encode(&mut stream).unwrap();
+ assert_eq!(stream.to_vec(), &[4]);
+ }
+
+ const TRANSACTION_NOT_SIGNED: &str = concat!(
+ "02", // Flags
+ "01", // Version
+ "01020300", // LockTime
+ "e807", // Fee
+ "0474657374", // Memo
+ "01", // PayloadType
+ "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender
+ "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver
+ "a09c01" // Amount
+ );
+
+ const TRANSACTION_SIGNED: &str = concat!(
+ "00", // Flags
+ "01", // Version
+ "01020300", // LockTime
+ "e807", // Fee
+ "0474657374", // Memo
+ "01", // PayloadType
+ "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender
+ "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver
+ "a09c01", // Amount
+ "50ac25c7125271489b0cd230549257c93fb8c6265f2914a988ba7b81c1bc47ff", // Signature
+ "f027412dd59447867911035ff69742d171060a1f132ac38b95acc6e39ec0bd09",
+ "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" // PublicKey
+ );
+
+ const TX_ID: &str = "34cd4656a98f7eb996e83efdc384cefbe3a9c52dca79a99245b4eacc0b0b4311";
+
+ #[test]
+ fn test_sign_signature() {
+ let expected_data = TRANSACTION_SIGNED.decode_hex().unwrap();
+ let expected_id = TX_ID.decode_hex().unwrap();
+
+ let sender = Address::from_str("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr").unwrap();
+ let receiver = Address::from_str("pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra").unwrap();
+ let payload = Box::new(TransferPayload::new(sender, receiver, Amount(20000)));
+ let mut trx = Transaction::new(0x00030201, Amount(1000), "test".to_string(), payload);
+
+ let private_key_data = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"
+ .decode_hex()
+ .unwrap();
+ let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap();
+ trx.sign(&private_key).unwrap();
+
+ assert_eq!(expected_data, trx.to_bytes().unwrap());
+ assert_eq!(expected_id, trx.id());
+ }
+
+ #[test]
+ fn test_encoding_not_signed() {
+ let data = TRANSACTION_NOT_SIGNED.decode_hex().unwrap();
+ let trx = Transaction::from_bytes(&data).unwrap();
+ let expected_id = TX_ID.decode_hex().unwrap();
+
+ let encoded_data = trx.to_bytes().unwrap();
+
+ assert_eq!(encoded_data, data);
+ assert_eq!(expected_id, trx.id());
+ assert_eq!(trx.encoded_size(), data.len());
+ assert!(!trx.is_signed());
+ }
+
+ #[test]
+ fn test_encoding_signed() {
+ let data = TRANSACTION_SIGNED.decode_hex().unwrap();
+ let trx = Transaction::from_bytes(&data).unwrap();
+ let expected_id = TX_ID.decode_hex().unwrap();
+
+ let encoded_data = trx.to_bytes().unwrap();
+
+ assert_eq!(encoded_data, data);
+ assert_eq!(expected_id, trx.id());
+ assert_eq!(trx.encoded_size(), data.len());
+ assert!(trx.is_signed());
+ }
+}
diff --git a/rust/chains/tw_pactus/src/transaction/payload/bond.rs b/rust/chains/tw_pactus/src/transaction/payload/bond.rs
new file mode 100644
index 00000000000..152fa5a6a7b
--- /dev/null
+++ b/rust/chains/tw_pactus/src/transaction/payload/bond.rs
@@ -0,0 +1,100 @@
+use crate::encoder::error::Error as EncoderError;
+use crate::{
+ encoder::{Decodable, Encodable},
+ types::{Address, Amount, ValidatorPublicKey},
+};
+
+use super::{Payload, PayloadType};
+
+pub const BLS_PUBLIC_KEY_SIZE: usize = 96;
+
+#[derive(Debug)]
+pub struct BondPayload {
+ sender: Address,
+ receiver: Address,
+ stake: Amount,
+ public_key: Option,
+}
+
+impl BondPayload {
+ pub fn new(
+ sender: Address,
+ receiver: Address,
+ stake: Amount,
+ public_key: Option,
+ ) -> Self {
+ BondPayload {
+ sender,
+ receiver,
+ stake,
+ public_key,
+ }
+ }
+}
+
+impl Encodable for BondPayload {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> {
+ self.sender.encode(w)?;
+ self.receiver.encode(w)?;
+
+ match self.public_key {
+ Some(ref public_key) => {
+ (BLS_PUBLIC_KEY_SIZE as u8).encode(w)?;
+ public_key.encode(w)?;
+ },
+ None => {
+ 0u8.encode(w)?;
+ },
+ }
+
+ self.stake.encode(w)?;
+ Ok(())
+ }
+
+ fn encoded_size(&self) -> usize {
+ self.sender.encoded_size()
+ + self.receiver.encoded_size()
+ + self.stake.encoded_size()
+ + match self.public_key {
+ Some(ref public_key) => 1 + public_key.encoded_size(),
+ None => 1,
+ }
+ }
+}
+
+impl Decodable for BondPayload {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let sender = Address::decode(r)?;
+ let receiver = Address::decode(r)?;
+
+ let mut public_key = None;
+ let public_key_size: u8 = u8::decode(r)?;
+
+ if public_key_size == BLS_PUBLIC_KEY_SIZE as u8 {
+ public_key = Some(ValidatorPublicKey::decode(r)?);
+ } else if public_key_size != 0 {
+ return Err(EncoderError::ParseFailed("invalid public key size"));
+ }
+
+ let stake = Amount::decode(r)?;
+
+ Ok(BondPayload {
+ sender,
+ receiver,
+ stake,
+ public_key,
+ })
+ }
+}
+
+impl Payload for BondPayload {
+ fn signer(&self) -> &Address {
+ &self.sender
+ }
+ fn value(&self) -> Amount {
+ self.stake.clone()
+ }
+ fn payload_type(&self) -> PayloadType {
+ PayloadType::Bond
+ }
+}
diff --git a/rust/chains/tw_pactus/src/transaction/payload/mod.rs b/rust/chains/tw_pactus/src/transaction/payload/mod.rs
new file mode 100644
index 00000000000..110155033dd
--- /dev/null
+++ b/rust/chains/tw_pactus/src/transaction/payload/mod.rs
@@ -0,0 +1,60 @@
+mod bond;
+mod transfer;
+
+pub use bond::BondPayload;
+pub use transfer::TransferPayload;
+
+use std::fmt::Debug;
+
+use crate::encoder::error::Error as EncoderError;
+use crate::{
+ encoder::{Decodable, Encodable},
+ types::{Address, Amount},
+};
+
+#[derive(Debug, Clone, Copy)]
+#[repr(u8)]
+pub enum PayloadType {
+ Transfer = 1,
+ Bond = 2,
+ Sortition = 3,
+ Unbond = 4,
+ Withdraw = 5,
+}
+
+impl TryFrom for PayloadType {
+ type Error = EncoderError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 1 => Ok(PayloadType::Transfer),
+ 2 => Ok(PayloadType::Bond),
+ 3 => Ok(PayloadType::Sortition),
+ 4 => Ok(PayloadType::Unbond),
+ 5 => Ok(PayloadType::Withdraw),
+ _ => Err(EncoderError::ParseFailed("Invalid PayloadType value")),
+ }
+ }
+}
+
+impl Encodable for PayloadType {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> {
+ (*self as u8).encode(w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ 1
+ }
+}
+
+impl Decodable for PayloadType {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ PayloadType::try_from(u8::decode(r)?)
+ }
+}
+
+pub trait Payload: Debug + Encodable {
+ fn signer(&self) -> &Address;
+ fn value(&self) -> Amount;
+ fn payload_type(&self) -> PayloadType;
+}
diff --git a/rust/chains/tw_pactus/src/transaction/payload/transfer.rs b/rust/chains/tw_pactus/src/transaction/payload/transfer.rs
new file mode 100644
index 00000000000..a39854c0429
--- /dev/null
+++ b/rust/chains/tw_pactus/src/transaction/payload/transfer.rs
@@ -0,0 +1,64 @@
+use crate::encoder::error::Error as EncoderError;
+use crate::{
+ encoder::{Decodable, Encodable},
+ types::{Address, Amount},
+};
+
+use super::{Payload, PayloadType};
+
+#[derive(Debug)]
+pub struct TransferPayload {
+ sender: Address,
+ receiver: Address,
+ amount: Amount,
+}
+
+impl TransferPayload {
+ pub fn new(sender: Address, receiver: Address, amount: Amount) -> Self {
+ TransferPayload {
+ sender,
+ receiver,
+ amount,
+ }
+ }
+}
+
+impl Encodable for TransferPayload {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), EncoderError> {
+ self.sender.encode(w)?;
+ self.receiver.encode(w)?;
+ self.amount.encode(w)?;
+
+ Ok(())
+ }
+
+ fn encoded_size(&self) -> usize {
+ self.sender.encoded_size() + self.receiver.encoded_size() + self.amount.encoded_size()
+ }
+}
+
+impl Decodable for TransferPayload {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let sender = Address::decode(r)?;
+ let receiver = Address::decode(r)?;
+ let amount = Amount::decode(r)?;
+
+ Ok(TransferPayload {
+ sender,
+ receiver,
+ amount,
+ })
+ }
+}
+
+impl Payload for TransferPayload {
+ fn signer(&self) -> &Address {
+ &self.sender
+ }
+ fn value(&self) -> Amount {
+ self.amount.clone()
+ }
+ fn payload_type(&self) -> PayloadType {
+ PayloadType::Transfer
+ }
+}
diff --git a/rust/chains/tw_pactus/src/types/address.rs b/rust/chains/tw_pactus/src/types/address.rs
new file mode 100644
index 00000000000..785e82c6639
--- /dev/null
+++ b/rust/chains/tw_pactus/src/types/address.rs
@@ -0,0 +1,335 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use std::fmt;
+use std::str::FromStr;
+
+use bech32::{FromBase32, ToBase32};
+use tw_coin_entry::coin_entry::CoinAddress;
+use tw_coin_entry::error::prelude::*;
+use tw_hash::blake2::blake2_b;
+use tw_hash::ripemd::ripemd_160;
+use tw_hash::H160;
+use tw_keypair::ed25519::sha512::PublicKey;
+use tw_memory::Data;
+
+use crate::encoder::error::Error;
+use crate::encoder::{Decodable, Encodable};
+
+const ADDRESS_HRP: &str = "pc";
+const TREASURY_ADDRESS_STRING: &str = "000000000000000000000000000000000000000000";
+
+/// Enum for Pactus address types.
+#[derive(Debug, Clone, PartialEq)]
+pub enum AddressType {
+ Treasury = 0,
+ Validator = 1,
+ BlsAccount = 2,
+ Ed25519Account = 3,
+}
+
+impl TryFrom for AddressType {
+ type Error = AddressError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 0 => Ok(AddressType::Treasury),
+ 1 => Ok(AddressType::Validator),
+ 2 => Ok(AddressType::BlsAccount),
+ 3 => Ok(AddressType::Ed25519Account),
+ _ => Err(AddressError::Unsupported),
+ }
+ }
+}
+
+impl Encodable for AddressType {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ (self.clone() as u8).encode(w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ 1
+ }
+}
+
+impl Decodable for AddressType {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ AddressType::try_from(u8::decode(r)?)
+ .map_err(|_| Error::ParseFailed("Invalid address type"))
+ }
+}
+
+/// Pactus addresses are 21 bytes long.
+/// The first byte indicates the address type, and the remaining 20 bytes
+/// represent the hash of the public key.
+/// The hash is computed as RIPEMD160(Blake2b(public key)).
+#[derive(Debug, Clone, PartialEq)]
+pub struct Address {
+ addr_type: AddressType,
+ pub_hash: H160,
+}
+
+impl Address {
+ pub fn from_public_key(public_key: &PublicKey) -> Result {
+ let pud_data = public_key.to_bytes();
+ let pub_hash_data =
+ ripemd_160(&blake2_b(pud_data.as_ref(), 32).map_err(|_| AddressError::Internal)?);
+ let pub_hash = Address::vec_to_pub_hash(pub_hash_data)?;
+
+ Ok(Address {
+ addr_type: AddressType::Ed25519Account,
+ pub_hash,
+ })
+ }
+
+ pub fn is_treasury(&self) -> bool {
+ self.addr_type == AddressType::Treasury && self.pub_hash.is_zero()
+ }
+
+ pub fn vec_to_pub_hash(vec: Vec) -> Result {
+ H160::try_from(vec.as_slice()).map_err(|_| AddressError::Internal)
+ }
+}
+
+impl CoinAddress for Address {
+ #[inline]
+ fn data(&self) -> Data {
+ let mut data = Vec::with_capacity(21);
+ data.push(self.addr_type.clone() as u8);
+ data.extend_from_slice(self.pub_hash.as_ref());
+
+ data
+ }
+}
+
+// Pactus addresses are encoded into a string format using the Bech32m encoding scheme.
+impl fmt::Display for Address {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_treasury() {
+ return f.write_str(TREASURY_ADDRESS_STRING);
+ }
+
+ let mut b32 = Vec::with_capacity(33);
+
+ b32.push(bech32::u5::try_from_u8(self.addr_type.clone() as u8).map_err(|_| fmt::Error)?);
+ b32.extend_from_slice(&self.pub_hash.to_vec().to_base32());
+ bech32::encode_to_fmt(f, ADDRESS_HRP, &b32, bech32::Variant::Bech32m)
+ .map_err(|_| fmt::Error)?
+ }
+}
+
+impl Encodable for Address {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ self.addr_type.encode(w)?;
+
+ if self.is_treasury() {
+ return Ok(());
+ }
+
+ self.pub_hash.encode(w)?;
+
+ Ok(())
+ }
+
+ fn encoded_size(&self) -> usize {
+ if self.is_treasury() {
+ return 1;
+ }
+
+ 21
+ }
+}
+
+impl Decodable for Address {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ let addr_type = AddressType::decode(r)?;
+ if addr_type == AddressType::Treasury {
+ return Ok(Address {
+ addr_type,
+ pub_hash: H160::new(),
+ });
+ }
+
+ let pub_hash = H160::decode(r)?;
+ Ok(Address {
+ addr_type,
+ pub_hash,
+ })
+ }
+}
+
+impl FromStr for Address {
+ type Err = AddressError;
+
+ fn from_str(s: &str) -> Result {
+ if s == TREASURY_ADDRESS_STRING {
+ return Ok(Address {
+ addr_type: AddressType::Treasury,
+ pub_hash: H160::new(),
+ });
+ }
+
+ let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?;
+
+ if hrp != ADDRESS_HRP {
+ return Err(AddressError::InvalidHrp);
+ }
+
+ if b32.len() != 33 {
+ return Err(AddressError::InvalidInput);
+ }
+
+ let addr_type = AddressType::try_from(b32[0].to_u8())?;
+ let b8 = Vec::::from_base32(&b32[1..]).map_err(|_| AddressError::InvalidInput)?;
+ let pub_hash = Address::vec_to_pub_hash(b8)?;
+
+ Ok(Address {
+ addr_type,
+ pub_hash,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use tw_encoding::hex::{DecodeHex, ToHex};
+ use tw_keypair::ed25519::sha512::PrivateKey;
+
+ use super::*;
+ use crate::encoder::{deserialize, Encodable};
+
+ #[test]
+ fn test_treasury_address_encoding() {
+ let addr = Address::from_str(TREASURY_ADDRESS_STRING).unwrap();
+ assert!(addr.is_treasury());
+
+ let mut w = Vec::new();
+ addr.encode(&mut w).unwrap();
+ assert_eq!(w.to_vec(), [0x00]);
+ assert_eq!(addr.encoded_size(), 1);
+ }
+
+ #[test]
+ fn test_treasury_address_decoding() {
+ let data = vec![0u8];
+
+ let addr = deserialize::(&data).unwrap();
+ assert!(addr.is_treasury());
+ assert_eq!(addr.to_string(), TREASURY_ADDRESS_STRING);
+ }
+
+ #[test]
+ fn test_address_encoding() {
+ let addr = Address::from_str("pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra").unwrap();
+ assert!(!addr.is_treasury());
+
+ let mut w = Vec::new();
+ addr.encode(&mut w).unwrap();
+ assert_eq!(
+ w.to_vec(),
+ "03000102030405060708090a0b0c0d0e0f00010203"
+ .decode_hex()
+ .unwrap()
+ );
+ assert_eq!(addr.encoded_size(), 21);
+ }
+
+ #[test]
+ fn test_address_decoding() {
+ let data = "03000102030405060708090a0b0c0d0e0f00010203"
+ .decode_hex()
+ .unwrap();
+
+ let addr = deserialize::(&data).unwrap();
+ assert!(!addr.is_treasury());
+ assert_eq!(
+ addr.to_string(),
+ "pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra"
+ );
+ }
+
+ #[test]
+ fn test_address_string() {
+ struct TestCase<'a> {
+ name: &'a str,
+ addr_type: AddressType,
+ pub_hash: &'a str,
+ expected_addr: &'a str,
+ }
+
+ // Define a list of test cases for encoding and decoding
+ let test_cases = vec![
+ TestCase {
+ name: "Type Treasury (0)",
+ addr_type: AddressType::Treasury,
+ pub_hash: "0000000000000000000000000000000000000000",
+ expected_addr: TREASURY_ADDRESS_STRING,
+ },
+ TestCase {
+ name: "Type Validator (1)",
+ addr_type: AddressType::Validator,
+ pub_hash: "000102030405060708090a0b0c0d0e0f00010203",
+ expected_addr: "pc1pqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr803qet",
+ },
+ TestCase {
+ name: "Type BLS-Account (2)",
+ addr_type: AddressType::BlsAccount,
+ pub_hash: "000102030405060708090a0b0c0d0e0f00010203",
+ expected_addr: "pc1zqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr6ypawk",
+ },
+ TestCase {
+ name: "Type Secp256k1-Account (3)",
+ addr_type: AddressType::Ed25519Account,
+ pub_hash: "000102030405060708090a0b0c0d0e0f00010203",
+ expected_addr: "pc1rqqqsyqcyq5rqwzqfpg9scrgwpuqqzqsr36kkra",
+ },
+ ];
+
+ for case in test_cases {
+ let pub_hash_data = case.pub_hash.decode_hex().unwrap();
+ let addr = Address {
+ addr_type: case.addr_type,
+ pub_hash: Address::vec_to_pub_hash(pub_hash_data).unwrap(),
+ };
+
+ let addr_str = addr.to_string();
+ assert_eq!(addr_str, case.expected_addr, "test {} failed", case.name);
+ }
+ }
+
+ #[test]
+ fn test_encodable() {
+ let expected_data = "03b281dee7850ca2272d9ba95b16d48030821aaf27"
+ .decode_hex()
+ .unwrap();
+ let private_key = PrivateKey::try_from(
+ "afeefca74d9a325cf1d6b6911d61a65c32afa8e02bd5e78e2e4ac2910bab45f5",
+ )
+ .unwrap();
+ let address = Address::from_public_key(&private_key.public()).unwrap();
+ let mut w = Vec::new();
+
+ address.encode(&mut w).unwrap();
+
+ assert_eq!(expected_data, w.to_vec(),);
+ assert_eq!(expected_data.len(), address.encoded_size());
+ }
+
+ #[test]
+ fn test_address_from_private_key() {
+ let private_key_data = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6"
+ .decode_hex()
+ .unwrap();
+ let private_key = PrivateKey::try_from(private_key_data.as_slice()).unwrap();
+ let public_key = private_key.public();
+ let address = Address::from_public_key(&public_key).unwrap();
+
+ let expected_public_key =
+ "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa";
+ let expected_address = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr";
+
+ assert_eq!(public_key.to_bytes().to_hex(), expected_public_key);
+ assert_eq!(address.to_string(), expected_address);
+ }
+}
diff --git a/rust/chains/tw_pactus/src/types/amount.rs b/rust/chains/tw_pactus/src/types/amount.rs
new file mode 100644
index 00000000000..84c9e156e6a
--- /dev/null
+++ b/rust/chains/tw_pactus/src/types/amount.rs
@@ -0,0 +1,20 @@
+use crate::encoder::{error::Error, var_int::VarInt, Decodable, Encodable};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Amount(pub i64);
+
+impl Encodable for Amount {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ VarInt::from(self.0 as usize).encode(w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ VarInt::from(self.0 as usize).encoded_size()
+ }
+}
+
+impl Decodable for Amount {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ Ok(Amount(*VarInt::decode(r)? as i64))
+ }
+}
diff --git a/rust/chains/tw_pactus/src/types/mod.rs b/rust/chains/tw_pactus/src/types/mod.rs
new file mode 100644
index 00000000000..ff66b91c4f4
--- /dev/null
+++ b/rust/chains/tw_pactus/src/types/mod.rs
@@ -0,0 +1,7 @@
+pub mod address;
+pub mod amount;
+pub mod validator_public_key;
+
+pub use address::Address;
+pub use amount::Amount;
+pub use validator_public_key::ValidatorPublicKey;
diff --git a/rust/chains/tw_pactus/src/types/validator_public_key.rs b/rust/chains/tw_pactus/src/types/validator_public_key.rs
new file mode 100644
index 00000000000..84425774143
--- /dev/null
+++ b/rust/chains/tw_pactus/src/types/validator_public_key.rs
@@ -0,0 +1,105 @@
+use crate::encoder::error::Error;
+use crate::encoder::{decode::decode_fix_slice, encode::encode_fix_slice};
+use crate::encoder::{Decodable, Encodable};
+use bech32::FromBase32;
+use std::str::FromStr;
+use tw_keypair::KeyPairError;
+
+pub const BLS_PUBLIC_KEY_SIZE: usize = 96;
+pub const PUBLIC_KEY_HRP: &str = "public";
+
+#[derive(Debug)]
+pub struct ValidatorPublicKey(pub [u8; BLS_PUBLIC_KEY_SIZE]);
+
+impl Encodable for ValidatorPublicKey {
+ fn encode(&self, w: &mut dyn std::io::Write) -> Result<(), Error> {
+ encode_fix_slice(&self.0, w)
+ }
+
+ fn encoded_size(&self) -> usize {
+ BLS_PUBLIC_KEY_SIZE
+ }
+}
+
+impl Decodable for ValidatorPublicKey {
+ fn decode(r: &mut dyn std::io::Read) -> Result {
+ Ok(ValidatorPublicKey(decode_fix_slice::(
+ r,
+ )?))
+ }
+}
+
+impl FromStr for ValidatorPublicKey {
+ type Err = KeyPairError;
+
+ fn from_str(s: &str) -> Result {
+ let (hrp, b32, _variant) = bech32::decode(s).map_err(|_| KeyPairError::InvalidPublicKey)?;
+ if hrp != PUBLIC_KEY_HRP {
+ return Err(KeyPairError::InvalidPublicKey);
+ }
+
+ let b8 = Vec::::from_base32(&b32[1..]).map_err(|_| KeyPairError::InvalidPublicKey)?;
+ let pub_data = b8.try_into().map_err(|_| KeyPairError::InvalidPublicKey)?;
+
+ Ok(ValidatorPublicKey(pub_data))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::str::FromStr;
+
+ use tw_encoding::hex::DecodeHex;
+
+ use crate::types::ValidatorPublicKey;
+
+ #[test]
+ fn test_public_key_string() {
+ struct TestCase<'a> {
+ name: &'a str,
+ pub_key_str: &'a str,
+ pub_key_data: &'a str,
+ }
+
+ // Define a list of test cases for encoding and decoding
+ let test_cases = vec![
+ TestCase {
+ name: "invalid checksum",
+ pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx470",
+ pub_key_data: "",
+ },
+ TestCase {
+ name: "invalid length: 95",
+ pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg73y98kl",
+ pub_key_data: "",
+ },
+ TestCase {
+ name: "invalid HRP",
+ pub_key_str: "xxx1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5evslaq",
+ pub_key_data: "",
+ },
+ TestCase {
+ name: "OK",
+ pub_key_str: "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a",
+ pub_key_data: "af0f74917f5065af94727ae9541b0ddcfb5b828a9e016b02498f477ed37fb44d5d882495afb6fd4f9773e4ea9deee436030c4d61c6e3a1151585e1d838cae1444a438d089ce77e10c492a55f6908125c5be9b236a246e4082d08de564e111e65",
+ },
+ ];
+
+ for case in test_cases {
+ let pub_key_data = case.pub_key_data.decode_hex().unwrap().to_vec();
+ let test_result = ValidatorPublicKey::from_str(case.pub_key_str);
+
+ if pub_key_data.is_empty() {
+ assert!(test_result.is_err());
+ } else {
+ assert!(test_result.is_ok());
+ assert_eq!(
+ test_result.unwrap().0.to_vec(),
+ pub_key_data,
+ "test {} failed",
+ case.name
+ );
+ }
+ }
+ }
+}
diff --git a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs
index 4a95a378699..ec4e3ae9d2e 100644
--- a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs
+++ b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs
@@ -64,17 +64,22 @@ mod tests {
let mut stream = Stream::default();
stream
- .append(&CompactInteger::from(0_usize))
+ .append(&CompactInteger::from(0x00_usize))
.append(&CompactInteger::from(0xfc_usize))
.append(&CompactInteger::from(0xfd_usize))
.append(&CompactInteger::from(0xffff_usize))
- .append(&CompactInteger::from(0x10000_usize))
+ .append(&CompactInteger::from(0x01_0000_usize))
.append(&CompactInteger::from(0xffff_ffff_usize))
- .append(&CompactInteger(0x1_0000_0000_u64));
+ .append(&CompactInteger(0x01_0000_0000_u64));
let expected = vec![
- 0_u8, 0xfc, 0xfd, 0xfd, 0x00, 0xfd, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x01, 0x00, 0xfe,
- 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x00, // 0x00
+ 0xfc, // 0xfc
+ 0xfd, 0xfd, 0x00, // 0xfd
+ 0xfd, 0xff, 0xff, // 0xffff
+ 0xfe, 0x00, 0x00, 0x01, 0x00, // 0x01_0000
+ 0xfe, 0xff, 0xff, 0xff, 0xff, // 0xffff_ffff
+ 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // 0x01_0000_0000
];
assert_eq!(stream.out(), expected);
diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml
index 40e2a51b0ef..7bff39d1ea0 100644
--- a/rust/tw_coin_registry/Cargo.toml
+++ b/rust/tw_coin_registry/Cargo.toml
@@ -25,6 +25,7 @@ tw_memory = { path = "../tw_memory" }
tw_misc = { path = "../tw_misc" }
tw_native_evmos = { path = "../chains/tw_native_evmos" }
tw_native_injective = { path = "../chains/tw_native_injective" }
+tw_pactus = { path = "../chains/tw_pactus" }
tw_ronin = { path = "../chains/tw_ronin" }
tw_solana = { path = "../chains/tw_solana" }
tw_sui = { path = "../chains/tw_sui" }
diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs
index 93630d0d66d..cde37c58cde 100644
--- a/rust/tw_coin_registry/src/blockchain_type.rs
+++ b/rust/tw_coin_registry/src/blockchain_type.rs
@@ -19,6 +19,7 @@ pub enum BlockchainType {
InternetComputer,
NativeEvmos,
NativeInjective,
+ Pactus,
Ronin,
Solana,
Sui,
diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs
index b7c3f7751b7..d08784ccd56 100644
--- a/rust/tw_coin_registry/src/dispatcher.rs
+++ b/rust/tw_coin_registry/src/dispatcher.rs
@@ -19,6 +19,7 @@ use tw_greenfield::entry::GreenfieldEntry;
use tw_internet_computer::entry::InternetComputerEntry;
use tw_native_evmos::entry::NativeEvmosEntry;
use tw_native_injective::entry::NativeInjectiveEntry;
+use tw_pactus::entry::PactusEntry;
use tw_ronin::entry::RoninEntry;
use tw_solana::entry::SolanaEntry;
use tw_sui::entry::SuiEntry;
@@ -39,6 +40,7 @@ const GREENFIELD: GreenfieldEntry = GreenfieldEntry;
const INTERNET_COMPUTER: InternetComputerEntry = InternetComputerEntry;
const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry;
const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry;
+const PACTUS: PactusEntry = PactusEntry;
const RONIN: RoninEntry = RoninEntry;
const SOLANA: SolanaEntry = SolanaEntry;
const SUI: SuiEntry = SuiEntry;
@@ -59,6 +61,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&INTERNET_COMPUTER),
BlockchainType::NativeEvmos => Ok(&NATIVE_EVMOS),
BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE),
+ BlockchainType::Pactus => Ok(&PACTUS),
BlockchainType::Ronin => Ok(&RONIN),
BlockchainType::Solana => Ok(&SOLANA),
BlockchainType::Sui => Ok(&SUI),
diff --git a/rust/tw_tests/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs
index b147a7b83e5..d048ec74a00 100644
--- a/rust/tw_tests/tests/chains/mod.rs
+++ b/rust/tw_tests/tests/chains/mod.rs
@@ -15,6 +15,7 @@ mod greenfield;
mod internet_computer;
mod native_evmos;
mod native_injective;
+mod pactus;
mod solana;
mod sui;
mod tbinance;
diff --git a/rust/tw_tests/tests/chains/pactus/mod.rs b/rust/tw_tests/tests/chains/pactus/mod.rs
new file mode 100644
index 00000000000..88e8edb4017
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/mod.rs
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+mod pactus_address;
+mod pactus_compile;
+mod pactus_sign;
+mod pactus_transaction_util;
+mod test_cases;
diff --git a/rust/tw_tests/tests/chains/pactus/pactus_address.rs b/rust/tw_tests/tests/chains/pactus/pactus_address.rs
new file mode 100644
index 00000000000..9d64d1590a7
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/pactus_address.rs
@@ -0,0 +1,96 @@
+// 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,
+};
+use tw_coin_registry::coin_type::CoinType;
+
+#[test]
+fn test_pactus_address_derive() {
+ test_address_derive(
+ CoinType::Pactus,
+ "2134ae97465505dfd5a1fd05a8a0f146209c601eb3f1b0363b4cfe4b47ba1ab4",
+ "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl",
+ );
+}
+
+#[test]
+fn test_pactus_address_normalization() {
+ test_address_normalization(
+ CoinType::Pactus,
+ "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl",
+ "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl",
+ );
+}
+
+#[test]
+fn test_pactus_address_is_valid() {
+ test_address_valid(
+ CoinType::Pactus,
+ "000000000000000000000000000000000000000000",
+ );
+ test_address_valid(
+ CoinType::Pactus,
+ "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr",
+ );
+ test_address_valid(
+ CoinType::Pactus,
+ "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x",
+ );
+ test_address_valid(
+ CoinType::Pactus,
+ "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl",
+ );
+}
+
+#[test]
+fn test_pactus_address_invalid() {
+ test_address_invalid(CoinType::Pactus, "");
+ test_address_invalid(CoinType::Pactus, "00");
+ test_address_invalid(CoinType::Pactus, "not_proper_encoded");
+ test_address_invalid(CoinType::Pactus, "pc1ioiooi");
+ test_address_invalid(CoinType::Pactus, "pc19p72rf");
+ test_address_invalid(
+ CoinType::Pactus,
+ "qc1z0hrct7eflrpw4ccrttxzs4qud2axex4dh8zz75",
+ );
+ test_address_invalid(
+ CoinType::Pactus,
+ "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dg8xaf5",
+ ); // invalid checksum
+ test_address_invalid(
+ CoinType::Pactus,
+ "pc1p0hrct7eflrpw4ccrttxzs4qud2axexs2dhdk8",
+ ); // invalid length
+ test_address_invalid(
+ CoinType::Pactus,
+ "pc1y0hrct7eflrpw4ccrttxzs4qud2axex4dksmred",
+ ); // invalid type
+}
+
+#[test]
+fn test_pactus_address_get_data() {
+ test_address_get_data(
+ CoinType::Pactus,
+ "000000000000000000000000000000000000000000",
+ "000000000000000000000000000000000000000000",
+ );
+ test_address_get_data(
+ CoinType::Pactus,
+ "pc1p0hrct7eflrpw4ccrttxzs4qud2axex4dcdzdfr",
+ "017dc785fb29f8c2eae3035acc28541c6aba6c9aad",
+ );
+ test_address_get_data(
+ CoinType::Pactus,
+ "pc1zzqkzzu4vyddss052as6c37qrdcfptegquw826x",
+ "02102c2172ac235b083e8aec3588f8036e1215e500",
+ );
+ test_address_get_data(
+ CoinType::Pactus,
+ "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl",
+ "03f4acc4cf284bc69a73afa245c2d872bc8d818c88",
+ );
+}
diff --git a/rust/tw_tests/tests/chains/pactus/pactus_compile.rs b/rust/tw_tests/tests/chains/pactus/pactus_compile.rs
new file mode 100644
index 00000000000..27a0c274c76
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/pactus_compile.rs
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use crate::chains::pactus::test_cases::PRIVATE_KEY;
+use crate::chains::pactus::test_cases::TEST_CASES;
+use tw_any_coin::ffi::tw_transaction_compiler::{
+ tw_transaction_compiler_compile, tw_transaction_compiler_pre_image_hashes,
+};
+use tw_coin_entry::error::prelude::*;
+use tw_coin_registry::coin_type::CoinType;
+use tw_encoding::hex::ToHex;
+use tw_keypair::ed25519;
+use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait};
+use tw_memory::test_utils::tw_data_helper::TWDataHelper;
+use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper;
+use tw_misc::traits::ToBytesVec;
+use tw_proto::Pactus::Proto;
+use tw_proto::TxCompiler::Proto as CompilerProto;
+use tw_proto::{deserialize, serialize};
+
+#[test]
+fn test_pactus_transaction_compile() {
+ for case in TEST_CASES.iter() {
+ // Step 1: Create signing input.
+ let input = (case.sign_input_fn)();
+
+ // Step 2: Obtain preimage hash
+ let input_data = TWDataHelper::create(serialize(&input).unwrap());
+ let preimage_data = TWDataHelper::wrap(unsafe {
+ tw_transaction_compiler_pre_image_hashes(CoinType::Pactus as u32, input_data.ptr())
+ })
+ .to_vec()
+ .expect("!tw_transaction_compiler_pre_image_hashes returned nullptr");
+
+ let preimage: CompilerProto::PreSigningOutput =
+ deserialize(&preimage_data).expect("Coin entry returned an invalid output");
+
+ assert_eq!(preimage.error, SigningErrorType::OK);
+ assert!(preimage.error_message.is_empty());
+ assert_eq!(preimage.data.to_hex(), case.data_to_sign);
+
+ // Step 3: Sign the data "externally"
+ let private_key = ed25519::sha512::KeyPair::try_from(PRIVATE_KEY).unwrap();
+ let public_key = private_key.public().to_vec();
+
+ let signature = private_key
+ .sign(preimage.data.to_vec())
+ .expect("Error signing data")
+ .to_vec();
+ assert_eq!(signature.to_hex(), case.signature);
+
+ // Step 4: Compile transaction info
+ let signatures = TWDataVectorHelper::create([signature]);
+ let public_keys = TWDataVectorHelper::create([public_key]);
+
+ let input_data = TWDataHelper::create(serialize(&input).unwrap());
+ let output_data = TWDataHelper::wrap(unsafe {
+ tw_transaction_compiler_compile(
+ CoinType::Pactus as u32,
+ input_data.ptr(),
+ signatures.ptr(),
+ public_keys.ptr(),
+ )
+ })
+ .to_vec()
+ .expect("!tw_transaction_compiler_compile returned nullptr");
+
+ let output: Proto::SigningOutput =
+ deserialize(&output_data).expect("Coin entry returned an invalid output");
+
+ assert_eq!(output.error, SigningErrorType::OK);
+ assert!(output.error_message.is_empty());
+ assert_eq!(output.transaction_id.to_hex(), case.transaction_id);
+ assert_eq!(output.signature.to_hex(), case.signature);
+ assert_eq!(output.signed_transaction_data.to_hex(), case.signed_data);
+ }
+}
diff --git a/rust/tw_tests/tests/chains/pactus/pactus_sign.rs b/rust/tw_tests/tests/chains/pactus/pactus_sign.rs
new file mode 100644
index 00000000000..f6547b1918d
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/pactus_sign.rs
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use crate::chains::pactus::test_cases::PRIVATE_KEY;
+use crate::chains::pactus::test_cases::TEST_CASES;
+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_encoding::hex::{DecodeHex, ToHex};
+use tw_memory::test_utils::tw_data_helper::TWDataHelper;
+use tw_proto::Pactus::Proto;
+use tw_proto::{deserialize, serialize};
+
+#[test]
+fn test_pactus_sign_transactions() {
+ for case in TEST_CASES.iter() {
+ let input = Proto::SigningInput {
+ private_key: PRIVATE_KEY.decode_hex().unwrap().into(),
+ ..(case.sign_input_fn)()
+ };
+
+ let input_data = TWDataHelper::create(serialize(&input).unwrap());
+
+ let output = TWDataHelper::wrap(unsafe {
+ tw_any_signer_sign(input_data.ptr(), CoinType::Pactus 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.transaction_id.to_hex(), case.transaction_id);
+ assert_eq!(output.signature.to_hex(), case.signature);
+ assert_eq!(output.signed_transaction_data.to_hex(), case.signed_data);
+ }
+}
diff --git a/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs b/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs
new file mode 100644
index 00000000000..989448e3210
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/pactus_transaction_util.rs
@@ -0,0 +1,14 @@
+use super::test_cases::TEST_CASES;
+use tw_any_coin::test_utils::transaction_calc_tx_hash_utils::TransactionUtilHelper;
+use tw_coin_registry::coin_type::CoinType;
+
+#[test]
+fn test_pactus_transaction_util_calc_tx_hash() {
+ for case in TEST_CASES {
+ let tx_hash = TransactionUtilHelper::calc_tx_hash(CoinType::Pactus, case.signed_data);
+ assert_eq!(tx_hash, case.transaction_id);
+
+ let tx_hash = TransactionUtilHelper::calc_tx_hash(CoinType::Pactus, case.signed_data);
+ assert_eq!(tx_hash, case.transaction_id);
+ }
+}
diff --git a/rust/tw_tests/tests/chains/pactus/test_cases.rs b/rust/tw_tests/tests/chains/pactus/test_cases.rs
new file mode 100644
index 00000000000..0a79e33bb2f
--- /dev/null
+++ b/rust/tw_tests/tests/chains/pactus/test_cases.rs
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+use tw_proto::Pactus::Proto;
+
+/// A macro to define test data.
+/// Transaction format explained here: https://docs.pactus.org/protocol/transaction/format/
+macro_rules! define_test_data {
+ ( $tx_id:expr,
+ $signature:expr,
+ $public_key:expr
+ $(, $param:expr)*,
+ ) => {
+ pub const DATA_TO_SIGN: &str = concat!(
+ $( $param, )*
+ );
+
+ pub const SIGNED_DATA: &str = concat!(
+ "00", // Signed Flag
+ $($param, )*
+ $signature,
+ $public_key
+ );
+
+ pub const TX_ID: &str = $tx_id;
+ pub const SIGNATURE: &str = $signature;
+ };
+}
+
+// Private key for all the test cases
+pub const PRIVATE_KEY: &str = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6";
+
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f
+pub mod transfer_test_case {
+ use super::*;
+ use tw_encoding::hex::DecodeHex;
+
+ define_test_data!(
+ "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f", // transaction ID
+ "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc\
+ 8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506", // Signature
+ "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey
+ "01", // Version
+ "24a32300", // LockTime
+ "80ade204", // Fee
+ "0b77616c6c65742d636f7265", // Memo
+ "01", // PayloadType
+ "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender
+ "037a14ae24533816e7faaa6ed28fcdde8e55a7df21", // Receiver
+ "8084af5f", // Amount
+ );
+
+ pub fn sign_input() -> Proto::SigningInput<'static> {
+ let transfer_payload = Proto::TransferPayload {
+ sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(),
+ receiver: "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra".into(),
+ amount: 200000000,
+ };
+
+ let transaction = Proto::TransactionMessage {
+ lock_time: 2335524,
+ fee: 10000000,
+ memo: "wallet-core".into(),
+ payload: Proto::mod_TransactionMessage::OneOfpayload::transfer(transfer_payload),
+ };
+
+ let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap();
+
+ Proto::SigningInput {
+ transaction: Some(transaction),
+ private_key: private_key_bytes.into(),
+ }
+ }
+}
+
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f
+pub mod bond_with_public_key_test_case {
+ use super::*;
+ use tw_encoding::hex::DecodeHex;
+
+ define_test_data!(
+ "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f", // transaction ID
+ "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7\
+ 9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300", // Signature
+ "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey
+ "01", // Version
+ "c1b02300", // LockTime
+ "80ade204", // Fee
+ "0b77616c6c65742d636f7265", // Memo
+ "02", // PayloadType
+ "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender
+ "0129288df0bf7bd4b5e9eeed8b932d0c76f451823d", // Receiver
+ "60\
+ 98bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a3935\
+ 5cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56bbc\
+ dde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13833b", // Validator Public key
+ "8094ebdc03", // Stake
+ );
+
+ pub fn sign_input() -> Proto::SigningInput<'static> {
+ let bond_payload = Proto::BondPayload {
+ sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(),
+ receiver: "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl".into(),
+ stake: 1000000000,
+ public_key: "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn".into()
+ };
+
+ let transaction = Proto::TransactionMessage {
+ lock_time: 2339009,
+ fee: 10000000,
+ memo: "wallet-core".into(),
+ payload: Proto::mod_TransactionMessage::OneOfpayload::bond(bond_payload),
+ };
+
+ let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap();
+
+ Proto::SigningInput {
+ transaction: Some(transaction),
+ private_key: private_key_bytes.into(),
+ }
+ }
+}
+
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80
+//
+// If the validator exists and has already been staked, there’s no need to send the public key.
+// If the validator does not exist, the public key is required, as it is not indexed on the chain.
+pub mod bond_without_public_key_test_case {
+ use super::*;
+ use tw_encoding::hex::DecodeHex;
+
+ define_test_data!(
+ "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80", // transaction ID
+ "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0\
+ 715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d", // Signature
+ "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa", // PublicKey
+ "01", // Version
+ "5ca32300", // LockTime
+ "80ade204", // Fee
+ "0b77616c6c65742d636f7265", // Memo
+ "02", // PayloadType
+ "037098338e0b6808119dfd4457ab806b9c2059b89b", // Sender
+ "01d2fa2a7d560502199995ea260954f064d90278be", // Receiver
+ "00", // Public key zero
+ "8094ebdc03", // Stake
+ );
+
+ pub fn sign_input() -> Proto::SigningInput<'static> {
+ let bond_payload = Proto::BondPayload {
+ sender: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr".into(),
+ receiver: "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd".into(),
+ stake: 1000000000,
+ public_key: Default::default(),
+ };
+
+ let transaction = Proto::TransactionMessage {
+ lock_time: 2335580,
+ fee: 10000000,
+ memo: "wallet-core".into(),
+ payload: Proto::mod_TransactionMessage::OneOfpayload::bond(bond_payload),
+ };
+
+ let private_key_bytes = PRIVATE_KEY.decode_hex().unwrap();
+
+ Proto::SigningInput {
+ transaction: Some(transaction),
+ private_key: private_key_bytes.into(),
+ }
+ }
+}
+
+pub(crate) struct TestCase {
+ pub sign_input_fn: fn() -> Proto::SigningInput<'static>,
+ pub transaction_id: &'static str,
+ pub signature: &'static str,
+ pub data_to_sign: &'static str,
+ pub signed_data: &'static str,
+}
+
+pub(crate) const TEST_CASES: &[TestCase; 3] = &[
+ TestCase {
+ sign_input_fn: transfer_test_case::sign_input,
+ transaction_id: transfer_test_case::TX_ID,
+ signature: transfer_test_case::SIGNATURE,
+ data_to_sign: transfer_test_case::DATA_TO_SIGN,
+ signed_data: transfer_test_case::SIGNED_DATA,
+ },
+ TestCase {
+ sign_input_fn: bond_with_public_key_test_case::sign_input,
+ transaction_id: bond_with_public_key_test_case::TX_ID,
+ signature: bond_with_public_key_test_case::SIGNATURE,
+ data_to_sign: bond_with_public_key_test_case::DATA_TO_SIGN,
+ signed_data: bond_with_public_key_test_case::SIGNED_DATA,
+ },
+ TestCase {
+ sign_input_fn: bond_without_public_key_test_case::sign_input,
+ transaction_id: bond_without_public_key_test_case::TX_ID,
+ signature: bond_without_public_key_test_case::SIGNATURE,
+ data_to_sign: bond_without_public_key_test_case::DATA_TO_SIGN,
+ signed_data: bond_without_public_key_test_case::SIGNED_DATA,
+ },
+];
diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs
index bb6c14842e3..4f7e27b9375 100644
--- a/rust/tw_tests/tests/coin_address_derivation_test.rs
+++ b/rust/tw_tests/tests/coin_address_derivation_test.rs
@@ -153,6 +153,7 @@ fn test_coin_address_derivation() {
CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC",
CoinType::Sui => "0x01a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905",
CoinType::TON => "UQCj3jAU_Ec2kXdAqweKt4rYjiwTNwiCfaUnIDHGh7wTwx_G",
+ CoinType::Pactus => "pc1rk2qaaeu9pj3zwtvm49d3d4yqxzpp4te87cx0am",
// 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 bb3d3717d54..447c5c2f5a0 100644
--- a/src/Coin.cpp
+++ b/src/Coin.cpp
@@ -67,6 +67,7 @@
#include "NativeEvmos/Entry.h"
#include "NativeInjective/Entry.h"
#include "BitcoinCash/Entry.h"
+#include "Pactus/Entry.h"
// end_of_coin_includes_marker_do_not_modify
using namespace TW;
@@ -127,6 +128,7 @@ InternetComputer::Entry InternetComputerDP;
NativeEvmos::Entry NativeEvmosDP;
NativeInjective::Entry NativeInjectiveDP;
BitcoinCash::Entry BitcoinCashDP;
+Pactus::Entry PactusDP;
// end_of_coin_dipatcher_declarations_marker_do_not_modify
CoinEntry* coinDispatcher(TWCoinType coinType) {
@@ -189,6 +191,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) {
case TWBlockchainNativeEvmos: entry = &NativeEvmosDP; break;
case TWBlockchainNativeInjective: entry = &NativeInjectiveDP; break;
case TWBlockchainBitcoinCash: entry = &BitcoinCashDP; break;
+ case TWBlockchainPactus: entry = &PactusDP; break;
// end_of_coin_dipatcher_switch_marker_do_not_modify
default: entry = nullptr; break;
diff --git a/src/Pactus/Entry.h b/src/Pactus/Entry.h
new file mode 100644
index 00000000000..2f41bcde419
--- /dev/null
+++ b/src/Pactus/Entry.h
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#pragma once
+
+#include "rust/RustCoinEntry.h"
+
+namespace TW::Pactus {
+
+/// Entry point for Pactus 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::Pactus
+
diff --git a/src/proto/Pactus.proto b/src/proto/Pactus.proto
new file mode 100644
index 00000000000..ce3f9d3a02e
--- /dev/null
+++ b/src/proto/Pactus.proto
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+syntax = "proto3";
+
+package TW.Pactus.Proto;
+option java_package = "wallet.core.jni.proto";
+
+import "Common.proto";
+
+message TransactionMessage {
+ // The lock time for the transaction.
+ uint32 lock_time = 1;
+ // The transaction fee in NanoPAC.
+ int64 fee = 2;
+ // A memo string for the transaction (optional).
+ string memo = 3;
+
+ oneof payload {
+ TransferPayload transfer = 10;
+ BondPayload bond = 11;
+ }
+}
+
+// Transfer payload for creating a Transfer transaction between two accounts.
+message TransferPayload {
+ // The sender's account address.
+ string sender = 1;
+ // The receiver's account address.
+ string receiver = 2;
+ // The amount to be transferred, specified in NanoPAC.
+ int64 amount = 3;
+}
+
+// Bond payload for creating a Bond transaction from an account to a validator.
+message BondPayload {
+ // The sender's account address.
+ string sender = 1;
+ // The receiver's validator address.
+ string receiver = 2;
+ // The stake amount in NanoPAC.
+ int64 stake = 3;
+ // The public key of the validator (only set when creating a new validator).
+ string public_key = 4;
+}
+
+// Input data necessary to create a signed transaction.
+message SigningInput {
+ bytes private_key = 1;
+ TransactionMessage transaction = 2;
+}
+
+// Transaction signing output.
+message SigningOutput {
+ // Transaction ID (Hash).
+ bytes transaction_id = 1;
+ // Signed and encoded transaction bytes.
+ bytes signed_transaction_data = 2;
+ // Signature the signed transaction.
+ bytes signature = 3;
+ // A possible error, `OK` if none.
+ Common.Proto.SigningError error = 4;
+ // Detailed error message, if any.
+ string error_message = 5;
+}
diff --git a/swift/Tests/Blockchains/PactusTests.swift b/swift/Tests/Blockchains/PactusTests.swift
new file mode 100644
index 00000000000..933bdeaebea
--- /dev/null
+++ b/swift/Tests/Blockchains/PactusTests.swift
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+import XCTest
+import WalletCore
+
+class PactusTests: XCTestCase {
+ var privateKey: PrivateKey!
+
+ override func setUp() {
+ super.setUp()
+ privateKey = PrivateKey(data: Data(hexString: "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6")!)!
+ }
+
+ func testAddress() {
+ let pubkey = privateKey.getPublicKeyEd25519()
+ let address = AnyAddress(publicKey: pubkey, coin: .pactus)
+ let addressFromString = AnyAddress(string: "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr", coin: .pactus)!
+
+ XCTAssertEqual(pubkey.data.hexString, "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa")
+ XCTAssertEqual(address.description, addressFromString.description)
+ }
+
+ func testTransferSign() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f
+ let input = PactusSigningInput.with {
+ $0.privateKey = privateKey.data
+ $0.transaction = PactusTransactionMessage.with {
+ $0.lockTime = 2335524
+ $0.fee = 10000000
+ $0.memo = "wallet-core"
+ $0.transfer = PactusTransferPayload.with {
+ $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ $0.receiver = "pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra"
+ $0.amount = 200000000
+ }
+ }
+ }
+
+ let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus)
+
+ let expectedTransactionID = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f"
+ let expectedSignature = "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc" +
+ "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506"
+ let expectedSignedData = "000124a3230080ade2040b77616c6c65742d636f726501037098338e0b680811" +
+ "9dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df" +
+ "218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9" +
+ "b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736" +
+ "693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560b" +
+ "b72145f4fa";
+ XCTAssertEqual(output.transactionID.hexString, expectedTransactionID)
+ XCTAssertEqual(output.signature.hexString, expectedSignature)
+ XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData)
+ }
+
+ func testBondWithPublicKeySign() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f
+ let input = PactusSigningInput.with {
+ $0.privateKey = privateKey.data
+ $0.transaction = PactusTransactionMessage.with {
+ $0.lockTime = 2339009
+ $0.fee = 10000000
+ $0.memo = "wallet-core"
+ $0.bond = PactusBondPayload.with {
+ $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ $0.receiver = "pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl"
+ $0.stake = 1000000000
+ $0.publicKey = "public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn"
+ }
+ }
+ }
+
+ let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus)
+
+ let expectedTransactionID = "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f"
+ let expectedSignature = "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7" +
+ "9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300"
+ let expectedSignedData = "0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b680811" +
+ "9dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f45182" +
+ "3d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a" +
+ "39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a5" +
+ "6bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13" +
+ "833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda5" +
+ "5b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff" +
+ "65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" +
+ "560bb72145f4fa"
+
+ XCTAssertEqual(output.transactionID.hexString, expectedTransactionID)
+ XCTAssertEqual(output.signature.hexString, expectedSignature)
+ XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData)
+ }
+
+ func testBondWithoutPublicKeySign() {
+ // Successfully broadcasted transaction:
+ // https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80
+ let input = PactusSigningInput.with {
+ $0.privateKey = privateKey.data
+ $0.transaction = PactusTransactionMessage.with {
+ $0.lockTime = 2335580
+ $0.fee = 10000000
+ $0.memo = "wallet-core"
+ $0.bond = PactusBondPayload.with {
+ $0.sender = "pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr"
+ $0.receiver = "pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd"
+ $0.stake = 1000000000
+ }
+ }
+ }
+
+ let output: PactusSigningOutput = AnySigner.sign(input: input, coin: .pactus)
+
+ let expectedTransactionID = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80"
+ let expectedSignature = "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0" +
+ "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d"
+ let expectedSignedData = "00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b680811" +
+ "9dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278" +
+ "be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d" +
+ "85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc4" +
+ "36aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf" +
+ "560bb72145f4fa";
+
+ XCTAssertEqual(output.transactionID.hexString, expectedTransactionID)
+ XCTAssertEqual(output.signature.hexString, expectedSignature)
+ XCTAssertEqual(output.signedTransactionData.hexString, expectedSignedData)
+ }
+}
diff --git a/swift/Tests/CoinAddressDerivationTests.swift b/swift/Tests/CoinAddressDerivationTests.swift
index 6a572ab6048..72c611f4083 100644
--- a/swift/Tests/CoinAddressDerivationTests.swift
+++ b/swift/Tests/CoinAddressDerivationTests.swift
@@ -130,7 +130,7 @@ class CoinAddressDerivationTests: XCTestCase {
assertCoinDerivation(coin, expectedResult, derivedAddress, address)
case .rootstock:
let expectedResult = "0xA2D7065F94F838a3aB9C04D67B312056846424Df"
- assertCoinDerivation(coin, expectedResult, derivedAddress, address)
+ assertCoinDerivation(coin, expectedResult, derivedAddress, address)
case .filecoin:
let expectedResult = "f1zzykebxldfcakj5wdb5n3n7priul522fnmjzori"
assertCoinDerivation(coin, expectedResult, derivedAddress, address)
@@ -400,6 +400,9 @@ class CoinAddressDerivationTests: XCTestCase {
case .dydx:
let expectedResult = "dydx142j9u5eaduzd7faumygud6ruhdwme98qeayaky"
assertCoinDerivation(coin, expectedResult, derivedAddress, address)
+ case .pactus:
+ let expectedResult = "pc1r7ys2g5a4xc2qtm0t4q987m4mvs57w5g0v4pvzg"
+ assertCoinDerivation(coin, expectedResult, derivedAddress, address)
@unknown default:
fatalError()
}
diff --git a/tests/chains/Pactus/AddressTests.cpp b/tests/chains/Pactus/AddressTests.cpp
new file mode 100644
index 00000000000..c3bcb0a260d
--- /dev/null
+++ b/tests/chains/Pactus/AddressTests.cpp
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#include "HexCoding.h"
+#include "Pactus/Entry.h"
+#include "PrivateKey.h"
+#include "PublicKey.h"
+#include "TestUtilities.h"
+#include
+#include
+
+namespace TW::Pactus::tests {
+
+TEST(PactusAddress, AddressData) {
+ auto string = STRING("pc1rspm7ps49gar9ft5g0tkl6lhxs8ygeakq87quh3");
+ auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypePactus));
+ auto string2 = WRAPS(TWAnyAddressDescription(addr.get()));
+ EXPECT_TRUE(TWStringEqual(string.get(), string2.get()));
+ auto keyHash = WRAPD(TWAnyAddressData(addr.get()));
+ assertHexEqual(keyHash, "038077e0c2a5474654ae887aedfd7ee681c88cf6c0");
+}
+
+TEST(PactusAddress, FromPrivateKey) {
+ auto privateKey = PrivateKey(parse_hex("2134ae97465505dfd5a1fd05a8a0f146209c601eb3f1b0363b4cfe4b47ba1ab4"));
+ auto pubkey = privateKey.getPublicKey(TWPublicKeyTypeED25519);
+ Entry entry;
+ auto address = entry.deriveAddress(TWCoinTypePactus, pubkey, TWDerivationDefault, std::monostate{});
+ ASSERT_EQ(address, "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl");
+}
+
+TEST(PactusAddress, FromPublicKey) {
+ auto publicKey = PublicKey(parse_hex("600d30a4373ae788e2d4a08f4728f45d259593fbdd9632bbe283c4c37ac6a3df"), TWPublicKeyTypeED25519);
+ Entry entry;
+ auto address = entry.deriveAddress(TWCoinTypePactus, publicKey, TWDerivationDefault, std::monostate{});
+ ASSERT_EQ(address, "pc1r7jkvfnegf0rf5ua05fzu9krjhjxcrrygl3v4nl");
+}
+
+} // namespace TW::Pactus::tests
\ No newline at end of file
diff --git a/tests/chains/Pactus/CoinTypeTests.cpp b/tests/chains/Pactus/CoinTypeTests.cpp
new file mode 100644
index 00000000000..cd28ee9de14
--- /dev/null
+++ b/tests/chains/Pactus/CoinTypeTests.cpp
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#include "TestUtilities.h"
+#include "Pactus/Entry.h"
+#include "PrivateKey.h"
+#include "PublicKey.h"
+#include
+#include
+#include
+#include
+
+namespace TW::Pactus::tests {
+
+TEST(PactusCoinType, TWCoinType) {
+ const auto coin = TWCoinTypePactus;
+ const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin));
+ const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin));
+ const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin));
+ const auto txId = WRAPS(TWStringCreateWithUTF8Bytes(""));
+ const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get()));
+ const auto accId = WRAPS(TWStringCreateWithUTF8Bytes(""));
+ const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get()));
+
+ assertStringsEqual(id, "pactus");
+ assertStringsEqual(name, "Pactus");
+ assertStringsEqual(symbol, "PAC");
+ ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), 9);
+ ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchainPactus);
+ ASSERT_EQ(TWCoinTypeP2pkhPrefix(coin), 0);
+ ASSERT_EQ(TWCoinTypeP2shPrefix(coin), 0);
+ ASSERT_EQ(TWCoinTypeStaticPrefix(coin), 0);
+ assertStringsEqual(txUrl, "https://pacviewer.com/transaction/");
+ assertStringsEqual(accUrl, "https://pacviewer.com/address/");
+}
+
+}
\ No newline at end of file
diff --git a/tests/chains/Pactus/CompilerTests.cpp b/tests/chains/Pactus/CompilerTests.cpp
new file mode 100644
index 00000000000..0d4a902e2d9
--- /dev/null
+++ b/tests/chains/Pactus/CompilerTests.cpp
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#include "HexCoding.h"
+#include "PrivateKey.h"
+#include "PublicKey.h"
+#include "TestCases.h"
+#include "TestUtilities.h"
+#include "TransactionCompiler.h"
+#include "proto/Pactus.pb.h"
+#include "proto/TransactionCompiler.pb.h"
+
+#include
+
+using namespace TW;
+
+TEST(PactusCompiler, CompileAndSign) {
+ for (const auto& testCase : TEST_CASES) {
+ auto input = testCase.createSigningInput();
+ auto inputString = input.SerializeAsString();
+ auto inputStrData = TW::Data(inputString.begin(), inputString.end());
+
+ // Pre-hash the transaction.
+ auto preImageHashesData = TransactionCompiler::preImageHashes(TWCoinTypePactus, inputStrData);
+ TxCompiler::Proto::PreSigningOutput preSigningOutput;
+ preSigningOutput.ParseFromArray(preImageHashesData.data(), static_cast(preImageHashesData.size()));
+ auto actualDataToSign = data(preSigningOutput.data());
+
+ EXPECT_EQ(preSigningOutput.error(), Common::Proto::OK);
+ EXPECT_EQ(hex(actualDataToSign), testCase.dataToSign);
+
+ // Sign the pre-hash data.
+ auto privateKey = PrivateKey(parse_hex(PRIVATE_KEY_HEX));
+ auto publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519).bytes;
+ auto signature = privateKey.sign(actualDataToSign, TWCurveED25519);
+ EXPECT_EQ(hex(signature), testCase.signature);
+
+ // Compile the transaction.
+ auto outputData = TransactionCompiler::compileWithSignatures(TWCoinTypePactus, inputStrData, {signature}, {publicKey});
+ TW::Pactus::Proto::SigningOutput output;
+ output.ParseFromArray(outputData.data(), static_cast(outputData.size()));
+
+ EXPECT_EQ(output.error(), Common::Proto::OK);
+ ASSERT_EQ(hex(output.signed_transaction_data()), testCase.signedData);
+ ASSERT_EQ(hex(output.signature()), testCase.signature);
+ ASSERT_EQ(hex(output.transaction_id()), testCase.transactionID);
+ }
+}
diff --git a/tests/chains/Pactus/SignerTests.cpp b/tests/chains/Pactus/SignerTests.cpp
new file mode 100644
index 00000000000..cbb469310c0
--- /dev/null
+++ b/tests/chains/Pactus/SignerTests.cpp
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#include "HexCoding.h"
+#include "PrivateKey.h"
+#include "PublicKey.h"
+#include "TestCases.h"
+#include "TestUtilities.h"
+#include "TransactionCompiler.h"
+#include "proto/Pactus.pb.h"
+#include "proto/TransactionCompiler.pb.h"
+
+#include
+
+using namespace TW;
+
+TEST(PactusSigner, Sign) {
+ for (const auto& testCase : TEST_CASES) {
+ auto input = testCase.createSigningInput();
+
+ auto privateKey = PrivateKey(parse_hex(PRIVATE_KEY_HEX));
+ input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size());
+
+ TW::Pactus::Proto::SigningOutput output;
+ ANY_SIGN(input, TWCoinTypePactus);
+
+ EXPECT_EQ(output.error(), Common::Proto::OK);
+ ASSERT_EQ(hex(output.signed_transaction_data()), testCase.signedData);
+ ASSERT_EQ(hex(output.signature()), testCase.signature);
+ ASSERT_EQ(hex(output.transaction_id()), testCase.transactionID);
+ }
+}
diff --git a/tests/chains/Pactus/TestCases.h b/tests/chains/Pactus/TestCases.h
new file mode 100644
index 00000000000..1fc953b54e4
--- /dev/null
+++ b/tests/chains/Pactus/TestCases.h
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#pragma once
+
+#include "proto/Pactus.pb.h"
+#include "proto/TransactionCompiler.pb.h"
+
+#include
+
+const std::string PRIVATE_KEY_HEX = "4e51f1f3721f644ac7a193be7f5e7b8c2abaa3467871daf4eacb5d3af080e5d6";
+
+namespace TransferTransaction {
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f
+static TW::Pactus::Proto::SigningInput createSigningInput() {
+ TW::Pactus::Proto::SigningInput input;
+ TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction();
+ trx->set_lock_time(2335524);
+ trx->set_fee(10000000);
+ trx->set_memo("wallet-core");
+
+ TW::Pactus::Proto::TransferPayload* pld = trx->mutable_transfer();
+ pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr");
+ pld->set_receiver("pc1r0g22ufzn8qtw0742dmfglnw73e260hep0k3yra");
+ pld->set_amount(200000000);
+
+ return input;
+}
+
+const std::string transactionID = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f";
+const std::string signature = "4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9b49d33a0fc"
+ "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506";
+const std::string dataToSign = "0124a3230080ade2040b77616c6c65742d636f726501037098338e0b6808119d"
+ "fd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df21"
+ "8084af5f";
+const std::string signedData = "000124a3230080ade2040b77616c6c65742d636f726501037098338e0b680811"
+ "9dfd4457ab806b9c2059b89b037a14ae24533816e7faaa6ed28fcdde8e55a7df"
+ "218084af5f4ed8fee3d8992e82660dd05bbe8608fc56ceabffdeeee61e3213b9"
+ "b49d33a0fc8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736"
+ "693eda850695794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560b"
+ "b72145f4fa";
+} // namespace TransferTransaction
+
+namespace BondWithPublicKeyTransaction {
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f
+
+static TW::Pactus::Proto::SigningInput createSigningInput() {
+ TW::Pactus::Proto::SigningInput input;
+ TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction();
+ trx->set_lock_time(2339009);
+ trx->set_fee(10000000);
+ trx->set_memo("wallet-core");
+
+ TW::Pactus::Proto::BondPayload* pld = trx->mutable_bond();
+ pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr");
+ pld->set_receiver("pc1p9y5gmu9l002tt60wak9extgvwm69rq3a9ackrl");
+ pld->set_stake(1000000000);
+ pld->set_public_key("public1pnz75msstqdrq5eguvcwanug0zauhqjw2cc4flmez3qethnp68y64ehc4k69amapj7x4na2uda0snqz4yxujgx3jsse4f64fgy7jkh0xauvhrc5ts09vfk48g85t0js66hvajm6xruemsvlxqv3xvkyur8v9v0mtn");
+
+ return input;
+}
+
+const std::string transactionID = "d194b445642a04ec78ced4448696e50b733f2f0b517a23871882c0eefaf1c28f";
+const std::string signature = "0d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda55b4338328adac7"
+ "9d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff65e39ce7006300";
+const std::string dataToSign = "01c1b0230080ade2040b77616c6c65742d636f726502037098338e0b6808119d"
+ "fd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f451823d"
+ "6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a39"
+ "355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a56b"
+ "bcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb1383"
+ "3b8094ebdc03";
+const std::string signedData = "0001c1b0230080ade2040b77616c6c65742d636f726502037098338e0b680811"
+ "9dfd4457ab806b9c2059b89b0129288df0bf7bd4b5e9eeed8b932d0c76f45182"
+ "3d6098bd4dc20b03460a651c661dd9f10f17797049cac62a9fef228832bbcc3a"
+ "39355cdf15b68bddf432f1ab3eab8debe1300aa43724834650866a9d552827a5"
+ "6bbcdde32e3c517079589b54e83d16f9435abb3b2de8c3e677067cc0644ccb13"
+ "833b8094ebdc030d7bc6d94927534b89e2f53bcfc9fc849e0e2982438955eda5"
+ "5b4338328adac79d4ee3216d143f0e1629764ab650734f8ba188e716d71f9eff"
+ "65e39ce700630095794161374b22c696dabb98e93f6ca9300b22f3b904921fbf"
+ "560bb72145f4fa";
+} // namespace BondWithPublicKeyTransaction
+
+namespace BondWithoutPublicKeyTransaction {
+// Successfully broadcasted transaction:
+// https://pacviewer.com/transaction/f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80
+
+static TW::Pactus::Proto::SigningInput createSigningInput() {
+ TW::Pactus::Proto::SigningInput input;
+ TW::Pactus::Proto::TransactionMessage* trx = input.mutable_transaction();
+ trx->set_lock_time(2335580);
+ trx->set_fee(10000000);
+ trx->set_memo("wallet-core");
+
+ TW::Pactus::Proto::BondPayload* pld = trx->mutable_bond();
+ pld->set_sender("pc1rwzvr8rstdqypr80ag3t6hqrtnss9nwymcxy3lr");
+ pld->set_receiver("pc1p6taz5l2kq5ppnxv4agnqj48svnvsy797xpe6wd");
+ pld->set_stake(1000000000);
+
+ return input;
+}
+
+const std::string transactionID = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80";
+const std::string signature = "9e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d85c087a8748ff0"
+ "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d";
+const std::string dataToSign = "015ca3230080ade2040b77616c6c65742d636f726502037098338e0b6808119d"
+ "fd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278be"
+ "008094ebdc03";
+const std::string signedData = "00015ca3230080ade2040b77616c6c65742d636f726502037098338e0b680811"
+ "9dfd4457ab806b9c2059b89b01d2fa2a7d560502199995ea260954f064d90278"
+ "be008094ebdc039e6279fb64067c7d7316ac74630bbb8589df268aa4548f1c7d"
+ "85c087a8748ff0715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc4"
+ "36aa58f9a8f00d95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf"
+ "560bb72145f4fa";
+
+} // namespace BondWithoutPublicKeyTransaction
+
+struct TestCase {
+ std::function createSigningInput;
+ std::string transactionID;
+ std::string signature;
+ std::string dataToSign;
+ std::string signedData;
+};
+
+const TestCase TEST_CASES[] = {
+ {
+ TransferTransaction::createSigningInput,
+ TransferTransaction::transactionID,
+ TransferTransaction::signature,
+ TransferTransaction::dataToSign,
+ TransferTransaction::signedData,
+ },
+ {
+ BondWithPublicKeyTransaction::createSigningInput,
+ BondWithPublicKeyTransaction::transactionID,
+ BondWithPublicKeyTransaction::signature,
+ BondWithPublicKeyTransaction::dataToSign,
+ BondWithPublicKeyTransaction::signedData,
+ },
+ {
+ BondWithoutPublicKeyTransaction::createSigningInput,
+ BondWithoutPublicKeyTransaction::transactionID,
+ BondWithoutPublicKeyTransaction::signature,
+ BondWithoutPublicKeyTransaction::dataToSign,
+ BondWithoutPublicKeyTransaction::signedData,
+ },
+};
\ No newline at end of file
diff --git a/tests/chains/Pactus/WalletTests.cpp b/tests/chains/Pactus/WalletTests.cpp
new file mode 100644
index 00000000000..bcd3076f398
--- /dev/null
+++ b/tests/chains/Pactus/WalletTests.cpp
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// Copyright © 2017 Trust Wallet.
+
+#include "TestUtilities.h"
+#include "Pactus/Entry.h"
+#include "PrivateKey.h"
+#include "PublicKey.h"
+#include
+#include
+#include
+#include
+
+namespace TW::Pactus::tests {
+
+TEST(PactusWallet, DerivationPath) {
+ auto derivationPath = TWCoinTypeDerivationPath(TWCoinTypePactus);
+ assertStringsEqual(WRAPS(derivationPath), "m/44'/21888'/3'/0'");
+}
+
+TEST(PactusWallet, HDWallet) {
+ auto mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon cactus";
+ auto passphrase = "";
+ auto wallet = WRAP(TWHDWallet, TWHDWalletCreateWithMnemonic(STRING(mnemonic).get(), STRING(passphrase).get()));
+
+ auto derivationPath1 = TWStringCreateWithUTF8Bytes("m/44'/21888'/3'/0'");
+ auto privateKey1 = WRAP(TWPrivateKey, TWHDWalletGetKey(wallet.get(), TWCoinTypePactus, derivationPath1));
+ auto publicKey1 = WRAP(TWPublicKey, TWPrivateKeyGetPublicKeyEd25519(privateKey1.get()));
+ auto address1 = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey1.get(), TWCoinTypePactus));
+ auto addressStr1 = WRAPS(TWAnyAddressDescription(address1.get()));
+
+ auto derivationPath2 = TWStringCreateWithUTF8Bytes("m/44'/21888'/3'/1'");
+ auto privateKey2 = WRAP(TWPrivateKey, TWHDWalletGetKey(wallet.get(), TWCoinTypePactus, derivationPath2));
+ auto publicKey2 = WRAP(TWPublicKey, TWPrivateKeyGetPublicKeyEd25519(privateKey2.get()));
+ auto address2 = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey2.get(), TWCoinTypePactus));
+ auto addressStr2 = WRAPS(TWAnyAddressDescription(address2.get()));
+
+ assertStringsEqual(addressStr1, "pc1rcx9x55nfme5juwdgxd2ksjdcmhvmvkrygmxpa3");
+ assertStringsEqual(addressStr2, "pc1r7aynw9urvh66ktr3fte2gskjjnxzruflkgde94");
+ TWStringDelete(derivationPath1);
+ TWStringDelete(derivationPath2);
+}
+
+}
\ No newline at end of file
diff --git a/tests/common/CoinAddressDerivationTests.cpp b/tests/common/CoinAddressDerivationTests.cpp
index 1273ea427eb..6df5336c5c0 100644
--- a/tests/common/CoinAddressDerivationTests.cpp
+++ b/tests/common/CoinAddressDerivationTests.cpp
@@ -396,6 +396,9 @@ TEST(Coin, DeriveAddress) {
case TWCoinTypeDydx:
EXPECT_EQ(address, "dydx1hkfq3zahaqkkzx5mjnamwjsfpq2jk7z0sz38vk");
break;
+ case TWCoinTypePactus:
+ EXPECT_EQ(address, "pc1rehvlc6tfn79z0zjqqaj8zas5j5h9c2fe59a4ff");
+ break;
// end_of_coin_address_derivation_tests_marker_do_not_modify
// no default branch here, intentionally, to better notice any missing coins
}
diff --git a/tests/interface/TWHRPTests.cpp b/tests/interface/TWHRPTests.cpp
index 14ddb39b657..d99e81a6bb8 100644
--- a/tests/interface/TWHRPTests.cpp
+++ b/tests/interface/TWHRPTests.cpp
@@ -34,6 +34,7 @@ TEST(TWHRP, StringForHRP) {
ASSERT_STREQ(stringForHRP(TWHRPCryptoOrg), "cro");
ASSERT_STREQ(stringForHRP(TWHRPOsmosis), "osmo");
ASSERT_STREQ(stringForHRP(TWHRPSecret), "secret");
+ ASSERT_STREQ(stringForHRP(TWHRPPactus), "pc");
}
TEST(TWHRP, HRPForString) {
@@ -62,6 +63,7 @@ TEST(TWHRP, HRPForString) {
ASSERT_EQ(hrpForString("osmo"), TWHRPOsmosis);
ASSERT_EQ(hrpForString("ecash"), TWHRPECash);
ASSERT_EQ(hrpForString("secret"), TWHRPSecret);
+ ASSERT_EQ(hrpForString("pc"), TWHRPPactus);
}
TEST(TWHPR, HPRByCoinType) {
@@ -89,6 +91,7 @@ TEST(TWHPR, HPRByCoinType) {
ASSERT_EQ(TWHRPOsmosis, TWCoinTypeHRP(TWCoinTypeOsmosis));
ASSERT_EQ(TWHRPECash, TWCoinTypeHRP(TWCoinTypeECash));
ASSERT_EQ(TWHRPSecret, TWCoinTypeHRP(TWCoinTypeSecret));
+ ASSERT_EQ(TWHRPPactus, TWCoinTypeHRP(TWCoinTypePactus));
ASSERT_EQ(TWHRPUnknown, TWCoinTypeHRP(TWCoinTypeAion));
ASSERT_EQ(TWHRPUnknown, TWCoinTypeHRP(TWCoinTypeCallisto));