Support reCAPTCHA v3

pull/7273/head
Viktor Baranov 2 years ago
parent 9d670a17fd
commit ab41f7e821
  1. 138
      apps/block_scout_web/assets/js/lib/csv_download.js
  2. 20
      apps/block_scout_web/lib/block_scout_web/captcha_helper.ex
  3. 30
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  4. 16
      apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex
  5. 3
      apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex
  6. 11
      apps/block_scout_web/lib/block_scout_web/views/csv_export.ex
  7. 4
      apps/block_scout_web/priv/gettext/default.pot
  8. 4
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  9. 23
      apps/explorer/lib/explorer/chain/csv_export/address_internal_transaction_csv_exporter.ex
  10. 20
      apps/explorer/lib/explorer/chain/csv_export/address_log_csv_exporter.ex
  11. 19
      apps/explorer/lib/explorer/chain/csv_export/address_token_transfer_csv_exporter.ex
  12. 20
      apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex
  13. 29
      apps/explorer/lib/explorer/chain/csv_export/helper.ex
  14. 5
      apps/explorer/test/explorer/chain/csv_export/address_internal_transaction_csv_exporter_test.exs
  15. 2
      apps/explorer/test/explorer/chain/csv_export/address_log_csv_exporter_test.exs
  16. 2
      apps/explorer/test/explorer/chain/csv_export/address_token_transfer_csv_exporter_test.exs
  17. 3
      apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs
  18. 8
      config/runtime.exs
  19. 2
      docker-compose/envs/common-blockscout.env
  20. 6
      docker/Makefile

