commit
e29123a02b
@ -1,6 +1,45 @@ |
||||
.transaction-details-address { |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
line-height: 1.2; |
||||
margin: 0 0 12px; |
||||
.transaction-bottom-panel { |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
} |
||||
|
||||
.transaction-bottom-panel { |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
} |
||||
|
||||
.download-all-transactions { |
||||
text-align: center; |
||||
color: #a3a9b5; |
||||
font-size: 13px; |
||||
margin-top: 10px; |
||||
@media (min-width: 768px) { |
||||
margin-top: 30px; |
||||
} |
||||
.download-all-transactions-link { |
||||
text-decoration: none; |
||||
svg { |
||||
position: relative; |
||||
margin-left: 2px; |
||||
top: -3px; |
||||
path { |
||||
fill: $primary; |
||||
} |
||||
} |
||||
&:hover { |
||||
span { |
||||
text-decoration: underline; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,119 @@ |
||||
defmodule Explorer.Chain.AddressTokenTransferCsvExporter do |
||||
@moduledoc """ |
||||
Exports token transfers to a csv file. |
||||
""" |
||||
|
||||
alias Explorer.{Chain, PagingOptions} |
||||
alias Explorer.Chain.{Address, TokenTransfer, Transaction} |
||||
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} |
||||
|
||||
def export(address) do |
||||
address |
||||
|> fetch_all_transactions(@paging_options) |
||||
|> to_token_transfers() |
||||
|> to_csv_format(address) |
||||
|> dump_to_stream() |
||||
end |
||||
|
||||
defp fetch_all_transactions(address, paging_options, acc \\ []) do |
||||
options = Keyword.merge(@necessity_by_association, paging_options: paging_options) |
||||
|
||||
transactions = |
||||
address |
||||
|> Chain.address_to_transactions_with_rewards(options) |
||||
|> Enum.filter(fn transaction -> Enum.count(transaction.token_transfers) > 0 end) |
||||
|
||||
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 to_token_transfers(transactions) do |
||||
transactions |
||||
|> Enum.flat_map(fn transaction -> |
||||
transaction.token_transfers |
||||
|> Enum.map(fn transfer -> %{transfer | transaction: transaction} end) |
||||
end) |
||||
end |
||||
|
||||
defp dump_to_stream(transactions) do |
||||
transactions |
||||
|> RFC4180.dump_to_stream() |
||||
end |
||||
|
||||
defp to_csv_format(token_transfers, address) do |
||||
row_names = [ |
||||
"TxHash", |
||||
"BlockNumber", |
||||
"UnixTimestamp", |
||||
"FromAddress", |
||||
"ToAddress", |
||||
"TokenContractAddress", |
||||
"Type", |
||||
"TokenSymbol", |
||||
"TokensTransferred", |
||||
"TransactionFee", |
||||
"Status", |
||||
"ErrCode" |
||||
] |
||||
|
||||
token_transfer_lists = |
||||
token_transfers |
||||
|> Stream.map(fn token_transfer -> |
||||
[ |
||||
to_string(token_transfer.transaction_hash), |
||||
token_transfer.transaction.block_number, |
||||
token_transfer.transaction.block.timestamp, |
||||
token_transfer.from_address |> to_string() |> String.downcase(), |
||||
token_transfer.to_address |> to_string() |> String.downcase(), |
||||
token_transfer.token_contract_address |> to_string() |> String.downcase(), |
||||
type(token_transfer, address), |
||||
token_transfer.token.symbol, |
||||
token_transfer.amount, |
||||
fee(token_transfer.transaction), |
||||
token_transfer.transaction.status, |
||||
token_transfer.transaction.error |
||||
] |
||||
end) |
||||
|
||||
Stream.concat([row_names], token_transfer_lists) |
||||
end |
||||
|
||||
defp type(%TokenTransfer{from_address_hash: from_address}, %Address{hash: from_address}), do: "OUT" |
||||
|
||||
defp type(%TokenTransfer{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 |
||||
end |
@ -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,72 @@ |
||||
defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Chain.AddressTokenTransferCsvExporter |
||||
|
||||
describe "export/1" do |
||||
test "exports token transfers to csv" do |
||||
address = insert(:address) |
||||
|
||||
transaction = |
||||
:transaction |
||||
|> insert(from_address: address) |
||||
|> with_block() |
||||
|
||||
token_transfer = insert(:token_transfer, transaction: transaction, from_address: address) |
||||
|
||||
[result] = |
||||
address |
||||
|> AddressTokenTransferCsvExporter.export() |
||||
|> Enum.to_list() |
||||
|> Enum.drop(1) |
||||
|> Enum.map(fn [ |
||||
tx_hash, |
||||
_, |
||||
block_number, |
||||
_, |
||||
timestamp, |
||||
_, |
||||
from_address, |
||||
_, |
||||
to_address, |
||||
_, |
||||
token_contract_address, |
||||
_, |
||||
type, |
||||
_, |
||||
token_symbol, |
||||
_, |
||||
tokens_transferred, |
||||
_, |
||||
transaction_fee, |
||||
_, |
||||
status, |
||||
_, |
||||
err_code, |
||||
_ |
||||
] -> |
||||
%{ |
||||
tx_hash: tx_hash, |
||||
block_number: block_number, |
||||
timestamp: timestamp, |
||||
from_address: from_address, |
||||
to_address: to_address, |
||||
token_contract_address: token_contract_address, |
||||
type: type, |
||||
token_symbol: token_symbol, |
||||
tokens_transferred: tokens_transferred, |
||||
transaction_fee: transaction_fee, |
||||
status: status, |
||||
err_code: err_code |
||||
} |
||||
end) |
||||
|
||||
assert result.block_number == to_string(transaction.block_number) |
||||
assert result.tx_hash == to_string(transaction.hash) |
||||
assert result.from_address == token_transfer.from_address_hash |> to_string() |> String.downcase() |
||||
assert result.to_address == token_transfer.to_address_hash |> to_string() |> String.downcase() |
||||
assert result.timestamp == to_string(transaction.block.timestamp) |
||||
assert result.type == "OUT" |
||||
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