Store variable-length DATA as Explorer.Chain.Data

Explorer.Chain.Data is like `Explorer.Chain.Hash`, but of variable
width.
pull/180/head
Luke Imhoff 7 years ago
parent ce6b98f868
commit a96f05d065
  1. 2
      apps/ethereum_jsonrpc/mix.exs
  2. 28
      apps/explorer/lib/explorer/chain.ex
  3. 383
      apps/explorer/lib/explorer/chain/data.ex
  4. 2
      apps/explorer/lib/explorer/chain/hash.ex
  5. 18
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  6. 6
      apps/explorer/lib/explorer/chain/log.ex
  7. 10
      apps/explorer/lib/explorer/chain/transaction.ex
  8. 4
      apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs
  9. 2
      apps/explorer/priv/repo/migrations/20180212222309_create_logs.exs
  10. 8
      apps/explorer/priv/repo/migrations/20180221001948_create_internal_transactions.exs
  11. 5
      apps/explorer/test/explorer/chain/data_test.exs
  12. 4
      apps/explorer/test/explorer/chain/internal_transaction_test.exs
  13. 11
      apps/explorer/test/explorer/chain/transaction_test.exs
  14. 5
      apps/explorer/test/string/chars/explorer/chain/data_test.exs
  15. 43
      apps/explorer/test/support/factory.ex
  16. 8
      apps/explorer_web/lib/phoenix/html/safe.ex
  17. 8
      apps/explorer_web/test/explorer_web/features/contributor_browsing_test.exs
  18. 2
      coveralls.json

@ -52,6 +52,8 @@ defmodule EthereumJsonrpc.MixProject do
{:credo, "0.9.2", only: [:dev, :test], runtime: false},
# Static Type Checking
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
# Casting Ethereum-native types to Elixir-native types
{:ecto, "~> 2.2"},
# Code coverage
{:excoveralls, "~> 0.8.1", only: [:test]},
# JSONRPC HTTP Post calls