@ -10,86 +10,115 @@ const DATE_FORMAT = 'YYYY-MM-DD'
const $button = $('#export-csv-button')
// eslint-disable-next-line
const _instance1 = new Pikaday({
field: $('.js-datepicker-from')[0],
onSelect: (date) => onSelect(date, 'from_period'),
defaultDate: moment().add(-1, 'months').toDate(),
setDefaultDate: true,
maxDate: new Date(),
format: DATE_FORMAT
})
const _instance1 = generateDatePicker('.js-datepicker-from', moment().add(-1, 'months').toDate())
// eslint-disable-next-line
const _instance2 = new Pikaday({
field: $('.js-datepicker-to')[0],
onSelect: (date) => onSelect(date, 'to_period'),
defaultDate: new Date(),
const _instance2 = generateDatePicker('.js-datepicker-to', new Date())
function generateDatePicker (classSelector, defaultDate) {
return new Pikaday({
field: $(classSelector)[0],
defaultDate,
setDefaultDate: true,
maxDate: new Date(),
format: DATE_FORMAT
})
})
}
$button.on('click', () => {
// @ts-ignore
// eslint-disable-next-line
const reCaptchaV2ClientKey = document.getElementById('re-captcha-client-key').value
// @ts-ignore
// eslint-disable-next-line
const reCaptchaV3ClientKey = document.getElementById('re-captcha-v3-client-key').value
const addressHash = $button.data('address-hash')
const from = $('.js-datepicker-from').val()
const to = $('.js-datepicker-to').val()
if (reCaptchaV3ClientKey) {
disableBtnWithSpinner()
// @ts-ignore
// eslint-disable-next-line
grecaptcha.ready(function () {
// @ts-ignore
// eslint-disable-next-line
grecaptcha.execute(reCaptchaV3ClientKey, { action: 'login' })
.then(function (token) {
const url = `${$button.data('link')}?address_id=${addressHash}&from_period=${from}&to_period=${to}&recaptcha_response=${token}`
download(url)
})
})
} else if (reCaptchaV2ClientKey) {
// @ts-ignore
// eslint-disable-next-line
const recaptchaResponse = grecaptcha.getResponse()
if (recaptchaResponse) {
$button.addClass('spinner')
$button.prop('disabled', true)
const downloadUrl = `${$button.data('link')}&recaptcha_response=${recaptchaResponse}`
disableBtnWithSpinner()
const url = `${$button.data('link')}?address_id=${addressHash}&from_period=${from}&to_period=${to}&recaptcha_response=${recaptchaResponse}`
$('body').append($('<iframe id="csv-iframe" style="display: none;"></iframe>'))
$('#csv-iframe').attr('src', downloadUrl)
download(url, true, true)
}
} else {
alertWhenRecaptchaNotConfigured()
}
const interval = setInterval(handleCSVDownloaded, 1000)
setTimeout(resetDownload, 60000)
function download (url, resetRecaptcha, disable) {
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
.then(response => {
if (response.status === 200) {
return response.blob()
}
})
.then(response => {
if (response) {
const blob = new Blob([response], { type: 'application/csv' })
const downloadUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = downloadUrl
a.download = `${$button.data('type')}_for_${addressHash}_from_${from}_to_${to}.csv`
document.body.appendChild(a)
a.click()
function handleCSVDownloaded () {
if (Cookies.get('csv-downloaded') === 'true') {
resetDownload()
resetBtn(resetRecaptcha, disable)
} else {
alertWhenRequestFailed()
resetBtn(resetRecaptcha, disable)
}
})
}
function resetDownload () {
function resetBtn (resetRecaptcha, disable) {
$button.removeClass('spinner')
if (!disable) {
$button.prop('disabled', false)
clearInterval(interval)
}
Cookies.remove('csv-downloaded')
if (resetRecaptcha) {
// @ts-ignore
// eslint-disable-next-line
grecaptcha.reset()
}
}
})
function onSelect (date, paramToReplace) {
const formattedDate = moment(date).format(DATE_FORMAT)
if (date) {
const csvExportPath = $button.data('link')
const updatedCsvExportUrl = replaceUrlParam(csvExportPath, paramToReplace, formattedDate)
$button.data('link', updatedCsvExportUrl)
function disableBtnWithSpinner () {
$button.addClass('spinner')
disableBtn()
}
}
function replaceUrlParam (url, paramName, paramValue) {
if (paramValue == null) {
paramValue = ''
}
const pattern = new RegExp('\\b(' + paramName + '=).*?(&|#|$)')
if (url.search(pattern) >= 0) {
return url.replace(pattern, '$1' + paramValue + '$2')
function disableBtn () {
$button.prop('disabled', true)
}
url = url.replace(/[?#]$/, '')
return url + (url.indexOf('?') > 0 ? '&' : '?') + paramName + '=' + paramValue
}
})
const onloadCallback = function () {
// @ts-ignore
// eslint-disable-next-line
const reCaptchaClientKey = document.getElementById('re-captcha-client-key').value
if (reCaptchaClientKey) {
// @ts-ignore
// eslint-disable-next-line
grecaptcha.render('recaptcha', {
@ -100,13 +129,22 @@ const onloadCallback = function () {
document.getElementById('export-csv-button').disabled = false
}
})
} else {
}
function alertWhenRecaptchaNotConfigured () {
Swal.fire({
title: 'Warning',
html: 'CSV download will not work since reCAPTCHA is not configured. Please advise server maintainer to configure RE_CAPTCHA_CLIENT_KEY and RE_CAPTCHA_SECRET_KEY environment variables.',
html: 'CSV download is disabled since reCAPTCHA is not configured on server side. Please advise server maintainer to configure RE_CAPTCHA_CLIENT_KEY and RE_CAPTCHA_SECRET_KEY environment variables in case of reCAPTCHAv2 or RE_CAPTCHA_V3_CLIENT_KEY and RE_CAPTCHA_V3_SECRET_KEY environment variables in case of reCAPTCHAv3.',
icon: 'warning'
})
}
function alertWhenRequestFailed () {
Swal.fire({
title: 'Warning',
html: 'Request failed, please try again later or decrease time range for exporting data.',
icon: 'warning'
})
}
}
// @ts-ignore

@ -8,7 +8,9 @@ defmodule BlockScoutWeb.CaptchaHelper do
def recaptcha_passed?(nil), do: false
def recaptcha_passed?(recaptcha_response) do
re_captcha_secret_key = Application.get_env(:block_scout_web, :re_captcha_secret_key)
re_captcha_v2_secret_key = Application.get_env(:block_scout_web, :recaptcha)[:v2_secret_key]
re_captcha_v3_secret_key = Application.get_env(:block_scout_web, :recaptcha)[:v3_secret_key]
re_captcha_secret_key = re_captcha_v2_secret_key || re_captcha_v3_secret_key
body = "secret=#{re_captcha_secret_key}&response=#{recaptcha_response}"
headers = [{"Content-type", "application/x-www-form-urlencoded"}]
@ -16,7 +18,7 @@ defmodule BlockScoutWeb.CaptchaHelper do
case HTTPoison.post("https://www.google.com/recaptcha/api/siteverify", body, headers, []) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode!(body) do
%{"success" => true} -> true
%{"success" => true} = resp -> is_success?(resp)
_ -> false
end
@ -24,4 +26,18 @@ defmodule BlockScoutWeb.CaptchaHelper do
false
end
end
defp is_success?(%{"score" => score}) do
check_recaptcha_v3_score(score)
end
defp is_success?(_resp), do: true
defp check_recaptcha_v3_score(score) do
if score >= 0.5 do
true
else
false
end
end
end

@ -14,14 +14,15 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias BlockScoutWeb.{AccessHelper, Controller, TransactionView}
alias Explorer.{Chain, Market}
alias Explorer.Chain.{
alias Explorer.Chain.CSVExport.{
AddressInternalTransactionCsvExporter,
AddressLogCsvExporter,
AddressTokenTransferCsvExporter,
AddressTransactionCsvExporter,
Wei
AddressTransactionCsvExporter
}
alias Explorer.Chain.Wei
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -171,10 +172,10 @@ defmodule BlockScoutWeb.AddressTransactionController do
|> Application.get_env(:captcha_helper)
end
defp put_resp_params(conn, file_name) do
defp put_resp_params(conn) do
conn
|> put_resp_content_type("application/csv")
|> put_resp_header("content-disposition", "attachment; filename=#{file_name}")
|> put_resp_header("content-disposition", "attachment;")
|> put_resp_cookie("csv-downloaded", "true", max_age: 86_400, http_only: false)
|> send_chunked(200)
end
@ -187,8 +188,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"to_period" => to_period,
"recaptcha_response" => recaptcha_response
},
csv_export_module,
file_name
csv_export_module
)
when is_binary(address_hash_string) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
@ -196,7 +196,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
{:recaptcha, true} <- {:recaptcha, captcha_helper().recaptcha_passed?(recaptcha_response)} do
address
|> csv_export_module.export(from_period, to_period)
|> Enum.reduce_while(put_resp_params(conn, file_name), fn chunk, conn ->
|> Enum.reduce_while(put_resp_params(conn), fn chunk, conn ->
case Conn.chunk(conn, chunk) do
{:ok, conn} ->
{:cont, conn}
@ -217,7 +217,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
end
end
defp items_csv(conn, _, _, _), do: not_found(conn)
defp items_csv(conn, _, _), do: not_found(conn)
def token_transfers_csv(conn, params) do
items_csv(
@ -228,8 +228,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"to_period" => params["to_period"],
"recaptcha_response" => params["recaptcha_response"]
},
AddressTokenTransferCsvExporter,
"token_transfers.csv"
AddressTokenTransferCsvExporter
)
end
@ -247,8 +246,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"to_period" => to_period,
"recaptcha_response" => recaptcha_response
},
AddressTransactionCsvExporter,
"transactions.csv"
AddressTransactionCsvExporter
)
end
@ -266,8 +264,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"to_period" => to_period,
"recaptcha_response" => recaptcha_response
},
AddressInternalTransactionCsvExporter,
"internal_transactions.csv"
AddressInternalTransactionCsvExporter
)
end
@ -285,8 +282,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"to_period" => to_period,
"recaptcha_response" => recaptcha_response
},
AddressLogCsvExporter,
"logs.csv"
AddressLogCsvExporter
)
end
end

