Skip to content

Commit 2d3cd3f

Browse files
feat: implement runtime ABI JSON loading and parameter decoding
Wires up complete end-to-end support for loading custom ABI JSON files at runtime and decoding transaction parameters: Core changes: - Add `abi_registry` field to `VisualSignOptions` for passing registries through both CLI and gRPC pipelines - Implement parameter decoding in AbiDecoder for common Solidity types: * uint256 and other uint types (decoded as decimal) * address (decoded as checksummed hex) * address[] (dynamic arrays decoded with proper offset handling) - Update Ethereum converter to extract AbiRegistry from options and use DynamicAbiVisualizer for unknown contracts - Add `load_and_map_abi()` helper to combine file loading and address mapping CLI changes: - Rename `--abi` to `--abi-json-mappings` with format: `AbiName:/path/to/file.json:0xAddress` - Build AbiRegistry from file-based mappings and pass through options to visualizer Verified with SushiSwapRouter example - all parameters decode correctly matching Etherscan.
1 parent b9b6670 commit 2d3cd3f

7 files changed

Lines changed: 233 additions & 37 deletions

File tree

src/chain_parsers/visualsign-ethereum/examples/using_abijson/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ To enable visualization for your custom contract:
4242
cargo run --release -- \
4343
--chain ethereum \
4444
--transaction 0x... \
45-
--abi SimpleToken:0x1234567890123456789012345678901234567890
45+
--abi-json-mappings SimpleToken:0x1234567890123456789012345678901234567890
4646
```
4747

4848
5. **Or via Rust code** in your application
@@ -60,7 +60,7 @@ cargo run --example using_abijson -- --help
6060
cargo run --example using_abijson -- \
6161
--chain ethereum \
6262
--transaction 0x... \
63-
--abi SimpleToken:0x<contract_address>
63+
--abi-json-mappings SimpleToken:0x<contract_address>
6464
```
6565

6666
#### Via Rust Code
@@ -117,24 +117,24 @@ mint(address,uint256)
117117

118118
## CLI Integration
119119

120-
The parser CLI now supports the `--abi` flag for mapping custom ABIs to contract addresses:
120+
The parser CLI now supports the `--abi-json-mappings` flag for mapping custom ABI JSON files to contract addresses:
121121

122122
### Format
123123

124124
```
125-
--abi AbiName:0xAddress
125+
--abi-json-mappings AbiName:0xAddress
126126
```
127127

128128
### Multiple Mappings
129129

130-
You can provide multiple `--abi` flags to register different ABIs:
130+
You can provide multiple `--abi-json-mappings` flags to register different ABIs:
131131

132132
```bash
133133
cargo run --release -- \
134134
--chain ethereum \
135135
--transaction 0x... \
136-
--abi Token:0x1111111111111111111111111111111111111111 \
137-
--abi Router:0x2222222222222222222222222222222222222222
136+
--abi-json-mappings Token:0x1111111111111111111111111111111111111111 \
137+
--abi-json-mappings Router:0x2222222222222222222222222222222222222222
138138
```
139139

140140
### Validation
@@ -151,7 +151,7 @@ The CLI validates each ABI mapping and reports:
151151
- ✅ Function selector matching (4-byte opcodes)
152152
- ✅ Structured PreviewLayout visualization
153153
- ✅ Multiple ABIs per binary
154-
- ✅ CLI `--abi` flag for address mapping
154+
- ✅ CLI `--abi-json-mappings` flag for address mapping
155155
- ✅ Optional ABI signatures (secp256k1) for validation (planned)
156156

157157
## Limitations

src/chain_parsers/visualsign-ethereum/src/abi_decoder.rs

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use std::sync::Arc;
77

88
use alloy_json_abi::{Function, JsonAbi};
9+
use alloy_primitives::U256;
910

