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
}