defmodule Web3.ABI.Signer do
@moduledoc """
Signer is a helper class for signing transactions.
Signer inspired by [Abx](https://github.com/Kabie/abx)
"""
# ignore EIP-2930 for now
@type post_1559_txn() :: %{
chain_id: pos_integer(),
nonce: non_neg_integer(),
priority_fee: non_neg_integer(),
gas_price: non_neg_integer(),
gas_limit: non_neg_integer(),
to: binary(),
value: non_neg_integer(),
data: binary()
}
@type legacy_post_155_txn() :: %{
chain_id: pos_integer(),
nonce: non_neg_integer(),
gas_price: non_neg_integer(),
gas_limit: non_neg_integer(),
to: binary(),
value: non_neg_integer(),
data: binary()
}
@type legacy_pre_155_txn() :: %{
nonce: non_neg_integer(),
gas_price: non_neg_integer(),
gas_limit: non_neg_integer(),
to: binary(),
value: non_neg_integer(),
data: binary()
}
@type txn_obj :: post_1559_txn() | legacy_post_155_txn() | legacy_pre_155_txn()
@type signed_raw_txn :: <<_::16, _::_*8>>
@doc """
Sign Transaction.
"""
@spec sign_transaction(txn_obj, binary, Keyword.t()) :: signed_raw_txn()
def sign_transaction(txn_obj, private_key, opts \\ []) do
tco =
txn_obj
|> Map.merge(Map.new(opts))
|> Map.put_new_lazy(:value, fn ->
0
end)
|> Map.update(:to, <<>>, &normalize_address/1)
|> Map.update(:data, <<>>, &normalize_data/1)
signed_txn =
tco
|> do_sign_transaction(private_key)
|> Base.encode16()
"0x" <> signed_txn
end
defp do_sign_transaction(transaction, private_key) do
case transaction do
# Post EIP-1559, with format EIP-2718
# TransactionType 2
# TransactionPayload rlp([chain_id, nonce, priority_fee, gas_price, gas_limit, to, amount, data, access_list, y_parity, r, s])
# we use empty access_list here for simplicity
%{chain_id: chain_id, nonce: nonce, priority_fee: priority_fee, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data} ->
msg_to_sign = <<0x02>> <> ExRLP.encode([chain_id, nonce, priority_fee, gas_price, gas_limit, to, value, data, []])
{r, s, v} = make_signature(msg_to_sign, private_key)
y_parity = v - 27
<<0x02>> <> ExRLP.encode([chain_id, nonce, priority_fee, gas_price, gas_limit, to, value, data, [], y_parity, r, s])
# LegacyTransaction, post EIP-155
# rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
%{chain_id: chain_id, nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data} ->
msg_to_sign = ExRLP.encode([nonce, gas_price, gas_limit, to, value, data, chain_id, 0, 0])
{r, s, v} = make_signature(msg_to_sign, private_key)
y_parity = v - 27
v = chain_id * 2 + 35 + y_parity
ExRLP.encode([nonce, gas_price, gas_limit, to, value, data, v, r, s])
# LegacyTransaction, pre EIP-155
%{nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data} ->
msg_to_sign = ExRLP.encode([nonce, gas_price, gas_limit, to, value, data])
{r, s, v} = make_signature(msg_to_sign, private_key)
y_parity = v - 27
v = y_parity + 27
ExRLP.encode([nonce, gas_price, gas_limit, to, value, data, v, r, s])
end
end
defp normalize_address(address) do
case Web3.Type.Address.cast(address) do
{:ok, address} -> address.bytes
_ -> <<>>
end
end
defp normalize_data(data) do
case data do
"0x" <> hex -> Base.decode16!(hex, case: :mixed)
binary when is_binary(binary) -> binary
_ -> <<>>
end
end
defp make_signature(msg_to_sign, private_key) do
<<v, r::256, s::256>> =
msg_to_sign
|> ExKeccak.hash_256()
|> Curvy.sign(private_key, compact: true, compressed: false, hash: false)
{r, s, v}
end
end