@ -21,10 +21,24 @@
</div>
<div id="recaptcha" class=mb-3></div>
<button id="export-csv-button" class="button button-primary" disabled style="padding: 10px 25px;" data-link=<%= address_transaction_path(@conn, type_download_path(@type), %{"address_id" => address_checksum(@address_hash_string), "from_period" => default_period_start(), "to_period" => default_period_end()}) %>><%= gettext("Download") %></button>
<button
<%= if Application.get_env(:block_scout_web, :recaptcha)[:v2_client_key], do: "disabled", else: "" %>
id="export-csv-button"
class="button button-primary"
style="padding: 10px 25px;"
data-link=<%= address_transaction_path(@conn, type_download_path(@type)) %>
data-address-hash=<%= address_checksum(@address_hash_string) %>
data-type=<%= @type %>
><%= gettext("Download") %></button>
</a>
</div>
</div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/csv-download.js") %>"></script>
<%= cond do %>
<% Application.get_env(:block_scout_web, :recaptcha)[:v2_client_key] -> %>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" defer></script>
<% Application.get_env(:block_scout_web, :recaptcha)[:v3_client_key] -> %>
<script src="https://www.google.com/recaptcha/api.js?render=<%= Application.get_env(:block_scout_web, :recaptcha)[:v3_client_key] %>" defer></script>
<% true -> %>
<% end %>
</section>

