From 6bd5ff4e818bd36c5e1f9dc4af7b3439eae7ddf0 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:13:54 +0000 Subject: [PATCH 01/14] Adds Wallet behaviour and protocol. --- lib/tt_eth/behaviours/wallet.ex | 17 +++++++++++++++++ lib/tt_eth/protocols/wallet.ex | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 lib/tt_eth/behaviours/wallet.ex create mode 100644 lib/tt_eth/protocols/wallet.ex diff --git a/lib/tt_eth/behaviours/wallet.ex b/lib/tt_eth/behaviours/wallet.ex new file mode 100644 index 0000000..d818300 --- /dev/null +++ b/lib/tt_eth/behaviours/wallet.ex @@ -0,0 +1,17 @@ +defmodule TTEth.Behaviours.Wallet do + @moduledoc """ + Behaviour for wallet adapters. + """ + + @typedoc """ + This represents the config for a wallet adapter. + + Check the documentation for the adapter for specific configuration options. + """ + @type config :: map() | binary() + + @doc """ + Returns a new populated wallet struct. + """ + @callback new(config) :: struct() +end diff --git a/lib/tt_eth/protocols/wallet.ex b/lib/tt_eth/protocols/wallet.ex new file mode 100644 index 0000000..8b60d9c --- /dev/null +++ b/lib/tt_eth/protocols/wallet.ex @@ -0,0 +1,17 @@ +defprotocol TTEth.Protocols.Wallet do + @moduledoc """ + Protocol for wallet adapters. + """ + + @type t :: any() + + @doc """ + Returns a map of attributes used to construct a `TTEth.Wallet.t()`. + """ + def wallet_attrs(t) + + @doc """ + Returns a signature. + """ + def sign(t, hash_digest) +end From f8e2fb87ee85e79cb8ee2ad92215fa33cedbf063 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:14:36 +0000 Subject: [PATCH 02/14] Adds LocalWallet implementation. --- lib/tt_eth/local_wallet.ex | 79 +++++++++++++++++++++++++++++++ test/tt_eth/local_wallet_test.exs | 52 ++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 lib/tt_eth/local_wallet.ex create mode 100644 test/tt_eth/local_wallet_test.exs diff --git a/lib/tt_eth/local_wallet.ex b/lib/tt_eth/local_wallet.ex new file mode 100644 index 0000000..b16c4cf --- /dev/null +++ b/lib/tt_eth/local_wallet.ex @@ -0,0 +1,79 @@ +defmodule TTEth.LocalWallet do + @moduledoc """ + A local wallet, derived from a private key. + + ## Config + + Expects the config to look like so: + + config TTEth, + wallet_adapter: TTEth.LocalWallet, + wallets: [ + primary: "0x0aF6...0000", + ] + """ + alias TTEth.Type.{Address, PublicKey, PrivateKey} + alias TTEth.Secp256k1 + alias TTEth.Behaviours.Wallet, as: WalletBehaviour + + @type t :: %__MODULE__{} + + @behaviour WalletBehaviour + + defstruct [ + :private_key, + :human_private_key + ] + + defimpl TTEth.Protocols.Wallet, for: __MODULE__ do + def wallet_attrs(%@for{} = wallet) do + pub = + wallet.private_key + |> PublicKey.from_private_key!() + |> PublicKey.from_human!() + + address = pub |> Address.from_public_key!() + + [ + adapter: @for, + address: address, + public_key: pub, + human_address: address |> Address.to_human!(), + human_public_key: pub |> PublicKey.to_human!(), + _adapter: wallet + ] + end + + def sign(%@for{} = wallet, "" <> digest), + do: + digest + |> Secp256k1.ecdsa_sign_compact(wallet.private_key) + end + + @impl WalletBehaviour + + def new(%{private_key: private_key} = _config), + do: + __MODULE__ + |> struct!(%{ + private_key: private_key |> PrivateKey.from_human!(), + human_private_key: private_key + }) + + def new("" <> private_key = _config), + do: + %{private_key: private_key} + |> new() + + ## Helpers. + + @doc """ + Generates a new `TTEth.LocalWallet.t` with a random private key. + """ + def generate() do + {_pub, priv} = TTEth.new_keypair() + + %{private_key: priv} + |> new() + end +end diff --git a/test/tt_eth/local_wallet_test.exs b/test/tt_eth/local_wallet_test.exs new file mode 100644 index 0000000..9df82e4 --- /dev/null +++ b/test/tt_eth/local_wallet_test.exs @@ -0,0 +1,52 @@ +defmodule TTEth.LocalWalletTest do + use TTEth.Case + alias TTEth.LocalWallet + alias TTEth.Type.PrivateKey + alias TTEth.Protocols.Wallet, as: WalletProtocol + + @human_private_key "0xfa015243f2e6d8694ab037a7987dc73b1630fc8cb1ce82860344684c15d24026" + + describe "implements TTEth.Protocols.Wallet protocol" do + setup :build_local_wallet + + test "wallet_attrs/1 - returns attributes needed when building a wallet", %{ + local_wallet: local_wallet + } do + local_wallet + |> WalletProtocol.wallet_attrs() + |> assert_match( + address: _, + public_key: _, + human_address: "0x" <> _, + human_public_key: "0x" <> _, + _adapter: ^local_wallet + ) + end + + test "sign/2 - signs the given digest with the wallet", %{ + local_wallet: local_wallet + } do + local_wallet + |> WalletProtocol.sign("some plaintext" |> TTEth.keccak()) + |> assert_match({:ok, {<<_signature::512>>, recovery_id}} when recovery_id in [0, 1]) + end + end + + describe "implements TTEth.Behaviours.Wallet behaviour" do + test "new/1 - initializes a new CloudKMS struct" do + decoded_private_key = @human_private_key |> PrivateKey.from_human!() + + %{private_key: @human_private_key} + |> LocalWallet.new() + |> assert_match(%LocalWallet{ + private_key: ^decoded_private_key, + human_private_key: @human_private_key + }) + end + end + + ## Private. + + defp build_local_wallet(_), + do: %{local_wallet: @human_private_key |> LocalWallet.new()} +end From e9d04168f01f09b26c1fd1d31ca9ec4c8b003a49 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:15:42 +0000 Subject: [PATCH 03/14] Refactors Wallet module and adds config. --- config/config.exs | 1 + lib/tt_eth/wallet.ex | 101 ++++++++++++++++++++++++++++-------- test/tt_eth/wallet_test.exs | 24 ++++----- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/config/config.exs b/config/config.exs index a7c9a72..9ae2c52 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,4 +10,5 @@ config :tt_eth, secondary: "0xe2187dc017e880dded10c403f7c0d397afb11736ac027c1202e318b0dd345086", ternary: "0xfa015243f2e6d8694ab037a7987dc73b1630fc8cb1ce82860344684c15d24026" ], + wallet_adapter: TTEth.LocalWallet, transaction_module: TTEth.Transactions.LegacyTransaction diff --git a/lib/tt_eth/wallet.ex b/lib/tt_eth/wallet.ex index 793e441..3c63ec9 100644 --- a/lib/tt_eth/wallet.ex +++ b/lib/tt_eth/wallet.ex @@ -2,45 +2,100 @@ defmodule TTEth.Wallet do @moduledoc """ Provides a handle struct - `TTEth.Wallet.t()` for encapsulating a wallet. """ - alias TTEth.Type.{Address, PublicKey, PrivateKey} + alias TTEth.Protocols.Wallet, as: WalletProtocol + alias TTEth.Type.Signature, as: EthSignature @type t :: %__MODULE__{} + defstruct [ + :adapter, :address, :public_key, - :private_key, :human_address, :human_public_key, - :human_private_key + :_adapter ] + @doc """ + Looks up a wallet by name from config and creates it. + + See the adapter for specific configuration options. + """ @spec named(atom) :: t() - def named(name), + def named(wallet_name) when is_atom(wallet_name), do: :tt_eth |> Application.fetch_env!(:wallets) - |> Keyword.fetch!(name) - |> from_private_key() + |> Keyword.fetch!(wallet_name) + |> build_with_adapter!( + :tt_eth + |> Application.fetch_env!(:wallet_adapter) + ) + |> new() - @doc "Constructs a wallet from a private key." + @doc """ + Creates a new `TTEth.Wallet.t` struct from a passed private key. + """ @spec from_private_key(binary) :: t() - def from_private_key(priv) when is_binary(priv) do - raw_priv = priv |> PrivateKey.from_human!() |> PublicKey.from_private_key!() - {raw_priv, priv} |> new() + def from_private_key("" <> private_key), + do: + private_key + |> build_with_adapter!() + |> new() + + @doc """ + Creates a new wallet from an underlying wallet or a random one. + """ + def new(wallet \\ TTEth.LocalWallet.generate()) when is_struct(wallet), + do: + __MODULE__ + |> struct!( + wallet + |> WalletProtocol.wallet_attrs() + ) + + @doc """ + Signs a digest using the passed wallet. + """ + def sign(%__MODULE__{_adapter: wallet}, "" <> hash_digest), + do: + wallet + |> WalletProtocol.sign(hash_digest) + + @doc """ + The same as `sign/2` but raises if the signing process is not successful. + """ + def sign!(%__MODULE__{} = wallet, "" <> hash_digest) do + {:ok, ret} = wallet |> sign(hash_digest) + ret end - @doc "Constructs a new wallet from a keypair or a random one." - @spec new({binary, binary}) :: t() - def new({pub, priv} \\ TTEth.new_keypair()) when is_binary(pub) and is_binary(priv) do - address = pub |> Address.from_public_key!() - - struct!(%__MODULE__{}, %{ - address: address |> Address.from_human!(), - public_key: pub |> PublicKey.from_human!(), - private_key: priv |> PrivateKey.from_human!(), - human_address: address |> Address.to_human!(), - human_public_key: pub |> PublicKey.to_human!(), - human_private_key: priv |> PrivateKey.to_human!() - }) + @doc """ + Signs a plaintext message using the passed wallet. + + This is for personal signed data, not for transaction data. + + SEE: https://eips.ethereum.org/EIPS/eip-191 + SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign + """ + def personal_sign(%__MODULE__{} = wallet, "" <> plaintext), + do: + wallet + |> sign(plaintext |> EthSignature.digest()) + + @doc """ + The same as `personal_sign/2` but raises if the signing process is not successful. + """ + def personal_sign!(%__MODULE__{} = wallet, "" <> plaintext) do + {:ok, comps} = wallet |> personal_sign(plaintext) + comps end + + ## Private. + + defp build_with_adapter!(config, adapter \\ TTEth.LocalWallet) + when (is_binary(config) or is_map(config)) and is_atom(adapter), + do: + config + |> adapter.new() end diff --git a/test/tt_eth/wallet_test.exs b/test/tt_eth/wallet_test.exs index 21de7bb..de83b0a 100644 --- a/test/tt_eth/wallet_test.exs +++ b/test/tt_eth/wallet_test.exs @@ -1,5 +1,6 @@ defmodule TTEth.WalletTest do use TTEth.Case, async: true + alias TTEth.LocalWallet @human_private_key "0xfa015243f2e6d8694ab037a7987dc73b1630fc8cb1ce82860344684c15d24026" @@ -25,13 +26,6 @@ defmodule TTEth.WalletTest do |> Wallet.from_private_key() |> assert_match_ternary_wallet() end - - test "reconstructs everything from a binary private key" do - @human_private_key - |> TTEth.Type.PrivateKey.from_human!() - |> Wallet.from_private_key() - |> assert_match_ternary_wallet() - end end describe "new/0+1" do @@ -40,7 +34,7 @@ defmodule TTEth.WalletTest do end test "generates a wallet deterministically given a keypair" do - kp = TTEth.new_keypair() + kp = LocalWallet.generate() assert Wallet.new(kp) == Wallet.new(kp) end end @@ -48,10 +42,11 @@ defmodule TTEth.WalletTest do ## Private. defp assert_match_ternary_wallet(wallet) do - private_key = @human_private_key |> TTEth.Type.PrivateKey.from_human!() human_address = "0x0aF6b8a8E5D56F0ab74D47Ac446EEa46817F32bC" address = human_address |> TTEth.Type.Address.from_human!() + private_key = @human_private_key |> TTEth.Type.PrivateKey.from_human!() + human_public_key = "0x58be6efb58e39ce4b5d1ca552d80f8c9009dfecec0e5a31fc8d22ee866320c506be5" <> "8730c77623df9862d8041c1bdef8a031e5d38a1ac1b83d053277391f974c" @@ -60,12 +55,15 @@ defmodule TTEth.WalletTest do wallet |> assert_match(%Wallet{ + adapter: LocalWallet, + address: ^address, + public_key: ^public_key, human_address: ^human_address, - human_private_key: @human_private_key, human_public_key: ^human_public_key, - address: ^address, - private_key: ^private_key, - public_key: ^public_key + _adapter: %LocalWallet{ + private_key: ^private_key, + human_private_key: @human_private_key + } }) end end From 918adfdbb042aad4adeeebaf58b8811ba3a002df Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:17:29 +0000 Subject: [PATCH 04/14] Refactors chain client to use wallet. --- lib/tt_eth.ex | 9 ++++----- lib/tt_eth/behaviours/chain_client.ex | 6 +++--- lib/tt_eth/chain_client.ex | 5 +++-- lib/tt_eth/chain_client_mock_impl.ex | 5 +++-- test/tt_eth_test.exs | 8 +++----- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/tt_eth.ex b/lib/tt_eth.ex index f3eece7..4118be6 100644 --- a/lib/tt_eth.ex +++ b/lib/tt_eth.ex @@ -69,16 +69,15 @@ defmodule TTEth do @doc "Builds the tx data using the configured `TTEth.ChainClient`." def build_tx_data(%Wallet{} = wallet, to, method, args, opts \\ []) do - with {_, %{private_key: private_key, human_address: human_address}} <- - {:wallet, wallet}, - {_, {:ok, "0x" <> raw_nonce}} <- - {:raw_nonce, human_address |> chain_client().eth_get_transaction_count("pending")}, + with {_, {:ok, "0x" <> raw_nonce}} <- + {:raw_nonce, + wallet.human_address |> chain_client().eth_get_transaction_count("pending")}, {_, {nonce, ""}} <- {:parse_nonce, raw_nonce |> Integer.parse(16)}, {_, abi_data} <- {:abi_encode, method |> ABI.encode(args)} do to - |> chain_client().build_tx_data(abi_data, private_key, nonce, opts) + |> chain_client().build_tx_data(abi_data, wallet, nonce, opts) end end diff --git a/lib/tt_eth/behaviours/chain_client.ex b/lib/tt_eth/behaviours/chain_client.ex index d850cb3..939f452 100644 --- a/lib/tt_eth/behaviours/chain_client.ex +++ b/lib/tt_eth/behaviours/chain_client.ex @@ -6,7 +6,7 @@ defmodule TTEth.Behaviours.ChainClient do """ @type address :: TTEth.Type.Address.t() - @type private_key :: TTEth.Type.PrivateKey.t() + @type wallet :: TTEth.Wallet.t() @type encoded_args :: binary @type opts :: keyword @type nonce :: non_neg_integer() @@ -23,8 +23,8 @@ defmodule TTEth.Behaviours.ChainClient do @callback eth_send_raw_transaction(tx_data) :: any @callback eth_send_raw_transaction(tx_data, opts) :: any - @callback build_tx_data(address, abi_data, private_key, nonce) :: tx_data - @callback build_tx_data(address, abi_data, private_key, nonce, keyword) :: tx_data + @callback build_tx_data(address, abi_data, wallet, nonce) :: tx_data + @callback build_tx_data(address, abi_data, wallet, nonce, keyword) :: tx_data @callback eth_get_transaction_count(account :: address, block_id) :: any @callback eth_get_transaction_count(account :: address, block_id, opts) :: any diff --git a/lib/tt_eth/chain_client.ex b/lib/tt_eth/chain_client.ex index 2215863..78728cb 100644 --- a/lib/tt_eth/chain_client.ex +++ b/lib/tt_eth/chain_client.ex @@ -5,6 +5,7 @@ defmodule TTEth.ChainClient do This is agnostic to the transaction version/type. """ alias TTEth.Behaviours.ChainClient + alias TTEth.Wallet alias Ethereumex.HttpClient import TTEth, only: [transaction_module: 0, hex_prefix!: 1] @@ -24,12 +25,12 @@ defmodule TTEth.ChainClient do # Delegate to the transaction module to serialize and sign the transaction. @impl ChainClient - def build_tx_data("" <> to_address, abi_data, private_key, nonce, opts \\ []) + def build_tx_data("" <> to_address, abi_data, %Wallet{} = wallet, nonce, opts \\ []) when is_integer(nonce), do: to_address |> transaction_module().new(abi_data, nonce, opts) - |> transaction_module().build(private_key) + |> transaction_module().build(wallet) |> Base.encode16(case: :lower) |> hex_prefix!() diff --git a/lib/tt_eth/chain_client_mock_impl.ex b/lib/tt_eth/chain_client_mock_impl.ex index 9b01fd8..25d1fe7 100644 --- a/lib/tt_eth/chain_client_mock_impl.ex +++ b/lib/tt_eth/chain_client_mock_impl.ex @@ -3,6 +3,7 @@ defmodule TTEth.ChainClientMockImpl do Implementation default for tests. """ alias TTEth.Behaviours.ChainClient + alias TTEth.Wallet @behaviour ChainClient @@ -15,8 +16,8 @@ defmodule TTEth.ChainClientMockImpl do do: :error @impl ChainClient - def build_tx_data(to, abi_data, private_key, nonce, opts \\ []), - do: to |> TTEth.ChainClient.build_tx_data(abi_data, private_key, nonce, opts) + def build_tx_data(to, abi_data, %Wallet{} = wallet, nonce, opts \\ []), + do: to |> TTEth.ChainClient.build_tx_data(abi_data, wallet, nonce, opts) @impl ChainClient def eth_get_transaction_count(_address, _block \\ "latest", _opts \\ []), diff --git a/test/tt_eth_test.exs b/test/tt_eth_test.exs index d6b9d46..06c4f0c 100644 --- a/test/tt_eth_test.exs +++ b/test/tt_eth_test.exs @@ -59,18 +59,16 @@ defmodule TTEthTest do tx_data = to - |> TTEth.ChainClient.build_tx_data(abi_data, wallet.private_key, nonce_dec, - chain_id: @chain_id - ) + |> TTEth.ChainClient.build_tx_data(abi_data, wallet, nonce_dec, chain_id: @chain_id) ChainClientMock # We don't care about this in this test. |> expect(:eth_get_transaction_count, fn _, _ -> {:ok, nonce_hex} end) # We want to make sure that the transaction building function was called with the correct params. - |> expect(:build_tx_data, fn to_, abi_data_, private_key_, nonce_, opts_ -> + |> expect(:build_tx_data, fn to_, abi_data_, wallet_, nonce_, opts_ -> assert to_ == to assert abi_data == abi_data_ - assert private_key_ == wallet.private_key + assert wallet_ == wallet assert nonce_ == nonce_dec assert opts_ == [chain_id: @chain_id] tx_data From f6c3e16b52a4ba3d7db4daa72a5d956ee6b50a64 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:23:17 +0000 Subject: [PATCH 05/14] Refactors signature type module. --- lib/tt_eth/type/signature.ex | 98 +++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/lib/tt_eth/type/signature.ex b/lib/tt_eth/type/signature.ex index df4d723..7ca8319 100644 --- a/lib/tt_eth/type/signature.ex +++ b/lib/tt_eth/type/signature.ex @@ -1,8 +1,9 @@ defmodule TTEth.Type.Signature do - @moduledoc "This module is an Ecto-compatible type that can represent Ethereum signatures." + @moduledoc """ + This module is an Ecto-compatible type that can represent Ethereum signatures. + """ use TTEth.Type, size: 65 - alias TTEth.BitHelper - alias TTEth.Secp256k1 + alias TTEth.{BitHelper, Secp256k1} import TTEth, only: [keccak: 1] @secp256k1n 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 @@ -11,6 +12,7 @@ defmodule TTEth.Type.Signature do @base_recovery_id_eip_155 35 @ethereum_magic <<0x19, "Ethereum Signed Message:", ?\n>> + @type recovery_id :: non_neg_integer() @type v :: byte @type r :: non_neg_integer() @type s :: non_neg_integer() @@ -18,6 +20,9 @@ defmodule TTEth.Type.Signature do @type chain_id :: number @type digest :: binary + @doc """ + Hashes and decorates the passed plaintext message using `decorate_message/1`. + """ @spec digest(binary) :: binary def digest(msg), do: @@ -25,31 +30,49 @@ defmodule TTEth.Type.Signature do |> decorate_message() |> keccak() + @doc """ + Decorates the passed message with the Ethereum magic as specified in EIP-191. + + SEE: https://eips.ethereum.org/EIPS/eip-191 + + This is version `0x45`. + """ @spec decorate_message(binary) :: String.t() def decorate_message(msg), do: <<@ethereum_magic, "#{byte_size(msg)}"::binary, msg::binary>> - @spec sign(message :: binary, privkey :: binary, chain_id | nil) :: - {:ok, components} | {:error, :cannot_sign} - def sign(msg, priv, chain_id \\ nil) do - msg - |> digest() - |> Secp256k1.ecdsa_sign_compact(priv) - |> case do - {:ok, {<>, v}} -> - {:ok, {v_from_recovery_id(v, chain_id), r, s}} + @doc """ + This is for personal signed data, not for transaction data. - {:error, _} -> - {:error, :cannot_sign} - end - end + `TTEth.Wallet.personal_sign/2` should be used instead. + + SEE: https://eips.ethereum.org/EIPS/eip-191 + SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign + """ + @deprecated "See Wallet.t() for using a wallet adapter instead." + @spec sign(message :: binary, private_key :: binary, chain_id | nil) :: + {:ok, components} | {:error, :cannot_sign} + def sign(message, private_key, chain_id \\ nil), + do: + message + |> digest() + |> Secp256k1.ecdsa_sign_compact(private_key) + |> compact_to_components(chain_id) @spec sign!(binary, binary, chain_id | nil) :: components - def sign!(msg, priv, chain_id \\ nil) do - {:ok, ret} = msg |> sign(priv, chain_id) + def sign!(message, private_key, chain_id \\ nil) do + {:ok, ret} = message |> sign(private_key, chain_id) ret end + @doc """ + Given a hash, signature components and optional chain id, returns the public key. + + Note that this is EIP-155 aware, so if you pass a chain id, it will use it to + recover the `recovery_id` from `v`. + + The `chain_id` should not be passed if this is for a EIP-191 message. + """ @spec recover(binary, components, chain_id | nil) :: {:ok, binary} | {:error, binary} def recover(hash, {v, r, s}, chain_id \\ nil) do sig = @@ -71,7 +94,7 @@ defmodule TTEth.Type.Signature do end @spec is_signature_valid?(components, chain_id, keyword) :: boolean - def is_signature_valid?({v, r, s}, _, max_s: :secp256k1n), + def is_signature_valid?({v, r, s}, _chain_id, max_s: :secp256k1n), do: (v == 27 || v == 28) and r > 0 and r < @secp256k1n and @@ -83,6 +106,22 @@ defmodule TTEth.Type.Signature do r > 0 and r < @secp256k1n and s > 0 and s <= @secp256k1n_2 + def compact_to_components(compact_signature, chain_id \\ nil) do + compact_signature + |> case do + {:ok, {<>, v}} -> + {:ok, {v_from_recovery_id(v, chain_id), r, s}} + + {:error, _} -> + {:error, :cannot_sign} + end + end + + def compact_to_components!(compact_signature, chain_id \\ nil) do + {:ok, {_v, _r, _s} = comps} = compact_signature |> compact_to_components(chain_id) + comps + end + @spec components(binary) :: {:error, :invalid_signature} | {:ok, components} def components(sig) do sig @@ -95,32 +134,35 @@ defmodule TTEth.Type.Signature do @spec components!(binary) :: components def components!(sig) do - <> = sig |> from_human!() - {v, r, s} + {:ok, {_v, _r, _s} = comps} = sig |> components() + comps end def from_components!({v, r, s}), do: <> @spec to_human_from_components!(components) :: <<_::16, _::_*8>> - def to_human_from_components!({_, _, _} = components), - do: components |> from_components!() |> to_human!() + def to_human_from_components!({_v, _r, _s} = components), + do: + components + |> from_components!() + |> to_human!() # Private. defp uses_chain_id?(v), do: v >= @base_recovery_id_eip_155 - @spec v_from_recovery_id(byte, chain_id) :: byte - defp v_from_recovery_id(v, chain_id) do + @spec v_from_recovery_id(recovery_id, chain_id) :: non_neg_integer() + defp v_from_recovery_id(recovery_id, chain_id) do if is_nil(chain_id) do - @base_recovery_id + v + @base_recovery_id + recovery_id else - chain_id * 2 + @base_recovery_id_eip_155 + v + chain_id * 2 + @base_recovery_id_eip_155 + recovery_id end end - @spec v_to_recovery_id(number, any) :: number + @spec v_to_recovery_id(non_neg_integer(), chain_id) :: non_neg_integer() defp v_to_recovery_id(v, chain_id) do if not is_nil(chain_id) and uses_chain_id?(v) do v - chain_id * 2 - @base_recovery_id_eip_155 From ba4665f4905190c1c72020fc5bd04a7eba2c3662 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:23:52 +0000 Subject: [PATCH 06/14] Refactors transaction types to use new wallet handling. --- lib/tt_eth/behaviours/transaction.ex | 4 +- .../transactions/eip1559_transaction.ex | 65 ++++++----------- lib/tt_eth/transactions/legacy_transaction.ex | 70 ++++++++----------- .../transactions/eip1559_transaction_test.exs | 18 +++-- 4 files changed, 68 insertions(+), 89 deletions(-) diff --git a/lib/tt_eth/behaviours/transaction.ex b/lib/tt_eth/behaviours/transaction.ex index f297def..810f1e6 100644 --- a/lib/tt_eth/behaviours/transaction.ex +++ b/lib/tt_eth/behaviours/transaction.ex @@ -9,9 +9,9 @@ defmodule TTEth.Behaviours.Transaction do @type opts :: Keyword.t() @type transaction :: struct() - @type private_key :: binary() + @type wallet :: TTEth.Wallet.t() @callback new(to_address, abi_data, nonce, opts) :: transaction - @callback build(transaction, private_key) :: binary() + @callback build(transaction, wallet) :: binary() end diff --git a/lib/tt_eth/transactions/eip1559_transaction.ex b/lib/tt_eth/transactions/eip1559_transaction.ex index 9cf2243..604fce6 100644 --- a/lib/tt_eth/transactions/eip1559_transaction.ex +++ b/lib/tt_eth/transactions/eip1559_transaction.ex @@ -4,7 +4,7 @@ defmodule TTEth.Transactions.EIP1559Transaction do SEE: https://eips.ethereum.org/EIPS/eip-1559 """ - alias TTEth.{BitHelper, Secp256k1} + alias TTEth.{BitHelper, Wallet} alias TTEth.Behaviours.Transaction import BitHelper, only: [encode_unsigned: 1] @@ -53,24 +53,22 @@ defmodule TTEth.Transactions.EIP1559Transaction do This will return a binary which can then be base16 encoded etc. """ @impl Transaction - def build(%__MODULE__{} = trx, private_key), + def build(%__MODULE__{} = trx, %Wallet{} = wallet), do: trx - |> sign_transaction(private_key) + |> sign_transaction(wallet) |> serialize(_include_signature = true) |> rlp_encode() |> put_transaction_envelope(trx) - @doc """ - Delegate to ExRLP to RLP encode values. - """ - def rlp_encode(data), + ## Private. + + # Delegate to ExRLP to RLP encode values. + defp rlp_encode(data), do: data |> ExRLP.encode() - @doc """ - Encodes a transaction such that it can be RLP-encoded. - """ - def serialize(%__MODULE__{} = trx, include_vrs \\ true), + # Encodes a transaction such that it can be RLP-encoded. + defp serialize(%__MODULE__{} = trx, include_vrs), do: [ trx.chain_id |> encode_unsigned(), @@ -85,19 +83,8 @@ defmodule TTEth.Transactions.EIP1559Transaction do ] |> maybe_add_yrs(trx, include_vrs) - @doc """ - Returns a ECDSA signature (v,r,s) for a given hashed value. - """ - def sign_hash(hash, private_key) do - {:ok, {<>, v}} = Secp256k1.ecdsa_sign_compact(hash, private_key) - - {v, r, s} - end - - @doc """ - Returns a hash of a given transaction. - """ - def transaction_hash(%__MODULE__{} = trx), + # Returns a hash of a given transaction. + defp transaction_hash(%__MODULE__{} = trx), do: trx |> serialize(_include_signature = false) @@ -105,28 +92,21 @@ defmodule TTEth.Transactions.EIP1559Transaction do |> put_transaction_envelope(trx) |> TTEth.keccak() - @doc """ - Takes a given transaction and returns a version signed with the given private key. - """ - def sign_transaction(%__MODULE__{} = trx, private_key) when is_binary(private_key) do - {y_parity, r, s} = - trx - |> transaction_hash() - |> sign_hash(private_key) + # Takes a given transaction and returns a version signed with the given private key. + defp sign_transaction(%__MODULE__{} = trx, %Wallet{} = wallet) do + {:ok, {<>, y_parity}} = + wallet + |> Wallet.sign(trx |> transaction_hash()) - %{trx | y_parity: y_parity, r: r, s: s} + %{trx | r: r, s: s, y_parity: y_parity} end - @doc """ - Wraps the RLP encoded transaction in a transaction envelope. - SEE: https://eips.ethereum.org/EIPS/eip-2718 - """ - def put_transaction_envelope(encoded, %__MODULE__{} = trx) when is_binary(encoded), + # Wraps the RLP encoded transaction in a transaction envelope. + # SEE: https://eips.ethereum.org/EIPS/eip-2718 + defp put_transaction_envelope(encoded, %__MODULE__{} = trx) when is_binary(encoded), do: <> <> encoded - ## Private. - - # Optionally add the YRS values. + # Maybe add the y_parity, r and s values. defp maybe_add_yrs(base, %__MODULE__{} = trx, _include_vrs = true), do: base ++ @@ -136,5 +116,6 @@ defmodule TTEth.Transactions.EIP1559Transaction do trx.s |> encode_unsigned() ] - defp maybe_add_yrs(base, %__MODULE__{}, _dont_include_vrs), do: base + defp maybe_add_yrs(base, %__MODULE__{}, _dont_include_vrs), + do: base end diff --git a/lib/tt_eth/transactions/legacy_transaction.ex b/lib/tt_eth/transactions/legacy_transaction.ex index 5cd3071..b3375ad 100644 --- a/lib/tt_eth/transactions/legacy_transaction.ex +++ b/lib/tt_eth/transactions/legacy_transaction.ex @@ -2,13 +2,13 @@ defmodule TTEth.Transactions.LegacyTransaction do @moduledoc """ Ported from [`Blockchain`](https://hex.pm/packages/blockchain). """ - alias TTEth.{BitHelper, Secp256k1} + alias TTEth.{BitHelper, Wallet} alias TTEth.Behaviours.Transaction + alias TTEth.Type.Signature, as: EthSignature + import BitHelper, only: [encode_unsigned: 1] @behaviour Transaction - @type private_key :: <<_::256>> - @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @@ -43,10 +43,6 @@ defmodule TTEth.Transactions.LegacyTransaction do init: <<>>, data: <<>> - # The follow are the maximum value for x in the signature, as defined in Eq.(212) - @base_recovery_id 27 - @base_recovery_id_eip_155 35 - @impl Transaction def new("" <> to_address, abi_data, nonce, opts) when is_integer(nonce), do: %__MODULE__{ @@ -60,10 +56,10 @@ defmodule TTEth.Transactions.LegacyTransaction do } @impl Transaction - def build(%__MODULE__{} = trx, private_key), + def build(%__MODULE__{} = trx, %Wallet{} = wallet), do: trx - |> sign_transaction(private_key) + |> sign_transaction(wallet) |> serialize(_include_signature = true) |> rlp_encode() @@ -95,20 +91,20 @@ defmodule TTEth.Transactions.LegacyTransaction do @spec serialize(t) :: ExRLP.t() def serialize(trx, include_vrs \\ true) do base = [ - trx.nonce |> BitHelper.encode_unsigned(), - trx.gas_price |> BitHelper.encode_unsigned(), - trx.gas_limit |> BitHelper.encode_unsigned(), + trx.nonce |> encode_unsigned(), + trx.gas_price |> encode_unsigned(), + trx.gas_limit |> encode_unsigned(), trx.to, - trx.value |> BitHelper.encode_unsigned(), + trx.value |> encode_unsigned(), if(trx.to == <<>>, do: trx.init, else: trx.data) ] if include_vrs do base ++ [ - trx.v |> BitHelper.encode_unsigned(), - trx.r |> BitHelper.encode_unsigned(), - trx.s |> BitHelper.encode_unsigned() + trx.v |> encode_unsigned(), + trx.r |> encode_unsigned(), + trx.s |> encode_unsigned() ] else base @@ -122,12 +118,14 @@ defmodule TTEth.Transactions.LegacyTransaction do ## Examples - iex> LegacyTransaction.sign_hash(<<2::256>>, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_hash(<<2::256>>, wallet) {28, 38938543279057362855969661240129897219713373336787331739561340553100525404231, 23772455091703794797226342343520955590158385983376086035257995824653222457926} - iex> LegacyTransaction.sign_hash(<<5::256>>, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_hash(<<5::256>>, wallet) {27, 74927840775756275467012999236208995857356645681540064312847180029125478834483, 56037731387691402801139111075060162264934372456622294904359821823785637523849} @@ -135,26 +133,18 @@ defmodule TTEth.Transactions.LegacyTransaction do iex> data = "ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080" |> TTEth.BitHelper.from_hex iex> hash = data |> TTEth.keccak() iex> private_key = "4646464646464646464646464646464646464646464646464646464646464646" |> TTEth.BitHelper.from_hex - iex> LegacyTransaction.sign_hash(hash, private_key, 1) + iex> wallet = TTEth.Wallet.from_private_key(private_key) + iex> LegacyTransaction.sign_hash(hash, wallet, 1) { 37, 18515461264373351373200002665853028612451056578545711640558177340181847433846, 46948507304638947509940763649030358759909902576025900602547168820602576006531 } """ - @spec sign_hash(BitHelper.keccak_hash(), private_key, integer() | nil) :: + @spec sign_hash(BitHelper.keccak_hash(), Wallet.t(), integer() | nil) :: {hash_v, hash_r, hash_s} - def sign_hash(hash, private_key, chain_id \\ nil) do - {:ok, {<>, recovery_id}} = - Secp256k1.ecdsa_sign_compact(hash, private_key) - - # Fork Ψ EIP-155 - recovery_id = - if chain_id do - chain_id * 2 + @base_recovery_id_eip_155 + recovery_id - else - @base_recovery_id + recovery_id - end - - {recovery_id, r, s} - end + def sign_hash(hash, %Wallet{} = wallet, chain_id \\ nil), + do: + wallet + |> Wallet.sign(hash) + |> EthSignature.compact_to_components!(chain_id) @doc """ Returns a hash of a given transaction according to the @@ -192,19 +182,21 @@ defmodule TTEth.Transactions.LegacyTransaction do ## Examples - iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, wallet) %LegacyTransaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 97037709922803580267279977200525583527127616719646548867384185721164615918250, s: 31446571475787755537574189222065166628755695553801403547291726929250860527755, to: "", v: 27, value: 5} - iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>, 1) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, wallet, 1) %LegacyTransaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 25739987953128435966549144317523422635562973654702886626580606913510283002553, s: 41423569377768420285000144846773344478964141018753766296386430811329935846420, to: "", v: 38, value: 5} """ - @spec sign_transaction(__MODULE__.t(), private_key, integer() | nil) :: __MODULE__.t() - def sign_transaction(trx, private_key, chain_id \\ nil) do + @spec sign_transaction(__MODULE__.t(), Wallet.t(), integer() | nil) :: __MODULE__.t() + def sign_transaction(trx, %Wallet{} = wallet, chain_id \\ nil) do {v, r, s} = trx |> transaction_hash(chain_id) - |> sign_hash(private_key, chain_id) + |> sign_hash(wallet, chain_id) %{trx | v: v, r: r, s: s} end diff --git a/test/tt_eth/transactions/eip1559_transaction_test.exs b/test/tt_eth/transactions/eip1559_transaction_test.exs index dd31a1c..1597a62 100644 --- a/test/tt_eth/transactions/eip1559_transaction_test.exs +++ b/test/tt_eth/transactions/eip1559_transaction_test.exs @@ -2,12 +2,12 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do use TTEth.Case alias TTEth.Transactions.EIP1559Transaction alias TTEth.Type.{Address, PrivateKey, PublicKey} + alias TTEth.Wallet # Polygon Mumbai. @chain_id 80001 @private_key_human "0x62aa6ec41b56439d2c5df352c45a00389cef262b3761e13c6481e35ab027d262" - @private_key @private_key_human |> PrivateKey.from_human!() @to_address_human "0x38f153fdd399ff2cf64704c6a4b16d3fd9ddcd69" @to_address @to_address_human |> Address.from_human!() # transfer(address,uint256) @@ -51,20 +51,23 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do end describe "build/2" do - setup :build_trx + setup [ + :build_trx, + :build_wallet + ] - test "builds a signed transaction", %{trx: trx} do + test "builds a signed transaction", %{trx: trx, wallet: wallet} do trx - |> EIP1559Transaction.build(@private_key) + |> EIP1559Transaction.build(wallet) |> encode_and_pad() |> assert_match(@valid_transaction_data) end - test "from address is correct when checking signature", %{trx: trx} do + test "from address is correct when checking signature", %{trx: trx, wallet: wallet} do # Build the trx_data but randomize the nonce. built_trx_data = %{trx | nonce: Enum.random(10..100)} - |> EIP1559Transaction.build(@private_key) + |> EIP1559Transaction.build(wallet) |> encode_and_pad() # Decode the transaction data. @@ -117,6 +120,9 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do } } + defp build_wallet(_), + do: %{wallet: @private_key_human |> Wallet.from_private_key()} + ## Helpers. defp encode_and_pad(bin), From 07f6c5ff17ab2cbe6a41d1e0ef14b00b10f0f595 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:24:03 +0000 Subject: [PATCH 07/14] Updates document groups. --- mix.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mix.exs b/mix.exs index dd75a3a..7008d35 100644 --- a/mix.exs +++ b/mix.exs @@ -40,6 +40,18 @@ defmodule TTEth.MixProject do defp groups_for_modules(), do: [ + "Transactions Types": [ + TTEth.Transactions.LegacyTransaction, + TTEth.Transactions.EIP1559Transaction + ], + Behaviours: [ + TTEth.Behaviours.ChainClient, + TTEth.Behaviours.Transaction, + TTEth.Behaviours.Wallet + ], + Protocols: [ + TTEth.Protocols.Wallet + ], Types: [ TTEth.Type.Address, TTEth.Type.Hash, From 9d09a0aeb00c7219e98c97af5913d8ad4eda8348 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 18:01:20 +0000 Subject: [PATCH 08/14] Refactors signature module and transaction modules. --- lib/tt_eth/secp256k1.ex | 4 +- lib/tt_eth/transactions/legacy_transaction.ex | 20 +++- lib/tt_eth/type/signature.ex | 98 +++++++------------ 3 files changed, 53 insertions(+), 69 deletions(-) diff --git a/lib/tt_eth/secp256k1.ex b/lib/tt_eth/secp256k1.ex index 28b184c..627da62 100644 --- a/lib/tt_eth/secp256k1.ex +++ b/lib/tt_eth/secp256k1.ex @@ -3,12 +3,14 @@ defmodule TTEth.Secp256k1 do Wrapper around `ExSecp256k1` functions. """ + @valid_recovery_ids [0, 1] + @doc """ Delegates to `ExSecp256k1.recover_compact/3` with guards around the `recovery_id` value. """ @spec ecdsa_recover_compact(binary(), binary(), non_neg_integer()) :: {:ok, binary()} | {:error, atom()} - def ecdsa_recover_compact(hash, sig, recovery_id) when recovery_id in [0, 1], + def ecdsa_recover_compact(hash, sig, recovery_id) when recovery_id in @valid_recovery_ids, do: ExSecp256k1.recover_compact(hash, sig, recovery_id) def ecdsa_recover_compact(_hash, _sig, _recovery_id), diff --git a/lib/tt_eth/transactions/legacy_transaction.ex b/lib/tt_eth/transactions/legacy_transaction.ex index b3375ad..0f7a822 100644 --- a/lib/tt_eth/transactions/legacy_transaction.ex +++ b/lib/tt_eth/transactions/legacy_transaction.ex @@ -4,7 +4,6 @@ defmodule TTEth.Transactions.LegacyTransaction do """ alias TTEth.{BitHelper, Wallet} alias TTEth.Behaviours.Transaction - alias TTEth.Type.Signature, as: EthSignature import BitHelper, only: [encode_unsigned: 1] @behaviour Transaction @@ -17,6 +16,9 @@ defmodule TTEth.Transactions.LegacyTransaction do @type address :: <<_::160>> @type hash :: <<_::256>> + @base_v 27 + @base_v_eip_155 35 + @type t :: %__MODULE__{ nonce: integer(), chain_id: integer(), @@ -140,11 +142,13 @@ defmodule TTEth.Transactions.LegacyTransaction do """ @spec sign_hash(BitHelper.keccak_hash(), Wallet.t(), integer() | nil) :: {hash_v, hash_r, hash_s} - def sign_hash(hash, %Wallet{} = wallet, chain_id \\ nil), - do: + def sign_hash(hash, %Wallet{} = wallet, chain_id \\ nil) do + {:ok, {<>, recovery_id}} = wallet |> Wallet.sign(hash) - |> EthSignature.compact_to_components!(chain_id) + + {recovery_id |> recovery_id_to_v(chain_id), r, s} + end @doc """ Returns a hash of a given transaction according to the @@ -200,4 +204,12 @@ defmodule TTEth.Transactions.LegacyTransaction do %{trx | v: v, r: r, s: s} end + + ## Private. + + defp recovery_id_to_v(recovery_id, _chain_id = nil), + do: @base_v + recovery_id + + defp recovery_id_to_v(recovery_id, chain_id) when is_integer(chain_id), + do: chain_id * 2 + @base_v_eip_155 + recovery_id end diff --git a/lib/tt_eth/type/signature.ex b/lib/tt_eth/type/signature.ex index 7ca8319..5de8ecc 100644 --- a/lib/tt_eth/type/signature.ex +++ b/lib/tt_eth/type/signature.ex @@ -8,12 +8,11 @@ defmodule TTEth.Type.Signature do @secp256k1n 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 @secp256k1n_2 round(Float.floor(@secp256k1n / 2)) - @base_recovery_id 27 - @base_recovery_id_eip_155 35 + @base_v 27 @ethereum_magic <<0x19, "Ethereum Signed Message:", ?\n>> @type recovery_id :: non_neg_integer() - @type v :: byte + @type v :: non_neg_integer() @type r :: non_neg_integer() @type s :: non_neg_integer() @type components :: {v, r, s} @@ -42,44 +41,48 @@ defmodule TTEth.Type.Signature do do: <<@ethereum_magic, "#{byte_size(msg)}"::binary, msg::binary>> @doc """ - This is for personal signed data, not for transaction data. + This is for a signed message, not for transaction data. `TTEth.Wallet.personal_sign/2` should be used instead. SEE: https://eips.ethereum.org/EIPS/eip-191 SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign """ - @deprecated "See Wallet.t() for using a wallet adapter instead." - @spec sign(message :: binary, private_key :: binary, chain_id | nil) :: + @deprecated "Use Wallet.personal_sign/2 to sign using a wallet adapter instead." + @spec sign(message :: binary, private_key :: binary) :: {:ok, components} | {:error, :cannot_sign} - def sign(message, private_key, chain_id \\ nil), - do: - message - |> digest() - |> Secp256k1.ecdsa_sign_compact(private_key) - |> compact_to_components(chain_id) - - @spec sign!(binary, binary, chain_id | nil) :: components - def sign!(message, private_key, chain_id \\ nil) do - {:ok, ret} = message |> sign(private_key, chain_id) + def sign(message, private_key) do + message + |> digest() + |> Secp256k1.ecdsa_sign_compact(private_key) + |> case do + {:ok, {<>, recovery_id}} -> + {:ok, {@base_v + recovery_id, r, s}} + + {:error, _} -> + {:error, :cannot_sign} + end + end + + @spec sign!(binary, binary) :: components + def sign!(message, private_key) do + {:ok, ret} = message |> sign(private_key) ret end @doc """ Given a hash, signature components and optional chain id, returns the public key. - Note that this is EIP-155 aware, so if you pass a chain id, it will use it to - recover the `recovery_id` from `v`. - - The `chain_id` should not be passed if this is for a EIP-191 message. + Note: The `chain_id` has been dropped and should not be passed if this is for a EIP-191 message. + not for EIP-155 transactions. """ - @spec recover(binary, components, chain_id | nil) :: {:ok, binary} | {:error, binary} - def recover(hash, {v, r, s}, chain_id \\ nil) do + @spec recover(binary, components) :: {:ok, binary} | {:error, binary} + def recover(hash, {v, r, s}) do sig = BitHelper.pad(:binary.encode_unsigned(r), 32) <> BitHelper.pad(:binary.encode_unsigned(s), 32) - recovery_id = v_to_recovery_id(v, chain_id) + recovery_id = v - @base_v case Secp256k1.ecdsa_recover_compact(hash, sig, recovery_id) do {:ok, public_key} -> {:ok, public_key} @@ -87,9 +90,9 @@ defmodule TTEth.Type.Signature do end end - @spec recover!(digest, components, chain_id | nil) :: binary - def recover!(digest, components, chain_id \\ nil) do - {:ok, pub} = digest |> recover(components, chain_id) + @spec recover!(digest, components) :: binary + def recover!(digest, components) do + {:ok, pub} = digest |> recover(components) pub end @@ -106,22 +109,6 @@ defmodule TTEth.Type.Signature do r > 0 and r < @secp256k1n and s > 0 and s <= @secp256k1n_2 - def compact_to_components(compact_signature, chain_id \\ nil) do - compact_signature - |> case do - {:ok, {<>, v}} -> - {:ok, {v_from_recovery_id(v, chain_id), r, s}} - - {:error, _} -> - {:error, :cannot_sign} - end - end - - def compact_to_components!(compact_signature, chain_id \\ nil) do - {:ok, {_v, _r, _s} = comps} = compact_signature |> compact_to_components(chain_id) - comps - end - @spec components(binary) :: {:error, :invalid_signature} | {:ok, components} def components(sig) do sig @@ -148,26 +135,9 @@ defmodule TTEth.Type.Signature do |> from_components!() |> to_human!() - # Private. - - defp uses_chain_id?(v), - do: v >= @base_recovery_id_eip_155 - - @spec v_from_recovery_id(recovery_id, chain_id) :: non_neg_integer() - defp v_from_recovery_id(recovery_id, chain_id) do - if is_nil(chain_id) do - @base_recovery_id + recovery_id - else - chain_id * 2 + @base_recovery_id_eip_155 + recovery_id - end - end - - @spec v_to_recovery_id(non_neg_integer(), chain_id) :: non_neg_integer() - defp v_to_recovery_id(v, chain_id) do - if not is_nil(chain_id) and uses_chain_id?(v) do - v - chain_id * 2 - @base_recovery_id_eip_155 - else - v - @base_recovery_id - end - end + def to_human_from_components!({<>, v} = _components) + when is_integer(v), + do: + {v, r, s} + |> to_human_from_components!() end From 5af4a0106ae9f443d0b2e5ba1b646b093f45d823 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 1 Feb 2024 10:39:48 +0000 Subject: [PATCH 09/14] Updates GitHub action version, excludes certain otp/elixir combinations. --- .github/workflows/main.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 04f3e25..975b9fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,20 +6,31 @@ on: push jobs: test: name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + runs-on: "ubuntu-20.04" strategy: matrix: - otp: ['22.x', '24.x'] - elixir: ['1.11.4', '1.13.4'] - runs-on: ubuntu-20.04 + otp: ["22.x", "24.x", "25.x"] + elixir: ["1.11.4", "1.13.4", "1.14.5"] + exclude: + - otp: "25.x" + elixir: "1.11.4" + - otp: "22.x" + elixir: "1.14.5" + - otp: "24.x" + elixir: "1.14.5" steps: - - uses: actions/checkout@v2 + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Setup Elixir / Erlang" uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} + - name: "Get the deps" run: mix deps.get + - name: "Run the tests" - run: mix test \ No newline at end of file + run: mix test From ce05ec959e969b248ad86d82c866482836e04bc5 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 1 Feb 2024 10:55:14 +0000 Subject: [PATCH 10/14] Drops unused adapter key from Wallet. --- lib/tt_eth/local_wallet.ex | 1 - lib/tt_eth/wallet.ex | 21 +++++++++++++++++---- test/tt_eth/wallet_test.exs | 1 - 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/tt_eth/local_wallet.ex b/lib/tt_eth/local_wallet.ex index b16c4cf..a869159 100644 --- a/lib/tt_eth/local_wallet.ex +++ b/lib/tt_eth/local_wallet.ex @@ -35,7 +35,6 @@ defmodule TTEth.LocalWallet do address = pub |> Address.from_public_key!() [ - adapter: @for, address: address, public_key: pub, human_address: address |> Address.to_human!(), diff --git a/lib/tt_eth/wallet.ex b/lib/tt_eth/wallet.ex index 3c63ec9..b23b08c 100644 --- a/lib/tt_eth/wallet.ex +++ b/lib/tt_eth/wallet.ex @@ -4,11 +4,24 @@ defmodule TTEth.Wallet do """ alias TTEth.Protocols.Wallet, as: WalletProtocol alias TTEth.Type.Signature, as: EthSignature + alias TTEth.Type.Address, as: EthAddress + alias TTEth.Type.PublicKey, as: EthPublicKey - @type t :: %__MODULE__{} + @typedoc """ + Represents a Wallet. + The underlying adapter lives under `_adapter`. + """ + @type t :: %__MODULE__{ + address: EthAddress.t(), + public_key: EthPublicKey.t(), + human_address: String.t(), + human_public_key: String.t(), + _adapter: struct() + } + + @enforce_keys [:address, :public_key, :human_address, :human_public_key, :_adapter] defstruct [ - :adapter, :address, :public_key, :human_address, @@ -63,7 +76,7 @@ defmodule TTEth.Wallet do |> WalletProtocol.sign(hash_digest) @doc """ - The same as `sign/2` but raises if the signing process is not successful. + Same as `sign/2` but raises if the signing process is not successful. """ def sign!(%__MODULE__{} = wallet, "" <> hash_digest) do {:ok, ret} = wallet |> sign(hash_digest) @@ -84,7 +97,7 @@ defmodule TTEth.Wallet do |> sign(plaintext |> EthSignature.digest()) @doc """ - The same as `personal_sign/2` but raises if the signing process is not successful. + Same as `personal_sign/2` but raises if the signing process is not successful. """ def personal_sign!(%__MODULE__{} = wallet, "" <> plaintext) do {:ok, comps} = wallet |> personal_sign(plaintext) diff --git a/test/tt_eth/wallet_test.exs b/test/tt_eth/wallet_test.exs index de83b0a..f56b324 100644 --- a/test/tt_eth/wallet_test.exs +++ b/test/tt_eth/wallet_test.exs @@ -55,7 +55,6 @@ defmodule TTEth.WalletTest do wallet |> assert_match(%Wallet{ - adapter: LocalWallet, address: ^address, public_key: ^public_key, human_address: ^human_address, From 24dd9677b86216baa10d8cf2841cf2b623aab05f Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 1 Feb 2024 12:28:18 +0000 Subject: [PATCH 11/14] Refactors the signature and wallet modules. --- lib/tt_eth/type/signature.ex | 68 +++++++++++++----------------------- lib/tt_eth/wallet.ex | 13 ++++--- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/lib/tt_eth/type/signature.ex b/lib/tt_eth/type/signature.ex index 5de8ecc..575d2e8 100644 --- a/lib/tt_eth/type/signature.ex +++ b/lib/tt_eth/type/signature.ex @@ -6,13 +6,12 @@ defmodule TTEth.Type.Signature do alias TTEth.{BitHelper, Secp256k1} import TTEth, only: [keccak: 1] - @secp256k1n 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 - @secp256k1n_2 round(Float.floor(@secp256k1n / 2)) @base_v 27 + @valid_vs [@base_v, @base_v + 1] @ethereum_magic <<0x19, "Ethereum Signed Message:", ?\n>> @type recovery_id :: non_neg_integer() - @type v :: non_neg_integer() + @type v :: 27 | 28 @type r :: non_neg_integer() @type s :: non_neg_integer() @type components :: {v, r, s} @@ -46,23 +45,18 @@ defmodule TTEth.Type.Signature do `TTEth.Wallet.personal_sign/2` should be used instead. SEE: https://eips.ethereum.org/EIPS/eip-191 + SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign """ - @deprecated "Use Wallet.personal_sign/2 to sign using a wallet adapter instead." + @deprecated "Use Wallet.personal_sign/2 instead." @spec sign(message :: binary, private_key :: binary) :: {:ok, components} | {:error, :cannot_sign} - def sign(message, private_key) do - message - |> digest() - |> Secp256k1.ecdsa_sign_compact(private_key) - |> case do - {:ok, {<>, recovery_id}} -> - {:ok, {@base_v + recovery_id, r, s}} - - {:error, _} -> - {:error, :cannot_sign} - end - end + def sign(message, private_key), + do: + message + |> digest() + |> Secp256k1.ecdsa_sign_compact(private_key) + |> compact_to_components() @spec sign!(binary, binary) :: components def sign!(message, private_key) do @@ -72,19 +66,16 @@ defmodule TTEth.Type.Signature do @doc """ Given a hash, signature components and optional chain id, returns the public key. - - Note: The `chain_id` has been dropped and should not be passed if this is for a EIP-191 message. - not for EIP-155 transactions. """ @spec recover(binary, components) :: {:ok, binary} | {:error, binary} - def recover(hash, {v, r, s}) do + def recover(digest, {v, r, s}) do sig = BitHelper.pad(:binary.encode_unsigned(r), 32) <> BitHelper.pad(:binary.encode_unsigned(s), 32) recovery_id = v - @base_v - case Secp256k1.ecdsa_recover_compact(hash, sig, recovery_id) do + case Secp256k1.ecdsa_recover_compact(digest, sig, recovery_id) do {:ok, public_key} -> {:ok, public_key} {:error, reason} -> {:error, to_string(reason)} end @@ -96,22 +87,18 @@ defmodule TTEth.Type.Signature do pub end - @spec is_signature_valid?(components, chain_id, keyword) :: boolean - def is_signature_valid?({v, r, s}, _chain_id, max_s: :secp256k1n), - do: - (v == 27 || v == 28) and - r > 0 and r < @secp256k1n and - s > 0 and s < @secp256k1n + @doc """ + Takes the compact signature and returns the components with `v` added. + """ + def compact_to_components({:ok, {<>, recovery_id}}), + do: {:ok, {@base_v + recovery_id, r, s}} - def is_signature_valid?({r, s, v}, chain_id, max_s: :secp256k1n_2), - do: - (v == 27 || v == 28 || v == chain_id * 2 + 35 || v == chain_id * 2 + 36) and - r > 0 and r < @secp256k1n and - s > 0 and s <= @secp256k1n_2 + def compact_to_components({:error, _}), + do: {:error, :cannot_sign} @spec components(binary) :: {:error, :invalid_signature} | {:ok, components} - def components(sig) do - sig + def components(signature) do + signature |> from_human() |> case do {:ok, <>} -> {:ok, {v, r, s}} @@ -120,12 +107,13 @@ defmodule TTEth.Type.Signature do end @spec components!(binary) :: components - def components!(sig) do - {:ok, {_v, _r, _s} = comps} = sig |> components() + def components!(signature) do + {:ok, {_v, _r, _s} = comps} = signature |> components() comps end - def from_components!({v, r, s}), + @spec from_components!(components) :: binary() + def from_components!({v, r, s} = _components) when v in @valid_vs, do: <> @spec to_human_from_components!(components) :: <<_::16, _::_*8>> @@ -134,10 +122,4 @@ defmodule TTEth.Type.Signature do components |> from_components!() |> to_human!() - - def to_human_from_components!({<>, v} = _components) - when is_integer(v), - do: - {v, r, s} - |> to_human_from_components!() end diff --git a/lib/tt_eth/wallet.ex b/lib/tt_eth/wallet.ex index b23b08c..f886fad 100644 --- a/lib/tt_eth/wallet.ex +++ b/lib/tt_eth/wallet.ex @@ -70,16 +70,16 @@ defmodule TTEth.Wallet do @doc """ Signs a digest using the passed wallet. """ - def sign(%__MODULE__{_adapter: wallet}, "" <> hash_digest), + def sign(%__MODULE__{_adapter: wallet}, "" <> digest), do: wallet - |> WalletProtocol.sign(hash_digest) + |> WalletProtocol.sign(digest) @doc """ Same as `sign/2` but raises if the signing process is not successful. """ - def sign!(%__MODULE__{} = wallet, "" <> hash_digest) do - {:ok, ret} = wallet |> sign(hash_digest) + def sign!(%__MODULE__{} = wallet, "" <> digest) do + {:ok, ret} = wallet |> sign(digest) ret end @@ -88,13 +88,18 @@ defmodule TTEth.Wallet do This is for personal signed data, not for transaction data. + Components of the signature are returned to maintain compatibility with + `TTEth.Type.Signature.sign/2`. + SEE: https://eips.ethereum.org/EIPS/eip-191 + SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign """ def personal_sign(%__MODULE__{} = wallet, "" <> plaintext), do: wallet |> sign(plaintext |> EthSignature.digest()) + |> EthSignature.compact_to_components() @doc """ Same as `personal_sign/2` but raises if the signing process is not successful. From d01d95ac08637d08ab1285ffaa66ab23b50c8837 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 1 Feb 2024 14:25:15 +0000 Subject: [PATCH 12/14] Tidies some types and adds documentation. --- lib/tt_eth/protocols/wallet.ex | 6 +++++- lib/tt_eth/secp256k1.ex | 14 ++++++++------ lib/tt_eth/type/signature.ex | 8 +++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/tt_eth/protocols/wallet.ex b/lib/tt_eth/protocols/wallet.ex index 8b60d9c..f88fc51 100644 --- a/lib/tt_eth/protocols/wallet.ex +++ b/lib/tt_eth/protocols/wallet.ex @@ -3,6 +3,9 @@ defprotocol TTEth.Protocols.Wallet do Protocol for wallet adapters. """ + @typedoc """ + All the types that implement this protocol. + """ @type t :: any() @doc """ @@ -13,5 +16,6 @@ defprotocol TTEth.Protocols.Wallet do @doc """ Returns a signature. """ - def sign(t, hash_digest) + @spec sign(t, binary()) :: binary() + def sign(t, digest) end diff --git a/lib/tt_eth/secp256k1.ex b/lib/tt_eth/secp256k1.ex index 627da62..57d8d19 100644 --- a/lib/tt_eth/secp256k1.ex +++ b/lib/tt_eth/secp256k1.ex @@ -5,13 +5,16 @@ defmodule TTEth.Secp256k1 do @valid_recovery_ids [0, 1] + @type recovery_id :: 0 | 1 + @type compact_signature_return :: {:ok, {binary, recovery_id}} | {:error, atom} + @doc """ Delegates to `ExSecp256k1.recover_compact/3` with guards around the `recovery_id` value. """ @spec ecdsa_recover_compact(binary(), binary(), non_neg_integer()) :: {:ok, binary()} | {:error, atom()} - def ecdsa_recover_compact(hash, sig, recovery_id) when recovery_id in @valid_recovery_ids, - do: ExSecp256k1.recover_compact(hash, sig, recovery_id) + def ecdsa_recover_compact(hash, signature, recovery_id) when recovery_id in @valid_recovery_ids, + do: ExSecp256k1.recover_compact(hash, signature, recovery_id) def ecdsa_recover_compact(_hash, _sig, _recovery_id), do: {:error, :invalid_recovery_id} @@ -19,8 +22,7 @@ defmodule TTEth.Secp256k1 do @doc """ Delegates to `ExSecp256k1.sign_compact/2`. """ - @spec ecdsa_sign_compact(binary(), binary()) :: - {:ok, {binary(), binary()}} | {:error, atom()} + @spec ecdsa_sign_compact(binary(), binary()) :: compact_signature_return def ecdsa_sign_compact(hash, private_key), do: ExSecp256k1.sign_compact(hash, private_key) @@ -28,6 +30,6 @@ defmodule TTEth.Secp256k1 do Delegates to `ExSecp256k1.create_public_key/1`. """ @spec ec_pubkey_create(binary()) :: {:ok, binary()} | atom() - def ec_pubkey_create(priv), - do: ExSecp256k1.create_public_key(priv) + def ec_pubkey_create(private_key), + do: ExSecp256k1.create_public_key(private_key) end diff --git a/lib/tt_eth/type/signature.ex b/lib/tt_eth/type/signature.ex index 575d2e8..35ac6a7 100644 --- a/lib/tt_eth/type/signature.ex +++ b/lib/tt_eth/type/signature.ex @@ -49,8 +49,7 @@ defmodule TTEth.Type.Signature do SEE: https://ethereum.org/en/developers/docs/apis/json-rpc#eth_sign """ @deprecated "Use Wallet.personal_sign/2 instead." - @spec sign(message :: binary, private_key :: binary) :: - {:ok, components} | {:error, :cannot_sign} + @spec sign(message :: binary, private_key :: binary) :: Secp256k1.compact_signature_return() def sign(message, private_key), do: message @@ -90,13 +89,16 @@ defmodule TTEth.Type.Signature do @doc """ Takes the compact signature and returns the components with `v` added. """ + @spec compact_to_components(Secp256k1.compact_signature_return()) :: + {:ok, components} | {:error, :cannot_sign} + def compact_to_components({:ok, {<>, recovery_id}}), do: {:ok, {@base_v + recovery_id, r, s}} def compact_to_components({:error, _}), do: {:error, :cannot_sign} - @spec components(binary) :: {:error, :invalid_signature} | {:ok, components} + @spec components(binary) :: {:ok, components} | {:error, :invalid_signature} def components(signature) do signature |> from_human() From 81e266bf6267da900a8502ed5906afe4c567f4c0 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Thu, 1 Feb 2024 17:25:49 +0000 Subject: [PATCH 13/14] Adds wallet tests. --- test/support/test_token.ex | 1 + .../transactions/eip1559_transaction_test.exs | 4 +- test/tt_eth/wallet_test.exs | 59 +++++++++++++++++++ test/tt_eth_test.exs | 2 +- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/test/support/test_token.ex b/test/support/test_token.ex index 7b11b63..0ff0051 100644 --- a/test/support/test_token.ex +++ b/test/support/test_token.ex @@ -1,4 +1,5 @@ defmodule TTEth.TestToken do + @moduledoc false use TTEth.Contract, abi_file: Application.app_dir(:tt_eth, "priv/fixtures/contract_abis/TestToken.abi") end diff --git a/test/tt_eth/transactions/eip1559_transaction_test.exs b/test/tt_eth/transactions/eip1559_transaction_test.exs index 1597a62..a19aad8 100644 --- a/test/tt_eth/transactions/eip1559_transaction_test.exs +++ b/test/tt_eth/transactions/eip1559_transaction_test.exs @@ -5,7 +5,7 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do alias TTEth.Wallet # Polygon Mumbai. - @chain_id 80001 + @chain_id 80_001 @private_key_human "0x62aa6ec41b56439d2c5df352c45a00389cef262b3761e13c6481e35ab027d262" @to_address_human "0x38f153fdd399ff2cf64704c6a4b16d3fd9ddcd69" @@ -113,7 +113,7 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do data: @tx_data, chain_id: @chain_id, nonce: @nonce, - gas_limit: 21000, + gas_limit: 21_000, max_fee_per_gas: 3_000_000_000, max_priority_fee_per_gas: 3_000_000_001, value: 0 diff --git a/test/tt_eth/wallet_test.exs b/test/tt_eth/wallet_test.exs index f56b324..41969a9 100644 --- a/test/tt_eth/wallet_test.exs +++ b/test/tt_eth/wallet_test.exs @@ -39,6 +39,62 @@ defmodule TTEth.WalletTest do end end + describe "sign/2" do + setup :build_wallet + + test "given a wallet and digest, signs the digest", %{wallet: wallet} do + wallet + |> Wallet.sign("some plaintext" |> TTEth.keccak()) + |> assert_match({:ok, {<<_signature::512>>, recovery_id}} when recovery_id in [0, 1]) + end + + test "returns an error tuple if a failure happens", %{wallet: wallet} do + wallet + |> Wallet.sign("some plaintext") + |> assert_equal({:error, :wrong_message_size}) + end + end + + describe "sign!/2" do + setup :build_wallet + + test "same as sign/2", %{wallet: wallet} do + digest = "some plaintext" |> TTEth.keccak() + + {:ok, compact_signature} = + wallet + |> Wallet.sign(digest) + + wallet + |> Wallet.sign!(digest) + |> assert_match(^compact_signature) + end + end + + describe "personal_sign/2" do + setup :build_wallet + + test "signs a plaintext using the EIP-191 standard", %{wallet: wallet} do + wallet + |> Wallet.personal_sign("some plaintext") + |> assert_match({:ok, {v, _r, _s}} when v in [27, 28]) + end + end + + describe "personal_sign!/2" do + setup :build_wallet + + test "same as personal_sign/2", %{wallet: wallet} do + {:ok, components} = + wallet + |> Wallet.personal_sign("some plaintext") + + wallet + |> Wallet.personal_sign!("some plaintext") + |> assert_match(^components) + end + end + ## Private. defp assert_match_ternary_wallet(wallet) do @@ -65,4 +121,7 @@ defmodule TTEth.WalletTest do } }) end + + defp build_wallet(_), + do: %{wallet: Wallet.new()} end diff --git a/test/tt_eth_test.exs b/test/tt_eth_test.exs index 06c4f0c..1971076 100644 --- a/test/tt_eth_test.exs +++ b/test/tt_eth_test.exs @@ -3,7 +3,7 @@ defmodule TTEthTest do import TTEth, only: [binary_to_hex!: 1] doctest TTEth, import: true - @chain_id 12345 + @chain_id 12_345 describe "new_keypair/0" do test "generates a public and private key" do From bd8e1eed6742278d6ae5f34eea591b02fb8a0982 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Mon, 11 Mar 2024 12:22:40 +0000 Subject: [PATCH 14/14] Merges the wallet protocol and behaviour into behaviour. --- lib/tt_eth/behaviours/wallet.ex | 17 -------- lib/tt_eth/behaviours/wallet_adapter.ex | 32 ++++++++++++++ lib/tt_eth/local_wallet.ex | 58 ++++++++++++------------- lib/tt_eth/protocols/wallet.ex | 21 --------- lib/tt_eth/wallet.ex | 28 ++++++------ mix.exs | 5 +-- test/tt_eth/local_wallet_test.exs | 41 ++++++++--------- 7 files changed, 95 insertions(+), 107 deletions(-) delete mode 100644 lib/tt_eth/behaviours/wallet.ex create mode 100644 lib/tt_eth/behaviours/wallet_adapter.ex delete mode 100644 lib/tt_eth/protocols/wallet.ex diff --git a/lib/tt_eth/behaviours/wallet.ex b/lib/tt_eth/behaviours/wallet.ex deleted file mode 100644 index d818300..0000000 --- a/lib/tt_eth/behaviours/wallet.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule TTEth.Behaviours.Wallet do - @moduledoc """ - Behaviour for wallet adapters. - """ - - @typedoc """ - This represents the config for a wallet adapter. - - Check the documentation for the adapter for specific configuration options. - """ - @type config :: map() | binary() - - @doc """ - Returns a new populated wallet struct. - """ - @callback new(config) :: struct() -end diff --git a/lib/tt_eth/behaviours/wallet_adapter.ex b/lib/tt_eth/behaviours/wallet_adapter.ex new file mode 100644 index 0000000..b386d03 --- /dev/null +++ b/lib/tt_eth/behaviours/wallet_adapter.ex @@ -0,0 +1,32 @@ +defmodule TTEth.Behaviours.WalletAdapter do + @moduledoc """ + Defines a shared behaviour for wallet adapters. + """ + + @typedoc """ + This represents the config for a wallet adapter. + + Check the documentation for the adapter for specific configuration options. + """ + @type config :: map() | binary() + + @typedoc """ + Represents a wallet adapter. + """ + @type wallet_adapter :: struct() + + @doc """ + Returns a new populated wallet adapter struct. + """ + @callback new(config) :: wallet_adapter + + @doc """ + Provides the attributes needed to build a `Wallet.t` using the passed `wallet_adapter`. + """ + @callback wallet_attrs(wallet_adapter) :: map() + + @doc """ + Signs `digest` using the given `wallet_adapter`. + """ + @callback sign(wallet_adapter, digest :: binary()) :: {:ok, binary()} | {:error, any()} +end diff --git a/lib/tt_eth/local_wallet.ex b/lib/tt_eth/local_wallet.ex index a869159..0723eb7 100644 --- a/lib/tt_eth/local_wallet.ex +++ b/lib/tt_eth/local_wallet.ex @@ -14,44 +14,19 @@ defmodule TTEth.LocalWallet do """ alias TTEth.Type.{Address, PublicKey, PrivateKey} alias TTEth.Secp256k1 - alias TTEth.Behaviours.Wallet, as: WalletBehaviour + alias TTEth.Behaviours.WalletAdapter, as: WalletAdapterBehaviour @type t :: %__MODULE__{} - @behaviour WalletBehaviour + @behaviour WalletAdapterBehaviour defstruct [ :private_key, :human_private_key ] - defimpl TTEth.Protocols.Wallet, for: __MODULE__ do - def wallet_attrs(%@for{} = wallet) do - pub = - wallet.private_key - |> PublicKey.from_private_key!() - |> PublicKey.from_human!() - - address = pub |> Address.from_public_key!() - - [ - address: address, - public_key: pub, - human_address: address |> Address.to_human!(), - human_public_key: pub |> PublicKey.to_human!(), - _adapter: wallet - ] - end - - def sign(%@for{} = wallet, "" <> digest), - do: - digest - |> Secp256k1.ecdsa_sign_compact(wallet.private_key) - end - - @impl WalletBehaviour - - def new(%{private_key: private_key} = _config), + @impl WalletAdapterBehaviour + def new(%{private_key: "" <> private_key} = _config), do: __MODULE__ |> struct!(%{ @@ -59,11 +34,36 @@ defmodule TTEth.LocalWallet do human_private_key: private_key }) + @impl WalletAdapterBehaviour def new("" <> private_key = _config), do: %{private_key: private_key} |> new() + @impl WalletAdapterBehaviour + def wallet_attrs(%__MODULE__{} = wallet) do + pub = + wallet.private_key + |> PublicKey.from_private_key!() + |> PublicKey.from_human!() + + address = pub |> Address.from_public_key!() + + %{ + address: address, + public_key: pub, + human_address: address |> Address.to_human!(), + human_public_key: pub |> PublicKey.to_human!(), + _adapter: wallet + } + end + + @impl WalletAdapterBehaviour + def sign(%__MODULE__{} = wallet, "" <> digest), + do: + digest + |> Secp256k1.ecdsa_sign_compact(wallet.private_key) + ## Helpers. @doc """ diff --git a/lib/tt_eth/protocols/wallet.ex b/lib/tt_eth/protocols/wallet.ex deleted file mode 100644 index f88fc51..0000000 --- a/lib/tt_eth/protocols/wallet.ex +++ /dev/null @@ -1,21 +0,0 @@ -defprotocol TTEth.Protocols.Wallet do - @moduledoc """ - Protocol for wallet adapters. - """ - - @typedoc """ - All the types that implement this protocol. - """ - @type t :: any() - - @doc """ - Returns a map of attributes used to construct a `TTEth.Wallet.t()`. - """ - def wallet_attrs(t) - - @doc """ - Returns a signature. - """ - @spec sign(t, binary()) :: binary() - def sign(t, digest) -end diff --git a/lib/tt_eth/wallet.ex b/lib/tt_eth/wallet.ex index f886fad..4d971a5 100644 --- a/lib/tt_eth/wallet.ex +++ b/lib/tt_eth/wallet.ex @@ -2,7 +2,6 @@ defmodule TTEth.Wallet do @moduledoc """ Provides a handle struct - `TTEth.Wallet.t()` for encapsulating a wallet. """ - alias TTEth.Protocols.Wallet, as: WalletProtocol alias TTEth.Type.Signature, as: EthSignature alias TTEth.Type.Address, as: EthAddress alias TTEth.Type.PublicKey, as: EthPublicKey @@ -59,21 +58,22 @@ defmodule TTEth.Wallet do @doc """ Creates a new wallet from an underlying wallet or a random one. """ - def new(wallet \\ TTEth.LocalWallet.generate()) when is_struct(wallet), - do: - __MODULE__ - |> struct!( - wallet - |> WalletProtocol.wallet_attrs() - ) + def new(%wallet_adapter{} = wallet \\ TTEth.LocalWallet.generate()) + when is_struct(wallet), + do: + __MODULE__ + |> struct!( + wallet + |> wallet_adapter.wallet_attrs() + ) @doc """ Signs a digest using the passed wallet. """ - def sign(%__MODULE__{_adapter: wallet}, "" <> digest), + def sign(%__MODULE__{_adapter: %wallet_adapter{} = wallet_adapter_state}, "" <> digest), do: - wallet - |> WalletProtocol.sign(digest) + wallet_adapter_state + |> wallet_adapter.sign(digest) @doc """ Same as `sign/2` but raises if the signing process is not successful. @@ -111,9 +111,9 @@ defmodule TTEth.Wallet do ## Private. - defp build_with_adapter!(config, adapter \\ TTEth.LocalWallet) - when (is_binary(config) or is_map(config)) and is_atom(adapter), + defp build_with_adapter!(config, wallet_adapter \\ TTEth.LocalWallet) + when (is_binary(config) or is_map(config)) and is_atom(wallet_adapter), do: config - |> adapter.new() + |> wallet_adapter.new() end diff --git a/mix.exs b/mix.exs index 7008d35..bd38b0f 100644 --- a/mix.exs +++ b/mix.exs @@ -47,10 +47,7 @@ defmodule TTEth.MixProject do Behaviours: [ TTEth.Behaviours.ChainClient, TTEth.Behaviours.Transaction, - TTEth.Behaviours.Wallet - ], - Protocols: [ - TTEth.Protocols.Wallet + TTEth.Behaviours.WalletAdapter ], Types: [ TTEth.Type.Address, diff --git a/test/tt_eth/local_wallet_test.exs b/test/tt_eth/local_wallet_test.exs index 9df82e4..aab8834 100644 --- a/test/tt_eth/local_wallet_test.exs +++ b/test/tt_eth/local_wallet_test.exs @@ -2,49 +2,46 @@ defmodule TTEth.LocalWalletTest do use TTEth.Case alias TTEth.LocalWallet alias TTEth.Type.PrivateKey - alias TTEth.Protocols.Wallet, as: WalletProtocol @human_private_key "0xfa015243f2e6d8694ab037a7987dc73b1630fc8cb1ce82860344684c15d24026" - describe "implements TTEth.Protocols.Wallet protocol" do - setup :build_local_wallet + describe "implements TTEth.Behaviours.Wallet behaviour" do + setup [:build_local_wallet] + + test "new/1 - initializes a new wallet adapter struct" do + decoded_private_key = @human_private_key |> PrivateKey.from_human!() + + %{private_key: @human_private_key} + |> LocalWallet.new() + |> assert_match(%LocalWallet{ + private_key: ^decoded_private_key, + human_private_key: @human_private_key + }) + end test "wallet_attrs/1 - returns attributes needed when building a wallet", %{ - local_wallet: local_wallet + local_wallet: %wallet_adapter{} = local_wallet } do local_wallet - |> WalletProtocol.wallet_attrs() - |> assert_match( + |> wallet_adapter.wallet_attrs() + |> assert_match(%{ address: _, public_key: _, human_address: "0x" <> _, human_public_key: "0x" <> _, _adapter: ^local_wallet - ) + }) end test "sign/2 - signs the given digest with the wallet", %{ - local_wallet: local_wallet + local_wallet: %wallet_adapter{} = local_wallet } do local_wallet - |> WalletProtocol.sign("some plaintext" |> TTEth.keccak()) + |> wallet_adapter.sign("some plaintext" |> TTEth.keccak()) |> assert_match({:ok, {<<_signature::512>>, recovery_id}} when recovery_id in [0, 1]) end end - describe "implements TTEth.Behaviours.Wallet behaviour" do - test "new/1 - initializes a new CloudKMS struct" do - decoded_private_key = @human_private_key |> PrivateKey.from_human!() - - %{private_key: @human_private_key} - |> LocalWallet.new() - |> assert_match(%LocalWallet{ - private_key: ^decoded_private_key, - human_private_key: @human_private_key - }) - end - end - ## Private. defp build_local_wallet(_),