Repository: hswick/exw3 Branch: master Commit: 17074b213e8a Files: 37 Total size: 92.2 KB Directory structure: gitextract_a2glllkx/ ├── .formatter.exs ├── .github/ │ ├── dependabot.yml │ ├── install_parity.sh │ └── workflows/ │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── config/ │ └── config.exs ├── docker-compose.yml ├── lib/ │ ├── exw3/ │ │ ├── abi.ex │ │ ├── address.ex │ │ ├── client.ex │ │ ├── contract.ex │ │ ├── normalize.ex │ │ ├── rpc.ex │ │ └── utils.ex │ └── exw3.ex ├── mix.exs ├── parity.sh └── test/ ├── examples/ │ ├── build/ │ │ ├── AddressTester.abi │ │ ├── ArrayTester.abi │ │ ├── Complex.abi │ │ ├── EventTester.abi │ │ └── SimpleStorage.abi │ └── contracts/ │ ├── AddressTester.sol │ ├── ArrayTester.sol │ ├── Complex.sol │ ├── EventTester.sol │ └── SimpleStorage.sol ├── exw3/ │ ├── abi_test.exs │ ├── address_test.exs │ ├── client_test.exs │ ├── contract_test.exs │ ├── rpc_test.exs │ └── utils_test.exs ├── exw3_test.exs └── test_helper.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .formatter.exs ================================================ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: mix directory: "/" schedule: interval: monthly open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: monthly open-pull-requests-limit: 10 ================================================ FILE: .github/install_parity.sh ================================================ # Install Parity blockchain tests on Github action echo > passfile # just to be safe wget https://releases.parity.io/ethereum/v2.7.2/x86_64-unknown-linux-gnu/parity chmod 755 ./parity echo > passfile ./parity --chain dev 2>&1 & PARITY_PID=$! sleep 10 kill -9 $(lsof -t -i:8545) # cleanup old zombie instances ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: - master pull_request: branches: - '*' jobs: test: runs-on: ubuntu-latest name: Test OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: otp: [22.x, 23.x, 24.x] elixir: [1.10.x, 1.11.x, 1.12.x] steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1.11 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - uses: actions-rs/toolchain@v1 with: toolchain: stable - name: Cache Dependencies uses: actions/cache@v3.0.8 with: path: | deps _build/dev _build/test key: elixir-cache-${{secrets.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} restore-keys: | elixir-cache-${{secrets.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- elixir-cache-${{secrets.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}- - name: Install Dependencies run: mix deps.get - name: Install Parity Blockchain run: .github/install_parity.sh - name: Run Parity Blockchain run: ./parity --chain dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --reseal-min-period 0 --password passfile & - name: Test run: mix test - name: Dialyzer run: mix dialyzer --halt-exit-status ================================================ FILE: .gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). exw3-*.tar # Temporary files, for example, from tests. /tmp/ # Misc. .env* passfile docker/openethereum ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================ # ExW3 [![Build Status](https://github.com/hswick/exw3/workflows/test/badge.svg?branch=master)](https://github.com/hswick/exw3/actions?query=workflow%3Atest) [![Module Version](https://img.shields.io/hexpm/v/exw3.svg?style=flat)](https://hex.pm/packages/exw3) [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg?style=flat)](https://hexdocs.pm/exw3/) [![Total Download](https://img.shields.io/hexpm/dt/exw3.svg?style=flat)](https://hex.pm/packages/exw3) [![License](https://img.shields.io/hexpm/l/exw3.svg?style=flat)](https://github.com/hswick/exw3/blob/master/LICENSE) [![Last Updated](https://img.shields.io/github/last-commit/hswick/exw3.svg?style=flat)](https://github.com/hswick/exw3/commits/master)

