Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 35 additions & 119 deletions packages/wasm-dot/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@
//!
//! Parses raw extrinsic bytes into structured data.
//!
//! Supports two modes of pallet/method resolution:
//! - **Dynamic (metadata-based)**: Uses runtime metadata to resolve pallet and call names
//! from their indices. This is the preferred approach as it handles runtime upgrades
//! and chain-specific index differences automatically.
//! - **Hardcoded fallback**: Uses a static mapping of known pallet/method indices.
//! Used when no metadata is provided in the parsing context.
//! Uses runtime metadata to resolve pallet and call names from their indices.
//! Metadata is required (enforced at the TypeScript level in `fromHex`/`fromBytes`).

use crate::address::encode_ss58;
use crate::error::WasmDotError;
Expand Down Expand Up @@ -148,44 +144,6 @@ fn resolve_call_from_metadata(
Some((pallet_name, call_name))
}

/// Resolve pallet and call names using the hardcoded static mapping.
///
/// This is the fallback when no metadata is available. It covers known
/// pallet indices for Polkadot, Kusama, and Westend.
fn resolve_call_hardcoded(pallet_index: u8, method_index: u8) -> (&'static str, &'static str) {
match (pallet_index, method_index) {
// Balances pallet
// Polkadot: 5, Kusama: 4, Westend: 10
(4, 0) | (5, 0) | (10, 0) => ("balances", "transfer"),
(4, 3) | (5, 3) | (10, 3) => ("balances", "transferKeepAlive"),
(4, 4) | (5, 4) | (10, 4) => ("balances", "transferAll"),

// Staking pallet
// Polkadot: 7, Kusama: 6, Westend: 8
(6, 0) | (7, 0) | (8, 0) => ("staking", "bond"),
(6, 1) | (7, 1) | (8, 1) => ("staking", "bondExtra"),
(6, 2) | (7, 2) | (8, 2) => ("staking", "unbond"),
(6, 3) | (7, 3) | (8, 3) => ("staking", "withdrawUnbonded"),
(6, 6) | (7, 6) | (8, 6) => ("staking", "chill"),
(6, 18) | (7, 18) | (8, 18) => ("staking", "payoutStakers"),

// Proxy pallet
// Polkadot: 29, Kusama: 29, Westend: 30
(29, 0) | (30, 0) => ("proxy", "proxy"),
(29, 1) | (30, 1) => ("proxy", "addProxy"),
(29, 2) | (30, 2) => ("proxy", "removeProxy"),
(29, 4) | (30, 4) => ("proxy", "createPure"),

// Utility pallet
// Polkadot: 26, Kusama: 24, Westend: 16
(16, 0) | (24, 0) | (26, 0) => ("utility", "batch"),
(16, 2) | (24, 2) | (26, 2) => ("utility", "batchAll"),

// Unknown
_ => ("unknown", "unknown"),
}
}

/// Convert snake_case to camelCase.
///
/// Examples:
Expand Down Expand Up @@ -213,8 +171,8 @@ fn snake_to_camel(s: &str) -> String {

/// Parse call data into method info.
///
/// When metadata is provided, uses dynamic resolution via `pallet_by_index()` and
/// `call_variant_by_index()`. Falls back to hardcoded mapping otherwise.
/// Uses metadata for dynamic resolution via `pallet_by_index()` and
/// `call_variant_by_index()`. Metadata is required.
fn parse_call_data(
call_data: &[u8],
address_prefix: u16,
Expand Down Expand Up @@ -247,16 +205,19 @@ fn parse_call_data_with_size(
let method_index = call_data[1];
let args_data = &call_data[2..];

// Resolve pallet and method names: prefer metadata, fall back to hardcoded
let (pallet, name) = if let Some(md) = metadata {
resolve_call_from_metadata(md, pallet_index, method_index).unwrap_or_else(|| {
let (p, n) = resolve_call_hardcoded(pallet_index, method_index);
(p.to_string(), n.to_string())
})
} else {
let (p, n) = resolve_call_hardcoded(pallet_index, method_index);
(p.to_string(), n.to_string())
};
// Resolve pallet and method names from metadata (required)
let md = metadata.ok_or_else(|| {
WasmDotError::InvalidTransaction(
"Metadata required to resolve pallet/method names".to_string(),
)
})?;
let (pallet, name) =
resolve_call_from_metadata(md, pallet_index, method_index).ok_or_else(|| {
WasmDotError::InvalidTransaction(format!(
"Unknown pallet index {} method index {} in metadata",
pallet_index, method_index
))
})?;

// Parse args based on method, getting bytes consumed
let (args, args_consumed) =
Expand Down Expand Up @@ -465,7 +426,7 @@ fn parse_proxy_args(
));
}

let proxy_type = resolve_proxy_type(args[cursor], metadata);
let proxy_type = resolve_proxy_type(args[cursor], metadata)?;
cursor += 1;

let delay = u32::from_le_bytes([
Expand Down Expand Up @@ -496,7 +457,7 @@ fn parse_create_pure_args(
"truncated createPure args".to_string(),
));
}
let proxy_type = resolve_proxy_type(args[0], metadata);
let proxy_type = resolve_proxy_type(args[0], metadata)?;
let delay = u32::from_le_bytes([args[1], args[2], args[3], args[4]]);
let index = u16::from_le_bytes([args[5], args[6]]);

Expand Down Expand Up @@ -563,7 +524,7 @@ fn parse_proxy_proxy_args(
"truncated proxy type".to_string(),
));
}
let pt = resolve_proxy_type(args[cursor], metadata);
let pt = resolve_proxy_type(args[cursor], metadata)?;
cursor += 1;
Some(pt)
} else {
Expand Down Expand Up @@ -616,28 +577,20 @@ fn parse_multi_address(args: &[u8], address_prefix: u16) -> Result<(String, usiz
}
}

