-module(gleeth@crypto@secp256k1). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gleeth/crypto/secp256k1.gleam"). -export([private_key_from_bytes/1, private_key_from_hex/1, private_key_to_hex/1, private_key_to_bytes/1, create_public_key/1, public_key_to_hex/1, public_key_to_bytes/1, public_key_to_address/1, private_key_to_address/1, address_to_string/1, sign_message_hash/2, sign_message/2, sign_personal_message/2, signature_to_compact/1, signature_to_hex/1, signature_to_vrs/1, signature_from_vrs/3, verify_signature/3, is_valid_private_key/1, generate_private_key/0, recover_public_key/2, recover_public_key_candidates/3, recover_address/2, recover_address_candidates/3, verify_signature_recovery/3, find_recovery_id/4, recover_public_key_compact/3, recover_address_compact/3]). -export_type([signature/0, private_key/0, public_key/0, ethereum_address/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 signature() :: {signature, bitstring(), bitstring(), integer()}. -type private_key() :: {private_key, bitstring()}. -type public_key() :: {public_key, bitstring()}. -type ethereum_address() :: {ethereum_address, binary()}. -file("src/gleeth/crypto/secp256k1.gleam", 34). ?DOC( " Create a private key from a 32-byte BitArray\n" " The private key must be exactly 32 bytes (256 bits)\n" ). -spec private_key_from_bytes(bitstring()) -> {ok, private_key()} | {error, binary()}. private_key_from_bytes(Bytes) -> case erlang:byte_size(Bytes) of 32 -> {ok, {private_key, Bytes}}; Size -> {error, <<"Private key must be exactly 32 bytes, got "/utf8, (gleam@string:inspect(Size))/binary>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 46). ?DOC( " Create a private key from a hex string (with or without 0x prefix)\n" " The hex string must represent exactly 32 bytes (64 hex characters)\n" ). -spec private_key_from_hex(binary()) -> {ok, private_key()} | {error, binary()}. private_key_from_hex(Hex_string) -> gleam@result:'try'( gleeth@utils@hex:decode(Hex_string), fun(Bytes) -> private_key_from_bytes(Bytes) end ). -file("src/gleeth/crypto/secp256k1.gleam", 52). ?DOC(" Convert a private key to hex string with 0x prefix\n"). -spec private_key_to_hex(private_key()) -> binary(). private_key_to_hex(Private_key) -> {private_key, Key} = Private_key, <<"0x"/utf8, (begin _pipe = gleam_stdlib:base16_encode(Key), string:lowercase(_pipe) end)/binary>>. -file("src/gleeth/crypto/secp256k1.gleam", 58). ?DOC(" Extract the raw bytes from a private key\n"). -spec private_key_to_bytes(private_key()) -> bitstring(). private_key_to_bytes(Private_key) -> {private_key, Key} = Private_key, Key. -file("src/gleeth/crypto/secp256k1.gleam", 68). ?DOC(" Create a public key from a private key\n"). -spec create_public_key(private_key()) -> {ok, public_key()} | {error, binary()}. create_public_key(Private_key) -> {private_key, Key} = Private_key, case 'Elixir.ExSecp256k1':create_public_key(Key) of {ok, Pub_key} -> {ok, {public_key, Pub_key}}; {error, Err} -> {error, <<"Failed to create public key: "/utf8, (gleam@string:inspect(Err))/binary>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 77). ?DOC(" Convert a public key to hex string with 0x prefix\n"). -spec public_key_to_hex(public_key()) -> binary(). public_key_to_hex(Public_key) -> {public_key, Key} = Public_key, <<"0x"/utf8, (begin _pipe = gleam_stdlib:base16_encode(Key), string:lowercase(_pipe) end)/binary>>. -file("src/gleeth/crypto/secp256k1.gleam", 83). ?DOC(" Extract the raw bytes from a public key\n"). -spec public_key_to_bytes(public_key()) -> bitstring(). public_key_to_bytes(Public_key) -> {public_key, Key} = Public_key, Key. -file("src/gleeth/crypto/secp256k1.gleam", 94). ?DOC( " Generate an Ethereum address from a public key\n" " Uses the last 20 bytes of keccak256(public_key) as the address\n" ). -spec public_key_to_address(public_key()) -> {ok, ethereum_address()} | {error, binary()}. public_key_to_address(Public_key) -> {public_key, Key} = Public_key, Public_key_bytes = case erlang:byte_size(Key) of 65 -> case gleam_stdlib:bit_array_slice(Key, 1, 64) of {ok, Sliced} -> Sliced; {error, _} -> Key end; 64 -> Key; _ -> Key end, Hash = gleeth@crypto@keccak:keccak256_binary(Public_key_bytes), case gleam_stdlib:bit_array_slice(Hash, 12, 20) of {ok, Address_bytes} -> Address_hex = begin _pipe = gleam_stdlib:base16_encode(Address_bytes), string:lowercase(_pipe) end, {ok, {ethereum_address, <<"0x"/utf8, Address_hex/binary>>}}; {error, _} -> {error, <<"Failed to extract address from hash"/utf8>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 129). ?DOC(" Generate an Ethereum address directly from a private key\n"). -spec private_key_to_address(private_key()) -> {ok, ethereum_address()} | {error, binary()}. private_key_to_address(Private_key) -> gleam@result:'try'( create_public_key(Private_key), fun(Public_key) -> public_key_to_address(Public_key) end ). -file("src/gleeth/crypto/secp256k1.gleam", 137). ?DOC(" Extract the address string from an EthereumAddress\n"). -spec address_to_string(ethereum_address()) -> binary(). address_to_string(Address) -> {ethereum_address, Addr} = Address, Addr. -file("src/gleeth/crypto/secp256k1.gleam", 148). ?DOC( " Sign a message hash with a private key\n" " The message should already be hashed (e.g., with keccak256)\n" ). -spec sign_message_hash(bitstring(), private_key()) -> {ok, signature()} | {error, binary()}. sign_message_hash(Message_hash, Private_key) -> {private_key, Key} = Private_key, case secp256k1_gleam:sign(Message_hash, Key) of {ok, Sig} -> {ok, {signature, erlang:element(2, Sig), erlang:element(3, Sig), erlang:element(4, Sig)}}; {error, Err} -> {error, <<"Failed to sign message: "/utf8, (gleam@string:inspect(Err))/binary>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 161). ?DOC(" Sign raw message bytes (will be hashed with keccak256)\n"). -spec sign_message(bitstring(), private_key()) -> {ok, signature()} | {error, binary()}. sign_message(Message, Private_key) -> Message_hash = gleeth@crypto@keccak:keccak256_binary(Message), sign_message_hash(Message_hash, Private_key). -file("src/gleeth/crypto/secp256k1.gleam", 170). ?DOC(" Sign an Ethereum personal message (prefixed with \"\\x19Ethereum Signed Message:\\n\")\n"). -spec sign_personal_message(binary(), private_key()) -> {ok, signature()} | {error, binary()}. sign_personal_message(Message, Private_key) -> Message_bytes = gleam_stdlib:identity(Message), Message_length = begin _pipe = erlang:byte_size(Message_bytes), gleam@string:inspect(_pipe) end, Prefix = <<"\\x19Ethereum Signed Message:\\n"/utf8, Message_length/binary>>, Prefix_bytes = gleam_stdlib:identity(Prefix), Full_message = gleam@bit_array:append(Prefix_bytes, Message_bytes), sign_message(Full_message, Private_key). -file("src/gleeth/crypto/secp256k1.gleam", 190). ?DOC( " Convert signature to compact format (r + s + v)\n" " Used for Ethereum transaction signatures\n" ). -spec signature_to_compact(signature()) -> bitstring(). signature_to_compact(Signature) -> {signature, R, S, Recovery_id} = Signature, V_byte = case Recovery_id of 0 -> <<27>>; 1 -> <<28>>; _ -> <<27>> end, <>. -file("src/gleeth/crypto/secp256k1.gleam", 205). ?DOC(" Convert signature to hex string with 0x prefix\n"). -spec signature_to_hex(signature()) -> binary(). signature_to_hex(Signature) -> Compact = signature_to_compact(Signature), <<"0x"/utf8, (begin _pipe = gleam_stdlib:base16_encode(Compact), string:lowercase(_pipe) end)/binary>>. -file("src/gleeth/crypto/secp256k1.gleam", 211). ?DOC(" Extract v, r, s components for Ethereum transactions\n"). -spec signature_to_vrs(signature()) -> {integer(), binary(), binary()}. signature_to_vrs(Signature) -> {signature, R, S, Recovery_id} = Signature, V = Recovery_id + 27, R_hex = <<"0x"/utf8, (begin _pipe = gleam_stdlib:base16_encode(R), string:lowercase(_pipe) end)/binary>>, S_hex = <<"0x"/utf8, (begin _pipe@1 = gleam_stdlib:base16_encode(S), string:lowercase(_pipe@1) end)/binary>>, {V, R_hex, S_hex}. -file("src/gleeth/crypto/secp256k1.gleam", 223). ?DOC(" Create signature from v, r, s components\n"). -spec signature_from_vrs(integer(), binary(), binary()) -> {ok, signature()} | {error, binary()}. signature_from_vrs(V, R, S) -> gleam@result:'try'( gleeth@utils@hex:decode(R), fun(R_bytes) -> gleam@result:'try'( gleeth@utils@hex:decode(S), fun(S_bytes) -> Recovery_id = case V of 27 -> 0; 28 -> 1; _ -> V - 27 end, {ok, {signature, R_bytes, S_bytes, Recovery_id}} end ) end ). -file("src/gleeth/crypto/secp256k1.gleam", 246). ?DOC(" Verify a signature against a message hash and public key\n"). -spec verify_signature(bitstring(), signature(), public_key()) -> {ok, boolean()} | {error, binary()}. verify_signature(Message_hash, Signature, Public_key) -> {signature, R, S, _} = Signature, {public_key, Key} = Public_key, Signature_obj = {signature, R, S, erlang:element(4, Signature)}, case secp256k1_gleam:verify(Message_hash, Signature_obj, Key) of {ok, _} -> {ok, true}; {error, _} -> {ok, false} end. -file("src/gleeth/crypto/secp256k1.gleam", 344). ?DOC(" Helper to convert public keys to addresses\n"). -spec convert_keys_to_addresses(list(public_key()), list(ethereum_address())) -> {ok, list(ethereum_address())} | {error, binary()}. convert_keys_to_addresses(Public_keys, Acc) -> case Public_keys of [] -> {ok, Acc}; [Public_key | Rest] -> case public_key_to_address(Public_key) of {ok, Address} -> convert_keys_to_addresses(Rest, [Address | Acc]); {error, Err} -> {error, Err} end end. -file("src/gleeth/crypto/secp256k1.gleam", 418). ?DOC(" Check if a private key is valid (non-zero and within secp256k1 curve order)\n"). -spec is_valid_private_key(private_key()) -> boolean(). is_valid_private_key(Private_key) -> {private_key, Key} = Private_key, case erlang:byte_size(Key) of 32 -> Key /= <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>; _ -> false end. -file("src/gleeth/crypto/secp256k1.gleam", 435). ?DOC( " Generate a random private key (placeholder - would need proper randomness)\n" " This is a placeholder implementation - in production, use proper cryptographic randomness\n" ). -spec generate_private_key() -> {ok, private_key()} | {error, binary()}. generate_private_key() -> {error, <<"Random private key generation not implemented - use external source"/utf8>>}. -file("src/gleeth/crypto/secp256k1.gleam", 267). ?DOC(" Recover the public key from a signature and message hash\n"). -spec recover_public_key(bitstring(), signature()) -> {ok, public_key()} | {error, binary()}. recover_public_key(Message_hash, Signature) -> {signature, R, S, Recovery_id} = Signature, case 'Elixir.ExSecp256k1':recover(Message_hash, R, S, Recovery_id) of {ok, Public_key_bytes} -> {ok, {public_key, Public_key_bytes}}; {error, Err} -> {error, <<"Failed to recover public key: "/utf8, (gleam@string:inspect(Err))/binary>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 292). ?DOC(" Helper function to recover candidates for each recovery ID\n"). -spec recover_candidates_helper( bitstring(), bitstring(), bitstring(), list(integer()), list(public_key()) ) -> {ok, list(public_key())} | {error, binary()}. recover_candidates_helper(Message_hash, R, S, Recovery_ids, Acc) -> case Recovery_ids of [] -> {ok, Acc}; [Recovery_id | Rest] -> case 'Elixir.ExSecp256k1':recover(Message_hash, R, S, Recovery_id) of {ok, Public_key_bytes} -> Public_key = {public_key, Public_key_bytes}, recover_candidates_helper( Message_hash, R, S, Rest, [Public_key | Acc] ); {error, _} -> recover_candidates_helper(Message_hash, R, S, Rest, Acc) end end. -file("src/gleeth/crypto/secp256k1.gleam", 281). ?DOC( " Recover multiple public key candidates (all 4 possible recovery IDs)\n" " This is useful when the recovery ID is unknown or needs to be determined\n" ). -spec recover_public_key_candidates(bitstring(), bitstring(), bitstring()) -> {ok, list(public_key())} | {error, binary()}. recover_public_key_candidates(Message_hash, R, S) -> Recovery_ids = [0, 1, 2, 3], recover_candidates_helper(Message_hash, R, S, Recovery_ids, []). -file("src/gleeth/crypto/secp256k1.gleam", 320). ?DOC(" Recover Ethereum address directly from signature and message hash\n"). -spec recover_address(bitstring(), signature()) -> {ok, ethereum_address()} | {error, binary()}. recover_address(Message_hash, Signature) -> gleam@result:'try'( recover_public_key(Message_hash, Signature), fun(Public_key) -> public_key_to_address(Public_key) end ). -file("src/gleeth/crypto/secp256k1.gleam", 329). ?DOC(" Recover multiple address candidates (all possible recovery IDs)\n"). -spec recover_address_candidates(bitstring(), bitstring(), bitstring()) -> {ok, list(ethereum_address())} | {error, binary()}. recover_address_candidates(Message_hash, R, S) -> gleam@result:'try'( recover_public_key_candidates(Message_hash, R, S), fun(Public_keys) -> convert_keys_to_addresses(Public_keys, []) end ). -file("src/gleeth/crypto/secp256k1.gleam", 360). ?DOC(" Verify signature recovery by checking if recovered address matches expected\n"). -spec verify_signature_recovery(bitstring(), signature(), binary()) -> {ok, boolean()} | {error, binary()}. verify_signature_recovery(Message_hash, Signature, Expected_address) -> gleam@result:'try'( recover_address(Message_hash, Signature), fun(Recovered_address) -> Recovered_address_str = address_to_string(Recovered_address), Expected_lower = string:lowercase(Expected_address), Recovered_lower = string:lowercase(Recovered_address_str), {ok, Expected_lower =:= Recovered_lower} end ). -file("src/gleeth/crypto/secp256k1.gleam", 389). ?DOC(" Helper function to find the correct recovery ID\n"). -spec find_recovery_id_helper( bitstring(), bitstring(), bitstring(), binary(), list(integer()) ) -> {ok, integer()} | {error, binary()}. find_recovery_id_helper(Message_hash, R, S, Expected_address, Recovery_ids) -> case Recovery_ids of [] -> {error, <<"No valid recovery ID found for the expected address"/utf8>>}; [Recovery_id | Rest] -> Signature = {signature, R, S, Recovery_id}, case verify_signature_recovery( Message_hash, Signature, Expected_address ) of {ok, true} -> {ok, Recovery_id}; {ok, false} -> find_recovery_id_helper( Message_hash, R, S, Expected_address, Rest ); {error, _} -> find_recovery_id_helper( Message_hash, R, S, Expected_address, Rest ) end end. -file("src/gleeth/crypto/secp256k1.gleam", 377). ?DOC( " Find the correct recovery ID for a given signature and expected address\n" " This is useful when you have r,s components but need to determine the recovery ID\n" ). -spec find_recovery_id(bitstring(), bitstring(), bitstring(), binary()) -> {ok, integer()} | {error, binary()}. find_recovery_id(Message_hash, R, S, Expected_address) -> Recovery_ids = [0, 1, 2, 3], find_recovery_id_helper(Message_hash, R, S, Expected_address, Recovery_ids). -file("src/gleeth/crypto/secp256k1.gleam", 461). ?DOC(" Recover public key from compact signature format\n"). -spec recover_public_key_compact(bitstring(), bitstring(), integer()) -> {ok, public_key()} | {error, binary()}. recover_public_key_compact(Message_hash, Compact_signature, Recovery_id) -> case 'Elixir.ExSecp256k1':recover_compact( Message_hash, Compact_signature, Recovery_id ) of {ok, Public_key_bytes} -> {ok, {public_key, Public_key_bytes}}; {error, Err} -> {error, <<"Failed to recover public key from compact signature: "/utf8, (gleam@string:inspect(Err))/binary>>} end. -file("src/gleeth/crypto/secp256k1.gleam", 477). ?DOC(" Recover address from compact signature format\n"). -spec recover_address_compact(bitstring(), bitstring(), integer()) -> {ok, ethereum_address()} | {error, binary()}. recover_address_compact(Message_hash, Compact_signature, Recovery_id) -> gleam@result:'try'( recover_public_key_compact(Message_hash, Compact_signature, Recovery_id), fun(Public_key) -> public_key_to_address(Public_key) end ).