## Installation The package can be installed by adding `:exw3` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:exw3, "~> 0.6"} ] end ``` ## Overview ExW3 is a wrapper around ethereumex to provide a high level, user friendly json rpc api. This library is focused on providing a handy abstraction for working with smart contracts, and any other relevant utilities. ## Usage Ensure you have an ethereum node to connect to at the specified url in your config. An easy local testnet to use is ganache-cli: ```bash $ ganache-cli ``` Or you can use parity: Install Parity, then run it with ```bash $ echo > passfile parity --chain dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --reseal-min-period 0 --password passfile ``` If Parity complains about password or missing account, try ```bash $ parity --chain dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 ``` ### HTTP To use Ethereumex's HttpClient simply set your config like this: ```elixir config :ethereumex, client_type: :http, url: "http://localhost:8545" ``` ### IPC If you want to use IpcClient set your config to something like this: ```elixir config :ethereumex, client_type: :ipc, ipc_path: "/.local/share/io.parity.ethereum/jsonrpc.ipc" ``` Provide an absolute path to the ipc socket provided by whatever Ethereum client you are running. You don't need to include the home directory, as that will be prepended to the path provided. **NOTE:** Use of IPC is recommended, as it is more secure and significantly faster. Currently, ExW3 supports a handful of JSON RPC commands. Primarily the ones that get used the most. If ExW3 doesn't provide a specific command, you can always use the [Ethereumex](https://github.com/exthereum/ethereumex) commands. Check out the [documentation](https://hexdocs.pm/exw3/ExW3.html) for more details of the API. ### Example ```elixir iex(1)> accounts = ExW3.accounts() ["0x00a329c0648769a73afac7f9381e08fb43dbea72"] iex(2)> ExW3.balance(Enum.at(accounts, 0)) 1606938044258990275541962092341162602522200978938292835291376 iex(3)> ExW3.block_number() 1252 iex(4)> simple_storage_abi = ExW3.Abi.load_abi("test/examples/build/SimpleStorage.abi") %{ "get" => %{ "constant" => true, "inputs" => [], "name" => "get", "outputs" => [%{"name" => "", "type" => "uint256"}], "payable" => false, "stateMutability" => "view", "type" => "function" }, "set" => %{ "constant" => false, "inputs" => [%{"name" => "_data", "type" => "uint256"}], "name" => "set", "outputs" => [], "payable" => false, "stateMutability" => "nonpayable", "type" => "function" } } iex(5)> ExW3.Contract.start_link {:ok, #PID<0.265.0>} iex(6)> ExW3.Contract.register(:SimpleStorage, abi: simple_storage_abi) :ok iex(7)> {:ok, address, tx_hash} = ExW3.Contract.deploy(:SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), options: %{gas: 300_000, from: Enum.at(accounts, 0)}) {:ok, "0x22018c2bb98387a39e864cf784e76cb8971889a5", "0x4ea539048c01194476004ef69f407a10628bed64e88ee8f8b17b4d030d0e7cb7"} iex(8)> ExW3.Contract.at(:SimpleStorage, address) :ok iex(9)> ExW3.Contract.call(:SimpleStorage, :get) {:ok, 0} iex(10)> ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(accounts, 0), gas: 50_000}) {:ok, "0x88838e84a401a1d6162290a1a765507c4a83f5e050658a83992a912f42149ca5"} iex(11)> ExW3.Contract.call(:SimpleStorage, :get) {:ok, 1} ``` ## Address Type If you are familiar with web3.js you may find the way ExW3 handles addresses unintuitive. ExW3's ABI encoder interprets the address type as an uint160. If you are using an address as an option to a transaction like `:from` or `:to` this will work as expected. However, if one of your smart contracts is expecting an address type for an input parameter then you will need to do this: ```elixir a = ExW3.Utils.hex_to_integer("0x88838e84a401a1d6162290a1a765507c4a83f5e050658a83992a912f42149ca5") ``` ## Events ExW3 allows the retrieval of event logs using filters or transaction receipts. In this example we will demonstrate a filter. Assume we have already deployed and registered a contract called EventTester. ```elixir # We can optionally specify extra parameters like `:fromBlock`, and `:toBlock` {:ok, filter_id} = ExW3.Contract.filter(:EventTester, "Simple", %{fromBlock: 42, toBlock: "latest"}) # After some point that we think there are some new changes {:ok, changes} = ExW3.Contract.get_filter_changes(filter_id) # We can then uninstall the filter after we are done using it ExW3.Contract.uninstall_filter(filter_id) ``` ## Indexed Events Ethereum allows a user to add topics to filters. This means the filter will only return events with the specific index parameters. For all of the extra options see [here](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter) If you have written your event in Solidity like this: ``` event SimpleIndex(uint256 indexed num, bytes32 indexed data, uint256 otherNum); ``` You can add a filter on which logs will be returned back to the RPC client based on the indexed fields. ExW3 allows for 2 ways of specifying these parameters (`:topics`) in two ways. The first, and probably more preferred way, is with a map: ```elixir indexed_filter_id = ExW3.Contract.filter( :EventTester, "SimpleIndex", %{ topics: %{num: 46, data: "Hello, World!"}, } ) ``` The other option is a list (mapped version is an abstraction over this). The downside here is this is order dependent. Any values you don't want to specify must be represented with a `nil`. This approach has been included because it is the implementation of the JSON RPC spec. ```elixir indexed_filter_id = ExW3.Contract.filter( :EventTester, "SimpleIndex", %{ topics: [nil, "Hello, World!"] } ) ``` Here we are skipping the `num` topic, and only filtering on the `data` parameter. NOTE!!! These two approaches are mutually exclusive, and for almost all cases you should prefer the map. ## Continuous Event Handling In many cases, you will want some process to continuously listen for events. We can implement this functionality using a recursive function. Since Elixir uses tail call optimization, we won't have to worry about blowing up the stack. ```elixir def listen_for_event do {:ok, changes} = ExW3.Contract.get_filter_changes(filter_id) # Get our changes from the blockchain handle_changes(changes) # Some function to deal with the data. Good place to use pattern matching. :timer.sleep(1000) # Some delay in milliseconds. Recommended to save bandwidth, and not spam. listen_for_event() # Recurse end ``` # Compiling Solidity To compile the test solidity contracts after making a change run this command: ```bash $ solc --abi --bin --overwrite -o test/examples/build test/examples/contracts/*.sol ``` # Contributing ## Test The full test suite requires a running blockchain. You can run your own or start `openethereum` with `docker-compose`. ```bash $ docker-compose up $ mix test ``` ## Copyright and License Copyright (c) 2018 Harley Swick Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: config/config.exs ================================================ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. use Mix.Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this # file won't be loaded nor affect the parent project. For this reason, # if you want to provide default values for your application for third- # party users, it should be done in your mix.exs file. # Sample configuration: # # config :logger, # level: :info # # config :logger, :console, # format: "$date $time [$level] $metadata$message\n", # metadata: [:user_id] # It is also possible to import configuration files, relative to this # directory. For example, you can emulate configuration per environment # by uncommenting the line below and defining dev.exs, test.exs and such. # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # # import_config "#{Mix.env}.exs" config :ethereumex, client_type: :ipc, url: "http://localhost:8545", ipc_path: System.get_env( "IPC_PATH", "#{System.user_home!()}/.local/share/io.parity.ethereum/jsonrpc.ipc" ) ================================================ FILE: docker-compose.yml ================================================ version: '3.8' services: openethereum: image: openethereum/openethereum:v3.3.0 command: '--chain=dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --password=/home/openethereum/.local/share/openethereum/passfile --jsonrpc-interface=0.0.0.0' ports: - '8545:8545' - '8546:8546' volumes: - ./docker:/home/openethereum/.local/share ================================================ FILE: lib/exw3/abi.ex ================================================ defmodule ExW3.Abi do @doc "Decodes event based on given data and provided signature" @spec decode_event(binary(), binary()) :: any() def decode_event(data, signature) do formatted_data = data |> String.slice(2..-1) |> Base.decode16!(case: :lower) fs = ABI.FunctionSelector.decode(signature) ABI.TypeDecoder.decode(formatted_data, fs) end @doc "Loads the abi at the file path and reformats it to a map" @spec load_abi(binary()) :: list() | {:error, atom()} def load_abi(file_path) do with {:ok, cwd} <- File.cwd(), {:ok, abi} <- File.read(Path.join([cwd, file_path])) do reformat_abi(Jason.decode!(abi)) end end @doc "Loads the bin ar the file path" @spec load_bin(binary()) :: binary() def load_bin(file_path) do with {:ok, cwd} <- File.cwd(), {:ok, bin} <- File.read(Path.join([cwd, file_path])) do bin end end @doc "Decodes data based on given type signature" @spec decode_data(binary(), binary()) :: any() def decode_data(types_signature, data) do {:ok, trim_data} = String.slice(data, 2..String.length(data)) |> Base.decode16(case: :lower) ABI.decode(types_signature, trim_data) |> List.first() end @doc "Decodes output based on specified functions return signature" @spec decode_output(map(), binary(), binary()) :: list() def decode_output(abi, name, output) do {:ok, trim_output} = String.slice(output, 2..String.length(output)) |> Base.decode16(case: :lower) output_types = Enum.map(abi[name]["outputs"], fn x -> x["type"] end) types_signature = Enum.join(["(", Enum.join(output_types, ","), ")"]) output_signature = "#{name}(#{types_signature})" outputs = ABI.decode(output_signature, trim_output) |> List.first() |> Tuple.to_list() outputs end @doc "Returns the type signature of a given function" @spec types_signature(map(), binary()) :: binary() def types_signature(abi, name) do input_types = Enum.map(abi[name]["inputs"], fn x -> x["type"] end) types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"]) types_signature end @doc "Returns the 4 character method id based on the hash of the method signature" @spec method_signature(map(), binary()) :: binary() def method_signature(abi, name) do if abi[name] do input_signature = ExKeccak.hash_256("#{name}#{types_signature(abi, name)}") # Take first four bytes <> = input_signature init else raise "#{name} method not found in the given abi" end end @doc "Encodes data into Ethereum hex string based on types signature" @spec encode_data(binary(), list()) :: binary() def encode_data(types_signature, data) do ABI.TypeEncoder.encode_raw( [List.to_tuple(data)], ABI.FunctionSelector.decode_raw(types_signature) ) end @doc "Encodes list of options and returns them as a map" @spec encode_options(map(), list()) :: map() def encode_options(options, keys) do keys |> Enum.filter(fn option -> Map.has_key?(options, option) end) |> Enum.map(fn option -> {option, encode_option(options[option])} end) |> Enum.into(%{}) end @doc "Encodes options into Ethereum JSON RPC hex string" @spec encode_option(integer()) :: binary() def encode_option(0), do: "0x0" def encode_option(nil), do: nil def encode_option(value) do "0x" <> (value |> :binary.encode_unsigned() |> Base.encode16(case: :lower) |> String.trim_leading("0")) end @doc "Encodes data and appends it to the encoded method id" @spec encode_method_call(map(), binary(), list()) :: binary() def encode_method_call(abi, name, input) do encoded_method_call = method_signature(abi, name) <> encode_data(types_signature(abi, name), input) encoded_method_call |> Base.encode16(case: :lower) end @doc "Encodes input from a method call based on function signature" @spec encode_input(map(), binary(), list()) :: binary() def encode_input(abi, name, input) do if abi[name]["inputs"] do input_types = Enum.map(abi[name]["inputs"], fn x -> x["type"] end) types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"]) input_signature = ExKeccak.hash_256("#{name}#{types_signature}") # Take first four bytes <> = input_signature encoded_input = init <> ABI.TypeEncoder.encode_raw( [List.to_tuple(input)], ABI.FunctionSelector.decode_raw(types_signature) ) encoded_input |> Base.encode16(case: :lower) else raise "#{name} method not found with the given abi" end end defp reformat_abi(abi) do abi |> Enum.map(&map_abi/1) |> Map.new() end defp map_abi(x) do case {x["name"], x["type"]} do {nil, "constructor"} -> {:constructor, x} {nil, "fallback"} -> {:fallback, x} {name, _} -> {name, x} end end end ================================================ FILE: lib/exw3/address.ex ================================================ defmodule ExW3.Address do @type t :: %__MODULE__{bytes: binary} defstruct ~w[bytes]a @spec from_bytes(binary) :: t def from_bytes(bytes) do %__MODULE__{bytes: bytes} end @spec from_hex(String.t()) :: t def from_hex(address) do case address do "0x" <> a -> from_hex(a) a -> bytes = a |> String.downcase() |> Base.decode16!(case: :lower) %__MODULE__{bytes: bytes} end end @spec to_bytes(t) :: binary def to_bytes(%__MODULE__{bytes: bytes}) do bytes end @spec to_string(t) :: String.t() def to_string(%__MODULE__{bytes: bytes}) do Base.encode16(bytes, case: :lower) end @spec to_hex(t) :: String.t() def to_hex(%__MODULE__{} = address) do "0x#{__MODULE__.to_string(address)}" end @spec to_checksum(t) :: String.t() def to_checksum(%__MODULE__{} = address) do address = address |> __MODULE__.to_string() address_hash = address |> ExKeccak.hash_256() |> Base.encode16(case: :lower) keccak_hash_list = address_hash |> String.split("", trim: true) |> Enum.map(fn x -> elem(Integer.parse(x, 16), 0) end) list_arr = for n <- 0..(String.length(address) - 1) do number = Enum.at(keccak_hash_list, n) cond do number >= 8 -> String.upcase(String.at(address, n)) true -> String.downcase(String.at(address, n)) end end "0x" <> List.to_string(list_arr) end @spec is_valid_checksum?(String.t()) :: boolean def is_valid_checksum?(hex_address) do address = hex_address |> __MODULE__.from_hex() __MODULE__.to_checksum(address) == hex_address end end ================================================ FILE: lib/exw3/client.ex ================================================ defmodule ExW3.Client do @type argument :: term @type request_error :: Ethereumex.Client.Behaviour.error() @type error :: {:error, :invalid_client_type} | request_error @spec call_client(atom) :: {:ok, term} | error @spec call_client(atom, [argument]) :: {:ok, term} | error def call_client(method_name, arguments \\ []) do url_opt = extract_url_opt(arguments) case client_type(url_opt) do :http -> apply(Ethereumex.HttpClient, method_name, arguments) :ipc -> apply(Ethereumex.IpcClient, method_name, arguments) _ -> {:error, :invalid_client_type} end end defp extract_url_opt(arguments) do arguments |> List.last() |> case do last when is_list(last) -> Keyword.get(last, :url) _ -> nil end end defp client_type(nil), do: Application.get_env(:ethereumex, :client_type, :http) defp client_type("http://" <> _), do: :http defp client_type("https://" <> _), do: :http defp client_type(_), do: :invalid end ================================================ FILE: lib/exw3/contract.ex ================================================ defmodule ExW3.Contract do use GenServer @doc "Begins the Contract process to manage all interactions with smart contracts" @spec start_link() :: {:ok, pid()} def start_link(_ \\ :ok) do GenServer.start_link(__MODULE__, %{filters: %{}}, name: ContractManager) end @doc "Deploys contracts with given arguments" @spec deploy(atom(), list()) :: {:ok, binary(), binary()} def deploy(name, args) do GenServer.call(ContractManager, {:deploy, {name, args}}) end @doc "Registers the contract with the ContractManager process. Only :abi is required field." @spec register(atom(), list()) :: :ok def register(name, contract_info) do GenServer.cast(ContractManager, {:register, {name, contract_info}}) end @doc "Uninstalls the filter, and deletes the data associated with the filter id" @spec uninstall_filter(binary()) :: :ok def uninstall_filter(filter_id) do GenServer.cast(ContractManager, {:uninstall_filter, filter_id}) end @doc "Sets the address for the contract specified by the name argument" @spec at(atom(), binary()) :: :ok def at(name, address) do GenServer.cast(ContractManager, {:at, {name, address}}) end @doc "Returns the current Contract GenServer's address" @spec address(atom()) :: {:ok, binary()} def address(name) do GenServer.call(ContractManager, {:address, name}) end @doc "Use a Contract's method with an eth_call" @spec call(atom(), atom(), list(), any()) :: {:ok, any()} def call(contract_name, method_name, args \\ [], timeout \\ :infinity) do GenServer.call(ContractManager, {:call, {contract_name, method_name, args}}, timeout) end @doc "Use a Contract's method with an eth_sendTransaction" @spec send(atom(), atom(), list(), map()) :: {:ok, binary()} def send(contract_name, method_name, args, options) do GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}}) end @doc "Returns a formatted transaction receipt for the given transaction hash(id)" @spec tx_receipt(atom(), binary()) :: map() def tx_receipt(contract_name, tx_hash) do GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}}) end @doc "Installs a filter on the Ethereum node. This also formats the parameters, and saves relevant information to format event logs." @spec filter(atom(), binary(), map()) :: {:ok, binary()} def filter(contract_name, event_name, event_data \\ %{}) do GenServer.call( ContractManager, {:filter, {contract_name, event_name, event_data}} ) end @doc "Using saved information related to the filter id, event logs are formatted properly" @spec get_filter_changes(binary()) :: {:ok, list()} def get_filter_changes(filter_id) do GenServer.call( ContractManager, {:get_filter_changes, filter_id} ) end def init(state) do {:ok, state} end defp data_signature_helper(name, fields) do non_indexed_types = Enum.map(fields, &Map.get(&1, "type")) Enum.join([name, "(", Enum.join(non_indexed_types, ","), ")"]) end defp topic_types_helper(fields) do if length(fields) > 0 do Enum.map(fields, fn field -> "(#{field["type"]})" end) else [] end end defp init_events(abi) do events = Enum.filter(abi, fn {_, v} -> v["type"] == "event" end) names_and_signature_types_map = Enum.map(events, fn {name, v} -> types = Enum.map(v["inputs"], &Map.get(&1, "type")) signature = Enum.join([name, "(", Enum.join(types, ","), ")"]) encoded_event_signature = ExW3.Utils.keccak256(signature) indexed_fields = Enum.filter(v["inputs"], fn input -> input["indexed"] end) indexed_names = Enum.map(indexed_fields, fn field -> field["name"] end) non_indexed_fields = Enum.filter(v["inputs"], fn input -> !input["indexed"] end) non_indexed_names = Enum.map(non_indexed_fields, fn field -> field["name"] end) data_signature = data_signature_helper(name, non_indexed_fields) event_attributes = %{ signature: data_signature, non_indexed_names: non_indexed_names, topic_types: topic_types_helper(indexed_fields), topic_names: indexed_names } {{encoded_event_signature, event_attributes}, {name, encoded_event_signature}} end) signature_types_map = Enum.map(names_and_signature_types_map, fn {signature_types, _} -> signature_types end) names_map = Enum.map(names_and_signature_types_map, fn {_, names} -> names end) [ events: Enum.into(signature_types_map, %{}), event_names: Enum.into(names_map, %{}) ] end def deploy_helper(bin, abi, args) do constructor_arg_data = if arguments = args[:args] do constructor_abi = Enum.find(abi, fn {_, v} -> v["type"] == "constructor" end) if constructor_abi do {_, constructor} = constructor_abi input_types = Enum.map(constructor["inputs"], fn x -> x["type"] end) types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"]) arg_count = Enum.count(arguments) input_types_count = Enum.count(input_types) if input_types_count != arg_count do raise "Number of provided arguments to constructor is incorrect. Was given #{ arg_count } args, looking for #{input_types_count}." end bin <> (ExW3.Abi.encode_data(types_signature, arguments) |> Base.encode16(case: :lower)) else # IO.warn("Could not find a constructor") bin end else bin end gas = ExW3.Abi.encode_option(args[:options][:gas]) gasPrice = ExW3.Abi.encode_option(args[:options][:gas_price]) tx = %{ from: args[:options][:from], data: "0x#{constructor_arg_data}", gas: gas, gasPrice: gasPrice } {:ok, tx_hash} = ExW3.Rpc.eth_send([tx]) {:ok, tx_receipt} = ExW3.Rpc.tx_receipt(tx_hash) {tx_receipt["contractAddress"], tx_hash} end def eth_call_helper(address, abi, method_name, args) do result = ExW3.Rpc.eth_call([ %{ to: address, data: "0x#{ExW3.Abi.encode_method_call(abi, method_name, args)}" } ]) case result do {:ok, data} -> ([:ok] ++ ExW3.Abi.decode_output(abi, method_name, data)) |> List.to_tuple() {:error, err} -> {:error, err} end end def eth_send_helper(address, abi, method_name, args, options) do encoded_options = ExW3.Abi.encode_options( options, [:gas, :gasPrice, :value, :nonce] ) gas = ExW3.Abi.encode_option(args[:options][:gas]) gasPrice = ExW3.Abi.encode_option(args[:options][:gas_price]) ExW3.Rpc.eth_send([ Map.merge( %{ to: address, data: "0x#{ExW3.Abi.encode_method_call(abi, method_name, args)}", gas: gas, gasPrice: gasPrice }, Map.merge(options, encoded_options) ) ]) end defp register_helper(contract_info) do if contract_info[:abi] do contract_info ++ init_events(contract_info[:abi]) else raise "ABI not provided upon initialization" end end # Options' checkers defp check_option(nil, error_atom), do: {:error, error_atom} defp check_option([], error_atom), do: {:error, error_atom} defp check_option([head | _tail], _atom) when head != nil, do: {:ok, head} defp check_option([_head | tail], atom), do: check_option(tail, atom) defp check_option(value, _atom), do: {:ok, value} # Casts def handle_cast({:at, {name, address}}, state) do contract_state = state[name] contract_state = Keyword.put(contract_state, :address, address) state = Map.put(state, name, contract_state) {:noreply, state} end def handle_cast({:register, {name, contract_info}}, state) do {:noreply, Map.put(state, name, register_helper(contract_info))} end def handle_cast({:uninstall_filter, filter_id}, state) do ExW3.uninstall_filter(filter_id) {:noreply, Map.put(state, :filters, Map.delete(state[:filters], filter_id))} end # Calls defp filter_topics_helper(event_signature, event_data, topic_types, topic_names) do topics = if is_map(event_data[:topics]) do Enum.map(topic_names, fn name -> event_data[:topics][String.to_atom(name)] end) else event_data[:topics] end if topics do formatted_topics = Enum.map(0..(length(topics) - 1), fn i -> topic = Enum.at(topics, i) if topic do if is_list(topic) do topic_type = Enum.at(topic_types, i) Enum.map(topic, fn t -> "0x" <> (ExW3.Abi.encode_data(topic_type, [t]) |> Base.encode16(case: :lower)) end) else topic_type = Enum.at(topic_types, i) "0x" <> (ExW3.Abi.encode_data(topic_type, [topic]) |> Base.encode16(case: :lower)) end else topic end end) [event_signature] ++ formatted_topics else [event_signature] end end def from_block_helper(event_data) do if event_data[:fromBlock] do new_from_block = if Enum.member?(["latest", "earliest", "pending"], event_data[:fromBlock]) do event_data[:fromBlock] else ExW3.Abi.encode_data("(uint256)", [event_data[:fromBlock]]) end Map.put(event_data, :fromBlock, new_from_block) else event_data end end defp param_helper(event_data, key) do if event_data[key] do new_param = if Enum.member?(["latest", "earliest", "pending"], event_data[key]) do event_data[key] else "0x" <> (ExW3.Abi.encode_data("(uint256)", [event_data[key]]) |> Base.encode16(case: :lower)) end Map.put(event_data, key, new_param) else event_data end end defp event_data_format_helper(event_data) do event_data |> param_helper(:fromBlock) |> param_helper(:toBlock) |> Map.delete(:topics) end def get_event_attributes(state, contract_name, event_name) do contract_info = state[contract_name] contract_info[:events][contract_info[:event_names][event_name]] end defp extract_non_indexed_fields(data, names, signature) do Enum.zip(names, ExW3.Abi.decode_event(data, signature)) |> Enum.into(%{}) end defp format_log_data(log, event_attributes) do non_indexed_fields = extract_non_indexed_fields( Map.get(log, "data"), event_attributes[:non_indexed_names], event_attributes[:signature] ) indexed_fields = if length(log["topics"]) > 1 do [_head | tail] = log["topics"] decoded_topics = Enum.map(0..(length(tail) - 1), fn i -> topic_type = Enum.at(event_attributes[:topic_types], i) topic_data = Enum.at(tail, i) {decoded} = ExW3.Abi.decode_data(topic_type, topic_data) decoded end) Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{}) else %{} end new_data = Map.merge(indexed_fields, non_indexed_fields) Map.put(log, "data", new_data) end def handle_call({:filter, {contract_name, event_name, event_data}}, _from, state) do contract_info = state[contract_name] event_signature = contract_info[:event_names][event_name] topic_types = contract_info[:events][event_signature][:topic_types] topic_names = contract_info[:events][event_signature][:topic_names] topics = filter_topics_helper(event_signature, event_data, topic_types, topic_names) payload = Map.merge( %{address: contract_info[:address], topics: topics}, event_data_format_helper(event_data) ) filter_id = ExW3.Rpc.new_filter(payload) {:reply, {:ok, filter_id}, Map.put( state, :filters, Map.put(state[:filters], filter_id, %{ contract_name: contract_name, event_name: event_name }) )} end def handle_call({:get_filter_changes, filter_id}, _from, state) do filter_info = Map.get(state[:filters], filter_id) event_attributes = get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name]) logs = ExW3.Rpc.get_filter_changes(filter_id) formatted_logs = if logs != [] do Enum.map(logs, fn log -> formatted_log = Enum.reduce( [ ExW3.Normalize.transform_to_integer(log, [ "blockNumber", "logIndex", "transactionIndex" ]), format_log_data(log, event_attributes) ], &Map.merge/2 ) formatted_log end) else logs end {:reply, {:ok, formatted_logs}, state} end def handle_call({:deploy, {name, args}}, _from, state) do contract_info = state[name] with {:ok, _} <- check_option(args[:options][:from], :missing_sender), {:ok, _} <- check_option(args[:options][:gas], :missing_gas), {:ok, bin} <- check_option([state[:bin], args[:bin]], :missing_binary) do {contract_addr, tx_hash} = deploy_helper(bin, contract_info[:abi], args) result = {:ok, contract_addr, tx_hash} {:reply, result, state} else err -> {:reply, err, state} end end def handle_call({:address, name}, _from, state) do {:reply, state[name][:address], state} end def handle_call({:call, {contract_name, method_name, args}}, _from, state) do contract_info = state[contract_name] with {:ok, address} <- check_option(contract_info[:address], :missing_address) do result = eth_call_helper(address, contract_info[:abi], Atom.to_string(method_name), args) {:reply, result, state} else err -> {:reply, err, state} end end def handle_call({:send, {contract_name, method_name, args, options}}, _from, state) do contract_info = state[contract_name] with {:ok, address} <- check_option(contract_info[:address], :missing_address), {:ok, _} <- check_option(options[:from], :missing_sender), {:ok, _} <- check_option(options[:gas], :missing_gas) do result = eth_send_helper( address, contract_info[:abi], Atom.to_string(method_name), args, options ) {:reply, result, state} else err -> {:reply, err, state} end end def handle_call({:tx_receipt, {contract_name, tx_hash}}, _from, state) do contract_info = state[contract_name] {:ok, receipt} = ExW3.tx_receipt(tx_hash) events = contract_info[:events] logs = receipt["logs"] formatted_logs = Enum.map(logs, fn log -> topic = Enum.at(log["topics"], 0) event_attributes = Map.get(events, topic) if event_attributes do non_indexed_fields = Enum.zip( event_attributes[:non_indexed_names], ExW3.Abi.decode_event(log["data"], event_attributes[:signature]) ) |> Enum.into(%{}) if length(log["topics"]) > 1 do [_head | tail] = log["topics"] decoded_topics = Enum.map(0..(length(tail) - 1), fn i -> topic_type = Enum.at(event_attributes[:topic_types], i) topic_data = Enum.at(tail, i) {decoded} = ExW3.Abi.decode_data(topic_type, topic_data) decoded end) indexed_fields = Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{}) Map.merge(indexed_fields, non_indexed_fields) else non_indexed_fields end else nil end end) {:reply, {:ok, {receipt, formatted_logs}}, state} end end ================================================ FILE: lib/exw3/normalize.ex ================================================ defmodule ExW3.Normalize do @spec transform_to_integer(map(), list()) :: map() def transform_to_integer(map, keys) do for k <- keys, into: %{} do {:ok, v} = map |> Map.get(k) |> ExW3.Utils.hex_to_integer() {k, v} end end end ================================================ FILE: lib/exw3/rpc.ex ================================================ defmodule ExW3.Rpc do import ExW3.Client @type invalid_hex_string_error :: ExW3.Utils.invalid_hex_string_error() @type request_error :: Ethereumex.Client.Behaviour.error() @type opts :: {:url, String.t()} @type hex_block_number :: String.t() @type latest :: String.t() @type earliest :: String.t() @type pending :: String.t() @doc "returns all available accounts" @spec accounts() :: list() @spec accounts([opts]) :: list() def accounts(opts \\ []) do case call_client(:eth_accounts, [opts]) do {:ok, accounts} -> accounts err -> err end end @doc "Returns the current block number" @spec block_number() :: {:ok, non_neg_integer} | {:error, ExW3.Utils.invalid_hex_string()} @spec block_number([opts]) :: {:ok, non_neg_integer} | {:error, ExW3.Utils.invalid_hex_string()} def block_number(opts \\ []) do case call_client(:eth_block_number, [opts]) do {:ok, hex_block_number} -> ExW3.Utils.hex_to_integer(hex_block_number) err -> err end end @doc "Returns current balance of account" @spec balance(binary()) :: integer() | {:error, any()} @spec balance(binary(), [opts]) :: integer() | {:error, any()} def balance(account, opts \\ []) do case call_client(:eth_get_balance, [account, "latest", opts]) do {:ok, hex_balance} -> {:ok, balance} = ExW3.Utils.hex_to_integer(hex_balance) balance err -> err end end @doc "Returns transaction receipt for specified transaction hash(id)" @spec tx_receipt(binary()) :: {:ok, map()} | {:error, any()} def tx_receipt(tx_hash) do case call_client(:eth_get_transaction_receipt, [tx_hash]) do {:ok, nil} -> {:error, :not_mined} {:ok, receipt} -> normalized_receipt = ExW3.Normalize.transform_to_integer(receipt, ~w(blockNumber cumulativeGasUsed gasUsed)) {:ok, Map.merge(receipt, normalized_receipt)} err -> {:error, err} end end @doc "Returns block data for specified block number" @spec block(integer()) :: any() | {:error, any()} def block(block_number) do case call_client(:eth_get_block_by_number, [block_number, true]) do {:ok, block} -> block err -> err end end @doc "Creates a new filter, returns filter id. For more sophisticated use, prefer ExW3.Contract.filter." @spec new_filter(map()) :: binary() | {:error, any()} def new_filter(map) do case call_client(:eth_new_filter, [map]) do {:ok, filter_id} -> filter_id err -> err end end @doc "Gets event changes (logs) by filter. Unlike ExW3.Contract.get_filter_changes it does not return the data in a formatted way" @spec get_filter_changes(binary()) :: any() def get_filter_changes(filter_id) do case call_client(:eth_get_filter_changes, [filter_id]) do {:ok, changes} -> changes err -> err end end @type log_filter :: %{ optional(:address) => String.t(), optional(:fromBlock) => hex_block_number | latest | earliest | pending, optional(:toBlock) => hex_block_number | latest | earliest | pending, optional(:topics) => [String.t()], optional(:blockhash) => String.t() } @spec get_logs(log_filter, [opts]) :: {:ok, list} | {:error, term} | request_error def get_logs(filter, opts \\ []) do with {:ok, _} = result <- call_client(:eth_get_logs, [filter, opts]) do result else err -> err end end @doc "Uninstalls filter from the ethereum node" @spec uninstall_filter(binary()) :: boolean() | {:error, any()} def uninstall_filter(filter_id) do case call_client(:eth_uninstall_filter, [filter_id]) do {:ok, result} -> result err -> err end end @doc "Mines number of blocks specified. Default is 1" @spec mine(integer()) :: any() | {:error, any()} def mine(num_blocks \\ 1) do for _ <- 0..(num_blocks - 1) do call_client(:request, ["evm_mine", [], []]) end end @doc "Using the personal api, returns list of accounts." @spec personal_list_accounts(list()) :: {:ok, list()} | {:error, any()} def personal_list_accounts(opts \\ []) do call_client(:request, ["personal_listAccounts", [], opts]) end @doc "Using the personal api, this method creates a new account with the passphrase, and returns new account address." @spec personal_new_account(binary(), list()) :: {:ok, binary()} | {:error, any()} def personal_new_account(password, opts \\ []) do call_client(:request, ["personal_newAccount", [password], opts]) end @doc "Using the personal api, this method unlocks account using the passphrase provided, and returns a boolean." @spec personal_unlock_account(binary(), list()) :: {:ok, boolean()} | {:error, any()} ### E.g. ExW3.personal_unlock_account(["0x1234","Password",30], []) def personal_unlock_account(params, opts \\ []) do call_client(:request, ["personal_unlockAccount", params, opts]) end @doc "Using the personal api, this method sends a transaction and signs it in one call, and returns a transaction id hash." @spec personal_send_transaction(map(), binary(), list()) :: {:ok, binary()} | {:error, any()} def personal_send_transaction(param_map, passphrase, opts \\ []) do call_client(:request, ["personal_sendTransaction", [param_map, passphrase], opts]) end @doc "Using the personal api, this method signs a transaction, and returns the signed transaction." @spec personal_sign_transaction(map(), binary(), list()) :: {:ok, map()} | {:error, any()} def personal_sign_transaction(param_map, passphrase, opts \\ []) do call_client(:request, ["personal_signTransaction", [param_map, passphrase], opts]) end @doc "Using the personal api, this method calculates an Ethereum specific signature, and returns that signature." @spec personal_sign(binary(), binary(), binary(), list()) :: {:ok, binary()} | {:error, any()} def personal_sign(data, address, passphrase, opts \\ []) do call_client(:request, ["personal_sign", [data, address, passphrase], opts]) end @doc "Using the personal api, this method returns the address associated with the private key that was used to calculate the signature with personal_sign." @spec personal_ec_recover(binary(), binary(), []) :: {:ok, binary()} | {:error, any()} def personal_ec_recover(data0, data1, opts \\ []) do call_client(:request, ["personal_ecRecover", [data0, data1], opts]) end @doc "Calculates an Ethereum specific signature and signs the data provided, using the accounts private key" @spec eth_sign(binary(), binary(), list()) :: {:ok, binary()} | {:error, any()} def eth_sign(data0, data1, opts \\ []) do call_client(:request, ["eth_sign", [data0, data1], opts]) end @doc "Simple eth_call to client. Recommended to use ExW3.Contract.call instead." @spec eth_call(list()) :: any() def eth_call(arguments) do call_client(:eth_call, arguments) end @doc "Simple eth_send_transaction. Recommended to use ExW3.Contract.send instead." @spec eth_send(list()) :: any() def eth_send(arguments) do call_client(:eth_send_transaction, arguments) end end ================================================ FILE: lib/exw3/utils.ex ================================================ defmodule ExW3.Utils do alias ExW3.Address @type invalid_hex_string :: :invalid_hex_string @type negative_integer :: :negative_integer @type non_integer :: :non_integer @type eth_hex :: String.t() @doc "Convert eth hex string to integer" @spec hex_to_integer(eth_hex) :: {:ok, non_neg_integer} | {:error, invalid_hex_string} def hex_to_integer(hex) do case hex do "0x" <> hex -> {:ok, String.to_integer(hex, 16)} _ -> {:error, :invalid_hex_string} end rescue ArgumentError -> {:error, :invalid_hex_string} end @doc "Convert an integer to eth hex string" @spec integer_to_hex(non_neg_integer) :: {:ok, eth_hex} | {:error, negative_integer | non_integer} def integer_to_hex(i) do case i do i when i < 0 -> {:error, :negative_integer} i -> {:ok, "0x" <> Integer.to_string(i, 16)} end rescue ArgumentError -> {:error, :non_integer} end @doc "Returns a 0x prepended 32 byte hash of the input string" @spec keccak256(String.t()) :: String.t() def keccak256(str) do "0x#{str |> ExKeccak.hash_256() |> Base.encode16(case: :lower)}" end @unit_map %{ :noether => 0, :wei => 1, :kwei => 1_000, :Kwei => 1_000, :babbage => 1_000, :femtoether => 1_000, :mwei => 1_000_000, :Mwei => 1_000_000, :lovelace => 1_000_000, :picoether => 1_000_000, :gwei => 1_000_000_000, :Gwei => 1_000_000_000, :shannon => 1_000_000_000, :nanoether => 1_000_000_000, :nano => 1_000_000_000, :szabo => 1_000_000_000_000, :microether => 1_000_000_000_000, :micro => 1_000_000_000_000, :finney => 1_000_000_000_000_000, :milliether => 1_000_000_000_000_000, :milli => 1_000_000_000_000_000, :ether => 1_000_000_000_000_000_000, :kether => 1_000_000_000_000_000_000_000, :grand => 1_000_000_000_000_000_000_000, :mether => 1_000_000_000_000_000_000_000_000, :gether => 1_000_000_000_000_000_000_000_000_000, :tether => 1_000_000_000_000_000_000_000_000_000_000 } @doc "Converts the value to whatever unit key is provided. See unit map for details." @spec to_wei(integer, atom) :: integer def to_wei(num, key) do if @unit_map[key] do num * @unit_map[key] else throw("#{key} not valid unit") end end @doc "Converts the value to whatever unit key is provided. See unit map for details." @spec from_wei(integer, atom) :: integer | float | no_return def from_wei(num, key) do if @unit_map[key] do num / @unit_map[key] else throw("#{key} not valid unit") end end @deprecated "Use ExW3.Address.to_checksum/1 instead." @doc "Returns a checksummed address conforming to EIP-55" @spec to_checksum_address(String.t()) :: String.t() def to_checksum_address(address) do address |> Address.from_hex() |> Address.to_checksum() end @deprecated "Use ExW3.Address.is_valid_checksum?/1 instead." @doc "Checks if the address is a valid checksummed address" @spec is_valid_checksum_address(String.t()) :: boolean def is_valid_checksum_address(address) do Address.is_valid_checksum?(address) end @doc "converts Ethereum style bytes to string" @spec bytes_to_string(binary()) :: binary() def bytes_to_string(bytes) do bytes |> Base.encode16(case: :lower) |> String.replace_trailing("0", "") |> Base.decode16!(case: :lower) end @doc "Converts an Ethereum address into a form that can be used by the ABI encoder" @spec format_address(binary()) :: integer() def format_address(address) do address |> String.slice(2..-1) |> Base.decode16!(case: :lower) |> :binary.decode_unsigned() end @deprecated "Use ExW3.Address.to_hex/1 instead." @doc "Converts bytes to Ethereum address" @spec to_address(binary()) :: binary() def to_address(bytes) do bytes |> Address.from_bytes() |> Address.to_hex() end end ================================================ FILE: lib/exw3.ex ================================================ defmodule ExW3 do defdelegate accounts(opts \\ []), to: ExW3.Rpc defdelegate block_number(opts \\ []), to: ExW3.Rpc defdelegate balance(account, opts \\ []), to: ExW3.Rpc defdelegate tx_receipt(tx_hash), to: ExW3.Rpc defdelegate block(block_number), to: ExW3.Rpc defdelegate new_filter(map), to: ExW3.Rpc defdelegate get_filter_changes(filter_id), to: ExW3.Rpc defdelegate get_logs(filter, opts \\ []), to: ExW3.Rpc defdelegate uninstall_filter(filter_id), to: ExW3.Rpc defdelegate mine(num_blocks \\ 1), to: ExW3.Rpc defdelegate personal_list_accounts(opts \\ []), to: ExW3.Rpc defdelegate personal_new_account(password, opts \\ []), to: ExW3.Rpc defdelegate personal_unlock_account(params, opts \\ []), to: ExW3.Rpc defdelegate personal_send_transaction(param_map, passphrase, opts \\ []), to: ExW3.Rpc defdelegate personal_sign_transaction(param_map, passphrase, opts \\ []), to: ExW3.Rpc defdelegate personal_sign(data, address, passphrase, opts \\ []), to: ExW3.Rpc defdelegate personal_ec_recover(data0, data1, opts \\ []), to: ExW3.Rpc defdelegate eth_sign(data0, data1, opts \\ []), to: ExW3.Rpc defdelegate eth_call(arguments), to: ExW3.Rpc defdelegate eth_send(arguments), to: ExW3.Rpc end ================================================ FILE: mix.exs ================================================ defmodule ExW3.MixProject do use Mix.Project @source_url "https://github.com/hswick/exw3" @version "0.6.1" def project do [ app: :exw3, version: @version, elixir: "~> 1.10", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, deps: deps(), docs: docs(), package: package(), name: "exw3", dialyzer: [ remove_defaults: [:unknown] ], preferred_cli_env: [ docs: :docs, "hex.publish": :docs ] ] end def application do [applications: [:logger, :ex_abi, :ethereumex]] end defp deps do [ {:ex_doc, ">= 0.0.0", only: :docs, runtime: false}, {:ethereumex, "~> 0.7.0"}, {:ex_keccak, "~> 0.2"}, {:ex_abi, "~> 0.5.4"}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:jason, "~> 1.2"} ] end defp package do [ name: "exw3", description: "A high level Ethereum JSON RPC Client for Elixir", files: ["lib", "mix.exs", "README*", "LICENSE*"], maintainers: ["Harley Swick"], licenses: ["Apache-2.0"], links: %{"GitHub" => @source_url} ] end defp docs do [ extras: [ LICENSE: [title: "License"], "README.md": [title: "Overview"] ], main: "readme", assets: "assets", source_url: @source_url, source_ref: "v#{@version}", formatters: ["html"] ] end end ================================================ FILE: parity.sh ================================================ parity --chain dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --reseal-min-period 0 --password passfile --jsonrpc-apis "web3,eth,personal,pubsub,net,parity,parity_pubsub,traces,rpc,secretstore" --ipc-apis "web3,eth,personal,pubsub,net,parity,parity_pubsub,parity_accounts,traces,rpc,secretstore" ================================================ FILE: test/examples/build/AddressTester.abi ================================================ [{"constant":true,"inputs":[{"name":"a","type":"address"}],"name":"get","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"pure","type":"function"}] ================================================ FILE: test/examples/build/ArrayTester.abi ================================================ [{"constant":true,"inputs":[{"name":"ints","type":"uint256[5]"}],"name":"staticUint","outputs":[{"name":"","type":"uint256[5]"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"ints","type":"uint256[]"}],"name":"dynamicUint","outputs":[{"name":"","type":"uint256[]"}],"payable":false,"stateMutability":"pure","type":"function"}] ================================================ FILE: test/examples/build/Complex.abi ================================================ [{"constant":true,"inputs":[],"name":"getBroAndBroBro","outputs":[{"name":"bro","type":"uint256"},{"name":"broBro","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBarFoo","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_foobar","type":"bytes32"}],"name":"setFooBar","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBoth","outputs":[{"name":"","type":"uint256"},{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_barFoo","type":"bool"}],"name":"setBarFoo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_fooboo","type":"uint256"}],"name":"getFooBoo","outputs":[{"name":"fooBoo","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getFooBar","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_foo","type":"uint256"}],"name":"setFoo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_foo","type":"uint256"},{"name":"_foobar","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"foo","type":"uint256"},{"indexed":false,"name":"person","type":"address"}],"name":"Bar","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"fooboo","type":"bool"},{"indexed":false,"name":"foo","type":"uint256"},{"indexed":false,"name":"foobar","type":"bytes32"}],"name":"FooBar","type":"event"}] ================================================ FILE: test/examples/build/EventTester.abi ================================================ [{"constant":false,"inputs":[{"name":"data","type":"bytes32"}],"name":"simpleIndex","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"data","type":"bytes32"}],"name":"simple","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"num","type":"uint256"},{"indexed":false,"name":"data","type":"bytes32"}],"name":"Simple","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"num","type":"uint256"},{"indexed":true,"name":"data","type":"bytes32"},{"indexed":false,"name":"otherNum","type":"uint256"}],"name":"SimpleIndex","type":"event"}] ================================================ FILE: test/examples/build/SimpleStorage.abi ================================================ [{"constant":false,"inputs":[{"name":"_data","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"}] ================================================ FILE: test/examples/contracts/AddressTester.sol ================================================ pragma solidity ^0.4.18; contract AddressTester { function get(address a) public pure returns (address) { return a; } } ================================================ FILE: test/examples/contracts/ArrayTester.sol ================================================ pragma solidity ^0.4.18; contract ArrayTester { function dynamicUint(uint[] ints) public pure returns (uint[]) { return ints; } function staticUint(uint[5] ints) public pure returns (uint[5]) { return ints; } } ================================================ FILE: test/examples/contracts/Complex.sol ================================================ pragma solidity ^0.4.0; contract Complex { uint foo; bytes32 foobar; bool barFoo; event Bar(uint foo, address person); event FooBar(bool fooboo, uint foo, bytes32 foobar); constructor(uint _foo, bytes32 _foobar) public { foo = _foo; foobar = _foobar; } function getBoth() public view returns (uint, bytes32) { return (foo, foobar); } function getBarFoo() public view returns (bool) { return barFoo; } function getFooBar() public view returns (bytes32) { return foobar; } function getFooBoo(uint _fooboo) public pure returns (uint fooBoo) { fooBoo = _fooboo + 42; } function getBroAndBroBro() public view returns (uint bro, bytes32 broBro) { return (foo + 42, foobar); } function setFoo(uint _foo) public { foo = _foo; } function setFooBar(bytes32 _foobar) public { foobar = _foobar; } function setBarFoo(bool _barFoo) public { barFoo = _barFoo; } function() public payable { } } ================================================ FILE: test/examples/contracts/EventTester.sol ================================================ pragma solidity ^0.4.18; contract EventTester { event Simple(uint256 num, bytes32 data); event SimpleIndex(uint256 indexed num, bytes32 indexed data, uint256 otherNum); function simple(bytes32 data) public { emit Simple(42, data); } function simpleIndex(bytes32 data) public { emit SimpleIndex(46, data, 42); } } ================================================ FILE: test/examples/contracts/SimpleStorage.sol ================================================ pragma solidity ^0.4.0; contract SimpleStorage { uint data; function set(uint _data) public { data = _data; } function get() public view returns (uint) { return data; } } ================================================ FILE: test/exw3/abi_test.exs ================================================ defmodule ExW3.AbiTest do use ExUnit.Case test ".load_abi/1 returns a map keyed by function & event name" do assert ExW3.Abi.load_abi("test/examples/build/SimpleStorage.abi") == %{ "get" => %{ "constant" => true, "inputs" => [], "name" => "get", "outputs" => [%{"name" => "", "type" => "uint256"}], "payable" => false, "stateMutability" => "view", "type" => "function" }, "set" => %{ "constant" => false, "inputs" => [%{"name" => "_data", "type" => "uint256"}], "name" => "set", "outputs" => [], "payable" => false, "stateMutability" => "nonpayable", "type" => "function" } } end end ================================================ FILE: test/exw3/address_test.exs ================================================ defmodule ExW3.AddressTest do use ExUnit.Case alias ExW3.Address @bytes_address <<25, 154, 209, 226, 223, 37, 243, 29, 135, 172, 137, 205, 225, 158, 82, 44, 130, 62, 90, 166, 30>> @string_address "199ad1e2df25f31d87ac89cde19e522c823e5aa61e" @hex_address "0x199ad1e2df25f31d87ac89cde19e522c823e5aa61e" @checksum_address "0x199ad1E2dF25f31D87AC89cdE19E522c823E5AA61e" test ".from_bytes/1 returns an address struct" do address = Address.from_bytes(@bytes_address) assert address.bytes == @bytes_address end test ".from_hex/1 returns an address struct with and without the checksum" do address = Address.from_hex(@hex_address) assert address.bytes == @bytes_address from_checksum_address = Address.from_hex(@checksum_address) assert from_checksum_address.bytes == @bytes_address end test ".to_bytes/1 returns the bytes of the address struct" do address = %Address{bytes: @bytes_address} assert Address.to_bytes(address) == @bytes_address end test ".to_string/1 returns the hex encoded string without a 0x prefix" do address = %Address{bytes: @bytes_address} assert Address.to_string(address) == @string_address end test ".to_hex/1 returns the hex encoded string with a 0x prefix" do address = %Address{bytes: @bytes_address} assert Address.to_hex(address) == @hex_address end test ".to_checksum/1 returns the hex encoded string with a 0x prefix conforming to EIP-55" do address = %Address{bytes: @bytes_address} assert Address.to_checksum(address) == @checksum_address end test ".is_valid_checksum?/1 is true when it conforms to EIP-55" do assert Address.is_valid_checksum?(@checksum_address) == true assert Address.is_valid_checksum?(@hex_address) == false end end ================================================ FILE: test/exw3/client_test.exs ================================================ defmodule ExW3.ClientTest do use ExUnit.Case test ".call_client/1 calls the JSON-RPC method with an empty list of arguments" do assert {:ok, hex} = ExW3.Client.call_client(:eth_block_number) assert "0x" <> _ = hex end test ".call_client/2 calls the JSON-RPC method with the given arguments" do assert {:ok, accounts} = ExW3.Client.call_client(:eth_accounts) assert Enum.count(accounts) > 0 assert ["0x" <> _ = account] = accounts assert {:ok, balance} = ExW3.Client.call_client(:eth_get_balance, [account]) assert "0x" <> _ = balance end test ".call_client/2 can specifiy a http url with & without params" do assert {:ok, accounts} = ExW3.Client.call_client(:eth_accounts, [[url: Ethereumex.Config.rpc_url()]]) account = Enum.at(accounts, 0) assert {:ok, "0x" <> _} = ExW3.Client.call_client(:eth_get_balance, [ account, "latest", [url: Ethereumex.Config.rpc_url()] ]) assert ExW3.Client.call_client(:eth_get_balance, [account, "latest", [url: "unsupported"]]) == {:error, :invalid_client_type} assert ExW3.Client.call_client(:eth_get_balance, [ account, "latest", [url: "http://localhost:1234"] ]) == {:error, :econnrefused} assert ExW3.Client.call_client(:eth_get_balance, [ account, "latest", [url: "https://localhost:1234"] ]) == {:error, :econnrefused} end end ================================================ FILE: test/exw3/contract_test.exs ================================================ defmodule EXW3.ContractTest do use ExUnit.Case doctest ExW3.Contract @simple_storage_abi ExW3.Abi.load_abi("test/examples/build/SimpleStorage.abi") setup_all do start_supervised!(ExW3.Contract) :ok end test ".at assigns the address to the state of the registered contract" do ExW3.Contract.register(:SimpleStorage, abi: @simple_storage_abi) assert ExW3.Contract.address(:SimpleStorage) == nil accounts = ExW3.accounts() {:ok, address, _} = ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ gas: 300_000, from: Enum.at(accounts, 0) } ) assert ExW3.Contract.at(:SimpleStorage, address) == :ok state = :sys.get_state(ContractManager) contract_state = state[:SimpleStorage] assert Keyword.get(contract_state, :address) == address assert Keyword.get(contract_state, :abi) == @simple_storage_abi end test ".address returns the registered address for the contract" do ExW3.Contract.register(:SimpleStorage, abi: @simple_storage_abi) assert ExW3.Contract.address(:SimpleStorage) == nil accounts = ExW3.accounts() {:ok, address, _} = ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ gas: 300_000, from: Enum.at(accounts, 0) } ) assert ExW3.Contract.at(:SimpleStorage, address) == :ok assert ExW3.Contract.address(:SimpleStorage) == address end end ================================================ FILE: test/exw3/rpc_test.exs ================================================ defmodule ExW3.RpcTest do use ExUnit.Case describe ".accounts" do test "returns a list from the eth_accounts JSON-RPC endpoint" do assert ExW3.accounts() |> is_list end test "can override the http endpoint" do assert ExW3.accounts(url: Ethereumex.Config.rpc_url()) |> is_list assert ExW3.accounts(url: "https://localhost:1234") == {:error, :econnrefused} end end describe ".block_number" do test "returns the integer block number from the eth_blockNumber JSON-RPC endpoint" do assert {:ok, bn} = ExW3.block_number() assert bn |> is_integer end test "can override the http endpoint" do assert {:ok, _} = ExW3.block_number(url: Ethereumex.Config.rpc_url()) assert ExW3.block_number(url: "https://localhost:1234") == {:error, :econnrefused} end end describe ".balance" do test "returns the latest integer balance from the eth_getBalance JSON-RPC endpoint" do account = ExW3.accounts() |> Enum.at(0) assert ExW3.balance(account) |> is_integer end test "can override the http endpoint" do account = ExW3.accounts() |> Enum.at(0) assert ExW3.balance(account, url: Ethereumex.Config.rpc_url()) |> is_integer assert ExW3.balance(account, url: "https://localhost:1234") == {:error, :econnrefused} end end end ================================================ FILE: test/exw3/utils_test.exs ================================================ defmodule ExW3.UtilsTest do use ExUnit.Case doctest ExW3.Utils describe ".hex_to_integer/1" do test "parses a hex encoded string to an integer" do assert ExW3.Utils.hex_to_integer("0x1") == {:ok, 1} assert ExW3.Utils.hex_to_integer("0x2") == {:ok, 2} assert ExW3.Utils.hex_to_integer("0x2a") == {:ok, 42} assert ExW3.Utils.hex_to_integer("0x2A") == {:ok, 42} end test "returns an error when the string is not a valid hexidecimal" do assert ExW3.Utils.hex_to_integer("0x") == {:error, :invalid_hex_string} assert ExW3.Utils.hex_to_integer("0a") == {:error, :invalid_hex_string} assert ExW3.Utils.hex_to_integer("0xZ") == {:error, :invalid_hex_string} end end describe ".integer_to_hex/1" do test "encodes an integer to hexadecimal" do assert ExW3.Utils.integer_to_hex(0) == {:ok, "0x0"} assert ExW3.Utils.integer_to_hex(1) == {:ok, "0x1"} assert ExW3.Utils.integer_to_hex(42) == {:ok, "0x2A"} end test "returns an error when the integer is negative" do assert ExW3.Utils.integer_to_hex(-1) == {:error, :negative_integer} end test "returns an error when the value is not an integer" do assert ExW3.Utils.integer_to_hex(1.1) == {:error, :non_integer} assert ExW3.Utils.integer_to_hex(nil) == {:error, :non_integer} end end describe ".keccak256/1" do test "returns a 0x prepended 32 byte hash of the input" do hex_hash = ExW3.Utils.keccak256("foo") assert "0x" <> hash = hex_hash assert hash == "41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d" num_bytes = byte_size(hash) assert trunc(num_bytes / 2) == 32 end end describe ".to_wei/2" do test "converts a unit to_wei" do assert ExW3.Utils.to_wei(1, :wei) == 1 assert ExW3.Utils.to_wei(1, :kwei) == 1_000 assert ExW3.Utils.to_wei(1, :Kwei) == 1_000 assert ExW3.Utils.to_wei(1, :babbage) == 1_000 assert ExW3.Utils.to_wei(1, :mwei) == 1_000_000 assert ExW3.Utils.to_wei(1, :Mwei) == 1_000_000 assert ExW3.Utils.to_wei(1, :lovelace) == 1_000_000 assert ExW3.Utils.to_wei(1, :gwei) == 1_000_000_000 assert ExW3.Utils.to_wei(1, :Gwei) == 1_000_000_000 assert ExW3.Utils.to_wei(1, :shannon) == 1_000_000_000 assert ExW3.Utils.to_wei(1, :szabo) == 1_000_000_000_000 assert ExW3.Utils.to_wei(1, :finney) == 1_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :ether) == 1_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :kether) == 1_000_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :grand) == 1_000_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :mether) == 1_000_000_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :gether) == 1_000_000_000_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :tether) == 1_000_000_000_000_000_000_000_000_000_000 assert ExW3.Utils.to_wei(1, :kwei) == ExW3.Utils.to_wei(1, :femtoether) assert ExW3.Utils.to_wei(1, :szabo) == ExW3.Utils.to_wei(1, :microether) assert ExW3.Utils.to_wei(1, :finney) == ExW3.Utils.to_wei(1, :milliether) assert ExW3.Utils.to_wei(1, :milli) == ExW3.Utils.to_wei(1, :milliether) assert ExW3.Utils.to_wei(1, :milli) == ExW3.Utils.to_wei(1000, :micro) {:ok, agent} = Agent.start_link(fn -> false end) try do ExW3.Utils.to_wei(1, :wei1) catch _ -> Agent.update(agent, fn _ -> true end) end assert Agent.get(agent, fn state -> state end) end end describe ".from_wei/2" do test "converts a unit from wei" do assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :wei) == 1_000_000_000_000_000_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :kwei) == 1_000_000_000_000_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :mwei) == 1_000_000_000_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :gwei) == 1_000_000_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :szabo) == 1_000_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :finney) == 1_000 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :ether) == 1 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :kether) == 0.001 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :grand) == 0.001 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :mether) == 0.000001 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :gether) == 0.000000001 assert ExW3.Utils.from_wei(1_000_000_000_000_000_000, :tether) == 0.000000000001 end end describe ".to_checksum_address/1" do test "returns checksum for all caps address" do assert ExW3.Utils.to_checksum_address( String.downcase("0x52908400098527886E0F7030069857D2E4169EE7") ) == "0x52908400098527886E0F7030069857D2E4169EE7" assert ExW3.Utils.to_checksum_address( String.downcase("0x8617E340B3D01FA5F11F306F4090FD50E238070D") ) == "0x8617E340B3D01FA5F11F306F4090FD50E238070D" end test "returns checksumfor all lowercase address" do assert ExW3.Utils.to_checksum_address( String.downcase("0xde709f2102306220921060314715629080e2fb77") ) == "0xde709f2102306220921060314715629080e2fb77" assert ExW3.Utils.to_checksum_address( String.downcase("0x27b1fdb04752bbc536007a920d24acb045561c26") ) == "0x27b1fdb04752bbc536007a920d24acb045561c26" end test "returns checksum for normal addresses" do assert ExW3.Utils.to_checksum_address( String.downcase("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") ) == "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" assert ExW3.Utils.to_checksum_address( String.downcase("0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359") ) == "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" assert ExW3.Utils.to_checksum_address( String.downcase("0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB") ) == "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB" assert ExW3.Utils.to_checksum_address( String.downcase("0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb") ) == "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" end end describe ".is_valid_checksum_address/1" do test "returns valid check for is_valid_checksum_address()" do assert ExW3.Utils.is_valid_checksum_address("0x52908400098527886E0F7030069857D2E4169EE7") == true assert ExW3.Utils.is_valid_checksum_address("0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB") == true assert ExW3.Utils.is_valid_checksum_address("0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb") == true assert ExW3.Utils.is_valid_checksum_address("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") == true assert ExW3.Utils.is_valid_checksum_address("0x27b1fdb04752bbc536007a920d24acb045561c26") == true assert ExW3.Utils.is_valid_checksum_address("0xde709f2102306220921060314715629080e2fb77") == true assert ExW3.Utils.is_valid_checksum_address("0x8617E340B3D01FA5F11F306F4090FD50E238070D") == true assert ExW3.Utils.is_valid_checksum_address("0x52908400098527886E0F7030069857D2E4169EE7") == true end test "returns invalid check for is_valid_checksum_address()" do assert ExW3.Utils.is_valid_checksum_address("0x2f015c60e0be116b1f0cd534704db9c92118fb6a") == false end end end ================================================ FILE: test/exw3_test.exs ================================================ defmodule ExW3Test do use ExUnit.Case doctest ExW3 setup_all do start_supervised!(ExW3.Contract) %{ simple_storage_abi: ExW3.Abi.load_abi("test/examples/build/SimpleStorage.abi"), array_tester_abi: ExW3.Abi.load_abi("test/examples/build/ArrayTester.abi"), event_tester_abi: ExW3.Abi.load_abi("test/examples/build/EventTester.abi"), complex_abi: ExW3.Abi.load_abi("test/examples/build/Complex.abi"), address_tester_abi: ExW3.Abi.load_abi("test/examples/build/AddressTester.abi"), accounts: ExW3.accounts() } end # Only works with ganache-cli # test "mines a block" do # block_number = ExW3.block_number() # ExW3.mine() # assert ExW3.block_number() == block_number + 1 # end # test "mines multiple blocks" do # block_number = ExW3.block_number() # ExW3.mine(5) # assert ExW3.block_number() == block_number + 5 # end test "starts a Contract GenServer for simple storage contract", context do ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) {:ok, address, _} = ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) ExW3.Contract.at(:SimpleStorage, address) assert address == ExW3.Contract.address(:SimpleStorage) {:ok, data} = ExW3.Contract.call(:SimpleStorage, :get) assert data == 0 ExW3.Contract.send(:SimpleStorage, :set, [1], %{ from: Enum.at(context[:accounts], 0), gas: 50_000 }) {:ok, data} = ExW3.Contract.call(:SimpleStorage, :get) assert data == 1 end test "starts a Contract GenServer for array tester contract", context do ExW3.Contract.register(:ArrayTester, abi: context[:array_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( :ArrayTester, bin: ExW3.Abi.load_bin("test/examples/build/ArrayTester.bin"), options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) ExW3.Contract.at(:ArrayTester, address) assert address == ExW3.Contract.address(:ArrayTester) arr = [1, 2, 3, 4, 5] {:ok, result} = ExW3.Contract.call(:ArrayTester, :staticUint, [arr]) assert result == arr {:ok, result} = ExW3.Contract.call(:ArrayTester, :dynamicUint, [arr]) assert result == arr end test "starts a Contract GenServer for event tester contract", context do ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( :EventTester, bin: ExW3.Abi.load_bin("test/examples/build/EventTester.bin"), options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) ExW3.Contract.at(:EventTester, address) assert address == ExW3.Contract.address(:EventTester) {:ok, tx_hash} = ExW3.Contract.send(:EventTester, :simple, ["Hello, World!"], %{ from: Enum.at(context[:accounts], 0), gas: 30_000 }) {:ok, {receipt, logs}} = ExW3.Contract.tx_receipt(:EventTester, tx_hash) assert receipt |> is_map data = logs |> Enum.at(0) |> Map.get("data") |> ExW3.Utils.bytes_to_string() assert data == "Hello, World!" {:ok, tx_hash2} = ExW3.Contract.send(:EventTester, :simpleIndex, ["Hello, World!"], %{ from: Enum.at(context[:accounts], 0), gas: 30_000 }) {:ok, {_receipt, logs}} = ExW3.Contract.tx_receipt(:EventTester, tx_hash2) otherNum = logs |> Enum.at(0) |> Map.get("otherNum") assert otherNum == 42 num = logs |> Enum.at(0) |> Map.get("num") assert num == 46 data = logs |> Enum.at(0) |> Map.get("data") |> ExW3.Utils.bytes_to_string() assert data == "Hello, World!" end test "Testing formatted get filter changes", context do ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( :EventTester, bin: ExW3.Abi.load_bin("test/examples/build/EventTester.bin"), options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) ExW3.Contract.at(:EventTester, address) # Test non indexed events {:ok, filter_id} = ExW3.Contract.filter(:EventTester, "Simple") {:ok, _tx_hash} = ExW3.Contract.send( :EventTester, :simple, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) {:ok, change_logs} = ExW3.Contract.get_filter_changes(filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 42 assert ExW3.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" ExW3.Contract.uninstall_filter(filter_id) # Test indexed events {:ok, indexed_filter_id} = ExW3.Contract.filter(:EventTester, "SimpleIndex") {:ok, _tx_hash} = ExW3.Contract.send( :EventTester, :simpleIndex, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 46 assert ExW3.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 ExW3.Contract.uninstall_filter(indexed_filter_id) # Test Indexing Indexed Events {:ok, indexed_filter_id} = ExW3.Contract.filter( :EventTester, "SimpleIndex", %{ topics: [nil, ["Hello, World", "Hello, World!"]], fromBlock: 1, toBlock: "latest" } ) {:ok, _tx_hash} = ExW3.Contract.send( :EventTester, :simpleIndex, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 46 assert ExW3.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 ExW3.Contract.uninstall_filter(indexed_filter_id) # Tests filter with map params {:ok, indexed_filter_id} = ExW3.Contract.filter( :EventTester, "SimpleIndex", %{ topics: %{num: 46, data: "Hello, World!"} } ) {:ok, _tx_hash} = ExW3.Contract.send( :EventTester, :simpleIndex, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) # Demonstrating the delay capability {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 46 assert ExW3.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 ExW3.Contract.uninstall_filter(indexed_filter_id) end test "starts a Contract GenServer for Complex contract", context do ExW3.Contract.register(:Complex, abi: context[:complex_abi]) {:ok, address, _} = ExW3.Contract.deploy( :Complex, bin: ExW3.Abi.load_bin("test/examples/build/Complex.bin"), args: [42, "Hello, world!"], options: %{ from: Enum.at(context[:accounts], 0), gas: 300_000 } ) ExW3.Contract.at(:Complex, address) assert address == ExW3.Contract.address(:Complex) {:ok, foo, foobar} = ExW3.Contract.call(:Complex, :getBoth) assert foo == 42 assert ExW3.Utils.bytes_to_string(foobar) == "Hello, world!" end test "starts a Contract GenServer for AddressTester contract", context do ExW3.Contract.register(:AddressTester, abi: context[:address_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( :AddressTester, bin: ExW3.Abi.load_bin("test/examples/build/AddressTester.bin"), options: %{ from: Enum.at(context[:accounts], 0), gas: 300_000 } ) ExW3.Contract.at(:AddressTester, address) assert address == ExW3.Contract.address(:AddressTester) formatted_address = Enum.at(context[:accounts], 0) |> ExW3.Utils.format_address() {:ok, same_address} = ExW3.Contract.call(:AddressTester, :get, [formatted_address]) assert %ExW3.Address{bytes: same_address} |> ExW3.Address.to_hex() == Enum.at(context[:accounts], 0) end test "returns proper error messages at contract deployment", context do ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) assert {:error, :missing_gas} == ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ from: Enum.at(context[:accounts], 0) } ) assert {:error, :missing_sender} == ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ gas: 300_000 } ) assert {:error, :missing_binary} == ExW3.Contract.deploy( :SimpleStorage, args: [], options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) end test "return proper error messages at send and call", context do ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) {:ok, address, _} = ExW3.Contract.deploy( :SimpleStorage, bin: ExW3.Abi.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) assert {:error, :missing_address} == ExW3.Contract.call(:SimpleStorage, :get) assert {:error, :missing_address} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{ from: Enum.at(context[:accounts], 0), gas: 50_000 }) ExW3.Contract.at(:SimpleStorage, address) assert {:error, :missing_sender} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{gas: 50_000}) assert {:error, :missing_gas} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)}) end test ".get_logs/1", context do ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( :EventTester, bin: ExW3.Abi.load_bin("test/examples/build/EventTester.bin"), options: %{ gas: 300_000, from: Enum.at(context[:accounts], 0) } ) ExW3.Contract.at(:EventTester, address) {:ok, current_block} = ExW3.block_number() {:ok, from_block} = ExW3.Utils.integer_to_hex(current_block) {:ok, simple_tx_hash} = ExW3.Contract.send(:EventTester, :simple, ["Hello, World!"], %{ from: Enum.at(context[:accounts], 0), gas: 30_000 }) {:ok, _} = ExW3.Contract.send(:EventTester, :simpleIndex, ["Hello, World!"], %{ from: Enum.at(context[:accounts], 0), gas: 30_000 }) filter = %{ fromBlock: from_block, toBlock: "latest", topics: [ExW3.Utils.keccak256("Simple(uint256,bytes32)")] } assert {:ok, logs} = ExW3.get_logs(filter) assert Enum.count(logs) == 1 log = Enum.at(logs, 0) assert log["transactionHash"] == simple_tx_hash end end ================================================ FILE: test/test_helper.exs ================================================ ExUnit.start()