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
[](https://github.com/hswick/exw3/actions?query=workflow%3Atest)
[](https://hex.pm/packages/exw3)
[](https://hexdocs.pm/exw3/)
[](https://hex.pm/packages/exw3)
[](https://github.com/hswick/exw3/blob/master/LICENSE)
[](https://github.com/hswick/exw3/commits/master)
<p align="center">
<img src="./assets/exw3_logo.jpg"/>
</p>
## 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
<<init::binary-size(4), _rest::binary>> = 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
<<init::binary-size(4), _rest::binary>> = 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()
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
SYMBOL INDEX (121 symbols across 16 files)
FILE: lib/exw3.ex
class ExW3 (line 1) | defmodule ExW3
FILE: lib/exw3/abi.ex
class ExW3.Abi (line 1) | defmodule ExW3.Abi
method decode_event (line 4) | def decode_event(data, signature) do
method load_abi (line 17) | def load_abi(file_path) do
method load_bin (line 26) | def load_bin(file_path) do
method decode_data (line 35) | def decode_data(types_signature, data) do
method decode_output (line 43) | def decode_output(abi, name, output) do
method types_signature (line 61) | def types_signature(abi, name) do
method method_signature (line 69) | def method_signature(abi, name) do
method encode_data (line 83) | def encode_data(types_signature, data) do
method encode_options (line 92) | def encode_options(options, keys) do
method encode_option (line 105) | def encode_option(0), do: "0x0"
method encode_option (line 107) | def encode_option(nil), do: nil
method encode_option (line 109) | def encode_option(value) do
method encode_method_call (line 119) | def encode_method_call(abi, name, input) do
method encode_input (line 128) | def encode_input(abi, name, input) do
method reformat_abi (line 150) | defp reformat_abi(abi) do
method map_abi (line 156) | defp map_abi(x) do
FILE: lib/exw3/address.ex
class ExW3.Address (line 1) | defmodule ExW3.Address
method from_bytes (line 7) | def from_bytes(bytes) do
method from_hex (line 12) | def from_hex(address) do
method to_bytes (line 24) | def to_bytes(%__MODULE__{bytes: bytes}) do
method to_string (line 29) | def to_string(%__MODULE__{bytes: bytes}) do
method to_hex (line 34) | def to_hex(%__MODULE__{} = address) do
method to_checksum (line 39) | def to_checksum(%__MODULE__{} = address) do
method is_valid_checksum? (line 62) | def is_valid_checksum?(hex_address) do
FILE: lib/exw3/client.ex
class ExW3.Client (line 1) | defmodule ExW3.Client
method call_client (line 8) | def call_client(method_name, arguments \\ []) do
method extract_url_opt (line 18) | defp extract_url_opt(arguments) do
method client_type (line 27) | defp client_type(nil), do: Application.get_env(:ethereumex, :client_ty...
method client_type (line 28) | defp client_type("http://" <> _), do: :http
method client_type (line 29) | defp client_type("https://" <> _), do: :http
method client_type (line 30) | defp client_type(_), do: :invalid
FILE: lib/exw3/contract.ex
class ExW3.Contract (line 1) | defmodule ExW3.Contract
method start_link (line 6) | def start_link(_ \\ :ok) do
method deploy (line 12) | def deploy(name, args) do
method register (line 18) | def register(name, contract_info) do
method uninstall_filter (line 24) | def uninstall_filter(filter_id) do
method at (line 30) | def at(name, address) do
method address (line 36) | def address(name) do
method call (line 42) | def call(contract_name, method_name, args \\ [], timeout \\ :infinity) do
method send (line 48) | def send(contract_name, method_name, args, options) do
method tx_receipt (line 54) | def tx_receipt(contract_name, tx_hash) do
method filter (line 60) | def filter(contract_name, event_name, event_data \\ %{}) do
method get_filter_changes (line 69) | def get_filter_changes(filter_id) do
method init (line 76) | def init(state) do
method data_signature_helper (line 80) | defp data_signature_helper(name, fields) do
method topic_types_helper (line 85) | defp topic_types_helper(fields) do
method init_events (line 95) | defp init_events(abi) do
method deploy_helper (line 155) | def deploy_helper(bin, abi, args) do
method eth_call_helper (line 203) | def eth_call_helper(address, abi, method_name, args) do
method eth_send_helper (line 221) | def eth_send_helper(address, abi, method_name, args, options) do
method register_helper (line 244) | defp register_helper(contract_info) do
method check_option (line 254) | defp check_option(nil, error_atom), do: {:error, error_atom}
method check_option (line 255) | defp check_option([], error_atom), do: {:error, error_atom}
method check_option (line 257) | defp check_option([_head | tail], atom), do: check_option(tail, atom)
method check_option (line 258) | defp check_option(value, _atom), do: {:ok, value}
method handle_cast (line 262) | def handle_cast({:at, {name, address}}, state) do
method handle_cast (line 269) | def handle_cast({:register, {name, contract_info}}, state) do
method handle_cast (line 273) | def handle_cast({:uninstall_filter, filter_id}, state) do
method filter_topics_helper (line 280) | defp filter_topics_helper(event_signature, event_data, topic_types, to...
method from_block_helper (line 317) | def from_block_helper(event_data) do
method param_helper (line 332) | defp param_helper(event_data, key) do
method event_data_format_helper (line 349) | defp event_data_format_helper(event_data) do
method get_event_attributes (line 356) | def get_event_attributes(state, contract_name, event_name) do
method extract_non_indexed_fields (line 361) | defp extract_non_indexed_fields(data, names, signature) do
method format_log_data (line 365) | defp format_log_data(log, event_attributes) do
method handle_call (line 397) | def handle_call({:filter, {contract_name, event_name, event_data}}, _f...
method handle_call (line 425) | def handle_call({:get_filter_changes, filter_id}, _from, state) do
method handle_call (line 458) | def handle_call({:deploy, {name, args}}, _from, state) do
method handle_call (line 472) | def handle_call({:address, name}, _from, state) do
method handle_call (line 476) | def handle_call({:call, {contract_name, method_name, args}}, _from, st...
method handle_call (line 487) | def handle_call({:send, {contract_name, method_name, args, options}}, ...
method handle_call (line 508) | def handle_call({:tx_receipt, {contract_name, tx_hash}}, _from, state) do
FILE: lib/exw3/normalize.ex
class ExW3.Normalize (line 1) | defmodule ExW3.Normalize
method transform_to_integer (line 3) | def transform_to_integer(map, keys) do
FILE: lib/exw3/rpc.ex
class ExW3.Rpc (line 1) | defmodule ExW3.Rpc
method accounts (line 15) | def accounts(opts \\ []) do
method block_number (line 25) | def block_number(opts \\ []) do
method balance (line 35) | def balance(account, opts \\ []) do
method tx_receipt (line 48) | def tx_receipt(tx_hash) do
method block (line 66) | def block(block_number) do
method new_filter (line 75) | def new_filter(map) do
method get_filter_changes (line 84) | def get_filter_changes(filter_id) do
method get_logs (line 100) | def get_logs(filter, opts \\ []) do
method uninstall_filter (line 110) | def uninstall_filter(filter_id) do
method mine (line 119) | def mine(num_blocks \\ 1) do
method personal_list_accounts (line 127) | def personal_list_accounts(opts \\ []) do
method personal_new_account (line 133) | def personal_new_account(password, opts \\ []) do
method personal_unlock_account (line 140) | def personal_unlock_account(params, opts \\ []) do
method personal_send_transaction (line 146) | def personal_send_transaction(param_map, passphrase, opts \\ []) do
method personal_sign_transaction (line 152) | def personal_sign_transaction(param_map, passphrase, opts \\ []) do
method personal_sign (line 158) | def personal_sign(data, address, passphrase, opts \\ []) do
method personal_ec_recover (line 164) | def personal_ec_recover(data0, data1, opts \\ []) do
method eth_sign (line 170) | def eth_sign(data0, data1, opts \\ []) do
method eth_call (line 176) | def eth_call(arguments) do
method eth_send (line 182) | def eth_send(arguments) do
FILE: lib/exw3/utils.ex
class ExW3.Utils (line 1) | defmodule ExW3.Utils
method hex_to_integer (line 12) | def hex_to_integer(hex) do
method integer_to_hex (line 25) | def integer_to_hex(i) do
method keccak256 (line 37) | def keccak256(str) do
method to_wei (line 73) | def to_wei(num, key) do
method from_wei (line 83) | def from_wei(num, key) do
method to_checksum_address (line 94) | def to_checksum_address(address) do
method is_valid_checksum_address (line 103) | def is_valid_checksum_address(address) do
method bytes_to_string (line 109) | def bytes_to_string(bytes) do
method format_address (line 118) | def format_address(address) do
method to_address (line 128) | def to_address(bytes) do
FILE: mix.exs
class ExW3.MixProject (line 1) | defmodule ExW3.MixProject
method project (line 7) | def project do
method application (line 28) | def application do
method deps (line 32) | defp deps do
method package (line 43) | defp package do
method docs (line 54) | defp docs do
FILE: test/exw3/abi_test.exs
class ExW3.AbiTest (line 1) | defmodule ExW3.AbiTest
FILE: test/exw3/address_test.exs
class ExW3.AddressTest (line 1) | defmodule ExW3.AddressTest
FILE: test/exw3/client_test.exs
class ExW3.ClientTest (line 1) | defmodule ExW3.ClientTest
FILE: test/exw3/contract_test.exs
class EXW3.ContractTest (line 1) | defmodule EXW3.ContractTest
FILE: test/exw3/rpc_test.exs
class ExW3.RpcTest (line 1) | defmodule ExW3.RpcTest
FILE: test/exw3/utils_test.exs
class ExW3.UtilsTest (line 1) | defmodule ExW3.UtilsTest
FILE: test/exw3_test.exs
class ExW3Test (line 1) | defmodule ExW3Test
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (101K chars).
[
{
"path": ".formatter.exs",
"chars": 97,
"preview": "# Used by \"mix format\"\n[\n inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
},
{
"path": ".github/dependabot.yml",
"chars": 245,
"preview": "version: 2\nupdates:\n- package-ecosystem: mix\n directory: \"/\"\n schedule:\n interval: monthly\n open-pull-requests-lim"
},
{
"path": ".github/install_parity.sh",
"chars": 313,
"preview": "# Install Parity blockchain tests on Github action\necho > passfile # just to be safe\n\nwget https://releases.parity.io/et"
},
{
"path": ".github/workflows/test.yml",
"chars": 1570,
"preview": "name: test\n\non:\n push:\n branches:\n - master\n pull_request:\n branches:\n - '*'\n\njobs:\n test:\n runs-o"
},
{
"path": ".gitignore",
"chars": 670,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": "LICENSE",
"chars": 10173,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 8441,
"preview": "# ExW3\n\n[](https://github.com/hswi"
},
{
"path": "config/config.exs",
"chars": 1257,
"preview": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module"
},
{
"path": "docker-compose.yml",
"chars": 371,
"preview": "version: '3.8'\n\nservices:\n openethereum:\n image: openethereum/openethereum:v3.3.0\n command: '--chain=dev --unlock"
},
{
"path": "lib/exw3/abi.ex",
"chars": 5062,
"preview": "defmodule ExW3.Abi do\n @doc \"Decodes event based on given data and provided signature\"\n @spec decode_event(binary(), b"
},
{
"path": "lib/exw3/address.ex",
"chars": 1652,
"preview": "defmodule ExW3.Address do\n @type t :: %__MODULE__{bytes: binary}\n\n defstruct ~w[bytes]a\n\n @spec from_bytes(binary) ::"
},
{
"path": "lib/exw3/client.ex",
"chars": 991,
"preview": "defmodule ExW3.Client do\n @type argument :: term\n @type request_error :: Ethereumex.Client.Behaviour.error()\n @type e"
},
{
"path": "lib/exw3/contract.ex",
"chars": 16310,
"preview": "defmodule ExW3.Contract do\n use GenServer\n\n @doc \"Begins the Contract process to manage all interactions with smart co"
},
{
"path": "lib/exw3/normalize.ex",
"chars": 251,
"preview": "defmodule ExW3.Normalize do\n @spec transform_to_integer(map(), list()) :: map()\n def transform_to_integer(map, keys) d"
},
{
"path": "lib/exw3/rpc.ex",
"chars": 7166,
"preview": "defmodule ExW3.Rpc do\n import ExW3.Client\n\n @type invalid_hex_string_error :: ExW3.Utils.invalid_hex_string_error()\n "
},
{
"path": "lib/exw3/utils.ex",
"chars": 3950,
"preview": "defmodule ExW3.Utils do\n alias ExW3.Address\n\n @type invalid_hex_string :: :invalid_hex_string\n @type negative_integer"
},
{
"path": "lib/exw3.ex",
"chars": 1240,
"preview": "defmodule ExW3 do\n defdelegate accounts(opts \\\\ []), to: ExW3.Rpc\n defdelegate block_number(opts \\\\ []), to: ExW3.Rpc\n"
},
{
"path": "mix.exs",
"chars": 1457,
"preview": "defmodule ExW3.MixProject do\n use Mix.Project\n\n @source_url \"https://github.com/hswick/exw3\"\n @version \"0.6.1\"\n\n def"
},
{
"path": "parity.sh",
"chars": 305,
"preview": "parity --chain dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --reseal-min-period 0 --password passfile --jsonr"
},
{
"path": "test/examples/build/AddressTester.abi",
"chars": 173,
"preview": "[{\"constant\":true,\"inputs\":[{\"name\":\"a\",\"type\":\"address\"}],\"name\":\"get\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payabl"
},
{
"path": "test/examples/build/ArrayTester.abi",
"chars": 376,
"preview": "[{\"constant\":true,\"inputs\":[{\"name\":\"ints\",\"type\":\"uint256[5]\"}],\"name\":\"staticUint\",\"outputs\":[{\"name\":\"\",\"type\":\"uint2"
},
{
"path": "test/examples/build/Complex.abi",
"chars": 1915,
"preview": "[{\"constant\":true,\"inputs\":[],\"name\":\"getBroAndBroBro\",\"outputs\":[{\"name\":\"bro\",\"type\":\"uint256\"},{\"name\":\"broBro\",\"type"
},
{
"path": "test/examples/build/EventTester.abi",
"chars": 694,
"preview": "[{\"constant\":false,\"inputs\":[{\"name\":\"data\",\"type\":\"bytes32\"}],\"name\":\"simpleIndex\",\"outputs\":[],\"payable\":false,\"stateM"
},
{
"path": "test/examples/build/SimpleStorage.abi",
"chars": 299,
"preview": "[{\"constant\":false,\"inputs\":[{\"name\":\"_data\",\"type\":\"uint256\"}],\"name\":\"set\",\"outputs\":[],\"payable\":false,\"stateMutabili"
},
{
"path": "test/examples/contracts/AddressTester.sol",
"chars": 136,
"preview": "pragma solidity ^0.4.18;\n\ncontract AddressTester {\n function get(address a) public pure returns (address) {\n r"
},
{
"path": "test/examples/contracts/ArrayTester.sol",
"chars": 244,
"preview": "pragma solidity ^0.4.18;\n\ncontract ArrayTester {\n function dynamicUint(uint[] ints) public pure returns (uint[]) {\n "
},
{
"path": "test/examples/contracts/Complex.sol",
"chars": 1068,
"preview": "pragma solidity ^0.4.0;\n\ncontract Complex {\n\n uint foo;\n bytes32 foobar;\n bool barFoo;\n\n event Bar(uint foo,"
},
{
"path": "test/examples/contracts/EventTester.sol",
"chars": 356,
"preview": "pragma solidity ^0.4.18;\n\ncontract EventTester {\n\n event Simple(uint256 num, bytes32 data);\n\n event SimpleIndex(ui"
},
{
"path": "test/examples/contracts/SimpleStorage.sol",
"chars": 209,
"preview": "pragma solidity ^0.4.0;\n\ncontract SimpleStorage {\n uint data;\n\n function set(uint _data) public {\n data = _"
},
{
"path": "test/exw3/abi_test.exs",
"chars": 868,
"preview": "defmodule ExW3.AbiTest do\n use ExUnit.Case\n\n test \".load_abi/1 returns a map keyed by function & event name\" do\n as"
},
{
"path": "test/exw3/address_test.exs",
"chars": 1775,
"preview": "defmodule ExW3.AddressTest do\n use ExUnit.Case\n alias ExW3.Address\n\n @bytes_address <<25, 154, 209, 226, 223, 37, 243"
},
{
"path": "test/exw3/client_test.exs",
"chars": 1564,
"preview": "defmodule ExW3.ClientTest do\n use ExUnit.Case\n\n test \".call_client/1 calls the JSON-RPC method with an empty list of a"
},
{
"path": "test/exw3/contract_test.exs",
"chars": 1633,
"preview": "defmodule EXW3.ContractTest do\n use ExUnit.Case\n doctest ExW3.Contract\n\n @simple_storage_abi ExW3.Abi.load_abi(\"test/"
},
{
"path": "test/exw3/rpc_test.exs",
"chars": 1340,
"preview": "defmodule ExW3.RpcTest do\n use ExUnit.Case\n\n describe \".accounts\" do\n test \"returns a list from the eth_accounts JS"
},
{
"path": "test/exw3/utils_test.exs",
"chars": 7761,
"preview": "defmodule ExW3.UtilsTest do\n use ExUnit.Case\n doctest ExW3.Utils\n\n describe \".hex_to_integer/1\" do\n test \"parses a"
},
{
"path": "test/exw3_test.exs",
"chars": 12427,
"preview": "defmodule ExW3Test do\n use ExUnit.Case\n doctest ExW3\n\n setup_all do\n start_supervised!(ExW3.Contract)\n\n %{\n "
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the hswick/exw3 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (92.2 KB), approximately 26.6k tokens, and a symbol index with 121 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.