clear-signing 0.1.0

ERC-7730 v2 clear signing library: decodes and formats Ethereum calldata and EIP-712 typed data for human-readable display.
Documentation
use super::ResolvedDescriptor;
use crate::eip712::TypedData;
use crate::types::descriptor::Descriptor;

pub(crate) fn safe_descriptor() -> Descriptor {
    let json = std::fs::read_to_string(format!(
        "{}/tests/fixtures/common-Safe.json",
        env!("CARGO_MANIFEST_DIR")
    ))
    .expect("read Safe descriptor");
    Descriptor::from_json(&json).expect("parse Safe descriptor")
}

pub(crate) fn erc20_descriptor() -> Descriptor {
    let json = std::fs::read_to_string(format!(
        "{}/tests/fixtures/erc20-transfer.json",
        env!("CARGO_MANIFEST_DIR")
    ))
    .expect("read ERC-20 descriptor");
    Descriptor::from_json(&json).expect("parse ERC-20 descriptor")
}

pub(crate) fn address_word(hex_addr: &str) -> Vec<u8> {
    let hex_str = hex_addr
        .strip_prefix("0x")
        .or_else(|| hex_addr.strip_prefix("0X"))
        .unwrap_or(hex_addr);
    let addr_bytes = hex::decode(hex_str).expect("valid hex address");
    let mut word = vec![0u8; 12];
    word.extend_from_slice(&addr_bytes);
    assert_eq!(word.len(), 32);
    word
}

pub(crate) fn uint_word(val: u128) -> Vec<u8> {
    let mut word = vec![0u8; 16];
    word.extend_from_slice(&val.to_be_bytes());
    assert_eq!(word.len(), 32);
    word
}

pub(crate) fn pad32(len: usize) -> usize {
    len.div_ceil(32) * 32
}

pub(crate) fn build_exec_transaction_calldata(to: &str, inner_calldata: &[u8]) -> Vec<u8> {
    let sig = crate::decoder::parse_signature(
        "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
    )
    .unwrap();

    let mut calldata = Vec::new();
    calldata.extend_from_slice(&sig.selector);
    calldata.extend_from_slice(&address_word(to));
    calldata.extend_from_slice(&uint_word(0));
    calldata.extend_from_slice(&uint_word(320));
    calldata.extend_from_slice(&uint_word(0));
    calldata.extend_from_slice(&uint_word(0));
    calldata.extend_from_slice(&uint_word(0));
    calldata.extend_from_slice(&uint_word(0));
    calldata.extend_from_slice(&[0u8; 32]);
    calldata.extend_from_slice(&[0u8; 32]);
    let data_offset = 320 + 32 + pad32(inner_calldata.len());
    calldata.extend_from_slice(&uint_word(data_offset as u128));

    calldata.extend_from_slice(&uint_word(inner_calldata.len() as u128));
    calldata.extend_from_slice(inner_calldata);
    let padding = pad32(inner_calldata.len()) - inner_calldata.len();
    calldata.extend_from_slice(&vec![0u8; padding]);
    calldata.extend_from_slice(&uint_word(0));

    calldata
}

pub(crate) fn build_erc20_transfer_calldata(to: &str, amount: u128) -> Vec<u8> {
    let sig = crate::decoder::parse_signature("transfer(address,uint256)").unwrap();
    let mut calldata = Vec::new();
    calldata.extend_from_slice(&sig.selector);
    calldata.extend_from_slice(&address_word(to));
    calldata.extend_from_slice(&uint_word(amount));
    calldata
}

pub(crate) fn permit2_descriptor(
    owner: &str,
    format_key: &str,
    extra_context: Option<serde_json::Value>,
) -> Descriptor {
    let mut eip712 = serde_json::json!({
        "deployments": [{
            "chainId": 1,
            "address": "0x000000000022d473030f116ddee9f6b43ac78ba3"
        }],
        "domain": {
            "name": "Permit2"
        }
    });

    if let Some(extra) = extra_context {
        let target = eip712.as_object_mut().expect("eip712 context object");
        for (key, value) in extra.as_object().expect("extra context object") {
            target.insert(key.clone(), value.clone());
        }
    }

    let descriptor = serde_json::json!({
        "context": { "eip712": eip712 },
        "metadata": {
            "owner": owner,
            "enums": {},
            "constants": {},
            "maps": {}
        },
        "display": {
            "definitions": {},
            "formats": {
                format_key: {
                    "intent": owner,
                    "fields": [{
                        "path": "spender",
                        "label": "Spender",
                        "format": "raw"
                    }]
                }
            }
        }
    });
    Descriptor::from_json(&descriptor.to_string()).expect("descriptor")
}

