├── .credo.exs ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── bin ├── run_parity └── setup_parity ├── config ├── config.exs ├── dev.exs └── test.exs ├── docker-compose.yml ├── lib ├── ethereumex.ex └── ethereumex │ ├── application.ex │ ├── client │ ├── base_client.ex │ ├── behaviour.ex │ ├── websocket_client.ex │ └── websocket_server.ex │ ├── config.ex │ ├── counter.ex │ ├── http_client.ex │ ├── ipc_client.ex │ └── ipc_server.ex ├── mix.exs ├── mix.lock └── test ├── ethereumex ├── client │ └── base_client_test.exs ├── config_test.exs ├── counter_reset_test.exs ├── counter_test.exs ├── http_client_test.exs ├── ipc_client_test.exs ├── websocket_client_test.exs └── websocket_server_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "src/", "web/", "apps/"], 7 | excluded: [] 8 | }, 9 | checks: [ 10 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, [parens: true]}, 11 | {Credo.Check.Refactor.LongQuoteBlocks, false}, 12 | {Credo.Check.Readability.StrictModuleLayout, []} 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: monthly 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | env: 12 | MIX_ENV: test 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - pair: 18 | elixir: 1.14.5 19 | otp: 25.3.2 20 | - pair: 21 | elixir: 1.15.7 22 | otp: 25.3.2 23 | - pair: 24 | elixir: 1.16.1 25 | otp: 25.3.2 26 | - pair: 27 | elixir: 1.14.5 28 | otp: 26.2.2 29 | - pair: 30 | elixir: 1.15.7 31 | otp: 26.2.2 32 | - pair: 33 | elixir: 1.16.1 34 | otp: 26.2.2 35 | lint: lint 36 | - pair: 37 | elixir: 1.17.3 38 | otp: 27.2 39 | 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - uses: erlef/setup-beam@v1 46 | with: 47 | otp-version: ${{matrix.pair.otp}} 48 | elixir-version: ${{matrix.pair.elixir}} 49 | 50 | - uses: actions/cache@v3 51 | with: 52 | path: | 53 | deps 54 | _build 55 | key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} 56 | restore-keys: | 57 | ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}- 58 | 59 | - name: Run Parity in background 60 | run: | 61 | wget https://releases.parity.io/ethereum/v1.10.7/x86_64-unknown-linux-gnu/parity 62 | chmod 755 ./parity 63 | ./parity --chain dev & 64 | 65 | - name: Run mix deps.get 66 | run: mix deps.get --only test 67 | 68 | - name: Run mix format 69 | run: mix format --check-formatted 70 | if: ${{ matrix.lint }} 71 | 72 | - name: Run mix deps.compile 73 | run: mix deps.compile 74 | 75 | - name: Run mix compile 76 | run: mix compile --warnings-as-errors 77 | if: ${{ matrix.lint }} 78 | 79 | - name: Run credo 80 | run: mix credo --strict 81 | if: ${{ matrix.lint }} 82 | 83 | - name: Run mix test 84 | run: mix test --include eth --include batch 85 | 86 | - name: Run dialyzer 87 | run: mix dialyzer 88 | if: ${{ matrix.lint }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ethereumex-*.tar 24 | 25 | # Misc. 26 | .tool-versions 27 | parity 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.4-otp-26 2 | erlang 26.0.2 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.12.1 - 2025-04-09 4 | * Add optional http request logs (https://github.com/mana-ethereum/ethereumex/pull/176) 5 | 6 | ## 0.12.0 - 2025-03-04 7 | * Make json library configurable (https://github.com/mana-ethereum/ethereumex/pull/169) 8 | 9 | ## 0.11.1 - 2025-02-27 10 | * Add WebSocket subscription support (https://github.com/mana-ethereum/ethereumex/pull/167) 11 | 12 | ## 0.11.0 - 2025-02-19 13 | * WebSocket Client Implementation (https://github.com/mana-ethereum/ethereumex/pull/165) 14 | 15 | ## 0.10.7 - 2025-01-10 16 | * Add eth_blob_base_fee support (https://github.com/mana-ethereum/ethereumex/pull/160) 17 | 18 | ## 0.10.6 - 2023-12-12 19 | * Add eth_chainId RPC function (https://github.com/mana-ethereum/ethereumex/pull/155) 20 | * Add json header by default (https://github.com/mana-ethereum/ethereumex/pull/156) 21 | 22 | ## 0.10.5 - 2023-09-06 23 | * Update deps (https://github.com/mana-ethereum/ethereumex/pull/153) 24 | 25 | ## 0.10.4 - 2022-12-07 26 | * Update finch to 0.14 (https://github.com/mana-ethereum/ethereumex/pull/141) 27 | 28 | ## 0.10.3 - 2022-09-15 29 | * Update jason to 1.4 (https://github.com/mana-ethereum/ethereumex/pull/137) 30 | 31 | ## 0.10.2 - 2022-08-02 32 | * Fix typespec in HttpClient (https://github.com/mana-ethereum/ethereumex/pull/134) 33 | 34 | ## 0.10.1 - 2022-07-29 35 | * Add option to not format batch requests (https://github.com/mana-ethereum/ethereumex/pull/131) 36 | 37 | ## 0.10.0 - 2022-05-10 38 | * Add EIP1559 support for eth_maxPriorityFeePerGas and eth_feeHistory (https://github.com/mana-ethereum/ethereumex/pull/127) 39 | * Update finch to 0.12.0 (https://github.com/mana-ethereum/ethereumex/pull/126) 40 | 41 | ## 0.9.2 - 2022-04-01 42 | * Update `finch` to `0.11` (https://github.com/mana-ethereum/ethereumex/commit/28fcb106c04cfc26b78cfbc558acc2385a816ebe) 43 | 44 | ## 0.9.1 - 2022-02-12 45 | * Namespace finch adapter under Ethereumex (https://github.com/mana-ethereum/ethereumex/pull/113) 46 | * Move application into a separate module (https://github.com/mana-ethereum/ethereumex/pull/108) 47 | * Add http_headers as optional param (https://github.com/mana-ethereum/ethereumex/pull/111) 48 | 49 | ## 0.9.0 - 2021-12-25 50 | * Allow adding custom headers (https://github.com/mana-ethereum/ethereumex/pull/97) 51 | * Handle errors in batch requests (https://github.com/mana-ethereum/ethereumex/pull/98) 52 | 53 | ## 0.8.0 - 2021-11-21 54 | * Switch from `httpoison` to `finch` (https://github.com/mana-ethereum/ethereumex/pull/94) 55 | 56 | ## 0.7.1 - 2021-10-10 57 | * Change telemetry version (https://github.com/mana-ethereum/ethereumex/pull/92) 58 | 59 | ## 0.7.0 - 2020-12-07 60 | * Remove unremovable default hackney pool (https://github.com/mana-ethereum/ethereumex/pull/82) 61 | * Change ipc_path to absolute path (https://github.com/mana-ethereum/ethereumex/pull/87) 62 | * Bump deps (https://github.com/mana-ethereum/ethereumex/pull/88) 63 | * Config.setup_children can inject client type (https://github.com/mana-ethereum/ethereumex/pull/83) 64 | * Improve typespecs (https://github.com/mana-ethereum/ethereumex/pull/80, https://github.com/mana-ethereum/ethereumex/pull/85, https://github.com/mana-ethereum/ethereumex/pull/86) 65 | 66 | ## 0.6.4 - 2020-07-24 67 | * Fix request id exhaustion due to exponential increment (https://github.com/mana-ethereum/ethereumex/pull/76) 68 | 69 | ## 0.6.3 - 2020-06-30 70 | * Make batch requests support configurable url via opts argument (https://github.com/mana-ethereum/ethereumex/pull/77) 71 | 72 | ## 0.6.2 - 2020-05-27 73 | * Make `telemetry` required dependency (https://github.com/mana-ethereum/ethereumex/pull/73) 74 | 75 | ## 0.6.1 - 2020-04-16 76 | * Dependency updates: :httpoison 1.4 -> 1.6, :jason 1.1 -> 1.2, :credo 0.10.2 -> 1.3, :ex_doc 0.19 -> 0.21, :dialyxir 1.0.0-rc.7 -> 1.0.0. And small refactors according to Credo. https://github.com/mana-ethereum/ethereumex/pull/69 77 | 78 | ## 0.6.0 - 2020-03-02 79 | * Deprecate measurements via adapter for :telemetry. https://github.com/mana-ethereum/ethereumex/pull/68 80 | 81 | ## 0.5.6 - 2020-02-28 82 | * Feature that allows measuring number of RPC calls via an adapter https://github.com/mana-ethereum/ethereumex/pull/67 83 | 84 | ## 0.5.5 - 2020-10-21 85 | * Allow request counter resetting (https://github.com/mana-ethereum/ethereumex/pull/65) 86 | 87 | ## 0.5.4 - 2020-05-23 88 | * Reported issue with dialyzer specs for boolean arguments in params (https://github.com/mana-ethereum/ethereumex/pull/61) 89 | 90 | ## 0.5.3 - 2019-02-10 91 | * Add special case for empty response (https://github.com/mana-ethereum/ethereumex/pull/59) 92 | 93 | ## 0.5.2 - 2018-12-29 94 | * Fix `eth_estimateGas` (https://github.com/mana-ethereum/ethereumex/pull/55) 95 | * Add `eth_getProof` (https://github.com/mana-ethereum/ethereumex/pull/57) 96 | 97 | ## 0.5.1 - 2018-11-04 98 | * Replaced poison with jason (https://github.com/mana-ethereum/ethereumex/pull/50) 99 | * Upgraded httpoison to v1.4 (https://github.com/mana-ethereum/ethereumex/pull/50) 100 | 101 | ## 0.5.0 - 2018-10-25 102 | * Remove tunneling requests (https://github.com/exthereum/ethereumex/pull/46) 103 | * Use poolboy for IpcClient (https://github.com/exthereum/ethereumex/pull/47) 104 | 105 | ## 0.4.0 - 2018-10-27 106 | * Use IPC with IpcClient. Choose client type based on config :client_type (https://github.com/exthereum/ethereumex/pull/40) 107 | * Update poison, credo, dialyzer (https://github.com/exthereum/ethereumex/pull/42) 108 | 109 | ## 0.3.4 - 2018-09-25 110 | * Allow configuring GenServer timeout for requests (https://github.com/exthereum/ethereumex/pull/39) 111 | 112 | ## 0.3.3 - 2018-07-31 113 | * Added dynamic url input(https://github.com/exthereum/ethereumex/pull/37) 114 | 115 | ## 0.3.2 - 2018-03-29 116 | * Fix eth_getLogs mathod params (https://github.com/exthereum/ethereumex/pull/20) 117 | 118 | ## 0.3.1 - 2018-02-09 119 | * Handle failed HTTP requests more gracefully (https://github.com/exthereum/ethereumex/pull/19) 120 | 121 | ## 0.3.0 - 2018-01-23 122 | * Breaking: Use `:url` config variable instead of `:host`, `:port` and `:scheme` (https://github.com/exthereum/ethereumex/pull/13) 123 | 124 | ## 0.2.0 - 2018-10-16 125 | * Breaking: explicit parameters (https://github.com/exthereum/ethereumex/pull/10) 126 | 127 | ## 0.1.2 - 2017-09-25 128 | * Added generic request method (https://github.com/exthereum/ethereumex/pull/4) 129 | 130 | ## 0.1.1 - 2017-09-16 131 | * Added necessary JSON header for parity (https://github.com/exthereum/ethereumex/pull/2) 132 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2018 Ayrat Badykov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: run.parity 2 | 3 | run.parity: 4 | ./bin/run_parity 5 | 6 | setup: setup.parity 7 | 8 | setup.parity: 9 | ./bin/setup_parity 10 | 11 | test:: 12 | mix test 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereumex 2 | 3 | [![Module Version](https://img.shields.io/hexpm/v/ethereumex.svg)](https://hex.pm/packages/ethereumex) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ethereumex/) 5 | [![Total Download](https://img.shields.io/hexpm/dt/ethereumex.svg)](https://hex.pm/packages/ethereumex) 6 | [![License](https://img.shields.io/hexpm/l/ethereumex.svg)](https://github.com/mana-ethereum/ethereumex/blob/master/LICENSE.md) 7 | [![Last Updated](https://img.shields.io/github/last-commit/mana-ethereum/ethereumex.svg)](https://github.com/mana-ethereum/ethereumex/commits/master) 8 | 9 | 10 | 11 | Elixir JSON-RPC client for the Ethereum blockchain. 12 | 13 | Check out the documentation [here](https://hexdocs.pm/ethereumex/Ethereumex.html#content). 14 | 15 | ## Installation 16 | 17 | Add `:ethereumex` to your list of dependencies in `mix.exs`: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:ethereumex, "~> 0.12.1"}, 23 | # json library is configurable 24 | {:jason, "~> 1.4"} 25 | ] 26 | end 27 | ``` 28 | 29 | ## Configuration 30 | 31 | ### HTTP 32 | 33 | In `config/config.exs`, add Ethereum protocol host params to your config file 34 | 35 | ```elixir 36 | config :ethereumex, 37 | url: "http://localhost:8545" 38 | ``` 39 | 40 | You can also configure the `HTTP` request timeout for requests sent to the Ethereum JSON-RPC 41 | (you can also overwrite this configuration in `opts` used when calling the client). 42 | 43 | ```elixir 44 | config :ethereumex, 45 | http_options: [pool_timeout: 5000, receive_timeout: 15_000], 46 | http_headers: [{"Content-Type", "application/json"}] 47 | ``` 48 | 49 | `:pool_timeout` - This timeout is applied when we check out a connection from the pool. Default value is `5_000`. 50 | `:receive_timeout` - The maximum time to wait for a response before returning an error. Default value is `15_000` 51 | `:enable_request_error_logs` - Optional request error logs. Default value is false 52 | 53 | ### IPC 54 | 55 | If you want to use IPC you will need to set a few things in your config. 56 | 57 | First, specify the `:client_type`: 58 | 59 | ```elixir 60 | config :ethereumex, 61 | client_type: :ipc 62 | ``` 63 | 64 | This will resolve to `:http` by default. 65 | 66 | Second, specify the `:ipc_path`: 67 | 68 | ```elixir 69 | config :ethereumex, 70 | ipc_path: "/path/to/ipc" 71 | ``` 72 | 73 | The IPC client type mode opens a pool of connection workers (default is 5 and 2, respectively). You can configure the pool size. 74 | 75 | ```elixir 76 | config :ethereumex, 77 | ipc_worker_size: 5, 78 | ipc_max_worker_overflow: 2, 79 | ipc_request_timeout: 60_000 80 | ``` 81 | 82 | ### WebSocket 83 | 84 | The WebSocket client supports both standard JSON-RPC requests and real-time subscriptions. 85 | To use it, configure your application with: 86 | 87 | ```elixir 88 | config :ethereumex, 89 | websocket_url: "ws://localhost:8545", 90 | client_type: :websocket 91 | ``` 92 | 93 | #### Standard RPC Calls 94 | 95 | All standard RPC methods work the same as with HTTP: 96 | 97 | ```elixir 98 | iex> Ethereumex.WebsocketClient.eth_block_number() 99 | {:ok, "0x1234"} 100 | ``` 101 | 102 | #### Real-time Subscriptions 103 | 104 | Subscribe to various blockchain events: 105 | 106 | ```elixir 107 | # Subscribe to new block headers 108 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:newHeads) 109 | {:ok, "0x9cef478923ff08bf67fde6c64013158d"} 110 | 111 | # Subscribe to logs/events from specific contracts 112 | iex> filter = %{ 113 | ...> address: "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", 114 | ...> topics: ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"] 115 | ...> } 116 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:logs, filter) 117 | {:ok, "0x4a8a4c0517381924f9838102c5a4dcb7"} 118 | 119 | # Subscribe to pending transactions 120 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:newPendingTransactions) 121 | {:ok, "0x1234567890abcdef1234567890abcdef"} 122 | 123 | # Receive notifications in your process 124 | receive do 125 | %{ 126 | "method" => "eth_subscription", 127 | "params" => %{ 128 | "subscription" => subscription_id, 129 | "result" => result 130 | } 131 | } -> handle_notification(result) 132 | end 133 | 134 | # Unsubscribe when done 135 | iex> Ethereumex.WebsocketClient.unsubscribe(subscription_id) 136 | {:ok, true} 137 | ``` 138 | 139 | Available subscription types: 140 | 141 | - `:newHeads` - New block headers 142 | - `:logs` - Contract events/logs with optional filtering 143 | - `:newPendingTransactions` - Pending transaction hashes 144 | 145 | ### Telemetry 146 | 147 | If you want to count the number of RPC calls per RPC method or overall, 148 | you can attach yourself to executed telemetry events. 149 | There are two events you can attach yourself to: 150 | `[:ethereumex]` # has RPC method name in metadata 151 | Emitted event: `{:event, [:ethereumex], %{counter: 1}, %{method_name: "method_name"}}` 152 | 153 | or more granular 154 | `[:ethereumex, ]` # %{} metadata 155 | Emitted event: `{:event, [:ethereumex, :method_name_as_atom], %{counter: 1}, %{}}` 156 | 157 | Each event caries a single ticker that you can pass into your counters (like `Statix.increment/2`). 158 | Be sure to add :telemetry as project dependency. 159 | 160 | 161 | ### Json library 162 | 163 | The default json library is set to `jason` but that can be overridden with a different module. The module should implement functions `encode/1`, `decode/2`, `encode!/`, `decode!/1` 164 | 165 | ```elixir 166 | config :ethereumex, json_module: MyCustomJson 167 | ``` 168 | 169 | ## Test 170 | 171 | Download `parity` and initialize the password file 172 | 173 | ``` 174 | $ make setup 175 | ``` 176 | 177 | Run `parity` 178 | 179 | ``` 180 | $ make run 181 | ``` 182 | 183 | Run tests 184 | 185 | ``` 186 | $ make test 187 | ``` 188 | 189 | ## Usage 190 | 191 | ### Available methods: 192 | 193 | - [`web3_clientVersion`](https://eth.wiki/json-rpc/API#web3_clientversion) 194 | - [`web3_sha3`](https://eth.wiki/json-rpc/API#web3_sha3) 195 | - [`net_version`](https://eth.wiki/json-rpc/API#net_version) 196 | - [`net_peerCount`](https://eth.wiki/json-rpc/API#net_peercount) 197 | - [`net_listening`](https://eth.wiki/json-rpc/API#net_listening) 198 | - [`eth_protocolVersion`](https://eth.wiki/json-rpc/API#eth_protocolversion) 199 | - [`eth_syncing`](https://eth.wiki/json-rpc/API#eth_syncing) 200 | - [`eth_coinbase`](https://eth.wiki/json-rpc/API#eth_coinbase) 201 | - [`eth_chainId`](https://eth.wiki/json-rpc/API#eth_chainId) 202 | - [`eth_mining`](https://eth.wiki/json-rpc/API#eth_mining) 203 | - [`eth_hashrate`](https://eth.wiki/json-rpc/API#eth_hashrate) 204 | - [`eth_gasPrice`](https://eth.wiki/json-rpc/API#eth_gasprice) 205 | - [`eth_accounts`](https://eth.wiki/json-rpc/API#eth_accounts) 206 | - [`eth_blockNumber`](https://eth.wiki/json-rpc/API#eth_blocknumber) 207 | - [`eth_getBalance`](https://eth.wiki/json-rpc/API#eth_getbalance) 208 | - [`eth_getStorageAt`](https://eth.wiki/json-rpc/API#eth_getstorageat) 209 | - [`eth_getTransactionCount`](https://eth.wiki/json-rpc/API#eth_gettransactioncount) 210 | - [`eth_getBlockTransactionCountByHash`](https://eth.wiki/json-rpc/API#eth_getblocktransactioncountbyhash) 211 | - [`eth_getBlockTransactionCountByNumber`](https://eth.wiki/json-rpc/API#eth_getblocktransactioncountbynumber) 212 | - [`eth_getUncleCountByBlockHash`](https://eth.wiki/json-rpc/API#eth_getunclecountbyblockhash) 213 | - [`eth_getUncleCountByBlockNumber`](https://eth.wiki/json-rpc/API#eth_getunclecountbyblocknumber) 214 | - [`eth_getCode`](https://eth.wiki/json-rpc/API#eth_getcode) 215 | - [`eth_sign`](https://eth.wiki/json-rpc/API#eth_sign) 216 | - [`eth_sendTransaction`](https://eth.wiki/json-rpc/API#eth_sendtransaction) 217 | - [`eth_sendRawTransaction`](https://eth.wiki/json-rpc/API#eth_sendrawtransaction) 218 | - [`eth_call`](https://eth.wiki/json-rpc/API#eth_call) 219 | - [`eth_estimateGas`](https://eth.wiki/json-rpc/API#eth_estimategas) 220 | - [`eth_getBlockByHash`](https://eth.wiki/json-rpc/API#eth_getblockbyhash) 221 | - [`eth_getBlockByNumber`](https://eth.wiki/json-rpc/API#eth_getblockbynumber) 222 | - [`eth_getTransactionByHash`](https://eth.wiki/json-rpc/API#eth_gettransactionbyhash) 223 | - [`eth_getTransactionByBlockHashAndIndex`](https://eth.wiki/json-rpc/API#eth_gettransactionbyblockhashandindex) 224 | - [`eth_getTransactionByBlockNumberAndIndex`](https://eth.wiki/json-rpc/API#eth_gettransactionbyblocknumberandindex) 225 | - [`eth_getTransactionReceipt`](https://eth.wiki/json-rpc/API#eth_gettransactionreceipt) 226 | - [`eth_getUncleByBlockHashAndIndex`](https://eth.wiki/json-rpc/API#eth_getunclebyblockhashandindex) 227 | - [`eth_getUncleByBlockNumberAndIndex`](https://eth.wiki/json-rpc/API#eth_getunclebyblocknumberandindex) 228 | - [`eth_getCompilers`](https://eth.wiki/json-rpc/API#eth_getcompilers) 229 | - [`eth_compileLLL`](https://eth.wiki/json-rpc/API#eth_compilelll) 230 | - [`eth_compileSolidity`](https://eth.wiki/json-rpc/API#eth_compilesolidity) 231 | - [`eth_compileSerpent`](https://eth.wiki/json-rpc/API#eth_compileserpent) 232 | - [`eth_newFilter`](https://eth.wiki/json-rpc/API#eth_newfilter) 233 | - [`eth_newBlockFilter`](https://eth.wiki/json-rpc/API#eth_newblockfilter) 234 | - [`eth_newPendingTransactionFilter`](https://eth.wiki/json-rpc/API#eth_newpendingtransactionfilter) 235 | - [`eth_uninstallFilter`](https://eth.wiki/json-rpc/API#eth_uninstallfilter) 236 | - [`eth_getFilterChanges`](https://eth.wiki/json-rpc/API#eth_getfilterchanges) 237 | - [`eth_getFilterLogs`](https://eth.wiki/json-rpc/API#eth_getfilterlogs) 238 | - [`eth_getLogs`](https://eth.wiki/json-rpc/API#eth_getlogs) 239 | - eth_getProof 240 | - [`eth_getWork`](https://eth.wiki/json-rpc/API#eth_getwork) 241 | - [`eth_submitWork`](https://eth.wiki/json-rpc/API#eth_submitwork) 242 | - [`eth_submitHashrate`](https://eth.wiki/json-rpc/API#eth_submithashrate) 243 | - [`db_putString`](https://eth.wiki/json-rpc/API#db_putstring) 244 | - [`db_getString`](https://eth.wiki/json-rpc/API#db_getstring) 245 | - [`db_putHex`](https://eth.wiki/json-rpc/API#db_puthex) 246 | - [`db_getHex`](https://eth.wiki/json-rpc/API#db_gethex) 247 | - [`shh_post`](https://eth.wiki/json-rpc/API#shh_post) 248 | - [`shh_version`](https://eth.wiki/json-rpc/API#shh_version) 249 | - [`shh_newIdentity`](https://eth.wiki/json-rpc/API#shh_newidentity) 250 | - [`shh_hasIdentity`](https://eth.wiki/json-rpc/API#shh_hasidentity) 251 | - [`shh_newGroup`](https://eth.wiki/json-rpc/API#shh_newgroup) 252 | - [`shh_addToGroup`](https://eth.wiki/json-rpc/API#shh_addtogroup) 253 | - [`shh_newFilter`](https://eth.wiki/json-rpc/API#shh_newfilter) 254 | - [`shh_uninstallFilter`](https://eth.wiki/json-rpc/API#shh_uninstallfilter) 255 | - [`shh_getFilterChanges`](https://eth.wiki/json-rpc/API#shh_getfilterchanges) 256 | - [`shh_getMessages`](https://eth.wiki/json-rpc/API#shh_getmessages) 257 | 258 | #### WebSocket Subscription Methods 259 | 260 | - [`eth_subscribe`](https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub#create-subscriptions) - Subscribe to real-time events 261 | - [`eth_unsubscribe`](https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub#cancel-subscriptions) - Unsubscribe from events 262 | 263 | ### IpcClient 264 | 265 | You can follow along with any of these examples using IPC by replacing `HttpClient` with `IpcClient`. 266 | 267 | ### Examples 268 | 269 | ```elixir 270 | iex> Ethereumex.HttpClient.web3_client_version 271 | {:ok, "Parity//v1.7.2-beta-9f47909-20170918/x86_64-macos/rustc1.19.0"} 272 | 273 | # Using the url option will overwrite the configuration 274 | iex> Ethereumex.HttpClient.web3_client_version(url: "http://localhost:8545") 275 | {:ok, "Parity//v1.7.2-beta-9f47909-20170918/x86_64-macos/rustc1.19.0"} 276 | 277 | iex> Ethereumex.HttpClient.web3_sha3("wrong_param") 278 | {:error, %{"code" => -32602, "message" => "Invalid params: invalid format."}} 279 | 280 | iex> Ethereumex.HttpClient.eth_get_balance("0x407d73d8a49eeb85d32cf465507dd71d507100c1") 281 | {:ok, "0x0"} 282 | ``` 283 | 284 | Note that all method names are snakecases, so, for example, shh_getMessages method has corresponding Ethereumex.HttpClient.shh_get_messages/1 method. Signatures can be found in Ethereumex.Client.Behaviour. There are more examples in tests. 285 | 286 | #### eth_call example - Read only smart contract calls 287 | 288 | In order to call a smart contract using the JSON-RPC interface you need to properly hash the data attribute (this will need to include the contract method signature along with arguments if any). You can do this manually or use a hex package like [ABI](https://hex.pm/packages/ex_abi) to parse your smart contract interface or encode individual calls. 289 | 290 | ```elixir 291 | defp deps do 292 | [ 293 | ... 294 | {:ethereumex, "~> 0.9"}, 295 | {:ex_abi, "~> 0.5"} 296 | ... 297 | ] 298 | end 299 | ``` 300 | 301 | Now load the ABI and pass the method signature. Note that the address needs to be converted to bytes: 302 | 303 | ```elixir 304 | address = "0xF742d4cE7713c54dD701AA9e92101aC42D63F895" |> String.slice(2..-1) |> Base.decode16!(case: :mixed) 305 | contract_address = "0xC28980830dD8b9c68a45384f5489ccdAF19D53cC" 306 | abi_encoded_data = ABI.encode("balanceOf(address)", [address]) |> Base.encode16(case: :lower) 307 | ``` 308 | 309 | Now you can use eth_call to execute this smart contract command: 310 | 311 | ```elixir 312 | balance_bytes = Ethereumex.HttpClient.eth_call(%{ 313 | data: "0x" <> abi_encoded_data, 314 | to: contract_address 315 | }) 316 | ``` 317 | 318 | To convert the balance into an integer: 319 | 320 | ```elixir 321 | balance_bytes 322 | |> String.slice(2..-1) 323 | |> Base.decode16!(case: :lower) 324 | |> TypeDecoder.decode_raw([{:uint, 256}]) 325 | |> List.first 326 | ``` 327 | 328 | ### Custom requests 329 | 330 | Many Ethereum protocol implementations support additional JSON-RPC API methods. To use them, you should call Ethereumex.HttpClient.request/3 method. 331 | 332 | For example, let's call parity's personal_listAccounts method. 333 | 334 | ```elixir 335 | iex> Ethereumex.HttpClient.request("personal_listAccounts", [], []) 336 | {:ok, 337 | ["0x71cf0b576a95c347078ec2339303d13024a26910", 338 | "0x7c12323a4fff6df1a25d38319d5692982f48ec2e"]} 339 | ``` 340 | 341 | ### Batch requests 342 | 343 | To send batch requests use Ethereumex.HttpClient.batch_request/1 or Ethereumex.HttpClient.batch_request/2 method. 344 | 345 | ```elixir 346 | requests = [ 347 | {:web3_client_version, []}, 348 | {:net_version, []}, 349 | {:web3_sha3, ["0x68656c6c6f20776f726c64"]} 350 | ] 351 | Ethereumex.HttpClient.batch_request(requests) 352 | { 353 | :ok, 354 | [ 355 | {:ok, "Parity//v1.7.2-beta-9f47909-20170918/x86_64-macos/rustc1.19.0"}, 356 | {:ok, "42"}, 357 | {:ok, "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"} 358 | ] 359 | } 360 | ``` 361 | 362 | 363 | 364 | ## Built on Ethereumex 365 | 366 | If you are curious what others are building with ethereumex, you might want to take a look at these projects: 367 | 368 | - [ethers](https://github.com/ExWeb3/elixir_ethers) - Interacting with EVM contracts like first-class Elixir functions similar to Ethers.js 369 | 370 | ## Contributing 371 | 372 | 1. [Fork it!](http://github.com/ayrat555/ethereumex/fork) 373 | 2. Create your feature branch (`git checkout -b my-new-feature`) 374 | 3. Commit your changes (`git commit -am 'Add some feature'`) 375 | 4. Push to the branch (`git push origin my-new-feature`) 376 | 5. Create new Pull Request 377 | 378 | ## Copyright and License 379 | 380 | Copyright (c) 2018 Ayrat Badykov 381 | 382 | Released under the MIT License, which can be found in the repository in 383 | [LICENSE.md](./LICENSE.md). 384 | -------------------------------------------------------------------------------- /bin/run_parity: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./parity/parity --chain dev 4 | -------------------------------------------------------------------------------- /bin/setup_parity: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p parity 4 | echo > ./parity/passfile 5 | 6 | curl https://releases.parity.io/ethereum/v1.10.7/x86_64-unknown-linux-gnu/parity --output ./parity/parity 7 | chmod +x ./parity/parity 8 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ethereumex, 4 | http_options: [pool_timeout: 5000, receive_timeout: 15_000], 5 | http_pool_options: %{}, 6 | http_headers: [], 7 | json_module: Jason, 8 | enable_request_error_logs: false 9 | 10 | # config :ethereumex, ipc_path: "/Library/Application Support/io.parity.ethereum/jsonrpc.ipc" 11 | import_config "#{Mix.env()}.exs" 12 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ethereumex, url: System.get_env("ETHEREUM_URL") 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ethereumex, url: "http://localhost:8545" 4 | 5 | config :ethereumex, ipc_path: "#{System.user_home!()}/.local/share/io.parity.ethereum/jsonrpc.ipc" 6 | 7 | # config :ethereumex, id_reset: true 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | openethereum: 5 | image: openethereum/openethereum:v3.3.0 6 | command: '--chain=dev --unlock=0x00a329c0648769a73afac7f9381e08fb43dbea72 --password=/home/openethereum/.local/share/openethereum/passfile --jsonrpc-interface=0.0.0.0' 7 | ports: 8 | - '8545:8545' 9 | - '8546:8546' 10 | volumes: 11 | - ./docker:/home/openethereum/.local/share 12 | -------------------------------------------------------------------------------- /lib/ethereumex.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex do 2 | @external_resource "README.md" 3 | @moduledoc "README.md" 4 | |> File.read!() 5 | |> String.split("") 6 | |> Enum.fetch!(1) 7 | end 8 | -------------------------------------------------------------------------------- /lib/ethereumex/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Application do 2 | @moduledoc """ 3 | Configures and starts the :ethereumex OTP application 4 | """ 5 | 6 | use Application 7 | import Supervisor.Spec, warn: false 8 | 9 | alias Ethereumex.Config 10 | alias Ethereumex.Counter 11 | 12 | def start(_type, _args) do 13 | :ok = Counter.setup() 14 | children = Config.setup_children() 15 | opts = [strategy: :one_for_one, name: Ethereumex.Supervisor] 16 | Supervisor.start_link(children, opts) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/ethereumex/client/base_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Client.BaseClient do 2 | @moduledoc """ 3 | The Base Client exposes the Ethereum Client RPC functionality. 4 | 5 | We use a macro so that exposed functions can be used in different behaviours 6 | (HTTP or IPC). 7 | """ 8 | 9 | alias Ethereumex.Client.Behaviour 10 | alias Ethereumex.Config 11 | alias Ethereumex.Counter 12 | 13 | defmacro __using__(_) do 14 | quote location: :keep do 15 | @behaviour Behaviour 16 | @type error :: Behaviour.error() 17 | 18 | @impl true 19 | def web3_client_version(opts \\ []) do 20 | request("web3_clientVersion", [], opts) 21 | end 22 | 23 | @impl true 24 | def web3_sha3(data, opts \\ []) do 25 | params = [data] 26 | 27 | request("web3_sha3", params, opts) 28 | end 29 | 30 | @impl true 31 | def net_version(opts \\ []) do 32 | request("net_version", [], opts) 33 | end 34 | 35 | @impl true 36 | def net_peer_count(opts \\ []) do 37 | request("net_peerCount", [], opts) 38 | end 39 | 40 | @impl true 41 | def net_listening(opts \\ []) do 42 | request("net_listening", [], opts) 43 | end 44 | 45 | @impl true 46 | def eth_protocol_version(opts \\ []) do 47 | request("eth_protocolVersion", [], opts) 48 | end 49 | 50 | @impl true 51 | def eth_syncing(opts \\ []) do 52 | request("eth_syncing", [], opts) 53 | end 54 | 55 | @impl true 56 | def eth_chain_id(opts \\ []) do 57 | request("eth_chainId", [], opts) 58 | end 59 | 60 | @impl true 61 | def eth_coinbase(opts \\ []) do 62 | request("eth_coinbase", [], opts) 63 | end 64 | 65 | @impl true 66 | def eth_mining(opts \\ []) do 67 | request("eth_mining", [], opts) 68 | end 69 | 70 | @impl true 71 | def eth_hashrate(opts \\ []) do 72 | request("eth_hashrate", [], opts) 73 | end 74 | 75 | @impl true 76 | def eth_gas_price(opts \\ []) do 77 | request("eth_gasPrice", [], opts) 78 | end 79 | 80 | @impl true 81 | def eth_max_priority_fee_per_gas(opts \\ []) do 82 | request("eth_maxPriorityFeePerGas", [], opts) 83 | end 84 | 85 | @impl true 86 | def eth_fee_history(block_count, newestblock, reward_percentiles, opts \\ []) do 87 | params = [block_count, newestblock, reward_percentiles] 88 | request("eth_feeHistory", params, opts) 89 | end 90 | 91 | @impl true 92 | def eth_blob_base_fee(opts \\ []) do 93 | request("eth_blobBaseFee", [], opts) 94 | end 95 | 96 | @impl true 97 | def eth_accounts(opts \\ []) do 98 | request("eth_accounts", [], opts) 99 | end 100 | 101 | @impl true 102 | def eth_block_number(opts \\ []) do 103 | request("eth_blockNumber", [], opts) 104 | end 105 | 106 | @impl true 107 | def eth_get_balance(address, block \\ "latest", opts \\ []) do 108 | params = [address, block] 109 | 110 | request("eth_getBalance", params, opts) 111 | end 112 | 113 | @impl true 114 | def eth_get_storage_at(address, position, block \\ "latest", opts \\ []) do 115 | params = [address, position, block] 116 | 117 | request("eth_getStorageAt", params, opts) 118 | end 119 | 120 | @impl true 121 | def eth_get_transaction_count(address, block \\ "latest", opts \\ []) do 122 | params = [address, block] 123 | 124 | request("eth_getTransactionCount", params, opts) 125 | end 126 | 127 | @impl true 128 | def eth_get_block_transaction_count_by_hash(hash, opts \\ []) do 129 | params = [hash] 130 | 131 | request("eth_getBlockTransactionCountByHash", params, opts) 132 | end 133 | 134 | @impl true 135 | def eth_get_block_transaction_count_by_number(block \\ "latest", opts \\ []) do 136 | params = [block] 137 | 138 | request("eth_getBlockTransactionCountByNumber", params, opts) 139 | end 140 | 141 | @impl true 142 | def eth_get_uncle_count_by_block_hash(hash, opts \\ []) do 143 | params = [hash] 144 | 145 | request("eth_getUncleCountByBlockHash", params, opts) 146 | end 147 | 148 | @impl true 149 | def eth_get_uncle_count_by_block_number(block \\ "latest", opts \\ []) do 150 | params = [block] 151 | 152 | request("eth_getUncleCountByBlockNumber", params, opts) 153 | end 154 | 155 | @impl true 156 | def eth_get_code(address, block \\ "latest", opts \\ []) do 157 | params = [address, block] 158 | 159 | request("eth_getCode", params, opts) 160 | end 161 | 162 | @impl true 163 | def eth_sign(address, message, opts \\ []) do 164 | params = [address, message] 165 | 166 | request("eth_sign", params, opts) 167 | end 168 | 169 | @impl true 170 | def eth_send_transaction(transaction, opts \\ []) do 171 | params = [transaction] 172 | 173 | request("eth_sendTransaction", params, opts) 174 | end 175 | 176 | @impl true 177 | def eth_send_raw_transaction(data, opts \\ []) do 178 | params = [data] 179 | 180 | request("eth_sendRawTransaction", params, opts) 181 | end 182 | 183 | @impl true 184 | def eth_call(transaction, block \\ "latest", opts \\ []) do 185 | params = [transaction, block] 186 | 187 | request("eth_call", params, opts) 188 | end 189 | 190 | @impl true 191 | def eth_estimate_gas(transaction, opts \\ []) do 192 | params = [transaction] 193 | 194 | request("eth_estimateGas", params, opts) 195 | end 196 | 197 | @impl true 198 | def eth_get_block_by_hash(hash, full, opts \\ []) do 199 | params = [hash, full] 200 | 201 | request("eth_getBlockByHash", params, opts) 202 | end 203 | 204 | @impl true 205 | def eth_get_block_by_number(number, full, opts \\ []) do 206 | params = [number, full] 207 | 208 | request("eth_getBlockByNumber", params, opts) 209 | end 210 | 211 | @impl true 212 | def eth_get_transaction_by_hash(hash, opts \\ []) do 213 | params = [hash] 214 | 215 | request("eth_getTransactionByHash", params, opts) 216 | end 217 | 218 | @impl true 219 | def eth_get_transaction_by_block_hash_and_index(hash, index, opts \\ []) do 220 | params = [hash, index] 221 | 222 | request("eth_getTransactionByBlockHashAndIndex", params, opts) 223 | end 224 | 225 | @impl true 226 | def eth_get_transaction_by_block_number_and_index(block, index, opts \\ []) do 227 | params = [block, index] 228 | 229 | request("eth_getTransactionByBlockNumberAndIndex", params, opts) 230 | end 231 | 232 | @impl true 233 | def eth_get_transaction_receipt(hash, opts \\ []) do 234 | params = [hash] 235 | 236 | request("eth_getTransactionReceipt", params, opts) 237 | end 238 | 239 | @impl true 240 | def eth_get_uncle_by_block_hash_and_index(hash, index, opts \\ []) do 241 | params = [hash, index] 242 | 243 | request("eth_getUncleByBlockHashAndIndex", params, opts) 244 | end 245 | 246 | @impl true 247 | def eth_get_uncle_by_block_number_and_index(block, index, opts \\ []) do 248 | params = [block, index] 249 | 250 | request("eth_getUncleByBlockNumberAndIndex", params, opts) 251 | end 252 | 253 | @impl true 254 | def eth_get_compilers(opts \\ []) do 255 | request("eth_getCompilers", [], opts) 256 | end 257 | 258 | @impl true 259 | def eth_compile_lll(data, opts \\ []) do 260 | params = [data] 261 | 262 | request("eth_compileLLL", params, opts) 263 | end 264 | 265 | @impl true 266 | def eth_compile_solidity(data, opts \\ []) do 267 | params = [data] 268 | 269 | request("eth_compileSolidity", params, opts) 270 | end 271 | 272 | @impl true 273 | def eth_compile_serpent(data, opts \\ []) do 274 | params = [data] 275 | 276 | request("eth_compileSerpent", params, opts) 277 | end 278 | 279 | @impl true 280 | def eth_new_filter(data, opts \\ []) do 281 | params = [data] 282 | 283 | request("eth_newFilter", params, opts) 284 | end 285 | 286 | @impl true 287 | def eth_new_block_filter(opts \\ []) do 288 | request("eth_newBlockFilter", [], opts) 289 | end 290 | 291 | @impl true 292 | def eth_new_pending_transaction_filter(opts \\ []) do 293 | request("eth_newPendingTransactionFilter", [], opts) 294 | end 295 | 296 | @impl true 297 | def eth_uninstall_filter(id, opts \\ []) do 298 | params = [id] 299 | 300 | request("eth_uninstallFilter", params, opts) 301 | end 302 | 303 | @impl true 304 | def eth_get_filter_changes(id, opts \\ []) do 305 | params = [id] 306 | 307 | request("eth_getFilterChanges", params, opts) 308 | end 309 | 310 | @impl true 311 | def eth_get_filter_logs(id, opts \\ []) do 312 | params = [id] 313 | 314 | request("eth_getFilterLogs", params, opts) 315 | end 316 | 317 | @impl true 318 | def eth_get_logs(filter, opts \\ []) do 319 | params = [filter] 320 | 321 | request("eth_getLogs", params, opts) 322 | end 323 | 324 | @impl true 325 | def eth_get_work(opts \\ []) do 326 | request("eth_getWork", [], opts) 327 | end 328 | 329 | @impl true 330 | def eth_get_proof(address, storage_keys, block \\ "latest", opts \\ []) do 331 | params = [address, storage_keys, block] 332 | 333 | request("eth_getProof", params, opts) 334 | end 335 | 336 | @impl true 337 | def eth_submit_work(nonce, header, digest, opts \\ []) do 338 | params = [nonce, header, digest] 339 | 340 | request("eth_submitWork", params, opts) 341 | end 342 | 343 | @impl true 344 | def eth_submit_hashrate(hashrate, id, opts \\ []) do 345 | params = [hashrate, id] 346 | 347 | request("eth_submitHashrate", params, opts) 348 | end 349 | 350 | @impl true 351 | def db_put_string(db, key, value, opts \\ []) do 352 | params = [db, key, value] 353 | 354 | request("db_putString", params, opts) 355 | end 356 | 357 | @impl true 358 | def db_get_string(db, key, opts \\ []) do 359 | params = [db, key] 360 | 361 | request("db_getString", params, opts) 362 | end 363 | 364 | @impl true 365 | def db_put_hex(db, key, data, opts \\ []) do 366 | params = [db, key, data] 367 | 368 | request("db_putHex", params, opts) 369 | end 370 | 371 | @impl true 372 | def db_get_hex(db, key, opts \\ []) do 373 | params = [db, key] 374 | 375 | request("db_getHex", params, opts) 376 | end 377 | 378 | @impl true 379 | def shh_post(whisper, opts \\ []) do 380 | params = [whisper] 381 | 382 | request("shh_post", params, opts) 383 | end 384 | 385 | @impl true 386 | def shh_version(opts \\ []) do 387 | request("shh_version", [], opts) 388 | end 389 | 390 | @impl true 391 | def shh_new_identity(opts \\ []) do 392 | request("shh_newIdentity", [], opts) 393 | end 394 | 395 | @impl true 396 | def shh_has_identity(address, opts \\ []) do 397 | params = [address] 398 | 399 | request("shh_hasIdentity", params, opts) 400 | end 401 | 402 | @impl true 403 | def shh_new_group(opts \\ []) do 404 | request("shh_newGroup", [], opts) 405 | end 406 | 407 | @impl true 408 | def shh_add_to_group(address, opts \\ []) do 409 | params = [address] 410 | 411 | request("shh_addToGroup", params, opts) 412 | end 413 | 414 | @impl true 415 | def shh_new_filter(filter_options, opts \\ []) do 416 | params = [filter_options] 417 | 418 | request("shh_newFilter", params, opts) 419 | end 420 | 421 | @impl true 422 | def shh_uninstall_filter(filter_id, opts \\ []) do 423 | params = [filter_id] 424 | 425 | request("shh_uninstallFilter", params, opts) 426 | end 427 | 428 | @impl true 429 | def shh_get_filter_changes(filter_id, opts \\ []) do 430 | params = [filter_id] 431 | 432 | request("shh_getFilterChanges", params, opts) 433 | end 434 | 435 | @impl true 436 | def shh_get_messages(filter_id, opts \\ []) do 437 | params = [filter_id] 438 | 439 | "shh_getMessages" |> request(params, opts) 440 | end 441 | 442 | @spec add_request_info(binary, [boolean() | binary | map | [binary]]) :: map 443 | defp add_request_info(method_name, params \\ []) do 444 | %{} 445 | |> Map.put("method", method_name) 446 | |> Map.put("jsonrpc", "2.0") 447 | |> Map.put("params", params) 448 | end 449 | 450 | @impl true 451 | def request(_name, _params, batch: true, url: _url), 452 | do: raise("Cannot use batch and url options at the same time") 453 | 454 | def request(name, params, batch: true) do 455 | name |> add_request_info(params) 456 | end 457 | 458 | def request(name, params, opts) do 459 | name 460 | |> add_request_info(params) 461 | |> server_request(opts) 462 | end 463 | 464 | @impl true 465 | def batch_request(methods, opts \\ []) do 466 | methods 467 | |> Enum.map(fn {method, params} -> 468 | opts = [batch: true] 469 | params = params ++ [opts] 470 | 471 | apply(__MODULE__, method, params) 472 | end) 473 | |> server_request(opts) 474 | end 475 | 476 | @impl true 477 | def single_request(payload, opts \\ []) do 478 | payload 479 | |> encode_payload 480 | |> post_request(opts) 481 | end 482 | 483 | @spec encode_payload(map()) :: binary() 484 | defp encode_payload(payload) do 485 | Config.json_module().encode!(payload) 486 | end 487 | 488 | @spec format_batch([map()]) :: [{:ok, map() | nil | binary()} | {:error, any}] 489 | def format_batch(list) do 490 | list 491 | |> Enum.sort(fn %{"id" => id1}, %{"id" => id2} -> 492 | id1 <= id2 493 | end) 494 | |> Enum.map(fn 495 | %{"result" => result} -> 496 | {:ok, result} 497 | 498 | %{"error" => error} -> 499 | {:error, error} 500 | 501 | other -> 502 | {:error, other} 503 | end) 504 | end 505 | 506 | defp post_request(payload, opts) do 507 | {:error, :not_implemented} 508 | end 509 | 510 | # The function that a behavior like HTTP or IPC needs to implement. 511 | defoverridable post_request: 2 512 | 513 | @spec server_request(list(map()) | map(), list()) :: {:ok, [any()]} | {:ok, any()} | error 514 | defp server_request(params, opts \\ []) do 515 | params 516 | |> prepare_request 517 | |> request(opts) 518 | end 519 | 520 | defp prepare_request(params) when is_list(params) do 521 | id = Counter.get(:rpc_counter) 522 | 523 | params = 524 | params 525 | |> Enum.with_index(1) 526 | |> Enum.map(fn {req_data, index} -> 527 | Map.put(req_data, "id", index + id) 528 | end) 529 | 530 | _ = Counter.increment(:rpc_counter, Enum.count(params), "eth_batch") 531 | 532 | params 533 | end 534 | 535 | defp prepare_request(params), 536 | do: Map.put(params, "id", Counter.increment(:rpc_counter, params["method"])) 537 | 538 | defp request(params, opts) do 539 | __MODULE__.single_request(params, opts) 540 | end 541 | end 542 | end 543 | end 544 | -------------------------------------------------------------------------------- /lib/ethereumex/client/behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Client.Behaviour do 2 | @moduledoc false 3 | @type error :: {:error, map() | binary() | atom()} 4 | 5 | # API methods 6 | 7 | @callback web3_client_version(keyword()) :: {:ok, binary()} | error 8 | @callback web3_sha3(binary(), keyword()) :: {:ok, binary()} | error 9 | @callback net_version(keyword()) :: {:ok, binary()} | error 10 | @callback net_peer_count(keyword()) :: {:ok, binary()} | error 11 | @callback net_listening(keyword()) :: {:ok, boolean()} | error 12 | @callback eth_protocol_version(keyword()) :: {:ok, binary()} | error 13 | @callback eth_syncing(keyword()) :: {:ok, map() | boolean()} | error 14 | @callback eth_chain_id(keyword()) :: {:ok, binary()} | error 15 | @callback eth_coinbase(keyword()) :: {:ok, binary()} | error 16 | @callback eth_mining(keyword()) :: {:ok, boolean()} | error 17 | @callback eth_hashrate(keyword()) :: {:ok, binary()} | error 18 | @callback eth_gas_price(keyword()) :: {:ok, binary()} | error 19 | @callback eth_max_priority_fee_per_gas(keyword()) :: {:ok, binary()} | error 20 | @callback eth_fee_history(binary(), binary(), list(binary()), keyword()) :: {:ok, map()} | error 21 | @callback eth_blob_base_fee(keyword()) :: {:ok, binary()} | error 22 | @callback eth_accounts(keyword()) :: {:ok, [binary()]} | error 23 | @callback eth_block_number(keyword()) :: {:ok, binary} | error 24 | @callback eth_get_balance(binary(), binary(), keyword()) :: {:ok, binary()} | error 25 | @callback eth_get_storage_at(binary(), binary(), binary(), keyword()) :: {:ok, binary()} | error 26 | @callback eth_get_transaction_count(binary(), binary(), keyword()) :: {:ok, binary()} | error 27 | @callback eth_get_block_transaction_count_by_hash(binary(), keyword()) :: 28 | {:ok, binary()} | error 29 | @callback eth_get_block_transaction_count_by_number(binary(), keyword()) :: 30 | {:ok, binary()} | error 31 | @callback eth_get_uncle_count_by_block_hash(binary(), keyword()) :: {:ok, binary()} | error 32 | @callback eth_get_uncle_count_by_block_number(binary(), keyword()) :: {:ok, binary()} | error 33 | @callback eth_get_code(binary(), binary(), keyword()) :: {:ok, binary()} | error 34 | @callback eth_sign(binary(), binary(), keyword()) :: {:ok, binary()} | error 35 | @callback eth_send_transaction(map(), keyword()) :: {:ok, binary()} | error 36 | @callback eth_send_raw_transaction(binary(), keyword()) :: {:ok, binary()} | error 37 | @callback eth_call(map, binary(), keyword()) :: {:ok, binary()} | error 38 | @callback eth_estimate_gas(map(), keyword()) :: {:ok, binary()} | error 39 | @callback eth_get_block_by_hash(binary(), boolean(), keyword()) :: {:ok, map()} | error 40 | @callback eth_get_block_by_number(binary(), boolean(), keyword()) :: {:ok, map()} | error 41 | @callback eth_get_transaction_by_hash(binary(), keyword()) :: {:ok, map()} | error 42 | @callback eth_get_transaction_by_block_hash_and_index(binary(), binary(), keyword()) :: 43 | {:ok, map()} | error 44 | @callback eth_get_transaction_by_block_number_and_index(binary(), binary(), keyword()) :: 45 | {:ok, binary()} | error 46 | @callback eth_get_transaction_receipt(binary(), keyword()) :: {:ok, map()} | error 47 | @callback eth_get_uncle_by_block_hash_and_index(binary(), binary(), keyword()) :: 48 | {:ok, map()} | error 49 | @callback eth_get_uncle_by_block_number_and_index(binary(), binary(), keyword()) :: 50 | {:ok, map()} | error 51 | @callback eth_get_compilers(keyword()) :: {:ok, [binary()]} | error 52 | @callback eth_compile_lll(binary(), keyword()) :: {:ok, binary()} | error 53 | @callback eth_compile_solidity(binary(), keyword()) :: {:ok, binary()} | error 54 | @callback eth_compile_serpent(binary(), keyword()) :: {:ok, binary()} | error 55 | @callback eth_new_filter(map(), keyword()) :: {:ok, binary()} | error 56 | @callback eth_new_block_filter(keyword()) :: {:ok, binary()} | error 57 | @callback eth_new_pending_transaction_filter(keyword()) :: {:ok, binary()} | error 58 | @callback eth_uninstall_filter(binary(), keyword()) :: {:ok, boolean()} | error 59 | @callback eth_get_filter_changes(binary(), keyword()) :: {:ok, [binary()] | [map()]} | error 60 | @callback eth_get_filter_logs(binary(), keyword()) :: {:ok, [binary()] | [map()]} | error 61 | @callback eth_get_logs(map(), keyword()) :: {:ok, [binary()] | [map()]} | error 62 | @callback eth_get_work(keyword()) :: {:ok, [binary()]} | error 63 | @callback eth_get_proof(binary(), list(binary()), binary(), keyword()) :: {:ok, map()} | error 64 | @callback eth_submit_work(binary(), binary(), binary(), keyword()) :: {:ok, boolean()} | error 65 | @callback eth_submit_hashrate(binary(), binary(), keyword()) :: {:ok, boolean()} | error 66 | @callback db_put_string(binary(), binary(), binary(), keyword()) :: {:ok, boolean()} | error 67 | @callback db_get_string(binary(), binary(), keyword()) :: {:ok, binary()} | error 68 | @callback db_put_hex(binary(), binary(), binary(), keyword()) :: {:ok, boolean()} | error 69 | @callback db_get_hex(binary(), binary(), keyword()) :: {:ok, binary()} | error 70 | @callback shh_post(map(), keyword()) :: {:ok, boolean()} | error 71 | @callback shh_version(keyword()) :: {:ok, binary()} | error 72 | @callback shh_new_identity(keyword()) :: {:ok, binary()} | error 73 | @callback shh_has_identity(binary(), keyword()) :: {:ok, boolean} | error 74 | @callback shh_new_group(keyword()) :: {:ok, binary()} | error 75 | @callback shh_add_to_group(binary(), keyword()) :: {:ok, boolean()} | error 76 | @callback shh_new_filter(map(), keyword()) :: {:ok, binary()} | error 77 | @callback shh_uninstall_filter(binary(), keyword()) :: {:ok, binary()} | error 78 | @callback shh_get_filter_changes(binary(), keyword()) :: {:ok, [map()]} | error 79 | @callback shh_get_messages(binary(), keyword()) :: {:ok, [map()]} | error 80 | 81 | # actual request methods 82 | 83 | @callback request(binary(), list(boolean() | binary()), keyword()) :: 84 | {:ok, any() | [any()]} | error 85 | @callback single_request(map(), keyword()) :: {:ok, any() | [any()]} | error 86 | @callback batch_request([{atom(), list(boolean() | binary())}], keyword()) :: 87 | {:ok, [any()]} | error 88 | end 89 | -------------------------------------------------------------------------------- /lib/ethereumex/client/websocket_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.WebsocketClient do 2 | @moduledoc """ 3 | WebSocket-based Ethereum JSON-RPC client implementation with real-time subscription support. 4 | 5 | This module provides a WebSocket client interface for both standard Ethereum JSON-RPC calls 6 | and real-time event subscriptions. It: 7 | 1. Inherits the standard JSON-RPC method definitions from BaseClient 8 | 2. Implements request handling through a persistent WebSocket connection 9 | 3. Provides subscription management for real-time events 10 | 11 | ## Standard RPC Usage 12 | 13 | iex> Ethereumex.WebsocketClient.eth_block_number() 14 | {:ok, "0x1234"} 15 | 16 | iex> Ethereumex.WebsocketClient.eth_get_balance("0x407d73d8a49eeb85d32cf465507dd71d507100c1") 17 | {:ok, "0x0234c8a3397aab58"} 18 | 19 | ## Subscription Usage 20 | 21 | ### Subscribe to New Blocks 22 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:newHeads) 23 | {:ok, "0x9cef478923ff08bf67fde6c64013158d"} 24 | 25 | # Receive notifications in the subscriber process 26 | receive do 27 | %{ 28 | "method" => "eth_subscription", 29 | "params" => %{ 30 | "subscription" => "0x9cef478923ff08bf67fde6c64013158d", 31 | "result" => %{"number" => "0x1b4", ...} 32 | } 33 | } -> :ok 34 | end 35 | 36 | ### Subscribe to Contract Events 37 | iex> filter = %{ 38 | ...> address: "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", 39 | ...> topics: ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"] 40 | ...> } 41 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:logs, filter) 42 | {:ok, "0x4a8a4c0517381924f9838102c5a4dcb7"} 43 | 44 | ### Subscribe to Pending Transactions 45 | iex> {:ok, subscription_id} = Ethereumex.WebsocketClient.subscribe(:newPendingTransactions) 46 | {:ok, "0x1234567890abcdef1234567890abcdef"} 47 | 48 | ### Unsubscribe 49 | # Single subscription 50 | iex> Ethereumex.WebsocketClient.unsubscribe("0x9cef478923ff08bf67fde6c64013158d") 51 | {:ok, true} 52 | 53 | # Multiple subscriptions 54 | iex> Ethereumex.WebsocketClient.unsubscribe([ 55 | ...> "0x9cef478923ff08bf67fde6c64013158d", 56 | ...> "0x4a8a4c0517381924f9838102c5a4dcb7" 57 | ...> ]) 58 | {:ok, true} 59 | 60 | ## Features 61 | 62 | * All standard JSON-RPC methods from `Ethereumex.Client.BaseClient` 63 | * Real-time event subscriptions: 64 | - New block headers (`:newHeads`) 65 | - Log events (`:logs`) 66 | - Pending transactions (`:newPendingTransactions`) 67 | * Automatic WebSocket connection management 68 | * Request-response matching 69 | * Automatic reconnection on failures 70 | 71 | The client maintains a persistent WebSocket connection through `WebsocketServer`, 72 | which handles connection management, request-response matching, subscription 73 | management, and automatic reconnection on failures. 74 | """ 75 | 76 | use Ethereumex.Client.BaseClient 77 | 78 | alias Ethereumex.Counter 79 | alias Ethereumex.WebsocketServer 80 | 81 | @event_types [:newHeads, :logs, :newPendingTransactions] 82 | 83 | def post_request(payload, _opts) do 84 | WebsocketServer.post(payload) 85 | end 86 | 87 | @doc """ 88 | Subscribes to Ethereum events. 89 | 90 | ## Parameters 91 | - event_type: Type of event to subscribe to (:newHeads, :logs, :newPendingTransactions) 92 | - filter_params: Optional parameters for filtering events (mainly used for :logs) 93 | 94 | ## Examples 95 | 96 | # Subscribe to new blocks 97 | iex> subscribe(:newHeads) 98 | {:ok, "0x9cef478923ff08bf67fde6c64013158d"} 99 | 100 | # Subscribe to specific logs 101 | iex> subscribe(:logs, %{address: "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd"}) 102 | {:ok, "0x4a8a4c0517381924f9838102c5a4dcb7"} 103 | 104 | Returns `{:ok, subscription_id}` on success or `{:error, reason}` on failure. 105 | """ 106 | @spec subscribe(WebsocketServer.event_type(), map() | nil) :: 107 | {:ok, String.t()} | {:error, term()} 108 | def subscribe(event_type, filter_params \\ nil) when event_type in @event_types do 109 | params = [Atom.to_string(event_type)] 110 | params = if filter_params, do: params ++ [filter_params], else: params 111 | 112 | method = "eth_subscribe" 113 | 114 | request = %{ 115 | jsonrpc: "2.0", 116 | method: method, 117 | params: params, 118 | id: Counter.increment(:rpc_counter, method) 119 | } 120 | 121 | WebsocketServer.subscribe(request) 122 | end 123 | 124 | @doc """ 125 | Unsubscribes from one or more existing subscriptions. 126 | 127 | ## Parameters 128 | - subscription_ids: A single subscription ID or a list of subscription IDs returned from subscribe/2 129 | 130 | ## Examples 131 | 132 | # Unsubscribe from a single subscription 133 | iex> unsubscribe("0x9cef478923ff08bf67fde6c64013158d") 134 | {:ok, true} 135 | 136 | # Unsubscribe from multiple subscriptions at once 137 | iex> unsubscribe(["0x9cef478923ff08bf67fde6c64013158d", "0x4a8a4c0517381924f9838102c5a4dcb7"]) 138 | {:ok, true} 139 | 140 | Returns `{:ok, true}` on success or `{:error, reason}` on failure. 141 | """ 142 | @spec unsubscribe(String.t() | list(String.t())) :: {:ok, boolean()} | {:error, term()} 143 | def unsubscribe(subscription_ids) when is_list(subscription_ids) do 144 | method = "eth_unsubscribe" 145 | 146 | request = %{ 147 | jsonrpc: "2.0", 148 | method: method, 149 | params: subscription_ids, 150 | id: Counter.increment(:rpc_counter, method) 151 | } 152 | 153 | WebsocketServer.unsubscribe(request) 154 | end 155 | 156 | def unsubscribe(subscription_id) when is_binary(subscription_id) do 157 | unsubscribe([subscription_id]) 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/ethereumex/client/websocket_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.WebsocketServer do 2 | @moduledoc """ 3 | WebSocket client implementation for Ethereum JSON-RPC API. 4 | 5 | This module manages a persistent WebSocket connection to an Ethereum node and handles 6 | the complete request-response cycle for JSON-RPC calls, including subscriptions. It maintains 7 | state of ongoing requests, subscriptions, and matches responses to their original callers. 8 | 9 | ## Features 10 | 11 | * Standard JSON-RPC requests via WebSocket 12 | * Real-time event subscriptions (newHeads, logs, newPendingTransactions) 13 | * Automatic reconnection with exponential backoff 14 | * Batch request support 15 | * Concurrent request handling 16 | 17 | ## Request Types 18 | 19 | ### Standard Requests 20 | 21 | Standard JSON-RPC requests are handled through the `post/1` function: 22 | 23 | ```elixir 24 | {:ok, result} = WebsocketServer.post(encoded_request) 25 | ``` 26 | 27 | ### Subscriptions 28 | 29 | The module supports Ethereum's pub/sub functionality for real-time events: 30 | 31 | ```elixir 32 | # Subscribe to new block headers 33 | {:ok, subscription_id} = WebsocketServer.subscribe(%{ 34 | jsonrpc: "2.0", 35 | method: "eth_subscribe", 36 | params: ["newHeads"], 37 | id: 1 38 | }) 39 | 40 | # Subscribe to logs with filter 41 | {:ok, subscription_id} = WebsocketServer.subscribe(%{ 42 | jsonrpc: "2.0", 43 | method: "eth_subscribe", 44 | params: ["logs", %{address: "0x123..."}], 45 | id: 2 46 | }) 47 | 48 | # Unsubscribe (single or multiple subscriptions) 49 | {:ok, true} = WebsocketServer.unsubscribe(%{ 50 | jsonrpc: "2.0", 51 | method: "eth_unsubscribe", 52 | params: [subscription_id], 53 | id: 3 54 | }) 55 | ``` 56 | 57 | ## State Management 58 | 59 | The module maintains several state maps: 60 | 61 | ```elixir 62 | %State{ 63 | requests: %{request_id => caller_pid}, # Standard requests 64 | subscription_requests: %{request_id => caller_pid}, # Pending subscriptions 65 | unsubscription_requests: %{request_id => caller_pid}, # Pending unsubscriptions 66 | subscriptions: %{subscription_id => subscriber_pid} # Active subscriptions 67 | } 68 | ``` 69 | 70 | ## Subscription Notifications 71 | 72 | When a subscribed event occurs, the notification is automatically forwarded to the 73 | subscriber process. Notifications are received as messages in the format: 74 | 75 | ```elixir 76 | # New headers notification 77 | %{ 78 | "jsonrpc" => "2.0", 79 | "method" => "eth_subscription", 80 | "params" => %{ 81 | "subscription" => "0x9cef478923ff08bf67fde6c64013158d", 82 | "result" => %{ 83 | "number" => "0x1b4", 84 | "hash" => "0x8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d", 85 | "parentHash" => "0x9646252be9520f6e71339a8df9c55e4d7619deeb018d2a3f2d21fc165dde5eb5" 86 | } 87 | } 88 | } 89 | 90 | # Logs notification 91 | %{ 92 | "jsonrpc" => "2.0", 93 | "method" => "eth_subscription", 94 | "params" => %{ 95 | "subscription" => "0x4a8a4c0517381924f9838102c5a4dcb7", 96 | "result" => %{ 97 | "address" => "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", 98 | "topics" => ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"], 99 | "data" => "0x000000000000000000000000000000000000000000000000000000000000000a" 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | ## Error Handling 106 | 107 | - Connection failures are automatically retried with exponential backoff: 108 | * Up to `@max_reconnect_attempts` attempts 109 | * Starting with `@backoff_initial_delay` ms delay, doubling each attempt 110 | * Reconnection attempts are logged 111 | - Request timeouts after `@request_timeout` ms 112 | - Invalid JSON responses are handled gracefully 113 | - Unmatched responses (no waiting caller) are safely ignored 114 | - Subscription errors are propagated to the subscriber 115 | """ 116 | use WebSockex 117 | 118 | alias Ethereumex.Config 119 | 120 | require Logger 121 | 122 | @request_timeout 5_000 123 | @max_reconnect_attempts 5 124 | @backoff_initial_delay 1_000 125 | 126 | @type request_id :: pos_integer() | String.t() 127 | @type subscription_id :: String.t() 128 | @type event_type :: :newHeads | :logs | :newPendingTransactions 129 | 130 | defmodule State do 131 | @moduledoc """ 132 | Server state containing: 133 | - url: WebSocket endpoint URL 134 | - requests: Map of pending requests with their corresponding sender PIDs 135 | - subscription requests: Map of pending subscription requests with their corresponding sender PIDs 136 | - unsubscription requests: Map of pending unsubscription requests with their corresponding sender PIDs 137 | - subscriptions: Map of subscription IDs to subscriber PIDs 138 | """ 139 | defstruct [ 140 | :url, 141 | requests: %{}, 142 | subscription_requests: %{}, 143 | unsubscription_requests: %{}, 144 | subscriptions: %{}, 145 | reconnect_attempts: 0 146 | ] 147 | 148 | @type t :: %__MODULE__{ 149 | url: String.t(), 150 | requests: %{Ethereumex.WebsocketServer.request_id() => pid()}, 151 | subscription_requests: %{Ethereumex.WebsocketServer.request_id() => pid()}, 152 | unsubscription_requests: %{Ethereumex.WebsocketServer.request_id() => pid()}, 153 | subscriptions: %{Ethereumex.WebsocketServer.subscription_id() => pid()}, 154 | reconnect_attempts: non_neg_integer() 155 | } 156 | end 157 | 158 | # Public API 159 | 160 | @doc """ 161 | Starts the WebSocket connection. 162 | 163 | ## Options 164 | - :url - WebSocket endpoint URL (defaults to Config.websocket_url()) 165 | - :name - Process name (defaults to __MODULE__) 166 | """ 167 | @spec start_link(keyword()) :: {:ok, pid()} | {:error, term()} 168 | def start_link(opts \\ []) do 169 | url = Keyword.get(opts, :url, Config.websocket_url()) 170 | name = Keyword.get(opts, :name, __MODULE__) 171 | 172 | WebSockex.start_link( 173 | url, 174 | __MODULE__, 175 | %State{url: url}, 176 | name: name, 177 | handle_initial_conn_failure: true 178 | ) 179 | end 180 | 181 | @doc """ 182 | Sends a JSON-RPC request and waits for response. 183 | 184 | Returns `{:ok, result}` on success or `{:error, reason}` on failure. 185 | Times out after #{@request_timeout}ms. 186 | """ 187 | @spec post(binary()) :: 188 | {:ok, term()} | {:error, :invalid_request_format | :timeout | :decoded_error} 189 | def post(encoded_request) when is_binary(encoded_request) do 190 | with {:ok, decoded} <- decode_request(encoded_request), 191 | id <- get_request_id(decoded), 192 | :ok <- send_request(id, encoded_request) do 193 | await_response(id) 194 | end 195 | end 196 | 197 | @doc """ 198 | Subscribes to Ethereum events via WebSocket. 199 | 200 | The request should be a map containing: 201 | - id: A unique request identifier 202 | - method: "eth_subscribe" 203 | - params: Parameters for the subscription, including the event type 204 | 205 | Returns `{:ok, subscription_id}` on success or `{:error, reason}` on failure. 206 | Times out after #{@request_timeout}ms. 207 | """ 208 | @spec subscribe(map()) :: 209 | {:ok, subscription_id()} | {:error, :invalid_request_format | :timeout | :decoded_error} 210 | def subscribe(request) do 211 | :ok = WebSockex.cast(__MODULE__, {:subscription, request, self()}) 212 | await_response(request.id) 213 | end 214 | 215 | @doc """ 216 | Unsubscribes from an existing Ethereum event subscription. 217 | 218 | The request should be a map containing: 219 | - id: A unique request identifier 220 | - method: "eth_unsubscribe" 221 | - params: A list containing the subscription IDs to unsubscribe from 222 | 223 | Returns `{:ok, true}` on success or `{:error, reason}` on failure. 224 | Times out after #{@request_timeout}ms. 225 | """ 226 | @spec unsubscribe(map()) :: 227 | {:ok, true} | {:error, :invalid_request_format | :timeout | :decoded_error} 228 | def unsubscribe(request) do 229 | :ok = WebSockex.cast(__MODULE__, {:unsubscribe, request, self()}) 230 | await_response(request.id) 231 | end 232 | 233 | # Callbacks 234 | 235 | @impl WebSockex 236 | def handle_connect(_conn, %State{} = state) do 237 | Logger.info("Connected to WebSocket server at #{state.url}") 238 | {:ok, %State{state | reconnect_attempts: 0}} 239 | end 240 | 241 | @impl WebSockex 242 | def handle_cast({:request, id, request, from}, %State{} = state) do 243 | requests = Map.put(state.requests, id, from) 244 | new_state = %State{state | requests: requests} 245 | {:reply, {:text, request}, new_state} 246 | end 247 | 248 | def handle_cast({:subscription, request, from}, %State{} = state) do 249 | subscription_requests = Map.put(state.subscription_requests, request.id, from) 250 | new_state = %State{state | subscription_requests: subscription_requests} 251 | {:reply, {:text, Config.json_module().encode!(request)}, new_state} 252 | end 253 | 254 | def handle_cast({:unsubscribe, request, from}, %State{} = state) do 255 | unsubscription_requests = 256 | Map.put(state.unsubscription_requests, request.id, {from, request.params}) 257 | 258 | new_state = %State{state | unsubscription_requests: unsubscription_requests} 259 | {:reply, {:text, Config.json_module().encode!(request)}, new_state} 260 | end 261 | 262 | @impl WebSockex 263 | def handle_frame({:text, body}, %State{} = state) do 264 | case Config.json_module().decode(body) do 265 | {:ok, response} -> handle_response(response, state) 266 | _ -> {:ok, state} 267 | end 268 | end 269 | 270 | @impl WebSockex 271 | def handle_disconnect(%{reason: {:local, _reason}}, state) do 272 | {:ok, state} 273 | end 274 | 275 | @impl WebSockex 276 | def handle_disconnect(connection_status, %State{} = state) do 277 | new_attempts = state.reconnect_attempts + 1 278 | 279 | if should_retry?(new_attempts) do 280 | handle_retry(connection_status, state, new_attempts) 281 | else 282 | handle_max_attempts_reached(connection_status, state) 283 | end 284 | end 285 | 286 | # Private Functions 287 | 288 | @spec decode_request(String.t()) :: {:ok, map()} | {:error, term()} 289 | defp decode_request(encoded_request) do 290 | case Config.json_module().decode(encoded_request) do 291 | {:ok, %{"id" => _id} = decoded} -> {:ok, decoded} 292 | {:ok, decoded} when is_list(decoded) -> {:ok, decoded} 293 | {:ok, _} -> {:error, :invalid_request_format} 294 | _error -> {:error, :decode_error} 295 | end 296 | end 297 | 298 | @spec send_request(request_id(), String.t()) :: :ok 299 | defp send_request(id, encoded_request) do 300 | WebSockex.cast(__MODULE__, {:request, id, encoded_request, self()}) 301 | end 302 | 303 | @spec await_response(request_id()) :: {:ok, term()} | {:error, :timeout} 304 | defp await_response(id) do 305 | receive do 306 | {:response, ^id, result} -> {:ok, result} 307 | after 308 | @request_timeout -> {:error, :timeout} 309 | end 310 | end 311 | 312 | # when a response is a subscription notification 313 | defp handle_response(%{"method" => "eth_subscription"} = notification, state) do 314 | subscription = notification["params"]["subscription"] 315 | subscriber = Map.get(state.subscriptions, subscription) 316 | 317 | if not is_nil(subscriber) do 318 | send(subscriber, notification) 319 | end 320 | 321 | {:ok, state} 322 | end 323 | 324 | # when a response is a regular JSON-RPC response 325 | defp handle_response(response, state) do 326 | id = get_request_id(response) 327 | result = get_response_result(response) 328 | 329 | state = 330 | cond do 331 | Map.has_key?(state.requests, id) -> 332 | handle_request_response(state, id, result) 333 | 334 | Map.has_key?(state.subscription_requests, id) -> 335 | handle_subscription_response(state, id, result) 336 | 337 | Map.has_key?(state.unsubscription_requests, id) -> 338 | handle_unsubscription_response(state, id, result) 339 | 340 | true -> 341 | state 342 | end 343 | 344 | {:ok, state} 345 | end 346 | 347 | defp handle_request_response(state, id, result) do 348 | send(Map.get(state.requests, id), {:response, id, result}) 349 | %State{state | requests: Map.delete(state.requests, id)} 350 | end 351 | 352 | defp handle_subscription_response(state, id, result) do 353 | pid = Map.get(state.subscription_requests, id) 354 | send(pid, {:response, id, result}) 355 | 356 | subscription_requests = Map.delete(state.subscription_requests, id) 357 | subscriptions = Map.put(state.subscriptions, result, pid) 358 | %State{state | subscription_requests: subscription_requests, subscriptions: subscriptions} 359 | end 360 | 361 | defp handle_unsubscription_response(state, id, result) do 362 | {pid, subscription_ids} = Map.get(state.unsubscription_requests, id) 363 | send(pid, {:response, id, result}) 364 | 365 | subscription_requests = Map.delete(state.subscription_requests, id) 366 | subscriptions = Map.drop(state.subscriptions, subscription_ids) 367 | %State{state | subscription_requests: subscription_requests, subscriptions: subscriptions} 368 | end 369 | 370 | @spec get_request_id(list(map()) | map()) :: request_id() 371 | defp get_request_id(%{"id" => id}), do: id 372 | 373 | defp get_request_id(decoded_request) when is_list(decoded_request) do 374 | Enum.map_join(decoded_request, "_", & &1["id"]) 375 | end 376 | 377 | @spec get_response_result(list(map()) | map()) :: term() | nil 378 | defp get_response_result(%{"result" => result}), do: result 379 | 380 | defp get_response_result(result) when is_list(result) do 381 | Enum.map(result, & &1["result"]) 382 | end 383 | 384 | defp get_response_result(_), do: nil 385 | 386 | defp should_retry?(attempts), do: attempts <= @max_reconnect_attempts 387 | 388 | defp handle_retry(connection_status, state, attempts) do 389 | log_retry_attempt(connection_status.reason, attempts) 390 | apply_backoff_delay(attempts) 391 | {:reconnect, %State{state | reconnect_attempts: attempts}} 392 | end 393 | 394 | defp handle_max_attempts_reached(connection_status, state) do 395 | Logger.error( 396 | "WebSocket disconnected: #{inspect(connection_status.reason)}. " <> 397 | "Max reconnection attempts (#{@max_reconnect_attempts}) reached." 398 | ) 399 | 400 | {:ok, state} 401 | end 402 | 403 | defp log_retry_attempt(reason, attempts) do 404 | Logger.warning( 405 | "WebSocket disconnected: #{inspect(reason)}. " <> 406 | "Attempting reconnection #{attempts}/#{@max_reconnect_attempts}" 407 | ) 408 | end 409 | 410 | defp apply_backoff_delay(attempts) do 411 | backoff = @backoff_initial_delay * :math.pow(2, attempts - 1) 412 | Process.sleep(trunc(backoff)) 413 | end 414 | end 415 | -------------------------------------------------------------------------------- /lib/ethereumex/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Config do 2 | @moduledoc false 3 | alias Ethereumex.IpcServer 4 | alias Ethereumex.WebsocketServer 5 | 6 | def setup_children(), do: setup_children(client_type()) 7 | 8 | def setup_children(:ipc) do 9 | [ 10 | :poolboy.child_spec(:worker, poolboy_config(), 11 | path: ipc_path(), 12 | ipc_request_timeout: ipc_request_timeout() 13 | ) 14 | ] 15 | end 16 | 17 | def setup_children(:http) do 18 | pool_opts = http_pool_options() 19 | 20 | [ 21 | {Finch, name: Ethereumex.Finch, pools: pool_opts} 22 | ] 23 | end 24 | 25 | def setup_children(:websocket) do 26 | [ 27 | {WebsocketServer, []} 28 | ] 29 | end 30 | 31 | def setup_children(opt), do: raise("Invalid :client option (#{opt}) in config") 32 | 33 | @spec rpc_url() :: binary() 34 | def rpc_url() do 35 | case Application.get_env(:ethereumex, :url) do 36 | url when is_binary(url) and url != "" -> 37 | url 38 | 39 | els -> 40 | raise ArgumentError, 41 | message: 42 | "Please set config variable `config :ethereumex, :url, \"http://...\", got: `#{inspect(els)}`" 43 | end 44 | end 45 | 46 | @spec websocket_url() :: binary() 47 | def websocket_url() do 48 | case Application.get_env(:ethereumex, :websocket_url, "") do 49 | url when is_binary(url) and url != "" -> 50 | url 51 | 52 | not_a_url -> 53 | raise ArgumentError, 54 | message: 55 | "Please set config variable `config :ethereumex, :websocket_url, \"ws://...\", got `#{not_a_url}`." 56 | end 57 | end 58 | 59 | @spec ipc_path() :: binary() 60 | def ipc_path() do 61 | case Application.get_env(:ethereumex, :ipc_path, "") do 62 | path when is_binary(path) and path != "" -> 63 | path 64 | 65 | not_a_path -> 66 | raise ArgumentError, 67 | message: 68 | "Please set config variable `config :ethereumex, :ipc_path, \"path/to/ipc\", got `#{not_a_path}`. Note: System.user_home! will be prepended to path for you on initialization" 69 | end 70 | end 71 | 72 | @spec http_options() :: keyword() 73 | def http_options() do 74 | Application.get_env(:ethereumex, :http_options, []) 75 | end 76 | 77 | @spec http_headers() :: [{String.t(), String.t()}] 78 | def http_headers() do 79 | Application.get_env(:ethereumex, :http_headers, []) 80 | end 81 | 82 | @spec client_type() :: atom() 83 | def client_type() do 84 | Application.get_env(:ethereumex, :client_type, :http) 85 | end 86 | 87 | @spec enable_request_error_logs() :: boolean() 88 | def enable_request_error_logs() do 89 | Application.get_env(:ethereumex, :enable_request_error_logs, false) 90 | end 91 | 92 | @spec format_batch() :: boolean() 93 | def format_batch() do 94 | Application.get_env(:ethereumex, :format_batch, true) 95 | end 96 | 97 | @spec poolboy_config() :: keyword() 98 | defp poolboy_config() do 99 | [ 100 | {:name, {:local, :ipc_worker}}, 101 | {:worker_module, IpcServer}, 102 | {:size, ipc_worker_size()}, 103 | {:max_overflow, ipc_max_worker_overflow()} 104 | ] 105 | end 106 | 107 | @spec json_module() :: module() 108 | def json_module() do 109 | Application.get_env(:ethereumex, :json_module, Jason) 110 | end 111 | 112 | @spec ipc_worker_size() :: integer() 113 | defp ipc_worker_size() do 114 | Application.get_env(:ethereumex, :ipc_worker_size, 5) 115 | end 116 | 117 | @spec ipc_max_worker_overflow() :: integer() 118 | defp ipc_max_worker_overflow() do 119 | Application.get_env(:ethereumex, :ipc_max_worker_overflow, 2) 120 | end 121 | 122 | @spec ipc_request_timeout() :: integer() 123 | defp ipc_request_timeout() do 124 | Application.get_env(:ethereumex, :ipc_request_timeout, 60_000) 125 | end 126 | 127 | @spec http_pool_options() :: map() 128 | defp http_pool_options() do 129 | Application.get_env(:ethereumex, :http_pool_options, %{}) 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/ethereumex/counter.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Counter do 2 | @moduledoc """ 3 | An application wide RPC2Ethereum client request counter. 4 | """ 5 | @tab :rpc_requests_counter 6 | 7 | @spec setup() :: :ok 8 | def setup() do 9 | @tab = 10 | :ets.new(@tab, [ 11 | :set, 12 | :named_table, 13 | :public, 14 | write_concurrency: true 15 | ]) 16 | 17 | :ok 18 | end 19 | 20 | @spec get(atom()) :: integer() 21 | def get(key) do 22 | case :ets.lookup(@tab, key) do 23 | [] -> 1 24 | [{^key, num}] -> num 25 | end 26 | end 27 | 28 | @spec increment(atom(), String.t()) :: integer() 29 | def increment(key, method) do 30 | do_increment(Application.get_env(:ethereumex, :id_reset), key, method) 31 | end 32 | 33 | @spec increment(atom(), integer(), String.t()) :: integer() 34 | def increment(key, count, method) do 35 | do_increment(Application.get_env(:ethereumex, :id_reset), key, count, method) 36 | end 37 | 38 | @spec do_increment(binary() | nil, atom(), String.t()) :: integer() 39 | defp do_increment(true, key, method) do 40 | :ets.insert(@tab, {key, 0}) 41 | inc(key, method) 42 | end 43 | 44 | defp do_increment(_, key, method) do 45 | inc(key, method) 46 | end 47 | 48 | @spec do_increment(boolean() | nil, atom(), integer(), String.t()) :: integer() 49 | defp do_increment(true, key, count, method) do 50 | :ets.insert(@tab, {key, 0}) 51 | inc(key, count, method) 52 | end 53 | 54 | defp do_increment(_, key, count, method) do 55 | inc(key, count, method) 56 | end 57 | 58 | defp inc(key, method) do 59 | _ = :telemetry.execute([:ethereumex], %{counter: 1}, %{method_name: method}) 60 | _ = :telemetry.execute([:ethereumex, String.to_atom(method)], %{counter: 1}) 61 | :ets.update_counter(@tab, key, {2, 1}, {key, 0}) 62 | end 63 | 64 | defp inc(key, count, method) do 65 | _ = :telemetry.execute([:ethereumex], %{counter: 1}, %{method_name: method}) 66 | _ = :telemetry.execute([:ethereumex, String.to_atom(method)], %{counter: 1}) 67 | :ets.update_counter(@tab, key, {2, count}, {key, 0}) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/ethereumex/http_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.HttpClient do 2 | @moduledoc false 3 | 4 | use Ethereumex.Client.BaseClient 5 | 6 | alias Ethereumex.Config 7 | 8 | require Logger 9 | 10 | @type empty_response :: :empty_response 11 | @type invalid_json :: {:invalid_json, any()} 12 | @type http_client_error :: {:error, empty_response() | invalid_json() | any()} 13 | 14 | @spec post_request(binary(), Keyword.t()) :: {:ok, any()} | http_client_error() 15 | def post_request(payload, opts) do 16 | headers = headers(opts) 17 | url = Keyword.get(opts, :url) || Config.rpc_url() 18 | 19 | format_batch = 20 | case Keyword.get(opts, :format_batch) do 21 | nil -> Config.format_batch() 22 | value -> value 23 | end 24 | 25 | request = Finch.build(:post, url, headers, payload) 26 | 27 | case Finch.request(request, Ethereumex.Finch, Config.http_options()) do 28 | {:ok, %Finch.Response{body: body, status: code}} -> 29 | case decode_body(body, code, format_batch) do 30 | {:ok, _response} = result -> 31 | result 32 | 33 | error -> 34 | maybe_log_error( 35 | "[#{__MODULE__}] Decode failed, body - #{inspect(body)}, payload - #{inspect(payload)}, error - #{inspect(error)}" 36 | ) 37 | 38 | error 39 | end 40 | 41 | {:error, error} -> 42 | maybe_log_error( 43 | "[#{__MODULE__}] Request failed, payload - #{inspect(payload)}, error - #{inspect(error)}" 44 | ) 45 | 46 | {:error, error} 47 | end 48 | end 49 | 50 | defp headers(opts) do 51 | headers = Keyword.get(opts, :http_headers) || Config.http_headers() 52 | 53 | [{"Content-Type", "application/json"} | headers] 54 | end 55 | 56 | @spec decode_body(binary(), non_neg_integer(), boolean()) :: {:ok, any()} | http_client_error() 57 | defp decode_body(body, code, format_batch) do 58 | case Config.json_module().decode(body) do 59 | {:ok, decoded_body} -> 60 | case {code, decoded_body} do 61 | {200, %{"error" => error}} -> 62 | {:error, error} 63 | 64 | {200, result = [%{} | _]} -> 65 | {:ok, maybe_format_batch(result, format_batch)} 66 | 67 | {200, %{"result" => result}} -> 68 | {:ok, result} 69 | 70 | _ -> 71 | {:error, decoded_body} 72 | end 73 | 74 | {:error, error} -> 75 | {:error, {:invalid_json, error}} 76 | end 77 | end 78 | 79 | defp maybe_format_batch(responses, true), do: format_batch(responses) 80 | 81 | defp maybe_format_batch(responses, _), do: responses 82 | 83 | defp maybe_log_error(message) do 84 | if Config.enable_request_error_logs() do 85 | Logger.error(message) 86 | else 87 | :ok 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/ethereumex/ipc_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.IpcClient do 2 | @moduledoc false 3 | 4 | use Ethereumex.Client.BaseClient 5 | 6 | alias Ethereumex.Config 7 | alias Ethereumex.IpcServer 8 | 9 | @timeout 60_000 10 | @spec post_request(binary(), []) :: {:ok | :error, any()} 11 | def post_request(payload, _opts) do 12 | with {:ok, response} <- call_ipc(payload), 13 | {:ok, decoded_body} <- jason_decode(response) do 14 | case decoded_body do 15 | %{"error" => error} -> {:error, error} 16 | result = [%{} | _] -> {:ok, format_batch(result)} 17 | result -> {:ok, Map.get(result, "result")} 18 | end 19 | else 20 | {:error, error} -> {:error, error} 21 | error -> {:error, error} 22 | end 23 | end 24 | 25 | defp call_ipc(payload) do 26 | :poolboy.transaction(:ipc_worker, fn pid -> IpcServer.post(pid, payload) end, @timeout) 27 | end 28 | 29 | defp jason_decode(response) do 30 | case Config.json_module().decode(response) do 31 | {:ok, _result} = result -> result 32 | {:error, error} -> {:error, {:decode_error, error}} 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ethereumex/ipc_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.IpcServer do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | def start_link(state \\ []) do 7 | GenServer.start_link(__MODULE__, Keyword.merge(state, socket: nil)) 8 | end 9 | 10 | def post(pid, request) do 11 | GenServer.call(pid, {:request, request}) 12 | end 13 | 14 | def init(state) do 15 | opts = [:binary, active: false, reuseaddr: true] 16 | 17 | response = :gen_tcp.connect({:local, state[:path]}, 0, opts) 18 | 19 | case response do 20 | {:ok, socket} -> {:ok, Keyword.put(state, :socket, socket)} 21 | {:error, reason} -> {:error, reason} 22 | end 23 | end 24 | 25 | def handle_call( 26 | {:request, request}, 27 | _from, 28 | [socket: socket, path: _, ipc_request_timeout: timeout] = state 29 | ) do 30 | response = 31 | socket 32 | |> :gen_tcp.send(request) 33 | |> receive_response(socket, timeout) 34 | 35 | {:reply, response, state} 36 | end 37 | 38 | defp receive_response(data, socket, timeout), do: receive_response(data, socket, timeout, <<>>) 39 | 40 | defp receive_response({:error, reason}, _socket, _timeout, _result) do 41 | {:error, reason} 42 | end 43 | 44 | defp receive_response(:ok, socket, timeout, result) do 45 | with {:ok, response} <- :gen_tcp.recv(socket, 0, timeout) do 46 | new_result = result <> response 47 | 48 | if String.ends_with?(response, "\n") do 49 | {:ok, new_result} 50 | else 51 | receive_response(:ok, socket, timeout, new_result) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/exthereum/ethereumex" 5 | @version "0.12.1" 6 | 7 | def project do 8 | [ 9 | app: :ethereumex, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | build_embedded: Mix.env() == :prod, 13 | start_permanent: Mix.env() == :prod, 14 | package: package(), 15 | deps: deps(), 16 | docs: docs(), 17 | preferred_cli_env: [ 18 | dialyzer: :test 19 | ], 20 | dialyzer: [ 21 | flags: [:underspecs, :unknown, :unmatched_returns], 22 | plt_add_apps: [:mix, :jason, :iex, :logger], 23 | plt_add_deps: :app_tree 24 | ] 25 | ] 26 | end 27 | 28 | def application do 29 | [ 30 | env: [], 31 | extra_applications: [:logger], 32 | mod: {Ethereumex.Application, []} 33 | ] 34 | end 35 | 36 | defp package do 37 | [ 38 | description: "Elixir JSON-RPC client for the Ethereum blockchain", 39 | maintainers: ["Ayrat Badykov"], 40 | licenses: ["MIT"], 41 | links: %{"GitHub" => "https://github.com/exthereum/ethereumex"} 42 | ] 43 | end 44 | 45 | defp deps do 46 | [ 47 | {:finch, "~> 0.16"}, 48 | {:jason, "~> 1.4", optional: true}, 49 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 50 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 51 | {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, 52 | {:mimic, "~> 1.10", only: :test}, 53 | {:poolboy, "~> 1.5"}, 54 | {:telemetry, "~> 0.4 or ~> 1.0"}, 55 | {:websockex, "~> 0.4.3"}, 56 | {:with_env, "~> 0.1", only: :test} 57 | ] 58 | end 59 | 60 | defp docs do 61 | [ 62 | extras: [ 63 | "CHANGELOG.md": [title: "Changelog"], 64 | "LICENSE.md": [title: "License"], 65 | "README.md": [title: "Overview"] 66 | ], 67 | main: "readme", 68 | source_url: @source_url, 69 | formatters: ["html"] 70 | ] 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, 8 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 9 | "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, 10 | "ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"}, 11 | "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, 12 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 13 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 16 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 17 | "mimic": {:hex, :mimic, "1.11.0", "49b126687520b6e179acab305068ad7d72bfea8abe94908a6c0c8ca0a5b7bdc7", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "8b16b1809ca947cffbaede146cd42da8c1c326af67a84b59b01c204d54e4f1a2"}, 18 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 19 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 21 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 22 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 23 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 24 | "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, 25 | "with_env": {:hex, :with_env, "0.1.0", "4c4d4bdf54733917945fa0c521b7a7bbad4f4c65ce75b346a566695898a4f59a", [:mix], [], "hexpm", "2345cf474a963184edb23f626b4f498472c7d9fe5aa2c71473981dc054bdef95"}, 26 | } 27 | -------------------------------------------------------------------------------- /test/ethereumex/client/base_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.Client.BaseClientTest do 2 | use ExUnit.Case 3 | 4 | defmodule ClientMock do 5 | use Ethereumex.Client.BaseClient 6 | 7 | def post_request(payload, opts) do 8 | case Ethereumex.Config.json_module().decode!(payload) do 9 | %{"method" => method, "jsonrpc" => "2.0", "params" => params} -> 10 | {method, params, opts} 11 | 12 | batch_requests when is_list(batch_requests) -> 13 | batch_requests 14 | end 15 | end 16 | end 17 | 18 | defmodule Helpers do 19 | def check(method, params \\ [], defaults \\ []) do 20 | method 21 | |> make_tuple 22 | |> assert_method(params, params ++ defaults) 23 | end 24 | 25 | def assert_method({ex_method, eth_method}, params, payload) do 26 | {result_eth_method, result_payload, _opts} = 27 | apply(ClientMock, String.to_atom(ex_method), params) 28 | 29 | assert result_eth_method == eth_method 30 | assert result_payload == payload 31 | end 32 | 33 | def assert_opts({ex_method, _eth_method}, params, opts) do 34 | {_eth_method, _payload, result_opts} = apply(ClientMock, String.to_atom(ex_method), params) 35 | 36 | assert result_opts == opts 37 | end 38 | 39 | def make_tuple(ex_method) do 40 | eth_method = 41 | ex_method 42 | |> String.split("_") 43 | |> uppercase 44 | 45 | {ex_method, eth_method} 46 | end 47 | 48 | def uppercase([first]), do: first 49 | def uppercase([first, second]), do: Enum.join([first, second], "_") 50 | # web3_client_version -> web3_clientVersion and keep this logic 51 | def uppercase([first, second | tail]) do 52 | uppered = Enum.map_join(tail, &String.capitalize/1) 53 | 54 | Enum.join([first, second], "_") <> uppered 55 | end 56 | end 57 | 58 | @address "0x407d73d8a49eeb85d32cf465507dd71d507100c1" 59 | @hash "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 60 | @hex_232 "0xe8" 61 | @transaction %{ 62 | "from" => @address, 63 | "to" => @address, 64 | "gas" => @hex_232, 65 | "gasPrice" => @hex_232, 66 | "value" => @hex_232, 67 | "data" => @hash 68 | } 69 | @source_code "(returnlll (suicide (caller)))" 70 | @filter %{ 71 | "fromBlock" => "0x1", 72 | "toBlock" => "0x2", 73 | "address" => "0x8888f1f195afa192cfee860698584c030f4c9db1", 74 | "topics" => ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b"] 75 | } 76 | 77 | describe ".web3_client_version/0" do 78 | test "with configuration url", do: Helpers.check("web3_client_version") 79 | 80 | test "with dynamic url", 81 | do: 82 | Helpers.assert_opts( 83 | {"web3_client_version", "web3_client_version"}, 84 | [[url: "http://localhost:4000"]], 85 | url: "http://localhost:4000" 86 | ) 87 | end 88 | 89 | test ".net_version/0", do: Helpers.check("net_version") 90 | test ".net_peer_count/0", do: Helpers.check("net_peer_count") 91 | test ".net_listening/0", do: Helpers.check("net_listening") 92 | test ".eth_protocol_version/0", do: Helpers.check("eth_protocol_version") 93 | test ".eth_syncing/0", do: Helpers.check("eth_syncing") 94 | test ".eth_coinbase/0", do: Helpers.check("eth_coinbase") 95 | test ".eth_chain_id/0", do: Helpers.check("eth_chain_id") 96 | test ".eth_mining/0", do: Helpers.check("eth_mining") 97 | test ".eth_hashrate/0", do: Helpers.check("eth_hashrate") 98 | test ".eth_gas_price/0", do: Helpers.check("eth_gas_price") 99 | test ".eth_max_priority_fee_per_gas/0", do: Helpers.check("eth_max_priority_fee_per_gas") 100 | test ".eth_fee_history/3", do: Helpers.check("eth_fee_history", [1, "latest", [25, 75]]) 101 | test ".eth_blob_base_fee/0", do: Helpers.check("eth_blob_base_fee") 102 | test ".eth_accounts/0", do: Helpers.check("eth_accounts") 103 | test ".eth_block_number/0", do: Helpers.check("eth_block_number") 104 | test ".eth_get_compilers/0", do: Helpers.check("eth_get_compilers") 105 | test ".eth_new_block_filter/0", do: Helpers.check("eth_new_block_filter") 106 | 107 | test ".eth_new_pending_transaction_filter/0", 108 | do: Helpers.check("eth_new_pending_transaction_filter") 109 | 110 | describe ".eth_get_proof/3" do 111 | test "w/o params", 112 | do: Helpers.check("eth_get_proof", [@address, [@hex_232, @hex_232]], ["latest"]) 113 | 114 | test "with number", 115 | do: Helpers.check("eth_get_proof", [@address, [@hex_232, @hex_232], [@hex_232]]) 116 | end 117 | 118 | test ".eth_get_work/0", do: Helpers.check("eth_get_work") 119 | test ".shh_version/0", do: Helpers.check("shh_version") 120 | test ".shh_new_identity/0", do: Helpers.check("shh_new_identity") 121 | test ".shh_new_group/0", do: Helpers.check("shh_new_group") 122 | 123 | describe "eth_get_block_transaction_count_by_number/0" do 124 | test "w/o params", 125 | do: Helpers.check("eth_get_block_transaction_count_by_number", [], ["latest"]) 126 | 127 | test "with number", do: Helpers.check("eth_get_block_transaction_count_by_number", [@hex_232]) 128 | end 129 | 130 | describe "eth_get_uncle_count_by_block_number/0" do 131 | test "w/o params", 132 | do: Helpers.check("eth_get_uncle_count_by_block_number", [], ["latest"]) 133 | 134 | test "with number", do: Helpers.check("eth_get_uncle_count_by_block_number", [@hex_232]) 135 | end 136 | 137 | test ".web3_sha3/1", do: Helpers.check("web3_sha3", ["string to be hashed"]) 138 | 139 | test ".eth_get_balance/1", 140 | do: Helpers.check("eth_get_balance", [@address], ["latest"]) 141 | 142 | test ".eth_get_storage_at/1", 143 | do: Helpers.check("eth_get_storage_at", [@address, "0x0"], ["latest"]) 144 | 145 | test ".eth_get_transaction_count/1", 146 | do: Helpers.check("eth_get_transaction_count", [@address], ["latest"]) 147 | 148 | test ".eth_get_block_transaction_count_by_hash/1", 149 | do: Helpers.check("eth_get_block_transaction_count_by_hash", [@hash]) 150 | 151 | test ".eth_get_uncle_count_by_block_hash/1", 152 | do: Helpers.check("eth_get_uncle_count_by_block_hash", [@hash]) 153 | 154 | test ".eth_get_code/1", 155 | do: Helpers.check("eth_get_code", [@address], ["latest"]) 156 | 157 | test ".eth_sign/2", 158 | do: Helpers.check("eth_sign", [@address, "data to sign"]) 159 | 160 | test ".eth_send_transaction/1", 161 | do: Helpers.check("eth_send_transaction", [@transaction]) 162 | 163 | test ".eth_send_raw_transaction/1", 164 | do: Helpers.check("eth_send_raw_transaction", [@hash]) 165 | 166 | test ".eth_call/1", 167 | do: Helpers.check("eth_call", [@transaction], ["latest"]) 168 | 169 | test ".eth_estimate_gas/1", 170 | do: Helpers.check("eth_estimate_gas", [@transaction]) 171 | 172 | test ".eth_get_block_by_hash/2", 173 | do: Helpers.check("eth_get_block_by_hash", [@hash, false]) 174 | 175 | test ".eth_get_block_by_number/2", 176 | do: Helpers.check("eth_get_block_by_number", [@hex_232, false]) 177 | 178 | test ".eth_get_transaction_by_hash/1", 179 | do: Helpers.check("eth_get_transaction_by_hash", [@hash]) 180 | 181 | test ".eth_get_transaction_by_block_hash_and_index/2", 182 | do: Helpers.check("eth_get_transaction_by_block_hash_and_index", [@hash, "0x0"]) 183 | 184 | test ".eth_get_transaction_by_block_number_and_index/2", 185 | do: Helpers.check("eth_get_transaction_by_block_number_and_index", ["0x29c", "0x0"]) 186 | 187 | test ".eth_get_transaction_receipt/1", 188 | do: Helpers.check("eth_get_transaction_receipt", [@hash]) 189 | 190 | test ".eth_get_uncle_by_block_hash_and_index/2", 191 | do: Helpers.check("eth_get_uncle_by_block_hash_and_index", [@hash, "0x0"]) 192 | 193 | test ".eth_get_uncle_by_block_number_and_index/2", 194 | do: Helpers.check("eth_get_uncle_by_block_number_and_index", ["0x29c", "0x0"]) 195 | 196 | test "eth_compile_lll/1" do 197 | Helpers.assert_method({"eth_compile_lll", "eth_compileLLL"}, [@source_code], [@source_code]) 198 | end 199 | 200 | test ".eth_compile_solidity/1", 201 | do: Helpers.check("eth_compile_solidity", [@source_code]) 202 | 203 | test ".eth_compile_serpent/1", 204 | do: Helpers.check("eth_compile_serpent", [@source_code]) 205 | 206 | test ".eth_new_filter/1", 207 | do: Helpers.check("eth_new_filter", [@filter]) 208 | 209 | test ".eth_uninstall_filter/1", 210 | do: Helpers.check("eth_uninstall_filter", ["0xb"]) 211 | 212 | test ".eth_get_filter_changes/1", 213 | do: Helpers.check("eth_get_filter_changes", ["0xb"]) 214 | 215 | test ".eth_get_filter_logs/1", 216 | do: Helpers.check("eth_get_filter_logs", ["0xb"]) 217 | 218 | test ".eth_get_logs/1", 219 | do: Helpers.check("eth_get_logs", [@filter]) 220 | 221 | test ".eth_submit_work/3" do 222 | params = [ 223 | "0x0000000000000001", 224 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 225 | "0xD1FE5700000000000000000000000000D1FE5700000000000000000000000000" 226 | ] 227 | 228 | Helpers.check("eth_submit_work", params) 229 | end 230 | 231 | test ".eth_submit_hashrate/2" do 232 | params = [ 233 | "0x0000000000000000000000000000000000000000000000000000000000500000", 234 | "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c" 235 | ] 236 | 237 | Helpers.check("eth_submit_hashrate", params) 238 | end 239 | 240 | test ".db_put_string/3" do 241 | params = [ 242 | "testDB", 243 | "myKey", 244 | "myString" 245 | ] 246 | 247 | Helpers.check("db_put_string", params) 248 | end 249 | 250 | test ".db_get_string/2" do 251 | params = [ 252 | "testDB", 253 | "myKey" 254 | ] 255 | 256 | Helpers.check("db_get_string", params) 257 | end 258 | 259 | test ".db_put_hex/3" do 260 | params = [ 261 | "testDB", 262 | "myKey", 263 | "0x68656c6c6f20776f726c64" 264 | ] 265 | 266 | Helpers.check("db_put_hex", params) 267 | end 268 | 269 | test ".db_get_hex/2" do 270 | params = [ 271 | "testDB", 272 | "myKey" 273 | ] 274 | 275 | Helpers.check("db_get_hex", params) 276 | end 277 | 278 | test ".shh_post/1" do 279 | params = %{ 280 | "from" => 281 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1", 282 | "to" => 283 | "0x3e245533f97284d442460f2998cd41858798ddf04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a0d4d661997d3940272b717b1", 284 | "topics" => [ 285 | "0x776869737065722d636861742d636c69656e74", 286 | "0x4d5a695276454c39425154466b61693532" 287 | ], 288 | "payload" => "0x7b2274797065223a226d6", 289 | "priority" => "0x64", 290 | "ttl" => "0x64" 291 | } 292 | 293 | Helpers.check("shh_post", [params]) 294 | end 295 | 296 | test ".shh_has_identity/1", 297 | do: Helpers.check("shh_has_identity", [@address]) 298 | 299 | test ".shh_add_to_group/1", 300 | do: Helpers.check("shh_add_to_group", [@address]) 301 | 302 | test ".shh_new_filter/2" do 303 | params = %{ 304 | "topics" => [~c"0x12341234bf4b564f"], 305 | "to" => 306 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 307 | } 308 | 309 | Helpers.check("shh_new_filter", [params]) 310 | end 311 | 312 | test ".shh_uninstall_filter/1", 313 | do: Helpers.check("shh_uninstall_filter", ["0x7"]) 314 | 315 | test ".shh_get_filter_changes/1", 316 | do: Helpers.check("shh_get_filter_changes", ["0x7"]) 317 | 318 | test ".shh_get_messages/1", 319 | do: Helpers.check("shh_get_messages", ["0x7"]) 320 | 321 | describe ".batch_request/1" do 322 | test "increases rpc_counter by request count" do 323 | _ = Ethereumex.Counter.increment(:rpc_counter, 42, "stub_method") 324 | initial_count = Ethereumex.Counter.get(:rpc_counter) 325 | 326 | requests = [ 327 | {:web3_client_version, []}, 328 | {:net_version, []}, 329 | {:web3_sha3, ["0x68656c6c6f20776f726c64"]} 330 | ] 331 | 332 | assert _ = ClientMock.batch_request(requests) 333 | 334 | assert Ethereumex.Counter.get(:rpc_counter) == initial_count + length(requests) 335 | end 336 | end 337 | 338 | describe ".format_batch/1" do 339 | test "formats batch response" do 340 | batch = [ 341 | %{ 342 | "error" => %{"code" => -32_000, "message" => "execution reverted"}, 343 | "id" => 86, 344 | "jsonrpc" => "2.0" 345 | }, 346 | %{ 347 | "result" => 42, 348 | "id" => 87, 349 | "jsonrpc" => "2.0" 350 | }, 351 | %{ 352 | "result" => 50, 353 | "id" => 85, 354 | "jsonrpc" => "2.0" 355 | } 356 | ] 357 | 358 | assert [ 359 | {:ok, 50}, 360 | {:error, %{"code" => -32_000, "message" => "execution reverted"}}, 361 | {:ok, 42} 362 | ] = ClientMock.format_batch(batch) 363 | end 364 | end 365 | end 366 | -------------------------------------------------------------------------------- /test/ethereumex/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.ConfigTest do 2 | use ExUnit.Case 3 | use WithEnv 4 | 5 | describe ".setup_children" do 6 | test ":ipc returns a list with 1 worker pool" do 7 | with_env put: [ 8 | ethereumex: [ 9 | ipc_worker_size: 3, 10 | ipc_max_worker_overflow: 4, 11 | ipc_request_timeout: 5, 12 | ipc_path: "/tmp/socket.ipc" 13 | ] 14 | ] do 15 | specs = Ethereumex.Config.setup_children(:ipc) 16 | 17 | assert Enum.count(specs) == 1 18 | 19 | assert Enum.at(specs, 0) == 20 | :poolboy.child_spec( 21 | :worker, 22 | [ 23 | {:name, {:local, :ipc_worker}}, 24 | {:worker_module, Ethereumex.IpcServer}, 25 | {:size, 3}, 26 | {:max_overflow, 4} 27 | ], 28 | path: "/tmp/socket.ipc", 29 | ipc_request_timeout: 5 30 | ) 31 | end 32 | end 33 | 34 | test ":http has Finch chiild" do 35 | assert Ethereumex.Config.setup_children(:http) == [ 36 | {Finch, [name: Ethereumex.Finch, pools: %{}]} 37 | ] 38 | end 39 | 40 | test "unsupported client types raise an error" do 41 | assert_raise( 42 | RuntimeError, 43 | "Invalid :client option (not_supported) in config", 44 | fn -> 45 | Ethereumex.Config.setup_children(:not_supported) 46 | end 47 | ) 48 | end 49 | 50 | test "defaults to the configured client_type" do 51 | with_env put: [ethereumex: [client_type: :http]] do 52 | assert Ethereumex.Config.setup_children() == [ 53 | {Finch, [name: Ethereumex.Finch, pools: %{}]} 54 | ] 55 | end 56 | end 57 | end 58 | 59 | describe ".rpc_url" do 60 | test "returns the application configured value" do 61 | with_env put: [ethereumex: [url: "http://foo.com"]] do 62 | assert Ethereumex.Config.rpc_url() == "http://foo.com" 63 | end 64 | end 65 | 66 | test "raises an error when not present" do 67 | with_env put: [ethereumex: [url: ""]] do 68 | assert_raise( 69 | ArgumentError, 70 | "Please set config variable `config :ethereumex, :url, \"http://...\", got: `\"\"`", 71 | fn -> Ethereumex.Config.rpc_url() end 72 | ) 73 | end 74 | end 75 | end 76 | 77 | describe ".ipc_path" do 78 | test "returns the application configured value" do 79 | with_env put: [ethereumex: [ipc_path: "/tmp/socket.ipc"]] do 80 | assert Ethereumex.Config.ipc_path() == "/tmp/socket.ipc" 81 | end 82 | end 83 | 84 | test "raises an error when not present" do 85 | with_env put: [ethereumex: [ipc_path: ""]] do 86 | assert_raise( 87 | ArgumentError, 88 | "Please set config variable `config :ethereumex, :ipc_path, \"path/to/ipc\", got ``. Note: System.user_home! will be prepended to path for you on initialization", 89 | fn -> Ethereumex.Config.ipc_path() end 90 | ) 91 | end 92 | end 93 | end 94 | 95 | describe ".http_options" do 96 | test "returns the application configured value" do 97 | with_env put: [ethereumex: [http_options: [timeout: 8000]]] do 98 | assert Ethereumex.Config.http_options() == [timeout: 8000] 99 | end 100 | end 101 | 102 | test "returns an empty list by default" do 103 | with_env delete: [ethereumex: [:http_options]] do 104 | assert Ethereumex.Config.http_options() == [] 105 | end 106 | end 107 | end 108 | 109 | describe ".client_type" do 110 | test "returns the application configured value" do 111 | with_env put: [ethereumex: [client_type: :ipc]] do 112 | assert Ethereumex.Config.client_type() == :ipc 113 | end 114 | end 115 | 116 | test "returns http by default" do 117 | with_env delete: [ethereumex: [:client_type]] do 118 | assert Ethereumex.Config.client_type() == :http 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/ethereumex/counter_reset_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.ResetLockTest do 2 | use ExUnit.Case 3 | alias Ethereumex.Counter 4 | 5 | setup_all do 6 | Application.put_env(:ethereumex, :id_reset, true) 7 | 8 | on_exit(fn -> 9 | Application.put_env(:ethereumex, :id_reset, false) 10 | end) 11 | end 12 | 13 | test "incrementing twice returns correct locked binary" do 14 | 1 = Counter.increment(:test_11, "method_11") 15 | 1 = Counter.increment(:test_11, "method_11") 16 | end 17 | 18 | test "incrementing twice and updating with a count returns correct locked binary" do 19 | 1 = Counter.increment(:test_22, "method_22") 20 | 2 = Counter.increment(:test_22, 2, "method_22") 21 | end 22 | 23 | test "incrementing twice, updating with a count and incrementing again returns correct locked binary" do 24 | 1 = Counter.increment(:test_33, "method_33") 25 | 2 = Counter.increment(:test_33, 2, "method_33") 26 | 1 = Counter.increment(:test_33, "method_33") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/ethereumex/counter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.CounterTest do 2 | use ExUnit.Case 3 | alias Ethereumex.Counter 4 | 5 | setup do 6 | handler_id = {:ethereumex_handler, :rand.uniform(100)} 7 | 8 | on_exit(fn -> 9 | :telemetry.detach(handler_id) 10 | end) 11 | 12 | {:ok, handler_id: handler_id} 13 | end 14 | 15 | test "incrementing twice returns correct number" do 16 | 1 = Counter.increment(:test_1, "method_1") 17 | 2 = Counter.increment(:test_1, "method_1") 18 | end 19 | 20 | test "incrementing twice and updating with a count returns correct number" do 21 | 1 = Counter.increment(:test_2, "method_2") 22 | 2 = Counter.increment(:test_2, "method_2") 23 | 4 = Counter.increment(:test_2, 2, "method_2") 24 | end 25 | 26 | test "incrementing twice, updating with a count and incrementing again returns correct number" do 27 | 1 = Counter.increment(:test_3, "method_3") 28 | 2 = Counter.increment(:test_3, "method_3") 29 | 4 = Counter.increment(:test_3, 2, "method_3") 30 | 5 = Counter.increment(:test_3, "method_3") 31 | end 32 | 33 | test "telemetry handler attached with method name gets called with increment/2", %{ 34 | handler_id: handler_id 35 | } do 36 | method_4 = "method_4" 37 | attach(handler_id, [:ethereumex, String.to_atom(method_4)]) 38 | :ok = Application.put_env(:ethereumex, :adapter, __MODULE__.Adapter) 39 | 1 = Counter.increment(:test_4, method_4) 40 | assert_received({:event, [:ethereumex, :method_4], %{counter: 1}, %{}}) 41 | end 42 | 43 | test "telemetry handler attached without method name gets called with increment/2", %{ 44 | handler_id: handler_id 45 | } do 46 | eth_batch = "eth_batch" 47 | attach(handler_id, [:ethereumex]) 48 | 2 = Counter.increment(:test_5, 2, eth_batch) 49 | assert_received({:event, [:ethereumex], %{counter: 1}, %{method_name: "eth_batch"}}) 50 | end 51 | 52 | defp attach(handler_id, event) do 53 | :telemetry.attach( 54 | handler_id, 55 | event, 56 | fn event, measurements, metadata, _ -> 57 | send(self(), {:event, event, measurements, metadata}) 58 | end, 59 | nil 60 | ) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/ethereumex/http_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.HttpClientTest do 2 | use ExUnit.Case 3 | alias Ethereumex.HttpClient 4 | 5 | @tag :web3 6 | describe "HttpClient.web3_client_version/0" do 7 | test "returns client version" do 8 | result = HttpClient.web3_client_version() 9 | 10 | {:ok, <<_::binary>>} = result 11 | end 12 | end 13 | 14 | @tag :web3 15 | describe "HttpClient.web3_sha3/1" do 16 | test "returns sha3 of the given data" do 17 | result = HttpClient.web3_sha3("0x68656c6c6f20776f726c64") 18 | 19 | { 20 | :ok, 21 | "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" 22 | } = result 23 | end 24 | end 25 | 26 | @tag :net 27 | describe "HttpClient.net_version/0" do 28 | test "returns network id" do 29 | result = HttpClient.net_version() 30 | 31 | {:ok, <<_::binary>>} = result 32 | end 33 | end 34 | 35 | @tag :net 36 | describe "HttpClient.net_peer_count/0" do 37 | test "returns number of peers currently connected to the client" do 38 | result = HttpClient.net_peer_count() 39 | 40 | {:ok, <<_::binary>>} = result 41 | end 42 | end 43 | 44 | @tag :net 45 | describe "HttpClient.net_listening/0" do 46 | test "returns true" do 47 | result = HttpClient.net_listening() 48 | 49 | {:ok, true} = result 50 | end 51 | end 52 | 53 | @tag :eth 54 | describe "HttpClient.eth_protocol_version/0" do 55 | test "returns true" do 56 | result = HttpClient.eth_protocol_version() 57 | 58 | {:ok, <<_::binary>>} = result 59 | end 60 | end 61 | 62 | @tag :eth 63 | describe "HttpClient.eth_syncing/1" do 64 | test "checks sync status" do 65 | {:ok, result} = HttpClient.eth_syncing() 66 | 67 | assert is_map(result) || is_boolean(result) 68 | end 69 | end 70 | 71 | @tag :eth_chain_id 72 | describe "HttpClient.eth_chain_id/1" do 73 | test "returns chain id of the RPC serveer" do 74 | result = HttpClient.eth_chain_id() 75 | 76 | {:ok, <<_::binary>>} = result 77 | end 78 | end 79 | 80 | @tag :eth 81 | describe "HttpClient.eth_coinbase/1" do 82 | test "returns coinbase address" do 83 | result = HttpClient.eth_coinbase() 84 | 85 | {:ok, <<_::binary>>} = result 86 | end 87 | end 88 | 89 | @tag :eth 90 | describe "HttpClient.eth_mining/1" do 91 | test "checks mining status" do 92 | result = HttpClient.eth_mining() 93 | 94 | {:ok, false} = result 95 | end 96 | end 97 | 98 | @tag :eth 99 | describe "HttpClient.eth_hashrate/1" do 100 | test "returns hashrate" do 101 | result = HttpClient.eth_hashrate() 102 | 103 | {:ok, <<_::binary>>} = result 104 | end 105 | end 106 | 107 | @tag :eth 108 | describe "HttpClient.eth_gas_price/1" do 109 | test "returns current price per gas" do 110 | result = HttpClient.eth_gas_price() 111 | 112 | {:ok, <<_::binary>>} = result 113 | end 114 | end 115 | 116 | @tag :skip 117 | describe "HttpClient.eth_max_priority_fee_per_gas/1" do 118 | test "returns current max priority fee per gas" do 119 | result = HttpClient.eth_max_priority_fee_per_gas() 120 | 121 | {:ok, <<_::binary>>} = result 122 | end 123 | end 124 | 125 | @tag :skip 126 | describe "HttpClient.eth_fee_history/3" do 127 | test "returns a collection of historical gas information" do 128 | result = HttpClient.eth_fee_history(1, "latest", [25, 75]) 129 | 130 | assert is_map(result) 131 | end 132 | end 133 | 134 | @tag :skip 135 | describe "HttpClient.eth_blob_base_fee/1" do 136 | test "returns the blob base fee of the latest block" do 137 | result = HttpClient.eth_blob_base_fee() 138 | 139 | {:ok, <<_::binary>>} = result 140 | end 141 | end 142 | 143 | @tag :eth 144 | describe "HttpClient.eth_accounts/1" do 145 | test "returns addresses owned by client" do 146 | {:ok, result} = HttpClient.eth_accounts() 147 | 148 | assert result |> is_list 149 | end 150 | end 151 | 152 | @tag :eth 153 | describe "HttpClient.eth_block_number/1" do 154 | test "returns number of most recent block" do 155 | result = HttpClient.eth_block_number() 156 | 157 | {:ok, <<_::binary>>} = result 158 | end 159 | end 160 | 161 | @tag :eth 162 | describe "HttpClient.eth_get_balance/3" do 163 | test "returns balance of given account" do 164 | result = HttpClient.eth_get_balance("0x001bdcde60cb916377a3a3bf4e8054051a4d02e7") 165 | 166 | {:ok, <<_::binary>>} = result 167 | end 168 | end 169 | 170 | @tag :skip 171 | describe "HttpClient.eth_get_storage_at/4" do 172 | test "returns value from a storage position at a given address." do 173 | result = 174 | HttpClient.eth_get_balance( 175 | "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7", 176 | "0x0" 177 | ) 178 | 179 | {:ok, <<_::binary>>} = result 180 | end 181 | end 182 | 183 | @tag :eth 184 | describe "HttpClient.eth_get_transaction_count/3" do 185 | test "returns number of transactions sent from an address." do 186 | result = HttpClient.eth_get_transaction_count("0x001bdcde60cb916377a3a3bf4e8054051a4d02e7") 187 | 188 | {:ok, <<_::binary>>} = result 189 | end 190 | end 191 | 192 | @tag :eth 193 | describe "HttpClient.eth_get_block_transaction_count_by_hash/2" do 194 | test "number of transactions in a block from a block matching the given block hash" do 195 | result = 196 | HttpClient.eth_get_block_transaction_count_by_hash( 197 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 198 | ) 199 | 200 | {:ok, nil} = result 201 | end 202 | end 203 | 204 | @tag :eth 205 | describe "HttpClient.eth_get_block_transaction_count_by_number/2" do 206 | test "returns number of transactions in a block from a block matching the given block number" do 207 | result = HttpClient.eth_get_block_transaction_count_by_number() 208 | 209 | {:ok, <<_::binary>>} = result 210 | end 211 | end 212 | 213 | @tag :eth 214 | describe "HttpClient.eth_get_uncle_count_by_block_hash/2" do 215 | test "the number of uncles in a block from a block matching the given block hash" do 216 | result = 217 | HttpClient.eth_get_uncle_count_by_block_hash( 218 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 219 | ) 220 | 221 | {:ok, nil} = result 222 | end 223 | end 224 | 225 | @tag :eth 226 | describe "HttpClient.eth_get_uncle_count_by_block_number/2" do 227 | test "the number of uncles in a block from a block matching the given block hash" do 228 | result = HttpClient.eth_get_uncle_count_by_block_number() 229 | 230 | {:ok, <<_::binary>>} = result 231 | end 232 | end 233 | 234 | @tag :eth 235 | describe "HttpClient.eth_get_code/3" do 236 | test "returns code at a given address" do 237 | result = HttpClient.eth_get_code("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b") 238 | 239 | {:ok, <<_::binary>>} = result 240 | end 241 | end 242 | 243 | @tag :eth_sign 244 | describe "HttpClient.eth_sign/3" do 245 | test "returns signature" do 246 | result = HttpClient.eth_sign("0x71cf0b576a95c347078ec2339303d13024a26910", "0xdeadbeaf") 247 | 248 | {:ok, <<_::binary>>} = result 249 | end 250 | end 251 | 252 | @tag :eth 253 | describe "HttpClient.eth_estimate_gas/3" do 254 | test "estimates gas" do 255 | data = 256 | "0x6060604052341561" <> 257 | "000f57600080fd5b60b38061001d6000396000f3006060604052" <> 258 | "63ffffffff7c0100000000000000000000000000000000000000" <> 259 | "00000000000000000060003504166360fe47b181146045578063" <> 260 | "6d4ce63c14605a57600080fd5b3415604f57600080fd5b605860" <> 261 | "0435607c565b005b3415606457600080fd5b606a6081565b6040" <> 262 | "5190815260200160405180910390f35b600055565b6000549056" <> 263 | "00a165627a7a7230582033edcee10845eead909dccb4e95bb7e4" <> 264 | "062e92234bf3cfaf804edbd265e599800029" 265 | 266 | from = "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7" 267 | transaction = %{data: data, from: from} 268 | 269 | result = HttpClient.eth_estimate_gas(transaction) 270 | 271 | {:ok, <<_::binary>>} = result 272 | end 273 | end 274 | 275 | @tag :eth 276 | describe "HttpClient.eth_get_block_by_hash/3" do 277 | test "returns information about a block by hash" do 278 | result = 279 | HttpClient.eth_get_block_by_hash( 280 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 281 | true 282 | ) 283 | 284 | {:ok, nil} = result 285 | end 286 | end 287 | 288 | @tag :eth 289 | describe "HttpClient.eth_get_block_by_number/3" do 290 | test "returns information about a block by number" do 291 | {:ok, result} = HttpClient.eth_get_block_by_number("0x1b4", true) 292 | 293 | assert is_nil(result) || is_map(result) 294 | end 295 | end 296 | 297 | @tag :eth 298 | describe "HttpClient.eth_get_transaction_by_hash/2" do 299 | test "returns the information about a transaction by its hash" do 300 | result = 301 | HttpClient.eth_get_transaction_by_hash( 302 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 303 | ) 304 | 305 | {:ok, nil} = result 306 | end 307 | end 308 | 309 | @tag :eth 310 | describe "HttpClient.eth_get_transaction_by_block_hash_and_index/3" do 311 | test "returns the information about a transaction by block hash and index" do 312 | result = 313 | HttpClient.eth_get_transaction_by_block_hash_and_index( 314 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 315 | "0x0" 316 | ) 317 | 318 | {:ok, nil} = result 319 | end 320 | end 321 | 322 | @tag :eth 323 | describe "HttpClient.eth_get_transaction_by_block_number_and_index/3" do 324 | test "returns the information about a transaction by block number and index" do 325 | result = HttpClient.eth_get_transaction_by_block_number_and_index("earliest", "0x0") 326 | 327 | {:ok, nil} = result 328 | end 329 | end 330 | 331 | @tag :eth 332 | describe "HttpClient.eth_get_transaction_receipt/2" do 333 | test "returns the receipt of a transaction by transaction hash" do 334 | result = 335 | HttpClient.eth_get_transaction_receipt( 336 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 337 | ) 338 | 339 | {:ok, nil} = result 340 | end 341 | end 342 | 343 | @tag :eth 344 | describe "HttpClient.eth_get_uncle_by_block_hash_and_index/3" do 345 | test "returns information about a uncle of a block by hash and uncle index position" do 346 | result = 347 | HttpClient.eth_get_uncle_by_block_hash_and_index( 348 | "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", 349 | "0x0" 350 | ) 351 | 352 | {:ok, nil} = result 353 | end 354 | end 355 | 356 | @tag :eth 357 | describe "HttpClient.eth_get_uncle_by_block_number_and_index/3" do 358 | test "returns information about a uncle of a block by number and uncle index position" do 359 | result = HttpClient.eth_get_uncle_by_block_number_and_index("0x29c", "0x0") 360 | 361 | {:ok, _} = result 362 | end 363 | end 364 | 365 | @tag :eth_compile 366 | describe "HttpClient.eth_get_compilers/1" do 367 | test "returns a list of available compilers in the client" do 368 | result = HttpClient.eth_get_compilers() 369 | 370 | {:ok, _} = result 371 | end 372 | end 373 | 374 | @tag :eth 375 | describe "HttpClient.eth_new_filter/2" do 376 | test "creates a filter object" do 377 | filter = %{ 378 | fromBlock: "0x1", 379 | toBlock: "0x2", 380 | address: "0x8888f1f195afa192cfee860698584c030f4c9db1", 381 | topics: [ 382 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 383 | nil, 384 | [ 385 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 386 | "0x0000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebccc" 387 | ] 388 | ] 389 | } 390 | 391 | result = HttpClient.eth_new_filter(filter) 392 | 393 | {:ok, <<_::binary>>} = result 394 | end 395 | end 396 | 397 | @tag :eth 398 | describe "HttpClient.eth_new_block_filter/1" do 399 | test "creates new block filter" do 400 | result = HttpClient.eth_new_block_filter() 401 | 402 | {:ok, <<_::binary>>} = result 403 | end 404 | end 405 | 406 | @tag :eth 407 | describe "HttpClient.eth_new_pending_transaction_filter/1" do 408 | test "creates new pending transaction filter" do 409 | result = HttpClient.eth_new_pending_transaction_filter() 410 | 411 | {:ok, <<_::binary>>} = result 412 | end 413 | end 414 | 415 | @tag :eth 416 | describe "HttpClient.eth_uninstall_filter/2" do 417 | test "uninstalls a filter with given id" do 418 | {:ok, result} = HttpClient.eth_uninstall_filter("0xb") 419 | 420 | assert is_boolean(result) 421 | end 422 | end 423 | 424 | @tag :eth 425 | describe "HttpClient.eth_get_filter_changes/2" do 426 | test "returns an array of logs which occurred since last poll" do 427 | result = HttpClient.eth_get_filter_changes("0x16") 428 | 429 | {:ok, []} = result 430 | end 431 | end 432 | 433 | @tag :eth 434 | describe "HttpClient.eth_get_filter_logs/2" do 435 | test "returns an array of all logs matching filter with given id" do 436 | result = HttpClient.eth_get_filter_logs("0x16") 437 | 438 | {:ok, []} = result 439 | end 440 | end 441 | 442 | @tag :eth 443 | describe "HttpClient.eth_get_logs/2" do 444 | test "returns an array of all logs matching a given filter object" do 445 | filter = %{ 446 | topics: ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b"] 447 | } 448 | 449 | result = HttpClient.eth_get_logs(filter) 450 | 451 | {:ok, []} = result 452 | end 453 | end 454 | 455 | @tag :eth_mine 456 | describe "HttpClient.eth_submit_work/4" do 457 | test "validates solution" do 458 | result = 459 | HttpClient.eth_submit_work( 460 | "0x0000000000000001", 461 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 462 | "0xD1FE5700000000000000000000000000D1FE5700000000000000000000000000" 463 | ) 464 | 465 | {:ok, false} = result 466 | end 467 | end 468 | 469 | @tag :eth_mine 470 | describe "HttpClient.eth_get_work/1" do 471 | test "returns the hash of the current block, the seedHash, and the boundary condition to be met " do 472 | result = HttpClient.eth_get_work() 473 | 474 | {:ok, [<<_::binary>>, <<_::binary>>, <<_::binary>>]} = result 475 | end 476 | end 477 | 478 | @tag :eth_mine 479 | describe "HttpClient.eth_submit_hashrate/3" do 480 | test "submits mining hashrate" do 481 | result = 482 | HttpClient.eth_submit_hashrate( 483 | "0x0000000000000000000000000000000000000000000000000000000000500000", 484 | "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c" 485 | ) 486 | 487 | {:ok, true} = result 488 | end 489 | end 490 | 491 | @tag :eth_db 492 | describe "HttpClient.db_put_string/4" do 493 | test "stores a string in the local database" do 494 | result = HttpClient.db_put_string("testDB", "myKey", "myString") 495 | 496 | {:ok, true} = result 497 | end 498 | end 499 | 500 | @tag :eth_db 501 | describe "HttpClient.db_get_string/3" do 502 | test "returns string from the local database" do 503 | result = HttpClient.db_get_string("db", "key") 504 | 505 | {:ok, nil} = result 506 | end 507 | end 508 | 509 | @tag :eth_db 510 | describe "HttpClient.db_put_hex/4" do 511 | test "stores binary data in the local database" do 512 | result = HttpClient.db_put_hex("db", "key", "data") 513 | 514 | {:ok, true} = result 515 | end 516 | end 517 | 518 | @tag :eth_db 519 | describe "HttpClient.db_get_hex/3" do 520 | test "returns binary data from the local database" do 521 | result = HttpClient.db_get_hex("db", "key") 522 | 523 | {:ok, nil} = result 524 | end 525 | end 526 | 527 | @tag :shh 528 | describe "HttpClient.shh_post/2" do 529 | test "sends a whisper message" do 530 | whisper = %{ 531 | from: 532 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1", 533 | to: 534 | "0x3e245533f97284d442460f2998cd41858798ddf04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a0d4d661997d3940272b717b1", 535 | topics: [ 536 | "0x776869737065722d636861742d636c69656e74", 537 | "0x4d5a695276454c39425154466b61693532" 538 | ], 539 | payload: "0x7b2274797065223a226d6", 540 | priority: "0x64", 541 | ttl: "0x64" 542 | } 543 | 544 | result = HttpClient.shh_post(whisper) 545 | 546 | {:ok, true} = result 547 | end 548 | end 549 | 550 | @tag :shh 551 | describe "HttpClient.shh_version/1" do 552 | test "returns the current whisper protocol version" do 553 | result = HttpClient.shh_version() 554 | 555 | {:ok, <<_::binary>>} = result 556 | end 557 | end 558 | 559 | @tag :shh 560 | describe "HttpClient.shh_new_identity/1" do 561 | test "creates new whisper identity in the client" do 562 | result = HttpClient.shh_new_identity() 563 | 564 | {:ok, <<_::binary>>} = result 565 | end 566 | end 567 | 568 | @tag :shh 569 | describe "HttpClient.shh_has_entity/2" do 570 | test "creates new whisper identity in the client" do 571 | result = 572 | HttpClient.shh_has_identity( 573 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 574 | ) 575 | 576 | {:ok, false} = result 577 | end 578 | end 579 | 580 | @tag :shh 581 | describe "HttpClient.shh_add_to_group/2" do 582 | test "adds adress to group" do 583 | result = 584 | HttpClient.shh_add_to_group( 585 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 586 | ) 587 | 588 | {:ok, false} = result 589 | end 590 | end 591 | 592 | @tag :shh 593 | describe "HttpClient.shh_new_group/1" do 594 | test "creates new group" do 595 | result = HttpClient.shh_new_group() 596 | 597 | {:ok, <<_::binary>>} = result 598 | end 599 | end 600 | 601 | @tag :shh 602 | describe "HttpClient.shh_new_filter/2" do 603 | test "creates filter to notify, when client receives whisper message matching the filter options" do 604 | filter_options = %{ 605 | topics: [~c"0x12341234bf4b564f"], 606 | to: 607 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 608 | } 609 | 610 | result = HttpClient.shh_new_filter(filter_options) 611 | 612 | {:ok, <<_::binary>>} = result 613 | end 614 | end 615 | 616 | @tag :shh 617 | describe "HttpClient.shh_uninstall_filter/2" do 618 | test "uninstalls a filter with given id" do 619 | result = HttpClient.shh_uninstall_filter("0x7") 620 | 621 | {:ok, false} = result 622 | end 623 | end 624 | 625 | @tag :shh 626 | describe "HttpClient.shh_get_filter_changes/2" do 627 | test "polls filter chages" do 628 | result = HttpClient.shh_get_filter_changes("0x7") 629 | 630 | {:ok, []} = result 631 | end 632 | end 633 | 634 | @tag :shh 635 | describe "HttpClient.shh_get_messages/2" do 636 | test "returns all messages matching a filter" do 637 | result = HttpClient.shh_get_messages("0x7") 638 | 639 | {:ok, []} = result 640 | end 641 | end 642 | 643 | @tag :batch 644 | describe "HttpClient.batch_request/1" do 645 | test "sends batch request" do 646 | requests = [ 647 | {:web3_client_version, []}, 648 | {:net_version, []}, 649 | {:web3_sha3, ["0x68656c6c6f20776f726c64"]} 650 | ] 651 | 652 | result = HttpClient.batch_request(requests) 653 | 654 | { 655 | :ok, 656 | [ 657 | {:ok, <<_::binary>>}, 658 | {:ok, <<_::binary>>}, 659 | {:ok, "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"} 660 | ] 661 | } = result 662 | end 663 | end 664 | end 665 | -------------------------------------------------------------------------------- /test/ethereumex/ipc_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.IpcClientTest do 2 | use ExUnit.Case 3 | alias Ethereumex.IpcClient 4 | alias Ethereumex.Config 5 | 6 | setup_all do 7 | _ = Application.put_env(:ethereumex, :client_type, :ipc) 8 | [ipc_pool_child] = Config.setup_children() 9 | {:ok, _pid} = Supervisor.start_child(Ethereumex.Supervisor, ipc_pool_child) 10 | 11 | on_exit(fn -> 12 | _ = Application.put_env(:ethereumex, :client_type, :http) 13 | end) 14 | end 15 | 16 | @tag :web3 17 | describe "IpcClient.web3_client_version/0" do 18 | test "returns client version" do 19 | result = IpcClient.web3_client_version() 20 | 21 | {:ok, <<_::binary>>} = result 22 | end 23 | end 24 | 25 | @tag :web3 26 | describe "IpcClient.web3_sha3/1" do 27 | test "returns sha3 of the given data" do 28 | result = IpcClient.web3_sha3("0x68656c6c6f20776f726c64") 29 | 30 | { 31 | :ok, 32 | "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" 33 | } = result 34 | end 35 | end 36 | 37 | @tag :net 38 | describe "IpcClient.net_version/0" do 39 | test "returns network id" do 40 | result = IpcClient.net_version() 41 | 42 | {:ok, <<_::binary>>} = result 43 | end 44 | end 45 | 46 | @tag :net 47 | describe "IpcClient.net_peer_count/0" do 48 | test "returns number of peers currently connected to the client" do 49 | result = IpcClient.net_peer_count() 50 | 51 | {:ok, <<_::binary>>} = result 52 | end 53 | end 54 | 55 | @tag :net 56 | describe "IpcClient.net_listening/0" do 57 | test "returns true" do 58 | result = IpcClient.net_listening() 59 | 60 | {:ok, true} = result 61 | end 62 | end 63 | 64 | @tag :eth 65 | describe "IpcClient.eth_protocol_version/0" do 66 | test "returns true" do 67 | result = IpcClient.eth_protocol_version() 68 | 69 | {:ok, <<_::binary>>} = result 70 | end 71 | end 72 | 73 | @tag :eth 74 | describe "IpcClient.eth_syncing/1" do 75 | test "checks sync status" do 76 | {:ok, result} = IpcClient.eth_syncing() 77 | 78 | assert is_map(result) || is_boolean(result) 79 | end 80 | end 81 | 82 | @tag :eth_chain_id 83 | describe "IpcClient.eth_chain_id/1" do 84 | test "returns chain id of the RPC serveer" do 85 | result = IpcClient.eth_chain_id() 86 | 87 | {:ok, <<_::binary>>} = result 88 | end 89 | end 90 | 91 | @tag :eth 92 | describe "IpcClient.eth_coinbase/1" do 93 | test "returns coinbase address" do 94 | result = IpcClient.eth_coinbase() 95 | 96 | {:ok, <<_::binary>>} = result 97 | end 98 | end 99 | 100 | @tag :eth 101 | describe "IpcClient.eth_mining/1" do 102 | test "checks mining status" do 103 | result = IpcClient.eth_mining() 104 | 105 | {:ok, false} = result 106 | end 107 | end 108 | 109 | @tag :eth 110 | describe "IpcClient.eth_hashrate/1" do 111 | test "returns hashrate" do 112 | result = IpcClient.eth_hashrate() 113 | 114 | {:ok, <<_::binary>>} = result 115 | end 116 | end 117 | 118 | @tag :eth 119 | describe "IpcClient.eth_gas_price/1" do 120 | test "returns current price per gas" do 121 | result = IpcClient.eth_gas_price() 122 | 123 | {:ok, <<_::binary>>} = result 124 | end 125 | end 126 | 127 | @tag :skip 128 | describe "IpcClient.eth_max_priority_fee_per_gas/1" do 129 | test "returns current max priority fee per gas" do 130 | result = IpcClient.eth_max_priority_fee_per_gas() 131 | 132 | {:ok, <<_::binary>>} = result 133 | end 134 | end 135 | 136 | @tag :skip 137 | describe "IpcClient.eth_fee_history/3" do 138 | test "returns a collection of historical gas information" do 139 | result = IpcClient.eth_fee_history(1, "latest", [25, 75]) 140 | 141 | assert is_map(result) 142 | end 143 | end 144 | 145 | @tag :skip 146 | describe "IpcClient.eth_blob_base_fee/1" do 147 | test "returns the base fee for blob transactions" do 148 | result = IpcClient.eth_blob_base_fee() 149 | 150 | {:ok, <<_::binary>>} = result 151 | end 152 | end 153 | 154 | @tag :eth 155 | describe "IpcClient.eth_accounts/1" do 156 | test "returns addresses owned by client" do 157 | {:ok, result} = IpcClient.eth_accounts() 158 | 159 | assert result |> is_list 160 | end 161 | end 162 | 163 | @tag :eth 164 | describe "IpcClient.eth_block_number/1" do 165 | test "returns number of most recent block" do 166 | result = IpcClient.eth_block_number() 167 | 168 | {:ok, <<_::binary>>} = result 169 | end 170 | end 171 | 172 | @tag :eth 173 | describe "IpcClient.eth_get_balance/3" do 174 | test "returns balance of given account" do 175 | result = IpcClient.eth_get_balance("0x001bdcde60cb916377a3a3bf4e8054051a4d02e7") 176 | 177 | {:ok, <<_::binary>>} = result 178 | end 179 | end 180 | 181 | @tag :skip 182 | describe "IpcClient.eth_get_storage_at/4" do 183 | test "returns value from a storage position at a given address." do 184 | result = 185 | IpcClient.eth_get_balance( 186 | "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7", 187 | "0x0" 188 | ) 189 | 190 | {:ok, <<_::binary>>} = result 191 | end 192 | end 193 | 194 | @tag :eth 195 | describe "IpcClient.eth_get_transaction_count/3" do 196 | test "returns number of transactions sent from an address." do 197 | result = IpcClient.eth_get_transaction_count("0x001bdcde60cb916377a3a3bf4e8054051a4d02e7") 198 | 199 | {:ok, <<_::binary>>} = result 200 | end 201 | end 202 | 203 | @tag :eth 204 | describe "IpcClient.eth_get_block_transaction_count_by_hash/2" do 205 | test "number of transactions in a block from a block matching the given block hash" do 206 | result = 207 | IpcClient.eth_get_block_transaction_count_by_hash( 208 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 209 | ) 210 | 211 | {:ok, nil} = result 212 | end 213 | end 214 | 215 | @tag :eth 216 | describe "IpcClient.eth_get_block_transaction_count_by_number/2" do 217 | test "returns number of transactions in a block from a block matching the given block number" do 218 | result = IpcClient.eth_get_block_transaction_count_by_number() 219 | 220 | {:ok, <<_::binary>>} = result 221 | end 222 | end 223 | 224 | @tag :eth 225 | describe "IpcClient.eth_get_uncle_count_by_block_hash/2" do 226 | test "the number of uncles in a block from a block matching the given block hash" do 227 | result = 228 | IpcClient.eth_get_uncle_count_by_block_hash( 229 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 230 | ) 231 | 232 | {:ok, nil} = result 233 | end 234 | end 235 | 236 | @tag :eth 237 | describe "IpcClient.eth_get_uncle_count_by_block_number/2" do 238 | test "the number of uncles in a block from a block matching the given block hash" do 239 | result = IpcClient.eth_get_uncle_count_by_block_number() 240 | 241 | {:ok, <<_::binary>>} = result 242 | end 243 | end 244 | 245 | @tag :eth 246 | describe "IpcClient.eth_get_code/3" do 247 | test "returns code at a given address" do 248 | result = IpcClient.eth_get_code("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b") 249 | 250 | {:ok, <<_::binary>>} = result 251 | end 252 | end 253 | 254 | @tag :eth_sign 255 | describe "IpcClient.eth_sign/3" do 256 | test "returns signature" do 257 | result = IpcClient.eth_sign("0x71cf0b576a95c347078ec2339303d13024a26910", "0xdeadbeaf") 258 | 259 | {:ok, <<_::binary>>} = result 260 | end 261 | end 262 | 263 | @tag :eth 264 | describe "IpcClient.eth_estimate_gas/3" do 265 | test "estimates gas" do 266 | data = 267 | "0x6060604052341561" <> 268 | "000f57600080fd5b60b38061001d6000396000f3006060604052" <> 269 | "63ffffffff7c0100000000000000000000000000000000000000" <> 270 | "00000000000000000060003504166360fe47b181146045578063" <> 271 | "6d4ce63c14605a57600080fd5b3415604f57600080fd5b605860" <> 272 | "0435607c565b005b3415606457600080fd5b606a6081565b6040" <> 273 | "5190815260200160405180910390f35b600055565b6000549056" <> 274 | "00a165627a7a7230582033edcee10845eead909dccb4e95bb7e4" <> 275 | "062e92234bf3cfaf804edbd265e599800029" 276 | 277 | from = "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7" 278 | transaction = %{data: data, from: from} 279 | 280 | result = IpcClient.eth_estimate_gas(transaction) 281 | 282 | {:ok, <<_::binary>>} = result 283 | end 284 | end 285 | 286 | @tag :eth 287 | describe "IpcClient.eth_get_block_by_hash/3" do 288 | test "returns information about a block by hash" do 289 | result = 290 | IpcClient.eth_get_block_by_hash( 291 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 292 | true 293 | ) 294 | 295 | {:ok, nil} = result 296 | end 297 | end 298 | 299 | @tag :eth 300 | describe "IpcClient.eth_get_block_by_number/3" do 301 | test "returns information about a block by number" do 302 | {:ok, result} = IpcClient.eth_get_block_by_number("0x1b4", true) 303 | 304 | assert is_nil(result) || is_map(result) 305 | end 306 | end 307 | 308 | @tag :eth 309 | describe "IpcClient.eth_get_transaction_by_hash/2" do 310 | test "returns the information about a transaction by its hash" do 311 | result = 312 | IpcClient.eth_get_transaction_by_hash( 313 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 314 | ) 315 | 316 | {:ok, nil} = result 317 | end 318 | end 319 | 320 | @tag :eth 321 | describe "IpcClient.eth_get_transaction_by_block_hash_and_index/3" do 322 | test "returns the information about a transaction by block hash and index" do 323 | result = 324 | IpcClient.eth_get_transaction_by_block_hash_and_index( 325 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 326 | "0x0" 327 | ) 328 | 329 | {:ok, nil} = result 330 | end 331 | end 332 | 333 | @tag :eth 334 | describe "IpcClient.eth_get_transaction_by_block_number_and_index/3" do 335 | test "returns the information about a transaction by block number and index" do 336 | result = IpcClient.eth_get_transaction_by_block_number_and_index("earliest", "0x0") 337 | 338 | {:ok, nil} = result 339 | end 340 | end 341 | 342 | @tag :eth 343 | describe "IpcClient.eth_get_transaction_receipt/2" do 344 | test "returns the receipt of a transaction by transaction hash" do 345 | result = 346 | IpcClient.eth_get_transaction_receipt( 347 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 348 | ) 349 | 350 | {:ok, nil} = result 351 | end 352 | end 353 | 354 | @tag :eth 355 | describe "IpcClient.eth_get_uncle_by_block_hash_and_index/3" do 356 | test "returns information about a uncle of a block by hash and uncle index position" do 357 | result = 358 | IpcClient.eth_get_uncle_by_block_hash_and_index( 359 | "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", 360 | "0x0" 361 | ) 362 | 363 | {:ok, nil} = result 364 | end 365 | end 366 | 367 | @tag :eth 368 | describe "IpcClient.eth_get_uncle_by_block_number_and_index/3" do 369 | test "returns information about a uncle of a block by number and uncle index position" do 370 | result = IpcClient.eth_get_uncle_by_block_number_and_index("0x29c", "0x0") 371 | 372 | {:ok, _} = result 373 | end 374 | end 375 | 376 | @tag :eth_compile 377 | describe "IpcClient.eth_get_compilers/1" do 378 | test "returns a list of available compilers in the client" do 379 | result = IpcClient.eth_get_compilers() 380 | 381 | {:ok, _} = result 382 | end 383 | end 384 | 385 | @tag :eth 386 | describe "IpcClient.eth_new_filter/2" do 387 | test "creates a filter object" do 388 | filter = %{ 389 | fromBlock: "0x1", 390 | toBlock: "0x2", 391 | address: "0x8888f1f195afa192cfee860698584c030f4c9db1", 392 | topics: [ 393 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 394 | nil, 395 | [ 396 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 397 | "0x0000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebccc" 398 | ] 399 | ] 400 | } 401 | 402 | result = IpcClient.eth_new_filter(filter) 403 | 404 | {:ok, <<_::binary>>} = result 405 | end 406 | end 407 | 408 | @tag :eth 409 | describe "IpcClient.eth_new_12" do 410 | test "creates a filter object" do 411 | filter = %{ 412 | fromBlock: "0x1", 413 | toBlock: "0x2", 414 | address: "0x8888f1f195afa192cfee860698584c030f4c9db1", 415 | topics: [ 416 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 417 | nil, 418 | [ 419 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 420 | "0x0000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebccc" 421 | ] 422 | ] 423 | } 424 | 425 | result = IpcClient.eth_new_filter(filter) 426 | 427 | {:ok, <<_::binary>>} = result 428 | end 429 | end 430 | 431 | @tag :eth 432 | describe "IpcClient.eth_new_block_filter/1" do 433 | test "creates new block filter" do 434 | result = IpcClient.eth_new_block_filter() 435 | 436 | {:ok, <<_::binary>>} = result 437 | end 438 | end 439 | 440 | @tag :eth 441 | describe "IpcClient.eth_new_pending_transaction_filter/1" do 442 | test "creates new pending transaction filter" do 443 | result = IpcClient.eth_new_pending_transaction_filter() 444 | 445 | {:ok, <<_::binary>>} = result 446 | end 447 | end 448 | 449 | @tag :eth 450 | describe "IpcClient.eth_uninstall_filter/2" do 451 | test "uninstalls a filter with given id" do 452 | {:ok, result} = IpcClient.eth_uninstall_filter("0xb") 453 | 454 | assert is_boolean(result) 455 | end 456 | end 457 | 458 | @tag :eth 459 | describe "IpcClient.eth_get_filter_changes/2" do 460 | test "returns an array of logs which occurred since last poll" do 461 | result = IpcClient.eth_get_filter_changes("0x16") 462 | 463 | {:ok, []} = result 464 | end 465 | end 466 | 467 | @tag :eth 468 | describe "IpcClient.eth_get_filter_logs/2" do 469 | test "returns an array of all logs matching filter with given id" do 470 | result = IpcClient.eth_get_filter_logs("0x16") 471 | 472 | {:ok, []} = result 473 | end 474 | end 475 | 476 | @tag :eth 477 | describe "IpcClient.eth_get_logs/2" do 478 | test "returns an array of all logs matching a given filter object" do 479 | filter = %{ 480 | topics: ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b"] 481 | } 482 | 483 | result = IpcClient.eth_get_logs(filter) 484 | 485 | {:ok, []} = result 486 | end 487 | end 488 | 489 | @tag :eth_mine 490 | describe "IpcClient.eth_submit_work/4" do 491 | test "validates solution" do 492 | result = 493 | IpcClient.eth_submit_work( 494 | "0x0000000000000001", 495 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 496 | "0xD1FE5700000000000000000000000000D1FE5700000000000000000000000000" 497 | ) 498 | 499 | {:ok, false} = result 500 | end 501 | end 502 | 503 | @tag :eth_mine 504 | describe "IpcClient.eth_get_work/1" do 505 | test "returns the hash of the current block, the seedHash, and the boundary condition to be met " do 506 | result = IpcClient.eth_get_work() 507 | 508 | {:ok, [<<_::binary>>, <<_::binary>>, <<_::binary>>]} = result 509 | end 510 | end 511 | 512 | @tag :eth_mine 513 | describe "IpcClient.eth_submit_hashrate/3" do 514 | test "submits mining hashrate" do 515 | result = 516 | IpcClient.eth_submit_hashrate( 517 | "0x0000000000000000000000000000000000000000000000000000000000500000", 518 | "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c" 519 | ) 520 | 521 | {:ok, true} = result 522 | end 523 | end 524 | 525 | @tag :eth_db 526 | describe "IpcClient.db_put_string/4" do 527 | test "stores a string in the local database" do 528 | result = IpcClient.db_put_string("testDB", "myKey", "myString") 529 | 530 | {:ok, true} = result 531 | end 532 | end 533 | 534 | @tag :eth_db 535 | describe "IpcClient.db_get_string/3" do 536 | test "returns string from the local database" do 537 | result = IpcClient.db_get_string("db", "key") 538 | 539 | {:ok, nil} = result 540 | end 541 | end 542 | 543 | @tag :eth_db 544 | describe "IpcClient.db_put_hex/4" do 545 | test "stores binary data in the local database" do 546 | result = IpcClient.db_put_hex("db", "key", "data") 547 | 548 | {:ok, true} = result 549 | end 550 | end 551 | 552 | @tag :eth_db 553 | describe "IpcClient.db_get_hex/3" do 554 | test "returns binary data from the local database" do 555 | result = IpcClient.db_get_hex("db", "key") 556 | 557 | {:ok, nil} = result 558 | end 559 | end 560 | 561 | @tag :shh 562 | describe "IpcClient.shh_post/2" do 563 | test "sends a whisper message" do 564 | whisper = %{ 565 | from: 566 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1", 567 | to: 568 | "0x3e245533f97284d442460f2998cd41858798ddf04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a0d4d661997d3940272b717b1", 569 | topics: [ 570 | "0x776869737065722d636861742d636c69656e74", 571 | "0x4d5a695276454c39425154466b61693532" 572 | ], 573 | payload: "0x7b2274797065223a226d6", 574 | priority: "0x64", 575 | ttl: "0x64" 576 | } 577 | 578 | result = IpcClient.shh_post(whisper) 579 | 580 | {:ok, true} = result 581 | end 582 | end 583 | 584 | @tag :shh 585 | describe "IpcClient.shh_version/1" do 586 | test "returns the current whisper protocol version" do 587 | result = IpcClient.shh_version() 588 | 589 | {:ok, <<_::binary>>} = result 590 | end 591 | end 592 | 593 | @tag :shh 594 | describe "IpcClient.shh_new_identity/1" do 595 | test "creates new whisper identity in the client" do 596 | result = IpcClient.shh_new_identity() 597 | 598 | {:ok, <<_::binary>>} = result 599 | end 600 | end 601 | 602 | @tag :shh 603 | describe "IpcClient.shh_has_entity/2" do 604 | test "creates new whisper identity in the client" do 605 | result = 606 | IpcClient.shh_has_identity( 607 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 608 | ) 609 | 610 | {:ok, false} = result 611 | end 612 | end 613 | 614 | @tag :shh 615 | describe "IpcClient.shh_add_to_group/2" do 616 | test "adds adress to group" do 617 | result = 618 | IpcClient.shh_add_to_group( 619 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 620 | ) 621 | 622 | {:ok, false} = result 623 | end 624 | end 625 | 626 | @tag :shh 627 | describe "IpcClient.shh_new_group/1" do 628 | test "creates new group" do 629 | result = IpcClient.shh_new_group() 630 | 631 | {:ok, <<_::binary>>} = result 632 | end 633 | end 634 | 635 | @tag :shh 636 | describe "IpcClient.shh_new_filter/2" do 637 | test "creates filter to notify, when client receives whisper message matching the filter options" do 638 | filter_options = %{ 639 | topics: [~c"0x12341234bf4b564f"], 640 | to: 641 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 642 | } 643 | 644 | result = IpcClient.shh_new_filter(filter_options) 645 | 646 | {:ok, <<_::binary>>} = result 647 | end 648 | end 649 | 650 | @tag :shh 651 | describe "IpcClient.shh_uninstall_filter/2" do 652 | test "uninstalls a filter with given id" do 653 | result = IpcClient.shh_uninstall_filter("0x7") 654 | 655 | {:ok, false} = result 656 | end 657 | end 658 | 659 | @tag :shh 660 | describe "IpcClient.shh_get_filter_changes/2" do 661 | test "polls filter chages" do 662 | result = IpcClient.shh_get_filter_changes("0x7") 663 | 664 | {:ok, []} = result 665 | end 666 | end 667 | 668 | @tag :shh 669 | describe "IpcClient.shh_get_messages/2" do 670 | test "returns all messages matching a filter" do 671 | result = IpcClient.shh_get_messages("0x7") 672 | 673 | {:ok, []} = result 674 | end 675 | end 676 | 677 | @tag :batch 678 | describe "IpcClient.batch_request/1" do 679 | test "sends batch request" do 680 | requests = [ 681 | {:web3_client_version, []}, 682 | {:net_version, []}, 683 | {:web3_sha3, ["0x68656c6c6f20776f726c64"]} 684 | ] 685 | 686 | result = IpcClient.batch_request(requests) 687 | 688 | { 689 | :ok, 690 | [ 691 | {:ok, <<_::binary>>}, 692 | {:ok, <<_::binary>>}, 693 | {:ok, "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"} 694 | ] 695 | } = result 696 | end 697 | end 698 | end 699 | -------------------------------------------------------------------------------- /test/ethereumex/websocket_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.WebsocketClientTest do 2 | use ExUnit.Case 3 | use Mimic 4 | 5 | alias Ethereumex.WebsocketClient 6 | alias Ethereumex.WebsocketServer 7 | 8 | @default_url "ws://localhost:8545" 9 | 10 | setup_all do 11 | _ = Application.put_env(:ethereumex, :client_type, :websocket) 12 | _ = Application.put_env(:ethereumex, :websocket_url, @default_url) 13 | 14 | on_exit(fn -> 15 | _ = Application.put_env(:ethereumex, :client_type, :http) 16 | end) 17 | end 18 | 19 | describe "WebsocketClient.web3_client_version/0" do 20 | test "returns client version" do 21 | version = "0.0.0" 22 | expect_ws_post(version) 23 | assert {:ok, ^version} = WebsocketClient.web3_client_version() 24 | end 25 | end 26 | 27 | describe "WebsocketClient.web3_sha3/1" do 28 | test "returns sha3 of the given data" do 29 | sha3_data = "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" 30 | expect_ws_post(sha3_data) 31 | assert {:ok, ^sha3_data} = WebsocketClient.web3_sha3("0x68656c6c6f20776f726c64") 32 | end 33 | end 34 | 35 | describe "WebsocketClient.net_version/0" do 36 | test "returns network id" do 37 | version = "1" 38 | expect_ws_post(version) 39 | assert {:ok, ^version} = WebsocketClient.net_version() 40 | end 41 | end 42 | 43 | describe "WebsocketClient.net_peer_count/0" do 44 | test "returns number of peers currently connected to the client" do 45 | peer_count = "0x2" 46 | expect_ws_post(peer_count) 47 | assert {:ok, ^peer_count} = WebsocketClient.net_peer_count() 48 | end 49 | end 50 | 51 | describe "WebsocketClient.net_listening/0" do 52 | test "returns true" do 53 | expect_ws_post(true) 54 | assert {:ok, true} = WebsocketClient.net_listening() 55 | end 56 | end 57 | 58 | describe "WebsocketClient.eth_protocol_version/0" do 59 | test "returns true" do 60 | protocol_version = "0x3f" 61 | expect_ws_post(protocol_version) 62 | assert {:ok, ^protocol_version} = WebsocketClient.eth_protocol_version() 63 | end 64 | end 65 | 66 | describe "WebsocketClient.eth_syncing/1" do 67 | test "checks sync status" do 68 | expect_ws_post(false) 69 | assert {:ok, false} = WebsocketClient.eth_syncing() 70 | end 71 | end 72 | 73 | describe "WebsocketClient.eth_chain_id/1" do 74 | test "returns chain id of the RPC serveer" do 75 | chain_id = "0x1" 76 | expect_ws_post(chain_id) 77 | assert {:ok, ^chain_id} = WebsocketClient.eth_chain_id() 78 | end 79 | end 80 | 81 | describe "WebsocketClient.eth_coinbase/1" do 82 | test "returns coinbase address" do 83 | address = "0x407d73d8a49eeb85d32cf465507dd71d507100c1" 84 | expect_ws_post(address) 85 | assert {:ok, ^address} = WebsocketClient.eth_coinbase() 86 | end 87 | end 88 | 89 | describe "WebsocketClient.eth_mining/1" do 90 | test "checks mining status" do 91 | expect_ws_post(false) 92 | assert {:ok, false} = WebsocketClient.eth_mining() 93 | end 94 | end 95 | 96 | describe "WebsocketClient.eth_hashrate/1" do 97 | test "returns hashrate" do 98 | hashrate = "0x0" 99 | expect_ws_post(hashrate) 100 | assert {:ok, ^hashrate} = WebsocketClient.eth_hashrate() 101 | end 102 | end 103 | 104 | describe "WebsocketClient.eth_gas_price/1" do 105 | test "returns current price per gas" do 106 | gas_price = "0x09184e72a000" 107 | expect_ws_post(gas_price) 108 | assert {:ok, ^gas_price} = WebsocketClient.eth_gas_price() 109 | end 110 | end 111 | 112 | describe "WebsocketClient.eth_max_priority_fee_per_gas/1" do 113 | test "returns current max priority fee per gas" do 114 | priority_fee = "0x09184e72a000" 115 | expect_ws_post(priority_fee) 116 | assert {:ok, ^priority_fee} = WebsocketClient.eth_max_priority_fee_per_gas() 117 | end 118 | end 119 | 120 | describe "WebsocketClient.eth_fee_history/3" do 121 | test "returns a collection of historical gas information" do 122 | fee_history = %{ 123 | "baseFeePerGas" => ["0x1", "0x2"], 124 | "gasUsedRatio" => [0.5, 0.6], 125 | "oldestBlock" => "0x1", 126 | "reward" => [["0x1", "0x2"], ["0x3", "0x4"]] 127 | } 128 | 129 | expect_ws_post(fee_history) 130 | assert {:ok, ^fee_history} = WebsocketClient.eth_fee_history(1, "latest", [25, 75]) 131 | end 132 | end 133 | 134 | describe "WebsocketClient.eth_blob_base_fee/1" do 135 | test "returns the base fee for blob transactions" do 136 | base_fee = "0x1234" 137 | expect_ws_post(base_fee) 138 | assert {:ok, ^base_fee} = WebsocketClient.eth_blob_base_fee() 139 | end 140 | end 141 | 142 | describe "WebsocketClient.eth_accounts/1" do 143 | test "returns addresses owned by client" do 144 | accounts = ["0x407d73d8a49eeb85d32cf465507dd71d507100c1"] 145 | expect_ws_post(accounts) 146 | assert {:ok, ^accounts} = WebsocketClient.eth_accounts() 147 | end 148 | end 149 | 150 | describe "WebsocketClient.eth_block_number/1" do 151 | test "returns number of most recent block" do 152 | block_number = "0x4b7" 153 | expect_ws_post(block_number) 154 | assert {:ok, ^block_number} = WebsocketClient.eth_block_number() 155 | end 156 | end 157 | 158 | describe "WebsocketClient.eth_get_balance/3" do 159 | test "returns balance of given account" do 160 | balance = "0x0234c8a3397aab58" 161 | expect_ws_post(balance) 162 | 163 | assert {:ok, ^balance} = 164 | WebsocketClient.eth_get_balance("0x001bdcde60cb916377a3a3bf4e8054051a4d02e7") 165 | end 166 | end 167 | 168 | describe "WebsocketClient.eth_get_storage_at/4" do 169 | test "returns value from a storage position at a given address." do 170 | storage_value = "0x0000000000000000000000000000000000000000000000000000000000000000" 171 | expect_ws_post(storage_value) 172 | 173 | assert {:ok, ^storage_value} = 174 | WebsocketClient.eth_get_balance( 175 | "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7", 176 | "0x0" 177 | ) 178 | end 179 | end 180 | 181 | describe "WebsocketClient.eth_get_transaction_count/3" do 182 | test "returns number of transactions sent from an address." do 183 | count = "0x1" 184 | expect_ws_post(count) 185 | 186 | assert {:ok, ^count} = 187 | WebsocketClient.eth_get_transaction_count( 188 | "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7" 189 | ) 190 | end 191 | end 192 | 193 | describe "WebsocketClient.eth_get_block_transaction_count_by_hash/2" do 194 | test "number of transactions in a block from a block matching the given block hash" do 195 | expect_ws_post(nil) 196 | 197 | assert {:ok, nil} = 198 | WebsocketClient.eth_get_block_transaction_count_by_hash( 199 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 200 | ) 201 | end 202 | end 203 | 204 | describe "WebsocketClient.eth_get_block_transaction_count_by_number/2" do 205 | test "returns number of transactions in a block from a block matching the given block number" do 206 | count = "0x1" 207 | expect_ws_post(count) 208 | assert {:ok, ^count} = WebsocketClient.eth_get_block_transaction_count_by_number() 209 | end 210 | end 211 | 212 | describe "WebsocketClient.eth_get_uncle_count_by_block_hash/2" do 213 | test "the number of uncles in a block from a block matching the given block hash" do 214 | expect_ws_post(nil) 215 | 216 | assert {:ok, nil} = 217 | WebsocketClient.eth_get_uncle_count_by_block_hash( 218 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 219 | ) 220 | end 221 | end 222 | 223 | describe "WebsocketClient.eth_get_uncle_count_by_block_number/2" do 224 | test "the number of uncles in a block from a block matching the given block hash" do 225 | count = "0x1" 226 | expect_ws_post(count) 227 | assert {:ok, ^count} = WebsocketClient.eth_get_uncle_count_by_block_number() 228 | end 229 | end 230 | 231 | describe "WebsocketClient.eth_get_code/3" do 232 | test "returns code at a given address" do 233 | code = 234 | "0x600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905056" 235 | 236 | expect_ws_post(code) 237 | 238 | assert {:ok, ^code} = 239 | WebsocketClient.eth_get_code("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b") 240 | end 241 | end 242 | 243 | describe "WebsocketClient.eth_sign/3" do 244 | test "returns signature" do 245 | signature = 246 | "0x2ac19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601" 247 | 248 | expect_ws_post(signature) 249 | 250 | assert {:ok, ^signature} = 251 | WebsocketClient.eth_sign( 252 | "0x71cf0b576a95c347078ec2339303d13024a26910", 253 | "0xdeadbeaf" 254 | ) 255 | end 256 | end 257 | 258 | describe "WebsocketClient.eth_estimate_gas/3" do 259 | test "estimates gas" do 260 | data = 261 | "0x6060604052341561" <> 262 | "000f57600080fd5b60b38061001d6000396000f3006060604052" <> 263 | "63ffffffff7c0100000000000000000000000000000000000000" <> 264 | "00000000000000000060003504166360fe47b181146045578063" <> 265 | "6d4ce63c14605a57600080fd5b3415604f57600080fd5b605860" <> 266 | "0435607c565b005b3415606457600080fd5b606a6081565b6040" <> 267 | "5190815260200160405180910390f35b600055565b6000549056" <> 268 | "00a165627a7a7230582033edcee10845eead909dccb4e95bb7e4" <> 269 | "062e92234bf3cfaf804edbd265e599800029" 270 | 271 | gas_estimate = "0x5208" 272 | expect_ws_post(gas_estimate) 273 | 274 | from = "0x001bdcde60cb916377a3a3bf4e8054051a4d02e7" 275 | transaction = %{data: data, from: from} 276 | 277 | assert {:ok, ^gas_estimate} = WebsocketClient.eth_estimate_gas(transaction) 278 | end 279 | end 280 | 281 | describe "WebsocketClient.eth_get_block_by_hash/3" do 282 | test "returns information about a block by hash" do 283 | expect_ws_post(nil) 284 | 285 | assert {:ok, nil} = 286 | WebsocketClient.eth_get_block_by_hash( 287 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 288 | true 289 | ) 290 | end 291 | end 292 | 293 | describe "WebsocketClient.eth_get_block_by_number/3" do 294 | test "returns information about a block by number" do 295 | block_info = nil 296 | expect_ws_post(block_info) 297 | assert {:ok, ^block_info} = WebsocketClient.eth_get_block_by_number("0x1b4", true) 298 | end 299 | end 300 | 301 | describe "WebsocketClient.eth_get_transaction_by_hash/2" do 302 | test "returns the information about a transaction by its hash" do 303 | expect_ws_post(nil) 304 | 305 | assert {:ok, nil} = 306 | WebsocketClient.eth_get_transaction_by_hash( 307 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 308 | ) 309 | end 310 | end 311 | 312 | describe "WebsocketClient.eth_get_transaction_by_block_hash_and_index/3" do 313 | test "returns the information about a transaction by block hash and index" do 314 | expect_ws_post(nil) 315 | 316 | assert {:ok, nil} = 317 | WebsocketClient.eth_get_transaction_by_block_hash_and_index( 318 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", 319 | "0x0" 320 | ) 321 | end 322 | end 323 | 324 | describe "WebsocketClient.eth_get_transaction_by_block_number_and_index/3" do 325 | test "returns the information about a transaction by block number and index" do 326 | expect_ws_post(nil) 327 | 328 | assert {:ok, nil} = 329 | WebsocketClient.eth_get_transaction_by_block_number_and_index("earliest", "0x0") 330 | end 331 | end 332 | 333 | describe "WebsocketClient.eth_get_transaction_receipt/2" do 334 | test "returns the receipt of a transaction by transaction hash" do 335 | expect_ws_post(nil) 336 | 337 | assert {:ok, nil} = 338 | WebsocketClient.eth_get_transaction_receipt( 339 | "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" 340 | ) 341 | end 342 | end 343 | 344 | describe "WebsocketClient.eth_get_uncle_by_block_hash_and_index/3" do 345 | test "returns information about a uncle of a block by hash and uncle index position" do 346 | expect_ws_post(nil) 347 | 348 | assert {:ok, nil} = 349 | WebsocketClient.eth_get_uncle_by_block_hash_and_index( 350 | "0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", 351 | "0x0" 352 | ) 353 | end 354 | end 355 | 356 | describe "WebsocketClient.eth_get_uncle_by_block_number_and_index/3" do 357 | test "returns information about a uncle of a block by number and uncle index position" do 358 | uncle_info = %{} 359 | expect_ws_post(uncle_info) 360 | 361 | assert {:ok, ^uncle_info} = 362 | WebsocketClient.eth_get_uncle_by_block_number_and_index("0x29c", "0x0") 363 | end 364 | end 365 | 366 | describe "WebsocketClient.eth_get_compilers/1" do 367 | test "returns a list of available compilers in the client" do 368 | compilers = ["solidity", "lll", "serpent"] 369 | expect_ws_post(compilers) 370 | assert {:ok, ^compilers} = WebsocketClient.eth_get_compilers() 371 | end 372 | end 373 | 374 | describe "WebsocketClient.eth_new_filter/2" do 375 | test "creates a filter object" do 376 | filter = %{ 377 | fromBlock: "0x1", 378 | toBlock: "0x2", 379 | address: "0x8888f1f195afa192cfee860698584c030f4c9db1", 380 | topics: [ 381 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 382 | nil, 383 | [ 384 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 385 | "0x0000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebccc" 386 | ] 387 | ] 388 | } 389 | 390 | filter_id = "0x1" 391 | expect_ws_post(filter_id) 392 | assert {:ok, ^filter_id} = WebsocketClient.eth_new_filter(filter) 393 | end 394 | end 395 | 396 | describe "WebsocketClient.eth_new_block_filter/1" do 397 | test "creates new block filter" do 398 | filter_id = "0x1" 399 | expect_ws_post(filter_id) 400 | assert {:ok, ^filter_id} = WebsocketClient.eth_new_block_filter() 401 | end 402 | end 403 | 404 | describe "WebsocketClient.eth_new_pending_transaction_filter/1" do 405 | test "creates new pending transaction filter" do 406 | filter_id = "0x1" 407 | expect_ws_post(filter_id) 408 | assert {:ok, ^filter_id} = WebsocketClient.eth_new_pending_transaction_filter() 409 | end 410 | end 411 | 412 | describe "WebsocketClient.eth_uninstall_filter/2" do 413 | test "uninstalls a filter with given id" do 414 | expect_ws_post(true) 415 | assert {:ok, true} = WebsocketClient.eth_uninstall_filter("0xb") 416 | end 417 | end 418 | 419 | describe "WebsocketClient.eth_get_filter_changes/2" do 420 | test "returns an array of logs which occurred since last poll" do 421 | expect_ws_post([]) 422 | assert {:ok, []} = WebsocketClient.eth_get_filter_changes("0x16") 423 | end 424 | end 425 | 426 | describe "WebsocketClient.eth_get_filter_logs/2" do 427 | test "returns an array of all logs matching filter with given id" do 428 | expect_ws_post([]) 429 | assert {:ok, []} = WebsocketClient.eth_get_filter_logs("0x16") 430 | end 431 | end 432 | 433 | describe "WebsocketClient.eth_get_logs/2" do 434 | test "returns an array of all logs matching a given filter object" do 435 | filter = %{ 436 | topics: ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b"] 437 | } 438 | 439 | expect_ws_post([]) 440 | assert {:ok, []} = WebsocketClient.eth_get_logs(filter) 441 | end 442 | end 443 | 444 | describe "WebsocketClient.eth_submit_work/4" do 445 | test "validates solution" do 446 | expect_ws_post(false) 447 | 448 | assert {:ok, false} = 449 | WebsocketClient.eth_submit_work( 450 | "0x0000000000000001", 451 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 452 | "0xD1FE5700000000000000000000000000D1FE5700000000000000000000000000" 453 | ) 454 | end 455 | end 456 | 457 | describe "WebsocketClient.eth_get_work/1" do 458 | test "returns the hash of the current block, the seedHash, and the boundary condition to be met " do 459 | work_data = ["0x1234", "0x5678", "0x9abc"] 460 | expect_ws_post(work_data) 461 | assert {:ok, ^work_data} = WebsocketClient.eth_get_work() 462 | end 463 | end 464 | 465 | describe "WebsocketClient.eth_submit_hashrate/3" do 466 | test "submits mining hashrate" do 467 | expect_ws_post(true) 468 | 469 | assert {:ok, true} = 470 | WebsocketClient.eth_submit_hashrate( 471 | "0x0000000000000000000000000000000000000000000000000000000000500000", 472 | "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c" 473 | ) 474 | end 475 | end 476 | 477 | describe "WebsocketClient.db_put_string/4" do 478 | test "stores a string in the local database" do 479 | expect_ws_post(true) 480 | assert {:ok, true} = WebsocketClient.db_put_string("testDB", "myKey", "myString") 481 | end 482 | end 483 | 484 | describe "WebsocketClient.db_get_string/3" do 485 | test "returns string from the local database" do 486 | expect_ws_post(nil) 487 | assert {:ok, nil} = WebsocketClient.db_get_string("db", "key") 488 | end 489 | end 490 | 491 | describe "WebsocketClient.db_put_hex/4" do 492 | test "stores binary data in the local database" do 493 | expect_ws_post(true) 494 | assert {:ok, true} = WebsocketClient.db_put_hex("db", "key", "data") 495 | end 496 | end 497 | 498 | describe "WebsocketClient.db_get_hex/3" do 499 | test "returns binary data from the local database" do 500 | expect_ws_post(nil) 501 | assert {:ok, nil} = WebsocketClient.db_get_hex("db", "key") 502 | end 503 | end 504 | 505 | describe "WebsocketClient.shh_post/2" do 506 | test "sends a whisper message" do 507 | whisper = %{ 508 | from: 509 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1", 510 | to: 511 | "0x3e245533f97284d442460f2998cd41858798ddf04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a0d4d661997d3940272b717b1", 512 | topics: [ 513 | "0x776869737065722d636861742d636c69656e74", 514 | "0x4d5a695276454c39425154466b61693532" 515 | ], 516 | payload: "0x7b2274797065223a226d6", 517 | priority: "0x64", 518 | ttl: "0x64" 519 | } 520 | 521 | expect_ws_post(true) 522 | assert {:ok, true} = WebsocketClient.shh_post(whisper) 523 | end 524 | end 525 | 526 | describe "WebsocketClient.shh_version/1" do 527 | test "returns the current whisper protocol version" do 528 | version = "2" 529 | expect_ws_post(version) 530 | assert {:ok, ^version} = WebsocketClient.shh_version() 531 | end 532 | end 533 | 534 | describe "WebsocketClient.shh_new_identity/1" do 535 | test "creates new whisper identity in the client" do 536 | identity = 537 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 538 | 539 | expect_ws_post(identity) 540 | assert {:ok, ^identity} = WebsocketClient.shh_new_identity() 541 | end 542 | end 543 | 544 | describe "WebsocketClient.shh_has_entity/2" do 545 | test "creates new whisper identity in the client" do 546 | expect_ws_post(false) 547 | 548 | assert {:ok, false} = 549 | WebsocketClient.shh_has_identity( 550 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 551 | ) 552 | end 553 | end 554 | 555 | describe "WebsocketClient.shh_add_to_group/2" do 556 | test "adds adress to group" do 557 | expect_ws_post(false) 558 | 559 | assert {:ok, false} = 560 | WebsocketClient.shh_add_to_group( 561 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 562 | ) 563 | end 564 | end 565 | 566 | describe "WebsocketClient.shh_new_group/1" do 567 | test "creates new group" do 568 | group_id = 569 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 570 | 571 | expect_ws_post(group_id) 572 | assert {:ok, ^group_id} = WebsocketClient.shh_new_group() 573 | end 574 | end 575 | 576 | describe "WebsocketClient.shh_new_filter/2" do 577 | test "creates filter to notify, when client receives whisper message matching the filter options" do 578 | filter_options = %{ 579 | topics: [~c"0x12341234bf4b564f"], 580 | to: 581 | "0x04f96a5e25610293e42a73908e93ccc8c4d4dc0edcfa9fa872f50cb214e08ebf61a03e245533f97284d442460f2998cd41858798ddfd4d661997d3940272b717b1" 582 | } 583 | 584 | filter_id = "0x7" 585 | expect_ws_post(filter_id) 586 | assert {:ok, ^filter_id} = WebsocketClient.shh_new_filter(filter_options) 587 | end 588 | end 589 | 590 | describe "WebsocketClient.shh_uninstall_filter/2" do 591 | test "uninstalls a filter with given id" do 592 | expect_ws_post(false) 593 | assert {:ok, false} = WebsocketClient.shh_uninstall_filter("0x7") 594 | end 595 | end 596 | 597 | describe "WebsocketClient.shh_get_filter_changes/2" do 598 | test "polls filter chages" do 599 | expect_ws_post([]) 600 | assert {:ok, []} = WebsocketClient.shh_get_filter_changes("0x7") 601 | end 602 | end 603 | 604 | describe "WebsocketClient.shh_get_messages/2" do 605 | test "returns all messages matching a filter" do 606 | expect_ws_post([]) 607 | assert {:ok, []} = WebsocketClient.shh_get_messages("0x7") 608 | end 609 | end 610 | 611 | describe "WebsocketClient.subscribe/2" do 612 | test "subscribes to newHeads events" do 613 | subscription_id = "0x9cef478923ff08bf67fde6c64013158d" 614 | expect_ws_subscribe(subscription_id) 615 | assert {:ok, ^subscription_id} = WebsocketClient.subscribe(:newHeads) 616 | end 617 | 618 | test "subscribes to logs with filter params" do 619 | subscription_id = "0x4a8a4c0517381924f9838102c5a4dcb7" 620 | filter_params = %{address: "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd"} 621 | expect_ws_subscribe(subscription_id) 622 | assert {:ok, ^subscription_id} = WebsocketClient.subscribe(:logs, filter_params) 623 | end 624 | 625 | test "subscribes to newPendingTransactions" do 626 | subscription_id = "0x1234567890abcdef1234567890abcdef" 627 | expect_ws_subscribe(subscription_id) 628 | assert {:ok, ^subscription_id} = WebsocketClient.subscribe(:newPendingTransactions) 629 | end 630 | end 631 | 632 | describe "WebsocketClient.unsubscribe/1" do 633 | test "unsubscribes from a single subscription" do 634 | subscription_id = "0x9cef478923ff08bf67fde6c64013158d" 635 | expect_ws_unsubscribe(true) 636 | assert {:ok, true} = WebsocketClient.unsubscribe(subscription_id) 637 | end 638 | 639 | test "unsubscribes from multiple subscriptions" do 640 | subscription_ids = [ 641 | "0x9cef478923ff08bf67fde6c64013158d", 642 | "0x4a8a4c0517381924f9838102c5a4dcb7" 643 | ] 644 | 645 | expect_ws_unsubscribe(true) 646 | assert {:ok, true} = WebsocketClient.unsubscribe(subscription_ids) 647 | end 648 | end 649 | 650 | defp expect_ws_post(response) do 651 | expect(WebsocketServer, :post, fn _ -> {:ok, response} end) 652 | end 653 | 654 | defp expect_ws_subscribe(response) do 655 | expect(WebsocketServer, :subscribe, fn _ -> {:ok, response} end) 656 | end 657 | 658 | defp expect_ws_unsubscribe(response) do 659 | expect(WebsocketServer, :unsubscribe, fn _ -> {:ok, response} end) 660 | end 661 | end 662 | -------------------------------------------------------------------------------- /test/ethereumex/websocket_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethereumex.WebsocketServerTest do 2 | use ExUnit.Case, async: true 3 | use Mimic 4 | 5 | import ExUnit.CaptureLog 6 | 7 | alias Ethereumex.WebsocketServer 8 | alias Ethereumex.WebsocketServer.State 9 | alias Ethereumex.Config 10 | 11 | @valid_request ~s({"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": "1"}) 12 | @valid_response ~s({"jsonrpc": "2.0", "id": "1", "result": "0x1234"}) 13 | @default_url "ws://localhost:8545" 14 | 15 | setup_all do 16 | _ = Application.put_env(:ethereumex, :client_type, :websocket) 17 | _ = Application.put_env(:ethereumex, :websocket_url, @default_url) 18 | 19 | on_exit(fn -> 20 | _ = Application.put_env(:ethereumex, :client_type, :http) 21 | end) 22 | end 23 | 24 | describe "start_link/1" do 25 | test "starts with default configuration" do 26 | expect(WebSockex, :start_link, fn @default_url, 27 | WebsocketServer, 28 | %State{url: @default_url}, 29 | name: Ethereumex.WebsocketServer, 30 | handle_initial_conn_failure: true -> 31 | {:ok, self()} 32 | end) 33 | 34 | assert {:ok, pid} = WebsocketServer.start_link() 35 | assert is_pid(pid) 36 | end 37 | 38 | test "starts with custom URL" do 39 | custom_url = "ws://custom:8545" 40 | 41 | expect(WebSockex, :start_link, fn ^custom_url, 42 | Ethereumex.WebsocketServer, 43 | %State{url: ^custom_url}, 44 | name: Ethereumex.WebsocketServer, 45 | handle_initial_conn_failure: true -> 46 | {:ok, self()} 47 | end) 48 | 49 | assert {:ok, pid} = WebsocketServer.start_link(url: custom_url) 50 | assert is_pid(pid) 51 | end 52 | end 53 | 54 | describe "handle_connect/2" do 55 | test "logs connection and returns state unchanged" do 56 | state = %State{url: "ws://test.com"} 57 | 58 | log = 59 | capture_log(fn -> 60 | assert {:ok, ^state} = WebsocketServer.handle_connect(nil, state) 61 | end) 62 | 63 | assert log =~ "Connected to WebSocket server at ws://test.com" 64 | end 65 | end 66 | 67 | describe "handle_cast/2" do 68 | test "stores request and prepares response" do 69 | state = %State{url: "ws://test.com", requests: %{}} 70 | 71 | {:reply, {:text, request}, new_state} = 72 | WebsocketServer.handle_cast({:request, "1", @valid_request, self()}, state) 73 | 74 | assert request == @valid_request 75 | assert Map.get(new_state.requests, "1") == self() 76 | end 77 | end 78 | 79 | describe "handle_frame/2" do 80 | test "processes valid response and notifies caller" do 81 | state = %State{ 82 | url: "ws://test.com", 83 | requests: %{"1" => self()} 84 | } 85 | 86 | assert {:ok, new_state} = WebsocketServer.handle_frame({:text, @valid_response}, state) 87 | assert new_state.requests == %{} 88 | assert_received {:response, "1", "0x1234"} 89 | end 90 | 91 | test "ignores response with unknown request id" do 92 | state = %State{url: "ws://test.com", requests: %{}} 93 | 94 | assert {:ok, ^state} = 95 | WebsocketServer.handle_frame({:text, @valid_response}, state) 96 | end 97 | 98 | test "handles invalid JSON response" do 99 | state = %State{url: "ws://test.com", requests: %{}} 100 | 101 | assert {:ok, ^state} = 102 | WebsocketServer.handle_frame({:text, "invalid json"}, state) 103 | end 104 | 105 | test "handles response without result" do 106 | state = %State{url: "ws://test.com", requests: %{}} 107 | response = ~s({"jsonrpc": "2.0", "id": "1"}) 108 | 109 | assert {:ok, ^state} = 110 | WebsocketServer.handle_frame({:text, response}, state) 111 | end 112 | end 113 | 114 | describe "post/1" do 115 | test "successfully posts request and receives response" do 116 | test_pid = self() 117 | 118 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 119 | {:request, "1", @valid_request, ^test_pid} -> 120 | send(test_pid, {:response, "1", "0x1234"}) 121 | :ok 122 | end) 123 | 124 | assert {:ok, "0x1234"} = WebsocketServer.post(@valid_request) 125 | end 126 | 127 | test "handles timeout" do 128 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 129 | {:request, "1", @valid_request, _pid} -> 130 | :ok 131 | end) 132 | 133 | assert {:error, :timeout} = WebsocketServer.post(@valid_request) 134 | end 135 | 136 | test "handles invalid JSON request" do 137 | assert {:error, :decode_error} = WebsocketServer.post("invalid json") 138 | end 139 | 140 | test "handles request without id" do 141 | request = ~s({"jsonrpc": "2.0", "method": "eth_blockNumber", "params": []}) 142 | assert {:error, :invalid_request_format} = WebsocketServer.post(request) 143 | end 144 | end 145 | 146 | describe "reconnection handling" do 147 | test "resets reconnection attempts after successful connection" do 148 | state = %WebsocketServer.State{ 149 | url: "ws://localhost:8545", 150 | reconnect_attempts: 3 151 | } 152 | 153 | {:ok, new_state} = WebsocketServer.handle_connect(%{}, state) 154 | assert new_state.reconnect_attempts == 0 155 | end 156 | 157 | test "does not attempt reconnection for local disconnects" do 158 | state = %WebsocketServer.State{ 159 | url: "ws://localhost:8545", 160 | reconnect_attempts: 0 161 | } 162 | 163 | disconnect_status = %{reason: {:local, :normal}} 164 | assert {:ok, ^state} = WebsocketServer.handle_disconnect(disconnect_status, state) 165 | end 166 | 167 | test "attempts reconnection with exponential backoff" do 168 | state = %WebsocketServer.State{ 169 | url: "ws://localhost:8545", 170 | reconnect_attempts: 0 171 | } 172 | 173 | disconnect_status = %{reason: {:remote, :closed}} 174 | 175 | log = 176 | capture_log(fn -> 177 | {:reconnect, new_state} = WebsocketServer.handle_disconnect(disconnect_status, state) 178 | assert new_state.reconnect_attempts == 1 179 | end) 180 | 181 | assert log =~ "Attempting reconnection 1/5" 182 | end 183 | 184 | test "stops reconnecting after max attempts" do 185 | state = %WebsocketServer.State{ 186 | url: "ws://localhost:8545", 187 | reconnect_attempts: 5 188 | } 189 | 190 | disconnect_status = %{reason: {:remote, :closed}} 191 | 192 | log = 193 | capture_log(fn -> 194 | {:ok, _new_state} = WebsocketServer.handle_disconnect(disconnect_status, state) 195 | end) 196 | 197 | assert log =~ "Max reconnection attempts (5) reached" 198 | end 199 | 200 | test "implements exponential backoff delay" do 201 | state = %WebsocketServer.State{ 202 | url: "ws://localhost:8545", 203 | reconnect_attempts: 0 204 | } 205 | 206 | disconnect_status = %{reason: {:remote, :closed}} 207 | 208 | {time, _} = 209 | :timer.tc(fn -> 210 | WebsocketServer.handle_disconnect(disconnect_status, state) 211 | end) 212 | 213 | assert_in_delta time / 1000, 1000, 100 214 | end 215 | end 216 | 217 | describe "subscribe/1" do 218 | test "successfully subscribes to newHeads" do 219 | subscription_id = "0x9cef478923ff08bf67fde6c64013158d" 220 | test_pid = self() 221 | 222 | subscribe_request = %{ 223 | jsonrpc: "2.0", 224 | method: "eth_subscribe", 225 | params: ["newHeads"], 226 | id: 1 227 | } 228 | 229 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 230 | {:subscription, ^subscribe_request, ^test_pid} -> 231 | send(test_pid, {:response, 1, subscription_id}) 232 | :ok 233 | end) 234 | 235 | assert {:ok, ^subscription_id} = WebsocketServer.subscribe(subscribe_request) 236 | end 237 | 238 | test "successfully subscribes to logs with filter" do 239 | subscription_id = "0x4a8a4c0517381924f9838102c5a4dcb7" 240 | test_pid = self() 241 | 242 | filter_params = %{address: "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd"} 243 | 244 | subscribe_request = %{ 245 | jsonrpc: "2.0", 246 | method: "eth_subscribe", 247 | params: ["logs", filter_params], 248 | id: 2 249 | } 250 | 251 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 252 | {:subscription, ^subscribe_request, ^test_pid} -> 253 | send(test_pid, {:response, 2, subscription_id}) 254 | :ok 255 | end) 256 | 257 | assert {:ok, ^subscription_id} = WebsocketServer.subscribe(subscribe_request) 258 | end 259 | 260 | test "handles subscription timeout" do 261 | test_pid = self() 262 | 263 | subscribe_request = %{ 264 | jsonrpc: "2.0", 265 | method: "eth_subscribe", 266 | params: ["newHeads"], 267 | id: 3 268 | } 269 | 270 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 271 | {:subscription, ^subscribe_request, ^test_pid} -> 272 | :ok 273 | end) 274 | 275 | assert {:error, :timeout} = WebsocketServer.subscribe(subscribe_request) 276 | end 277 | end 278 | 279 | describe "unsubscribe/1" do 280 | test "successfully unsubscribes from single subscription" do 281 | test_pid = self() 282 | subscription_id = "0x9cef478923ff08bf67fde6c64013158d" 283 | 284 | unsubscribe_request = %{ 285 | jsonrpc: "2.0", 286 | method: "eth_unsubscribe", 287 | params: [subscription_id], 288 | id: 4 289 | } 290 | 291 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 292 | {:unsubscribe, ^unsubscribe_request, ^test_pid} -> 293 | send(test_pid, {:response, 4, true}) 294 | :ok 295 | end) 296 | 297 | assert {:ok, true} = WebsocketServer.unsubscribe(unsubscribe_request) 298 | end 299 | 300 | test "successfully unsubscribes from multiple subscriptions" do 301 | test_pid = self() 302 | 303 | subscription_ids = [ 304 | "0x9cef478923ff08bf67fde6c64013158d", 305 | "0x4a8a4c0517381924f9838102c5a4dcb7" 306 | ] 307 | 308 | unsubscribe_request = %{ 309 | jsonrpc: "2.0", 310 | method: "eth_unsubscribe", 311 | params: subscription_ids, 312 | id: 5 313 | } 314 | 315 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 316 | {:unsubscribe, ^unsubscribe_request, ^test_pid} -> 317 | send(test_pid, {:response, 5, true}) 318 | :ok 319 | end) 320 | 321 | assert {:ok, true} = WebsocketServer.unsubscribe(unsubscribe_request) 322 | end 323 | 324 | test "handles unsubscribe timeout" do 325 | test_pid = self() 326 | subscription_id = "0x9cef478923ff08bf67fde6c64013158d" 327 | 328 | unsubscribe_request = %{ 329 | jsonrpc: "2.0", 330 | method: "eth_unsubscribe", 331 | params: [subscription_id], 332 | id: 6 333 | } 334 | 335 | expect(WebSockex, :cast, fn Ethereumex.WebsocketServer, 336 | {:unsubscribe, ^unsubscribe_request, ^test_pid} -> 337 | :ok 338 | end) 339 | 340 | assert {:error, :timeout} = WebsocketServer.unsubscribe(unsubscribe_request) 341 | end 342 | end 343 | 344 | describe "subscription notifications" do 345 | test "handles newHeads subscription notification" do 346 | state = %State{ 347 | url: "ws://test.com", 348 | subscriptions: %{"0x9cef478923ff08bf67fde6c64013158d" => self()} 349 | } 350 | 351 | notification = %{ 352 | "jsonrpc" => "2.0", 353 | "method" => "eth_subscription", 354 | "params" => %{ 355 | "subscription" => "0x9cef478923ff08bf67fde6c64013158d", 356 | "result" => %{ 357 | "number" => "0x1b4", 358 | "hash" => "0x8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d", 359 | "parentHash" => "0x9646252be9520f6e71339a8df9c55e4d7619deeb018d2a3f2d21fc165dde5eb5" 360 | } 361 | } 362 | } 363 | 364 | assert {:ok, ^state} = 365 | WebsocketServer.handle_frame({:text, json_encode!(notification)}, state) 366 | 367 | assert_received ^notification 368 | end 369 | 370 | test "handles logs subscription notification" do 371 | state = %State{ 372 | url: "ws://test.com", 373 | subscriptions: %{"0x4a8a4c0517381924f9838102c5a4dcb7" => self()} 374 | } 375 | 376 | notification = %{ 377 | "jsonrpc" => "2.0", 378 | "method" => "eth_subscription", 379 | "params" => %{ 380 | "subscription" => "0x4a8a4c0517381924f9838102c5a4dcb7", 381 | "result" => %{ 382 | "address" => "0x8320fe7702b96808f7bbc0d4a888ed1468216cfd", 383 | "topics" => ["0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902"], 384 | "data" => "0x0000000000000000000000000000000000000000000000000000000000000001" 385 | } 386 | } 387 | } 388 | 389 | assert {:ok, ^state} = 390 | WebsocketServer.handle_frame({:text, json_encode!(notification)}, state) 391 | 392 | assert_received ^notification 393 | end 394 | 395 | test "ignores notification for unknown subscription" do 396 | state = %State{ 397 | url: "ws://test.com", 398 | subscriptions: %{} 399 | } 400 | 401 | notification = %{ 402 | "jsonrpc" => "2.0", 403 | "method" => "eth_subscription", 404 | "params" => %{ 405 | "subscription" => "0xunknown", 406 | "result" => %{"number" => "0x1b4"} 407 | } 408 | } 409 | 410 | assert {:ok, ^state} = 411 | WebsocketServer.handle_frame({:text, json_encode!(notification)}, state) 412 | 413 | refute_received ^notification 414 | end 415 | end 416 | 417 | defp json_encode!(map) do 418 | Config.json_module().encode!(map) 419 | end 420 | end 421 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure( 2 | exclude: [ 3 | :skip, 4 | :web3, 5 | :net, 6 | :eth, 7 | :eth_mine, 8 | :eth_db, 9 | :shh, 10 | :eth_compile, 11 | :eth_sign, 12 | :batch, 13 | :eth_chain_id 14 | ] 15 | ) 16 | 17 | Mimic.copy(WebSockex) 18 | Mimic.copy(Ethereumex.WebsocketServer) 19 | 20 | Application.ensure_all_started(:telemetry) 21 | ExUnit.start() 22 | --------------------------------------------------------------------------------