@ -63,7 +63,8 @@
<div id="indexer-first-block" class="d-none" ><%= Application.get_env(:indexer, :first_block) %></div>
<input id="js-chain-id" class="d-none" value="<%= Application.get_env(:block_scout_web, :chain_id) %>" />
<input id="js-json-rpc" class="d-none" value="<%= Application.get_env(:block_scout_web, :json_rpc) %>" />
<input id="re-captcha-client-key" class="d-none" value="<%= Application.get_env(:block_scout_web, :re_captcha_client_key) %>" />
<input id="re-captcha-client-key" class="d-none" value="<%= Application.get_env(:block_scout_web, :recaptcha)[:v2_client_key] %>" />
<input id="re-captcha-v3-client-key" class="d-none" value="<%= Application.get_env(:block_scout_web, :recaptcha)[:v3_client_key] %>" />
<input id="network-path" class="d-none" value="<%= Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] %>" />
<!-- -->
<% show_maintenance_alert = Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:show_maintenance_alert] %>

@ -30,15 +30,4 @@ defmodule BlockScoutWeb.CsvExportView do
|> Address.checksum()
end
end
defp default_period_start do
DateTime.utc_now()
|> Timex.shift(months: -1)
|> Timex.format!("{YYYY}-{0M}-{0D}")
end
defp default_period_end do
DateTime.utc_now()
|> Timex.format!("{YYYY}-{0M}-{0D}")
end
end

@ -81,7 +81,7 @@ msgstr ""
msgid ") may be added for each contract. Click the Add Library button to add an additional one."
msgstr ""
#: lib/block_scout_web/templates/layout/app.html.eex:85
#: lib/block_scout_web/templates/layout/app.html.eex:86
#, elixir-autogen, elixir-format
msgid "- We're indexing this chain right now. Some of the counts may be inaccurate."
msgstr ""
@ -1072,7 +1072,7 @@ msgstr ""
msgid "Displaying the init data provided of the creating transaction."
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:24
#: lib/block_scout_web/templates/csv_export/index.html.eex:32
#, elixir-autogen, elixir-format
msgid "Download"
msgstr ""

@ -81,7 +81,7 @@ msgstr ""
msgid ") may be added for each contract. Click the Add Library button to add an additional one."
msgstr ""
#: lib/block_scout_web/templates/layout/app.html.eex:85
#: lib/block_scout_web/templates/layout/app.html.eex:86
#, elixir-autogen, elixir-format
msgid "- We're indexing this chain right now. Some of the counts may be inaccurate."
msgstr ""
@ -1072,7 +1072,7 @@ msgstr ""
msgid "Displaying the init data provided of the creating transaction."
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:24
#: lib/block_scout_web/templates/csv_export/index.html.eex:32
#, elixir-autogen, elixir-format
msgid "Download"
msgstr ""

