├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── elixir.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── ethers_logo.png └── exdoc_logo.png ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── coveralls.json ├── guides ├── configuration.md ├── typed-arguments.md └── upgrading.md ├── lib ├── ethers.ex └── ethers │ ├── ccip_read.ex │ ├── contract.ex │ ├── contract_helpers.ex │ ├── contracts │ ├── ccip_read.ex │ ├── ens.ex │ ├── erc1155.ex │ ├── erc165.ex │ ├── erc20.ex │ ├── erc721.ex │ ├── erc777.ex │ └── multicall3.ex │ ├── error.ex │ ├── event.ex │ ├── event_filter.ex │ ├── execution_error.ex │ ├── multicall.ex │ ├── name_service.ex │ ├── rpc_client.ex │ ├── rpc_client │ ├── adapter.ex │ └── ethereumex_http_client.ex │ ├── signer.ex │ ├── signer │ ├── json_rpc.ex │ └── local.ex │ ├── transaction.ex │ ├── transaction │ ├── eip1559.ex │ ├── eip2930.ex │ ├── eip4844.ex │ ├── helpers.ex │ ├── legacy.ex │ ├── metadata.ex │ ├── protocol.ex │ └── signed.ex │ ├── tx_data.ex │ ├── types.ex │ └── utils.ex ├── mix.exs ├── mix.lock ├── priv └── abi │ ├── ccip_read.json │ ├── ens.json │ ├── ens_extended_resolver.json │ ├── ens_resolver.json │ ├── erc1155.json │ ├── erc165.json │ ├── erc20.json │ ├── erc721.json │ ├── erc777.json │ └── multicall3.json └── test ├── ethers ├── ccip_read_test.exs ├── contract_helpers_test.exs ├── counter_contract_test.exs ├── event_argument_types_contract_test.exs ├── event_mixed_index_contract_test.exs ├── event_test.exs ├── multi_arity_contract_test.exs ├── multi_clause_contract_test.exs ├── multicall_test.exs ├── name_service_test.exs ├── owner_contract_test.exs ├── pay_ether_contract_test.exs ├── registry_contract_test.exs ├── revert_contract_test.exs ├── signer │ ├── json_rpc_test.exs │ └── local_test.exs ├── transaction_test.exs ├── tx_data_test.exs ├── types_contract_test.exs ├── types_test.exs └── utils_test.exs ├── ethers_test.exs ├── support ├── contracts.ex ├── contracts │ ├── ccip_read.sol │ ├── counter.sol │ ├── event_argument_types.sol │ ├── event_mixed_index.sol │ ├── hello_world.sol │ ├── multi_arity.sol │ ├── multi_clause.sol │ ├── owner.sol │ ├── pay_ether.sol │ ├── registry.sol │ ├── revert.sol │ └── types.sol ├── test_helpers.ex └── test_rpc_module.ex ├── test_helper.exs └── test_prepare.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "mix" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Elixir CI 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | test: 19 | name: Test - Lint - Dialyze 20 | 21 | runs-on: ubuntu-latest 22 | 23 | env: 24 | MIX_ENV: test 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | strategy: 28 | matrix: 29 | variation: 30 | - otp: "27.x" 31 | elixir: "1.18" 32 | report_coverage: true 33 | - otp: "26.x" 34 | elixir: "1.18" 35 | report_coverage: false 36 | - otp: "27.x" 37 | elixir: "1.17" 38 | report_coverage: false 39 | - otp: "26.x" 40 | elixir: "1.17" 41 | report_coverage: false 42 | 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Set up Elixir 47 | uses: erlef/setup-beam@v1 48 | with: 49 | otp-version: ${{matrix.variation.otp}} 50 | elixir-version: ${{matrix.variation.elixir}} 51 | 52 | - name: Install Solidity 53 | run: | 54 | sudo add-apt-repository ppa:ethereum/ethereum 55 | sudo apt-get update 56 | sudo apt-get install solc 57 | 58 | - name: Install Foundry 59 | uses: foundry-rs/foundry-toolchain@v1 60 | 61 | - name: Restore dependencies cache 62 | uses: actions/cache@v3 63 | with: 64 | path: deps 65 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 66 | restore-keys: ${{ runner.os }}-mix- 67 | 68 | - name: Install dependencies 69 | run: mix deps.get 70 | 71 | - name: Start Anvil (Background) 72 | run: anvil & 73 | 74 | - name: Prepare for tests 75 | run: elixir test/test_prepare.exs 76 | 77 | - name: Run tests and report coverage 78 | if: ${{matrix.variation.report_coverage}} 79 | run: mix coveralls.github 80 | 81 | - name: Run tests 82 | if: ${{!matrix.variation.report_coverage}} 83 | run: mix coveralls 84 | 85 | - name: Credo 86 | run: mix credo --strict 87 | 88 | - name: Dialyzer 89 | run: mix dialyzer 90 | -------------------------------------------------------------------------------- /.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 | elixirium-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # LSP 29 | /.elixir_ls/ 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Elixir Ethers 2 | 3 | Thank you for your interest in contributing to Elixir Ethers! We welcome contributions from the community. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork the repository 8 | 2. Create a new branch for your feature or bugfix (`git checkout -b your-feature-name`) 9 | 3. Make your changes 10 | 4. Run the test suite (`mix test`) 11 | 5. Ensure code formatting is correct (`mix format`) 12 | 6. Commit your changes with a descriptive commit message 13 | 7. Push to your fork 14 | 8. Open a Pull Request 15 | 16 | ## Development Setup 17 | 18 | 1. Ensure you have Elixir installed 19 | 2. Clone the repository 20 | 3. Install and Run `anvil` (From https://getfoundry.sh/) 21 | 4. Install dependencies with `mix deps.get` 22 | 5. Prepare the tests (only needs to be ran once) with `mix run test/test_prepare.exs` 23 | 6. Run tests with `mix test` 24 | 25 | ## Pull Request Guidelines 26 | 27 | - Include tests for any new functionality 28 | - Update documentation as needed 29 | - Follow the existing code style 30 | - Keep your changes focused and atomic 31 | - Write clear commit messages 32 | 33 | ## Questions or Issues? 34 | 35 | If you have questions or run into issues, please open a GitHub issue with a clear description of the problem or question. 36 | 37 | ## License 38 | 39 | By contributing to Elixir Ethers, you agree that your contributions will be licensed under its [License](/LICENSE). 40 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | No formal support exists for versions before version 1 (major version 1). 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Security disclosures should be sent privately to alisina.bm@gmail.com. 10 | -------------------------------------------------------------------------------- /assets/ethers_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExWeb3/elixir_ethers/03575e7d2a17e8d33948c03cc4ade4f3c5b6eb5f/assets/ethers_logo.png -------------------------------------------------------------------------------- /assets/exdoc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExWeb3/elixir_ethers/03575e7d2a17e8d33948c03cc4ade4f3c5b6eb5f/assets/exdoc_logo.png -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ethereumex, url: "https://eth.llamarpc.com" 4 | 5 | import_config "#{config_env()}.exs" 6 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ethereumex, url: "http://localhost:8545" 4 | 5 | config :ethers, ignore_error_consolidation?: true 6 | 7 | config :ethers, ccip_req_opts: [plug: {Req.Test, Ethers.CcipReq}, retry: false] 8 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/ethers/contracts/", 4 | "test" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /guides/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration Guide 2 | 3 | This guide provides detailed information about configuring Ethers for your Elixir project. We'll cover all available configuration options, their purposes, and best practices for different scenarios. 4 | 5 | ## Json RPC Configuration 6 | 7 | Ethers uses Ethereumex as an Ethereum RPC client by default. A default URL can be set using 8 | the elixir config statements like the example below. 9 | 10 | You can use one of the RPC URLs for your chain/wallet of choice or try out one of them from 11 | [chainlist.org](https://chainlist.org). We recommend using a reliable RPC provider (line infura 12 | or quicknodes) for production. 13 | 14 | ```elixir 15 | # Configure the default JSON-RPC endpoint URL 16 | config :ethereumex, url: "https://..." 17 | ``` 18 | 19 | Note: If your app requires multiple RPC endpoints (e.g. multi-chain) then you need to pass in the 20 | URL for each operation via `:rpc_opts` key. (e.g. `Ethers.call(my_fn, rpc_opts: [url: "https://..."])` 21 | 22 | ## Configuration Options 23 | 24 | ### RPC Client `:rpc_client` 25 | 26 | Specifies the module responsible for making JSON-RPC calls to the Ethereum node. This module must implement 27 | `Ethers.RpcClient.Adapter` behaviour. 28 | 29 | #### Example 30 | 31 | ```elixir 32 | config :ethers, rpc_client: Ethereumex.HttpClient 33 | ``` 34 | 35 | ### Keccak Module `:keccak_module` 36 | 37 | Module for Keccak-256 hashing operations. Uses `ExKeccak` by default. 38 | 39 | #### Example 40 | 41 | ```elixir 42 | config :ethers, keccak_module: ExKeccak 43 | ``` 44 | 45 | ### JSON Module `json_module` 46 | 47 | Handles JSON encoding/decoding. Uses `Jason` by default. 48 | 49 | #### Example 50 | 51 | ```elixir 52 | config :ethers, json_module: Jason # If you prefer using Poison 53 | ``` 54 | 55 | ### Secp256k1 Module `:secp256k1_module` 56 | 57 | Handles elliptic curve operations for signing and public key operations. 58 | 59 | ```elixir 60 | config :ethers, secp256k1_module: ExSecp256k1 61 | ``` 62 | 63 | ### Default Signer `:default_signer` and `:default_signer_opts` 64 | 65 | Specifies the default module for transaction signing by default. 66 | Also use `default_signer_opts` as default signer options if needed (See example). 67 | 68 | #### Built-in Siginers 69 | 70 | - `Ethers.Signer.Local`: For local private key signing 71 | - `Ethers.Signer.JsonRPC`: For remote signing via RPC 72 | 73 | #### Example 74 | 75 | ```elixir 76 | config :ethers, 77 | default_signer: Ethers.Signer.Local, 78 | default_signer_opts: [private_key: System.fetch_env!("ETH_PRIVATE_KEY")] 79 | ``` 80 | 81 | ### Gas Margins 82 | 83 | #### Default Gas Margin `:default_gas_margin` 84 | 85 | Safety margin for gas estimation. Precision is 0.01%. Default is 11000 = 110%. 86 | 87 | This will increase the estimated gas value so transactions are less likely to run out of gas. 88 | 89 | #### Example 90 | 91 | ```elixir 92 | config :ethers, default_gas_margin: 11000 # 110% gas margin 93 | ``` 94 | 95 | #### Max Fee Per Gas Margin `:default_max_fee_per_gas_margin` 96 | 97 | Safety margin for max fee per gas in EIP-1559 transactions. Precision is 0.01%. Default is 12000 = 120%. 98 | 99 | ```elixir 100 | config :ethers, default_max_fee_per_gas_margin: 12000 # 120% of current gas price. 101 | ``` 102 | 103 | ## Best Practices 104 | 105 | 1. **Security**: 106 | 107 | - Never commit private keys or sensitive configuration 108 | - Use environment variables for sensitive values 109 | - Consider using runtime configuration for flexibility 110 | 111 | 2. **Gas Management**: 112 | 113 | - Adjust gas margins based on network conditions 114 | - Use higher margins on networks with more volatility 115 | 116 | 3. **RPC Endpoints**: 117 | 118 | - Use reliable RPC providers in production 119 | - Consider fallback RPC endpoints 120 | - Monitor RPC endpoint performance 121 | 122 | 4. **Signing**: 123 | - If possible, Use [ethers_kms](https://hexdocs.pm/ethers_kms) in production for better security 124 | - Keep private keys secure when using `Ethers.Signer.Local` 125 | 126 | ## Troubleshooting 127 | 128 | ### Common Issues 129 | 130 | - **RPC Connection Issues**: 131 | 132 | ```elixir 133 | # Verify your RPC connection 134 | config :ethereumex, 135 | url: "https://your-ethereum-node.com", 136 | http_options: [recv_timeout: 60_000] # Increase timeout if needed 137 | ``` 138 | 139 | - **Gas Estimation Failures**: 140 | Increase gas margin for complex contracts 141 | 142 | ```elixir 143 | config :ethers, default_gas_margin: 15000 # 150% 144 | ``` 145 | 146 | Or manually provide the gas estimation when sending/signing transactions. 147 | -------------------------------------------------------------------------------- /guides/typed-arguments.md: -------------------------------------------------------------------------------- 1 | # Typed Arguments 2 | 3 | Typed arguments help Ethers with determining the exact function to use when there are multiple overloads of 4 | the same function with same arity. 5 | 6 | ## Problem 7 | 8 | In solidity, contract functions (and events) can be overloaded. 9 | This means a function with the same name can be defined with different argument types and even different arities. 10 | 11 | ### Example 12 | 13 | ```solidity 14 | contract Overloaded { 15 | function transfer(uint256 amount) public pure returns (string memory) { 16 | ... 17 | } 18 | 19 | function transfer(int256 amount) public pure returns (string memory) { 20 | ... 21 | } 22 | } 23 | ``` 24 | 25 | In the above contract, the function transfer is once implemented with `uint256` and another time with `int256`. 26 | 27 | Since Elixir is dynamically typed, we need a way to specify which function we need to call in this scenario. 28 | 29 | ## Solution 30 | 31 | Ethers provides a simple helper function called `Ethers.Types.typed/2`. This function helps you with specifying the type for your parameter. It will help Ethers to know which function to select when you want to call it. 32 | 33 | Let's try it with the example contract above. If we assume we want to call the transfer function with `uint256` type, here is the code we need. 34 | 35 | ```elixir 36 | defmodule Overloaded do 37 | use Ethers.Contract, abi: ... 38 | end 39 | 40 | Overloaded.transfer(Ethers.Types.typed({:uint, 256}, 100)) 41 | |> Ethers.send_transaction!(...) 42 | ``` 43 | 44 | This way we have explicitly told Ethers to use the `uint256` type for the first argument. 45 | 46 | ## Supported Types 47 | 48 | Ethers supports all generic data types from EVM. Here is a list of them. 49 | 50 | | Elixir Type | Solidity Type | Description | 51 | | ------------------------ | ----------------- | --------------------------------------------------- | 52 | | `:address` | `address` | Ethereum wallet address | 53 | | `:bool` | `bool` | Boolean value | 54 | | `:string` | `string` | Dynamic length string | 55 | | `:bytes` | `bytes` | Dynamic length byte array [^1] | 56 | | `{:bytes, size}` | `bytes{size}` | Fixed length byte array [^1] | 57 | | `{:uint, bitsize}` | `uint{bitsize}` | Unsigned integer [^2] | 58 | | `{:int, bitsize}` | `int{bitsize}` | Signed integer [^2] | 59 | | `{:array, type}` | `T[]` | Dynamic length array of type | 60 | | `{:array, type, length}` | `T[{length}]` | Fixed length array of type | 61 | | `{:tuple, types}` | Tuples or Structs | A tuple with types (structs in solidity are tuples) | 62 | 63 | [^1]: For fixed length byte array (bytes1, bytes2, ..., bytes32) the size must be between 1 and 32. 64 | [^2]: For `int` and `uint` data types, the bitsize must be between 8 and 256 and also dividable to 8. 65 | -------------------------------------------------------------------------------- /guides/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | This guide provides information about upgrading between different versions of Ethers and handling breaking changes. 4 | 5 | ## Upgrading to 0.6.x 6 | 7 | Version 0.6.x and onwards introduce several breaking changes to improve type safety and explicitness. 8 | Here's what you need to know: 9 | 10 | ### Key Changes 11 | 12 | #### Native Elixir Types 13 | 14 | All inputs to functions now require native Elixir types 15 | 16 | Example: Use integers instead of hex strings 17 | ```elixir 18 | # Before (0.5.x) 19 | Ethers.call(ERC20.name(), gas: "0x1") 20 | 21 | # After (0.6.x) 22 | Ethers.call(ERC20.name(), gas: 1) 23 | ``` 24 | 25 | #### Explicit Gas Limits 26 | 27 | When sending transactions without a signer, the gas limit (and no other field) will not be 28 | automatically set. Only when using a signer, these values will be fetched from the network for you. 29 | 30 | ```elixir 31 | # Before (0.5.x) 32 | MyContract.my_function() |> Ethers.send_transaction() 33 | 34 | # After (0.6.x) 35 | MyContract.my_function() |> Ethers.send_transaction(gas: 100_000) 36 | ``` 37 | 38 | #### Transaction Types 39 | 40 | Transaction struct split into separate EIP-1559, EIP-4844 and EIP-2930 and Legacy types. 41 | 42 | ```elixir 43 | # Before (0.5.x) 44 | Ethers.send_transaction(tx, tx_type: :eip1559) 45 | 46 | # After (0.6.x) 47 | Ethers.send_transaction(tx, type: Ethers.Transaction.Eip1559) 48 | ``` 49 | 50 | ### Function Changes 51 | 52 | #### Transaction Sending 53 | 54 | Use `Ethers.send_transaction/2` instead of `Ethers.send/2` 55 | 56 | ```elixir 57 | # Before (0.5.x) 58 | Ethers.send(tx) 59 | 60 | # After (0.6.x) 61 | Ethers.send_transaction(tx) 62 | ``` 63 | 64 | #### Transaction Creation 65 | 66 | Use `Ethers.Transaction.from_rpc_map/1` instead of `from_map/1` 67 | 68 | ```elixir 69 | # Before (0.5.x) 70 | Ethers.Transaction.from_map(tx_map) 71 | 72 | # After (0.6.x) 73 | Ethers.Transaction.from_rpc_map(tx_map) 74 | ``` 75 | 76 | ### Migration Checklist 77 | 78 | 1. [ ] Update all function inputs to use native Elixir types 79 | 2. [ ] Add explicit gas limits to all transactions 80 | 3. [ ] Update transaction type specifications 81 | 4. [ ] Replace deprecated function calls 82 | 5. [ ] Test all contract interactions 83 | 84 | ## Upgrading from Earlier Versions 85 | 86 | For upgrades from versions prior to 0.5.x, please refer to the [CHANGELOG.md](../CHANGELOG.md) file. -------------------------------------------------------------------------------- /lib/ethers/ccip_read.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.CcipRead do 2 | @moduledoc """ 3 | CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) implementation 4 | 5 | NOTE: Currently supports URLs with "https" scheme only 6 | """ 7 | 8 | require Logger 9 | 10 | alias Ethers.Contracts.CcipRead.Errors.OffchainLookup 11 | alias Ethers.TxData 12 | alias Ethers.Utils 13 | 14 | @error_selector "0x556f1830" 15 | @error_selector_bin Utils.hex_decode!(@error_selector) 16 | @supported_schemas ["https"] 17 | 18 | @doc """ 19 | Same as `Ethers.call/2` but will handle `Ethers.Contacts.CcipRead.Errors.OffchainLookup` errors 20 | and performs an offchain lookup as per [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668) specs. 21 | 22 | ## Options and Overrides 23 | Accepts same options as `Ethers.call/2` 24 | """ 25 | @spec call(TxData.t(), Keyword.t()) :: {:ok, [term()] | term()} | {:error, term()} 26 | def call(tx_data, opts) do 27 | case Ethers.call(tx_data, opts) do 28 | {:ok, result} -> 29 | {:ok, result} 30 | 31 | {:error, %_{} = error} -> 32 | if offchain_lookup_error?(error) do 33 | ccip_resolve(error, tx_data, opts) 34 | else 35 | {:error, error} 36 | end 37 | 38 | {:error, %{"data" => <<@error_selector, _::binary>> = error_data}} -> 39 | with {:ok, decoded_error} <- Utils.hex_decode(error_data), 40 | {:ok, lookup_error} <- OffchainLookup.decode(decoded_error) do 41 | ccip_resolve(lookup_error, tx_data, opts) 42 | end 43 | 44 | {:error, reason} -> 45 | {:error, reason} 46 | end 47 | end 48 | 49 | defp ccip_resolve(error, tx_data, opts) do 50 | with {:ok, data} <- 51 | error.urls 52 | |> Enum.filter(fn url -> 53 | url |> String.downcase() |> String.starts_with?(@supported_schemas) 54 | end) 55 | |> resolve_first(error) do 56 | data = ABI.TypeEncoder.encode([data, error.extra_data], [:bytes, :bytes]) 57 | tx_data = %{tx_data | data: error.callback_function <> data} 58 | Ethers.call(tx_data, opts) 59 | end 60 | end 61 | 62 | defp resolve_first([], _), do: {:error, :ccip_read_failed} 63 | 64 | defp resolve_first([url | rest], error) do 65 | case do_resolve_single(url, error) do 66 | {:ok, data} -> 67 | {:ok, data} 68 | 69 | {:error, reason} -> 70 | Logger.error("CCIP READ: failed resolving #{url} error: #{inspect(reason)}") 71 | 72 | resolve_first(rest, error) 73 | end 74 | end 75 | 76 | defp do_resolve_single(url_template, error) do 77 | sender = Ethers.Utils.hex_encode(error.sender) 78 | data = Ethers.Utils.hex_encode(error.call_data) 79 | 80 | req_opts = 81 | if String.contains?(url_template, "{data}") do 82 | [method: :get] 83 | else 84 | [method: :post, json: %{data: data, sender: sender}] 85 | end 86 | 87 | url = url_template |> String.replace("{sender}", sender) |> String.replace("{data}", data) 88 | req_opts = req_opts |> Keyword.put(:url, url) |> Keyword.merge(ccip_req_opts()) 89 | 90 | Logger.debug("CCIP READ: trying #{url}") 91 | 92 | case Req.request(req_opts) do 93 | {:ok, %Req.Response{status: 200, body: %{"data" => data}}} -> 94 | case Utils.hex_decode(data) do 95 | {:ok, hex} -> {:ok, hex} 96 | :error -> {:error, :hex_decode_failed} 97 | end 98 | 99 | {:ok, resp} -> 100 | {:error, resp} 101 | 102 | {:error, reason} -> 103 | {:error, reason} 104 | end 105 | end 106 | 107 | defp offchain_lookup_error?(%mod{}) do 108 | mod.function_selector().method_id == @error_selector_bin 109 | rescue 110 | UndefinedFunctionError -> 111 | false 112 | end 113 | 114 | defp ccip_req_opts do 115 | Application.get_env(:ethers, :ccip_req_opts, []) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/ethers/contracts/ccip_read.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.CcipRead do 2 | @moduledoc """ 3 | CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) contract 4 | """ 5 | 6 | use Ethers.Contract, abi: :ccip_read 7 | end 8 | -------------------------------------------------------------------------------- /lib/ethers/contracts/ens.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ENS do 2 | @moduledoc """ 3 | Ethereum Name Service (ENS) Contract 4 | """ 5 | 6 | @ens_address "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" 7 | 8 | use Ethers.Contract, abi: :ens, default_address: @ens_address 9 | 10 | defmodule Resolver do 11 | @moduledoc """ 12 | Ethereum Name Service (ENS) Resolver Contract 13 | """ 14 | 15 | use Ethers.Contract, abi: :ens_resolver 16 | end 17 | 18 | defmodule ExtendedResolver do 19 | @moduledoc """ 20 | Extended ENS resolver as per [ENSIP-10](https://docs.ens.domains/ensip/10) 21 | """ 22 | 23 | use Ethers.Contract, abi: :ens_extended_resolver 24 | 25 | @behaviour Ethers.Contracts.ERC165 26 | 27 | # ERC-165 Interface ID 28 | @interface_id Ethers.Utils.hex_decode!("0x9061b923") 29 | 30 | @impl Ethers.Contracts.ERC165 31 | def erc165_interface_id, do: @interface_id 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ethers/contracts/erc1155.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ERC1155 do 2 | @moduledoc """ 3 | ERC1155 token interface 4 | 5 | More info: https://eips.ethereum.org/EIPS/eip-1155 6 | """ 7 | 8 | use Ethers.Contract, abi: :erc1155 9 | 10 | @behaviour Ethers.Contracts.ERC165 11 | 12 | # ERC-165 Interface ID 13 | @interface_id Ethers.Utils.hex_decode!("0xd9b67a26") 14 | 15 | @impl Ethers.Contracts.ERC165 16 | def erc165_interface_id, do: @interface_id 17 | end 18 | -------------------------------------------------------------------------------- /lib/ethers/contracts/erc165.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ERC165 do 2 | @moduledoc """ 3 | ERC-165 Standard Interface Detection 4 | 5 | More info: https://eips.ethereum.org/EIPS/eip-165 6 | 7 | ## Modules as Interface IDs 8 | 9 | Contract modules can opt to implement EIP-165 behaviour so that their name can be used 10 | directly with the `supports_interface/1` function in this module. See below example: 11 | 12 | ```elixir 13 | defmodule MyEIP165CompatibleContract do 14 | use Ethers.Contract, abi: ... 15 | @behaviour Ethers.Contracts.ERC165 16 | 17 | @impl true 18 | def erc165_interface_id, do: Ethers.Utils.hex_decode("[interface_id]") 19 | end 20 | ``` 21 | 22 | Now module name can be used instead of interface_id and will have the same result. 23 | 24 | ```elixir 25 | iex> Ethers.Contracts.ERC165.supports_interface("[interface_id]") == 26 | Ethers.Contracts.ERC165.supports_interface(MyEIP165CompatibleContract) 27 | true 28 | ``` 29 | """ 30 | use Ethers.Contract, abi: :erc165, skip_docs: true 31 | 32 | @behaviour __MODULE__ 33 | 34 | @callback erc165_interface_id() :: <<_::32>> 35 | 36 | @interface_id Ethers.Utils.hex_decode!("0x01ffc9a7") 37 | 38 | defmodule Errors.NotERC165CompatibleError do 39 | defexception [:message] 40 | end 41 | 42 | @impl __MODULE__ 43 | def erc165_interface_id, do: @interface_id 44 | 45 | @doc """ 46 | Prepares `supportsInterface(bytes4 interfaceId)` call parameters on the contract. 47 | 48 | This function also accepts a module that implements the ERC165 behaviour as input. Example: 49 | 50 | ```elixir 51 | iex> #{Macro.to_string(__MODULE__)}.supports_interface(Ethers.Contracts.ERC721) 52 | #Ethers.TxData 53 | ``` 54 | 55 | This function should only be called for result and never in a transaction on 56 | its own. (Use Ethers.call/2) 57 | 58 | State mutability: view 59 | 60 | ## Function Parameter Types 61 | 62 | - interfaceId: `{:bytes, 4}` 63 | 64 | ## Return Types (when called with `Ethers.call/2`) 65 | 66 | - :bool 67 | """ 68 | @spec supports_interface(<<_::32>> | atom()) :: Ethers.TxData.t() 69 | def supports_interface(module_or_interface_id) 70 | 71 | def supports_interface(module) when is_atom(module) do 72 | supports_interface(module.erc165_interface_id()) 73 | rescue 74 | UndefinedFunctionError -> 75 | reraise __MODULE__.Errors.NotERC165CompatibleError, 76 | "module #{module} does not implement ERC165 behaviour", 77 | __STACKTRACE__ 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ethers/contracts/erc20.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ERC20 do 2 | @moduledoc """ 3 | ERC20 token interface 4 | 5 | More info: https://ethereum.org/en/developers/docs/standards/tokens/erc-20/ 6 | """ 7 | 8 | use Ethers.Contract, abi: :erc20 9 | end 10 | -------------------------------------------------------------------------------- /lib/ethers/contracts/erc721.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ERC721 do 2 | @moduledoc """ 3 | ERC721 token interface 4 | 5 | More info: https://eips.ethereum.org/EIPS/eip-721 6 | """ 7 | 8 | use Ethers.Contract, abi: :erc721 9 | 10 | @behaviour Ethers.Contracts.ERC165 11 | 12 | # ERC-165 Interface ID 13 | @interface_id Ethers.Utils.hex_decode!("0x80ac58cd") 14 | 15 | @impl Ethers.Contracts.ERC165 16 | def erc165_interface_id, do: @interface_id 17 | end 18 | -------------------------------------------------------------------------------- /lib/ethers/contracts/erc777.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.ERC777 do 2 | @moduledoc """ 3 | ERC777 token interface 4 | 5 | More info: https://ethereum.org/en/developers/docs/standards/tokens/erc-777/ 6 | """ 7 | 8 | use Ethers.Contract, abi: :erc777 9 | end 10 | -------------------------------------------------------------------------------- /lib/ethers/contracts/multicall3.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contracts.Multicall3 do 2 | @moduledoc """ 3 | Multicall3 token interface 4 | 5 | More info: https://www.multicall3.com 6 | """ 7 | 8 | @multicall3_address "0xcA11bde05977b3631167028862bE2a173976CA11" 9 | 10 | use Ethers.Contract, abi: :multicall3, default_address: @multicall3_address 11 | end 12 | -------------------------------------------------------------------------------- /lib/ethers/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Error do 2 | @moduledoc false 3 | 4 | import Inspect.Algebra 5 | 6 | alias Ethers.Utils 7 | 8 | def inspect(%error_module{} = error, opts) do 9 | arguments = Enum.map(error_module.ordered_argument_keys(), &Map.fetch!(error, &1)) 10 | 11 | selector = error_module.function_selector() 12 | 13 | arguments_doc = 14 | Enum.zip([selector.types, input_names(selector), arguments]) 15 | |> Enum.map(fn {type, name, arg} -> 16 | [ 17 | color(ABI.FunctionSelector.encode_type(type), :atom, opts), 18 | " ", 19 | if(name, do: color(name, :variable, opts)), 20 | if(name, do: " "), 21 | human_arg(arg, type, opts) 22 | ] 23 | |> Enum.reject(&is_nil/1) 24 | |> concat() 25 | end) 26 | |> Enum.intersperse(concat(color(",", :operator, opts), break(" "))) 27 | 28 | arguments_doc = 29 | case arguments_doc do 30 | [] -> 31 | [ 32 | color("(", :operator, opts), 33 | color(")", :operator, opts) 34 | ] 35 | 36 | _ -> 37 | [ 38 | color("(", :operator, opts), 39 | nest(concat([break("") | arguments_doc]), 2), 40 | break(""), 41 | color(")", :operator, opts) 42 | ] 43 | end 44 | 45 | inner = 46 | concat([ 47 | break(""), 48 | color("error", :atom, opts), 49 | " ", 50 | color(selector.function, :call, opts), 51 | concat(arguments_doc) 52 | ]) 53 | 54 | concat([ 55 | color("#Ethers.Error<", :map, opts), 56 | nest(inner, 2), 57 | break(""), 58 | color(">", :map, opts) 59 | ]) 60 | end 61 | 62 | defp input_names(selector) do 63 | if Enum.count(selector.types) == Enum.count(selector.input_names) do 64 | selector.input_names 65 | else 66 | 1..Enum.count(selector.types) 67 | |> Enum.map(fn _ -> nil end) 68 | end 69 | end 70 | 71 | defp human_arg(arg, type, opts), do: Inspect.inspect(Utils.human_arg(arg, type), opts) 72 | end 73 | -------------------------------------------------------------------------------- /lib/ethers/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Event do 2 | @moduledoc """ 3 | EVM Event struct and helpers 4 | """ 5 | 6 | alias Ethers.ContractHelpers 7 | alias Ethers.{Types, Utils} 8 | alias ABI.{FunctionSelector, TypeDecoder} 9 | 10 | defstruct [ 11 | :address, 12 | :block_hash, 13 | :block_number, 14 | :data_raw, 15 | :topics, 16 | :topics_raw, 17 | :transaction_hash, 18 | :transaction_index, 19 | data: [], 20 | log_index: 0, 21 | removed: false 22 | ] 23 | 24 | @type t :: %__MODULE__{ 25 | address: Types.t_address(), 26 | block_hash: Types.t_hash(), 27 | block_number: non_neg_integer(), 28 | topics: [term(), ...], 29 | topics_raw: [String.t(), ...], 30 | transaction_hash: Types.t_hash(), 31 | transaction_index: non_neg_integer(), 32 | data_raw: String.t(), 33 | data: [term()], 34 | log_index: non_neg_integer(), 35 | removed: boolean() 36 | } 37 | 38 | @doc """ 39 | Decodes a log entry with the given Event function selector and returns an Event struct 40 | """ 41 | @spec decode(map(), ABI.FunctionSelector.t()) :: t() 42 | def decode(log, %ABI.FunctionSelector{} = selector) when is_map(log) do 43 | data = 44 | case log do 45 | %{"data" => "0x"} -> 46 | [] 47 | 48 | %{"data" => raw_data} -> 49 | data_bin = Utils.hex_decode!(raw_data) 50 | returns = ContractHelpers.event_non_indexed_types(selector) 51 | 52 | selector 53 | |> Map.put(:returns, returns) 54 | |> ABI.decode(data_bin, :output) 55 | |> Enum.zip(returns) 56 | |> Enum.map(fn {return, type} -> Utils.human_arg(return, type) end) 57 | end 58 | 59 | [_ | sub_topics_raw] = topics_raw = Map.fetch!(log, "topics") 60 | 61 | decoded_topics = 62 | sub_topics_raw 63 | |> Enum.map(&Utils.hex_decode!/1) 64 | |> Enum.zip(ContractHelpers.event_indexed_types(selector)) 65 | |> Enum.map(fn 66 | {data, :string} -> 67 | {Utils.hex_encode(data), :string} 68 | 69 | {data, type} -> 70 | [decoded] = TypeDecoder.decode_raw(data, [type]) 71 | {decoded, type} 72 | end) 73 | |> Enum.map(fn {data, type} -> Utils.human_arg(data, type) end) 74 | 75 | topics = [FunctionSelector.encode(selector) | decoded_topics] 76 | 77 | {:ok, block_number} = Utils.hex_to_integer(Map.fetch!(log, "blockNumber")) 78 | {:ok, log_index} = Utils.hex_to_integer(Map.fetch!(log, "logIndex")) 79 | {:ok, transaction_index} = Utils.hex_to_integer(Map.fetch!(log, "transactionIndex")) 80 | 81 | %__MODULE__{ 82 | address: Map.fetch!(log, "address"), 83 | block_hash: Map.fetch!(log, "blockHash"), 84 | block_number: block_number, 85 | data: data, 86 | data_raw: Map.fetch!(log, "data"), 87 | log_index: log_index, 88 | removed: Map.fetch!(log, "removed"), 89 | topics: topics, 90 | topics_raw: topics_raw, 91 | transaction_hash: Map.fetch!(log, "transactionHash"), 92 | transaction_index: transaction_index 93 | } 94 | end 95 | 96 | @doc """ 97 | Given a log entry and an EventFilters module (e.g. `Ethers.Contracts.ERC20.EventFilters`) 98 | will find a matching event filter in the given module and decodes the log using the event selector. 99 | """ 100 | @spec find_and_decode(map(), module()) :: {:ok, t()} | {:error, :not_found} 101 | def find_and_decode(log, event_filters_module) do 102 | [topic | _] = Map.fetch!(log, "topics") 103 | 104 | topic_raw = Utils.hex_decode!(topic) 105 | 106 | selector = 107 | Enum.find( 108 | event_filters_module.__events__(), 109 | fn %ABI.FunctionSelector{method_id: method_id} -> method_id == topic_raw end 110 | ) 111 | 112 | case selector do 113 | nil -> 114 | {:error, :not_found} 115 | 116 | %ABI.FunctionSelector{} -> 117 | {:ok, decode(log, selector)} 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/ethers/event_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.EventFilter do 2 | @moduledoc """ 3 | Event Filter struct and helper functions to work with the event filters 4 | """ 5 | 6 | @typedoc """ 7 | Holds event filter topics, the event selector and the default address. 8 | 9 | Can be passed in to `Ethers.get_logs/2` filter and fetch the logs. 10 | """ 11 | @type t :: %__MODULE__{ 12 | topics: [binary()], 13 | selector: ABI.FunctionSelector.t(), 14 | default_address: nil | Ethers.Types.t_address() 15 | } 16 | 17 | @enforce_keys [:topics, :selector] 18 | defstruct [:topics, :selector, :default_address] 19 | 20 | @doc false 21 | @spec new([binary()], ABI.FunctionSelector.t(), Ethers.Types.t_address() | nil) :: t() 22 | def new(topics, selector, default_address) do 23 | %__MODULE__{ 24 | topics: topics, 25 | selector: selector, 26 | default_address: default_address 27 | } 28 | end 29 | 30 | @doc false 31 | @spec to_map(t() | map(), Keyword.t()) :: map() 32 | def to_map(%__MODULE__{} = tx_data, overrides) do 33 | tx_data 34 | |> event_filter_map() 35 | |> to_map(overrides) 36 | end 37 | 38 | def to_map(event_filter, overrides) do 39 | Enum.into(overrides, event_filter) 40 | end 41 | 42 | defp event_filter_map(%{selector: %{type: :event}} = event_filter) do 43 | %{topics: event_filter.topics} 44 | |> maybe_add_address(event_filter.default_address) 45 | end 46 | 47 | defp maybe_add_address(tx_map, nil), do: tx_map 48 | defp maybe_add_address(tx_map, address), do: Map.put(tx_map, :address, address) 49 | 50 | defimpl Inspect do 51 | import Inspect.Algebra 52 | 53 | alias Ethers.Utils 54 | 55 | def inspect( 56 | %{selector: selector, topics: [_t0 | topics], default_address: default_address}, 57 | opts 58 | ) do 59 | default_address = 60 | case default_address do 61 | nil -> 62 | [] 63 | 64 | _ -> 65 | [ 66 | line(), 67 | color("default_address: ", :default, opts), 68 | color(inspect(default_address), :string, opts) 69 | ] 70 | end 71 | 72 | argument_doc = 73 | case argument_doc(selector, topics, opts) do 74 | [] -> 75 | [ 76 | color("(", :operator, opts), 77 | color(")", :operator, opts) 78 | ] 79 | 80 | argument_doc -> 81 | [ 82 | color("(", :operator, opts), 83 | nest(concat([break("") | argument_doc]), 2), 84 | break(""), 85 | color(")", :operator, opts) 86 | ] 87 | end 88 | 89 | inner = 90 | concat( 91 | [ 92 | break(""), 93 | color("event", :atom, opts), 94 | " ", 95 | color(selector.function, :call, opts) 96 | ] ++ argument_doc ++ default_address 97 | ) 98 | 99 | concat([ 100 | color("#Ethers.EventFilter<", :map, opts), 101 | nest(inner, 2), 102 | break(""), 103 | color(">", :map, opts) 104 | ]) 105 | end 106 | 107 | defp input_names(selector) do 108 | if Enum.count(selector.types) == Enum.count(selector.input_names) do 109 | selector.input_names 110 | else 111 | 1..Enum.count(selector.types) 112 | |> Enum.map(fn _ -> nil end) 113 | end 114 | end 115 | 116 | defp argument_doc(selector, topics, opts), 117 | do: 118 | argument_doc( 119 | selector.types, 120 | input_names(selector), 121 | selector.inputs_indexed, 122 | topics, 123 | [], 124 | opts 125 | ) 126 | 127 | defp argument_doc(types, input_names, inputs_indexed, topics, acc, opts) 128 | 129 | defp argument_doc([], _, _, _, acc, opts) do 130 | Enum.reverse(acc) 131 | |> Enum.intersperse(concat(color(",", :operator, opts), break(" "))) 132 | end 133 | 134 | defp argument_doc( 135 | [type | types], 136 | [name | input_names], 137 | [true | inputs_indexed], 138 | [topic | topics], 139 | acc, 140 | opts 141 | ) do 142 | doc = 143 | [ 144 | color(ABI.FunctionSelector.encode_type(type), :atom, opts), 145 | " ", 146 | color("indexed", nil, opts), 147 | if(name, do: " "), 148 | if(name, do: color(name, :variable, opts)), 149 | " ", 150 | if(is_nil(topic), do: color("any", :string, opts), else: human_topic(type, topic)) 151 | ] 152 | |> Enum.reject(&is_nil/1) 153 | |> concat() 154 | 155 | argument_doc(types, input_names, inputs_indexed, topics, [doc | acc], opts) 156 | end 157 | 158 | defp argument_doc( 159 | [type | types], 160 | [name | input_names], 161 | [false | inputs_indexed], 162 | topics, 163 | acc, 164 | opts 165 | ) do 166 | doc = 167 | [ 168 | color(ABI.FunctionSelector.encode_type(type), :atom, opts), 169 | if(name, do: " "), 170 | if(name, do: color(name, :variable, opts)) 171 | ] 172 | |> Enum.reject(&is_nil/1) 173 | |> concat() 174 | 175 | argument_doc(types, input_names, inputs_indexed, topics, [doc | acc], opts) 176 | end 177 | 178 | defp human_topic(type, topic) do 179 | hashed? = 180 | case type do 181 | type when type in [:string, :bytes] -> true 182 | {:array, _} -> true 183 | {:array, _, _} -> true 184 | {:tuple, _} -> true 185 | _ -> false 186 | end 187 | 188 | if hashed? do 189 | "(hashed) #{inspect(topic)}" 190 | else 191 | [value] = 192 | Utils.hex_decode!(topic) 193 | |> ABI.TypeDecoder.decode([type]) 194 | 195 | inspect(Utils.human_arg(value, type)) 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /lib/ethers/execution_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.ExecutionError do 2 | @moduledoc """ 3 | Execution Error Exception. 4 | 5 | The Exception struct will have these values: 6 | 7 | - `message`: Human readable error message 8 | - `evm_error`: A custom error from the contract. (An Error Struct) 9 | """ 10 | 11 | defexception [:message, :evm_error] 12 | 13 | @impl true 14 | def exception(%{"code" => _} = evm_error) do 15 | %__MODULE__{ 16 | message: Map.get(evm_error, "message", "unknown error!"), 17 | evm_error: evm_error 18 | } 19 | end 20 | 21 | def exception(error) when is_exception(error), do: error 22 | 23 | def exception(error) when is_struct(error) do 24 | %__MODULE__{message: inspect(error), evm_error: error} 25 | end 26 | 27 | def exception(error) do 28 | %__MODULE__{message: "Unexpected error: #{maybe_inspect_error(error)}"} 29 | end 30 | 31 | defp maybe_inspect_error(error) when is_atom(error), do: Atom.to_string(error) 32 | defp maybe_inspect_error(error), do: inspect(error) 33 | end 34 | -------------------------------------------------------------------------------- /lib/ethers/name_service.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.NameService do 2 | @moduledoc """ 3 | Name Service resolution implementation for ENS (Ethereum Name Service). 4 | Supports both forward and reverse resolution plus reverse lookups. 5 | 6 | This module implements [Cross Chain / Offchain Resolvers](https://docs.ens.domains/resolvers/ccip-read) 7 | (is CCIP-Read aware), allowing it to resolve names that are stored: 8 | - On-chain (traditional L1 ENS resolution on Ethereum) 9 | - Off-chain (via CCIP-Read gateway servers) 10 | - Cross-chain (on other L2s and EVM-compatible blockchains) 11 | 12 | The resolution process automatically handles these different scenarios transparently, 13 | following the ENS standards for name resolution including ENSIP-10 and ENSIP-11. 14 | """ 15 | 16 | import Ethers, only: [keccak_module: 0] 17 | 18 | alias Ethers.CcipRead 19 | alias Ethers.Contracts.ENS 20 | alias Ethers.Contracts.ERC165 21 | 22 | @zero_address Ethers.Types.default(:address) 23 | 24 | @doc """ 25 | Resolves a name on blockchain. 26 | 27 | ## Parameters 28 | - name: Domain name to resolve. (Example: `foo.eth`) 29 | - opts: Resolve options. 30 | - resolve_call: TxData for resolution (Defaults to 31 | `Ethers.Contracts.ENS.Resolver.addr(Ethers.NameService.name_hash(name))`) 32 | - to: Resolver contract address. Defaults to ENS 33 | - Accepts all other Execution options from `Ethers.call/2`. 34 | 35 | ## Examples 36 | 37 | ```elixir 38 | Ethers.NameService.resolve("vitalik.eth") 39 | {:ok, "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"} 40 | ``` 41 | """ 42 | @spec resolve(String.t(), Keyword.t()) :: 43 | {:ok, Ethers.Types.t_address()} 44 | | {:error, :domain_not_found | :record_not_found | term()} 45 | def resolve(name, opts \\ []) do 46 | with {:ok, resolver} <- get_last_resolver(name, opts) do 47 | do_resolve(resolver, name, opts) 48 | end 49 | end 50 | 51 | defp do_resolve(resolver, name, opts) do 52 | {resolve_call, opts} = 53 | Keyword.pop_lazy(opts, :resolve_call, fn -> 54 | name 55 | |> name_hash() 56 | |> ENS.Resolver.addr() 57 | end) 58 | 59 | case supports_extended_resolver(resolver, opts) do 60 | {:ok, true} -> 61 | # ENSIP-10 support 62 | opts = Keyword.put(opts, :to, resolver) 63 | 64 | resolve_call 65 | |> ensip10_resolve(name, opts) 66 | |> handle_result() 67 | 68 | {:ok, false} -> 69 | opts = Keyword.put(opts, :to, resolver) 70 | 71 | resolve_call 72 | |> Ethers.call(opts) 73 | |> handle_result() 74 | 75 | {:error, reason} -> 76 | {:error, reason} 77 | end 78 | end 79 | 80 | defp handle_result(result) do 81 | case result do 82 | {:ok, @zero_address} -> {:error, :record_not_found} 83 | {:ok, address} -> {:ok, address} 84 | {:error, reason} -> {:error, reason} 85 | end 86 | end 87 | 88 | defp ensip10_resolve(resolve_call, name, opts) do 89 | resolve_call_data = resolve_call.data 90 | dns_encoded_name = dns_encode(name) 91 | wildcard_call = ENS.ExtendedResolver.resolve(dns_encoded_name, resolve_call_data) 92 | 93 | with {:ok, result} <- CcipRead.call(wildcard_call, opts) do 94 | Ethers.TxData.abi_decode(result, resolve_call) 95 | end 96 | end 97 | 98 | defp supports_extended_resolver(resolver, opts) do 99 | opts = Keyword.put(opts, :to, resolver) 100 | 101 | call = ERC165.supports_interface(ENS.ExtendedResolver) 102 | 103 | Ethers.call(call, opts) 104 | end 105 | 106 | @doc """ 107 | Same as `resolve/2` but raises on errors. 108 | 109 | ## Examples 110 | 111 | ```elixir 112 | Ethers.NameService.resolve!("vitalik.eth") 113 | "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" 114 | ``` 115 | """ 116 | @spec resolve!(String.t(), Keyword.t()) :: Ethers.Types.t_address() | no_return() 117 | def resolve!(name, opts \\ []) do 118 | case resolve(name, opts) do 119 | {:ok, addr} -> addr 120 | {:error, reason} -> raise "Name Resolution failed: #{inspect(reason)}" 121 | end 122 | end 123 | 124 | @doc """ 125 | Resolves an address to a name on blockchain. 126 | 127 | ## Parameters 128 | - address: Address to resolve. 129 | - opts: Resolve options. 130 | - to: Resolver contract address. Defaults to ENS. 131 | - chain_id: Chain ID of the target chain Defaults to `1`. 132 | - Accepts all other Execution options from `Ethers.call/2`. 133 | 134 | ## Examples 135 | 136 | ```elixir 137 | Ethers.NameService.reverse_resolve("0xd8da6bf26964af9d7eed9e03e53415d37aa96045") 138 | {:ok, "vitalik.eth"} 139 | ``` 140 | """ 141 | @spec reverse_resolve(Ethers.Types.t_address(), Keyword.t()) :: 142 | {:ok, String.t()} 143 | | {:error, :domain_not_found | :invalid_name | :forward_resolution_mismatch | term()} 144 | def reverse_resolve(address, opts \\ []) do 145 | address = String.downcase(address) 146 | chain_id = Keyword.get(opts, :chain_id, 1) 147 | 148 | {reverse_name, coin_type} = get_reverse_name(address, chain_id) 149 | name_hash = name_hash(reverse_name) 150 | 151 | with {:ok, resolver} <- get_resolver(name_hash, opts), 152 | {:ok, name} <- resolve_name(resolver, name_hash, opts), 153 | # Return early if no name found and we're not on default 154 | {:ok, name} <- handle_empty_name(name, coin_type, address, opts), 155 | # Verify forward resolution matches 156 | :ok <- verify_forward_resolution(name, address, opts) do 157 | {:ok, name} 158 | end 159 | end 160 | 161 | defp get_reverse_name("0x" <> address, 1), do: {"#{address}.addr.reverse", 60} 162 | 163 | defp get_reverse_name("0x" <> address, chain_id) do 164 | # ENSIP-11: coinType = 0x80000000 | chainId 165 | coin_type = Bitwise.bor(0x80000000, chain_id) 166 | coin_type_hex = Integer.to_string(coin_type, 16) 167 | {"#{address}.#{coin_type_hex}.reverse", coin_type} 168 | end 169 | 170 | defp handle_empty_name("", coin_type, address, opts) when coin_type != 0 do 171 | "0x" <> address = address 172 | # Try default reverse name 173 | reverse_name = "#{address}.default.reverse" 174 | name_hash = name_hash(reverse_name) 175 | 176 | case get_resolver(name_hash, []) do 177 | {:ok, resolver} -> resolve_name(resolver, name_hash, opts) 178 | {:error, reason} -> {:error, reason} 179 | end 180 | end 181 | 182 | defp handle_empty_name(name, _coin_type, _address_hash, _opts), do: {:ok, name} 183 | 184 | defp verify_forward_resolution(name, address, opts) do 185 | with {:ok, resolved_addr} <- resolve(name, opts) do 186 | if String.downcase(resolved_addr) == String.downcase(address) do 187 | :ok 188 | else 189 | {:error, :forward_resolution_mismatch} 190 | end 191 | end 192 | end 193 | 194 | @doc """ 195 | Same as `reverse_resolve/2` but raises on errors. 196 | 197 | ## Examples 198 | 199 | ```elixir 200 | Ethers.NameService.reverse_resolve!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045") 201 | "vitalik.eth" 202 | ``` 203 | """ 204 | @spec reverse_resolve!(Ethers.Types.t_address(), Keyword.t()) :: String.t() | no_return() 205 | def reverse_resolve!(address, opts \\ []) do 206 | case reverse_resolve(address, opts) do 207 | {:ok, name} -> name 208 | {:error, reason} -> raise "Reverse Name Resolution failed: #{inspect(reason)}" 209 | end 210 | end 211 | 212 | @doc """ 213 | Implementation of namehash function in Elixir. 214 | 215 | See https://docs.ens.domains/contract-api-reference/name-processing 216 | 217 | ## Examples 218 | 219 | iex> Ethers.NameService.name_hash("foo.eth") 220 | Ethers.Utils.hex_decode!("0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f") 221 | 222 | iex> Ethers.NameService.name_hash("alisina.eth") 223 | Ethers.Utils.hex_decode!("0x1b557b3901bef3a986febf001c3b19370b34064b130d49ea967bf150f6d23dfe") 224 | """ 225 | @spec name_hash(String.t()) :: <<_::256>> 226 | def name_hash(name) do 227 | name 228 | |> normalize_dns_name() 229 | |> String.split(".") 230 | |> do_name_hash() 231 | end 232 | 233 | defp do_name_hash([label | rest]) do 234 | keccak_module().hash_256(do_name_hash(rest) <> keccak_module().hash_256(label)) 235 | end 236 | 237 | defp do_name_hash([]), do: <<0::256>> 238 | 239 | defp get_last_resolver(name, opts) do 240 | # HACK: get all resolvers at once using Multicall 241 | name 242 | |> name_hash() 243 | |> ENS.resolver() 244 | |> Ethers.call(opts) 245 | |> case do 246 | {:ok, @zero_address} -> 247 | parent = get_name_parent(name) 248 | 249 | if parent != name do 250 | get_last_resolver(parent, opts) 251 | else 252 | :error 253 | end 254 | 255 | {:ok, resolver} -> 256 | {:ok, resolver} 257 | 258 | {:error, reason} -> 259 | {:error, reason} 260 | end 261 | end 262 | 263 | defp get_resolver(name_hash, opts) do 264 | params = ENS.resolver(name_hash) 265 | 266 | case Ethers.call(params, opts) do 267 | {:ok, @zero_address} -> {:error, :domain_not_found} 268 | {:ok, resolver} -> {:ok, resolver} 269 | {:error, reason} -> {:error, reason} 270 | end 271 | end 272 | 273 | defp resolve_name(resolver, name_hash, opts) do 274 | opts = Keyword.put(opts, :to, resolver) 275 | 276 | name_hash 277 | |> ENS.Resolver.name() 278 | |> Ethers.call(opts) 279 | end 280 | 281 | defp normalize_dns_name(name) do 282 | name 283 | |> String.to_charlist() 284 | |> :idna.encode(transitional: false, std3_rules: true, uts46: true) 285 | |> to_string() 286 | end 287 | 288 | # Encodes a DNS name according to section 3.1 of RFC1035. 289 | defp dns_encode(name) when is_binary(name) do 290 | name 291 | |> normalize_dns_name() 292 | |> to_fqdn() 293 | |> String.split(".") 294 | |> encode_labels() 295 | end 296 | 297 | defp to_fqdn(dns_name) do 298 | if String.ends_with?(dns_name, ".") do 299 | dns_name 300 | else 301 | dns_name <> "." 302 | end 303 | end 304 | 305 | defp encode_labels(labels) do 306 | labels 307 | |> Enum.reduce(<<>>, fn label, acc -> 308 | label_length = byte_size(label) 309 | acc <> <> <> label 310 | end) 311 | end 312 | 313 | defp get_name_parent(name) do 314 | case String.split(name, ".", parts: 2) do 315 | [_, parent] -> parent 316 | [tld] -> tld 317 | end 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /lib/ethers/rpc_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.RpcClient do 2 | @moduledoc false 3 | 4 | @doc false 5 | @spec rpc_client() :: atom() 6 | def rpc_client do 7 | case Application.get_env(:ethers, :rpc_client, Ethereumex.HttpClient) do 8 | Ethereumex.HttpClient -> Ethers.RpcClient.EthereumexHttpClient 9 | module when is_atom(module) -> module 10 | _ -> raise ArgumentError, "Invalid ethers configuration. :rpc_client must be a module" 11 | end 12 | end 13 | 14 | @doc false 15 | @spec get_rpc_client(Keyword.t()) :: {atom(), Keyword.t()} 16 | def get_rpc_client(opts) do 17 | module = 18 | case Keyword.fetch(opts, :rpc_client) do 19 | {:ok, module} when is_atom(module) -> module 20 | :error -> Ethers.rpc_client() 21 | end 22 | 23 | {module, Keyword.get(opts, :rpc_opts, [])} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/ethers/rpc_client/adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.RpcClient.Adapter do 2 | @moduledoc false 3 | 4 | @type error :: {:error, map() | binary() | atom()} 5 | 6 | @callback batch_request([{atom(), list(boolean() | binary())}], keyword()) :: 7 | {:ok, [any()]} | error 8 | 9 | @callback eth_block_number(keyword()) :: {:ok, binary()} | error() 10 | 11 | @callback eth_call(map(), binary(), keyword()) :: {:ok, binary()} | error() 12 | 13 | @callback eth_chain_id(keyword()) :: {:ok, binary()} | error() 14 | 15 | @callback eth_estimate_gas(map(), keyword()) :: {:ok, binary()} | error() 16 | 17 | @callback eth_gas_price(keyword()) :: {:ok, binary()} | error() 18 | 19 | @callback eth_get_balance(binary(), binary(), keyword()) :: {:ok, binary()} | error() 20 | 21 | @callback eth_get_block_by_number(binary() | non_neg_integer(), boolean(), keyword()) :: 22 | {:ok, map()} | error() 23 | 24 | @callback eth_get_transaction_by_hash(binary(), keyword()) :: {:ok, map()} | error() 25 | 26 | @callback eth_get_transaction_count(binary(), binary(), keyword()) :: {:ok, binary()} | error() 27 | 28 | @callback eth_get_transaction_receipt(binary(), keyword()) :: {:ok, map()} | error() 29 | 30 | @callback eth_max_priority_fee_per_gas(keyword()) :: {:ok, binary()} | error() 31 | 32 | @callback eth_blob_base_fee(keyword()) :: {:ok, binary()} | error() 33 | 34 | @callback eth_get_logs(map(), keyword()) :: {:ok, [binary()] | [map()]} | error() 35 | 36 | @callback eth_send_transaction(map(), keyword()) :: {:ok, binary()} | error() 37 | 38 | @callback eth_send_raw_transaction(binary(), keyword()) :: {:ok, binary()} | error() 39 | end 40 | -------------------------------------------------------------------------------- /lib/ethers/rpc_client/ethereumex_http_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.RpcClient.EthereumexHttpClient do 2 | @moduledoc false 3 | 4 | alias Ethers.RpcClient.Adapter 5 | 6 | @behaviour Ethers.RpcClient.Adapter 7 | 8 | @exclude_delegation [:eth_get_logs] 9 | 10 | for {func, arity} <- Adapter.behaviour_info(:callbacks), func not in @exclude_delegation do 11 | args = Macro.generate_arguments(arity - 1, __MODULE__) 12 | 13 | @impl true 14 | def unquote(func)(unquote_splicing(args), opts \\ []) do 15 | apply(Ethereumex.HttpClient, unquote(func), [unquote_splicing(args), opts]) 16 | end 17 | end 18 | 19 | @impl true 20 | def eth_get_logs(params, opts \\ []) do 21 | params 22 | |> replace_key(:from_block, :fromBlock) 23 | |> replace_key(:to_block, :toBlock) 24 | |> Ethereumex.HttpClient.eth_get_logs(opts) 25 | end 26 | 27 | defp replace_key(map, ethers_key, ethereumex_key) do 28 | case Map.fetch(map, ethers_key) do 29 | {:ok, value} -> 30 | map 31 | |> Map.put(ethereumex_key, value) 32 | |> Map.delete(ethers_key) 33 | 34 | :error -> 35 | map 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/ethers/signer.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Signer do 2 | @moduledoc """ 3 | Signer behaviour. 4 | 5 | A signer module is (at least) capable of signing transactions and listing accounts in the signer. 6 | 7 | ## Builtin Signers 8 | Ethers ships with some default signers that you can use. 9 | 10 | - `Ethers.Signer.JsonRPC`: Can be used with most wallets, geth, web3signer or any other platform 11 | which exposes a JsonRPC endpoint and implements `eth_signTransaction` and `eth_accounts` 12 | functions. 13 | - `Ethers.Signer.Local`: This signs transactions locally but is highly discouraged to use in 14 | a production environment as it does not have any security measures built in. 15 | 16 | ## Custom Signers 17 | Custom signers can also be implemented which must adhere to this behvaviour. 18 | 19 | For signing transactions in custom signers the functions in `Ethers.Transaction` module might 20 | become handy. Check out the source code of built in signers for in depth info. 21 | 22 | ## Globally Default Signer 23 | If desired, a signer can be configured to be used for all operations in Ethers using elixir 24 | config. 25 | 26 | ```elixir 27 | config :ethers, 28 | default_signer: Ethers.Signer.JsonRPC, 29 | default_signer_opts: [url: "..."] 30 | ``` 31 | """ 32 | 33 | alias Ethers.Types 34 | 35 | @doc """ 36 | Signs a binary and returns the signature 37 | 38 | ## Parameters 39 | - tx: The transaction object. (An `Ethers.Transaction` struct) 40 | - opts: Other options passed to the signer as `signer_opts`. 41 | """ 42 | @callback sign_transaction( 43 | tx :: Ethers.Transaction.t_payload(), 44 | opts :: Keyword.t() 45 | ) :: 46 | {:ok, encoded_signed_transaction :: binary()} | {:error, reason :: term()} 47 | 48 | @doc """ 49 | Returns the available signer account addresses. 50 | 51 | This method might not be supported by all signers. If a signer does not support this function 52 | it should return `{:error, :not_supported}`. 53 | 54 | ## Parameters 55 | - opts: Other options passed to the signer as `signer_opts` 56 | """ 57 | @callback accounts(opts :: Keyword.t()) :: 58 | {:ok, [Types.t_address()]} | {:error, reason :: :not_supported | term()} 59 | end 60 | -------------------------------------------------------------------------------- /lib/ethers/signer/json_rpc.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Signer.JsonRPC do 2 | @moduledoc """ 3 | Signer capable of signing transactions with a JSON RPC server 4 | capable of `eth_signTransaction` and `eth_accounts` RPC functions. 5 | 6 | ## Signer Options 7 | 8 | - `:rpc_module`: The RPC Module to use. (Optional, Defaults to `Ethereumex.HttpClient`) 9 | - `:url`: The RPC URL to use. (Optional) 10 | 11 | ** All other options will be passed to the RPC module `request/3` function in 12 | the third argument ** 13 | """ 14 | 15 | @behaviour Ethers.Signer 16 | 17 | alias Ethers.Transaction 18 | 19 | @impl true 20 | def sign_transaction(tx, opts) do 21 | tx_map = Transaction.to_rpc_map(tx) 22 | 23 | tx_map = 24 | if from = Keyword.get(opts, :from) do 25 | Map.put_new(tx_map, :from, from) 26 | else 27 | tx_map 28 | end 29 | 30 | {rpc_module, opts} = Keyword.pop(opts, :rpc_module, Ethereumex.HttpClient) 31 | 32 | rpc_module.request("eth_signTransaction", [tx_map], opts) 33 | end 34 | 35 | @impl true 36 | def accounts(opts) do 37 | {rpc_module, opts} = Keyword.pop(opts, :rpc_module, Ethereumex.HttpClient) 38 | 39 | rpc_module.request("eth_accounts", [], opts) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ethers/signer/local.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Signer.Local do 2 | @moduledoc """ 3 | Local signer works with a private key. 4 | 5 | IMPORTANT: Please note that using this signer is discouraged in production 6 | environment since handling private keys in those environment can be challenging 7 | if you don't know what you are doing. 8 | 9 | ## Signer Options 10 | 11 | - `:private_key`: The private key to use for signing and calculating account address. 12 | Private key can either be in binary format (32 bytes) or it's hex encoded format with or 13 | without `0x` prefix. 14 | """ 15 | 16 | @behaviour Ethers.Signer 17 | 18 | import Ethers, only: [secp256k1_module: 0, keccak_module: 0] 19 | 20 | alias Ethers.Transaction 21 | alias Ethers.Transaction.Signed 22 | alias Ethers.Utils 23 | 24 | if not Code.ensure_loaded?(secp256k1_module()) do 25 | @impl true 26 | def sign_transaction(_tx, _opts), do: {:error, :secp256k1_module_not_loaded} 27 | 28 | @impl true 29 | def accounts(_opts), do: {:error, :secp256k1_module_not_loaded} 30 | end 31 | 32 | @impl true 33 | def sign_transaction(transaction, opts) do 34 | with {:ok, private_key} <- private_key(opts), 35 | :ok <- validate_private_key(private_key, Keyword.get(opts, :from)), 36 | encoded = Transaction.encode(transaction), 37 | sign_hash = keccak_module().hash_256(encoded), 38 | {:ok, {r, s, recovery_id}} <- secp256k1_module().sign(sign_hash, private_key) do 39 | signed_transaction = 40 | %Signed{ 41 | payload: transaction, 42 | signature_r: r, 43 | signature_s: s, 44 | signature_y_parity_or_v: Signed.calculate_y_parity_or_v(transaction, recovery_id) 45 | } 46 | 47 | encoded_signed_transaction = Transaction.encode(signed_transaction) 48 | 49 | {:ok, Utils.hex_encode(encoded_signed_transaction)} 50 | end 51 | end 52 | 53 | @impl true 54 | def accounts(opts) do 55 | with {:ok, private_key} <- private_key(opts), 56 | {:ok, address} <- do_get_address(private_key) do 57 | {:ok, [address]} 58 | end 59 | end 60 | 61 | defp do_get_address(private_key) do 62 | with {:ok, pub} <- priv_to_pub(private_key) do 63 | {:ok, Utils.public_key_to_address(pub)} 64 | end 65 | end 66 | 67 | defp validate_private_key(_private_key, nil), do: :ok 68 | 69 | defp validate_private_key(private_key, address) do 70 | with {:ok, private_key_address} <- do_get_address(private_key) do 71 | private_key_address_bin = Utils.decode_address!(private_key_address) 72 | address_bin = Utils.decode_address!(address) 73 | 74 | if address_bin == private_key_address_bin do 75 | :ok 76 | else 77 | {:error, :wrong_key} 78 | end 79 | end 80 | end 81 | 82 | defp priv_to_pub(private_key), do: secp256k1_module().create_public_key(private_key) 83 | 84 | defp private_key(opts) do 85 | case Keyword.get(opts, :private_key) do 86 | <> -> {:ok, key} 87 | <<"0x", _::binary-64>> = key -> Ethers.Utils.hex_decode(key) 88 | <> -> Ethers.Utils.hex_decode(key) 89 | nil -> {:error, :no_private_key} 90 | _ -> {:error, :invalid_private_key} 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/ethers/transaction/eip1559.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Eip1559 do 2 | @moduledoc """ 3 | Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 1559 4 | transactions. EIP-1559 introduced a new fee market mechanism with base fee and priority fee. 5 | 6 | See: https://eips.ethereum.org/EIPS/eip-1559 7 | """ 8 | 9 | import Ethers.Transaction.Helpers 10 | 11 | alias Ethers.Types 12 | alias Ethers.Utils 13 | 14 | @behaviour Ethers.Transaction 15 | 16 | @type_id 2 17 | 18 | @enforce_keys [:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas] 19 | defstruct [ 20 | :chain_id, 21 | :nonce, 22 | :max_priority_fee_per_gas, 23 | :max_fee_per_gas, 24 | :gas, 25 | :to, 26 | :value, 27 | :input, 28 | access_list: [] 29 | ] 30 | 31 | @typedoc """ 32 | A transaction type following EIP-1559 (Type-2) and incorporating the following fields: 33 | - `chain_id` - chain ID of network where the transaction is to be executed 34 | - `nonce` - sequence number for the transaction from this sender 35 | - `max_priority_fee_per_gas` - maximum fee per gas (in wei) to give to validators as priority fee (introduced in EIP-1559) 36 | - `max_fee_per_gas` - maximum total fee per gas (in wei) willing to pay (introduced in EIP-1559) 37 | - `gas` - maximum amount of gas allowed for transaction execution 38 | - `to` - destination address for transaction, nil for contract creation 39 | - `value` - amount of ether (in wei) to transfer 40 | - `input` - data payload of the transaction 41 | - `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930) 42 | """ 43 | @type t :: %__MODULE__{ 44 | chain_id: non_neg_integer(), 45 | nonce: non_neg_integer(), 46 | max_priority_fee_per_gas: non_neg_integer(), 47 | max_fee_per_gas: non_neg_integer(), 48 | gas: non_neg_integer(), 49 | to: Types.t_address() | nil, 50 | value: non_neg_integer(), 51 | input: binary(), 52 | access_list: [{binary(), [binary()]}] 53 | } 54 | 55 | @impl Ethers.Transaction 56 | def new(params) do 57 | to = params[:to] 58 | input = params[:input] || params[:data] || "" 59 | value = params[:value] || 0 60 | 61 | with :ok <- validate_non_neg_integer(params.chain_id), 62 | :ok <- validate_non_neg_integer(params.nonce), 63 | :ok <- validate_non_neg_integer(params.max_priority_fee_per_gas), 64 | :ok <- validate_non_neg_integer(params.max_fee_per_gas), 65 | :ok <- validate_non_neg_integer(params.gas), 66 | :ok <- validate_non_neg_integer(value), 67 | :ok <- validate_binary(input) do 68 | {:ok, 69 | %__MODULE__{ 70 | chain_id: params.chain_id, 71 | nonce: params.nonce, 72 | max_priority_fee_per_gas: params.max_priority_fee_per_gas, 73 | max_fee_per_gas: params.max_fee_per_gas, 74 | gas: params.gas, 75 | to: to && Utils.to_checksum_address(to), 76 | value: value, 77 | input: input, 78 | access_list: params[:access_list] || [] 79 | }} 80 | end 81 | end 82 | 83 | @impl Ethers.Transaction 84 | def auto_fetchable_fields do 85 | [:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas] 86 | end 87 | 88 | @impl Ethers.Transaction 89 | def type_envelope, do: <> 90 | 91 | @impl Ethers.Transaction 92 | def type_id, do: @type_id 93 | 94 | @impl Ethers.Transaction 95 | def from_rlp_list([ 96 | chain_id, 97 | nonce, 98 | max_priority_fee_per_gas, 99 | max_fee_per_gas, 100 | gas, 101 | to, 102 | value, 103 | input, 104 | access_list | rest 105 | ]) do 106 | {:ok, 107 | %__MODULE__{ 108 | chain_id: :binary.decode_unsigned(chain_id), 109 | nonce: :binary.decode_unsigned(nonce), 110 | max_priority_fee_per_gas: :binary.decode_unsigned(max_priority_fee_per_gas), 111 | max_fee_per_gas: :binary.decode_unsigned(max_fee_per_gas), 112 | gas: :binary.decode_unsigned(gas), 113 | to: (to != "" && Utils.encode_address!(to)) || nil, 114 | value: :binary.decode_unsigned(value), 115 | input: input, 116 | access_list: access_list 117 | }, rest} 118 | end 119 | 120 | def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed} 121 | 122 | defimpl Ethers.Transaction.Protocol do 123 | def type_id(_transaction), do: @for.type_id() 124 | 125 | def type_envelope(_transaction), do: @for.type_envelope() 126 | 127 | def to_rlp_list(tx, _mode) do 128 | # Eip1559 does not discriminate in RLP encoding between payload and hash 129 | [ 130 | tx.chain_id, 131 | tx.nonce, 132 | tx.max_priority_fee_per_gas, 133 | tx.max_fee_per_gas, 134 | tx.gas, 135 | (tx.to && Utils.decode_address!(tx.to)) || "", 136 | tx.value, 137 | tx.input, 138 | tx.access_list || [] 139 | ] 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/ethers/transaction/eip2930.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Eip2930 do 2 | @moduledoc """ 3 | Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 2930 4 | transactions. EIP-2930 introduced a new transaction type that includes an access list, 5 | allowing transactions to pre-specify and pre-pay for account and storage access to mitigate 6 | gas cost changes from EIP-2929 and prevent contract breakage. The access list format also 7 | enables future use cases like block-wide witnesses and static state access patterns. 8 | 9 | See: https://eips.ethereum.org/EIPS/eip-2930 10 | """ 11 | 12 | import Ethers.Transaction.Helpers 13 | 14 | alias Ethers.Types 15 | alias Ethers.Utils 16 | 17 | @behaviour Ethers.Transaction 18 | 19 | @type_id 1 20 | 21 | @enforce_keys [:chain_id, :nonce, :gas_price, :gas] 22 | defstruct [ 23 | :chain_id, 24 | :nonce, 25 | :gas_price, 26 | :gas, 27 | :to, 28 | :value, 29 | :input, 30 | access_list: [] 31 | ] 32 | 33 | @typedoc """ 34 | A transaction type following EIP-2930 (Type-1) and incorporating the following fields: 35 | - `chain_id` - chain ID of network where the transaction is to be executed 36 | - `nonce` - sequence number for the transaction from this sender 37 | - `gas_price`: Price willing to pay for each unit of gas (in wei) 38 | - `gas` - maximum amount of gas allowed for transaction execution 39 | - `to` - destination address for transaction, nil for contract creation 40 | - `value` - amount of ether (in wei) to transfer 41 | - `input` - data payload of the transaction 42 | - `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930) 43 | """ 44 | @type t :: %__MODULE__{ 45 | chain_id: non_neg_integer(), 46 | nonce: non_neg_integer(), 47 | gas_price: non_neg_integer(), 48 | gas: non_neg_integer(), 49 | to: Types.t_address() | nil, 50 | value: non_neg_integer(), 51 | input: binary(), 52 | access_list: [{binary(), [binary()]}] 53 | } 54 | 55 | @impl Ethers.Transaction 56 | def new(params) do 57 | to = params[:to] 58 | input = params[:input] || params[:data] || "" 59 | value = params[:value] || 0 60 | 61 | with :ok <- validate_common_fields(params), 62 | :ok <- validate_non_neg_integer(params.gas_price), 63 | :ok <- validate_non_neg_integer(value), 64 | :ok <- validate_binary(input) do 65 | {:ok, 66 | %__MODULE__{ 67 | chain_id: params.chain_id, 68 | nonce: params.nonce, 69 | gas_price: params.gas_price, 70 | gas: params.gas, 71 | to: to && Utils.to_checksum_address(to), 72 | value: value, 73 | input: input, 74 | access_list: params[:access_list] || [] 75 | }} 76 | end 77 | end 78 | 79 | @impl Ethers.Transaction 80 | def auto_fetchable_fields do 81 | [:chain_id, :nonce, :gas_price, :gas] 82 | end 83 | 84 | @impl Ethers.Transaction 85 | def type_envelope, do: <> 86 | 87 | @impl Ethers.Transaction 88 | def type_id, do: @type_id 89 | 90 | @impl Ethers.Transaction 91 | def from_rlp_list([ 92 | chain_id, 93 | nonce, 94 | gas_price, 95 | gas, 96 | to, 97 | value, 98 | input, 99 | access_list | rest 100 | ]) do 101 | {:ok, 102 | %__MODULE__{ 103 | chain_id: :binary.decode_unsigned(chain_id), 104 | nonce: :binary.decode_unsigned(nonce), 105 | gas_price: :binary.decode_unsigned(gas_price), 106 | gas: :binary.decode_unsigned(gas), 107 | to: (to != "" && Utils.encode_address!(to)) || nil, 108 | value: :binary.decode_unsigned(value), 109 | input: input, 110 | access_list: access_list 111 | }, rest} 112 | end 113 | 114 | def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed} 115 | 116 | defimpl Ethers.Transaction.Protocol do 117 | def type_id(_transaction), do: @for.type_id() 118 | 119 | def type_envelope(_transaction), do: @for.type_envelope() 120 | 121 | def to_rlp_list(tx, _mode) do 122 | # Eip2930 does not discriminate in RLP encoding between payload and hash 123 | [ 124 | tx.chain_id, 125 | tx.nonce, 126 | tx.gas_price, 127 | tx.gas, 128 | (tx.to && Utils.decode_address!(tx.to)) || "", 129 | tx.value, 130 | tx.input, 131 | tx.access_list || [] 132 | ] 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/ethers/transaction/eip4844.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Eip4844 do 2 | @moduledoc """ 3 | Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 4844 4 | transactions. EIP-4844 introduced "blob-carrying transactions" which contain a large amount 5 | of data that cannot be accessed by EVM execution, but whose commitment can be accessed. 6 | 7 | See: https://eips.ethereum.org/EIPS/eip-4844 8 | """ 9 | 10 | import Ethers.Transaction.Helpers 11 | 12 | alias Ethers.Types 13 | alias Ethers.Utils 14 | 15 | @behaviour Ethers.Transaction 16 | 17 | @type_id 3 18 | 19 | @enforce_keys [ 20 | :chain_id, 21 | :nonce, 22 | :max_priority_fee_per_gas, 23 | :max_fee_per_gas, 24 | :gas, 25 | :max_fee_per_blob_gas 26 | ] 27 | defstruct [ 28 | :chain_id, 29 | :nonce, 30 | :max_priority_fee_per_gas, 31 | :max_fee_per_gas, 32 | :gas, 33 | :to, 34 | :value, 35 | :input, 36 | :max_fee_per_blob_gas, 37 | access_list: [], 38 | blob_versioned_hashes: [] 39 | ] 40 | 41 | @typedoc """ 42 | A transaction type following EIP-4844 (Type-3) and incorporating the following fields: 43 | - `chain_id` - chain ID of network where the transaction is to be executed 44 | - `nonce` - sequence number for the transaction from this sender 45 | - `max_priority_fee_per_gas` - maximum fee per gas (in wei) to give to validators as priority fee (introduced in EIP-1559) 46 | - `max_fee_per_gas` - maximum total fee per gas (in wei) willing to pay (introduced in EIP-1559) 47 | - `gas` - maximum amount of gas allowed for transaction execution 48 | - `to` - destination address for transaction, nil for contract creation 49 | - `value` - amount of ether (in wei) to transfer 50 | - `input` - data payload of the transaction 51 | - `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930) 52 | - `max_fee_per_blob_gas` - maximum fee per blob gas (in wei) willing to pay (introduced in EIP-4844) 53 | - `blob_versioned_hashes` - list of versioned hashes of the blobs (introduced in EIP-4844) 54 | """ 55 | @type t :: %__MODULE__{ 56 | chain_id: non_neg_integer(), 57 | nonce: non_neg_integer(), 58 | max_priority_fee_per_gas: non_neg_integer(), 59 | max_fee_per_gas: non_neg_integer(), 60 | gas: non_neg_integer(), 61 | to: Types.t_address() | nil, 62 | value: non_neg_integer(), 63 | input: binary(), 64 | access_list: [{binary(), [binary()]}], 65 | max_fee_per_blob_gas: non_neg_integer(), 66 | blob_versioned_hashes: [{binary(), [binary()]}] 67 | } 68 | 69 | @impl Ethers.Transaction 70 | def new(params) do 71 | to = params[:to] 72 | input = params[:input] || params[:data] || "" 73 | value = params[:value] || 0 74 | 75 | with :ok <- validate_common_fields(params), 76 | :ok <- validate_non_neg_integer(params.max_priority_fee_per_gas), 77 | :ok <- validate_non_neg_integer(params.max_fee_per_gas), 78 | :ok <- validate_non_neg_integer(params.max_fee_per_blob_gas), 79 | :ok <- validate_non_neg_integer(value), 80 | :ok <- validate_binary(input) do 81 | {:ok, 82 | %__MODULE__{ 83 | chain_id: params.chain_id, 84 | nonce: params.nonce, 85 | max_priority_fee_per_gas: params.max_priority_fee_per_gas, 86 | max_fee_per_gas: params.max_fee_per_gas, 87 | gas: params.gas, 88 | to: to && Utils.to_checksum_address(to), 89 | value: value, 90 | input: input, 91 | access_list: params[:access_list] || [], 92 | max_fee_per_blob_gas: params.max_fee_per_blob_gas, 93 | blob_versioned_hashes: params[:blob_versioned_hashes] || [] 94 | }} 95 | end 96 | end 97 | 98 | @impl Ethers.Transaction 99 | def auto_fetchable_fields do 100 | [:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas, :max_fee_per_blob_gas] 101 | end 102 | 103 | @impl Ethers.Transaction 104 | def type_envelope, do: <> 105 | 106 | @impl Ethers.Transaction 107 | def type_id, do: @type_id 108 | 109 | @impl Ethers.Transaction 110 | def from_rlp_list([ 111 | chain_id, 112 | nonce, 113 | max_priority_fee_per_gas, 114 | max_fee_per_gas, 115 | gas, 116 | to, 117 | value, 118 | input, 119 | access_list, 120 | max_fee_per_blob_gas, 121 | blob_versioned_hashes 122 | | rest 123 | ]) do 124 | {:ok, 125 | %__MODULE__{ 126 | chain_id: :binary.decode_unsigned(chain_id), 127 | nonce: :binary.decode_unsigned(nonce), 128 | max_priority_fee_per_gas: :binary.decode_unsigned(max_priority_fee_per_gas), 129 | max_fee_per_gas: :binary.decode_unsigned(max_fee_per_gas), 130 | gas: :binary.decode_unsigned(gas), 131 | to: (to != "" && Utils.encode_address!(to)) || nil, 132 | value: :binary.decode_unsigned(value), 133 | input: input, 134 | access_list: access_list, 135 | max_fee_per_blob_gas: :binary.decode_unsigned(max_fee_per_blob_gas), 136 | blob_versioned_hashes: blob_versioned_hashes 137 | }, rest} 138 | end 139 | 140 | def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed} 141 | 142 | defimpl Ethers.Transaction.Protocol do 143 | def type_id(_transaction), do: @for.type_id() 144 | 145 | def type_envelope(_transaction), do: @for.type_envelope() 146 | 147 | def to_rlp_list(tx, _mode) do 148 | # Eip4844 requires Eip1559 fields 149 | [ 150 | tx.chain_id, 151 | tx.nonce, 152 | tx.max_priority_fee_per_gas, 153 | tx.max_fee_per_gas, 154 | tx.gas, 155 | (tx.to && Utils.decode_address!(tx.to)) || "", 156 | tx.value, 157 | tx.input, 158 | tx.access_list || [], 159 | tx.max_fee_per_blob_gas, 160 | tx.blob_versioned_hashes || [] 161 | ] 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/ethers/transaction/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Helpers do 2 | @moduledoc false 3 | 4 | @spec validate_non_neg_integer(term()) :: :ok | {:error, :expected_non_neg_integer_value} 5 | def validate_non_neg_integer(value) when is_integer(value) and value >= 0, do: :ok 6 | def validate_non_neg_integer(_), do: {:error, :expected_non_neg_integer_value} 7 | 8 | @spec validate_binary(term()) :: :ok | {:error, :expected_binary_value} 9 | def validate_binary(value) when is_binary(value), do: :ok 10 | def validate_binary(_), do: {:error, :expected_binary_value} 11 | 12 | @spec validate_address(term()) :: :ok | {:error, :invalid_address_length | :invalid_address} 13 | def validate_address("0x" <> address) do 14 | case Ethers.Utils.hex_decode(address) do 15 | {:ok, <<_address_bin::binary-20>>} -> :ok 16 | {:ok, _bad_address} -> {:error, :invalid_address_length} 17 | _ -> {:error, :invalid_address} 18 | end 19 | end 20 | 21 | def validate_address(nil), do: :ok 22 | def validate_address(_invalid), do: {:error, :invalid_address} 23 | 24 | @spec validate_common_fields(map()) :: 25 | :ok | {:error, :expected_non_neg_integer_value | :expected_binary_value} 26 | def validate_common_fields(params) do 27 | with :ok <- validate_non_neg_integer(params.chain_id), 28 | :ok <- validate_non_neg_integer(params.nonce), 29 | :ok <- validate_address(params[:to]) do 30 | validate_non_neg_integer(params.gas) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ethers/transaction/legacy.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Legacy do 2 | @moduledoc """ 3 | Legacy transaction struct and implementation of Transaction.Protocol. 4 | """ 5 | 6 | import Ethers.Transaction.Helpers 7 | 8 | alias Ethers.Types 9 | alias Ethers.Utils 10 | 11 | @behaviour Ethers.Transaction 12 | 13 | @type_id 0 14 | 15 | @enforce_keys [:nonce, :gas_price, :gas] 16 | defstruct [ 17 | :nonce, 18 | :gas_price, 19 | :gas, 20 | :to, 21 | :value, 22 | :input, 23 | :chain_id 24 | ] 25 | 26 | @typedoc """ 27 | Legacy transaction type (Type-0) incorporating the following fields: 28 | - nonce: Transaction sequence number for the sending account 29 | - gas_price: Price willing to pay for each unit of gas (in wei) 30 | - gas: Maximum number of gas units willing to pay for 31 | - to: Recipient address or nil for contract creation 32 | - value: Amount of ether to transfer in wei 33 | - input: Transaction data payload, also called 'data' 34 | - chain_id: Network ID from [EIP-155](https://eips.ethereum.org/EIPS/eip-155), defaults to nil for legacy 35 | """ 36 | @type t :: %__MODULE__{ 37 | nonce: non_neg_integer(), 38 | gas_price: non_neg_integer(), 39 | gas: non_neg_integer(), 40 | to: Types.t_address() | nil, 41 | value: non_neg_integer(), 42 | input: binary(), 43 | chain_id: non_neg_integer() | nil 44 | } 45 | 46 | @impl Ethers.Transaction 47 | def new(params) do 48 | to = params[:to] 49 | input = params[:input] || params[:data] || "" 50 | value = params[:value] || 0 51 | 52 | with :ok <- validate_common_fields(params), 53 | :ok <- validate_non_neg_integer(params.gas_price), 54 | :ok <- validate_non_neg_integer(value), 55 | :ok <- validate_binary(input) do 56 | {:ok, 57 | %__MODULE__{ 58 | nonce: params.nonce, 59 | gas_price: params.gas_price, 60 | gas: params.gas, 61 | to: to && Utils.to_checksum_address(to), 62 | value: value, 63 | input: input, 64 | chain_id: params[:chain_id] 65 | }} 66 | end 67 | end 68 | 69 | @impl Ethers.Transaction 70 | def auto_fetchable_fields do 71 | [:chain_id, :nonce, :gas_price, :gas] 72 | end 73 | 74 | # Legacy transactions do not have a type envelope 75 | @impl Ethers.Transaction 76 | def type_envelope, do: "" 77 | 78 | @impl Ethers.Transaction 79 | def type_id, do: @type_id 80 | 81 | @impl Ethers.Transaction 82 | def from_rlp_list([nonce, gas_price, gas, to, value, input | rest]) do 83 | {:ok, 84 | %__MODULE__{ 85 | nonce: :binary.decode_unsigned(nonce), 86 | gas_price: :binary.decode_unsigned(gas_price), 87 | gas: :binary.decode_unsigned(gas), 88 | to: (to != "" && Utils.encode_address!(to)) || nil, 89 | value: :binary.decode_unsigned(value), 90 | input: input 91 | }, rest} 92 | end 93 | 94 | def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed} 95 | 96 | defimpl Ethers.Transaction.Protocol do 97 | def type_id(_transaction), do: @for.type_id() 98 | 99 | def type_envelope(_transaction), do: @for.type_envelope() 100 | 101 | def to_rlp_list(tx, mode) do 102 | [ 103 | tx.nonce, 104 | tx.gas_price, 105 | tx.gas, 106 | (tx.to && Utils.decode_address!(tx.to)) || "", 107 | tx.value, 108 | tx.input 109 | ] 110 | |> maybe_add_eip_155(tx, mode) 111 | end 112 | 113 | defp maybe_add_eip_155(base_list, _tx, :payload), do: base_list 114 | 115 | defp maybe_add_eip_155(base_list, %@for{chain_id: nil}, :hash), do: base_list 116 | 117 | defp maybe_add_eip_155(base_list, %@for{chain_id: chain_id}, :hash) do 118 | base_list ++ [chain_id, 0, 0] 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/ethers/transaction/metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Metadata do 2 | @moduledoc """ 3 | Metadata for a transaction like block hash, block number, and transaction index. 4 | """ 5 | 6 | defstruct block_hash: nil, 7 | block_number: nil, 8 | transaction_index: nil 9 | 10 | @typedoc """ 11 | Transaction metadata type incorporating the following fields: 12 | - `block_hash` - hash of the block where the transaction was included 13 | - `block_number` - block number where the transaction was included 14 | - `transaction_index` - index of the transaction in the block 15 | """ 16 | @type t :: %__MODULE__{ 17 | block_hash: binary() | nil, 18 | block_number: non_neg_integer() | nil, 19 | transaction_index: non_neg_integer() | nil 20 | } 21 | 22 | @doc false 23 | def new!(params) do 24 | %__MODULE__{ 25 | block_hash: params[:block_hash], 26 | block_number: params[:block_number], 27 | transaction_index: params[:transaction_index] 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ethers/transaction/protocol.ex: -------------------------------------------------------------------------------- 1 | defprotocol Ethers.Transaction.Protocol do 2 | @moduledoc """ 3 | Protocol for handling Ethereum Virtual Machine (EVM) transactions. 4 | """ 5 | 6 | @doc """ 7 | Returns the binary value of the transaction type envelope. 8 | For legacy transactions, returns an empty binary. 9 | """ 10 | @fallback_to_any true 11 | @spec type_envelope(t) :: binary() 12 | def type_envelope(transaction) 13 | 14 | @doc """ 15 | Returns type of transaction as an integer. 16 | """ 17 | @fallback_to_any true 18 | @spec type_id(t) :: non_neg_integer() 19 | def type_id(transaction) 20 | 21 | @doc """ 22 | Returns a list ready to be RLP encoded for a given transaction. 23 | 24 | ## Parameters 25 | - `transaction` - Transaction struct containing the transaction data 26 | - `mode` - Encoding mode: 27 | - `:payload` - For encoding the transaction payload 28 | - `:hash` - For encoding the transaction hash 29 | """ 30 | @spec to_rlp_list(t, mode :: :payload | :hash) :: [binary() | [binary()]] 31 | def to_rlp_list(transaction, mode) 32 | end 33 | 34 | defimpl Ethers.Transaction.Protocol, for: Any do 35 | alias Ethers.Transaction 36 | 37 | def type_id(transaction) do 38 | type = Map.get(transaction, :type, Transaction.default_transaction_type()) 39 | type.type_id() 40 | end 41 | 42 | @dialyzer {:no_return, {:type_envelope, 1}} 43 | def type_envelope(transaction), do: raise_no_impl(transaction) 44 | 45 | @dialyzer {:no_return, {:to_rlp_list, 2}} 46 | def to_rlp_list(transaction, _mode), do: raise_no_impl(transaction) 47 | 48 | @dialyzer {:nowarn_function, {:raise_no_impl, 1}} 49 | defp raise_no_impl(transaction) do 50 | raise ArgumentError, "Transaction protocol not implemented for #{inspect(transaction)}" 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ethers/transaction/signed.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Transaction.Signed do 2 | @moduledoc """ 3 | A struct that wraps a transaction and its signature values. 4 | """ 5 | 6 | alias Ethers.Transaction 7 | alias Ethers.Transaction.Legacy 8 | alias Ethers.Transaction.Metadata 9 | 10 | @enforce_keys [:payload, :signature_r, :signature_s, :signature_y_parity_or_v] 11 | defstruct [ 12 | :payload, 13 | :signature_r, 14 | :signature_s, 15 | :signature_y_parity_or_v, 16 | metadata: nil 17 | ] 18 | 19 | @typedoc """ 20 | A transaction signature envelope that wraps transaction data with its signature components. 21 | 22 | This type supports both Legacy (pre-EIP-155), EIP-155 Legacy, and EIP-1559 transaction formats. 23 | The signature components consist of: 24 | - `signature_r`, `signature_s`: The ECDSA signature values as defined in Ethereum's Yellow Paper 25 | - `signature_y_parity_or_v`: The recovery value that varies by transaction type: 26 | - For pre-EIP-155 Legacy transactions: v = recovery_id + 27 27 | - For EIP-155 Legacy transactions: v = recovery_id + chain_id * 2 + 35 28 | - For EIP-1559 transactions: Just the recovery_id (0 or 1) as specified in EIP-2930 29 | 30 | Related EIPs: 31 | - [EIP-155](https://eips.ethereum.org/EIPS/eip-155): Simple replay attack protection 32 | - [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559): Fee market change for ETH 1.0 chain 33 | - [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930): Optional access lists 34 | """ 35 | @type t :: %__MODULE__{ 36 | payload: Transaction.t_payload(), 37 | signature_r: binary(), 38 | signature_s: binary(), 39 | signature_y_parity_or_v: non_neg_integer(), 40 | metadata: Metadata.t() | nil 41 | } 42 | 43 | @legacy_parity_magic_number 27 44 | @legacy_parity_with_chain_magic_number 35 45 | 46 | @doc false 47 | def new(params) do 48 | {:ok, 49 | %__MODULE__{ 50 | payload: params.payload, 51 | signature_r: params.signature_r, 52 | signature_s: params.signature_s, 53 | signature_y_parity_or_v: params.signature_y_parity_or_v, 54 | metadata: Metadata.new!(params) 55 | }} 56 | end 57 | 58 | @doc false 59 | def from_rlp_list(rlp_list, payload) do 60 | case rlp_list do 61 | [signature_y_parity_or_v, signature_r, signature_s] -> 62 | signed_tx = 63 | maybe_add_chain_id(%__MODULE__{ 64 | payload: payload, 65 | signature_r: signature_r, 66 | signature_s: signature_s, 67 | signature_y_parity_or_v: :binary.decode_unsigned(signature_y_parity_or_v) 68 | }) 69 | 70 | {:ok, signed_tx} 71 | 72 | [] -> 73 | {:error, :no_signature} 74 | 75 | _rlp_list -> 76 | {:error, :signature_decode_failed} 77 | end 78 | end 79 | 80 | defp maybe_add_chain_id(%__MODULE__{payload: %Legacy{chain_id: nil} = legacy_tx} = signed_tx) do 81 | {chain_id, _recovery_id} = extract_chain_id_and_recovery_id(signed_tx) 82 | %__MODULE__{signed_tx | payload: %Legacy{legacy_tx | chain_id: chain_id}} 83 | end 84 | 85 | defp maybe_add_chain_id(%__MODULE__{} = tx), do: tx 86 | 87 | @doc """ 88 | Calculates the from address of a signed transaction using its signature. 89 | 90 | The from address is inferred from the signature of the transaction rather than being explicitly 91 | specified. This is done by recovering the signer's public key from the signature and then 92 | deriving the corresponding Ethereum address. 93 | 94 | ## Returns 95 | - `{:ok, address}` - Successfully recovered from address 96 | - `{:error, reason}` - Failed to recover address 97 | """ 98 | @spec from_address(t()) :: {:ok, Ethers.Types.t_address()} | {:error, atom()} 99 | def from_address(%__MODULE__{} = transaction) do 100 | hash_bin = Transaction.transaction_hash(transaction.payload, :bin) 101 | 102 | {_chain_id, recovery_id} = extract_chain_id_and_recovery_id(transaction) 103 | 104 | case Ethers.secp256k1_module().recover( 105 | hash_bin, 106 | transaction.signature_r, 107 | transaction.signature_s, 108 | recovery_id 109 | ) do 110 | {:ok, pubkey} -> Ethers.Utils.public_key_to_address(pubkey) 111 | {:error, reason} -> {:error, reason} 112 | end 113 | end 114 | 115 | @doc """ 116 | Calculates the y-parity or v value for transaction signatures. 117 | 118 | Handles both legacy and EIP-1559 transaction types according to their specifications. 119 | 120 | ## Parameters 121 | - `tx` - Transaction struct 122 | - `recovery_id` - Recovery ID from the signature 123 | 124 | ## Returns 125 | - `integer` - Calculated y-parity or v value 126 | """ 127 | @spec calculate_y_parity_or_v(Transaction.t_payload(), binary() | non_neg_integer()) :: 128 | non_neg_integer() 129 | def calculate_y_parity_or_v(tx, recovery_id) do 130 | case tx do 131 | %Legacy{chain_id: nil} -> 132 | # EIP-155 133 | recovery_id + @legacy_parity_magic_number 134 | 135 | %Legacy{chain_id: chain_id} -> 136 | # EIP-155 137 | recovery_id + chain_id * 2 + @legacy_parity_with_chain_magic_number 138 | 139 | _tx -> 140 | # EIP-1559 141 | recovery_id 142 | end 143 | end 144 | 145 | @spec extract_chain_id_and_recovery_id(t()) :: {non_neg_integer() | nil, non_neg_integer()} 146 | defp extract_chain_id_and_recovery_id(%__MODULE__{payload: tx, signature_y_parity_or_v: v}) do 147 | case tx do 148 | %Legacy{} -> 149 | if v >= @legacy_parity_with_chain_magic_number do 150 | chain_id = div(v - @legacy_parity_with_chain_magic_number, 2) 151 | recovery_id = v - chain_id * 2 - @legacy_parity_with_chain_magic_number 152 | {chain_id, recovery_id} 153 | else 154 | {nil, v - @legacy_parity_magic_number} 155 | end 156 | 157 | _tx -> 158 | {tx.chain_id, v} 159 | end 160 | end 161 | 162 | defimpl Transaction.Protocol do 163 | import Ethers.Utils, only: [remove_leading_zeros: 1] 164 | 165 | def type_id(signed_tx), do: Transaction.Protocol.type_id(signed_tx.payload) 166 | 167 | def type_envelope(signed_tx), do: Transaction.Protocol.type_envelope(signed_tx.payload) 168 | 169 | def to_rlp_list(signed_tx, mode) do 170 | base_list = Transaction.Protocol.to_rlp_list(signed_tx.payload, mode) 171 | 172 | base_list ++ signature_fields(signed_tx) 173 | end 174 | 175 | defp signature_fields(signed_tx) do 176 | [ 177 | signed_tx.signature_y_parity_or_v, 178 | remove_leading_zeros(signed_tx.signature_r), 179 | remove_leading_zeros(signed_tx.signature_s) 180 | ] 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/ethers/tx_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.TxData do 2 | @moduledoc """ 3 | Transaction struct to hold information about the ABI selector, encoded data 4 | and the target `to` address. 5 | """ 6 | 7 | alias Ethers.Utils 8 | 9 | @typedoc """ 10 | Holds transaction data, the function selector and the default `to` address. 11 | 12 | Can be passed in to `Ethers.call/2` or `Ethers.send_transaction/2` to execute. 13 | """ 14 | @type t :: %__MODULE__{ 15 | data: binary() | [binary()], 16 | selector: ABI.FunctionSelector.t(), 17 | default_address: nil | Ethers.Types.t_address(), 18 | base_module: atom() | nil 19 | } 20 | 21 | @enforce_keys [:data, :selector] 22 | defstruct [:data, :selector, :default_address, :base_module] 23 | 24 | @doc false 25 | @spec new(binary(), ABI.FunctionSelector.t(), Ethers.Types.t_address() | nil, atom() | nil) :: 26 | t() 27 | def new(data, selector, default_address, base_module) do 28 | %__MODULE__{ 29 | data: data, 30 | selector: selector, 31 | default_address: default_address, 32 | base_module: base_module 33 | } 34 | end 35 | 36 | @doc """ 37 | Converts a TxData struct and optional overrides to a map ready for RPC data. 38 | """ 39 | @spec to_map(t() | map(), Keyword.t()) :: map() 40 | def to_map(tx_data, overrides \\ []) 41 | 42 | def to_map(%__MODULE__{} = tx_data, overrides) do 43 | tx_data 44 | |> get_tx_map() 45 | |> to_map(overrides) 46 | end 47 | 48 | def to_map(tx_map, overrides) when is_map(tx_map) do 49 | Enum.into(overrides, tx_map) 50 | end 51 | 52 | @doc """ 53 | ABI decodes a function input/output given a TxData or FunctionSelector 54 | """ 55 | @spec abi_decode(binary(), ABI.FunctionSelector.t() | t(), type :: :input | :output) :: 56 | {:ok, any() | [any()]} 57 | def abi_decode(data, tx_data_or_selector, type \\ :output) 58 | 59 | def abi_decode(data, %{selector: %ABI.FunctionSelector{} = selector}, type), 60 | do: abi_decode(data, selector, type) 61 | 62 | def abi_decode(data, %ABI.FunctionSelector{} = selector, type) do 63 | types = 64 | case type do 65 | :input -> selector.types 66 | :output -> selector.returns 67 | end 68 | 69 | selector 70 | |> ABI.decode(data, type) 71 | |> Enum.zip(types) 72 | |> Enum.map(fn {return, type} -> Utils.human_arg(return, type) end) 73 | |> case do 74 | [element] -> {:ok, element} 75 | elements -> {:ok, elements} 76 | end 77 | end 78 | 79 | defp get_tx_map(%{selector: %{type: :function}} = tx_data) do 80 | %{data: tx_data.data} 81 | |> maybe_add_to_address(tx_data.default_address) 82 | end 83 | 84 | defp maybe_add_to_address(tx_map, nil), do: tx_map 85 | defp maybe_add_to_address(tx_map, address), do: Map.put(tx_map, :to, address) 86 | 87 | defimpl Inspect do 88 | import Inspect.Algebra 89 | 90 | alias Ethers.Utils 91 | 92 | def inspect(%{selector: selector, data: data, default_address: default_address}, opts) do 93 | arguments = ABI.decode(selector, data, :input) 94 | 95 | arguments_doc = 96 | Enum.zip([selector.types, input_names(selector), arguments]) 97 | |> Enum.map(fn {type, name, arg} -> 98 | [ 99 | color(ABI.FunctionSelector.encode_type(type), :atom, opts), 100 | " ", 101 | if(name, do: color(name, :variable, opts)), 102 | if(name, do: " "), 103 | human_arg(arg, type, opts) 104 | ] 105 | |> Enum.reject(&is_nil/1) 106 | |> concat() 107 | end) 108 | |> Enum.intersperse(concat(color(",", :operator, opts), break(" "))) 109 | 110 | returns = 111 | Enum.zip(selector.returns, selector.return_names) 112 | |> Enum.map(fn 113 | {type, ""} -> 114 | color(ABI.FunctionSelector.encode_type(type), :atom, opts) 115 | 116 | {type, name} -> 117 | concat([ 118 | color(ABI.FunctionSelector.encode_type(type), :atom, opts), 119 | " ", 120 | color(name, :variable, opts) 121 | ]) 122 | end) 123 | |> Enum.intersperse(concat(color(",", :operator, opts), break(" "))) 124 | 125 | returns_doc = 126 | if Enum.count(returns) > 0 do 127 | [ 128 | " ", 129 | color("returns ", :atom, opts), 130 | color("(", :operator, opts), 131 | nest(concat([break("") | returns]), 2), 132 | break(""), 133 | color(")", :operator, opts) 134 | ] 135 | else 136 | [] 137 | end 138 | 139 | default_address = 140 | case default_address do 141 | nil -> 142 | [] 143 | 144 | _ -> 145 | [ 146 | line(), 147 | color("default_address: ", :default, opts), 148 | color(inspect(default_address), :string, opts) 149 | ] 150 | end 151 | 152 | arguments_doc = 153 | case arguments_doc do 154 | [] -> 155 | [ 156 | color("(", :operator, opts), 157 | color(")", :operator, opts) 158 | ] 159 | 160 | _ -> 161 | [ 162 | color("(", :operator, opts), 163 | nest(concat([break("") | arguments_doc]), 2), 164 | break(""), 165 | color(")", :operator, opts) 166 | ] 167 | end 168 | 169 | inner = 170 | concat( 171 | [ 172 | break(""), 173 | color("function", :atom, opts), 174 | " ", 175 | color(selector.function, :call, opts), 176 | concat(arguments_doc), 177 | " ", 178 | state_mutability(selector, opts) 179 | ] ++ returns_doc ++ default_address 180 | ) 181 | 182 | concat([ 183 | color("#Ethers.TxData<", :map, opts), 184 | nest(inner, 2), 185 | break(""), 186 | color(">", :map, opts) 187 | ]) 188 | end 189 | 190 | defp input_names(selector) do 191 | if Enum.count(selector.types) == Enum.count(selector.input_names) do 192 | selector.input_names 193 | else 194 | 1..Enum.count(selector.types) 195 | |> Enum.map(fn _ -> nil end) 196 | end 197 | end 198 | 199 | defp state_mutability(%{state_mutability: state_mutability}, opts) 200 | when state_mutability in [:non_payable, :payable] do 201 | color(Atom.to_string(state_mutability), nil, opts) 202 | end 203 | 204 | defp state_mutability(%{state_mutability: nil}, opts) do 205 | color("unknown", nil, opts) 206 | end 207 | 208 | defp state_mutability(%{state_mutability: state_mutability}, opts) do 209 | color(Atom.to_string(state_mutability), :string, opts) 210 | end 211 | 212 | defp human_arg(arg, type, opts), do: Inspect.inspect(Utils.human_arg(arg, type), opts) 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /lib/ethers/types.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Types do 2 | @moduledoc "EVM types and compound type definitions" 3 | 4 | require Logger 5 | 6 | @typedoc """ 7 | Ethereum address in its hex format with 0x or in its binary format 8 | 9 | ## Examples 10 | - `"0xdAC17F958D2ee523a2206206994597C13D831ec7"` 11 | - `<<218, 193, 127, 149, 141, 46, 229, 35, 162, 32, 98, 6, 153, 69, 151, 193, 61, 131, 30, 199>>` 12 | """ 13 | @type t_address :: <<_::336>> | <<_::160>> 14 | 15 | @typedoc """ 16 | Public key either in its uncompressed format (64 bytes MAY BE prefixed with 0x04) or its 17 | compressed format (32 bytes MUST BE prefixed with 0x02 or 0x03) 18 | 19 | It can be hex encoded but only with 0x prefix. 20 | 21 | Compressed public key MUST have a prefix. 22 | """ 23 | @type t_pub_key :: 24 | <<_::520>> 25 | | <<_::512>> 26 | | <<_::264>> 27 | | <<_::1042>> 28 | | <<_::1026>> 29 | | <<_::530>> 30 | 31 | @typedoc """ 32 | keccak hash in its hex format with 0x 33 | 34 | ## Examples 35 | - `"0xd4288c8e733eb71a39fe2e8dd4912ce54d8d26d9874f30309b26b4b071260422"` 36 | """ 37 | @type t_hash :: <<_::528>> 38 | 39 | @type t_bitsizes :: unquote(8..256//8 |> Enum.reduce(&{:|, [], [&1, &2]})) 40 | @type t_bytesizes :: unquote(1..32 |> Enum.reduce(&{:|, [], [&1, &2]})) 41 | @type t_evm_types :: 42 | {:uint, t_bitsizes()} 43 | | {:int, t_bitsizes()} 44 | | {:bytes, t_bytesizes()} 45 | | :bytes 46 | | :string 47 | | :address 48 | | {:array, t_evm_types()} 49 | | {:array, t_evm_types(), non_neg_integer()} 50 | | {:tuple, [t_evm_types()]} 51 | 52 | @valid_bitsize_range 8..256//8 53 | 54 | defguardp valid_bitsize(bitsize) when bitsize >= 8 and bitsize <= 256 and rem(bitsize, 8) == 0 55 | defguardp valid_bytesize(bytesize) when bytesize >= 1 and bytesize <= 32 56 | 57 | @doc """ 58 | Converts EVM data types to typespecs for documentation 59 | """ 60 | def to_elixir_type(:address) do 61 | quote do: Ethers.Types.t_address() 62 | end 63 | 64 | def to_elixir_type({:array, sub_type, _element_count}) do 65 | to_elixir_type({:array, sub_type}) 66 | end 67 | 68 | def to_elixir_type({:array, sub_type}) do 69 | sub_type = to_elixir_type(sub_type) 70 | 71 | quote do 72 | [unquote(sub_type)] 73 | end 74 | end 75 | 76 | def to_elixir_type({:bytes, size}) when valid_bytesize(size) do 77 | quote do: <<_::unquote(size * 8)>> 78 | end 79 | 80 | def to_elixir_type(:bytes) do 81 | quote do: binary() 82 | end 83 | 84 | def to_elixir_type(:bool) do 85 | quote do: boolean() 86 | end 87 | 88 | def to_elixir_type(:function) do 89 | raise "Function type not supported!" 90 | end 91 | 92 | def to_elixir_type({:ufixed, _element_count, _precision}) do 93 | quote do: float() 94 | end 95 | 96 | def to_elixir_type({:fixed, _element_count, _precision}) do 97 | quote do: float() 98 | end 99 | 100 | def to_elixir_type({:int, _}) do 101 | quote do: integer 102 | end 103 | 104 | def to_elixir_type(:string) do 105 | quote do: String.t() 106 | end 107 | 108 | def to_elixir_type({:tuple, sub_types}) do 109 | sub_types = Enum.map(sub_types, &to_elixir_type/1) 110 | 111 | quote do: {unquote_splicing(sub_types)} 112 | end 113 | 114 | def to_elixir_type({:uint, _}) do 115 | quote do: non_neg_integer 116 | end 117 | 118 | def to_elixir_type(unknown) do 119 | Logger.warning("Unknown type #{inspect(unknown)}") 120 | quote do: term 121 | end 122 | 123 | @doc """ 124 | Returns the maximum possible value in the given type if supported. 125 | 126 | ## Examples 127 | 128 | iex> Ethers.Types.max({:uint, 8}) 129 | 255 130 | 131 | iex> Ethers.Types.max({:int, 8}) 132 | 127 133 | 134 | iex> Ethers.Types.max({:uint, 16}) 135 | 65535 136 | 137 | iex> Ethers.Types.max({:int, 16}) 138 | 32767 139 | 140 | iex> Ethers.Types.max({:uint, 128}) 141 | 340282366920938463463374607431768211455 142 | 143 | iex> Ethers.Types.max({:int, 128}) 144 | 170141183460469231731687303715884105727 145 | """ 146 | def max(type) 147 | 148 | Enum.each(@valid_bitsize_range, fn bitsize -> 149 | {int_res, uint_res} = 150 | Enum.reduce(1..bitsize, {1, 1}, fn _bsize, {_, acc} -> {acc, 2 * acc} end) 151 | 152 | def max({:uint, unquote(bitsize)}) do 153 | unquote(uint_res - 1) 154 | end 155 | 156 | def max({:int, unquote(bitsize)}) do 157 | unquote(int_res - 1) 158 | end 159 | end) 160 | 161 | @doc """ 162 | Returns the minimum possible value in the given type if supported. 163 | 164 | ## Examples 165 | 166 | iex> Ethers.Types.min({:uint, 8}) 167 | 0 168 | 169 | iex> Ethers.Types.min({:int, 8}) 170 | -128 171 | 172 | iex> Ethers.Types.min({:uint, 16}) 173 | 0 174 | 175 | iex> Ethers.Types.min({:int, 16}) 176 | -32768 177 | 178 | iex> Ethers.Types.min({:int, 24}) 179 | -8388608 180 | 181 | iex> Ethers.Types.min({:int, 128}) 182 | -170141183460469231731687303715884105728 183 | """ 184 | def min(type) 185 | 186 | def min({:uint, bitsize}) when valid_bitsize(bitsize), do: 0 187 | 188 | Enum.each(@valid_bitsize_range, fn bitsize -> 189 | int_res = Enum.reduce(1..(bitsize - 1), 1, fn _bsize, acc -> 2 * acc end) 190 | 191 | def min({:int, unquote(bitsize)}) do 192 | unquote(-int_res) 193 | end 194 | end) 195 | 196 | @doc """ 197 | Returns the default value in the given type if supported. 198 | 199 | ## Examples 200 | 201 | iex> Ethers.Types.default(:address) 202 | "0x0000000000000000000000000000000000000000" 203 | 204 | iex> Ethers.Types.default({:int, 32}) 205 | 0 206 | 207 | iex> Ethers.Types.default({:uint, 8}) 208 | 0 209 | 210 | iex> Ethers.Types.default({:int, 128}) 211 | 0 212 | 213 | iex> Ethers.Types.default(:string) 214 | "" 215 | 216 | iex> Ethers.Types.default(:bytes) 217 | "" 218 | 219 | iex> Ethers.Types.default({:bytes, 8}) 220 | <<0, 0, 0, 0, 0, 0, 0, 0>> 221 | """ 222 | def default({type, _}) when type in [:int, :uint], do: 0 223 | 224 | def default(:address), do: "0x0000000000000000000000000000000000000000" 225 | 226 | def default(type) when type in [:string, :bytes], do: "" 227 | 228 | def default({:bytes, size}) when valid_bytesize(size), do: <<0::size*8>> 229 | 230 | @doc """ 231 | Checks if a given data matches a given solidity type 232 | 233 | ## Examples 234 | 235 | iex> Ethers.Types.matches_type?(false, :bool) 236 | true 237 | 238 | iex> Ethers.Types.matches_type?(200, {:uint, 8}) 239 | true 240 | 241 | iex> Ethers.Types.matches_type?(400, {:uint, 8}) 242 | false 243 | 244 | iex> Ethers.Types.matches_type?("0xdAC17F958D2ee523a2206206994597C13D831ec7", :address) 245 | true 246 | """ 247 | @spec matches_type?(term(), t_evm_types()) :: boolean() 248 | def matches_type?(value, type) 249 | 250 | def matches_type?(value, {:uint, _bsize} = type), 251 | do: is_integer(value) and value >= 0 and value <= max(type) 252 | 253 | def matches_type?(value, {:int, _bsize} = type), 254 | do: is_integer(value) and value >= min(type) and value <= max(type) 255 | 256 | def matches_type?(value, :address) when is_binary(value) do 257 | byte_size(value) == 20 or (byte_size(value) == 42 and String.starts_with?(value, "0x")) 258 | end 259 | 260 | def matches_type?(_value, :address), do: false 261 | 262 | def matches_type?(value, :string), do: is_binary(value) and String.valid?(value) 263 | 264 | def matches_type?(value, :bytes), do: is_binary(value) 265 | 266 | def matches_type?(value, {:bytes, size}) when valid_bytesize(size), 267 | do: is_binary(value) && byte_size(value) == size 268 | 269 | def matches_type?(_value, {:bytes, size}), 270 | do: raise(ArgumentError, "Invalid size: #{inspect(size)} (must be 1 <= size <= 32)") 271 | 272 | def matches_type?(value, :bool), do: is_boolean(value) 273 | 274 | def matches_type?(values, {:array, sub_type, element_count}) do 275 | matches_type?(values, {:array, sub_type}) and Enum.count(values) == element_count 276 | end 277 | 278 | def matches_type?(values, {:array, sub_type}) do 279 | is_list(values) and Enum.all?(values, &matches_type?(&1, sub_type)) 280 | end 281 | 282 | def matches_type?(values, {:tuple, sub_types}) do 283 | if is_tuple(values) and tuple_size(values) == Enum.count(sub_types) do 284 | Enum.zip(sub_types, Tuple.to_list(values)) 285 | |> Enum.all?(fn {type, value} -> matches_type?(value, type) end) 286 | else 287 | false 288 | end 289 | end 290 | 291 | @doc """ 292 | Validates and creates typed values to use with functions or events. 293 | 294 | Typed values are useful when there are multiple overloads of same function or event and you need 295 | to specify one of them to be used. 296 | 297 | Also raises with ArgumentError in case value does not match the given type. 298 | 299 | ## Examples 300 | 301 | iex> Ethers.Types.typed({:uint, 256}, 5) 302 | {:typed, {:uint, 256}, 5} 303 | 304 | iex> Ethers.Types.typed(:bytes, <<0, 1, 2>>) 305 | {:typed, :bytes, <<0, 1, 2>>} 306 | """ 307 | @spec typed(term(), t_evm_types() | nil) :: {:typed, term(), term()} | no_return() 308 | def typed(type, nil), do: {:typed, type, nil} 309 | 310 | def typed(type, value) do 311 | if matches_type?(value, type) do 312 | {:typed, type, value} 313 | else 314 | raise ArgumentError, "Value #{inspect(value)} does not match type #{inspect(type)}" 315 | end 316 | end 317 | end 318 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.6.7" 5 | @source_url "https://github.com/ExWeb3/elixir_ethers" 6 | 7 | def project do 8 | [ 9 | app: :ethers, 10 | version: @version, 11 | elixir: "~> 1.11", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | name: "Ethers", 15 | source_url: @source_url, 16 | deps: deps(), 17 | test_coverage: [tool: ExCoveralls], 18 | preferred_cli_env: [ 19 | coveralls: :test, 20 | "coveralls.detail": :test, 21 | "coveralls.post": :test, 22 | "coveralls.html": :test, 23 | test_prepare: :test 24 | ], 25 | description: 26 | "A comprehensive Web3 library for interacting with smart contracts on Ethereum using Elixir.", 27 | package: package(), 28 | docs: docs(), 29 | aliases: aliases(), 30 | dialyzer: dialyzer() 31 | ] 32 | end 33 | 34 | # Run "mix help compile.app" to learn about applications. 35 | def application do 36 | [ 37 | extra_applications: [:logger, :ethereumex] 38 | ] 39 | end 40 | 41 | defp package do 42 | [ 43 | licenses: ["Apache-2.0"], 44 | links: %{"GitHub" => @source_url}, 45 | maintainers: ["Alisina Bahadori"], 46 | files: ["lib", "priv", "mix.exs", "README*", "LICENSE*", "CHANGELOG*"] 47 | ] 48 | end 49 | 50 | defp docs do 51 | source_ref = 52 | if String.ends_with?(@version, "-dev") do 53 | "main" 54 | else 55 | "v#{@version}" 56 | end 57 | 58 | [ 59 | main: "readme", 60 | extras: [ 61 | "README.md": [title: "Introduction"], 62 | "CHANGELOG.md": [title: "Changelog"], 63 | "guides/typed-arguments.md": [title: "Typed Arguments"], 64 | "guides/configuration.md": [title: "Configuration"], 65 | "guides/upgrading.md": [title: "Upgrading"] 66 | ], 67 | source_url: @source_url, 68 | source_ref: source_ref, 69 | nest_modules_by_prefix: [ 70 | Ethers.Contracts 71 | ], 72 | groups_for_modules: [ 73 | Transactions: [ 74 | "Ethers.Transaction", 75 | ~r/^Ethers\.Transaction\..*$/ 76 | ], 77 | "Builtin Contracts": [ 78 | ~r/^Ethers\.Contracts\.(?:(?!EventFilters$|Errors\.).)*$/ 79 | ], 80 | "Builtin EventFilters": [ 81 | ~r/^Ethers\.Contracts\.[A-Za-z0-9.]+\.EventFilters$/ 82 | ], 83 | Signer: [ 84 | ~r/^Ethers\.Signer\.[A-Za-z0-9.]+$/, 85 | ~r/^Ethers\.Signer$/ 86 | ], 87 | "Builtin Contract Errors": [ 88 | ~r/^Ethers\.Contracts\..*$/ 89 | ] 90 | ], 91 | logo: "assets/exdoc_logo.png", 92 | markdown_processor: {ExDoc.Markdown.Earmark, footnotes: true} 93 | ] 94 | end 95 | 96 | def dialyzer do 97 | [flags: [:error_handling, :extra_return, :underspecs, :unknown, :unmatched_returns]] 98 | end 99 | 100 | # Specifies which paths to compile per environment. 101 | defp elixirc_paths(:test), do: ["lib", "test/support"] 102 | defp elixirc_paths(_), do: ["lib"] 103 | 104 | # Run "mix help deps" to learn about dependencies. 105 | defp deps do 106 | [ 107 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 108 | {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, 109 | {:ethereumex, "~> 0.10 and >= 0.10.7"}, 110 | {:ex_abi, "~> 0.8.0", optional: System.get_env("SKIP_EX_KECCAK") == "true"}, 111 | {:ex_doc, "~> 0.32", only: :dev, runtime: false}, 112 | {:ex_keccak, "~> 0.7.5"}, 113 | {:ex_rlp, "~> 0.6.0"}, 114 | {:ex_secp256k1, "~> 0.7.2", optional: true}, 115 | {:excoveralls, "~> 0.10", only: :test}, 116 | {:idna, "~> 6.1"}, 117 | {:jason, "~> 1.4"}, 118 | {:makeup_syntect, "~> 0.1", only: :dev, runtime: false}, 119 | {:plug, ">= 1.0.0", only: :test}, 120 | {:req, "~> 0.5"} 121 | ] 122 | end 123 | 124 | def aliases do 125 | [ 126 | test_prepare: ["run test/test_prepare.exs"], 127 | test: ["test_prepare", "test"] 128 | ] 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /priv/abi/ccip_read.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "sender", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "string[]", 11 | "name": "urls", 12 | "type": "string[]" 13 | }, 14 | { 15 | "internalType": "bytes", 16 | "name": "callData", 17 | "type": "bytes" 18 | }, 19 | { 20 | "internalType": "bytes4", 21 | "name": "callbackFunction", 22 | "type": "bytes4" 23 | }, 24 | { 25 | "internalType": "bytes", 26 | "name": "extraData", 27 | "type": "bytes" 28 | } 29 | ], 30 | "name": "OffchainLookup", 31 | "type": "error" 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /priv/abi/ens_extended_resolver.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "bytes", 6 | "name": "name", 7 | "type": "bytes" 8 | }, 9 | { 10 | "internalType": "bytes", 11 | "name": "data", 12 | "type": "bytes" 13 | } 14 | ], 15 | "name": "resolve", 16 | "outputs": [ 17 | { 18 | "internalType": "bytes", 19 | "name": "result", 20 | "type": "bytes" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /priv/abi/erc1155.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": 4 | [ 5 | { 6 | "internalType": "address", 7 | "name": "sender", 8 | "type": "address" 9 | }, 10 | { 11 | "internalType": "uint256", 12 | "name": "balance", 13 | "type": "uint256" 14 | }, 15 | { 16 | "internalType": "uint256", 17 | "name": "needed", 18 | "type": "uint256" 19 | }, 20 | { 21 | "internalType": "uint256", 22 | "name": "tokenId", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "ERC1155InsufficientBalance", 27 | "type": "error" 28 | }, 29 | { 30 | "inputs": 31 | [ 32 | { 33 | "internalType": "address", 34 | "name": "approver", 35 | "type": "address" 36 | } 37 | ], 38 | "name": "ERC1155InvalidApprover", 39 | "type": "error" 40 | }, 41 | { 42 | "inputs": 43 | [ 44 | { 45 | "internalType": "uint256", 46 | "name": "idsLength", 47 | "type": "uint256" 48 | }, 49 | { 50 | "internalType": "uint256", 51 | "name": "valuesLength", 52 | "type": "uint256" 53 | } 54 | ], 55 | "name": "ERC1155InvalidArrayLength", 56 | "type": "error" 57 | }, 58 | { 59 | "inputs": 60 | [ 61 | { 62 | "internalType": "address", 63 | "name": "operator", 64 | "type": "address" 65 | } 66 | ], 67 | "name": "ERC1155InvalidOperator", 68 | "type": "error" 69 | }, 70 | { 71 | "inputs": 72 | [ 73 | { 74 | "internalType": "address", 75 | "name": "receiver", 76 | "type": "address" 77 | } 78 | ], 79 | "name": "ERC1155InvalidReceiver", 80 | "type": "error" 81 | }, 82 | { 83 | "inputs": 84 | [ 85 | { 86 | "internalType": "address", 87 | "name": "sender", 88 | "type": "address" 89 | } 90 | ], 91 | "name": "ERC1155InvalidSender", 92 | "type": "error" 93 | }, 94 | { 95 | "inputs": 96 | [ 97 | { 98 | "internalType": "address", 99 | "name": "operator", 100 | "type": "address" 101 | }, 102 | { 103 | "internalType": "address", 104 | "name": "owner", 105 | "type": "address" 106 | } 107 | ], 108 | "name": "ERC1155MissingApprovalForAll", 109 | "type": "error" 110 | }, 111 | { 112 | "anonymous": false, 113 | "inputs": 114 | [ 115 | { 116 | "indexed": true, 117 | "internalType": "address", 118 | "name": "account", 119 | "type": "address" 120 | }, 121 | { 122 | "indexed": true, 123 | "internalType": "address", 124 | "name": "operator", 125 | "type": "address" 126 | }, 127 | { 128 | "indexed": false, 129 | "internalType": "bool", 130 | "name": "approved", 131 | "type": "bool" 132 | } 133 | ], 134 | "name": "ApprovalForAll", 135 | "type": "event" 136 | }, 137 | { 138 | "anonymous": false, 139 | "inputs": 140 | [ 141 | { 142 | "indexed": true, 143 | "internalType": "address", 144 | "name": "operator", 145 | "type": "address" 146 | }, 147 | { 148 | "indexed": true, 149 | "internalType": "address", 150 | "name": "from", 151 | "type": "address" 152 | }, 153 | { 154 | "indexed": true, 155 | "internalType": "address", 156 | "name": "to", 157 | "type": "address" 158 | }, 159 | { 160 | "indexed": false, 161 | "internalType": "uint256[]", 162 | "name": "ids", 163 | "type": "uint256[]" 164 | }, 165 | { 166 | "indexed": false, 167 | "internalType": "uint256[]", 168 | "name": "values", 169 | "type": "uint256[]" 170 | } 171 | ], 172 | "name": "TransferBatch", 173 | "type": "event" 174 | }, 175 | { 176 | "anonymous": false, 177 | "inputs": 178 | [ 179 | { 180 | "indexed": true, 181 | "internalType": "address", 182 | "name": "operator", 183 | "type": "address" 184 | }, 185 | { 186 | "indexed": true, 187 | "internalType": "address", 188 | "name": "from", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": true, 193 | "internalType": "address", 194 | "name": "to", 195 | "type": "address" 196 | }, 197 | { 198 | "indexed": false, 199 | "internalType": "uint256", 200 | "name": "id", 201 | "type": "uint256" 202 | }, 203 | { 204 | "indexed": false, 205 | "internalType": "uint256", 206 | "name": "value", 207 | "type": "uint256" 208 | } 209 | ], 210 | "name": "TransferSingle", 211 | "type": "event" 212 | }, 213 | { 214 | "anonymous": false, 215 | "inputs": 216 | [ 217 | { 218 | "indexed": false, 219 | "internalType": "string", 220 | "name": "value", 221 | "type": "string" 222 | }, 223 | { 224 | "indexed": true, 225 | "internalType": "uint256", 226 | "name": "id", 227 | "type": "uint256" 228 | } 229 | ], 230 | "name": "URI", 231 | "type": "event" 232 | }, 233 | { 234 | "inputs": 235 | [ 236 | { 237 | "internalType": "address", 238 | "name": "account", 239 | "type": "address" 240 | }, 241 | { 242 | "internalType": "uint256", 243 | "name": "id", 244 | "type": "uint256" 245 | } 246 | ], 247 | "name": "balanceOf", 248 | "outputs": 249 | [ 250 | { 251 | "internalType": "uint256", 252 | "name": "", 253 | "type": "uint256" 254 | } 255 | ], 256 | "stateMutability": "view", 257 | "type": "function" 258 | }, 259 | { 260 | "inputs": 261 | [ 262 | { 263 | "internalType": "address[]", 264 | "name": "accounts", 265 | "type": "address[]" 266 | }, 267 | { 268 | "internalType": "uint256[]", 269 | "name": "ids", 270 | "type": "uint256[]" 271 | } 272 | ], 273 | "name": "balanceOfBatch", 274 | "outputs": 275 | [ 276 | { 277 | "internalType": "uint256[]", 278 | "name": "", 279 | "type": "uint256[]" 280 | } 281 | ], 282 | "stateMutability": "view", 283 | "type": "function" 284 | }, 285 | { 286 | "inputs": 287 | [ 288 | { 289 | "internalType": "address", 290 | "name": "account", 291 | "type": "address" 292 | }, 293 | { 294 | "internalType": "address", 295 | "name": "operator", 296 | "type": "address" 297 | } 298 | ], 299 | "name": "isApprovedForAll", 300 | "outputs": 301 | [ 302 | { 303 | "internalType": "bool", 304 | "name": "", 305 | "type": "bool" 306 | } 307 | ], 308 | "stateMutability": "view", 309 | "type": "function" 310 | }, 311 | { 312 | "inputs": 313 | [ 314 | { 315 | "internalType": "address", 316 | "name": "from", 317 | "type": "address" 318 | }, 319 | { 320 | "internalType": "address", 321 | "name": "to", 322 | "type": "address" 323 | }, 324 | { 325 | "internalType": "uint256[]", 326 | "name": "ids", 327 | "type": "uint256[]" 328 | }, 329 | { 330 | "internalType": "uint256[]", 331 | "name": "values", 332 | "type": "uint256[]" 333 | }, 334 | { 335 | "internalType": "bytes", 336 | "name": "data", 337 | "type": "bytes" 338 | } 339 | ], 340 | "name": "safeBatchTransferFrom", 341 | "outputs": [], 342 | "stateMutability": "nonpayable", 343 | "type": "function" 344 | }, 345 | { 346 | "inputs": 347 | [ 348 | { 349 | "internalType": "address", 350 | "name": "from", 351 | "type": "address" 352 | }, 353 | { 354 | "internalType": "address", 355 | "name": "to", 356 | "type": "address" 357 | }, 358 | { 359 | "internalType": "uint256", 360 | "name": "id", 361 | "type": "uint256" 362 | }, 363 | { 364 | "internalType": "uint256", 365 | "name": "value", 366 | "type": "uint256" 367 | }, 368 | { 369 | "internalType": "bytes", 370 | "name": "data", 371 | "type": "bytes" 372 | } 373 | ], 374 | "name": "safeTransferFrom", 375 | "outputs": [], 376 | "stateMutability": "nonpayable", 377 | "type": "function" 378 | }, 379 | { 380 | "inputs": 381 | [ 382 | { 383 | "internalType": "address", 384 | "name": "operator", 385 | "type": "address" 386 | }, 387 | { 388 | "internalType": "bool", 389 | "name": "approved", 390 | "type": "bool" 391 | } 392 | ], 393 | "name": "setApprovalForAll", 394 | "outputs": [], 395 | "stateMutability": "nonpayable", 396 | "type": "function" 397 | }, 398 | { 399 | "inputs": 400 | [ 401 | { 402 | "internalType": "bytes4", 403 | "name": "interfaceId", 404 | "type": "bytes4" 405 | } 406 | ], 407 | "name": "supportsInterface", 408 | "outputs": 409 | [ 410 | { 411 | "internalType": "bool", 412 | "name": "", 413 | "type": "bool" 414 | } 415 | ], 416 | "stateMutability": "view", 417 | "type": "function" 418 | }, 419 | { 420 | "inputs": 421 | [ 422 | { 423 | "internalType": "uint256", 424 | "name": "", 425 | "type": "uint256" 426 | } 427 | ], 428 | "name": "uri", 429 | "outputs": 430 | [ 431 | { 432 | "internalType": "string", 433 | "name": "", 434 | "type": "string" 435 | } 436 | ], 437 | "stateMutability": "view", 438 | "type": "function" 439 | } 440 | ] -------------------------------------------------------------------------------- /priv/abi/erc165.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": 4 | [ 5 | { 6 | "internalType": "bytes4", 7 | "name": "interfaceId", 8 | "type": "bytes4" 9 | } 10 | ], 11 | "name": "supportsInterface", 12 | "outputs": 13 | [ 14 | { 15 | "internalType": "bool", 16 | "name": "", 17 | "type": "bool" 18 | } 19 | ], 20 | "stateMutability": "view", 21 | "type": "function" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /priv/abi/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": 4 | [ 5 | { 6 | "internalType": "address", 7 | "name": "spender", 8 | "type": "address" 9 | }, 10 | { 11 | "internalType": "uint256", 12 | "name": "allowance", 13 | "type": "uint256" 14 | }, 15 | { 16 | "internalType": "uint256", 17 | "name": "needed", 18 | "type": "uint256" 19 | } 20 | ], 21 | "name": "ERC20InsufficientAllowance", 22 | "type": "error" 23 | }, 24 | { 25 | "inputs": 26 | [ 27 | { 28 | "internalType": "address", 29 | "name": "sender", 30 | "type": "address" 31 | }, 32 | { 33 | "internalType": "uint256", 34 | "name": "balance", 35 | "type": "uint256" 36 | }, 37 | { 38 | "internalType": "uint256", 39 | "name": "needed", 40 | "type": "uint256" 41 | } 42 | ], 43 | "name": "ERC20InsufficientBalance", 44 | "type": "error" 45 | }, 46 | { 47 | "inputs": 48 | [ 49 | { 50 | "internalType": "address", 51 | "name": "approver", 52 | "type": "address" 53 | } 54 | ], 55 | "name": "ERC20InvalidApprover", 56 | "type": "error" 57 | }, 58 | { 59 | "inputs": 60 | [ 61 | { 62 | "internalType": "address", 63 | "name": "receiver", 64 | "type": "address" 65 | } 66 | ], 67 | "name": "ERC20InvalidReceiver", 68 | "type": "error" 69 | }, 70 | { 71 | "inputs": 72 | [ 73 | { 74 | "internalType": "address", 75 | "name": "sender", 76 | "type": "address" 77 | } 78 | ], 79 | "name": "ERC20InvalidSender", 80 | "type": "error" 81 | }, 82 | { 83 | "inputs": 84 | [ 85 | { 86 | "internalType": "address", 87 | "name": "spender", 88 | "type": "address" 89 | } 90 | ], 91 | "name": "ERC20InvalidSpender", 92 | "type": "error" 93 | }, 94 | { 95 | "anonymous": false, 96 | "inputs": 97 | [ 98 | { 99 | "indexed": true, 100 | "internalType": "address", 101 | "name": "owner", 102 | "type": "address" 103 | }, 104 | { 105 | "indexed": true, 106 | "internalType": "address", 107 | "name": "spender", 108 | "type": "address" 109 | }, 110 | { 111 | "indexed": false, 112 | "internalType": "uint256", 113 | "name": "value", 114 | "type": "uint256" 115 | } 116 | ], 117 | "name": "Approval", 118 | "type": "event" 119 | }, 120 | { 121 | "anonymous": false, 122 | "inputs": 123 | [ 124 | { 125 | "indexed": true, 126 | "internalType": "address", 127 | "name": "from", 128 | "type": "address" 129 | }, 130 | { 131 | "indexed": true, 132 | "internalType": "address", 133 | "name": "to", 134 | "type": "address" 135 | }, 136 | { 137 | "indexed": false, 138 | "internalType": "uint256", 139 | "name": "value", 140 | "type": "uint256" 141 | } 142 | ], 143 | "name": "Transfer", 144 | "type": "event" 145 | }, 146 | { 147 | "inputs": 148 | [ 149 | { 150 | "internalType": "address", 151 | "name": "owner", 152 | "type": "address" 153 | }, 154 | { 155 | "internalType": "address", 156 | "name": "spender", 157 | "type": "address" 158 | } 159 | ], 160 | "name": "allowance", 161 | "outputs": 162 | [ 163 | { 164 | "internalType": "uint256", 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "stateMutability": "view", 170 | "type": "function" 171 | }, 172 | { 173 | "inputs": 174 | [ 175 | { 176 | "internalType": "address", 177 | "name": "spender", 178 | "type": "address" 179 | }, 180 | { 181 | "internalType": "uint256", 182 | "name": "value", 183 | "type": "uint256" 184 | } 185 | ], 186 | "name": "approve", 187 | "outputs": 188 | [ 189 | { 190 | "internalType": "bool", 191 | "name": "", 192 | "type": "bool" 193 | } 194 | ], 195 | "stateMutability": "nonpayable", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": 200 | [ 201 | { 202 | "internalType": "address", 203 | "name": "account", 204 | "type": "address" 205 | } 206 | ], 207 | "name": "balanceOf", 208 | "outputs": 209 | [ 210 | { 211 | "internalType": "uint256", 212 | "name": "", 213 | "type": "uint256" 214 | } 215 | ], 216 | "stateMutability": "view", 217 | "type": "function" 218 | }, 219 | { 220 | "inputs": [], 221 | "name": "decimals", 222 | "outputs": 223 | [ 224 | { 225 | "internalType": "uint8", 226 | "name": "", 227 | "type": "uint8" 228 | } 229 | ], 230 | "stateMutability": "view", 231 | "type": "function" 232 | }, 233 | { 234 | "inputs": [], 235 | "name": "name", 236 | "outputs": 237 | [ 238 | { 239 | "internalType": "string", 240 | "name": "", 241 | "type": "string" 242 | } 243 | ], 244 | "stateMutability": "view", 245 | "type": "function" 246 | }, 247 | { 248 | "inputs": [], 249 | "name": "symbol", 250 | "outputs": 251 | [ 252 | { 253 | "internalType": "string", 254 | "name": "", 255 | "type": "string" 256 | } 257 | ], 258 | "stateMutability": "view", 259 | "type": "function" 260 | }, 261 | { 262 | "inputs": [], 263 | "name": "totalSupply", 264 | "outputs": 265 | [ 266 | { 267 | "internalType": "uint256", 268 | "name": "", 269 | "type": "uint256" 270 | } 271 | ], 272 | "stateMutability": "view", 273 | "type": "function" 274 | }, 275 | { 276 | "inputs": 277 | [ 278 | { 279 | "internalType": "address", 280 | "name": "to", 281 | "type": "address" 282 | }, 283 | { 284 | "internalType": "uint256", 285 | "name": "value", 286 | "type": "uint256" 287 | } 288 | ], 289 | "name": "transfer", 290 | "outputs": 291 | [ 292 | { 293 | "internalType": "bool", 294 | "name": "", 295 | "type": "bool" 296 | } 297 | ], 298 | "stateMutability": "nonpayable", 299 | "type": "function" 300 | }, 301 | { 302 | "inputs": 303 | [ 304 | { 305 | "internalType": "address", 306 | "name": "from", 307 | "type": "address" 308 | }, 309 | { 310 | "internalType": "address", 311 | "name": "to", 312 | "type": "address" 313 | }, 314 | { 315 | "internalType": "uint256", 316 | "name": "value", 317 | "type": "uint256" 318 | } 319 | ], 320 | "name": "transferFrom", 321 | "outputs": 322 | [ 323 | { 324 | "internalType": "bool", 325 | "name": "", 326 | "type": "bool" 327 | } 328 | ], 329 | "stateMutability": "nonpayable", 330 | "type": "function" 331 | } 332 | ] -------------------------------------------------------------------------------- /priv/abi/erc721.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": 4 | [ 5 | { 6 | "internalType": "address", 7 | "name": "sender", 8 | "type": "address" 9 | }, 10 | { 11 | "internalType": "uint256", 12 | "name": "tokenId", 13 | "type": "uint256" 14 | }, 15 | { 16 | "internalType": "address", 17 | "name": "owner", 18 | "type": "address" 19 | } 20 | ], 21 | "name": "ERC721IncorrectOwner", 22 | "type": "error" 23 | }, 24 | { 25 | "inputs": 26 | [ 27 | { 28 | "internalType": "address", 29 | "name": "operator", 30 | "type": "address" 31 | }, 32 | { 33 | "internalType": "uint256", 34 | "name": "tokenId", 35 | "type": "uint256" 36 | } 37 | ], 38 | "name": "ERC721InsufficientApproval", 39 | "type": "error" 40 | }, 41 | { 42 | "inputs": 43 | [ 44 | { 45 | "internalType": "address", 46 | "name": "approver", 47 | "type": "address" 48 | } 49 | ], 50 | "name": "ERC721InvalidApprover", 51 | "type": "error" 52 | }, 53 | { 54 | "inputs": 55 | [ 56 | { 57 | "internalType": "address", 58 | "name": "operator", 59 | "type": "address" 60 | } 61 | ], 62 | "name": "ERC721InvalidOperator", 63 | "type": "error" 64 | }, 65 | { 66 | "inputs": 67 | [ 68 | { 69 | "internalType": "address", 70 | "name": "owner", 71 | "type": "address" 72 | } 73 | ], 74 | "name": "ERC721InvalidOwner", 75 | "type": "error" 76 | }, 77 | { 78 | "inputs": 79 | [ 80 | { 81 | "internalType": "address", 82 | "name": "receiver", 83 | "type": "address" 84 | } 85 | ], 86 | "name": "ERC721InvalidReceiver", 87 | "type": "error" 88 | }, 89 | { 90 | "inputs": 91 | [ 92 | { 93 | "internalType": "address", 94 | "name": "sender", 95 | "type": "address" 96 | } 97 | ], 98 | "name": "ERC721InvalidSender", 99 | "type": "error" 100 | }, 101 | { 102 | "inputs": 103 | [ 104 | { 105 | "internalType": "uint256", 106 | "name": "tokenId", 107 | "type": "uint256" 108 | } 109 | ], 110 | "name": "ERC721NonexistentToken", 111 | "type": "error" 112 | }, 113 | { 114 | "anonymous": false, 115 | "inputs": 116 | [ 117 | { 118 | "indexed": true, 119 | "internalType": "address", 120 | "name": "owner", 121 | "type": "address" 122 | }, 123 | { 124 | "indexed": true, 125 | "internalType": "address", 126 | "name": "approved", 127 | "type": "address" 128 | }, 129 | { 130 | "indexed": true, 131 | "internalType": "uint256", 132 | "name": "tokenId", 133 | "type": "uint256" 134 | } 135 | ], 136 | "name": "Approval", 137 | "type": "event" 138 | }, 139 | { 140 | "anonymous": false, 141 | "inputs": 142 | [ 143 | { 144 | "indexed": true, 145 | "internalType": "address", 146 | "name": "owner", 147 | "type": "address" 148 | }, 149 | { 150 | "indexed": true, 151 | "internalType": "address", 152 | "name": "operator", 153 | "type": "address" 154 | }, 155 | { 156 | "indexed": false, 157 | "internalType": "bool", 158 | "name": "approved", 159 | "type": "bool" 160 | } 161 | ], 162 | "name": "ApprovalForAll", 163 | "type": "event" 164 | }, 165 | { 166 | "anonymous": false, 167 | "inputs": 168 | [ 169 | { 170 | "indexed": true, 171 | "internalType": "address", 172 | "name": "from", 173 | "type": "address" 174 | }, 175 | { 176 | "indexed": true, 177 | "internalType": "address", 178 | "name": "to", 179 | "type": "address" 180 | }, 181 | { 182 | "indexed": true, 183 | "internalType": "uint256", 184 | "name": "tokenId", 185 | "type": "uint256" 186 | } 187 | ], 188 | "name": "Transfer", 189 | "type": "event" 190 | }, 191 | { 192 | "inputs": 193 | [ 194 | { 195 | "internalType": "address", 196 | "name": "to", 197 | "type": "address" 198 | }, 199 | { 200 | "internalType": "uint256", 201 | "name": "tokenId", 202 | "type": "uint256" 203 | } 204 | ], 205 | "name": "approve", 206 | "outputs": [], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": 212 | [ 213 | { 214 | "internalType": "address", 215 | "name": "owner", 216 | "type": "address" 217 | } 218 | ], 219 | "name": "balanceOf", 220 | "outputs": 221 | [ 222 | { 223 | "internalType": "uint256", 224 | "name": "", 225 | "type": "uint256" 226 | } 227 | ], 228 | "stateMutability": "view", 229 | "type": "function" 230 | }, 231 | { 232 | "inputs": 233 | [ 234 | { 235 | "internalType": "uint256", 236 | "name": "tokenId", 237 | "type": "uint256" 238 | } 239 | ], 240 | "name": "getApproved", 241 | "outputs": 242 | [ 243 | { 244 | "internalType": "address", 245 | "name": "", 246 | "type": "address" 247 | } 248 | ], 249 | "stateMutability": "view", 250 | "type": "function" 251 | }, 252 | { 253 | "inputs": 254 | [ 255 | { 256 | "internalType": "address", 257 | "name": "owner", 258 | "type": "address" 259 | }, 260 | { 261 | "internalType": "address", 262 | "name": "operator", 263 | "type": "address" 264 | } 265 | ], 266 | "name": "isApprovedForAll", 267 | "outputs": 268 | [ 269 | { 270 | "internalType": "bool", 271 | "name": "", 272 | "type": "bool" 273 | } 274 | ], 275 | "stateMutability": "view", 276 | "type": "function" 277 | }, 278 | { 279 | "inputs": [], 280 | "name": "name", 281 | "outputs": 282 | [ 283 | { 284 | "internalType": "string", 285 | "name": "", 286 | "type": "string" 287 | } 288 | ], 289 | "stateMutability": "view", 290 | "type": "function" 291 | }, 292 | { 293 | "inputs": 294 | [ 295 | { 296 | "internalType": "uint256", 297 | "name": "tokenId", 298 | "type": "uint256" 299 | } 300 | ], 301 | "name": "ownerOf", 302 | "outputs": 303 | [ 304 | { 305 | "internalType": "address", 306 | "name": "", 307 | "type": "address" 308 | } 309 | ], 310 | "stateMutability": "view", 311 | "type": "function" 312 | }, 313 | { 314 | "inputs": 315 | [ 316 | { 317 | "internalType": "address", 318 | "name": "from", 319 | "type": "address" 320 | }, 321 | { 322 | "internalType": "address", 323 | "name": "to", 324 | "type": "address" 325 | }, 326 | { 327 | "internalType": "uint256", 328 | "name": "tokenId", 329 | "type": "uint256" 330 | } 331 | ], 332 | "name": "safeTransferFrom", 333 | "outputs": [], 334 | "stateMutability": "nonpayable", 335 | "type": "function" 336 | }, 337 | { 338 | "inputs": 339 | [ 340 | { 341 | "internalType": "address", 342 | "name": "from", 343 | "type": "address" 344 | }, 345 | { 346 | "internalType": "address", 347 | "name": "to", 348 | "type": "address" 349 | }, 350 | { 351 | "internalType": "uint256", 352 | "name": "tokenId", 353 | "type": "uint256" 354 | }, 355 | { 356 | "internalType": "bytes", 357 | "name": "data", 358 | "type": "bytes" 359 | } 360 | ], 361 | "name": "safeTransferFrom", 362 | "outputs": [], 363 | "stateMutability": "nonpayable", 364 | "type": "function" 365 | }, 366 | { 367 | "inputs": 368 | [ 369 | { 370 | "internalType": "address", 371 | "name": "operator", 372 | "type": "address" 373 | }, 374 | { 375 | "internalType": "bool", 376 | "name": "approved", 377 | "type": "bool" 378 | } 379 | ], 380 | "name": "setApprovalForAll", 381 | "outputs": [], 382 | "stateMutability": "nonpayable", 383 | "type": "function" 384 | }, 385 | { 386 | "inputs": 387 | [ 388 | { 389 | "internalType": "bytes4", 390 | "name": "interfaceId", 391 | "type": "bytes4" 392 | } 393 | ], 394 | "name": "supportsInterface", 395 | "outputs": 396 | [ 397 | { 398 | "internalType": "bool", 399 | "name": "", 400 | "type": "bool" 401 | } 402 | ], 403 | "stateMutability": "view", 404 | "type": "function" 405 | }, 406 | { 407 | "inputs": [], 408 | "name": "symbol", 409 | "outputs": 410 | [ 411 | { 412 | "internalType": "string", 413 | "name": "", 414 | "type": "string" 415 | } 416 | ], 417 | "stateMutability": "view", 418 | "type": "function" 419 | }, 420 | { 421 | "inputs": 422 | [ 423 | { 424 | "internalType": "uint256", 425 | "name": "tokenId", 426 | "type": "uint256" 427 | } 428 | ], 429 | "name": "tokenURI", 430 | "outputs": 431 | [ 432 | { 433 | "internalType": "string", 434 | "name": "", 435 | "type": "string" 436 | } 437 | ], 438 | "stateMutability": "view", 439 | "type": "function" 440 | }, 441 | { 442 | "inputs": 443 | [ 444 | { 445 | "internalType": "address", 446 | "name": "from", 447 | "type": "address" 448 | }, 449 | { 450 | "internalType": "address", 451 | "name": "to", 452 | "type": "address" 453 | }, 454 | { 455 | "internalType": "uint256", 456 | "name": "tokenId", 457 | "type": "uint256" 458 | } 459 | ], 460 | "name": "transferFrom", 461 | "outputs": [], 462 | "stateMutability": "nonpayable", 463 | "type": "function" 464 | } 465 | ] -------------------------------------------------------------------------------- /test/ethers/ccip_read_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.CcipReadTest do 2 | use ExUnit.Case 3 | 4 | import Ethers.TestHelpers 5 | 6 | alias Ethers.CcipRead 7 | alias Ethers.Contract.Test.CcipReadTestContract 8 | alias Ethers.Utils 9 | 10 | @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 11 | 12 | setup_all do 13 | address = deploy(CcipReadTestContract, from: @from) 14 | [address: address] 15 | end 16 | 17 | describe "call/2" do 18 | test "returns successful result when no offchain lookup is needed", %{address: address} do 19 | assert {:ok, "direct value"} = 20 | CcipReadTestContract.get_direct_value() 21 | |> CcipRead.call(to: address) 22 | end 23 | 24 | test "handles OffchainLookup error and performs offchain lookup", %{address: address} do 25 | Req.Test.expect(Ethers.CcipReq, fn conn -> 26 | assert ["ccip", sender, data] = conn.path_info 27 | 28 | # Verify the request parameters 29 | assert String.starts_with?(sender, "0x") 30 | assert String.starts_with?(data, "0x") 31 | 32 | Req.Test.json(conn, %{data: data}) 33 | end) 34 | 35 | assert {:ok, 100} = 36 | CcipReadTestContract.get_value(100) 37 | |> CcipRead.call(to: address) 38 | end 39 | 40 | test "filters out non-https URLs from the lookup list", %{address: address} do 41 | # The contract provides both https and non-https URLs 42 | # Our implementation should only try the https ones 43 | Req.Test.expect(Ethers.CcipReq, fn conn -> 44 | assert conn.scheme == :https 45 | assert ["ccip", _sender, data] = conn.path_info 46 | Req.Test.json(conn, %{data: data}) 47 | end) 48 | 49 | assert {:ok, 300} = 50 | CcipReadTestContract.get_value(300) 51 | |> CcipRead.call(to: address) 52 | end 53 | 54 | test "tries next URL when first URL fails", %{address: address} do 55 | # First request fails 56 | Req.Test.expect(Ethers.CcipReq, 2, fn conn -> 57 | if conn.host == "example.com" do 58 | conn 59 | |> Plug.Conn.put_status(500) 60 | |> Req.Test.json(%{data: "0x"}) 61 | else 62 | # Second URL succeeds 63 | Req.Test.json(conn, %{ 64 | data: ABI.TypeEncoder.encode([700], [{:uint, 256}]) |> Utils.hex_encode() 65 | }) 66 | end 67 | end) 68 | 69 | assert {:ok, 700} = 70 | CcipReadTestContract.get_value(400) 71 | |> CcipRead.call(to: address) 72 | end 73 | 74 | test "returns error when all URLs fail", %{address: address} do 75 | # Both URLs fail 76 | Req.Test.stub(Ethers.CcipReq, fn conn -> 77 | Plug.Conn.put_status(conn, 500) 78 | |> Req.Test.text("Failed") 79 | end) 80 | 81 | assert {:error, :ccip_read_failed} = 82 | CcipReadTestContract.get_value(500) 83 | |> CcipRead.call(to: address) 84 | end 85 | 86 | test "returns error when response is not 200", %{address: address} do 87 | Req.Test.stub(Ethers.CcipReq, fn conn -> 88 | conn 89 | |> Plug.Conn.put_status(404) 90 | |> Req.Test.json(%{error: "Not found"}) 91 | end) 92 | 93 | assert {:error, :ccip_read_failed} = 94 | CcipReadTestContract.get_value(600) 95 | |> CcipRead.call(to: address) 96 | end 97 | 98 | test "returns error when response body is invalid", %{address: address} do 99 | Req.Test.stub(Ethers.CcipReq, fn conn -> 100 | conn 101 | |> Plug.Conn.put_status(200) 102 | |> Req.Test.text("invalid json") 103 | end) 104 | 105 | assert {:error, :ccip_read_failed} = 106 | CcipReadTestContract.get_value(700) 107 | |> CcipRead.call(to: address) 108 | end 109 | 110 | test "returns error when hex decoding fails", %{address: address} do 111 | Req.Test.stub(Ethers.CcipReq, fn conn -> 112 | Req.Test.json(conn, %{data: "invalid hex"}) 113 | end) 114 | 115 | assert {:error, :ccip_read_failed} = 116 | CcipReadTestContract.get_value(800) 117 | |> CcipRead.call(to: address) 118 | end 119 | 120 | test "returns original error when it's not an OffchainLookup error", %{address: address} do 121 | assert {:error, %Ethers.Contract.Test.CcipReadTestContract.Errors.InvalidValue{}} = 122 | CcipReadTestContract.get_value(0) 123 | |> CcipRead.call(to: address) 124 | 125 | # Sending value to a non-payable function should return the original error 126 | assert {:error, %{"code" => 3}} = 127 | CcipReadTestContract.get_value(1) 128 | |> CcipRead.call(to: address, value: 1000, from: @from) 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/ethers/contract_helpers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.ContractHelpersTest do 2 | use ExUnit.Case 3 | alias Ethers.ContractHelpers 4 | 5 | describe "read_abi" do 6 | test "works with default abis" do 7 | assert {abi_results, abi_file} = ContractHelpers.read_abi(abi: :erc20) 8 | assert is_list(abi_results) 9 | assert String.ends_with?(abi_file, "priv/abi/erc20.json") 10 | end 11 | 12 | test "returns error with invalid parameters" do 13 | assert_raise ArgumentError, fn -> 14 | ContractHelpers.read_abi(abi: :erc20, abi_file: "file") 15 | end 16 | 17 | assert_raise ArgumentError, fn -> 18 | assert {:error, :bad_argument} = ContractHelpers.read_abi(bad_arg: true) 19 | end 20 | end 21 | end 22 | 23 | describe "maybe_read_contract_binary" do 24 | test "returns error with invalid parameters" do 25 | assert_raise ArgumentError, "Invalid options", fn -> 26 | ContractHelpers.maybe_read_contract_binary(abi: :erc20, abi_file: "file") 27 | end 28 | 29 | assert_raise ArgumentError, "Invalid options", fn -> 30 | ContractHelpers.maybe_read_contract_binary(bad_arg: true) 31 | end 32 | end 33 | 34 | test "returns nil if no binary is found" do 35 | assert is_nil(ContractHelpers.maybe_read_contract_binary(abi: [])) 36 | assert is_nil(ContractHelpers.maybe_read_contract_binary(abi: %{})) 37 | assert is_nil(ContractHelpers.maybe_read_contract_binary(abi: :erc20)) 38 | end 39 | end 40 | 41 | describe "document_types/2" do 42 | test "returns correct type with name" do 43 | assert " - amount: `{:uint, 256}`" == 44 | ContractHelpers.document_types([{:uint, 256}], ["amount"]) 45 | end 46 | 47 | test "returns correct type if names not provided" do 48 | assert " - `{:uint, 256}`" == ContractHelpers.document_types([{:uint, 256}]) 49 | end 50 | end 51 | 52 | describe "generate_arguments" do 53 | test "works with correct names" do 54 | assert [{:amount, [], _}, {:sender, [], _}] = 55 | ContractHelpers.generate_arguments(Ethers.TestModuleName, 2, ["amount", "sender"]) 56 | end 57 | 58 | test "works with invalid names" do 59 | assert [{:arg1, [], _}, {:arg2, [], _}] = 60 | ContractHelpers.generate_arguments(Ethers.TestModuleName, 2, ["amount"]) 61 | end 62 | end 63 | 64 | describe "human_signature" do 65 | test "returns the human signature of a given function" do 66 | assert "name(uint256 id, address address)" == 67 | ContractHelpers.human_signature(%ABI.FunctionSelector{ 68 | function: "name", 69 | input_names: ["id", "address"], 70 | types: [{:uint, 256}, :address] 71 | }) 72 | end 73 | 74 | test "returns human signature with invalid names length" do 75 | assert "name(uint256, address)" == 76 | ContractHelpers.human_signature(%ABI.FunctionSelector{ 77 | function: "name", 78 | input_names: ["id"], 79 | types: [{:uint, 256}, :address] 80 | }) 81 | 82 | assert "name(uint256, address)" == 83 | ContractHelpers.human_signature(%ABI.FunctionSelector{ 84 | function: "name", 85 | input_names: [], 86 | types: [{:uint, 256}, :address] 87 | }) 88 | 89 | assert "name(uint256, address)" == 90 | ContractHelpers.human_signature(%ABI.FunctionSelector{ 91 | function: "name", 92 | input_names: nil, 93 | types: [{:uint, 256}, :address] 94 | }) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/ethers/event_argument_types_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.EventArgumentTypesContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/event_argument_types_abi.json" 4 | end 5 | 6 | defmodule Ethers.EventArgumentTypesContractTest do 7 | use ExUnit.Case 8 | 9 | alias Ethers.Contract.Test.EventArgumentTypesContract 10 | 11 | describe "event filters" do 12 | test "works with strings" do 13 | filter = 14 | EventArgumentTypesContract.EventFilters.test_event(Ethers.Types.typed(:string, "ethers")) 15 | 16 | assert %Ethers.EventFilter{ 17 | selector: %ABI.FunctionSelector{ 18 | function: "TestEvent", 19 | input_names: ["numbers", "has_won"], 20 | inputs_indexed: [true, false], 21 | method_id: 22 | Ethers.Utils.hex_decode!( 23 | "0x05fa34d0d20b7c225e7b176f34bcf7538f55be08ce7caf15cc5789c3fc32646c" 24 | ), 25 | returns: [], 26 | state_mutability: nil, 27 | type: :event, 28 | types: [:string, :bool] 29 | }, 30 | topics: [ 31 | "0x05fa34d0d20b7c225e7b176f34bcf7538f55be08ce7caf15cc5789c3fc32646c", 32 | "0x86192adb5c990d8714151ec0eb2d7767d35c867add4a59bc860d0ef09cd76ee7" 33 | ], 34 | default_address: nil 35 | } == filter 36 | 37 | assert "#Ethers.EventFilter" == 38 | inspect(filter) 39 | end 40 | 41 | test "works with bytes" do 42 | filter = 43 | EventArgumentTypesContract.EventFilters.test_event( 44 | Ethers.Types.typed(:bytes, <<1, 2, 3>>) 45 | ) 46 | 47 | assert %Ethers.EventFilter{ 48 | selector: %ABI.FunctionSelector{ 49 | function: "TestEvent", 50 | input_names: ["numbers", "has_won"], 51 | inputs_indexed: [true, false], 52 | method_id: 53 | Ethers.Utils.hex_decode!( 54 | "0x9b6d1eff0add9c1c52995c5d2e7b50ba11dc2535256cb88d7ed507bff2794a42" 55 | ), 56 | returns: [], 57 | state_mutability: nil, 58 | type: :event, 59 | types: [:bytes, :bool] 60 | }, 61 | topics: [ 62 | "0x9b6d1eff0add9c1c52995c5d2e7b50ba11dc2535256cb88d7ed507bff2794a42", 63 | "0xf1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" 64 | ], 65 | default_address: nil 66 | } == filter 67 | 68 | assert "#Ethers.EventFilter" == 69 | inspect(filter) 70 | end 71 | 72 | test "works with unbounded arrays" do 73 | filter = EventArgumentTypesContract.EventFilters.test_event([1, 2, 3, 4, 5]) 74 | 75 | assert %Ethers.EventFilter{ 76 | selector: %ABI.FunctionSelector{ 77 | function: "TestEvent", 78 | input_names: ["numbers", "has_won"], 79 | inputs_indexed: [true, false], 80 | method_id: 81 | Ethers.Utils.hex_decode!( 82 | "0x452899094966d30dee615ca51e9f6f0f5ef486fee956f3bc3d8d38381a830ae7" 83 | ), 84 | returns: [], 85 | state_mutability: nil, 86 | type: :event, 87 | types: [{:array, {:uint, 256}}, :bool] 88 | }, 89 | topics: [ 90 | "0x452899094966d30dee615ca51e9f6f0f5ef486fee956f3bc3d8d38381a830ae7", 91 | "0x5917e5a395fb9b454434de59651d36822a9e29c5ec57474df3e67937b969460c" 92 | ], 93 | default_address: nil 94 | } == filter 95 | 96 | assert "#Ethers.EventFilter" == 97 | inspect(filter) 98 | end 99 | 100 | test "works with bounded arrays" do 101 | filter = 102 | EventArgumentTypesContract.EventFilters.test_event( 103 | Ethers.Types.typed({:array, {:uint, 256}, 3}, [1, 2, 3]) 104 | ) 105 | 106 | assert %Ethers.EventFilter{ 107 | selector: %ABI.FunctionSelector{ 108 | function: "TestEvent", 109 | input_names: ["numbers", "has_won"], 110 | inputs_indexed: [true, false], 111 | method_id: 112 | Ethers.Utils.hex_decode!( 113 | "0xda46d13c877fe85be32813ad8ae8e248bdb8cfc433c47cb648bf18229e3e79b5" 114 | ), 115 | returns: [], 116 | state_mutability: nil, 117 | type: :event, 118 | types: [{:array, {:uint, 256}, 3}, :bool] 119 | }, 120 | topics: [ 121 | "0xda46d13c877fe85be32813ad8ae8e248bdb8cfc433c47cb648bf18229e3e79b5", 122 | "0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c" 123 | ], 124 | default_address: nil 125 | } == filter 126 | 127 | assert "#Ethers.EventFilter" == 128 | inspect(filter) 129 | end 130 | 131 | test "works with tuples (structs)" do 132 | filter = EventArgumentTypesContract.EventFilters.test_event({1, 2, 3}) 133 | 134 | assert %Ethers.EventFilter{ 135 | selector: %ABI.FunctionSelector{ 136 | function: "TestEvent", 137 | input_names: ["numbers", "has_won"], 138 | inputs_indexed: [true, false], 139 | method_id: 140 | Ethers.Utils.hex_decode!( 141 | "0x586e63bbc89d8901dcdf36aacc9068837356d52ccafae5ecf041e3b03fc373c1" 142 | ), 143 | returns: [], 144 | state_mutability: nil, 145 | type: :event, 146 | types: [{:tuple, [{:uint, 256}, {:uint, 256}, {:uint, 256}]}, :bool] 147 | }, 148 | topics: [ 149 | "0x586e63bbc89d8901dcdf36aacc9068837356d52ccafae5ecf041e3b03fc373c1", 150 | "0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c" 151 | ], 152 | default_address: nil 153 | } == filter 154 | 155 | assert "#Ethers.EventFilter" == 156 | inspect(filter) 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /test/ethers/event_mixed_index_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.EventMixedIndexContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/event_mixed_index_abi.json" 4 | end 5 | 6 | defmodule Ethers.EventMixedIndexContractTest do 7 | use ExUnit.Case 8 | 9 | import Ethers.TestHelpers 10 | 11 | alias Ethers.Contract.Test.EventMixedIndexContract 12 | 13 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 14 | 15 | describe "event filters" do 16 | test "works with mixed indexed events" do 17 | assert %Ethers.EventFilter{ 18 | selector: %ABI.FunctionSelector{ 19 | function: "Transfer", 20 | method_id: 21 | Ethers.Utils.hex_decode!( 22 | "0x0f1459b71050cedb12633644ebaa16569e1bb49626ab8a0f4c7d1cf0d574abe7" 23 | ), 24 | type: :event, 25 | inputs_indexed: [false, true, false, true], 26 | state_mutability: nil, 27 | input_names: ["amount", "sender", "isFinal", "receiver"], 28 | types: [{:uint, 256}, :address, :bool, :address], 29 | returns: [] 30 | }, 31 | topics: [ 32 | "0x0f1459b71050cedb12633644ebaa16569e1bb49626ab8a0f4c7d1cf0d574abe7", 33 | "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", 34 | "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" 35 | ], 36 | default_address: nil 37 | } == EventMixedIndexContract.EventFilters.transfer(@from, @from) 38 | end 39 | 40 | test "can filter and show the correct events" do 41 | encoded_constructor = EventMixedIndexContract.constructor() 42 | 43 | assert {:ok, tx_hash} = 44 | Ethers.deploy(EventMixedIndexContract, 45 | encoded_constructor: encoded_constructor, 46 | from: @from 47 | ) 48 | 49 | wait_for_transaction!(tx_hash) 50 | 51 | assert {:ok, address} = Ethers.deployed_address(tx_hash) 52 | 53 | EventMixedIndexContract.transfer(100, @from, true, @from) 54 | |> Ethers.send_transaction!(to: address, from: @from) 55 | |> wait_for_transaction!() 56 | 57 | filter = EventMixedIndexContract.EventFilters.transfer(@from, @from) 58 | 59 | assert [ 60 | %Ethers.Event{ 61 | address: ^address, 62 | topics: ["Transfer(uint256,address,bool,address)", @from, @from], 63 | data: [100, true], 64 | removed: false, 65 | log_index: 0, 66 | transaction_index: 0, 67 | topics_raw: [ 68 | "0x0f1459b71050cedb12633644ebaa16569e1bb49626ab8a0f4c7d1cf0d574abe7", 69 | "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", 70 | "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" 71 | ], 72 | data_raw: 73 | "0x00000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000001" 74 | } 75 | ] = Ethers.get_logs!(filter) 76 | end 77 | 78 | test "inspect returns correct value" do 79 | assert ~s'#Ethers.EventFilter' == 80 | inspect( 81 | EventMixedIndexContract.EventFilters.transfer( 82 | "0x90f8bf6a479f320ead074411a4b0e7944ea80000", 83 | "0x90f8bf6a479f320ead074411a4b0e7944ea80001" 84 | ) 85 | ) 86 | 87 | assert ~s'#Ethers.EventFilter' == 88 | inspect( 89 | EventMixedIndexContract.EventFilters.transfer( 90 | nil, 91 | "0x90f8bf6a479f320ead074411a4b0e7944ea80001" 92 | ) 93 | ) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/ethers/event_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.EventTest do 2 | use ExUnit.Case 3 | alias Ethers.Event 4 | doctest Event 5 | 6 | describe "decode/2" do 7 | test "decode log with no data returns empty list" do 8 | selector = %ABI.FunctionSelector{ 9 | function: "Approval", 10 | method_id: <<140, 91, 225, 229>>, 11 | type: :event, 12 | inputs_indexed: [true, true, true], 13 | state_mutability: nil, 14 | input_names: ["owner", "spender", "value"], 15 | types: [:address, :address, {:uint, 256}], 16 | returns: [uint: 256] 17 | } 18 | 19 | assert %Ethers.Event{data: []} = 20 | Event.decode( 21 | %{ 22 | "address" => "0xaa107ccfe230a29c345fd97bc6eb9bd2fccd0750", 23 | "blockHash" => 24 | "0xe8885761ec559c5e267c48f44b4b12e4169f7d3a116f5e8f43314147722f0d83", 25 | "blockNumber" => "0x1138b39", 26 | "data" => "0x", 27 | "logIndex" => "0x1a1", 28 | "removed" => false, 29 | "topics" => [ 30 | "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", 31 | "0x00000000000000000000000023c5d7a16cf2e14a00f1c81be9443259f3cbc4ce", 32 | "0x0000000000000000000000000000000000000000000000000000000000000000", 33 | "0x0000000000000000000000000000000000000000000000000000000000000ef7" 34 | ], 35 | "transactionHash" => 36 | "0xf6e06e4f3fbd67088e8278843e55862957537760c63bae7b682a0e39da75b45d", 37 | "transactionIndex" => "0x83" 38 | }, 39 | selector 40 | ) 41 | end 42 | end 43 | 44 | describe "find_and_decode/2" do 45 | test "finds correct selector and decodes log" do 46 | assert {:ok, %Ethers.Event{data: [3831]}} = 47 | Event.find_and_decode( 48 | %{ 49 | "address" => "0xaa107ccfe230a29c345fd97bc6eb9bd2fccd0750", 50 | "blockHash" => 51 | "0xe8885761ec559c5e267c48f44b4b12e4169f7d3a116f5e8f43314147722f0d83", 52 | "blockNumber" => "0x1138b39", 53 | "data" => "0x0000000000000000000000000000000000000000000000000000000000000ef7", 54 | "logIndex" => "0x1a1", 55 | "removed" => false, 56 | "topics" => [ 57 | "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", 58 | "0x00000000000000000000000023c5d7a16cf2e14a00f1c81be9443259f3cbc4ce", 59 | "0x0000000000000000000000000000000000000000000000000000000000000000" 60 | ], 61 | "transactionHash" => 62 | "0xf6e06e4f3fbd67088e8278843e55862957537760c63bae7b682a0e39da75b45d", 63 | "transactionIndex" => "0x83" 64 | }, 65 | Ethers.Contracts.ERC20.EventFilters 66 | ) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/ethers/multi_arity_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.MultiArityContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/multi_arity_abi.json" 4 | end 5 | 6 | defmodule Ethers.MultiArityContractTest do 7 | use ExUnit.Case 8 | 9 | import Ethers.TestHelpers 10 | 11 | alias Ethers.Contract.Test.MultiArityContract 12 | 13 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 14 | 15 | describe "next function" do 16 | test "can override RPC client" do 17 | encoded_constructor = MultiArityContract.constructor() 18 | 19 | assert {:ok, tx_hash} = 20 | Ethers.deploy(MultiArityContract, 21 | encoded_constructor: encoded_constructor, 22 | from: @from 23 | ) 24 | 25 | wait_for_transaction!(tx_hash) 26 | 27 | assert {:ok, address} = Ethers.deployed_address(tx_hash) 28 | 29 | assert {:ok, 0} = MultiArityContract.next() |> Ethers.call(to: address) 30 | assert {:ok, 6} = MultiArityContract.next(5) |> Ethers.call(to: address) 31 | assert {:ok, 7} = MultiArityContract.next(6) |> Ethers.call(to: address) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/ethers/multi_clause_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.MultiClauseContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/multi_clause_abi.json" 4 | end 5 | 6 | defmodule Ethers.MultiClauseContractTest do 7 | use ExUnit.Case 8 | 9 | import Ethers.Types, only: [typed: 2] 10 | import Ethers.TestHelpers 11 | 12 | alias Ethers.Contract.Test.MultiClauseContract 13 | 14 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 15 | 16 | setup_all :deploy_multi_clause_contract 17 | 18 | describe "next function" do 19 | test "will raise on ambiguous arguments" do 20 | assert_raise ArgumentError, 21 | "Ambiguous parameters\n\n## Arguments\n[1]\n\n## Possible signatures\nsay(uint128 n)\nsay(uint8 n)\nsay(int256 n)\nsay(uint256 n)\n", 22 | fn -> 23 | MultiClauseContract.say(1) 24 | end 25 | end 26 | 27 | test "will raise on non matching arguments" do 28 | assert_raise ArgumentError, 29 | "No function selector matches current arguments!\n\n## Arguments\n[{:typed, {:uint, 64}, 1}]\n\n## Available signatures\nsay(address n)\nsay(uint128 n)\nsay(uint8 n)\nsay(int256 n)\nsay(uint256 n)\nsay(string n)\n", 30 | fn -> 31 | MultiClauseContract.say(typed({:uint, 64}, 1)) 32 | end 33 | end 34 | 35 | test "will work with typed arguments", %{address: address} do 36 | assert "uint256" == 37 | MultiClauseContract.say(typed({:uint, 256}, 101)) |> Ethers.call!(to: address) 38 | 39 | assert "int256" == 40 | MultiClauseContract.say(typed({:int, 256}, 101)) |> Ethers.call!(to: address) 41 | 42 | assert "int256" == 43 | MultiClauseContract.say(typed({:int, 256}, 101)) |> Ethers.call!(to: address) 44 | end 45 | end 46 | 47 | describe "smart function" do 48 | test "can deduce type based on properties", %{address: address} do 49 | assert "uint8" == MultiClauseContract.smart(255) |> Ethers.call!(to: address) 50 | assert "int8" == MultiClauseContract.smart(-1) |> Ethers.call!(to: address) 51 | end 52 | end 53 | 54 | describe "multi clause events" do 55 | test "listens on the right event", %{address: address} do 56 | MultiClauseContract.emit_event(typed({:uint, 256}, 10)) 57 | |> Ethers.send_transaction!(to: address, from: @from) 58 | |> wait_for_transaction!() 59 | 60 | uint_filter = MultiClauseContract.EventFilters.multi_event(typed({:uint, 256}, 10)) 61 | 62 | assert {:ok, [%Ethers.Event{address: ^address, topics: ["MultiEvent(uint256)", 10]}]} = 63 | Ethers.get_logs(uint_filter, address: address) 64 | 65 | MultiClauseContract.emit_event(typed({:int, 256}, -20)) 66 | |> Ethers.send_transaction!(to: address, from: @from) 67 | |> wait_for_transaction!() 68 | 69 | int_filter = MultiClauseContract.EventFilters.multi_event(typed({:int, 256}, -20)) 70 | 71 | assert {:ok, [%Ethers.Event{address: ^address, topics: ["MultiEvent(int256)", -20]}]} = 72 | Ethers.get_logs(int_filter, address: address) 73 | 74 | MultiClauseContract.emit_event("Hello") 75 | |> Ethers.send_transaction!(to: address, from: @from) 76 | |> wait_for_transaction!() 77 | 78 | string_filter = MultiClauseContract.EventFilters.multi_event(typed(:string, "Hello")) 79 | 80 | assert {:ok, [%Ethers.Event{address: ^address, topics: ["MultiEvent(string)", _]}]} = 81 | Ethers.get_logs(string_filter, address: address) 82 | 83 | string_filter = MultiClauseContract.EventFilters.multi_event(typed(:string, "Good Bye")) 84 | 85 | assert {:ok, []} = Ethers.get_logs(string_filter, address: address) 86 | end 87 | 88 | test "listens on the right event with nil values", %{address: address} do 89 | MultiClauseContract.emit_event(typed({:uint, 256}, 10)) 90 | |> Ethers.send_transaction!(to: address, from: @from) 91 | |> wait_for_transaction!() 92 | 93 | uint_filter = MultiClauseContract.EventFilters.multi_event(typed({:uint, 256}, nil)) 94 | 95 | assert {:ok, [%Ethers.Event{address: ^address, topics: ["MultiEvent(uint256)", 10]}]} = 96 | Ethers.get_logs(uint_filter, address: address) 97 | 98 | MultiClauseContract.emit_event(typed({:int, 256}, -20)) 99 | |> Ethers.send_transaction!(to: address, from: @from) 100 | |> wait_for_transaction!() 101 | 102 | int_filter = MultiClauseContract.EventFilters.multi_event(typed({:int, 256}, nil)) 103 | 104 | assert {:ok, [%Ethers.Event{address: ^address, topics: ["MultiEvent(int256)", -20]}]} = 105 | Ethers.get_logs(int_filter, address: address) 106 | end 107 | 108 | test "raises on conflicting parameters" do 109 | assert_raise ArgumentError, 110 | ~s'Ambiguous parameters\n\n## Arguments\n~c"\\n"\n\n## Possible signatures\nMultiEvent(uint256 n)\nMultiEvent(int256 n)\n', 111 | fn -> 112 | MultiClauseContract.EventFilters.multi_event(10) 113 | end 114 | end 115 | 116 | test "renders correct values when inspected" do 117 | uint_filter = MultiClauseContract.EventFilters.multi_event(typed({:uint, 256}, nil)) 118 | int_filter = MultiClauseContract.EventFilters.multi_event(-30) 119 | string_filter = MultiClauseContract.EventFilters.multi_event("value to filter") 120 | 121 | assert "#Ethers.EventFilter" == 122 | inspect(uint_filter) 123 | 124 | assert "#Ethers.EventFilter" == inspect(int_filter) 125 | 126 | assert ~s'#Ethers.EventFilter' == 127 | inspect(string_filter) 128 | end 129 | end 130 | 131 | defp deploy_multi_clause_contract(_ctx) do 132 | encoded_constructor = MultiClauseContract.constructor() 133 | 134 | address = deploy(MultiClauseContract, encoded_constructor: encoded_constructor, from: @from) 135 | 136 | [address: address] 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/ethers/name_service_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.NameServiceTest do 2 | use ExUnit.Case 3 | alias Ethers.NameService 4 | doctest NameService 5 | end 6 | -------------------------------------------------------------------------------- /test/ethers/owner_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.OwnerContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/owner_abi.json" 4 | end 5 | 6 | defmodule Ethers.OwnerContractTest do 7 | use ExUnit.Case 8 | doctest Ethers.Contract 9 | 10 | import Ethers.TestHelpers 11 | 12 | alias Ethers.Contract.Test.OwnerContract 13 | 14 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 15 | @sample_address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" 16 | 17 | test "can deploy and get owner" do 18 | encoded_constructor = OwnerContract.constructor(@sample_address) 19 | 20 | assert {:ok, tx_hash} = 21 | Ethers.deploy(OwnerContract, encoded_constructor: encoded_constructor, from: @from) 22 | 23 | wait_for_transaction!(tx_hash) 24 | 25 | assert {:ok, address} = Ethers.deployed_address(tx_hash) 26 | 27 | assert {:ok, @sample_address} = OwnerContract.get_owner() |> Ethers.call(to: address) 28 | end 29 | 30 | describe "overriding RPC options" do 31 | test "can override RPC client" do 32 | encoded_constructor = OwnerContract.constructor(@sample_address) 33 | 34 | assert {:ok, "tx_hash"} = 35 | Ethers.deploy(OwnerContract, 36 | encoded_constructor: encoded_constructor, 37 | from: @from, 38 | rpc_client: Ethers.TestRPCModule 39 | ) 40 | end 41 | 42 | test "can override RPC options" do 43 | encoded_constructor = OwnerContract.constructor(@sample_address) 44 | 45 | assert {:ok, tx_hash} = 46 | Ethers.deploy(OwnerContract, encoded_constructor: encoded_constructor, from: @from) 47 | 48 | wait_for_transaction!(tx_hash) 49 | 50 | assert {:ok, address} = Ethers.deployed_address(tx_hash) 51 | 52 | assert {:ok, @sample_address} = 53 | OwnerContract.get_owner() 54 | |> Ethers.call( 55 | to: address, 56 | rpc_client: Ethers.TestRPCModule, 57 | rpc_opts: [send_back_to_pid: self()] 58 | ) 59 | 60 | assert_receive :eth_call 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/ethers/pay_ether_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.PayEtherContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/pay_ether_abi.json" 4 | end 5 | 6 | defmodule Ethers.PayEtherContractTest do 7 | use ExUnit.Case 8 | 9 | import Ethers.TestHelpers 10 | 11 | alias Ethers.Contract.Test.PayEtherContract 12 | 13 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 14 | 15 | setup_all :deploy_pay_ether_contract 16 | 17 | describe "pay functions" do 18 | test "can pay payable functions", %{address: address} do 19 | assert {:ok, tx_hash} = 20 | PayEtherContract.pay_me() 21 | |> Ethers.send_transaction( 22 | to: address, 23 | value: Ethers.Utils.to_wei(1), 24 | from: @from, 25 | signer: Ethers.Signer.JsonRPC 26 | ) 27 | 28 | wait_for_transaction!(tx_hash) 29 | 30 | assert {:error, %{"code" => 3}} = 31 | PayEtherContract.dont_pay_me() 32 | |> Ethers.send_transaction( 33 | to: address, 34 | value: Ethers.Utils.to_wei(1), 35 | from: @from, 36 | signer: Ethers.Signer.JsonRPC 37 | ) 38 | end 39 | end 40 | 41 | def deploy_pay_ether_contract(_ctx) do 42 | address = 43 | deploy(PayEtherContract, encoded_constructor: PayEtherContract.constructor(), from: @from) 44 | 45 | [address: address] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/ethers/registry_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.RegistryContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/registry_abi.json" 4 | end 5 | 6 | defmodule Ethers.RegistryContractTest do 7 | use ExUnit.Case 8 | doctest Ethers.Contract 9 | 10 | import Ethers.TestHelpers 11 | 12 | alias Ethers.Contract.Test.RegistryContract 13 | 14 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 15 | @from1 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" 16 | @from2 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" 17 | 18 | setup :deploy_registry_contract 19 | 20 | describe "can send and receive structs" do 21 | test "can send transaction with structs", %{address: address} do 22 | RegistryContract.register({"alisina", 27}) 23 | |> Ethers.send_transaction!(from: @from, to: address) 24 | |> wait_for_transaction!() 25 | end 26 | 27 | test "can call functions returning structs", %{address: address} do 28 | {:ok, {"", 0}} = RegistryContract.info(@from) |> Ethers.call(to: address) 29 | 30 | RegistryContract.register({"alisina", 27}) 31 | |> Ethers.send_transaction!(from: @from, to: address) 32 | |> wait_for_transaction!() 33 | 34 | {:ok, {"alisina", 27}} = RegistryContract.info(@from) |> Ethers.call(to: address) 35 | end 36 | end 37 | 38 | describe "can handle tuples and arrays" do 39 | test "can call functions returning array of structs", %{address: address} do 40 | RegistryContract.register({"alisina", 27}) 41 | |> Ethers.send_transaction!(from: @from, to: address) 42 | |> wait_for_transaction!() 43 | 44 | RegistryContract.register({"bob", 13}) 45 | |> Ethers.send_transaction!(from: @from1, to: address) 46 | |> wait_for_transaction!() 47 | 48 | RegistryContract.register({"blaze", 37}) 49 | |> Ethers.send_transaction!(from: @from2, to: address) 50 | |> wait_for_transaction!() 51 | 52 | {:ok, [{"alisina", 27}, {"bob", 13}, {"blaze", 37}]} = 53 | RegistryContract.info_many([@from, @from1, @from2]) |> Ethers.call(to: address) 54 | end 55 | 56 | test "can call functions returning tuple", %{address: address} do 57 | RegistryContract.register({"alisina", 27}) 58 | |> Ethers.send_transaction!(from: @from, to: address) 59 | |> wait_for_transaction!() 60 | 61 | {:ok, ["alisina", 27]} = RegistryContract.info_as_tuple(@from) |> Ethers.call(to: address) 62 | end 63 | end 64 | 65 | describe "event filters" do 66 | test "can create event filters and fetch register events", %{address: address} do 67 | RegistryContract.register({"alisina", 27}) 68 | |> Ethers.send_transaction!(from: @from, to: address) 69 | |> wait_for_transaction!() 70 | 71 | empty_filter = RegistryContract.EventFilters.registered(nil) 72 | search_filter = RegistryContract.EventFilters.registered(@from) 73 | 74 | assert {:ok, [%Ethers.Event{address: ^address}]} = Ethers.get_logs(empty_filter) 75 | 76 | assert {:ok, 77 | [%{topics: ["Registered(address,(string,uint8))", @from], data: [{"alisina", 27}]}]} = 78 | Ethers.get_logs(search_filter) 79 | end 80 | 81 | test "does not return any events for a non existing contract", %{address: address} do 82 | RegistryContract.register({"alisina", 27}) 83 | |> Ethers.send_transaction!(from: @from, to: address) 84 | |> wait_for_transaction!() 85 | 86 | empty_filter = RegistryContract.EventFilters.registered(nil) 87 | 88 | assert {:ok, [%Ethers.Event{address: ^address}]} = 89 | Ethers.get_logs(empty_filter, address: address) 90 | 91 | assert {:ok, []} = Ethers.get_logs(empty_filter, address: @from) 92 | end 93 | end 94 | 95 | defp deploy_registry_contract(_ctx) do 96 | address = 97 | deploy(RegistryContract, encoded_constructor: RegistryContract.constructor(), from: @from) 98 | 99 | [address: address] 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/ethers/revert_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.RevertContractTest do 2 | use ExUnit.Case 3 | 4 | import Ethers.TestHelpers 5 | 6 | alias Ethers.Contract.Test.RevertContract 7 | alias Ethers.ExecutionError 8 | 9 | @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" 10 | 11 | setup_all :deploy_revert_contract 12 | 13 | describe "using require" do 14 | test "will cause a revert including revert message", %{address: address} do 15 | assert {:ok, true} = RevertContract.get(true) |> Ethers.call(to: address, from: @from) 16 | 17 | assert {:error, %{"message" => message}} = 18 | RevertContract.get(false) |> Ethers.call(to: address, from: @from) 19 | 20 | assert message =~ "success must be true" 21 | 22 | assert_raise Ethers.ExecutionError, 23 | ~r/execution reverted: (?:revert: )?success must be true/, 24 | fn -> 25 | RevertContract.get(false) |> Ethers.call!(to: address, from: @from) 26 | end 27 | end 28 | end 29 | 30 | describe "using revert" do 31 | test "will cause a revert including revert message", %{address: address} do 32 | assert {:error, %{"message" => message}} = 33 | RevertContract.reverting() |> Ethers.call(to: address, from: @from) 34 | 35 | assert message =~ "revert message" 36 | end 37 | end 38 | 39 | describe "using revert with error" do 40 | test "will cause a revert including revert message", %{address: address} do 41 | assert {:error, %RevertContract.Errors.RevertWithMessage{message: message}} = 42 | RevertContract.reverting_with_message() 43 | |> Ethers.call(to: address, from: @from) 44 | 45 | assert message =~ "this is sad!" 46 | end 47 | 48 | test "will raise an exception", %{address: address} do 49 | assert_raise ExecutionError, 50 | ~s'#Ethers.Error', 51 | fn -> 52 | RevertContract.reverting_with_message() 53 | |> Ethers.call!(to: address, from: @from) 54 | end 55 | end 56 | end 57 | 58 | defp deploy_revert_contract(_ctx) do 59 | address = 60 | deploy(RevertContract, encoded_constructor: RevertContract.constructor(), from: @from) 61 | 62 | [address: address] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/ethers/signer/json_rpc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Signer.JsonRPCTest do 2 | use ExUnit.Case 3 | 4 | alias Ethers.Signer 5 | alias Ethers.Utils 6 | 7 | describe "sign_transaction/2" do 8 | test "signs the transaction with the correct data" do 9 | transaction = %Ethers.Transaction.Eip1559{ 10 | to: "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", 11 | input: Utils.hex_decode!("0x06fdde03"), 12 | value: 0, 13 | chain_id: 31_337, 14 | nonce: 2918, 15 | gas: 23_170, 16 | max_fee_per_gas: 87_119_557_365, 17 | max_priority_fee_per_gas: 0 18 | } 19 | 20 | assert {:ok, 21 | "0x02f86f827a69820b6680851448baf2f5825a8294ffcf8fdee72ac11b5c542428b35eef5769c409f0808406fdde03c080a03d39a64cec141391314296113f494c750619792b845966975d5f9862307edd83a06027e4f44dceae37b773933587e68e5b3174cd490ba0e2f0628dc33eb5f53f97"} == 22 | Signer.JsonRPC.sign_transaction(transaction, []) 23 | end 24 | 25 | test "fails signing transaction with wrong from address" do 26 | transaction = %Ethers.Transaction.Eip1559{ 27 | to: "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", 28 | input: Utils.hex_decode!("0x06fdde03"), 29 | value: 0, 30 | chain_id: 31_337, 31 | nonce: 2918, 32 | gas: 23_170, 33 | max_fee_per_gas: 87_119_557_365, 34 | max_priority_fee_per_gas: 0 35 | } 36 | 37 | assert {:error, error} = 38 | Signer.JsonRPC.sign_transaction(transaction, 39 | from: "0xbba94ef8bd5ffee41947b4585a84bda5a3d3da6e" 40 | ) 41 | 42 | assert error["message"] =~ "No Signer available" 43 | end 44 | end 45 | 46 | describe "accounts/1" do 47 | test "returns account list" do 48 | assert {:ok, accounts} = Signer.JsonRPC.accounts([]) 49 | 50 | assert accounts == [ 51 | "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 52 | "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", 53 | "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", 54 | "0x90f79bf6eb2c4f870365e785982e1f101e93b906", 55 | "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65", 56 | "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc", 57 | "0x976ea74026e726554db657fa54763abd0c3a0aa9", 58 | "0x14dc79964da2c08b23698b3d3cc7ca32193d9955", 59 | "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", 60 | "0xa0ee7a142d267c1f36714e4a8f75612f20a79720" 61 | ] 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/ethers/signer/local_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Signer.LocalTest do 2 | use ExUnit.Case 3 | 4 | alias Ethers.Signer 5 | alias Ethers.Transaction.Eip1559 6 | alias Ethers.Utils 7 | 8 | @private_key "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 9 | 10 | describe "sign_transaction/2" do 11 | test "signs the transaction with the correct data" do 12 | transaction = %Eip1559{ 13 | chain_id: 1337, 14 | nonce: 2918, 15 | gas: 23_170, 16 | to: "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", 17 | value: 0, 18 | input: Utils.hex_decode!("0x06fdde03"), 19 | max_fee_per_gas: 87_119_557_365, 20 | max_priority_fee_per_gas: 0 21 | } 22 | 23 | assert {:ok, 24 | "0x02f86f820539820b6680851448baf2f5825a8294ffcf8fdee72ac11b5c542428b35eef5769c409f0808406fdde03c001a064b0b82fe12d59f11993ea978ef8595a4e21e1c2bb811b083ccb6eed230059fca025e4f674692eb3bbd57505d35a328855d4de4abef31fe26ab2e8eb543cfea285"} == 25 | Signer.Local.sign_transaction(transaction, private_key: @private_key) 26 | end 27 | end 28 | 29 | describe "accounts/1" do 30 | test "returns the correct address for a given private key as binary" do 31 | key = 32 | Ethers.Utils.hex_decode!( 33 | "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 34 | ) 35 | 36 | assert {:ok, ["0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"]} == 37 | Signer.Local.accounts(private_key: key) 38 | end 39 | 40 | test "returns the correct address for a given private key as hex" do 41 | assert {:ok, ["0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"]} == 42 | Signer.Local.accounts( 43 | private_key: "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 44 | ) 45 | 46 | assert {:ok, ["0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E"]} == 47 | Signer.Local.accounts( 48 | private_key: "0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4" 49 | ) 50 | 51 | assert {:ok, ["0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E"]} == 52 | Signer.Local.accounts( 53 | private_key: "829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4" 54 | ) 55 | end 56 | 57 | test "fails if private key is not given" do 58 | assert {:error, :no_private_key} == Signer.Local.accounts(other_opts: :ignore) 59 | end 60 | 61 | test "fails if private key is incorrect" do 62 | assert {:error, :invalid_private_key} == Signer.Local.accounts(private_key: "invalid") 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/ethers/tx_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.TxDataTest do 2 | use ExUnit.Case 3 | 4 | alias Ethers.TxData 5 | 6 | @function_selector %ABI.FunctionSelector{ 7 | function: "get", 8 | method_id: <<109, 76, 230, 60>>, 9 | type: :function, 10 | inputs_indexed: nil, 11 | state_mutability: :view, 12 | input_names: [], 13 | types: [], 14 | returns: [uint: 256], 15 | return_names: ["amount"] 16 | } 17 | 18 | describe "to_map/2" do 19 | test "converts a TxData to transaction map" do 20 | tx_data = TxData.new("0xffff", @function_selector, nil, nil) 21 | 22 | assert %{data: "0xffff"} == TxData.to_map(tx_data, []) 23 | end 24 | 25 | test "includes the default address if any" do 26 | tx_data = TxData.new("0xffff", @function_selector, "0xdefault", nil) 27 | 28 | assert %{data: "0xffff", to: "0xdefault"} == TxData.to_map(tx_data, []) 29 | end 30 | 31 | test "includes overrides in transaction map" do 32 | tx_data = TxData.new("0xffff", @function_selector, "0xdefault", nil) 33 | 34 | assert %{data: "0xffff", to: "0xdefault", from: "0xfrom"} == 35 | TxData.to_map(tx_data, from: "0xfrom") 36 | end 37 | 38 | test "can override default address" do 39 | tx_data = TxData.new("0xffff", @function_selector, "0xdefault", nil) 40 | 41 | assert %{data: "0xffff", to: "0xnotdefault"} == 42 | TxData.to_map(tx_data, to: "0xnotdefault") 43 | end 44 | 45 | test "integer overrides are converted to hex" do 46 | tx_data = TxData.new("0xffff", @function_selector, nil, nil) 47 | 48 | assert %{data: "0xffff", gas: 1} == 49 | TxData.to_map(tx_data, gas: 1) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/ethers/types_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.Contract.Test.TypesContract do 2 | @moduledoc false 3 | use Ethers.Contract, abi_file: "tmp/types_abi.json" 4 | end 5 | 6 | defmodule Ethers.TypesContractTest do 7 | use ExUnit.Case 8 | doctest Ethers.Contract 9 | 10 | import Ethers.TestHelpers 11 | 12 | alias Ethers.Contract.Test.TypesContract 13 | 14 | @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 15 | @sample_address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" 16 | 17 | setup_all :deploy_types_contract 18 | 19 | describe "encode/decode" do 20 | test "uint types", %{address: address} do 21 | assert {:ok, 100} = TypesContract.get_uint8(100) |> Ethers.call(to: address) 22 | assert {:ok, 100} = TypesContract.get_uint16(100) |> Ethers.call(to: address) 23 | assert {:ok, 100} = TypesContract.get_uint32(100) |> Ethers.call(to: address) 24 | assert {:ok, 100} = TypesContract.get_uint64(100) |> Ethers.call(to: address) 25 | assert {:ok, 100} = TypesContract.get_uint128(100) |> Ethers.call(to: address) 26 | assert {:ok, 100} = TypesContract.get_uint256(100) |> Ethers.call(to: address) 27 | end 28 | 29 | test "int types", %{address: address} do 30 | assert {:ok, -101} = TypesContract.get_int8(-101) |> Ethers.call(to: address) 31 | assert {:ok, -101} = TypesContract.get_int16(-101) |> Ethers.call(to: address) 32 | assert {:ok, -101} = TypesContract.get_int32(-101) |> Ethers.call(to: address) 33 | assert {:ok, -101} = TypesContract.get_int64(-101) |> Ethers.call(to: address) 34 | assert {:ok, -101} = TypesContract.get_int128(-101) |> Ethers.call(to: address) 35 | assert {:ok, -101} = TypesContract.get_int256(-101) |> Ethers.call(to: address) 36 | end 37 | 38 | test "boolean type", %{address: address} do 39 | assert {:ok, false} = TypesContract.get_bool(false) |> Ethers.call(to: address) 40 | assert {:ok, true} = TypesContract.get_bool(true) |> Ethers.call(to: address) 41 | end 42 | 43 | test "string type", %{address: address} do 44 | assert {:ok, "a string"} = 45 | TypesContract.get_string("a string") |> Ethers.call(to: address) 46 | 47 | assert {:ok, ""} = TypesContract.get_string("") |> Ethers.call(to: address) 48 | assert {:ok, <<0>>} = TypesContract.get_string(<<0>>) |> Ethers.call(to: address) 49 | end 50 | 51 | test "address type", %{address: address} do 52 | assert {:ok, @sample_address} = 53 | TypesContract.get_address(@sample_address) |> Ethers.call(to: address) 54 | end 55 | 56 | test "bytes type", %{address: address} do 57 | assert {:ok, <<0, 1, 2, 3>>} = 58 | TypesContract.get_bytes(<<0, 1, 2, 3>>) |> Ethers.call(to: address) 59 | 60 | assert {:ok, <<1234::1024>>} = 61 | TypesContract.get_bytes(<<1234::1024>>) |> Ethers.call(to: address) 62 | end 63 | 64 | test "fixed bytes type", %{address: address} do 65 | assert {:ok, <<1>>} = TypesContract.get_bytes1(<<1>>) |> Ethers.call(to: address) 66 | 67 | assert {:ok, <<1::20*8>>} = 68 | TypesContract.get_bytes20(<<1::20*8>>) |> Ethers.call(to: address) 69 | 70 | assert {:ok, <<1::32*8>>} = 71 | TypesContract.get_bytes32(<<1::32*8>>) |> Ethers.call(to: address) 72 | end 73 | 74 | test "struct type", %{address: address} do 75 | assert {:ok, {100, -101, @sample_address}} = 76 | TypesContract.get_struct({100, -101, @sample_address}) |> Ethers.call(to: address) 77 | end 78 | 79 | test "array type", %{address: address} do 80 | assert {:ok, [1, 2, -3]} = 81 | TypesContract.get_int256_array([1, 2, -3]) |> Ethers.call(to: address) 82 | 83 | assert {:ok, [100, 2, 4]} = 84 | TypesContract.get_fixed_uint_array([100, 2, 4]) |> Ethers.call(to: address) 85 | 86 | assert {:ok, [{5, -10, @sample_address}, {1, 900, @sample_address}]} = 87 | TypesContract.get_struct_array([ 88 | {5, -10, @sample_address}, 89 | {1, 900, @sample_address} 90 | ]) 91 | |> Ethers.call(to: address) 92 | end 93 | end 94 | 95 | defp deploy_types_contract(_ctx) do 96 | address = deploy(TypesContract, encoded_constructor: TypesContract.constructor(), from: @from) 97 | 98 | [address: address] 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/ethers/types_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.TypesTest do 2 | use ExUnit.Case 3 | alias Ethers.Types 4 | doctest Ethers.Types 5 | 6 | import ExUnit.CaptureLog 7 | 8 | describe "to_elixir_type" do 9 | test "emits warning in case of no type match" do 10 | assert capture_log(fn -> 11 | assert {:term, [], _} = Ethers.Types.to_elixir_type(:non_existing_type) 12 | end) =~ "Unknown type :non_existing_type" 13 | end 14 | 15 | test "does not support function type" do 16 | assert_raise RuntimeError, "Function type not supported!", fn -> 17 | Types.to_elixir_type(:function) 18 | end 19 | end 20 | end 21 | 22 | describe "valid_bitsize guard" do 23 | test "accepts all valid bitsizes" do 24 | Enum.reduce(8..256//8, 0, fn bitsize, last_uint_max -> 25 | uint_max = Types.max({:uint, bitsize}) 26 | int_max = Types.max({:int, bitsize}) 27 | assert uint_max > int_max 28 | assert uint_max > last_uint_max 29 | uint_max 30 | end) 31 | end 32 | 33 | test "raises on invalid bitsize" do 34 | assert_raise FunctionClauseError, fn -> Types.max({:uint, 9}) end 35 | end 36 | end 37 | 38 | describe "matches_type?/2" do 39 | test "works with uint" do 40 | assert Types.matches_type?(0, {:uint, 8}) 41 | assert Types.matches_type?(255, {:uint, 8}) 42 | refute Types.matches_type?(256, {:uint, 8}) 43 | refute Types.matches_type?(-1, {:uint, 8}) 44 | 45 | assert Types.matches_type?(Types.max({:uint, 256}), {:uint, 256}) 46 | refute Types.matches_type?(Types.max({:uint, 256}) + 1, {:uint, 256}) 47 | end 48 | 49 | test "works with int" do 50 | assert Types.matches_type?(0, {:int, 8}) 51 | assert Types.matches_type?(127, {:int, 8}) 52 | assert Types.matches_type?(-1, {:int, 8}) 53 | assert Types.matches_type?(-128, {:int, 8}) 54 | 55 | refute Types.matches_type?(128, {:int, 8}) 56 | refute Types.matches_type?(-129, {:int, 8}) 57 | 58 | assert Types.matches_type?(Types.max({:int, 256}), {:int, 256}) 59 | refute Types.matches_type?(Types.max({:int, 256}) + 1, {:int, 256}) 60 | 61 | assert Types.matches_type?(Types.min({:int, 256}), {:int, 256}) 62 | refute Types.matches_type?(Types.min({:int, 256}) - 1, {:int, 256}) 63 | end 64 | 65 | test "works with address" do 66 | assert Types.matches_type?("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", :address) 67 | refute Types.matches_type?("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20", :address) 68 | refute Types.matches_type?("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", :address) 69 | refute Types.matches_type?("0x0", :address) 70 | 71 | assert Types.matches_type?(:crypto.strong_rand_bytes(20), :address) 72 | end 73 | 74 | test "works with strings" do 75 | assert Types.matches_type?("hi", :string) 76 | assert Types.matches_type?("hi this is a longer string", :string) 77 | 78 | refute Types.matches_type?(<<0xFFFF::16>>, :string) 79 | refute Types.matches_type?(100, :string) 80 | end 81 | 82 | test "works with dynamic sized byte arrays" do 83 | assert Types.matches_type?(<<>>, :bytes) 84 | assert Types.matches_type?(<<1, 2, 3>>, :bytes) 85 | assert Types.matches_type?(:crypto.strong_rand_bytes(100), :bytes) 86 | 87 | refute Types.matches_type?(100, :bytes) 88 | end 89 | 90 | test "works with static sized byte arrays" do 91 | assert Types.matches_type?(<<1>>, {:bytes, 1}) 92 | assert Types.matches_type?(<<1, 2, 3>>, {:bytes, 3}) 93 | assert Types.matches_type?(:crypto.strong_rand_bytes(32), {:bytes, 32}) 94 | 95 | refute Types.matches_type?(<<>>, {:bytes, 1}) 96 | refute Types.matches_type?(<<1, 2>>, {:bytes, 1}) 97 | refute Types.matches_type?(<<1, 2, 3>>, {:bytes, 16}) 98 | refute Types.matches_type?(<<1, 2, 3>>, {:bytes, 32}) 99 | 100 | assert_raise(ArgumentError, "Invalid size: 0 (must be 1 <= size <= 32)", fn -> 101 | Types.matches_type?(<<>>, {:bytes, 0}) 102 | end) 103 | 104 | assert_raise(ArgumentError, "Invalid size: 33 (must be 1 <= size <= 32)", fn -> 105 | Types.matches_type?(<<>>, {:bytes, 33}) 106 | end) 107 | end 108 | 109 | test "works with booleans" do 110 | assert Types.matches_type?(true, :bool) 111 | assert Types.matches_type?(false, :bool) 112 | 113 | refute Types.matches_type?(nil, :bool) 114 | refute Types.matches_type?(0, :bool) 115 | refute Types.matches_type?(1, :bool) 116 | end 117 | 118 | test "works with dynamic length arrays" do 119 | assert Types.matches_type?([], {:array, :bool}) 120 | assert Types.matches_type?([true], {:array, :bool}) 121 | assert Types.matches_type?([false, true], {:array, :bool}) 122 | 123 | refute Types.matches_type?([0], {:array, :bool}) 124 | refute Types.matches_type?([true, 0], {:array, :bool}) 125 | end 126 | 127 | test "works with static length arrays" do 128 | assert Types.matches_type?([true], {:array, :bool, 1}) 129 | assert Types.matches_type?([false, true], {:array, :bool, 2}) 130 | 131 | refute Types.matches_type?([], {:array, :bool, 1}) 132 | refute Types.matches_type?([false, true], {:array, :bool, 3}) 133 | refute Types.matches_type?([true, 0], {:array, :bool, 2}) 134 | end 135 | 136 | test "works with tuples" do 137 | assert Types.matches_type?({true}, {:tuple, [:bool]}) 138 | assert Types.matches_type?({true, 100}, {:tuple, [:bool, {:uint, 256}]}) 139 | 140 | refute Types.matches_type?({true, -100}, {:tuple, [:bool, {:uint, 256}]}) 141 | refute Types.matches_type?([true, 100], {:tuple, [:bool, {:uint, 256}]}) 142 | end 143 | end 144 | 145 | describe "typed/2" do 146 | test "works with every type" do 147 | [ 148 | {{:uint, 256}, 200}, 149 | {{:int, 256}, -100}, 150 | {{:int, 256}, 100}, 151 | {{:bytes, 2}, <<1, 2>>}, 152 | {:bytes, <<1, 2, 3>>}, 153 | {:bool, true}, 154 | {:bool, false}, 155 | {:string, "hello"}, 156 | {{:array, :bool}, [true, false]}, 157 | {{:array, :bool, 2}, [true, false]}, 158 | {{:tuple, [:bool, :bool]}, {true, false}} 159 | ] 160 | |> Enum.each(fn {type, value} -> 161 | assert {:typed, type, value} == Types.typed(type, value) 162 | end) 163 | end 164 | 165 | test "works with nil values" do 166 | assert {:typed, _, nil} = Types.typed(:string, nil) 167 | assert {:typed, _, nil} = Types.typed({:uint, 256}, nil) 168 | end 169 | 170 | test "raises on type mismatch" do 171 | [ 172 | {{:uint, 16}, 100_000}, 173 | {{:uint, 16}, -1}, 174 | {{:int, 8}, -300}, 175 | {{:int, 8}, 256}, 176 | {{:bytes, 2}, <<1, 2, 3>>}, 177 | {:bytes, false}, 178 | {:bool, 1}, 179 | {:bool, 0}, 180 | {:string, <<0xFFFF::16>>}, 181 | {{:array, :bool}, [true, 1]}, 182 | {{:array, :bool, 2}, [true, false, false]}, 183 | {{:tuple, [:bool, :bool]}, {true, 1}}, 184 | {{:tuple, [:bool, :bool]}, {true, false, false}} 185 | ] 186 | |> Enum.each(fn {type, value} -> 187 | assert_raise ArgumentError, 188 | "Value #{inspect(value)} does not match type #{inspect(type)}", 189 | fn -> 190 | Types.typed(type, value) 191 | end 192 | end) 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/ethers/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ethers.UtilsTest do 2 | use ExUnit.Case 3 | alias Ethers.Utils 4 | doctest Ethers.Utils 5 | 6 | @rsk_mainnet_addresses [ 7 | "0x27b1FdB04752BBc536007A920D24ACB045561c26", 8 | "0x3599689E6292B81B2D85451025146515070129Bb", 9 | "0x42712D45473476B98452f434E72461577d686318", 10 | "0x52908400098527886E0F7030069857D2E4169ee7", 11 | "0x5aaEB6053f3e94c9b9a09f33669435E7ef1bEAeD", 12 | "0x6549F4939460DE12611948B3F82B88C3C8975323", 13 | "0x66F9664f97f2B50F62d13EA064982F936de76657", 14 | "0x8617E340b3D01Fa5f11f306f4090fd50E238070D", 15 | "0x88021160c5C792225E4E5452585947470010289d", 16 | "0xD1220A0Cf47c7B9BE7a2e6ba89F429762E7B9adB", 17 | "0xDBF03B407c01E7CD3cBea99509D93F8Dddc8C6FB", 18 | "0xDe709F2102306220921060314715629080e2FB77", 19 | "0xFb6916095cA1Df60bb79ce92cE3EA74c37c5d359" 20 | ] 21 | 22 | @rsk_testnet_addresses [ 23 | "0x27B1FdB04752BbC536007a920D24acB045561C26", 24 | "0x3599689e6292b81b2D85451025146515070129Bb", 25 | "0x42712D45473476B98452F434E72461577D686318", 26 | "0x52908400098527886E0F7030069857D2e4169EE7", 27 | "0x5aAeb6053F3e94c9b9A09F33669435E7EF1BEaEd", 28 | "0x6549f4939460dE12611948b3f82b88C3c8975323", 29 | "0x66f9664F97F2b50f62d13eA064982F936DE76657", 30 | "0x8617e340b3D01fa5F11f306F4090Fd50e238070d", 31 | "0x88021160c5C792225E4E5452585947470010289d", 32 | "0xd1220a0CF47c7B9Be7A2E6Ba89f429762E7b9adB", 33 | "0xdbF03B407C01E7cd3cbEa99509D93f8dDDc8C6fB", 34 | "0xDE709F2102306220921060314715629080e2Fb77", 35 | "0xFb6916095CA1dF60bb79CE92ce3Ea74C37c5D359" 36 | ] 37 | 38 | describe "get_block_timestamp" do 39 | test "returns the block timestamp" do 40 | assert {:ok, n} = Ethers.current_block_number() 41 | assert {:ok, t} = Utils.get_block_timestamp(n) 42 | assert is_integer(t) 43 | end 44 | 45 | test "can override the rpc opts" do 46 | assert {:ok, 500} = 47 | Utils.get_block_timestamp(100, 48 | rpc_client: Ethers.TestRPCModule, 49 | rpc_opts: [timestamp: 400] 50 | ) 51 | end 52 | end 53 | 54 | describe "date_to_block_number" do 55 | test "calculates the right block number for a given date" do 56 | assert {:ok, n} = Ethers.current_block_number() 57 | {:ok, t} = Utils.get_block_timestamp(n) 58 | 59 | assert {:ok, ^n} = Utils.date_to_block_number(t) 60 | assert {:ok, ^n} = Utils.date_to_block_number(t, n) 61 | assert {:ok, ^n} = Utils.date_to_block_number(t |> DateTime.from_unix!()) 62 | end 63 | 64 | test "can override the rpc opts" do 65 | assert {:ok, 1001} = 66 | Utils.date_to_block_number( 67 | 1000, 68 | nil, 69 | rpc_client: Ethers.TestRPCModule, 70 | rpc_opts: [timestamp: 111, block: "0x3E9"] 71 | ) 72 | 73 | assert {:ok, 1_693_699_010} = 74 | Utils.date_to_block_number( 75 | ~D[2023-09-03], 76 | nil, 77 | rpc_client: Ethers.TestRPCModule, 78 | rpc_opts: [timestamp: 123, block: "0x11e8fba"] 79 | ) 80 | end 81 | 82 | test "returns error for non existing blocks" do 83 | assert {:error, :no_block_found} = Utils.date_to_block_number(~D[2001-01-13]) 84 | end 85 | end 86 | 87 | describe "maybe_add_gas_limit" do 88 | test "adds gas limit to the transaction params" do 89 | assert {:ok, %{gas: gas}} = 90 | Ethers.Utils.maybe_add_gas_limit(%{ 91 | from: "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", 92 | to: "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0", 93 | value: 100_000_000_000_000_000 94 | }) 95 | 96 | assert is_binary(gas) 97 | assert Ethers.Utils.hex_to_integer!(gas) > 0 98 | end 99 | 100 | test "does not add anything if the params already includes gas" do 101 | assert {:ok, %{gas: 100}} = Ethers.Utils.maybe_add_gas_limit(%{gas: 100}) 102 | end 103 | end 104 | 105 | describe "hex_to_integer!" do 106 | test "raises when the hex input is invalid" do 107 | assert_raise ArgumentError, 108 | "Invalid integer HEX input \"0xrubbish\" reason :invalid_hex", 109 | fn -> Ethers.Utils.hex_to_integer!("0xrubbish") end 110 | end 111 | end 112 | 113 | describe "hex_decode!" do 114 | test "raises when the hex input is invalid" do 115 | assert_raise ArgumentError, 116 | "Invalid HEX input \"0xrubbish\"", 117 | fn -> Ethers.Utils.hex_decode!("0xrubbish") end 118 | end 119 | end 120 | 121 | describe "to_checksum_address/1" do 122 | test "converts address to checksum form" do 123 | assert Ethers.Utils.to_checksum_address("0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1") == 124 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 125 | 126 | assert Ethers.Utils.to_checksum_address("0x90F8BF6A479F320EAD074411A4B0E7944EA8C9C1") == 127 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 128 | end 129 | 130 | test "works with binary addresses" do 131 | bin_address = Ethers.Utils.hex_decode!("0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1") 132 | 133 | assert Ethers.Utils.to_checksum_address(bin_address) == 134 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 135 | end 136 | 137 | test "does erc-1191 checksum" do 138 | %{30 => @rsk_mainnet_addresses, 31 => @rsk_testnet_addresses} 139 | |> Enum.each(fn {chain_id, addresses} -> 140 | Enum.each(addresses, fn address -> 141 | assert Ethers.Utils.to_checksum_address(address, chain_id) == address 142 | end) 143 | end) 144 | end 145 | end 146 | 147 | describe "public_key_to_address/2" do 148 | @public_key "0x04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde2250d5f271f3563606672ebc45e0b7ea2e816ecb70ca03137b1c9476eec63d4632e990020b7b6fba39" 149 | test "converts public_key to address" do 150 | assert Ethers.Utils.public_key_to_address(@public_key) == 151 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 152 | 153 | assert Ethers.Utils.public_key_to_address(@public_key, false) == 154 | "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1" 155 | end 156 | 157 | test "works with binary public_key" do 158 | bin_public_key = Ethers.Utils.hex_decode!(@public_key) 159 | 160 | assert Ethers.Utils.public_key_to_address(bin_public_key) == 161 | "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/support/contracts.ex: -------------------------------------------------------------------------------- 1 | # Contracts which require compile time consolidation 2 | # (For now the ones testing Inspect protocol) 3 | 4 | defmodule Ethers.Contract.Test.RevertContract do 5 | @moduledoc false 6 | use Ethers.Contract, abi_file: "tmp/revert_abi.json" 7 | end 8 | 9 | defmodule Ethers.Contract.Test.CcipReadTestContract do 10 | @moduledoc false 11 | use Ethers.Contract, abi_file: "tmp/ccip_read_abi.json" 12 | end 13 | -------------------------------------------------------------------------------- /test/support/contracts/ccip_read.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract CcipReadTest { 5 | error OffchainLookup( 6 | address sender, 7 | string[] urls, 8 | bytes callData, 9 | bytes4 callbackFunction, 10 | bytes extraData 11 | ); 12 | 13 | error InvalidValue(); 14 | 15 | function getValue(uint256 value) external view returns (uint256) { 16 | string[] memory urls = new string[](3); 17 | urls[0] = "invalid://example.com/ccip/{sender}/{data}"; 18 | urls[1] = "https://example.com/ccip/{sender}/{data}"; 19 | urls[2] = "https://backup.example.com/ccip"; 20 | 21 | if (value == 0) { 22 | revert InvalidValue(); 23 | } 24 | 25 | revert OffchainLookup( 26 | address(this), 27 | urls, 28 | abi.encode(value), 29 | this.handleResponse.selector, 30 | bytes("testing") 31 | ); 32 | } 33 | 34 | function handleResponse(bytes calldata response, bytes calldata extraData) 35 | external 36 | pure 37 | returns (uint256) 38 | { 39 | // Validate extraData 40 | require(keccak256(abi.encodePacked(extraData)) == keccak256(bytes("testing"))); 41 | 42 | // Decode the response - in real contract you'd validate this 43 | return abi.decode(response, (uint256)); 44 | } 45 | 46 | // Helper function to test non-CCIP functionality 47 | function getDirectValue() external pure returns (string memory) { 48 | return "direct value"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/support/contracts/counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract Counter { 5 | uint256 public storeAmount; 6 | 7 | constructor(uint256 initialAmount) { 8 | storeAmount = initialAmount; 9 | } 10 | 11 | function get() public view returns (uint256 amount) { 12 | return storeAmount; 13 | } 14 | 15 | function getNoReturnName() public view returns (uint256) { 16 | return storeAmount; 17 | } 18 | 19 | function set(uint256 newAmount) public { 20 | emit SetCalled(storeAmount, newAmount); 21 | storeAmount = newAmount; 22 | } 23 | 24 | event SetCalled(uint256 indexed oldAmount, uint256 newAmount); 25 | } 26 | -------------------------------------------------------------------------------- /test/support/contracts/event_argument_types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract EventArgumentTypes { 5 | struct InnerType { 6 | uint256 number0; 7 | uint256 number1; 8 | uint256 number2; 9 | } 10 | 11 | event TestEvent(uint256[3] indexed numbers, bool has_won); 12 | event TestEvent(uint256[] indexed numbers, bool has_won); 13 | event TestEvent(InnerType indexed numbers, bool has_won); 14 | event TestEvent(string indexed numbers, bool has_won); 15 | event TestEvent(bytes indexed numbers, bool has_won); 16 | } 17 | -------------------------------------------------------------------------------- /test/support/contracts/event_mixed_index.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract EventMixedIndex { 5 | function transfer( 6 | uint256 amount, 7 | address sender, 8 | bool isFinal, 9 | address receiver 10 | ) public { 11 | emit Transfer(amount, sender, isFinal, receiver); 12 | } 13 | 14 | event Transfer( 15 | uint256 amount, 16 | address indexed sender, 17 | bool isFinal, 18 | address indexed receiver 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /test/support/contracts/hello_world.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract HelloWorld { 5 | string str = "Hello World!"; 6 | 7 | function sayHello() public view returns (string memory) { 8 | return str; 9 | } 10 | 11 | function setHello(string calldata message) external { 12 | emit HelloSet(message); 13 | str = message; 14 | } 15 | 16 | event HelloSet(string message); 17 | } 18 | -------------------------------------------------------------------------------- /test/support/contracts/multi_arity.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract MultiArity { 5 | function next() public pure returns (uint256) { 6 | return 0; 7 | } 8 | 9 | function next(uint256 _n) public pure returns (uint256) { 10 | return _n + 1; 11 | } 12 | 13 | event Next(int256 indexed n); 14 | event Next(string indexed n, string indexed b); 15 | } 16 | -------------------------------------------------------------------------------- /test/support/contracts/multi_clause.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract MultiClause { 5 | uint256 gn; 6 | 7 | function say(uint256 n) public pure returns (string memory) { 8 | require(n > 100); 9 | return "uint256"; 10 | } 11 | 12 | function say(int256 n) public pure returns (string memory) { 13 | require(n > 100); 14 | return "int256"; 15 | } 16 | 17 | function say(uint128 n) public pure returns (string memory) { 18 | require(n > 100); 19 | return "uint128"; 20 | } 21 | 22 | function say(address n) public pure returns (string memory) { 23 | require(n != address(0)); 24 | return "address"; 25 | } 26 | 27 | function say(string memory n) public pure returns (string memory) { 28 | require(bytes(n).length > 0); 29 | return "string"; 30 | } 31 | 32 | function say(uint8 n) public view returns (string memory) { 33 | if (n == gn) { 34 | return "uint8"; 35 | } else { 36 | revert("Impossible"); 37 | } 38 | } 39 | 40 | function smart(uint8 n) public pure returns (string memory) { 41 | require(n != 0); 42 | return "uint8"; 43 | } 44 | 45 | function smart(int8 n) public pure returns (string memory) { 46 | require(n != 0); 47 | return "int8"; 48 | } 49 | 50 | function emitEvent(uint256 n) public { 51 | emit MultiEvent(n); 52 | } 53 | 54 | function emitEvent(int256 n) public { 55 | emit MultiEvent(n); 56 | } 57 | 58 | function emitEvent(string calldata n) public { 59 | emit MultiEvent(n); 60 | } 61 | 62 | event MultiEvent(uint256 indexed n); 63 | event MultiEvent(int256 indexed n); 64 | event MultiEvent(string indexed n); 65 | } 66 | -------------------------------------------------------------------------------- /test/support/contracts/owner.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract Owner { 5 | address _owner; 6 | 7 | constructor(address owner) { 8 | _owner = owner; 9 | } 10 | 11 | function getOwner() public view returns (address) { 12 | return _owner; 13 | } 14 | 15 | error NotOwner(); 16 | 17 | function changeOwner(address newOwner) public { 18 | if (msg.sender != _owner) revert NotOwner(); 19 | _owner = newOwner; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/support/contracts/pay_ether.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract PayEther { 5 | function payMe() public payable {} 6 | 7 | function dontPayMe() public {} 8 | } 9 | -------------------------------------------------------------------------------- /test/support/contracts/registry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | struct RegisterParams { 5 | string name; 6 | uint8 age; 7 | } 8 | 9 | contract Registry { 10 | mapping(address => RegisterParams) registry; 11 | 12 | function register(RegisterParams memory params) public { 13 | registry[msg.sender] = params; 14 | emit Registered(msg.sender, params); 15 | } 16 | 17 | function info(address owner) public view returns (RegisterParams memory) { 18 | return registry[owner]; 19 | } 20 | 21 | function infoMany(address[] calldata owners) 22 | public 23 | view 24 | returns (RegisterParams[] memory) 25 | { 26 | RegisterParams[] memory params = new RegisterParams[](owners.length); 27 | 28 | for (uint256 i = 0; i < owners.length; i++) 29 | params[i] = registry[owners[i]]; 30 | 31 | return params; 32 | } 33 | 34 | function infoAsTuple(address owner) 35 | public 36 | view 37 | returns (string memory, uint8) 38 | { 39 | return (registry[owner].name, registry[owner].age); 40 | } 41 | 42 | event Registered(address indexed, RegisterParams); 43 | } 44 | -------------------------------------------------------------------------------- /test/support/contracts/revert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract Revert { 5 | function get(bool success) public pure returns (bool) { 6 | require(success, "success must be true"); 7 | return true; 8 | } 9 | 10 | function reverting() public pure { 11 | revert("revert message"); 12 | } 13 | 14 | error RevertError(); 15 | error RevertWithMessage(string message); 16 | error RevertWithUnnamedArg(uint8); 17 | 18 | function revertingWithError() public pure { 19 | revert RevertError(); 20 | } 21 | 22 | function revertingWithMessage() public pure { 23 | revert RevertWithMessage("this is sad!"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/support/contracts/types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | struct StructType { 5 | uint256 item1; 6 | int256 item2; 7 | address item3; 8 | } 9 | 10 | contract Types { 11 | // uint 12 | function getUint8(uint8 inp) public pure returns (uint8) { 13 | return inp; 14 | } 15 | 16 | function getUint16(uint16 inp) public pure returns (uint16) { 17 | return inp; 18 | } 19 | 20 | function getUint32(uint32 inp) public pure returns (uint32) { 21 | return inp; 22 | } 23 | 24 | function getUint64(uint64 inp) public pure returns (uint64) { 25 | return inp; 26 | } 27 | 28 | function getUint128(uint128 inp) public pure returns (uint128) { 29 | return inp; 30 | } 31 | 32 | function getUint256(uint256 inp) public pure returns (uint256) { 33 | return inp; 34 | } 35 | 36 | // int 37 | function getInt8(int8 inp) public pure returns (int8) { 38 | return inp; 39 | } 40 | 41 | function getInt16(int16 inp) public pure returns (int16) { 42 | return inp; 43 | } 44 | 45 | function getInt32(int32 inp) public pure returns (int32) { 46 | return inp; 47 | } 48 | 49 | function getInt64(int64 inp) public pure returns (int64) { 50 | return inp; 51 | } 52 | 53 | function getInt128(int128 inp) public pure returns (int128) { 54 | return inp; 55 | } 56 | 57 | function getInt256(int256 inp) public pure returns (int256) { 58 | return inp; 59 | } 60 | 61 | // bool 62 | function getBool(bool inp) public pure returns (bool) { 63 | return inp; 64 | } 65 | 66 | // string 67 | function getString(string memory str) public pure returns (string memory) { 68 | return str; 69 | } 70 | 71 | // address 72 | function getAddress(address addr) public pure returns (address) { 73 | return addr; 74 | } 75 | 76 | // array 77 | function getInt256Array(int256[] memory arr) 78 | public 79 | pure 80 | returns (int256[] memory) 81 | { 82 | return arr; 83 | } 84 | 85 | // fixed array 86 | function getFixedUintArray(uint256[3] memory inp) 87 | public 88 | pure 89 | returns (uint256[3] memory) 90 | { 91 | return inp; 92 | } 93 | 94 | // struct 95 | function getStruct(StructType memory inp) 96 | public 97 | pure 98 | returns (StructType memory) 99 | { 100 | return inp; 101 | } 102 | 103 | // array of structs 104 | function getStructArray(StructType[] memory inp) 105 | public 106 | pure 107 | returns (StructType[] memory) 108 | { 109 | return inp; 110 | } 111 | 112 | // bytes 113 | function getBytes(bytes memory inp) public pure returns (bytes memory) { 114 | return inp; 115 | } 116 | 117 | function getBytes1(bytes1 inp) public pure returns (bytes1) { 118 | return inp; 119 | } 120 | 121 | function getBytes20(bytes20 inp) public pure returns (bytes20) { 122 | return inp; 123 | } 124 | 125 | function getBytes32(bytes32 inp) public pure returns (bytes32) { 126 | return inp; 127 | } 128 | 129 | // Not implemented yet bo Solidity 130 | // // ufixed 131 | // function getUfixed(ufixed inp) public pure returns (ufixed) { 132 | // return inp; 133 | // } 134 | // 135 | // // fixed 136 | // function getFixed(fixed inp) public pure returns (fixed) { 137 | // return inp; 138 | // } 139 | } 140 | -------------------------------------------------------------------------------- /test/support/test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.TestHelpers do 2 | @moduledoc false 3 | 4 | @max_tries 5 5 | 6 | def wait_for_transaction!(tx_hash, opts \\ [], try_count \\ 0) 7 | 8 | def wait_for_transaction!(_tx_hash, _opts, @max_tries) do 9 | raise "Transaction was not found after #{@max_tries} tries" 10 | end 11 | 12 | def wait_for_transaction!(tx_hash, opts, try_count) do 13 | case Ethers.get_transaction_receipt(tx_hash, opts) do 14 | {:ok, _} -> 15 | :ok 16 | 17 | {:error, _} -> 18 | Process.sleep(try_count * 50) 19 | 20 | wait_for_transaction!(tx_hash, opts, try_count + 1) 21 | end 22 | end 23 | 24 | def deploy(bin_or_module, opts \\ []) do 25 | {:ok, tx_hash} = Ethers.deploy(bin_or_module, opts) 26 | wait_for_transaction!(tx_hash, opts) 27 | {:ok, address} = Ethers.deployed_address(tx_hash, opts) 28 | address 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/test_rpc_module.ex: -------------------------------------------------------------------------------- 1 | defmodule Ethers.TestRPCModule do 2 | @moduledoc false 3 | 4 | import Ethers.Utils 5 | 6 | def eth_estimate_gas(_params, _opts) do 7 | {:ok, "0x100"} 8 | end 9 | 10 | def eth_send_transaction(params, opts) do 11 | if pid = opts[:send_params_to_pid] do 12 | send(pid, params) 13 | end 14 | 15 | {:ok, opts[:tx_hash] || "tx_hash"} 16 | end 17 | 18 | def eth_send_raw_transaction(params, opts) do 19 | if pid = opts[:send_params_to_pid] do 20 | send(pid, params) 21 | end 22 | 23 | {:ok, opts[:tx_hash] || "tx_hash"} 24 | end 25 | 26 | def eth_call(params, block, opts) do 27 | if pid = opts[:send_back_to_pid] do 28 | send(pid, :eth_call) 29 | end 30 | 31 | Ethereumex.HttpClient.eth_call(params, block, opts) 32 | end 33 | 34 | def eth_block_number(opts) do 35 | {:ok, opts[:block]} 36 | end 37 | 38 | def eth_get_block_by_number(number, _include_all?, opts) do 39 | timestamp = 40 | number 41 | |> hex_to_integer!() 42 | |> Kernel.+(opts[:timestamp]) 43 | |> integer_to_hex() 44 | 45 | {:ok, %{"timestamp" => timestamp}} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) 2 | -------------------------------------------------------------------------------- /test/test_prepare.exs: -------------------------------------------------------------------------------- 1 | test_contracts_path = "test/support/contracts/" 2 | File.mkdir_p!("tmp") 3 | 4 | for file <- File.ls!(test_contracts_path) do 5 | [name, "sol"] = String.split(file, ".") 6 | 7 | {_, 0} = 8 | System.cmd( 9 | "/bin/bash", 10 | [ 11 | "-c", 12 | "solc #{Path.join(test_contracts_path, name)}.sol --combined-json abi,bin | jq \".contracts | to_entries | .[0].value\" > tmp/#{name}_abi.json" 13 | ] 14 | ) 15 | end 16 | --------------------------------------------------------------------------------