Packages
signet
1.0.0-alpha8
1.6.1
1.6.0
1.5.0
1.4.2
1.4.1
1.4.0
1.3.12
1.3.11
1.3.10
1.3.9
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0-echo8
1.0.0-echo7
1.0.0-echo6
1.0.0-echo5
1.0.0-echo4
1.0.0-echo3
1.0.0-echo2
1.0.0-echo1
1.0.0-delta8
1.0.0-delta7
1.0.0-delta6
1.0.0-delta5
1.0.0-delta4
1.0.0-delta3
1.0.0-delta2
1.0.0-delta1
1.0.0-charlie9
1.0.0-charlie8
1.0.0-charlie7
1.0.0-charlie6
1.0.0-charlie5
1.0.0-charlie4
1.0.0-charlie3
1.0.0-charlie2
1.0.0-charlie1
1.0.0-beta9
1.0.0-beta8
1.0.0-beta7
1.0.0-beta6
1.0.0-beta5
1.0.0-beta4
1.0.0-beta3
1.0.0-beta2
1.0.0-beta1
1.0.0-alpha9
1.0.0-alpha8
1.0.0-alpha7
1.0.0-alpha6
1.0.0-alpha5
1.0.0-alpha4
1.0.0-alpha3
1.0.0-alpha2
1.0.0-alpha10
1.0.0-alpha1
0.2.0-alpha6
0.2.0-alpha5
0.2.0-alpha4
0.2.0-alpha1
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.1.0-rc7
0.1.0-rc6
0.1.0-rc5
0.1.0-rc4
0.1.0-rc3
0.1.0-rc2
0.1.0-rc1
0.1.0-rc0
Lightweight Ethereum and Solana RPC client for Elixir
Current section
Files
Jump to
Current section
Files
lib/signet/util.ex
defmodule Signet.Util do
defdelegate keccak(value), to: Signet.Hash
@doc ~S"""
Decodes a hex string, specifically requiring that the string begins
with `0x` and allows mixed-case typing.
## Examples
iex> Signet.Util.decode_hex("0x1122")
{:ok, <<0x11, 0x22>>}
iex> Signet.Util.decode_hex("0x1")
{:ok, <<0x1>>}
iex> Signet.Util.decode_hex("0xGG")
:error
"""
@spec decode_hex(String.t()) :: {:ok, binary()} | :error
def decode_hex("0x" <> hex) do
hex_padded =
if rem(byte_size(hex), 2) == 1 do
"0" <> hex
else
hex
end
Base.decode16(hex_padded, case: :mixed)
end
@doc ~S"""
Decodes a hex string, specifically requiring that the string begins
with `0x` and allows mixed-case typing.
Similar to `decode_hex/1`, but raises on error
## Examples
iex> Signet.Util.decode_hex!("0x1122")
<<0x11, 0x22>>
iex> Signet.Util.decode_hex!("0x1")
<<0x1>>
iex> Signet.Util.decode_hex!("0xGG")
** (RuntimeError) invalid hex
"""
@spec decode_hex!(String.t()) :: binary() | no_return()
def decode_hex!(hex) do
case decode_hex(hex) do
{:ok, result} ->
result
_ ->
raise "invalid hex"
end
end
@doc ~S"""
Decodes hex but requires the result be a given set of bytes, or
otherwise raises.
## Examples
iex> Signet.Util.decode_sized_hex!("0x1122", 2)
<<0x11, 0x22>>
iex> Signet.Util.decode_sized_hex!("0x1122", 3)
** (RuntimeError) mismatch byte size. expected 3, got: 2
iex> Signet.Util.decode_sized_hex!("0xGG", 3)
** (RuntimeError) invalid hex
"""
@spec decode_sized_hex!(String.t(), integer()) :: binary() | no_return()
def decode_sized_hex!(hex, size) do
result = decode_hex!(hex)
if byte_size(result) == size do
result
else
raise "mismatch byte size. expected #{size}, got: #{byte_size(result)}"
end
end
@doc ~S"""
Decodes hex if the size is exactly one 32-byte word, otherwise raises.
## Examples
iex> Signet.Util.decode_word!("0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
<<0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff>>
iex> Signet.Util.decode_word!("0x1122")
** (RuntimeError) mismatch byte size. expected 32, got: 2
"""
@spec decode_word!(String.t()) :: <<_::256>> | no_return()
def decode_word!(hex), do: decode_sized_hex!(hex, 32)
@doc ~S"""
Decodes hex if the size is exactly one 20-byte address, otherwise raises.
## Examples
iex> Signet.Util.decode_address!("0x00112233445566778899aabbccddeeff00112233")
<<0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x00,0x11,0x22,0x33>>
iex> Signet.Util.decode_address!("0x1122")
** (RuntimeError) mismatch byte size. expected 20, got: 2
"""
@spec decode_address!(String.t()) :: <<_::160>> | no_return()
def decode_address!(hex), do: decode_sized_hex!(hex, 20)
@doc ~S"""
Decodes hex to an integer.
## Examples
iex> Signet.Util.decode_hex_number!("0x11223344")
0x11223344
"""
@spec decode_hex_number!(String.t()) :: integer() | no_return()
def decode_hex_number!(hex), do: decode_hex!(hex) |> :binary.decode_unsigned()
@doc ~S"""
Decodes hex, allowing it to either by "0x..." or <<1::160>>.
Note: a hex-printed string, in this case, must start with 0x,
otherwise it will be interpreted as its ASCII values.
## Examples
iex> Signet.Util.decode_hex_input!("0x55")
<<0x55>>
iex> Signet.Util.decode_hex_input!(<<0x55>>)
<<0x55>>
"""
def decode_hex_input!(hex = "0x" <> _), do: decode_hex!(hex)
def decode_hex_input!(hex) when is_binary(hex), do: hex
@doc ~S"""
Encodes a hex string, adding a `0x` prefix.
Note: if `short` is set, then any leading zeros will be stripped.
## Examples
iex> Signet.Util.encode_hex(<<0x11, 0x22>>)
"0x1122"
iex> Signet.Util.encode_hex(<<0xc>>)
"0x0C"
iex> Signet.Util.encode_hex(<<0xc>>, true)
"0xC"
iex> Signet.Util.encode_hex(<<0x0>>, true)
"0x0"
"""
def encode_hex(hex, short \\ false)
def encode_hex(nil, _short), do: nil
def encode_hex(hex, short) when is_binary(hex) do
enc = Base.encode16(hex)
"0x" <>
if short do
case String.replace_leading(enc, "0", "") do
"" ->
"0"
els ->
els
end
else
enc
end
end
def encode_hex(v, short) when is_integer(v), do: encode_hex(:binary.encode_unsigned(v), short)
@doc ~S"""
Encodes a number as a binary representation of a certain number of
bytes.
## Examples
iex> Signet.Util.encode_bytes(257, 4)
<<0, 0, 1, 1>>
iex> Signet.Util.encode_bytes(nil, 4)
nil
"""
def encode_bytes(nil, _), do: nil
def encode_bytes(b, size) do
pad(:binary.encode_unsigned(b), size)
end
@doc ~S"""
Pads a binary to a given length
## Examples
iex> Signet.Util.pad(<<1, 2>>, 2)
<<1, 2>>
iex> Signet.Util.pad(<<1, 2>>, 4)
<<0, 0, 1, 2>>
iex> Signet.Util.pad(<<1, 2>>, 1)
** (FunctionClauseError) no function clause matching in Signet.Util.pad/2
"""
def pad(bin, size) when size > byte_size(bin) do
padding_len_bits = (size - byte_size(bin)) * 8
<<0::size(padding_len_bits)>> <> bin
end
def pad(bin, size) when size == byte_size(bin), do: bin
@doc ~S"""
Returns an Ethereum address from a given DER-encoded public key.
## Examples
iex> public_key = Signet.Util.decode_hex!("0x0422")
iex> Signet.Util.get_eth_address(public_key) |> Signet.Util.encode_hex()
"0x759F1AFDC24ABA433A3E18B683F8C04A6EAA69F0"
"""
def get_eth_address(public_key) do
<<4, public_key_raw::binary>> = public_key
<<_::bitstring-size(96), address::bitstring-size(160)>> = keccak(public_key_raw)
address
end
@doc ~S"""
Converts a number to wei, possibly from gwei, etc.
## Examples
iex> Signet.Util.to_wei(100)
100
iex> Signet.Util.to_wei({100, :gwei})
100000000000
"""
@spec to_wei(integer() | {integer, :gwei}) :: number()
def to_wei(amount) when is_integer(amount), do: amount
def to_wei({amount, :wei}) when is_integer(amount), do: amount
def to_wei({amount, :gwei}) when is_integer(amount), do: amount * 1_000_000_000
@chains %{
mainnet: 1,
ropsten: 2,
rinkeby: 4,
goerli: 5,
kovan: 42
}
@doc ~S"""
Parses a chain id, which can be given as an integer or an atom of a known network.
## Examples
iex> Signet.Util.parse_chain_id(5)
5
iex> Signet.Util.parse_chain_id(:goerli)
5
"""
def parse_chain_id(chain_id) when is_atom(chain_id), do: Map.fetch!(@chains, chain_id)
def parse_chain_id(chain_id) when is_integer(chain_id), do: chain_id
@doc ~S"""
Checksums an Ethereum address per [EIP-55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md),
the result is a string-encoded version of the address.
## Examples
iex> Signet.Util.checksum_address("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
iex> Signet.Util.checksum_address("0xFB6916095CA1DF60BB79CE92CE3EA74C37C5D359")
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"
iex> Signet.Util.checksum_address("0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb")
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"
iex> Signet.Util.checksum_address("0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb")
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
"""
def checksum_address(address = "0x" <> _), do: checksum_address(decode_hex!(address))
def checksum_address(address) when is_binary(address) and byte_size(address) == 20 do
# Weirdly instead of keccaking the address, we keccak the string representation...
"0x" <> address_enc = encode_hex(address)
hash = Signet.Hash.keccak(String.downcase(address_enc))
# Use a charlist to semi-quickly get the correct hex digit
lower = ~c"0123456789abcdef"
upper = ~c"0123456789ABCDEF"
res =
for {nibble, hash_val} <- Enum.zip(nibbles(address), nibbles(hash)), into: [] do
casing = if hash_val >= 8, do: upper, else: lower
Enum.at(casing, nibble)
end
"0x" <> to_string(res)
end
@doc ~S"""
Returns the nibbles of a binary as a list.
## Examples
iex> Signet.Util.nibbles(<<0xF5,0xE6,0xD0>>)
[0xF, 0x5, 0xE, 0x6, 0xD, 0x0]
"""
def nibbles(v) do
Enum.reverse(do_nibbles(v, []))
end
defp do_nibbles(<<>>, acc), do: acc
defp do_nibbles(<<high::4, low::4, rest::binary>>, acc),
do: do_nibbles(rest, [low, high | acc])
defmodule RecoveryBit do
@moduledoc """
There are a number of ways to look at recovery bits. Either:
* `:base`: In the range `{0,1}`, which are the outputs of a signer library
* `:ethereum`: In the range `{27,28}`, as defined in the yellow paper
* `:eip155`: In the range `{35+chain_id*2,35+chain_id*2+1}`, as defined in EIP-155
This module provides tools between switching through these choices.
"""
@rec_types [:base, :ethereum, :eip155]
@type rec_type() :: :base | :ethereum | :eip155
@doc """
Normalizes a binary-encoded signature to the given requested type,
i.e. `:base`, `:ethereum`, or `:eip155`.
## Examples
iex> Signet.Util.RecoveryBit.normalize(28, :eip155)
46
iex> Signet.Util.RecoveryBit.normalize(1, :ethereum)
28
iex> Signet.Util.RecoveryBit.normalize(45, :base)
0
"""
@spec normalize(non_neg_integer(), rec_type()) :: non_neg_integer() | :no_return
def normalize(recovery_bit, rec_type \\ :eip155) when rec_type in @rec_types do
base = recover_base(recovery_bit)
case rec_type do
:base ->
base
:ethereum ->
base + 27
:eip155 ->
35 + Signet.Application.chain_id() * 2 + base
end
end
@doc """
Normalizes a binary-encoded signature to the given requested type,
i.e. `:base`, `:ethereum`, or `:eip155`.
## Examples
iex> Signet.Util.RecoveryBit.normalize_signature(<<1::256, 2::256, 28>>, :eip155)
<<1::256, 2::256, 46>>
iex> Signet.Util.RecoveryBit.normalize_signature(<<1::256, 2::256, 1>>, :ethereum)
<<1::256, 2::256, 28>>
iex> Signet.Util.RecoveryBit.normalize_signature(<<1::256, 2::256, 45>>, :base)
<<1::256, 2::256, 0>>
"""
@spec normalize_signature(Signet.signature(), rec_type()) :: Signet.signature() | :no_return
def normalize_signature(<<rs::binary-size(64), v>>, rec_type \\ :eip155)
when rec_type in @rec_types do
v_normalized = normalize(v, rec_type)
<<rs::binary-size(64), v_normalized::8>>
end
@doc """
Normalizes a recovery bit to be either 0 or 1.
## Examples
iex> Signet.Util.RecoveryBit.recover_base(0)
0
iex> Signet.Util.RecoveryBit.recover_base(1)
1
iex> Signet.Util.RecoveryBit.recover_base(27)
0
iex> Signet.Util.RecoveryBit.recover_base(28)
1
iex> Signet.Util.RecoveryBit.recover_base(45)
0
iex> Signet.Util.RecoveryBit.recover_base(46)
1
iex> Signet.Util.RecoveryBit.recover_base(47)
** (RuntimeError) Invalid EIP-155 Signature: recovery_bit=47, chain_id=5
iex> Signet.Util.RecoveryBit.recover_base(2)
** (FunctionClauseError) no function clause matching in Signet.Util.RecoveryBit.recover_base/1
"""
@spec recover_base(non_neg_integer()) :: 0 | 1 | no_return()
def recover_base(v) when v in [0, 1], do: v
def recover_base(v) when v in [27, 28], do: v - 27
def recover_base(v) when v >= 35 do
case v - Signet.Application.chain_id() * 2 - 35 do
base when base in [0, 1] ->
base
_ ->
raise "Invalid EIP-155 Signature: recovery_bit=#{v}, chain_id=#{Signet.Application.chain_id()}"
end
end
end
end