1011
use visualsign::{
1112
AnnotatedPayloadField, SignablePayloadField, SignablePayloadFieldCommon,
@@ -14,6 +15,80 @@ use visualsign::{
1415

1516
use crate::registry::ContractRegistry;
1617

18+
/// Decodes a single Solidity value from calldata
19+
/// Simple implementation that handles common types
20+
fn decode_solidity_value(ty: &str, data: &[u8], offset: &mut usize) -> String {
21+
if ty == "address" {
22+
// Addresses are 32 bytes (20 bytes address padded to 32)
23+
if *offset + 32 <= data.len() {
24+
let bytes = &data[*offset..*offset + 32];
25+
let addr_bytes = &bytes[12..32]; // Take last 20 bytes
26+
*offset += 32;
27+
return format!("0x{}", hex::encode(addr_bytes));
28+
}
29+
} else if ty == "uint256" || ty == "uint" {
30+
// uint256 is 32 bytes
31+
if *offset + 32 <= data.len() {
32+
let bytes = &data[*offset..*offset + 32];
33+
let val = U256::from_be_bytes(bytes.try_into().unwrap_or([0; 32]));
34+
*offset += 32;
35+
return val.to_string();
36+
}
37+
} else if ty.starts_with("uint") {
38+
// Other uint types - still 32 bytes in encoding
39+
if *offset + 32 <= data.len() {
40+
let bytes = &data[*offset..*offset + 32];
41+
let val = U256::from_be_bytes(bytes.try_into().unwrap_or([0; 32]));
42+
*offset += 32;
43+
return val.to_string();
44+
}
45+
} else if ty == "address[]" {
46+
// Dynamic address arrays - offset points to location of array
47+
if *offset + 32 <= data.len() {
48+
let array_offset = U256::from_be_bytes(data[*offset..*offset + 32].try_into().unwrap_or([0; 32]));
49+
*offset += 32;
50+
51+
// Read array length at the offset
52+
let array_offset_usize = array_offset.try_into().unwrap_or(0usize);
53+
if array_offset_usize + 32 <= data.len() {
54+
let array_len_val = U256::from_be_bytes(data[array_offset_usize..array_offset_usize + 32].try_into().unwrap_or([0; 32]));
55+
let array_len: usize = array_len_val.try_into().unwrap_or(0);
56+
let mut addresses = Vec::new();
57+
58+
for i in 0..array_len {
59+
let addr_offset_val: usize = (U256::from(array_offset_usize) + U256::from(32) + U256::from(i * 32)).try_into().unwrap_or(0);
60+
if addr_offset_val + 32 <= data.len() {
61+
let addr_bytes = &data[addr_offset_val + 12..addr_offset_val + 32]; // Take last 20 bytes
62+
addresses.push(format!("0x{}", hex::encode(addr_bytes)));
63+
}
64+
}
65+
66+
if addresses.is_empty() {
67+
return "[]".to_string();
68+
} else {
69+
return format!("[{}]", addresses.join(", "));
70+
}
71+
}
72+
}
73+
} else if ty.ends_with("[]") {
74+
// Other dynamic arrays - just show offset for now
75+
if *offset + 32 <= data.len() {
76+
let array_offset_val = U256::from_be_bytes(data[*offset..*offset + 32].try_into().unwrap_or([0; 32]));
77+
*offset += 32;
78+
return format!("(dynamic array at offset {})", array_offset_val);
79+
}
80+
}
81+
82+
// Fallback for unknown types
83+
if *offset + 32 <= data.len() {
84+
let hex_val = hex::encode(&data[*offset..(*offset + 32).min(data.len())]);
85+
*offset = (*offset + 32).min(data.len());
86+
format!("{}: 0x{}", ty, hex_val)
87+
} else {
88+
format!("{}: (insufficient data)", ty)
89+
}
90+
}
91+
1792
/// Decodes function calls using a JSON ABI
1893
pub struct AbiDecoder {
1994
abi: Arc<JsonAbi>,
@@ -77,16 +152,19 @@ impl AbiDecoder {
77152

78153
let input_data = &calldata[4..];
79154

80-
// Build field for each input parameter (showing parameter names and types for now)
81155
let mut expanded_fields = Vec::new();
156+
let mut offset = 0;
157+
158+
// Build field for each input parameter
82159
for (i, input) in function.inputs.iter().enumerate() {
83160
let param_name = if !input.name.is_empty() {
84161
input.name.clone()
85162
} else {
86163
format!("param{}", i)
87164
};
88165

89-
let formatted = format!("{} ({})", input.ty, hex::encode(&input_data[..(8.min(input_data.len()))]));
166+
// Simple decoding based on type
167+
let formatted = decode_solidity_value(&input.ty, input_data, &mut offset);
90168

91169
let field = AnnotatedPayloadField {
92170
signable_payload_field: SignablePayloadField::TextV2 {
@@ -103,11 +181,7 @@ impl AbiDecoder {
103181
}
104182

105183
// Build function signature
106-
let param_types: Vec<&str> = function
107-
.inputs
108-
.iter()
109-
.map(|i| i.ty.as_str())
110-
.collect();
184+
let param_types: Vec<&str> = function.inputs.iter().map(|i| i.ty.as_str()).collect();
111185
let signature = format!("{}({})", function.name, param_types.join(","));
112186

113187
let title = SignablePayloadFieldTextV2 {

src/chain_parsers/visualsign-ethereum/src/embedded_abis.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub enum AbiEmbeddingError {
2929
/// Invalid JSON in ABI file
3030
#[error("Invalid ABI JSON: {0}")]
3131
InvalidJson(String),
32+
/// File I/O error
33+
#[error("Failed to read ABI file: {0}")]
34+
FileError(String),
3235
}
3336

3437
/// Registers a compile-time embedded ABI JSON string with an AbiRegistry
@@ -103,6 +106,42 @@ pub fn parse_abi_address_mapping(mapping_str: &str) -> Option<(&str, Address)> {
103106
Some((abi_name, address))
104107
}
105108

109+
/// Loads an ABI JSON from a file and registers it with the given name
110+
///
111+
/// # Arguments
112+
/// * `registry` - The ABI registry to register with (mutable)
113+
/// * `name` - Name for this ABI (e.g., "MyToken")
114+
/// * `file_path` - Path to the ABI JSON file
115+
///
116+
/// # Returns
117+
/// * `Ok(())` on successful registration
118+
/// * `Err(AbiEmbeddingError)` if file cannot be read or JSON is invalid
119+
pub fn load_abi_from_file(
120+
registry: &mut AbiRegistry,
121+
name: &str,
122+
file_path: &str,
123+
) -> Result<(), AbiEmbeddingError> {
124+
let abi_json = std::fs::read_to_string(file_path)
125+
.map_err(|e| AbiEmbeddingError::FileError(format!("{}: {}", file_path, e)))?;
126+
register_embedded_abi(registry, name, &abi_json)
127+
}
128+
129+
/// Loads an ABI from a file and maps it to an address
130+
///
131+
/// Convenience function that combines loading and address mapping
132+
pub fn load_and_map_abi(
133+
registry: &mut AbiRegistry,
134+
name: &str,
135+
file_path: &str,
136+
chain_id: u64,
137+
address_str: &str,
138+
) -> Result<(), Box<dyn std::error::Error>> {
139+
load_abi_from_file(registry, name, file_path)?;
140+
let address = address_str.parse::<Address>()?;
141+
registry.map_address(chain_id, address, name);
142+
Ok(())
143+
}
144+
106145
#[cfg(test)]
107146
mod tests {
108147
use super::*;

src/chain_parsers/visualsign-ethereum/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::fmt::{format_ether, format_gwei};
22
use crate::registry::ContractType;
3+
use crate::visualizer::CalldataVisualizer;
34
use alloy_consensus::{Transaction as _, TxType, TypedTransaction};
45
use alloy_rlp::{Buf, Decodable};
56
use base64::{Engine as _, engine::general_purpose::STANDARD as b64};
@@ -329,6 +330,11 @@ fn convert_to_visual_sign_payload(
329330
// Extract chain ID to determine the network
330331
let chain_id = transaction.chain_id();
331332

333+
// Try to extract AbiRegistry from options
334+
let abi_registry = options.abi_registry.as_ref().and_then(|any_reg| {
335+
any_reg.downcast_ref::<abi_registry::AbiRegistry>()
336+
});
337+
332338
let chain_name = chains::get_chain_name(chain_id);
333339

334340
let mut fields = vec![SignablePayloadField::TextV2 {
@@ -448,6 +454,20 @@ fn convert_to_visual_sign_payload(
448454
}
449455
}
450456

457+
// Try dynamic ABI visualization if available
458+
if input_fields.is_empty() {
459+
if let (Some(to_address), Some(abi_reg)) = (transaction.to(), abi_registry) {
460+
let chain_id_val = chain_id.unwrap_or(1);
461+
if let Some(abi) = abi_reg.get_abi_for_address(chain_id_val, to_address) {
462+
if let Some(field) = (contracts::core::DynamicAbiVisualizer::new(abi))
463+
.visualize_calldata(input, chain_id_val, None)
464+
{
465+
input_fields.push(field);
466+
}
467+
}
468+
}
469+
}
470+
451471
// Fallback: Try ERC20 if decode_transfers is enabled
452472
if input_fields.is_empty() && options.decode_transfers {
453473
if let Some(field) = (contracts::core::ERC20Visualizer {}).visualize_tx_commands(input)

src/parser/app/src/routes/parse.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub fn parse(
3232
decode_transfers: true,
3333
transaction_name: None,
3434
metadata: parse_request.chain_metadata.clone(),
35+
abi_registry: None,
3536
};
3637
let registry = create_registry();
3738
let proto_chain = ProtoChain::from_i32(parse_request.chain)

0 commit comments

Comments
 (0)