eip712 0.1.0

tool to generate EIP-712 compatible Solidity
Documentation
// Copyright (c) 2022 Sam Wilson.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// TODO: Verify that the generated solidity works!

use eip712::Eip712;

#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;

#[test]
fn simple() {
    let abi = include_str!("abi/simple.json");
    let mut actual = String::new();

    Eip712::<()>::new("Simple")
        .version("1")
        .read_str(abi)
        .unwrap()
        .generate(&mut actual)
        .unwrap();

    let expected = r#"contract SimpleImpl is Simple {

    function chainId() private view returns (uint256 r) {
        assembly { r := chainid() }
    }

    bytes32 constant private DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 immutable private DOMAIN_SEPARATOR = keccak256(abi.encode(
        DOMAIN_SEPARATOR_TYPE_HASH,
        keccak256(bytes("Simple")),
        keccak256(bytes("1")),
        chainId(),
        address(this)
    ));
    bytes32 constant private NONPAYABLE_BARE_TYPE_HASH = keccak256("NonpayableBare()");
    function nonpayableBare(
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        override
    {
        bytes memory buffer = abi.encodePacked(
            NONPAYABLE_BARE_TYPE_HASH
        );
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return nonpayableBare(signer);
    }

    bytes32 constant private PAYABLE_TWO_RETURNS_TYPE_HASH = keccak256("PayableTwoReturns()");
    function payableTwoReturns(
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        payable
        override
        returns (
            uint8,
            uint256
        )
    {
        bytes memory buffer = abi.encodePacked(
            PAYABLE_TWO_RETURNS_TYPE_HASH
        );
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return payableTwoReturns(signer);
    }

    bytes32 constant private VIEW_ONE_ARG_TYPE_HASH = keccak256("ViewOneArg(uint256 hello)");
    function viewOneArg(
        uint256 hello,
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        view
        override
    {
        bytes memory buffer = abi.encodePacked(
            VIEW_ONE_ARG_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    abi.encode(hello)
                );
        }
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return viewOneArg(signer, hello);
    }

}
"#;

    assert_eq!(actual, expected);
}

#[test]
fn level1() {
    let abi = include_str!("abi/level1.json");
    let mut actual = String::new();

    Eip712::<()>::new("Level1")
        .version("1")
        .read_str(abi)
        .unwrap()
        .generate(&mut actual)
        .unwrap();

    let expected = r#"contract Level1Impl is Level1 {
    bytes32 constant private SOME_STRUCT_TYPE_HASH = keccak256("SomeStruct(uint256 a1,bytes b2)");
    function hashSomeStruct(SomeStruct calldata input712) private pure returns (bytes32) {
        bytes memory buffer = abi.encodePacked(
            SOME_STRUCT_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    abi.encode(input712.a1)
                );
        }
        {
                buffer = abi.encodePacked(
                    buffer,
                    keccak256(bytes(input712.b2))
                );
        }
        return keccak256(buffer);
    }


    function chainId() private view returns (uint256 r) {
        assembly { r := chainid() }
    }

    bytes32 constant private DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 immutable private DOMAIN_SEPARATOR = keccak256(abi.encode(
        DOMAIN_SEPARATOR_TYPE_HASH,
        keccak256(bytes("Level1")),
        keccak256(bytes("1")),
        chainId(),
        address(this)
    ));
    bytes32 constant private DO_SOMETHING_TYPE_HASH = keccak256("DoSomething(SomeStruct foo)SomeStruct(uint256 a1,bytes b2)");
    function doSomething(
        SomeStruct calldata foo,
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        override
    {
        bytes memory buffer = abi.encodePacked(
            DO_SOMETHING_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    hashSomeStruct(foo)
                );
        }
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return doSomething(signer, foo);
    }

}
"#;

    assert_eq!(actual, expected);
}

