diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt index f99db4e987c..b9cb1fe23c8 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/CoinAddressDerivationTests.kt @@ -102,6 +102,7 @@ class CoinAddressDerivationTests { ACALA -> assertEquals("25GGezx3LWFQj6HZpYzoWoVzLsHojGtybef3vthC9nd19ms3", address) KUSAMA -> assertEquals("G9xV2EatmrjRC1FLPexc3ddqNRRzCsAdURU8RFiAAJX6ppY", address) POLKADOT -> assertEquals("13nN6BGAoJwd7Nw1XxeBCx5YcBXuYnL94Mh7i3xBprqVSsFk", address) + POLYMESH -> assertEquals("2DHK8VhBpacs9quk78AVP9TmmcG5iXi2oKtZqneSNsVXxCKw", address) PIVX -> assertEquals("D81AqC8zKma3Cht4TbVuh4jyVVyLkZULCm", address) KAVA -> assertEquals("kava1drpa0x9ptz0fql3frv562rcrhj2nstuz3pas87", address) CARDANO -> assertEquals("addr1qyr8jjfnypp95eq74aqzn7ss687ehxclgj7mu6gratmg3mul2040vt35dypp042awzsjk5xm3zr3zm5qh7454uwdv08s84ray2", address) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt new file mode 100644 index 00000000000..a7e77d9a3e0 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshAddress.kt @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.polymesh + +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.* + +class TestPolymeshAddress { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testAddress() { + + val key = PrivateKey("0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4".toHexByteArray()) + val pubkey = key.publicKeyEd25519 + val address = AnyAddress(pubkey, CoinType.POLYMESH) + val expected = AnyAddress("2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys", CoinType.POLYMESH) + + assertEquals(pubkey.data().toHex(), "0x4bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c") + assertEquals(address.description(), expected.description()) + } +} diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt new file mode 100644 index 00000000000..4adc768d4ad --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/polymesh/TestPolymeshSigner.kt @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.polymesh + +import com.trustwallet.core.app.utils.Numeric +import com.trustwallet.core.app.utils.toHexBytesInByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.java.AnySigner +import wallet.core.jni.CoinType.POLYMESH +import wallet.core.jni.proto.Polymesh +import wallet.core.jni.proto.Polymesh.SigningOutput + +class TestPolymeshSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + val genesisHashStr = "0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063".toHexBytesInByteString() + // Private key for testing. DO NOT USE, since this is public. + val TestKey1 = "0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4".toHexBytesInByteString() + + @Test + fun PolymeshTransactionSigning() { + // https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04 + + // Step 1: Prepare input. + val blockHashStr = "77d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d".toHexBytesInByteString() + + var call = Polymesh.Balance.Transfer.newBuilder().apply { + toAddress = "2CpqFh8VnwJAjenw4xSUWCaaJ2QwGdhnCikoSEczMhjgqyj7" + value = "0x0F4240".toHexBytesInByteString() + } + + val input = Polymesh.SigningInput.newBuilder().apply { + genesisHash = genesisHashStr + blockHash = blockHashStr + era = Polymesh.Era.newBuilder().apply { + blockNumber = 16_102_106 + period = 64 + }.build() + network = POLYMESH.ss58Prefix() + nonce = 1 + specVersion = 7000005 + transactionVersion = 7 + privateKey = TestKey1 + runtimeCall = Polymesh.RuntimeCall.newBuilder().apply { + balanceCall = Polymesh.Balance.newBuilder().apply { + transfer = call.build() + }.build() + }.build() + } + + val output = AnySigner.sign(input.build(), POLYMESH, SigningOutput.parser()) + val encoded = Numeric.toHexString(output.encoded.toByteArray()) + + val expected = "0x390284004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210ba501040005000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00" + assertEquals(encoded, expected) + } +} diff --git a/docs/registry.md b/docs/registry.md index 1473aa7b5ca..8d391648677 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -64,6 +64,7 @@ This list is generated from [./registry.json](../registry.json) | 508 | MultiversX | eGLD | | | | 529 | Secret | SCRT | | | | 564 | Agoric | BLD | | | +| 595 | Polymesh | POLYX | | | | 607 | TON | TON | | | | 637 | Aptos | APT | | | | 714 | BNB Beacon Chain | BNB | | | diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index eeb2f744474..eb66cbad9b4 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -67,6 +67,7 @@ enum TWBlockchain { TWBlockchainNativeInjective = 54, // Cosmos TWBlockchainBitcoinCash = 55, TWBlockchainPactus = 56, + TWBlockchainPolymesh = 57, // Substrate }; TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCoinType.h b/include/TrustWalletCore/TWCoinType.h index 9d2f10e36c9..0ff955048be 100644 --- a/include/TrustWalletCore/TWCoinType.h +++ b/include/TrustWalletCore/TWCoinType.h @@ -187,6 +187,7 @@ enum TWCoinType { TWCoinTypeBounceBit = 6001, TWCoinTypeZkLinkNova = 810180, TWCoinTypePactus = 21888, + TWCoinTypePolymesh = 595, // end_of_tw_coin_type_marker_do_not_modify }; diff --git a/registry.json b/registry.json index 72a5e13ce3a..8033b095519 100644 --- a/registry.json +++ b/registry.json @@ -4814,5 +4814,35 @@ "rpc": "https://docs.pactus.org/api/http", "documentation": "https://docs.pactus.org" } + }, + { + "id": "polymesh", + "name": "Polymesh", + "coinId": 595, + "symbol": "POLYX", + "decimals": 6, + "blockchain": "Polymesh", + "derivation": [ + { + "path": "m/44'/595'/0'/0'/0'" + } + ], + "curve": "ed25519", + "publicKeyType": "ed25519", + "addressHasher": "keccak256", + "ss58Prefix": 12, + "explorer": { + "url": "https://polymesh.subscan.io", + "txPath": "/extrinsic/", + "accountPath": "/account/", + "sampleTx": "0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04", + "sampleAccount": "2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv" + }, + "info": { + "url": "https://polymesh.network", + "source": "https://github.com/PolymeshAssociation/Polymesh", + "rpc": "wss://rpc.polymesh.network/", + "documentation": "https://developers.polymesh.network/" + } } ] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a1c8d016f1d..754561e711d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1864,6 +1864,7 @@ dependencies = [ "tw_native_injective", "tw_pactus", "tw_polkadot", + "tw_polymesh", "tw_ronin", "tw_solana", "tw_substrate", @@ -2116,6 +2117,22 @@ dependencies = [ "tw_substrate", ] +[[package]] +name = "tw_polymesh" +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" @@ -2182,6 +2199,7 @@ name = "tw_substrate" version = "0.1.0" dependencies = [ "tw_coin_entry", + "tw_encoding", "tw_hash", "tw_keypair", "tw_memory", @@ -2204,6 +2222,7 @@ dependencies = [ "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_proto", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 75b9d7761ab..34f6073d047 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -13,6 +13,7 @@ members = [ "chains/tw_native_injective", "chains/tw_pactus", "chains/tw_polkadot", + "chains/tw_polymesh", "chains/tw_ronin", "chains/tw_solana", "chains/tw_sui", diff --git a/rust/chains/tw_polkadot/src/call_encoder/mod.rs b/rust/chains/tw_polkadot/src/call_encoder/mod.rs index db5cb90c2b7..6b2548e3a38 100644 --- a/rust/chains/tw_polkadot/src/call_encoder/mod.rs +++ b/rust/chains/tw_polkadot/src/call_encoder/mod.rs @@ -1,4 +1,4 @@ -use crate::{ctx_from_tw, KUSAMA, POLKADOT, POLYMESH}; +use crate::{ctx_from_tw, KUSAMA, POLKADOT}; use tw_proto::Polkadot::Proto::{ self, mod_Balance::{BatchAssetTransfer, BatchTransfer, OneOfmessage_oneof as BalanceVariant}, @@ -19,9 +19,6 @@ 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 { @@ -56,7 +53,6 @@ impl CallEncoder { 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 }) diff --git a/rust/chains/tw_polkadot/tests/extrinsic.rs b/rust/chains/tw_polkadot/tests/extrinsic.rs index f576982c449..1a379ff1d4d 100644 --- a/rust/chains/tw_polkadot/tests/extrinsic.rs +++ b/rust/chains/tw_polkadot/tests/extrinsic.rs @@ -5,7 +5,6 @@ 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, }; @@ -27,37 +26,6 @@ fn custom_call_indices(module: u8, method: u8) -> Option { }) } -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 { @@ -75,122 +43,25 @@ fn staking_call( } #[test] -fn polymesh_encode_transfer_with_memo() { - // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000 - +fn polkadot_encode_transfer() { let input = Proto::SigningInput { - network: 12, + network: 0, multi_address: true, message_oneof: balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { - to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + to_address: "14ixj163bkk2UEKLEXsEWosuFNuijpqEWZbX5JzN4yMHbUVD".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" + "050000a4b558a0342ae6e379a7ed00d23ff505f1101646cb279844496ad608943eda0d04" ); } -#[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 @@ -318,7 +189,7 @@ fn encode_staking_chill() { #[test] fn encode_staking_bond_with_controller() { let input = Proto::SigningInput { - network: 12, + network: 0, multi_address: true, message_oneof: staking_call(Proto::mod_Staking::OneOfmessage_oneof::bond(Bond { controller: "13wQDQTMM6E9g5WD27e6UsWWTwHLaW763FQxnkbVaoKmsBQy".into(), @@ -332,7 +203,7 @@ fn encode_staking_bond_with_controller() { let encoded = encode_input(&input).expect("error encoding call"); assert_eq!( encoded.to_hex(), - "11000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" + "07000081f5dd1432e5dd60aa71819e1141ad5e54d6f4277d7d128030154114444b8c914652310002" ); } diff --git a/rust/chains/tw_polymesh/Cargo.toml b/rust/chains/tw_polymesh/Cargo.toml new file mode 100644 index 00000000000..1b9e2cfefd9 --- /dev/null +++ b/rust/chains/tw_polymesh/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_polymesh" +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/polymesh.rs b/rust/chains/tw_polymesh/src/call_encoder.rs similarity index 58% rename from rust/chains/tw_polkadot/src/call_encoder/polymesh.rs rename to rust/chains/tw_polymesh/src/call_encoder.rs index dde4a047308..2f4e9f56e5e 100644 --- a/rust/chains/tw_polkadot/src/call_encoder/polymesh.rs +++ b/rust/chains/tw_polymesh/src/call_encoder.rs @@ -1,45 +1,40 @@ use std::str::FromStr; +use crate::ctx_from_tw; +use crate::types::*; use tw_coin_entry::error::prelude::*; -use tw_hash::H256; use tw_number::U256; -use tw_proto::Polkadot::Proto::{ +use tw_proto::Polymesh::Proto::{ + self, mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, - mod_Identity::{AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant}, - mod_PolymeshCall::OneOfmessage_oneof as PolymeshVariant, + mod_CallIndices::OneOfvariant as CallIndicesVariant, + mod_Identity::{ + mod_AddAuthorization::mod_Authorization::OneOfauth_oneof as AuthVariant, AddAuthorization, + JoinIdentityAsKey, LeaveIdentityAsKey, OneOfmessage_oneof as IdentityVariant, + }, + mod_RuntimeCall::OneOfpallet_oneof as RuntimeCallVariant, mod_Staking::{ Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, WithdrawUnbonded, }, - Balance, Identity, Staking, + mod_Utility::{BatchKind, OneOfmessage_oneof as UtilityVariant}, + Balance, CallIndices, Identity, Staking, Utility, }; -use tw_scale::{impl_enum_scale, impl_struct_scale, Compact, RawOwned, ToScale}; +use tw_scale::{impl_enum_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()) - } +use tw_substrate::*; + +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) } -impl_struct_scale!( - #[derive(Clone, Debug)] - pub struct IdentityId(H256); -); - impl_enum_scale!( #[derive(Clone, Debug)] pub enum PolymeshBalances { @@ -84,26 +79,15 @@ impl PolymeshBalances { BalanceVariant::transfer(t) => Self::encode_transfer(t), _ => Err(EncodeError::NotSupported) .into_tw() - .context("Unsupported balance call"), + .context("Unsupported balance call".to_string()), } } } -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, + JoinIdentity { permissions: Permissions } = 0x05, } ); @@ -113,6 +97,7 @@ impl_enum_scale!( JoinIdentity { auth_id: u64, } = 0x04, + LeaveIdentity = 0x05, AddAuthorization { target: Signatory, data: AuthorizationData, @@ -122,52 +107,43 @@ impl_enum_scale!( ); impl PolymeshIdentity { - fn encode_join_identity(join: &JoinIdentityAsKey) -> WithCallIndexResult { - let ci = validate_call_index(&join.call_indices)?; + fn encode_join_identity(msg: &JoinIdentityAsKey) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; Ok(ci.wrap(Self::JoinIdentity { - auth_id: join.auth_id, + auth_id: msg.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); - } + fn encode_leave_identity(msg: &LeaveIdentityAsKey) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; + Ok(ci.wrap(Self::LeaveIdentity)) + } - if let Some(portfolio) = &auth_data.portfolio { - data.push(0x01); - data.extend_from_slice(&portfolio.data); - } else { - data.push(0x00); + fn encode_add_authorization(msg: &AddAuthorization) -> WithCallIndexResult { + let ci = validate_call_index(&msg.call_indices)?; + let target = SS58Address::from_str(&msg.target).map_err(|_| EncodeError::InvalidAddress)?; + let data = if let Some(auth) = &msg.authorization { + match &auth.auth_oneof { + AuthVariant::join_identity(perms) => AuthorizationData::JoinIdentity { + permissions: perms.try_into().map_err(|_| EncodeError::InvalidValue)?, + }, + AuthVariant::None => { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported Authorization".to_string()); + }, } } else { - // Mark everything as authorized (asset, extrinsic, portfolio) - data.push(0x00); - data.push(0x00); - data.push(0x00); - } + return Err(EncodeError::NotSupported) + .into_tw() + .context("Missing Authorization".to_string()); + }; + Ok(ci.wrap(Self::AddAuthorization { target: Signatory::Account(SubstrateAddress(target)), - data: AuthorizationData::JoinIdentity { - permissions: RawOwned(data), - }, - expiry: if auth.expiry > 0 { - Some(auth.expiry) + data, + expiry: if msg.expiry > 0 { + Some(msg.expiry) } else { None }, @@ -177,10 +153,11 @@ impl PolymeshIdentity { pub fn encode_call(ident: &Identity) -> WithCallIndexResult { match &ident.message_oneof { IdentityVariant::join_identity_as_key(t) => Self::encode_join_identity(t), + IdentityVariant::leave_identity_as_key(t) => Self::encode_leave_identity(t), IdentityVariant::add_authorization(a) => Self::encode_add_authorization(a), _ => Err(EncodeError::NotSupported) .into_tw() - .context("Unsupported identity call"), + .context("Unsupported identity call".to_string()), } } } @@ -208,7 +185,7 @@ impl_enum_scale!( Chill = 0x06, Rebond { value: Compact, - } = 0x18, + } = 0x13, } ); @@ -302,7 +279,47 @@ impl PolymeshStaking { StakingVariant::nominate(b) => Self::encode_nominate(b), _ => Err(EncodeError::NotSupported) .into_tw() - .context("Unsupported staking call"), + .context("Unsupported staking call".to_string()), + } + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum PolymeshUtility { + Batch { calls: Vec } = 0x00, + BatchAll { calls: Vec } = 0x02, + ForceBatch { calls: Vec } = 0x04, + } +); + +impl PolymeshUtility { + pub fn encode_call(encoder: &mut CallEncoder, u: &Utility) -> WithCallIndexResult { + if encoder.batch_depth > 0 { + return Err(EncodeError::NotSupported) + .into_tw() + .context("Nested batch calls not allowed"); + } + encoder.batch_depth += 1; + match &u.message_oneof { + UtilityVariant::batch(b) => { + let ci = validate_call_index(&b.call_indices)?; + let calls = b + .calls + .iter() + .map(|call| encoder.encode_runtime_call(call)) + .collect::>>()?; + encoder.batch_depth -= 1; + let batch = match b.kind { + BatchKind::StopOnError => Self::Batch { calls }, + BatchKind::Atomic => Self::BatchAll { calls }, + BatchKind::Optimistic => Self::ForceBatch { calls }, + }; + Ok(ci.wrap(batch)) + }, + _ => Err(EncodeError::NotSupported) + .into_tw() + .context("Unsupported utility call"), } } } @@ -313,48 +330,51 @@ impl_enum_scale!( Balances(PolymeshBalances) = 0x05, Identity(PolymeshIdentity) = 0x07, Staking(PolymeshStaking) = 0x11, - Utility(GenericUtility) = 0x29, + Utility(PolymeshUtility) = 0x29, } ); -pub struct PolymeshCallEncoder; +pub struct CallEncoder { + pub batch_depth: u32, +} + +impl CallEncoder { + pub fn from_ctx(_ctx: &SubstrateContext) -> EncodeResult { + Ok(Self { batch_depth: 0 }) + } -impl PolymeshCallEncoder { - pub fn new_boxed(_ctx: &SubstrateContext) -> Box { - Box::new(Self) + pub fn encode_input(input: &'_ Proto::SigningInput<'_>) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let mut encoder = Self::from_ctx(&ctx)?; + let call = input + .runtime_call + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing runtime call")?; + encoder.encode_runtime_call(call) } -} -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) + pub fn encode_runtime_call(&mut self, call: &Proto::RuntimeCall) -> EncodeResult { + let call = match &call.pallet_oneof { + RuntimeCallVariant::balance_call(msg) => { + PolymeshBalances::encode_call(msg)?.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"); - }, + RuntimeCallVariant::identity_call(msg) => { + PolymeshIdentity::encode_call(msg)?.map(PolymeshCall::Identity) + }, + RuntimeCallVariant::staking_call(msg) => { + PolymeshStaking::encode_call(msg)?.map(PolymeshCall::Staking) }, - SigningVariant::staking_call(s) => { - PolymeshStaking::encode_call(s)?.map(PolymeshCall::Staking) + RuntimeCallVariant::utility_call(msg) => { + PolymeshUtility::encode_call(self, msg)?.map(PolymeshCall::Utility) }, - SigningVariant::None => { + RuntimeCallVariant::None => { return Err(EncodeError::NotSupported) .into_tw() - .context("Staking call variant is None"); + .context("Runtime call variant is None".to_string()); }, }; 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_polymesh/src/entry.rs b/rust/chains/tw_polymesh/src/entry.rs new file mode 100644 index 00000000000..603efb21872 --- /dev/null +++ b/rust/chains/tw_polymesh/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::Polymesh::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 PolymeshEntry; + +impl PolymeshEntry { + #[inline] + fn get_keypair_impl( + &self, + _coin: &dyn CoinContext, + input: &Proto::SigningInput<'_>, + ) -> SigningResult { + Ok(KeyPair::try_from(input.private_key.as_ref())?) + } + + #[inline] + fn build_transaction_impl( + &self, + _coin: &dyn CoinContext, + public_key: Option, + input: &Proto::SigningInput<'_>, + ) -> EncodeResult { + let ctx = ctx_from_tw(input)?; + let mut encoder = CallEncoder::from_ctx(&ctx)?; + let call = input + .runtime_call + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing runtime call")?; + let call = encoder.encode_runtime_call(call)?; + 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(true, 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)); + builder.extension(ChargeTransactionPayment::new(tip)); + 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 PolymeshEntry { + 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_polymesh/src/lib.rs b/rust/chains/tw_polymesh/src/lib.rs new file mode 100644 index 00000000000..cc814de9bbd --- /dev/null +++ b/rust/chains/tw_polymesh/src/lib.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_proto::Polymesh::Proto; +use tw_ss58_address::NetworkId; +use tw_substrate::*; + +pub mod call_encoder; +pub mod entry; +pub mod types; + +pub const POLYMESH_PREFIX: u16 = 12; +pub const POLYMESH: NetworkId = NetworkId::new_unchecked(POLYMESH_PREFIX); + +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; + + Ok(SubstrateContext { + multi_address: true, + network, + spec_version, + transaction_version: input.transaction_version, + fee_asset_id: None, + check_metadata: false, + }) +} diff --git a/rust/chains/tw_polymesh/src/types.rs b/rust/chains/tw_polymesh/src/types.rs new file mode 100644 index 00000000000..a4798a2a11c --- /dev/null +++ b/rust/chains/tw_polymesh/src/types.rs @@ -0,0 +1,341 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use tw_coin_entry::error::prelude::*; +use tw_hash::{Hash, H256}; +use tw_proto::Polymesh::Proto::{ + mod_SecondaryKeyPermissions::{ + AssetPermissions as TWAssetPermissions, ExtrinsicPermissions as TWExtrinsicPermissions, + PalletPermissions as TWPalletPermissions, PortfolioPermissions as TWPortfolioPermissions, + RestrictionKind as TWRestrictionKind, + }, + AssetId as TWAssetId, IdentityId as TWIdentityId, PortfolioId as TWPortfolioId, + RewardDestination as TWRewardDestination, SecondaryKeyPermissions, +}; +use tw_scale::{impl_enum_scale, impl_struct_scale, ToScale}; + +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()) + } +} + +pub type H128 = Hash<16>; + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct AssetId(H128); +); + +impl TryFrom<&TWAssetId<'_>> for AssetId { + type Error = TWError; + + fn try_from(id: &TWAssetId) -> Result { + let did = H128::try_from(id.id.as_ref()) + .map_err(|_| EncodeError::InvalidValue) + .into_tw() + .context("Expected 16 byte AssetId")?; + Ok(Self(did)) + } +} + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct IdentityId(H256); +); + +impl TryFrom<&TWIdentityId<'_>> for IdentityId { + type Error = TWError; + + fn try_from(id: &TWIdentityId) -> Result { + let did = H256::try_from(id.id.as_ref()) + .map_err(|_| EncodeError::InvalidValue) + .into_tw() + .context("Expected 32 byte IdentityId")?; + Ok(Self(did)) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub enum PortfolioKind { + #[default] + Default = 0x00, + User(u64) = 0x01, + } +); + +impl_struct_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] + pub struct PortfolioId { + did: IdentityId, + kind: PortfolioKind, + } +); + +impl TryFrom<&TWPortfolioId<'_>> for PortfolioId { + type Error = TWError; + + fn try_from(portfolio: &TWPortfolioId) -> Result { + Ok(Self { + did: portfolio + .identity + .as_ref() + .ok_or(EncodeError::InvalidValue) + .into_tw() + .context("Missing portfolio identity")? + .try_into()?, + kind: if portfolio.default { + PortfolioKind::Default + } else { + PortfolioKind::User(portfolio.user) + }, + }) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum Signatory { + Identity(IdentityId) = 0x00, + Account(AccountId) = 0x01, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub enum RestrictionKind { + #[default] + Whole = 0x00, + These = 0x01, + Except = 0x02, + } +); + +impl From for RestrictionKind { + fn from(kind: TWRestrictionKind) -> Self { + match kind { + TWRestrictionKind::Whole => Self::Whole, + TWRestrictionKind::These => Self::These, + TWRestrictionKind::Except => Self::Except, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct AssetPermissions { + kind: RestrictionKind, + assets: BTreeSet, +} + +impl AssetPermissions { + /// Empty permissions means no access. + pub fn empty() -> Self { + Self { + kind: RestrictionKind::These, + assets: BTreeSet::new(), + } + } +} + +impl ToScale for AssetPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.assets.to_scale_into(data); + } + } +} + +impl TryFrom<&TWAssetPermissions<'_>> for AssetPermissions { + type Error = TWError; + + fn try_from(perms: &TWAssetPermissions) -> Result { + Ok(Self { + kind: perms.kind.into(), + assets: perms + .assets + .iter() + .map(|asset| asset.try_into()) + .collect::>>()?, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct PortfolioPermissions { + kind: RestrictionKind, + portfolios: BTreeSet, +} + +impl PortfolioPermissions { + /// Empty permissions means no access. + pub fn empty() -> Self { + Self { + kind: RestrictionKind::These, + portfolios: BTreeSet::new(), + } + } +} + +impl ToScale for PortfolioPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.portfolios.to_scale_into(data); + } + } +} + +impl TryFrom<&TWPortfolioPermissions<'_>> for PortfolioPermissions { + type Error = TWError; + + fn try_from(perms: &TWPortfolioPermissions) -> Result { + Ok(Self { + kind: perms.kind.into(), + portfolios: perms + .portfolios + .iter() + .map(|portfolio| portfolio.try_into()) + .collect::>>()?, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct PalletPermissions { + kind: RestrictionKind, + extrinsic_names: BTreeSet, +} + +impl ToScale for PalletPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.extrinsic_names.to_scale_into(data); + } + } +} + +impl From<&TWPalletPermissions<'_>> for PalletPermissions { + fn from(perms: &TWPalletPermissions) -> Self { + Self { + kind: perms.kind.into(), + extrinsic_names: perms + .extrinsic_names + .iter() + .map(|name| name.to_string()) + .collect(), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ExtrinsicPermissions { + kind: RestrictionKind, + pallets: BTreeMap, +} + +impl ExtrinsicPermissions { + /// Empty permissions means no access. + pub fn empty() -> Self { + Self { + kind: RestrictionKind::These, + pallets: BTreeMap::new(), + } + } +} + +impl ToScale for ExtrinsicPermissions { + fn to_scale_into(&self, data: &mut Vec) { + self.kind.to_scale_into(data); + if self.kind != RestrictionKind::Whole { + self.pallets.to_scale_into(data); + } + } +} + +impl From<&TWExtrinsicPermissions<'_>> for ExtrinsicPermissions { + fn from(perms: &TWExtrinsicPermissions) -> Self { + Self { + kind: perms.kind.into(), + pallets: perms + .pallets + .iter() + .map(|pallet| (pallet.pallet_name.to_string(), pallet.into())) + .collect(), + } + } +} + +impl_struct_scale!( + #[derive(Clone, Debug, Default)] + pub struct Permissions { + asset: AssetPermissions, + extrinsic: ExtrinsicPermissions, + portfolio: PortfolioPermissions, + } +); + +impl TryFrom<&SecondaryKeyPermissions<'_>> for Permissions { + type Error = TWError; + + fn try_from(perms: &SecondaryKeyPermissions) -> Result { + Ok(Self { + asset: if let Some(perms) = &perms.asset { + perms.try_into()? + } else { + AssetPermissions::default() + }, + extrinsic: if let Some(perms) = &perms.extrinsic { + perms.into() + } else { + ExtrinsicPermissions::default() + }, + portfolio: if let Some(perms) = &perms.portfolio { + perms.try_into()? + } else { + PortfolioPermissions::default() + }, + }) + } +} + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum AuthorizationData { + JoinIdentity { permissions: Permissions } = 0x05, + } +); + +impl_enum_scale!( + #[derive(Clone, Debug)] + pub enum RewardDestination { + Staked = 0x00, + Stash = 0x01, + Controller = 0x02, + Account(AccountId) = 0x03, + 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), + } + } +} diff --git a/rust/chains/tw_polymesh/tests/extrinsic.rs b/rust/chains/tw_polymesh/tests/extrinsic.rs new file mode 100644 index 00000000000..a7bf864e192 --- /dev/null +++ b/rust/chains/tw_polymesh/tests/extrinsic.rs @@ -0,0 +1,416 @@ +use std::borrow::Cow; +use std::default::Default; + +use tw_encoding::hex::ToHex; +use tw_number::U256; +use tw_proto::Polymesh::Proto::{ + self, + mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, + mod_Identity::{ + mod_AddAuthorization::{mod_Authorization::OneOfauth_oneof as AuthVariant, Authorization}, + AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant, + }, + mod_RuntimeCall::OneOfpallet_oneof as CallVariant, + mod_SecondaryKeyPermissions::{ + AssetPermissions, ExtrinsicPermissions, PortfolioPermissions, RestrictionKind, + }, + mod_Staking::{ + Bond, BondExtra, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Rebond, Unbond, + WithdrawUnbonded, + }, + mod_Utility::{Batch, BatchKind, OneOfmessage_oneof as UtilityVariant}, + Balance, Identity, RuntimeCall, SecondaryKeyPermissions, Staking, Utility, +}; + +use tw_polymesh::call_encoder::CallEncoder; + +fn expect_encoded(input: &Proto::SigningInput<'_>, expected_value: &str) { + let encoded = CallEncoder::encode_input(input).expect("error encoding call"); + assert_eq!(encoded.0.to_hex(), expected_value); +} + +fn polymesh_identity_call(call: IdentityVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::identity_call(Identity { + message_oneof: call, + }), + } +} + +fn polymesh_add_auth_call(add_auth: AddAuthorization) -> RuntimeCall<'_> { + polymesh_identity_call(IdentityVariant::add_authorization(add_auth)) +} + +fn polymesh_join_identity(auth_id: u64) -> RuntimeCall<'static> { + polymesh_identity_call(IdentityVariant::join_identity_as_key(JoinIdentityAsKey { + call_indices: None, + auth_id, + })) +} + +fn balance_call(call: BalanceVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::balance_call(Balance { + message_oneof: call, + }), + } +} + +fn staking_call(call: StakingVariant) -> RuntimeCall<'_> { + RuntimeCall { + pallet_oneof: CallVariant::staking_call(Staking { + message_oneof: call, + }), + } +} + +fn batch_calls(kind: BatchKind, calls: Vec>) -> RuntimeCall<'static> { + RuntimeCall { + pallet_oneof: CallVariant::utility_call(Utility { + message_oneof: UtilityVariant::batch(Batch { + kind, + calls, + ..Default::default() + }), + }), + } +} + +fn build_input(runtime_call: RuntimeCall<'_>) -> Proto::SigningInput<'_> { + Proto::SigningInput { + network: 12, + transaction_version: 7, + runtime_call: Some(runtime_call), + ..Default::default() + } +} + +/// Test POLYX transfer with memo. +#[test] +fn polymesh_encode_transfer_with_memo() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000 + + let input = build_input(balance_call( + Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + }), + )); + + expect_encoded(&input, "0501004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e0204014d454d4f20504144444544205749544820535041434553000000000000000000"); +} + +/// Test add authorization to join identity with default permissions (`Whole` meaning all permissions). +#[test] +fn polymesh_encode_authorization_join_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions::default()), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000", + ); +} + +/// Test add authorization to join identity with no permissions. +#[test] +fn polymesh_encode_authorization_join_identity_with_zero_data() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // No asset permissions. + asset: Some(AssetPermissions { + kind: RestrictionKind::These, + assets: vec![], + }), + // No extrinsic permissions. + extrinsic: Some(ExtrinsicPermissions { + kind: RestrictionKind::These, + pallets: vec![], + }), + // No portfolio permissions. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::These, + portfolios: vec![], + }), + }), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320501000100010000", + ); +} + +/// Test add authorization to join identity with all permissions. Each permission is set to `None`, which defaults to `Whole`. +#[test] +fn polymesh_encode_authorization_join_identity_allowing_everything() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000 + + let input = build_input(polymesh_add_auth_call(AddAuthorization { + target: "2FM6FpjQ6r5HTt7FGYSzskDNkwUyFsonMtwBpsnr9vwmCjhc".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // All asset permissions. + asset: None, + // All extrinsic permissions. + extrinsic: None, + // All portfolio permissions. + portfolio: None, + }), + }), + ..Default::default() + })); + + expect_encoded( + &input, + "070a0180436894d47a18e0bcfea6940bd90226f7104fbd037a259aeff6b47b8257c1320500000000", + ); +} + +/// Test accepting a join identity authorization. +#[test] +fn polymesh_encode_identity_join_identity() { + // https://mainnet-app.polymesh.network/#/extrinsics/decode/0x07040b13000000000000 + + let input = build_input(polymesh_join_identity(4875)); + + expect_encoded(&input, "07040b13000000000000"); +} + +/// Test staking nominate. +#[test] +fn encode_staking_nominate() { + let input = build_input(staking_call(StakingVariant::nominate(Nominate { + nominators: vec![ + "2DxgKKS53wsAeETAZXhmT5A1bTt7h1aV4bKdtkMDwwSzSMXm".into(), + "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + ], + call_indices: None, + }))); + + expect_encoded(&input, "1105080042ef301451c7f596f974daec8ca1234f66809905d13e16c18e23896b0c57e53e00ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e67148"); +} + +/// Test staking chill. +#[test] +fn encode_staking_chill() { + let input = build_input(staking_call(StakingVariant::chill(Chill { + call_indices: None, + }))); + + expect_encoded(&input, "1106"); +} + +/// Test staking bond. +#[test] +fn encode_staking_bond() { + let input = build_input(staking_call(StakingVariant::bond(Bond { + controller: "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::STAKED, + call_indices: None, + }))); + + expect_encoded( + &input, + "110000ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e671484652310000", + ); +} + +/// Test staking bond extra. +#[test] +fn encode_staking_bond_extra() { + let input = build_input(staking_call(StakingVariant::bond_extra(BondExtra { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "110146523100"); +} + +/// Test staking rebond. +#[test] +fn encode_staking_rebond() { + let input = build_input(staking_call(StakingVariant::rebond(Rebond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "111346523100"); +} + +/// Test staking unbond. +#[test] +fn encode_staking_unbond() { + let input = build_input(staking_call(StakingVariant::unbond(Unbond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + }))); + + expect_encoded(&input, "110246523100"); +} + +/// Test staking withdraw unbonded. +#[test] +fn encode_staking_withdraw_unbonded() { + let input = build_input(staking_call(StakingVariant::withdraw_unbonded( + WithdrawUnbonded { + slashing_spans: 84, + call_indices: None, + }, + ))); + + expect_encoded(&input, "110354000000"); +} + +/// Test atomic batching staking calls bond and nominate. +#[test] +fn encode_staking_batch_bond_and_nominate() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::bond(Bond { + controller: "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + value: U256::from(808081u64).to_big_endian().to_vec().into(), + reward_destination: Proto::RewardDestination::STAKED, + call_indices: None, + })), + staking_call(StakingVariant::nominate(Nominate { + nominators: vec![ + "2DxgKKS53wsAeETAZXhmT5A1bTt7h1aV4bKdtkMDwwSzSMXm".into(), + "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F".into(), + ], + call_indices: None, + })), + ], + )); + + expect_encoded( + &input, + "290208110000ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e6714846523100001105080042ef301451c7f596f974daec8ca1234f66809905d13e16c18e23896b0c57e53e00ee93a4f66f8d16b819bb9beb9ffccdfcdc1412e87fee6a324c2a99a1e0e67148" + ); +} + +/// Test atomic batching of staking calls chill and unbond. +#[test] +fn encode_staking_batch_chill_and_unbond() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::chill(Chill { call_indices: None })), + staking_call(StakingVariant::unbond(Unbond { + value: U256::from(808081u64).to_big_endian().to_vec().into(), + call_indices: None, + })), + ], + )); + + expect_encoded(&input, "2902081106110246523100"); +} + +/// Test optimistic batch of POLYX transfers. +#[test] +fn encode_batch_transfers() { + let input = build_input(batch_calls( + BatchKind::Optimistic, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + })), + ], + )); + + expect_encoded( + &input, + "2904080500004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e02040501004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c08014d454d4f20504144444544205749544820535041434553000000000000000000", + ); +} + +/// Test stop on first error batch of POLYX transfers. +#[test] +fn encode_batch_transfers_stop_on_first_error() { + let input = build_input(batch_calls( + BatchKind::StopOnError, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + })), + ], + )); + + expect_encoded( + &input, + "2900080500004c6c63e3dc083959f876788716b78885460b5f3c7ed9379f8d5f408e08639e02040501004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c08014d454d4f20504144444544205749544820535041434553000000000000000000", + ); +} + +/// Test that nesting of batch calls is not allowed. +#[test] +fn encode_nested_batch_calls() { + let input = build_input(batch_calls( + BatchKind::Atomic, + vec![ + balance_call(Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EB7wW2fYfFskkSx2d65ivn34ewpuEjcowfJYBL79ty5FsZF".into(), + value: Cow::Owned(U256::from(1u64).to_big_endian().to_vec()), + ..Default::default() + })), + batch_calls( + BatchKind::Atomic, + vec![balance_call( + Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys".into(), + value: Cow::Owned(U256::from(2u64).to_big_endian().to_vec()), + // The memo field is padded with nulls. + memo: "MEMO PADDED WITH SPACES".into(), + ..Default::default() + }), + )], + ), + ], + )); + + let tw_err = + CallEncoder::encode_input(&input).expect_err("nested batch calls should not be allowed"); + assert_eq!( + tw_err.error_type(), + &tw_substrate::EncodeError::NotSupported + ); + // Ensure the error message contains the expected context. + let context = format!("{:?}", tw_err); + assert!(context.contains("Nested batch calls not allowed")); +} diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index ce69b29fde6..a257822548c 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -27,6 +27,7 @@ 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_polymesh = { path = "../chains/tw_polymesh" } tw_ronin = { path = "../chains/tw_ronin" } tw_solana = { path = "../chains/tw_solana" } tw_substrate = { path = "../frameworks/tw_substrate" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index 92420c6491a..b611ecbf5c6 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -17,11 +17,12 @@ pub enum BlockchainType { Ethereum, Greenfield, InternetComputer, + Kusama, NativeEvmos, NativeInjective, Pactus, Polkadot, - Kusama, + Polymesh, Ronin, Solana, Sui, diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index ee3e958ea37..2941d42293e 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -21,6 +21,7 @@ use tw_native_evmos::entry::NativeEvmosEntry; use tw_native_injective::entry::NativeInjectiveEntry; use tw_pactus::entry::PactusEntry; use tw_polkadot::entry::PolkadotEntry; +use tw_polymesh::entry::PolymeshEntry; use tw_ronin::entry::RoninEntry; use tw_solana::entry::SolanaEntry; use tw_substrate::entry::SubstrateEntry; @@ -44,6 +45,7 @@ const NATIVE_EVMOS: NativeEvmosEntry = NativeEvmosEntry; const NATIVE_INJECTIVE: NativeInjectiveEntry = NativeInjectiveEntry; const PACTUS: PactusEntry = PactusEntry; const POLKADOT: SubstrateEntry = SubstrateEntry(PolkadotEntry); +const POLYMESH: SubstrateEntry = SubstrateEntry(PolymeshEntry); const RONIN: RoninEntry = RoninEntry; const SOLANA: SolanaEntry = SolanaEntry; const SUI: SuiEntry = SuiEntry; @@ -62,11 +64,12 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(ÐEREUM), BlockchainType::Greenfield => Ok(&GREENFIELD), BlockchainType::InternetComputer => Ok(&INTERNET_COMPUTER), + BlockchainType::Kusama => Ok(&POLKADOT), BlockchainType::NativeEvmos => Ok(&NATIVE_EVMOS), BlockchainType::NativeInjective => Ok(&NATIVE_INJECTIVE), BlockchainType::Pactus => Ok(&PACTUS), BlockchainType::Polkadot => Ok(&POLKADOT), - BlockchainType::Kusama => Ok(&POLKADOT), + BlockchainType::Polymesh => Ok(&POLYMESH), BlockchainType::Ronin => Ok(&RONIN), BlockchainType::Solana => Ok(&SOLANA), BlockchainType::Sui => Ok(&SUI), diff --git a/rust/tw_scale/src/lib.rs b/rust/tw_scale/src/lib.rs index 2da23537483..dec8344929b 100644 --- a/rust/tw_scale/src/lib.rs +++ b/rust/tw_scale/src/lib.rs @@ -128,6 +128,13 @@ where } } +impl ToScale for String { + fn to_scale_into(&self, out: &mut Vec) { + self.as_bytes().to_scale_into(out) + } +} + +/// RawOwned is used to wrap data that is already encoded in SCALE format. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct RawOwned(pub Vec); @@ -143,6 +150,35 @@ impl ToScale for RawOwned { } } +// Implement ToScale for BTreeSet collection. +impl ToScale for std::collections::BTreeSet +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); + } + } +} + +// Implement ToScale for BTreeMap collection. +impl ToScale for std::collections::BTreeMap +where + K: ToScale, + V: ToScale, +{ + fn to_scale_into(&self, out: &mut Vec) { + Compact(self.len()).to_scale_into(out); + for (k, v) in self.iter() { + k.to_scale_into(out); + v.to_scale_into(out); + } + } +} + +// Implement ToScale for Vec collection. impl ToScale for Vec where T: ToScale, @@ -152,6 +188,7 @@ where } } +// Implement ToScale for references to types that implement ToScale. impl ToScale for &T { fn to_scale_into(&self, out: &mut Vec) { (*self).to_scale_into(out) @@ -395,4 +432,45 @@ mod tests { &[0x18, 0x04, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x17, 0x00, 0x2a, 0x00], ); } + + // Test SCALE encoding of String + #[test] + fn test_string() { + assert_eq!("".to_string().to_scale(), &[0x00]); + assert_eq!( + "hello".to_string().to_scale(), + &[0x14, 0x68, 0x65, 0x6c, 0x6c, 0x6f] + ); + assert_eq!( + "hello world".to_string().to_scale(), + &[0x2c, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64] + ); + } + + // Test SCALE encoding of BTreeSet + #[test] + fn test_btree_set() { + use std::collections::BTreeSet; + let mut set = BTreeSet::new(); + set.insert(10u8); + set.insert(30u8); + set.insert(20u8); + // The values are encoded in sorted order. + assert_eq!(set.to_scale(), &[0x0c, 10, 20, 30]); + } + + // Test SCALE encoding of BTreeMap + #[test] + fn test_btree_map() { + use std::collections::BTreeMap; + let mut map = BTreeMap::new(); + map.insert(30u8, 300u16); + map.insert(10u8, 100u16); + map.insert(20u8, 200u16); + // The keys/value pairs are encoded in sorted order (by key order). + assert_eq!( + map.to_scale(), + &[0x0c, 10, 0x64, 0x00, 20, 0xc8, 0x00, 30, 0x2c, 0x01] + ); + } } diff --git a/rust/tw_scale/src/macros.rs b/rust/tw_scale/src/macros.rs index 3e5e345ba8d..3340790eda1 100644 --- a/rust/tw_scale/src/macros.rs +++ b/rust/tw_scale/src/macros.rs @@ -117,7 +117,7 @@ macro_rules! impl_enum_scale { $( $variant_field_name : $variant_field_ty ),+ - })? = $variant_index, + })?, )* } @@ -200,6 +200,8 @@ mod tests { Variant10 = 10, /// Struct variant. Struct { id: u8, id2: u8 } = 11, + /// Struct variant v2. Variants can use the same index. This allows for backwards compatibility. + StructV2 { id: u8, id2: u16 } = 11, } ); @@ -212,5 +214,9 @@ mod tests { TestEnum::Struct { id: 1, id2: 2 }.to_scale(), &[0x0B, 0x01, 0x02] ); + assert_eq!( + TestEnum::StructV2 { id: 1, id2: 2 }.to_scale(), + &[0x0B, 0x01, 0x02, 0x00] + ); } } diff --git a/rust/tw_tests/tests/chains/polkadot/mod.rs b/rust/tw_tests/tests/chains/polkadot/mod.rs index 5e57ab33dc0..ff4f5541cc5 100644 --- a/rust/tw_tests/tests/chains/polkadot/mod.rs +++ b/rust/tw_tests/tests/chains/polkadot/mod.rs @@ -17,8 +17,6 @@ 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"; @@ -94,7 +92,7 @@ pub fn helper_encode_and_compile( (preimage, signed) } -pub fn balance_call( +fn balance_call( call: Proto::mod_Balance::OneOfmessage_oneof, ) -> Proto::mod_SigningInput::OneOfmessage_oneof { Proto::mod_SigningInput::OneOfmessage_oneof::balance_call(Proto::Balance { @@ -109,15 +107,3 @@ pub fn staking_call( 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_sign.rs b/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs index 98b8631c1d0..d12a3ac2a5a 100644 --- a/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs +++ b/rust/tw_tests/tests/chains/polkadot/polkadot_sign.rs @@ -3,9 +3,8 @@ // 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, + balance_call, helper_encode, helper_encode_and_maybe_sign, helper_sign, staking_call, + ACCOUNT_2, GENESIS_HASH, PRIVATE_KEY, PRIVATE_KEY_2, PRIVATE_KEY_IOS, PRIVATE_KEY_POLKADOT, }; use std::borrow::Cow; use tw_any_coin::any_address::AnyAddress; @@ -427,148 +426,6 @@ fn test_polkadot_sign_chill_and_unbond() { ); } -// 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() { @@ -802,97 +659,6 @@ fn test_statemint_encode_transaction_usdt_transfer_keep_alive() { 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() { diff --git a/rust/tw_tests/tests/chains/polymesh/mod.rs b/rust/tw_tests/tests/chains/polymesh/mod.rs index 157974d25e6..578833cc23f 100644 --- a/rust/tw_tests/tests/chains/polymesh/mod.rs +++ b/rust/tw_tests/tests/chains/polymesh/mod.rs @@ -1 +1,151 @@ +// 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::Polymesh::Proto::{self, SigningInput}; +use tw_proto::TxCompiler::Proto::{self as CompilerProto, PreSigningOutput}; + mod polymesh_address; +mod polymesh_compile; +mod polymesh_sign; + +pub const GENESIS_HASH: &str = "0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"; +/// Private key for testing. DO NOT USE, since this is public. +pub const PRIVATE_KEY_1: &str = + "0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4"; +pub const PUBLIC_KEY_1: &str = "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys"; +pub const PUBLIC_KEY_HEX_1: &str = + "0x4bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c"; + +pub const PUBLIC_KEY_2: &str = "2CpqFh8VnwJAjenw4xSUWCaaJ2QwGdhnCikoSEczMhjgqyj7"; + +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 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() +} + +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() +} + +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) +} + +fn balance_call(call: Proto::mod_Balance::OneOfmessage_oneof) -> Proto::RuntimeCall<'_> { + Proto::RuntimeCall { + pallet_oneof: Proto::mod_RuntimeCall::OneOfpallet_oneof::balance_call(Proto::Balance { + message_oneof: call, + }), + } +} + +fn identity_call(call: Proto::mod_Identity::OneOfmessage_oneof) -> Proto::RuntimeCall<'_> { + Proto::RuntimeCall { + pallet_oneof: Proto::mod_RuntimeCall::OneOfpallet_oneof::identity_call(Proto::Identity { + message_oneof: call, + }), + } +} + +fn identity_add_auth_call( + add_auth: Proto::mod_Identity::AddAuthorization, +) -> Proto::RuntimeCall<'_> { + identity_call(Proto::mod_Identity::OneOfmessage_oneof::add_authorization( + add_auth, + )) +} + +fn identity_join_identity( + auth_id: u64, + call_indices: Option, +) -> Proto::RuntimeCall<'static> { + identity_call( + Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key( + Proto::mod_Identity::JoinIdentityAsKey { + call_indices, + auth_id, + }, + ), + ) +} + +fn staking_call(call: Proto::mod_Staking::OneOfmessage_oneof) -> Proto::RuntimeCall<'_> { + Proto::RuntimeCall { + pallet_oneof: Proto::mod_RuntimeCall::OneOfpallet_oneof::staking_call(Proto::Staking { + message_oneof: call, + }), + } +} + +fn batch_calls( + kind: Proto::mod_Utility::BatchKind, + calls: Vec>, +) -> Proto::RuntimeCall<'static> { + Proto::RuntimeCall { + pallet_oneof: Proto::mod_RuntimeCall::OneOfpallet_oneof::utility_call(Proto::Utility { + message_oneof: Proto::mod_Utility::OneOfmessage_oneof::batch( + Proto::mod_Utility::Batch { + kind, + calls, + ..Default::default() + }, + ), + }), + } +} diff --git a/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs b/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs index 15957c6c549..4712270ef18 100644 --- a/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs +++ b/rust/tw_tests/tests/chains/polymesh/polymesh_address.rs @@ -2,32 +2,55 @@ // // Copyright © 2017 Trust Wallet. +use crate::chains::polymesh::{PRIVATE_KEY_1, PUBLIC_KEY_1, PUBLIC_KEY_2, PUBLIC_KEY_HEX_1}; use tw_any_coin::test_utils::address_utils::{ - test_address_ss58_is_invalid, test_address_ss58_is_valid, + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_ss58_is_invalid, test_address_ss58_is_valid, test_address_valid, }; use tw_coin_registry::coin_type::CoinType; +#[test] +fn test_polymesh_address_derive() { + test_address_derive(CoinType::Polymesh, PRIVATE_KEY_1, PUBLIC_KEY_1); +} + +#[test] +fn test_polymesh_address_normalization() { + test_address_normalization(CoinType::Polymesh, PUBLIC_KEY_1, PUBLIC_KEY_1); +} + #[test] fn test_polymesh_address_is_valid() { // Polymesh test_address_ss58_is_valid( - CoinType::Polkadot, + CoinType::Polymesh, "2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG", 12, // Polymesh ss58 ); + test_address_valid(CoinType::Polymesh, PUBLIC_KEY_1); + test_address_valid(CoinType::Polymesh, PUBLIC_KEY_2); } #[test] fn test_polymesh_address_invalid() { // Substrate ed25519 test_address_ss58_is_invalid( - CoinType::Polkadot, + CoinType::Polymesh, "5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ", 12, // Polymesh ss58 ); test_address_ss58_is_invalid( - CoinType::Polkadot, + CoinType::Polymesh, "JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A", 12, // Polymesh ss58 ); + test_address_invalid( + CoinType::Polymesh, + "5HUUBD9nsjaKKUVB3XV87CcjcEDu7sDH2G32NAj6uNqgWp9G", + ); +} + +#[test] +fn test_polymesh_address_get_data() { + test_address_get_data(CoinType::Polymesh, PUBLIC_KEY_1, PUBLIC_KEY_HEX_1); } diff --git a/rust/tw_tests/tests/chains/polymesh/polymesh_compile.rs b/rust/tw_tests/tests/chains/polymesh/polymesh_compile.rs new file mode 100644 index 00000000000..29b4a82c5f3 --- /dev/null +++ b/rust/tw_tests/tests/chains/polymesh/polymesh_compile.rs @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::polymesh::{ + balance_call, identity_call, GENESIS_HASH, PUBLIC_KEY_1, PUBLIC_KEY_2, PUBLIC_KEY_HEX_1, +}; +use std::borrow::Cow; +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_number::U256; +use tw_proto::Common::Proto::SigningError; +use tw_proto::Polymesh::Proto::{ + self, + mod_Balance::Transfer, + mod_Identity::{ + mod_AddAuthorization::{mod_Authorization::OneOfauth_oneof as AuthVariant, Authorization}, + AddAuthorization, JoinIdentityAsKey, LeaveIdentityAsKey, + }, + mod_SecondaryKeyPermissions::{ + AssetPermissions, ExtrinsicPermissions, PortfolioPermissions, RestrictionKind, + }, + SecondaryKeyPermissions, +}; +use tw_proto::TxCompiler::Proto as CompilerProto; + +// Test compile of AddAuthorization: JoinIdentity +#[test] +fn test_polymesh_compile_add_authorization() { + // https://polymesh.subscan.io/extrinsic/16102080-1 + + // Step 1: Prepare input. + let block_hash = "b569a6fcba97252a9987f7beac2fe6dbb560b78a45e623be1e2f54fe18778512" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + nonce: 11, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_074, + period: 64, + }), + runtime_call: Some(identity_call( + Proto::mod_Identity::OneOfmessage_oneof::add_authorization(AddAuthorization { + target: PUBLIC_KEY_1.into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // No asset permissions. + asset: Some(AssetPermissions { + kind: RestrictionKind::These, + assets: vec![], + }), + // No extrinsic permissions. + extrinsic: Some(ExtrinsicPermissions { + kind: RestrictionKind::These, + pallets: vec![], + }), + // No portfolio permissions. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::These, + portfolios: vec![], + }), + }), + }), + ..Default::default() + }), + )), + ..Default::default() + }; + + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Polymesh, &input); + + assert_eq!(preimage_output.error, SigningError::OK); + + assert_eq!(preimage_output.data.to_hex(), "070a014bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c0501000100010000a5032c00c5cf6a00070000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063b569a6fcba97252a9987f7beac2fe6dbb560b78a45e623be1e2f54fe18778512"); + + // SR25519 signature is not supported yet. +} + +// Test compile of JoinIdentityAsKey transaction. +#[test] +fn test_polymesh_compile_join_identity() { + // https://polymesh.subscan.io/extrinsic/16102090-1 + + // Step 1: Prepare input. + let block_hash = "cd19ce1ee3d725d5a62f29c41925d25f0655043e579231d24fb0175268b7e340" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_087, + period: 64, + }), + runtime_call: Some(identity_call( + Proto::mod_Identity::OneOfmessage_oneof::join_identity_as_key(JoinIdentityAsKey { + auth_id: 52_188, + ..Default::default() + }), + )), + ..Default::default() + }; + + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Polymesh, &input); + + assert_eq!(preimage_output.error, SigningError::OK); + + assert_eq!(preimage_output.data.to_hex(), "0704dccb00000000000075000000c5cf6a00070000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063cd19ce1ee3d725d5a62f29c41925d25f0655043e579231d24fb0175268b7e340"); + + // Step 3: Compile transaction info + + // Simulate signature, normally obtained from signature server + let signature_bytes = "b40292db45bc8f910b580a586ff81f6c1655fc928d0bf0f41929385b26fda364985d9dee576dec47712a215bb7f70f4c926d1853533cdb693a45c65e8c017904".decode_hex().unwrap(); + let signature = Signature::try_from(signature_bytes.as_slice()).unwrap(); + let public_key = PUBLIC_KEY_HEX_1.decode_hex().unwrap(); + let public = PublicKey::try_from(public_key.as_slice()).unwrap(); + + // Verify signature (pubkey & hash & signature) + assert!(public.verify(signature, preimage_output.data.into())); + + // Compile transaction info + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::Polymesh, + &input, + vec![signature_bytes], + vec![public_key], + ); + assert_eq!(output.error, SigningError::OK); + + assert_eq!( + output.encoded.to_hex(), + "c50184004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00b40292db45bc8f910b580a586ff81f6c1655fc928d0bf0f41929385b26fda364985d9dee576dec47712a215bb7f70f4c926d1853533cdb693a45c65e8c017904750000000704dccb000000000000" + ); +} + +#[test] +fn test_polymesh_compile_transfer() { + // https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04 + + // Step 1: Prepare input. + let block_hash = "77d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + let value = 1_000_000u64; // 1.0 POLYX + + let input = Proto::SigningInput { + network: 12, + nonce: 1, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_106, + period: 64, + }), + runtime_call: Some(balance_call( + Proto::mod_Balance::OneOfmessage_oneof::transfer(Transfer { + to_address: PUBLIC_KEY_2.into(), + value: Cow::Owned(U256::from(value).to_big_endian().to_vec()), + ..Default::default() + }), + )), + ..Default::default() + }; + + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Polymesh, &input); + + assert_eq!(preimage_output.error, SigningError::OK); + + assert_eq!( + preimage_output.data.to_hex(), + "05000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00a5010400c5cf6a00070000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f406377d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d" + ); + + // Step 3: Compile transaction info + + // Simulate signature, normally obtained from signature server + let signature_bytes = "e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210b".decode_hex().unwrap(); + let signature = Signature::try_from(signature_bytes.as_slice()).unwrap(); + let public_key = PUBLIC_KEY_HEX_1.decode_hex().unwrap(); + let public = PublicKey::try_from(public_key.as_slice()).unwrap(); + + // Verify signature (pubkey & hash & signature) + assert!(public.verify(signature, preimage_output.data.into())); + + // Compile transaction info + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::Polymesh, + &input, + vec![signature_bytes], + vec![public_key], + ); + assert_eq!(output.error, SigningError::OK); + + assert_eq!( + output.encoded.to_hex(), + "390284004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210ba501040005000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00" + ); +} + +// Test Leave identity transaction. +#[test] +fn test_polymesh_compile_leave_identity() { + // https://polymesh.subscan.io/extrinsic/16102113-1 + + // Step 1: Prepare input. + let block_hash = "6651325ae8f7c1726f8a610827b5e4300a504081d5fc85c17199d95bb6d9605c" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + nonce: 2, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_110, + period: 64, + }), + runtime_call: Some(identity_call( + Proto::mod_Identity::OneOfmessage_oneof::leave_identity_as_key(LeaveIdentityAsKey { + ..Default::default() + }), + )), + ..Default::default() + }; + + // Step 2: Obtain preimage hash + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Polymesh, &input); + + assert_eq!(preimage_output.error, SigningError::OK); + + assert_eq!(preimage_output.data.to_hex(), "0705e5010800c5cf6a00070000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f40636651325ae8f7c1726f8a610827b5e4300a504081d5fc85c17199d95bb6d9605c"); + + // Step 3: Compile transaction info + + // Simulate signature, normally obtained from signature server + let signature_bytes = "57232b54338939c6d9742f7a982cc668b45933bbabcb1df100f5e25ec0879eed803c04bcea28734f5e4e034f0f02aac0a8b81dcc860ddcc6b910458fc8cddb08".decode_hex().unwrap(); + let signature = Signature::try_from(signature_bytes.as_slice()).unwrap(); + let public_key = PUBLIC_KEY_HEX_1.decode_hex().unwrap(); + let public = PublicKey::try_from(public_key.as_slice()).unwrap(); + + // Verify signature (pubkey & hash & signature) + assert!(public.verify(signature, preimage_output.data.into())); + + // Compile transaction info + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::Polymesh, + &input, + vec![signature_bytes], + vec![public_key], + ); + assert_eq!(output.error, SigningError::OK); + + assert_eq!( + output.encoded.to_hex(), + "a50184004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c0057232b54338939c6d9742f7a982cc668b45933bbabcb1df100f5e25ec0879eed803c04bcea28734f5e4e034f0f02aac0a8b81dcc860ddcc6b910458fc8cddb08e50108000705" + ); +} diff --git a/rust/tw_tests/tests/chains/polymesh/polymesh_sign.rs b/rust/tw_tests/tests/chains/polymesh/polymesh_sign.rs new file mode 100644 index 00000000000..ac2a0685dc5 --- /dev/null +++ b/rust/tw_tests/tests/chains/polymesh/polymesh_sign.rs @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::polymesh::{ + balance_call, batch_calls, custom_call_indices, helper_encode, helper_encode_and_compile, + helper_sign, identity_add_auth_call, identity_call, identity_join_identity, staking_call, + GENESIS_HASH, PRIVATE_KEY_1, PUBLIC_KEY_2, +}; +use std::borrow::Cow; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_number::U256; +use tw_proto::Polymesh::Proto::{ + self, + mod_Balance::{OneOfmessage_oneof as BalanceVariant, Transfer}, + mod_Identity::{ + mod_AddAuthorization::{mod_Authorization::OneOfauth_oneof as AuthVariant, Authorization}, + AddAuthorization, JoinIdentityAsKey, OneOfmessage_oneof as IdentityVariant, + }, + mod_SecondaryKeyPermissions::{ + AssetPermissions, ExtrinsicPermissions, PortfolioPermissions, RestrictionKind, + }, + mod_Staking::{Bond, Chill, Nominate, OneOfmessage_oneof as StakingVariant, Unbond}, + mod_Utility::BatchKind, + SecondaryKeyPermissions, +}; + +/// Test a join identity as key transaction. +#[test] +fn test_polymesh_sign_join_identity() { + // join identity + // https://polymesh.subscan.io/extrinsic/16102090-1 + + // Step 1: Prepare input. + let private_key = PRIVATE_KEY_1.decode_hex().unwrap(); + let block_hash = "cd19ce1ee3d725d5a62f29c41925d25f0655043e579231d24fb0175268b7e340" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + private_key: private_key.into(), + nonce: 0, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_087, + period: 64, + }), + runtime_call: Some(identity_call(IdentityVariant::join_identity_as_key( + JoinIdentityAsKey { + auth_id: 52_188, + ..Default::default() + }, + ))), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polymesh, input); + + assert_eq!( + signed, + "c50184004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00b40292db45bc8f910b580a586ff81f6c1655fc928d0bf0f41929385b26fda364985d9dee576dec47712a215bb7f70f4c926d1853533cdb693a45c65e8c017904750000000704dccb000000000000" + ); +} + +/// Test a simple POLYX transfer. +#[test] +fn test_polymesh_sign_transfer() { + // https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04 + + // Step 1: Prepare input. + let private_key = PRIVATE_KEY_1.decode_hex().unwrap(); + let block_hash = "77d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d" + .decode_hex() + .unwrap(); + let genesis_hash = GENESIS_HASH.decode_hex().unwrap(); + let value = 1_000_000u64; // 1.0 POLYX + + let input = Proto::SigningInput { + network: 12, + private_key: private_key.into(), + nonce: 1, + block_hash: block_hash.into(), + genesis_hash: genesis_hash.into(), + spec_version: 7_000_005, + transaction_version: 7, + era: Some(Proto::Era { + block_number: 16_102_106, + period: 64, + }), + runtime_call: Some(balance_call(BalanceVariant::transfer(Transfer { + to_address: PUBLIC_KEY_2.into(), + value: Cow::Owned(U256::from(value).to_big_endian().to_vec()), + ..Default::default() + }))), + ..Default::default() + }; + + let signed = helper_sign(CoinType::Polymesh, input); + + assert_eq!( + signed, + "390284004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210ba501040005000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00" + ); +} + +// 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 = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + 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, + }), + runtime_call: Some(balance_call(BalanceVariant::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(), + ..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::Polymesh, 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 = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + 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, + }), + runtime_call: Some(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::bond(Bond { + controller: "2EYbDVDVWiFbXZWJgqGDJsiH5MfNeLr5fxqH3tX84LQZaETG".into(), + value: Cow::Owned(U256::from(4000000u64).to_big_endian().to_vec()), // 4.0 POLYX + reward_destination: Proto::RewardDestination::STAKED.into(), + call_indices: None, + })), + staking_call(StakingVariant::nominate(Nominate { + nominators: vec!["2Gw8mSc4CUMxXMKEDqEsumQEXE5yTF8ACq2KdHGuigyXkwtz".into()], + call_indices: None, + })), + ], + )), + ..Default::default() + }; + + let preimage = helper_encode(CoinType::Polymesh, &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::Polymesh, 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 = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + 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, + }), + runtime_call: Some(batch_calls( + BatchKind::Atomic, + vec![ + staking_call(StakingVariant::chill(Chill { call_indices: None })), + staking_call(StakingVariant::unbond(Unbond { + value: Cow::Owned(U256::from(42000000u64).to_big_endian().to_vec()), // 42.0 POLYX + call_indices: None, + })), + ], + )), + ..Default::default() + }; + + let preimage = helper_encode(CoinType::Polymesh, &input); + + assert_eq!( + preimage, + "29020811061102027a030a150300006a995b00040000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063ab67744c78f1facfec9e517810a47ae23bc438315a01dac5ffee46beed5ad3d8"); +} + +// 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 = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + 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, + }), + runtime_call: Some(identity_add_auth_call(AddAuthorization { + target: "2HEVN4PHYKj7B1krQ9bctAQXZxHQQkANVNCcfbdYk2gZ4cBR".into(), + authorization: Some(Authorization { + auth_oneof: AuthVariant::join_identity(SecondaryKeyPermissions { + // No asset permissions. + asset: Some(AssetPermissions { + kind: RestrictionKind::These, + // Set empty "These". + assets: vec![], + }), + // No extrinsic permissions. + extrinsic: Some(ExtrinsicPermissions { + kind: RestrictionKind::These, + // Set empty "These". + pallets: vec![], + }), + // No portfolio permissions. + portfolio: Some(PortfolioPermissions { + kind: RestrictionKind::These, + // Set empty "These". + portfolios: vec![], + }), + }), + }), + // Old Polymesh v4.x call indices. + 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::Polymesh, 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 = GENESIS_HASH.decode_hex().unwrap(); + + let input = Proto::SigningInput { + network: 12, + 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, + }), + runtime_call: Some(identity_join_identity( + 21435, + // Old Polymesh v4.x call indices. + custom_call_indices(0x07, 0x05), + )), + ..Default::default() + }; + + let public_key = "d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054"; + let signature = "7f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006"; + let (_preimage, signed) = + helper_encode_and_compile(CoinType::Polymesh, input, signature, public_key, true); + assert_eq!(signed, "c5018400d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054007f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006750000000705bb53000000000000"); +} diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs index 1b8b9f19aac..933adbc67e0 100644 --- a/rust/tw_tests/tests/coin_address_derivation_test.rs +++ b/rust/tw_tests/tests/coin_address_derivation_test.rs @@ -157,6 +157,7 @@ fn test_coin_address_derivation() { CoinType::Polkadot => "12dyy3fArMPDXLsnRtapTqZsC2KCEimeqs1dop4AEERaKC6x", CoinType::Acala => "22WaYy5ChG8V5vvRVDP4ErE7esk8nZ4rjGYwxeVArnNT8dU3", CoinType::Kusama => "EDJV2jycw8fqTgiExLsDe6iUzbnM62hDk7u3BLm9wcYswkY", + CoinType::Polymesh => "2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv", // end_of_coin_address_derivation_tests_marker_do_not_modify _ => panic!("{:?} must be covered", coin), }; diff --git a/src/Coin.cpp b/src/Coin.cpp index 447c5c2f5a0..80e87baf92a 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -68,6 +68,7 @@ #include "NativeInjective/Entry.h" #include "BitcoinCash/Entry.h" #include "Pactus/Entry.h" +#include "Polymesh/Entry.h" // end_of_coin_includes_marker_do_not_modify using namespace TW; @@ -129,6 +130,7 @@ NativeEvmos::Entry NativeEvmosDP; NativeInjective::Entry NativeInjectiveDP; BitcoinCash::Entry BitcoinCashDP; Pactus::Entry PactusDP; +Polymesh::Entry PolymeshDP; // end_of_coin_dipatcher_declarations_marker_do_not_modify CoinEntry* coinDispatcher(TWCoinType coinType) { @@ -192,6 +194,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) { case TWBlockchainNativeInjective: entry = &NativeInjectiveDP; break; case TWBlockchainBitcoinCash: entry = &BitcoinCashDP; break; case TWBlockchainPactus: entry = &PactusDP; break; + case TWBlockchainPolymesh: entry = &PolymeshDP; break; // end_of_coin_dipatcher_switch_marker_do_not_modify default: entry = nullptr; break; diff --git a/src/Polymesh/Entry.h b/src/Polymesh/Entry.h new file mode 100644 index 00000000000..e32f339be46 --- /dev/null +++ b/src/Polymesh/Entry.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/RustCoinEntry.h" + +namespace TW::Polymesh { + +/// Entry point for Polymesh coin. +/// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file +class Entry : public Rust::RustCoinEntry { +}; + +} // namespace TW::Polymesh + diff --git a/src/proto/Polkadot.proto b/src/proto/Polkadot.proto index 719ccdddef5..59f8aa00ba4 100644 --- a/src/proto/Polkadot.proto +++ b/src/proto/Polkadot.proto @@ -218,60 +218,6 @@ message Staking { } } -// Identity module -message Identity { - // Identity::join_identity_as_key call - message JoinIdentityAsKey { - // call indices - CallIndices call_indices = 1; - - // auth id - uint64 auth_id = 2; - } - - // Identity::add_authorization call - message AddAuthorization { - message Data { - bytes data = 1; - } - - message AuthData { - // authorization data, empty means all permissions, null means no permissions - Data asset = 1; - - // authorization data, empty means all permissions, null means no permissions - Data extrinsic = 2; - - // authorization data, empty means all permissions, null means no permissions - Data portfolio = 3; - } - - // call indices - CallIndices call_indices = 1; - - // address that will be added to the Identity - string target = 2; - - // authorization data, null means all permissions - AuthData data = 3; - - // expire time, unix seconds - uint64 expiry = 4; - } - - oneof message_oneof { - JoinIdentityAsKey join_identity_as_key = 1; - AddAuthorization add_authorization = 2; - } -} - -// Polymesh call -message PolymeshCall { - oneof message_oneof { - Identity identity_call = 2; - } -} - // Input data necessary to create a signed transaction. message SigningInput { // Recent block hash, or genesis hash if era is not set @@ -308,7 +254,6 @@ message SigningInput { oneof message_oneof { Balance balance_call = 11; Staking staking_call = 12; - PolymeshCall polymesh_call = 13; } } @@ -322,4 +267,4 @@ message SigningOutput { // error code description string error_message = 3; -} +} \ No newline at end of file diff --git a/src/proto/Polymesh.proto b/src/proto/Polymesh.proto new file mode 100644 index 00000000000..54d1562a95d --- /dev/null +++ b/src/proto/Polymesh.proto @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +syntax = "proto3"; + +package TW.Polymesh.Proto; +option java_package = "wallet.core.jni.proto"; + +import "Common.proto"; + +// Destination options for reward +enum RewardDestination { + STAKED = 0; + STASH = 1; + CONTROLLER = 2; +} + +// An era, a period defined by a starting block and length +message Era { + // recent block number (called phase in polkadot code), should match block hash + uint64 block_number = 1; + + // length of period, calculated from block number, e.g. 64 + uint64 period = 2; +} + +// Readable decoded call indices can be found at https://www.subscan.io/ +message CustomCallIndices { + // Module index. + int32 module_index = 4; + + // Method index. + int32 method_index = 5; +} + +// Optional call indices. +// Must be set if `SigningInput::network` is different from `Polymesh`. +message CallIndices { + oneof variant { + CustomCallIndices custom = 1; + } +} + +// Balance transfer transaction +message Balance { + // transfer + message Transfer { + // destination address + string to_address = 1; + + // amount (uint256, serialized big endian) + bytes value = 2; + + // max 32 chars + string memo = 3; + + // call indices + CallIndices call_indices = 4; + } + + oneof message_oneof { + Transfer transfer = 1; + } +} + +// Staking transaction +message Staking { + // Bond to a controller + message Bond { + // controller ID (optional) + string controller = 1; + + // amount (uint256, serialized big endian) + bytes value = 2; + + // destination for rewards + RewardDestination reward_destination = 3; + + // call indices + CallIndices call_indices = 4; + } + + // Bond extra amount + message BondExtra { + // amount (uint256, serialized big endian) + bytes value = 1; + + // call indices + CallIndices call_indices = 2; + } + + // Unbond + message Unbond { + // amount (uint256, serialized big endian) + bytes value = 1; + + // call indices + CallIndices call_indices = 2; + } + + // Rebond + message Rebond { + // amount (uint256, serialized big endian) + bytes value = 1; + + // call indices + CallIndices call_indices = 2; + } + + // Withdraw unbonded amounts + message WithdrawUnbonded { + int32 slashing_spans = 1; + + // call indices + CallIndices call_indices = 2; + } + + // Nominate + message Nominate { + // list of nominators + repeated string nominators = 1; + + // call indices + CallIndices call_indices = 2; + } + + // Chill + message Chill { + // call indices + CallIndices call_indices = 1; + } + + // Payload messsage + oneof message_oneof { + Bond bond = 1; + BondExtra bond_extra = 2; + Unbond unbond = 3; + WithdrawUnbonded withdraw_unbonded = 4; + Nominate nominate = 5; + Chill chill = 6; + Rebond rebond = 7; + } +} + +message IdentityId { + // 32 byte identity id. + bytes id = 1; +} + +message AssetId { + // 16 byte asset id. + bytes id = 1; +} + +message PortfolioId { + // IdentityId of the portfolio owner. + IdentityId identity = 1; + // If `default` is true ignore the `user` field. + bool default = 2; + // The users portfolio number. (ignored if `default = true`) + uint64 user = 3; +} + +message SecondaryKeyPermissions { + enum RestrictionKind { + Whole = 0; + These = 1; + Except = 2; + } + + message AssetPermissions { + RestrictionKind kind = 1; + repeated AssetId assets = 2; + } + + message PortfolioPermissions { + RestrictionKind kind = 1; + repeated PortfolioId portfolios = 2; + } + + message PalletPermissions { + string pallet_name = 1; + RestrictionKind kind = 2; + repeated string extrinsic_names = 3; + } + + message ExtrinsicPermissions { + RestrictionKind kind = 1; + repeated PalletPermissions pallets = 2; + } + + // The assets permissions of the secondary key. + AssetPermissions asset = 1; + + // The pallet/extrinsic permissions of the secondary key. + ExtrinsicPermissions extrinsic = 2; + + // The portfolio permissions of the secondary key. + PortfolioPermissions portfolio = 3; +} + +// Identity module +message Identity { + // Identity::join_identity_as_key call + message JoinIdentityAsKey { + // call indices + CallIndices call_indices = 1; + + // auth id + uint64 auth_id = 2; + } + + // Identity::leave_identity_as_key call + message LeaveIdentityAsKey { + // call indices + CallIndices call_indices = 1; + } + + // Identity::add_authorization call + message AddAuthorization { + message Authorization { + // Authorization data. + oneof auth_oneof { + // AttestPrimaryKeyRotation(IdentityId) = 1 + // RotatePrimaryKey = 2 + // TransferTicker(Ticker) = 3 + // AddMultiSigSigner(AccountId) = 4 + // TransferAssetOwnership(AssetId) = 5 + SecondaryKeyPermissions join_identity = 6; + // PortfolioCustody(PortfolioId) = 7 + // BecomeAgent(AssetId, AgentGroup) = 8 + // AddRelayerPayingKey(AccountId, AccountId, Balance) = 9 + // RotatePrimaryKeyToSecondary(Permissions) = 10 + } + } + + // call indices + CallIndices call_indices = 1; + + // address that will be added to the Identity + string target = 2; + + // Authorization. + Authorization authorization = 3; + + // expire time, unix seconds + uint64 expiry = 4; + } + + oneof message_oneof { + JoinIdentityAsKey join_identity_as_key = 1; + AddAuthorization add_authorization = 2; + LeaveIdentityAsKey leave_identity_as_key = 3; + } +} + +// Utility pallet transaction +message Utility { + enum BatchKind { + // Batch multiple calls, stoping on the first error. + // + // Each call in the batch is executed in its own transaction. + // When one call fails only that transaction will be rolled back + // and any following calls in the batch will be skipped. + StopOnError = 0; + // Batch multiple calls and execute them in a single atomic transaction. + // The whole transaction will rollback if any of the calls fail. + Atomic = 1; + // Batch multiple calls. Unlike `Batch` this will continue even + // if one of the calls failed. + // + // Each call in the batch is executed in its own transaction. + // When a call fails its transaction will be rolled back and the error + // will be emitted in an event. + // + // Execution will continue until all calls in the batch have been executed. + Optimistic = 2; + } + + message Batch { + // The type of batch. + BatchKind kind = 1; + + // batched calls. + repeated RuntimeCall calls = 2; + + // call indices + CallIndices call_indices = 3; + } + + oneof message_oneof { + Batch batch = 1; + } +} + +// Polymesh runtime call. +message RuntimeCall { + // Top-level pallets. + oneof pallet_oneof { + Balance balance_call = 1; + Staking staking_call = 2; + Identity identity_call = 3; + Utility utility_call = 4; + } +} + +// Input data necessary to create a signed transaction. +message SigningInput { + // Recent block hash, or genesis hash if era is not set + bytes block_hash = 1; + + // Genesis block hash (identifies the chain) + bytes genesis_hash = 2; + + // Current account nonce + uint64 nonce = 3; + + // Specification version, e.g. 26. + uint32 spec_version = 4; + + // Transaction version, e.g. 5. + uint32 transaction_version = 5; + + // Optional tip to pay, big integer + bytes tip = 6; + + // Optional time validity limit, recommended, for replay-protection. Empty means Immortal. + Era era = 7; + + // The secret private key used for signing (32 bytes). + bytes private_key = 8; + + // Network type + uint32 network = 9; + + // Payload call + RuntimeCall runtime_call = 10; +} + +// Result containing the signed and encoded transaction. +message SigningOutput { + // Signed and encoded transaction bytes. + bytes encoded = 1; + + // error code, 0 is ok, other codes will be treated as errors + Common.Proto.SigningError error = 2; + + // error code description + string error_message = 3; +} \ No newline at end of file diff --git a/swift/Tests/Blockchains/PolymeshTests.swift b/swift/Tests/Blockchains/PolymeshTests.swift new file mode 100644 index 00000000000..ec4db4bcfd7 --- /dev/null +++ b/swift/Tests/Blockchains/PolymeshTests.swift @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import WalletCore +import XCTest + +class PolymeshTests: XCTestCase { + let genesisHash = Data(hexString: "0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063")! + // Private key for testing. DO NOT USE, since this is public. + let testKey1 = Data(hexString: "0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4")! + + func testAddress() { + let key = PrivateKey(data: Data(hexString: "0x790a0a01ec2e7c7db4abcaffc92ce70a960ef9ad3021dbe3bf327c1c6343aee4")!)! + let pubkey = key.getPublicKeyEd25519() + let address = AnyAddress(publicKey: pubkey, coin: .polymesh) + let addressFromString = AnyAddress(string: "2EANwBfNsFu9KV8JsW5sbhF6ft8bzvw5EW1LCrgHhrqtK6Ys", coin: .polymesh)! + + XCTAssertEqual(pubkey.data.hexString, "0x4bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c") + XCTAssertEqual(address.description, addressFromString.description) + } + + func testSignTransfer() { + // https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04 + + // Step 1: Prepare input. + let blockHash = Data(hexString: "77d32517dcc7b74501096afdcff3af72008a2c489e17083f56629d195e5c6a1d")! + + let input = PolymeshSigningInput.with { + $0.genesisHash = genesisHash + $0.blockHash = blockHash + $0.nonce = 1 + $0.specVersion = 7000005 + $0.network = CoinType.polymesh.ss58Prefix + $0.transactionVersion = 7 + $0.privateKey = testKey1 + $0.era = PolymeshEra.with { + $0.blockNumber = 16102106 + $0.period = 64 + } + $0.runtimeCall = PolymeshRuntimeCall.with { + $0.balanceCall = PolymeshBalance.Transfer.with { + $0.toAddress = "2CpqFh8VnwJAjenw4xSUWCaaJ2QwGdhnCikoSEczMhjgqyj7" + $0.value = Data(hexString: "0x0F4240")! // 1.0 POLYX + } + } + } + let output: PolymeshSigningOutput = AnySigner.sign(input: input, coin: .polkadot) + + XCTAssertEqual(output.encoded.hexString, "390284004bdb9ef424035e1621e228bd11c5917d7d1dac5965d244c4c72fc91170244f0c00e9b4742a2b66931e0cf29f6811e4d44545b4f278a667b9eb1217c4b2de8763c8037e4501dd4a21179b737beb33415f458788f2d1093b527cae8bee8b2d55210ba501040005000010b713ceeb165c1ac7c450f5b138a6da0eba50bb18849f5b8e83985daa45a87e02093d00") + } +} diff --git a/swift/Tests/CoinAddressDerivationTests.swift b/swift/Tests/CoinAddressDerivationTests.swift index 72c611f4083..8d57ad595d8 100644 --- a/swift/Tests/CoinAddressDerivationTests.swift +++ b/swift/Tests/CoinAddressDerivationTests.swift @@ -203,6 +203,9 @@ class CoinAddressDerivationTests: XCTestCase { case .polkadot: let expectedResult = "13nN6BGAoJwd7Nw1XxeBCx5YcBXuYnL94Mh7i3xBprqVSsFk" assertCoinDerivation(coin, expectedResult, derivedAddress, address) + case .polymesh: + let expectedResult = "2DHK8VhBpacs9quk78AVP9TmmcG5iXi2oKtZqneSNsVXxCKw" + assertCoinDerivation(coin, expectedResult, derivedAddress, address) case .qtum: let expectedResult = "QhceuaTdeCZtcxmVc6yyEDEJ7Riu5gWFoF" assertCoinDerivation(coin, expectedResult, derivedAddress, address) diff --git a/tests/chains/Polkadot/TWAnyAddressTests.cpp b/tests/chains/Polkadot/TWAnyAddressTests.cpp index 5b57279f2e3..2a18ba00c41 100644 --- a/tests/chains/Polkadot/TWAnyAddressTests.cpp +++ b/tests/chains/Polkadot/TWAnyAddressTests.cpp @@ -18,7 +18,6 @@ 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) { @@ -40,10 +39,6 @@ TEST(PolkadotAddress, Validation) { 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) { @@ -85,15 +80,6 @@ TEST(PolkadotAddress, FromPublicKeyWithPrefix) { 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) { @@ -117,14 +103,6 @@ TEST(PolkadotAddress, FromStringWithPrefix) { 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 index 8cff6ef551b..7c6328d4d98 100644 --- a/tests/chains/Polkadot/TWAnySignerTests.cpp +++ b/tests/chains/Polkadot/TWAnySignerTests.cpp @@ -24,7 +24,6 @@ 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")); diff --git a/tests/chains/Polymesh/TWAnyAddressTests.cpp b/tests/chains/Polymesh/TWAnyAddressTests.cpp new file mode 100644 index 00000000000..1602ff3e1a0 --- /dev/null +++ b/tests/chains/Polymesh/TWAnyAddressTests.cpp @@ -0,0 +1,100 @@ +// 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 + +using namespace TW; + +namespace TW::Polymesh::tests { +extern uint32_t polymeshPrefix; + +TEST(TWPolymesh, Address) { + auto string = STRING("2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv"); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypePolymesh)); + auto string2 = WRAPS(TWAnyAddressDescription(addr.get())); + EXPECT_TRUE(TWStringEqual(string.get(), string2.get())); + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "4870d56d074c50e891506d78faa4fb69ca039cc5f131eb491e166b975880e867"); +} + +TEST(PolymeshAddress, Validation) { + // Substrate ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("5FqqU2rytGPhcwQosKRtW1E3ha6BJKAjHgtcodh71dSyXhoZ").get(), TWCoinTypePolymesh)); + // Bitcoin + ASSERT_FALSE(TWAnyAddressIsValid(STRING("1ES14c7qLb5CYhLMUekctxLgc1FV2Ti9DA").get(), TWCoinTypePolymesh)); + // Kusama ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("FHKAe66mnbk8ke8zVWE9hFVFrJN1mprFPVmD5rrevotkcDZ").get(), TWCoinTypePolymesh)); + // Kusama secp256k1 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("FxQFyTorsjVsjjMyjdgq8w5vGx8LiA1qhWbRYcFijxKKchx").get(), TWCoinTypePolymesh)); + // Kusama sr25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("EJ5UJ12GShfh7EWrcNZFLiYU79oogdtXFUuDDZzk7Wb2vCe").get(), TWCoinTypePolymesh)); + + // Polkadot ed25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("15KRsCq9LLNmCxNFhGk55s5bEyazKefunDxUH24GFZwsTxyu").get(), TWCoinTypePolymesh)); + // Polkadot sr25519 + ASSERT_FALSE(TWAnyAddressIsValid(STRING("15AeCjMpcSt3Fwa47jJBd7JzQ395Kr2cuyF5Zp4UBf1g9ony").get(), TWCoinTypePolymesh)); + + // Polymesh + ASSERT_TRUE(TWAnyAddressIsValid(STRING("2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG").get(), TWCoinTypePolymesh)); + ASSERT_FALSE(TWAnyAddressIsValid(STRING("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A").get(), TWCoinTypePolymesh)); + ASSERT_TRUE(TWAnyAddressIsValidSS58(STRING("2DxwekgWwK7sqVeuXGmaXLZUvwnewLTs2rvU2CFKLgvvYwCG").get(), TWCoinTypePolymesh, polymeshPrefix)); + ASSERT_FALSE(TWAnyAddressIsValidSS58(STRING("JCViCkwMdGWKpf7Wogb8EFtDmaYTEZGEg6ah4svUPGnnpc7A").get(), TWCoinTypePolymesh, polymeshPrefix)); +} + +TEST(PolymeshAddress, 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(), TWCoinTypePolymesh)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypePolymesh)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("2GmLy7KywpsV5fDpZfJMcgGgzoJWyrEA3Wc3fDmsoq5iqtBT").get())); +} + +TEST(PolymeshAddress, FromPublicKey) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("0xbeff0e5d6f6e6e6d573d3044f3e2bfb353400375dc281da3337468d4aa527908").get(), TWPublicKeyTypeED25519)); + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKey(publicKey.get(), TWCoinTypePolymesh)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), STRING("2GmLy7KywpsV5fDpZfJMcgGgzoJWyrEA3Wc3fDmsoq5iqtBT").get())); +} + +TEST(PolymeshAddress, FromPublicKeyWithPrefix) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f83").get(), TWPublicKeyTypeED25519)); + const auto addressPolymesh = STRING("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58WithPublicKey(publicKey.get(), TWCoinTypePolymesh, polymeshPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressPolymesh.get())); + } +} + +TEST(PolymeshAddress, FromString) { + auto string = STRING("2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv"); + auto addr = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(string.get(), TWCoinTypePolymesh)); + auto string2 = WRAPS(TWAnyAddressDescription(addr.get())); + EXPECT_TRUE(TWStringEqual(string.get(), string2.get())); + auto keyHash = WRAPD(TWAnyAddressData(addr.get())); + assertHexEqual(keyHash, "4870d56d074c50e891506d78faa4fb69ca039cc5f131eb491e166b975880e867"); +} + +TEST(PolymeshAddress, FromStringWithPrefix) { + // polymesh + auto addressPolymesh = STRING("2FSoQykVV3uWe5ChZuazMDHBoaZmCPPuoYx5KHL5VqXooDQW"); + { + const auto address = WRAP(TWAnyAddress, TWAnyAddressCreateSS58(addressPolymesh.get(), TWCoinTypePolymesh, polymeshPrefix)); + const auto addressStr = WRAPS(TWAnyAddressDescription(address.get())); + EXPECT_TRUE(TWStringEqual(addressStr.get(), addressPolymesh.get())); + } +} + +} // namespace TW::Polymesh::tests \ No newline at end of file diff --git a/tests/chains/Polymesh/TWAnySignerTests.cpp b/tests/chains/Polymesh/TWAnySignerTests.cpp new file mode 100644 index 00000000000..8fc6cdbde4b --- /dev/null +++ b/tests/chains/Polymesh/TWAnySignerTests.cpp @@ -0,0 +1,181 @@ +// 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/Polymesh.pb.h" +#include "proto/TransactionCompiler.pb.h" +#include "uint256.h" + +#include +#include +#include +#include +#include + +#include "TestUtilities.h" +#include +#include + +using namespace TW; +using namespace TW::Polymesh; + +namespace TW::Polymesh::tests { +auto polymeshPrefix = 12; + +Data helper_encodeTransaction(TWCoinType coin, const Proto::SigningInput& input, const Data& pubKey, const Data& signature) { + auto txInputData = data(input.SerializeAsString()); + auto txInputDataPtr = WRAPD(TWDataCreateWithBytes(txInputData.data(), txInputData.size())); + + const auto outputData = WRAPD(TWTransactionCompilerCompileWithSignatures( + coin, txInputDataPtr.get(), + WRAP(TWDataVector, TWDataVectorCreateWithData((TWData*)&signature)).get(), + WRAP(TWDataVector, TWDataVectorCreateWithData((TWData*)&pubKey)).get())); + + Polymesh::Proto::SigningOutput output; + output.ParseFromArray(TWDataBytes(outputData.get()), + (int)TWDataSize(outputData.get())); + EXPECT_EQ(output.error(), Common::Proto::OK); + + return data(output.encoded()); +} + +TEST(TWAnySignerPolymesh, PolymeshEncodeAndSign) { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x9a4283cc38f7e769c53ad2d1c5cf292fc85a740ec1c1aa80c180847e51928650 + + /// Step 1: Prepare transaction input (protobuf) + const auto coin = TWCoinTypePolymesh; + + Polymesh::Proto::SigningInput input; + input.set_network(12); + 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_runtime_call()->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); + + /// Step 2: Obtain preimage hash + 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 preImage = data(preSigningOutput.data()); + + ASSERT_EQ(hex(preImage), "050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f2050414444454420574954482053504143455300000000000000000025010400c20b0000020000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063898bba6413c38f79a284aec8749f297f6c8734c501f67517b5a6aadc338d1102"); + + auto pubKey = parse_hex("4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"); + auto signature = parse_hex("0791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f"); + + /// Step 3: Compile transaction info + const auto outputData = WRAPD(TWTransactionCompilerCompileWithSignatures( + coin, txInputDataPtr.get(), + WRAP(TWDataVector, TWDataVectorCreateWithData((TWData*)&signature)).get(), + WRAP(TWDataVector, TWDataVectorCreateWithData((TWData*)&pubKey)).get())); + + const auto ExpectedTx = + "bd0284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee000791ee378775eaff34ef7e529ab742f0d81d281fdf20ace0aa765ca484f5909c4eea0a59c8dbbc534c832704924b424ba3230c38acd0ad5360cef023ca2a420f25010400050100849e2f6b165d4b28b39ef3d98f86c0520d82bc349536324365c10af08f323f8302093d00014d454d4f20504144444544205749544820535041434553000000000000000000"; + { + Polymesh::Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(TWDataBytes(outputData.get()), + (int)TWDataSize(outputData.get()))); + + EXPECT_EQ(hex(output.encoded()), ExpectedTx); + } +} + +TEST(TWAnySignerPolymesh, encodeTransaction_Add_authorization) { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x7d9b9109027b36b72d37ba0648cb70e5254524d3d6752cc6b41601f4bdfb1af0 + + Polymesh::Proto::SigningInput input; + input.set_network(12); + 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_runtime_call()->mutable_identity_call()->mutable_add_authorization(); + addAuthorization->set_target("2HEVN4PHYKj7B1krQ9bctAQXZxHQQkANVNCcfbdYk2gZ4cBR"); + auto* keyPerms = addAuthorization->mutable_authorization()->mutable_join_identity(); + // Set empty "These". + auto* assets = keyPerms->mutable_asset(); + assets->set_kind(Polymesh::Proto::SecondaryKeyPermissions_RestrictionKind_These); + auto* extrinsics = keyPerms->mutable_extrinsic(); + extrinsics->set_kind(Polymesh::Proto::SecondaryKeyPermissions_RestrictionKind_These); + auto* portfolios = keyPerms->mutable_portfolio(); + portfolios->set_kind(Polymesh::Proto::SecondaryKeyPermissions_RestrictionKind_These); + + auto* callIndices = addAuthorization->mutable_call_indices()->mutable_custom(); + callIndices->set_module_index(0x07); + callIndices->set_method_index(0x0d); + + auto pubKey = parse_hex("4322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee"); + auto signature = parse_hex("81e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01"); + auto encoded = helper_encodeTransaction(TWCoinTypePolymesh, input, pubKey, signature); + ASSERT_EQ(hex(encoded), "490284004322cf71da08f9d56181a707af7c0c437dfcb93e6caac9825a5aba57548142ee0081e6561e4391862b5da961d7033baced1c4b25f0e27f938b02321af1118e0b859e1c2bd5607576a258f2c2befbc5f397ea4adb62938f30eb73c8060ab0eabf01b5031400070d01d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d0610540501000100010000"); +} + +TEST(TWAnySignerPolymesh, encodeTransaction_JoinIdentityAsKey) { + // tx on mainnet + // https://polymesh.subscan.io/extrinsic/0x9d7297d8b38af5668861996cb115f321ed681989e87024fda64eae748c2dc542 + + Polymesh::Proto::SigningInput input; + input.set_network(12); + 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_runtime_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 pubKey = parse_hex("d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054"); + auto signature = parse_hex("7f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006"); + auto encoded = helper_encodeTransaction(TWCoinTypePolymesh, input, pubKey, signature); + ASSERT_EQ(hex(encoded), "c5018400d3b2f1c41b9b4522eb3e23329b81aca6cc0231167ecfa3580c5a71ff6d061054007f5adbb2749e2f0ace29b409c41dd717681495b1f22dc5358311646a9fb8af8a173fc47f1b19748fb56831c2128773e2976986685adee83c741ab49934d80006750000000705bb53000000000000"); +} + +} // namespace TW::Polymesh::tests \ No newline at end of file diff --git a/tests/chains/Polymesh/TWCoinTypeTests.cpp b/tests/chains/Polymesh/TWCoinTypeTests.cpp new file mode 100644 index 00000000000..4271fff9bbe --- /dev/null +++ b/tests/chains/Polymesh/TWCoinTypeTests.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include +#include + +TEST(TWPolymeshCoinType, TWCoinType) { + const auto coin = TWCoinTypePolymesh; + const auto symbol = WRAPS(TWCoinTypeConfigurationGetSymbol(coin)); + const auto id = WRAPS(TWCoinTypeConfigurationGetID(coin)); + const auto name = WRAPS(TWCoinTypeConfigurationGetName(coin)); + const auto txId = WRAPS(TWStringCreateWithUTF8Bytes("0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04")); + const auto txUrl = WRAPS(TWCoinTypeConfigurationGetTransactionURL(coin, txId.get())); + const auto accId = WRAPS(TWStringCreateWithUTF8Bytes("2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv")); + const auto accUrl = WRAPS(TWCoinTypeConfigurationGetAccountURL(coin, accId.get())); + + assertStringsEqual(id, "polymesh"); + assertStringsEqual(name, "Polymesh"); + assertStringsEqual(symbol, "POLYX"); + ASSERT_EQ(TWCoinTypeConfigurationGetDecimals(coin), 6); + ASSERT_EQ(TWCoinTypeBlockchain(coin), TWBlockchainPolymesh); + ASSERT_EQ(TWCoinTypeP2pkhPrefix(coin), 0); + ASSERT_EQ(TWCoinTypeP2shPrefix(coin), 0); + ASSERT_EQ(TWCoinTypeStaticPrefix(coin), 0); + assertStringsEqual(txUrl, "https://polymesh.subscan.io/extrinsic/0x98cb5e33d8ff3dd5838c384e2ef9e291314ed8db13f5d4f42cdd70bad54a5e04"); + assertStringsEqual(accUrl, "https://polymesh.subscan.io/account/2E5u4xA1TqswQ3jMJH96zekxwr2itvKu79fDC1mmnVZRh6Uv"); +} diff --git a/tests/common/CoinAddressDerivationTests.cpp b/tests/common/CoinAddressDerivationTests.cpp index 6df5336c5c0..489265a1eec 100644 --- a/tests/common/CoinAddressDerivationTests.cpp +++ b/tests/common/CoinAddressDerivationTests.cpp @@ -399,6 +399,9 @@ TEST(Coin, DeriveAddress) { case TWCoinTypePactus: EXPECT_EQ(address, "pc1rehvlc6tfn79z0zjqqaj8zas5j5h9c2fe59a4ff"); break; + case TWCoinTypePolymesh: + EXPECT_EQ(address, "2HqjMm2goapWvXQBqjjEdVaTZsUmunWwEq1TSToDR1pDzQ1F"); + break; // end_of_coin_address_derivation_tests_marker_do_not_modify // no default branch here, intentionally, to better notice any missing coins }