Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesduncombe committed Jan 30, 2024
1 parent 527d605 commit 76d791d
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 100 deletions.
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
8 changes: 8 additions & 0 deletions lib/tt_eth/behaviours/wallet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule TTEth.Behaviours.Wallet do
@moduledoc """
Behaviour for wallet adapters.
"""
@type config :: map() | binary()

@callback new(config) :: struct()
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
66 changes: 66 additions & 0 deletions lib/tt_eth/local_wallet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule TTEth.LocalWallet do
@moduledoc """
A local wallet, derived from a private key.
"""
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 new_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: new(%{private_key: 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: 16 additions & 0 deletions lib/tt_eth/protocols/wallet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defprotocol TTEth.Protocols.Wallet do
@moduledoc """
Protocol for wallet adapters.
"""
alias TTEth.Wallet

Check warning on line 5 in lib/tt_eth/protocols/wallet.ex

View workflow job for this annotation

GitHub Actions / OTP 22.x / Elixir 1.11.4

unused alias Wallet

Check warning on line 5 in lib/tt_eth/protocols/wallet.ex

View workflow job for this annotation

GitHub Actions / OTP 22.x / Elixir 1.13.4

unused alias Wallet

Check warning on line 5 in lib/tt_eth/protocols/wallet.ex

View workflow job for this annotation

GitHub Actions / OTP 24.x / Elixir 1.11.4

unused alias Wallet

Check warning on line 5 in lib/tt_eth/protocols/wallet.ex

View workflow job for this annotation

GitHub Actions / OTP 24.x / Elixir 1.13.4

unused alias Wallet

@doc """
Given a wallet and returns a map of attributes used to construct a `TTEth.Wallet.t()`.
"""
def new_attrs(t)

@doc """
Given a wallet and a hash digest and returns a signature.
"""
def sign(t, digest)
end
28 changes: 14 additions & 14 deletions lib/tt_eth/transactions/eip1559_transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -53,10 +53,10 @@ 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)
Expand Down Expand Up @@ -88,8 +88,10 @@ defmodule TTEth.Transactions.EIP1559Transaction do
@doc """
Returns a ECDSA signature (v,r,s) for a given hashed value.
"""
def sign_hash(hash, private_key) do
{:ok, {<<r::size(256), s::size(256)>>, v}} = Secp256k1.ecdsa_sign_compact(hash, private_key)
def sign_hash(hash, %Wallet{} = wallet) do
{:ok, {<<r::size(256), s::size(256)>>, v}} =
wallet
|> Wallet.sign(hash)

{v, r, s}
end
Expand All @@ -108,24 +110,22 @@ defmodule TTEth.Transactions.EIP1559Transaction do
@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
def sign_transaction(%__MODULE__{} = trx, %Wallet{} = wallet) do
{y_parity, r, s} =
trx
|> transaction_hash()
|> sign_hash(private_key)
|> sign_hash(wallet)

%{trx | y_parity: y_parity, r: r, s: s}
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),
do: <<trx.type>> <> encoded

## Private.

# 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: <<trx.type>> <> encoded

# Optionally add the YRS values.
defp maybe_add_yrs(base, %__MODULE__{} = trx, _include_vrs = true),
do:
Expand Down
51 changes: 28 additions & 23 deletions lib/tt_eth/transactions/legacy_transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ 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
import BitHelper, only: [encode_unsigned: 1]

@behaviour Transaction

@type private_key :: <<_::256>>

@type hash_v :: integer()
@type hash_r :: integer()
@type hash_s :: integer()
Expand Down Expand Up @@ -60,10 +59,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()

Expand Down Expand Up @@ -95,20 +94,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
Expand All @@ -122,28 +121,32 @@ 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}
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
def sign_hash(hash, %Wallet{} = wallet, chain_id \\ nil) do
{:ok, {<<r::size(256), s::size(256)>>, recovery_id}} =
Secp256k1.ecdsa_sign_compact(hash, private_key)
wallet
|> Wallet.sign(hash)

# Fork Ψ EIP-155
recovery_id =
Expand Down Expand Up @@ -192,19 +195,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
Expand Down
3 changes: 1 addition & 2 deletions lib/tt_eth/type/signature.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
defmodule TTEth.Type.Signature do
@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
Expand Down
Loading

0 comments on commit 76d791d

Please sign in to comment.