-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implement CCIP Read * Remove extra function * Improve Ethers.deploy/2 * Add tests for CCIP read * Update Changelog * Improve coverage
- Loading branch information
Showing
14 changed files
with
369 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import Config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
defmodule Ethers.CcipRead do | ||
@moduledoc """ | ||
CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) implementation | ||
NOTE: Currently supports URLs with "https" scheme only | ||
""" | ||
|
||
require Logger | ||
|
||
alias Ethers.Contracts.CcipRead.Errors.OffchainLookup | ||
alias Ethers.TxData | ||
alias Ethers.Utils | ||
|
||
@error_selector "0x556f1830" | ||
@error_selector_bin Utils.hex_decode!(@error_selector) | ||
@supported_schemas ["https"] | ||
|
||
@doc """ | ||
Same as `Ethers.call/2` but will handle `Ethers.Contacts.CcipRead.Errors.OffchainLookup` errors | ||
and performs an offchain lookup as per [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668) specs. | ||
## Options and Overrides | ||
Accepts same options as `Ethers.call/2` | ||
""" | ||
@spec call(TxData.t(), Keyword.t()) :: {:ok, [term()] | term()} | {:error, term()} | ||
def call(tx_data, opts) do | ||
case Ethers.call(tx_data, opts) do | ||
{:ok, result} -> | ||
{:ok, result} | ||
|
||
{:error, %_{} = error} -> | ||
if offchain_lookup_error?(error) do | ||
ccip_resolve(error, tx_data, opts) | ||
else | ||
{:error, error} | ||
end | ||
|
||
{:error, %{"data" => <<@error_selector, _::binary>> = error_data}} -> | ||
with {:ok, decoded_error} <- Utils.hex_decode(error_data), | ||
{:ok, lookup_error} <- OffchainLookup.decode(decoded_error) do | ||
ccip_resolve(lookup_error, tx_data, opts) | ||
end | ||
|
||
{:error, reason} -> | ||
{:error, reason} | ||
end | ||
end | ||
|
||
defp ccip_resolve(error, tx_data, opts) do | ||
with {:ok, data} <- | ||
error.urls | ||
|> Enum.filter(fn url -> | ||
url |> String.downcase() |> String.starts_with?(@supported_schemas) | ||
end) | ||
|> resolve_first(error) do | ||
data = ABI.TypeEncoder.encode([data, error.extra_data], [:bytes, :bytes]) | ||
tx_data = %{tx_data | data: Utils.hex_encode(error.callback_function <> data)} | ||
Ethers.call(tx_data, opts) | ||
end | ||
end | ||
|
||
defp resolve_first([], _), do: {:error, :ccip_read_failed} | ||
|
||
defp resolve_first([url | rest], error) do | ||
case do_resolve_single(url, error) do | ||
{:ok, data} -> | ||
{:ok, data} | ||
|
||
{:error, reason} -> | ||
Logger.error("CCIP READ: failed resolving #{url} error: #{inspect(reason)}") | ||
|
||
resolve_first(rest, error) | ||
end | ||
end | ||
|
||
defp do_resolve_single(url_template, error) do | ||
sender = Ethers.Utils.hex_encode(error.sender) | ||
data = Ethers.Utils.hex_encode(error.call_data) | ||
|
||
req_opts = | ||
if String.contains?(url_template, "{data}") do | ||
[method: :get] | ||
else | ||
[method: :post, json: %{data: data, sender: sender}] | ||
end | ||
|
||
url = url_template |> String.replace("{sender}", sender) |> String.replace("{data}", data) | ||
req_opts = req_opts |> Keyword.put(:url, url) |> Keyword.merge(ccip_req_opts()) | ||
|
||
Logger.debug("CCIP READ: trying #{url}") | ||
|
||
case Req.request(req_opts) do | ||
{:ok, %Req.Response{status: 200, body: %{"data" => data}}} -> | ||
case Utils.hex_decode(data) do | ||
{:ok, hex} -> {:ok, hex} | ||
:error -> {:error, :hex_decode_failed} | ||
end | ||
|
||
{:ok, resp} -> | ||
{:error, resp} | ||
|
||
{:error, reason} -> | ||
{:error, reason} | ||
end | ||
end | ||
|
||
defp offchain_lookup_error?(%mod{}) do | ||
mod.function_selector().method_id == @error_selector_bin | ||
rescue | ||
UndefinedFunctionError -> | ||
false | ||
end | ||
|
||
defp ccip_req_opts do | ||
Application.get_env(:ethers, :ccip_req_opts, []) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
defmodule Ethers.Contracts.CcipRead do | ||
@moduledoc """ | ||
CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) contract | ||
""" | ||
|
||
use Ethers.Contract, abi: :ccip_read | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
[ | ||
{ | ||
"inputs": [ | ||
{ | ||
"internalType": "address", | ||
"name": "sender", | ||
"type": "address" | ||
}, | ||
{ | ||
"internalType": "string[]", | ||
"name": "urls", | ||
"type": "string[]" | ||
}, | ||
{ | ||
"internalType": "bytes", | ||
"name": "callData", | ||
"type": "bytes" | ||
}, | ||
{ | ||
"internalType": "bytes4", | ||
"name": "callbackFunction", | ||
"type": "bytes4" | ||
}, | ||
{ | ||
"internalType": "bytes", | ||
"name": "extraData", | ||
"type": "bytes" | ||
} | ||
], | ||
"name": "OffchainLookup", | ||
"type": "error" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
defmodule Ethers.CcipReadTest do | ||
use ExUnit.Case | ||
|
||
import Ethers.TestHelpers | ||
|
||
alias Ethers.CcipRead | ||
alias Ethers.Contract.Test.CcipReadTestContract | ||
alias Ethers.Utils | ||
|
||
@from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" | ||
|
||
setup do | ||
address = deploy(CcipReadTestContract, from: @from) | ||
[address: address] | ||
end | ||
|
||
describe "call/2" do | ||
test "returns successful result when no offchain lookup is needed", %{address: address} do | ||
assert {:ok, "direct value"} = | ||
CcipReadTestContract.get_direct_value() | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "handles OffchainLookup error and performs offchain lookup", %{address: address} do | ||
Req.Test.expect(Ethers.CcipReq, fn conn -> | ||
assert ["ccip", sender, data] = conn.path_info | ||
|
||
# Verify the request parameters | ||
assert String.starts_with?(sender, "0x") | ||
assert String.starts_with?(data, "0x") | ||
|
||
Req.Test.json(conn, %{data: data}) | ||
end) | ||
|
||
assert {:ok, 100} = | ||
CcipReadTestContract.get_value(100) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "filters out non-https URLs from the lookup list", %{address: address} do | ||
# The contract provides both https and non-https URLs | ||
# Our implementation should only try the https ones | ||
Req.Test.expect(Ethers.CcipReq, fn conn -> | ||
assert conn.scheme == :https | ||
assert ["ccip", _sender, data] = conn.path_info | ||
Req.Test.json(conn, %{data: data}) | ||
end) | ||
|
||
assert {:ok, 300} = | ||
CcipReadTestContract.get_value(300) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "tries next URL when first URL fails", %{address: address} do | ||
# First request fails | ||
Req.Test.expect(Ethers.CcipReq, 2, fn conn -> | ||
if conn.host == "example.com" do | ||
conn | ||
|> Plug.Conn.put_status(500) | ||
|> Req.Test.json(%{data: "0x"}) | ||
else | ||
# Second URL succeeds | ||
Req.Test.json(conn, %{ | ||
data: ABI.TypeEncoder.encode([700], [{:uint, 256}]) |> Utils.hex_encode() | ||
}) | ||
end | ||
end) | ||
|
||
assert {:ok, 700} = | ||
CcipReadTestContract.get_value(400) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "returns error when all URLs fail", %{address: address} do | ||
# Both URLs fail | ||
Req.Test.stub(Ethers.CcipReq, fn conn -> | ||
Plug.Conn.put_status(conn, 500) | ||
|> Req.Test.text("Failed") | ||
end) | ||
|
||
assert {:error, :ccip_read_failed} = | ||
CcipReadTestContract.get_value(500) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "returns error when response is not 200", %{address: address} do | ||
Req.Test.stub(Ethers.CcipReq, fn conn -> | ||
conn | ||
|> Plug.Conn.put_status(404) | ||
|> Req.Test.json(%{error: "Not found"}) | ||
end) | ||
|
||
assert {:error, :ccip_read_failed} = | ||
CcipReadTestContract.get_value(600) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "returns error when response body is invalid", %{address: address} do | ||
Req.Test.stub(Ethers.CcipReq, fn conn -> | ||
conn | ||
|> Plug.Conn.put_status(200) | ||
|> Req.Test.text("invalid json") | ||
end) | ||
|
||
assert {:error, :ccip_read_failed} = | ||
CcipReadTestContract.get_value(700) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "returns error when hex decoding fails", %{address: address} do | ||
Req.Test.stub(Ethers.CcipReq, fn conn -> | ||
Req.Test.json(conn, %{data: "invalid hex"}) | ||
end) | ||
|
||
assert {:error, :ccip_read_failed} = | ||
CcipReadTestContract.get_value(800) | ||
|> CcipRead.call(to: address) | ||
end | ||
|
||
test "returns original error when it's not an OffchainLookup error", %{address: address} do | ||
assert {:error, %Ethers.Contract.Test.CcipReadTestContract.Errors.InvalidValue{}} = | ||
CcipReadTestContract.get_value(0) | ||
|> CcipRead.call(to: address) | ||
|
||
# Sending value to a non-payable function should return the original error | ||
assert {:error, %{"code" => 3}} = | ||
CcipReadTestContract.get_value(1) | ||
|> CcipRead.call(to: address, value: 1000) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.