commit
5fb9c146cf
@ -0,0 +1,139 @@ |
||||
defmodule Explorer.Chain.AddressTransactionCsvExporter do |
||||
@moduledoc """ |
||||
Exports transactions to a csv file. |
||||
""" |
||||
|
||||
import Ecto.Query, |
||||
only: [ |
||||
from: 2 |
||||
] |
||||
|
||||
alias Explorer.{Chain, Market, PagingOptions, Repo} |
||||
alias Explorer.Market.MarketHistory |
||||
alias Explorer.Chain.{Address, Transaction, Wei} |
||||
alias Explorer.ExchangeRates.Token |
||||
alias NimbleCSV.RFC4180 |
||||
|
||||
@necessity_by_association [ |
||||
necessity_by_association: %{ |
||||
[created_contract_address: :names] => :optional, |
||||
[from_address: :names] => :optional, |
||||
[to_address: :names] => :optional, |
||||
[token_transfers: :token] => :optional, |
||||
[token_transfers: :to_address] => :optional, |
||||
[token_transfers: :from_address] => :optional, |
||||
[token_transfers: :token_contract_address] => :optional, |
||||
:block => :required |
||||
} |
||||
] |
||||
|
||||
@page_size 150 |
||||
|
||||
@paging_options %PagingOptions{page_size: @page_size + 1} |
||||
|
||||
@spec export(Address.t()) :: Enumerable.t() |
||||
def export(address) do |
||||
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||
|
||||
address |
||||
|> fetch_all_transactions(@paging_options) |
||||
|> to_csv_format(address, exchange_rate) |
||||
|> dump_to_stream() |
||||
end |
||||
|
||||
defp fetch_all_transactions(address, paging_options, acc \\ []) do |
||||
options = Keyword.merge(@necessity_by_association, paging_options: paging_options) |
||||
|
||||
transactions = Chain.address_to_transactions_with_rewards(address, options) |
||||
|
||||
new_acc = transactions ++ acc |
||||
|
||||
case Enum.split(transactions, @page_size) do |
||||
{_transactions, [%Transaction{block_number: block_number, index: index}]} -> |
||||
new_paging_options = %{@paging_options | key: {block_number, index}} |
||||
fetch_all_transactions(address, new_paging_options, new_acc) |
||||
|
||||
{_, []} -> |
||||
new_acc |
||||
end |
||||
end |
||||
|
||||
defp dump_to_stream(transactions) do |
||||
transactions |
||||
|> RFC4180.dump_to_stream() |
||||
end |
||||
|
||||
defp to_csv_format(transactions, address, exchange_rate) do |
||||
row_names = [ |
||||
"TxHash", |
||||
"BlockNumber", |
||||
"UnixTimestamp", |
||||
"FromAddress", |
||||
"ToAddress", |
||||
"ContractAddress", |
||||
"Type", |
||||
"Value", |
||||
"Fee", |
||||
"Status", |
||||
"ErrCode", |
||||
"CurrentPrice", |
||||
"TxDateOpeningPrice", |
||||
"TxDateClosingPrice" |
||||
] |
||||
|
||||
transaction_lists = |
||||
transactions |
||||
|> Stream.map(fn transaction -> |
||||
{opening_price, closing_price} = price_at_date(transaction.block.timestamp) |
||||
|
||||
[ |
||||
to_string(transaction.hash), |
||||
transaction.block_number, |
||||
transaction.block.timestamp, |
||||
to_string(transaction.from_address), |
||||
to_string(transaction.to_address), |
||||
to_string(transaction.created_contract_address), |
||||
type(transaction, address), |
||||
Wei.to(transaction.value, :wei), |
||||
fee(transaction), |
||||
transaction.status, |
||||
transaction.error, |
||||
exchange_rate.usd_value, |
||||
opening_price, |
||||
closing_price |
||||
] |
||||
end) |
||||
|
||||
Stream.concat([row_names], transaction_lists) |
||||
end |
||||
|
||||
defp type(%Transaction{from_address_hash: from_address}, %Address{hash: from_address}), do: "OUT" |
||||
|
||||
defp type(%Transaction{to_address_hash: to_address}, %Address{hash: to_address}), do: "IN" |
||||
|
||||
defp type(_, _), do: "" |
||||
|
||||
defp fee(transaction) do |
||||
transaction |
||||
|> Chain.fee(:wei) |
||||
|> case do |
||||
{:actual, value} -> value |
||||
{:maximum, value} -> "Max of #{value}" |
||||
end |
||||
end |
||||
|
||||
defp price_at_date(datetime) do |
||||
date = DateTime.to_date(datetime) |
||||
|
||||
query = |
||||
from( |
||||
mh in MarketHistory, |
||||
where: mh.date == ^date |
||||
) |
||||
|
||||
case Repo.one(query) do |
||||
nil -> {nil, nil} |
||||
price -> {price.opening_price, price.closing_price} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,105 @@ |
||||
defmodule Explorer.Chain.AddressTransactionCsvExporterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Chain.{AddressTransactionCsvExporter, Wei} |
||||
|
||||
describe "export/1" do |
||||
test "exports address transactions to csv" do |
||||
address = insert(:address) |
||||
|
||||
transaction = |
||||
:transaction |
||||
|> insert(from_address: address) |
||||
|> with_block() |
||||
|> Repo.preload(:token_transfers) |
||||
|
||||
[result] = |
||||
address |
||||
|> AddressTransactionCsvExporter.export() |
||||
|> Enum.to_list() |
||||
|> Enum.drop(1) |
||||
|> Enum.map(fn [ |
||||
hash, |
||||
_, |
||||
block_number, |
||||
_, |
||||
timestamp, |
||||
_, |
||||
from_address, |
||||
_, |
||||
to_address, |
||||
_, |
||||
created_address, |
||||
_, |
||||
type, |
||||
_, |
||||
value, |
||||
_, |
||||
fee, |
||||
_, |
||||
status, |
||||
_, |
||||
error, |
||||
_, |
||||
cur_price, |
||||
_, |
||||
op_price, |
||||
_, |
||||
cl_price, |
||||
_ |
||||
] -> |
||||
%{ |
||||
hash: hash, |
||||
block_number: block_number, |
||||
timestamp: timestamp, |
||||
from_address: from_address, |
||||
to_address: to_address, |
||||
created_address: created_address, |
||||
type: type, |
||||
value: value, |
||||
fee: fee, |
||||
status: status, |
||||
error: error, |
||||
current_price: cur_price, |
||||
opening_price: op_price, |
||||
closing_price: cl_price |
||||
} |
||||
end) |
||||
|
||||
assert result.block_number == to_string(transaction.block_number) |
||||
assert result.timestamp |
||||
assert result.created_address == to_string(transaction.created_contract_address_hash) |
||||
assert result.from_address == to_string(transaction.from_address) |
||||
assert result.to_address == to_string(transaction.to_address) |
||||
assert result.hash == to_string(transaction.hash) |
||||
assert result.type == "OUT" |
||||
assert result.value == transaction.value |> Wei.to(:wei) |> to_string() |
||||
assert result.fee |
||||
assert result.status == to_string(transaction.status) |
||||
assert result.error == to_string(transaction.error) |
||||
assert result.current_price |
||||
assert result.opening_price |
||||
assert result.closing_price |
||||
end |
||||
|
||||
test "fetches all transactions" do |
||||
address = insert(:address) |
||||
|
||||
1..200 |
||||
|> Enum.map(fn _ -> |
||||
:transaction |
||||
|> insert(from_address: address) |
||||
|> with_block() |
||||
end) |
||||
|> Enum.count() |
||||
|
||||
result = |
||||
address |
||||
|> AddressTransactionCsvExporter.export() |
||||
|> Enum.to_list() |
||||
|> Enum.drop(1) |
||||
|
||||
assert Enum.count(result) == 200 |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue