Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors wallet handling to allow for adapters. #7

Merged
merged 14 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
run: mix test
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ config :tt_eth,
secondary: "0xe2187dc017e880dded10c403f7c0d397afb11736ac027c1202e318b0dd345086",
ternary: "0xfa015243f2e6d8694ab037a7987dc73b1630fc8cb1ce82860344684c15d24026"
],
wallet_adapter: TTEth.LocalWallet,
transaction_module: TTEth.Transactions.LegacyTransaction
9 changes: 4 additions & 5 deletions lib/tt_eth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions lib/tt_eth/behaviours/chain_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/tt_eth/behaviours/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions lib/tt_eth/behaviours/wallet_adapter.ex
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions lib/tt_eth/chain_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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!()

Expand Down
5 changes: 3 additions & 2 deletions lib/tt_eth/chain_client_mock_impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule TTEth.ChainClientMockImpl do
Implementation default for tests.
"""
alias TTEth.Behaviours.ChainClient
alias TTEth.Wallet

@behaviour ChainClient

Expand All @@ -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 \\ []),
Expand Down
78 changes: 78 additions & 0 deletions lib/tt_eth/local_wallet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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.WalletAdapter, as: WalletAdapterBehaviour

@type t :: %__MODULE__{}

@behaviour WalletAdapterBehaviour

defstruct [
:private_key,
:human_private_key
]

@impl WalletAdapterBehaviour
def new(%{private_key: "" <> private_key} = _config),
do:
__MODULE__
|> struct!(%{
private_key: private_key |> PrivateKey.from_human!(),
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 """
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
16 changes: 10 additions & 6 deletions lib/tt_eth/secp256k1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@ defmodule TTEth.Secp256k1 do
Wrapper around `ExSecp256k1` functions.
"""

@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 [0, 1],
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}

@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)

@doc """
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
Loading
Loading