Skip to content

Commit

Permalink
new Setter module and mature public API
Browse files Browse the repository at this point in the history
  • Loading branch information
izelnakri committed Sep 9, 2017
1 parent b999371 commit bc83398
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 240 deletions.
70 changes: 67 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
# Eth
[![Hex Version](http://img.shields.io/hexpm/v/eth.svg?style=flat)](https://hex.pm/packages/eth) [![Hex docs](http://img.shields.io/badge/hex.pm-docs-green.svg?style=flat)](https://hexdocs.pm/eth/ETH.html)

Transaction signing not working perfectly yet.
# ETH
The essential Elixir library for interacting with Ethereum blockchain. You can now query the blockchain, create Ethereum wallets and sign/send transactions, all from Elixir / Erlang Virtual Machine. Absolute awesomeness.

## Example

**TODO: Add description**
```elixir
wallet = ETH.Wallet.create
# %{eth_address: "0x31CF67A272A23C7A11128C97FC3B2F4C13AFD87F",
# private_key: "C8E2F24A806A422034990C7391B4CEB7133CD3680987FEBB5750555F99F0FC83",
# public_key: "04C4AA07F234226CA90FB3E8BB1590D5BEB703E449700FE0B2DF539A948289EA75220CC837CA68F429F3FB3D6677B2D63CF66277888B8209D0B3F3229CE339654C"}

specific_private_key = "9756371A51D7FC25EDFC95A49DEF3806ED34DF2EBCA2065E543369E708C47374"
another_wallet = ETH.Wallet.create(specific_private_key)
# %{eth_address: "0x6A26B49D8046DC5B74D41E29F9A5CA7AD78EEC9B",
# private_key: "9756371A51D7FC25EDFC95A49DEF3806ED34DF2EBCA2065E543369E708C47374",
# public_key: "04D1F70F6048D1E22FBEBBF3AF462E3356747AB2BB81EC269C600BE6A53C3223472AA336DF0060719C6F3AEC45E40AE57ED39735B61B8F5EF989466D46CA1B72C0"}

accounts_in_your_client = ETH.get_accounts
# ["0xfdf5d02f2082753dda0817492d6efff7e76e47aa",
# "0xb9906b679aa8edd03fdf7fe396af4d9a77af4108",
# "0xae6463bf32efc106ad4300d902e572e1c43e6e9c",
# "0xb764c82ae23467be2cf90ab9019ee2464a3946f9",
# "0xfc5b5a6cd171f4123439f28fad9986c70572b35f",
# "0x7605c8812cfb51a7d2d16e598f521c9302d0ed7f",
# "0x7dab29cc88c2ecd69ec216b7d089a82bb95fe1ad",
# "0xba3a30f3c4fd2b4a44936b42ceea87ec3e53294a",
# "0xb3f4869ce14d6bbd659dc5d2f9a515b58b2765d2",
# "0x8c9cec7feacdbbf472ebcc4f61224d83c880896b"]

first_account_in_your_client = List.first(accounts_in_your_client)
first_account_private_key = "f121f608500f7e3379c813aa6df62864e35aa0b6cd11a2ff2c20ac84b5771fb2"

ETH.get_balance("0x31CF67A272A23C7A11128C97FC3B2F4C13AFD87F") # 0 # this account holds no ether
ETH.get_balance(first_account_in_your_client, :wei) # 1.0e20 -> in this example this address holds 100 ether / 1.0e20 wei

ETH.get_transaction_count(first_account_in_your_client) # 0

{:ok, tx_hash} = ETH.send_transaction(%{
from: first_account_in_your_client, to: wallet[:eth_address], value: 22
}, first_account_private_key)
# {:ok, "0x13893d677251ddb9259263490504f3e611a0a7bff23b108641d2cb08b7af21dc"}

ETH.get_transaction(tx_hash)
# %{block_hash: "0xf9917088fc6750677cc1cfb4f7dcab453b21c7de2cb22ed7e6753df058bec5cf",
# block_number: 1, from: "0xfdf5d02f2082753dda0817492d6efff7e76e47aa",
# gas: 21000, gas_price: 20000000000,
# hash: "0x13893d677251ddb9259263490504f3e611a0a7bff23b108641d2cb08b7af21dc",
# input: "0", nonce: 0, to: "0x725316bb37d202b0eb203cd83238c31e983a7936",
# transaction_index: 0, value: 22}

ETH.get_balance(first_account_in_your_client) # 99.99958 # in ether
ETH.get_balance(wallet[:eth_address], :wei) # 22.0 # in wei
```

Warning: This library uses the Ethereum JSON-RPC under the hood, so you need an ethereum client such as parity/geth or testrpc to use of most of the API.

### Credits

- [Izel Nakri](https://github.com/izelnakri) - Spent serious mental energy to build this library, I reverse engineered ethereum JavaScript libraries in Elixir so you don't have to.

Additional thanks to:
- ExRLP
- Ethereumex
- keccakf1600
- libsecp256k1

### TODO:
contract creation via

## Installation

Expand Down
5 changes: 0 additions & 5 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ use Mix.Config
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

config :ethereumex,
scheme: "http",
host: "localhost",
port: 8545

# You can configure your application as:
#
# config :eth, key: :value
Expand Down
82 changes: 0 additions & 82 deletions lib/eth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,86 +14,4 @@ defmodule ETH do
:world
"""

# def sign_transaction(
# source_eth_address, value, target_eth_address,
# options \\ [gas_price: 100, gas_limit: 1000, data: "", chain_id: 3]
# ) do
# gas_price = options[:gas_price] |> Hexate.encode
# gas_limit = options[:gas_limit] |> Hexate.encode
# data = options[:data] |> Hexate.encode
#
# nonce = case options[:nonce] do
# nil -> ETH.Query.get_transaction_count(source_eth_address)
# _ -> options[:nonce]
# end
#
# # NOTE: calc nonce
# %{
# to: target_eth_address, value: Hexate.encode(value), gas_price: gas_price,
# gas_limit: gas_limit, data: data, chain_id: 3
# }
# # get nonce and make a transaction map -> sign_transaction -> send it to client
# end


# def sign_transaction(transaction, private_key) do # must have chain_id
# hash = hash_transaction(transaction)
# decoded_private_key = Base.decode16!(private_key, case: :lower)
# [signature: signature, recovery: recovery] = secp256k1_signature(hash, decoded_private_key)
#
# << r :: binary-size(32) >> <> << s :: binary-size(32) >> = signature
#
# transaction
# |> Map.merge(%{r: encode16(r), s: encode16(s), v: encode16(<<recovery + 27>>)})
# |> adjust_v_for_chain_id
# |> transaction_list
# |> Enum.map(fn(x) -> Base.decode16!(x, case: :lower) end)
# |> ExRLP.encode
# end
#
# def adjust_v_for_chain_id(transaction) do
# if transaction.chain_id > 0 do
# current_v_bytes = Base.decode16!(transaction.v, case: :lower) |> :binary.decode_unsigned
# target_v_bytes = current_v_bytes + (transaction.chain_id * 2 + 8)
# transaction |> Map.merge(%{v: encode16(<< target_v_bytes >>) })
# else
# transaction
# end
# end
#
# def secp256k1_signature(hash, private_key) do
# {:ok, signature, recovery} = :libsecp256k1.ecdsa_sign_compact(hash, private_key, :default, <<>>)
# [signature: signature, recovery: recovery]
# end
#
# # must have [nonce, gasPrice, gasLimit, to, value, data] # and chainId inside the transaction?
# def hash_transaction(transaction) do
# # NOTE: if transaction is decoded no need to encode
# # EIP155 spec:
# # when computing the hash of a transaction for purposes of signing or recovering,
# # instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
# # hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
# transaction |> Map.merge(%{v: encode16(<<transaction.chain_id>>), r: <<>>, s: <<>> })
# |> transaction_list
# |> Enum.map(fn(x) -> Base.decode16!(x, case: :lower) end)
# |> hash
# end
#
# def hash(transaction_list) do
# transaction_list
# |> ExRLP.encode
# |> keccak256
# end
#
# def transaction_list(transaction \\ %{}) do
# %{
# nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data
# } = transaction
#
# v = if Map.get(transaction, :v), do: transaction.v, else: Base.encode16(<<28>>, case: :lower)
# r = Map.get(transaction, :r, "")
# s = Map.get(transaction, :s, "")
# [nonce, gas_price, gas_limit, to, value, data, v, r, s]
# end
end
17 changes: 16 additions & 1 deletion lib/eth/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ defmodule ETH.Query do
|> get_result
end

def get_balance(eth_address, denomination \\ :ether) do
def call(call_params) do
Ethereumex.HttpClient.eth_call([call_params])
|> get_result
end

def get_balance(param, denomination \\ :ether)
def get_balance(wallet, denomination) when is_map(wallet) do
Ethereumex.HttpClient.eth_get_balance([wallet.eth_address])
|> get_number_result
|> convert(denomination)
end
def get_balance(eth_address, denomination) do
Ethereumex.HttpClient.eth_get_balance([eth_address])
|> get_number_result
|> convert(denomination)
Expand Down Expand Up @@ -63,6 +74,10 @@ defmodule ETH.Query do
end)
end

def get_transaction_count(wallet) when is_map(wallet) do
Ethereumex.HttpClient.eth_get_transaction_count([wallet.eth_address])
|> get_number_result
end
def get_transaction_count(eth_address) do
Ethereumex.HttpClient.eth_get_transaction_count([eth_address])
|> get_number_result
Expand Down
104 changes: 64 additions & 40 deletions lib/eth/transaction.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
require IEx
defmodule ETH.Transaction do
import ETH.Utils

alias ETH.Query

defdelegate set(params), to: ETH.Transaction.Setter
defdelegate set(wallet, params), to: ETH.Transaction.Setter
defdelegate set(sender_wallet, receiver_wallet, params), to: ETH.Transaction.Setter
defdelegate parse(data), to: ETH.Transaction.Parser # NOTE: improve this one
defdelegate to_list(data), to: ETH.Transaction.Parser
defdelegate hash_transaction(transaction), to: ETH.Transaction.Signer
defdelegate hash_transaction(transaction, include_signature), to: ETH.Transaction.Signer
defdelegate hash_transaction_list(transaction_list), to: ETH.Transaction.Signer
defdelegate hash_transaction_list(transaction_list, include_signature), to: ETH.Transaction.Signer
# TODO: allow ETH.Transaction.Wallet for signing
defdelegate sign_transaction(transaction, private_key), to: ETH.Transaction.Signer
defdelegate sign_transaction_list(transaction_list, private_key), to: ETH.Transaction.Signer
defdelegate decode(rlp_encoded_transaction), to: ETH.Transaction.Signer
Expand All @@ -24,49 +25,46 @@ defmodule ETH.Transaction do
ETH.Transaction.Signer.hash_transaction(transaction, include_signature)
end

# TODO: what if its a wallet
def set(params) when is_list(params) do
gas_price = Keyword.get(params, :gas_price, ETH.Query.gas_price())
data = Keyword.get(params, :data, "")
nonce = Keyword.get(params, :nonce, ETH.Query.get_transaction_count(params[:from]))
chain_id = Keyword.get(params, :chain_id, 3)
gas_limit = Keyword.get(params, :gas_limit, ETH.Query.estimate_gas(%{
to: params[:to], value: params[:value], data: data, nonce: nonce, chain_id: chain_id
}))
# gas_limit = 100000000

%{
nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: params[:to],
value: params[:value], data: params[:data]
}
|> parse
end
def set(params) do
gas_price = Map.get(params, :gas_price, ETH.Query.gas_price())
data = Map.get(params, :data, "")
nonce = Map.get(params, :nonce, ETH.Query.get_transaction_count(params.from))
chain_id = Map.get(params, :chain_id, 3)
gas_limit = Map.get(params, :gas_limit, ETH.Query.estimate_gas(%{
to: params.to, value: params.value, data: data, nonce: nonce, chain_id: chain_id
}))
# gas_limit = 100000000

%{nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: params.to, value: params.value, data: data}
|> parse
def send_transaction(wallet, params) when is_map(params) do
params
|> Map.merge(%{from: wallet.eth_address})
|> to_transaction(wallet.private_key)
end

def send_transaction(params, private_key) when is_list(params) do
params
|> set
|> sign_transaction(private_key)
# |> send
|> to_transaction(private_key)
end
def send_transaction(params, private_key) when is_map(params) do
params
|> to_transaction(private_key)
end
def send_transaction(sender_wallet, receiver_wallet, value) when is_number(value) do
%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address, value: value}
|> to_transaction(sender_wallet.private_key)
end
def send_transaction(sender_wallet, receiver_wallet, params) when is_map(params) do
params
|> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address})
|> to_transaction(sender_wallet.private_key)
end
def send_transaction(sender_wallet, receiver_wallet, params) when is_list(params) do
params
|> Keyword.merge([from: sender_wallet.eth_address, to: receiver_wallet.eth_address])
|> to_transaction(sender_wallet.private_key)
end
def send_transaction(sender_wallet, receiver_wallet, value, private_key) when is_number(value) do
%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address, value: value}
|> to_transaction(private_key)
end
def send_transaction(sender_wallet, receiver_wallet, params, private_key) when is_map(params) do
params
|> Map.merge(%{from: sender_wallet.eth_address, to: receiver_wallet.eth_address})
|> to_transaction(private_key)
end
# TODO: what if its a wallet
def send_transaction(params, private_key) do
def send_transaction(sender_wallet, receiver_wallet, params, private_key) when is_list(params) do
params
|> set
|> sign_transaction(private_key)
# |> send
|> Keyword.merge([from: sender_wallet.eth_address, to: receiver_wallet.eth_address])
|> to_transaction(private_key)
end

def send(signature), do: Ethereumex.HttpClient.eth_send_raw_transaction([signature])
Expand All @@ -76,6 +74,12 @@ defmodule ETH.Transaction do
|> ExRLP.decode
|> to_senders_public_key
end
def get_senders_public_key(<<encoded_tx>>) do
encoded_tx
|> Base.decode(case: :mixed)
|> ExRLP.decode
|> to_senders_public_key
end
def get_senders_public_key(transaction_list = [
nonce, gas_price, gas_limit, to, value, data, v, r, s
]), do: to_senders_public_key(transaction_list)
Expand All @@ -86,6 +90,13 @@ defmodule ETH.Transaction do
|> get_senders_public_key
|> get_address
end
def get_sender_address(<<encoded_tx>>) do
encoded_tx
|> Base.decode(case: :mixed)
|> ExRLP.decode
|> to_senders_public_key
|> get_address
end
def get_sender_address(transaction_list = [
nonce, gas_price, gas_limit, to, value, data, v, r, s
]), do: get_senders_public_key(transaction_list) |> get_address
Expand All @@ -104,4 +115,17 @@ defmodule ETH.Transaction do
{:ok, public_key} = :libsecp256k1.ecdsa_recover_compact(message_hash, signature, :uncompressed, recovery_id)
public_key
end

defp to_transaction(params, private_key) do
result = params
|> set
|> sign_transaction(private_key)
|> Base.encode16
|> send

case result do
{:ok, transaction_details} -> transaction_details["result"]
_ -> result
end
end
end
13 changes: 7 additions & 6 deletions lib/eth/transaction/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,19 @@ defmodule ETH.Transaction.Parser do
|> Enum.map(fn(value) -> to_buffer(value) end)
end
def to_list(transaction = %{
nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data
nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, value: value, data: data
}) do
to = Map.get(transaction, :to, "")
v = Map.get(transaction, :v, <<28>>)
r = Map.get(transaction, :r, "")
s = Map.get(transaction, :s, "")

[nonce, gas_price, gas_limit, to, value, data, v, r, s]
|> Enum.map(fn(value) -> to_buffer(value) end)
end
def to_list(transaction = %{}) do # TODO: adjust this probably
v = Map.get(transaction, :v, <<28>>)
["", "", "", "", "", "", v, "", ""]
|> Enum.map(fn(value) -> to_buffer(value) end)
end
# def to_list(transaction = %{}) do # TODO: adjust this probably
# v = Map.get(transaction, :v, <<28>>)
# ["", "", "", "", "", "", v, "", ""]
# |> Enum.map(fn(value) -> to_buffer(value) end)
# end
end
Loading

0 comments on commit bc83398

Please sign in to comment.