@ -1,27 +1,24 @@
defmodule Explorer.Chain.AddressInternalTransactionCsvExporter do
defmodule Explorer.Chain.CSVExport.AddressInternalTransactionCsvExporter do
@moduledoc """
Exports internal transactions to a csv file.
"""
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, InternalTransaction, Wei}
alias NimbleCSV.RFC4180
alias Explorer.Chain.CSVExport.Helper
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1}
@paging_options %PagingOptions{page_size: Helper.page_size() + 1}
@spec export(Address.t(), String.t(), String.t()) :: Enumerable.t()
def export(address, from_period, to_period) do
from_block = Chain.convert_date_to_min_block(from_period)
to_block = Chain.convert_date_to_max_block(to_period)
{from_block, to_block} = Helper.block_from_period(from_period, to_period)
res =
address.hash
|> fetch_all_internal_transactions(from_block, to_block, @paging_options)
|> Enum.sort_by(&{&1.block_number, &1.index, &1.transaction_index}, :desc)
|> to_csv_format()
|> dump_to_stream()
|> Helper.dump_to_stream()
res
end
@ -37,7 +34,7 @@ defmodule Explorer.Chain.AddressInternalTransactionCsvExporter do
new_acc = internal_transactions ++ acc
case Enum.split(internal_transactions, @page_size) do
case Enum.split(internal_transactions, Helper.page_size()) do
{_internal_transactions,
[%InternalTransaction{block_number: block_number, transaction_index: transaction_index, index: index}]} ->
new_paging_options = %{@paging_options | key: {block_number, transaction_index, index}}
@ -48,14 +45,6 @@ defmodule Explorer.Chain.AddressInternalTransactionCsvExporter do
end
end
defp dump_to_stream(internal_transactions) do
res =
internal_transactions
|> RFC4180.dump_to_stream()
res
end
defp to_csv_format(internal_transactions) do
row_names = [
"TxHash",

@ -1,25 +1,22 @@
defmodule Explorer.Chain.AddressLogCsvExporter do
defmodule Explorer.Chain.CSVExport.AddressLogCsvExporter do
@moduledoc """
Exports internal transactions to a csv file.
"""
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, Log, Transaction}
alias NimbleCSV.RFC4180
alias Explorer.Chain.CSVExport.Helper
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1}
@paging_options %PagingOptions{page_size: Helper.page_size() + 1}
@spec export(Address.t(), String.t(), String.t()) :: Enumerable.t()
def export(address, from_period, to_period) do
from_block = Chain.convert_date_to_min_block(from_period)
to_block = Chain.convert_date_to_max_block(to_period)
{from_block, to_block} = Helper.block_from_period(from_period, to_period)
address.hash
|> fetch_all_logs(from_block, to_block, @paging_options)
|> to_csv_format()
|> dump_to_stream()
|> Helper.dump_to_stream()
end
defp fetch_all_logs(address_hash, from_block, to_block, paging_options, acc \\ []) do
@ -33,7 +30,7 @@ defmodule Explorer.Chain.AddressLogCsvExporter do
new_acc = logs ++ acc
case Enum.split(logs, @page_size) do
case Enum.split(logs, Helper.page_size()) do
{_logs, [%Log{block_number: block_number, transaction: %Transaction{index: transaction_index}, index: index}]} ->
new_paging_options = %{@paging_options | key: {block_number, transaction_index, index}}
fetch_all_logs(address_hash, from_block, to_block, new_paging_options, new_acc)
@ -43,11 +40,6 @@ defmodule Explorer.Chain.AddressLogCsvExporter do
end
end
defp dump_to_stream(logs) do
logs
|> RFC4180.dump_to_stream()
end
defp to_csv_format(logs) do
row_names = [
"TxHash",

@ -1,24 +1,22 @@
defmodule Explorer.Chain.AddressTokenTransferCsvExporter do
defmodule Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter do
@moduledoc """
Exports token transfers to a csv file.
"""
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, TokenTransfer}
alias NimbleCSV.RFC4180
alias Explorer.Chain.CSVExport.Helper
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1, asc_order: true}
@paging_options %PagingOptions{page_size: Helper.page_size() + 1, asc_order: true}
@spec export(Address.t(), String.t(), String.t()) :: Enumerable.t()
def export(address, from_period, to_period) do
from_block = Chain.convert_date_to_min_block(from_period)
to_block = Chain.convert_date_to_max_block(to_period)
{from_block, to_block} = Helper.block_from_period(from_period, to_period)
address.hash
|> fetch_all_token_transfers(from_block, to_block, @paging_options)
|> to_csv_format(address)
|> dump_to_stream()
|> Helper.dump_to_stream()
end
def fetch_all_token_transfers(address_hash, from_block, to_block, paging_options, acc \\ []) do
@ -32,7 +30,7 @@ defmodule Explorer.Chain.AddressTokenTransferCsvExporter do
new_acc = acc ++ token_transfers
case Enum.split(token_transfers, @page_size) do
case Enum.split(token_transfers, Helper.page_size()) do
{_token_transfers, [%TokenTransfer{block_number: block_number, log_index: log_index}]} ->
new_paging_options = %{@paging_options | key: {block_number, log_index}}
fetch_all_token_transfers(address_hash, from_block, to_block, new_paging_options, new_acc)
@ -42,11 +40,6 @@ defmodule Explorer.Chain.AddressTokenTransferCsvExporter do
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",

