Merge pull request #182 from poanetwork/ams-verify-contract
Developer verifies a Smart Contractpull/269/head
commit
c32fcda0e9
@ -0,0 +1,54 @@ |
|||||||
|
defmodule Explorer.Chain.SmartContract do |
||||||
|
@moduledoc """ |
||||||
|
The representation of a verified Smart Contract. |
||||||
|
|
||||||
|
"A contract in the sense of Solidity is a collection of code (its functions) |
||||||
|
and data (its state) that resides at a specific address on the Ethereum |
||||||
|
blockchain." |
||||||
|
http://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.Chain.{Address, Hash} |
||||||
|
|
||||||
|
use Explorer.Schema |
||||||
|
|
||||||
|
@type t :: %Explorer.Chain.SmartContract{ |
||||||
|
name: String.t(), |
||||||
|
compiler_version: String.t(), |
||||||
|
optimization: boolean, |
||||||
|
contract_source_code: String.t(), |
||||||
|
abi: {:array, :map} |
||||||
|
} |
||||||
|
|
||||||
|
schema "smart_contracts" do |
||||||
|
field(:name, :string) |
||||||
|
field(:compiler_version, :string) |
||||||
|
field(:optimization, :boolean) |
||||||
|
field(:contract_source_code, :string) |
||||||
|
field(:abi, {:array, :map}) |
||||||
|
|
||||||
|
belongs_to( |
||||||
|
:address, |
||||||
|
Address, |
||||||
|
foreign_key: :address_hash, |
||||||
|
references: :hash, |
||||||
|
type: Hash.Truncated |
||||||
|
) |
||||||
|
|
||||||
|
timestamps() |
||||||
|
end |
||||||
|
|
||||||
|
def changeset(%__MODULE__{} = smart_contract, attrs) do |
||||||
|
smart_contract |
||||||
|
|> cast(attrs, [:name, :compiler_version, :optimization, :contract_source_code, :address_hash, :abi]) |
||||||
|
|> validate_required([:name, :compiler_version, :optimization, :contract_source_code, :abi, :address_hash]) |
||||||
|
|> unique_constraint(:address_hash) |
||||||
|
end |
||||||
|
|
||||||
|
def invalid_contract_changeset(%__MODULE__{} = smart_contract, attrs) do |
||||||
|
smart_contract |
||||||
|
|> cast(attrs, [:name, :compiler_version, :optimization, :contract_source_code, :address_hash]) |
||||||
|
|> validate_required([:name, :compiler_version, :optimization, :address_hash]) |
||||||
|
|> add_error(:contract_source_code, "there was an error validating your contract, please try again.") |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,64 @@ |
|||||||
|
defmodule Explorer.SmartContract.Publisher do |
||||||
|
@moduledoc """ |
||||||
|
Module responsible to control the contract verification. |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.SmartContract |
||||||
|
alias Explorer.SmartContract.Verifier |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Evaluates smart contract authenticity and saves its details. |
||||||
|
|
||||||
|
## Examples |
||||||
|
Explorer.SmartContract.Publisher.publish( |
||||||
|
"0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", |
||||||
|
%{ |
||||||
|
"compiler" => "0.4.24", |
||||||
|
"contract_source_code" => "pragma solidity ^0.4.24; contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public constant returns (uint) { return storedData; } }", |
||||||
|
"name" => "SimpleStorage", |
||||||
|
"optimization" => false |
||||||
|
} |
||||||
|
) |
||||||
|
#=> {:ok, %Explorer.Chain.SmartContract{}} |
||||||
|
|
||||||
|
""" |
||||||
|
def publish(address_hash, params) do |
||||||
|
case Verifier.evaluate_authenticity(address_hash, params) do |
||||||
|
{:ok, %{abi: abi}} -> |
||||||
|
publish_smart_contract(address_hash, params, abi) |
||||||
|
|
||||||
|
{:error, _} -> |
||||||
|
{:error, unverified_smart_contract(address_hash, params)} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp publish_smart_contract(address_hash, params, abi) do |
||||||
|
address_hash |
||||||
|
|> attributes(params, abi) |
||||||
|
|> Chain.create_smart_contract() |
||||||
|
end |
||||||
|
|
||||||
|
defp unverified_smart_contract(address_hash, params) do |
||||||
|
attrs = attributes(address_hash, params) |
||||||
|
|
||||||
|
changeset = |
||||||
|
SmartContract.invalid_contract_changeset( |
||||||
|
%SmartContract{address_hash: address_hash}, |
||||||
|
attrs |
||||||
|
) |
||||||
|
|
||||||
|
%{changeset | action: :insert} |
||||||
|
end |
||||||
|
|
||||||
|
defp attributes(address_hash, params, abi \\ %{}) do |
||||||
|
%{ |
||||||
|
address_hash: address_hash, |
||||||
|
name: params["name"], |
||||||
|
compiler_version: params["compiler"], |
||||||
|
optimization: params["optimization"], |
||||||
|
contract_source_code: params["contract_source_code"], |
||||||
|
abi: abi |
||||||
|
} |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,104 @@ |
|||||||
|
defmodule Explorer.SmartContract.Solidity.CodeCompiler do |
||||||
|
@moduledoc """ |
||||||
|
Module responsible to compile the Solidity code of a given Smart Contract. |
||||||
|
""" |
||||||
|
|
||||||
|
@doc ~S""" |
||||||
|
Compiles a code in the solidity command line. |
||||||
|
|
||||||
|
Returns a `Map`. |
||||||
|
|
||||||
|
## Examples |
||||||
|
iex(1)> Explorer.SmartContract.Solidity.CodeCompiler.run("SimpleStorage", "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", false) |
||||||
|
%{ |
||||||
|
"contracts" => %{ |
||||||
|
"SimpleStorage" => %{ |
||||||
|
"SimpleStorage" => %{ |
||||||
|
"abi" => [ |
||||||
|
%{ |
||||||
|
"constant" => false, |
||||||
|
"inputs" => [%{"name" => "x", "type" => "uint256"}], |
||||||
|
"name" => "set", |
||||||
|
"outputs" => [], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "nonpayable", |
||||||
|
"type" => "function" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
"constant" => true, |
||||||
|
"inputs" => [], |
||||||
|
"name" => "get", |
||||||
|
"outputs" => [%{"name" => "", "type" => "uint256"}], |
||||||
|
"payable" => false, |
||||||
|
"stateMutability" => "view", |
||||||
|
"type" => "function" |
||||||
|
} |
||||||
|
], |
||||||
|
"evm" => %{ |
||||||
|
"bytecode" => %{ |
||||||
|
"linkReferences" => %{}, |
||||||
|
"object" => "608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820017172d01c000255d5c74c0efce764adf7c4ae444d7f7e2ed852f6fb9b73df5d0029", |
||||||
|
"opcodes" => "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xDF DUP1 PUSH2 0x1F PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x49 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x60FE47B1 EQ PUSH1 0x4E JUMPI DUP1 PUSH4 0x6D4CE63C EQ PUSH1 0x78 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x59 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x76 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0xA0 JUMP JUMPDEST STOP JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x83 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x8A PUSH1 0xAA JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 ADD PUSH18 0x72D01C000255D5C74C0EFCE764ADF7C4AE44 0x4d PUSH32 0x7E2ED852F6FB9B73DF5D00290000000000000000000000000000000000000000 ", |
||||||
|
"sourceMap" => "25:157:0:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;25:157:0;;;;;;;" |
||||||
|
}, |
||||||
|
"deployedBytecode" => %{ |
||||||
|
"linkReferences" => %{}, |
||||||
|
"object" => "6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820017172d01c000255d5c74c0efce764adf7c4ae444d7f7e2ed852f6fb9b73df5d0029", |
||||||
|
"opcodes" => "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x49 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x60FE47B1 EQ PUSH1 0x4E JUMPI DUP1 PUSH4 0x6D4CE63C EQ PUSH1 0x78 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x59 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x76 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0xA0 JUMP JUMPDEST STOP JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x83 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x8A PUSH1 0xAA JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 ADD PUSH18 0x72D01C000255D5C74C0EFCE764ADF7C4AE44 0x4d PUSH32 0x7E2ED852F6FB9B73DF5D00290000000000000000000000000000000000000000 ", |
||||||
|
"sourceMap" => "25:157:0:-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;66:46;;8:9:-1;5:2;;;30:1;27;20:12;5:2;66:46:0;;;;;;;;;;;;;;;;;;;;;;;;;;113:67;;8:9:-1;5:2;;;30:1;27;20:12;5:2;113:67:0;;;;;;;;;;;;;;;;;;;;;;;66:46;108:1;95:10;:14;;;;66:46;:::o;113:67::-;153:4;167:10;;160:17;;113:67;:::o" |
||||||
|
}, |
||||||
|
"gasEstimates" => %{ |
||||||
|
"creation" => %{ |
||||||
|
"codeDepositCost" => "44600", |
||||||
|
"executionCost" => "93", |
||||||
|
"totalCost" => "44693" |
||||||
|
}, |
||||||
|
"external" => %{"get()" => "424", "set(uint256)" => "20205"} |
||||||
|
} |
||||||
|
}, |
||||||
|
"metadata" => "{\"compiler\":{\"version\":\"0.4.24+commit.e67f0147\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"constant\":false,\"inputs\":[{\"name\":\"x\",\"type\":\"uint256\"}],\"name\":\"set\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"get\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"methods\":{}},\"userdoc\":{\"methods\":{}}},\"settings\":{\"compilationTarget\":{\"SimpleStorage\":\"SimpleStorage\"},\"evmVersion\":\"byzantium\",\"libraries\":{},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"SimpleStorage\":{\"keccak256\":\"0x3f5ecc4c6077dffdfc98d9781295205833bf0558bc2a0c86fc3d5f246808ba34\",\"urls\":[\"bzzr://60d588b13340f26a038f51934f9b8c3cf3928372bc4358848a86960040a3a8e2\"]}},\"version\":1}" |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"sources" => %{"SimpleStorage" => %{"id" => 0}} |
||||||
|
} |
||||||
|
|
||||||
|
""" |
||||||
|
def run(name, code, optimization) do |
||||||
|
{response, _status} = |
||||||
|
System.cmd( |
||||||
|
Application.app_dir(:explorer, "priv/solc.bash"), |
||||||
|
[generate_settings(name, code, optimization)] |
||||||
|
) |
||||||
|
|
||||||
|
Jason.decode!(response) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
For more output options check the documentation. |
||||||
|
https://solidity.readthedocs.io/en/v0.4.24/using-the-compiler.html#compiler-input-and-output-json-description |
||||||
|
""" |
||||||
|
def generate_settings(name, code, optimization) do |
||||||
|
""" |
||||||
|
{ |
||||||
|
"language": "Solidity", |
||||||
|
"sources": { |
||||||
|
"#{name}": |
||||||
|
{ |
||||||
|
"content": "#{code}" |
||||||
|
} |
||||||
|
}, |
||||||
|
"settings": { |
||||||
|
"optimizer": { |
||||||
|
"enabled": #{optimization} |
||||||
|
}, |
||||||
|
"outputSelection": { |
||||||
|
"*": { |
||||||
|
"*": [ "evm.bytecode", "evm.deployedBytecode", "evm.gasEstimates", "abi", "metadata" ] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
""" |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,47 @@ |
|||||||
|
defmodule Explorer.SmartContract.Solidity.CompilerVersion do |
||||||
|
@moduledoc """ |
||||||
|
Adapter for fetching compiler versions from https://solc-bin.ethereum.org/bin/list.json. |
||||||
|
""" |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetches a list of compilers from the Ethereum Solidity API. |
||||||
|
""" |
||||||
|
@spec fetch_versions :: {atom, [map]} |
||||||
|
def fetch_versions do |
||||||
|
headers = [{"Content-Type", "application/json"}] |
||||||
|
|
||||||
|
case HTTPoison.get(source_url(), headers) do |
||||||
|
{:ok, %{status_code: 200, body: body}} -> |
||||||
|
{:ok, format_data(body)} |
||||||
|
|
||||||
|
{:ok, %{status_code: _status_code, body: body}} -> |
||||||
|
{:error, decode_json(body)["error"]} |
||||||
|
|
||||||
|
{:error, %{reason: reason}} -> |
||||||
|
{:error, reason} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp format_data(json) do |
||||||
|
{:ok, releases} = |
||||||
|
json |
||||||
|
|> Jason.decode!() |
||||||
|
|> Map.fetch("releases") |
||||||
|
|
||||||
|
releases |
||||||
|
|> Map.to_list() |
||||||
|
|> Enum.map(fn {key, _value} -> {key, key} end) |
||||||
|
|> Enum.sort() |
||||||
|
|> Enum.reverse() |
||||||
|
end |
||||||
|
|
||||||
|
defp decode_json(json) do |
||||||
|
Jason.decode!(json) |
||||||
|
end |
||||||
|
|
||||||
|
defp source_url do |
||||||
|
solc_bin_api_url = Application.get_env(:explorer, :solc_bin_api_url) |
||||||
|
|
||||||
|
"#{solc_bin_api_url}/bin/list.json" |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,74 @@ |
|||||||
|
defmodule Explorer.SmartContract.Verifier do |
||||||
|
@moduledoc """ |
||||||
|
Module responsible to verify the Smart Contract. |
||||||
|
|
||||||
|
Given a contract source code the bytecode will be generated and matched |
||||||
|
against the existing Creation Address Bytecode, if it matches the contract is |
||||||
|
then Verified. |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.SmartContract.Solidity.CodeCompiler |
||||||
|
|
||||||
|
def evaluate_authenticity(_, %{"name" => ""}), do: {:error, :name} |
||||||
|
def evaluate_authenticity(_, %{"contract_source_code" => ""}), do: {:error, :contract_source_code} |
||||||
|
|
||||||
|
def evaluate_authenticity(address_hash, %{ |
||||||
|
"name" => name, |
||||||
|
"contract_source_code" => contract_source_code, |
||||||
|
"optimization" => optimization |
||||||
|
}) do |
||||||
|
solc_output = CodeCompiler.run(name, contract_source_code, optimization) |
||||||
|
|
||||||
|
case solc_output do |
||||||
|
%{ |
||||||
|
"contracts" => %{ |
||||||
|
^name => %{ |
||||||
|
^name => %{ |
||||||
|
"abi" => abi, |
||||||
|
"evm" => %{ |
||||||
|
"bytecode" => %{"object" => bytecode} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} -> |
||||||
|
compare_bytecodes(address_hash, abi, bytecode) |
||||||
|
|
||||||
|
_ -> |
||||||
|
{:error, :compilation} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
In order to discover the bytecode we need to remove the `swarm source` from |
||||||
|
the hash. |
||||||
|
|
||||||
|
`64` characters to the left of `0029` are the `swarm source`. The rest on |
||||||
|
the left is the `bytecode` to be validated. |
||||||
|
""" |
||||||
|
def extract_bytecode(code) do |
||||||
|
{bytecode, _swarm_source} = |
||||||
|
code |
||||||
|
|> String.split("0029") |
||||||
|
|> List.first() |
||||||
|
|> String.split_at(-64) |
||||||
|
|
||||||
|
bytecode |
||||||
|
end |
||||||
|
|
||||||
|
defp compare_bytecodes(address_hash, abi, bytecode) do |
||||||
|
generated_bytecode = extract_bytecode(bytecode) |
||||||
|
|
||||||
|
"0x" <> blockchain_bytecode = |
||||||
|
address_hash |
||||||
|
|> Chain.smart_contract_bytecode() |
||||||
|
|> extract_bytecode |
||||||
|
|
||||||
|
if generated_bytecode == blockchain_bytecode do |
||||||
|
{:ok, %{abi: abi}} |
||||||
|
else |
||||||
|
{:error, :generated_bytecode} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,19 @@ |
|||||||
|
defmodule Explorer.Repo.Migrations.CreateSmartContractVerified do |
||||||
|
use Ecto.Migration |
||||||
|
|
||||||
|
def change do |
||||||
|
create table(:smart_contracts) do |
||||||
|
add(:name, :string, null: false) |
||||||
|
add(:compiler_version, :string, null: false) |
||||||
|
add(:optimization, :boolean, null: false) |
||||||
|
add(:contract_source_code, :text, null: false) |
||||||
|
add(:abi, :jsonb, null: false) |
||||||
|
|
||||||
|
add(:address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false) |
||||||
|
|
||||||
|
timestamps() |
||||||
|
end |
||||||
|
|
||||||
|
create(unique_index(:smart_contracts, :address_hash)) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,3 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
echo "$1" | solc --standard-json |
@ -0,0 +1,57 @@ |
|||||||
|
defmodule Explorer.SmartContract.PublisherTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
doctest Explorer.SmartContract.Publisher |
||||||
|
|
||||||
|
alias Explorer.Chain.{SmartContract, Hash} |
||||||
|
alias Explorer.SmartContract.Publisher |
||||||
|
|
||||||
|
describe "publish/2" do |
||||||
|
test "with valid data creates a smart_contract" do |
||||||
|
address_hash = "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" |
||||||
|
|
||||||
|
smart_contract_bytecode = |
||||||
|
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" |
||||||
|
|
||||||
|
created_contract_address = insert(:address, hash: address_hash, contract_code: smart_contract_bytecode) |
||||||
|
|
||||||
|
insert( |
||||||
|
:internal_transaction, |
||||||
|
index: 0, |
||||||
|
created_contract_address_hash: created_contract_address.hash, |
||||||
|
created_contract_code: smart_contract_bytecode |
||||||
|
) |
||||||
|
|
||||||
|
valid_attrs = %{ |
||||||
|
"contract_source_code" => |
||||||
|
"pragma solidity ^0.4.23;\r\n\r\ncontract SimpleStorage {\r\n uint storedData;\r\n\r\n function set(uint x) public {\r\n storedData = x;\r\n }\r\n\r\n function get() public constant returns (uint) {\r\n return storedData;\r\n }\r\n}", |
||||||
|
"compiler" => "0.4.24", |
||||||
|
"name" => "SimpleStorage", |
||||||
|
"optimization" => false |
||||||
|
} |
||||||
|
|
||||||
|
assert {:ok, %SmartContract{} = smart_contract} = Publisher.publish(address_hash, valid_attrs) |
||||||
|
assert smart_contract.name == valid_attrs["name"] |
||||||
|
assert Hash.to_string(smart_contract.address_hash) == address_hash |
||||||
|
assert smart_contract.compiler_version == valid_attrs["compiler"] |
||||||
|
assert smart_contract.optimization == valid_attrs["optimization"] |
||||||
|
assert smart_contract.contract_source_code == valid_attrs["contract_source_code"] |
||||||
|
assert smart_contract.abi != nil |
||||||
|
end |
||||||
|
|
||||||
|
test "with invalid data returns error changeset" do |
||||||
|
address_hash = "" |
||||||
|
|
||||||
|
invalid_attrs = %{ |
||||||
|
"contract_source_code" => "", |
||||||
|
"compiler" => "", |
||||||
|
"name" => "", |
||||||
|
"optimization" => "" |
||||||
|
} |
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Publisher.publish(address_hash, invalid_attrs) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,73 @@ |
|||||||
|
defmodule Explorer.SmartContract.Solidity.CodeCompilerTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
|
||||||
|
doctest Explorer.SmartContract.Solidity.CodeCompiler |
||||||
|
|
||||||
|
alias Explorer.SmartContract.Solidity.CodeCompiler |
||||||
|
|
||||||
|
describe "run/2" do |
||||||
|
test "compiles a smart contract using the solidity command line" do |
||||||
|
name = "SimpleStorage" |
||||||
|
optimization = false |
||||||
|
|
||||||
|
code = """ |
||||||
|
pragma solidity ^0.4.24; |
||||||
|
|
||||||
|
contract SimpleStorage { |
||||||
|
uint storedData; |
||||||
|
|
||||||
|
function set(uint x) public { |
||||||
|
storedData = x; |
||||||
|
} |
||||||
|
|
||||||
|
function get() public constant returns (uint) { |
||||||
|
return storedData; |
||||||
|
} |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
response = CodeCompiler.run(name, code, optimization) |
||||||
|
|
||||||
|
assert %{ |
||||||
|
"contracts" => %{ |
||||||
|
^name => %{ |
||||||
|
^name => %{ |
||||||
|
"abi" => _, |
||||||
|
"evm" => %{ |
||||||
|
"bytecode" => %{"object" => _} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} = response |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "generate_settings/2" do |
||||||
|
test "creates a json file with the solidity compiler expected settings" do |
||||||
|
name = "SimpleStorage" |
||||||
|
optimization = false |
||||||
|
|
||||||
|
code = """ |
||||||
|
pragma solidity ^0.4.24; |
||||||
|
|
||||||
|
contract SimpleStorage { |
||||||
|
uint storedData; |
||||||
|
|
||||||
|
function set(uint x) public { |
||||||
|
storedData = x; |
||||||
|
} |
||||||
|
|
||||||
|
function get() public constant returns (uint) { |
||||||
|
return storedData; |
||||||
|
} |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
generated = CodeCompiler.generate_settings(name, code, optimization) |
||||||
|
|
||||||
|
assert String.contains?(generated, "contract SimpleStorage") == true |
||||||
|
assert String.contains?(generated, "settings") == true |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,48 @@ |
|||||||
|
defmodule Explorer.SmartContract.Solidity.CompilerVersionTest do |
||||||
|
use ExUnit.Case |
||||||
|
|
||||||
|
doctest Explorer.SmartContract.Solidity.CompilerVersion |
||||||
|
|
||||||
|
alias Explorer.SmartContract.Solidity.CompilerVersion |
||||||
|
alias Plug.Conn |
||||||
|
|
||||||
|
describe "fetch_versions" do |
||||||
|
setup do |
||||||
|
bypass = Bypass.open() |
||||||
|
|
||||||
|
Application.put_env(:explorer, :solc_bin_api_url, "http://localhost:#{bypass.port}") |
||||||
|
|
||||||
|
{:ok, bypass: bypass} |
||||||
|
end |
||||||
|
|
||||||
|
test "fetches the list of the solidity compiler versions", %{bypass: bypass} do |
||||||
|
Bypass.expect(bypass, fn conn -> |
||||||
|
assert "GET" == conn.method |
||||||
|
assert "/bin/list.json" == conn.request_path |
||||||
|
|
||||||
|
Conn.resp(conn, 200, solc_bin_versions()) |
||||||
|
end) |
||||||
|
|
||||||
|
assert {:ok, versions} = CompilerVersion.fetch_versions() |
||||||
|
assert Enum.any?(versions, fn item -> item == {"0.4.9", "0.4.9"} end) == true |
||||||
|
end |
||||||
|
|
||||||
|
test "returns error when list of versions is not available", %{bypass: bypass} do |
||||||
|
Bypass.expect(bypass, fn conn -> |
||||||
|
Conn.resp(conn, 400, ~S({"error": "bad request"})) |
||||||
|
end) |
||||||
|
|
||||||
|
assert {:error, "bad request"} = CompilerVersion.fetch_versions() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns error when there is server error", %{bypass: bypass} do |
||||||
|
Bypass.down(bypass) |
||||||
|
|
||||||
|
assert {:error, :econnrefused} = CompilerVersion.fetch_versions() |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def solc_bin_versions() do |
||||||
|
File.read!("./test/support/fixture/smart_contract/solc_bin.json") |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,94 @@ |
|||||||
|
defmodule Explorer.SmartContract.VerifierTest do |
||||||
|
use ExUnit.Case, async: true |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
doctest Explorer.SmartContract.Verifier |
||||||
|
|
||||||
|
alias Explorer.SmartContract.Verifier |
||||||
|
|
||||||
|
describe "evaluate_authenticity/2" do |
||||||
|
test "verifies the generated bytecode against bytecode retrieved from the blockchain" do |
||||||
|
address_hash = "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" |
||||||
|
|
||||||
|
smart_contract_bytecode = |
||||||
|
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" |
||||||
|
|
||||||
|
created_contract_address = insert(:address, hash: address_hash, contract_code: smart_contract_bytecode) |
||||||
|
|
||||||
|
insert( |
||||||
|
:internal_transaction, |
||||||
|
index: 0, |
||||||
|
created_contract_address_hash: created_contract_address.hash, |
||||||
|
created_contract_code: smart_contract_bytecode |
||||||
|
) |
||||||
|
|
||||||
|
params = %{ |
||||||
|
"contract_source_code" => |
||||||
|
"pragma solidity ^0.4.24; contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public constant returns (uint) { return storedData; } }", |
||||||
|
"compiler" => "0.4.24", |
||||||
|
"name" => "SimpleStorage", |
||||||
|
"optimization" => false |
||||||
|
} |
||||||
|
|
||||||
|
assert {:ok, %{abi: abi}} = Verifier.evaluate_authenticity(address_hash, params) |
||||||
|
assert abi != nil |
||||||
|
end |
||||||
|
|
||||||
|
test "returns error when bytecoed doesn't match" do |
||||||
|
address_hash = "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" |
||||||
|
|
||||||
|
wrong_smart_contract_bytecode = |
||||||
|
"0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a723058207722b6ddfe522b31e50b878ced2f22d051e8e2cd19be7b4fba9686602b90ba2b0029" |
||||||
|
|
||||||
|
created_contract_address = insert(:address, hash: address_hash, contract_code: wrong_smart_contract_bytecode) |
||||||
|
|
||||||
|
insert( |
||||||
|
:internal_transaction, |
||||||
|
index: 0, |
||||||
|
created_contract_address_hash: created_contract_address.hash, |
||||||
|
created_contract_code: wrong_smart_contract_bytecode |
||||||
|
) |
||||||
|
|
||||||
|
params = %{ |
||||||
|
"contract_source_code" => |
||||||
|
"pragma solidity ^0.4.24; contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public constant returns (uint) { return storedData; } }", |
||||||
|
"compiler" => "0.4.24", |
||||||
|
"name" => "SimpleStorage", |
||||||
|
"optimization" => false |
||||||
|
} |
||||||
|
|
||||||
|
assert {:error, :generated_bytecode} = Verifier.evaluate_authenticity(address_hash, params) |
||||||
|
end |
||||||
|
|
||||||
|
test "returns error when there is a compilation problem" do |
||||||
|
address_hash = "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" |
||||||
|
|
||||||
|
params = %{ |
||||||
|
"contract_source_code" => "pragma solidity ^0.4.24; contract SimpleStorage { ", |
||||||
|
"compiler" => "0.4.24", |
||||||
|
"name" => "SimpleStorage", |
||||||
|
"optimization" => false |
||||||
|
} |
||||||
|
|
||||||
|
assert {:error, :compilation} = Verifier.evaluate_authenticity(address_hash, params) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "extract_bytecode/1" do |
||||||
|
test "extracts the bytecode from the hash" do |
||||||
|
code = |
||||||
|
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a723058203c381c1b48b38d050c54d7ef296ecd411040e19420dfec94772b9c49ae106a0b0029" |
||||||
|
|
||||||
|
swarm_source = "3c381c1b48b38d050c54d7ef296ecd411040e19420dfec94772b9c49ae106a0b" |
||||||
|
|
||||||
|
bytecode = |
||||||
|
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820" |
||||||
|
|
||||||
|
assert bytecode == Verifier.extract_bytecode(code) |
||||||
|
assert bytecode != code |
||||||
|
assert String.contains?(code, bytecode) == true |
||||||
|
assert String.contains?(bytecode, "0029") == false |
||||||
|
assert String.contains?(bytecode, swarm_source) == false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@ |
|||||||
|
@import "section"; |
||||||
|
@import "dot"; |
||||||
|
|
||||||
|
@import "address"; |
||||||
|
@import "block"; |
||||||
|
@import "blocks"; |
||||||
|
@import "chain"; |
||||||
|
@import "footer"; |
||||||
|
@import "header"; |
||||||
|
@import "internal_transaction"; |
||||||
|
@import "pagination"; |
||||||
|
@import "transaction"; |
||||||
|
@import "transaction_log"; |
||||||
|
@import "transactions"; |
@ -0,0 +1,34 @@ |
|||||||
|
defmodule ExplorerWeb.AddressContractVerificationController do |
||||||
|
use ExplorerWeb, :controller |
||||||
|
|
||||||
|
alias Explorer.Chain.SmartContract |
||||||
|
alias Explorer.SmartContract.{Solidity.CompilerVersion, Publisher} |
||||||
|
|
||||||
|
def new(conn, %{"address_id" => address_hash_string}) do |
||||||
|
changeset = |
||||||
|
SmartContract.changeset( |
||||||
|
%SmartContract{address_hash: address_hash_string}, |
||||||
|
%{} |
||||||
|
) |
||||||
|
|
||||||
|
{:ok, compiler_versions} = CompilerVersion.fetch_versions() |
||||||
|
|
||||||
|
render(conn, "new.html", changeset: changeset, compiler_versions: compiler_versions) |
||||||
|
end |
||||||
|
|
||||||
|
def create(conn, %{ |
||||||
|
"address_id" => address_hash_string, |
||||||
|
"smart_contract" => smart_contract, |
||||||
|
"locale" => locale |
||||||
|
}) do |
||||||
|
case Publisher.publish(address_hash_string, smart_contract) do |
||||||
|
{:ok, _smart_contract} -> |
||||||
|
redirect(conn, to: address_contract_path(conn, :index, locale, address_hash_string)) |
||||||
|
|
||||||
|
{:error, changeset} -> |
||||||
|
{:ok, compiler_versions} = CompilerVersion.fetch_versions() |
||||||
|
|
||||||
|
render(conn, "new.html", changeset: changeset, compiler_versions: compiler_versions) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,66 @@ |
|||||||
|
<section class="container-fluid"> |
||||||
|
<h1>Smart Contract</h1> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<div class="card-header"> |
||||||
|
<ul class="nav nav-tabs card-header-tabs"> |
||||||
|
<li class="nav-item"> |
||||||
|
<%= link( |
||||||
|
gettext("Contract Source Code"), |
||||||
|
class: "nav-link active", |
||||||
|
to: '#' |
||||||
|
) %> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<%= form_for @changeset, |
||||||
|
address_verify_contract_path(@conn, :create, @conn.assigns.locale, @conn.params["address_id"]), |
||||||
|
fn f -> %> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<%= label f, :address_hash, "Contract Address" %> |
||||||
|
<%= text_input f, :address_hash, class: "form-control", disabled: true %> |
||||||
|
<%= error_tag f, :address_hash %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<%= label f, :name, "Contract Name" %> |
||||||
|
<%= text_input f, :name, class: "form-control" %> |
||||||
|
<%= error_tag f, :name %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<%= label f, :compiler, "Compiler" %> |
||||||
|
<%= select f, :compiler, @compiler_versions, class: "form-control", selected: "0.4.24" %> |
||||||
|
<%= error_tag f, :compiler %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<%= label f, "Optimization", class: "d-block" %> |
||||||
|
|
||||||
|
<div class="form-check form-check-inline"> |
||||||
|
<%= radio_button f, :optimization, false, checked: true, class: "form-check-input" %> |
||||||
|
<%= label :smart_contract_optimization, :false, "No", class: "form-check-label" %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-check form-check-inline"> |
||||||
|
<%= radio_button f, :optimization, true, class: "form-check-input" %> |
||||||
|
<%= label :smart_contract_optimization, :true, "Yes", class: "form-check-label" %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<%= error_tag f, :optimization %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<%= label f, :contract_source_code, "Enter the Solidity Contract Code below" %> |
||||||
|
<%= textarea f, :contract_source_code, class: "form-control", rows: 3 %> |
||||||
|
<%= error_tag f, :contract_source_code %> |
||||||
|
</div> |
||||||
|
|
||||||
|
<%= submit "Verify and publish", class: "button button--secondary button--sm" %> |
||||||
|
<%= reset "Reset", class: "button button--tertiary button--sm" %> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
@ -0,0 +1,3 @@ |
|||||||
|
defmodule ExplorerWeb.AddressContractVerificationView do |
||||||
|
use ExplorerWeb, :view |
||||||
|
end |
@ -1,3 +1,10 @@ |
|||||||
defmodule ExplorerWeb.AddressContractView do |
defmodule ExplorerWeb.AddressContractView do |
||||||
use ExplorerWeb, :view |
use ExplorerWeb, :view |
||||||
|
|
||||||
|
import ExplorerWeb.AddressView, only: [smart_contract_verified?: 1] |
||||||
|
|
||||||
|
def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: true) |
||||||
|
|
||||||
|
def format_optimization(true), do: gettext("true") |
||||||
|
def format_optimization(false), do: gettext("false") |
||||||
end |
end |
||||||
|
@ -0,0 +1,40 @@ |
|||||||
|
defmodule ExplorerWeb.ErrorHelpers do |
||||||
|
@moduledoc """ |
||||||
|
Conveniences for translating and building error messages. |
||||||
|
""" |
||||||
|
|
||||||
|
use Phoenix.HTML |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Generates tag for inlined form input errors. |
||||||
|
""" |
||||||
|
def error_tag(form, field, opts \\ []) do |
||||||
|
Enum.map(Keyword.get_values(form.errors, field), fn error -> |
||||||
|
content_tag(:span, translate_error(error), Keyword.merge([class: "has-error"], opts)) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Translates an error message using gettext. |
||||||
|
""" |
||||||
|
def translate_error({msg, opts}) do |
||||||
|
# Because error messages were defined within Ecto, we must |
||||||
|
# call the Gettext module passing our Gettext backend. We |
||||||
|
# also use the "errors" domain as translations are placed |
||||||
|
# in the errors.po file. |
||||||
|
# Ecto will pass the :count keyword if the error message is |
||||||
|
# meant to be pluralized. |
||||||
|
# On your own code and templates, depending on whether you |
||||||
|
# need the message to be pluralized or not, this could be |
||||||
|
# written simply as: |
||||||
|
# |
||||||
|
# dngettext "errors", "1 file", "%{count} files", count |
||||||
|
# dgettext "errors", "is invalid" |
||||||
|
# |
||||||
|
if count = opts[:count] do |
||||||
|
Gettext.dngettext(ExplorerWeb.Gettext, "errors", msg, msg, count, opts) |
||||||
|
else |
||||||
|
Gettext.dgettext(ExplorerWeb.Gettext, "errors", msg, opts) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,74 @@ |
|||||||
|
defmodule ExplorerWeb.AddressContractVerificationTest do |
||||||
|
use ExplorerWeb.FeatureCase, async: true |
||||||
|
|
||||||
|
import Wallaby.Query |
||||||
|
|
||||||
|
alias Plug.Conn |
||||||
|
|
||||||
|
setup do |
||||||
|
bypass = Bypass.open() |
||||||
|
|
||||||
|
Application.put_env(:explorer, :solc_bin_api_url, "http://localhost:#{bypass.port}") |
||||||
|
|
||||||
|
{:ok, bypass: bypass} |
||||||
|
end |
||||||
|
|
||||||
|
test "users validates smart contract", %{session: session, bypass: bypass} do |
||||||
|
Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, solc_bin_versions()) end) |
||||||
|
|
||||||
|
address_hash = "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" |
||||||
|
|
||||||
|
smart_contract_bytecode = |
||||||
|
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" |
||||||
|
|
||||||
|
created_contract_address = insert(:address, hash: address_hash, contract_code: smart_contract_bytecode) |
||||||
|
|
||||||
|
insert( |
||||||
|
:internal_transaction, |
||||||
|
index: 0, |
||||||
|
created_contract_address_hash: created_contract_address.hash, |
||||||
|
created_contract_code: smart_contract_bytecode |
||||||
|
) |
||||||
|
|
||||||
|
code = """ |
||||||
|
pragma solidity ^0.4.24; |
||||||
|
|
||||||
|
contract SimpleStorage { |
||||||
|
uint storedData; |
||||||
|
|
||||||
|
function set(uint x) public { |
||||||
|
storedData = x; |
||||||
|
} |
||||||
|
|
||||||
|
function get() public constant returns (uint) { |
||||||
|
return storedData; |
||||||
|
} |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
session |
||||||
|
|> visit("/en/addresses/#{address_hash}/contract_verifications/new") |
||||||
|
|> fill_in(text_field("Contract Name"), with: "SimpleStorage") |
||||||
|
|> click(option("0.4.24")) |
||||||
|
|> click(radio_button("No")) |
||||||
|
|> fill_in(text_field("Enter the Solidity Contract Code below"), with: code) |
||||||
|
|> click(button("Verify and publish")) |
||||||
|
|
||||||
|
assert current_path(session) =~ ~r/\/en\/addresses\/#{address_hash}\/contracts/ |
||||||
|
end |
||||||
|
|
||||||
|
test "with invalid data shows error messages", %{session: session, bypass: bypass} do |
||||||
|
Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, solc_bin_versions()) end) |
||||||
|
|
||||||
|
session |
||||||
|
|> visit("/en/addresses/0x1e0eaa06d02f965be2dfe0bc9ff52b2d82133461/contract_verifications/new") |
||||||
|
|> fill_in(text_field("Contract Name"), with: "") |
||||||
|
|> fill_in(text_field("Enter the Solidity Contract Code below"), with: "") |
||||||
|
|> click(button("Verify and publish")) |
||||||
|
|> assert_has(css(".has-error", text: "there was an error validating your contract, please try again.")) |
||||||
|
end |
||||||
|
|
||||||
|
def solc_bin_versions() do |
||||||
|
File.read!("./test/support/fixture/smart_contract/solc_bin.json") |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,30 @@ |
|||||||
|
defmodule ExplorerWeb.ErrorHelpersTest do |
||||||
|
use ExplorerWeb.ConnCase, async: true |
||||||
|
import Phoenix.HTML.Tag, only: [content_tag: 3] |
||||||
|
|
||||||
|
alias ExplorerWeb.ErrorHelpers |
||||||
|
|
||||||
|
@changeset %{ |
||||||
|
errors: [ |
||||||
|
contract_code: {"has already been taken", []} |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
test "error_tag/2 renders spans with default options" do |
||||||
|
assert ErrorHelpers.error_tag(@changeset, :contract_code) == [ |
||||||
|
content_tag(:span, "has already been taken", class: "has-error") |
||||||
|
] |
||||||
|
end |
||||||
|
|
||||||
|
test "error_tag/3 overrides default options" do |
||||||
|
assert ErrorHelpers.error_tag(@changeset, :contract_code, class: "something-else") == [ |
||||||
|
content_tag(:span, "has already been taken", class: "something-else") |
||||||
|
] |
||||||
|
end |
||||||
|
|
||||||
|
test "error_tag/3 merges given options with default ones" do |
||||||
|
assert ErrorHelpers.error_tag(@changeset, :contract_code, data_hidden: true) == [ |
||||||
|
content_tag(:span, "has already been taken", class: "has-error", data_hidden: true) |
||||||
|
] |
||||||
|
end |
||||||
|
end |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue