Skip to content

Commit

Permalink
big progress with transaction module + testing
Browse files Browse the repository at this point in the history
  • Loading branch information
izelnakri committed Aug 31, 2017
1 parent 1882521 commit e492f00
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 163 deletions.
180 changes: 81 additions & 99 deletions lib/eth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,103 +15,85 @@ defmodule ETH do
"""

def sign_transaction(
source_wallet, value, target_wallet, 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

Ethereumex.HttpClient.eth_get_transaction_count([source_wallet[:eth_address]]) |> elem(1) |> Map.get("result")

# NOTE: calc nonce
%{
to: target_wallet[: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 = default_is_empty(transaction, :r)
s = default_is_empty(transaction, :s)
[nonce, gas_price, gas_limit, to, value, data, v, r, s]
end

def convert(number, magnitute \\ :ether) do
denomination = [
wei: 1,
kwei: 1000,
mwei: 1000000,
gwei: 1000000000,
shannon: 1000000000,
nano: 1000000000,
szabo: 1000000000000,
micro: 1000000000000,
finney: 1000000000000000,
milli: 1000000000000000,
ether: 1000000000000000000,
] |> List.keyfind(magnitute, 0) |> elem(1)

number / denomination
end

def keccak256(data), do: :keccakf1600.hash(:sha3_256, data)
defp encode16(value), do: Base.encode16(value, case: :lower)
defp default_is_empty(map, key), do: if Map.get(map, key), do: Map.get(map, key), else: ""
# 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
93 changes: 86 additions & 7 deletions lib/eth/query.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,97 @@
defmodule ETH.Query do
import ETH.Utils

def block_number do
Ethereumex.HttpClient.eth_block_number
|> get_number_result
end

def syncing do
Ethereumex.HttpClient.eth_syncing
|> get_result
end

def get_accounts do
Ethereumex.HttpClient.eth_accounts
|> elem(1)
|> Map.get("result")
|> get_result
end

def get_balance(eth_address, denomination \\ :ether) do
result = Ethereumex.HttpClient.eth_get_balance([eth_address])
|> elem(1)
|> Map.get("result")
Ethereumex.HttpClient.eth_get_balance([eth_address])
|> get_number_result
|> convert(denomination)
end

def get_transaction(transaction_hash) do
Ethereumex.HttpClient.eth_get_transaction_by_hash([transaction_hash])
|> get_result
|> Enum.reduce(%{}, fn(tuple, acc) ->
{key, value} = tuple

case key do
"blockHash" -> Map.put(acc, :block_hash, value)
"blockNumber" -> Map.put(acc, :block_number, convert_to_number(value))
"gas" -> Map.put(acc, :gas, convert_to_number(value))
"gasPrice" -> Map.put(acc, :gas_price, convert_to_number(value))
"input" -> # NOTE: this could be wrong
input = value |> String.slice(2..-1) |> ExRLP.decode
Map.put(acc, :input, input)
"nonce" -> Map.put(acc, :nonce, convert_to_number(value))
"transactionIndex" -> Map.put(acc, :transaction_index, convert_to_number(value))
"value" -> Map.put(acc, :value, convert_to_number(value))
_ -> Map.put(acc, String.to_atom(key), value)
end
end)
end

def get_transaction_receipt(transaction_hash) do
Ethereumex.HttpClient.eth_get_transaction_receipt([transaction_hash])
|> get_result
|> Enum.reduce(%{}, fn(tuple, acc) ->
{key, value} = tuple

case key do
"transactionHash" -> Map.put(acc, :transaction_hash, value)
"transactionIndex" -> Map.put(acc, :transaction_index, convert_to_number(value))
"blockHash" -> Map.put(acc, :block_hash, value)
"blockNumber" -> Map.put(acc, :block_number, convert_to_number(value))
"gasUsed" -> Map.put(acc, :gas_used, convert_to_number(value))
"cumulativeGasUsed" -> Map.put(acc, :cumulative_gas_used, convert_to_number(value))
"contractAddress" -> Map.put(acc, :contract_address, value)
"logs" -> Map.put(acc, :logs, value)
end
end)
end

def get_transaction_count(eth_address) do
Ethereumex.HttpClient.eth_get_transaction_count([eth_address])
|> get_number_result
end

def gas_price do
Ethereumex.HttpClient.eth_gas_price()
|> get_number_result
end

def estimate_gas(transaction \\ %{data: ""})
def estimate_gas(transaction = %{to: to, data: data}) do
Ethereumex.HttpClient.eth_estimate_gas([transaction])
|> get_number_result
end

defp get_result(eth_result), do: eth_result |> elem(1) |> Map.get("result")

defp get_number_result(eth_result) do
get_result(eth_result) |> convert_to_number
end

defp convert_to_number(result) do
result
|> String.slice(2..String.length(result))
|> String.slice(2..-1)
|> Hexate.to_integer
|> ETH.convert(denomination)
end
end

# TODO:
# get_block(hash or block_number), get_block_transaction_count(hash or block_number),
# get_transaction_from_block, call, perhaps get_block_transactions
Loading

0 comments on commit e492f00

Please # to comment.