@ -1,4 +1,4 @@
defmodule Explorer.Chain.AddressTransactionCsvExporter do
defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do
@moduledoc """
Exports transactions to a csv file.
"""
@ -11,8 +11,8 @@ defmodule Explorer.Chain.AddressTransactionCsvExporter do
alias Explorer.{Chain, Market, PagingOptions, Repo}
alias Explorer.Market.MarketHistory
alias Explorer.Chain.{Address, Transaction, Wei}
alias Explorer.Chain.CSVExport.Helper
alias Explorer.ExchangeRates.Token
alias NimbleCSV.RFC4180
@necessity_by_association [
necessity_by_association: %{
@ -27,20 +27,17 @@ defmodule Explorer.Chain.AddressTransactionCsvExporter do
}
]
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1}
@paging_options %PagingOptions{page_size: Helper.page_size() + 1}
@spec export(Address.t(), String.t(), String.t()) :: Enumerable.t()
def export(address, from_period, to_period) do
from_block = Chain.convert_date_to_min_block(from_period)
to_block = Chain.convert_date_to_max_block(to_period)
{from_block, to_block} = Helper.block_from_period(from_period, to_period)
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
address.hash
|> fetch_all_transactions(from_block, to_block, @paging_options)
|> to_csv_format(address, exchange_rate)
|> dump_to_stream()
|> Helper.dump_to_stream()
end
def fetch_all_transactions(address_hash, from_block, to_block, paging_options, acc \\ []) do
@ -53,7 +50,7 @@ defmodule Explorer.Chain.AddressTransactionCsvExporter do
transactions = Chain.address_to_transactions_without_rewards(address_hash, options)
new_acc = transactions ++ acc
case Enum.split(transactions, @page_size) do
case Enum.split(transactions, Helper.page_size()) do
{_transactions, [%Transaction{block_number: block_number, index: index}]} ->
new_paging_options = %{@paging_options | key: {block_number, index}}
fetch_all_transactions(address_hash, from_block, to_block, new_paging_options, new_acc)
@ -63,11 +60,6 @@ defmodule Explorer.Chain.AddressTransactionCsvExporter do
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",