@ -6,7 +6,7 @@ defmodule Explorer.Chain do
import Ecto.Query, only: [from: 2, join: 4, or_where: 3, order_by: 2, order_by: 3, preload: 2, where: 2, where: 3]
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, Log, Receipt, Transaction, Wei}
alias Explorer.Chain.{Address, Block, Data, Hash, InternalTransaction, Log, Receipt, Transaction, Wei}
alias Explorer.Repo
@typedoc """
@ -283,6 +283,30 @@ defmodule Explorer.Chain do
|> Repo.insert()
end
@doc """
Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users effciently.
iex> %Explorer.Chain.Data{
...> bytes: <<>>
...> } |>
...> Explorer.Chain.data_to_iodata() |>
...> IO.iodata_to_binary()
"0x"
iex> %Explorer.Chain.Data{
...> bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
...> 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
...> 179, 239>>
...> } |>
...> Explorer.Chain.data_to_iodata() |>
...> IO.iodata_to_binary()
"0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef"
"""
@spec data_to_iodata(Data.t()) :: iodata()
def data_to_iodata(data) do
Data.to_iodata(data)
end
@doc """
The fee a `transaction` paid for the `t:Explorer.Transaction.t/0` `gas`
@ -376,7 +400,7 @@ defmodule Explorer.Chain do
end
@doc """
Converts the `t:t/0` to string representation shown to users.
Converts the `Explorer.Chain.Hash.t:t/0` to `iodata` representation that can be written efficiently to users.
iex> %Explorer.Chain.Hash{
...> byte_count: 32,

@ -0,0 +1,383 @@
defmodule Explorer.Chain.Data do
@moduledoc """
The Elixir-native representation of `t:EthereumJSONRPC.data/0`.
`t:EthereumJSONRPC.data/0` is an unpadded hexadecimal number with 0 or more digits. Each pair of digits maps directly
to a byte in the underlying binary representation. When interpreted as a number, it should be treated as big-endian.
"""
alias Explorer.Chain.Data
@behaviour Ecto.Type
@typedoc """
A variable-byte-length binary, wrapped in a struct, so that it can use protocols.
"""
@type t :: %Data{bytes: <<_::_*8>>}
defstruct ~w(bytes)a
@doc """
Converts the `t:t/0` to `iodata` representation written efficiently to users.
iex> %Explorer.Chain.Data{
...> bytes: <<>>
...> } |>
...> Explorer.Chain.Data.to_iodata() |>
...> IO.iodata_to_binary()
"0x"
iex> %Explorer.Chain.Data{
...> bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
...> 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
...> 179, 239>>
...> } |>
...> Explorer.Chain.Data.to_iodata() |>
...> IO.iodata_to_binary()
"0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef"
"""
@spec to_iodata(t) :: iodata()
def to_iodata(%Data{bytes: bytes}) do
["0x", Base.encode16(bytes, case: :lower)]
end
@doc """
Converts the `t:t/0` to string representation shown to users.
iex> Explorer.Chain.Data.to_string(
...> %Explorer.Chain.Data{
...> bytes: <<>>
...> }
...> )
"0x"
iex> Explorer.Chain.Data.to_string(
...> %Explorer.Chain.Data{
...> bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
...> 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
...> 179, 239>>
...> }
...> )
"0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef"
"""
@spec to_string(t) :: String.t()
def to_string(%Data{} = data) do
data
|> to_iodata()
|> IO.iodata_to_binary()
end
@doc """
Casts `term` to `t:t/0`.
An empty `t:t/0` will pass through unchanged
iex> Explorer.Chain.Data.cast(%Explorer.Chain.Data{bytes: <<>>})
{:ok, %Explorer.Chain.Data{bytes: <<>>}}
An empty `t:t/0` is presented as `0x` in Ethereum JSONRPC.
iex> Explorer.Chain.Data.cast("0x")
{:ok, %Explorer.Chain.Data{bytes: <<>>}}
Hashes can be represented as `Explorer.Chain.Data`, but it is better to use `Explorer.Chain.Hash.Full` or
`Explorer.Chain.Hash.Truncated`.
iex> Explorer.Chain.Data.cast("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")
{
:ok,
%Explorer.Chain.Data{
bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>
}
}
iex> Explorer.Chain.Data.cast("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
{
:ok,
%Explorer.Chain.Data{
bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
}
}
Input to transactions can be represented as `Explorer.Chain.Data`
iex> Explorer.Chain.Data.cast("0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029")
{
:ok,
%Explorer.Chain.Data{
bytes: <<96, 96, 96, 64, 82, 52, 21, 97, 0, 15, 87, 96, 0, 128, 253, 91, 51,
96, 0, 128, 97, 1, 0, 10, 129, 84, 129, 115, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 2,
25, 22, 144, 131, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 2, 23, 144, 85, 80,
97, 2, 219, 128, 97, 0, 94, 96, 0, 57, 96, 0, 243, 0, 96, 96, 96, 64, 82,
96, 4, 54, 16, 97, 0, 98, 87, 96, 0, 53, 124, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 4, 99, 255,
255, 255, 255, 22, 128, 99, 9, 0, 240, 16, 20, 97, 0, 103, 87, 128, 99, 68,
93, 240, 172, 20, 97, 0, 160, 87, 128, 99, 141, 165, 203, 91, 20, 97, 0,
201, 87, 128, 99, 253, 172, 213, 118, 20, 97, 1, 30, 87, 91, 96, 0, 128,
253, 91, 52, 21, 97, 0, 114, 87, 96, 0, 128, 253, 91, 97, 0, 158, 96, 4,
128, 128, 53, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 144, 96, 32, 1, 144, 145,
144, 80, 80, 97, 1, 65, 86, 91, 0, 91, 52, 21, 97, 0, 171, 87, 96, 0, 128,
253, 91, 97, 0, 179, 97, 2, 36, 86, 91, 96, 64, 81, 128, 130, 129, 82, 96,
32, 1, 145, 80, 80, 96, 64, 81, 128, 145, 3, 144, 243, 91, 52, 21, 97, 0,
212, 87, 96, 0, 128, 253, 91, 97, 0, 220, 97, 2, 42, 86, 91, 96, 64, 81,
128, 130, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 22, 115, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
22, 129, 82, 96, 32, 1, 145, 80, 80, 96, 64, 81, 128, 145, 3, 144, 243, 91,
52, 21, 97, 1, 41, 87, 96, 0, 128, 253, 91, 97, 1, 63, 96, 4, 128, 128, 53,
144, 96, 32, 1, 144, 145, 144, 80, 80, 97, 2, 79, 86, 91, 0, 91, 96, 0,
128, 96, 0, 144, 84, 144, 97, 1, 0, 10, 144, 4, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 22, 51, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 20, 21, 97, 2, 32, 87, 129, 144, 80, 128, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 99, 253, 172, 213, 118, 96, 1, 84, 96, 64, 81, 130, 99, 255, 255,
255, 255, 22, 124, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 129, 82, 96, 4, 1, 128, 130, 129, 82, 96,
32, 1, 145, 80, 80, 96, 0, 96, 64, 81, 128, 131, 3, 129, 96, 0, 135, 128,
59, 21, 21, 97, 2, 11, 87, 96, 0, 128, 253, 91, 97, 2, 198, 90, 3, 241, 21,
21, 97, 2, 28, 87, 96, 0, 128, 253, 91, 80, 80, 80, 91, 80, 80, 86, 91, 96,
1, 84, 129, 86, 91, 96, 0, 128, 144, 84, 144, 97, 1, 0, 10, 144, 4, 115,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 22, 129, 86, 91, 96, 0, 128, 144, 84, 144, 97, 1,
0, 10, 144, 4, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 51, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 20, 21, 97, 2, 172, 87,
128, 96, 1, 129, 144, 85, 80, 91, 80, 86, 0, 161, 101, 98, 122, 122, 114,
48, 88, 32, 169, 198, 40, 119, 94, 251, 251, 193, 116, 119, 164, 114, 65,
60, 1, 238, 155, 51, 136, 31, 85, 12, 89, 210, 27, 238, 153, 40, 131, 92,
133, 75, 0, 41>>
}
}
The public key of the signer of a transaction can be represented as `Explorer.Chain.Data`.
iex> Explorer.Chain.Data.cast("0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9")
{
:ok,
%Explorer.Chain.Data{
bytes: <<229, 209, 150, 173, 76, 234, 218, 113, 157, 158, 89, 47, 113, 102,
208, 199, 87, 0, 246, 234, 178, 227, 195, 222, 52, 186, 117, 30, 167, 134,
82, 124, 179, 246, 235, 150, 173, 159, 223, 219, 153, 137, 255, 87, 45,
245, 15, 28, 66, 239, 128, 10, 249, 197, 32, 122, 56, 185, 41, 175, 249,
105, 181, 201>>
}
}
Log data can be represented as `Explorer.Chain.Data`.
iex> Explorer.Chain.Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef")
{:ok,
%Explorer.Chain.Data{
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
179, 239>>
}}
The init argument to create the contarct and the created contract code can be represented as `Explorer.Chain.Data`.
iex> Explorer.Chain.Data.cast("0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029")
{:ok,
%Explorer.Chain.Data{
bytes: <<96, 96, 96, 64, 82, 52, 21, 97, 0, 15, 87, 96, 0, 128, 253, 91, 51,
96, 0, 128, 97, 1, 0, 10, 129, 84, 129, 115, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 2,
25, 22, 144, 131, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 2, 23, 144, 85, 80,
97, 2, 219, 128, 97, 0, 94, 96, 0, 57, 96, 0, 243, 0, 96, 96, 96, 64, 82,
96, 4, 54, 16, 97, 0, 98, 87, 96, 0, 53, 124, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 4, 99, 255,
255, 255, 255, 22, 128, 99, 9, 0, 240, 16, 20, 97, 0, 103, 87, 128, 99, 68,
93, 240, 172, 20, 97, 0, 160, 87, 128, 99, 141, 165, 203, 91, 20, 97, 0,
201, 87, 128, 99, 253, 172, 213, 118, 20, 97, 1, 30, 87, 91, 96, 0, 128,
253, 91, 52, 21, 97, 0, 114, 87, 96, 0, 128, 253, 91, 97, 0, 158, 96, 4,
128, 128, 53, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 144, 96, 32, 1, 144, 145,
144, 80, 80, 97, 1, 65, 86, 91, 0, 91, 52, 21, 97, 0, 171, 87, 96, 0, 128,
253, 91, 97, 0, 179, 97, 2, 36, 86, 91, 96, 64, 81, 128, 130, 129, 82, 96,
32, 1, 145, 80, 80, 96, 64, 81, 128, 145, 3, 144, 243, 91, 52, 21, 97, 0,
212, 87, 96, 0, 128, 253, 91, 97, 0, 220, 97, 2, 42, 86, 91, 96, 64, 81,
128, 130, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 22, 115, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
22, 129, 82, 96, 32, 1, 145, 80, 80, 96, 64, 81, 128, 145, 3, 144, 243, 91,
52, 21, 97, 1, 41, 87, 96, 0, 128, 253, 91, 97, 1, 63, 96, 4, 128, 128, 53,
144, 96, 32, 1, 144, 145, 144, 80, 80, 97, 2, 79, 86, 91, 0, 91, 96, 0,
128, 96, 0, 144, 84, 144, 97, 1, 0, 10, 144, 4, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 22, 51, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 20, 21, 97, 2, 32, 87, 129, 144, 80, 128, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 99, 253, 172, 213, 118, 96, 1, 84, 96, 64, 81, 130, 99, 255, 255,
255, 255, 22, 124, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 129, 82, 96, 4, 1, 128, 130, 129, 82, 96,
32, 1, 145, 80, 80, 96, 0, 96, 64, 81, 128, 131, 3, 129, 96, 0, 135, 128,
59, 21, 21, 97, 2, 11, 87, 96, 0, 128, 253, 91, 97, 2, 198, 90, 3, 241, 21,
21, 97, 2, 28, 87, 96, 0, 128, 253, 91, 80, 80, 80, 91, 80, 80, 86, 91, 96,
1, 84, 129, 86, 91, 96, 0, 128, 144, 84, 144, 97, 1, 0, 10, 144, 4, 115,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 22, 129, 86, 91, 96, 0, 128, 144, 84, 144, 97, 1,
0, 10, 144, 4, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 115, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 22, 51, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 20, 21, 97, 2, 172, 87,
128, 96, 1, 129, 144, 85, 80, 91, 80, 86, 0, 161, 101, 98, 122, 122, 114,
48, 88, 32, 169, 198, 40, 119, 94, 251, 251, 193, 116, 119, 164, 114, 65,
60, 1, 238, 155, 51, 136, 31, 85, 12, 89, 210, 27, 238, 153, 40, 131, 92,
133, 75, 0, 41>>
}}
iex> Explorer.Chain.Data.cast("0x606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029")
{:ok,
%Explorer.Chain.Data{
bytes: <<96, 96, 96, 64, 82, 96, 4, 54, 16, 97, 0, 98, 87, 96, 0, 53, 124, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 144, 4, 99, 255, 255, 255, 255, 22, 128, 99, 9, 0, 240, 16, 20,
97, 0, 103, 87, 128, 99, 68, 93, 240, 172, 20, 97, 0, 160, 87, 128, 99,
141, 165, 203, 91, 20, 97, 0, 201, 87, 128, 99, 253, 172, 213, 118, 20, 97,
1, 30, 87, 91, 96, 0, 128, 253, 91, 52, 21, 97, 0, 114, 87, 96, 0, 128,
253, 91, 97, 0, 158, 96, 4, 128, 128, 53, 115, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
22, 144, 96, 32, 1, 144, 145, 144, 80, 80, 97, 1, 65, 86, 91, 0, 91, 52,
21, 97, 0, 171, 87, 96, 0, 128, 253, 91, 97, 0, 179, 97, 2, 36, 86, 91, 96,
64, 81, 128, 130, 129, 82, 96, 32, 1, 145, 80, 80, 96, 64, 81, 128, 145, 3,
144, 243, 91, 52, 21, 97, 0, 212, 87, 96, 0, 128, 253, 91, 97, 0, 220, 97,
2, 42, 86, 91, 96, 64, 81, 128, 130, 115, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 22,
115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 22, 129, 82, 96, 32, 1, 145, 80, 80, 96, 64,
81, 128, 145, 3, 144, 243, 91, 52, 21, 97, 1, 41, 87, 96, 0, 128, 253, 91,
97, 1, 63, 96, 4, 128, 128, 53, 144, 96, 32, 1, 144, 145, 144, 80, 80, 97,
2, 79, 86, 91, 0, 91, 96, 0, 128, 96, 0, 144, 84, 144, 97, 1, 0, 10, 144,
4, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 22, 115, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 22,
51, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 22, 20, 21, 97, 2, 32, 87, 129, 144, 80,
128, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 22, 99, 253, 172, 213, 118, 96, 1, 84,
96, 64, 81, 130, 99, 255, 255, 255, 255, 22, 124, 1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 129, 82,
96, 4, 1, 128, 130, 129, 82, 96, 32, 1, 145, 80, 80, 96, 0, 96, 64, 81,
128, 131, 3, 129, 96, 0, 135, 128, 59, 21, 21, 97, 2, 11, 87, 96, 0, 128,
253, 91, 97, 2, 198, 90, 3, 241, 21, 21, 97, 2, 28, 87, 96, 0, 128, 253,
91, 80, 80, 80, 91, 80, 80, 86, 91, 96, 1, 84, 129, 86, 91, 96, 0, 128,
144, 84, 144, 97, 1, 0, 10, 144, 4, 115, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 129,
86, 91, 96, 0, 128, 144, 84, 144, 97, 1, 0, 10, 144, 4, 115, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 22, 115, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 22, 51, 115, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 22, 20, 21, 97, 2, 172, 87, 128, 96, 1, 129, 144, 85, 80, 91, 80,
86, 0, 161, 101, 98, 122, 122, 114, 48, 88, 32, 169, 198, 40, 119, 94, 251,
251, 193, 116, 119, 164, 114, 65, 60, 1, 238, 155, 51, 136, 31, 85, 12, 89,
210, 27, 238, 153, 40, 131, 92, 133, 75, 0, 41>>
}}
A hexadecimal number MUST be
[byte](https://en.wikipedia.org/wiki/Byte)-[aligned](https://en.wikipedia.org/wiki/Data_structure_alignment). If it
has an odd number of digits, then it is only [nibble](https://en.wikipedia.org/wiki/Nibble)-aligned and can't a
valid `t:EthereumJSONRPC.data/0`, which MUST be in bytes.
iex> Explorer.Chain.Data.cast("0xF")
:error
"""
@impl Ecto.Type
@spec cast(term()) :: {:ok, t()} | :error
def cast("0x" <> rest) do
with {:ok, bytes} <- Base.decode16(rest, case: :mixed) do
{:ok, %Data{bytes: bytes}}
end
end
def cast(%Data{} = data), do: {:ok, data}
def cast(_), do: :error
@doc """
Dumps the binary data to `:binary` (`bytea`) format used in database.
If the field from the struct is `t:t/0`, then it succeeds.
iex> Explorer.Chain.Data.dump(
...> %Explorer.Chain.Data{
...> bytes: <<>>
...> }
...> )
{:ok, <<>>}
If the field is from a different struct, `:error` is returned.
iex> Explorer.Chain.Data.dump(
...> %Explorer.Chain.Hash{
...> byte_count: 20,
...> bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
...> }
...> )
:error
"""
@impl Ecto.Type
@spec dump(term()) :: {:ok, binary()} | :error
def dump(%Data{bytes: bytes}), do: {:ok, bytes}
def dump(_), do: :error
@doc """
Loads the binary data from the database.
iex> Explorer.Chain.Data.load(<<>>)
{:ok, %Explorer.Chain.Data{bytes: <<>>}}
`EthereumJSONRPC.data/0` that has been converted to an integer cannot be loaded
iex> Explorer.Chain.Data.load(0)
:error
"""
@impl Ecto.Type
@spec load(term()) :: {:ok, t()} | :error
def load(<<_::binary>> = bytes), do: {:ok, %Data{bytes: bytes}}
def load(_), do: :error
@doc """
The underlying database type: `:binary`.
"""
@impl Ecto.Type
@spec type() :: :binary
def type, do: :binary
defimpl String.Chars do
@doc """
Converts the `#{@for}:t/0` to string representation shown to users.
iex> to_string(
...> %Explorer.Chain.Data{
...> bytes: <<>>
...> }
...> )
"0x"
iex> to_string(
...> %Explorer.Chain.Data{
...> bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7,
...> 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26,
...> 179, 239>>
...> }
...> )
"0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef"
"""
def to_string(data) do
@for.to_string(data)
end
end
end

@ -123,7 +123,7 @@ defmodule Explorer.Chain.Hash do
end
@doc """
Converts the `t:t/0` to string representation shown to users.
Converts the `t:t/0` to `iodata` representation shown to users.
iex> %Explorer.Chain.Hash{
...> byte_count: 32,

@ -3,17 +3,19 @@ defmodule Explorer.Chain.InternalTransaction do
use Explorer.Schema
alias Explorer.Chain.{Address, Gas, Hash, Transaction, Wei}
alias Explorer.Chain.{Address, Data, Gas, Hash, Transaction, Wei}
alias Explorer.Chain.InternalTransaction.{CallType, Type}
@typedoc """
* `call_type` - the type of call. `nil` when `type` is not `:call`.
* `created_contract_code` - the code of the contract that was crarted when `type` is `:create`.
* `error` - error message when `:call` `type` errors
* `from_address` - the source of the `value`
* `from_address_hash` - hash of the source of the `value`
* `gas` - the amount of gas allowed
* `gas_used` - the amount of gas used. `nil` when a call errors.
* `index` - the index of this internal transaction inside the `transaction`
* `init` - the constructor arguments for creating `created_contract_code` when `type` is `:create`.
* `input` - input bytes to the call
* `output` - output bytes from the call. `nil` when a call errors.
* `to_address` - the sink of the `value`
@ -26,14 +28,16 @@ defmodule Explorer.Chain.InternalTransaction do
"""
@type t :: %__MODULE__{
call_type: CallType.t() | nil,
created_contract_code: Data.t() | nil,
error: String.t(),
from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_hash: Hash.Truncated.t(),
gas: Gas.t(),
gas_used: Gas.t() | nil,
index: non_neg_integer(),
input: String.t(),
output: String.t() | nil,
init: Data.t() | nil,
input: Data.t(),
output: Data.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t(),
to_address_hash: Hash.Truncated.t(),
trace_address: [non_neg_integer()],
@ -45,14 +49,14 @@ defmodule Explorer.Chain.InternalTransaction do
schema "internal_transactions" do
field(:call_type, CallType)
field(:created_contract_code, :string)
field(:created_contract_code, Data)
field(:error, :string)
field(:gas, :decimal)
field(:gas_used, :decimal)
field(:index, :integer)
field(:init, :string)
field(:input, :string)
field(:output, :string)
field(:init, Data)
field(:input, Data)
field(:output, Data)
field(:trace_address, {:array, :integer})
field(:type, Type)
field(:value, Wei)

@ -3,7 +3,7 @@ defmodule Explorer.Chain.Log do
use Explorer.Schema
alias Explorer.Chain.{Address, Hash, Receipt, Transaction}
alias Explorer.Chain.{Address, Data, Hash, Receipt, Transaction}
@required_attrs ~w(address_hash data index transaction_hash type)a
@optional_attrs ~w(first_topic second_topic third_topic fourth_topic)a
@ -27,7 +27,7 @@ defmodule Explorer.Chain.Log do
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Truncated.t(),
data: String.t(),
data: Data.t(),
first_topic: String.t(),
fourth_topic: String.t(),
index: non_neg_integer(),
@ -40,7 +40,7 @@ defmodule Explorer.Chain.Log do
}
schema "logs" do
field(:data, :string)
field(:data, Data)
field(:first_topic, :string)
field(:fourth_topic, :string)
field(:index, :integer)

@ -4,7 +4,7 @@ defmodule Explorer.Chain.Transaction do
use Explorer.Schema
alias Ecto.Changeset
alias Explorer.Chain.{Address, Block, Gas, Hash, InternalTransaction, Receipt, Wei}
alias Explorer.Chain.{Address, Block, Data, Gas, Hash, InternalTransaction, Receipt, Wei}
@optional_attrs ~w(block_hash from_address_hash index to_address_hash)a
@required_attrs ~w(gas gas_price hash input nonce public_key r s standard_v v value)a
@ -12,7 +12,7 @@ defmodule Explorer.Chain.Transaction do
@typedoc """
The full public key of the signer of the transaction.
"""
@type public_key :: String.t()
@type public_key :: Data.t()
@typedoc """
X coordinate module n in
@ -103,7 +103,7 @@ defmodule Explorer.Chain.Transaction do
gas_price: wei_per_gas,
hash: Hash.t(),
index: non_neg_integer() | nil,
input: String.t(),
input: Data.t(),
internal_transactions: %Ecto.Association.NotLoaded{} | [InternalTransaction.t()],
nonce: non_neg_integer(),
public_key: public_key(),
@ -122,9 +122,9 @@ defmodule Explorer.Chain.Transaction do
field(:gas, :decimal)
field(:gas_price, Wei)
field(:index, :integer)
field(:input, :string)
field(:input, Data)
field(:nonce, :integer)
field(:public_key, :string)
field(:public_key, Data)
field(:r, :string)
field(:s, :string)
field(:standard_v, :string)

@ -10,9 +10,9 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do
# `null` when a pending transaction
add(:index, :integer, null: true)
add(:input, :text, null: false)
add(:input, :bytea, null: false)
add(:nonce, :integer, null: false)
add(:public_key, :string, null: false)
add(:public_key, :bytea, null: false)
add(:r, :string, null: false)
add(:s, :string, null: false)
add(:standard_v, :string, null: false)

@ -3,7 +3,7 @@ defmodule Explorer.Repo.Migrations.CreateLogs do
def change do
create table(:logs) do
add(:data, :text, null: false)
add(:data, :bytea, null: false)
add(:index, :integer, null: false)
add(:type, :string, null: false)
add(:first_topic, :string, null: true)

@ -4,7 +4,7 @@ defmodule Explorer.Repo.Migrations.CreateInternalTransactions do
def change do
create table(:internal_transactions) do
add(:call_type, :string, null: true)
add(:created_contract_code, :text, null: true)
add(:created_contract_code, :bytea, null: true)
# null unless there is an error
add(:error, :string, null: true)
# no gas budget for suicide
@ -13,10 +13,10 @@ defmodule Explorer.Repo.Migrations.CreateInternalTransactions do
# no gas_used for suicide
add(:gas_used, :numeric, precision: 100, null: true)
add(:index, :integer, null: false)
add(:init, :text)
add(:input, :text)
add(:init, :bytea)
add(:input, :bytea)
# can be null when `error` is not `null`
add(:output, :text)
add(:output, :bytea)
add(:trace_address, {:array, :integer}, null: false)
add(:type, :string, null: false)
add(:value, :numeric, precision: 100, null: false)

@ -0,0 +1,5 @@
defmodule Explrer.Chain.DataTest do
use ExUnit.Case, async: true
doctest Explorer.Chain.Data
end

@ -16,8 +16,8 @@ defmodule Explorer.Chain.InternalTransactionTest do
gas: 100,
gas_used: 100,
index: 0,
input: "pintos",
output: "refried",
input: "0x70696e746f73",
output: "0x72656672696564",
to_address_hash: "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f",
trace_address: [0, 1],
transaction_hash: transaction.hash,

@ -1,13 +1,14 @@
defmodule Explorer.Chain.TransactionTest do
use Explorer.DataCase
alias Ecto.Changeset
alias Explorer.Chain.Transaction
doctest Transaction
describe "changeset/2" do
test "with valid attributes" do
changeset =
assert %Changeset{valid?: true} =
Transaction.changeset(%Transaction{}, %{
hash: "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b",
value: 1,
@ -15,15 +16,13 @@ defmodule Explorer.Chain.TransactionTest do
gas_price: 10000,
input: "0x5c8eff12",
nonce: "31337",
public_key: "0xb39af9c",
public_key: "0xb39af9cb",
r: "0x9",
s: "0x10",
standard_v: "0x11",
transaction_index: "0x12",
v: "0x13"
})
assert changeset.valid?
end
test "with invalid attributes" do
@ -35,8 +34,8 @@ defmodule Explorer.Chain.TransactionTest do
params = params_for(:transaction)
to_address_params = %{hash: "sk8orDi3"}
changeset_params = Map.merge(params, %{to_address: to_address_params})
changeset = Transaction.changeset(%Transaction{}, changeset_params)
assert changeset.valid?
assert %Changeset{valid?: true} = Transaction.changeset(%Transaction{}, changeset_params)
end
end
end

@ -0,0 +1,5 @@
defmodule String.Chars.Explorer.Chain.DataTest do
use ExUnit.Case, async: true
doctest String.Chars.Explorer.Chain.Data
end

@ -1,7 +1,7 @@
defmodule Explorer.Factory do
use ExMachina.Ecto, repo: Explorer.Repo
alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, Log, Receipt, Transaction}
alias Explorer.Chain.{Address, Block, Data, Hash, InternalTransaction, Log, Receipt, Transaction}
alias Explorer.Market.MarketHistory
alias Explorer.Repo
@ -47,6 +47,25 @@ defmodule Explorer.Factory do
block_hash
end
def data(sequence_name) do
unpadded =
sequence_name
|> sequence(& &1)
|> Integer.to_string(16)
unpadded_length = String.length(unpadded)
padded =
case rem(unpadded_length, 2) do
0 -> unpadded
1 -> "0" <> unpadded
end
{:ok, data} = Data.cast("0x#{padded}")
data
end
def internal_transaction_factory do
type = internal_transaction_type()
@ -61,7 +80,7 @@ defmodule Explorer.Factory do
def log_factory do
%Log{
address_hash: insert(:address).hash,
data: sequence("0x"),
data: data(:log_data),
first_topic: nil,
fourth_topic: nil,
index: 0,
@ -72,6 +91,10 @@ defmodule Explorer.Factory do
}
end
def public_key do
data(:public_key)
end
def market_history_factory do
%MarketHistory{
closing_price: price(),
@ -94,9 +117,9 @@ defmodule Explorer.Factory do
gas: Enum.random(21_000..100_000),
gas_price: Enum.random(10..99) * 1_000_000_00,
hash: transaction_hash(),
input: sequence("0x"),
input: transaction_input(),
nonce: Enum.random(1..1_000),
public_key: sequence("0x"),
public_key: public_key(),
r: sequence("0x"),
s: sequence("0x"),
standard_v: sequence("0x"),
@ -115,6 +138,10 @@ defmodule Explorer.Factory do
transaction_hash
end
def transaction_input do
data(:transaction_input)
end
@doc """
Validates the pending `transaction`(s) by add it to a `t:Explorer.Chain.Block.t/0` and giving it a `receipt`
"""
@ -137,10 +164,6 @@ defmodule Explorer.Factory do
Repo.preload(block_transaction, [:block, :receipt])
end
defp integer_to_hexadecimal(integer) when is_integer(integer) do
"0x" <> Integer.to_string(integer, 16)
end
defp internal_transaction_factory(:create = type) do
gas = Enum.random(21_000..100_000)
gas_used = Enum.random(0..gas)
@ -150,14 +173,14 @@ defmodule Explorer.Factory do
receipt = insert(:receipt, transaction_hash: transaction.hash, transaction_index: transaction.index)
%InternalTransaction{
created_contract_code: sequence("internal_transaction_created_contract_code", &integer_to_hexadecimal/1),
created_contract_code: data(:internal_transaction_created_contract_code),
created_contract_address_hash: insert(:address).hash,
from_address_hash: insert(:address).hash,
gas: gas,
gas_used: gas_used,
index: 0,
# caller MUST suppy `index`
init: sequence("internal_transaction_init", &integer_to_hexadecimal/1),
init: data(:internal_transaction_init),
trace_address: [],
transaction_hash: receipt.transaction_hash,
type: type,

@ -1,5 +1,5 @@
alias Explorer.Chain
alias Explorer.Chain.{Address, Block, Hash, Transaction}
alias Explorer.Chain.{Address, Block, Data, Hash, Transaction}
defimpl Phoenix.HTML.Safe, for: [Address, Transaction] do
def to_iodata(%@for{hash: hash}) do
@ -13,6 +13,12 @@ defimpl Phoenix.HTML.Safe, for: Block do
end
end
defimpl Phoenix.HTML.Safe, for: Data do
def to_iodata(data) do
Chain.data_to_iodata(data)
end
end
defimpl Phoenix.HTML.Safe, for: Hash do
def to_iodata(hash) do
Chain.hash_to_iodata(hash)

@ -29,7 +29,7 @@ defmodule ExplorerWeb.ContributorBrowsingTest do
end
test "search for transactions", %{session: session} do
transaction = insert(:transaction, input: "socks")
transaction = insert(:transaction, input: "0x736f636b73")
session
|> visit("/")
@ -38,7 +38,7 @@ defmodule ExplorerWeb.ContributorBrowsingTest do
with: to_string(transaction.hash)
)
|> send_keys([:enter])
|> assert_has(css(".transaction__item", text: transaction.input))
|> assert_has(css(".transaction__item", text: to_string(transaction.input)))
end
test "search for address", %{session: session} do
@ -124,7 +124,7 @@ defmodule ExplorerWeb.ContributorBrowsingTest do
gas: Decimal.new(1_230_000_000_000_123_123),
gas_price: Decimal.new(7_890_000_000_898_912_300_045),
index: 4,
input: "0x00012",
input: "0x000012",
nonce: 99045,
inserted_at: Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}"),
updated_at: Timex.parse!("1980-01-01T00:00:18-00:00", "{ISO:Extended}"),
@ -202,7 +202,7 @@ defmodule ExplorerWeb.ContributorBrowsingTest do
)
)
|> assert_has(css(".transaction__item", text: "1,230,000,000,000,123,123 Gas"))
|> assert_has(css(".transaction__item", text: "0x00012"))
|> assert_has(css(".transaction__item", text: to_string(transaction.input)))
|> assert_has(css(".transaction__item", text: "99045"))
|> assert_has(css(".transaction__item", text: "123,987"))
|> assert_has(css(".transaction__item", text: to_string(lincoln.hash)))

@ -1,7 +1,7 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 91.2
"minimum_coverage": 91.1
},
"terminal_options": {
"file_column_width": 120

Loading…
Cancel
Save