Merge branch 'master' into feature/#2038-new-network-selector

pull/2044/head
Victor Baranov 5 years ago committed by GitHub
commit e29123a02b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 49
      apps/block_scout_web/assets/css/components/_transaction.scss
  3. 8
      apps/block_scout_web/assets/js/lib/async_listing_load.js
  4. 41
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  5. 4
      apps/block_scout_web/lib/block_scout_web/router.ex
  6. 9
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  7. 10
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  8. 11
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  9. 9
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex
  10. 42
      apps/block_scout_web/priv/gettext/default.pot
  11. 45
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  12. 36
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  13. 119
      apps/explorer/lib/explorer/chain/address_token_transfer_csv_exporter.ex
  14. 139
      apps/explorer/lib/explorer/chain/address_transaction_csv_exporter.ex
  15. 2
      apps/explorer/mix.exs
  16. 72
      apps/explorer/test/explorer/chain/address_token_transfer_csv_exporter_test.exs
  17. 105
      apps/explorer/test/explorer/chain/address_transaction_csv_exporter_test.exs
  18. 2
      apps/indexer/mix.exs
  19. 1
      mix.lock

@ -11,6 +11,8 @@
### Fixes ### Fixes
- [#2263](https://github.com/poanetwork/blockscout/pull/2263) - added an ability to close network selector on outside click - [#2263](https://github.com/poanetwork/blockscout/pull/2263) - added an ability to close network selector on outside click
- [#2257](https://github.com/poanetwork/blockscout/pull/2257) - 'download csv' button added to different tabs
- [#2242](https://github.com/poanetwork/blockscout/pull/2242) - added styles for 'download csv' button
- [#2261](https://github.com/poanetwork/blockscout/pull/2261) - header logo aligned to the center properly - [#2261](https://github.com/poanetwork/blockscout/pull/2261) - header logo aligned to the center properly
- [#2254](https://github.com/poanetwork/blockscout/pull/2254) - search length issue, tile link wrapping issue - [#2254](https://github.com/poanetwork/blockscout/pull/2254) - search length issue, tile link wrapping issue
- [#2238](https://github.com/poanetwork/blockscout/pull/2238) - header content alignment issue, hide navbar on outside click - [#2238](https://github.com/poanetwork/blockscout/pull/2238) - header content alignment issue, hide navbar on outside click
@ -90,6 +92,8 @@
### Fixes ### Fixes
- [#2228](https://github.com/poanetwork/blockscout/pull/2228) - favorites duplication issues, active radio issue - [#2228](https://github.com/poanetwork/blockscout/pull/2228) - favorites duplication issues, active radio issue
- [#2207](https://github.com/poanetwork/blockscout/pull/2207) - new 'download csv' button design
- [#2206](https://github.com/poanetwork/blockscout/pull/2206) - added styles for 'Download All Transactions as CSV' button
- [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width - [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width
- [#2098](https://github.com/poanetwork/blockscout/pull/2098) - nav dropdown issue, logo size issue - [#2098](https://github.com/poanetwork/blockscout/pull/2098) - nav dropdown issue, logo size issue
- [#2082](https://github.com/poanetwork/blockscout/pull/2082) - dropdown styles, tooltip gap fix, 404 page added - [#2082](https://github.com/poanetwork/blockscout/pull/2082) - dropdown styles, tooltip gap fix, 404 page added

@ -1,6 +1,45 @@
.transaction-details-address { .transaction-bottom-panel {
font-size: 12px; display: flex;
font-weight: bold; flex-direction: column;
line-height: 1.2; @media (min-width: 768px) {
margin: 0 0 12px; 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;
}
}
}
} }

@ -219,6 +219,14 @@ export const elements = {
$el.hide() $el.hide()
} }
},
'[csv-download]': {
render ($el, state) {
if (state.emptyResponse) {
return $el.hide()
}
return $el.show()
}
} }
} }

@ -10,6 +10,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias BlockScoutWeb.TransactionView alias BlockScoutWeb.TransactionView
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.{AddressTokenTransferCsvExporter, AddressTransactionCsvExporter}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View alias Phoenix.View
@ -106,4 +107,44 @@ defmodule BlockScoutWeb.AddressTransactionController do
not_found(conn) not_found(conn)
end end
end end
def token_transfers_csv(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
address
|> AddressTokenTransferCsvExporter.export()
|> Enum.into(
conn
|> put_resp_content_type("application/csv")
|> put_resp_header("content-disposition", "attachment; filename=token_transfers.csv")
|> send_chunked(200)
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def transactions_csv(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
address
|> AddressTransactionCsvExporter.export()
|> Enum.into(
conn
|> put_resp_content_type("application/csv")
|> put_resp_header("content-disposition", "attachment; filename=transactions.csv")
|> send_chunked(200)
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end end

@ -242,8 +242,12 @@ defmodule BlockScoutWeb.Router do
get("/search_logs", AddressLogsController, :search_logs) get("/search_logs", AddressLogsController, :search_logs)
get("/transactions_csv", AddressTransactionController, :transactions_csv)
get("/token_autocomplete", ChainController, :token_autocomplete) get("/token_autocomplete", ChainController, :token_autocomplete)
get("/token_transfers_csv", AddressTransactionController, :token_transfers_csv)
get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks) get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks)
get("/api_docs", APIDocsController, :index) get("/api_docs", APIDocsController, :index)

@ -68,7 +68,16 @@
<div data-items></div> <div data-items></div>
<!--<div class="transaction-bottom-panel">
<div class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :token_transfers_csv, %{"address_id" => to_string(@address.hash)}) %>><%= gettext("CSV") %></span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</a>
</div>-->
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
<!--</div>-->
</div> </div>
</div> </div>

@ -21,8 +21,18 @@
<div data-items></div> <div data-items></div>
<div class="transaction-bottom-panel">
<div csv-download class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :token_transfers_csv, %{"address_id" => to_string(@address.hash)}) %>><%= gettext("CSV") %></span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</a>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div> </div>
</div>
</div> </div>
</section> </section>
</section> </section>

@ -51,6 +51,8 @@
</div> </div>
</div> </div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;"> <button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
@ -65,7 +67,16 @@
<div data-items></div> <div data-items></div>
<div class="transaction-bottom-panel">
<div class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :token_transfers_csv, %{"address_id" => to_string(@address.hash)}) %>><%= gettext("CSV") %></span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</a>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div>
</div> </div>
</div> </div>

@ -28,7 +28,16 @@
<div data-items></div> <div data-items></div>
<!--<div class="transaction-bottom-panel">
<div class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :token_transfers_csv, %{"address_id" => to_string(@address.hash)}) %>><%= gettext("CSV") %></span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</a>
</div>-->
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
<!--</div>-->
</div> </div>
</div> </div>

@ -744,7 +744,7 @@ msgid "There are no tokens."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:62 #: lib/block_scout_web/templates/address_transaction/index.html.eex:64
msgid "There are no transactions for this address." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -1211,7 +1211,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:21 #: lib/block_scout_web/templates/address_logs/index.html.eex:21
#: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token/index.html.eex:13
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:57 #: lib/block_scout_web/templates/address_transaction/index.html.eex:59
#: lib/block_scout_web/templates/address_validation/index.html.eex:22 #: lib/block_scout_web/templates/address_validation/index.html.eex:22
#: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:23
#: lib/block_scout_web/templates/chain/show.html.eex:91 #: lib/block_scout_web/templates/chain/show.html.eex:91
@ -1690,11 +1690,6 @@ msgstr ""
msgid " Token Transfer" msgid " Token Transfer"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
msgid "There is no decompilded contracts for this address."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14
msgid " is recommended." msgid " is recommended."
@ -1705,16 +1700,6 @@ msgstr ""
msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:44
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:45
msgid "ERC-721 "
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4
msgid "ETH RPC API Documentation" msgid "ETH RPC API Documentation"
@ -1730,6 +1715,11 @@ msgstr ""
msgid "However, in general, the" msgid "However, in general, the"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
msgid "There is no decompilded contracts for this address."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7
msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found "
@ -1779,3 +1769,21 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:23 #: lib/block_scout_web/templates/layout/_network_selector.html.eex:23
msgid "Testnet" msgid "Testnet"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:73
#: lib/block_scout_web/templates/address_token/index.html.eex:26
#: lib/block_scout_web/templates/address_transaction/index.html.eex:72
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:33
msgid "CSV"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:44
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:45
msgid "ERC-721 "
msgstr ""

@ -734,7 +734,7 @@ msgid "There are no token transfers for this address."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_token/index.html.eex:18 #: lib/block_scout_web/templates/address_token/index.html.eex:19
msgid "There are no tokens for this address." msgid "There are no tokens for this address."
msgstr "" msgstr ""
@ -744,7 +744,7 @@ msgid "There are no tokens."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:62 #: lib/block_scout_web/templates/address_transaction/index.html.eex:64
msgid "There are no transactions for this address." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -812,7 +812,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:8 #: lib/block_scout_web/templates/address/_tabs.html.eex:8
#: lib/block_scout_web/templates/address_token/index.html.eex:8 #: lib/block_scout_web/templates/address_token/index.html.eex:9
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:9 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:9
#: lib/block_scout_web/views/address_view.ex:304 #: lib/block_scout_web/views/address_view.ex:304
msgid "Tokens" msgid "Tokens"
@ -1209,9 +1209,9 @@ msgstr ""
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61
#: lib/block_scout_web/templates/address_logs/index.html.eex:21 #: lib/block_scout_web/templates/address_logs/index.html.eex:21
#: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token/index.html.eex:14
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:57 #: lib/block_scout_web/templates/address_transaction/index.html.eex:59
#: lib/block_scout_web/templates/address_validation/index.html.eex:22 #: lib/block_scout_web/templates/address_validation/index.html.eex:22
#: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:23
#: lib/block_scout_web/templates/chain/show.html.eex:91 #: lib/block_scout_web/templates/chain/show.html.eex:91
@ -1690,11 +1690,6 @@ msgstr ""
msgid " Token Transfer" msgid " Token Transfer"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
msgid "There is no decompilded contracts for this address."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14
msgid " is recommended." msgid " is recommended."
@ -1705,16 +1700,6 @@ msgstr ""
msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:44
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:45
msgid "ERC-721 "
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4
msgid "ETH RPC API Documentation" msgid "ETH RPC API Documentation"
@ -1730,6 +1715,11 @@ msgstr ""
msgid "However, in general, the" msgid "However, in general, the"
msgstr "" msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
msgid "There is no decompilded contracts for this address."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7
msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found "
@ -1779,3 +1769,18 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:22 #: lib/block_scout_web/templates/layout/_network_selector.html.eex:22
msgid "Testnet" msgid "Testnet"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:74
msgid "CSV"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:44
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:45
msgid "ERC-721 "
msgstr ""

@ -132,4 +132,40 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
end) end)
end end
end end
describe "GET token_transfers_csv/2" do
test "exports token transfers to csv", %{conn: conn} do
address = insert(:address)
transaction =
:transaction
|> insert(from_address: address)
|> with_block()
insert(:token_transfer, transaction: transaction, from_address: address)
insert(:token_transfer, transaction: transaction, to_address: address)
conn = get(conn, "/token_transfers_csv", %{"address_id" => to_string(address.hash)})
assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
end
end
describe "GET transactions_csv/2" do
test "download csv file with transactions", %{conn: conn} do
address = insert(:address)
:transaction
|> insert(from_address: address)
|> with_block()
:transaction
|> insert(from_address: address)
|> with_block()
conn = get(conn, "/transactions_csv", %{"address_id" => to_string(address.hash)})
assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
end
end
end end

@ -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

@ -92,7 +92,9 @@ defmodule Explorer.Mixfile do
{:math, "~> 0.3.0"}, {:math, "~> 0.3.0"},
{:mock, "~> 0.3.0", only: [:test], runtime: false}, {:mock, "~> 0.3.0", only: [:test], runtime: false},
{:mox, "~> 0.4", only: [:test]}, {:mox, "~> 0.4", only: [:test]},
{:nimble_csv, "~> 0.6.0"},
{:poison, "~> 3.1"}, {:poison, "~> 3.1"},
{:nimble_csv, "~> 0.6.0"},
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.0.0"},
# For compatibility with `prometheus_process_collector`, which hasn't been updated yet # For compatibility with `prometheus_process_collector`, which hasn't been updated yet
{:prometheus, "~> 4.0", override: true}, {:prometheus, "~> 4.0", override: true},

@ -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

@ -49,7 +49,7 @@ defmodule Indexer.MixProject do
# JSONRPC access to Parity for `Explorer.Indexer` # JSONRPC access to Parity for `Explorer.Indexer`
{:ethereum_jsonrpc, in_umbrella: true}, {:ethereum_jsonrpc, in_umbrella: true},
# RLP encoding # RLP encoding
{:ex_rlp, "~> 0.3"}, {:ex_rlp, "~> 0.5.2"},
# Code coverage # Code coverage
{:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"}, {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"},
# Importing to database # Importing to database

@ -74,6 +74,7 @@
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"}, "mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},
"msgpax": {:hex, :msgpax, "2.2.2", "559a07806bbe5fdd31e4597455be030bd96356f7a621ca9eed832747c83bfb67", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "msgpax": {:hex, :msgpax, "2.2.2", "559a07806bbe5fdd31e4597455be030bd96356f7a621ca9eed832747c83bfb67", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"nimble_csv": {:hex, :nimble_csv, "0.6.0", "a3673f26d41f986774fe6060e309615343d3cb83a6d435754d8b1fdbd5764879", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], []}, "parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], []},

Loading…
Cancel
Save