Merge pull request #2264 from poanetwork/ab-async-contract-verification

add background smart contract verification
pull/2342/head
Victor Baranov 5 years ago committed by GitHub
commit 998db6abf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .dialyzer-ignore
  2. 2
      apps/block_scout_web/assets/js/app.js
  3. 28
      apps/block_scout_web/assets/js/lib/smart_contract/new_smart_contract_form.js
  4. 144
      apps/block_scout_web/assets/js/pages/verification_form.js
  5. 14
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  6. 20
      apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex
  7. 36
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  8. 1
      apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex
  9. 8
      apps/block_scout_web/lib/block_scout_web/router.ex
  10. 26
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  11. 14
      apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex
  12. 6
      apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
  13. 77
      apps/block_scout_web/priv/gettext/default.pot
  14. 83
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  15. 9
      apps/explorer/lib/explorer/chain.ex
  16. 2
      apps/explorer/lib/explorer/chain/events/publisher.ex
  17. 4
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  18. 6
      apps/explorer/lib/explorer/chain/smart_contract.ex
  19. 12
      apps/explorer/lib/explorer/chain/smart_contract/external_library.ex
  20. 23
      apps/explorer/lib/explorer/smart_contract/publisher.ex
  21. 23
      apps/explorer/lib/explorer/smart_contract/publisher_worker.ex
  22. 1
      apps/explorer/mix.exs
  23. 13
      apps/explorer/priv/repo/migrations/20190709103104_add_external_libraries_to_smart_contracts.exs
  24. 3
      mix.lock

@ -4,4 +4,6 @@
apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:400: Function timestamp_to_datetime/1 has no local return
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer()
apps/block_scout_web/lib/block_scout_web/views/layout_view.ex:174: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t()
apps/block_scout_web/lib/block_scout_web/views/layout_view.ex:174: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t()
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The pattern 'false' can never match the type 'true'
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The test 5 == 'infinity' can never evaluate to 'true'

@ -34,6 +34,7 @@ import './pages/transactions'
import './pages/favorites'
import './pages/network-search'
import './pages/layout'
import './pages/verification_form'
import './pages/admin/tasks.js'
@ -46,7 +47,6 @@ import './lib/market_history_chart'
import './lib/pending_transactions_toggle'
import './lib/pretty_json'
import './lib/reload_button'
import './lib/smart_contract/new_smart_contract_form'
import './lib/smart_contract/read_only_functions'
import './lib/smart_contract/wei_ether_converter'
import './lib/stop_propagation'

@ -1,28 +0,0 @@
import $ from 'jquery'
$(function () {
$('.js-btn-add-contract-libraries').on('click', function () {
$('.js-smart-contract-libraries-wrapper').show()
$(this).hide()
})
$('.js-smart-contract-form-reset').on('click', function () {
$('.js-contract-library-form-group').removeClass('active')
$('.js-contract-library-form-group').first().addClass('active')
$('.js-smart-contract-libraries-wrapper').hide()
$('.js-btn-add-contract-libraries').show()
$('.js-add-contract-library-wrapper').show()
})
$('.js-btn-add-contract-library').on('click', function () {
let nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
if (nextContractLibrary) {
nextContractLibrary.addClass('active')
}
if ($('.js-contract-library-form-group.active').length === $('.js-contract-library-form-group').length) {
$('.js-add-contract-library-wrapper').hide()
}
})
})

