Merge pull request #1184 from poanetwork/wsa-save-emission-reward-data-from-blockchain-call
Save reward data from blockchain callsa-turn-off-invalidconsensus-checker
commit
48fe3a450c
@ -0,0 +1,247 @@ |
|||||||
|
defmodule EthereumJSONRPC.Parity.FetchedBeneficiariesTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
alias EthereumJSONRPC |
||||||
|
alias EthereumJSONRPC.Parity.FetchedBeneficiaries |
||||||
|
|
||||||
|
describe "from_responses/2" do |
||||||
|
test "when block is not found" do |
||||||
|
block_quantity = EthereumJSONRPC.integer_to_quantity(1_000) |
||||||
|
responses = [%{id: 0, result: nil}] |
||||||
|
id_to_params = %{0 => %{block_quantity: block_quantity}} |
||||||
|
|
||||||
|
expected_output = %EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [%{code: 404, data: %{block_quantity: block_quantity}, message: "Not Found"}], |
||||||
|
params_set: MapSet.new([]) |
||||||
|
} |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.from_responses(responses, id_to_params) == expected_output |
||||||
|
end |
||||||
|
|
||||||
|
test "with an error result" do |
||||||
|
block_number = 1_000 |
||||||
|
block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) |
||||||
|
error_code = -32603 |
||||||
|
error_message = "Internal error occurred: {}, this should not be the case with eth_call, most likely a bug." |
||||||
|
|
||||||
|
responses = [%{id: 0, error: %{code: error_code, message: error_message}}] |
||||||
|
|
||||||
|
id_to_params = %{0 => %{block_quantity: block_quantity}} |
||||||
|
|
||||||
|
expected_output = %EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [%{code: error_code, data: %{block_quantity: block_quantity}, message: error_message}], |
||||||
|
params_set: MapSet.new() |
||||||
|
} |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.from_responses(responses, id_to_params) == expected_output |
||||||
|
end |
||||||
|
|
||||||
|
test "when reward type is external" do |
||||||
|
block_hash = "0x52a8d2185282506ce681364d2aa0c085ba45fdeb5d6c0ddec1131617a71ee2ca" |
||||||
|
block_number = 1_000 |
||||||
|
block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) |
||||||
|
hash1 = "0xef481b4e2c3ed62265617f2e9dfcdf3cf3efc11a" |
||||||
|
hash2 = "0x523b6539ff08d72a6c8bb598af95bf50c1ea839c" |
||||||
|
reward = "0xde0b6b3a7640000" |
||||||
|
|
||||||
|
responses = [ |
||||||
|
%{ |
||||||
|
id: 0, |
||||||
|
result: [ |
||||||
|
%{ |
||||||
|
"action" => %{"author" => hash1, "rewardType" => "external", "value" => reward}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => nil, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => nil, |
||||||
|
"transactionPosition" => nil, |
||||||
|
"type" => "reward" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"action" => %{"author" => hash2, "rewardType" => "external", "value" => reward}, |
||||||
|
"blockHash" => "0x52a8d2185282506ce681364d2aa0c085ba45fdeb5d6c0ddec1131617a71ee2ca", |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => nil, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => nil, |
||||||
|
"transactionPosition" => nil, |
||||||
|
"type" => "reward" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
id_to_params = %{0 => %{block_quantity: block_quantity}} |
||||||
|
|
||||||
|
expected_output = %EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [], |
||||||
|
params_set: |
||||||
|
MapSet.new([ |
||||||
|
%{ |
||||||
|
address_hash: hash1, |
||||||
|
address_type: :validator, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward |
||||||
|
}, |
||||||
|
%{ |
||||||
|
address_hash: hash2, |
||||||
|
address_type: :emission_funds, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward |
||||||
|
} |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.from_responses(responses, id_to_params) == expected_output |
||||||
|
end |
||||||
|
|
||||||
|
test "when reward type is block with uncles" do |
||||||
|
block_hash = "0x52a8d2185282506ce681364d2aa0c085ba45fdeb5d6c0ddec1131617a71ee2ca" |
||||||
|
block_number = 1_000 |
||||||
|
block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) |
||||||
|
hash1 = "0xef481b4e2c3ed62265617f2e9dfcdf3cf3efc11a" |
||||||
|
hash2 = "0x523b6539ff08d72a6c8bb598af95bf50c1ea839c" |
||||||
|
reward = "0xde0b6b3a7640000" |
||||||
|
|
||||||
|
responses = [ |
||||||
|
%{ |
||||||
|
id: 0, |
||||||
|
result: [ |
||||||
|
%{ |
||||||
|
"action" => %{"author" => hash1, "rewardType" => "block", "value" => reward}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => nil, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => nil, |
||||||
|
"transactionPosition" => nil, |
||||||
|
"type" => "reward" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"action" => %{"author" => hash2, "rewardType" => "uncle", "value" => reward}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => nil, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => nil, |
||||||
|
"transactionPosition" => nil, |
||||||
|
"type" => "reward" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
id_to_params = %{0 => %{block_quantity: block_quantity}} |
||||||
|
|
||||||
|
expected_output = %EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [], |
||||||
|
params_set: |
||||||
|
MapSet.new([ |
||||||
|
%{ |
||||||
|
address_hash: hash1, |
||||||
|
address_type: :validator, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward |
||||||
|
}, |
||||||
|
%{ |
||||||
|
address_hash: hash2, |
||||||
|
address_type: :uncle, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward |
||||||
|
} |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.from_responses(responses, id_to_params) == expected_output |
||||||
|
end |
||||||
|
|
||||||
|
test "ignores non-reward responses" do |
||||||
|
block_hash = "0x52a8d2185282506ce681364d2aa0c085ba45fdeb5d6c0ddec1131617a71ee2ca" |
||||||
|
block_number = 1_000 |
||||||
|
block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) |
||||||
|
hash1 = "0xef481b4e2c3ed62265617f2e9dfcdf3cf3efc11a" |
||||||
|
hash2 = "0x523b6539ff08d72a6c8bb598af95bf50c1ea839c" |
||||||
|
reward = "0xde0b6b3a7640000" |
||||||
|
|
||||||
|
responses = [ |
||||||
|
%{ |
||||||
|
id: 0, |
||||||
|
result: [ |
||||||
|
%{ |
||||||
|
"action" => %{ |
||||||
|
"callType" => "call", |
||||||
|
"from" => hash1, |
||||||
|
"gas" => "0x0", |
||||||
|
"input" => "0x", |
||||||
|
"to" => hash2, |
||||||
|
"value" => "0x4a817c800" |
||||||
|
}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => %{"gasUsed" => "0x0", "output" => "0x"}, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => "0x5acf90f846b8216bdbc309cf4eb24adc69d730bf29304dc0e740cf6df850666e", |
||||||
|
"transactionPosition" => 0, |
||||||
|
"type" => "call" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"action" => %{"author" => hash1, "rewardType" => "block", "value" => reward}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"result" => nil, |
||||||
|
"subtraces" => 0, |
||||||
|
"traceAddress" => [], |
||||||
|
"transactionHash" => nil, |
||||||
|
"transactionPosition" => nil, |
||||||
|
"type" => "reward" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
id_to_params = %{0 => %{block_quantity: block_quantity}} |
||||||
|
|
||||||
|
expected_output = %EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [], |
||||||
|
params_set: |
||||||
|
MapSet.new([ |
||||||
|
%{ |
||||||
|
address_hash: hash1, |
||||||
|
address_type: :validator, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward |
||||||
|
} |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.from_responses(responses, id_to_params) == expected_output |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "requests/1" do |
||||||
|
test "maps multiple ids to request params map" do |
||||||
|
input = %{ |
||||||
|
0 => %{block_quantity: EthereumJSONRPC.integer_to_quantity(0)}, |
||||||
|
1 => %{block_quantity: EthereumJSONRPC.integer_to_quantity(1)} |
||||||
|
} |
||||||
|
|
||||||
|
expected_output = [ |
||||||
|
%{id: 0, jsonrpc: "2.0", method: "trace_block", params: [EthereumJSONRPC.integer_to_quantity(0)]}, |
||||||
|
%{id: 1, jsonrpc: "2.0", method: "trace_block", params: [EthereumJSONRPC.integer_to_quantity(1)]} |
||||||
|
] |
||||||
|
|
||||||
|
assert FetchedBeneficiaries.requests(input) == expected_output |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,27 @@ |
|||||||
|
defmodule Explorer.Chain.Block.EmissionReward do |
||||||
|
@moduledoc """ |
||||||
|
Represents the static reward given to the miner of a block in a range of block numbers. |
||||||
|
""" |
||||||
|
|
||||||
|
use Ecto.Schema |
||||||
|
|
||||||
|
alias Explorer.Chain.Block.{EmissionReward, Range} |
||||||
|
alias Explorer.Chain.Wei |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
The static reward given to the miner of a block. |
||||||
|
|
||||||
|
* `:block_range` - Range of block numbers |
||||||
|
* `:reward` - Reward given in Wei |
||||||
|
""" |
||||||
|
@type t :: %EmissionReward{ |
||||||
|
block_range: Range.t(), |
||||||
|
reward: Wei.t() |
||||||
|
} |
||||||
|
|
||||||
|
@primary_key false |
||||||
|
schema "emission_rewards" do |
||||||
|
field(:block_range, Range) |
||||||
|
field(:reward, Wei) |
||||||
|
end |
||||||
|
end |
@ -1,27 +1,43 @@ |
|||||||
defmodule Explorer.Chain.Block.Reward do |
defmodule Explorer.Chain.Block.Reward do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Represents the static reward given to the miner of a block in a range of block numbers. |
Represents the total reward given to an address in a block. |
||||||
""" |
""" |
||||||
|
|
||||||
use Ecto.Schema |
use Explorer.Schema |
||||||
|
|
||||||
alias Explorer.Chain.Block.{Range, Reward} |
alias Explorer.Chain.Block.Reward.AddressType |
||||||
alias Explorer.Chain.Wei |
alias Explorer.Chain.{Hash, Wei} |
||||||
|
|
||||||
|
@required_attrs ~w(address_hash address_type block_hash reward)a |
||||||
|
|
||||||
@typedoc """ |
@typedoc """ |
||||||
The static reward given to the miner of a block. |
The validation reward given related to a block. |
||||||
|
|
||||||
* `:block_range` - Range of block numbers |
* `:address_hash` - Hash of address who received the reward |
||||||
* `:reward` - Reward given in Wei |
* `:address_type` - Type of the address_hash, either emission_funds, uncle or validator |
||||||
|
* `:block_hash` - Hash of the validated block |
||||||
|
* `:reward` - Total block reward |
||||||
""" |
""" |
||||||
@type t :: %Reward{ |
@type t :: %__MODULE__{ |
||||||
block_range: Range.t(), |
address_hash: Hash.Address.t(), |
||||||
|
address_type: AddressType.t(), |
||||||
|
block_hash: Hash.Full.t(), |
||||||
reward: Wei.t() |
reward: Wei.t() |
||||||
} |
} |
||||||
|
|
||||||
@primary_key false |
@primary_key false |
||||||
schema "block_rewards" do |
schema "block_rewards" do |
||||||
field(:block_range, Range) |
field(:address_hash, Hash.Address) |
||||||
|
field(:address_type, AddressType) |
||||||
|
field(:block_hash, Hash.Full) |
||||||
field(:reward, Wei) |
field(:reward, Wei) |
||||||
|
|
||||||
|
timestamps() |
||||||
|
end |
||||||
|
|
||||||
|
def changeset(%__MODULE__{} = reward, attrs) do |
||||||
|
reward |
||||||
|
|> cast(attrs, @required_attrs) |
||||||
|
|> validate_required(@required_attrs) |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,103 @@ |
|||||||
|
defmodule Explorer.Chain.Block.Reward.AddressType do |
||||||
|
@moduledoc """ |
||||||
|
Block reward address types |
||||||
|
""" |
||||||
|
|
||||||
|
@behaviour Ecto.Type |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
* `:emission_funds` |
||||||
|
* `:uncle` |
||||||
|
* `:validator` |
||||||
|
""" |
||||||
|
@type t :: :emission_funds | :uncle | :validator |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Casts `term` to `t:t/0` |
||||||
|
|
||||||
|
If the `term` is already in `t:t/0`, then it is returned |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast(:emission_funds) |
||||||
|
{:ok, :emission_funds} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast(:uncle) |
||||||
|
{:ok, :uncle} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast(:validator) |
||||||
|
{:ok, :validator} |
||||||
|
|
||||||
|
If `term` is a `String.t`, then it is converted to the corresponding `t:t/0`. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast("emission_funds") |
||||||
|
{:ok, :emission_funds} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast("uncle") |
||||||
|
{:ok, :uncle} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast("validator") |
||||||
|
{:ok, :validator} |
||||||
|
|
||||||
|
Unsupported `String.t` return an `:error`. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.cast("hard-fork") |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec cast(term()) :: {:ok, t()} | :error |
||||||
|
def cast(t) when t in ~w(emission_funds uncle validator)a, do: {:ok, t} |
||||||
|
def cast("emission_funds"), do: {:ok, :emission_funds} |
||||||
|
def cast("uncle"), do: {:ok, :uncle} |
||||||
|
def cast("validator"), do: {:ok, :validator} |
||||||
|
def cast(_), do: :error |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Dumps the `atom` format to `String.t` format used in the database. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.dump(:emission_funds) |
||||||
|
{:ok, "emission_funds"} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.dump(:uncle) |
||||||
|
{:ok, "uncle"} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.dump(:validator) |
||||||
|
{:ok, "validator"} |
||||||
|
|
||||||
|
|
||||||
|
Other atoms return an error |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.dump(:other) |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec dump(term()) :: {:ok, String.t()} | :error |
||||||
|
def dump(:emission_funds), do: {:ok, "emission_funds"} |
||||||
|
def dump(:uncle), do: {:ok, "uncle"} |
||||||
|
def dump(:validator), do: {:ok, "validator"} |
||||||
|
def dump(_), do: :error |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Loads the `t:String.t/0` from the database. |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.load("emission_funds") |
||||||
|
{:ok, :emission_funds} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.load("uncle") |
||||||
|
{:ok, :uncle} |
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.load("validator") |
||||||
|
{:ok, :validator} |
||||||
|
|
||||||
|
Other `t:String.t/0` return `:error` |
||||||
|
|
||||||
|
iex> Explorer.Chain.Block.Reward.AddressType.load("other") |
||||||
|
:error |
||||||
|
|
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec load(term()) :: {:ok, t()} | :error |
||||||
|
def load("emission_funds"), do: {:ok, :emission_funds} |
||||||
|
def load("uncle"), do: {:ok, :uncle} |
||||||
|
def load("validator"), do: {:ok, :validator} |
||||||
|
def load(_), do: :error |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The underlying database type: `:string` |
||||||
|
""" |
||||||
|
@impl Ecto.Type |
||||||
|
@spec type() :: :string |
||||||
|
def type, do: :string |
||||||
|
end |
@ -0,0 +1,78 @@ |
|||||||
|
defmodule Explorer.Chain.Import.Block.Rewards do |
||||||
|
@moduledoc """ |
||||||
|
Bulk imports `t:Explorer.Chain.Block.Reward.t/0`. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, only: [from: 2] |
||||||
|
|
||||||
|
alias Ecto.{Changeset, Multi} |
||||||
|
alias Explorer.Chain.Block.Reward |
||||||
|
alias Explorer.Chain.Import |
||||||
|
|
||||||
|
@behaviour Import.Runner |
||||||
|
|
||||||
|
# milliseconds |
||||||
|
@timeout 60_000 |
||||||
|
|
||||||
|
@type imported :: [Reward.t()] |
||||||
|
|
||||||
|
@impl Import.Runner |
||||||
|
def ecto_schema_module, do: Reward |
||||||
|
|
||||||
|
@impl Import.Runner |
||||||
|
def option_key, do: :block_rewards |
||||||
|
|
||||||
|
@impl Import.Runner |
||||||
|
def imported_table_row do |
||||||
|
%{ |
||||||
|
value_type: "[#{ecto_schema_module()}.t()]", |
||||||
|
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
@impl Import.Runner |
||||||
|
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||||
|
insert_options = |
||||||
|
options |
||||||
|
|> Map.put_new(:timeout, @timeout) |
||||||
|
|> Map.put(:timestamps, timestamps) |
||||||
|
|
||||||
|
Multi.run(multi, option_key(), fn _ -> insert(changes_list, insert_options) end) |
||||||
|
end |
||||||
|
|
||||||
|
@impl Import.Runner |
||||||
|
def timeout, do: @timeout |
||||||
|
|
||||||
|
@spec insert([map()], %{ |
||||||
|
required(:timeout) => timeout, |
||||||
|
required(:timestamps) => Import.timestamps() |
||||||
|
}) :: {:ok, [Reward.t()]} | {:error, [Changeset.t()]} |
||||||
|
defp insert(changes_list, %{timeout: timeout, timestamps: timestamps}) |
||||||
|
when is_list(changes_list) do |
||||||
|
Import.insert_changes_list( |
||||||
|
changes_list, |
||||||
|
conflict_target: [:address_hash, :address_type, :block_hash], |
||||||
|
on_conflict: on_conflict(), |
||||||
|
for: ecto_schema_module(), |
||||||
|
returning: true, |
||||||
|
timeout: timeout, |
||||||
|
timestamps: timestamps |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
defp on_conflict do |
||||||
|
from( |
||||||
|
block_reward in Reward, |
||||||
|
update: [ |
||||||
|
set: [ |
||||||
|
address_hash: block_reward.address_hash, |
||||||
|
address_type: block_reward.address_type, |
||||||
|
block_hash: block_reward.block_hash, |
||||||
|
reward: block_reward.reward, |
||||||
|
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", block_reward.inserted_at), |
||||||
|
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", block_reward.updated_at) |
||||||
|
] |
||||||
|
] |
||||||
|
) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
defmodule Explorer.Repo.Migrations.RenameBlockRewardsToEmissionRewards do |
||||||
|
use Ecto.Migration |
||||||
|
|
||||||
|
def change do |
||||||
|
rename(table(:block_rewards), to: table(:emission_rewards)) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,16 @@ |
|||||||
|
defmodule Explorer.Repo.Migrations.CreateNewBlockRewards do |
||||||
|
use Ecto.Migration |
||||||
|
|
||||||
|
def change do |
||||||
|
create table(:block_rewards, primary_key: false) do |
||||||
|
add(:address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false) |
||||||
|
add(:address_type, :string, null: false) |
||||||
|
add(:block_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: false) |
||||||
|
add(:reward, :numeric, precision: 100, null: true) |
||||||
|
|
||||||
|
timestamps(null: false, type: :utc_datetime) |
||||||
|
end |
||||||
|
|
||||||
|
create(unique_index(:block_rewards, [:address_hash, :address_type, :block_hash])) |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue