-module(gleeth@eip712). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gleeth/eip712.gleam"). -export([domain/0, domain_name/2, domain_version/2, domain_chain_id/2, domain_verifying_contract/2, domain_salt/2, field/2, string_val/1, int_val/1, bool_val/1, address_val/1, bytes32_val/1, bytes_val/1, array_val/1, struct_val/1, typed_data/4, encode_type/2, hash_type/2, hash_struct/3, hash_domain/2, hash_typed_data/1, sign_typed_data/2, recover_typed_data/2]). -export_type([domain/0, typed_field/0, typed_value/0, typed_data/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " EIP-712 typed structured data hashing and signing.\n" "\n" " Implements the encoding rules from the EIP-712 specification for signing\n" " structured data with domain separation. Used for ERC-2612 permits,\n" " DEX order signing, meta-transactions, and other off-chain authorization.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let domain = eip712.domain()\n" " |> eip712.domain_name(\"MyDapp\")\n" " |> eip712.domain_version(\"1\")\n" " |> eip712.domain_chain_id(1)\n" "\n" " let types = dict.from_list([\n" " #(\"Mail\", [\n" " eip712.field(\"from\", \"address\"),\n" " eip712.field(\"to\", \"address\"),\n" " eip712.field(\"contents\", \"string\"),\n" " ]),\n" " ])\n" "\n" " let message = dict.from_list([\n" " #(\"from\", eip712.address_val(\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\")),\n" " #(\"to\", eip712.address_val(\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\")),\n" " #(\"contents\", eip712.string_val(\"Hello, Bob!\")),\n" " ])\n" "\n" " let data = eip712.typed_data(types, \"Mail\", domain, message)\n" " let assert Ok(sig) = eip712.sign_typed_data(data, wallet)\n" " ```\n" ). -type domain() :: {domain, gleam@option:option(binary()), gleam@option:option(binary()), gleam@option:option(integer()), gleam@option:option(binary()), gleam@option:option(binary())}. -type typed_field() :: {typed_field, binary(), binary()}. -type typed_value() :: {string_val, binary()} | {int_val, integer()} | {bool_val, boolean()} | {address_val, binary()} | {bytes32_val, bitstring()} | {bytes_val, bitstring()} | {array_val, list(typed_value())} | {struct_val, gleam@dict:dict(binary(), typed_value())}. -type typed_data() :: {typed_data, gleam@dict:dict(binary(), list(typed_field())), binary(), domain(), gleam@dict:dict(binary(), typed_value())}. -file("src/gleeth/eip712.gleam", 93). ?DOC(" Create an empty domain. Use `domain_name`, `domain_version`, etc. to set fields.\n"). -spec domain() -> domain(). domain() -> {domain, none, none, none, none, none}. -file("src/gleeth/eip712.gleam", 104). ?DOC(" Set the domain name field.\n"). -spec domain_name(domain(), binary()) -> domain(). domain_name(D, Name) -> {domain, {some, Name}, erlang:element(3, D), erlang:element(4, D), erlang:element(5, D), erlang:element(6, D)}. -file("src/gleeth/eip712.gleam", 109). ?DOC(" Set the domain version field.\n"). -spec domain_version(domain(), binary()) -> domain(). domain_version(D, Version) -> {domain, erlang:element(2, D), {some, Version}, erlang:element(4, D), erlang:element(5, D), erlang:element(6, D)}. -file("src/gleeth/eip712.gleam", 114). ?DOC(" Set the domain chain ID field.\n"). -spec domain_chain_id(domain(), integer()) -> domain(). domain_chain_id(D, Id) -> {domain, erlang:element(2, D), erlang:element(3, D), {some, Id}, erlang:element(5, D), erlang:element(6, D)}. -file("src/gleeth/eip712.gleam", 119). ?DOC(" Set the domain verifying contract address.\n"). -spec domain_verifying_contract(domain(), binary()) -> domain(). domain_verifying_contract(D, Address) -> {domain, erlang:element(2, D), erlang:element(3, D), erlang:element(4, D), {some, Address}, erlang:element(6, D)}. -file("src/gleeth/eip712.gleam", 124). ?DOC(" Set the domain salt field (hex-encoded bytes32).\n"). -spec domain_salt(domain(), binary()) -> domain(). domain_salt(D, Salt) -> {domain, erlang:element(2, D), erlang:element(3, D), erlang:element(4, D), erlang:element(5, D), {some, Salt}}. -file("src/gleeth/eip712.gleam", 129). ?DOC(" Create a typed field definition.\n"). -spec field(binary(), binary()) -> typed_field(). field(Name, Type_name) -> {typed_field, Name, Type_name}. -file("src/gleeth/eip712.gleam", 134). ?DOC(" Convenience constructors for typed values.\n"). -spec string_val(binary()) -> typed_value(). string_val(S) -> {string_val, S}. -file("src/gleeth/eip712.gleam", 139). ?DOC(" Wrap an integer as a typed value (for uint256, int256, etc.).\n"). -spec int_val(integer()) -> typed_value(). int_val(N) -> {int_val, N}. -file("src/gleeth/eip712.gleam", 144). ?DOC(" Wrap a boolean as a typed value.\n"). -spec bool_val(boolean()) -> typed_value(). bool_val(B) -> {bool_val, B}. -file("src/gleeth/eip712.gleam", 149). ?DOC(" Wrap an Ethereum address as a typed value.\n"). -spec address_val(binary()) -> typed_value(). address_val(Addr) -> {address_val, Addr}. -file("src/gleeth/eip712.gleam", 154). ?DOC(" Wrap a fixed 32-byte value as a typed value.\n"). -spec bytes32_val(bitstring()) -> typed_value(). bytes32_val(B) -> {bytes32_val, B}. -file("src/gleeth/eip712.gleam", 159). ?DOC(" Wrap dynamic bytes as a typed value.\n"). -spec bytes_val(bitstring()) -> typed_value(). bytes_val(B) -> {bytes_val, B}. -file("src/gleeth/eip712.gleam", 164). ?DOC(" Wrap a list of typed values as an array value.\n"). -spec array_val(list(typed_value())) -> typed_value(). array_val(Items) -> {array_val, Items}. -file("src/gleeth/eip712.gleam", 169). ?DOC(" Wrap a dict of named fields as a struct value.\n"). -spec struct_val(gleam@dict:dict(binary(), typed_value())) -> typed_value(). struct_val(Fields) -> {struct_val, Fields}. -file("src/gleeth/eip712.gleam", 174). ?DOC(" Create a complete typed data structure.\n"). -spec typed_data( gleam@dict:dict(binary(), list(typed_field())), binary(), domain(), gleam@dict:dict(binary(), typed_value()) ) -> typed_data(). typed_data(Types, Primary_type, D, Message) -> {typed_data, Types, Primary_type, D, Message}. -file("src/gleeth/eip712.gleam", 281). -spec encode_single_type( binary(), gleam@dict:dict(binary(), list(typed_field())) ) -> binary(). encode_single_type(Type_name, Types) -> case gleam_stdlib:map_get(Types, Type_name) of {ok, Fields} -> Field_strs = gleam@list:map( Fields, fun(F) -> <<<<(erlang:element(3, F))/binary, " "/utf8>>/binary, (erlang:element(2, F))/binary>> end ), <<<<<>/binary, (gleam@string:join(Field_strs, <<","/utf8>>))/binary>>/binary, ")"/utf8>>; {error, _} -> <> end. -file("src/gleeth/eip712.gleam", 327). -spec strip_array_suffix(binary()) -> binary(). strip_array_suffix(Type_name) -> case gleam_stdlib:string_ends_with(Type_name, <<"[]"/utf8>>) of true -> gleam@string:drop_end(Type_name, 2); false -> Type_name end. -file("src/gleeth/eip712.gleam", 296). ?DOC( " Recursively collect all struct types referenced by a type's fields,\n" " excluding the root type itself. Deduplicates across siblings.\n" ). -spec collect_referenced_types( binary(), gleam@dict:dict(binary(), list(typed_field())), list(binary()) ) -> list(binary()). collect_referenced_types(Type_name, Types, Seen) -> case gleam_stdlib:map_get(Types, Type_name) of {ok, Fields} -> {Result, _} = gleam@list:fold( Fields, {[], Seen}, fun(Acc, F) -> {Collected, Current_seen} = Acc, Base_type = strip_array_suffix(erlang:element(3, F)), case gleam@dict:has_key(Types, Base_type) andalso not gleam@list:contains( Current_seen, Base_type ) of true -> New_seen = [Base_type | Current_seen], Nested = collect_referenced_types( Base_type, Types, New_seen ), All_new = [Base_type | Nested], Updated_seen = lists:append(All_new, New_seen), {lists:append(Collected, All_new), Updated_seen}; false -> Acc end end ), Result; {error, _} -> [] end. -file("src/gleeth/eip712.gleam", 268). ?DOC( " Build the canonical type encoding string.\n" " Example: \"Mail(address from,address to,string contents)\"\n" " Referenced structs are collected, sorted, and appended.\n" ). -spec encode_type(binary(), gleam@dict:dict(binary(), list(typed_field()))) -> binary(). encode_type(Type_name, Types) -> Primary = encode_single_type(Type_name, Types), Referenced = collect_referenced_types(Type_name, Types, [Type_name]), Sorted = gleam@list:sort(Referenced, fun gleam@string:compare/2), Suffix = begin _pipe = gleam@list:map( Sorted, fun(Name) -> encode_single_type(Name, Types) end ), erlang:list_to_binary(_pipe) end, <>. -file("src/gleeth/eip712.gleam", 228). ?DOC(" Compute typeHash: keccak256(encodeType(typeName)).\n"). -spec hash_type(binary(), gleam@dict:dict(binary(), list(typed_field()))) -> bitstring(). hash_type(Type_name, Types) -> Type_string = encode_type(Type_name, Types), gleeth@crypto@keccak:keccak256_binary(gleam_stdlib:identity(Type_string)). -file("src/gleeth/eip712.gleam", 495). -spec append_if( list(typed_field()), gleam@option:option(any()), binary(), binary() ) -> list(typed_field()). append_if(Acc, Opt, Type_name, Name) -> case Opt of {some, _} -> [{typed_field, Name, Type_name} | Acc]; none -> Acc end. -file("src/gleeth/eip712.gleam", 467). -spec build_domain_fields(domain()) -> list(typed_field()). build_domain_fields(D) -> _pipe = [], _pipe@1 = append_if( _pipe, erlang:element(2, D), <<"string"/utf8>>, <<"name"/utf8>> ), _pipe@2 = append_if( _pipe@1, erlang:element(3, D), <<"string"/utf8>>, <<"version"/utf8>> ), _pipe@3 = append_if( _pipe@2, erlang:element(4, D), <<"uint256"/utf8>>, <<"chainId"/utf8>> ), _pipe@4 = append_if( _pipe@3, erlang:element(5, D), <<"address"/utf8>>, <<"verifyingContract"/utf8>> ), _pipe@5 = append_if( _pipe@4, erlang:element(6, D), <<"bytes32"/utf8>>, <<"salt"/utf8>> ), lists:reverse(_pipe@5). -file("src/gleeth/eip712.gleam", 507). -spec append_val_if( list({binary(), typed_value()}), gleam@option:option(PKI), binary(), fun((PKI) -> typed_value()) ) -> list({binary(), typed_value()}). append_val_if(Acc, Opt, Name, To_val) -> case Opt of {some, V} -> [{Name, To_val(V)} | Acc]; none -> Acc end. -file("src/gleeth/eip712.gleam", 477). -spec build_domain_values(domain()) -> gleam@dict:dict(binary(), typed_value()). build_domain_values(D) -> Entries = begin _pipe = [], _pipe@1 = append_val_if( _pipe, erlang:element(2, D), <<"name"/utf8>>, fun(V) -> {string_val, V} end ), _pipe@2 = append_val_if( _pipe@1, erlang:element(3, D), <<"version"/utf8>>, fun(V@1) -> {string_val, V@1} end ), _pipe@3 = append_val_if( _pipe@2, erlang:element(4, D), <<"chainId"/utf8>>, fun(V@2) -> {int_val, V@2} end ), _pipe@4 = append_val_if( _pipe@3, erlang:element(5, D), <<"verifyingContract"/utf8>>, fun(V@3) -> {address_val, V@3} end ), append_val_if( _pipe@4, erlang:element(6, D), <<"salt"/utf8>>, fun(V@4) -> case gleeth@utils@hex:decode(V@4) of {ok, Bytes} -> {bytes32_val, Bytes}; {error, _} -> {bytes32_val, <<>>} end end ) end, maps:from_list(Entries). -file("src/gleeth/eip712.gleam", 523). -spec pad_left(bitstring(), integer()) -> bitstring(). pad_left(Data, Target) -> Size = erlang:byte_size(Data), case Size >= Target of true -> case gleam_stdlib:bit_array_slice(Data, Size - Target, Target) of {ok, Sliced} -> Sliced; {error, _} -> Data end; false -> Padding = gleeth@utils@hex:make_zeros(Target - Size), gleam_stdlib:bit_array_concat([Padding, Data]) end. -file("src/gleeth/eip712.gleam", 440). -spec encode_address(binary()) -> {ok, bitstring()} | {error, binary()}. encode_address(Addr) -> case gleeth@utils@hex:decode(Addr) of {ok, Bytes} -> {ok, pad_left(Bytes, 32)}; {error, _} -> {error, <<"Invalid address: "/utf8, Addr/binary>>} end. -file("src/gleeth/eip712.gleam", 540). -spec pad_right(bitstring(), integer()) -> bitstring(). pad_right(Data, Target) -> Size = erlang:byte_size(Data), case Size >= Target of true -> case gleam_stdlib:bit_array_slice(Data, 0, Target) of {ok, Sliced} -> Sliced; {error, _} -> Data end; false -> Padding = gleeth@utils@hex:make_zeros(Target - Size), gleam_stdlib:bit_array_concat([Data, Padding]) end. -file("src/gleeth/eip712.gleam", 563). -spec int_to_bytes_acc(integer(), bitstring()) -> bitstring(). int_to_bytes_acc(N, Acc) -> case N of 0 -> Acc; _ -> Byte = erlang:'band'(N, 16#ff), Rest = erlang:'bsr'(N, 8), int_to_bytes_acc(Rest, <>) end. -file("src/gleeth/eip712.gleam", 556). -spec int_to_bytes(integer()) -> bitstring(). int_to_bytes(N) -> case N of 0 -> <<0>>; _ -> int_to_bytes_acc(N, <<>>) end. -file("src/gleeth/eip712.gleam", 580). -spec invert_bytes(bitstring(), bitstring()) -> bitstring(). invert_bytes(Data, Acc) -> case Data of <> -> invert_bytes( Rest, <> ); _ -> Acc end. -file("src/gleeth/eip712.gleam", 596). -spec add_one_at(bitstring(), integer(), integer()) -> bitstring(). add_one_at(Data, Pos, Carry) -> case Pos < 0 of true -> Data; false -> case gleam_stdlib:bit_array_slice(Data, Pos, 1) of {ok, <>} -> Sum = Byte + Carry, New_byte = erlang:'band'(Sum, 16#ff), New_carry = erlang:'bsr'(Sum, 8), Before = case gleam_stdlib:bit_array_slice(Data, 0, Pos) of {ok, B} -> B; {error, _} -> <<>> end, After_start = Pos + 1, After_len = erlang:byte_size(Data) - After_start, After = case gleam_stdlib:bit_array_slice( Data, After_start, After_len ) of {ok, A} -> A; {error, _} -> <<>> end, New_data = gleam_stdlib:bit_array_concat( [Before, <>, After] ), case New_carry of 0 -> New_data; _ -> add_one_at(New_data, Pos - 1, New_carry) end; _ -> Data end end. -file("src/gleeth/eip712.gleam", 591). -spec add_one(bitstring()) -> bitstring(). add_one(Data) -> Size = erlang:byte_size(Data), add_one_at(Data, Size - 1, 1). -file("src/gleeth/eip712.gleam", 574). -spec twos_complement(bitstring()) -> bitstring(). twos_complement(Bytes) -> Inverted = invert_bytes(Bytes, <<>>), add_one(Inverted). -file("src/gleeth/eip712.gleam", 447). -spec encode_int(integer()) -> {ok, bitstring()} | {error, binary()}. encode_int(N) -> case N >= 0 of true -> Bytes = int_to_bytes(N), {ok, pad_left(Bytes, 32)}; false -> Pos = int_to_bytes(- N), Padded = pad_left(Pos, 32), {ok, twos_complement(Padded)} end. -file("src/gleeth/eip712.gleam", 372). -spec encode_non_array_value( binary(), typed_value(), gleam@dict:dict(binary(), list(typed_field())) ) -> {ok, bitstring()} | {error, binary()}. encode_non_array_value(Type_name, Value, Types) -> case {Type_name, Value} of {<<"string"/utf8>>, {string_val, S}} -> {ok, gleeth@crypto@keccak:keccak256_binary(gleam_stdlib:identity(S))}; {<<"bytes"/utf8>>, {bytes_val, B}} -> {ok, gleeth@crypto@keccak:keccak256_binary(B)}; {<<"address"/utf8>>, {address_val, Addr}} -> encode_address(Addr); {<<"bool"/utf8>>, {bool_val, B@1}} -> {ok, case B@1 of true -> pad_left(<<1>>, 32); false -> pad_left(<<>>, 32) end}; {<<"bytes32"/utf8>>, {bytes32_val, B@2}} -> {ok, pad_right(B@2, 32)}; {_, {int_val, N}} -> case gleam_stdlib:string_starts_with(Type_name, <<"uint"/utf8>>) orelse gleam_stdlib:string_starts_with(Type_name, <<"int"/utf8>>) of true -> encode_int(N); false -> {error, <<<<"Type mismatch: expected "/utf8, Type_name/binary>>/binary, ", got int"/utf8>>} end; {_, {bytes32_val, B@3}} -> case gleam_stdlib:string_starts_with(Type_name, <<"bytes"/utf8>>) of true -> {ok, pad_right(B@3, 32)}; false -> {error, <<<<"Type mismatch: expected "/utf8, Type_name/binary>>/binary, ", got bytes"/utf8>>} end; {_, {struct_val, Fields}} -> case gleam@dict:has_key(Types, Type_name) of true -> gleam@result:'try'( hash_struct(Type_name, Fields, Types), fun(Hashed) -> {ok, Hashed} end ); false -> {error, <<"Unknown struct type: "/utf8, Type_name/binary>>} end; {_, _} -> {error, <<<<"Cannot encode "/utf8, Type_name/binary>>/binary, " with given value"/utf8>>} end. -file("src/gleeth/eip712.gleam", 217). ?DOC(" Compute hashStruct: keccak256(typeHash || encodeData(s)).\n"). -spec hash_struct( binary(), gleam@dict:dict(binary(), typed_value()), gleam@dict:dict(binary(), list(typed_field())) ) -> {ok, bitstring()} | {error, binary()}. hash_struct(Type_name, Data, Types) -> Type_hash = hash_type(Type_name, Types), gleam@result:'try'( encode_data(Type_name, Data, Types), fun(Encoded) -> {ok, gleeth@crypto@keccak:keccak256_binary( gleam_stdlib:bit_array_concat([Type_hash, Encoded]) )} end ). -file("src/gleeth/eip712.gleam", 338). -spec encode_data( binary(), gleam@dict:dict(binary(), typed_value()), gleam@dict:dict(binary(), list(typed_field())) ) -> {ok, bitstring()} | {error, binary()}. encode_data(Type_name, Data, Types) -> case gleam_stdlib:map_get(Types, Type_name) of {ok, Fields} -> gleam@result:'try'( gleam@list:try_map( Fields, fun(F) -> case gleam_stdlib:map_get(Data, erlang:element(2, F)) of {ok, Value} -> encode_value(erlang:element(3, F), Value, Types); {error, _} -> {error, <<"Missing field: "/utf8, (erlang:element(2, F))/binary>>} end end ), fun(Encoded_parts) -> {ok, gleam_stdlib:bit_array_concat(Encoded_parts)} end ); {error, _} -> {error, <<"Unknown type: "/utf8, Type_name/binary>>} end. -file("src/gleeth/eip712.gleam", 360). ?DOC(" Encode a single value to exactly 32 bytes per EIP-712 rules.\n"). -spec encode_value( binary(), typed_value(), gleam@dict:dict(binary(), list(typed_field())) ) -> {ok, bitstring()} | {error, binary()}. encode_value(Type_name, Value, Types) -> case gleam_stdlib:string_ends_with(Type_name, <<"[]"/utf8>>) of true -> encode_array_value(Type_name, Value, Types); false -> encode_non_array_value(Type_name, Value, Types) end. -file("src/gleeth/eip712.gleam", 205). ?DOC(" Compute the domain separator hash.\n"). -spec hash_domain(domain(), gleam@dict:dict(binary(), list(typed_field()))) -> {ok, bitstring()} | {error, binary()}. hash_domain(D, Custom_types) -> Domain_fields = build_domain_fields(D), Domain_values = build_domain_values(D), Types = gleam@dict:insert( Custom_types, <<"EIP712Domain"/utf8>>, Domain_fields ), hash_struct(<<"EIP712Domain"/utf8>>, Domain_values, Types). -file("src/gleeth/eip712.gleam", 194). ?DOC( " Compute the EIP-712 digest: keccak256(\"\\x19\\x01\" || domainSeparator || hashStruct(message)).\n" " This is the hash that gets signed.\n" ). -spec hash_typed_data(typed_data()) -> {ok, bitstring()} | {error, binary()}. hash_typed_data(Data) -> gleam@result:'try'( hash_domain(erlang:element(4, Data), erlang:element(2, Data)), fun(Domain_sep) -> gleam@result:'try'( hash_struct( erlang:element(3, Data), erlang:element(5, Data), erlang:element(2, Data) ), fun(Struct_hash) -> {ok, gleeth@crypto@keccak:keccak256_binary( <<16#19, 16#01, Domain_sep/bitstring, Struct_hash/bitstring>> )} end ) end ). -file("src/gleeth/eip712.gleam", 241). ?DOC(" Sign typed data with a wallet. Returns the signature.\n"). -spec sign_typed_data(typed_data(), gleeth@crypto@wallet:wallet()) -> {ok, gleeth@crypto@secp256k1:signature()} | {error, binary()}. sign_typed_data(Data, W) -> gleam@result:'try'( hash_typed_data(Data), fun(Digest) -> _pipe = gleeth@crypto@wallet:sign_hash(W, Digest), gleam@result:map_error( _pipe, fun gleeth@crypto@wallet:error_to_string/1 ) end ). -file("src/gleeth/eip712.gleam", 251). ?DOC(" Recover the signer address from a typed data signature.\n"). -spec recover_typed_data(typed_data(), binary()) -> {ok, binary()} | {error, binary()}. recover_typed_data(Data, Signature_hex) -> gleam@result:'try'( hash_typed_data(Data), fun(Digest) -> gleam@result:'try'( gleeth@crypto@secp256k1:signature_from_hex(Signature_hex), fun(Signature) -> gleam@result:'try'( gleeth@crypto@secp256k1:recover_address( Digest, Signature ), fun(Address) -> {ok, gleeth@crypto@secp256k1:address_to_string( Address )} end ) end ) end ). -file("src/gleeth/eip712.gleam", 423). -spec encode_array_value( binary(), typed_value(), gleam@dict:dict(binary(), list(typed_field())) ) -> {ok, bitstring()} | {error, binary()}. encode_array_value(Type_name, Value, Types) -> Element_type = gleam@string:drop_end(Type_name, 2), case Value of {array_val, Items} -> gleam@result:'try'( gleam@list:try_map( Items, fun(Item) -> encode_value(Element_type, Item, Types) end ), fun(Encoded_items) -> {ok, gleeth@crypto@keccak:keccak256_binary( gleam_stdlib:bit_array_concat(Encoded_items) )} end ); _ -> {error, <<"Expected array value for "/utf8, Type_name/binary>>} end.