diff --git a/include/TrustWalletCore/TWSS58AddressType.h b/include/TrustWalletCore/TWSS58AddressType.h deleted file mode 100644 index 9f9010c670e..00000000000 --- a/include/TrustWalletCore/TWSS58AddressType.h +++ /dev/null @@ -1,21 +0,0 @@ - -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "TWBase.h" - -TW_EXTERN_C_BEGIN - -/// Substrate based chains Address Type -/// -/// \see https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)#address-type -TW_EXPORT_ENUM(uint8_t) -enum TWSS58AddressType { - TWSS58AddressTypePolkadot = 0, - TWSS58AddressTypeKusama = 2, -}; - -TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0c16a96d581..2c981aef1f8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1858,8 +1858,10 @@ dependencies = [ "tw_native_evmos", "tw_native_injective", "tw_pactus", + "tw_polkadot", "tw_ronin", "tw_solana", + "tw_substrate", "tw_sui", "tw_thorchain", "tw_ton", @@ -2093,6 +2095,22 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_polkadot" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", + "tw_proto", + "tw_scale", + "tw_ss58_address", + "tw_substrate", +] + [[package]] name = "tw_proto" version = "0.1.0" @@ -2115,6 +2133,14 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_scale" +version = "0.1.0" +dependencies = [ + "tw_hash", + "tw_number", +] + [[package]] name = "tw_solana" version = "0.1.0" @@ -2133,6 +2159,33 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_ss58_address" +version = "0.1.0" +dependencies = [ + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_scale", +] + +[[package]] +name = "tw_substrate" +version = "0.1.0" +dependencies = [ + "tw_coin_entry", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_number", + "tw_proto", + "tw_scale", + "tw_ss58_address", +] + [[package]] name = "tw_sui" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 692beec2692..75b9d7761ab 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "chains/tw_aptos", "chains/tw_binance", @@ -11,11 +12,13 @@ members = [ "chains/tw_native_evmos", "chains/tw_native_injective", "chains/tw_pactus", + "chains/tw_polkadot", "chains/tw_ronin", "chains/tw_solana", "chains/tw_sui", "chains/tw_thorchain", "chains/tw_ton", + "frameworks/tw_substrate", "frameworks/tw_ton_sdk", "frameworks/tw_utxo", "tw_any_coin", @@ -32,6 +35,8 @@ members = [ "tw_misc", "tw_number", "tw_proto", + "tw_scale", + "tw_ss58_address", "tw_tests", "wallet_core_bin", "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..7355cb67419 --- /dev/null +++ b/rust/chains/tw_polkadot/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_polkadot" +version = "0.1.0" +edition = "2021" + +[dependencies] +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" } +tw_number = { path = "../../tw_number" } +tw_proto = { path = "../../tw_proto" } +tw_scale = { path = "../../tw_scale" } +tw_ss58_address = { path = "../../tw_ss58_address" } +tw_substrate = { path = "../../frameworks/tw_substrate" } diff --git a/rust/chains/tw_polkadot/src/call_encoder/generic.rs b/rust/chains/tw_polkadot/src/call_encoder/generic.rs new file mode 100644 index 00000000000..9eff4ee52ba --- /dev/null +++ b/rust/chains/tw_polkadot/src/call_encoder/generic.rs @@ -0,0 +1,239 @@ +use std::str::FromStr; + +use tw_coin_entry::error::prelude::*; +use tw_number::U256; +use tw_proto::Polkadot::Proto::{ + mod_Balance::{AssetTransfer, OneOfmessage_oneof as BalanceVariant, Transfer}, + mod_Staking::{ + Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, + WithdrawUnbonded, + }, + Balance, RewardDestination as TWRewardDestination, Staking, +}; +use tw_scale::{impl_enum_scale, Compact, RawOwned, ToScale}; +use tw_ss58_address::SS58Address; +use tw_substrate::*; + +use super::{required_call_index, validate_call_index}; + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum GenericBalances { + TransferAllowDeath { + dest: MultiAddress, + value: Compact, + } = 0x00, + AssetTransfer { + id: Compact, + target: MultiAddress, + amount: Compact, + } = 0x05, + } +); + +impl GenericBalances { + fn encode_transfer(ctx: &SubstrateContext, t: &Transfer) -> WithCallIndexResult { + let ci = validate_call_index(&t.call_indices)?; + let address = + SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; + let value = U256::from_big_endian_slice(&t.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::TransferAllowDeath { + dest: ctx.multi_address(address.into()), + value: Compact(value), + })) + } + + fn encode_asset_transfer( + ctx: &SubstrateContext, + t: &AssetTransfer, + ) -> WithCallIndexResult { + let ci = required_call_index(&t.call_indices)?; + let address = + SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; + let amount = U256::from_big_endian_slice(&t.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + let asset_id = t.asset_id; + if asset_id > 0 { + Ok(ci.wrap(Self::AssetTransfer { + id: Compact(asset_id), + target: ctx.multi_address(address.into()), + amount: Compact(amount), + })) + } else { + Ok(ci.wrap(Self::TransferAllowDeath { + dest: ctx.multi_address(address.into()), + value: Compact(amount), + })) + } + } + + pub fn encode_call(ctx: &SubstrateContext, b: &Balance) -> WithCallIndexResult { + match &b.message_oneof { + BalanceVariant::transfer(t) => Self::encode_transfer(ctx, t), + BalanceVariant::asset_transfer(t) => Self::encode_asset_transfer(ctx, t), + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported batched balance variants here (maybe nested batch calls?)"), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum RewardDestination { + Staked = 0x00, + Stash = 0x01, + Controller = 0x02, + None = 0x04, + } +); + +impl RewardDestination { + pub fn from_tw(dest: &TWRewardDestination) -> EncodeResult { + match dest { + TWRewardDestination::STAKED => Ok(Self::Staked), + TWRewardDestination::STASH => Ok(Self::Stash), + TWRewardDestination::CONTROLLER => Ok(Self::Controller), + } + } +} + +#[derive(Clone, Debug)] +pub struct BondCall { + controller: Option, + value: Compact, + reward: RewardDestination, +} + +impl ToScale for BondCall { + fn to_scale_into(&self, out: &mut Vec) { + if let Some(controller) = &self.controller { + controller.to_scale_into(out); + } + self.value.to_scale_into(out); + self.reward.to_scale_into(out); + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum GenericStaking { + Bond(BondCall) = 0x00, + BondExtra { max_additional: Compact } = 0x01, + Unbond { value: Compact } = 0x02, + WithdrawUnbonded { num_slashing_spans: u32 } = 0x03, + Nominate { targets: Vec } = 0x05, + Chill = 0x06, + Rebond { value: Compact } = 0x13, + } +); + +impl GenericStaking { + fn encode_bond(ctx: &SubstrateContext, b: &Bond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let controller = SS58Address::from_str(&b.controller) + .map(|addr| ctx.multi_address(addr.into())) + .ok(); + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::Bond(BondCall { + controller, + value: Compact(value), + reward: RewardDestination::from_tw(&b.reward_destination)?, + }))) + } + + fn encode_bond_extra(b: &BondExtra) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::BondExtra { + max_additional: Compact(value), + })) + } + + fn encode_chill(c: &Chill) -> WithCallIndexResult { + let ci = validate_call_index(&c.call_indices)?; + Ok(ci.wrap(Self::Chill)) + } + + fn encode_unbond(b: &Unbond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::Unbond { + value: Compact(value), + })) + } + + fn encode_rebond(b: &Rebond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::Rebond { + value: Compact(value), + })) + } + + fn encode_withdraw_unbonded(b: &WithdrawUnbonded) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + Ok(ci.wrap(Self::WithdrawUnbonded { + num_slashing_spans: b.slashing_spans as u32, + })) + } + + fn encode_nominate(ctx: &SubstrateContext, b: &Nominate) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let targets = b + .nominators + .iter() + .map(|target| { + let account = + SS58Address::from_str(target).map_err(|_| EncodeError::InvalidAddress)?; + Ok(ctx.multi_address(account.into())) + }) + .collect::>>()?; + Ok(ci.wrap(Self::Nominate { targets })) + } + + pub fn encode_call(ctx: &SubstrateContext, s: &Staking) -> WithCallIndexResult { + match &s.message_oneof { + StakingVariant::bond(b) => Self::encode_bond(ctx, b), + StakingVariant::bond_extra(b) => Self::encode_bond_extra(b), + StakingVariant::chill(b) => Self::encode_chill(b), + StakingVariant::unbond(b) => Self::encode_unbond(b), + StakingVariant::withdraw_unbonded(b) => Self::encode_withdraw_unbonded(b), + StakingVariant::rebond(b) => Self::encode_rebond(b), + StakingVariant::nominate(b) => Self::encode_nominate(ctx, b), + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported batched staking variants here (maybe nested batch calls?)"), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum GenericUtility { + BatchAll { calls: Vec } = 0x02, + } +); diff --git a/rust/chains/tw_polkadot/src/call_encoder/mod.rs b/rust/chains/tw_polkadot/src/call_encoder/mod.rs new file mode 100644 index 00000000000..db5cb90c2b7 --- /dev/null +++ b/rust/chains/tw_polkadot/src/call_encoder/mod.rs @@ -0,0 +1,198 @@ +use crate::{ctx_from_tw, KUSAMA, POLKADOT, POLYMESH}; +use tw_proto::Polkadot::Proto::{ + self, + mod_Balance::{BatchAssetTransfer, BatchTransfer, OneOfmessage_oneof as BalanceVariant}, + mod_CallIndices::OneOfvariant as CallIndicesVariant, + mod_SigningInput::OneOfmessage_oneof as SigningVariant, + mod_Staking::{ + Bond, BondAndNominate, Chill, ChillAndUnbond, Nominate, + OneOfmessage_oneof as StakingVariant, Unbond, + }, + Balance, CallIndices, Staking, +}; +use tw_scale::{RawOwned, ToScale}; +use tw_substrate::*; + +pub mod generic; +use generic::*; + +pub mod polkadot; +use polkadot::*; + +pub mod polymesh; +use polymesh::*; + +pub fn validate_call_index(call_index: &Option) -> EncodeResult { + let index = match call_index { + Some(CallIndices { + variant: CallIndicesVariant::custom(c), + }) => Some((c.module_index, c.method_index)), + _ => None, + }; + CallIndex::from_tw(index) +} + +pub fn required_call_index(call_index: &Option) -> EncodeResult { + let index = match call_index { + Some(CallIndices { + variant: CallIndicesVariant::custom(c), + }) => Some((c.module_index, c.method_index)), + _ => None, + }; + CallIndex::required_from_tw(index) +} + +pub trait TWPolkadotCallEncoder { + fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult; + fn encode_batch(&self, calls: Vec) -> EncodeResult; +} + +pub struct CallEncoder { + encoder: Box, +} + +impl CallEncoder { + pub fn from_ctx(ctx: &SubstrateContext) -> EncodeResult { + let encoder = match ctx.network { + POLKADOT => PolkadotCallEncoder::new_boxed(ctx), + KUSAMA => KusamaCallEncoder::new_boxed(ctx), + POLYMESH => PolymeshCallEncoder::new_boxed(ctx), + _ => PolkadotCallEncoder::new_boxed(ctx), + }; + Ok(Self { encoder }) + } + + pub fn encode_input(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let encoder = Self::from_ctx(&ctx)?; + encoder.encode_call(&input.message_oneof) + } + + fn encode_batch_transfer(&self, bt: &BatchTransfer) -> EncodeResult { + let transfers = bt + .transfers + .iter() + .map(|t| { + let call = SigningVariant::balance_call(Proto::Balance { + message_oneof: BalanceVariant::transfer(t.clone()), + }); + self.encode_call(&call) + }) + .collect::>>()?; + + self.encode_batch(transfers, &bt.call_indices) + } + + fn encode_batch_asset_transfer(&self, bat: &BatchAssetTransfer) -> EncodeResult { + let transfers = bat + .transfers + .iter() + .map(|t| { + let call = SigningVariant::balance_call(Proto::Balance { + message_oneof: BalanceVariant::asset_transfer(t.clone()), + }); + self.encode_call(&call) + }) + .collect::>>()?; + + self.encode_batch(transfers, &bat.call_indices) + } + + fn encode_balance_batch_call(&self, b: &Balance) -> EncodeResult> { + match &b.message_oneof { + BalanceVariant::batchTransfer(bt) => { + let batch = self.encode_batch_transfer(bt)?; + Ok(Some(batch)) + }, + BalanceVariant::batch_asset_transfer(bat) => { + let batch = self.encode_batch_asset_transfer(bat)?; + Ok(Some(batch)) + }, + _ => Ok(None), + } + } + + fn encode_staking_bond_and_nominate(&self, ban: &BondAndNominate) -> EncodeResult { + // Encode a bond call + let first = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::bond(Bond { + controller: ban.controller.clone(), + value: ban.value.clone(), + reward_destination: ban.reward_destination, + call_indices: ban.bond_call_indices.clone(), + }), + }))?; + + // Encode a nominate call + let second = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::nominate(Nominate { + nominators: ban.nominators.clone(), + call_indices: ban.nominate_call_indices.clone(), + }), + }))?; + + // Encode both calls as batched + self.encode_batch(vec![first, second], &ban.call_indices) + } + + fn encode_staking_chill_and_unbond(&self, cau: &ChillAndUnbond) -> EncodeResult { + let first = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::chill(Chill { + call_indices: cau.chill_call_indices.clone(), + }), + }))?; + + let second = self.encode_call(&SigningVariant::staking_call(Proto::Staking { + message_oneof: StakingVariant::unbond(Unbond { + value: cau.value.clone(), + call_indices: cau.unbond_call_indices.clone(), + }), + }))?; + + // Encode both calls as batched + self.encode_batch(vec![first, second], &cau.call_indices) + } + + fn encode_staking_batch_call(&self, s: &Staking) -> EncodeResult> { + match &s.message_oneof { + StakingVariant::bond_and_nominate(ban) => { + let batch = self.encode_staking_bond_and_nominate(ban)?; + Ok(Some(batch)) + }, + StakingVariant::chill_and_unbond(cau) => { + let batch = self.encode_staking_chill_and_unbond(cau)?; + Ok(Some(batch)) + }, + _ => Ok(None), + } + } + + pub fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult { + // Special case for batches. + match msg { + SigningVariant::balance_call(b) => { + if let Some(batch) = self.encode_balance_batch_call(b)? { + return Ok(batch); + } + }, + SigningVariant::staking_call(s) => { + if let Some(batch) = self.encode_staking_batch_call(s)? { + return Ok(batch); + } + }, + _ => (), + } + // non-batch calls. + self.encoder.encode_call(msg) + } + + fn encode_batch( + &self, + calls: Vec, + ci: &Option, + ) -> EncodeResult { + let ci = validate_call_index(ci)?; + let call = ci.wrap(self.encoder.encode_batch(calls)?); + Ok(RawOwned(call.to_scale())) + } +} diff --git a/rust/chains/tw_polkadot/src/call_encoder/polkadot.rs b/rust/chains/tw_polkadot/src/call_encoder/polkadot.rs new file mode 100644 index 00000000000..a40fcd09456 --- /dev/null +++ b/rust/chains/tw_polkadot/src/call_encoder/polkadot.rs @@ -0,0 +1,88 @@ +use tw_scale::{impl_enum_scale, RawOwned}; + +use tw_coin_entry::error::prelude::*; +use tw_proto::Polkadot::Proto::mod_SigningInput::OneOfmessage_oneof as SigningVariant; + +use super::*; + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolkadotCall { + Balances(GenericBalances) = 0x05, + Staking(GenericStaking) = 0x07, + Utility(GenericUtility) = 0x1a, + } +); + +pub struct PolkadotCallEncoder(SubstrateContext); + +impl PolkadotCallEncoder { + pub fn new_boxed(ctx: &SubstrateContext) -> Box { + Box::new(Self(ctx.clone())) + } +} + +impl TWPolkadotCallEncoder for PolkadotCallEncoder { + fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult { + let call = match msg { + SigningVariant::balance_call(b) => { + GenericBalances::encode_call(&self.0, b)?.map(PolkadotCall::Balances) + }, + SigningVariant::staking_call(s) => { + GenericStaking::encode_call(&self.0, s)?.map(PolkadotCall::Staking) + }, + _ => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported call variant."); + }, + }; + Ok(RawOwned(call.to_scale())) + } + + fn encode_batch(&self, calls: Vec) -> EncodeResult { + let call = PolkadotCall::Utility(GenericUtility::BatchAll { calls }); + Ok(RawOwned(call.to_scale())) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum KusamaCall { + Balances(GenericBalances) = 0x04, + Staking(GenericStaking) = 0x06, + Utility(GenericUtility) = 0x18, + } +); + +pub struct KusamaCallEncoder(SubstrateContext); + +impl KusamaCallEncoder { + pub fn new_boxed(ctx: &SubstrateContext) -> Box { + Box::new(Self(ctx.clone())) + } +} + +impl TWPolkadotCallEncoder for KusamaCallEncoder { + fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult { + let call = match msg { + SigningVariant::balance_call(b) => { + GenericBalances::encode_call(&self.0, b)?.map(KusamaCall::Balances) + }, + SigningVariant::staking_call(s) => { + GenericStaking::encode_call(&self.0, s)?.map(KusamaCall::Staking) + }, + _ => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported call variant."); + }, + }; + Ok(RawOwned(call.to_scale())) + } + + fn encode_batch(&self, calls: Vec) -> EncodeResult { + let call = KusamaCall::Utility(GenericUtility::BatchAll { calls }); + Ok(RawOwned(call.to_scale())) + } +} diff --git a/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs b/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs new file mode 100644 index 00000000000..dde4a047308 --- /dev/null +++ b/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs @@ -0,0 +1,360 @@ +use std::str::FromStr; + +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_number::U256; +use tw_proto::Polkadot::Proto::{ + mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, + mod_Identity::{AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant}, + mod_PolymeshCall::OneOfmessage_oneof as PolymeshVariant, + mod_Staking::{ + Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, + WithdrawUnbonded, + }, + Balance, Identity, Staking, +}; +use tw_scale::{impl_enum_scale, impl_struct_scale, Compact, RawOwned, ToScale}; +use tw_ss58_address::SS58Address; +use tw_substrate::address::SubstrateAddress; + +use super::*; + +impl_struct_scale!( + #[derive(Clone, Debug)] + pub struct Memo(H256); +); + +impl Memo { + pub fn new(memo: &str) -> Self { + let memo = memo.as_bytes(); + let mut bytes = [0; 32]; + let len = memo.len().min(32); + bytes[0..len].copy_from_slice(&memo[0..len]); + + Self(bytes.into()) + } +} + +impl_struct_scale!( + #[derive(Clone, Debug)] + pub struct IdentityId(H256); +); + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshBalances { + Transfer { + dest: MultiAddress, + value: Compact, + } = 0x00, + TransferWithMemo { + dest: MultiAddress, + value: Compact, + memo: Option, + } = 0x01, + } +); + +impl PolymeshBalances { + fn encode_transfer(t: &Transfer) -> WithCallIndexResult { + let ci = validate_call_index(&t.call_indices)?; + let address = + SS58Address::from_str(&t.to_address).map_err(|_| EncodeError::InvalidAddress)?; + let value = U256::from_big_endian_slice(&t.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + if !t.memo.is_empty() { + Ok(ci.wrap(Self::TransferWithMemo { + dest: address.into(), + value: Compact(value), + memo: Some(Memo::new(&t.memo)), + })) + } else { + Ok(ci.wrap(Self::Transfer { + dest: address.into(), + value: Compact(value), + })) + } + } + + pub fn encode_call(b: &Balance) -> WithCallIndexResult { + match &b.message_oneof { + BalanceVariant::transfer(t) => Self::encode_transfer(t), + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported balance call"), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum Signatory { + Identity(IdentityId) = 0x00, + Account(AccountId) = 0x01, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum AuthorizationData { + JoinIdentity { + // TODO: Polymesh permissions. + permissions: RawOwned, + } = 0x05, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshIdentity { + JoinIdentity { + auth_id: u64, + } = 0x04, + AddAuthorization { + target: Signatory, + data: AuthorizationData, + expiry: Option, + } = 0x0a, + } +); + +impl PolymeshIdentity { + fn encode_join_identity(join: &JoinIdentityAsKey) -> WithCallIndexResult { + let ci = validate_call_index(&join.call_indices)?; + Ok(ci.wrap(Self::JoinIdentity { + auth_id: join.auth_id, + })) + } + + fn encode_add_authorization(auth: &AddAuthorization) -> WithCallIndexResult { + let ci = validate_call_index(&auth.call_indices)?; + let target = + SS58Address::from_str(&auth.target).map_err(|_| EncodeError::InvalidAddress)?; + let mut data = Vec::new(); + if let Some(auth_data) = &auth.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.push(0x00); + data.push(0x00); + data.push(0x00); + } + Ok(ci.wrap(Self::AddAuthorization { + target: Signatory::Account(SubstrateAddress(target)), + data: AuthorizationData::JoinIdentity { + permissions: RawOwned(data), + }, + expiry: if auth.expiry > 0 { + Some(auth.expiry) + } else { + None + }, + })) + } + + pub fn encode_call(ident: &Identity) -> WithCallIndexResult { + match &ident.message_oneof { + IdentityVariant::join_identity_as_key(t) => Self::encode_join_identity(t), + IdentityVariant::add_authorization(a) => Self::encode_add_authorization(a), + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported identity call"), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshStaking { + Bond { + controller: MultiAddress, + value: Compact, + reward: RewardDestination, + } = 0x00, + BondExtra { + max_additional: Compact, + } = 0x01, + Unbond { + value: Compact, + } = 0x02, + WithdrawUnbonded { + num_slashing_spans: u32, + } = 0x03, + Nominate { + targets: Vec, + } = 0x05, + Chill = 0x06, + Rebond { + value: Compact, + } = 0x18, + } +); + +impl PolymeshStaking { + fn encode_bond(b: &Bond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + let controller = + SS58Address::from_str(&b.controller).map_err(|_| EncodeError::InvalidAddress)?; + + Ok(ci.wrap(Self::Bond { + controller: controller.into(), + value: Compact(value), + reward: RewardDestination::from_tw(&b.reward_destination)?, + })) + } + + fn encode_bond_extra(b: &BondExtra) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::BondExtra { + max_additional: Compact(value), + })) + } + + fn encode_chill(c: &Chill) -> WithCallIndexResult { + let ci = validate_call_index(&c.call_indices)?; + Ok(ci.wrap(Self::Chill)) + } + + fn encode_unbond(b: &Unbond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::Unbond { + value: Compact(value), + })) + } + + fn encode_rebond(b: &Rebond) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let value = U256::from_big_endian_slice(&b.value) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + Ok(ci.wrap(Self::Rebond { + value: Compact(value), + })) + } + + fn encode_withdraw_unbonded(b: &WithdrawUnbonded) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + Ok(ci.wrap(Self::WithdrawUnbonded { + num_slashing_spans: b.slashing_spans as u32, + })) + } + + fn encode_nominate(b: &Nominate) -> WithCallIndexResult { + let ci = validate_call_index(&b.call_indices)?; + let targets = b + .nominators + .iter() + .map(|target| { + let account = + SS58Address::from_str(target).map_err(|_| EncodeError::InvalidAddress)?; + Ok(account.into()) + }) + .collect::>>()?; + Ok(ci.wrap(Self::Nominate { targets })) + } + + pub fn encode_call(s: &Staking) -> WithCallIndexResult { + match &s.message_oneof { + StakingVariant::bond(b) => Self::encode_bond(b), + StakingVariant::bond_extra(b) => Self::encode_bond_extra(b), + StakingVariant::chill(b) => Self::encode_chill(b), + StakingVariant::unbond(b) => Self::encode_unbond(b), + StakingVariant::withdraw_unbonded(b) => Self::encode_withdraw_unbonded(b), + StakingVariant::rebond(b) => Self::encode_rebond(b), + StakingVariant::nominate(b) => Self::encode_nominate(b), + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported staking call"), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshCall { + Balances(PolymeshBalances) = 0x05, + Identity(PolymeshIdentity) = 0x07, + Staking(PolymeshStaking) = 0x11, + Utility(GenericUtility) = 0x29, + } +); + +pub struct PolymeshCallEncoder; + +impl PolymeshCallEncoder { + pub fn new_boxed(_ctx: &SubstrateContext) -> Box { + Box::new(Self) + } +} + +impl TWPolkadotCallEncoder for PolymeshCallEncoder { + fn encode_call(&self, msg: &SigningVariant<'_>) -> EncodeResult { + let call = match msg { + SigningVariant::balance_call(b) => { + PolymeshBalances::encode_call(b)?.map(PolymeshCall::Balances) + }, + SigningVariant::polymesh_call(msg) => match &msg.message_oneof { + PolymeshVariant::identity_call(msg) => { + PolymeshIdentity::encode_call(msg)?.map(PolymeshCall::Identity) + }, + PolymeshVariant::None => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Polymesh call variant is None"); + }, + }, + SigningVariant::staking_call(s) => { + PolymeshStaking::encode_call(s)?.map(PolymeshCall::Staking) + }, + SigningVariant::None => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Staking call variant is None"); + }, + }; + Ok(RawOwned(call.to_scale())) + } + + fn encode_batch(&self, calls: Vec) -> EncodeResult { + let call = PolymeshCall::Utility(GenericUtility::BatchAll { calls }); + Ok(RawOwned(call.to_scale())) + } +} diff --git a/rust/chains/tw_polkadot/src/entry.rs b/rust/chains/tw_polkadot/src/entry.rs new file mode 100644 index 00000000000..fc7f469e3b9 --- /dev/null +++ b/rust/chains/tw_polkadot/src/entry.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::ctx_from_tw; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::signing_output_error; +use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +use tw_number::U256; +use tw_proto::Polkadot::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; +use tw_scale::{RawOwned, ToScale}; +use tw_ss58_address::SS58Address; +use tw_substrate::*; + +use crate::call_encoder::CallEncoder; + +pub struct PolkadotEntry; + +impl PolkadotEntry { + #[inline] + fn get_keypair_impl( + &self, + _coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + Ok(KeyPair::try_from(input.private_key.as_ref())?) + } + + fn build_transaction_impl( + &self, + _coin: &dyn CoinContext, + public_key: Option, + input: &Proto::SigningInput<'_>, + ) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let encoder = CallEncoder::from_ctx(&ctx)?; + let call = encoder.encode_call(&input.message_oneof)?; + let era = match &input.era { + Some(era) => Era::mortal(era.period, era.block_number), + None => Era::immortal(), + }; + let genesis_hash = input.genesis_hash.as_ref().try_into().unwrap_or_default(); + let current_hash = input.block_hash.as_ref().try_into().unwrap_or_default(); + let tip = U256::from_big_endian_slice(&input.tip) + .map_err(|_| EncodeError::InvalidValue)? + .try_into() + .map_err(|_| EncodeError::InvalidValue)?; + + let mut builder = TransactionBuilder::new(ctx.multi_address, call); + // Add chain extensions. + builder.extension(CheckVersion(input.spec_version)); + builder.extension(CheckVersion(input.transaction_version)); + builder.extension(CheckGenesis(genesis_hash)); + builder.extension(CheckEra { era, current_hash }); + builder.extension(CheckNonce::new(input.nonce as u32)); + if let Some(fee_asset_id) = ctx.fee_asset_id { + builder.extension(ChargeAssetTxPayment::new(tip, fee_asset_id)); + } else { + builder.extension(ChargeTransactionPayment::new(tip)); + } + if ctx.check_metadata { + builder.extension(CheckMetadataHash::default()); + } + if let Some(public_key) = public_key { + let account = SubstrateAddress( + SS58Address::from_public_key(&public_key, ctx.network).map_err(|e| { + TWError::new(EncodeError::InvalidAddress).context(format!("{e:?}")) + })?, + ); + builder.set_account(account); + } + Ok(builder) + } + + #[inline] + fn signing_output_impl( + &self, + _coin: &dyn CoinContext, + result: SigningResult, + ) -> SigningResult> { + let encoded = result?.to_scale(); + Ok(Proto::SigningOutput { + encoded: encoded.into(), + ..Default::default() + }) + } + + #[inline] + fn presigning_output_impl( + &self, + _coin: &dyn CoinContext, + result: SigningResult, + ) -> SigningResult> { + let pre_image = result?.to_scale(); + Ok(CompilerProto::PreSigningOutput { + // `pre_image` is already hashed if it is larger then 256 bytes. + data_hash: pre_image.clone().into(), + data: pre_image.into(), + ..Default::default() + }) + } +} + +impl SubstrateCoinEntry for PolkadotEntry { + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = CompilerProto::PreSigningOutput<'static>; + + #[inline] + fn get_keypair( + &self, + coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + self.get_keypair_impl(coin, input) + } + + #[inline] + fn build_transaction( + &self, + coin: &dyn CoinContext, + public_key: Option, + input: &Self::SigningInput<'_>, + ) -> SigningResult { + self.build_transaction_impl(coin, public_key, input) + .map_err(|e| e.map_err(SigningErrorType::from)) + } + + #[inline] + fn signing_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::SigningOutput { + self.signing_output_impl(coin, result) + .unwrap_or_else(|e| signing_output_error!(Proto::SigningOutput, e)) + } + + #[inline] + fn presigning_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::PreSigningOutput { + self.presigning_output_impl(coin, result) + .unwrap_or_else(|e| signing_output_error!(CompilerProto::PreSigningOutput, e)) + } +} diff --git a/rust/chains/tw_polkadot/src/lib.rs b/rust/chains/tw_polkadot/src/lib.rs new file mode 100644 index 00000000000..651203de009 --- /dev/null +++ b/rust/chains/tw_polkadot/src/lib.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_proto::Polkadot::Proto::{ + self, mod_Balance::OneOfmessage_oneof as BalanceVariant, + mod_SigningInput::OneOfmessage_oneof as SigningVariant, +}; +use tw_ss58_address::NetworkId; +use tw_substrate::*; + +pub mod call_encoder; +pub mod entry; + +pub const POLKADOT: NetworkId = NetworkId::new_unchecked(0); +pub const KUSAMA: NetworkId = NetworkId::new_unchecked(2); +pub const ACALA: NetworkId = NetworkId::new_unchecked(10); +pub const POLYMESH: NetworkId = NetworkId::new_unchecked(12); +pub const GENERIC_SUBSTRATE: NetworkId = NetworkId::new_unchecked(42); + +pub const POLKADOT_MULTI_ADDRESS_SPEC: u32 = 28; +pub const KUSAMA_MULTI_ADDRESS_SPEC: u32 = 2028; + +pub fn network_id_from_tw(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + Ok(NetworkId::try_from(input.network as u16).map_err(|_| EncodeError::InvalidNetworkId)?) +} + +pub fn fee_asset_id_from_tw(input: &'_ Proto::SigningInput<'_>) -> Option { + // Special case for batches. + match &input.message_oneof { + SigningVariant::balance_call(b) => match &b.message_oneof { + BalanceVariant::asset_transfer(at) => Some(at.fee_asset_id), + BalanceVariant::batch_asset_transfer(bat) => Some(bat.fee_asset_id), + _ => None, + }, + _ => None, + } +} + +pub fn ctx_from_tw(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + let network = + NetworkId::try_from(input.network as u16).map_err(|_| EncodeError::InvalidNetworkId)?; + let spec_version = input.spec_version; + let multi_address = match (input.multi_address, network) { + (true, _) => true, + (_, POLKADOT) if spec_version >= POLKADOT_MULTI_ADDRESS_SPEC => true, + (_, KUSAMA) if spec_version >= KUSAMA_MULTI_ADDRESS_SPEC => true, + _ => false, + }; + let check_metadata = match network { + POLKADOT | KUSAMA if spec_version >= 1_002_005 => true, + ACALA if spec_version >= 2_270 => true, + _ => false, + }; + let fee_asset_id = fee_asset_id_from_tw(input); + + Ok(SubstrateContext { + multi_address, + check_metadata, + network, + spec_version, + transaction_version: input.transaction_version, + fee_asset_id, + }) +} diff --git a/rust/chains/tw_polkadot/tests/extrinsic.rs b/rust/chains/tw_polkadot/tests/extrinsic.rs new file mode 100644 index 00000000000..f576982c449 --- /dev/null +++ b/rust/chains/tw_polkadot/tests/extrinsic.rs @@ -0,0 +1,423 @@ +use std::borrow::Cow; +use std::default::Default; + +use tw_encoding::hex::ToHex; +use tw_number::U256; +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, +}; +use tw_substrate::EncodeResult; + +use tw_polkadot::call_encoder::CallEncoder; + +fn encode_input(input: &Proto::SigningInput<'_>) -> EncodeResult> { + let encoded = CallEncoder::encode_input(input)?; + Ok(encoded.0) +} + +fn custom_call_indices(module: u8, method: u8) -> Option { + Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom(Proto::CustomCallIndices { + module_index: module as i32, + method_index: method as i32, + }), + }) +} + +fn polymesh_identity_call( + call: Proto::mod_Identity::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call(Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: call, + }, + ), + }) +} + +fn polymesh_add_auth_call( + add_auth: Proto::mod_Identity::AddAuthorization, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + polymesh_identity_call(Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + add_auth, + )) +} + +fn polymesh_join_identity(auth_id: u64) -> Proto::mod_SigningInput::OneOfmessage_oneof<'static> { + polymesh_identity_call( + Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key( + Proto::mod_Identity::JoinIdentityAsKey { + call_indices: None, + auth_id, + }, + ), + ) +} + +fn balance_call( + call: Proto::mod_Balance::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: call, + }) +} + +fn staking_call( + call: Proto::mod_Staking::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: call, + }) +} + +#[test] +fn polymesh_encode_transfer_with_memo() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000 + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + memo: "MEMO PADDED WITH SPACES".into(), + call_indices: custom_call_indices(0x05, 0x01), + })), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + ..Default::default() + }), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity_with_zero_data() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000 + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { + 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 encoded = encode_input(&input).expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000" + ); +} + +#[test] +fn polymesh_encode_authorization_join_identity_allowing_everything() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: polymesh_add_auth_call(Proto::mod_Identity::AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + data: Some(AuthData { + asset: None, + extrinsic: None, + portfolio: None, + }), + ..Default::default() + }), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000" + ); +} + +#[test] +fn polymesh_encode_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x07040b13000000000000 + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: polymesh_join_identity(4875), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!(encoded.to_hex(), "07040b13000000000000"); +} + +#[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: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + call_indices: custom_call_indices(0x32, 0x05), + to_address: "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), + value: Cow::Owned(U256::from(999500000u64).to_big_endian().to_vec()), + asset_id: 1984, + ..Default::default() + }, + )), + ..Default::default() + }; + + let encoded = encode_input(&input).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: balance_call( + Proto::mod_Balance::OneOfmessage_oneof::batch_asset_transfer(BatchAssetTransfer { + call_indices: custom_call_indices(0x28, 0x00), + fee_asset_id: 0x00, + transfers: vec![AssetTransfer { + call_indices: custom_call_indices(0x32, 0x06), + to_address: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + asset_id: 1984, + ..Default::default() + }], + }), + ), + ..Default::default() + }; + + let encoded = encode_input(&input).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: balance_call( + 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_big_endian().to_vec().into(), + asset_id: 1984, + ..Default::default() + }], + ..Default::default() + }), + ), + ..Default::default() + }; + + encode_input(&input).expect_err("unexpected success"); +} + +#[test] +fn encode_staking_nominate() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::nominate(Nominate { + nominators: vec![ + "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), + ], + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).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: staking_call(Proto::mod_Staking::OneOfmessage_oneof::chill(Chill { + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!(encoded.to_hex(), "0706"); +} + +#[test] +fn encode_staking_bond_with_controller() { + let input = Proto::SigningInput { + network: 12, + multi_address: true, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::CONTROLLER, + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!( + encoded.to_hex(), + "11000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" + ); +} + +#[test] +fn encode_staking_bond() { + let input = Proto::SigningInput { + network: 0, + multi_address: true, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { + controller: Default::default(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::STAKED, + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).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: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond_extra( + BondExtra { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }, + )), + ..Default::default() + }; + + let encoded = encode_input(&input).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: staking_call(Proto::mod_Staking::OneOfmessage_oneof::rebond(Rebond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).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: staking_call(Proto::mod_Staking::OneOfmessage_oneof::unbond(Unbond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + })), + ..Default::default() + }; + + let encoded = encode_input(&input).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: staking_call(Proto::mod_Staking::OneOfmessage_oneof::withdraw_unbonded( + WithdrawUnbonded { + slashing_spans: 84, + call_indices: None, + }, + )), + ..Default::default() + }; + + let encoded = encode_input(&input).expect("error encoding call"); + assert_eq!(encoded.to_hex(), "070354000000"); +} diff --git a/rust/frameworks/tw_substrate/Cargo.toml b/rust/frameworks/tw_substrate/Cargo.toml new file mode 100644 index 00000000000..ada930cd707 --- /dev/null +++ b/rust/frameworks/tw_substrate/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tw_substrate" +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_number = { path = "../../tw_number" } +tw_proto = { path = "../../tw_proto" } +tw_scale = { path = "../../tw_scale" } +tw_ss58_address = { path = "../../tw_ss58_address" } diff --git a/rust/frameworks/tw_substrate/src/address.rs b/rust/frameworks/tw_substrate/src/address.rs new file mode 100644 index 00000000000..6e476e23527 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/address.rs @@ -0,0 +1,126 @@ +// 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_coin_entry::prefix::AddressPrefix; +use tw_memory::Data; +use tw_scale::{impl_struct_scale, ToScale}; +use tw_ss58_address::{NetworkId, SS58Address}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SubstratePrefix(NetworkId); + +impl SubstratePrefix { + pub fn new(network: NetworkId) -> Self { + Self(network) + } + + pub fn new_unchecked(prefix: u16) -> Self { + Self(NetworkId::new_unchecked(prefix)) + } + + pub fn network(self) -> NetworkId { + self.0 + } +} + +impl TryFrom for SubstratePrefix { + type Error = AddressError; + + fn try_from(prefix: AddressPrefix) -> Result { + match prefix { + AddressPrefix::SubstrateNetwork(network) => NetworkId::from_u16(network).map(Self), + _ => Err(AddressError::UnexpectedAddressPrefix), + } + } +} + +impl_struct_scale!( + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct SubstrateAddress(pub SS58Address); +); + +impl SubstrateAddress { + pub fn with_network_check(self, prefix: Option) -> AddressResult { + if let Some(prefix) = prefix { + if self.0.network() != prefix.network() { + return Err(AddressError::UnexpectedAddressPrefix); + } + } + Ok(self) + } +} + +impl CoinAddress for SubstrateAddress { + #[inline] + fn data(&self) -> Data { + self.0.key_bytes().into() + } +} + +impl FromStr for SubstrateAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + SS58Address::from_str(s).map(SubstrateAddress) + } +} + +impl From for SubstrateAddress { + fn from(other: SS58Address) -> Self { + Self(other) + } +} + +impl fmt::Display for SubstrateAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +pub type AccountId = SubstrateAddress; +pub type AccountIndex = u32; + +#[derive(Clone, Debug, Default)] +pub struct MultiAddress { + account: AccountId, + multi: bool, +} + +impl MultiAddress { + pub fn new(account: AccountId, multi: bool) -> Self { + Self { account, multi } + } +} + +impl ToScale for MultiAddress { + fn to_scale_into(&self, out: &mut Vec) { + if self.multi { + // MultiAddress::Id variant. + out.push(0); + } + self.account.to_scale_into(out); + } +} + +impl From for MultiAddress { + fn from(account: AccountId) -> Self { + Self { + account, + multi: true, + } + } +} + +impl From for MultiAddress { + fn from(other: SS58Address) -> Self { + Self { + account: SubstrateAddress(other), + multi: true, + } + } +} diff --git a/rust/frameworks/tw_substrate/src/entry.rs b/rust/frameworks/tw_substrate/src/entry.rs new file mode 100644 index 00000000000..8f930110fd3 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/entry.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::{SubstrateAddress, SubstratePrefix}; +use crate::substrate_coin_entry::SubstrateCoinEntry; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::common::compile_input::SingleSignaturePubkey; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::transaction_util::NoTransactionUtil; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_keypair::{ed25519, traits::KeyPairTrait, tw::PublicKey}; +use tw_scale::RawOwned; +use tw_ss58_address::{NetworkId, SS58Address}; + +pub struct SubstrateEntry(pub T); + +impl SubstrateEntry { + #[inline] + fn sign_impl( + &self, + coin: &dyn CoinContext, + input: T::SigningInput<'_>, + ) -> SigningResult { + let keypair = self.0.get_keypair(coin, &input)?; + let public_key = Some(keypair.public().clone()); + let unsigned_tx = self.0.build_transaction(coin, public_key, &input)?; + let signed_tx = unsigned_tx.sign(&keypair)?; + Ok(RawOwned::new(signed_tx)) + } + + #[inline] + fn preimage_hashes_impl( + &self, + coin: &dyn CoinContext, + input: T::SigningInput<'_>, + ) -> SigningResult { + let keypair = self.0.get_keypair(coin, &input).ok(); + let public_key = keypair.map(|p| p.public().clone()); + let unsigned_tx = self.0.build_transaction(coin, public_key, &input)?; + let pre_image = unsigned_tx.encode_payload()?; + Ok(RawOwned(pre_image)) + } + + #[inline] + fn compile_impl( + &self, + coin: &dyn CoinContext, + input: T::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> SigningResult { + let SingleSignaturePubkey { + signature, + public_key, + } = SingleSignaturePubkey::from_sign_pubkey_list(signatures, public_keys)?; + let signature = ed25519::Signature::try_from(signature.as_slice())?; + let public_key = ed25519::sha512::PublicKey::try_from(public_key.as_slice())?; + + let unsigned_tx = self.0.build_transaction(coin, Some(public_key), &input)?; + let signed_tx = unsigned_tx.into_signed(signature)?; + Ok(RawOwned::new(signed_tx)) + } +} + +impl CoinEntry for SubstrateEntry { + type AddressPrefix = SubstratePrefix; + type Address = SubstrateAddress; + type SigningInput<'a> = T::SigningInput<'a>; + type SigningOutput = T::SigningOutput; + type PreSigningOutput = T::PreSigningOutput; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = NoPlanBuilder; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = NoTransactionUtil; + + #[inline] + fn parse_address( + &self, + coin: &dyn CoinContext, + address: &str, + prefix: Option, + ) -> AddressResult { + let prefix = prefix.or_else(|| coin.ss58_prefix().map(SubstratePrefix::new_unchecked)); + SubstrateAddress::from_str(address)?.with_network_check(prefix) + } + + #[inline] + fn parse_address_unchecked(&self, address: &str) -> AddressResult { + SubstrateAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + coin: &dyn CoinContext, + public_key: PublicKey, + _derivation: Derivation, + prefix: Option, + ) -> AddressResult { + let network = prefix + .map(SubstratePrefix::network) + .or_else(|| { + coin.ss58_prefix() + .and_then(|prefix| NetworkId::from_u16(prefix).ok()) + }) + .unwrap_or_default(); + let public_key = public_key + .to_ed25519() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + + SS58Address::from_public_key(public_key, network).map(SubstrateAddress) + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + let res = self.sign_impl(coin, input); + self.0.signing_output(coin, res) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + let res = self.preimage_hashes_impl(coin, input); + self.0.presigning_output(coin, res) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + let res = self.compile_impl(coin, input, signatures, public_keys); + self.0.signing_output(coin, res) + } +} diff --git a/rust/frameworks/tw_substrate/src/extensions.rs b/rust/frameworks/tw_substrate/src/extensions.rs new file mode 100644 index 00000000000..3221a63aff6 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/extensions.rs @@ -0,0 +1,275 @@ +//! Extensions for Substrate transactions. +//! +//! This module provides types and traits for handling Substrate transaction extensions. Extensions are a way to +//! include additional data and checks in a transaction, such as nonce, fee payment, era, and version checks. +//! +//! # Example +//! +//! ```rust +//! use tw_substrate::extensions::*; +//! use tw_substrate::TransactionBuilder; +//! use tw_scale::RawOwned; +//! +//! // Create a transaction builder +//! let mut builder = TransactionBuilder::new(true, RawOwned::default()); +//! +//! // Add various extensions +//! builder.extension(CheckVersion(1)); // Spec version +//! builder.extension(CheckVersion(1)); // Transaction version +//! builder.extension(CheckGenesis(Default::default())); +//! builder.extension(CheckEra { +//! era: Era::immortal(), +//! current_hash: Default::default(), +//! }); +//! builder.extension(CheckNonce::new(1)); +//! builder.extension(ChargeTransactionPayment::new(0)); +//! ``` + +use crate::extrinsic::BlockHash; +use tw_scale::{impl_enum_scale, Compact, RawOwned, ToScale}; + +/// Data container for transaction extensions. +/// +/// Contains two parts: +/// - `data`: Raw extension data included in the transaction +/// - `signed`: Additional data included in the signature payload +#[derive(Clone, Debug, Default)] +pub struct TxExtensionData { + /// Raw extension data included in the transaction + pub data: RawOwned, + /// Additional data included in the signature payload + pub signed: RawOwned, +} + +impl TxExtensionData { + /// Encode extension data into the transaction data field + pub fn encode_data(&mut self, data: &T) { + data.to_scale_into(&mut self.data.0); + } + + /// Encode extension data into the signature payload field + pub fn encode_signed(&mut self, signed: &T) { + signed.to_scale_into(&mut self.signed.0); + } +} + +/// Trait for types that can be used as transaction extensions. +pub trait TxExtension { + /// Encodes the extension data into the transaction. + fn encode(&self, tx: &mut TxExtensionData); +} + +impl TxExtension for () { + fn encode(&self, _tx: &mut TxExtensionData) {} +} + +/// Version check extension. +/// +/// Used for both spec version and transaction version checks. +#[derive(Clone, Debug, Default)] +pub struct CheckVersion( + /// Version number to check + pub u32, +); +pub type CheckSpecVersion = CheckVersion; +pub type CheckTxVersion = CheckVersion; + +impl TxExtension for CheckVersion { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_signed(&self.0); + } +} + +/// Extension for checking block genesis hash +#[derive(Clone, Debug, Default)] +pub struct CheckGenesis( + /// Genesis block hash + pub BlockHash, +); + +impl TxExtension for CheckGenesis { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_signed(&self.0); + } +} + +/// Transaction era for time-based validity. +/// +/// Can be either: +/// - `Immortal`: Transaction never expires +/// - `Mortal(period, phase)`: Transaction is valid for a specific time window +#[derive(Clone, Copy, Default, Debug, PartialEq)] +pub enum Era { + #[default] + Immortal, + Mortal(u64, u64), +} + +impl Era { + /// Creates a mortal era with the given period and current block number + /// + /// # Arguments + /// * `period` - The number of blocks the transaction remains valid for + /// * `block` - Current block number used to calculate the era phase + pub fn mortal(period: u64, block: u64) -> Self { + // Based off `sp_runtime::generic::Era`: + // See https://github.com/paritytech/polkadot-sdk/blob/657b5503a04e97737696fa7344641019350fb521/substrate/primitives/runtime/src/generic/era.rs#L65 + let period = period + .checked_next_power_of_two() + .unwrap_or(1 << 16) + .clamp(4, 1 << 16); + let phase = block % period; + let quantize_factor = (period >> 12).max(1); + let quantized_phase = phase / quantize_factor * quantize_factor; + Self::Mortal(period, quantized_phase) + } + + /// Creates an immortal era (transaction never expires) + pub fn immortal() -> Self { + Self::Immortal + } +} + +impl ToScale for Era { + fn to_scale_into(&self, out: &mut Vec) { + match self { + Self::Immortal => { + out.push(0); + }, + Self::Mortal(period, phase) => { + // Based off `sp_runtime::generic::Era`: + // See https://github.com/paritytech/polkadot-sdk/blob/657b5503a04e97737696fa7344641019350fb521/substrate/primitives/runtime/src/generic/era.rs#L107 + let quantize_factor = (period >> 12).max(1); + + let encoded = (period.trailing_zeros() - 1).clamp(1, 15) as u16 + | ((phase / quantize_factor) << 4) as u16; + encoded.to_scale_into(out); + }, + } + } +} + +/// Extension for checking transaction era and block hash. +#[derive(Clone, Debug, Default)] +pub struct CheckEra { + /// Transaction validity period + pub era: Era, + /// Current block hash + pub current_hash: BlockHash, +} + +impl TxExtension for CheckEra { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_data(&self.era); + tx.encode_signed(&self.current_hash); + } +} + +/// Extension for checking transaction nonce. +#[derive(Clone, Debug)] +pub struct CheckNonce( + /// Account nonce as compact encoding + pub Compact, +); + +impl CheckNonce { + /// Creates a new nonce check extension + /// + /// # Arguments + /// * `nonce` - Account nonce value + pub fn new(nonce: u32) -> Self { + Self(Compact(nonce)) + } +} + +impl TxExtension for CheckNonce { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_data(&self.0); + } +} + +/// Extension for handling native token transaction fees. +#[derive(Clone, Debug)] +pub struct ChargeTransactionPayment( + /// Transaction tip amount as compact encoding + pub Compact, +); + +impl ChargeTransactionPayment { + /// Creates a new payment extension with the given tip amount + /// + /// # Arguments + /// * `tip` - Optional tip amount in addition to the base fee + pub fn new(tip: u128) -> Self { + Self(Compact(tip)) + } +} + +impl TxExtension for ChargeTransactionPayment { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_data(&self.0); + } +} + +/// Extension for handling asset-based transaction fees. +#[derive(Clone, Debug)] +pub struct ChargeAssetTxPayment { + /// Transaction tip amount as compact encoding + tip: Compact, + /// Optional asset ID for fee payment (None = native token) + asset_id: Option, +} + +impl ChargeAssetTxPayment { + /// Creates a new asset payment extension + /// + /// # Arguments + /// * `tip` - Optional tip amount in addition to the base fee + /// * `asset_id` - Asset ID to pay fees with (0 = native token) + pub fn new(tip: u128, asset_id: u32) -> Self { + Self { + tip: Compact(tip), + asset_id: if asset_id > 0 { + Some(asset_id) + } else { + // native token + None + }, + } + } +} + +impl TxExtension for ChargeAssetTxPayment { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_data(&self.tip); + tx.encode_data(&self.asset_id); + } +} + +impl_enum_scale!( + /// Mode for checking runtime metadata. + #[derive(Clone, Copy, Debug, Default)] + pub enum CheckMetadataMode { + /// Metadata check is disabled + #[default] + Disabled = 0x00, + /// Metadata check is enabled + Enabled = 0x01, + } +); + +/// Extension for checking runtime metadata hash. +#[derive(Clone, Debug, Default)] +pub struct CheckMetadataHash { + /// Whether metadata checking is enabled + pub mode: CheckMetadataMode, + /// Optional metadata hash to check against + pub hash: Option, +} + +impl TxExtension for CheckMetadataHash { + fn encode(&self, tx: &mut TxExtensionData) { + tx.encode_data(&self.mode); + tx.encode_signed(&self.hash); + } +} diff --git a/rust/frameworks/tw_substrate/src/extrinsic.rs b/rust/frameworks/tw_substrate/src/extrinsic.rs new file mode 100644 index 00000000000..93bc9625232 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/extrinsic.rs @@ -0,0 +1,265 @@ +use tw_coin_entry::error::prelude::*; +use tw_hash::{blake2::blake2_b, H256, H512}; +use tw_keypair::{ + ed25519::{sha512::KeyPair, Signature}, + traits::SigningKeyTrait, + KeyPairError, +}; +use tw_scale::{impl_enum_scale, impl_struct_scale, RawOwned, ToScale}; + +use crate::address::*; +use crate::extensions::*; +use crate::{EncodeError, EncodeResult}; + +/// Transaction hash type. +pub type TxHash = H256; +/// Block hash type. +pub type BlockHash = H256; +/// Block number type. +pub type BlockNumber = u32; + +impl_enum_scale!( + /// Supported signature types for Substrate transactions. + #[derive(Clone, Debug)] + pub enum MultiSignature { + /// Ed25519 signature represented as 64 bytes. + Ed25519(H512) = 0x0, + /// Sr25519 signature represented as 64 bytes. + Sr25519(H512) = 0x1, + //Ecdsa([u8; 65]) = 0x2, + } +); + +impl From for MultiSignature { + fn from(sig: Signature) -> Self { + Self::Ed25519(sig.to_bytes()) + } +} + +/// Represents a module and method call index pair for a Substrate transaction. +#[derive(Clone, Debug)] +pub struct CallIndex(Option<(u8, u8)>); + +impl CallIndex { + /// Creates a CallIndex from TrustWallet's representation of call indices. + pub fn from_tw(call_index: Option<(i32, i32)>) -> EncodeResult { + let call_index = match call_index { + Some((module_index, method_index)) => { + if module_index > u8::MAX as i32 || method_index > u8::MAX as i32 { + Err(EncodeError::InvalidCallIndex) + .into_tw() + .context("Module or method call index too large.")?; + } + Some((module_index as u8, method_index as u8)) + }, + _ => None, + }; + Ok(Self(call_index)) + } + + /// Creates a CallIndex that must contain valid indices. + pub fn required_from_tw(call_index: Option<(i32, i32)>) -> EncodeResult { + if call_index.is_none() { + Err(EncodeError::MissingCallIndices) + .into_tw() + .context("Call indices are required.")?; + } + Self::from_tw(call_index) + } + + /// Returns true if the CallIndex contains valid indices. + pub fn has_call_index(&self) -> bool { + self.0.is_some() + } + + /// Wraps a SCALE-encodable value with this CallIndex. + pub fn wrap(self, value: T) -> WithCallIndex { + WithCallIndex { + value, + call_index: self, + } + } +} + +/// Wrapper type that combines a SCALE-encodable value with optional call indices. +#[derive(Clone, Debug)] +pub struct WithCallIndex { + /// The wrapped SCALE-encodable value. + value: T, + /// Associated call indices. + call_index: CallIndex, +} + +impl WithCallIndex { + pub fn map U>(self, f: F) -> WithCallIndex { + WithCallIndex { + value: f(self.value), + call_index: self.call_index, + } + } +} + +impl ToScale for WithCallIndex { + fn to_scale_into(&self, out: &mut Vec) { + if let Some(call_index) = &self.call_index.0 { + let mut value = self.value.to_scale(); + if value.len() < 2 { + debug_assert!( + false, + "`WithCallIndex` inner value must include call indices in Scale representation" + ); + return; + } + // Override the first two bytes with the custom call index. + value[0] = call_index.0; + value[1] = call_index.1; + out.extend(&value); + } else { + self.value.to_scale_into(out); + } + } +} + +/// Builder pattern implementation for creating Substrate transactions. +#[derive(Debug, Default)] +pub struct TransactionBuilder { + /// Whether to use the newer multi-address format. + multi_address: bool, + /// The call data to be executed. + call: RawOwned, + /// Additional data attached to the transaction. + extensions: TxExtensionData, + /// The sender's address. + account: MultiAddress, +} + +impl TransactionBuilder { + /// Creates a new transaction builder. + /// + /// * `multi_address` - Whether to use the newer multi-address format + /// * `call` - The SCALE-encoded call data + pub fn new(multi_address: bool, call: RawOwned) -> Self { + Self { + multi_address, + call, + ..Default::default() + } + } + + /// Sets the sender's account. + pub fn set_account(&mut self, account: AccountId) { + self.account = MultiAddress::new(account, self.multi_address); + } + + /// Adds an extension to the transaction. + pub fn extension(&mut self, extension: E) { + extension.encode(&mut self.extensions); + } + + /// Encodes the payload that needs to be signed. + /// Returns the encoded bytes or a hash of the payload if it exceeds MAX_PAYLOAD_SIZE. + pub fn encode_payload(&self) -> Result, KeyPairError> { + // SCALE encode the payload that needs to be signed: (call, extensions.data, extensions.signed). + let mut payload = self.call.to_scale(); + self.extensions.data.to_scale_into(&mut payload); + self.extensions.signed.to_scale_into(&mut payload); + + // if the payload is large then we sign a hash of the payload. + if payload.len() > MAX_PAYLOAD_SIZE { + Ok(blake2_b(&payload, PAYLOAD_HASH_SIZE).map_err(|_| KeyPairError::InternalError)?) + } else { + Ok(payload) + } + } + + /// Signs the transaction with the given keypair. + pub fn sign(self, keypair: &KeyPair) -> Result { + let payload = self.encode_payload()?; + let signature = keypair.sign(payload)?; + self.into_signed(signature) + } + + /// Creates a signed extrinsic using a pre-computed signature. + pub fn into_signed(self, signature: Signature) -> Result { + Ok(ExtrinsicV4::signed( + self.account, + signature.into(), + self.extensions.data, + self.call, + )) + } +} + +impl_struct_scale!( + #[derive(Clone, Debug)] + pub struct ExtrinsicSignature { + /// The sender's address. + pub account: MultiAddress, + /// The signature of the extrinsic. + pub signature: MultiSignature, + /// Additional signed data (e.g., era, nonce, tip). + pub extra: RawOwned, + } +); + +/// Current version of the `UncheckedExtrinsic` format. +pub const EXTRINSIC_VERSION: u8 = 4; +pub const SIGNED_EXTRINSIC_BIT: u8 = 0b1000_0000; +pub const UNSIGNED_EXTRINSIC_MASK: u8 = 0b0111_1111; +pub const MAX_PAYLOAD_SIZE: usize = 256; +pub const PAYLOAD_HASH_SIZE: usize = 32; + +/// Represents a Substrate transaction (extrinsic) using format version 4. +#[derive(Clone, Debug)] +pub struct ExtrinsicV4 { + /// Optional signature data for signed transactions. + pub signature: Option, + /// The actual call data to be executed. + pub call: RawOwned, +} + +impl ExtrinsicV4 { + /// Creates a new signed extrinsic. + /// + /// * `account` - The sender's address + /// * `sig` - The signature of the extrinsic + /// * `extra` - Additional signed data + /// * `call` - The call data to be executed + pub fn signed( + account: MultiAddress, + sig: MultiSignature, + extra: RawOwned, + call: RawOwned, + ) -> Self { + Self { + signature: Some(ExtrinsicSignature { + account, + signature: sig, + extra, + }), + call, + } + } +} + +impl ToScale for ExtrinsicV4 { + fn to_scale_into(&self, out: &mut Vec) { + // We use a temp buffer here for the `Compact` length prefix. + let mut buf = Vec::with_capacity(512); + + // 1 byte version id and signature if signed. + match &self.signature { + Some(sig) => { + buf.push(EXTRINSIC_VERSION | SIGNED_EXTRINSIC_BIT); + sig.to_scale_into(&mut buf); + }, + None => { + buf.push(EXTRINSIC_VERSION & UNSIGNED_EXTRINSIC_MASK); + }, + } + self.call.to_scale_into(&mut buf); + + // SCALE encode the tmp buffer to `out`. + buf.to_scale_into(out); + } +} diff --git a/rust/frameworks/tw_substrate/src/lib.rs b/rust/frameworks/tw_substrate/src/lib.rs new file mode 100644 index 00000000000..0412862eb82 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/lib.rs @@ -0,0 +1,66 @@ +use tw_coin_entry::error::prelude::*; +use tw_ss58_address::NetworkId; + +pub mod entry; +pub use entry::*; + +pub mod substrate_coin_entry; +pub use substrate_coin_entry::*; + +pub mod address; +pub use address::*; + +pub mod extrinsic; +pub use extrinsic::*; + +pub mod extensions; +pub use extensions::*; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum EncodeError { + InvalidNetworkId, + MissingCallIndices, + InvalidCallIndex, + InvalidAddress, + InvalidValue, + NotSupported, +} + +impl From for SigningErrorType { + #[inline] + fn from(err: EncodeError) -> Self { + match err { + EncodeError::InvalidAddress => SigningErrorType::Error_invalid_address, + EncodeError::InvalidValue => SigningErrorType::Error_input_parse, + EncodeError::MissingCallIndices => SigningErrorType::Error_not_supported, + EncodeError::NotSupported => SigningErrorType::Error_not_supported, + _ => SigningErrorType::Error_invalid_params, + } + } +} + +pub type EncodeResult = Result>; +pub type WithCallIndexResult = Result, TWError>; + +#[derive(Debug, Clone)] +pub struct SubstrateContext { + pub multi_address: bool, + pub check_metadata: bool, + pub network: NetworkId, + pub spec_version: u32, + pub transaction_version: u32, + pub fee_asset_id: Option, +} + +impl SubstrateContext { + pub fn multi_address(&self, account: AccountId) -> MultiAddress { + MultiAddress::new(account, self.multi_address) + } + + pub fn multi_addresses(&self, accounts: Vec) -> Vec { + accounts + .into_iter() + .map(|account| MultiAddress::new(account, self.multi_address)) + .collect() + } +} diff --git a/rust/frameworks/tw_substrate/src/substrate_coin_entry.rs b/rust/frameworks/tw_substrate/src/substrate_coin_entry.rs new file mode 100644 index 00000000000..aed9f09c336 --- /dev/null +++ b/rust/frameworks/tw_substrate/src/substrate_coin_entry.rs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! Substrate chain entry implementation traits and helpers. +//! +//! This module provides the base trait and implementation patterns for Substrate-based chains. +//! It handles the common functionality needed for transaction building, signing, and output generation. +//! +//! # Example +//! +//! Here's an example of implementing a custom Substrate chain entry: +//! +//! ```rust,ignore +//! use tw_proto::MyChain::Proto; +//! use tw_substrate::substrate_coin_entry::SubstrateCoinEntry; +//! use tw_substrate::TransactionBuilder; +//! use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +//! use tw_coin_entry::{coin_context::CoinContext, error::SigningResult}; +//! use tw_scale::RawOwned; +//! +//! pub struct MyChainEntry; +//! +//! impl SubstrateCoinEntry for MyChainEntry { +//! type SigningInput<'a> = Proto::SigningInput<'a>; +//! type SigningOutput = Proto::SigningOutput<'static>; +//! type PreSigningOutput = Proto::PreSigningOutput<'static>; +//! +//! fn get_keypair( +//! &self, +//! _coin: &dyn CoinContext, +//! input: &Self::SigningInput<'_>, +//! ) -> SigningResult { +//! // Convert private key from input into KeyPair +//! KeyPair::try_from(input.private_key.as_ref()) +//! } +//! +//! fn build_transaction( +//! &self, +//! coin: &dyn CoinContext, +//! public_key: Option, +//! input: &Self::SigningInput<'_>, +//! ) -> SigningResult { +//! // Build the transaction using chain-specific logic +//! let context = my_chain::ctx_from_tw(input)?; +//! let call = my_chain::call_encoder::encode_call(input)?; +//! TransactionBuilder::new(context, call) +//! } +//! +//! fn signing_output( +//! &self, +//! _coin: &dyn CoinContext, +//! result: SigningResult, +//! ) -> Self::SigningOutput { +//! let mut output = Proto::SigningOutput::new(); +//! match result { +//! Ok(encoded) => { +//! output.encoded = encoded.to_vec(); +//! } +//! Err(e) => { +//! output.error = true; +//! output.error_message = e.to_string(); +//! } +//! } +//! output +//! } +//! +//! fn presigning_output( +//! &self, +//! _coin: &dyn CoinContext, +//! result: SigningResult, +//! ) -> Self::PreSigningOutput { +//! let mut output = Proto::PreSigningOutput::new(); +//! match result { +//! Ok(encoded) => { +//! output.data = encoded.to_vec(); +//! } +//! Err(e) => { +//! output.error = true; +//! output.error_message = e.to_string(); +//! } +//! } +//! output +//! } +//! } +//! ``` +//! +//! The example above demonstrates: +//! * Implementing the required associated types for protocol buffer messages +//! * Converting private keys to keypairs +//! * Building transactions using chain-specific context and call encoding +//! * Handling success and error cases in output generation +//! +//! Once implemented, the entry can be wrapped with `SubstrateEntry` to provide +//! the complete `CoinEntry` implementation. + +use crate::TransactionBuilder; +use tw_coin_entry::{coin_context::CoinContext, error::prelude::*}; +use tw_keypair::ed25519::sha512::{KeyPair, PublicKey}; +use tw_proto::{MessageRead, MessageWrite}; +use tw_scale::RawOwned; + +/// Trait for implementing a Substrate-based chain entry. +/// +/// This trait provides the core functionality needed for Substrate-based chains, +/// including transaction building, signing, and output generation. +/// +/// # Type Parameters +/// * `SigningInput`: The protobuf message type for transaction input +/// * `SigningOutput`: The protobuf message type for transaction output +/// * `PreSigningOutput`: The protobuf message type for pre-signing output +pub trait SubstrateCoinEntry { + /// The protobuf message type for transaction input. + type SigningInput<'a>: MessageRead<'a> + MessageWrite; + /// The protobuf message type for transaction output. + type SigningOutput: MessageWrite; + /// The protobuf message type for pre-signing output. + type PreSigningOutput: MessageWrite; + + /// Retrieves the keypair from the signing input. + /// + /// # Parameters + /// * `coin`: The coin context + /// * `input`: The signing input containing private key data + /// + /// # Returns + /// Returns a `SigningResult` containing the Ed25519 keypair if successful. + fn get_keypair( + &self, + coin: &dyn CoinContext, + input: &Self::SigningInput<'_>, + ) -> SigningResult; + + /// Builds an unsigned transaction from the input data. + /// + /// The returned transaction builder can be used to sign the transaction. + /// + /// # Parameters + /// * `coin`: The coin context + /// * `public_key`: Optional public key for address derivation + /// * `input`: The signing input containing transaction data + /// + /// # Returns + /// Returns a `SigningResult` containing the transaction builder if successful. + fn build_transaction( + &self, + coin: &dyn CoinContext, + public_key: Option, + input: &Self::SigningInput<'_>, + ) -> SigningResult; + + /// Creates the signing output from the signed transaction built from the transaction builder. + /// + /// # Parameters + /// * `coin`: The coin context + /// * `result`: The results from building and signing the transaction. + /// + /// # Returns + /// Returns the protobuf signing output message. + fn signing_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::SigningOutput; + + /// Creates the pre-signing output from the unsigned transaction built from the transaction builder. + /// + /// # Parameters + /// * `coin`: The coin context + /// * `result`: The result from building the transaction. + /// + /// # Returns + /// Returns the protobuf pre-signing output message. + fn presigning_output( + &self, + coin: &dyn CoinContext, + result: SigningResult, + ) -> Self::PreSigningOutput; +} diff --git a/rust/frameworks/tw_substrate/tests/address.rs b/rust/frameworks/tw_substrate/tests/address.rs new file mode 100644 index 00000000000..36820f9192a --- /dev/null +++ b/rust/frameworks/tw_substrate/tests/address.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +use tw_substrate::address::SubstrateAddress; + +#[test] +fn test_polkadot_address_valid() { + // Substrate ed25519 + SubstrateAddress::from_str("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu") + .expect("error parsing address"); + + // Substrate sr25519 + SubstrateAddress::from_str("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony") + .expect("error parsing address"); +} + +#[test] +fn test_polkadot_address_invalid() { + // Empty address + SubstrateAddress::from_str("").expect_err("no error parsing invalid address"); + + // Invalid address + SubstrateAddress::from_str("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsT^^^") + .expect_err("no error parsing invalid address"); +} diff --git a/rust/frameworks/tw_substrate/tests/era.rs b/rust/frameworks/tw_substrate/tests/era.rs new file mode 100644 index 00000000000..9ee3e1989d6 --- /dev/null +++ b/rust/frameworks/tw_substrate/tests/era.rs @@ -0,0 +1,54 @@ +use tw_scale::ToScale; +use tw_substrate::Era; + +#[test] +fn encode_era() { + let e1 = Era::mortal(8, 429119); + let e2 = Era::mortal(4, 428861); + let e3 = Era::mortal(64, 4246319); + + assert_eq!(e1.to_scale(), vec![0x72, 0x00]); + assert_eq!(e2.to_scale(), vec![0x11, 0x00]); + assert_eq!(e3.to_scale(), vec![0xf5, 0x02]); +} + +// Era tests ported from: https://github.com/paritytech/polkadot-sdk/blob/657b5503a04e97737696fa7344641019350fb521/substrate/primitives/runtime/src/generic/era.rs#L182 +#[test] +fn immortal_works() { + let e = Era::immortal(); + assert_eq!(e.to_scale(), vec![0u8]); +} + +#[test] +fn mortal_codec_works() { + let e = Era::mortal(64, 42); + + let expected = vec![5 + 42 % 16 * 16, 42 / 16]; + assert_eq!(e.to_scale(), expected); +} + +#[test] +fn long_period_mortal_codec_works() { + let e = Era::mortal(32768, 20000); + + let expected = vec![(14 + 2500 % 16 * 16) as u8, (2500 / 16) as u8]; + assert_eq!(e.to_scale(), expected); +} + +#[test] +fn era_initialization_works() { + assert_eq!(Era::mortal(64, 42), Era::Mortal(64, 42)); + assert_eq!(Era::mortal(32768, 20000), Era::Mortal(32768, 20000)); + assert_eq!(Era::mortal(200, 513), Era::Mortal(256, 1)); + assert_eq!(Era::mortal(2, 1), Era::Mortal(4, 1)); + assert_eq!(Era::mortal(4, 5), Era::Mortal(4, 1)); +} + +#[test] +fn quantized_clamped_era_initialization_works() { + // clamp 1000000 to 65536, quantize 1000001 % 65536 to the nearest 4 + assert_eq!( + Era::mortal(1000000, 1000001), + Era::Mortal(65536, 1000001 % 65536 / 4 * 4) + ); +} diff --git a/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs index 4bdabd6d642..3e25f6c3400 100644 --- a/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs +++ b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs @@ -35,6 +35,7 @@ impl TryFrom for StandardBitcoinPrefix { match prefix { AddressPrefix::BitcoinBase58(base58) => Ok(StandardBitcoinPrefix::Base58(base58)), AddressPrefix::Hrp(hrp) => Ok(StandardBitcoinPrefix::Bech32(Bech32Prefix { hrp })), + AddressPrefix::SubstrateNetwork(_) => Err(AddressError::UnexpectedAddressPrefix), } } } diff --git a/rust/tw_any_coin/src/ffi/tw_any_address.rs b/rust/tw_any_coin/src/ffi/tw_any_address.rs index dc94948e479..4d239208ae0 100644 --- a/rust/tw_any_coin/src/ffi/tw_any_address.rs +++ b/rust/tw_any_coin/src/ffi/tw_any_address.rs @@ -81,6 +81,27 @@ pub unsafe extern "C" fn tw_any_address_is_valid_base58( AnyAddress::is_valid(coin, string, Some(prefix)) } +/// Determines if the string is a valid Any address with the given SS58 prefixes. +/// +/// \param string address to validate. +/// \param coin coin type of the address. +/// \param ss58 ss58 address prefix. +/// \return bool indicating if the address is valid. +#[no_mangle] +pub unsafe extern "C" fn tw_any_address_is_valid_ss58( + string: *const TWString, + coin: u32, + ss58: u16, +) -> bool { + let string = try_or_false!(TWString::from_ptr_as_ref(string)); + let string = try_or_false!(string.as_str()); + + let coin = try_or_false!(CoinType::try_from(coin)); + + let prefix = AddressPrefix::SubstrateNetwork(ss58); + AnyAddress::is_valid(coin, string, Some(prefix)) +} + /// Creates an address from a string representation and a coin type. Must be deleted with `TWAnyAddressDelete` after use. /// /// \param string address to create. @@ -179,6 +200,32 @@ pub unsafe extern "C" fn tw_any_address_create_base58_with_public_key( .unwrap_or_else(|_| std::ptr::null_mut()) } +/// Creates an SS58 Substrate address from a public key and a given ss58 prefix. +/// +/// \param public_key derivates the address from the public key. +/// \param coin coin type of the address. +/// \param ss58 SS58 address prefix. +/// \return TWAnyAddress pointer or nullptr if public key is invalid. +#[no_mangle] +pub unsafe extern "C" fn tw_any_address_create_ss58_with_public_key( + public_key: *mut TWPublicKey, + coin: u32, + ss58: u16, +) -> *mut TWAnyAddress { + let public_key = try_or_else!(TWPublicKey::from_ptr_as_ref(public_key), std::ptr::null_mut); + let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); + + let prefix = AddressPrefix::SubstrateNetwork(ss58); + AnyAddress::with_public_key( + coin, + public_key.as_ref().clone(), + Derivation::default(), + Some(prefix), + ) + .map(|any_address| TWAnyAddress(any_address).into_ptr()) + .unwrap_or_else(|_| std::ptr::null_mut()) +} + /// Deletes an address. /// /// \param address address to delete. diff --git a/rust/tw_any_coin/src/test_utils/address_utils.rs b/rust/tw_any_coin/src/test_utils/address_utils.rs index b7e896d2664..d7ed34a0b1c 100644 --- a/rust/tw_any_coin/src/test_utils/address_utils.rs +++ b/rust/tw_any_coin/src/test_utils/address_utils.rs @@ -7,7 +7,7 @@ use crate::ffi::tw_any_address::{ tw_any_address_create_with_public_key_derivation, tw_any_address_create_with_string, tw_any_address_data, tw_any_address_delete, tw_any_address_description, tw_any_address_is_valid, tw_any_address_is_valid_base58, tw_any_address_is_valid_bech32, - TWAnyAddress, + tw_any_address_is_valid_ss58, TWAnyAddress, }; use tw_coin_registry::coin_type::CoinType; use tw_coin_registry::registry::get_coin_item; @@ -80,6 +80,15 @@ pub fn test_address_valid(coin: CoinType, address: &str) { ); } +pub fn test_address_ss58_is_valid(coin: CoinType, address: &str, ss58: u16) { + let addr = TWStringHelper::create(address); + assert!( + unsafe { tw_any_address_is_valid_ss58(addr.ptr(), coin as u32, ss58) }, + "'{}' expected to be valid", + address + ); +} + pub fn test_address_invalid(coin: CoinType, address: &str) { let addr = TWStringHelper::create(address); assert!( @@ -89,6 +98,15 @@ pub fn test_address_invalid(coin: CoinType, address: &str) { ); } +pub fn test_address_ss58_is_invalid(coin: CoinType, address: &str, ss58: u16) { + let addr = TWStringHelper::create(address); + assert!( + !unsafe { tw_any_address_is_valid_ss58(addr.ptr(), coin as u32, ss58) }, + "'{}' expected to be invalid", + address + ); +} + pub fn test_address_get_data(coin: CoinType, address: &str, data_hex: &str) { let address_str = TWStringHelper::create(address); let any_address = TWAnyAddressHelper::wrap(unsafe { diff --git a/rust/tw_coin_entry/src/coin_context.rs b/rust/tw_coin_entry/src/coin_context.rs index c9952aa4eb5..bc80004caae 100644 --- a/rust/tw_coin_entry/src/coin_context.rs +++ b/rust/tw_coin_entry/src/coin_context.rs @@ -23,6 +23,9 @@ pub trait CoinContext { /// Optional p2sh prefix (Bitcoin specific). fn p2sh_prefix(&self) -> Option; + /// Optional ss58 prefix (Substrate specific). + fn ss58_prefix(&self) -> Option; + /// Returns coin derivations. fn derivations(&self) -> &[DerivationWithPath]; } diff --git a/rust/tw_coin_entry/src/prefix.rs b/rust/tw_coin_entry/src/prefix.rs index 059a8c57a8f..8b3a3618802 100644 --- a/rust/tw_coin_entry/src/prefix.rs +++ b/rust/tw_coin_entry/src/prefix.rs @@ -10,6 +10,7 @@ use crate::error::prelude::*; pub enum AddressPrefix { Hrp(String), BitcoinBase58(BitcoinBase58Prefix), + SubstrateNetwork(u16), } /// A blockchain's address prefix should be convertable from an `AddressPrefix`. diff --git a/rust/tw_coin_entry/src/test_utils/test_context.rs b/rust/tw_coin_entry/src/test_utils/test_context.rs index 9bc467a1138..5aaedceb221 100644 --- a/rust/tw_coin_entry/src/test_utils/test_context.rs +++ b/rust/tw_coin_entry/src/test_utils/test_context.rs @@ -51,6 +51,10 @@ impl CoinContext for TestCoinContext { self.p2sh } + fn ss58_prefix(&self) -> Option { + unimplemented!() + } + fn derivations(&self) -> &[DerivationWithPath] { unimplemented!() } diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index 7bff39d1ea0..ce69b29fde6 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -26,8 +26,10 @@ tw_misc = { path = "../tw_misc" } tw_native_evmos = { path = "../chains/tw_native_evmos" } tw_native_injective = { path = "../chains/tw_native_injective" } tw_pactus = { path = "../chains/tw_pactus" } +tw_polkadot = { path = "../chains/tw_polkadot" } tw_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } +tw_substrate = { path = "../frameworks/tw_substrate" } tw_sui = { path = "../chains/tw_sui" } tw_thorchain = { path = "../chains/tw_thorchain" } tw_ton = { path = "../chains/tw_ton" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index cde37c58cde..92420c6491a 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -20,6 +20,8 @@ pub enum BlockchainType { NativeEvmos, NativeInjective, Pactus, + Polkadot, + Kusama, Ronin, Solana, Sui, diff --git a/rust/tw_coin_registry/src/coin_context.rs b/rust/tw_coin_registry/src/coin_context.rs index f06cdf57ebc..9f38d9cd28b 100644 --- a/rust/tw_coin_registry/src/coin_context.rs +++ b/rust/tw_coin_registry/src/coin_context.rs @@ -45,6 +45,11 @@ impl CoinContext for CoinRegistryContext { self.item.p2sh_prefix } + #[inline] + fn ss58_prefix(&self) -> Option { + self.item.ss58_prefix + } + #[inline] fn derivations(&self) -> &[DerivationWithPath] { &self.item.derivation diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index d08784ccd56..ee3e958ea37 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -20,8 +20,10 @@ use tw_internet_computer::entry::InternetComputerEntry; use tw_native_evmos::entry::NativeEvmosEntry; use tw_native_injective::entry::NativeInjectiveEntry; use tw_pactus::entry::PactusEntry; +use tw_polkadot::entry::PolkadotEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; +use tw_substrate::entry::SubstrateEntry; use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; use tw_ton::entry::TheOpenNetworkEntry; @@ -41,6 +43,7 @@ const INTERNET_COMPUTER: InternetComputerEntry = InternetComputerEntry; const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; const PACTUS: PactusEntry = PactusEntry; +const POLKADOT: SubstrateEntry = SubstrateEntry(PolkadotEntry); const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; const SUI: SuiEntry = SuiEntry; @@ -62,6 +65,8 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&NATIVE_EVMOS), BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE), BlockchainType::Pactus => Ok(&PACTUS), + BlockchainType::Polkadot => Ok(&POLKADOT), + BlockchainType::Kusama => Ok(&POLKADOT), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Sui => Ok(&SUI), diff --git a/rust/tw_coin_registry/src/registry.rs b/rust/tw_coin_registry/src/registry.rs index 29825f4c58e..48a270a45e9 100644 --- a/rust/tw_coin_registry/src/registry.rs +++ b/rust/tw_coin_registry/src/registry.rs @@ -36,6 +36,7 @@ pub struct CoinItem { pub hrp: Option, pub p2pkh_prefix: Option, pub p2sh_prefix: Option, + pub ss58_prefix: Option, } #[inline] diff --git a/rust/tw_number/src/u256.rs b/rust/tw_number/src/u256.rs index 1b2d2df4c17..e6136b8ce29 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); @@ -178,19 +177,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; @@ -245,6 +284,7 @@ impl_map_from!(U256, u8); impl_map_from!(U256, u16); impl_map_from!(U256, u32); impl_map_from!(U256, u64); +impl_map_from!(U256, u128); impl_map_from!(U256, usize); #[cfg(test)] diff --git a/rust/tw_scale/Cargo.toml b/rust/tw_scale/Cargo.toml new file mode 100644 index 00000000000..9f593456176 --- /dev/null +++ b/rust/tw_scale/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "tw_scale" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_hash = { path = "../tw_hash" } +tw_number = { path = "../tw_number" } diff --git a/rust/tw_scale/src/lib.rs b/rust/tw_scale/src/lib.rs new file mode 100644 index 00000000000..2da23537483 --- /dev/null +++ b/rust/tw_scale/src/lib.rs @@ -0,0 +1,398 @@ +use tw_hash::Hash; +use tw_number::U256; + +pub mod macros; + +/// +/// SCALE encoding implementation (see https://docs.substrate.io/reference/scale-codec) +/// +pub trait ToScale { + 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_into(&self, out: &mut Vec) { + out.push(*self as u8); + } +} + +macro_rules! fixed_impl { + ($($t:ty),+) => { + $(impl ToScale for $t { + fn to_scale_into(&self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()) + } + })+ + }; +} + +fixed_impl!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize); + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub struct Compact(pub T); + +// SCALE encoding for common Compact numbers. + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + Compact(U256::from(self.0)).to_scale_into(out) + } +} + +const COMPACT_1_BYTE_MAX: u32 = 0b0011_1111; +const COMPACT_2_BYTE_MAX: u32 = 0b0011_1111_1111_1111; +const COMPACT_4_BYTE_MAX: u32 = 0b0011_1111_1111_1111_1111_1111_1111_1111; + +impl ToScale for Compact { + fn to_scale_into(&self, out: &mut Vec) { + match u32::try_from(self.0) { + Ok(val) if val <= COMPACT_1_BYTE_MAX => out.push((val as u8) << 2), + Ok(val) if val <= COMPACT_2_BYTE_MAX => (((val as u16) << 2) | 0b01).to_scale_into(out), + Ok(val) if val <= COMPACT_4_BYTE_MAX => ((val << 2) | 0b10).to_scale_into(out), + _ => { + let bytes = self.0.to_little_endian_compact(); + let bytes_needed = bytes.len(); + out.reserve(bytes_needed); + out.push(0b11 + ((bytes_needed - 4) << 2) as u8); + out.extend_from_slice(&bytes[..]); + }, + } + } +} + +impl ToScale for Hash { + fn to_scale_into(&self, out: &mut Vec) { + out.extend_from_slice(self.as_slice()); + } +} + +impl ToScale for Option +where + T: ToScale, +{ + fn to_scale_into(&self, out: &mut Vec) { + if let Some(t) = &self { + out.push(1u8); + t.to_scale_into(out); + } else { + out.push(0u8); + } + } +} + +impl ToScale for &[T] +where + T: ToScale, +{ + fn to_scale_into(&self, out: &mut Vec) { + Compact(self.len()).to_scale_into(out); + for ts in self.iter() { + ts.to_scale_into(out); + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RawOwned(pub Vec); + +impl RawOwned { + pub fn new(val: T) -> Self { + Self(val.to_scale()) + } +} + +impl ToScale for RawOwned { + fn to_scale_into(&self, out: &mut Vec) { + out.extend(&self.0); + } +} + +impl ToScale for Vec +where + T: ToScale, +{ + fn to_scale_into(&self, out: &mut Vec) { + self.as_slice().to_scale_into(out) + } +} + +impl ToScale for &T { + fn to_scale_into(&self, out: &mut Vec) { + (*self).to_scale_into(out) + } +} + +#[cfg(test)] +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(0u128).to_scale(), &[0x00]); + assert_eq!(Compact(1u128).to_scale(), &[0x04]); + assert_eq!(Compact(18u128).to_scale(), &[0x48]); + assert_eq!(Compact(42u128).to_scale(), &[0xa8]); + assert_eq!(Compact(63u128).to_scale(), &[0xfc]); + assert_eq!(Compact(64u128).to_scale(), &[0x01, 0x01]); + assert_eq!(Compact(69u128).to_scale(), &[0x15, 0x01]); + assert_eq!(Compact(12345u128).to_scale(), &[0xe5, 0xc0]); + assert_eq!(Compact(16383u128).to_scale(), &[0xfd, 0xff]); + assert_eq!(Compact(16384u128).to_scale(), &[0x02, 0x00, 0x01, 0x00]); + assert_eq!(Compact(65535u128).to_scale(), &[0xfe, 0xff, 0x03, 0x00]); + assert_eq!( + Compact(1073741823u128).to_scale(), + &[0xfe, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(1073741824u128).to_scale(), + &[0x03, 0x00, 0x00, 0x00, 0x40] + ); + assert_eq!( + Compact(4294967295u128).to_scale(), + &[0x03, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(4294967296u128).to_scale(), + &[0x07, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(100000000000000u128).to_scale(), + &[0x0b, 0x00, 0x40, 0x7a, 0x10, 0xf3, 0x5a] + ); + assert_eq!( + Compact(1099511627776u128).to_scale(), + &[0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(281474976710656u128).to_scale(), + &[0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(72057594037927935u128).to_scale(), + &[0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] + ); + assert_eq!( + Compact(72057594037927936u128).to_scale(), + &[0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + ); + assert_eq!( + Compact(18446744073709551615u128).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_option() { + assert_eq!(Some(1u8).to_scale(), &[0x01, 0x01]); + assert_eq!(None::.to_scale(), &[0x00]); + assert_eq!(Some(Compact(1u64)).to_scale(), &[0x01, 0x04]); + } + + #[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_scale/src/macros.rs b/rust/tw_scale/src/macros.rs new file mode 100644 index 00000000000..3e5e345ba8d --- /dev/null +++ b/rust/tw_scale/src/macros.rs @@ -0,0 +1,216 @@ +#[macro_export] +macro_rules! replace_ident { + ($_t:tt $sub:ident) => { + $sub + }; +} + +#[macro_export] +macro_rules! replace_expr { + ($_t:tt $sub:expr) => { + $sub + }; +} + +/// Macro to implement `ToScale` trait for a struct. +/// +/// # Example +/// ```rust +/// use tw_scale::{impl_struct_scale, ToScale}; +/// +/// impl_struct_scale!( +/// #[derive(Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq)] +/// pub struct TestStruct { +/// id: u8, +/// id2: u8, +/// data: Vec, +/// } +/// ); +/// ``` +#[macro_export] +macro_rules! impl_struct_scale { + // "New Type" struct + ( + $(#[$struct_meta:meta])* + pub struct $struct_name:ident ( + $(#[$struct_field_meta:meta])* + $struct_field_vis:vis $struct_field_ty:ty + ); + ) => { + $(#[$struct_meta])* + pub struct $struct_name( + $(#[$struct_field_meta])* + $struct_field_vis $struct_field_ty + ); + + impl $crate::ToScale for $struct_name { + fn to_scale_into(&self, out: &mut Vec) { + self.0.to_scale_into(out); + } + } + }; + // Normal named fields struct. + ($(#[$struct_meta:meta])* + pub struct $struct_name:ident { + $( + $(#[$struct_field_meta:meta])* + $struct_field_vis:vis $struct_field_name:ident : $struct_field_ty:ty + ),+ $(,)? + }) => { + $(#[$struct_meta])* + pub struct $struct_name { + $( + $(#[$struct_field_meta])* + $struct_field_vis $struct_field_name : $struct_field_ty + ),+ + } + + impl $crate::ToScale for $struct_name { + fn to_scale_into(&self, out: &mut Vec) { + $(self.$struct_field_name.to_scale_into(out);)+ + } + } + } +} + +/// Macro to implement `ToScale` trait for an enum. +/// +/// # Example +/// ```rust +/// use tw_scale::{impl_enum_scale, ToScale}; +/// +/// impl_enum_scale!( +/// #[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +/// pub enum TestEnum { +/// #[default] +/// Variant0 = 0, +/// Variant1(u8) = 1, +/// Variant10 = 10, +/// StructVariant { +/// id: u8, +/// id2: u8, +/// } = 11, +/// } +/// ); +/// ``` +#[macro_export] +macro_rules! impl_enum_scale { + ( + $(#[$enum_meta:meta])* + pub enum $enum_name:ident { + $( + $(#[$variant_meta:meta])* + $variant_name:ident $(($variant_tuple_ty:ty))? $({ + $( + $variant_field_name:ident : $variant_field_ty:ty + ),+ $(,)? + })? = $variant_index:expr, + )* + } + ) => { + $(#[$enum_meta])* + #[repr(u8)] + pub enum $enum_name { + $( + $(#[$variant_meta])* + $variant_name $(($variant_tuple_ty))? $({ + $( + $variant_field_name : $variant_field_ty + ),+ + })? = $variant_index, + )* + } + + impl $crate::ToScale for $enum_name { + fn to_scale_into(&self, out: &mut Vec) { + match self { + $( + Self::$variant_name $(($crate::replace_ident!($variant_tuple_ty p0)))? $({ + $($variant_field_name),+ + })? => { + // variant index. + out.push($variant_index as u8); + // Encode tuple variants. + $( + $crate::replace_expr!($variant_tuple_ty { + p0.to_scale_into(out); + }); + )? + // Encode struct variants. + $( + $($variant_field_name.to_scale_into(out);)+ + )? + } + )* + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::ToScale; + + impl_struct_scale!( + /// Test struct. + #[derive(Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct TestStruct { + /// Field id. + id: u8, + id2: u8, + data: Vec, + } + ); + + #[test] + fn test_struct_scale() { + assert_eq!( + TestStruct { + id: 1, + id2: 2, + data: vec![3], + } + .to_scale(), + &[0x01, 0x02, 0x04, 0x03] + ); + } + + impl_struct_scale!( + /// Test new type struct. + #[derive(Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct TestNewType(pub u8); + ); + + #[test] + fn test_new_type_struct_scale() { + assert_eq!(TestNewType(1).to_scale(), &[0x01]); + } + + impl_enum_scale!( + /// Test enum. + #[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub enum TestEnum { + /// Default variant. + #[default] + Variant0 = 0, + /// Tuple variant. + Variant1(u8) = 0x01, + /// Variant with index 10. + Variant10 = 10, + /// Struct variant. + Struct { id: u8, id2: u8 } = 11, + } + ); + + #[test] + fn test_enum_scale() { + assert_eq!(TestEnum::Variant0.to_scale(), &[0x00]); + assert_eq!(TestEnum::Variant1(2).to_scale(), &[0x01, 0x02]); + assert_eq!(TestEnum::Variant10.to_scale(), &[0x0A]); + assert_eq!( + TestEnum::Struct { id: 1, id2: 2 }.to_scale(), + &[0x0B, 0x01, 0x02] + ); + } +} diff --git a/rust/tw_ss58_address/Cargo.toml b/rust/tw_ss58_address/Cargo.toml new file mode 100644 index 00000000000..a688bf11ddf --- /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] +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" } +tw_scale = { path = "../tw_scale" } diff --git a/rust/tw_ss58_address/src/lib.rs b/rust/tw_ss58_address/src/lib.rs new file mode 100644 index 00000000000..f4314d40d16 --- /dev/null +++ b/rust/tw_ss58_address/src/lib.rs @@ -0,0 +1,441 @@ +use std::fmt::Formatter; +use std::str::FromStr; + +use tw_coin_entry::error::prelude::*; +use tw_encoding::{base58, hex}; +use tw_hash::blake2::blake2_b; +use tw_keypair::ed25519::sha512::PublicKey; +use tw_scale::ToScale; + +// +// 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 +// + +/// Represents a Substrate network identifier used in SS58 addresses. +/// +/// The network ID is a 16-bit unsigned integer that identifies different Substrate-based networks. +/// Valid network IDs range from 0 to 16383 (0x3fff). +/// +/// Common network IDs include: +/// - 0: Polkadot +/// - 2: Kusama +/// - 5: Astar +/// - 12: Polymesh +/// - 42: Generic Substrate +/// - 172: Parallel +#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct NetworkId(u16); + +impl NetworkId { + /// Creates a new NetworkId without checking the value range. + /// This should only be used for constant values that are known to be valid. + pub const fn new_unchecked(value: u16) -> Self { + Self(value) + } + + /// Creates a new NetworkId from a u16 value, checking that it's within the valid range (0..=0x3fff). + pub fn from_u16(value: u16) -> AddressResult { + match value { + 0..=0x3fff => Ok(Self::new_unchecked(value)), + _ => Err(AddressError::InvalidInput), + } + } + + /// Extracts a NetworkId from the prefix bytes of an SS58 address. + /// The first byte determines if it's a single-byte (0..=63) or double-byte (64..=127) prefix. + pub fn from_bytes(bytes: &[u8]) -> AddressResult { + if bytes.is_empty() { + return Err(AddressError::MissingPrefix); + } + + match bytes[0] { + 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((lower as u16) | ((upper as u16) << 8)) + }, + _ => Err(AddressError::UnexpectedAddressPrefix), + } + .map(Self::new_unchecked) + } + + /// Returns the raw network identifier value. + pub fn value(&self) -> u16 { + self.0 + } + + /// Returns the length of the network prefix in bytes (1 or 2). + pub fn prefix_len(&self) -> usize { + if self.value() < 64 { + 1 + } else { + 2 + } + } + + /// Converts the network ID to its byte representation used in SS58 addresses. + pub fn to_bytes(&self) -> Vec { + let network = self.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] + }, + } + } +} + +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) + } +} + +/// Represents a Substrate SS58 address. +/// +/// SS58 is an address format designed for Substrate-based chains. It is derived from +/// the base-58 encoding format with some modifications. Each address contains: +/// - A network identifier prefix +/// - A public key (32 bytes) +/// - A checksum +/// +/// The format ensures that addresses are human-readable and network-specific, +/// preventing accidental cross-chain transactions. +/// +/// # Example +/// ``` +/// use tw_ss58_address::{SS58Address, NetworkId}; +/// use std::str::FromStr; +/// +/// // Parse an existing SS58 address +/// let address = SS58Address::from_str("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu").unwrap(); +/// assert_eq!(address.network().value(), 0); // Polkadot network +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SS58Address { + /// The 32-byte public key component of the address + key: Vec, + /// The network identifier component of the address + network: NetworkId, +} + +impl Default for SS58Address { + fn default() -> Self { + Self { + key: vec![0; Self::KEY_SIZE], + network: Default::default(), + } + } +} + +impl SS58Address { + /// Size of the checksum in bytes + const CHECKSUM_SIZE: usize = 2; + /// Size of the public key in bytes + const KEY_SIZE: usize = 32; + /// SS58 format registry prefix used in checksum calculation + const SS58_PREFIX: &'static [u8] = b"SS58PRE"; + + /// Computes the Blake2-b checksum for the address data + fn compute_expected_checksum(decoded: &[u8]) -> Vec { + let mut data = Vec::from(Self::SS58_PREFIX); + data.extend(decoded); + + let mut bytes = blake2_b(&data, 64).expect("hash length should be valid"); + bytes.truncate(Self::CHECKSUM_SIZE); + bytes + } + + /// Parses an SS58 address from its base-58 string representation. + /// + /// # Errors + /// - `AddressError::FromBase58Error` if the string is not valid base-58 + /// - `AddressError::MissingPrefix` if the decoded bytes are empty + /// - `AddressError::UnexpectedAddressPrefix` if the prefix is invalid + /// - `AddressError::InvalidChecksum` if the checksum verification fails + pub fn parse(repr: &str) -> AddressResult { + let decoded = base58::decode(repr, base58::Alphabet::Bitcoin) + .map_err(|_| AddressError::FromBase58Error)?; + + let network = NetworkId::from_bytes(&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, + }) + } + + /// Creates an SS58 address from a public key and network identifier. + pub fn from_public_key(key: &PublicKey, network: NetworkId) -> AddressResult { + Ok(Self { + key: key.as_slice().to_owned(), + network, + }) + } + + /// Returns the network identifier of the address. + pub fn network(&self) -> NetworkId { + self.network + } + + /// Returns the public key bytes of the address. + pub fn key_bytes(&self) -> &[u8] { + &self.key + } + + /// Returns the complete address as bytes, including network prefix, public key, and checksum. + 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 + } + + /// Returns the base-58 string representation of the address. + pub fn to_base58_string(&self) -> String { + base58::encode(&self.to_bytes(), base58::Alphabet::Bitcoin) + } + + /// Returns the hexadecimal string representation of the address bytes. + pub fn to_hex_string(&self) -> String { + hex::encode(self.to_bytes(), false) + } +} + +impl ToScale for SS58Address { + fn to_scale_into(&self, out: &mut Vec) { + out.extend_from_slice(self.key_bytes()) + } +} + +impl FromStr for SS58Address { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + +impl std::fmt::Display for SS58Address { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_base58_string()) + } +} + +#[cfg(test)] +mod tests { + use super::{NetworkId, SS58Address}; + use std::str::FromStr; + use tw_coin_entry::error::prelude::AddressError; + use tw_keypair::ed25519::sha512::PublicKey; + + const POLKADOT: NetworkId = NetworkId::new_unchecked(0); + const KUSAMA: NetworkId = NetworkId::new_unchecked(2); + const ASTAR: NetworkId = NetworkId::new_unchecked(5); + const PARALLEL: NetworkId = NetworkId::new_unchecked(172); + const GENERIC_SUBSTRATE: NetworkId = NetworkId::new_unchecked(42); + + 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_network_from_bytes() { + fn test_case(prefix: &[u8], expected_network: u16) { + 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!( + network.prefix_len(), + expected_prefix_length, + "for expected network {}", + expected_network + ); + assert_eq!( + network.value(), + 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_network_from_bytes_invalid() { + // at least one byte is expected + let res = NetworkId::from_bytes(&[]); + assert_eq!(res, Err(AddressError::MissingPrefix)); + + // the first byte should be in 0..=127 + let res = NetworkId::from_bytes(&[0xFF]); + assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); + + // a second byte should follow + let res = NetworkId::from_bytes(&[0x40]); + assert_eq!(res, Err(AddressError::UnexpectedAddressPrefix)); + } + + #[test] + fn test_network_as_bytes() { + fn test_case(expected_prefix: &[u8], network: u16) { + let prefix = NetworkId::new_unchecked(network).to_bytes(); + + assert_eq!(prefix, expected_prefix, "for network {}", network); + } + + for (prefix, network) in networks() { + test_case(&prefix, network); + } + } + + #[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", + POLKADOT.value(), + ); + test_case( + "5CK8D1sKNwF473wbuBP6NuhQfPaWUetNsWUNAAzVwTfxqjfr", + GENERIC_SUBSTRATE.value(), + ); + test_case( + "CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp", + KUSAMA.value(), + ); + test_case( + "Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", + KUSAMA.value(), + ); + 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, 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, ASTAR).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, PARALLEL).expect("error creating address"); + assert_eq!(addr.network().value(), 172); + assert_eq!(addr.key_bytes(), key.as_slice()); + } + + #[test] + fn test_as_base58_string() { + fn test_case(repr: &str) { + let addr = SS58Address::from_str(repr).expect("error parsing address"); + assert_eq!(addr.to_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.to_hex_string(), + "000aff6865635ae11013a83835c019d44ec3f865145943f487ae82a8e7bed3a66b29d7" + ); + } +} diff --git a/rust/tw_tests/tests/chains/kusama/kusama_address.rs b/rust/tw_tests/tests/chains/kusama/kusama_address.rs new file mode 100644 index 00000000000..9f45db702d6 --- /dev/null +++ b/rust/tw_tests/tests/chains/kusama/kusama_address.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{test_address_invalid, test_address_valid}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_kusama_address_is_valid() { + // Kusama ed25519 + test_address_valid( + CoinType::Kusama, + "FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ", + ); + // Kusama secp256k1 + test_address_valid( + CoinType::Kusama, + "FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx", + ); + // Kusama sr25519 + test_address_valid( + CoinType::Kusama, + "EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe", + ); +} + +#[test] +fn test_kusama_address_invalid() { + // Substrate ed25519 + test_address_invalid( + CoinType::Kusama, + "5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ", + ); + // Polkadot ed25519 + test_address_invalid( + CoinType::Kusama, + "15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony", + ); + // Polkadot sr25519 + test_address_invalid( + CoinType::Kusama, + "15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony", + ); + // Bitcoin + test_address_invalid(CoinType::Kusama, "1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA"); +} diff --git a/rust/tw_tests/tests/chains/kusama/mod.rs b/rust/tw_tests/tests/chains/kusama/mod.rs new file mode 100644 index 00000000000..750831f7407 --- /dev/null +++ b/rust/tw_tests/tests/chains/kusama/mod.rs @@ -0,0 +1 @@ +mod kusama_address; diff --git a/rust/tw_tests/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs index d048ec74a00..ab7a59fccb1 100644 --- a/rust/tw_tests/tests/chains/mod.rs +++ b/rust/tw_tests/tests/chains/mod.rs @@ -13,9 +13,12 @@ mod ecash; mod ethereum; mod greenfield; mod internet_computer; +mod kusama; mod native_evmos; mod native_injective; mod pactus; +mod polkadot; +mod polymesh; mod solana; mod sui; mod tbinance; diff --git a/rust/tw_tests/tests/chains/polkadot/mod.rs b/rust/tw_tests/tests/chains/polkadot/mod.rs new file mode 100644 index 00000000000..5e57ab33dc0 --- /dev/null +++ b/rust/tw_tests/tests/chains/polkadot/mod.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_any_coin::test_utils::sign_utils::{CompilerHelper, PreImageHelper}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::{DecodeHex, ToHex}; +use tw_keypair::ed25519::{sha512::PublicKey, Signature}; +use tw_keypair::traits::VerifyingKeyTrait; +use tw_proto::Common::Proto::SigningError; +use tw_proto::Polkadot::Proto::{self, SigningInput}; +use tw_proto::TxCompiler::Proto::{self as CompilerProto, PreSigningOutput}; + +mod polkadot_address; +mod polkadot_compile; +mod polkadot_sign; + +const GENESIS_HASH: &str = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"; +const POLYMESH_GENESIS_HASH: &str = + "6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"; +const PRIVATE_KEY: &str = "abf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115"; +const PRIVATE_KEY_IOS: &str = "37932b086586a6675e66e562fe68bd3eeea4177d066619c602fe3efc290ada62"; +const PRIVATE_KEY_2: &str = "70a794d4f1019c3ce002f33062f45029c4f930a56b3d20ec477f7668c6bbc37f"; +const PRIVATE_KEY_POLKADOT: &str = + "298fcced2b497ed48367261d8340f647b3fca2d9415d57c2e3c5ef90482a2266"; +const ACCOUNT_2: &str = "14Ztd3KJDaB9xyJtRkREtSZDdhLSbm7UUKt8Z7AwSv7q85G2"; + +pub fn helper_sign(coin: CoinType, input: SigningInput<'_>) -> String { + let mut signer = AnySignerHelper::::default(); + let signed_output = signer.sign(coin, input); + assert_eq!(signed_output.error, SigningError::OK); + + signed_output.encoded.to_hex() +} + +pub fn helper_encode(coin: CoinType, input: &SigningInput<'_>) -> String { + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(coin, input); + + assert_eq!(preimage_output.error, SigningError::OK); + preimage_output.data.to_hex() +} + +pub fn helper_encode_and_maybe_sign( + coin: CoinType, + input: SigningInput<'_>, +) -> (String, Option) { + // Step 1: Obtain preimage hash + let preimage = helper_encode(coin, &input); + if input.private_key.is_empty() { + return (preimage, None); + } + + // Step 2: If we have the private key try signing. + let signed = helper_sign(coin, input); + + (preimage, Some(signed)) +} + +pub fn helper_encode_and_compile( + coin: CoinType, + input: Proto::SigningInput, + signature: &str, + public_key: &str, + ed25519: bool, +) -> (String, String) { + // Step 1: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(coin, &input); + + assert_eq!(preimage_output.error, SigningError::OK); + let preimage = preimage_output.data.to_hex(); + + // Step 2: Compile transaction info + + // Simulate signature, normally obtained from signature server + let signature_bytes = signature.decode_hex().unwrap(); + let public_key = public_key.decode_hex().unwrap(); + + // Verify signature (pubkey & hash & signature) + if !ed25519 { + let signature = Signature::try_from(signature_bytes.as_slice()).unwrap(); + let public = PublicKey::try_from(public_key.as_slice()).unwrap(); + assert!(public.verify(signature, preimage_output.data.into())); + } + + // Compile transaction info + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile(coin, &input, vec![signature_bytes], vec![public_key]); + assert_eq!(output.error, SigningError::OK); + let signed = output.encoded.to_hex(); + + (preimage, signed) +} + +pub fn balance_call( + call: Proto::mod_Balance::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { + message_oneof: call, + }) +} + +pub fn staking_call( + call: Proto::mod_Staking::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::staking_call(Proto::Staking { + message_oneof: call, + }) +} + +pub fn polymesh_call( + call: Proto::mod_Identity::OneOfmessage_oneof, +) -> Proto::mod_SigningInput::OneOfmessage_oneof { + Proto::mod_SigningInput::OneOfmessage_oneof::polymesh_call(Proto::PolymeshCall { + message_oneof: Proto::mod_PolymeshCall::OneOfmessage_oneof::identity_call( + Proto::Identity { + message_oneof: call, + }, + ), + }) +} diff --git a/rust/tw_tests/tests/chains/polkadot/polkadot_address.rs b/rust/tw_tests/tests/chains/polkadot/polkadot_address.rs new file mode 100644 index 00000000000..a734122a88d --- /dev/null +++ b/rust/tw_tests/tests/chains/polkadot/polkadot_address.rs @@ -0,0 +1,89 @@ +// 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_ss58_is_valid, test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_polkadot_address_normalization() { + test_address_normalization( + CoinType::Polkadot, + "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + ); +} + +#[test] +fn test_polkadot_address_is_valid() { + test_address_valid( + CoinType::Polkadot, + "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + ); + test_address_ss58_is_valid( + CoinType::Polkadot, + "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + 0, + ); + + // Polkadot ed25519 + test_address_valid( + CoinType::Polkadot, + "15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", + ); + // Polkadot sr25519 + test_address_valid( + CoinType::Polkadot, + "15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony", + ); + + test_address_ss58_is_valid( + CoinType::Polkadot, + "cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", + 64, + ); +} + +#[test] +fn test_polkadot_address_invalid() { + test_address_invalid(CoinType::Polkadot, ""); + test_address_invalid( + CoinType::Polkadot, + "5DhgpiQ6za7k5osGUFXpKgjiLQKYYRDWmNH9eX4og9Q48huk...", + ); + + // Substrate ed25519 + test_address_invalid( + CoinType::Polkadot, + "5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ", + ); + // Bitcoin + test_address_invalid(CoinType::Polkadot, "1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA"); + // Kusama ed25519 + test_address_invalid( + CoinType::Polkadot, + "FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ", + ); + // Kusama secp256k1 + test_address_invalid( + CoinType::Polkadot, + "FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx", + ); + // Kusama sr25519 + test_address_invalid( + CoinType::Polkadot, + "EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe", + ); +} + +#[test] +fn test_polkadot_address_get_data() { + test_address_get_data( + CoinType::Polkadot, + "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + "0x4870d56d074c50e891506d78faa4fb69ca039cc5f131eb491e166b975880e867", + ); +} diff --git a/rust/tw_tests/tests/chains/polkadot/polkadot_compile.rs b/rust/tw_tests/tests/chains/polkadot/polkadot_compile.rs new file mode 100644 index 00000000000..1f622f427c3 --- /dev/null +++ b/rust/tw_tests/tests/chains/polkadot/polkadot_compile.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::polkadot::{balance_call, helper_encode_and_compile, GENESIS_HASH}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_proto::Polkadot::Proto::{self, mod_Balance::Transfer}; + +#[test] +fn test_polkadot_compile_transfer() { + // Step 1: Prepare input. + let block_hash = "40cee3c3b7f8422f4c512e9ebebdeeff1c28e81cc678ee4864d945d641e05f9b" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + let value = "210fdc0c00".decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 25, + transaction_version: 5, + era: Some(Proto::Era { + block_number: 5898150, + period: 10000, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "15JWiQUmczAFU3hrZrD2gDyuJdL2BbFaX9yngivb1UWiBJWA".into(), + value: value.into(), + ..Default::default() + })), + ..Default::default() + }; + + // Simulate signature, normally obtained from signature server + let signature = "fb43727477caaa12542b9060856816d42eedef6ebf2e98e4f8dff4355fe384751925833c4a26b2fed1707aebe655cb3317504a61ee59697c086f7baa6ca06a09"; + let public_key = "d84accbb64934815506288fafbfc7d275e64aa4e3cd9c5392db6e83b13256bf3"; + + let (preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + + assert_eq!( + preimage, + "0500be4c21aa92dcba057e9b719ce1de970f774f064c09b13a3ea3009affb8cb5ec707000cdc0f219dfe0000190000000500000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c340cee3c3b7f8422f4c512e9ebebdeeff1c28e81cc678ee4864d945d641e05f9b" + ); + assert_eq!( + signed, + "390284d84accbb64934815506288fafbfc7d275e64aa4e3cd9c5392db6e83b13256bf300fb43727477caaa12542b9060856816d42eedef6ebf2e98e4f8dff4355fe384751925833c4a26b2fed1707aebe655cb3317504a61ee59697c086f7baa6ca06a099dfe00000500be4c21aa92dcba057e9b719ce1de970f774f064c09b13a3ea3009affb8cb5ec707000cdc0f21" + ); +} diff --git a/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs b/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs new file mode 100644 index 00000000000..98b8631c1d0 --- /dev/null +++ b/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs @@ -0,0 +1,1013 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::polkadot::{ + balance_call, helper_encode, helper_encode_and_maybe_sign, helper_sign, polymesh_call, + staking_call, ACCOUNT_2, GENESIS_HASH, POLYMESH_GENESIS_HASH, PRIVATE_KEY, PRIVATE_KEY_2, + PRIVATE_KEY_IOS, PRIVATE_KEY_POLKADOT, +}; +use std::borrow::Cow; +use tw_any_coin::any_address::AnyAddress; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_keypair::ed25519::sha512::PrivateKey; +use tw_number::U256; +use tw_proto::Polkadot::Proto; +use tw_proto::Polkadot::Proto::mod_Balance::{AssetTransfer, BatchAssetTransfer, Transfer}; + +use super::helper_encode_and_compile; + +fn custom_call_indices(module: u8, method: u8) -> Option { + Some(Proto::CallIndices { + variant: Proto::mod_CallIndices::OneOfvariant::custom(Proto::CustomCallIndices { + module_index: module as i32, + method_index: method as i32, + }), + }) +} + +// TEST(TWAnySignerPolkadot, SignTransfer_9fd062) +#[test] +fn test_polkadot_sign_transfer() { + // Step 1: Prepare input. + let to_address = "13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"; + { + let private_key = PrivateKey::try_from(PRIVATE_KEY_2).unwrap(); + let public_key = private_key.public(); + let address = AnyAddress::with_string(CoinType::Polkadot, ACCOUNT_2, None) + .unwrap() + .get_data() + .unwrap(); + assert_eq!(public_key.as_slice().to_vec(), address); + } + let private_key = PRIVATE_KEY_2.decode_hex().unwrap(); + let block_hash = "5d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + let value = 2000000000u64; + + let input = Proto::SigningInput { + network: 0, + private_key: private_key.into(), + nonce: 3, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 26, + transaction_version: 5, + era: Some(Proto::Era { + block_number: 3541050, + period: 64, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: to_address.into(), + value: Cow::Owned(U256::from(value).to_big_endian().to_vec()), + ..Default::default() + })), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/0x9fd06208a6023e489147d8d93f0182b0cb7e45a40165247319b87278e08362d8 + let (preimage, signed) = helper_encode_and_maybe_sign(CoinType::Polkadot, input); + assert_eq!(preimage, "05007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577a5030c001a0000000500000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); + + assert_eq!( + signed.as_deref(), + Some("3502849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f7830073e59cef381aedf56d7af076bafff9857ffc1e3bd7d1d7484176ff5b58b73f1211a518e1ed1fd2ea201bd31869c0798bba4ffe753998c409d098b65d25dff801a5030c0005007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577") + ); +} + +// TEST(TWAnySignerPolkadot, SignTransferDOT) +#[test] +fn test_polkadot_sign_transfer_dot() { + let block_hash = "343a3f4258fd92f5ca6ca5abdf473d86a78b0bcd0dc09c568ca594245cc8c642" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 17, + transaction_version: 3, + era: Some(Proto::Era { + block_number: 927699, + period: 8, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3".into(), + value: Cow::Owned(U256::from(12345u64).to_big_endian().to_vec()), + ..Default::default() + })), + ..Default::default() + }; + + let (preimage, signed) = helper_encode_and_maybe_sign(CoinType::Polkadot, input); + assert_eq!(preimage, "05008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c032000000110000000300000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3343a3f4258fd92f5ca6ca5abdf473d86a78b0bcd0dc09c568ca594245cc8c642"); + + assert_eq!( + signed.as_deref(), + Some("29028488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee003d91a06263956d8ce3ce5c55455baefff299d9cb2bb3f76866b6828ee4083770b6c03b05d7b6eb510ac78d047002c1fe5c6ee4b37c9c5a8b09ea07677f12e50d3200000005008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0") + ); +} + +// TEST(TWAnySignerPolkadot, SignTransfer_72dd5b) +#[test] +fn test_polkadot_sign_transfer_ios() { + let block_hash = "7d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY_IOS.decode_hex().unwrap().into(), + nonce: 1, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 28, + transaction_version: 6, + era: Some(Proto::Era { + block_number: 3910736, + period: 64, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5".into(), + value: Cow::Owned(U256::from(10000000000u64).to_big_endian().to_vec()), + ..Default::default() + })), + ..Default::default() + }; + + let (preimage, signed) = helper_encode_and_maybe_sign(CoinType::Polkadot, input); + assert_eq!(preimage, "0500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402050104001c0000000600000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c37d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd"); + assert_eq!(signed.as_deref(), Some("410284008d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa0038ec4973ab9773dfcbf170b8d27d36d89b85c3145e038d68914de83cf1f7aca24af64c55ec51ba9f45c5a4d74a9917dee380e9171108921c3e5546e05be15206050104000500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402")); +} + +// TEST(TWAnySignerPolkadot, SignBond_8da66d) +#[test] +fn test_polkadot_sign_bond() { + let block_hash = "f1eee612825f29abd3299b486e401299df2faa55b7ce1e34bf2243bd591905fc" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY_2.decode_hex().unwrap().into(), + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 26, + transaction_version: 5, + era: Some(Proto::Era { + block_number: 3540912, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond( + Proto::mod_Staking::Bond { + controller: ACCOUNT_2.into(), + value: Cow::Owned(U256::from(11000000000u64).to_big_endian().to_vec()), + reward_destination: Proto::RewardDestination::STASH.into(), + ..Default::default() + }, + )), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/0x8da66d3fe0f592cff714ec107289370365117a1abdb72a19ac91181fdcf62bba + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!( + signed, + "3d02849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f783009025843bc49c1c4fbc99dbbd290c92f9879665d55b02f110abfb4800f0e7630877d2cffd853deae7466c22fbc8616a609e1b92615bb365ea8adccba5ef7624050503000007009dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f7830700aea68f0201" + ); +} + +// TEST(TWAnySignerPolkadot, SignBondAndNominate_4955314_2) +#[test] +fn test_polkadot_sign_bond_and_nominate() { + let private_key = "7f44b19b391a8015ca4c7d94097b3695867a448d1391e7f3243f06987bdb6858" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: private_key.into(), + nonce: 4, + block_hash: genesis_hash.clone().into(), + genesis_hash: genesis_hash.into(), + spec_version: 30, + transaction_version: 7, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond_and_nominate( + Proto::mod_Staking::BondAndNominate { + controller: "13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5".into(), + value: Cow::Owned(U256::from(10000000000u64).to_big_endian().to_vec()), + reward_destination: Proto::RewardDestination::STASH.into(), + nominators: vec![ + "1zugcavYA9yCuYwiEYeMHNJm9gXznYjNfXQjZsZukF1Mpow".into(), + "15oKi7HoBQbwwdQc47k71q4sJJWnu5opn1pqoGx4NAEYZSHs".into(), + ], + ..Default::default() + }, + )), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/4955314-2 + let signed = helper_sign(CoinType::Polkadot, input); + + assert_eq!(signed, "6103840036092fac541e0e5feda19e537c679b487566d7101141c203ac8322c27e5f076a00a8b1f859d788f11a958e98b731358f89cf3fdd41a667ea992522e8d4f46915f4c03a1896f2ac54bdc5f16e2ce8a2a3bf233d02aad8192332afd2113ed6688e0d0010001a02080700007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b540201070508002c2a55b5ffdca266bd0207df97565b03255f70783ca1a349be5ed9f44589c36000d44533a4d21fd9d6f5d57c8cd05c61a6f23f9131cec8ae386b6b437db399ec3d"); +} + +// TEST(TWAnySignerPolkadot, SignNominate_452522) +#[test] +fn test_polkadot_sign_nominate_452522() { + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY_2.decode_hex().unwrap().into(), + nonce: 1, + block_hash: "0x211787d016e39007ac054547737a10542620013e73648b3134541d536cb44e2c" + .decode_hex() + .unwrap() + .into(), + genesis_hash: GENESIS_HASH.decode_hex().unwrap().into(), + spec_version: 26, + transaction_version: 5, + era: Some(Proto::Era { + block_number: 3540945, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::nominate( + Proto::mod_Staking::Nominate { + nominators: vec![ + "14xKzzU1ZYDnzFj7FgdtDAYSMJNARjDc2gNw4XAFDgr4uXgp".into(), + "1REAJ1k691g5Eqqg9gL7vvZCBG7FCCZ8zgQkZWd4va5ESih".into(), + ], + ..Default::default() + }, + )), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/0x4525224b7d8f3e58de3a54a9fbfd071401c2b737f314c972a2bb087a0ff508a6 + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!(signed, "a502849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f78300d73ff0dc456704743f70173a56e6c13e88a6e1dddb38a23552a066e44fb64e2c9d8a5e9a76afb9489b8540365f668bddd34b7d9c8dbdc4600e6316080e55a30315010400070508aee72821ca00e62304e4f0d858122a65b87c8df4f0eae224ae064b951d39f610127a30e486492921e58f2564b36ab1ca21ff630672f0e76920edd601f8f2b89a"); +} + +// TEST(TWAnySignerPolkadot, SignNominate2) +#[test] +fn test_polkadot_sign_nominate_2() { + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + nonce: 0, + block_hash: "d22a6b2e3e61325050718bd04a14da9efca1f41c9f0a525c375d36106e25af68" + .decode_hex() + .unwrap() + .into(), + genesis_hash: GENESIS_HASH.decode_hex().unwrap().into(), + spec_version: 17, + transaction_version: 3, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::nominate( + Proto::mod_Staking::Nominate { + nominators: vec![ + "1zugcabYjgfQdMLC3cAzQ8tJZMo45tMnGpivpAzpxB4CZyK".into(), + "1REAJ1k691g5Eqqg9gL7vvZCBG7FCCZ8zgQkZWd4va5ESih".into(), + "1WG3jyNqniQMRZGQUc7QD2kVLT8hkRPGMSqAb5XYQM1UDxN".into(), + "16QFrtU6kDdBjxY8qEKz5EEfuDkHxqG8pix3wSGKQzRcuWHo".into(), + "14ShUZUYUR35RBZW6uVVt1zXDxmSQddkeDdXf1JkMA6P721N".into(), + "15MUBwP6dyVw5CXF9PjSSv7SdXQuDSwjX86v1kBodCSWVR7c".into(), + ], + ..Default::default() + }, + )), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!(signed, "a1048488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee00135bbc68b67fffadaf7e98b6402c4fc60382765f543225083a024b0e0ff8071d4ec4ddd67a65828113cc76f3208765608be010d2fcfdcd47e8fe342872704c000000000705182c2a55b5a116a4c88aff57e8f2b70ba72dda72dda4b78630e16ad0ca69006f18127a30e486492921e58f2564b36ab1ca21ff630672f0e76920edd601f8f2b89a1650c532ed1a8641e8922aa24ade0ff411d03edd9ed1c6b7fe42f1a801cee37ceee9d5d071a418b51c02b456d5f5cefd6231041ad59b0e8379c59c11ba4a2439984e16482c99cfad1436111e321a86d87d0fac203bf64538f888e45d793b5413c08d5de7a5d97bea2c7ddf516d0635bddc43f326ae2f80e2595b49d4a08c4619"); +} + +// TEST(TWAnySignerPolkadot, SignChill) +#[test] +fn test_polkadot_sign_chill() { + let block_hash = "1d4a1ecc8b1c37bf0ba5d3e0bf14ec5402fbb035eeaf6d8042c07ca5f8c57429" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 17, + transaction_version: 3, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::chill( + Proto::mod_Staking::Chill { + ..Default::default() + }, + )), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!( + signed, + "9d018488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0088b5e1cd93ba74b82e329f95e1b22660385970182172b2ae280801fdd1ee5652cf7bf319e5e176ccc299dd8eb1e7fccb0ea7717efaf4aacd7640789dd09c1e070000000706" + ); +} + +// TEST(TWAnySignerPolkadot, SignWithdraw) +#[test] +fn test_polkadot_sign_withdraw() { + let block_hash = "7b4d1d1e2573eabcc90a3e96058eb0d8d21d7a0b636e8030d152d9179a345dda" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 17, + transaction_version: 3, + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::withdraw_unbonded( + Proto::mod_Staking::WithdrawUnbonded { + slashing_spans: 10, + ..Default::default() + }, + )), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!(signed, "ad018488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee002e49bf0dec9bef01dd3bd25419e2147dc983613d0860108f889f9ff2d062c5e3267e309e2dbc35dd2fc2b877b57d86a5f12cbeb8217485be32be3c34d2507d0e00000007030a000000"); +} + +// TEST(TWAnySignerPolkadot, SignUnbond_070957) +#[test] +fn test_polkadot_sign_unbond() { + let block_hash = "53040c71c6061bd256346b81fcb3545c13b5c34c7cd0c2c25f00aa6e564b16d5" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY_2.decode_hex().unwrap().into(), + nonce: 2, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 26, + transaction_version: 5, + era: Some(Proto::Era { + block_number: 3540983, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::unbond( + Proto::mod_Staking::Unbond { + value: Cow::Owned(U256::from(4000000000u64).to_big_endian().to_vec()), + ..Default::default() + }, + )), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/0x070957ab697adbe11f7d72a1314d0a81d272a747d2e6880818073317125f980a + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!( + signed, + "b501849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f783003a762d9dc3f2aba8922c4babf7e6622ca1d74da17ab3f152d8f29b0ffee53c7e5e150915912a9dfd98ef115d272e096543eef9f513207dd606eea97d023a64087503080007020300286bee" + ); +} + +// TEST(TWAnySignerPolkadot, SignChillAndUnbond) +#[test] +fn test_polkadot_sign_chill_and_unbond() { + let block_hash = "35ba668bb19453e8da6334cadcef2a27c8d4141bfc8b49e78e853c3d73e1ecd0" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 0, + private_key: PRIVATE_KEY_POLKADOT.decode_hex().unwrap().into(), + nonce: 6, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9200, + transaction_version: 12, + era: Some(Proto::Era { + block_number: 10541373, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::chill_and_unbond( + Proto::mod_Staking::ChillAndUnbond { + value: Cow::Owned(U256::from(100500000000u64).to_big_endian().to_vec()), + ..Default::default() + }, + )), + ..Default::default() + }; + + // https://polkadot.subscan.io/extrinsic/10541383-2 + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!( + signed, + "d10184008361bd08ddca5fda28b5e2aa84dc2621de566e23e089e555a42194c3eaf2da7900c891ba102db672e378945d74cf7f399226a76b43cab502436971599255451597fc2599902e4b62c7ce85ecc3f653c693fef3232be620984b5bb5bcecbbd7b209d50318001a02080706070207004d446617" + ); +} + +// TEST(TWAnySignerPolkadot, PolymeshEncodeAndSign) +#[test] +fn test_polymesh_encode_and_sign() { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x9a4283cc38f7e769c53ad2d1c5cf292fc85a740ec1c1aa80c180847e51928650 + + let block_hash = "898bba6413c38f79a284aec8749f297f6c8734c501f67517b5a6aadc338d1102" + .decode_hex() + .unwrap(); + let genesis_hash = POLYMESH_GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + nonce: 1, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 3010, + transaction_version: 2, + era: Some(Proto::Era { + block_number: 4298130, + period: 64, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW".into(), + value: Cow::Owned(U256::from(1000000u64).to_big_endian().to_vec()), + // The original C++ test had the wrong memo, since it didn't space pad the memo to 32 bytes. + memo: "MEMO PADDED WITH SPACES ".into(), + call_indices: custom_call_indices(0x05, 0x01), + ..Default::default() + })), + ..Default::default() + }; + + let public_key = "4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"; + let signature = "0791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f"; + + // Compile and verify the ED25519 signature. + let (preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + + assert_eq!(preimage, "050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f2050414444454420574954482053504143455320202020202020202025010400c20b0000020000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063898bba6413c38f79a284aec8749f297f6c8734c501f67517b5a6aadc338d1102"); + // This signed tranaction is different from the original C++ test, but matches the transaction on Polymesh. + assert_eq!(signed, "bd0284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee000791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f25010400050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f20504144444544205749544820535041434553202020202020202020"); +} + +// TEST(TWAnySignerPolkadot, PolymeshEncodeBondAndNominate) +#[test] +fn test_polymesh_encode_bond_and_nominate() { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0xd516d4cb1f5ade29e557586e370e98c141c90d87a0b7547d98c6580eb2afaeeb + + let block_hash = "ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8" + .decode_hex() + .unwrap(); + let genesis_hash = POLYMESH_GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 6003050, + transaction_version: 4, + era: Some(Proto::Era { + block_number: 15742961, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond_and_nominate( + Proto::mod_Staking::BondAndNominate { + controller: "2EYbDVDVWiFbXZWJgqGDJsiH5MfNeLr5fxqH3tX84LQZaETG".into(), + value: Cow::Owned(U256::from(4000000u64).to_big_endian().to_vec()), // 4.0 POLYX + reward_destination: Proto::RewardDestination::STAKED.into(), + nominators: vec!["2Gw8mSc4CUMxXMKEDqEsumQEXE5yTF8ACq2KdHGuigyXkwtz".into()], + call_indices: custom_call_indices(0x29, 0x02), + bond_call_indices: custom_call_indices(0x11, 0x00), + nominate_call_indices: custom_call_indices(0x11, 0x05), + ..Default::default() + }, + )), + ..Default::default() + }; + + let preimage = helper_encode(CoinType::Polkadot, &input); + + assert_eq!(preimage, "2902081100005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a290610224f4000011050400c6766ff780e1f506e41622f7798ec9323ab3b8bea43767d8c107e1e920581958150300006a995b00040000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); + + // Can't compile a transaction with an SR25519 signature. + /* + // The public key is an SR25519 key and the signature is an SR25519 signature. + let public_key = "5ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a29061"; + let signature = "685a2fd4b1bdf7775c55eb97302a0f86b0c10848fd9db3a7f6bbe912c4c2fa28bed16f6032852ec14f27f0553523dd2fc181a6dca79f19f9c7ed6cb660cf6480"; + + let (preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + assert_eq!(signed, "d90284005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a2906101685a2fd4b1bdf7775c55eb97302a0f86b0c10848fd9db3a7f6bbe912c4c2fa28bed16f6032852ec14f27f0553523dd2fc181a6dca79f19f9c7ed6cb660cf6480150300002902081100005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a290610224f4000011050400c6766ff780e1f506e41622f7798ec9323ab3b8bea43767d8c107e1e920581958"); + */ +} + +// TEST(TWAnySignerPolkadot, PolymeshEncodeChillAndUnbond) +#[test] +fn test_polymesh_encode_chill_and_unbond() { + // extrinsic on mainnet + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x29020811061102027a030a + + let block_hash = "ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8" + .decode_hex() + .unwrap(); + let genesis_hash = POLYMESH_GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 6003050, + transaction_version: 4, + era: Some(Proto::Era { + block_number: 15742961, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::chill_and_unbond( + Proto::mod_Staking::ChillAndUnbond { + value: Cow::Owned(U256::from(42000000u64).to_big_endian().to_vec()), // 42.0 POLYX + call_indices: custom_call_indices(0x29, 0x02), + chill_call_indices: custom_call_indices(0x11, 0x06), + unbond_call_indices: custom_call_indices(0x11, 0x02), + ..Default::default() + }, + )), + ..Default::default() + }; + + let preimage = helper_encode(CoinType::Polkadot, &input); + + assert_eq!( + preimage, + "29020811061102027a030a150300006a995b00040000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); +} + +// TEST(TWAnySignerPolkadot, Statemint_encodeTransaction_transfer) +#[test] +fn test_statemint_encode_transaction_transfer() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2686030-2 + + let block_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + let genesis_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9320, + transaction_version: 9, + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 1984, + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x32, 0x05), + ..Default::default() + }, + )), + ..Default::default() + }; + + let public_key = "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"; + let signature = "e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903"; + + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + + assert_eq!(signed, "4102840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000003205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); + + // 3d 02840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000 3205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600 + // 41 02840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000 00 3205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600 +} + +// TEST(TWAnySignerPolkadot, Statemint_encodeTransaction_transfer_keep_alive) +#[test] +fn test_statemint_encode_transaction_transfer_keep_alive() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2686081-2 + + let block_hash = "e8f10f9a841dc73578148c763afa17638670c8655542172a80af2e03bf3cbe62" + .decode_hex() + .unwrap(); + let genesis_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + nonce: 2, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9320, + transaction_version: 9, + era: Some(Proto::Era { + block_number: 2686056, + period: 64, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 1984, + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x32, 0x06), + ..Default::default() + }, + )), + ..Default::default() + }; + + let public_key = "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"; + let signature = "68c40526bd9e56e340bfc9385ea463afce34e5c49be75b5946974d9ef6a357f90842036cd1b811b60882ae7183aa23545ef5825aafc8aaa6274d71a03414dc0a"; + + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + + assert_eq!(signed, "4502840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c910068c40526bd9e56e340bfc9385ea463afce34e5c49be75b5946974d9ef6a357f90842036cd1b811b60882ae7183aa23545ef5825aafc8aaa6274d71a03414dc0a85020800003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); +} + +// TEST(TWAnySignerPolkadot, Statemint_encodeTransaction_batch_transfer_keep_alive) +#[test] +fn test_statemint_encode_transaction_batch_transfer_keep_alive() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2711054-2 + + let block_hash = "c8a2e9492f822f8c07f3717a00e36f68a3090a878b07998724ec1f178f4cf514" + .decode_hex() + .unwrap(); + let genesis_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + nonce: 3, + spec_version: 9320, + transaction_version: 9, + era: Some(Proto::Era { + block_number: 2711016, + period: 64, + }), + message_oneof: balance_call( + Proto::mod_Balance::OneOfmessage_oneof::batch_asset_transfer(BatchAssetTransfer { + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x28, 0x00), + transfers: vec![ + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 1984, + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x32, 0x06), + }, + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 1984, + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x32, 0x06), + }, + ], + }), + ), + ..Default::default() + }; + + let public_key = "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"; + let signature = "e1d541271965858ff2ba1a1296f0b4d28c8cbcaddf0ea06a9866869caeca3d16eff1265591d11b46d66882493079fde9e425cd941f166260135e9d81f7daf60c"; + + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + + assert_eq!(signed, "f502840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e1d541271965858ff2ba1a1296f0b4d28c8cbcaddf0ea06a9866869caeca3d16eff1265591d11b46d66882493079fde9e425cd941f166260135e9d81f7daf60c85020c00002800083206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a06003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); +} + +// TEST(TWAnySignerPolkadot, Statemint_encodeTransaction_dot_transfer_keep_alive) +#[test] +fn test_statemint_encode_transaction_dot_transfer_keep_alive() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2789245-2 + let block_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + let genesis_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + nonce: 7, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9320, + transaction_version: 9, + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 0x00, + fee_asset_id: 0x00, + call_indices: custom_call_indices(0x0a, 0x03), + ..Default::default() + }, + )), + ..Default::default() + }; + + let public_key = "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"; + let signature = "c4f7cb46605986ff6dd1a192736feddd8ae468a10b1b458eadfa855ed6b59ad442a96c18e7109ad594d11ba2fd52920545f8a450234e9b03ee3e8f59a8f06f00"; + + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + assert_eq!(signed, "3902840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100c4f7cb46605986ff6dd1a192736feddd8ae468a10b1b458eadfa855ed6b59ad442a96c18e7109ad594d11ba2fd52920545f8a450234e9b03ee3e8f59a8f06f00001c00000a030050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); +} + +// TEST(TWAnySignerPolkadot, Statemint_encodeTransaction_usdt_transfer_keep_alive) +#[test] +fn test_statemint_encode_transaction_usdt_transfer_keep_alive() { + // tx on mainnet + // https://statemint.subscan.io/extrinsic/2789377-2 + let block_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + let genesis_hash = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 0, + multi_address: true, + nonce: 8, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9320, + transaction_version: 9, + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + to_address: "12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH".into(), + value: Cow::Owned(U256::from(100000u64).to_big_endian().to_vec()), + asset_id: 1984, + fee_asset_id: 1984, + call_indices: custom_call_indices(0x32, 0x06), + ..Default::default() + }, + )), + ..Default::default() + }; + + let public_key = "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"; + let signature = "d22583408806c005a24caf16f2084691f4c6dcb6015e6645adc86fc1474369b0e0b7dbcc0ef25b17eae43844aff6fb42a0b279a19e822c76043cac015be5e40a"; + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + assert_eq!(signed, "5102840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100d22583408806c005a24caf16f2084691f4c6dcb6015e6645adc86fc1474369b0e0b7dbcc0ef25b17eae43844aff6fb42a0b279a19e822c76043cac015be5e40a00200001c00700003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); +} + +// TEST(TWAnySignerPolkadot, encodeTransaction_Add_authorization) +#[test] +fn test_encode_transaction_add_authorization() { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x7d9b9109027b36b72d37ba0648cb70e5254524d3d6752cc6b41601f4bdfb1af0 + + let block_hash = "ce0c2109db498e45abf8fd447580dcfa7b7a07ffc2bfb1a0fbdd1af3e8816d2b" + .decode_hex() + .unwrap(); + let genesis_hash = POLYMESH_GENESIS_HASH.decode_hex().unwrap(); + + // Set empty "These". + let empty = Proto::mod_Identity::mod_AddAuthorization::Data { + data: vec![0x00u8].into(), + }; + let input = Proto::SigningInput { + network: 12, + multi_address: true, + nonce: 5, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 3010, + transaction_version: 2, + era: Some(Proto::Era { + block_number: 4395451, + period: 64, + }), + message_oneof: polymesh_call(Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + Proto::mod_Identity::AddAuthorization { + target: "2HEVN4PHYKj7B1krQ9bctAQXZxHQQkANVNCcfbdYk2gZ4cBR".into(), + data: Some(Proto::mod_Identity::mod_AddAuthorization::AuthData { + asset: Some(empty.clone()), + extrinsic: Some(empty.clone()), + portfolio: Some(empty.clone()), + }), + call_indices: custom_call_indices(0x07, 0x0d), + ..Default::default() + }, + )), + ..Default::default() + }; + + let public_key = "4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"; + let signature = "81e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01"; + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + assert_eq!(signed, "490284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee0081e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01b5031400070d01d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d0610540501000100010000"); +} + +// TEST(TWAnySignerPolkadot, encodeTransaction_JoinIdentityAsKey) +#[test] +fn test_encode_transaction_join_identity_as_key() { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x9d7297d8b38af5668861996cb115f321ed681989e87024fda64eae748c2dc542 + + let block_hash = "45c80153c47f5d16acc7a66d473870e8d4574437a7d8c813f47da74cae3812c2" + .decode_hex() + .unwrap(); + let genesis_hash = POLYMESH_GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + multi_address: true, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 3010, + transaction_version: 2, + era: Some(Proto::Era { + block_number: 4395527, + period: 64, + }), + message_oneof: polymesh_call( + Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key( + Proto::mod_Identity::JoinIdentityAsKey { + auth_id: 21435, + call_indices: custom_call_indices(0x07, 0x05), + ..Default::default() + }, + ), + ), + ..Default::default() + }; + + let public_key = "d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054"; + let signature = "7f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006"; + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polkadot, input, signature, public_key, true); + assert_eq!(signed, "c5018400d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054007f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006750000000705bb53000000000000"); +} + +// TEST(TWAnySignerPolkadot, Kusama_SignBond_NoController) +#[test] +fn test_kusama_sign_bond_no_controller() { + // tx on mainnet + // https://kusama.subscan.io/extrinsic/0x4e52e59b63910cbdb8c5430c2d100908934f473363c8994cddfd6d1501b017f5 + + let block_hash = "beb02a3ee782f4bd60ffcfc3de473e3c5a00b2cf124dd302c559b0e77b4331eb" + .decode_hex() + .unwrap(); + let genesis_hash = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe" + .decode_hex() + .unwrap(); + + // Ignore `controller` as it was removed from the `Staking::bond` function at `spec_version = 9430` + // https://kusama.subscan.io/runtime/Staking?version=9430 + + let input = Proto::SigningInput { + network: 2, + private_key: PRIVATE_KEY.decode_hex().unwrap().into(), + multi_address: true, + nonce: 3, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 9430, + transaction_version: 23, + era: Some(Proto::Era { + block_number: 18672490, + period: 64, + }), + message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond( + Proto::mod_Staking::Bond { + value: Cow::Owned(U256::from(120_000_000_000u64).to_big_endian().to_vec()), // 0.12 + reward_destination: Proto::RewardDestination::CONTROLLER, + ..Default::default() + }, + )), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polkadot, input); + assert_eq!(signed, "c101840088dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee00bc4d7a166bd1e7e2bfe9b53e81239c9e340d5a326f17c0a3d2768fcc127f20f4f85d888ecb90aa3ed9a0943f8ae8116b9a19747e563c8d8151dfe3b1b5deb40ca5020c0006000700b08ef01b02"); +} + +// TEST(TWAnySignerPolkadot, SignTransfer_KusamaNewSpec) +#[test] +fn test_sign_transfer_kusama_new_spec() { + let block_hash = "0c731c2b7f5332749432eae61cd5a919592965b28181cf9b73b0a1258ea73303" + .decode_hex() + .unwrap(); + let genesis_hash = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe" + .decode_hex() + .unwrap(); + + let input = Proto::SigningInput { + network: 2, + private_key: PRIVATE_KEY_2.decode_hex().unwrap().into(), + multi_address: true, + nonce: 150, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 1002005, + transaction_version: 26, + era: Some(Proto::Era { + block_number: 23610713, + period: 64, + }), + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer( + Proto::mod_Balance::Transfer { + to_address: "DAbYHrSQTULYZsuA1kvH2cQ33oBsCxxSRPM1XkhzGLeJuHG".into(), + value: Cow::Owned(U256::from(2000000000u64).to_big_endian().to_vec()), // 2.0 + ..Default::default() + }, + )), + ..Default::default() + }; + + let (preimage, signed) = helper_encode_and_maybe_sign(CoinType::Polkadot, input); + assert_eq!(preimage, "0400001a2447c661c9b168bba4a2a178baef7d79eee006c1d145ffc832be76ff6ee9ce0300943577950159020000154a0f001a000000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe0c731c2b7f5332749432eae61cd5a919592965b28181cf9b73b0a1258ea7330300"); + assert_eq!(signed.as_deref(), Some("450284009dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f78300fc5a463d3b6972ac7e0b701110f9d95d377be5b6a2f356765553104c04765fc0066c235c11dabde650d487760dc310003d607abceaf85a0a0f47f1a90e3680029501590200000400001a2447c661c9b168bba4a2a178baef7d79eee006c1d145ffc832be76ff6ee9ce0300943577")); +} + +// TEST(PolkadotExtrinsic, Polkadot_EncodePayloadWithNewSpec) +#[test] +fn test_encode_payload_with_new_spec() { + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + let block_hash = "0x5d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351" + .decode_hex() + .unwrap(); + + let mut input = Proto::SigningInput { + network: 0, + genesis_hash: genesis_hash.into(), + block_hash: block_hash.into(), + multi_address: true, + message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::asset_transfer( + AssetTransfer { + to_address: "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".into(), + value: Cow::Owned(U256::from(999500000u64).to_big_endian().to_vec()), + asset_id: 1984, + call_indices: custom_call_indices(0x32, 0x05), + ..Default::default() + }, + )), + ..Default::default() + }; + + input.spec_version = 1002000; // breaking change happens at version 1002005 + let preimage = helper_encode(CoinType::Polkadot, &input); + assert_eq!(preimage, "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee00000000104a0f000000000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); + + input.spec_version = 1002005; // >= 1002005 + let preimage = helper_encode(CoinType::Polkadot, &input); + assert_eq!(preimage, "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee0000000000154a0f000000000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea4035100"); + + input.spec_version = 1002006; // >= 1002005 + let preimage = helper_encode(CoinType::Polkadot, &input); + assert_eq!(preimage, "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee0000000000164a0f000000000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea4035100"); +} diff --git a/rust/tw_tests/tests/chains/polymesh/mod.rs b/rust/tw_tests/tests/chains/polymesh/mod.rs new file mode 100644 index 00000000000..157974d25e6 --- /dev/null +++ b/rust/tw_tests/tests/chains/polymesh/mod.rs @@ -0,0 +1 @@ +mod polymesh_address; diff --git a/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs b/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs new file mode 100644 index 00000000000..15957c6c549 --- /dev/null +++ b/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_ss58_is_invalid, test_address_ss58_is_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_polymesh_address_is_valid() { + // Polymesh + test_address_ss58_is_valid( + CoinType::Polkadot, + "2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG", + 12, // Polymesh ss58 + ); +} + +#[test] +fn test_polymesh_address_invalid() { + // Substrate ed25519 + test_address_ss58_is_invalid( + CoinType::Polkadot, + "5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ", + 12, // Polymesh ss58 + ); + test_address_ss58_is_invalid( + CoinType::Polkadot, + "JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A", + 12, // Polymesh ss58 + ); +} diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs index 4f7e27b9375..1b8b9f19aac 100644 --- a/rust/tw_tests/tests/coin_address_derivation_test.rs +++ b/rust/tw_tests/tests/coin_address_derivation_test.rs @@ -154,6 +154,9 @@ fn test_coin_address_derivation() { CoinType::Sui => "0x01a5c6c1b74cec4fbd12b3e17252b83448136065afcdf24954dc3a9c26df4905", CoinType::TON => "UQCj3jAU_Ec2kXdAqweKt4rYjiwTNwiCfaUnIDHGh7wTwx_G", CoinType::Pactus => "pc1rk2qaaeu9pj3zwtvm49d3d4yqxzpp4te87cx0am", + CoinType::Polkadot => "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", + CoinType::Acala => "22WaYy5ChG8V5vvRVDP4ErE7esk8nZ4rjGYwxeVArnNT8dU3", + CoinType::Kusama => "EDJV2jycw8fqTgiExLsDe6iUzbnM62hDk7u3BLm9wcYswkY", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/src/Kusama/Address.h b/src/Kusama/Address.h deleted file mode 100644 index 6d4758002c2..00000000000 --- a/src/Kusama/Address.h +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "../PublicKey.h" -#include "../Polkadot/SS58Address.h" -#include - -#include - -namespace TW::Kusama { - -class Address: public SS58Address { - public: - /// Determines whether a string makes a valid address. - static bool isValid(const std::string& string) { return SS58Address::isValid(string, TWSS58AddressTypeKusama); } - - /// Initializes a Kusama address with a string representation. - Address(const std::string& string): SS58Address(string, TWSS58AddressTypeKusama) {} - - /// Initializes a Kusama address with a public key. - Address(const PublicKey& publicKey): SS58Address(publicKey, TWSS58AddressTypeKusama) {} -}; -} // namespace TW::Kusama diff --git a/src/Kusama/Entry.cpp b/src/Kusama/Entry.cpp deleted file mode 100644 index 46df5c4bfff..00000000000 --- a/src/Kusama/Entry.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Entry.h" - -#include "Address.h" -#include "Polkadot/Signer.h" - -namespace TW::Kusama { - -// Note: avoid business logic from here, rather just call into classes like Address, Signer, etc. - -bool Entry::validateAddress([[maybe_unused]] TWCoinType coin, const std::string& address, [[maybe_unused]] const PrefixVariant& addressPrefix) const { - return Address::isValid(address); -} - -std::string Entry::deriveAddress([[maybe_unused]] TWCoinType coin, const PublicKey& publicKey, [[maybe_unused]] TWDerivation derivation, [[maybe_unused]] const PrefixVariant& addressPrefix) const { - return Address(publicKey).string(); -} - -Data Entry::addressToData([[maybe_unused]] TWCoinType coin, const std::string& address) const { - const auto addr = Address(address); - return {addr.bytes.begin() + 1, addr.bytes.end()}; -} - -void Entry::sign([[maybe_unused]] TWCoinType coin, const TW::Data& dataIn, TW::Data& dataOut) const { - signTemplate(dataIn, dataOut); -} - -} // namespace TW::Kusama diff --git a/src/Kusama/Entry.h b/src/Kusama/Entry.h index d127bd39a4d..b1e819b039e 100644 --- a/src/Kusama/Entry.h +++ b/src/Kusama/Entry.h @@ -4,18 +4,13 @@ #pragma once -#include "../CoinEntry.h" +#include "rust/RustCoinEntry.h" namespace TW::Kusama { /// Entry point for implementation of Kusama coin. See also Polkadot. /// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file -class Entry final : public CoinEntry { -public: - bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const; - std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const; - Data addressToData(TWCoinType coin, const std::string& address) const; - void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const; +class Entry : public Rust::RustCoinEntry { }; } // namespace TW::Kusama diff --git a/src/Polkadot/Address.h b/src/Polkadot/Address.h deleted file mode 100644 index c717bebe016..00000000000 --- a/src/Polkadot/Address.h +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "../PublicKey.h" -#include "SS58Address.h" -#include - -#include - -namespace TW::Polkadot { - -class Address: public SS58Address { - public: - /// Determines whether a string makes a valid address. - static bool isValid(const std::string& string) { return SS58Address::isValid(string, TWSS58AddressTypePolkadot); } - static bool isValid(const std::string& string, uint32_t ss58Prefix) { return SS58Address::isValid(string, ss58Prefix); } - - /// Initializes a Polkadot address with a string representation. - explicit Address(const std::string& string): SS58Address(string, TWSS58AddressTypePolkadot) {} - - /// Initializes a Polkadot address with a string representation. - explicit Address(const std::string& string, uint32_t ss58Prefix): SS58Address(string, ss58Prefix) {} - - /// Initializes a Polkadot address with a public key. - explicit Address(const PublicKey& publicKey): SS58Address(publicKey, TWSS58AddressTypePolkadot) {} - /// Initializes a Polkadot address with a public key and a given ss58Prefix. - explicit Address(const PublicKey& publicKey, std::uint32_t ss58Prefix): SS58Address(publicKey, ss58Prefix) {} -}; -} // namespace TW::Polkadot diff --git a/src/Polkadot/Entry.cpp b/src/Polkadot/Entry.cpp deleted file mode 100644 index 705acc11d9d..00000000000 --- a/src/Polkadot/Entry.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Entry.h" - -#include "Address.h" -#include "Coin.h" -#include "Signer.h" -#include "../proto/TransactionCompiler.pb.h" - -namespace TW::Polkadot { - -// Note: avoid business logic from here, rather just call into classes like Address, Signer, etc. - -bool Entry::validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const { - if (auto* prefix = std::get_if(&addressPrefix); prefix) { - return Address::isValid(address, *prefix); - } - const auto ss58Prefix = TW::ss58Prefix(coin); - return Address::isValid(address, ss58Prefix); -} - -std::string Entry::deriveAddress(TWCoinType coin, const PublicKey& publicKey, [[maybe_unused]] TWDerivation derivation, const PrefixVariant& addressPrefix) const { - if (auto* ss58Prefix = std::get_if(&addressPrefix); ss58Prefix) { - return Address(publicKey, *ss58Prefix).string(); - } - const auto ss58Prefix = TW::ss58Prefix(coin); - return Address(publicKey, ss58Prefix).string(); -} - -Data Entry::addressToData(TWCoinType coin, const std::string& address) const { - const auto ss58Prefix = TW::ss58Prefix(coin); - const auto addr = Address(address, ss58Prefix); - return {addr.bytes.begin() + 1, addr.bytes.end()}; -} - -void Entry::sign([[maybe_unused]] TWCoinType coin, const TW::Data& dataIn, TW::Data& dataOut) const { - signTemplate(dataIn, dataOut); -} - -Data Entry::preImageHashes([[maybe_unused]] TWCoinType coin, const Data& txInputData) const { - return txCompilerTemplate( - txInputData, [](const auto& input, auto& output) { - - auto preImage = Signer::signaturePreImage(input); - auto preImageHash = Signer::hash(preImage); - output.set_data_hash(preImageHash.data(), preImageHash.size()); - output.set_data(preImage.data(), preImage.size()); - }); -} - -void Entry::compile([[maybe_unused]] TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const { - dataOut = txCompilerSingleTemplate( - txInputData, signatures, publicKeys, - [](const auto& input, auto& output, const auto& signature, const auto& publicKey) { - auto signedTx = Signer::encodeTransaction(input, publicKey.bytes, signature); - output.set_encoded(signedTx.data(), signedTx.size()); - }); -} -} // namespace TW::Polkadot diff --git a/src/Polkadot/Entry.h b/src/Polkadot/Entry.h index de481bca381..d4e4a719a73 100644 --- a/src/Polkadot/Entry.h +++ b/src/Polkadot/Entry.h @@ -4,20 +4,13 @@ #pragma once -#include "../CoinEntry.h" +#include "rust/RustCoinEntry.h" namespace TW::Polkadot { /// Entry point for implementation of Polkadot coin. /// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file -class Entry final : public CoinEntry { -public: - bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const; - std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const; - Data addressToData(TWCoinType coin, const std::string& address) const; - void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const; - Data preImageHashes(TWCoinType coin, const Data& txInputData) const; - void compile(TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const; +class Entry : public Rust::RustCoinEntry { }; } // namespace TW::Polkadot diff --git a/src/Polkadot/Extrinsic.cpp b/src/Polkadot/Extrinsic.cpp deleted file mode 100644 index 7cc3540c3a5..00000000000 --- a/src/Polkadot/Extrinsic.cpp +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Extrinsic.h" -#include -#include - -namespace TW::Polkadot { - -static constexpr uint8_t signedBit = 0x80; -static constexpr uint8_t sigTypeEd25519 = 0x00; -static constexpr uint8_t extrinsicFormat = 4; -static constexpr uint32_t multiAddrSpecVersion = 28; -static constexpr uint32_t multiAddrSpecVersionKsm = 2028; - -static const std::string balanceTransfer = "Balances.transfer"; -static const std::string utilityBatch = "Utility.batch_all"; -static const std::string stakingBond = "Staking.bond"; -static const std::string stakingBondExtra = "Staking.bond_extra"; -static const std::string stakingUnbond = "Staking.unbond"; -static const std::string stakingWithdrawUnbond = "Staking.withdraw_unbonded"; -static const std::string stakingNominate = "Staking.nominate"; -static const std::string stakingChill = "Staking.chill"; -static const std::string stakingReBond = "Staking.rebond"; - -// Non-existent modules and methods on Polkadot and Kusama chains: -static const std::string assetsTransfer = "Assets.transfer"; -static const std::string joinIdentityAsKey = "Identity.join_identity_as_key"; -static const std::string identityAddAuthorization = "Identity.add_authorization"; - -// Readable decoded call index can be found from https://polkascan.io -const static std::map polkadotCallIndices = { - {balanceTransfer, Data{0x05, 0x00}}, - {stakingBond, Data{0x07, 0x00}}, - {stakingBondExtra, Data{0x07, 0x01}}, - {stakingUnbond, Data{0x07, 0x02}}, - {stakingWithdrawUnbond, Data{0x07, 0x03}}, - {stakingNominate, Data{0x07, 0x05}}, - {stakingChill, Data{0x07, 0x06}}, - {utilityBatch, Data{0x1a, 0x02}}, - {stakingReBond, Data{0x07, 0x13}}, -}; - -// Default Kusama call indices. -const static std::map kusamaCallIndices = { - {balanceTransfer, Data{0x04, 0x00}}, - {stakingBond, Data{0x06, 0x00}}, - {stakingBondExtra, Data{0x06, 0x01}}, - {stakingUnbond, Data{0x06, 0x02}}, - {stakingWithdrawUnbond, Data{0x06, 0x03}}, - {stakingNominate, Data{0x06, 0x05}}, - {stakingChill, Data{0x06, 0x06}}, - {utilityBatch, Data{0x18, 0x02}}, - {stakingReBond, Data{0x06, 0x13}}, -}; - -static Data getCallIndex(const Proto::CallIndices& callIndices, uint32_t network, const std::string& key) { - if (callIndices.has_custom()) { - return encodeCallIndex(callIndices.custom().module_index(), callIndices.custom().method_index()); - } - if (network == static_cast(TWSS58AddressTypePolkadot) && polkadotCallIndices.contains(key)) { - return polkadotCallIndices.at(key); - } - if (network == static_cast(TWSS58AddressTypeKusama) && kusamaCallIndices.contains(key)) { - return kusamaCallIndices.at(key); - } - throw std::invalid_argument("'call_indices' is not set"); -} - -bool Extrinsic::encodeRawAccount() const { - if (multiAddress || - (network == static_cast(TWSS58AddressTypePolkadot) && specVersion >= multiAddrSpecVersion) || - (network == static_cast(TWSS58AddressTypeKusama) && specVersion >= multiAddrSpecVersionKsm)) { - return false; - } - return true; -} - -Data Extrinsic::encodeEraNonceTip() const { - Data data; - // era - append(data, era); - // nonce - append(data, encodeCompact(nonce)); - // tip - append(data, encodeCompact(tip)); - return data; -} - -Data Extrinsic::encodeCall(const Proto::SigningInput& input) const { - // call index from MetadataV11 - Data data; - if (input.has_balance_call()) { - data = encodeBalanceCall(input.balance_call()); - } else if (input.has_staking_call()) { - data = encodeStakingCall(input.staking_call()); - } else if (input.has_polymesh_call()) { - data = encodePolymeshCall(input.polymesh_call()); - } - return data; -} - -Data Extrinsic::encodeBalanceCall(const Proto::Balance& balance) const { - Data data; - if (balance.has_transfer()) { - data = encodeTransfer(balance.transfer()); - } else if (balance.has_batchtransfer()) { - //init call array - auto calls = std::vector(); - for (const auto& transfer : balance.batchtransfer().transfers()) { - Data itemData = encodeTransfer(transfer); - // put into calls array - calls.push_back(itemData); - } - data = encodeBatchCall(balance.batchtransfer().call_indices(), calls); - } else if (balance.has_asset_transfer()) { - data = encodeAssetTransfer(balance.asset_transfer()); - } else if (balance.has_batch_asset_transfer()) { - // init call array - auto calls = std::vector(); - for (const auto& transfer : balance.batch_asset_transfer().transfers()) { - // put into calls array - calls.push_back(encodeAssetTransfer(transfer)); - } - data = encodeBatchCall(balance.batch_asset_transfer().call_indices(), calls); - } - - return data; -} - -Data Extrinsic::encodeTransfer(const Proto::Balance::Transfer& transfer) const { - Data data; - - auto address = SS58Address(transfer.to_address(), network); - auto value = load(transfer.value()); - // call index - append(data, getCallIndex(transfer.call_indices(), network, balanceTransfer)); - // destination - append(data, encodeAccountId(address.keyBytes(), encodeRawAccount())); - // value - append(data, encodeCompact(value)); - // memo - if (transfer.memo().length() > 0) { - append(data, 0x01); - auto memo = transfer.memo(); - if (memo.length() < 32) { - // padding memo with null - memo.append(32 - memo.length(), '\0'); - } - append(data, TW::data(memo)); - } - - return data; -} - -Data Extrinsic::encodeAssetTransfer(const Proto::Balance::AssetTransfer& transfer) const { - Data data; - - auto address = SS58Address(transfer.to_address(), network); - auto value = load(transfer.value()); - - // call index - append(data, getCallIndex(transfer.call_indices(), network, assetsTransfer)); - // asset id - if (transfer.asset_id() > 0) { - // For native token transfer, should ignore asset id - append(data, Polkadot::encodeCompact(transfer.asset_id())); - } - // destination - append(data, Polkadot::encodeAccountId(address.keyBytes(), encodeRawAccount())); - // value - append(data, Polkadot::encodeCompact(value)); - - return data; -} - -Data Extrinsic::encodeBatchCall(const Proto::CallIndices& batchCallIndices, const std::vector& calls) const { - Data data; - append(data, getCallIndex(batchCallIndices, network, utilityBatch)); - append(data, encodeVector(calls)); - return data; -} - -Data Extrinsic::encodeStakingCall(const Proto::Staking& staking) const { - Data data; - switch (staking.message_oneof_case()) { - case Proto::Staking::kBond: { - auto value = load(staking.bond().value()); - auto reward = byte(staking.bond().reward_destination()); - // call index - append(data, getCallIndex(staking.bond().call_indices(), network, stakingBond)); - // controller - if (!staking.bond().controller().empty()) { - auto controller = SS58Address(staking.bond().controller(), network); - append(data, encodeAccountId(controller.keyBytes(), encodeRawAccount())); - } - // value - append(data, encodeCompact(value)); - // reward destination - append(data, reward); - } break; - - case Proto::Staking::kBondAndNominate: { - // encode call1 - Data call1; - { - auto staking1 = Proto::Staking(); - auto* bond = staking1.mutable_bond(); - bond->set_controller(staking.bond_and_nominate().controller()); - bond->set_value(staking.bond_and_nominate().value()); - bond->set_reward_destination(staking.bond_and_nominate().reward_destination()); - auto callIndices = staking.bond_and_nominate().bond_call_indices(); - bond->mutable_call_indices()->CopyFrom(callIndices); - // recursive call - call1 = encodeStakingCall(staking1); - } - - // encode call2 - Data call2; - { - auto staking2 = Proto::Staking(); - auto* nominate = staking2.mutable_nominate(); - for (auto i = 0; i < staking.bond_and_nominate().nominators_size(); ++i) { - nominate->add_nominators(staking.bond_and_nominate().nominators(i)); - } - auto callIndices = staking.bond_and_nominate().nominate_call_indices(); - nominate->mutable_call_indices()->CopyFrom(callIndices); - // recursive call - call2 = encodeStakingCall(staking2); - } - - auto calls = std::vector{call1, call2}; - data = encodeBatchCall(staking.bond_and_nominate().call_indices(), calls); - } break; - - case Proto::Staking::kBondExtra: { - auto value = load(staking.bond_extra().value()); - // call index - append(data, getCallIndex(staking.bond_extra().call_indices(), network, stakingBondExtra)); - // value - append(data, encodeCompact(value)); - } break; - - case Proto::Staking::kUnbond: { - auto value = load(staking.unbond().value()); - // call index - append(data, getCallIndex(staking.unbond().call_indices(), network, stakingUnbond)); - // value - append(data, encodeCompact(value)); - } break; - - case Proto::Staking::kRebond: { - auto value = load(staking.rebond().value()); - // call index - append(data, getCallIndex(staking.rebond().call_indices(), network, stakingReBond)); - // value - append(data, encodeCompact(value)); - } break; - - case Proto::Staking::kWithdrawUnbonded: { - auto spans = staking.withdraw_unbonded().slashing_spans(); - // call index - append(data, getCallIndex(staking.withdraw_unbonded().call_indices(), network, stakingWithdrawUnbond)); - // num_slashing_spans - encode32LE(spans, data); - } break; - - case Proto::Staking::kNominate: { - std::vector accountIds; - for (auto& n : staking.nominate().nominators()) { - accountIds.emplace_back(SS58Address(n, network)); - } - // call index - append(data, getCallIndex(staking.nominate().call_indices(), network, stakingNominate)); - // nominators - append(data, encodeAccountIds(accountIds, encodeRawAccount())); - } break; - - case Proto::Staking::kChill: - // call index - append(data, getCallIndex(staking.chill().call_indices(), network, stakingChill)); - break; - - case Proto::Staking::kChillAndUnbond: { - // encode call1 - Data call1; - { - auto staking1 = Proto::Staking(); - auto* chill = staking1.mutable_chill(); - auto callIndices = staking.chill_and_unbond().chill_call_indices(); - chill->mutable_call_indices()->CopyFrom(callIndices); - // recursive call - call1 = encodeStakingCall(staking1); - } - - // encode call2 - Data call2; - { - auto staking2 = Proto::Staking(); - auto* unbond = staking2.mutable_unbond(); - unbond->set_value(staking.chill_and_unbond().value()); - auto callIndices = staking.chill_and_unbond().unbond_call_indices(); - unbond->mutable_call_indices()->CopyFrom(callIndices); - // recursive call - call2 = encodeStakingCall(staking2); - } - - auto calls = std::vector{call1, call2}; - data = encodeBatchCall(staking.chill_and_unbond().call_indices(), calls); - } break; - - default: - break; - } - return data; -} - -Data Extrinsic::encodePolymeshCall(const Proto::PolymeshCall& polymesh) const { - Data data; - - if (polymesh.has_identity_call()) { - const auto& identity = polymesh.identity_call(); - if (identity.has_join_identity_as_key()) { - data = encodeIdentityJoinIdentityAsKey(identity.join_identity_as_key()); - } else if (identity.has_add_authorization()) { - data = encodeIdentityAddAuthorization(identity.add_authorization()); - } - } - - return data; -} - -Data Extrinsic::encodeIdentityJoinIdentityAsKey(const Proto::Identity::JoinIdentityAsKey& joinIdentity) const { - Data data; - - // call index - append(data, getCallIndex(joinIdentity.call_indices(), network, joinIdentityAsKey)); - - // data - encode64LE(joinIdentity.auth_id(), data); - - return data; -} - -Data Extrinsic::encodeIdentityAddAuthorization(const Proto::Identity::AddAuthorization & addAuthorization) const { - Data data; - - // call index - append(data, getCallIndex(addAuthorization.call_indices(), network, identityAddAuthorization)); - - // target - append(data, 0x01); - SS58Address address(addAuthorization.target(), network); - append(data, Polkadot::encodeAccountId(address.keyBytes(), true)); - - // join identity - append(data, 0x05); - - if (addAuthorization.has_data()) { - const auto& authData = addAuthorization.data(); - - // asset - if (authData.has_asset()) { - append(data, 0x01); - append(data, TW::data(authData.asset().data())); - } else { - append(data, 0x00); - } - - // extrinsic - if (authData.has_extrinsic()) { - append(data, 0x01); - append(data, TW::data(authData.extrinsic().data())); - } else { - append(data, 0x00); - } - - // portfolio - if (authData.has_portfolio()) { - append(data, 0x01); - append(data, TW::data(authData.portfolio().data())); - } else { - append(data, 0x00); - } - } else { - // authorize all permissions - append(data, {0x01, 0x00}); // asset - append(data, {0x01, 0x00}); // extrinsic - append(data, {0x01, 0x00}); // portfolio - } - - // expiry - append(data, encodeCompact(addAuthorization.expiry())); - - return data; -} - -static bool requires_new_spec_compatbility(uint32_t network, uint32_t specVersion) noexcept { - // version 1002005 introduces a breaking change for Polkadot and Kusama - return ((network == 0 || network == 2) && specVersion >= 1002005) || (network == 10 && specVersion >= 2270); -} - -Data Extrinsic::encodePayload() const { - Data data; - auto use_new_spec = requires_new_spec_compatbility(network, specVersion); - - // call - append(data, call); - // era / nonce / tip - append(data, encodeEraNonceTip()); - // fee asset id - if (!feeAssetId.empty()) { - append(data, feeAssetId); - } - - if (use_new_spec) { - // mode (currently always 0) - data.push_back(0x00); - } - - // specVersion - encode32LE(specVersion, data); - // transactionVersion - encode32LE(version, data); - // genesis hash - append(data, genesisHash); - // block hash - append(data, blockHash); - - if (use_new_spec) { - // empty metadata hash - data.push_back(0x00); - } - return data; -} - -Data Extrinsic::encodeSignature(const PublicKey& signer, const Data& signature) const { - Data data; - auto use_new_spec = requires_new_spec_compatbility(network, specVersion); - - // version header - append(data, Data{extrinsicFormat | signedBit}); - // signer public key - append(data, encodeAccountId(signer.bytes, encodeRawAccount())); - // signature type - append(data, sigTypeEd25519); - // signature - append(data, signature); - // era / nonce / tip - append(data, encodeEraNonceTip()); - - // fee asset id - if (!feeAssetId.empty()) { - append(data, feeAssetId); - } - - if (use_new_spec) { - // mode (currently always 0) - data.push_back(0x00); - } - - // call - append(data, call); - // append length - encodeLengthPrefix(data); - return data; -} - -Data Extrinsic::encodeFeeAssetId(const Proto::SigningInput& input) { - if (!input.has_balance_call()) { - return {}; - } - - uint32_t rawFeeAssetId {0}; - if (input.balance_call().has_asset_transfer()) { - rawFeeAssetId = input.balance_call().asset_transfer().fee_asset_id(); - } else if (input.balance_call().has_batch_asset_transfer()) { - rawFeeAssetId = input.balance_call().batch_asset_transfer().fee_asset_id(); - } else { - return {}; - } - - Data feeAssetId; - if (rawFeeAssetId > 0) { - feeAssetId.push_back(0x01); - Data feeEncoding; - encode32LE(rawFeeAssetId, feeEncoding); - append(feeAssetId, feeEncoding); - } else { - // use native token - feeAssetId.push_back(0x00); - } - - return feeAssetId; -} - -} // namespace TW::Polkadot diff --git a/src/Polkadot/Extrinsic.h b/src/Polkadot/Extrinsic.h deleted file mode 100644 index 885040a3bc1..00000000000 --- a/src/Polkadot/Extrinsic.h +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Address.h" -#include "Data.h" -#include "../proto/Polkadot.pb.h" -#include "../uint256.h" -#include "ScaleCodec.h" - -namespace TW::Polkadot { - -// ExtrinsicV4 -class Extrinsic { - public: - Data blockHash; - Data genesisHash; - uint64_t nonce; - // Runtime spec version - uint32_t specVersion; - // transaction version - uint32_t version; - // balances::TakeFees - uint256_t tip; - // encoded Era data - Data era; - // encoded Call data - Data call; - // network - uint32_t network; - // enable multi-address - bool multiAddress; - // fee asset id - Data feeAssetId; - - explicit Extrinsic(const Proto::SigningInput& input) - : blockHash(input.block_hash().begin(), input.block_hash().end()) - , genesisHash(input.genesis_hash().begin(), input.genesis_hash().end()) - , nonce(input.nonce()) - , specVersion(input.spec_version()) - , version(input.transaction_version()) - , tip(load(input.tip())) - , network(input.network()) - , multiAddress(input.multi_address()) { - if (input.has_era()) { - era = encodeEra(input.era().block_number(), input.era().period()); - } else { - // immortal era - era = encodeCompact(0); - } - // keep fee asset id encoding which will be used in encodePayload & encodeSignature. - // see: https://github.com/paritytech/substrate/blob/d1221692968b8bc62d6eab9d10cb6b5bf38c5dc2/frame/transaction-payment/asset-tx-payment/src/lib.rs#L152 - feeAssetId = encodeFeeAssetId(input); - call = encodeCall(input); - } - - Data encodeCall(const Proto::SigningInput& input) const; - // Payload to sign. - Data encodePayload() const; - // Encode final data with signer public key and signature. - Data encodeSignature(const PublicKey& signer, const Data& signature) const; - - protected: - bool encodeRawAccount() const; - Data encodeBalanceCall(const Proto::Balance& balance) const; - Data encodeTransfer(const Proto::Balance::Transfer& transfer) const; - Data encodeAssetTransfer(const Proto::Balance::AssetTransfer& transfer) const; - Data encodeStakingCall(const Proto::Staking& staking) const; - Data encodePolymeshCall(const Proto::PolymeshCall& polymesh) const; - Data encodeIdentityJoinIdentityAsKey(const Proto::Identity::JoinIdentityAsKey& joinIdentityAsKey) const; - Data encodeIdentityAddAuthorization(const Proto::Identity::AddAuthorization& addAuthorization) const; - Data encodeBatchCall(const Proto::CallIndices& batchCallIndices, const std::vector& calls) const; - Data encodeEraNonceTip() const; - - static Data encodeFeeAssetId(const Proto::SigningInput& input); -}; - -} // namespace TW::Polkadot diff --git a/src/Polkadot/SS58Address.cpp b/src/Polkadot/SS58Address.cpp deleted file mode 100644 index d73535352da..00000000000 --- a/src/Polkadot/SS58Address.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "SS58Address.h" - -using namespace TW; -using namespace std; - -bool SS58Address::isValid(const std::string& string, uint32_t network) { - const auto decoded = Base58::decode(string); - byte decodedNetworkSize = 0; - uint32_t decodedNetwork = 0; - if (!decodeNetwork(decoded, decodedNetworkSize, decodedNetwork)) { - return false; - } - // check size - if ((decodedNetworkSize + PublicKey::ed25519Size + checksumSize) != decoded.size()) { - return false; - } - // compare network - if (decodedNetwork != network) { - return false; - } - auto checksum = computeChecksum(Data(decoded.begin(), decoded.end() - checksumSize)); - // compare checksum - if (!std::equal(decoded.end() - checksumSize, decoded.end(), checksum.begin())) { - return false; - } - return true; -} - -template -Data SS58Address::computeChecksum(const T& data) { - auto prefix = Data(gSS58Prefix.begin(), gSS58Prefix.end()); - append(prefix, Data(data.begin(), data.end())); - auto hash = Hash::blake2b(prefix, 64); - auto checksum = Data(checksumSize); - std::copy(hash.begin(), hash.begin() + checksumSize, checksum.data()); - return checksum; -} - -/// Initializes an address with a string representation. -SS58Address::SS58Address(const std::string& string, uint32_t network) { - if (!isValid(string, network)) { - throw std::invalid_argument("Invalid address string"); - } - const auto decoded = Base58::decode(string); - bytes.resize(decoded.size() - checksumSize); - std::copy(decoded.begin(), decoded.end() - checksumSize, bytes.begin()); -} - -/// Initializes an address with a public key and network. -SS58Address::SS58Address(const PublicKey& publicKey, uint32_t network) { - if (publicKey.type != TWPublicKeyTypeED25519) { - throw std::invalid_argument("SS58Address expects an ed25519 public key."); - } - if (!encodeNetwork(network, bytes)) { - throw std::invalid_argument(std::string("network out of range ") + std::to_string(network)); - } - TW::append(bytes, publicKey.bytes); -} - -/// Returns a string representation of the address. -std::string SS58Address::string() const { - auto result = Data(bytes.begin(), bytes.end()); - auto checksum = computeChecksum(bytes); - append(result, checksum); - return Base58::encode(result); -} - -/// Returns public key bytes -Data SS58Address::keyBytes() const { - byte networkSize; - uint32_t networkTemp; - decodeNetwork(bytes, networkSize, networkTemp); - return Data(bytes.begin() + networkSize, bytes.end()); -} - -// Return true and the network size (1 or 2) and network if input is valid -bool SS58Address::decodeNetwork(const Data& data, byte& networkSize, uint32_t& network) { - networkSize = 0; - network = 0; - if (data.size() >= 1 && data[0] < networkSimpleLimit) { // 0 -- 63 - networkSize = 1; - network = (uint32_t)(data[0]); - return true; - } - // src https://github.com/paritytech/substrate/blob/master/primitives/core/src/crypto.rs - if (data.size() >= 2 && data[0] >= networkSimpleLimit && data[0] < networkFullLimit) { // 64 -- 127 - networkSize = 2; - byte lower = (byte)((data[0] & 0b00111111) << 2) | (byte)((data[1] & 0b11000000) >> 6); - byte upper = data[1] & 0b00111111; - network = ((uint32_t)upper << 8) + lower; - return (network >= networkSimpleLimit); - } - return false; -} - -bool SS58Address::encodeNetwork(uint32_t network, Data& data) { - if (network < networkSimpleLimit) { // 0 -- 63 - // Simple account/address/network - data = {(byte)network}; - return true; - } - if (network < 0x4000) { // 64 -- 16383 - // Full address/address/network identifier. - byte first = networkSimpleLimit + (byte)((network & 0b0000000011111100) >> 2); - byte second = (byte)((network & 0b0011111100000000) >> 8) | (byte)((byte)(network & 0b0000000000000011) << 6); - data = {first, second}; - return true; - } - // not supported - return false; -} diff --git a/src/Polkadot/SS58Address.h b/src/Polkadot/SS58Address.h deleted file mode 100644 index 44ad512874b..00000000000 --- a/src/Polkadot/SS58Address.h +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Base58.h" -#include "Data.h" -#include "PublicKey.h" - -#include - -inline const std::string gSS58Prefix{"SS58PRE"}; - -namespace TW { - -/// Base-58-encodeed Substrate address. -class SS58Address { - public: - static const size_t checksumSize = 2; - - // networks 0 -- 63 are encoded in one byte (00aaaaaa) - static const byte networkSimpleLimit = 0x40; - // networks 64 -- 16383 are encoded in 2 bytes: network 00cccccc_aaaaaabb is encoded as 01aaaaaa, bbcccccc (first byte between 64 and 127) - // see: https://github.com/paritytech/substrate/blob/master/primitives/core/src/crypto.rs - // https://docs.substrate.io/v3/advanced/ss58/#address-type - static const byte networkFullLimit = 0x80; - - /// Address data consisting of one or more network byte(s) followed by the public key. - Data bytes; - - /// Determines whether a string makes a valid address - static bool isValid(const std::string& string, uint32_t network); - - template - static Data computeChecksum(const T& data); - - SS58Address() = default; - - /// Initializes an address with a string representation. - SS58Address(const std::string& string, uint32_t network); - - /// Initializes an address with a public key and network. - SS58Address(const PublicKey& publicKey, uint32_t network); - - /// Returns a string representation of the address. - std::string string() const; - - /// Returns public key bytes - Data keyBytes() const; - - // Return true and the network size (1 or 2) and network if input is valid - static bool decodeNetwork(const Data& data, byte& networkSize, uint32_t& network); - - static bool encodeNetwork(uint32_t network, Data& data); -}; - -inline bool operator==(const SS58Address& lhs, const SS58Address& rhs) { - return lhs.bytes == rhs.bytes; -} - -} // namespace TW diff --git a/src/Polkadot/ScaleCodec.h b/src/Polkadot/ScaleCodec.h deleted file mode 100644 index eb7dbadab12..00000000000 --- a/src/Polkadot/ScaleCodec.h +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "BinaryCoding.h" -#include "Data.h" -#include "PublicKey.h" -#include "SS58Address.h" -#include "uint256.h" - -#include -#include -#include - -using CompactInteger = TW::uint256_t; - -namespace TW::Polkadot { - -static constexpr size_t kMinUint16 = (1ul << 6u); -static constexpr size_t kMinUint32 = (1ul << 14u); -static constexpr size_t kMinBigInteger = (1ul << 30u); -// max uint8 -static constexpr byte kMaxByte = 255; - -inline size_t countBytes(CompactInteger value) { - if (0 == value) { - return 1; - } - - size_t size = 0; - while (value > 0) { - ++size; - value >>= 8; - } - - return size; -} - -inline Data encodeCallIndex(int32_t moduleIndex, int32_t methodIndex) { - if (moduleIndex > kMaxByte) { - throw std::invalid_argument("module index too large"); - } - if (methodIndex > kMaxByte) { - throw std::invalid_argument("method index too large"); - } - - return Data{static_cast(moduleIndex), static_cast(methodIndex)}; -} - -inline Data encodeCompact(CompactInteger value) { - auto data = Data{}; - - if (value < kMinUint16) { - auto v = value.convert_to() << 2u; - data.push_back(static_cast(v)); - return data; - } else if (value < kMinUint32) { - auto v = (value.convert_to() << 2u); - v += 0x01; // set 0b01 flag - auto minor_byte = static_cast(v & 0xffu); - data.push_back(minor_byte); - v >>= 8u; - auto major_byte = static_cast(v & 0xffu); - data.push_back(major_byte); - return data; - } else if (value < kMinBigInteger) { - uint32_t v = (value.convert_to() << 2u); - v += 0x02; // set 0b10 flag - encode32LE(v, data); - return data; - } - - auto length = countBytes(value); - if (length > 67) { - // too big - return data; - } - uint8_t header = (static_cast(length) - 4) * 4; - header += 0x03; // set 0b11 flag; - data.push_back(header); - - auto v = CompactInteger{value}; - for (size_t i = 0; i < length; ++i) { - data.push_back(static_cast(v & 0xff)); // push back least significant byte - v >>= 8; - } - return data; -} - -// append length prefix -inline void encodeLengthPrefix(Data& data) { - size_t len = data.size(); - auto prefix = encodeCompact(len); - data.insert(data.begin(), prefix.begin(), prefix.end()); -} - -inline Data encodeBool(bool value) { - return Data{uint8_t(value ? 0x01 : 0x00)}; -} - -inline Data encodeVector(const std::vector& vec) { - auto data = encodeCompact(vec.size()); - for (auto v : vec) { - append(data, v); - } - return data; -} - -inline Data encodeAccountId(const Data& bytes, bool raw) { - auto data = Data{}; - if (!raw) { - // MultiAddress::AccountId - // https://github.com/paritytech/substrate/blob/master/primitives/runtime/src/multiaddress.rs#L28 - append(data, 0x00); - } - append(data, bytes); - return data; -} - -inline Data encodeAccountIds(const std::vector& addresses, bool raw) { - std::vector vec; - for (auto addr : addresses) { - vec.push_back(encodeAccountId(addr.keyBytes(), raw)); - } - return encodeVector(vec); -} - -inline Data encodeEra(const uint64_t block, const uint64_t period) { - // MortalEra(phase, period) - // See decodeMortalObject at https://github.com/polkadot-js/api/blob/master/packages/types/src/extrinsic/ExtrinsicEra.ts#L87 - // See toU8a at https://github.com/polkadot-js/api/blob/master/packages/types/src/extrinsic/ExtrinsicEra.ts#L167 - uint64_t calPeriod = uint64_t(pow(2, ceil(log2(double(period))))); - calPeriod = std::min(std::max(calPeriod, uint64_t(4)), uint64_t(1) << 16); - uint64_t phase = block % calPeriod; - uint64_t quantizeFactor = std::max(calPeriod >> uint64_t(12), uint64_t(1)); - uint64_t quantizedPhase = phase / quantizeFactor * quantizeFactor; - - auto bitset = std::bitset<64>(calPeriod); - int trailingZeros = 0; - for (int i = 0; i < 64 - 1; i++) { - if (bitset[i] == 0) { - trailingZeros += 1; - } else { - break; - } - } - auto encoded = std::min(15, std::max(1, trailingZeros - 1)) + (((quantizedPhase / quantizeFactor) << 4)); - return Data{byte(encoded & 0xff), byte(encoded >> 8)}; -} - -} // namespace TW::Polkadot diff --git a/src/Polkadot/Signer.cpp b/src/Polkadot/Signer.cpp deleted file mode 100644 index 8107a1bf196..00000000000 --- a/src/Polkadot/Signer.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Signer.h" -#include "Extrinsic.h" -#include "../Hash.h" -#include "../PrivateKey.h" - -namespace TW::Polkadot { - -static constexpr size_t hashTreshold = 256; - -Proto::SigningOutput Signer::sign(const Proto::SigningInput &input) noexcept { - auto privateKey = PrivateKey(Data(input.private_key().begin(), input.private_key().end())); - auto publicKey = privateKey.getPublicKey(TWPublicKeyTypeED25519); - auto extrinsic = Extrinsic(input); - auto payload = extrinsic.encodePayload(); - // check if need to hash - if (payload.size() > hashTreshold) { - payload = Hash::blake2b(payload, 32); - } - auto signature = privateKey.sign(payload, TWCurveED25519); - auto encoded = extrinsic.encodeSignature(publicKey, signature); - - auto protoOutput = Proto::SigningOutput(); - protoOutput.set_encoded(encoded.data(), encoded.size()); - return protoOutput; -} - -Data Signer::signaturePreImage(const Proto::SigningInput &input) { - auto extrinsic = Extrinsic(input); - auto payload = extrinsic.encodePayload(); - // check if need to hash - if (payload.size() > hashTreshold) { - payload = Hash::blake2b(payload, 32); - } - return payload; -} - -Data Signer::encodeTransaction(const Proto::SigningInput &input, const Data &publicKey, const Data &signature) { - auto pbk = PublicKey(publicKey, TWPublicKeyTypeED25519); - auto extrinsic = Extrinsic(input); - auto encoded = extrinsic.encodeSignature(pbk, signature); - return encoded; -} - -Data Signer::hash(const Data &payload) { - // check if need to hash - if (payload.size() > hashTreshold) { - return Hash::blake2b(payload, 32); - } - - return payload; -} - -} // namespace TW::Polkadot diff --git a/src/Polkadot/Signer.h b/src/Polkadot/Signer.h deleted file mode 100644 index fa0204b2b01..00000000000 --- a/src/Polkadot/Signer.h +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "Data.h" -#include "../PrivateKey.h" -#include "../proto/Polkadot.pb.h" - -namespace TW::Polkadot { - -/// Helper class that performs Polkadot transaction signing. -class Signer { -public: - /// Hide default constructor - explicit Signer(); - - /// Signs a Proto::SigningInput transaction - static Proto::SigningOutput sign(const Proto::SigningInput& input) noexcept; - - static Data signaturePreImage(const Proto::SigningInput &input); - static Data encodeTransaction(const Proto::SigningInput &input, const Data &publicKey, const Data &signature); - static Data hash(const Data &payload); -}; - -} // namespace TW::Polkadot diff --git a/src/rust/RustCoinEntry.cpp b/src/rust/RustCoinEntry.cpp index 870bbe2c076..5daa97f68f5 100644 --- a/src/rust/RustCoinEntry.cpp +++ b/src/rust/RustCoinEntry.cpp @@ -15,8 +15,10 @@ bool RustCoinEntry::validateAddress(TWCoinType coin, const std::string &address, } else if (const auto* hrpPrefix = std::get_if(&addressPrefix); hrpPrefix) { Rust::TWStringWrapper hrpStr = std::string(*hrpPrefix); return Rust::tw_any_address_is_valid_bech32(addressStr.get(), static_cast(coin), hrpStr.get()); + } else if (const auto* ss58Prefix = std::get_if(&addressPrefix); ss58Prefix) { + return Rust::tw_any_address_is_valid_ss58(addressStr.get(), static_cast(coin), static_cast(*ss58Prefix)); } else { - throw std::invalid_argument("`Rust::tw_any_address_is_valid_ss58`, `Rust::tw_any_address_create_with_public_key_filecoin_address_type` are not supported yet"); + throw std::invalid_argument("`Rust::tw_any_address_create_with_public_key_filecoin_address_type` are not supported yet"); } } @@ -53,8 +55,12 @@ std::string RustCoinEntry::deriveAddress(TWCoinType coin, const PublicKey& publi anyAddressRaw = Rust::tw_any_address_create_bech32_with_public_key(twPublicKey.get(), static_cast(coin), hrpStr.get()); + } else if (const auto* ss58Prefix = std::get_if(&addressPrefix); ss58Prefix) { + anyAddressRaw = Rust::tw_any_address_create_ss58_with_public_key(twPublicKey.get(), + static_cast(coin), + static_cast(*ss58Prefix)); } else { - throw std::invalid_argument("`Rust::tw_any_address_is_valid_ss58`, `Rust::tw_any_address_create_with_public_key_filecoin_address_type` are not supported yet"); + throw std::invalid_argument("`Rust::tw_any_address_create_with_public_key_filecoin_address_type` are not supported yet"); } auto anyAddress = Rust::wrapTWAnyAddress(anyAddressRaw); diff --git a/tests/chains/Acala/TWAnySignerTests.cpp b/tests/chains/Acala/TWAnySignerTests.cpp index cf8cb5f1641..8bce503bce8 100644 --- a/tests/chains/Acala/TWAnySignerTests.cpp +++ b/tests/chains/Acala/TWAnySignerTests.cpp @@ -5,9 +5,10 @@ #include "HexCoding.h" #include "uint256.h" #include "proto/Polkadot.pb.h" -#include "Polkadot/Extrinsic.h" +#include "proto/TransactionCompiler.pb.h" #include "PrivateKey.h" #include +#include #include "TestUtilities.h" #include @@ -16,6 +17,7 @@ namespace TW::Polkadot::tests { TEST(TWAnySignerAcala, Sign) { // Successfully broadcasted: https://acala.subscan.io/extrinsic/3893620-3 + const auto coin = TWCoinTypePolkadot; auto secret = parse_hex("9066aa168c379a403becb235c15e7129c133c244e56a757ab07bc369288bcab0"); auto genesisHash = parse_hex("fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c"); @@ -46,9 +48,18 @@ TEST(TWAnySignerAcala, Sign) { callIndices->set_module_index(0x0a); callIndices->set_method_index(0x00); - auto extrinsic = Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - EXPECT_EQ(hex(preimage), "0a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8d502000000de08000002000000fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c707ffa05b7dc6cdb6356bd8bd51ff20b2757c3214a76277516080a10f1bc753700"); + auto txInputData = data(input.SerializeAsString()); + auto txInputDataPtr = WRAPD(TWDataCreateWithBytes(txInputData.data(), txInputData.size())); + const auto preImageHashes = WRAPD(TWTransactionCompilerPreImageHashes(coin, txInputDataPtr.get())); + auto preImageHash = data(TWDataBytes(preImageHashes.get()), TWDataSize(preImageHashes.get())); + + TxCompiler::Proto::PreSigningOutput preSigningOutput; + ASSERT_TRUE(preSigningOutput.ParseFromArray(preImageHash.data(), int(preImageHash.size()))); + ASSERT_EQ(preSigningOutput.error(), Common::Proto::OK); + + const auto preImageHashData = data(preSigningOutput.data_hash()); + + EXPECT_EQ(hex(preImageHashData), "0a0000c8c602ded977c56076ae38d98026fa669ca10d6a2b5a0bfc4086ae7668ed1c60070010a5d4e8d502000000de08000002000000fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c707ffa05b7dc6cdb6356bd8bd51ff20b2757c3214a76277516080a10f1bc753700"); Proto::SigningOutput output; ANY_SIGN(input, TWCoinTypePolkadot); diff --git a/tests/chains/Kusama/AddressTests.cpp b/tests/chains/Kusama/AddressTests.cpp deleted file mode 100644 index a29aa2abf54..00000000000 --- a/tests/chains/Kusama/AddressTests.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "HexCoding.h" -#include "Kusama/Address.h" -#include "PublicKey.h" -#include "PrivateKey.h" -#include -#include - -namespace TW::Kusama::tests { - -TEST(KusamaAddress, Validation) { - // Substrate ed25519 - ASSERT_FALSE(Address::isValid("5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ")); - // Polkadot ed25519 - ASSERT_FALSE(Address::isValid("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony")); - // Polkadot sr25519 - ASSERT_FALSE(Address::isValid("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony")); - // Bitcoin - ASSERT_FALSE(Address::isValid("1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA")); - - // Kusama ed25519 - ASSERT_TRUE(Address::isValid("FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ")); - // Kusama secp256k1 - ASSERT_TRUE(Address::isValid("FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx")); - // Kusama sr25519 - ASSERT_TRUE(Address::isValid("EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe")); -} - -TEST(KusamaAddress, FromPrivateKey) { - // from subkey: tiny escape drive pupil flavor endless love walk gadget match filter luxury - auto privateKey = PrivateKey(parse_hex("0xa21981f3bb990c40837df44df639541ff57c5e600f9eb4ac00ed8d1f718364e5")); - auto address = Address(privateKey.getPublicKey(TWPublicKeyTypeED25519)); - ASSERT_EQ(address.string(), "CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM"); -} - -TEST(KusamaAddress, FromPublicKey) { - auto publicKey = PublicKey(parse_hex("0x032eb287017c5cde2940b5dd062d413f9d09f8aa44723fc80bf46b96c81ac23d"), TWPublicKeyTypeED25519); - auto address = Address(publicKey); - ASSERT_EQ(address.string(), "CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM"); -} - -TEST(KusamaAddress, FromString) { - auto address = Address("CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM"); - ASSERT_EQ(address.string(), "CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM"); -} - -} // namespace TW::Kusama::tests \ No newline at end of file diff --git a/tests/chains/Kusama/SignerTests.cpp b/tests/chains/Kusama/SignerTests.cpp deleted file mode 100644 index f271547e1e8..00000000000 --- a/tests/chains/Kusama/SignerTests.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "HexCoding.h" -#include "Polkadot/Extrinsic.h" -#include "Polkadot/SS58Address.h" -#include "Polkadot/Signer.h" -#include "Coin.h" -#include "PrivateKey.h" -#include "PublicKey.h" -#include "proto/Polkadot.pb.h" -#include "uint256.h" - -#include -#include - - -namespace TW::Polkadot::tests { - extern PrivateKey privateKey; - extern PublicKey toPublicKey; - auto genesisHashKSM = parse_hex("b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"); - -TEST(KusamaSigner, SignTransferKSM) { - auto blockHash = parse_hex("4955dd4813f3e91ef3fd5a825b928af2fc50a71380085f753ccef00bb1582891"); - auto toAddress = SS58Address(toPublicKey, TWSS58AddressTypeKusama); - - auto input = TW::Polkadot::Proto::SigningInput(); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_genesis_hash(genesisHashKSM.data(), genesisHashKSM.size()); - input.set_nonce(0); - input.set_spec_version(2019); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - input.set_network(ss58Prefix(TWCoinType::TWCoinTypeKusama)); - input.set_transaction_version(2); - - auto balanceCall = input.mutable_balance_call(); - auto& transfer = *balanceCall->mutable_transfer(); - auto value = store(uint256_t(12345)); - transfer.set_to_address(toAddress.string()); - transfer.set_value(value.data(), value.size()); - - auto extrinsic = TW::Polkadot::Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - auto output = TW::Polkadot::Signer::sign(input); - - ASSERT_EQ(hex(preimage), "04008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0000000e307000002000000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe4955dd4813f3e91ef3fd5a825b928af2fc50a71380085f753ccef00bb1582891"); - ASSERT_EQ(hex(output.encoded()), "25028488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee000765cfa76cfe19499f4f19ef7dc4527652ec5b2e6b5ecfaf68725dafd48ae2694ad52e61f44152a544784e847de10ddb2c56bee4406574dcbcfdb5e5d35b6d0300000004008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); -} - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Kusama/TWAnyAddressTests.cpp b/tests/chains/Kusama/TWAnyAddressTests.cpp new file mode 100644 index 00000000000..426c15989de --- /dev/null +++ b/tests/chains/Kusama/TWAnyAddressTests.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include +#include +#include + +#include "TestUtilities.h" +#include +#include + +namespace TW::Kusama::tests { + +TEST(KusamaAddress, Validation) { + // Substrate ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ").get(), TWCoinTypeKusama)); + // Polkadot ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony").get(), TWCoinTypeKusama)); + // Polkadot sr25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony").get(), TWCoinTypeKusama)); + // Bitcoin + ASSERT_FALSE(TWAnyAddressIsValid(STRING("1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA").get(), TWCoinTypeKusama)); + + // Kusama ed25519 + ASSERT_TRUE(TWAnyAddressIsValid(STRING("FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ").get(), TWCoinTypeKusama)); + // Kusama secp256k1 + ASSERT_TRUE(TWAnyAddressIsValid(STRING("FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx").get(), TWCoinTypeKusama)); + // Kusama sr25519 + ASSERT_TRUE(TWAnyAddressIsValid(STRING("EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe").get(), TWCoinTypeKusama)); +} + +TEST(KusamaAddress, FromPrivateKey) { + // from subkey: tiny escape drive pupil flavor endless love walk gadget match filter luxury + const auto privateKey = WRAP(TWPrivateKey, TWPrivateKeyCreateWithData(DATA("0xa21981f3bb990c40837df44df639541ff57c5e600f9eb4ac00ed8d1f718364e5").get())); + const auto publicKey = WRAP(TWPublicKey, TWPrivateKeyGetPublicKey(privateKey.get(), TWCoinTypeKusama)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypeKusama)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM").get())); +} + +TEST(KusamaAddress, FromPublicKey) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("0x032eb287017c5cde2940b5dd062d413f9d09f8aa44723fc80bf46b96c81ac23d").get(), TWPublicKeyTypeED25519)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypeKusama)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM").get())); +} + +TEST(KusamaAddress, FromString) { + auto addressStr1 = STRING("CeVXtoU4py9e7F6upfM2ZarVave299TjcdaTSxhDDZrYgnM"); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(addressStr1.get(), TWCoinTypeKusama)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressStr1.get())); +} + +} // namespace TW::Kusama::tests diff --git a/tests/chains/Kusama/TWAnySignerTests.cpp b/tests/chains/Kusama/TWAnySignerTests.cpp index cb61bf2ee82..988e99cc0e4 100644 --- a/tests/chains/Kusama/TWAnySignerTests.cpp +++ b/tests/chains/Kusama/TWAnySignerTests.cpp @@ -2,15 +2,54 @@ // // Copyright © 2017 Trust Wallet. +#include "Coin.h" #include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "TestUtilities.h" #include "proto/Polkadot.pb.h" #include "uint256.h" -#include "TestUtilities.h" + +#include #include +#include +#include "TestUtilities.h" #include namespace TW::Polkadot::tests { +extern uint32_t kusamaPrefix; +extern PrivateKey privateKey; +extern TWPublicKey* publicKey; +auto genesisHashKSM = parse_hex("b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"); + +extern Data helper_encodePayload(TWCoinType coin, const Proto::SigningInput& input); + +TEST(TWAnySignerKusama, SignTransferKSM) { + auto blockHash = parse_hex("4955dd4813f3e91ef3fd5a825b928af2fc50a71380085f753ccef00bb1582891"); + + auto input = TW::Polkadot::Proto::SigningInput(); + input.set_block_hash(blockHash.data(), blockHash.size()); + input.set_genesis_hash(genesisHashKSM.data(), genesisHashKSM.size()); + input.set_nonce(0); + input.set_spec_version(2019); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + input.set_network(kusamaPrefix); + input.set_transaction_version(2); + + auto balanceCall = input.mutable_balance_call(); + auto& transfer = *balanceCall->mutable_transfer(); + auto value = store(uint256_t(12345)); + transfer.set_to_address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"); + transfer.set_value(value.data(), value.size()); + + auto preimage = helper_encodePayload(TWCoinTypeKusama, input); + ASSERT_EQ(hex(preimage), "04008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0000000e307000002000000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe4955dd4813f3e91ef3fd5a825b928af2fc50a71380085f753ccef00bb1582891"); + + Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypeKusama); + ASSERT_EQ(hex(output.encoded()), "25028488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee000765cfa76cfe19499f4f19ef7dc4527652ec5b2e6b5ecfaf68725dafd48ae2694ad52e61f44152a544784e847de10ddb2c56bee4406574dcbcfdb5e5d35b6d0300000004008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); +} TEST(TWAnySignerKusama, Sign) { auto key = parse_hex("0x8cdc538e96f460da9d639afc5c226f477ce98684d77fb31e88db74c1f1dd86b2"); diff --git a/tests/chains/Polkadot/AddressTests.cpp b/tests/chains/Polkadot/AddressTests.cpp deleted file mode 100644 index 47fb3a5d8ca..00000000000 --- a/tests/chains/Polkadot/AddressTests.cpp +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "HexCoding.h" -#include "Polkadot/Address.h" -#include "PublicKey.h" -#include "PrivateKey.h" -#include -#include - -namespace TW::Polkadot::tests { - -TEST(PolkadotAddress, Validation) { - // Substrate ed25519 - ASSERT_FALSE(Address::isValid("5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ")); - // Bitcoin - ASSERT_FALSE(Address::isValid("1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA")); - // Kusama ed25519 - ASSERT_FALSE(Address::isValid("FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ")); - // Kusama secp256k1 - ASSERT_FALSE(Address::isValid("FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx")); - // Kusama sr25519 - ASSERT_FALSE(Address::isValid("EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe")); - - // Polkadot ed25519 - ASSERT_TRUE(Address::isValid("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu")); - // Polkadot sr25519 - ASSERT_TRUE(Address::isValid("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony")); - - ASSERT_TRUE(Address::isValid("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64)); - ASSERT_FALSE(Address::isValid("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A", 64)); - - // Polymesh - ASSERT_TRUE(Address::isValid("2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG", 12)); - ASSERT_FALSE(Address::isValid("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A", 12)); -} - -TEST(PolkadotAddress, FromPrivateKey) { - // subkey phrase `chief menu kingdom stereo hope hazard into island bag trick egg route` - auto privateKey = PrivateKey(parse_hex("0x612d82bc053d1b4729057688ecb1ebf62745d817ddd9b595bc822f5f2ba0e41a")); - auto address = Address(privateKey.getPublicKey(TWPublicKeyTypeED25519)); - ASSERT_EQ(address.string(), "15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); -} - -TEST(PolkadotAddress, FromPublicKey) { - auto publicKey = PublicKey(parse_hex("0xbeff0e5d6f6e6e6d573d3044f3e2bfb353400375dc281da3337468d4aa527908"), TWPublicKeyTypeED25519); - auto address = Address(publicKey); - ASSERT_EQ(address.string(), "15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); -} - -TEST(PolkadotAddress, FromPublicKeyWithPrefix) { - auto publicKey = PublicKey(parse_hex("0x92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"), TWPublicKeyTypeED25519); - auto addressPolkadot = Address(publicKey, 0); - ASSERT_EQ(addressPolkadot.string(), "14KjL5vGAYJCbKgZJmFKDSjewtBpvaxx9YvRZvi7qmb5s8CC"); - - auto addressAstar = Address(publicKey, 5); - ASSERT_EQ(addressAstar.string(), "ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd"); - - auto addressParallel = Address(publicKey, 172); - ASSERT_EQ(addressParallel.string(), "p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); - - // polymesh - publicKey = PublicKey(parse_hex("849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f83"), TWPublicKeyTypeED25519); - auto addressPolymesh = Address(publicKey, 12); - ASSERT_EQ(addressPolymesh.string(), "2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); -} - -TEST(PolkadotAddress, FromString) { - auto address = Address("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); - ASSERT_EQ(address.string(), "15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); -} - -TEST(PolkadotAddress, FromStringWithPrefix) { - auto addressKusama = Address("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); - ASSERT_EQ(addressKusama.string(), "Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); - - auto addressParallel = Address("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); - ASSERT_EQ(addressParallel.string(), "p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); - - // polymesh - auto addressPolymesh = Address("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW", 12); - ASSERT_EQ(addressPolymesh.string(), "2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); -} - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/ExtrinsicTests.cpp b/tests/chains/Polkadot/ExtrinsicTests.cpp deleted file mode 100644 index 4f5c969722a..00000000000 --- a/tests/chains/Polkadot/ExtrinsicTests.cpp +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Data.h" -#include "HexCoding.h" -#include "Polkadot/Extrinsic.h" -#include "proto/Polkadot.pb.h" -#include "uint256.h" - -#include - -using namespace std; -using namespace TW; - -namespace TW::Polkadot::tests { - -TEST(PolkadotExtrinsic, Polymesh_encodeTransferWithMemo) { - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - - auto* transfer = input.mutable_balance_call()->mutable_transfer(); - transfer->set_to_address("2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF"); - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x05); - callIndices->set_method_index(0x01); - - auto value = store(1); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_memo("MEMO PADDED WITH SPACES"); - - auto result = Polkadot::Extrinsic(input).encodeCall(input); - EXPECT_EQ(hex(result), "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000"); -} - -TEST(PolkadotExtrinsic, Polymesh_encodeAuthorizationJoinIdentity) { - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - - auto* identity = input.mutable_polymesh_call()->mutable_identity_call()->mutable_add_authorization(); - identity->set_target("2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc"); - auto* authCallIndices = identity->mutable_call_indices()->mutable_custom(); - authCallIndices->set_module_index(0x07); - authCallIndices->set_method_index(0x0d); - - auto result = Polkadot::Extrinsic(input).encodeCall(input); - EXPECT_EQ(hex(result), "070d0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000"); - - auto* authData = identity->mutable_data(); - authData->mutable_asset()->set_data({0x00}); - authData->mutable_extrinsic()->set_data({0x00}); - authData->mutable_portfolio()->set_data({0x00}); - - EXPECT_EQ(hex(result), hex(Polkadot::Extrinsic(input).encodeCall(input))); - - // clear data - authData->clear_asset(); - authData->clear_extrinsic(); - authData->clear_portfolio(); - - EXPECT_EQ(hex(Polkadot::Extrinsic(input).encodeCall(input)), "070d0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000"); -} - -TEST(PolkadotExtrinsic, Polymesh_encodeIdentity) { - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - - auto* key = input.mutable_polymesh_call()->mutable_identity_call()->mutable_join_identity_as_key(); - key->set_auth_id(4875); - auto* callIndices = key->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x07); - callIndices->set_method_index(0x05); - - auto result = Polkadot::Extrinsic(input).encodeCall(input); - EXPECT_EQ(hex(result), "07050b13000000000000"); -} - -TEST(PolkadotExtrinsic, Statemint_encodeAssetTransfer) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2619512-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD"); - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x32); - callIndices->set_method_index(0x05); - - auto value = store(999500000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(1984); - - auto result = Polkadot::Extrinsic(input).encodeCall(input); - // clang-format off - EXPECT_EQ(hex(result), "3205" - "011f" - "00" - "a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d" - "82a34cee"); - // clang-format on -} - -TEST(PolkadotExtrinsic, Statemint_encodeBatchAssetTransfer) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2571849-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - - auto* transfer = input.mutable_balance_call()->mutable_batch_asset_transfer(); - auto* batchCallIndices = transfer->mutable_call_indices()->mutable_custom(); - batchCallIndices->set_module_index(0x28); - batchCallIndices->set_method_index(0x00); - transfer->set_fee_asset_id(0x00); - - auto* t = transfer->add_transfers(); - t->set_to_address("13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy"); - auto value = store(808081); - t->set_value(std::string(value.begin(), value.end())); - t->set_asset_id(1984); - - auto* transferCallIndices = t->mutable_call_indices()->mutable_custom(); - transferCallIndices->set_module_index(0x32); - transferCallIndices->set_method_index(0x06); - - auto result = Polkadot::Extrinsic(input).encodeCall(input); - // clang-format off - EXPECT_EQ(hex(result), "2800" - "04" - "3206" - "011f" - "00" - "81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91" - "46523100"); - // clang-format on -} - -TEST(PolkadotExtrinsic, Kusama_encodeAssetTransferNoCallIndices) { - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - - auto* transfer = input.mutable_balance_call()->mutable_batch_asset_transfer(); - transfer->set_fee_asset_id(0x00); - - auto* t = transfer->add_transfers(); - t->set_to_address("13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy"); - auto value = store(808081); - t->set_value(std::string(value.begin(), value.end())); - t->set_asset_id(1984); - - EXPECT_THROW(Polkadot::Extrinsic(input).encodeCall(input), std::invalid_argument); -} - -TEST(PolkadotExtrinsic, Polkadot_EncodePayloadWithNewSpec) { - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD"); - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x32); - callIndices->set_method_index(0x05); - - auto value = store(999500000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(1984); - - input.set_spec_version(1002000); // breaking change happens at version 1002005 - auto result = Polkadot::Extrinsic(input).encodePayload(); - EXPECT_EQ(hex(result), "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee00000000104a0f0000000000"); - - input.set_spec_version(1002005); // >= 1002005 - result = Polkadot::Extrinsic(input).encodePayload(); - EXPECT_EQ(hex(result), "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee0000000000154a0f000000000000"); - - input.set_spec_version(1002006); // >= 1002005 - result = Polkadot::Extrinsic(input).encodePayload(); - EXPECT_EQ(hex(result), "3205011f00a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d82a34cee0000000000164a0f000000000000"); -} - - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/SS58AddressTests.cpp b/tests/chains/Polkadot/SS58AddressTests.cpp deleted file mode 100644 index 6482c70f1f0..00000000000 --- a/tests/chains/Polkadot/SS58AddressTests.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "Polkadot/SS58Address.h" -#include "HexCoding.h" -#include "PublicKey.h" -#include "TestUtilities.h" - -#include -#include - -using namespace TW; - -namespace TW::Polkadot::tests { - -TEST(SS58Address, IsValid) { - EXPECT_TRUE(SS58Address::isValid("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu", 0)); - - EXPECT_TRUE(SS58Address::isValid("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 5)); - EXPECT_FALSE(SS58Address::isValid("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd", 6)); - - EXPECT_TRUE(SS58Address::isValid("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2)); - EXPECT_FALSE(SS58Address::isValid("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 5)); - - EXPECT_TRUE(SS58Address::isValid("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 64)); - EXPECT_FALSE(SS58Address::isValid("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb", 65)); - EXPECT_FALSE(SS58Address::isValid("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A", 64)); - - EXPECT_TRUE(SS58Address::isValid("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172)); - EXPECT_FALSE(SS58Address::isValid("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 171)); - - EXPECT_TRUE(SS58Address::isValid("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE", 4096)); - EXPECT_FALSE(SS58Address::isValid("VDSyeURSP7ykE1zJPJGeqx6GcDZQF2DT3hAKhPMuwM5FuN9HE", 64)); - - EXPECT_TRUE(SS58Address::isValid("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6", 8219)); - EXPECT_FALSE(SS58Address::isValid("YDTv3GdhXPP3pQMqQtntGVg5hMno4jqanfYUgMPX2rLGJBKX6", 322)); -} - -const auto pubkeyString1 = "92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3"; - -TEST(SS58Address, FromPublicKey) { - auto publicKey = PublicKey(parse_hex(pubkeyString1), TWPublicKeyTypeED25519); - auto addressPolkadot = SS58Address(publicKey, 0); - EXPECT_EQ(addressPolkadot.string(), "14KjL5vGAYJCbKgZJmFKDSjewtBpvaxx9YvRZvi7qmb5s8CC"); - EXPECT_EQ(hex(addressPolkadot.keyBytes()), pubkeyString1); - - auto addressAstar = SS58Address(publicKey, 5); - EXPECT_EQ(addressAstar.string(), "ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd"); - EXPECT_EQ(hex(addressAstar.keyBytes()), pubkeyString1); - - auto addressParallel = SS58Address(publicKey, 172); - EXPECT_EQ(addressParallel.string(), "p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); - EXPECT_EQ(hex(addressParallel.keyBytes()), pubkeyString1); -} - -TEST(SS58Address, FromPublicKeyInvalid) { - auto publicKey = PublicKey(parse_hex(pubkeyString1), TWPublicKeyTypeED25519); - EXPECT_EXCEPTION(SS58Address(publicKey, 32771), "network out of range 32771"); -} - -TEST(SS58Address, FromString) { - auto addressKusama = SS58Address("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D", 2); - EXPECT_EQ(addressKusama.string(), "Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); - - auto addressParallel = SS58Address("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 172); - EXPECT_EQ(addressParallel.string(), "p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); -} - -TEST(SS58Address, FromStringInvalid) { - EXPECT_EXCEPTION(SS58Address("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL", 130), "Invalid address string"); -} - -std::map networkData = { - {0x00, "00"}, - {0x01, "01"}, - {0x02, "02"}, - {0x03, "03"}, - {0x04, "04"}, - {0x08, "08"}, - {0x0b, "0b"}, - {0x10, "10"}, - {0x20, "20"}, - {0x23, "23"}, - {0x30, "30"}, - {0x3f, "3f"}, - {0x40, "5000"}, - {0x41, "5040"}, - {0x80, "6000"}, - {0x0100, "4001"}, - {0x0123, "48c1"}, - {0x0200, "4002"}, - {0x0300, "4003"}, - {0x0400, "4004"}, - {0x0800, "4008"}, - {0x0fff, "7fcf"}, - {0x1000, "4010"}, - {0x1003, "40d0"}, - {0x2000, "4020"}, - {0x3000, "4030"}, - {0x3fff, "7fff"}, -}; - -TEST(SS58Address, DecodeNetwork) { - byte networkSize = 0; - uint32_t network = 0; - for (auto& d: networkData) { - std::string input = d.second + std::string("000102030405"); - EXPECT_TRUE(SS58Address::decodeNetwork(parse_hex(input), networkSize, network)); - EXPECT_EQ(network, d.first); - if (d.first < 64) { - EXPECT_EQ(networkSize, 1); - } else { - EXPECT_EQ(networkSize, 2); - } - } - - // 1. byte from invalid range - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("ab" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("8000" "000102030405"), networkSize, network)); - - // 2-byte, but decoded network is < 64 - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4000" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4040" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4080" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4100" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4200" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4400" "000102030405"), networkSize, network)); - EXPECT_FALSE(SS58Address::decodeNetwork(parse_hex("4800" "000102030405"), networkSize, network)); -} - -TEST(SS58Address, EncodeNetwork) { - Data data; - for (auto& d: networkData) { - EXPECT_TRUE(SS58Address::encodeNetwork(d.first, data)); - EXPECT_EQ(hex(data), d.second); - } - - // network > 16383 - EXPECT_FALSE(SS58Address::encodeNetwork(0x4000, data)); - EXPECT_FALSE(SS58Address::encodeNetwork(0x8000, data)); -} - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/ScaleCodecTests.cpp b/tests/chains/Polkadot/ScaleCodecTests.cpp deleted file mode 100644 index 7202e84037d..00000000000 --- a/tests/chains/Polkadot/ScaleCodecTests.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - - -#include "HexCoding.h" -#include "Polkadot/ScaleCodec.h" -#include "Kusama/Address.h" -#include "uint256.h" - -#include - -using namespace TW; -namespace TW::Polkadot::tests { - -TEST(PolkadotCodec, EncodeCompact) { - ASSERT_EQ(hex(encodeCompact(0)), "00"); - ASSERT_EQ(hex(encodeCompact(18)), "48"); - ASSERT_EQ(hex(encodeCompact(63)), "fc"); - ASSERT_EQ(hex(encodeCompact(64)), "0101"); - - ASSERT_EQ(hex(encodeCompact(12345)), "e5c0"); - ASSERT_EQ(hex(encodeCompact(16383)), "fdff"); - ASSERT_EQ(hex(encodeCompact(16384)), "02000100"); - - ASSERT_EQ(hex(encodeCompact(1073741823)), "feffffff"); - ASSERT_EQ(hex(encodeCompact(1073741824)), "0300000040"); - - ASSERT_EQ(hex(encodeCompact(4294967295)), "03ffffffff"); - ASSERT_EQ(hex(encodeCompact(4294967296)), "070000000001"); - - ASSERT_EQ(hex(encodeCompact(1099511627776)), "0b000000000001"); - ASSERT_EQ(hex(encodeCompact(281474976710656)), "0f00000000000001"); - - ASSERT_EQ(hex(encodeCompact(72057594037927935)), "0fffffffffffffff"); - ASSERT_EQ(hex(encodeCompact(72057594037927936)), "130000000000000001"); - - ASSERT_EQ(hex(encodeCompact(18446744073709551615u)), "13ffffffffffffffff"); -} - -TEST(PolkadotCodec, EncodeBool) { - ASSERT_EQ(hex(encodeBool(true)), "01"); - ASSERT_EQ(hex(encodeBool(false)), "00"); -} - -TEST(PolkadotCodec, EncodeLengthPrefix) { - auto encoded = parse_hex("84ff88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0034a113577b56545c45e18969471eebe11ed434f3b2f06e2e3dc8dc137ba804caf60757787ebdeb298327e2f29d68c5520965405ef5582db0445c06e1c11a8a0e0000000400ff8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); - encodeLengthPrefix(encoded); - - ASSERT_EQ(hex(encoded), "2d0284ff88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0034a113577b56545c45e18969471eebe11ed434f3b2f06e2e3dc8dc137ba804caf60757787ebdeb298327e2f29d68c5520965405ef5582db0445c06e1c11a8a0e0000000400ff8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); -} - -TEST(PolkadotCodec, encodeAccountId) { - auto address = Kusama::Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"); - auto encoded = encodeAccountId(address.keyBytes(), true); - auto encoded2 = encodeAccountId(address.keyBytes(), false); - - ASSERT_EQ(hex(encoded), "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"); - ASSERT_EQ(hex(encoded2), "008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"); -} - -TEST(PolkadotCodec, EncodeVectorAccountIds) { - auto addresses = std::vector{ - Kusama::Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP"), - Kusama::Address("CtwdfrhECFs3FpvCGoiE4hwRC4UsSiM8WL899HjRdQbfYZY") - }; - auto encoded = encodeAccountIds(addresses, false); - ASSERT_EQ(hex(encoded), "08008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000e33fdfb980e4499e5c3576e742a563b6a4fc0f6f598b1917fd7a6fe393ffc72"); -} - -TEST(PolkadotCodec, EncodeVectorAccountIdsKusama) { - auto addresses = std::vector{ - SS58Address("FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", TWSS58AddressTypeKusama), - SS58Address("CtwdfrhECFs3FpvCGoiE4hwRC4UsSiM8WL899HjRdQbfYZY", TWSS58AddressTypeKusama)}; - auto encoded = encodeAccountIds(addresses, false); - ASSERT_EQ(hex(encoded), "08008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000e33fdfb980e4499e5c3576e742a563b6a4fc0f6f598b1917fd7a6fe393ffc72"); -} - -TEST(PolkadotCodec, EncodeEra) { - auto era1 = encodeEra(429119, 8); - auto era2 = encodeEra(428861, 4); - auto era3 = encodeEra(4246319, 64); - ASSERT_EQ(hex(era1), "7200"); - ASSERT_EQ(hex(era2), "1100"); - EXPECT_EQ(hex(era3), "f502"); -} - -TEST(PolkadotCodec, CountBytes) { - EXPECT_EQ(size_t(1), countBytes(uint256_t(0))); - EXPECT_EQ(size_t(1), countBytes(uint256_t(1))); - EXPECT_EQ(size_t(2), countBytes(uint256_t("0x1ff"))); -} - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/SignerTests.cpp b/tests/chains/Polkadot/SignerTests.cpp deleted file mode 100644 index 2e4ae5b7f71..00000000000 --- a/tests/chains/Polkadot/SignerTests.cpp +++ /dev/null @@ -1,802 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#include "HexCoding.h" -#include "Polkadot/Address.h" -#include "Polkadot/Extrinsic.h" -#include "Polkadot/SS58Address.h" -#include "Polkadot/Signer.h" -#include "PrivateKey.h" -#include "PublicKey.h" -#include "proto/Polkadot.pb.h" -#include "Coin.h" -#include "uint256.h" - -#include -#include - - -namespace TW::Polkadot::tests { - auto privateKey = PrivateKey(parse_hex("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115")); - auto privateKeyIOS = PrivateKey(parse_hex("37932b086586a6675e66e562fe68bd3eeea4177d066619c602fe3efc290ada62")); - auto privateKeyThrow2 = PrivateKey(parse_hex("70a794d4f1019c3ce002f33062f45029c4f930a56b3d20ec477f7668c6bbc37f")); - auto privateKeyPolkadot = PrivateKey(parse_hex("298fcced2b497ed48367261d8340f647b3fca2d9415d57c2e3c5ef90482a2266")); - auto addressThrow2 = "14Ztd3KJDaB9xyJtRkREtSZDdhLSbm7UUKt8Z7AwSv7q85G2"; - auto toPublicKey = PublicKey(parse_hex("0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"), TWPublicKeyTypeED25519); - auto genesisHash = parse_hex("91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"); - auto controller1 = "14xKzzU1ZYDnzFj7FgdtDAYSMJNARjDc2gNw4XAFDgr4uXgp"; - -TEST(PolkadotSigner, SignTransfer_9fd062) { - auto toAddress = Address("13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"); - - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - auto blockHash = parse_hex("0x5d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(3); - input.set_spec_version(26); - { - PublicKey publicKey = privateKeyThrow2.getPublicKey(TWPublicKeyTypeED25519); - Address address = Address(publicKey); - EXPECT_EQ(address.string(), addressThrow2); - } - input.set_private_key(privateKeyThrow2.bytes.data(), privateKeyThrow2.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(5); - - // era: for blockhash and block number, use curl -H "Content-Type: application/json" -H "Accept: text/plain" https:///transaction/material?noMeta=true - auto era = input.mutable_era(); - era->set_block_number(3541050); - era->set_period(64); - - auto balanceCall = input.mutable_balance_call(); - auto transfer = balanceCall->mutable_transfer(); - auto value = store(uint256_t(2000000000)); // 0.2 - transfer->set_to_address(toAddress.string()); - transfer->set_value(value.data(), value.size()); - - auto extrinsic = Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - EXPECT_EQ(hex(preimage), "05007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577a5030c001a0000000500000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/0x9fd06208a6023e489147d8d93f0182b0cb7e45a40165247319b87278e08362d8 - EXPECT_EQ(hex(output.encoded()), "3502849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f7830073e59cef381aedf56d7af076bafff9857ffc1e3bd7d1d7484176ff5b58b73f1211a518e1ed1fd2ea201bd31869c0798bba4ffe753998c409d098b65d25dff801a5030c0005007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577"); -} - -TEST(PolkadotSigner, SignTransferDOT) { - - auto blockHash = parse_hex("0x343a3f4258fd92f5ca6ca5abdf473d86a78b0bcd0dc09c568ca594245cc8c642"); - auto toAddress = SS58Address(toPublicKey, TWSS58AddressTypePolkadot); - - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - - input.set_nonce(0); - input.set_spec_version(17); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(3); - - auto& era = *input.mutable_era(); - era.set_block_number(927699); - era.set_period(8); - - auto balanceCall = input.mutable_balance_call(); - auto& transfer = *balanceCall->mutable_transfer(); - auto value = store(uint256_t(12345)); - transfer.set_to_address(toAddress.string()); - transfer.set_value(value.data(), value.size()); - - auto extrinsic = Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - auto output = Signer::sign(input); - - ASSERT_EQ(hex(preimage), "05008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c032000000110000000300000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3343a3f4258fd92f5ca6ca5abdf473d86a78b0bcd0dc09c568ca594245cc8c642"); - ASSERT_EQ(hex(output.encoded()), "29028488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee003d91a06263956d8ce3ce5c55455baefff299d9cb2bb3f76866b6828ee4083770b6c03b05d7b6eb510ac78d047002c1fe5c6ee4b37c9c5a8b09ea07677f12e50d3200000005008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48e5c0"); -} - -TEST(PolkadotSigner, SignTransfer_72dd5b) { - - auto blockHash = parse_hex("7d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd"); - - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - - input.set_nonce(1); - input.set_spec_version(28); - input.set_private_key(privateKeyIOS.bytes.data(), privateKeyIOS.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(6); - - auto& era = *input.mutable_era(); - era.set_block_number(3910736); - era.set_period(64); - - auto balanceCall = input.mutable_balance_call(); - auto& transfer = *balanceCall->mutable_transfer(); - auto value = store(uint256_t(10000000000)); - transfer.set_to_address("13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"); - transfer.set_value(value.data(), value.size()); - - auto extrinsic = Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - auto output = Signer::sign(input); - - ASSERT_EQ(hex(preimage), "0500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402050104001c0000000600000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c37d5fa17b70251d0806f26156b1b698dfd09e040642fa092595ce0a78e9e84fcd"); - ASSERT_EQ(hex(output.encoded()), "410284008d96660f14babe708b5e61853c9f5929bc90dd9874485bf4d6dc32d3e6f22eaa0038ec4973ab9773dfcbf170b8d27d36d89b85c3145e038d68914de83cf1f7aca24af64c55ec51ba9f45c5a4d74a9917dee380e9171108921c3e5546e05be15206050104000500007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b5402"); -} - -TEST(PolkadotSigner, SignBond_8da66d) { - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - auto blockHash = parse_hex("0xf1eee612825f29abd3299b486e401299df2faa55b7ce1e34bf2243bd591905fc"); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(0); - input.set_spec_version(26); - { - PublicKey publicKey = privateKeyThrow2.getPublicKey(TWPublicKeyTypeED25519); - Address address = Address(publicKey); - EXPECT_EQ(address.string(), addressThrow2); - } - input.set_private_key(privateKeyThrow2.bytes.data(), privateKeyThrow2.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(5); - - // era: for blockhash and block number, use curl -H "Content-Type: application/json" -H "Accept: text/plain" https:///transaction/material?noMeta=true - auto era = input.mutable_era(); - era->set_block_number(3540912); - era->set_period(64); - - auto stakingCall = input.mutable_staking_call(); - auto bond = stakingCall->mutable_bond(); - auto value = store(uint256_t(11000000000)); // 1.1 - bond->set_controller(addressThrow2); // myself - bond->set_value(value.data(), value.size()); - bond->set_reward_destination(Proto::RewardDestination::STASH); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/0x8da66d3fe0f592cff714ec107289370365117a1abdb72a19ac91181fdcf62bba - ASSERT_EQ(hex(output.encoded()), "3d02849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f783009025843bc49c1c4fbc99dbbd290c92f9879665d55b02f110abfb4800f0e7630877d2cffd853deae7466c22fbc8616a609e1b92615bb365ea8adccba5ef7624050503000007009dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f7830700aea68f0201"); -} - -TEST(PolkadotSigner, SignBondAndNominate_4955314_2) { - - auto key = parse_hex("7f44b19b391a8015ca4c7d94097b3695867a448d1391e7f3243f06987bdb6858"); - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(genesisHash.data(), genesisHash.size()); - input.set_nonce(4); - input.set_spec_version(30); - input.set_private_key(key.data(), key.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(7); - - auto stakingCall = input.mutable_staking_call(); - auto bondnom = stakingCall->mutable_bond_and_nominate(); - auto value = store(uint256_t(10000000000)); // 1 DOT - bondnom->set_controller("13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"); - bondnom->set_value(value.data(), value.size()); - bondnom->set_reward_destination(Proto::RewardDestination::STASH); - bondnom->add_nominators("1zugcavYA9yCuYwiEYeMHNJm9gXznYjNfXQjZsZukF1Mpow"); - bondnom->add_nominators("15oKi7HoBQbwwdQc47k71q4sJJWnu5opn1pqoGx4NAEYZSHs"); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/4955314-2 - ASSERT_EQ(hex(output.encoded()), "6103840036092fac541e0e5feda19e537c679b487566d7101141c203ac8322c27e5f076a00a8b1f859d788f11a958e98b731358f89cf3fdd41a667ea992522e8d4f46915f4c03a1896f2ac54bdc5f16e2ce8a2a3bf233d02aad8192332afd2113ed6688e0d0010001a02080700007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0700e40b540201070508002c2a55b5ffdca266bd0207df97565b03255f70783ca1a349be5ed9f44589c36000d44533a4d21fd9d6f5d57c8cd05c61a6f23f9131cec8ae386b6b437db399ec3d"); -} - -TEST(PolkadotSigner, SignNominate_452522) { - auto input = Proto::SigningInput(); - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - auto blockHash = parse_hex("0x211787d016e39007ac054547737a10542620013e73648b3134541d536cb44e2c"); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(1); - input.set_spec_version(26); - input.set_private_key(privateKeyThrow2.bytes.data(), privateKeyThrow2.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(5); - - // era: for blockhash and block number, use curl -H "Content-Type: application/json" -H "Accept: text/plain" https:///transaction/material?noMeta=true - auto era = input.mutable_era(); - era->set_block_number(3540945); - era->set_period(64); - - auto stakingCall = input.mutable_staking_call(); - auto nominate = stakingCall->mutable_nominate(); - - nominate->add_nominators(controller1); - nominate->add_nominators("1REAJ1k691g5Eqqg9gL7vvZCBG7FCCZ8zgQkZWd4va5ESih"); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/0x4525224b7d8f3e58de3a54a9fbfd071401c2b737f314c972a2bb087a0ff508a6 - ASSERT_EQ(hex(output.encoded()), "a502849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f78300d73ff0dc456704743f70173a56e6c13e88a6e1dddb38a23552a066e44fb64e2c9d8a5e9a76afb9489b8540365f668bddd34b7d9c8dbdc4600e6316080e55a30315010400070508aee72821ca00e62304e4f0d858122a65b87c8df4f0eae224ae064b951d39f610127a30e486492921e58f2564b36ab1ca21ff630672f0e76920edd601f8f2b89a"); -} - -TEST(PolkadotSigner, SignNominate2) { - auto blockHash = parse_hex("d22a6b2e3e61325050718bd04a14da9efca1f41c9f0a525c375d36106e25af68"); - auto input = Proto::SigningInput(); - - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(0); - input.set_spec_version(17); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(3); - - auto stakingCall = input.mutable_staking_call(); - auto& nominate = *stakingCall->mutable_nominate(); - // payload size larger than 256, will be hashed - nominate.add_nominators("1zugcabYjgfQdMLC3cAzQ8tJZMo45tMnGpivpAzpxB4CZyK"); - nominate.add_nominators("1REAJ1k691g5Eqqg9gL7vvZCBG7FCCZ8zgQkZWd4va5ESih"); - nominate.add_nominators("1WG3jyNqniQMRZGQUc7QD2kVLT8hkRPGMSqAb5XYQM1UDxN"); - nominate.add_nominators("16QFrtU6kDdBjxY8qEKz5EEfuDkHxqG8pix3wSGKQzRcuWHo"); - nominate.add_nominators("14ShUZUYUR35RBZW6uVVt1zXDxmSQddkeDdXf1JkMA6P721N"); - nominate.add_nominators("15MUBwP6dyVw5CXF9PjSSv7SdXQuDSwjX86v1kBodCSWVR7c"); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(output.encoded()), "a1048488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee00135bbc68b67fffadaf7e98b6402c4fc60382765f543225083a024b0e0ff8071d4ec4ddd67a65828113cc76f3208765608be010d2fcfdcd47e8fe342872704c000000000705182c2a55b5a116a4c88aff57e8f2b70ba72dda72dda4b78630e16ad0ca69006f18127a30e486492921e58f2564b36ab1ca21ff630672f0e76920edd601f8f2b89a1650c532ed1a8641e8922aa24ade0ff411d03edd9ed1c6b7fe42f1a801cee37ceee9d5d071a418b51c02b456d5f5cefd6231041ad59b0e8379c59c11ba4a2439984e16482c99cfad1436111e321a86d87d0fac203bf64538f888e45d793b5413c08d5de7a5d97bea2c7ddf516d0635bddc43f326ae2f80e2595b49d4a08c4619"); -} - -TEST(PolkadotSigner, SignChill) { - auto blockHash = parse_hex("1d4a1ecc8b1c37bf0ba5d3e0bf14ec5402fbb035eeaf6d8042c07ca5f8c57429"); - auto input = Proto::SigningInput(); - - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(0); - input.set_spec_version(17); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(3); - - auto stakingCall = input.mutable_staking_call(); - auto __attribute__((unused))& chill = *stakingCall->mutable_chill(); - auto output = Signer::sign(input); - - ASSERT_EQ(hex(output.encoded()), "9d018488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0088b5e1cd93ba74b82e329f95e1b22660385970182172b2ae280801fdd1ee5652cf7bf319e5e176ccc299dd8eb1e7fccb0ea7717efaf4aacd7640789dd09c1e070000000706"); -} - -TEST(PolkadotSigner, SignWithdraw) { - auto blockHash = parse_hex("7b4d1d1e2573eabcc90a3e96058eb0d8d21d7a0b636e8030d152d9179a345dda"); - auto input = Proto::SigningInput(); - - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(0); - input.set_spec_version(17); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(3); - - auto stakingCall = input.mutable_staking_call(); - auto& withdraw = *stakingCall->mutable_withdraw_unbonded(); - withdraw.set_slashing_spans(10); - - auto output = Signer::sign(input); - - ASSERT_EQ(hex(output.encoded()), "ad018488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee002e49bf0dec9bef01dd3bd25419e2147dc983613d0860108f889f9ff2d062c5e3267e309e2dbc35dd2fc2b877b57d86a5f12cbeb8217485be32be3c34d2507d0e00000007030a000000"); -} - -TEST(PolkadotSigner, SignUnbond_070957) { - auto input = Proto::SigningInput(); - - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - auto blockHash = parse_hex("0x53040c71c6061bd256346b81fcb3545c13b5c34c7cd0c2c25f00aa6e564b16d5"); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(2); - input.set_spec_version(26); - input.set_private_key(privateKeyThrow2.bytes.data(), privateKeyThrow2.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(5); - - auto era = input.mutable_era(); - era->set_block_number(3540983); - era->set_period(64); - - auto stakingCall = input.mutable_staking_call(); - auto unbond = stakingCall->mutable_unbond(); - auto value = store(uint256_t(4000000000)); - unbond->set_value(value.data(), value.size()); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/0x070957ab697adbe11f7d72a1314d0a81d272a747d2e6880818073317125f980a - ASSERT_EQ(hex(output.encoded()), "b501849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f783003a762d9dc3f2aba8922c4babf7e6622ca1d74da17ab3f152d8f29b0ffee53c7e5e150915912a9dfd98ef115d272e096543eef9f513207dd606eea97d023a64087503080007020300286bee"); -} - -TEST(PolkadotSigner, SignChillAndUnbond) { - auto blockHash = parse_hex("0x35ba668bb19453e8da6334cadcef2a27c8d4141bfc8b49e78e853c3d73e1ecd0"); - auto input = Proto::SigningInput(); - - input.set_genesis_hash(genesisHash.data(), genesisHash.size()); - input.set_block_hash(blockHash.data(), blockHash.size()); - input.set_nonce(6); - input.set_spec_version(9200); - input.set_private_key(privateKeyPolkadot.bytes.data(), privateKeyPolkadot.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypePolkadot)); - input.set_transaction_version(12); - - auto era = input.mutable_era(); - era->set_block_number(10541373); - era->set_period(64); - - auto stakingCall = input.mutable_staking_call(); - auto chillBond = stakingCall->mutable_chill_and_unbond(); - auto value = store(uint256_t(100500000000)); // 10.05 DOT - chillBond->set_value(value.data(), value.size()); - - auto output = Signer::sign(input); - // https://polkadot.subscan.io/extrinsic/10541383-2 - ASSERT_EQ(hex(output.encoded()), "d10184008361bd08ddca5fda28b5e2aa84dc2621de566e23e089e555a42194c3eaf2da7900c891ba102db672e378945d74cf7f399226a76b43cab502436971599255451597fc2599902e4b62c7ce85ecc3f653c693fef3232be620984b5bb5bcecbbd7b209d50318001a02080706070207004d446617"); -} - -TEST(PolkadotSigner, PolymeshEncodeAndSign) { - // tx on mainnet - // https://polymesh.subscan.io/extrinsic/0x9a4283cc38f7e769c53ad2d1c5cf292fc85a740ec1c1aa80c180847e51928650 - - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - auto blockHash = parse_hex("898bba6413c38f79a284aec8749f297f6c8734c501f67517b5a6aadc338d1102"); - auto vGenesisHash = parse_hex("6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(1UL); - input.set_spec_version(3010u); - input.set_transaction_version(2u); - - auto* era = input.mutable_era(); - era->set_block_number(4298130UL); - era->set_period(64UL); - - auto* transfer = input.mutable_balance_call()->mutable_transfer(); - transfer->set_to_address("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); - auto value = store(1000000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_memo("MEMO PADDED WITH SPACES"); - - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x05); - callIndices->set_method_index(0x01); - - auto preImage = Signer::signaturePreImage(input); - ASSERT_EQ(hex(preImage), "050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f2050414444454420574954482053504143455300000000000000000025010400c20b0000020000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063898bba6413c38f79a284aec8749f297f6c8734c501f67517b5a6aadc338d1102"); - - auto publicKey = parse_hex("4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"); - auto signature = parse_hex("0791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "bd0284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee000791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f25010400050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f20504144444544205749544820535041434553000000000000000000"); -} - -TEST(PolkadotSigner, PolymeshEncodeBondAndNominate) { - // tx on mainnet - // https://polymesh.subscan.io/extrinsic/0xd516d4cb1f5ade29e557586e370e98c141c90d87a0b7547d98c6580eb2afaeeb - - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - auto blockHash = parse_hex("ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); - auto vGenesisHash = parse_hex("6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(0UL); - input.set_spec_version(6003050u); - input.set_transaction_version(4u); - - auto* era = input.mutable_era(); - era->set_block_number(15742961UL); - era->set_period(64UL); - - auto stakingCall = input.mutable_staking_call(); - auto bondnom = stakingCall->mutable_bond_and_nominate(); - auto value = store(uint256_t(4000000)); // 4.0 POLYX - bondnom->set_controller("2EYbDVDVWiFbXZWJgqGDJsiH5MfNeLr5fxqH3tX84LQZaETG"); - bondnom->set_value(value.data(), value.size()); - bondnom->set_reward_destination(Proto::RewardDestination::STAKED); - bondnom->add_nominators("2Gw8mSc4CUMxXMKEDqEsumQEXE5yTF8ACq2KdHGuigyXkwtz"); - - // Utiltity.batch_all - { - auto* callIndices = bondnom->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x29); - callIndices->set_method_index(0x02); - } - - // Staking.bond - { - auto* callIndices = bondnom->mutable_bond_call_indices()->mutable_custom(); - callIndices->set_module_index(0x11); - callIndices->set_method_index(0x00); - } - - // Staking.nominate - { - auto* callIndices = bondnom->mutable_nominate_call_indices()->mutable_custom(); - callIndices->set_module_index(0x11); - callIndices->set_method_index(0x05); - } - - auto preImage = Signer::signaturePreImage(input); - ASSERT_EQ(hex(preImage), "2902081100005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a290610224f4000011050400c6766ff780e1f506e41622f7798ec9323ab3b8bea43767d8c107e1e920581958150300006a995b00040000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); - - auto publicKey = parse_hex("5ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a29061"); - auto signature = parse_hex("685a2fd4b1bdf7775c55eb97302a0f86b0c10848fd9db3a7f6bbe912c4c2fa28bed16f6032852ec14f27f0553523dd2fc181a6dca79f19f9c7ed6cb660cf6480"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "d90284005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a2906100685a2fd4b1bdf7775c55eb97302a0f86b0c10848fd9db3a7f6bbe912c4c2fa28bed16f6032852ec14f27f0553523dd2fc181a6dca79f19f9c7ed6cb660cf6480150300002902081100005ccc5c9276ab7976e7c93c70c190fbf1761578c07b892d0d1fe65972f6a290610224f4000011050400c6766ff780e1f506e41622f7798ec9323ab3b8bea43767d8c107e1e920581958"); -} - -TEST(PolkadotSigner, PolymeshEncodeChillAndUnbond) { - // extrinsic on mainnet - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x29020811061102027a030a - - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - auto blockHash = parse_hex("ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); - auto vGenesisHash = parse_hex("6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(0UL); - input.set_spec_version(6003050u); - input.set_transaction_version(4u); - - auto* era = input.mutable_era(); - era->set_block_number(15742961UL); - era->set_period(64UL); - - auto stakingCall = input.mutable_staking_call(); - auto chillBond = stakingCall->mutable_chill_and_unbond(); - auto value = store(uint256_t(42000000)); // 42.0 POLYX - chillBond->set_value(value.data(), value.size()); - - // Utiltity.batch_all - { - auto* callIndices = chillBond->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x29); - callIndices->set_method_index(0x02); - } - - // Staking.bond - { - auto* callIndices = chillBond->mutable_chill_call_indices()->mutable_custom(); - callIndices->set_module_index(0x11); - callIndices->set_method_index(0x06); - } - - // Staking.nominate - { - auto* callIndices = chillBond->mutable_unbond_call_indices()->mutable_custom(); - callIndices->set_module_index(0x11); - callIndices->set_method_index(0x02); - } - - auto preImage = Signer::signaturePreImage(input); - ASSERT_EQ(hex(preImage), "29020811061102027a030a150300006a995b00040000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); -} - -TEST(PolkadotSigner, Statemint_encodeTransaction_transfer) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2686030-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - auto blockHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - auto vGenesisHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(0UL); - input.set_spec_version(9320U); - input.set_transaction_version(9U); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - - auto value = store(100000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(1984); - transfer->set_fee_asset_id(0x00); // native token - - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x32); - callIndices->set_method_index(0x05); - - auto publicKey = parse_hex("81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"); - auto signature = parse_hex("e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "4102840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000003205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); - - // 3d 02840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000 3205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600 - // 41 02840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e0ae36a5ceaaa7ff53fadfecc8a285a436b15e39c43ea09e8897f34fa3fe55133028eb7d8a9ea2cd42ff1c786e945cd47a02243454ecb39c81acc3409d96f903000000 00 3205011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600 -} - -TEST(PolkadotSigner, Statemint_encodeTransaction_transfer_keep_alive) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2686081-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - auto blockHash = parse_hex("e8f10f9a841dc73578148c763afa17638670c8655542172a80af2e03bf3cbe62"); - auto vGenesisHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(2UL); - input.set_spec_version(9320U); - input.set_transaction_version(9U); - - auto* era = input.mutable_era(); - era->set_block_number(2686056UL); - era->set_period(64UL); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - - auto value = store(100000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(1984); - transfer->set_fee_asset_id(0x00); // native token - - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x32); - callIndices->set_method_index(0x06); - - auto publicKey = parse_hex("81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"); - auto signature = parse_hex("68c40526bd9e56e340bfc9385ea463afce34e5c49be75b5946974d9ef6a357f90842036cd1b811b60882ae7183aa23545ef5825aafc8aaa6274d71a03414dc0a"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "4502840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c910068c40526bd9e56e340bfc9385ea463afce34e5c49be75b5946974d9ef6a357f90842036cd1b811b60882ae7183aa23545ef5825aafc8aaa6274d71a03414dc0a85020800003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); -} - -TEST(PolkadotSigner, Statemint_encodeTransaction_batch_transfer_keep_alive) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2711054-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - auto blockHash = parse_hex("c8a2e9492f822f8c07f3717a00e36f68a3090a878b07998724ec1f178f4cf514"); - auto vGenesisHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(3UL); - input.set_spec_version(9320U); - input.set_transaction_version(9U); - - auto* era = input.mutable_era(); - era->set_block_number(2711016UL); - era->set_period(64UL); - - auto* transfer = input.mutable_balance_call()->mutable_batch_asset_transfer(); - transfer->set_fee_asset_id(0x00); - auto* batchCallIndices = transfer->mutable_call_indices()->mutable_custom(); - batchCallIndices->set_module_index(0x28); - batchCallIndices->set_method_index(0x00); - - auto* t1 = transfer->add_transfers(); - t1->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - auto value = store(100000); - t1->set_value(std::string(value.begin(), value.end())); - t1->set_asset_id(1984); - t1->set_fee_asset_id(0x00); // native token - auto* t1CallIndices = t1->mutable_call_indices()->mutable_custom(); - t1CallIndices->set_module_index(0x32); - t1CallIndices->set_method_index(0x06); - - auto* t2 = transfer->add_transfers(); - t2->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - t2->set_value(std::string(value.begin(), value.end())); - t2->set_asset_id(1984); - t2->set_fee_asset_id(0x00); // native token - auto* t2CallIndices = t2->mutable_call_indices()->mutable_custom(); - t2CallIndices->set_module_index(0x32); - t2CallIndices->set_method_index(0x06); - - auto publicKey = parse_hex("81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"); - auto signature = parse_hex("e1d541271965858ff2ba1a1296f0b4d28c8cbcaddf0ea06a9866869caeca3d16eff1265591d11b46d66882493079fde9e425cd941f166260135e9d81f7daf60c"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "f502840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100e1d541271965858ff2ba1a1296f0b4d28c8cbcaddf0ea06a9866869caeca3d16eff1265591d11b46d66882493079fde9e425cd941f166260135e9d81f7daf60c85020c00002800083206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a06003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); -} - -TEST(PolkadotSigner, Statemint_encodeTransaction_dot_transfer_keep_alive) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2789245-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - auto blockHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - auto vGenesisHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(7UL); - input.set_spec_version(9320U); - input.set_transaction_version(9U); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - - auto value = store(100000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(0x00); - transfer->set_fee_asset_id(0x00); // native token - - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x0a); - callIndices->set_method_index(0x03); - - auto publicKey = parse_hex("81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"); - auto signature = parse_hex("c4f7cb46605986ff6dd1a192736feddd8ae468a10b1b458eadfa855ed6b59ad442a96c18e7109ad594d11ba2fd52920545f8a450234e9b03ee3e8f59a8f06f00"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "3902840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100c4f7cb46605986ff6dd1a192736feddd8ae468a10b1b458eadfa855ed6b59ad442a96c18e7109ad594d11ba2fd52920545f8a450234e9b03ee3e8f59a8f06f00001c00000a030050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); -} - -TEST(PolkadotSigner, Statemint_encodeTransaction_usdt_transfer_keep_alive) { - // tx on mainnet - // https://statemint.subscan.io/extrinsic/2789377-2 - - Polkadot::Proto::SigningInput input; - input.set_network(0); - input.set_multi_address(true); - auto blockHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - auto vGenesisHash = parse_hex("68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(8UL); - input.set_spec_version(9320U); - input.set_transaction_version(9U); - - auto* transfer = input.mutable_balance_call()->mutable_asset_transfer(); - transfer->set_to_address("12q4hq1dgqHZVGzHbwZmqq1cFwatN15Visfd7YmUiMB5ZWkH"); - - auto value = store(100000); - transfer->set_value(std::string(value.begin(), value.end())); - transfer->set_asset_id(1984); - transfer->set_fee_asset_id(1984); - - auto* callIndices = transfer->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x32); - callIndices->set_method_index(0x06); - - auto publicKey = parse_hex("81f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c91"); - auto signature = parse_hex("d22583408806c005a24caf16f2084691f4c6dcb6015e6645adc86fc1474369b0e0b7dbcc0ef25b17eae43844aff6fb42a0b279a19e822c76043cac015be5e40a"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "5102840081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c9100d22583408806c005a24caf16f2084691f4c6dcb6015e6645adc86fc1474369b0e0b7dbcc0ef25b17eae43844aff6fb42a0b279a19e822c76043cac015be5e40a00200001c00700003206011f0050e47b3c8aef60bc4fc744d8d979cb0eb2d45fa25c2e9da74e1e5ebd9e117518821a0600"); -} - -TEST(PolkadotSigner, encodeTransaction_Add_authorization) { - // tx on mainnet - // https://polymesh.subscan.io/extrinsic/0x7d9b9109027b36b72d37ba0648cb70e5254524d3d6752cc6b41601f4bdfb1af0 - - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - auto blockHash = parse_hex("ce0c2109db498e45abf8fd447580dcfa7b7a07ffc2bfb1a0fbdd1af3e8816d2b"); - auto vGenesisHash = parse_hex("6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(5UL); - input.set_spec_version(3010U); - input.set_transaction_version(2U); - - auto* era = input.mutable_era(); - era->set_block_number(4395451UL); - era->set_period(64UL); - - auto* addAuthorization = input.mutable_polymesh_call()->mutable_identity_call()->mutable_add_authorization(); - addAuthorization->set_target("2HEVN4PHYKj7B1krQ9bctAQXZxHQQkANVNCcfbdYk2gZ4cBR"); - auto* callIndices = addAuthorization->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x07); - callIndices->set_method_index(0x0d); - - auto publicKey = parse_hex("4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"); - auto signature = parse_hex("81e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "490284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee0081e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01b5031400070d01d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d0610540501000100010000"); -} - -TEST(PolkadotSigner, encodeTransaction_JoinIdentityAsKey) { - // tx on mainnet - // https://polymesh.subscan.io/extrinsic/0x9d7297d8b38af5668861996cb115f321ed681989e87024fda64eae748c2dc542 - - Polkadot::Proto::SigningInput input; - input.set_network(12); - input.set_multi_address(true); - auto blockHash = parse_hex("45c80153c47f5d16acc7a66d473870e8d4574437a7d8c813f47da74cae3812c2"); - auto vGenesisHash = parse_hex("6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(0UL); - input.set_spec_version(3010U); - input.set_transaction_version(2U); - - auto* era = input.mutable_era(); - era->set_block_number(4395527UL); - era->set_period(64UL); - - auto* key = input.mutable_polymesh_call()->mutable_identity_call()->mutable_join_identity_as_key(); - key->set_auth_id(21435); - auto* callIndices = key->mutable_call_indices()->mutable_custom(); - callIndices->set_module_index(0x07); - callIndices->set_method_index(0x05); - - auto publicKey = parse_hex("d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054"); - auto signature = parse_hex("7f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006"); - auto encoded = Signer::encodeTransaction(input, publicKey, signature); - ASSERT_EQ(hex(encoded), "c5018400d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054007f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006750000000705bb53000000000000"); -} - -TEST(PolkadotSigner, Kusama_SignBond_NoController) { - // tx on mainnet - // https://kusama.subscan.io/extrinsic/0x4e52e59b63910cbdb8c5430c2d100908934f473363c8994cddfd6d1501b017f5 - - Polkadot::Proto::SigningInput input; - input.set_network(ss58Prefix(TWCoinTypeKusama)); - auto blockHash = parse_hex("beb02a3ee782f4bd60ffcfc3de473e3c5a00b2cf124dd302c559b0e77b4331eb"); - auto vGenesisHash = parse_hex("b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"); - input.set_block_hash(std::string(blockHash.begin(), blockHash.end())); - input.set_genesis_hash(std::string(vGenesisHash.begin(), vGenesisHash.end())); - input.set_nonce(3UL); - input.set_spec_version(9430U); - input.set_transaction_version(23U); - input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); - - auto* era = input.mutable_era(); - era->set_block_number(18672490UL); - era->set_period(64UL); - - // Ignore `controller` as it was removed from the `Staking::bond` function at `spec_version = 9430` - // https://kusama.subscan.io/runtime/Staking?version=9430 - auto* bond = input.mutable_staking_call()->mutable_bond(); - auto value = store(uint256_t(120'000'000'000)); // 0.12 - bond->set_value(value.data(), value.size()); - bond->set_reward_destination(Proto::RewardDestination::CONTROLLER); - - auto output = Signer::sign(input); - ASSERT_EQ(hex(output.encoded()), "c101840088dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee00bc4d7a166bd1e7e2bfe9b53e81239c9e340d5a326f17c0a3d2768fcc127f20f4f85d888ecb90aa3ed9a0943f8ae8116b9a19747e563c8d8151dfe3b1b5deb40ca5020c0006000700b08ef01b02"); -} - -TEST(PolkadotSigner, SignTransfer_KusamaNewSpec) { - auto toAddress = Address("DAbYHrSQTULYZsuA1kvH2cQ33oBsCxxSRPM1XkhzGLeJuHG", ss58Prefix(TWCoinTypeKusama)); - - auto input = Proto::SigningInput(); - auto genesisHashData = parse_hex("0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"); - input.set_genesis_hash(genesisHashData.data(), genesisHashData.size()); - auto blockHashData = parse_hex("0x0c731c2b7f5332749432eae61cd5a919592965b28181cf9b73b0a1258ea73303"); - input.set_block_hash(blockHashData.data(), blockHashData.size()); - input.set_nonce(150); - input.set_spec_version(1002005); - { - PublicKey publicKey = privateKeyThrow2.getPublicKey(TWPublicKeyTypeED25519); - Address address = Address(publicKey); - EXPECT_EQ(address.string(), addressThrow2); - } - input.set_private_key(privateKeyThrow2.bytes.data(), privateKeyThrow2.bytes.size()); - input.set_network(ss58Prefix(TWCoinTypeKusama)); - input.set_transaction_version(26); - - // era: for blockhash and block number, use curl -H "Content-Type: application/json" -H "Accept: text/plain" https:///transaction/material?noMeta=true - auto era = input.mutable_era(); - era->set_block_number(23610713); - era->set_period(64); - - auto balanceCall = input.mutable_balance_call(); - auto transfer = balanceCall->mutable_transfer(); - auto value = store(uint256_t(2000000000)); // 0.2 - transfer->set_to_address(toAddress.string()); - transfer->set_value(value.data(), value.size()); - - auto extrinsic = Extrinsic(input); - auto preimage = extrinsic.encodePayload(); - EXPECT_EQ(hex(preimage), "0400001a2447c661c9b168bba4a2a178baef7d79eee006c1d145ffc832be76ff6ee9ce0300943577950159020000154a0f001a000000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe0c731c2b7f5332749432eae61cd5a919592965b28181cf9b73b0a1258ea7330300"); - - auto output = Signer::sign(input); - EXPECT_EQ(hex(output.encoded()), "450284009dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f78300fc5a463d3b6972ac7e0b701110f9d95d377be5b6a2f356765553104c04765fc0066c235c11dabde650d487760dc310003d607abceaf85a0a0f47f1a90e3680029501590200000400001a2447c661c9b168bba4a2a178baef7d79eee006c1d145ffc832be76ff6ee9ce0300943577"); -} - - -} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/TWAnyAddressTests.cpp b/tests/chains/Polkadot/TWAnyAddressTests.cpp new file mode 100644 index 00000000000..5b57279f2e3 --- /dev/null +++ b/tests/chains/Polkadot/TWAnyAddressTests.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "Coin.h" +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include +#include +#include + +#include "TestUtilities.h" +#include +#include + +namespace TW::Polkadot::tests { +extern uint32_t polkadotPrefix; +extern uint32_t kusamaPrefix; +extern uint32_t astarPrefix; +extern uint32_t polymeshPrefix; +extern uint32_t parallelPrefix; + +TEST(PolkadotAddress, Validation) { + // Substrate ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ").get(), TWCoinTypePolkadot)); + // Bitcoin + ASSERT_FALSE(TWAnyAddressIsValid(STRING("1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA").get(), TWCoinTypePolkadot)); + // Kusama ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ").get(), TWCoinTypePolkadot)); + // Kusama secp256k1 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx").get(), TWCoinTypePolkadot)); + // Kusama sr25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe").get(), TWCoinTypePolkadot)); + + // Polkadot ed25519 + ASSERT_TRUE(TWAnyAddressIsValid(STRING("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu").get(), TWCoinTypePolkadot)); + // Polkadot sr25519 + ASSERT_TRUE(TWAnyAddressIsValid(STRING("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony").get(), TWCoinTypePolkadot)); + + ASSERT_TRUE(TWAnyAddressIsValidSS58(STRING("cEYtw6AVMB27hFUs4gVukajLM7GqxwxUfJkbPY3rNToHMcCgb").get(), TWCoinTypePolkadot, 64)); + ASSERT_FALSE(TWAnyAddressIsValidSS58(STRING("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A").get(), TWCoinTypePolkadot, 64)); + + // Polymesh + ASSERT_TRUE(TWAnyAddressIsValidSS58(STRING("2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG").get(), TWCoinTypePolkadot, polymeshPrefix)); + ASSERT_FALSE(TWAnyAddressIsValidSS58(STRING("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A").get(), TWCoinTypePolkadot, polymeshPrefix)); +} + +TEST(PolkadotAddress, FromPrivateKey) { + // subkey phrase `chief menu kingdom stereo hope hazard into island bag trick egg route` + const auto privateKey = WRAP(TWPrivateKey, TWPrivateKeyCreateWithData(DATA("0x612d82bc053d1b4729057688ecb1ebf62745d817ddd9b595bc822f5f2ba0e41a").get())); + const auto publicKey = WRAP(TWPublicKey, TWPrivateKeyGetPublicKey(privateKey.get(), TWCoinTypePolkadot)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypePolkadot)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu").get())); +} + +TEST(PolkadotAddress, FromPublicKey) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("0xbeff0e5d6f6e6e6d573d3044f3e2bfb353400375dc281da3337468d4aa527908").get(), TWPublicKeyTypeED25519)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypePolkadot)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu").get())); +} + +TEST(PolkadotAddress, FromPublicKeyWithPrefix) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("0x92fd9c237030356e26cfcc4568dc71055d5ec92dfe0ff903767e00611971bad3").get(), TWPublicKeyTypeED25519)); + + const auto addressPolkadot = STRING("14KjL5vGAYJCbKgZJmFKDSjewtBpvaxx9YvRZvi7qmb5s8CC"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58WithPublicKey(publicKey.get(), TWCoinTypePolkadot, polkadotPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressPolkadot.get())); + } + + const auto addressAstar = STRING("ZG2d3dH5zfqNchsqReS6x4nBJuJCW7Z6Fh5eLvdA3ZXGkPd"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58WithPublicKey(publicKey.get(), TWCoinTypePolkadot, astarPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressAstar.get())); + } + + const auto addressParallel = STRING("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58WithPublicKey(publicKey.get(), TWCoinTypePolkadot, parallelPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressParallel.get())); + } + + // polymesh + publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f83").get(), TWPublicKeyTypeED25519)); + const auto addressPolymesh = STRING("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58WithPublicKey(publicKey.get(), TWCoinTypePolkadot, polymeshPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressPolymesh.get())); + } +} + +TEST(PolkadotAddress, FromString) { + auto addressStr1 = STRING("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu"); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(addressStr1.get(), TWCoinTypePolkadot)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressStr1.get())); +} + +TEST(PolkadotAddress, FromStringWithPrefix) { + const auto kusamaAddress = STRING("Fu3r514w83euSVV7q1MyFGWErUR2xDzXS2goHzimUn4S12D"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58(kusamaAddress.get(), TWCoinTypeKusama, kusamaPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), kusamaAddress.get())); + } + + auto addressParallel = STRING("p8EGHjWt7e1MYoD7V6WXvbPZWK9GSJiiK85kv2R7Ur7FisPUL"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58(addressParallel.get(), TWCoinTypePolkadot, parallelPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressParallel.get())); + } + + // polymesh + auto addressPolymesh = STRING("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58(addressPolymesh.get(), TWCoinTypePolkadot, polymeshPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressPolymesh.get())); + } +} + +} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/TWAnySignerTests.cpp b/tests/chains/Polkadot/TWAnySignerTests.cpp new file mode 100644 index 00000000000..8cff6ef551b --- /dev/null +++ b/tests/chains/Polkadot/TWAnySignerTests.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "AnyAddress.h" +#include "Coin.h" +#include "HexCoding.h" +#include "PrivateKey.h" +#include "PublicKey.h" +#include "proto/Polkadot.pb.h" +#include "proto/TransactionCompiler.pb.h" +#include "uint256.h" + +#include +#include +#include +#include +#include + +#include "TestUtilities.h" +#include + +namespace TW::Polkadot::tests { +uint32_t polkadotPrefix = ss58Prefix(TWCoinTypePolkadot); +uint32_t kusamaPrefix = ss58Prefix(TWCoinTypeKusama); +uint32_t astarPrefix = 5; +uint32_t polymeshPrefix = 12; +uint32_t parallelPrefix = 172; + +auto privateKey = PrivateKey(parse_hex("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115")); +auto privateKeyThrow2Data = DATA("70a794d4f1019c3ce002f33062f45029c4f930a56b3d20ec477f7668c6bbc37f"); +auto privateKeyThrow2 = TWPrivateKeyCreateWithData(privateKeyThrow2Data.get()); +auto addressThrow2 = "14Ztd3KJDaB9xyJtRkREtSZDdhLSbm7UUKt8Z7AwSv7q85G2"; +auto genesisHash = parse_hex("91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"); + +Data helper_encodePayload(TWCoinType coin, const Proto::SigningInput& input) { + auto txInputData = data(input.SerializeAsString()); + auto txInputDataPtr = WRAPD(TWDataCreateWithBytes(txInputData.data(), txInputData.size())); + const auto preImageHashes = WRAPD(TWTransactionCompilerPreImageHashes(coin, txInputDataPtr.get())); + auto preImageHash = data(TWDataBytes(preImageHashes.get()), TWDataSize(preImageHashes.get())); + + TxCompiler::Proto::PreSigningOutput preSigningOutput; + preSigningOutput.ParseFromArray(preImageHash.data(), int(preImageHash.size())); + + EXPECT_EQ(preSigningOutput.error(), Common::Proto::OK); + return data(preSigningOutput.data()); +} + +TEST(TWAnySignerPolkadot, SignTransfer_9fd062) { + auto toAddressStr = STRING("13ZLCqJNPsRZYEbwjtZZFpWt9GyFzg5WahXCVWKpWdUJqrQ5"); + + auto input = Proto::SigningInput(); + input.set_genesis_hash(genesisHash.data(), genesisHash.size()); + auto blockHash = parse_hex("0x5d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); + input.set_block_hash(blockHash.data(), blockHash.size()); + input.set_nonce(3); + input.set_spec_version(26); + { + auto pubKey = WRAP(TWPublicKey, TWPrivateKeyGetPublicKeyEd25519(privateKeyThrow2)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(pubKey.get(), TWCoinTypePolkadot)); + auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + ASSERT_STREQ(TWStringUTF8Bytes(addressStr.get()), addressThrow2); + } + input.set_private_key(TWDataBytes(privateKeyThrow2Data.get()), TWDataSize(privateKeyThrow2Data.get())); + input.set_network(ss58Prefix(TWCoinTypePolkadot)); + input.set_transaction_version(5); + + // era: for blockhash and block number, use curl -H "Content-Type: application/json" -H "Accept: text/plain" https:///transaction/material?noMeta=true + auto era = input.mutable_era(); + era->set_block_number(3541050); + era->set_period(64); + + auto balanceCall = input.mutable_balance_call(); + auto transfer = balanceCall->mutable_transfer(); + auto value = store(uint256_t(2000000000)); // 0.2 + transfer->set_to_address(TWStringUTF8Bytes(toAddressStr.get())); + transfer->set_value(value.data(), value.size()); + + auto preimage = helper_encodePayload(TWCoinTypePolkadot, input); + EXPECT_EQ(hex(preimage), "05007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577a5030c001a0000000500000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c35d2143bb808626d63ad7e1cda70fa8697059d670a992e82cd440fbb95ea40351"); + + Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypePolkadot); + + // https://polkadot.subscan.io/extrinsic/0x9fd06208a6023e489147d8d93f0182b0cb7e45a40165247319b87278e08362d8 + EXPECT_EQ(hex(output.encoded()), "3502849dca538b7a925b8ea979cc546464a3c5f81d2398a3a272f6f93bdf4803f2f7830073e59cef381aedf56d7af076bafff9857ffc1e3bd7d1d7484176ff5b58b73f1211a518e1ed1fd2ea201bd31869c0798bba4ffe753998c409d098b65d25dff801a5030c0005007120f76076bcb0efdf94c7219e116899d0163ea61cb428183d71324eb33b2bce0300943577"); +} + +} // namespace TW::Polkadot::tests diff --git a/tests/chains/Polkadot/TransactionCompilerTests.cpp b/tests/chains/Polkadot/TransactionCompilerTests.cpp index 2476fe54ab8..eddf587de1d 100644 --- a/tests/chains/Polkadot/TransactionCompilerTests.cpp +++ b/tests/chains/Polkadot/TransactionCompilerTests.cpp @@ -86,7 +86,7 @@ TEST(PolkadotCompiler, CompileWithSignatures) { Polkadot::Proto::SigningOutput output; ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); EXPECT_EQ(output.encoded().size(), 0ul); - EXPECT_EQ(output.error(), Common::Proto::Error_invalid_params); + EXPECT_EQ(output.error(), Common::Proto::Error_signatures_count); } }