@ -0,0 +1,144 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import { subscribeChannel } from '../socket'
import { createStore, connectElements } from '../lib/redux_helpers.js'
export const initialState = {
channelDisconnected: false,
addressHash: null,
newForm: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, {
channelDisconnected: true
})
}
case 'RECEIVED_VERIFICATION_RESULT': {
if (action.msg.verificationResult === 'ok') {
return window.location.replace(window.location.href.split('/contract_verifications')[0] + '/contracts')
} else {
return Object.assign({}, state, {
newForm: action.msg.verificationResult
})
}
}
default:
return state
}
}
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
},
'[data-page="contract-verification"]': {
render ($el, state) {
if (state.newForm) {
$el.replaceWith(state.newForm)
$('button[data-button-loading="animation"]').click(event => {
$('#loading').removeClass('d-none')
})
$(function () {
$('.js-btn-add-contract-libraries').on('click', function () {
$('.js-smart-contract-libraries-wrapper').show()
$(this).hide()
})
$('.js-smart-contract-form-reset').on('click', function () {
$('.js-contract-library-form-group').removeClass('active')
$('.js-contract-library-form-group').first().addClass('active')
$('.js-smart-contract-libraries-wrapper').hide()
$('.js-btn-add-contract-libraries').show()
$('.js-add-contract-library-wrapper').show()
})
$('.js-btn-add-contract-library').on('click', function () {
let nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
if (nextContractLibrary) {
nextContractLibrary.addClass('active')
}
if ($('.js-contract-library-form-group.active').length === $('.js-contract-library-form-group').length) {
$('.js-add-contract-library-wrapper').hide()
}
})
})
return $el
}
return $el
}
}
}
const $contractVerificationPage = $('[data-page="contract-verification"]')
if ($contractVerificationPage.length) {
const store = createStore(reducer)
const addressHash = $('#smart_contract_address_hash').val()
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
store.dispatch({
type: 'PAGE_LOAD',
addressHash,
filter,
beyondPageOne: !!blockNumber
})
connectElements({ store, elements })
const addressChannel = subscribeChannel(`addresses:${addressHash}`)
addressChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
addressChannel.on('verification', (msg) => store.dispatch({
type: 'RECEIVED_VERIFICATION_RESULT',
msg: humps.camelizeKeys(msg)
}))
$('button[data-button-loading="animation"]').click(event => {
$('#loading').removeClass('d-none')
})
$(function () {
$('.js-btn-add-contract-libraries').on('click', function () {
$('.js-smart-contract-libraries-wrapper').show()
$(this).hide()
})
$('.js-smart-contract-form-reset').on('click', function () {
$('.js-contract-library-form-group').removeClass('active')
$('.js-contract-library-form-group').first().addClass('active')
$('.js-smart-contract-libraries-wrapper').hide()
$('.js-btn-add-contract-libraries').show()
$('.js-add-contract-library-wrapper').show()
})
$('.js-btn-add-contract-library').on('click', function () {
let nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
if (nextContractLibrary) {
nextContractLibrary.addClass('active')
}
if ($('.js-contract-library-form-group.active').length === $('.js-contract-library-form-group').length) {
$('.js-add-contract-library-wrapper').hide()
}
})
})
}

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AddressChannel do
alias Explorer.ExchangeRates.Token
alias Phoenix.View
intercept(["balance_update", "coin_balance", "count", "internal_transaction", "transaction"])
intercept(["balance_update", "coin_balance", "count", "internal_transaction", "transaction", "verification_result"])
def join("addresses:" <> address_hash, _params, socket) do
{:ok, %{}, assign(socket, :address_hash, address_hash)}
@ -58,6 +58,18 @@ defmodule BlockScoutWeb.AddressChannel do
end
end
def handle_out("verification_result", result, socket) do
case result[:result] do
{:ok, _contract} ->
push(socket, "verification", %{verification_result: :ok})
{:noreply, socket}
{:error, result} ->
push(socket, "verification", %{verification_result: result})
{:noreply, socket}
end
end
def handle_out("count", %{count: count}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
use BlockScoutWeb, :controller
alias Explorer.Chain.SmartContract
alias Explorer.SmartContract.{Publisher, Solidity.CodeCompiler, Solidity.CompilerVersion}
alias Explorer.SmartContract.{PublisherWorker, Solidity.CodeCompiler, Solidity.CompilerVersion}
def new(conn, %{"address_id" => address_hash_string}) do
changeset =
@ -16,31 +16,21 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
render(conn, "new.html",
changeset: changeset,
compiler_versions: compiler_versions,
evm_versions: CodeCompiler.allowed_evm_versions()
evm_versions: CodeCompiler.allowed_evm_versions(),
address_hash: address_hash_string
)
end
def create(
conn,
%{
"address_id" => address_hash_string,
"smart_contract" => smart_contract,
"external_libraries" => external_libraries
}
) do
case Publisher.publish(address_hash_string, smart_contract, external_libraries) do
{:ok, _smart_contract} ->
redirect(conn, to: address_contract_path(conn, :index, address_hash_string))
Que.add(PublisherWorker, {smart_contract["address_hash"], smart_contract, external_libraries, conn})
{:error, changeset} ->
{:ok, compiler_versions} = CompilerVersion.fetch_versions()
render(conn, "new.html",
changeset: changeset,
compiler_versions: compiler_versions,
evm_versions: CodeCompiler.allowed_evm_versions()
)
end
send_resp(conn, 204, "")
end
def parse_optimization_runs(%{"runs" => runs}) do

@ -4,11 +4,13 @@ defmodule BlockScoutWeb.Notifier do
"""
alias Absinthe.Subscription
alias BlockScoutWeb.Endpoint
alias BlockScoutWeb.{AddressContractVerificationView, Endpoint}
alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Address, InternalTransaction, Transaction}
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion}
alias Phoenix.View
def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_with_balance_from_cache()})
@ -23,6 +25,38 @@ defmodule BlockScoutWeb.Notifier do
Enum.each(address_coin_balances, &broadcast_address_coin_balance/1)
end
def handle_event(
{:chain_event, :contract_verification_result, :on_demand, {address_hash, contract_verification_result, conn}}
) do
contract_verification_result =
case contract_verification_result do
{:ok, _} = result ->
result
{:error, changeset} ->
{:ok, compiler_versions} = CompilerVersion.fetch_versions()
result =
View.render_to_string(AddressContractVerificationView, "new.html",
changeset: changeset,
compiler_versions: compiler_versions,
evm_versions: CodeCompiler.allowed_evm_versions(),
address_hash: address_hash,
conn: conn
)
{:error, result}
end
Endpoint.broadcast(
"addresses:#{address_hash}",
"verification_result",
%{
result: contract_verification_result
}
)
end
def handle_event({:chain_event, :block_rewards, :realtime, rewards}) do
if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do
broadcast_rewards(rewards)

@ -23,6 +23,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do
Subscriber.to(:transactions, :realtime)
Subscriber.to(:addresses, :on_demand)
Subscriber.to(:address_coin_balances, :on_demand)
Subscriber.to(:contract_verification_result, :on_demand)
# Does not come from the indexer
Subscriber.to(:exchange_rate)
{:ok, []}

@ -29,6 +29,12 @@ defmodule BlockScoutWeb.Router do
resources("/verified_smart_contracts", VerifiedSmartContractController, only: [:create])
end
scope "/verify_smart_contract" do
pipe_through(:api)
post("/contract_verifications", BlockScoutWeb.AddressContractVerificationController, :create)
end
scope "/api", BlockScoutWeb.API.RPC do
pipe_through(:api)
@ -161,7 +167,7 @@ defmodule BlockScoutWeb.Router do
resources(
"/contract_verifications",
AddressContractVerificationController,
only: [:new, :create],
only: [:new],
as: :verify_contract
)

@ -52,17 +52,6 @@
<dd class="col-sm-8 col-md-10"><%= @address.smart_contract.constructor_arguments %></dd>
</dl>
<% end %>
<%= if @address.smart_contract.external_libraries do %>
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "External libraries" %></h3>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= format_smart_contract_abi(@address.smart_contract.abi) %></code>
</pre>
</div>
</section>
<% end %>
</div>
<hr/>
<section>
@ -109,7 +98,7 @@
</div>
<% {:ok, contract_code} -> %>
<div class="d-flex justify-content-between align-items-baseline">
<h2 class="card-title"><%= gettext "Contract Byte Code" %></h2>
<h3><%= gettext "Contract Byte Code" %></h3>
<button type="button" class="btn-line" id="button" data-clipboard-text="<%= contract_code %>" aria-label="copy contract creation code">
<%= gettext "Copy Contract Byte Code" %>
</button>
@ -120,6 +109,19 @@
<% end %>
</section>
<%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<%= if @address.smart_contract.external_libraries && @address.smart_contract.external_libraries != [] do %>
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "External libraries" %></h3>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= format_external_libraries(@address.smart_contract.external_libraries) %></code>
</pre>
</div>
</section>
<% end %>
<% end %>
</div>
</div>
</section>

@ -1,9 +1,15 @@
<section class="container new-smart-contract-container">
<section data-page="contract-verification" class="container new-smart-contract-container">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost" %></a>
</div>
</div>
<div class="new-smart-contract-form">
<h1 class="smart-contract-title"><%= gettext "New Smart Contract Verification" %></h1>
<%= form_for @changeset,
address_verify_contract_path(@conn, :create, @conn.params["address_id"]),
address_contract_verification_path(@conn, :create),
[],
fn f -> %>
@ -240,13 +246,13 @@
</span>
<%= gettext("Loading....") %>
</button>
<%= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-loading": "animation" %>
<%= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation" %>
<%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %>
<%=
link(
gettext("Cancel"),
class: "btn-no-border",
to: address_contract_path(@conn, :index, @conn.params["address_id"])
to: address_contract_path(@conn, :index, @address_hash)
)
%>
</div>

@ -21,6 +21,12 @@ defmodule BlockScoutWeb.AddressContractView do
def format_optimization_text(true), do: gettext("true")
def format_optimization_text(false), do: gettext("false")
def format_external_libraries(libraries) do
Enum.reduce(libraries, "", fn %{name: name, address_hash: address_hash}, acc ->
acc <> name <> " : " <> address_hash <> "\n"
end)
end
def contract_lines_with_index(contract_source_code) do
contract_lines = String.split(contract_source_code, "\n")

@ -197,7 +197,7 @@ msgid "Blocks Validated"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:247
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:253
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47
msgid "Cancel"
msgstr ""
@ -225,7 +225,7 @@ msgid "Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:34
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40
msgid "Compiler"
msgstr ""
@ -258,12 +258,12 @@ msgid "Connection Lost, click to load newer validations"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:82
#: lib/block_scout_web/templates/address_contract/index.html.eex:71
msgid "Contract ABI"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:12
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18
#: lib/block_scout_web/views/address_view.ex:97
msgid "Contract Address"
msgstr ""
@ -286,7 +286,7 @@ msgid "Contract Creation"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:23
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:29
msgid "Contract Name"
msgstr ""
@ -296,7 +296,7 @@ msgid "Contract name:"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:70
#: lib/block_scout_web/templates/address_contract/index.html.eex:59
msgid "Contract source code"
msgstr ""
@ -560,7 +560,7 @@ msgid "Name"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:61
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:67
msgid "No"
msgstr ""
@ -651,7 +651,7 @@ msgid "Request URL"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:244
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:250
msgid "Reset"
msgstr ""
@ -939,7 +939,7 @@ msgid "Verify & Publish"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:243
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:249
msgid "Verify & publish"
msgstr ""
@ -989,7 +989,7 @@ msgid "Wei"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:66
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:72
msgid "Yes"
msgstr ""
@ -1062,7 +1062,7 @@ msgid "Loading..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:241
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:247
msgid "Loading...."
msgstr ""
@ -1405,17 +1405,17 @@ msgid "Support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:84
#: lib/block_scout_web/templates/address_contract/index.html.eex:73
msgid "Copy ABI"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:100
#: lib/block_scout_web/templates/address_contract/index.html.eex:89
msgid "Copy Contract Creation Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:72
#: lib/block_scout_web/templates/address_contract/index.html.eex:61
msgid "Copy Source Code"
msgstr ""
@ -1425,7 +1425,7 @@ msgid "Genesis Block"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:112
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:118
msgid "Contract Libraries"
msgstr ""
@ -1488,7 +1488,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:35
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:45
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:51
msgid "EVM Version"
msgstr ""
@ -1519,7 +1519,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:45
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:83
msgid "Optimization runs"
msgstr ""
@ -1585,27 +1585,27 @@ msgid "Block Details"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:112
#: lib/block_scout_web/templates/address_contract/index.html.eex:101
msgid "Contract Byte Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:98
#: lib/block_scout_web/templates/address_contract/index.html.eex:87
msgid "Contract Creation Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:104
#: lib/block_scout_web/templates/address_contract/index.html.eex:93
msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:114
#: lib/block_scout_web/templates/address_contract/index.html.eex:103
msgid "Copy Contract Byte Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:105
#: lib/block_scout_web/templates/address_contract/index.html.eex:94
msgid "Displaying the init data provided of the creating transaction."
msgstr ""
@ -1655,35 +1655,35 @@ msgid "Topic"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:98
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:104
msgid "ABI-encoded Constructor Arguments (if required by the contract)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:93
msgid "Enter the Solidity Contract Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:127
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:149
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:171
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:193
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:215
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:133
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:155
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:177
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:199
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:221
msgid "Library Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:117
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:139
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:161
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:183
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:205
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:123
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:145
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:167
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:189
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:211
msgid "Library Name"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:3
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:9
msgid "New Smart Contract Verification"
msgstr ""
@ -1769,7 +1769,7 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:58
#: lib/block_scout_web/templates/address_contract/index.html.eex:116
msgid "External libraries"
msgstr ""
@ -1797,3 +1797,8 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:12
msgid "Use the search box to find a hosted network, or select from the list of available networks below."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:4
msgid "Connection Lost"
msgstr ""

@ -197,7 +197,7 @@ msgid "Blocks Validated"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:247
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:253
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47
msgid "Cancel"
msgstr ""
@ -225,7 +225,7 @@ msgid "Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:34
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40
msgid "Compiler"
msgstr ""
@ -258,12 +258,12 @@ msgid "Connection Lost, click to load newer validations"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:82
#: lib/block_scout_web/templates/address_contract/index.html.eex:71
msgid "Contract ABI"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:12
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18
#: lib/block_scout_web/views/address_view.ex:97
msgid "Contract Address"
msgstr ""
@ -286,7 +286,7 @@ msgid "Contract Creation"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:23
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:29
msgid "Contract Name"
msgstr ""
@ -296,7 +296,7 @@ msgid "Contract name:"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:70
#: lib/block_scout_web/templates/address_contract/index.html.eex:59
msgid "Contract source code"
msgstr ""
@ -560,7 +560,7 @@ msgid "Name"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:61
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:67
msgid "No"
msgstr ""
@ -651,7 +651,7 @@ msgid "Request URL"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:244
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:250
msgid "Reset"
msgstr ""
@ -674,7 +674,7 @@ msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:28
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:30
msgid "Search tokens"
msgstr ""
@ -877,7 +877,7 @@ msgid "Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:19
#: lib/block_scout_web/templates/address/_tile.html.eex:31
msgid "Transactions sent"
msgstr ""
@ -923,7 +923,7 @@ msgid "Validated Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:23
#: lib/block_scout_web/templates/address/_tile.html.eex:35
msgid "Validations"
msgstr ""
@ -939,7 +939,7 @@ msgid "Verify & Publish"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:243
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:249
msgid "Verify & publish"
msgstr ""
@ -989,7 +989,7 @@ msgid "Wei"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:66
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:72
msgid "Yes"
msgstr ""
@ -1062,7 +1062,7 @@ msgid "Loading..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:241
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:247
msgid "Loading...."
msgstr ""
@ -1405,17 +1405,17 @@ msgid "Support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:84
#: lib/block_scout_web/templates/address_contract/index.html.eex:73
msgid "Copy ABI"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:100
#: lib/block_scout_web/templates/address_contract/index.html.eex:89
msgid "Copy Contract Creation Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:72
#: lib/block_scout_web/templates/address_contract/index.html.eex:61
msgid "Copy Source Code"
msgstr ""
@ -1425,7 +1425,7 @@ msgid "Genesis Block"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:112
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:118
msgid "Contract Libraries"
msgstr ""
@ -1488,7 +1488,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:35
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:45
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:51
msgid "EVM Version"
msgstr ""
@ -1519,7 +1519,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:45
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:83
msgid "Optimization runs"
msgstr ""
@ -1585,27 +1585,27 @@ msgid "Block Details"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:112
#: lib/block_scout_web/templates/address_contract/index.html.eex:101
msgid "Contract Byte Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:98
#: lib/block_scout_web/templates/address_contract/index.html.eex:87
msgid "Contract Creation Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:104
#: lib/block_scout_web/templates/address_contract/index.html.eex:93
msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:114
#: lib/block_scout_web/templates/address_contract/index.html.eex:103
msgid "Copy Contract Byte Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:105
#: lib/block_scout_web/templates/address_contract/index.html.eex:94
msgid "Displaying the init data provided of the creating transaction."
msgstr ""
@ -1655,35 +1655,35 @@ msgid "Topic"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:98
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:104
msgid "ABI-encoded Constructor Arguments (if required by the contract)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:93
msgid "Enter the Solidity Contract Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:127
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:149
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:171
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:193
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:215
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:133
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:155
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:177
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:199
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:221
msgid "Library Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:117
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:139
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:161
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:183
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:205
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:123
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:145
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:167
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:189
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:211
msgid "Library Name"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:3
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:9
msgid "New Smart Contract Verification"
msgstr ""
@ -1769,7 +1769,7 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:58
#: lib/block_scout_web/templates/address_contract/index.html.eex:116
msgid "External libraries"
msgstr ""
@ -1797,3 +1797,8 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_network_selector.html.eex:12
msgid "Use the search box to find a hosted network, or select from the list of available networks below."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:4
msgid "Connection Lost"
msgstr ""

@ -2426,8 +2426,13 @@ defmodule Explorer.Chain do
naming the address for reference.
"""
@spec create_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()}
def create_smart_contract(attrs \\ %{}) do
smart_contract_changeset = SmartContract.changeset(%SmartContract{}, attrs)
def create_smart_contract(attrs \\ %{}, external_libraries \\ []) do
new_contract = %SmartContract{}
smart_contract_changeset =
new_contract
|> SmartContract.changeset(attrs)
|> Changeset.put_change(:external_libraries, external_libraries)
insert_result =
Multi.new()