@ -0,0 +1,29 @@
defmodule Explorer.Chain.CSVExport.Helper do
@moduledoc """
CSV export helper functions.
"""
alias Explorer.Chain
alias NimbleCSV.RFC4180
@page_size 150
def dump_to_stream(items) do
res =
items
|> RFC4180.dump_to_stream()
res
end
def page_size do
@page_size
end
def block_from_period(from_period, to_period) do
from_block = Chain.convert_date_to_min_block(from_period)
to_block = Chain.convert_date_to_max_block(to_period)
{from_block, to_block}
end
end

@ -1,7 +1,8 @@
defmodule Explorer.Chain.AddressInternalTransactionCsvExporterTest do
defmodule Explorer.Chain.CSVExport.AddressInternalTransactionCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.{AddressInternalTransactionCsvExporter, Wei}
alias Explorer.Chain.CSVExport.AddressInternalTransactionCsvExporter
alias Explorer.Chain.Wei
describe "export/3" do
test "exports address internal transactions to csv" do

@ -1,7 +1,7 @@
defmodule Explorer.Chain.AddressLogCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.AddressLogCsvExporter
alias Explorer.Chain.CSVExport.AddressLogCsvExporter
describe "export/3" do
test "exports address logs to csv" do

@ -1,7 +1,7 @@
defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.AddressTokenTransferCsvExporter
alias Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter
describe "export/3" do
test "exports token transfers to csv" do

@ -1,7 +1,8 @@
defmodule Explorer.Chain.AddressTransactionCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.{AddressTransactionCsvExporter, Wei}
alias Explorer.Chain.CSVExport.AddressTransactionCsvExporter
alias Explorer.Chain.Wei
describe "export/3" do
test "exports address transactions to csv" do

@ -23,8 +23,6 @@ config :block_scout_web,
dark_forest_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST"),
dark_forest_addresses_v_0_5: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST_V_0_5"),
circles_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_CIRCLES"),
re_captcha_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY"),
re_captcha_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY"),
new_tags: System.get_env("NEW_TAGS"),
chain_id: System.get_env("CHAIN_ID"),
json_rpc: System.get_env("JSON_RPC"),
@ -35,6 +33,12 @@ config :block_scout_web,
hide_block_miner: ConfigHelper.parse_bool_env_var("HIDE_BLOCK_MINER"),
show_tenderly_link: ConfigHelper.parse_bool_env_var("SHOW_TENDERLY_LINK")
config :block_scout_web, :recaptcha,
v2_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY"),
v2_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY"),
v3_client_key: System.get_env("RE_CAPTCHA_V3_CLIENT_KEY"),
v3_secret_key: System.get_env("RE_CAPTCHA_V3_SECRET_KEY")
network_path =
"NETWORK_PATH"
|> System.get_env("/")

@ -141,6 +141,8 @@ SHOW_TENDERLY_LINK=false
TENDERLY_CHAIN_PATH=
RE_CAPTCHA_SECRET_KEY=
RE_CAPTCHA_CLIENT_KEY=
RE_CAPTCHA_V3_SECRET_KEY=
RE_CAPTCHA_V3_CLIENT_KEY=
JSON_RPC=
#API_RATE_LIMIT_DISABLED=true
API_RATE_LIMIT_TIME_INTERVAL=1s

@ -348,6 +348,12 @@ endif
ifdef RE_CAPTCHA_CLIENT_KEY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'RE_CAPTCHA_CLIENT_KEY=$(RE_CAPTCHA_CLIENT_KEY)'
endif
ifdef RE_CAPTCHA_V3_SECRET_KEY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'RE_CAPTCHA_V3_SECRET_KEY=$(RE_CAPTCHA_V3_SECRET_KEY)'
endif
ifdef RE_CAPTCHA_V3_CLIENT_KEY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'RE_CAPTCHA_V3_CLIENT_KEY=$(RE_CAPTCHA_V3_CLIENT_KEY)'
endif
ifdef CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD
BLOCKSCOUT_CONTAINER_PARAMS += -e 'CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD=$(CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD)'
endif

Loading…
Cancel
Save