#[test]
fn level2() {
    let abi = include_str!("abi/level2.json");
    let mut actual = String::new();

    Eip712::<()>::new("Level2")
        .version("1")
        .read_str(abi)
        .unwrap()
        .generate(&mut actual)
        .unwrap();

    let expected = r#"contract Level2Impl is Level2 {
    bytes32 constant private INNER_STRUCT_TYPE_HASH = keccak256("InnerStruct(uint256 v)");
    function hashInnerStruct(InnerStruct calldata input712) private pure returns (bytes32) {
        bytes memory buffer = abi.encodePacked(
            INNER_STRUCT_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    abi.encode(input712.v)
                );
        }
        return keccak256(buffer);
    }

    bytes32 constant private OUTER_STRUCT_TYPE_HASH = keccak256("OuterStruct(InnerStruct inner)InnerStruct(uint256 v)");
    function hashOuterStruct(OuterStruct calldata input712) private pure returns (bytes32) {
        bytes memory buffer = abi.encodePacked(
            OUTER_STRUCT_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    hashInnerStruct(input712.inner)
                );
        }
        return keccak256(buffer);
    }


    function chainId() private view returns (uint256 r) {
        assembly { r := chainid() }
    }

    bytes32 constant private DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 immutable private DOMAIN_SEPARATOR = keccak256(abi.encode(
        DOMAIN_SEPARATOR_TYPE_HASH,
        keccak256(bytes("Level2")),
        keccak256(bytes("1")),
        chainId(),
        address(this)
    ));
    bytes32 constant private DO_SOMETHING_TYPE_HASH = keccak256("DoSomething(OuterStruct input)InnerStruct(uint256 v)OuterStruct(InnerStruct inner)");
    function doSomething(
        OuterStruct calldata input,
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        override
    {
        bytes memory buffer = abi.encodePacked(
            DO_SOMETHING_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    hashOuterStruct(input)
                );
        }
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return doSomething(signer, input);
    }

}
"#;

    assert_eq!(actual, expected);
}

#[test]
fn array_in_struct() {
    let abi = include_str!("abi/arraystruct.json");
    let mut actual = String::new();

    Eip712::<()>::new("ArrayInStruct")
        .version("1")
        .read_str(abi)
        .unwrap()
        .generate(&mut actual)
        .unwrap();

    let expected = r#"contract ArrayInStructImpl is ArrayInStruct {
    bytes32 constant private SOME_STRUCT_TYPE_HASH = keccak256("SomeStruct(uint256[][] array)");
    function hashSomeStruct(SomeStruct calldata input712) private pure returns (bytes32) {
        bytes memory buffer = abi.encodePacked(
            SOME_STRUCT_TYPE_HASH
        );
        {
            bytes memory b0;
            for (uint a0 = 0; a0 < input712.array.length; ++a0) {
            bytes memory b1;
            for (uint a1 = 0; a1 < input712.array[a0].length; ++a1) {
                b1 = abi.encodePacked(
                    b1,
                    abi.encode(input712.array[a0][a1])
                );
            }
            b0 = abi.encodePacked(
                b0,
                keccak256(b1)
            );
            }
            buffer = abi.encodePacked(
                buffer,
                keccak256(b0)
            );
        }
        return keccak256(buffer);
    }


    function chainId() private view returns (uint256 r) {
        assembly { r := chainid() }
    }

    bytes32 constant private DOMAIN_SEPARATOR_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 immutable private DOMAIN_SEPARATOR = keccak256(abi.encode(
        DOMAIN_SEPARATOR_TYPE_HASH,
        keccak256(bytes("ArrayInStruct")),
        keccak256(bytes("1")),
        chainId(),
        address(this)
    ));
    bytes32 constant private DO_SOMETHING_TYPE_HASH = keccak256("DoSomething(SomeStruct input)SomeStruct(uint256[][] array)");
    function doSomething(
        SomeStruct calldata input,
        uint8 v,
        bytes32 r,
        bytes32 s
    )
        public
        override
    {
        bytes memory buffer = abi.encodePacked(
            DO_SOMETHING_TYPE_HASH
        );
        {
                buffer = abi.encodePacked(
                    buffer,
                    hashSomeStruct(input)
                );
        }
        bytes32 message = keccak256(abi.encodePacked(
            hex"1901",
            DOMAIN_SEPARATOR,
            keccak256(buffer)
        ));
        address signer = ecrecover(message, v, r, s);
        require(address(0) != signer);
        return doSomething(signer, input);
    }

}
"#;

    assert_eq!(actual, expected);
}