pub(crate) fn resolved_permit2_descriptor(
    owner: &str,
    format_key: &str,
    extra_context: Option<serde_json::Value>,
) -> ResolvedDescriptor {
    ResolvedDescriptor {
        descriptor: permit2_descriptor(owner, format_key, extra_context),
        chain_id: 1,
        address: "0x000000000022d473030f116ddee9f6b43ac78ba3".to_string(),
    }
}

pub(crate) fn exclusive_dutch_order_typed_data() -> TypedData {
    serde_json::from_value(serde_json::json!({
        "types": {
            "PermitWitnessTransferFrom": [
                { "name": "permitted", "type": "TokenPermissions" },
                { "name": "spender", "type": "address" },
                { "name": "nonce", "type": "uint256" },
                { "name": "deadline", "type": "uint256" },
                { "name": "witness", "type": "ExclusiveDutchOrder" }
            ],
            "TokenPermissions": [
                { "name": "token", "type": "address" },
                { "name": "amount", "type": "uint256" }
            ],
            "ExclusiveDutchOrder": [
                { "name": "info", "type": "OrderInfo" },
                { "name": "decayStartTime", "type": "uint256" },
                { "name": "decayEndTime", "type": "uint256" },
                { "name": "exclusiveFiller", "type": "address" },
                { "name": "exclusivityOverrideBps", "type": "uint256" },
                { "name": "inputToken", "type": "address" },
                { "name": "inputStartAmount", "type": "uint256" },
                { "name": "inputEndAmount", "type": "uint256" },
                { "name": "outputs", "type": "DutchOutput[]" }
            ],
            "OrderInfo": [
                { "name": "reactor", "type": "address" },
                { "name": "swapper", "type": "address" },
                { "name": "nonce", "type": "uint256" },
                { "name": "deadline", "type": "uint256" },
                { "name": "additionalValidationContract", "type": "address" },
                { "name": "additionalValidationData", "type": "bytes" }
            ],
            "DutchOutput": [
                { "name": "token", "type": "address" },
                { "name": "startAmount", "type": "uint256" },
                { "name": "endAmount", "type": "uint256" },
                { "name": "recipient", "type": "address" }
            ],
            "EIP712Domain": [
                { "name": "name", "type": "string" },
                { "name": "chainId", "type": "uint256" },
                { "name": "verifyingContract", "type": "address" }
            ]
        },
        "domain": {
            "name": "Permit2",
            "chainId": "1",
            "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
        },
        "primaryType": "PermitWitnessTransferFrom",
        "message": {
            "permitted": {
                "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
                "amount": "100000000000000"
            },
            "spender": "0x6000da47483062a0d734ba3dc7576ce6a0b645c4",
            "nonce": "1993349843209468715141873868895370562722298771555073489698616037339384894721",
            "deadline": "1774866877",
            "witness": {
                "info": {
                    "reactor": "0x6000da47483062a0d734ba3dc7576ce6a0b645c4",
                    "swapper": "0xbf01daf454dce008d3e2bfd47d5e186f71477253",
                    "nonce": "1993349843209468715141873868895370562722298771555073489698616037339384894721",
                    "deadline": "1774866877",
                    "additionalValidationContract": "0x0000000000000000000000000000000000000000",
                    "additionalValidationData": "0x"
                },
                "decayStartTime": "1774780477",
                "decayEndTime": "1774780477",
                "exclusiveFiller": "0x0000000000000000000000000000000000000000",
                "exclusivityOverrideBps": "0",
                "inputToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
                "inputStartAmount": "100000000000000",
                "inputEndAmount": "100000000000000",
                "outputs": [{
                    "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                    "startAmount": "199179",
                    "endAmount": "199179",
                    "recipient": "0xbf01daf454dce008d3e2bfd47d5e186f71477253"
                }]
            }
        }
    }))
    .expect("typed data")
}