@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Publisher do
Publishes events related to the Chain context.
"""
@allowed_events ~w(addresses address_coin_balances blocks block_rewards internal_transactions token_transfers transactions)a
@allowed_events ~w(addresses address_coin_balances blocks block_rewards internal_transactions token_transfers transactions contract_verification_result)a
def broadcast(_data, false), do: :ok

@ -3,9 +3,9 @@ defmodule Explorer.Chain.Events.Subscriber do
Subscribes to events related to the Chain context.
"""
@allowed_broadcast_events ~w(addresses address_coin_balances blocks block_rewards internal_transactions token_transfers transactions)a
@allowed_broadcast_events ~w(addresses address_coin_balances blocks block_rewards internal_transactions token_transfers transactions contract_verification_result)a
@allowed_broadcast_types ~w(catchup realtime on_demand)a
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a
@allowed_events ~w(exchange_rate)a

@ -13,6 +13,7 @@ defmodule Explorer.Chain.SmartContract do
use Explorer.Schema
alias Explorer.Chain.{Address, ContractMethod, DecompiledSmartContract, Hash}
alias Explorer.Chain.SmartContract.ExternalLibrary
alias Explorer.Repo
@typedoc """
@ -212,7 +213,7 @@ defmodule Explorer.Chain.SmartContract do
field(:constructor_arguments, :string)
field(:evm_version, :string)
field(:optimization_runs, :integer)
field(:external_libraries, :map)
embeds_many(:external_libraries, ExternalLibrary)
field(:abi, {:array, :map})
has_many(
@ -247,8 +248,7 @@ defmodule Explorer.Chain.SmartContract do
:abi,
:constructor_arguments,
:evm_version,
:optimization_runs,
:external_libraries
:optimization_runs
])
|> validate_required([:name, :compiler_version, :optimization, :contract_source_code, :abi, :address_hash])
|> unique_constraint(:address_hash)

@ -0,0 +1,12 @@
defmodule Explorer.Chain.SmartContract.ExternalLibrary do
@moduledoc """
The representation of an external library that was used for a smart contract.
"""
use Ecto.Schema
embedded_schema do
field(:name)
field(:address_hash)
end
end

@ -28,17 +28,17 @@ defmodule Explorer.SmartContract.Publisher do
case Verifier.evaluate_authenticity(address_hash, params_with_external_libaries) do
{:ok, %{abi: abi}} ->
publish_smart_contract(address_hash, params, abi)
publish_smart_contract(address_hash, params_with_external_libaries, abi)
{:error, error} ->
{:error, unverified_smart_contract(address_hash, params, error)}
{:error, unverified_smart_contract(address_hash, params_with_external_libaries, error)}
end
end
defp publish_smart_contract(address_hash, params, abi) do
address_hash
|> attributes(params, abi)
|> Chain.create_smart_contract()
attrs = address_hash |> attributes(params, abi)
Chain.create_smart_contract(attrs, attrs.external_libraries)
end
defp unverified_smart_contract(address_hash, params, error) do
@ -64,6 +64,8 @@ defmodule Explorer.SmartContract.Publisher do
nil
end
prepared_external_libraries = prepare_external_libraies(params["external_libraries"])
%{
address_hash: address_hash,
name: params["name"],
@ -73,11 +75,20 @@ defmodule Explorer.SmartContract.Publisher do
optimization: params["optimization"],
contract_source_code: params["contract_source_code"],
constructor_arguments: clean_constructor_arguments,
external_libaries: params["external_libraries"],
external_libraries: prepared_external_libraries,
abi: abi
}
end
defp prepare_external_libraies(nil), do: []
defp prepare_external_libraies(map) do
map
|> Enum.map(fn {key, value} ->
%{name: key, address_hash: value}
end)
end
defp add_external_libraries(params, external_libraries) do
clean_external_libraries =
Enum.reduce(1..5, %{}, fn number, acc ->

@ -0,0 +1,23 @@
defmodule Explorer.SmartContract.PublisherWorker do
@moduledoc """
Background smart contract verification worker.
"""
use Que.Worker, concurrency: 5
alias Explorer.Chain.Events.Publisher, as: EventsPublisher
alias Explorer.SmartContract.Publisher
def perform({address_hash, params, external_libraries, conn}) do
result =
case Publisher.publish(address_hash, params, external_libraries) do
{:ok, _contract} = result ->
result
{:error, changeset} ->
{:error, changeset}
end
EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result, conn}}], :on_demand)
end
end

@ -106,6 +106,7 @@ defmodule Explorer.Mixfile do
},
# bypass optional dependency
{:plug_cowboy, "~> 2.0", only: [:dev, :test]},
{:que, "~> 0.10.1"},
{:sobelow, ">= 0.7.0", only: [:dev, :test], runtime: false},
# Tracing
{:spandex, github: "spandex-project/spandex", branch: "allow-setting-trace-key", override: true},

@ -0,0 +1,13 @@
defmodule Explorer.Repo.Migrations.AddExternalLibrariesToSmartContracts do
use Ecto.Migration
def change do
alter table(:smart_contracts) do
remove(:external_libraries)
end
alter table(:smart_contracts) do
add(:external_libraries, {:array, :map}, default: [])
end
end
end

@ -42,6 +42,7 @@
"ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.2", "d84217a6fb7840ff771d2561b8aa6d74a0d8968e4b10ecc0d7e9890dc8fb1c6a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
"ex_rlp": {:hex, :ex_rlp, "0.5.2", "7f4ce7bd55e543c054ce6d49629b01e9833c3462e3d547952be89865f39f2c58", [:mix], [], "hexpm"},
"ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], []},
"excoveralls": {:git, "https://github.com/KronicDeth/excoveralls.git", "0a859b68851eeba9b43eba59fbc8f9098299cfe1", [branch: "circle-workflows"]},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
@ -66,6 +67,7 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"math": {:hex, :math, "0.3.0", "e14e7291115201cb155a3567e66d196bf5088a6f55b030d598107d7ae934a11c", [:mix], []},
"meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm"},
"memento": {:hex, :memento, "0.3.1", "b2909390820550d8b90b68ec96f9e15ff8a45a28b6f97fa4a62ef50e87c2f9d9", [:mix], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
@ -98,6 +100,7 @@
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"},
"prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
"qrcode": {:hex, :qrcode, "0.1.4", "544dc67ba42eed5ebce3d2a691d053387937740561d251f83f0a067917fae2dc", [:mix], [], "hexpm"},
"que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"set_locale": {:git, "https://github.com/minifast/set_locale.git", "da9ae029642bc0fbd9212c2aaf86c0adca70c084", [branch: "master"]},
"sobelow": {:hex, :sobelow, "0.7.4", "228cc6185b448b63ecc88429b43e864e8dd570e8e09f2d04b3aa71894db1bdbb", [:mix], [], "hexpm"},

Loading…
Cancel
Save