Land #335: Geth non-incompatibility
commit
fadba343e0
@ -1,4 +0,0 @@ |
|||||||
# Used by "mix format" |
|
||||||
[ |
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] |
|
||||||
] |
|
@ -0,0 +1,5 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :ethereum_jsonrpc, |
||||||
|
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY", |
||||||
|
variant: EthereumJSONRPC.Geth |
@ -0,0 +1,9 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :ethereum_jsonrpc, |
||||||
|
url: "https://sokol.poa.network", |
||||||
|
method_to_url: [ |
||||||
|
eth_getBalance: "https://sokol-trace.poa.network", |
||||||
|
trace_replayTransaction: "https://sokol-trace.poa.network" |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Parity |
@ -0,0 +1,59 @@ |
|||||||
|
defmodule EthereumJSONRPC.DecodeError do |
||||||
|
@moduledoc """ |
||||||
|
An error has occurred decoding the response to an `EthereumJSONRPC.json_rpc` request. |
||||||
|
""" |
||||||
|
|
||||||
|
@enforce_keys [:request, :response] |
||||||
|
defexception [:request, :response] |
||||||
|
|
||||||
|
defmodule Request do |
||||||
|
@moduledoc """ |
||||||
|
Ethereum JSONRPC request whose `EthererumJSONRPC.DecodeError.Response` had a decode error. |
||||||
|
""" |
||||||
|
|
||||||
|
@enforce_keys [:url, :body] |
||||||
|
defstruct [:url, :body] |
||||||
|
end |
||||||
|
|
||||||
|
defmodule Response do |
||||||
|
@moduledoc """ |
||||||
|
Ethereum JSONRPC response that had a decode error. |
||||||
|
""" |
||||||
|
|
||||||
|
@enforce_keys [:status_code, :body] |
||||||
|
defstruct [:status_code, :body] |
||||||
|
end |
||||||
|
|
||||||
|
@impl Exception |
||||||
|
def exception(named_arguments) do |
||||||
|
request_fields = Keyword.fetch!(named_arguments, :request) |
||||||
|
request = struct!(EthereumJSONRPC.DecodeError.Request, request_fields) |
||||||
|
|
||||||
|
response_fields = Keyword.fetch!(named_arguments, :response) |
||||||
|
response = struct!(EthereumJSONRPC.DecodeError.Response, response_fields) |
||||||
|
|
||||||
|
%EthereumJSONRPC.DecodeError{request: request, response: response} |
||||||
|
end |
||||||
|
|
||||||
|
@impl Exception |
||||||
|
def message(%EthereumJSONRPC.DecodeError{ |
||||||
|
request: %EthereumJSONRPC.DecodeError.Request{url: request_url, body: request_body}, |
||||||
|
response: %EthereumJSONRPC.DecodeError.Response{status_code: response_status_code, body: response_body} |
||||||
|
}) do |
||||||
|
""" |
||||||
|
Failed to decode Ethereum JSONRPC response: |
||||||
|
|
||||||
|
request: |
||||||
|
|
||||||
|
url: #{request_url} |
||||||
|
|
||||||
|
body: #{IO.iodata_to_binary(request_body)} |
||||||
|
|
||||||
|
response: |
||||||
|
|
||||||
|
status code: #{response_status_code} |
||||||
|
|
||||||
|
body: #{response_body} |
||||||
|
""" |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,34 @@ |
|||||||
|
defmodule EthereumJSONRPC.Geth do |
||||||
|
@moduledoc """ |
||||||
|
Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth). |
||||||
|
""" |
||||||
|
|
||||||
|
@behaviour EthereumJSONRPC.Variant |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Internal transaction fetching is not supported currently for Geth. |
||||||
|
|
||||||
|
To signal to the caller that fetching is not supported, `:ignore` is returned |
||||||
|
|
||||||
|
iex> EthereumJSONRPC.Geth.fetch_internal_transactions([ |
||||||
|
...> "0x2ec382949ba0b22443aa4cb38267b1fb5e68e188109ac11f7a82f67571a0adf3" |
||||||
|
...> ]) |
||||||
|
:ignore |
||||||
|
|
||||||
|
""" |
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_internal_transactions(transaction_params) when is_list(transaction_params), |
||||||
|
do: :ignore |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Pending transaction fetching is not supported currently for Geth. |
||||||
|
|
||||||
|
To signal to the caller that fetching is not supported, `:ignore` is returned |
||||||
|
|
||||||
|
iex> EthereumJSONRPC.Geth.fetch_pending_transactions() |
||||||
|
:ignore |
||||||
|
|
||||||
|
""" |
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_pending_transactions, do: :ignore |
||||||
|
end |
@ -0,0 +1,35 @@ |
|||||||
|
defmodule EthereumJSONRPC.Variant do |
||||||
|
@moduledoc """ |
||||||
|
A variant of the Ethereum JSONRPC API. Each Ethereum client supports slightly different versions of the non-standard |
||||||
|
Ethereum JSONRPC API. The variant callbacks abstract over this difference. |
||||||
|
""" |
||||||
|
|
||||||
|
alias EthereumJSONRPC.Transaction |
||||||
|
|
||||||
|
@type internal_transaction_params :: map() |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the variant of the Ethereum JSONRPC API. |
||||||
|
|
||||||
|
## Returns |
||||||
|
|
||||||
|
* `{:ok, [internal_transaction_params]}` - internal transactions were successfully fetched for all transactions |
||||||
|
* `{:error, reason}` - there was one or more errors with `reason` in fetching at least one of the transaction's |
||||||
|
internal transactions |
||||||
|
* `:ignore` - the variant does not support fetching internal transactions. |
||||||
|
""" |
||||||
|
@callback fetch_internal_transactions([Transaction.params()]) :: |
||||||
|
{:ok, [internal_transaction_params]} | {:error, reason :: term} | :ignore |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetch the `t:Explorer.Chain.Transaction.changeset/2` params for pending transactions from the variant of the Ethereum |
||||||
|
JSONRPC API. |
||||||
|
|
||||||
|
## Returns |
||||||
|
|
||||||
|
* `{:ok, [transaction_params]}` - pending transactions were succucessfully fetched |
||||||
|
* `{:error, reason}` - there was one or more errors with `reason` in fetching the pending transactions |
||||||
|
* `:ignore` - the variant does not support fetching pending transactions. |
||||||
|
""" |
||||||
|
@callback fetch_pending_transactions() :: {:ok, [Transaction.params()]} | {:error, reason :: term} | :ignore |
||||||
|
end |
@ -1,130 +1,134 @@ |
|||||||
defmodule EthereumJSONRPCTest do |
defmodule EthereumJSONRPCTest do |
||||||
use ExUnit.Case, async: true |
use ExUnit.Case, async: true |
||||||
|
|
||||||
doctest EthereumJSONRPC |
import EthereumJSONRPC.Case |
||||||
|
|
||||||
describe "fetch_balances/1" do |
@moduletag :capture_log |
||||||
test "with all valid hash_data returns {:ok, addresses_params}" do |
|
||||||
assert EthereumJSONRPC.fetch_balances([ |
|
||||||
%{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"} |
|
||||||
]) == |
|
||||||
{:ok, |
|
||||||
[ |
|
||||||
%{ |
|
||||||
fetched_balance: 1, |
|
||||||
fetched_balance_block_number: 1, |
|
||||||
hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
|
||||||
} |
|
||||||
]} |
|
||||||
end |
|
||||||
|
|
||||||
test "with all invalid hash_data returns {:error, reasons}" do |
|
||||||
assert EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}]) == |
|
||||||
{:error, |
|
||||||
[ |
|
||||||
%{ |
|
||||||
"blockNumber" => "0x1", |
|
||||||
"code" => -32602, |
|
||||||
"hash" => "0x0", |
|
||||||
"message" => |
|
||||||
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." |
|
||||||
} |
|
||||||
]} |
|
||||||
end |
|
||||||
|
|
||||||
test "with a mix of valid and invalid hash_data returns {:error, reasons}" do |
setup do |
||||||
assert EthereumJSONRPC.fetch_balances([ |
%{variant: EthereumJSONRPC.config(:variant)} |
||||||
# start with :ok |
|
||||||
%{ |
|
||||||
block_quantity: "0x1", |
|
||||||
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
|
||||||
}, |
|
||||||
# :ok, :ok clause |
|
||||||
%{ |
|
||||||
block_quantity: "0x34", |
|
||||||
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" |
|
||||||
}, |
|
||||||
# :ok, :error clause |
|
||||||
%{ |
|
||||||
block_quantity: "0x2", |
|
||||||
hash_data: "0x3" |
|
||||||
}, |
|
||||||
# :error, :ok clause |
|
||||||
%{ |
|
||||||
block_quantity: "0x35", |
|
||||||
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
|
||||||
}, |
|
||||||
# :error, :error clause |
|
||||||
%{ |
|
||||||
block_quantity: "0x4", |
|
||||||
hash_data: "0x5" |
|
||||||
} |
|
||||||
]) == |
|
||||||
{:error, |
|
||||||
[ |
|
||||||
%{ |
|
||||||
"blockNumber" => "0x2", |
|
||||||
"code" => -32602, |
|
||||||
"hash" => "0x3", |
|
||||||
"message" => |
|
||||||
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." |
|
||||||
}, |
|
||||||
%{ |
|
||||||
"blockNumber" => "0x4", |
|
||||||
"code" => -32602, |
|
||||||
"hash" => "0x5", |
|
||||||
"message" => |
|
||||||
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." |
|
||||||
} |
|
||||||
]} |
|
||||||
end |
|
||||||
end |
end |
||||||
|
|
||||||
describe "json_rpc/2" do |
describe "fetch_balances/1" do |
||||||
# regression test for https://github.com/poanetwork/poa-explorer/issues/254 |
test "with all valid hash_data returns {:ok, addresses_params}", %{variant: variant} do |
||||||
test "transparently splits batch payloads that would trigger a 413 Request Entity Too Large" do |
assert {:ok, |
||||||
block_numbers = 0..13000 |
[ |
||||||
|
%{ |
||||||
|
fetched_balance: fetched_balance, |
||||||
|
fetched_balance_block_number: 1, |
||||||
|
hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||||
|
} |
||||||
|
]} = |
||||||
|
EthereumJSONRPC.fetch_balances([ |
||||||
|
%{block_quantity: "0x1", hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"} |
||||||
|
]) |
||||||
|
|
||||||
|
case variant do |
||||||
|
EthereumJSONRPC.Geth -> |
||||||
|
assert fetched_balance == 0 |
||||||
|
|
||||||
|
EthereumJSONRPC.Parity -> |
||||||
|
assert fetched_balance == 1 |
||||||
|
|
||||||
|
_ -> |
||||||
|
raise ArgumentError, "Unsupported variant (#{variant}})" |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
payload = |
test "with all invalid hash_data returns {:error, reasons}", %{variant: variant} do |
||||||
block_numbers |
assert {:error, reasons} = EthereumJSONRPC.fetch_balances([%{block_quantity: "0x1", hash_data: "0x0"}]) |
||||||
|> Stream.with_index() |
assert is_list(reasons) |
||||||
|> Enum.map(&get_block_by_number_request/1) |
assert length(reasons) == 1 |
||||||
|
|
||||||
assert_payload_too_large(payload) |
[reason] = reasons |
||||||
|
|
||||||
url = EthereumJSONRPC.config(:url) |
assert %{ |
||||||
|
"blockNumber" => "0x1", |
||||||
|
"code" => -32602, |
||||||
|
"hash" => "0x0", |
||||||
|
"message" => message |
||||||
|
} = reason |
||||||
|
|
||||||
assert {:ok, responses} = EthereumJSONRPC.json_rpc(payload, url) |
case variant do |
||||||
assert Enum.count(responses) == Enum.count(block_numbers) |
EthereumJSONRPC.Geth -> |
||||||
|
assert message == |
||||||
|
"invalid argument 0: json: cannot unmarshal hex string of odd length into Go value of type common.Address" |
||||||
|
|
||||||
block_number_set = MapSet.new(block_numbers) |
EthereumJSONRPC.Parity -> |
||||||
|
assert message == |
||||||
|
"Invalid params: invalid length 1, expected a 0x-prefixed, padded, hex-encoded hash with length 40." |
||||||
|
|
||||||
response_block_number_set = |
_ -> |
||||||
Enum.into(responses, MapSet.new(), fn %{"result" => %{"number" => quantity}} -> |
raise ArgumentError, "Unsupported variant (#{variant}})" |
||||||
EthereumJSONRPC.quantity_to_integer(quantity) |
end |
||||||
end) |
end |
||||||
|
|
||||||
assert MapSet.equal?(response_block_number_set, block_number_set) |
test "with a mix of valid and invalid hash_data returns {:error, reasons}" do |
||||||
|
assert {:error, reasons} = |
||||||
|
EthereumJSONRPC.fetch_balances([ |
||||||
|
# start with :ok |
||||||
|
%{ |
||||||
|
block_quantity: "0x1", |
||||||
|
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||||
|
}, |
||||||
|
# :ok, :ok clause |
||||||
|
%{ |
||||||
|
block_quantity: "0x34", |
||||||
|
hash_data: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" |
||||||
|
}, |
||||||
|
# :ok, :error clause |
||||||
|
%{ |
||||||
|
block_quantity: "0x2", |
||||||
|
hash_data: "0x3" |
||||||
|
}, |
||||||
|
# :error, :ok clause |
||||||
|
%{ |
||||||
|
block_quantity: "0x35", |
||||||
|
hash_data: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||||
|
}, |
||||||
|
# :error, :error clause |
||||||
|
%{ |
||||||
|
block_quantity: "0x4", |
||||||
|
hash_data: "0x5" |
||||||
|
} |
||||||
|
]) |
||||||
|
|
||||||
|
assert is_list(reasons) |
||||||
|
assert length(reasons) > 1 |
||||||
end |
end |
||||||
end |
end |
||||||
|
|
||||||
defp assert_payload_too_large(payload) do |
describe "fetch_block_number_by_tag/1" do |
||||||
json = Jason.encode_to_iodata!(payload) |
@tag capture_log: false |
||||||
headers = [{"Content-Type", "application/json"}] |
test "with earliest" do |
||||||
url = EthereumJSONRPC.config(:url) |
log_bad_gateway( |
||||||
|
fn -> EthereumJSONRPC.fetch_block_number_by_tag("earliest") end, |
||||||
assert {:ok, %HTTPoison.Response{body: body, status_code: 413}} = |
fn result -> |
||||||
HTTPoison.post(url, json, headers, EthereumJSONRPC.config(:http)) |
assert {:ok, 0} = result |
||||||
|
end |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
assert body =~ "413 Request Entity Too Large" |
@tag capture_log: false |
||||||
end |
test "with latest" do |
||||||
|
log_bad_gateway( |
||||||
|
fn -> EthereumJSONRPC.fetch_block_number_by_tag("latest") end, |
||||||
|
fn result -> |
||||||
|
assert {:ok, number} = result |
||||||
|
assert number > 0 |
||||||
|
end |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
defp get_block_by_number_request({block_number, id}) do |
@tag capture_log: false |
||||||
%{ |
test "with pending" do |
||||||
"id" => id, |
log_bad_gateway( |
||||||
"jsonrpc" => "2.0", |
fn -> EthereumJSONRPC.fetch_block_number_by_tag("pending") end, |
||||||
"method" => "eth_getBlockByNumber", |
fn result -> |
||||||
"params" => [EthereumJSONRPC.integer_to_quantity(block_number), true] |
assert {:ok, number} = result |
||||||
} |
assert number > 0 |
||||||
|
end |
||||||
|
) |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
defmodule EthereumJSONRPC.GethTest do |
||||||
|
use ExUnit.Case, async: false |
||||||
|
|
||||||
|
doctest EthereumJSONRPC.Geth |
||||||
|
end |
@ -0,0 +1,10 @@ |
|||||||
|
defmodule EthereumJSONRPC.Case do |
||||||
|
require Logger |
||||||
|
|
||||||
|
def log_bad_gateway(under_test, assertions) do |
||||||
|
case under_test.() do |
||||||
|
{:error, {:bad_gateway, url}} -> Logger.error(fn -> ["Bad Gateway to ", url, ". Check CloudFlare."] end) |
||||||
|
other -> assertions.(other) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,146 @@ |
|||||||
|
defmodule Explorer.Chain.Hash.Nonce do |
||||||
|
@moduledoc """ |
||||||
|
The nonce (16 (hex) characters / 128 bits / 8 bytes) is derived from the Proof-of-Work. |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.Chain.Hash |
||||||
|
|
||||||
|
@behaviour Ecto.Type |
||||||
|
@behaviour Hash |
||||||
|
|
||||||
|
@byte_count 8 |
||||||
|
@hexadecimal_digit_count Hash.hexadecimal_digits_per_byte() * @byte_count |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
A #{@byte_count}-byte hash of the address public key. |
||||||
|
""" |
||||||
|
@type t :: %Hash{byte_count: unquote(@byte_count), bytes: <<_::unquote(@byte_count * Hash.bits_per_byte())>>} |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Casts `term` to `t:t/0`. |
||||||
|
|
||||||
|
If the `term` is already in `t:t/0`, then it is returned |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.cast( |
||||||
|
...> %Explorer.Chain.Hash{ |
||||||
|
...> byte_count: 8, |
||||||
|
...> bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
...> } |
||||||
|
...> ) |
||||||
|
{ |
||||||
|
:ok, |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 8, |
||||||
|
bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
If the `term` is an `non_neg_integer`, then it is converted to `t:t/0` |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.cast(0x7bb9369dcbaec019) |
||||||
|
{ |
||||||
|
:ok, |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 8, |
||||||
|
bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
If the `non_neg_integer` is too large, then `:error` is returned. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.cast(0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b) |
||||||
|
:error |
||||||
|
|
||||||
|
If the `term` is a `String.t` that starts with `0x`, then is converted to an integer and then to `t:t/0`. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.cast("0x7bb9369dcbaec019") |
||||||
|
{ |
||||||
|
:ok, |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 8, |
||||||
|
bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
While `non_neg_integers` don't have to be the correct width (because zero padding it difficult with numbers), |
||||||
|
`String.t` format must always have #{@hexadecimal_digit_count} digits after the `0x` base prefix. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Address.cast("0x0") |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec cast(term()) :: {:ok, t()} | :error |
||||||
|
def cast(term) do |
||||||
|
Hash.cast(__MODULE__, term) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Dumps the binary hash to `:binary` (`bytea`) format used in database. |
||||||
|
|
||||||
|
If the field from the struct is `t:t/0`, then it succeeds |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.dump( |
||||||
|
...> %Explorer.Chain.Hash{ |
||||||
|
...> byte_count: 8, |
||||||
|
...> bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
...> } |
||||||
|
...> ) |
||||||
|
{:ok, <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>>} |
||||||
|
|
||||||
|
If the field from the struct is an incorrect format such as `t:Explorer.Chain.Hash.t/0`, `:error` is returned |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.dump( |
||||||
|
...> %Explorer.Chain.Hash{ |
||||||
|
...> byte_count: 32, |
||||||
|
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: |
||||||
|
...> big-integer-size(32)-unit(8)>> |
||||||
|
...> } |
||||||
|
...> ) |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec dump(term()) :: {:ok, binary} | :error |
||||||
|
def dump(term) do |
||||||
|
Hash.dump(__MODULE__, term) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Loads the binary hash from the database. |
||||||
|
|
||||||
|
If the binary hash is the correct format, it is returned. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.load(<<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>>) |
||||||
|
{ |
||||||
|
:ok, |
||||||
|
%Explorer.Chain.Hash{ |
||||||
|
byte_count: 8, |
||||||
|
bytes: <<0x7bb9369dcbaec019 :: big-integer-size(8)-unit(8)>> |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
If the binary hash is an incorrect format, such as if an `Explorer.Chain.Hash` field is loaded, `:error` is returned. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Hash.Nonce.load( |
||||||
|
...> <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> |
||||||
|
...> ) |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec load(term()) :: {:ok, t} | :error |
||||||
|
def load(term) do |
||||||
|
Hash.load(__MODULE__, term) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The underlying database type: `binary`. `binary` is used because no Postgres integer type is 20 bytes long. |
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec type() :: :binary |
||||||
|
def type, do: :binary |
||||||
|
|
||||||
|
@impl Hash |
||||||
|
def byte_count, do: @byte_count |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
defmodule Explorer.Chain.Hash.AddressTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
doctest Explorer.Chain.Hash.Address |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
defmodule Explorer.Chain.Hash.NonceTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
doctest Explorer.Chain.Hash.Nonce |
||||||
|
end |
@ -1,5 +0,0 @@ |
|||||||
defmodule Explorer.Chain.Hash.TruncatedTest do |
|
||||||
use ExUnit.Case, async: true |
|
||||||
|
|
||||||
doctest Explorer.Chain.Hash.Truncated |
|
||||||
end |
|
@ -1,4 +0,0 @@ |
|||||||
# Used by "mix format" |
|
||||||
[ |
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] |
|
||||||
] |
|
Loading…
Reference in new issue