-module(gleeth@crypto@transaction). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gleeth/crypto/transaction.gleam"). -export([sign_transaction/2, sign_eip1559_transaction/2, create_legacy_transaction/7, create_eth_transfer/6, create_contract_call/6, create_eip1559_transaction/9, signed_transaction_to_string/1, hash_raw_transaction/1, get_transaction_hash/1, get_eip1559_transaction_hash/1, signed_eip1559_transaction_to_string/1, error_to_string/1, decode_legacy/1, decode_eip1559/1, decode/1]). -export_type([unsigned_transaction/0, signed_transaction/0, eip1559_transaction/0, access_list_entry/0, signed_eip1559_transaction/0, transaction_type/0, transaction_error/0, decoded_transaction/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. -type unsigned_transaction() :: {unsigned_transaction, binary(), binary(), binary(), binary(), binary(), binary(), integer()}. -type signed_transaction() :: {signed_transaction, binary(), binary(), binary(), binary(), binary(), binary(), integer(), binary(), binary(), binary(), binary()}. -type eip1559_transaction() :: {eip1559_transaction, binary(), binary(), binary(), binary(), binary(), binary(), binary(), integer(), list(access_list_entry())}. -type access_list_entry() :: {access_list_entry, binary(), list(binary())}. -type signed_eip1559_transaction() :: {signed_eip1559_transaction, binary(), binary(), binary(), binary(), binary(), binary(), binary(), integer(), list(access_list_entry()), binary(), binary(), binary(), binary()}. -type transaction_type() :: legacy | access_list | eip1559. -type transaction_error() :: {invalid_address, binary()} | {invalid_amount, binary()} | {invalid_gas, binary()} | {invalid_nonce, binary()} | {invalid_chain_id, binary()} | {invalid_data, binary()} | {signing_failed, binary()} | {encoding_failed, binary()}. -type decoded_transaction() :: {decoded_legacy, signed_transaction()} | {decoded_eip1559, signed_eip1559_transaction()}. -file("src/gleeth/crypto/transaction.gleam", 459). ?DOC( " Encode a hex string as raw bytes without stripping leading zeros.\n" " Used for address and data fields which are byte strings, not integers.\n" ). -spec encode_raw_hex(binary()) -> gleeth@encoding@rlp:rlp_item(). encode_raw_hex(Hex_string) -> case gleeth@utils@hex:decode(Hex_string) of {ok, Bytes} -> {rlp_bytes, Bytes}; {error, _} -> {rlp_bytes, <<>>} end. -file("src/gleeth/crypto/transaction.gleam", 365). ?DOC(" EIP-155 signing hash: keccak256(RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]))\n"). -spec create_signing_hash(unsigned_transaction()) -> bitstring(). create_signing_hash(Transaction) -> Items = {rlp_list, [gleeth@encoding@rlp:encode_hex_field(erlang:element(6, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(5, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(4, Transaction)), encode_raw_hex(erlang:element(2, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(3, Transaction)), encode_raw_hex(erlang:element(7, Transaction)), gleeth@encoding@rlp:encode_int(erlang:element(8, Transaction)), gleeth@encoding@rlp:encode_int(0), gleeth@encoding@rlp:encode_int(0)]}, gleeth@crypto@keccak:keccak256_binary(gleeth@encoding@rlp:encode(Items)). -file("src/gleeth/crypto/transaction.gleam", 452). -spec encode_access_list_entry(access_list_entry()) -> gleeth@encoding@rlp:rlp_item(). encode_access_list_entry(Entry) -> Storage_keys = gleam@list:map( erlang:element(3, Entry), fun encode_raw_hex/1 ), {rlp_list, [encode_raw_hex(erlang:element(2, Entry)), {rlp_list, Storage_keys}]}. -file("src/gleeth/crypto/transaction.gleam", 448). ?DOC(" Encode an access list as an RLP list of [address, [storageKeys]] entries\n"). -spec encode_access_list(list(access_list_entry())) -> gleeth@encoding@rlp:rlp_item(). encode_access_list(Entries) -> {rlp_list, gleam@list:map(Entries, fun encode_access_list_entry/1)}. -file("src/gleeth/crypto/transaction.gleam", 404). ?DOC(" EIP-1559 signing hash: keccak256(0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))\n"). -spec create_eip1559_signing_hash(eip1559_transaction()) -> bitstring(). create_eip1559_signing_hash(Transaction) -> Items = {rlp_list, [gleeth@encoding@rlp:encode_int(erlang:element(9, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(7, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(6, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(5, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(4, Transaction)), encode_raw_hex(erlang:element(2, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(3, Transaction)), encode_raw_hex(erlang:element(8, Transaction)), encode_access_list(erlang:element(10, Transaction))]}, Payload = gleeth@encoding@rlp:encode(Items), gleeth@crypto@keccak:keccak256_binary(<<16#02, Payload/bitstring>>). -file("src/gleeth/crypto/transaction.gleam", 467). ?DOC(" Strip leading zero bytes for minimal big-endian integer encoding\n"). -spec strip_leading_zeros(bitstring()) -> bitstring(). strip_leading_zeros(Data) -> case Data of <<0:8, Rest/bitstring>> -> strip_leading_zeros(Rest); _ -> Data end. -file("src/gleeth/crypto/transaction.gleam", 382). ?DOC(" RLP-encode the signed transaction: [nonce, gasPrice, gasLimit, to, value, data, v, r, s]\n"). -spec create_raw_transaction( unsigned_transaction(), integer(), bitstring(), bitstring() ) -> binary(). create_raw_transaction(Transaction, V, R, S) -> Items = {rlp_list, [gleeth@encoding@rlp:encode_hex_field(erlang:element(6, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(5, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(4, Transaction)), encode_raw_hex(erlang:element(2, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(3, Transaction)), encode_raw_hex(erlang:element(7, Transaction)), gleeth@encoding@rlp:encode_int(V), {rlp_bytes, strip_leading_zeros(R)}, {rlp_bytes, strip_leading_zeros(S)}]}, gleeth@utils@hex:encode(gleeth@encoding@rlp:encode(Items)). -file("src/gleeth/crypto/transaction.gleam", 278). ?DOC( " Sign a legacy (EIP-155) transaction with a wallet.\n" " Produces an RLP-encoded raw transaction suitable for eth_sendRawTransaction.\n" ). -spec sign_transaction(unsigned_transaction(), gleeth@crypto@wallet:wallet()) -> {ok, signed_transaction()} | {error, transaction_error()}. sign_transaction(Transaction, Wallet) -> Signing_hash = create_signing_hash(Transaction), gleam@result:'try'( begin _pipe = gleeth@crypto@wallet:sign_hash(Wallet, Signing_hash), gleam@result:map_error( _pipe, fun(Err) -> {signing_failed, <<"Failed to sign transaction: "/utf8, (gleeth@crypto@wallet:error_to_string(Err))/binary>>} end ) end, fun(Signature) -> Eip155_v = (erlang:element(4, Signature) + (2 * erlang:element( 8, Transaction ))) + 35, V_hex = <<"0x"/utf8, (string:lowercase(gleam@int:to_base16(Eip155_v)))/binary>>, R_hex = gleeth@utils@hex:encode(erlang:element(2, Signature)), S_hex = gleeth@utils@hex:encode(erlang:element(3, Signature)), Raw_tx = create_raw_transaction( Transaction, Eip155_v, erlang:element(2, Signature), erlang:element(3, Signature) ), {ok, {signed_transaction, erlang:element(2, Transaction), erlang:element(3, Transaction), erlang:element(4, Transaction), erlang:element(5, Transaction), erlang:element(6, Transaction), erlang:element(7, Transaction), erlang:element(8, Transaction), V_hex, R_hex, S_hex, Raw_tx}} end ). -file("src/gleeth/crypto/transaction.gleam", 422). ?DOC(" RLP-encode the signed EIP-1559 transaction: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s])\n"). -spec create_eip1559_raw_transaction( eip1559_transaction(), integer(), bitstring(), bitstring() ) -> binary(). create_eip1559_raw_transaction(Transaction, V, R, S) -> Items = {rlp_list, [gleeth@encoding@rlp:encode_int(erlang:element(9, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(7, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(6, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(5, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(4, Transaction)), encode_raw_hex(erlang:element(2, Transaction)), gleeth@encoding@rlp:encode_hex_field(erlang:element(3, Transaction)), encode_raw_hex(erlang:element(8, Transaction)), encode_access_list(erlang:element(10, Transaction)), gleeth@encoding@rlp:encode_int(V), {rlp_bytes, strip_leading_zeros(R)}, {rlp_bytes, strip_leading_zeros(S)}]}, Payload = gleeth@encoding@rlp:encode(Items), gleeth@utils@hex:encode(<<16#02, Payload/bitstring>>). -file("src/gleeth/crypto/transaction.gleam", 319). ?DOC( " Sign an EIP-1559 (Type 2) transaction with a wallet.\n" " Produces a type-prefixed RLP-encoded raw transaction: 0x02 || RLP([...]).\n" ). -spec sign_eip1559_transaction( eip1559_transaction(), gleeth@crypto@wallet:wallet() ) -> {ok, signed_eip1559_transaction()} | {error, transaction_error()}. sign_eip1559_transaction(Transaction, Wallet) -> Signing_hash = create_eip1559_signing_hash(Transaction), gleam@result:'try'( begin _pipe = gleeth@crypto@wallet:sign_hash(Wallet, Signing_hash), gleam@result:map_error( _pipe, fun(Err) -> {signing_failed, <<"Failed to sign transaction: "/utf8, (gleeth@crypto@wallet:error_to_string(Err))/binary>>} end ) end, fun(Signature) -> V = erlang:element(4, Signature), V_hex = <<"0x"/utf8, (string:lowercase(gleam@int:to_base16(V)))/binary>>, R_hex = gleeth@utils@hex:encode(erlang:element(2, Signature)), S_hex = gleeth@utils@hex:encode(erlang:element(3, Signature)), Raw_tx = create_eip1559_raw_transaction( Transaction, V, erlang:element(2, Signature), erlang:element(3, Signature) ), {ok, {signed_eip1559_transaction, erlang:element(2, Transaction), erlang:element(3, Transaction), erlang:element(4, Transaction), erlang:element(5, Transaction), erlang:element(6, Transaction), erlang:element(7, Transaction), erlang:element(8, Transaction), erlang:element(9, Transaction), erlang:element(10, Transaction), V_hex, R_hex, S_hex, Raw_tx}} end ). -file("src/gleeth/crypto/transaction.gleam", 531). ?DOC(" Validate chain ID\n"). -spec validate_chain_id(integer()) -> {ok, nil} | {error, transaction_error()}. validate_chain_id(Chain_id) -> case Chain_id > 0 of true -> {ok, nil}; false -> {error, {invalid_chain_id, <<"Chain ID must be positive"/utf8>>}} end. -file("src/gleeth/crypto/transaction.gleam", 539). ?DOC(" Check if character is valid hex\n"). -spec is_hex_char(binary()) -> boolean(). is_hex_char(Char) -> case Char of <<"0"/utf8>> -> true; <<"1"/utf8>> -> true; <<"2"/utf8>> -> true; <<"3"/utf8>> -> true; <<"4"/utf8>> -> true; <<"5"/utf8>> -> true; <<"6"/utf8>> -> true; <<"7"/utf8>> -> true; <<"8"/utf8>> -> true; <<"9"/utf8>> -> true; <<"a"/utf8>> -> true; <<"b"/utf8>> -> true; <<"c"/utf8>> -> true; <<"d"/utf8>> -> true; <<"e"/utf8>> -> true; <<"f"/utf8>> -> true; <<"A"/utf8>> -> true; <<"B"/utf8>> -> true; <<"C"/utf8>> -> true; <<"D"/utf8>> -> true; <<"E"/utf8>> -> true; <<"F"/utf8>> -> true; _ -> false end. -file("src/gleeth/crypto/transaction.gleam", 479). ?DOC(" Validate Ethereum address format\n"). -spec validate_address(binary()) -> {ok, nil} | {error, transaction_error()}. validate_address(Address) -> Cleaned = case gleam_stdlib:string_starts_with(Address, <<"0x"/utf8>>) of true -> gleam@string:drop_start(Address, 2); false -> Address end, case string:length(Cleaned) of 40 -> case begin _pipe = gleam@string:to_graphemes(Cleaned), gleam@list:all(_pipe, fun is_hex_char/1) end of true -> {ok, nil}; false -> {error, {invalid_address, <<"Address contains non-hex characters"/utf8>>}} end; 0 -> {ok, nil}; _ -> {error, {invalid_address, <<"Address must be 40 hex characters"/utf8>>}} end. -file("src/gleeth/crypto/transaction.gleam", 499). ?DOC(" Validate hex amount (like value, gas, nonce)\n"). -spec validate_hex_amount(binary()) -> {ok, nil} | {error, transaction_error()}. validate_hex_amount(Amount) -> Cleaned = case gleam_stdlib:string_starts_with(Amount, <<"0x"/utf8>>) of true -> gleam@string:drop_start(Amount, 2); false -> Amount end, case begin _pipe = gleam@string:to_graphemes(Cleaned), gleam@list:all(_pipe, fun is_hex_char/1) end of true -> {ok, nil}; false -> {error, {invalid_amount, <<"Amount contains non-hex characters"/utf8>>}} end. -file("src/gleeth/crypto/transaction.gleam", 512). ?DOC(" Validate hex data\n"). -spec validate_hex_data(binary()) -> {ok, nil} | {error, transaction_error()}. validate_hex_data(Data) -> case Data of <<"0x"/utf8>> -> {ok, nil}; _ -> Cleaned = case gleam_stdlib:string_starts_with(Data, <<"0x"/utf8>>) of true -> gleam@string:drop_start(Data, 2); false -> Data end, case begin _pipe = gleam@string:to_graphemes(Cleaned), gleam@list:all(_pipe, fun is_hex_char/1) end of true -> {ok, nil}; false -> {error, {invalid_data, <<"Data contains non-hex characters"/utf8>>}} end end. -file("src/gleeth/crypto/transaction.gleam", 553). ?DOC(" Normalize address to lowercase with 0x prefix\n"). -spec normalize_address(binary()) -> binary(). normalize_address(Address) -> case Address of <<""/utf8>> -> <<""/utf8>>; _ -> Cleaned = case gleam_stdlib:string_starts_with( Address, <<"0x"/utf8>> ) of true -> gleam@string:drop_start(Address, 2); false -> Address end, <<"0x"/utf8, (string:lowercase(Cleaned))/binary>> end. -file("src/gleeth/crypto/transaction.gleam", 567). ?DOC(" Normalize hex string to lowercase with 0x prefix\n"). -spec normalize_hex(binary()) -> binary(). normalize_hex(Hex) -> Cleaned = case gleam_stdlib:string_starts_with(Hex, <<"0x"/utf8>>) of true -> gleam@string:drop_start(Hex, 2); false -> Hex end, <<"0x"/utf8, (string:lowercase(Cleaned))/binary>>. -file("src/gleeth/crypto/transaction.gleam", 576). ?DOC(" Normalize hex data (keep \"0x\" for empty data)\n"). -spec normalize_hex_data(binary()) -> binary(). normalize_hex_data(Data) -> case Data of <<""/utf8>> -> <<"0x"/utf8>>; <<"0x"/utf8>> -> <<"0x"/utf8>>; _ -> normalize_hex(Data) end. -file("src/gleeth/crypto/transaction.gleam", 148). ?DOC( " Create a new unsigned legacy (Type 0) transaction.\n" "\n" " All numeric parameters must be `0x`-prefixed hex strings. The Ethereum\n" " JSON-RPC API returns values in this format, so results from\n" " `methods.get_gas_price`, `methods.get_transaction_count`, etc. can be\n" " passed directly.\n" "\n" " - `to` - recipient address (`\"0x...\"`, 40 hex chars after prefix)\n" " - `value` - amount in wei as hex (e.g. `\"0xde0b6b3a7640000\"` for 1 ETH)\n" " - `gas_limit` - gas limit as hex (e.g. `\"0x5208\"` for 21000)\n" " - `gas_price` - gas price in wei as hex\n" " - `nonce` - sender's transaction count as hex\n" " - `data` - calldata as hex (`\"0x\"` for simple transfers)\n" " - `chain_id` - network chain ID as integer (1 for mainnet, 11155111 for Sepolia)\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let assert Ok(tx) = transaction.create_legacy_transaction(\n" " \"0x70997970C51812dc3A010C7d01b50e0d17dc79C8\",\n" " \"0xde0b6b3a7640000\", // 1 ETH in wei\n" " \"0x5208\", // 21000 gas\n" " \"0x3b9aca00\", // 1 gwei gas price\n" " \"0x0\", // nonce 0\n" " \"0x\", // no calldata\n" " 1, // mainnet\n" " )\n" " ```\n" ). -spec create_legacy_transaction( binary(), binary(), binary(), binary(), binary(), binary(), integer() ) -> {ok, unsigned_transaction()} | {error, transaction_error()}. create_legacy_transaction( To, Value, Gas_limit, Gas_price, Nonce, Data, Chain_id ) -> gleam@result:'try'( validate_address(To), fun(_) -> gleam@result:'try'( validate_hex_amount(Value), fun(_) -> gleam@result:'try'( validate_hex_amount(Gas_limit), fun(_) -> gleam@result:'try'( validate_hex_amount(Gas_price), fun(_) -> gleam@result:'try'( validate_hex_amount(Nonce), fun(_) -> gleam@result:'try'( validate_hex_data(Data), fun(_) -> gleam@result:'try'( validate_chain_id( Chain_id ), fun(_) -> {ok, {unsigned_transaction, normalize_address( To ), normalize_hex( Value ), normalize_hex( Gas_limit ), normalize_hex( Gas_price ), normalize_hex( Nonce ), normalize_hex_data( Data ), Chain_id}} end ) end ) end ) end ) end ) end ) end ). -file("src/gleeth/crypto/transaction.gleam", 181). ?DOC( " Create a simple ETH transfer transaction (no calldata).\n" "\n" " Convenience wrapper around `create_legacy_transaction` with `data` set to\n" " `\"0x\"`. All numeric parameters are `0x`-prefixed hex strings.\n" ). -spec create_eth_transfer( binary(), binary(), binary(), binary(), binary(), integer() ) -> {ok, unsigned_transaction()} | {error, transaction_error()}. create_eth_transfer(To, Value_wei, Gas_limit, Gas_price, Nonce, Chain_id) -> create_legacy_transaction( To, Value_wei, Gas_limit, Gas_price, Nonce, <<"0x"/utf8>>, Chain_id ). -file("src/gleeth/crypto/transaction.gleam", 204). ?DOC( " Create a contract interaction transaction (zero value).\n" "\n" " Convenience wrapper around `create_legacy_transaction` with `value` set to\n" " `\"0x0\"`. All numeric parameters are `0x`-prefixed hex strings.\n" ). -spec create_contract_call( binary(), binary(), binary(), binary(), binary(), integer() ) -> {ok, unsigned_transaction()} | {error, transaction_error()}. create_contract_call( Contract_address, Call_data, Gas_limit, Gas_price, Nonce, Chain_id ) -> create_legacy_transaction( Contract_address, <<"0x0"/utf8>>, Gas_limit, Gas_price, Nonce, Call_data, Chain_id ). -file("src/gleeth/crypto/transaction.gleam", 239). ?DOC( " Create and validate an EIP-1559 (Type 2) transaction.\n" "\n" " All numeric parameters must be `0x`-prefixed hex strings. Fee parameters\n" " from `methods.get_gas_price` and `methods.get_max_priority_fee` can be\n" " passed directly.\n" "\n" " - `to` - recipient address\n" " - `value` - amount in wei as hex\n" " - `gas_limit` - gas limit as hex\n" " - `max_fee_per_gas` - maximum total fee per gas in wei as hex\n" " - `max_priority_fee_per_gas` - tip per gas in wei as hex\n" " - `nonce` - sender's transaction count as hex\n" " - `data` - calldata as hex (`\"0x\"` for simple transfers)\n" " - `chain_id` - network chain ID as integer\n" " - `access_list` - EIP-2930 access list (pass `[]` if not needed)\n" ). -spec create_eip1559_transaction( binary(), binary(), binary(), binary(), binary(), binary(), binary(), integer(), list(access_list_entry()) ) -> {ok, eip1559_transaction()} | {error, transaction_error()}. create_eip1559_transaction( To, Value, Gas_limit, Max_fee_per_gas, Max_priority_fee_per_gas, Nonce, Data, Chain_id, Access_list ) -> gleam@result:'try'( validate_address(To), fun(_) -> gleam@result:'try'( validate_hex_amount(Value), fun(_) -> gleam@result:'try'( validate_hex_amount(Gas_limit), fun(_) -> gleam@result:'try'( validate_hex_amount(Max_fee_per_gas), fun(_) -> gleam@result:'try'( validate_hex_amount( Max_priority_fee_per_gas ), fun(_) -> gleam@result:'try'( validate_hex_amount(Nonce), fun(_) -> gleam@result:'try'( validate_hex_data(Data), fun(_) -> gleam@result:'try'( validate_chain_id( Chain_id ), fun(_) -> {ok, {eip1559_transaction, normalize_address( To ), normalize_hex( Value ), normalize_hex( Gas_limit ), normalize_hex( Max_fee_per_gas ), normalize_hex( Max_priority_fee_per_gas ), normalize_hex( Nonce ), normalize_hex_data( Data ), Chain_id, Access_list}} end ) end ) end ) end ) end ) end ) end ) end ). -file("src/gleeth/crypto/transaction.gleam", 589). ?DOC(" Convert signed transaction to string for display\n"). -spec signed_transaction_to_string(signed_transaction()) -> binary(). signed_transaction_to_string(Tx) -> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"SignedTransaction {\n"/utf8, " to: "/utf8>>/binary, (erlang:element( 2, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " value: "/utf8>>/binary, (erlang:element( 3, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " gas_limit: "/utf8>>/binary, (erlang:element( 4, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " gas_price: "/utf8>>/binary, (erlang:element( 5, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " nonce: "/utf8>>/binary, (erlang:element( 6, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " data: "/utf8>>/binary, (erlang:element( 7, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " chain_id: "/utf8>>/binary, (erlang:integer_to_binary( erlang:element( 8, Tx ) ))/binary>>/binary, "\n"/utf8>>/binary, " v: "/utf8>>/binary, (erlang:element(9, Tx))/binary>>/binary, "\n"/utf8>>/binary, " r: "/utf8>>/binary, (erlang:element(10, Tx))/binary>>/binary, "\n"/utf8>>/binary, " s: "/utf8>>/binary, (erlang:element(11, Tx))/binary>>/binary, "\n"/utf8>>/binary, " raw: "/utf8>>/binary, (erlang:element(12, Tx))/binary>>/binary, "\n"/utf8>>/binary, "}"/utf8>>. -file("src/gleeth/crypto/transaction.gleam", 638). ?DOC(" Hash a raw transaction hex string to get its transaction hash\n"). -spec hash_raw_transaction(binary()) -> binary(). hash_raw_transaction(Raw_transaction) -> case gleeth@utils@hex:decode(Raw_transaction) of {ok, Raw_bytes} -> gleeth@utils@hex:encode( gleeth@crypto@keccak:keccak256_binary(Raw_bytes) ); {error, _} -> <<""/utf8>> end. -file("src/gleeth/crypto/transaction.gleam", 628). ?DOC(" Get transaction hash from a signed legacy transaction\n"). -spec get_transaction_hash(signed_transaction()) -> binary(). get_transaction_hash(Tx) -> hash_raw_transaction(erlang:element(12, Tx)). -file("src/gleeth/crypto/transaction.gleam", 633). ?DOC(" Get transaction hash from a signed EIP-1559 transaction\n"). -spec get_eip1559_transaction_hash(signed_eip1559_transaction()) -> binary(). get_eip1559_transaction_hash(Tx) -> hash_raw_transaction(erlang:element(14, Tx)). -file("src/gleeth/crypto/transaction.gleam", 646). ?DOC(" Convert signed EIP-1559 transaction to string for display\n"). -spec signed_eip1559_transaction_to_string(signed_eip1559_transaction()) -> binary(). signed_eip1559_transaction_to_string(Tx) -> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"SignedEip1559Transaction {\n"/utf8, " to: "/utf8>>/binary, (erlang:element( 2, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " value: "/utf8>>/binary, (erlang:element( 3, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " gas_limit: "/utf8>>/binary, (erlang:element( 4, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " max_fee_per_gas: "/utf8>>/binary, (erlang:element( 5, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " max_priority_fee_per_gas: "/utf8>>/binary, (erlang:element( 6, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " nonce: "/utf8>>/binary, (erlang:element( 7, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " data: "/utf8>>/binary, (erlang:element( 8, Tx ))/binary>>/binary, "\n"/utf8>>/binary, " chain_id: "/utf8>>/binary, (erlang:integer_to_binary( erlang:element( 9, Tx ) ))/binary>>/binary, "\n"/utf8>>/binary, " v: "/utf8>>/binary, (erlang:element(11, Tx))/binary>>/binary, "\n"/utf8>>/binary, " r: "/utf8>>/binary, (erlang:element(12, Tx))/binary>>/binary, "\n"/utf8>>/binary, " s: "/utf8>>/binary, (erlang:element(13, Tx))/binary>>/binary, "\n"/utf8>>/binary, " raw: "/utf8>>/binary, (erlang:element(14, Tx))/binary>>/binary, "\n"/utf8>>/binary, "}"/utf8>>. -file("src/gleeth/crypto/transaction.gleam", 690). ?DOC(" Convert TransactionError to string\n"). -spec error_to_string(transaction_error()) -> binary(). error_to_string(Error) -> case Error of {invalid_address, Msg} -> <<"Invalid address: "/utf8, Msg/binary>>; {invalid_amount, Msg@1} -> <<"Invalid amount: "/utf8, Msg@1/binary>>; {invalid_gas, Msg@2} -> <<"Invalid gas: "/utf8, Msg@2/binary>>; {invalid_nonce, Msg@3} -> <<"Invalid nonce: "/utf8, Msg@3/binary>>; {invalid_chain_id, Msg@4} -> <<"Invalid chain ID: "/utf8, Msg@4/binary>>; {invalid_data, Msg@5} -> <<"Invalid data: "/utf8, Msg@5/binary>>; {signing_failed, Msg@6} -> <<"Signing failed: "/utf8, Msg@6/binary>>; {encoding_failed, Msg@7} -> <<"Encoding failed: "/utf8, Msg@7/binary>> end. -file("src/gleeth/crypto/transaction.gleam", 953). ?DOC(" Convert RLP bytes to an address string (0x-prefixed, full 40 chars)\n"). -spec rlp_to_address(gleeth@encoding@rlp:rlp_item()) -> binary(). rlp_to_address(Item) -> case Item of {rlp_bytes, <<>>} -> <<""/utf8>>; {rlp_bytes, Bytes} -> <<"0x"/utf8, (string:lowercase(gleam_stdlib:base16_encode(Bytes)))/binary>>; _ -> <<""/utf8>> end. -file("src/gleeth/crypto/transaction.gleam", 915). -spec decode_access_list_entry(gleeth@encoding@rlp:rlp_item()) -> {ok, access_list_entry()} | {error, nil}. decode_access_list_entry(Item) -> case Item of {rlp_list, [Addr_rlp, Keys_rlp]} -> Address = rlp_to_address(Addr_rlp), Storage_keys = case Keys_rlp of {rlp_list, Keys} -> gleam@list:map(Keys, fun(K) -> case K of {rlp_bytes, Bytes} -> <<"0x"/utf8, (string:lowercase( gleam_stdlib:base16_encode(Bytes) ))/binary>>; _ -> <<"0x"/utf8>> end end); _ -> [] end, {ok, {access_list_entry, Address, Storage_keys}}; _ -> {error, nil} end. -file("src/gleeth/crypto/transaction.gleam", 908). -spec decode_access_list(gleeth@encoding@rlp:rlp_item()) -> list(access_list_entry()). decode_access_list(Item) -> case Item of {rlp_list, Entries} -> gleam@list:filter_map(Entries, fun decode_access_list_entry/1); _ -> [] end. -file("src/gleeth/crypto/transaction.gleam", 963). ?DOC(" Convert RLP bytes to hex data string (0x-prefixed, preserves all bytes)\n"). -spec rlp_to_hex_data(gleeth@encoding@rlp:rlp_item()) -> binary(). rlp_to_hex_data(Item) -> case Item of {rlp_bytes, <<>>} -> <<"0x"/utf8>>; {rlp_bytes, Bytes} -> <<"0x"/utf8, (string:lowercase(gleam_stdlib:base16_encode(Bytes)))/binary>>; _ -> <<"0x"/utf8>> end. -file("src/gleeth/crypto/transaction.gleam", 973). ?DOC(" Convert RLP bytes to hex string preserving all bytes (for r, s values)\n"). -spec rlp_to_hex_bytes(gleeth@encoding@rlp:rlp_item()) -> binary(). rlp_to_hex_bytes(Item) -> case Item of {rlp_bytes, Bytes} -> <<"0x"/utf8, (string:lowercase(gleam_stdlib:base16_encode(Bytes)))/binary>>; _ -> <<"0x"/utf8>> end. -file("src/gleeth/crypto/transaction.gleam", 995). -spec do_bytes_to_int(bitstring(), integer()) -> integer(). do_bytes_to_int(Data, Acc) -> case Data of <> -> do_bytes_to_int(Rest, (Acc * 256) + Byte); _ -> Acc end. -file("src/gleeth/crypto/transaction.gleam", 991). ?DOC(" Convert big-endian bytes to an integer\n"). -spec bytes_to_int(bitstring()) -> integer(). bytes_to_int(Data) -> do_bytes_to_int(Data, 0). -file("src/gleeth/crypto/transaction.gleam", 982). ?DOC(" Convert RLP bytes to an integer\n"). -spec rlp_to_int(gleeth@encoding@rlp:rlp_item()) -> integer(). rlp_to_int(Item) -> case Item of {rlp_bytes, <<>>} -> 0; {rlp_bytes, Bytes} -> bytes_to_int(Bytes); _ -> 0 end. -file("src/gleeth/crypto/transaction.gleam", 1003). ?DOC(" Drop leading zeros from a hex string (but keep at least one digit)\n"). -spec drop_leading_hex_zeros(binary()) -> binary(). drop_leading_hex_zeros(Hex_str) -> case Hex_str of <<"0"/utf8, Rest/binary>> -> case Rest of <<""/utf8>> -> <<"0"/utf8>>; _ -> drop_leading_hex_zeros(Rest) end; _ -> Hex_str end. -file("src/gleeth/crypto/transaction.gleam", 941). ?DOC(" Convert RLP bytes to a hex amount string (0x-prefixed, minimal encoding)\n"). -spec rlp_to_hex_amount(gleeth@encoding@rlp:rlp_item()) -> binary(). rlp_to_hex_amount(Item) -> case Item of {rlp_bytes, <<>>} -> <<"0x0"/utf8>>; {rlp_bytes, Bytes} -> Hex_str = string:lowercase(gleam_stdlib:base16_encode(Bytes)), <<"0x"/utf8, (drop_leading_hex_zeros(Hex_str))/binary>>; _ -> <<"0x0"/utf8>> end. -file("src/gleeth/crypto/transaction.gleam", 774). -spec decode_legacy_items(list(gleeth@encoding@rlp:rlp_item()), binary()) -> {ok, signed_transaction()} | {error, transaction_error()}. decode_legacy_items(Items, Raw_hex) -> case Items of [Nonce_rlp, Gas_price_rlp, Gas_limit_rlp, To_rlp, Value_rlp, Data_rlp, V_rlp, R_rlp, S_rlp] -> Nonce = rlp_to_hex_amount(Nonce_rlp), Gas_price = rlp_to_hex_amount(Gas_price_rlp), Gas_limit = rlp_to_hex_amount(Gas_limit_rlp), To = rlp_to_address(To_rlp), Value = rlp_to_hex_amount(Value_rlp), Data = rlp_to_hex_data(Data_rlp), V_int = rlp_to_int(V_rlp), R = rlp_to_hex_bytes(R_rlp), S = rlp_to_hex_bytes(S_rlp), Chain_id = (V_int - 35) div 2, V_hex = <<"0x"/utf8, (string:lowercase(gleam@int:to_base16(V_int)))/binary>>, {ok, {signed_transaction, To, Value, Gas_limit, Gas_price, Nonce, Data, Chain_id, V_hex, R, S, Raw_hex}}; _ -> {error, {encoding_failed, <<"Legacy transaction must have 9 RLP items, got "/utf8, (erlang:integer_to_binary(erlang:length(Items)))/binary>>}} end. -file("src/gleeth/crypto/transaction.gleam", 757). ?DOC( " Decode a raw signed legacy transaction from its RLP-encoded hex string.\n" " Recovers chain_id from the EIP-155 v value: chain_id = (v - 35) / 2.\n" ). -spec decode_legacy(binary()) -> {ok, signed_transaction()} | {error, transaction_error()}. decode_legacy(Raw_hex) -> gleam@result:'try'( begin _pipe = gleeth@utils@hex:decode(Raw_hex), gleam@result:map_error( _pipe, fun(_) -> {encoding_failed, <<"Invalid hex string"/utf8>>} end ) end, fun(Raw_bytes) -> gleam@result:'try'( begin _pipe@1 = gleeth@encoding@rlp:decode(Raw_bytes), gleam@result:map_error( _pipe@1, fun(_) -> {encoding_failed, <<"Invalid RLP encoding"/utf8>>} end ) end, fun(Rlp_item) -> case Rlp_item of {rlp_list, Items} -> decode_legacy_items(Items, Raw_hex); _ -> {error, {encoding_failed, <<"Expected RLP list for legacy transaction"/utf8>>}} end end ) end ). -file("src/gleeth/crypto/transaction.gleam", 851). -spec decode_eip1559_items(list(gleeth@encoding@rlp:rlp_item()), binary()) -> {ok, signed_eip1559_transaction()} | {error, transaction_error()}. decode_eip1559_items(Items, Raw_hex) -> case Items of [Chain_id_rlp, Nonce_rlp, Max_priority_fee_rlp, Max_fee_rlp, Gas_limit_rlp, To_rlp, Value_rlp, Data_rlp, Access_list_rlp, V_rlp, R_rlp, S_rlp] -> Chain_id = rlp_to_int(Chain_id_rlp), Nonce = rlp_to_hex_amount(Nonce_rlp), Max_priority_fee = rlp_to_hex_amount(Max_priority_fee_rlp), Max_fee = rlp_to_hex_amount(Max_fee_rlp), Gas_limit = rlp_to_hex_amount(Gas_limit_rlp), To = rlp_to_address(To_rlp), Value = rlp_to_hex_amount(Value_rlp), Data = rlp_to_hex_data(Data_rlp), Access_list = decode_access_list(Access_list_rlp), V_int = rlp_to_int(V_rlp), V_hex = <<"0x"/utf8, (string:lowercase(gleam@int:to_base16(V_int)))/binary>>, R = rlp_to_hex_bytes(R_rlp), S = rlp_to_hex_bytes(S_rlp), {ok, {signed_eip1559_transaction, To, Value, Gas_limit, Max_fee, Max_priority_fee, Nonce, Data, Chain_id, Access_list, V_hex, R, S, Raw_hex}}; _ -> {error, {encoding_failed, <<"EIP-1559 transaction must have 12 RLP items, got "/utf8, (erlang:integer_to_binary(erlang:length(Items)))/binary>>}} end. -file("src/gleeth/crypto/transaction.gleam", 829). ?DOC( " Decode a raw signed EIP-1559 (Type 2) transaction from its hex string.\n" " The input must start with 0x02 (the type prefix).\n" ). -spec decode_eip1559(binary()) -> {ok, signed_eip1559_transaction()} | {error, transaction_error()}. decode_eip1559(Raw_hex) -> gleam@result:'try'( begin _pipe = gleeth@utils@hex:decode(Raw_hex), gleam@result:map_error( _pipe, fun(_) -> {encoding_failed, <<"Invalid hex string"/utf8>>} end ) end, fun(Raw_bytes) -> case Raw_bytes of <<16#02, Payload/bitstring>> -> gleam@result:'try'( begin _pipe@1 = gleeth@encoding@rlp:decode(Payload), gleam@result:map_error( _pipe@1, fun(_) -> {encoding_failed, <<"Invalid RLP encoding"/utf8>>} end ) end, fun(Rlp_item) -> case Rlp_item of {rlp_list, Items} -> decode_eip1559_items(Items, Raw_hex); _ -> {error, {encoding_failed, <<"Expected RLP list for EIP-1559 transaction"/utf8>>}} end end ); _ -> {error, {encoding_failed, <<"EIP-1559 transaction must start with 0x02"/utf8>>}} end end ). -file("src/gleeth/crypto/transaction.gleam", 738). ?DOC( " Decode a raw transaction hex string, auto-detecting the type.\n" " Legacy transactions start with an RLP list prefix (0xc0-0xff).\n" " EIP-1559 transactions start with 0x02.\n" ). -spec decode(binary()) -> {ok, decoded_transaction()} | {error, transaction_error()}. decode(Raw_hex) -> gleam@result:'try'( begin _pipe = gleeth@utils@hex:decode(Raw_hex), gleam@result:map_error( _pipe, fun(_) -> {encoding_failed, <<"Invalid hex string"/utf8>>} end ) end, fun(Raw_bytes) -> case Raw_bytes of <<16#02, _/bitstring>> -> gleam@result:map( decode_eip1559(Raw_hex), fun(Tx) -> {decoded_eip1559, Tx} end ); _ -> gleam@result:map( decode_legacy(Raw_hex), fun(Tx@1) -> {decoded_legacy, Tx@1} end ) end end ).