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));