From 9a4556650bef7382b74154d4fd5e070263562491 Mon Sep 17 00:00:00 2001 From: doom Date: Mon, 27 May 2024 13:22:40 +0200 Subject: [PATCH 01/18] Add the tw_ss58_address crate --- rust/Cargo.lock | 25 ++ rust/Cargo.toml | 4 +- rust/chains/tw_polkadot/Cargo.toml | 11 + rust/chains/tw_polkadot/src/address.rs | 35 ++ rust/chains/tw_polkadot/src/compiler.rs | 50 +++ rust/chains/tw_polkadot/src/entry.rs | 93 +++++ rust/chains/tw_polkadot/src/extrinsic.rs | 13 + rust/chains/tw_polkadot/src/lib.rs | 9 + rust/chains/tw_polkadot/src/signer.rs | 27 ++ rust/tw_any_coin/tests/chains/mod.rs | 1 + rust/tw_any_coin/tests/chains/polkadot/mod.rs | 7 + .../tests/chains/polkadot/polkadot_address.rs | 28 ++ .../tests/chains/polkadot/polkadot_compile.rs | 8 + .../tests/chains/polkadot/polkadot_sign.rs | 8 + .../tests/coin_address_derivation_test.rs | 1 + rust/tw_coin_registry/Cargo.toml | 1 + rust/tw_coin_registry/src/blockchain_type.rs | 1 + rust/tw_coin_registry/src/dispatcher.rs | 3 + rust/tw_ss58_address/Cargo.toml | 13 + rust/tw_ss58_address/src/lib.rs | 352 ++++++++++++++++++ 20 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 rust/chains/tw_polkadot/Cargo.toml create mode 100644 rust/chains/tw_polkadot/src/address.rs create mode 100644 rust/chains/tw_polkadot/src/compiler.rs create mode 100644 rust/chains/tw_polkadot/src/entry.rs create mode 100644 rust/chains/tw_polkadot/src/extrinsic.rs create mode 100644 rust/chains/tw_polkadot/src/lib.rs create mode 100644 rust/chains/tw_polkadot/src/signer.rs create mode 100644 rust/tw_any_coin/tests/chains/polkadot/mod.rs create mode 100644 rust/tw_any_coin/tests/chains/polkadot/polkadot_address.rs create mode 100644 rust/tw_any_coin/tests/chains/polkadot/polkadot_compile.rs create mode 100644 rust/tw_any_coin/tests/chains/polkadot/polkadot_sign.rs create mode 100644 rust/tw_ss58_address/Cargo.toml create mode 100644 rust/tw_ss58_address/src/lib.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3d7c0c29603..cf5e0854fd7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1752,6 +1752,7 @@ dependencies = [ "tw_misc", "tw_native_evmos", "tw_native_injective", + "tw_polkadot", "tw_ronin", "tw_solana", "tw_sui", @@ -1967,6 +1968,17 @@ dependencies = [ "tw_memory", ] +[[package]] +name = "tw_polkadot" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", +] + [[package]] name = "tw_proto" version = "0.1.0" @@ -2006,6 +2018,19 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_ss58_address" +version = "0.1.0" +dependencies = [ + "blake2", + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", +] + [[package]] name = "tw_sui" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 183f29e20a6..c83cd2c7f99 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,10 +4,11 @@ members = [ "chains/tw_binance", "chains/tw_cosmos", "chains/tw_ethereum", - "chains/tw_internet_computer", "chains/tw_greenfield", + "chains/tw_internet_computer", "chains/tw_native_evmos", "chains/tw_native_injective", + "chains/tw_polkadot", "chains/tw_ronin", "chains/tw_solana", "chains/tw_sui", @@ -26,6 +27,7 @@ members = [ "tw_misc", "tw_number", "tw_proto", + "tw_ss58_address", "tw_utxo", "wallet_core_rs", ] diff --git a/rust/chains/tw_polkadot/Cargo.toml b/rust/chains/tw_polkadot/Cargo.toml new file mode 100644 index 00000000000..64fd7c1b772 --- /dev/null +++ b/rust/chains/tw_polkadot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tw_polkadot" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_polkadot/src/address.rs b/rust/chains/tw_polkadot/src/address.rs new file mode 100644 index 00000000000..847ec855f26 --- /dev/null +++ b/rust/chains/tw_polkadot/src/address.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +pub struct PolkadotAddress { + // bytes: + // TODO add necessary fields. +} + +impl CoinAddress for PolkadotAddress { + #[inline] + fn data(&self) -> Data { + todo!() + } +} + +impl FromStr for PolkadotAddress { + type Err = AddressError; + + fn from_str(_s: &str) -> Result { + todo!() + } +} + +impl fmt::Display for PolkadotAddress { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } +} diff --git a/rust/chains/tw_polkadot/src/compiler.rs b/rust/chains/tw_polkadot/src/compiler.rs new file mode 100644 index 00000000000..68d6a889ad6 --- /dev/null +++ b/rust/chains/tw_polkadot/src/compiler.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_proto::Polkadot::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct PolkadotCompiler; + +impl PolkadotCompiler { + #[inline] + pub fn preimage_hashes( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + ) -> CompilerProto::PreSigningOutput<'static> { + Self::preimage_hashes_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } + + fn preimage_hashes_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + ) -> SigningResult> { + todo!() + } + + #[inline] + pub fn compile( + coin: &dyn CoinContext, + input: Proto::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Proto::SigningOutput<'static> { + Self::compile_impl(coin, input, signatures, public_keys) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + fn compile_impl( + _coin: &dyn CoinContext, + _input: Proto::SigningInput<'_>, + _signatures: Vec, + _public_keys: Vec, + ) -> SigningResult> { + todo!() + } +} diff --git a/rust/chains/tw_polkadot/src/entry.rs b/rust/chains/tw_polkadot/src/entry.rs new file mode 100644 index 00000000000..b8807f43f86 --- /dev/null +++ b/rust/chains/tw_polkadot/src/entry.rs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::PolkadotAddress; +use crate::compiler::PolkadotCompiler; +use crate::signer::PolkadotSigner; +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::Polkadot::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct PolkadotEntry; + +impl CoinEntry for PolkadotEntry { + type AddressPrefix = NoPrefix; + type Address = PolkadotAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + _address: &str, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + PolkadotAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + _public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + PolkadotSigner::sign(coin, input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + PolkadotCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + PolkadotCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs new file mode 100644 index 00000000000..3a88e3e1217 --- /dev/null +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -0,0 +1,13 @@ +use tw_hash::H32; +use tw_proto::Polkadot::Proto; + +#[derive(Debug, Clone)] +pub struct Extrinsic; + +impl Extrinsic { + pub fn from_input(input: Proto::SigningInput<'_>) -> Self { + // let x = H32::from(input.block_hash); + + Self {} + } +} diff --git a/rust/chains/tw_polkadot/src/lib.rs b/rust/chains/tw_polkadot/src/lib.rs new file mode 100644 index 00000000000..0ee39a4bff9 --- /dev/null +++ b/rust/chains/tw_polkadot/src/lib.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod compiler; +pub mod entry; +mod extrinsic; +pub mod signer; diff --git a/rust/chains/tw_polkadot/src/signer.rs b/rust/chains/tw_polkadot/src/signer.rs new file mode 100644 index 00000000000..12c54f24432 --- /dev/null +++ b/rust/chains/tw_polkadot/src/signer.rs @@ -0,0 +1,27 @@ +// 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_proto::Polkadot::Proto; + +pub struct PolkadotSigner; + +impl PolkadotSigner { + 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> { + todo!() + } +} diff --git a/rust/tw_any_coin/tests/chains/mod.rs b/rust/tw_any_coin/tests/chains/mod.rs index c4e5020a1dd..9e34690a137 100644 --- a/rust/tw_any_coin/tests/chains/mod.rs +++ b/rust/tw_any_coin/tests/chains/mod.rs @@ -12,6 +12,7 @@ mod greenfield; mod internet_computer; mod native_evmos; mod native_injective; +mod polkadot; mod solana; mod sui; mod tbinance; diff --git a/rust/tw_any_coin/tests/chains/polkadot/mod.rs b/rust/tw_any_coin/tests/chains/polkadot/mod.rs new file mode 100644 index 00000000000..210534e9646 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/polkadot/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod polkadot_address; +mod polkadot_compile; +mod polkadot_sign; diff --git a/rust/tw_any_coin/tests/chains/polkadot/polkadot_address.rs b/rust/tw_any_coin/tests/chains/polkadot/polkadot_address.rs new file mode 100644 index 00000000000..22740d5fb4d --- /dev/null +++ b/rust/tw_any_coin/tests/chains/polkadot/polkadot_address.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_get_data, test_address_invalid, test_address_normalization, test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_polkadot_address_normalization() { + test_address_normalization(CoinType::Polkadot, "DENORMALIZED", "EXPECTED"); +} + +#[test] +fn test_polkadot_address_is_valid() { + test_address_valid(CoinType::Polkadot, "VALID ADDRESS"); +} + +#[test] +fn test_polkadot_address_invalid() { + test_address_invalid(CoinType::Polkadot, "INVALID ADDRESS"); +} + +#[test] +fn test_polkadot_address_get_data() { + test_address_get_data(CoinType::Polkadot, "ADDRESS", "HEX(DATA)"); +} diff --git a/rust/tw_any_coin/tests/chains/polkadot/polkadot_compile.rs b/rust/tw_any_coin/tests/chains/polkadot/polkadot_compile.rs new file mode 100644 index 00000000000..e6d6db3957b --- /dev/null +++ b/rust/tw_any_coin/tests/chains/polkadot/polkadot_compile.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_polkadot_compile() { + todo!() +} diff --git a/rust/tw_any_coin/tests/chains/polkadot/polkadot_sign.rs b/rust/tw_any_coin/tests/chains/polkadot/polkadot_sign.rs new file mode 100644 index 00000000000..42c62684e02 --- /dev/null +++ b/rust/tw_any_coin/tests/chains/polkadot/polkadot_sign.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_polkadot_sign() { + todo!() +} diff --git a/rust/tw_any_coin/tests/coin_address_derivation_test.rs b/rust/tw_any_coin/tests/coin_address_derivation_test.rs index 2e9c4b0c018..c9555d997eb 100644 --- a/rust/tw_any_coin/tests/coin_address_derivation_test.rs +++ b/rust/tw_any_coin/tests/coin_address_derivation_test.rs @@ -154,6 +154,7 @@ fn test_coin_address_derivation() { CoinType::Dydx => "dydx1ten42eesehw0ktddcp0fws7d3ycsqez3kaamq3", CoinType::Solana => "5sn9QYhDaq61jLXJ8Li5BKqGL4DDMJQvU1rdN8XgVuwC", CoinType::Sui => "0x01a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905", + CoinType::Polkadot => todo!(), // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index dfbbc4b7b0d..b7293c7b951 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -24,6 +24,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_polkadot = { path = "../chains/tw_polkadot" } 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 3aff7a6d250..746bf5adb05 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -18,6 +18,7 @@ pub enum BlockchainType { InternetComputer, NativeEvmos, NativeInjective, + Polkadot, Ronin, Solana, Sui, diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index 5a335fa5c3b..28bd70a3e6c 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -18,6 +18,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_polkadot::entry::PolkadotEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; use tw_sui::entry::SuiEntry; @@ -36,6 +37,7 @@ const GREENFIELD: GreenfieldEntry = GreenfieldEntry; const INTERNET_COMPUTER: InternetComputerEntry = InternetComputerEntry; const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; +const POLKADOT: PolkadotEntry = PolkadotEntry; const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; const SUI: SuiEntry = SuiEntry; @@ -54,6 +56,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&INTERNET_COMPUTER), BlockchainType::NativeEvmos => Ok(&NATIVE_EVMOS), BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE), + BlockchainType::Polkadot => Ok(&POLKADOT), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Sui => Ok(&SUI), diff --git a/rust/tw_ss58_address/Cargo.toml b/rust/tw_ss58_address/Cargo.toml new file mode 100644 index 00000000000..bd68945fc59 --- /dev/null +++ b/rust/tw_ss58_address/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tw_ss58_address" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake2 = "0.10.6" +serde = { version = "1.0", features = ["derive"] } +tw_coin_entry = { path = "../tw_coin_entry" } +tw_encoding = { path = "../tw_encoding" } +tw_hash = { path = "../tw_hash" } +tw_keypair = { path = "../tw_keypair" } +tw_memory = { path = "../tw_memory" } diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs new file mode 100644 index 00000000000..7aef675e597 --- /dev/null +++ b/rust/tw_ss58_address/src/lib.rs @@ -0,0 +1,352 @@ +use blake2::{Blake2b512, Digest}; +use std::fmt::Formatter; +use std::str::FromStr; +use tw_coin_entry::error::prelude::*; +use tw_encoding::{base58, hex}; +use tw_keypair::tw::{PrivateKey, PublicKey}; + +// +// Most of the materials implemented here is based on the following resources: +// - https://wiki.polkadot.network/docs/learn-account-advanced#address-format +// - https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/core/src/crypto.rs +// + +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct NetworkId(u16); + +impl NetworkId { + fn new_unchecked(value: u16) -> Self { + return Self(value); + } + + pub fn from_u16(value: u16) -> AddressResult { + match value { + 0..=0x3fff => Ok(Self::new_unchecked(value)), + _ => Err(AddressError::InvalidInput), + } + } + + pub fn value(&self) -> u16 { + self.0 + } +} + +impl TryFrom for NetworkId { + type Error = AddressError; + + fn try_from(value: u16) -> Result { + Self::from_u16(value) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SS58Address { + key: Vec, + network: NetworkId, +} + +impl SS58Address { + const CHECKSUM_SIZE: usize = 2; + const KEY_SIZE: usize = 32; + const SS58_PREFIX: &'static [u8] = b"SS58PRE"; + + fn extract_network(bytes: &[u8]) -> AddressResult<(usize, u16)> { + if bytes.len() == 0 { + return Err(AddressError::MissingPrefix); + } + + match bytes[0] { + 0..=63 => Ok((1, bytes[0] as u16)), + 64..=127 if bytes.len() >= 2 => { + let lower = (bytes[0] << 2) | (bytes[1] >> 6); + let upper = bytes[1] & 0b0011_1111; + Ok((2, (lower as u16) | ((upper as u16) << 8))) + }, + _ => Err(AddressError::UnexpectedAddressPrefix), + } + } + + fn encode_network(network_id: NetworkId) -> Vec { + let network = network_id.value(); + match network { + 0..=63 => { + vec![network as u8] + }, + _ => { + let first = ((network & 0b0000_0000_1111_1100) as u8) >> 2; + let second = + ((network >> 8) as u8) | ((network & 0b0000_0000_0000_0011) as u8) << 6; + vec![first | 0b01000000, second] + }, + } + } + + fn compute_expected_checksum(decoded: &[u8]) -> Vec { + // XXX: tried using tw_hash::blake2::blake2_b, but it did not produce the correct result + let mut ctx = Blake2b512::new(); + ctx.update(Self::SS58_PREFIX); + ctx.update(decoded); + + let mut bytes = ctx.finalize().to_vec(); + bytes.truncate(Self::CHECKSUM_SIZE); + bytes + } + + pub fn from_str(repr: &str) -> AddressResult { + let decoded = base58::decode(repr, base58::Alphabet::BITCOIN) + .map_err(|_| AddressError::FromBase58Error)?; + + let (network_prefix_len, network) = Self::extract_network(&decoded)?; + + if network_prefix_len + Self::KEY_SIZE + Self::CHECKSUM_SIZE != decoded.len() { + return Err(AddressError::FromBase58Error); + } + + let expected_checksum = + Self::compute_expected_checksum(&decoded[..decoded.len() - Self::CHECKSUM_SIZE]); + let checksum = &decoded[decoded.len() - Self::CHECKSUM_SIZE..]; + + if expected_checksum != checksum { + return Err(AddressError::InvalidChecksum); + } + + Ok(Self { + key: decoded[network_prefix_len..network_prefix_len + Self::KEY_SIZE].to_owned(), + network: NetworkId::new_unchecked(network), + }) + } + + pub fn from_public_key(key: &PublicKey, network: u16) -> AddressResult { + let network = NetworkId::try_from(network)?; + + let bytes = match key { + PublicKey::Ed25519(k) => k.as_slice(), + _ => return Err(AddressError::InvalidInput), + }; + + Ok(Self { + key: bytes.to_owned(), + network, + }) + } + + pub fn network(&self) -> NetworkId { + self.network + } + + pub fn key_bytes(&self) -> &[u8] { + &self.key + } + + fn as_bytes(&self) -> Vec { + let mut res = Self::encode_network(self.network); + res.extend(self.key_bytes()); + res.extend(Self::compute_expected_checksum(&res)); + res + } + + fn as_base58_string(&self) -> String { + base58::encode(&self.as_bytes(), base58::Alphabet::BITCOIN) + } + + fn as_hex_string(&self) -> String { + hex::encode(&self.as_bytes(), false) + } +} + +impl FromStr for SS58Address { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + Self::from_str(s) + } +} + +impl std::fmt::Display for SS58Address { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_base58_string()) + } +} + +#[cfg(test)] +mod tests { + use super::{NetworkId, SS58Address}; + use std::string::ToString; + use tw_coin_entry::error::prelude::AddressError; + use tw_encoding::hex; + use tw_keypair::tw::{PublicKey, PublicKeyType}; + + fn networks() -> [(Vec, u16); 27] { + [ + (vec![0x00], 0x00), + (vec![0x01], 0x01), + (vec![0x02], 0x02), + (vec![0x03], 0x03), + (vec![0x04], 0x04), + (vec![0x08], 0x08), + (vec![0x0b], 0x0b), + (vec![0x10], 0x10), + (vec![0x20], 0x20), + (vec![0x23], 0x23), + (vec![0x30], 0x30), + (vec![0x3f], 0x3f), + (vec![0x50, 0x00], 0x40), + (vec![0x50, 0x40], 0x41), + (vec![0x60, 0x00], 0x80), + (vec![0x40, 0x01], 0x0100), + (vec![0x48, 0xc1], 0x0123), + (vec![0x40, 0x02], 0x0200), + (vec![0x40, 0x03], 0x0300), + (vec![0x40, 0x04], 0x0400), + (vec![0x40, 0x08], 0x0800), + (vec![0x7f, 0xcf], 0x0fff), + (vec![0x40, 0x10], 0x1000), + (vec![0x40, 0xd0], 0x1003), + (vec![0x40, 0x20], 0x2000), + (vec![0x40, 0x30], 0x3000), + (vec![0x7f, 0xff], 0x3fff), + ] + } + + #[test] + fn test_network_id() { + for (_, network) in networks() { + let n = NetworkId::try_from(network).expect("error parsing network"); + assert_eq!(n.value(), network); + } + + assert_eq!(NetworkId::try_from(0x4000), Err(AddressError::InvalidInput)); + } + + #[test] + fn test_address_from_str_valid() { + fn test_case(repr: &str, expected_network: u16) { + let addr = SS58Address::from_str(repr).expect("error parsing address"); + assert_eq!(addr.network().value(), expected_network); + } + + test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", 0); + test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", 42); + test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", 2); + test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); + test_case("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 5); + test_case("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64); + test_case("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); + test_case("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE", 4096); + test_case("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6", 8219); + } + + #[test] + fn test_address_from_public_key() { + let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; + let key_bytes = hex::decode(key_hex).expect("error decoding public key"); + let key = PublicKey::new(key_bytes, PublicKeyType::Ed25519) + .expect("error creating test public key"); + + let addr = SS58Address::from_public_key(&key, 0).expect("error creating address"); + assert_eq!(addr.network().value(), 0); + assert_eq!(addr.key_bytes(), key.to_bytes()); + + let addr = SS58Address::from_public_key(&key, 5).expect("error creating address"); + assert_eq!(addr.network().value(), 5); + assert_eq!(addr.key_bytes(), key.to_bytes()); + + let addr = SS58Address::from_public_key(&key, 172).expect("error creating address"); + assert_eq!(addr.network().value(), 172); + assert_eq!(addr.key_bytes(), key.to_bytes()); + } + + #[test] + fn test_address_from_public_key_with_invalid_network() { + let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; + let key_bytes = hex::decode(key_hex).expect("error decoding public key"); + let key = PublicKey::new(key_bytes, PublicKeyType::Ed25519) + .expect("error creating test public key"); + + let res = SS58Address::from_public_key(&key, 32771); + assert_eq!(res, Err(AddressError::InvalidInput)); + } + + #[test] + fn test_extract_network_valid() { + fn test_case(prefix: &[u8], expected_network: u16) { + let (prefix_length, network) = + SS58Address::extract_network(prefix).expect("error extracting network"); + let expected_prefix_length = if network < 64 { 1 } else { 2 } as usize; + assert_eq!( + prefix_length, expected_prefix_length, + "for expected network {}", + expected_network + ); + assert_eq!( + network, expected_network, + "for expected network {}", + expected_network + ); + } + + for (prefix, network) in networks() { + test_case(&prefix, network); + } + + // ensure prefix length is returned as expected + test_case(&[0x00, 0x00], 0x00) + } + + #[test] + fn test_extract_network_invalid() { + // at least one byte is expected + let res = SS58Address::extract_network(&[]); + assert_eq!(res, Err(AddressError::MissingPrefix)); + + // the first byte should be in 0..=127 + let res = SS58Address::extract_network(&[0xFF]); + assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); + + // a second byte should follow + let res = SS58Address::extract_network(&[0x40]); + assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); + } + + #[test] + fn test_encode_network() { + fn test_case(expected_prefix: &[u8], network: u16) { + let prefix = SS58Address::encode_network(NetworkId::new_unchecked(network)); + + assert_eq!(prefix, expected_prefix, "for network {}", network); + } + + for (prefix, network) in networks() { + test_case(&prefix, network); + } + } + + #[test] + fn test_as_base58_string() { + fn test_case(repr: &str) { + let addr = SS58Address::from_str(repr).expect("error parsing address"); + assert_eq!(addr.as_base58_string(), repr); + } + + test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); + test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); + test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr"); + test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp"); + test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); + test_case("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd"); + test_case("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb"); + test_case("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); + test_case("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE"); + test_case("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6"); + } + + #[test] + fn test_as_hex_string() { + let addr = SS58Address::from_str("1FRMM8PEiWXYax7rpS6X4XZX1aAAxSWx1CrKTyrVYhV24fg") + .expect("error parsing address"); + assert_eq!( + addr.as_hex_string(), + "000aff6865635ae11013a83835c019d44ec3f865145943f487ae82a8e7bed3a66b29d7" + ); + } +} From c934b779f10caeb87a0164cae228e4d840d8b884 Mon Sep 17 00:00:00 2001 From: doom Date: Mon, 27 May 2024 14:42:16 +0200 Subject: [PATCH 02/18] Apply suggestions from code review, refactor some utility functions --- rust/tw_ss58_address/src/lib.rs | 250 ++++++++++++++++---------------- 1 file changed, 128 insertions(+), 122 deletions(-) diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs index 7aef675e597..124f35d8016 100644 --- a/rust/tw_ss58_address/src/lib.rs +++ b/rust/tw_ss58_address/src/lib.rs @@ -1,12 +1,13 @@ -use blake2::{Blake2b512, Digest}; use std::fmt::Formatter; use std::str::FromStr; + use tw_coin_entry::error::prelude::*; use tw_encoding::{base58, hex}; -use tw_keypair::tw::{PrivateKey, PublicKey}; +use tw_hash::blake2::blake2_b; +use tw_keypair::ed25519::sha512::PublicKey; // -// Most of the materials implemented here is based on the following resources: +// Most of the materials implemented here are based on the following resources: // - https://wiki.polkadot.network/docs/learn-account-advanced#address-format // - https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/core/src/crypto.rs // @@ -16,7 +17,7 @@ pub struct NetworkId(u16); impl NetworkId { fn new_unchecked(value: u16) -> Self { - return Self(value); + Self(value) } pub fn from_u16(value: u16) -> AddressResult { @@ -26,48 +27,37 @@ impl NetworkId { } } - pub fn value(&self) -> u16 { - self.0 - } -} - -impl TryFrom for NetworkId { - type Error = AddressError; - - fn try_from(value: u16) -> Result { - Self::from_u16(value) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct SS58Address { - key: Vec, - network: NetworkId, -} - -impl SS58Address { - const CHECKSUM_SIZE: usize = 2; - const KEY_SIZE: usize = 32; - const SS58_PREFIX: &'static [u8] = b"SS58PRE"; - - fn extract_network(bytes: &[u8]) -> AddressResult<(usize, u16)> { - if bytes.len() == 0 { + pub fn from_bytes(bytes: &[u8]) -> AddressResult { + if bytes.is_empty() { return Err(AddressError::MissingPrefix); } match bytes[0] { - 0..=63 => Ok((1, bytes[0] as u16)), + 0..=63 => Ok(bytes[0] as u16), 64..=127 if bytes.len() >= 2 => { let lower = (bytes[0] << 2) | (bytes[1] >> 6); let upper = bytes[1] & 0b0011_1111; - Ok((2, (lower as u16) | ((upper as u16) << 8))) + Ok((lower as u16) | ((upper as u16) << 8)) }, _ => Err(AddressError::UnexpectedAddressPrefix), } + .map(Self::new_unchecked) } - fn encode_network(network_id: NetworkId) -> Vec { - let network = network_id.value(); + pub fn value(&self) -> u16 { + self.0 + } + + pub fn prefix_len(&self) -> usize { + if self.value() < 64 { + 1 + } else { + 2 + } + } + + pub fn to_bytes(&self) -> Vec { + let network = self.value(); match network { 0..=63 => { vec![network as u8] @@ -80,14 +70,40 @@ impl SS58Address { }, } } +} + +impl TryFrom for NetworkId { + type Error = AddressError; + + fn try_from(value: u16) -> Result { + Self::from_u16(value) + } +} + +impl TryFrom<&[u8]> for NetworkId { + type Error = AddressError; + + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SS58Address { + key: Vec, + network: NetworkId, +} + +impl SS58Address { + const CHECKSUM_SIZE: usize = 2; + const KEY_SIZE: usize = 32; + const SS58_PREFIX: &'static [u8] = b"SS58PRE"; fn compute_expected_checksum(decoded: &[u8]) -> Vec { - // XXX: tried using tw_hash::blake2::blake2_b, but it did not produce the correct result - let mut ctx = Blake2b512::new(); - ctx.update(Self::SS58_PREFIX); - ctx.update(decoded); + let mut data = Vec::from(Self::SS58_PREFIX); + data.extend(decoded); - let mut bytes = ctx.finalize().to_vec(); + let mut bytes = blake2_b(&data, 64).expect("hash length should be valid"); bytes.truncate(Self::CHECKSUM_SIZE); bytes } @@ -96,9 +112,9 @@ impl SS58Address { let decoded = base58::decode(repr, base58::Alphabet::BITCOIN) .map_err(|_| AddressError::FromBase58Error)?; - let (network_prefix_len, network) = Self::extract_network(&decoded)?; + let network = NetworkId::from_bytes(&decoded)?; - if network_prefix_len + Self::KEY_SIZE + Self::CHECKSUM_SIZE != decoded.len() { + if network.prefix_len() + Self::KEY_SIZE + Self::CHECKSUM_SIZE != decoded.len() { return Err(AddressError::FromBase58Error); } @@ -111,21 +127,16 @@ impl SS58Address { } Ok(Self { - key: decoded[network_prefix_len..network_prefix_len + Self::KEY_SIZE].to_owned(), - network: NetworkId::new_unchecked(network), + key: decoded[network.prefix_len()..network.prefix_len() + Self::KEY_SIZE].to_owned(), + network, }) } pub fn from_public_key(key: &PublicKey, network: u16) -> AddressResult { let network = NetworkId::try_from(network)?; - let bytes = match key { - PublicKey::Ed25519(k) => k.as_slice(), - _ => return Err(AddressError::InvalidInput), - }; - Ok(Self { - key: bytes.to_owned(), + key: key.as_slice().to_owned(), network, }) } @@ -138,19 +149,19 @@ impl SS58Address { &self.key } - fn as_bytes(&self) -> Vec { - let mut res = Self::encode_network(self.network); + pub fn to_bytes(&self) -> Vec { + let mut res = self.network.to_bytes(); res.extend(self.key_bytes()); res.extend(Self::compute_expected_checksum(&res)); res } - fn as_base58_string(&self) -> String { - base58::encode(&self.as_bytes(), base58::Alphabet::BITCOIN) + pub fn to_base58_string(&self) -> String { + base58::encode(&self.to_bytes(), base58::Alphabet::BITCOIN) } - fn as_hex_string(&self) -> String { - hex::encode(&self.as_bytes(), false) + pub fn to_hex_string(&self) -> String { + hex::encode(self.to_bytes(), false) } } @@ -164,17 +175,15 @@ impl FromStr for SS58Address { impl std::fmt::Display for SS58Address { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.as_base58_string()) + f.write_str(&self.to_base58_string()) } } #[cfg(test)] mod tests { use super::{NetworkId, SS58Address}; - use std::string::ToString; use tw_coin_entry::error::prelude::AddressError; - use tw_encoding::hex; - use tw_keypair::tw::{PublicKey, PublicKeyType}; + use tw_keypair::ed25519::sha512::PublicKey; fn networks() -> [(Vec, u16); 27] { [ @@ -219,67 +228,19 @@ mod tests { } #[test] - fn test_address_from_str_valid() { - fn test_case(repr: &str, expected_network: u16) { - let addr = SS58Address::from_str(repr).expect("error parsing address"); - assert_eq!(addr.network().value(), expected_network); - } - - test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", 0); - test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", 42); - test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", 2); - test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); - test_case("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 5); - test_case("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64); - test_case("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); - test_case("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE", 4096); - test_case("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6", 8219); - } - - #[test] - fn test_address_from_public_key() { - let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; - let key_bytes = hex::decode(key_hex).expect("error decoding public key"); - let key = PublicKey::new(key_bytes, PublicKeyType::Ed25519) - .expect("error creating test public key"); - - let addr = SS58Address::from_public_key(&key, 0).expect("error creating address"); - assert_eq!(addr.network().value(), 0); - assert_eq!(addr.key_bytes(), key.to_bytes()); - - let addr = SS58Address::from_public_key(&key, 5).expect("error creating address"); - assert_eq!(addr.network().value(), 5); - assert_eq!(addr.key_bytes(), key.to_bytes()); - - let addr = SS58Address::from_public_key(&key, 172).expect("error creating address"); - assert_eq!(addr.network().value(), 172); - assert_eq!(addr.key_bytes(), key.to_bytes()); - } - - #[test] - fn test_address_from_public_key_with_invalid_network() { - let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; - let key_bytes = hex::decode(key_hex).expect("error decoding public key"); - let key = PublicKey::new(key_bytes, PublicKeyType::Ed25519) - .expect("error creating test public key"); - - let res = SS58Address::from_public_key(&key, 32771); - assert_eq!(res, Err(AddressError::InvalidInput)); - } - - #[test] - fn test_extract_network_valid() { + fn test_network_from_bytes() { fn test_case(prefix: &[u8], expected_network: u16) { - let (prefix_length, network) = - SS58Address::extract_network(prefix).expect("error extracting network"); - let expected_prefix_length = if network < 64 { 1 } else { 2 } as usize; + let network = NetworkId::from_bytes(prefix).expect("error extracting network"); + let expected_prefix_length = if network.value() < 64 { 1 } else { 2 } as usize; assert_eq!( - prefix_length, expected_prefix_length, + network.prefix_len(), + expected_prefix_length, "for expected network {}", expected_network ); assert_eq!( - network, expected_network, + network.value(), + expected_network, "for expected network {}", expected_network ); @@ -294,24 +255,24 @@ mod tests { } #[test] - fn test_extract_network_invalid() { + fn test_network_from_bytes_invalid() { // at least one byte is expected - let res = SS58Address::extract_network(&[]); + let res = NetworkId::from_bytes(&[]); assert_eq!(res, Err(AddressError::MissingPrefix)); // the first byte should be in 0..=127 - let res = SS58Address::extract_network(&[0xFF]); + let res = NetworkId::from_bytes(&[0xFF]); assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); // a second byte should follow - let res = SS58Address::extract_network(&[0x40]); + let res = NetworkId::from_bytes(&[0x40]); assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); } #[test] - fn test_encode_network() { + fn test_network_as_bytes() { fn test_case(expected_prefix: &[u8], network: u16) { - let prefix = SS58Address::encode_network(NetworkId::new_unchecked(network)); + let prefix = NetworkId::new_unchecked(network).to_bytes(); assert_eq!(prefix, expected_prefix, "for network {}", network); } @@ -321,11 +282,56 @@ mod tests { } } + #[test] + fn test_address_from_str() { + fn test_case(repr: &str, expected_network: u16) { + let addr = SS58Address::from_str(repr).expect("error parsing address"); + assert_eq!(addr.network().value(), expected_network); + } + + test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", 0); + test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", 42); + test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", 2); + test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); + test_case("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 5); + test_case("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64); + test_case("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); + test_case("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE", 4096); + test_case("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6", 8219); + } + + #[test] + fn test_address_from_public_key() { + let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; + let key = PublicKey::try_from(key_hex).expect("error creating test public key"); + + let addr = SS58Address::from_public_key(&key, 0).expect("error creating address"); + assert_eq!(addr.network().value(), 0); + assert_eq!(addr.key_bytes(), key.as_slice()); + + let addr = SS58Address::from_public_key(&key, 5).expect("error creating address"); + assert_eq!(addr.network().value(), 5); + assert_eq!(addr.key_bytes(), key.as_slice()); + + let addr = SS58Address::from_public_key(&key, 172).expect("error creating address"); + assert_eq!(addr.network().value(), 172); + assert_eq!(addr.key_bytes(), key.as_slice()); + } + + #[test] + fn test_address_from_public_key_with_invalid_network() { + let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; + let key = PublicKey::try_from(key_hex).expect("error creating test public key"); + + let res = SS58Address::from_public_key(&key, 32771); + assert_eq!(res, Err(AddressError::InvalidInput)); + } + #[test] fn test_as_base58_string() { fn test_case(repr: &str) { let addr = SS58Address::from_str(repr).expect("error parsing address"); - assert_eq!(addr.as_base58_string(), repr); + assert_eq!(addr.to_base58_string(), repr); } test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); @@ -345,7 +351,7 @@ mod tests { let addr = SS58Address::from_str("1FRMM8PEiWXYax7rpS6X4XZX1aAAxSWx1CrKTyrVYhV24fg") .expect("error parsing address"); assert_eq!( - addr.as_hex_string(), + addr.to_hex_string(), "000aff6865635ae11013a83835c019d44ec3f865145943f487ae82a8e7bed3a66b29d7" ); } From da1ad8b9edcf0a4cd16257b6ff80d6179f312aa8 Mon Sep 17 00:00:00 2001 From: doom Date: Mon, 27 May 2024 15:39:15 +0200 Subject: [PATCH 03/18] Remove unused dependency --- rust/tw_ss58_address/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/tw_ss58_address/Cargo.toml b/rust/tw_ss58_address/Cargo.toml index bd68945fc59..5df9156b1f1 100644 --- a/rust/tw_ss58_address/Cargo.toml +++ b/rust/tw_ss58_address/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -blake2 = "0.10.6" serde = { version = "1.0", features = ["derive"] } tw_coin_entry = { path = "../tw_coin_entry" } tw_encoding = { path = "../tw_encoding" } From 59665b418a06dddc5cd7e524ecf3c0e39f0d3d3d Mon Sep 17 00:00:00 2001 From: doom Date: Tue, 28 May 2024 15:39:10 +0200 Subject: [PATCH 04/18] Add PolkadotAddress --- rust/Cargo.lock | 2 +- rust/chains/tw_polkadot/Cargo.toml | 1 + rust/chains/tw_polkadot/src/address.rs | 26 ++++++++++++++++-------- rust/chains/tw_polkadot/tests/address.rs | 24 ++++++++++++++++++++++ rust/tw_ss58_address/src/lib.rs | 2 +- 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 rust/chains/tw_polkadot/tests/address.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cf5e0854fd7..8b12269dd05 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1977,6 +1977,7 @@ dependencies = [ "tw_keypair", "tw_memory", "tw_proto", + "tw_ss58_address", ] [[package]] @@ -2022,7 +2023,6 @@ dependencies = [ name = "tw_ss58_address" version = "0.1.0" dependencies = [ - "blake2", "serde", "tw_coin_entry", "tw_encoding", diff --git a/rust/chains/tw_polkadot/Cargo.toml b/rust/chains/tw_polkadot/Cargo.toml index 64fd7c1b772..942365e765a 100644 --- a/rust/chains/tw_polkadot/Cargo.toml +++ b/rust/chains/tw_polkadot/Cargo.toml @@ -9,3 +9,4 @@ tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } tw_proto = { path = "../../tw_proto" } +tw_ss58_address = { path = "../../tw_ss58_address" } diff --git a/rust/chains/tw_polkadot/src/address.rs b/rust/chains/tw_polkadot/src/address.rs index 847ec855f26..57aedf59d93 100644 --- a/rust/chains/tw_polkadot/src/address.rs +++ b/rust/chains/tw_polkadot/src/address.rs @@ -7,29 +7,39 @@ use std::str::FromStr; use tw_coin_entry::coin_entry::CoinAddress; use tw_coin_entry::error::prelude::*; use tw_memory::Data; +use tw_ss58_address::SS58Address; -pub struct PolkadotAddress { - // bytes: - // TODO add necessary fields. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PolkadotAddress(SS58Address); + +const POLKADOT_NETWORK_ID: u16 = 0; + +impl PolkadotAddress { + pub fn with_network_check(self) -> AddressResult { + if self.0.network().value() != POLKADOT_NETWORK_ID { + return Err(AddressError::UnexpectedAddressPrefix); + } + Ok(self) + } } impl CoinAddress for PolkadotAddress { #[inline] fn data(&self) -> Data { - todo!() + self.0.to_bytes() } } impl FromStr for PolkadotAddress { type Err = AddressError; - fn from_str(_s: &str) -> Result { - todo!() + fn from_str(s: &str) -> Result { + SS58Address::from_str(s).map(PolkadotAddress) } } impl fmt::Display for PolkadotAddress { - fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) } } diff --git a/rust/chains/tw_polkadot/tests/address.rs b/rust/chains/tw_polkadot/tests/address.rs new file mode 100644 index 00000000000..eed4133ffb3 --- /dev/null +++ b/rust/chains/tw_polkadot/tests/address.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +use tw_polkadot::address::PolkadotAddress; + +#[test] +fn test_polkadot_address_valid() { + // Polkadot ed25519 + PolkadotAddress::from_str("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu") + .expect("error parsing address"); + + // Polkadot sr25519 + PolkadotAddress::from_str("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony") + .expect("error parsing address"); +} + +#[test] +fn test_polkadot_address_invalid() { + // Empty address + PolkadotAddress::from_str("").expect_err("no error parsing invalid address"); + + // Invalid address + PolkadotAddress::from_str("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsT^^^") + .expect_err("no error parsing invalid address"); +} diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs index 124f35d8016..5711987b14d 100644 --- a/rust/tw_ss58_address/src/lib.rs +++ b/rust/tw_ss58_address/src/lib.rs @@ -88,7 +88,7 @@ impl TryFrom<&[u8]> for NetworkId { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SS58Address { key: Vec, network: NetworkId, From 2d896db285d2d7436097e35b3eb6eec6158ce877 Mon Sep 17 00:00:00 2001 From: doom Date: Tue, 28 May 2024 16:51:11 +0200 Subject: [PATCH 05/18] Add NetworkId constants and Hash derivation --- rust/tw_ss58_address/src/lib.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs index 5711987b14d..807c0869650 100644 --- a/rust/tw_ss58_address/src/lib.rs +++ b/rust/tw_ss58_address/src/lib.rs @@ -12,11 +12,17 @@ use tw_keypair::ed25519::sha512::PublicKey; // - https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/core/src/crypto.rs // -#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct NetworkId(u16); impl NetworkId { - fn new_unchecked(value: u16) -> Self { + pub const POLKADOT: Self = Self::new_unchecked(0); + pub const KUSAMA: Self = Self::new_unchecked(2); + pub const GENERIC_SUBSTRATE: Self = Self::new_unchecked(42); +} + +impl NetworkId { + const fn new_unchecked(value: u16) -> Self { Self(value) } @@ -88,7 +94,7 @@ impl TryFrom<&[u8]> for NetworkId { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SS58Address { key: Vec, network: NetworkId, @@ -289,10 +295,10 @@ mod tests { assert_eq!(addr.network().value(), expected_network); } - test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", 0); - test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", 42); - test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", 2); - test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); + test_case("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", NetworkId::POLKADOT.value()); + test_case("5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", NetworkId::GENERIC_SUBSTRATE.value()); + test_case("CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", NetworkId::KUSAMA.value()); + test_case("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", NetworkId::KUSAMA.value()); test_case("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 5); test_case("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64); test_case("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); From ae5f5db0f7c72dc366f671470375fa84d6da7325 Mon Sep 17 00:00:00 2001 From: doom Date: Wed, 29 May 2024 16:30:44 +0200 Subject: [PATCH 06/18] Add SCALE encoding for integers (fixed and compact) --- rust/chains/tw_polkadot/Cargo.toml | 1 + rust/chains/tw_polkadot/src/lib.rs | 3 +- rust/chains/tw_polkadot/src/scale.rs | 338 +++++++++++++++++++++++++++ rust/tw_number/src/u256.rs | 68 +++++- 4 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 rust/chains/tw_polkadot/src/scale.rs diff --git a/rust/chains/tw_polkadot/Cargo.toml b/rust/chains/tw_polkadot/Cargo.toml index 942365e765a..321fd8c9a9a 100644 --- a/rust/chains/tw_polkadot/Cargo.toml +++ b/rust/chains/tw_polkadot/Cargo.toml @@ -8,5 +8,6 @@ tw_coin_entry = { path = "../../tw_coin_entry" } tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } +tw_number = { path = "../../tw_number" } tw_proto = { path = "../../tw_proto" } tw_ss58_address = { path = "../../tw_ss58_address" } diff --git a/rust/chains/tw_polkadot/src/lib.rs b/rust/chains/tw_polkadot/src/lib.rs index 0ee39a4bff9..9adabac1d89 100644 --- a/rust/chains/tw_polkadot/src/lib.rs +++ b/rust/chains/tw_polkadot/src/lib.rs @@ -5,5 +5,6 @@ pub mod address; pub mod compiler; pub mod entry; -mod extrinsic; +pub mod extrinsic; +mod scale; pub mod signer; diff --git a/rust/chains/tw_polkadot/src/scale.rs b/rust/chains/tw_polkadot/src/scale.rs new file mode 100644 index 00000000000..b34475b8021 --- /dev/null +++ b/rust/chains/tw_polkadot/src/scale.rs @@ -0,0 +1,338 @@ +use tw_number::U256; + +/// +/// SCALE encoding implementation (see https://docs.substrate.io/reference/scale-codec) +/// TODO: this is a substrate-specific encoding, but consider moving to tw_encoding crate +/// + +pub trait ToScale { + fn to_scale(&self) -> Vec; +} + +impl ToScale for bool { + fn to_scale(&self) -> Vec { + (if *self == true { 0x01 } else { 0x00 } as u8).to_scale() + } +} + +macro_rules! fixed_impl { + ($($t:ty),+) => { + $(impl ToScale for $t { + fn to_scale(&self) -> Vec { + self.to_le_bytes().to_vec() + } + })+ + }; +} + +fixed_impl!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize); + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub struct Compact(pub T); + +// Implementations for Compact + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + match self.0 { + 0..=0b0011_1111 => vec![self.0 << 2], + _ => (((self.0 as u16) << 2) | 0b01).to_scale(), + } + } +} + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + match self.0 { + 0..=0b0011_1111 => vec![(self.0 as u8) << 2], + 0..=0b0011_1111_1111_1111 => ((self.0 << 2) | 0b01).to_scale(), + _ => (((self.0 as u32) << 2) | 0b10).to_scale(), + } + } +} + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + match self.0 { + 0..=0b0011_1111 => vec![(self.0 as u8) << 2], + 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale(), + 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => ((self.0 << 2) | 0b10).to_scale(), + _ => { + let mut v = vec![0b11]; + v.extend(self.0.to_scale()); + v + }, + } + } +} + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + match self.0 { + 0..=0b0011_1111 => vec![(self.0 as u8) << 2], + 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale(), + 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => { + (((self.0 as u32) << 2) | 0b10).to_scale() + }, + _ => { + let bytes_needed = 8 - self.0.leading_zeros() / 8; + let mut v = Vec::with_capacity(bytes_needed as usize); + v.push(0b11 + ((bytes_needed - 4) << 2) as u8); + let mut x = self.0; + for _ in 0..bytes_needed { + v.push(x as u8); + x >>= 8; + } + v + }, + } + } +} + +// TODO: implement U128 support (we don't have it yet in tw_number) + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + // match syntax gets a bit cluttered without u256 literals, falling back to if's + + if self.0 <= 0b0011_1111u64.into() { + return vec![self.0.low_u8() << 2]; + } + + if self.0 <= 0b0011_1111_1111_1111u64.into() { + let v = u16::try_from(self.0).expect("cannot happen as we just checked the value"); + return ((v << 2) | 0b01).to_scale(); + } + + if self.0 <= 0b0011_1111_1111_1111_1111_1111_1111_1111u64.into() { + let v = u32::try_from(self.0).expect("cannot happen as we just checked the value"); + return ((v << 2) | 0b10).to_scale(); + } + + let bytes_needed = 32 - self.0.leading_zeros() / 8; + let mut v = Vec::with_capacity(bytes_needed as usize); + v.push(0b11 + ((bytes_needed - 4) << 2) as u8); + let mut x = self.0; + for _ in 0..bytes_needed { + v.push(x.low_u8()); + x >>= 8; + } + + v + } +} + +impl ToScale for Compact { + fn to_scale(&self) -> Vec { + Compact(self.0 as u64).to_scale() + } +} + +impl ToScale for &[T] +where + T: ToScale, +{ + fn to_scale(&self) -> Vec { + let mut data = Compact(self.len()).to_scale(); + for ts in self.iter() { + data.extend(ts.to_scale()); + } + data + } +} + +impl ToScale for Vec +where + T: ToScale, +{ + fn to_scale(&self) -> Vec { + self.as_slice().to_scale() + } +} + +pub struct Raw(pub Vec); + +impl ToScale for Raw { + fn to_scale(&self) -> Vec { + self.0.clone() + } +} + +mod tests { + use super::{Compact, ToScale}; + use tw_number::U256; + + #[test] + fn test_fixed_width_integers() { + assert_eq!(69i8.to_scale(), &[0x45]); + assert_eq!(42u16.to_scale(), &[0x2a, 0x00]); + assert_eq!(16777215u32.to_scale(), &[0xff, 0xff, 0xff, 0x00]); + } + + #[test] + fn test_bool() { + assert_eq!(true.to_scale(), &[0x01]); + assert_eq!(false.to_scale(), &[0x00]); + } + + #[test] + fn test_compact_integers() { + assert_eq!(Compact(0u8).to_scale(), &[0x00]); + assert_eq!(Compact(1u8).to_scale(), &[0x04]); + assert_eq!(Compact(18u8).to_scale(), &[0x48]); + assert_eq!(Compact(42u8).to_scale(), &[0xa8]); + assert_eq!(Compact(63u8).to_scale(), &[0xfc]); + assert_eq!(Compact(64u8).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(69u8).to_scale(), &[0x15, 0x01]); + + assert_eq!(Compact(0u16).to_scale(), &[0x00]); + assert_eq!(Compact(1u16).to_scale(), &[0x04]); + assert_eq!(Compact(18u16).to_scale(), &[0x48]); + assert_eq!(Compact(42u16).to_scale(), &[0xa8]); + assert_eq!(Compact(63u16).to_scale(), &[0xfc]); + assert_eq!(Compact(64u16).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(69u16).to_scale(), &[0x15, 0x01]); + assert_eq!(Compact(12345u16).to_scale(), &[0xe5, 0xc0]); + assert_eq!(Compact(16383u16).to_scale(), &[0xfd, 0xff]); + assert_eq!(Compact(16384u16).to_scale(), &[0x02, 0x00, 0x01, 0x00]); + assert_eq!(Compact(65535u16).to_scale(), &[0xfe, 0xff, 0x03, 0x00]); + + assert_eq!(Compact(0u32).to_scale(), &[0x00]); + assert_eq!(Compact(1u32).to_scale(), &[0x04]); + assert_eq!(Compact(18u32).to_scale(), &[0x48]); + assert_eq!(Compact(42u32).to_scale(), &[0xa8]); + assert_eq!(Compact(63u32).to_scale(), &[0xfc]); + assert_eq!(Compact(64u32).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(69u32).to_scale(), &[0x15, 0x01]); + assert_eq!(Compact(12345u32).to_scale(), &[0xe5, 0xc0]); + assert_eq!(Compact(16383u32).to_scale(), &[0xfd, 0xff]); + assert_eq!(Compact(16384u32).to_scale(), &[0x02, 0x00, 0x01, 0x00]); + assert_eq!(Compact(65535u32).to_scale(), &[0xfe, 0xff, 0x03, 0x00]); + assert_eq!(Compact(1073741823u32).to_scale(), &[0xfe, 0xff, 0xff, 0xff]); + assert_eq!( + Compact(1073741824u32).to_scale(), + &[0x03, 0x00, 0x00, 0x00, 0x40] + ); + assert_eq!( + Compact(4294967295u32).to_scale(), + &[0x03, 0xff, 0xff, 0xff, 0xff] + ); + + assert_eq!(Compact(0u64).to_scale(), &[0x00]); + assert_eq!(Compact(1u64).to_scale(), &[0x04]); + assert_eq!(Compact(18u64).to_scale(), &[0x48]); + assert_eq!(Compact(42u64).to_scale(), &[0xa8]); + assert_eq!(Compact(63u64).to_scale(), &[0xfc]); + assert_eq!(Compact(64u64).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(69u64).to_scale(), &[0x15, 0x01]); + assert_eq!(Compact(12345u64).to_scale(), &[0xe5, 0xc0]); + assert_eq!(Compact(16383u64).to_scale(), &[0xfd, 0xff]); + assert_eq!(Compact(16384u64).to_scale(), &[0x02, 0x00, 0x01, 0x00]); + assert_eq!(Compact(65535u64).to_scale(), &[0xfe, 0xff, 0x03, 0x00]); + assert_eq!(Compact(1073741823u64).to_scale(), &[0xfe, 0xff, 0xff, 0xff]); + assert_eq!( + Compact(1073741824u64).to_scale(), + &[0x03, 0x00, 0x00, 0x00, 0x40] + ); + assert_eq!( + Compact(4294967295u64).to_scale(), + &[0x03, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(4294967296u64).to_scale(), + &[0x07, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(100000000000000u64).to_scale(), + &[0x0b, 0x00, 0x40, 0x7a, 0x10, 0xf3, 0x5a] + ); + assert_eq!( + Compact(1099511627776u64).to_scale(), + &[0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(281474976710656u64).to_scale(), + &[0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(72057594037927935u64).to_scale(), + &[0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(72057594037927936u64).to_scale(), + &[0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(18446744073709551615u64).to_scale(), + &[0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] + ); + + assert_eq!(Compact(U256::from(0u64)).to_scale(), &[0x00]); + assert_eq!(Compact(U256::from(1u64)).to_scale(), &[0x04]); + assert_eq!(Compact(U256::from(18u64)).to_scale(), &[0x48]); + assert_eq!(Compact(U256::from(42u64)).to_scale(), &[0xa8]); + assert_eq!(Compact(U256::from(63u64)).to_scale(), &[0xfc]); + assert_eq!(Compact(U256::from(64u64)).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(U256::from(69u64)).to_scale(), &[0x15, 0x01]); + assert_eq!(Compact(U256::from(12345u64)).to_scale(), &[0xe5, 0xc0]); + assert_eq!(Compact(U256::from(16383u64)).to_scale(), &[0xfd, 0xff]); + assert_eq!( + Compact(U256::from(16384u64)).to_scale(), + &[0x02, 0x00, 0x01, 0x00] + ); + assert_eq!( + Compact(U256::from(65535u64)).to_scale(), + &[0xfe, 0xff, 0x03, 0x00] + ); + assert_eq!( + Compact(U256::from(1073741823u64)).to_scale(), + &[0xfe, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(U256::from(1073741824u64)).to_scale(), + &[0x03, 0x00, 0x00, 0x00, 0x40] + ); + assert_eq!( + Compact(U256::from(4294967295u64)).to_scale(), + &[0x03, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(U256::from(4294967296u64)).to_scale(), + &[0x07, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(U256::from(100000000000000u64)).to_scale(), + &[0x0b, 0x00, 0x40, 0x7a, 0x10, 0xf3, 0x5a] + ); + assert_eq!( + Compact(U256::from(1099511627776u64)).to_scale(), + &[0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(U256::from(281474976710656u64)).to_scale(), + &[0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(U256::from(72057594037927935u64)).to_scale(), + &[0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(U256::from(72057594037927936u64)).to_scale(), + &[0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(U256::from(18446744073709551615u64)).to_scale(), + &[0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] + ); + } + + #[test] + fn test_slice() { + let empty: [u8; 0] = []; + assert_eq!(empty.as_slice().to_scale(), &[0x00]); + assert_eq!( + [4u16, 8, 15, 16, 23, 42].as_slice().to_scale(), + &[0x18, 0x04, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x17, 0x00, 0x2a, 0x00], + ); + } +} diff --git a/rust/tw_number/src/u256.rs b/rust/tw_number/src/u256.rs index a4285f739b4..fd6edcc5438 100644 --- a/rust/tw_number/src/u256.rs +++ b/rust/tw_number/src/u256.rs @@ -6,12 +6,11 @@ use crate::{NumberError, NumberResult}; use std::borrow::Cow; use std::fmt; use std::fmt::Formatter; -use std::ops::Add; use std::str::FromStr; use tw_hash::H256; use tw_memory::Data; -#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct U256(pub(crate) primitive_types::U256); @@ -144,6 +143,11 @@ impl U256 { fn leading_zero_bytes(&self) -> usize { U256::BYTES - (self.0.bits() + 7) / 8 } + + #[inline] + pub fn leading_zeros(&self) -> u32 { + self.0.leading_zeros() + } } #[cfg(feature = "helpers")] @@ -177,19 +181,59 @@ impl fmt::Display for U256 { } } -/// Implements `Add`, `Add` etc for [U256]. -impl Add for U256 -where - T: Into, -{ - type Output = U256; +/// Implements std::ops traits for [U256] and types that can be converted into it. +macro_rules! impl_ops { + ($trait_name:ident, $func_name:ident, $op:tt) => { + impl std::ops::$trait_name for U256 + where + T: Into, + { + type Output = U256; - #[inline] - fn add(self, rhs: T) -> Self::Output { - U256(self.0 + rhs.into()) - } + #[inline] + fn $func_name(self, rhs: T) -> Self::Output { + U256(self.0 $op rhs.into()) + } + } + }; +} + +macro_rules! impl_ops_assign { + ($trait_name:ident, $func_name:ident, $op:tt) => { + impl std::ops::$trait_name for U256 + where + T: Into, + { + #[inline] + fn $func_name(&mut self, rhs: T) { + *self = *self $op rhs; + } + } + }; } +impl_ops!(Add, add, +); +impl_ops!(Sub, sub, -); +impl_ops!(Mul, mul, *); +impl_ops!(Div, div, /); +impl_ops!(Rem, rem, %); +impl_ops!(BitAnd, bitand, &); +impl_ops!(BitOr, bitor, |); +impl_ops!(BitXor, bitxor, ^); +impl_ops!(Shl, shl, <<); +impl_ops!(Shr, shr, >>); + +impl_ops_assign!(AddAssign, add_assign, +); +impl_ops_assign!(SubAssign, sub_assign, -); +impl_ops_assign!(MulAssign, mul_assign, *); +impl_ops_assign!(DivAssign, div_assign, /); +impl_ops_assign!(RemAssign, rem_assign, %); +impl_ops_assign!(BitAndAssign, bitand_assign, &); +impl_ops_assign!(BitOrAssign, bitor_assign, |); +impl_ops_assign!(BitXorAssign, bitxor_assign, ^); +impl_ops_assign!(ShlAssign, shl_assign, <<); +impl_ops_assign!(ShrAssign, shr_assign, >>); + #[cfg(feature = "serde")] mod impl_serde { use super::U256; From 6ea7c1ee4ff2bab0866bd4df23da45b4f999f19b Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 11:15:45 +0200 Subject: [PATCH 07/18] Fix tiny things in scale.rs --- rust/chains/tw_polkadot/src/scale.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/chains/tw_polkadot/src/scale.rs b/rust/chains/tw_polkadot/src/scale.rs index b34475b8021..918cf63ba24 100644 --- a/rust/chains/tw_polkadot/src/scale.rs +++ b/rust/chains/tw_polkadot/src/scale.rs @@ -11,7 +11,7 @@ pub trait ToScale { impl ToScale for bool { fn to_scale(&self) -> Vec { - (if *self == true { 0x01 } else { 0x00 } as u8).to_scale() + (if *self { 0x01 } else { 0x00 } as u8).to_scale() } } @@ -158,6 +158,7 @@ impl ToScale for Raw { } } +#[cfg(test)] mod tests { use super::{Compact, ToScale}; use tw_number::U256; From be5112367d5a1c2b49544568f1838362e8c31e9e Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 11:20:36 +0200 Subject: [PATCH 08/18] Add extrinsics --- rust/Cargo.lock | 3 + rust/chains/tw_polkadot/Cargo.toml | 2 + rust/chains/tw_polkadot/src/address.rs | 6 +- rust/chains/tw_polkadot/src/extrinsic.rs | 542 ++++++++++++++++++++- rust/chains/tw_polkadot/tests/extrinsic.rs | 320 ++++++++++++ 5 files changed, 863 insertions(+), 10 deletions(-) create mode 100644 rust/chains/tw_polkadot/tests/extrinsic.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8b12269dd05..7e74bdab29c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1972,10 +1972,13 @@ dependencies = [ name = "tw_polkadot" version = "0.1.0" dependencies = [ + "lazy_static", "tw_coin_entry", + "tw_encoding", "tw_hash", "tw_keypair", "tw_memory", + "tw_number", "tw_proto", "tw_ss58_address", ] diff --git a/rust/chains/tw_polkadot/Cargo.toml b/rust/chains/tw_polkadot/Cargo.toml index 321fd8c9a9a..a1002c778f2 100644 --- a/rust/chains/tw_polkadot/Cargo.toml +++ b/rust/chains/tw_polkadot/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] +lazy_static = "1.4.0" tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } diff --git a/rust/chains/tw_polkadot/src/address.rs b/rust/chains/tw_polkadot/src/address.rs index 57aedf59d93..caebbd45e8f 100644 --- a/rust/chains/tw_polkadot/src/address.rs +++ b/rust/chains/tw_polkadot/src/address.rs @@ -7,16 +7,14 @@ use std::str::FromStr; use tw_coin_entry::coin_entry::CoinAddress; use tw_coin_entry::error::prelude::*; use tw_memory::Data; -use tw_ss58_address::SS58Address; +use tw_ss58_address::{NetworkId, SS58Address}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PolkadotAddress(SS58Address); -const POLKADOT_NETWORK_ID: u16 = 0; - impl PolkadotAddress { pub fn with_network_check(self) -> AddressResult { - if self.0.network().value() != POLKADOT_NETWORK_ID { + if self.0.network() != NetworkId::POLKADOT { return Err(AddressError::UnexpectedAddressPrefix); } Ok(self) diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs index 3a88e3e1217..3cc1a9a3cea 100644 --- a/rust/chains/tw_polkadot/src/extrinsic.rs +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -1,13 +1,543 @@ -use tw_hash::H32; +use std::collections::HashMap; +use std::iter::{repeat, Iterator}; + +use lazy_static::lazy_static; + +use tw_number::U256; use tw_proto::Polkadot::Proto; +use tw_proto::Polkadot::Proto::mod_Balance::{ + AssetTransfer, BatchAssetTransfer, BatchTransfer, OneOfmessage_oneof as BalanceVariant, + Transfer, +}; +use tw_proto::Polkadot::Proto::mod_CallIndices::OneOfvariant as CallIndicesVariant; +use tw_proto::Polkadot::Proto::mod_Identity::{ + AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as PolymeshIdentityVariant, +}; +use tw_proto::Polkadot::Proto::mod_PolymeshCall::OneOfmessage_oneof as PolymeshVariant; +use tw_proto::Polkadot::Proto::mod_SigningInput::OneOfmessage_oneof as SigningVariant; +use tw_proto::Polkadot::Proto::mod_Staking::{ + Bond, BondAndNominate, BondExtra, Chill, ChillAndUnbond, Nominate, + OneOfmessage_oneof as StakingVariant, Rebond, Unbond, WithdrawUnbonded, +}; +use tw_proto::Polkadot::Proto::{Balance, PolymeshCall, Staking}; +use tw_ss58_address::{NetworkId, SS58Address}; + +use crate::address::PolkadotAddress; +use crate::scale::{Compact, ToScale}; + +const POLKADOT_MULTI_ADDRESS_SPEC: u32 = 28; +const KUSAMA_MULTI_ADDRESS_SPEC: u32 = 2028; + +// Common calls +const BALANCE_TRANSFER: &str = "Balances.transfer"; +const STAKING_BOND: &str = "Staking.bond"; +const STAKING_BOND_EXTRA: &str = "Staking.bond_extra"; +const STAKING_CHILL: &str = "Staking.chill"; +const STAKING_NOMINATE: &str = "Staking.nominate"; +const STAKING_REBOND: &str = "Staking.rebond"; +const STAKING_UNBOND: &str = "Staking.unbond"; +const STAKING_WITHDRAW_UNBONDED: &str = "Staking.withdraw_unbonded"; +const UTILITY_BATCH: &str = "Utility.batch_all"; + +// Non-existent calls on Polkadot and Kusama chains +const ASSETS_TRANSFER: &str = "Assets.transfer"; +const JOIN_IDENTITY_AS_KEY: &str = "Identity.join_identity_as_key"; +const IDENTITY_ADD_AUTHORIZATION: &str = "Identity.add_authorization"; + +type CallIndicesTable = HashMap<&'static str, Vec>; + +macro_rules! call_indices { + ($($chain:expr => { $($name:expr => [$($value:expr),*] $(,)?)* } $(,)? )*) => { + [ + $(( + $chain, std::collections::HashMap::from_iter( + [$(($name, vec![$($value as u8),+])),+] + ) + )),+ + ].into_iter().collect() + } +} + +lazy_static! { + static ref CALL_INDICES_BY_NETWORK: HashMap = call_indices! { + NetworkId::POLKADOT => { + BALANCE_TRANSFER => [0x05, 0x00], + STAKING_BOND => [0x07, 0x00], + STAKING_BOND_EXTRA => [0x07, 0x01], + STAKING_CHILL => [0x07, 0x06], + STAKING_NOMINATE => [0x07, 0x05], + STAKING_REBOND => [0x07, 0x13], + STAKING_UNBOND => [0x07, 0x02], + STAKING_WITHDRAW_UNBONDED => [0x07, 0x03], + UTILITY_BATCH => [0x1a, 0x02], + }, + NetworkId::KUSAMA => { + BALANCE_TRANSFER => [0x04, 0x00], + STAKING_BOND => [0x06, 0x00], + STAKING_BOND_EXTRA => [0x06, 0x01], + STAKING_CHILL => [0x06, 0x06], + STAKING_NOMINATE => [0x06, 0x05], + STAKING_REBOND => [0x06, 0x13], + STAKING_UNBOND => [0x06, 0x02], + STAKING_WITHDRAW_UNBONDED => [0x06, 0x03], + UTILITY_BATCH => [0x18, 0x02], + } + }; +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum EncodeError { + InvalidNetworkId, + MissingCallIndicesTable, + InvalidCallIndex, + InvalidAddress, + InvalidValue, +} + +type EncodeResult = Result; + +// `Extrinsic` is (for now) just a lightweight wrapper over the actual protobuf object. +// In the future, we will refine the latter to let the caller specify arbitrary extrinsics. #[derive(Debug, Clone)] -pub struct Extrinsic; +pub struct Extrinsic<'a> { + inner: Proto::SigningInput<'a>, +} + +impl<'a> Extrinsic<'a> { + pub fn from_input(input: Proto::SigningInput<'a>) -> Self { + Self { + inner: input.to_owned(), + } + } + + fn get_call_index_for_network(network: NetworkId, key: &str) -> EncodeResult> { + CALL_INDICES_BY_NETWORK + .get(&network) + .and_then(|table| table.get(key)) + .cloned() + .ok_or(EncodeError::MissingCallIndicesTable) + } + + fn get_custom_call_index(civ: &Option) -> EncodeResult> { + if let Some(CallIndicesVariant::custom(c)) = civ.as_ref().map(|i| &i.variant) { + if c.module_index > 0xff || c.method_index > 0xff { + return Err(EncodeError::InvalidCallIndex); + } + return Ok(vec![c.module_index as u8, c.method_index as u8]); + } + Err(EncodeError::MissingCallIndicesTable) + } + + fn get_custom_call_index_or_network( + &self, + key: &str, + civ: &Option, + ) -> EncodeResult> { + Self::get_custom_call_index(civ).or_else(|_| { + let network = NetworkId::try_from(self.inner.network as u16) + .map_err(|_| EncodeError::InvalidNetworkId)?; + + Self::get_call_index_for_network(network, key) + }) + } + + fn should_encode_raw_account(&self) -> bool { + if self.inner.multi_address { + return false; + } + + let (network, spec) = ( + NetworkId::try_from(self.inner.network as u16), + self.inner.spec_version, + ); + + match (network, spec) { + (Ok(NetworkId::POLKADOT), _) if spec >= POLKADOT_MULTI_ADDRESS_SPEC => false, + (Ok(NetworkId::KUSAMA), _) if spec > KUSAMA_MULTI_ADDRESS_SPEC => false, + _ => true, + } + } + + fn encode_account_id(key: &[u8], raw: bool) -> Vec { + let mut data = Vec::with_capacity(key.len() + 1); + + if !raw { + data.push(0x00); + } + data.extend(key); + data + } + + fn encode_transfer(&self, t: &Transfer) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(BALANCE_TRANSFER, &t.call_indices)?; + data.extend(call_index); + + // Encode destination account ID, TODO: check address network ? + let address = + SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; + data.extend(Self::encode_account_id( + address.key_bytes(), + self.should_encode_raw_account(), + )); + + // Encode value + let value = + U256::from_little_endian_slice(&t.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); + + // Encode memo if present, padding it to 32 bytes + if !t.memo.is_empty() { + data.push(0x01); + data.extend(t.memo.as_bytes()); + if t.memo.len() < 32 { + data.extend(repeat(0x00).take(32 - t.memo.len())); + } + } + + Ok(data) + } + + fn encode_asset_transfer(&self, at: &AssetTransfer) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(ASSETS_TRANSFER, &at.call_indices)?; + data.extend(call_index); + + // Encode asset ID if not native token + if at.asset_id != 0 { + data.extend(Compact(at.asset_id).to_scale()); + } + + // Encode destination account ID, TODO: check address network ? + let address = + SS58Address::from_str(&at.to_address).map_err(|_| EncodeError::InvalidAddress)?; + data.extend(Self::encode_account_id( + address.key_bytes(), + self.should_encode_raw_account(), + )); + + // Encode value + let value = + U256::from_little_endian_slice(&at.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); + + Ok(data) + } + + fn encode_batch( + &self, + encoded_calls: &[Vec], + call_indices: &Option, + ) -> EncodeResult> { + let mut data = Vec::new(); + data.extend(self.get_custom_call_index_or_network(UTILITY_BATCH, call_indices)?); + data.extend(Compact(encoded_calls.len()).to_scale()); + for c in encoded_calls { + data.extend(c); + } + Ok(data) + } + + fn encode_batch_transfer(&self, bt: &BatchTransfer) -> EncodeResult> { + let transfers = bt + .transfers + .iter() + .map(|t| self.encode_transfer(t)) + .collect::>>()?; + + self.encode_batch(&transfers, &bt.call_indices) + } + + fn encode_batch_asset_transfer(&self, bat: &BatchAssetTransfer) -> EncodeResult> { + let transfers = bat + .transfers + .iter() + .map(|t| self.encode_asset_transfer(t)) + .collect::>>()?; + + self.encode_batch(&transfers, &bat.call_indices) + } + + fn encode_balance_call(&self, b: &Balance) -> EncodeResult> { + match &b.message_oneof { + BalanceVariant::transfer(t) => self.encode_transfer(t), + BalanceVariant::batchTransfer(bt) => self.encode_batch_transfer(bt), + BalanceVariant::asset_transfer(at) => self.encode_asset_transfer(at), + BalanceVariant::batch_asset_transfer(bat) => self.encode_batch_asset_transfer(bat), + BalanceVariant::None => Ok(Vec::new()), + } + } + + fn encode_staking_bond(&self, b: &Bond) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = self.get_custom_call_index_or_network(STAKING_BOND, &b.call_indices)?; + data.extend(call_index); + + // Encode controller account ID, TODO: check address network ? + if !b.controller.is_empty() { + let address = + SS58Address::from_str(&b.controller).map_err(|_| EncodeError::InvalidAddress)?; + data.extend(Self::encode_account_id( + address.key_bytes(), + self.should_encode_raw_account(), + )); + } + + // Encode value + let value = + U256::from_little_endian_slice(&b.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); -impl Extrinsic { - pub fn from_input(input: Proto::SigningInput<'_>) -> Self { - // let x = H32::from(input.block_hash); + // Encode reward destination + data.extend((b.reward_destination as u8).to_scale()); + + Ok(data) + } + + fn encode_staking_bond_and_nominate(&self, ban: &BondAndNominate) -> EncodeResult> { + // Encode a bond call + let first = self.encode_staking_call(&Staking { + message_oneof: StakingVariant::bond(Bond { + controller: ban.controller.clone(), + value: ban.value.clone(), + reward_destination: ban.reward_destination, + call_indices: ban.call_indices.clone(), + }), + })?; + + // Encode a nominate call + let second = self.encode_staking_call(&Staking { + message_oneof: StakingVariant::nominate(Nominate { + nominators: ban.nominators.clone(), + call_indices: ban.call_indices.clone(), + }), + })?; + + // Encode both calls as batched + self.encode_batch(&[first, second], &ban.call_indices) + } + + fn encode_staking_bond_extra(&self, be: &BondExtra) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(STAKING_BOND_EXTRA, &be.call_indices)?; + data.extend(call_index); + + // Encode value + let value = + U256::from_little_endian_slice(&be.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); + + Ok(data) + } + + fn encode_staking_unbond(&self, u: &Unbond) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = self.get_custom_call_index_or_network(STAKING_UNBOND, &u.call_indices)?; + data.extend(call_index); + + // Encode value + let value = + U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); + + Ok(data) + } + + fn encode_staking_rebond(&self, u: &Rebond) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = self.get_custom_call_index_or_network(STAKING_REBOND, &u.call_indices)?; + data.extend(call_index); + + // Encode value + let value = + U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; + data.extend(Compact(value).to_scale()); + + Ok(data) + } + + fn encode_staking_withdraw_unbonded(&self, wu: &WithdrawUnbonded) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(STAKING_WITHDRAW_UNBONDED, &wu.call_indices)?; + data.extend(call_index); + + // Encode slashing spans as fixed-width u32 + data.extend((wu.slashing_spans as u32).to_scale()); + + Ok(data) + } + + fn encode_staking_nominate(&self, n: &Nominate) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(STAKING_NOMINATE, &n.call_indices)?; + data.extend(call_index); + + // Encode account IDs for nominators + let raw = self.should_encode_raw_account(); + let addresses = n + .nominators + .iter() + .map(|s| SS58Address::from_str(s).map_err(|_| EncodeError::InvalidAddress)) + .map(|a| a.map(|a| Self::encode_account_id(a.key_bytes(), raw))) + .collect::>>()?; + + for addr in addresses { + data.extend(addr); + } + + Ok(data) + } + + fn encode_staking_chill(&self, c: &Chill) -> EncodeResult> { + // Encode call index + let call_index = self.get_custom_call_index_or_network(STAKING_CHILL, &c.call_indices)?; + + Ok(call_index) + } + + fn encode_staking_chill_and_unbond(&self, cau: &ChillAndUnbond) -> EncodeResult> { + let first = self.encode_staking_call(&Staking { + message_oneof: StakingVariant::chill(Chill { + call_indices: cau.call_indices.clone(), + }), + })?; + + let second = self.encode_staking_call(&Staking { + message_oneof: StakingVariant::unbond(Unbond { + value: cau.value.clone(), + call_indices: cau.call_indices.clone(), + }), + })?; + + // Encode both calls as batched + self.encode_batch(&[first, second], &cau.call_indices) + } + + fn encode_staking_call(&self, s: &Staking) -> EncodeResult> { + match &s.message_oneof { + StakingVariant::bond(b) => self.encode_staking_bond(b), + StakingVariant::bond_and_nominate(ban) => self.encode_staking_bond_and_nominate(ban), + StakingVariant::bond_extra(be) => self.encode_staking_bond_extra(be), + StakingVariant::unbond(u) => self.encode_staking_unbond(u), + StakingVariant::withdraw_unbonded(wu) => self.encode_staking_withdraw_unbonded(wu), + StakingVariant::nominate(n) => self.encode_staking_nominate(n), + StakingVariant::chill(c) => self.encode_staking_chill(c), + StakingVariant::chill_and_unbond(cau) => self.encode_staking_chill_and_unbond(cau), + StakingVariant::rebond(r) => self.encode_staking_rebond(r), + StakingVariant::None => Ok(Vec::new()), + } + } + + fn encode_polymesh_join_identity_as_key(&self, j: &JoinIdentityAsKey) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(JOIN_IDENTITY_AS_KEY, &j.call_indices)?; + data.extend(call_index); + + // Encode auth ID + data.extend(j.auth_id.to_scale()); + + Ok(data) + } + + fn encode_polymesh_add_authorization(&self, a: &AddAuthorization) -> EncodeResult> { + let mut data = Vec::new(); + + // Encode call index + let call_index = + self.get_custom_call_index_or_network(IDENTITY_ADD_AUTHORIZATION, &a.call_indices)?; + data.extend(call_index); + + // Encode target + data.push(0x01); + let address = SS58Address::from_str(&a.target).map_err(|_| EncodeError::InvalidAddress)?; + data.extend(Self::encode_account_id(address.key_bytes(), true)); + + // Encode join identity + data.push(0x05); + + if let Some(auth_data) = &a.data { + if let Some(asset) = &auth_data.asset { + data.push(0x01); + data.extend_from_slice(&asset.data); + } else { + data.push(0x00); + } + + if let Some(extrinsic) = &auth_data.extrinsic { + data.push(0x01); + data.extend_from_slice(&extrinsic.data); + } else { + data.push(0x00); + } + + if let Some(portfolio) = &auth_data.portfolio { + data.push(0x01); + data.extend_from_slice(&portfolio.data); + } else { + data.push(0x00); + } + } else { + // Mark everything as authorized (asset, extrinsic, portfolio) + data.extend(&[0x01, 0x00]); + data.extend(&[0x01, 0x00]); + data.extend(&[0x01, 0x00]); + } + + data.extend(Compact(a.expiry).to_scale()); + + Ok(data) + } + + fn encode_polymesh_identity(&self, i: &Proto::Identity) -> EncodeResult> { + match &i.message_oneof { + PolymeshIdentityVariant::join_identity_as_key(j) => { + self.encode_polymesh_join_identity_as_key(j) + }, + PolymeshIdentityVariant::add_authorization(a) => { + self.encode_polymesh_add_authorization(a) + }, + PolymeshIdentityVariant::None => Ok(Vec::new()), + } + } + + fn encode_polymesh_call(&self, p: &PolymeshCall) -> EncodeResult> { + match &p.message_oneof { + PolymeshVariant::identity_call(i) => self.encode_polymesh_identity(i), + PolymeshVariant::None => Ok(Vec::new()), + } + } - Self {} + pub fn encode_call(&self) -> EncodeResult> { + match &self.inner.message_oneof { + SigningVariant::balance_call(b) => self.encode_balance_call(b), + SigningVariant::staking_call(s) => self.encode_staking_call(s), + SigningVariant::polymesh_call(p) => self.encode_polymesh_call(p), + SigningVariant::None => Ok(Vec::new()), + } } } diff --git a/rust/chains/tw_polkadot/tests/extrinsic.rs b/rust/chains/tw_polkadot/tests/extrinsic.rs new file mode 100644 index 00000000000..12b07f9a52e --- /dev/null +++ b/rust/chains/tw_polkadot/tests/extrinsic.rs @@ -0,0 +1,320 @@ +use std::borrow::Cow; +use std::default::Default; + +use tw_encoding::hex::ToHex; +use tw_number::U256; +use tw_polkadot::extrinsic::Extrinsic; +use tw_proto::Polkadot::Proto; +use tw_proto::Polkadot::Proto::mod_Balance::{AssetTransfer, BatchAssetTransfer, Transfer}; +use tw_proto::Polkadot::Proto::mod_Identity::mod_AddAuthorization::{AuthData, Data}; + +#[test] +fn polymesh_encode_transfer_with_memo() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_little_endian().to_vec()), + memo: "MEMO PADDED WITH SPACES".into(), + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x05, + method_index: 0x01, + }, + ), + }), + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call( + Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + Proto::mod_Identity::AddAuthorization { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x07, + method_index: 0x0d, + }, + ), + }), + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + ..Default::default() + }, + ), + }, + ), + }, + ), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070d0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity_with_zero_data() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call( + Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + Proto::mod_Identity::AddAuthorization { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x07, + method_index: 0x0d, + }, + ), + }), + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + data: Some(AuthData { + asset: Some(Data { + data: (&[0x00]).into(), + }), + extrinsic: Some(Data { + data: (&[0x00]).into(), + }), + portfolio: Some(Data { + data: (&[0x00]).into(), + }), + }), + ..Default::default() + }, + ), + }, + ), + }, + ), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070d0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity_allowing_everything() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call( + Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + Proto::mod_Identity::AddAuthorization { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x07, + method_index: 0x0d, + }, + ), + }), + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + data: Some(AuthData { + asset: None, + extrinsic: None, + portfolio: None, + }), + ..Default::default() + }, + ), + }, + ), + }, + ), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070d0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000" + ); +} + +#[test] +fn polymesh_encode_identity() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call( + Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: + Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key( + Proto::mod_Identity::JoinIdentityAsKey { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x07, + method_index: 0x05, + }, + ), + }), + auth_id: 4875, + }, + ), + }, + ), + }, + ), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "07050b13000000000000"); +} + +#[test] +fn statemint_encode_asset_transfer() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2619512-2 + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: Proto::mod_Balance::OneOfmessage_oneof::asset_transfer(AssetTransfer { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x32, + method_index: 0x05, + }, + ), + }), + to_address: "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), + value: Cow::Owned(U256::from(999500000u64).to_little_endian().to_vec()), + asset_id: 1984, + ..Default::default() + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "3205\ + 011f\ + 00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d\ + 82a34cee" + ); +} + +#[test] +fn statemint_encode_batch_asset_transfer() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: Proto::mod_Balance::OneOfmessage_oneof::batch_asset_transfer( + BatchAssetTransfer { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x28, + method_index: 0x00, + }, + ), + }), + fee_asset_id: 0x00, + transfers: vec![AssetTransfer { + call_indices: Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom( + Proto::CustomCallIndices { + module_index: 0x32, + method_index: 0x06, + }, + ), + }), + to_address: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + value: U256::from(808081u64).to_little_endian().to_vec().into(), + asset_id: 1984, + ..Default::default() + }], + }, + ), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "2800\ + 04\ + 3206\ + 011f\ + 00\ + 81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91\ + 46523100" + ); +} + +#[test] +fn kusama_encode_asset_transfer_without_call_indices() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: Proto::mod_Balance::OneOfmessage_oneof::batch_asset_transfer( + BatchAssetTransfer { + fee_asset_id: 0x00, + transfers: vec![AssetTransfer { + to_address: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + value: U256::from(808081u64).to_little_endian().to_vec().into(), + asset_id: 1984, + ..Default::default() + }], + ..Default::default() + }, + ), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + extrinsic.encode_call().expect_err("unexpected success"); +} From a5bf37e005eee6c9fa07279919a2c12c6a6a293e Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 18:43:49 +0200 Subject: [PATCH 09/18] Add PolkadotPrefix --- rust/chains/tw_polkadot/src/address.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/rust/chains/tw_polkadot/src/address.rs b/rust/chains/tw_polkadot/src/address.rs index caebbd45e8f..191602d7b8f 100644 --- a/rust/chains/tw_polkadot/src/address.rs +++ b/rust/chains/tw_polkadot/src/address.rs @@ -6,11 +6,32 @@ use std::fmt; use std::str::FromStr; use tw_coin_entry::coin_entry::CoinAddress; use tw_coin_entry::error::prelude::*; +use tw_coin_entry::prefix::AddressPrefix; use tw_memory::Data; use tw_ss58_address::{NetworkId, SS58Address}; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct PolkadotPrefix(NetworkId); + +impl PolkadotPrefix { + pub fn network(self) -> NetworkId { + self.0 + } +} + +impl TryFrom for PolkadotPrefix { + type Error = AddressError; + + fn try_from(prefix: AddressPrefix) -> Result { + match prefix { + AddressPrefix::SubstrateNetwork(network) => NetworkId::from_u16(network).map(Self), + _ => Err(AddressError::UnexpectedAddressPrefix), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PolkadotAddress(SS58Address); +pub struct PolkadotAddress(pub SS58Address); impl PolkadotAddress { pub fn with_network_check(self) -> AddressResult { From a6c5f85662485feea20c9784dacdacb3dabacbe1 Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 23:15:18 +0200 Subject: [PATCH 10/18] Require a pre-built NetworkId when deriving SS58Address --- rust/tw_ss58_address/src/lib.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs index 807c0869650..0868bdb1fff 100644 --- a/rust/tw_ss58_address/src/lib.rs +++ b/rust/tw_ss58_address/src/lib.rs @@ -138,9 +138,7 @@ impl SS58Address { }) } - pub fn from_public_key(key: &PublicKey, network: u16) -> AddressResult { - let network = NetworkId::try_from(network)?; - + pub fn from_public_key(key: &PublicKey, network: NetworkId) -> AddressResult { Ok(Self { key: key.as_slice().to_owned(), network, @@ -311,28 +309,19 @@ mod tests { let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; let key = PublicKey::try_from(key_hex).expect("error creating test public key"); - let addr = SS58Address::from_public_key(&key, 0).expect("error creating address"); + let addr = SS58Address::from_public_key(&key, NetworkId::POLKADOT).expect("error creating address"); assert_eq!(addr.network().value(), 0); assert_eq!(addr.key_bytes(), key.as_slice()); - let addr = SS58Address::from_public_key(&key, 5).expect("error creating address"); + let addr = SS58Address::from_public_key(&key, NetworkId::new_unchecked(5)).expect("error creating address"); assert_eq!(addr.network().value(), 5); assert_eq!(addr.key_bytes(), key.as_slice()); - let addr = SS58Address::from_public_key(&key, 172).expect("error creating address"); + let addr = SS58Address::from_public_key(&key, NetworkId::new_unchecked(172)).expect("error creating address"); assert_eq!(addr.network().value(), 172); assert_eq!(addr.key_bytes(), key.as_slice()); } - #[test] - fn test_address_from_public_key_with_invalid_network() { - let key_hex = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; - let key = PublicKey::try_from(key_hex).expect("error creating test public key"); - - let res = SS58Address::from_public_key(&key, 32771); - assert_eq!(res, Err(AddressError::InvalidInput)); - } - #[test] fn test_as_base58_string() { fn test_case(repr: &str) { From 0937ed7f9dbdb4ddf0db0d3779bd32491be97dcc Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 23:17:25 +0200 Subject: [PATCH 11/18] Format extrinsic.rs --- rust/chains/tw_polkadot/src/extrinsic.rs | 36 +++++++++++------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs index 3cc1a9a3cea..192ba91964c 100644 --- a/rust/chains/tw_polkadot/src/extrinsic.rs +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -61,26 +61,26 @@ macro_rules! call_indices { lazy_static! { static ref CALL_INDICES_BY_NETWORK: HashMap = call_indices! { NetworkId::POLKADOT => { - BALANCE_TRANSFER => [0x05, 0x00], - STAKING_BOND => [0x07, 0x00], - STAKING_BOND_EXTRA => [0x07, 0x01], - STAKING_CHILL => [0x07, 0x06], - STAKING_NOMINATE => [0x07, 0x05], - STAKING_REBOND => [0x07, 0x13], - STAKING_UNBOND => [0x07, 0x02], + BALANCE_TRANSFER => [0x05, 0x00], + STAKING_BOND => [0x07, 0x00], + STAKING_BOND_EXTRA => [0x07, 0x01], + STAKING_CHILL => [0x07, 0x06], + STAKING_NOMINATE => [0x07, 0x05], + STAKING_REBOND => [0x07, 0x13], + STAKING_UNBOND => [0x07, 0x02], STAKING_WITHDRAW_UNBONDED => [0x07, 0x03], - UTILITY_BATCH => [0x1a, 0x02], + UTILITY_BATCH => [0x1a, 0x02], }, NetworkId::KUSAMA => { - BALANCE_TRANSFER => [0x04, 0x00], - STAKING_BOND => [0x06, 0x00], - STAKING_BOND_EXTRA => [0x06, 0x01], - STAKING_CHILL => [0x06, 0x06], - STAKING_NOMINATE => [0x06, 0x05], - STAKING_REBOND => [0x06, 0x13], - STAKING_UNBOND => [0x06, 0x02], + BALANCE_TRANSFER => [0x04, 0x00], + STAKING_BOND => [0x06, 0x00], + STAKING_BOND_EXTRA => [0x06, 0x01], + STAKING_CHILL => [0x06, 0x06], + STAKING_NOMINATE => [0x06, 0x05], + STAKING_REBOND => [0x06, 0x13], + STAKING_UNBOND => [0x06, 0x02], STAKING_WITHDRAW_UNBONDED => [0x06, 0x03], - UTILITY_BATCH => [0x18, 0x02], + UTILITY_BATCH => [0x18, 0x02], } }; } @@ -106,9 +106,7 @@ pub struct Extrinsic<'a> { impl<'a> Extrinsic<'a> { pub fn from_input(input: Proto::SigningInput<'a>) -> Self { - Self { - inner: input.to_owned(), - } + Self { inner: input } } fn get_call_index_for_network(network: NetworkId, key: &str) -> EncodeResult> { From adf5906e022f3c2f0b94702ee22c48d897df38b6 Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 23:17:43 +0200 Subject: [PATCH 12/18] Add address parsing and derivation in entry.rs --- rust/chains/tw_polkadot/src/entry.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/rust/chains/tw_polkadot/src/entry.rs b/rust/chains/tw_polkadot/src/entry.rs index b8807f43f86..c8741f49246 100644 --- a/rust/chains/tw_polkadot/src/entry.rs +++ b/rust/chains/tw_polkadot/src/entry.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::address::PolkadotAddress; +use crate::address::{PolkadotAddress, PolkadotPrefix}; use crate::compiler::PolkadotCompiler; use crate::signer::PolkadotSigner; use std::str::FromStr; @@ -15,15 +15,15 @@ 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::Polkadot::Proto; use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_ss58_address::{NetworkId, SS58Address}; pub struct PolkadotEntry; impl CoinEntry for PolkadotEntry { - type AddressPrefix = NoPrefix; + type AddressPrefix = PolkadotPrefix; type Address = PolkadotAddress; type SigningInput<'a> = Proto::SigningInput<'a>; type SigningOutput = Proto::SigningOutput<'static>; @@ -40,10 +40,10 @@ impl CoinEntry for PolkadotEntry { fn parse_address( &self, _coin: &dyn CoinContext, - _address: &str, + address: &str, _prefix: Option, ) -> AddressResult { - todo!() + PolkadotAddress::from_str(address)?.with_network_check() } #[inline] @@ -59,11 +59,21 @@ impl CoinEntry for PolkadotEntry { fn derive_address( &self, _coin: &dyn CoinContext, - _public_key: PublicKey, + public_key: PublicKey, _derivation: Derivation, - _prefix: Option, + prefix: Option, ) -> AddressResult { - todo!() + let public_key = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + + SS58Address::from_public_key( + public_key, + prefix + .map(PolkadotPrefix::network) + .unwrap_or(NetworkId::POLKADOT), + ) + .map(PolkadotAddress) } #[inline] From 8943f6b80fe7404bf71e8f26f36a2a71a53e654e Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 23:25:12 +0200 Subject: [PATCH 13/18] Rename SS58Address::from_str to SS58Address::parse --- rust/chains/tw_polkadot/src/extrinsic.rs | 2 +- rust/tw_ss58_address/src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs index 192ba91964c..7223e4d8853 100644 --- a/rust/chains/tw_polkadot/src/extrinsic.rs +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::iter::{repeat, Iterator}; +use std::str::FromStr; use lazy_static::lazy_static; @@ -22,7 +23,6 @@ use tw_proto::Polkadot::Proto::mod_Staking::{ use tw_proto::Polkadot::Proto::{Balance, PolymeshCall, Staking}; use tw_ss58_address::{NetworkId, SS58Address}; -use crate::address::PolkadotAddress; use crate::scale::{Compact, ToScale}; const POLKADOT_MULTI_ADDRESS_SPEC: u32 = 28; diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs index 0868bdb1fff..8c2f7bf12c5 100644 --- a/rust/tw_ss58_address/src/lib.rs +++ b/rust/tw_ss58_address/src/lib.rs @@ -114,7 +114,7 @@ impl SS58Address { bytes } - pub fn from_str(repr: &str) -> AddressResult { + pub fn parse(repr: &str) -> AddressResult { let decoded = base58::decode(repr, base58::Alphabet::BITCOIN) .map_err(|_| AddressError::FromBase58Error)?; @@ -173,7 +173,7 @@ impl FromStr for SS58Address { type Err = AddressError; fn from_str(s: &str) -> Result { - Self::from_str(s) + Self::parse(s) } } @@ -185,6 +185,7 @@ impl std::fmt::Display for SS58Address { #[cfg(test)] mod tests { + use std::str::FromStr; use super::{NetworkId, SS58Address}; use tw_coin_entry::error::prelude::AddressError; use tw_keypair::ed25519::sha512::PublicKey; From 2dbcf0bc1a647ec0a3f5672fd1270d1c193bc81a Mon Sep 17 00:00:00 2001 From: doom Date: Fri, 31 May 2024 23:26:11 +0200 Subject: [PATCH 14/18] Add SubstrateNetwork to AddressPrefix enum --- rust/tw_bech32_address/src/bech32_prefix.rs | 1 + rust/tw_coin_entry/src/prefix.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/rust/tw_bech32_address/src/bech32_prefix.rs b/rust/tw_bech32_address/src/bech32_prefix.rs index fa548edd09a..f9e74351264 100644 --- a/rust/tw_bech32_address/src/bech32_prefix.rs +++ b/rust/tw_bech32_address/src/bech32_prefix.rs @@ -15,6 +15,7 @@ impl TryFrom for Bech32Prefix { fn try_from(prefix: AddressPrefix) -> Result { match prefix { AddressPrefix::Hrp(hrp) => Ok(Bech32Prefix { hrp }), + _ => Err(AddressError::UnexpectedAddressPrefix), } } } diff --git a/rust/tw_coin_entry/src/prefix.rs b/rust/tw_coin_entry/src/prefix.rs index 942b7fec832..1428b70df71 100644 --- a/rust/tw_coin_entry/src/prefix.rs +++ b/rust/tw_coin_entry/src/prefix.rs @@ -9,6 +9,7 @@ use crate::error::prelude::*; #[derive(Clone)] pub enum AddressPrefix { Hrp(String), + SubstrateNetwork(u16), } /// A blockchain's address prefix should be convertable from an `AddressPrefix`. From 307fbf4e3612858db46e663389a4c816e7978865 Mon Sep 17 00:00:00 2001 From: doom Date: Sun, 2 Jun 2024 16:56:03 +0200 Subject: [PATCH 15/18] Rework ToScale trait to allow encoding to an existing Vec, add more implementations --- rust/chains/tw_polkadot/src/scale.rs | 188 +++++++++++++++++++-------- 1 file changed, 132 insertions(+), 56 deletions(-) diff --git a/rust/chains/tw_polkadot/src/scale.rs b/rust/chains/tw_polkadot/src/scale.rs index 918cf63ba24..3b1c7b84c33 100644 --- a/rust/chains/tw_polkadot/src/scale.rs +++ b/rust/chains/tw_polkadot/src/scale.rs @@ -6,20 +6,26 @@ use tw_number::U256; /// pub trait ToScale { - fn to_scale(&self) -> Vec; + fn to_scale(&self) -> Vec { + let mut data = Vec::new(); + self.to_scale_into(&mut data); + data + } + + fn to_scale_into(&self, out: &mut Vec); } impl ToScale for bool { - fn to_scale(&self) -> Vec { - (if *self { 0x01 } else { 0x00 } as u8).to_scale() + fn to_scale_into(&self, out: &mut Vec) { + out.push(if *self { 0x01 } else { 0x00 } as u8); } } macro_rules! fixed_impl { ($($t:ty),+) => { $(impl ToScale for $t { - fn to_scale(&self) -> Vec { - self.to_le_bytes().to_vec() + fn to_scale_into(&self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()) } })+ }; @@ -33,58 +39,58 @@ pub struct Compact(pub T); // Implementations for Compact impl ToScale for Compact { - fn to_scale(&self) -> Vec { + fn to_scale_into(&self, out: &mut Vec) { match self.0 { - 0..=0b0011_1111 => vec![self.0 << 2], - _ => (((self.0 as u16) << 2) | 0b01).to_scale(), + 0..=0b0011_1111 => out.push(self.0 << 2), + _ => (((self.0 as u16) << 2) | 0b01).to_scale_into(out), } } } impl ToScale for Compact { - fn to_scale(&self) -> Vec { + fn to_scale_into(&self, out: &mut Vec) { match self.0 { - 0..=0b0011_1111 => vec![(self.0 as u8) << 2], - 0..=0b0011_1111_1111_1111 => ((self.0 << 2) | 0b01).to_scale(), - _ => (((self.0 as u32) << 2) | 0b10).to_scale(), + 0..=0b0011_1111 => out.push((self.0 as u8) << 2), + 0..=0b0011_1111_1111_1111 => ((self.0 << 2) | 0b01).to_scale_into(out), + _ => (((self.0 as u32) << 2) | 0b10).to_scale_into(out), } } } impl ToScale for Compact { - fn to_scale(&self) -> Vec { + fn to_scale_into(&self, out: &mut Vec) { match self.0 { - 0..=0b0011_1111 => vec![(self.0 as u8) << 2], - 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale(), - 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => ((self.0 << 2) | 0b10).to_scale(), + 0..=0b0011_1111 => out.push((self.0 as u8) << 2), + 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale_into(out), + 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => { + ((self.0 << 2) | 0b10).to_scale_into(out) + } _ => { - let mut v = vec![0b11]; - v.extend(self.0.to_scale()); - v - }, + out.push(0b11); + self.0.to_scale_into(out); + } } } } impl ToScale for Compact { - fn to_scale(&self) -> Vec { + fn to_scale_into(&self, out: &mut Vec) { match self.0 { - 0..=0b0011_1111 => vec![(self.0 as u8) << 2], - 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale(), + 0..=0b0011_1111 => out.push((self.0 as u8) << 2), + 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale_into(out), 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => { - (((self.0 as u32) << 2) | 0b10).to_scale() - }, + (((self.0 as u32) << 2) | 0b10).to_scale_into(out) + } _ => { let bytes_needed = 8 - self.0.leading_zeros() / 8; - let mut v = Vec::with_capacity(bytes_needed as usize); - v.push(0b11 + ((bytes_needed - 4) << 2) as u8); + out.reserve(bytes_needed as usize); + out.push(0b11 + ((bytes_needed - 4) << 2) as u8); let mut x = self.0; for _ in 0..bytes_needed { - v.push(x as u8); + out.push(x as u8); x >>= 8; } - v - }, + } } } } @@ -92,75 +98,111 @@ impl ToScale for Compact { // TODO: implement U128 support (we don't have it yet in tw_number) impl ToScale for Compact { - fn to_scale(&self) -> Vec { + fn to_scale_into(&self, out: &mut Vec) { // match syntax gets a bit cluttered without u256 literals, falling back to if's if self.0 <= 0b0011_1111u64.into() { - return vec![self.0.low_u8() << 2]; + return out.push(self.0.low_u8() << 2); } if self.0 <= 0b0011_1111_1111_1111u64.into() { let v = u16::try_from(self.0).expect("cannot happen as we just checked the value"); - return ((v << 2) | 0b01).to_scale(); + return ((v << 2) | 0b01).to_scale_into(out); } if self.0 <= 0b0011_1111_1111_1111_1111_1111_1111_1111u64.into() { let v = u32::try_from(self.0).expect("cannot happen as we just checked the value"); - return ((v << 2) | 0b10).to_scale(); + return ((v << 2) | 0b10).to_scale_into(out); } let bytes_needed = 32 - self.0.leading_zeros() / 8; - let mut v = Vec::with_capacity(bytes_needed as usize); - v.push(0b11 + ((bytes_needed - 4) << 2) as u8); + out.reserve(bytes_needed as usize); + out.push(0b11 + ((bytes_needed - 4) << 2) as u8); let mut x = self.0; for _ in 0..bytes_needed { - v.push(x.low_u8()); + out.push(x.low_u8()); x >>= 8; } - - v } } impl ToScale for Compact { - fn to_scale(&self) -> Vec { - Compact(self.0 as u64).to_scale() + fn to_scale_into(&self, out: &mut Vec) { + Compact(self.0 as u64).to_scale_into(out) + } +} + +impl ToScale for Option where T: ToScale { + fn to_scale_into(&self, out: &mut Vec) { + if let Some(t) = &self { + t.to_scale_into(out); + } } } impl ToScale for &[T] -where - T: ToScale, + where + T: ToScale, { - fn to_scale(&self) -> Vec { - let mut data = Compact(self.len()).to_scale(); + fn to_scale_into(&self, out: &mut Vec) { + Compact(self.len()).to_scale_into(out); for ts in self.iter() { - data.extend(ts.to_scale()); + ts.to_scale_into(out); } - data } } impl ToScale for Vec -where - T: ToScale, + where + T: ToScale, { - fn to_scale(&self) -> Vec { - self.as_slice().to_scale() + fn to_scale_into(&self, out: &mut Vec) { + self.as_slice().to_scale_into(out) + } +} + +macro_rules! tuple_impl { + ($(,)?) => {}; + ($first:tt, $($rest:tt),* $(,)?) => { + tuple_impl!($($rest),*,); + tuple_impl!(@make_impl $first $($rest)*); + }; + (@make_impl $($t:tt)+) => { + #[allow(non_snake_case, unused_parens, unconditional_recursion)] // the compiler seems confused here + impl <$($t),+> ToScale for ($($t),+,) where $($t: ToScale),+ { + fn to_scale_into(&self, out: &mut Vec) { + let ($($t),*) = self; + $( + $t.to_scale_into(out); + )* + } + } + }; +} + +tuple_impl!(T0, T1, T2, T3, T4, T5, T6, T7); + +pub struct FixedLength<'a, T>(pub &'a [T]); + +impl<'a, T> ToScale for FixedLength<'a, T> where T: ToScale { + fn to_scale_into(&self, out: &mut Vec) { + for ts in self.0.iter() { + ts.to_scale_into(out); + } } } -pub struct Raw(pub Vec); +pub struct Raw<'a>(pub &'a [u8]); -impl ToScale for Raw { - fn to_scale(&self) -> Vec { - self.0.clone() +impl<'a> ToScale for Raw<'a> { + fn to_scale_into(&self, out: &mut Vec) { + out.extend_from_slice(&self.0); } } #[cfg(test)] mod tests { - use super::{Compact, ToScale}; + use super::{Compact, FixedLength, Raw, ToScale}; use tw_number::U256; #[test] @@ -327,6 +369,14 @@ mod tests { ); } + #[test] + fn test_option() { + let empty: [u8; 0] = []; + assert_eq!(Some(1u8).to_scale(), &[0x01]); + assert_eq!(None::.to_scale(), empty); + assert_eq!(Some(Compact(1u64)).to_scale(), &[0x04]); + } + #[test] fn test_slice() { let empty: [u8; 0] = []; @@ -336,4 +386,30 @@ mod tests { &[0x18, 0x04, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x17, 0x00, 0x2a, 0x00], ); } + + #[test] + fn test_fixed_length() { + let empty: [u8; 0] = []; + assert_eq!(FixedLength(&empty).to_scale(), empty); + assert_eq!( + FixedLength(&[4u16, 8, 15, 16, 23, 42]).to_scale(), + &[0x04, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x17, 0x00, 0x2a, 0x00], + ); + } + + #[test] + fn test_raw() { + let empty: [u8; 0] = []; + assert_eq!(Raw(empty.as_slice()).to_scale(), empty); + assert_eq!( + Raw([4u8, 8, 15, 16, 23, 42].as_slice()).to_scale(), + &[0x04, 0x08, 0x0f, 0x10, 0x17, 0x2a], + ); + } + + #[test] + fn test_tuple() { + assert_eq!((Compact(3u32), false).to_scale(), &[0x0c, 0x00]); + assert_eq!((1u8, 2u8, 3u8).to_scale(), &[0x01, 0x02, 0x03]); + } } From 136962d27064e87c9967a6e270a8c368a27aaffc Mon Sep 17 00:00:00 2001 From: doom Date: Sun, 2 Jun 2024 17:07:54 +0200 Subject: [PATCH 16/18] Refactor Extrinsic to use in-place encoding --- rust/chains/tw_polkadot/src/extrinsic.rs | 181 ++++++++++++----------- 1 file changed, 93 insertions(+), 88 deletions(-) diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs index 7223e4d8853..aa944e9ae29 100644 --- a/rust/chains/tw_polkadot/src/extrinsic.rs +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::convert::identity; use std::iter::{repeat, Iterator}; use std::str::FromStr; @@ -23,7 +24,7 @@ use tw_proto::Polkadot::Proto::mod_Staking::{ use tw_proto::Polkadot::Proto::{Balance, PolymeshCall, Staking}; use tw_ss58_address::{NetworkId, SS58Address}; -use crate::scale::{Compact, ToScale}; +use crate::scale::{Compact, Raw, ToScale}; const POLKADOT_MULTI_ADDRESS_SPEC: u32 = 28; const KUSAMA_MULTI_ADDRESS_SPEC: u32 = 2028; @@ -44,14 +45,15 @@ const ASSETS_TRANSFER: &str = "Assets.transfer"; const JOIN_IDENTITY_AS_KEY: &str = "Identity.join_identity_as_key"; const IDENTITY_ADD_AUTHORIZATION: &str = "Identity.add_authorization"; -type CallIndicesTable = HashMap<&'static str, Vec>; +type CallIndex = (u8, u8); +type CallIndicesTable = HashMap<&'static str, CallIndex>; macro_rules! call_indices { - ($($chain:expr => { $($name:expr => [$($value:expr),*] $(,)?)* } $(,)? )*) => { + ($($chain:expr => { $($name:expr => ($($value:expr),*) $(,)?)* } $(,)? )*) => { [ $(( $chain, std::collections::HashMap::from_iter( - [$(($name, vec![$($value as u8),+])),+] + [$(($name, ($($value as u8),+))),+] ) )),+ ].into_iter().collect() @@ -61,26 +63,26 @@ macro_rules! call_indices { lazy_static! { static ref CALL_INDICES_BY_NETWORK: HashMap = call_indices! { NetworkId::POLKADOT => { - BALANCE_TRANSFER => [0x05, 0x00], - STAKING_BOND => [0x07, 0x00], - STAKING_BOND_EXTRA => [0x07, 0x01], - STAKING_CHILL => [0x07, 0x06], - STAKING_NOMINATE => [0x07, 0x05], - STAKING_REBOND => [0x07, 0x13], - STAKING_UNBOND => [0x07, 0x02], - STAKING_WITHDRAW_UNBONDED => [0x07, 0x03], - UTILITY_BATCH => [0x1a, 0x02], + BALANCE_TRANSFER => (0x05, 0x00), + STAKING_BOND => (0x07, 0x00), + STAKING_BOND_EXTRA => (0x07, 0x01), + STAKING_CHILL => (0x07, 0x06), + STAKING_NOMINATE => (0x07, 0x05), + STAKING_REBOND => (0x07, 0x13), + STAKING_UNBOND => (0x07, 0x02), + STAKING_WITHDRAW_UNBONDED => (0x07, 0x03), + UTILITY_BATCH => (0x1a, 0x02), }, NetworkId::KUSAMA => { - BALANCE_TRANSFER => [0x04, 0x00], - STAKING_BOND => [0x06, 0x00], - STAKING_BOND_EXTRA => [0x06, 0x01], - STAKING_CHILL => [0x06, 0x06], - STAKING_NOMINATE => [0x06, 0x05], - STAKING_REBOND => [0x06, 0x13], - STAKING_UNBOND => [0x06, 0x02], - STAKING_WITHDRAW_UNBONDED => [0x06, 0x03], - UTILITY_BATCH => [0x18, 0x02], + BALANCE_TRANSFER => (0x04, 0x00), + STAKING_BOND => (0x06, 0x00), + STAKING_BOND_EXTRA => (0x06, 0x01), + STAKING_CHILL => (0x06, 0x06), + STAKING_NOMINATE => (0x06, 0x05), + STAKING_REBOND => (0x06, 0x13), + STAKING_UNBOND => (0x06, 0x02), + STAKING_WITHDRAW_UNBONDED => (0x06, 0x03), + UTILITY_BATCH => (0x18, 0x02), } }; } @@ -96,6 +98,12 @@ pub enum EncodeError { type EncodeResult = Result; +impl ToScale for SS58Address { + fn to_scale_into(&self, out: &mut Vec) { + Raw(self.key_bytes()).to_scale_into(out) + } +} + // `Extrinsic` is (for now) just a lightweight wrapper over the actual protobuf object. // In the future, we will refine the latter to let the caller specify arbitrary extrinsics. @@ -109,7 +117,7 @@ impl<'a> Extrinsic<'a> { Self { inner: input } } - fn get_call_index_for_network(network: NetworkId, key: &str) -> EncodeResult> { + fn get_call_index_for_network(network: NetworkId, key: &str) -> EncodeResult<(u8, u8)> { CALL_INDICES_BY_NETWORK .get(&network) .and_then(|table| table.get(key)) @@ -117,12 +125,12 @@ impl<'a> Extrinsic<'a> { .ok_or(EncodeError::MissingCallIndicesTable) } - fn get_custom_call_index(civ: &Option) -> EncodeResult> { + fn get_custom_call_index(civ: &Option) -> EncodeResult<(u8, u8)> { if let Some(CallIndicesVariant::custom(c)) = civ.as_ref().map(|i| &i.variant) { if c.module_index > 0xff || c.method_index > 0xff { return Err(EncodeError::InvalidCallIndex); } - return Ok(vec![c.module_index as u8, c.method_index as u8]); + return Ok((c.module_index as u8, c.method_index as u8)); } Err(EncodeError::MissingCallIndicesTable) } @@ -131,7 +139,7 @@ impl<'a> Extrinsic<'a> { &self, key: &str, civ: &Option, - ) -> EncodeResult> { + ) -> EncodeResult<(u8, u8)> { Self::get_custom_call_index(civ).or_else(|_| { let network = NetworkId::try_from(self.inner.network as u16) .map_err(|_| EncodeError::InvalidNetworkId)?; @@ -157,36 +165,26 @@ impl<'a> Extrinsic<'a> { } } - fn encode_account_id(key: &[u8], raw: bool) -> Vec { - let mut data = Vec::with_capacity(key.len() + 1); - - if !raw { - data.push(0x00); - } - data.extend(key); - data - } - fn encode_transfer(&self, t: &Transfer) -> EncodeResult> { let mut data = Vec::new(); // Encode call index let call_index = self.get_custom_call_index_or_network(BALANCE_TRANSFER, &t.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode destination account ID, TODO: check address network ? let address = SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; - data.extend(Self::encode_account_id( - address.key_bytes(), - self.should_encode_raw_account(), - )); + if !self.should_encode_raw_account() { + data.push(0x00); + } + address.to_scale_into(&mut data); // Encode value let value = U256::from_little_endian_slice(&t.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); // Encode memo if present, padding it to 32 bytes if !t.memo.is_empty() { @@ -206,25 +204,25 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(ASSETS_TRANSFER, &at.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode asset ID if not native token if at.asset_id != 0 { - data.extend(Compact(at.asset_id).to_scale()); + Compact(at.asset_id).to_scale_into(&mut data); } // Encode destination account ID, TODO: check address network ? let address = SS58Address::from_str(&at.to_address).map_err(|_| EncodeError::InvalidAddress)?; - data.extend(Self::encode_account_id( - address.key_bytes(), - self.should_encode_raw_account(), - )); + if !self.should_encode_raw_account() { + data.push(0x00); + } + address.to_scale_into(&mut data); // Encode value let value = U256::from_little_endian_slice(&at.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); Ok(data) } @@ -235,8 +233,13 @@ impl<'a> Extrinsic<'a> { call_indices: &Option, ) -> EncodeResult> { let mut data = Vec::new(); - data.extend(self.get_custom_call_index_or_network(UTILITY_BATCH, call_indices)?); - data.extend(Compact(encoded_calls.len()).to_scale()); + + // Encode call index + let call_index = self.get_custom_call_index_or_network(UTILITY_BATCH, call_indices)?; + call_index.to_scale_into(&mut data); + + // Encode batched calls + Compact(encoded_calls.len()).to_scale_into(&mut data); for c in encoded_calls { data.extend(c); } @@ -278,25 +281,25 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_BOND, &b.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode controller account ID, TODO: check address network ? if !b.controller.is_empty() { let address = SS58Address::from_str(&b.controller).map_err(|_| EncodeError::InvalidAddress)?; - data.extend(Self::encode_account_id( - address.key_bytes(), - self.should_encode_raw_account(), - )); + if !self.should_encode_raw_account() { + data.push(0x00); + } + address.to_scale_into(&mut data); } // Encode value let value = U256::from_little_endian_slice(&b.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); // Encode reward destination - data.extend((b.reward_destination as u8).to_scale()); + (b.reward_destination as u8).to_scale_into(&mut data); Ok(data) } @@ -330,12 +333,12 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_BOND_EXTRA, &be.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode value let value = U256::from_little_endian_slice(&be.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); Ok(data) } @@ -345,12 +348,12 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_UNBOND, &u.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode value let value = U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); Ok(data) } @@ -360,12 +363,12 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_REBOND, &u.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode value let value = U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; - data.extend(Compact(value).to_scale()); + Compact(value).to_scale_into(&mut data); Ok(data) } @@ -376,10 +379,10 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_WITHDRAW_UNBONDED, &wu.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode slashing spans as fixed-width u32 - data.extend((wu.slashing_spans as u32).to_scale()); + (wu.slashing_spans as u32).to_scale_into(&mut data); Ok(data) } @@ -390,20 +393,22 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_NOMINATE, &n.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode account IDs for nominators + Compact(n.nominators.len()).to_scale_into(&mut data); let raw = self.should_encode_raw_account(); - let addresses = n - .nominators + n.nominators .iter() - .map(|s| SS58Address::from_str(s).map_err(|_| EncodeError::InvalidAddress)) - .map(|a| a.map(|a| Self::encode_account_id(a.key_bytes(), raw))) - .collect::>>()?; - - for addr in addresses { - data.extend(addr); - } + .map(|s| { + let addr = SS58Address::from_str(s).map_err(|_| EncodeError::InvalidAddress)?; + if !raw { + data.push(0x00); + } + addr.to_scale_into(&mut data); + Ok(()) + }) + .try_for_each(identity)?; Ok(data) } @@ -412,7 +417,7 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_CHILL, &c.call_indices)?; - Ok(call_index) + Ok(call_index.to_scale()) } fn encode_staking_chill_and_unbond(&self, cau: &ChillAndUnbond) -> EncodeResult> { @@ -454,10 +459,10 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(JOIN_IDENTITY_AS_KEY, &j.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode auth ID - data.extend(j.auth_id.to_scale()); + j.auth_id.to_scale_into(&mut data); Ok(data) } @@ -468,12 +473,12 @@ impl<'a> Extrinsic<'a> { // Encode call index let call_index = self.get_custom_call_index_or_network(IDENTITY_ADD_AUTHORIZATION, &a.call_indices)?; - data.extend(call_index); + call_index.to_scale_into(&mut data); // Encode target data.push(0x01); let address = SS58Address::from_str(&a.target).map_err(|_| EncodeError::InvalidAddress)?; - data.extend(Self::encode_account_id(address.key_bytes(), true)); + address.to_scale_into(&mut data); // Encode join identity data.push(0x05); @@ -481,32 +486,32 @@ impl<'a> Extrinsic<'a> { if let Some(auth_data) = &a.data { if let Some(asset) = &auth_data.asset { data.push(0x01); - data.extend_from_slice(&asset.data); + Raw(&asset.data).to_scale_into(&mut data); } else { data.push(0x00); } if let Some(extrinsic) = &auth_data.extrinsic { data.push(0x01); - data.extend_from_slice(&extrinsic.data); + Raw(&extrinsic.data).to_scale_into(&mut data); } else { data.push(0x00); } if let Some(portfolio) = &auth_data.portfolio { data.push(0x01); - data.extend_from_slice(&portfolio.data); + Raw(&portfolio.data).to_scale_into(&mut data); } else { data.push(0x00); } } else { // Mark everything as authorized (asset, extrinsic, portfolio) - data.extend(&[0x01, 0x00]); - data.extend(&[0x01, 0x00]); - data.extend(&[0x01, 0x00]); + (0x01u8, 0x00u8).to_scale_into(&mut data); + (0x01u8, 0x00u8).to_scale_into(&mut data); + (0x01u8, 0x00u8).to_scale_into(&mut data); } - data.extend(Compact(a.expiry).to_scale()); + Compact(a.expiry).to_scale_into(&mut data); Ok(data) } From 33ff36ca442978b1af9d2a0467c729deb65fcb71 Mon Sep 17 00:00:00 2001 From: doom Date: Mon, 3 Jun 2024 09:52:39 +0200 Subject: [PATCH 17/18] Add more tests for staking extrinsics --- rust/chains/tw_polkadot/tests/extrinsic.rs | 169 +++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/rust/chains/tw_polkadot/tests/extrinsic.rs b/rust/chains/tw_polkadot/tests/extrinsic.rs index 12b07f9a52e..7b2c45e4832 100644 --- a/rust/chains/tw_polkadot/tests/extrinsic.rs +++ b/rust/chains/tw_polkadot/tests/extrinsic.rs @@ -7,6 +7,9 @@ use tw_polkadot::extrinsic::Extrinsic; use tw_proto::Polkadot::Proto; use tw_proto::Polkadot::Proto::mod_Balance::{AssetTransfer, BatchAssetTransfer, Transfer}; use tw_proto::Polkadot::Proto::mod_Identity::mod_AddAuthorization::{AuthData, Data}; +use tw_proto::Polkadot::Proto::mod_Staking::{ + Bond, BondExtra, Chill, Nominate, Rebond, Unbond, WithdrawUnbonded, +}; #[test] fn polymesh_encode_transfer_with_memo() { @@ -318,3 +321,169 @@ fn kusama_encode_asset_transfer_without_call_indices() { let extrinsic = Extrinsic::from_input(input); extrinsic.encode_call().expect_err("unexpected success"); } + +#[test] +fn encode_staking_nominate() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::nominate(Nominate { + nominators: vec![ + "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), + ], + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "0705080081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d", + ); +} + +#[test] +fn encode_staking_chill() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::chill(Chill { + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "0706"); +} + +#[test] +fn encode_staking_bond_with_controller() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + value: U256::from(808081u64).to_little_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::CONTROLLER, + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "07000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" + ); +} + +#[test] +fn encode_staking_bond() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: Default::default(), + value: U256::from(808081u64).to_little_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::STAKED, + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "07004652310000"); +} + +#[test] +fn encode_staking_bond_extra() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::bond_extra(BondExtra { + value: U256::from(808081u64).to_little_endian().to_vec().into(), + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "070146523100"); +} + +#[test] +fn encode_staking_rebond() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::rebond(Rebond { + value: U256::from(808081u64).to_little_endian().to_vec().into(), + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "071346523100"); +} + +#[test] +fn encode_staking_unbond() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::unbond(Unbond { + value: U256::from(808081u64).to_little_endian().to_vec().into(), + call_indices: None, + }), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "070246523100"); +} + +#[test] +fn encode_staking_withdraw_unbonded() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: Proto::mod_Staking::OneOfmessage_oneof::withdraw_unbonded( + WithdrawUnbonded { + slashing_spans: 84, + call_indices: None, + }, + ), + }), + ..Default::default() + }; + + let extrinsic = Extrinsic::from_input(input); + let encoded = extrinsic.encode_call().expect("error encoding call"); + assert_eq!(encoded.to_hex(), "070354000000"); +} From efc475e819302625216525c6458756713c6e0d47 Mon Sep 17 00:00:00 2001 From: doom Date: Mon, 3 Jun 2024 12:07:14 +0200 Subject: [PATCH 18/18] Introduce ExtrinsicEncoder helper to build encoded extrinsics --- rust/chains/tw_polkadot/src/extrinsic.rs | 172 ++++++++++++----------- rust/chains/tw_polkadot/src/scale.rs | 49 +++++-- 2 files changed, 128 insertions(+), 93 deletions(-) diff --git a/rust/chains/tw_polkadot/src/extrinsic.rs b/rust/chains/tw_polkadot/src/extrinsic.rs index aa944e9ae29..0f77a31f3ac 100644 --- a/rust/chains/tw_polkadot/src/extrinsic.rs +++ b/rust/chains/tw_polkadot/src/extrinsic.rs @@ -24,7 +24,7 @@ use tw_proto::Polkadot::Proto::mod_Staking::{ use tw_proto::Polkadot::Proto::{Balance, PolymeshCall, Staking}; use tw_ss58_address::{NetworkId, SS58Address}; -use crate::scale::{Compact, Raw, ToScale}; +use crate::scale::{Compact, Raw, RawIter, ToScale}; const POLKADOT_MULTI_ADDRESS_SPEC: u32 = 28; const KUSAMA_MULTI_ADDRESS_SPEC: u32 = 2028; @@ -104,6 +104,36 @@ impl ToScale for SS58Address { } } +#[derive(Debug, Clone, Default)] +pub struct ExtrinsicEncoder { + encoded: Vec, +} + +impl ExtrinsicEncoder { + pub fn new() -> Self { + Default::default() + } + + fn from_encoded(encoded: Vec) -> Self { + Self { encoded } + } + + pub fn add(self, t: impl ToScale) -> Self { + let mut encoded = self.encoded; + t.to_scale_into(&mut encoded); + Self::from_encoded(encoded) + } + + pub fn add_mut(&mut self, t: impl ToScale) -> &mut Self { + t.to_scale_into(&mut self.encoded); + self + } + + pub fn finalize(self) -> Vec { + self.encoded + } +} + // `Extrinsic` is (for now) just a lightweight wrapper over the actual protobuf object. // In the future, we will refine the latter to let the caller specify arbitrary extrinsics. @@ -166,63 +196,53 @@ impl<'a> Extrinsic<'a> { } fn encode_transfer(&self, t: &Transfer) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(BALANCE_TRANSFER, &t.call_indices)?; - call_index.to_scale_into(&mut data); - - // Encode destination account ID, TODO: check address network ? let address = SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; - if !self.should_encode_raw_account() { - data.push(0x00); - } - address.to_scale_into(&mut data); - - // Encode value let value = U256::from_little_endian_slice(&t.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); + + let mut encoder = ExtrinsicEncoder::new(); + encoder.add_mut(call_index); + if !self.should_encode_raw_account() { + encoder.add_mut(0x00u8); + } + encoder.add_mut(address); + encoder.add_mut(Compact(value)); // Encode memo if present, padding it to 32 bytes if !t.memo.is_empty() { - data.push(0x01); - data.extend(t.memo.as_bytes()); + encoder.add_mut(0x01u8); + encoder.add_mut(Raw(t.memo.as_bytes())); if t.memo.len() < 32 { - data.extend(repeat(0x00).take(32 - t.memo.len())); + encoder.add_mut(RawIter(repeat(0x00).take(32 - t.memo.len()))); } } - Ok(data) + Ok(encoder.finalize()) } fn encode_asset_transfer(&self, at: &AssetTransfer) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(ASSETS_TRANSFER, &at.call_indices)?; - call_index.to_scale_into(&mut data); + let address = + SS58Address::from_str(&at.to_address).map_err(|_| EncodeError::InvalidAddress)?; + let value = + U256::from_little_endian_slice(&at.value).map_err(|_| EncodeError::InvalidValue)?; + + let mut encoder = ExtrinsicEncoder::new().add(call_index); // Encode asset ID if not native token if at.asset_id != 0 { - Compact(at.asset_id).to_scale_into(&mut data); + encoder.add_mut(Compact(at.asset_id)); } - // Encode destination account ID, TODO: check address network ? - let address = - SS58Address::from_str(&at.to_address).map_err(|_| EncodeError::InvalidAddress)?; if !self.should_encode_raw_account() { - data.push(0x00); + encoder.add_mut(0x00u8); } - address.to_scale_into(&mut data); - // Encode value - let value = - U256::from_little_endian_slice(&at.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); + let data = encoder.add(address).add(Compact(value)).finalize(); Ok(data) } @@ -232,17 +252,16 @@ impl<'a> Extrinsic<'a> { encoded_calls: &[Vec], call_indices: &Option, ) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(UTILITY_BATCH, call_indices)?; - call_index.to_scale_into(&mut data); + let encoder = ExtrinsicEncoder::new() + .add(call_index) + .add(Compact(encoded_calls.len())); + + let data = encoded_calls + .iter() + .fold(encoder, |encoder, call| encoder.add(Raw(call))) + .finalize(); - // Encode batched calls - Compact(encoded_calls.len()).to_scale_into(&mut data); - for c in encoded_calls { - data.extend(c); - } Ok(data) } @@ -277,29 +296,26 @@ impl<'a> Extrinsic<'a> { } fn encode_staking_bond(&self, b: &Bond) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_BOND, &b.call_indices)?; - call_index.to_scale_into(&mut data); + let value = + U256::from_little_endian_slice(&b.value).map_err(|_| EncodeError::InvalidValue)?; + + let mut encoder = ExtrinsicEncoder::new().add(call_index); - // Encode controller account ID, TODO: check address network ? if !b.controller.is_empty() { + // TODO: check address network ? let address = SS58Address::from_str(&b.controller).map_err(|_| EncodeError::InvalidAddress)?; if !self.should_encode_raw_account() { - data.push(0x00); + encoder.add_mut(0x00u8); } - address.to_scale_into(&mut data); + encoder.add_mut(address); } - // Encode value - let value = - U256::from_little_endian_slice(&b.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); - - // Encode reward destination - (b.reward_destination as u8).to_scale_into(&mut data); + let data = encoder + .add(Compact(value)) + .add(b.reward_destination as u8) + .finalize(); Ok(data) } @@ -328,61 +344,53 @@ impl<'a> Extrinsic<'a> { } fn encode_staking_bond_extra(&self, be: &BondExtra) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_BOND_EXTRA, &be.call_indices)?; - call_index.to_scale_into(&mut data); - - // Encode value let value = U256::from_little_endian_slice(&be.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); + + let data = ExtrinsicEncoder::new() + .add(call_index) + .add(Compact(value)) + .finalize(); Ok(data) } fn encode_staking_unbond(&self, u: &Unbond) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_UNBOND, &u.call_indices)?; - call_index.to_scale_into(&mut data); - - // Encode value let value = U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); + + let data = ExtrinsicEncoder::new() + .add(call_index) + .add(Compact(value)) + .finalize(); Ok(data) } fn encode_staking_rebond(&self, u: &Rebond) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_REBOND, &u.call_indices)?; - call_index.to_scale_into(&mut data); - - // Encode value let value = U256::from_little_endian_slice(&u.value).map_err(|_| EncodeError::InvalidValue)?; - Compact(value).to_scale_into(&mut data); + + let data = ExtrinsicEncoder::new() + .add(call_index) + .add(Compact(value)) + .finalize(); Ok(data) } fn encode_staking_withdraw_unbonded(&self, wu: &WithdrawUnbonded) -> EncodeResult> { - let mut data = Vec::new(); - - // Encode call index let call_index = self.get_custom_call_index_or_network(STAKING_WITHDRAW_UNBONDED, &wu.call_indices)?; - call_index.to_scale_into(&mut data); - // Encode slashing spans as fixed-width u32 - (wu.slashing_spans as u32).to_scale_into(&mut data); + let data = ExtrinsicEncoder::new() + .add(call_index) + .add(wu.slashing_spans as u32) + .finalize(); Ok(data) } diff --git a/rust/chains/tw_polkadot/src/scale.rs b/rust/chains/tw_polkadot/src/scale.rs index 3b1c7b84c33..73ac658c01d 100644 --- a/rust/chains/tw_polkadot/src/scale.rs +++ b/rust/chains/tw_polkadot/src/scale.rs @@ -64,11 +64,11 @@ impl ToScale for Compact { 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale_into(out), 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => { ((self.0 << 2) | 0b10).to_scale_into(out) - } + }, _ => { out.push(0b11); self.0.to_scale_into(out); - } + }, } } } @@ -80,7 +80,7 @@ impl ToScale for Compact { 0..=0b0011_1111_1111_1111 => (((self.0 as u16) << 2) | 0b01).to_scale_into(out), 0..=0b0011_1111_1111_1111_1111_1111_1111_1111 => { (((self.0 as u32) << 2) | 0b10).to_scale_into(out) - } + }, _ => { let bytes_needed = 8 - self.0.leading_zeros() / 8; out.reserve(bytes_needed as usize); @@ -90,7 +90,7 @@ impl ToScale for Compact { out.push(x as u8); x >>= 8; } - } + }, } } } @@ -132,7 +132,10 @@ impl ToScale for Compact { } } -impl ToScale for Option where T: ToScale { +impl ToScale for Option +where + T: ToScale, +{ fn to_scale_into(&self, out: &mut Vec) { if let Some(t) = &self { t.to_scale_into(out); @@ -141,8 +144,8 @@ impl ToScale for Option where T: ToScale { } impl ToScale for &[T] - where - T: ToScale, +where + T: ToScale, { fn to_scale_into(&self, out: &mut Vec) { Compact(self.len()).to_scale_into(out); @@ -153,8 +156,8 @@ impl ToScale for &[T] } impl ToScale for Vec - where - T: ToScale, +where + T: ToScale, { fn to_scale_into(&self, out: &mut Vec) { self.as_slice().to_scale_into(out) @@ -184,7 +187,10 @@ tuple_impl!(T0, T1, T2, T3, T4, T5, T6, T7); pub struct FixedLength<'a, T>(pub &'a [T]); -impl<'a, T> ToScale for FixedLength<'a, T> where T: ToScale { +impl<'a, T> ToScale for FixedLength<'a, T> +where + T: ToScale, +{ fn to_scale_into(&self, out: &mut Vec) { for ts in self.0.iter() { ts.to_scale_into(out); @@ -200,9 +206,20 @@ impl<'a> ToScale for Raw<'a> { } } +pub struct RawIter(pub T); + +impl ToScale for RawIter +where + T: IntoIterator + Clone, +{ + fn to_scale_into(&self, out: &mut Vec) { + out.extend(self.0.clone()); + } +} + #[cfg(test)] mod tests { - use super::{Compact, FixedLength, Raw, ToScale}; + use super::{Compact, FixedLength, Raw, RawIter, ToScale}; use tw_number::U256; #[test] @@ -407,6 +424,16 @@ mod tests { ); } + #[test] + fn test_raw_iter() { + let empty: [u8; 0] = []; + assert_eq!(RawIter(empty.into_iter()).to_scale(), empty); + assert_eq!( + RawIter([4u8, 8, 15, 16, 23, 42].into_iter()).to_scale(), + &[0x04, 0x08, 0x0f, 0x10, 0x17, 0x2a], + ); + } + #[test] fn test_tuple() { assert_eq!((Compact(3u32), false).to_scale(), &[0x0c, 0x00]);