/// Resolve proxy type name from metadata, falling back to hardcoded Polkadot mainnet mapping.
/// Resolve proxy type name from metadata. Metadata is required.
fn resolve_proxy_type(
proxy_type_byte: u8,
metadata: Option<&subxt_core::metadata::Metadata>,
) -> String {
if let Some(md) = metadata {
if let Some(name) = resolve_proxy_type_from_metadata(md, proxy_type_byte) {
return name;
}
}
// Fallback: Polkadot mainnet proxy type indices
match proxy_type_byte {
0 => "Any".to_string(),
1 => "NonTransfer".to_string(),
2 => "Governance".to_string(),
3 => "Staking".to_string(),
4 => "IdentityJudgement".to_string(),
5 => "CancelProxy".to_string(),
6 => "Auction".to_string(),
7 => "NominationPools".to_string(),
_ => format!("Unknown({})", proxy_type_byte),
}
) -> Result<String, WasmDotError> {
let md = metadata.ok_or_else(|| {
WasmDotError::InvalidTransaction("Metadata required to resolve proxy type".to_string())
})?;
resolve_proxy_type_from_metadata(md, proxy_type_byte).ok_or_else(|| {
WasmDotError::InvalidTransaction(format!(
"Unknown proxy type index {} in metadata",
proxy_type_byte
))
})
}

/// Look up the ProxyType enum variant name from chain metadata.
Expand Down Expand Up @@ -764,47 +717,10 @@ mod tests {
}

#[test]
fn test_resolve_hardcoded_polkadot_balances() {
assert_eq!(
resolve_call_hardcoded(5, 3),
("balances", "transferKeepAlive")
);
}

#[test]
fn test_resolve_hardcoded_kusama_balances() {
assert_eq!(
resolve_call_hardcoded(4, 3),
("balances", "transferKeepAlive")
);
}

#[test]
fn test_resolve_hardcoded_westend_balances() {
assert_eq!(
resolve_call_hardcoded(10, 3),
("balances", "transferKeepAlive")
);
}

#[test]
fn test_resolve_hardcoded_unknown() {
assert_eq!(resolve_call_hardcoded(255, 255), ("unknown", "unknown"));
}

#[test]
fn test_parse_call_data_without_metadata() {
// Polkadot balances::transferKeepAlive (pallet=5, method=3) with a dummy address + amount
let mut call_data = vec![5u8, 3u8]; // pallet=5, method=3
call_data.push(0x00); // MultiAddress::Id variant
call_data.extend_from_slice(&[0u8; 32]); // dummy pubkey
call_data.push(0x04); // compact amount = 1

let result = parse_call_data(&call_data, 42, None).unwrap();
assert_eq!(result.pallet, "balances");
assert_eq!(result.name, "transferKeepAlive");
assert_eq!(result.pallet_index, 5);
assert_eq!(result.method_index, 3);
fn test_parse_call_data_without_metadata_returns_error() {
let call_data = vec![5u8, 3u8, 0x00];
let result = parse_call_data(&call_data, 42, None);
assert!(result.is_err());
}

#[test]
Expand Down
35 changes: 7 additions & 28 deletions packages/wasm-dot/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,40 +674,19 @@ fn parse_signing_payload(

/// Parse signed extensions from extrinsic bytes.
///
/// When metadata is available, iterates the runtime's signed extension list
/// and decodes each extension by its type ID. This handles runtimes with
/// extra extensions like CheckMetadataHash or ChargeAssetTxPayment.
///
/// Without metadata, falls back to the hardcoded layout: era + nonce + tip.
/// Iterates the runtime's signed extension list from metadata and decodes
/// each extension by its type ID. This handles runtimes with extra extensions
/// like CheckMetadataHash or ChargeAssetTxPayment. Metadata is required.
///
/// Returns (era, nonce, tip, bytes_consumed).
fn parse_signed_extensions(
bytes: &[u8],
metadata: Option<&Metadata>,
) -> Result<(Era, u32, u128, usize), WasmDotError> {
use parity_scale_codec::{Compact, Decode};

if let Some(md) = metadata {
parse_signed_extensions_from_metadata(bytes, md)
} else {
// Fallback: hardcoded era + Compact<u32> nonce + Compact<u128> tip
let mut cursor = 0;

let (era, era_size) = decode_era_bytes(&bytes[cursor..])?;
cursor += era_size;

let mut input = &bytes[cursor..];
let nonce = <Compact<u32>>::decode(&mut input)
.map_err(|e| WasmDotError::InvalidTransaction(format!("Invalid nonce: {}", e)))?;
cursor = bytes.len() - input.len();

let mut input = &bytes[cursor..];
let tip = <Compact<u128>>::decode(&mut input)
.map_err(|e| WasmDotError::InvalidTransaction(format!("Invalid tip: {}", e)))?;
cursor = bytes.len() - input.len();

Ok((era, nonce.0, tip.0, cursor))
}
let md = metadata.ok_or_else(|| {
WasmDotError::InvalidTransaction("Metadata required to parse signed extensions".to_string())
})?;
parse_signed_extensions_from_metadata(bytes, md)
}

/// Parse signed extensions using metadata to determine the layout.
Expand Down
Loading