├── .tool-versions ├── test ├── test_helper.exs ├── personal_type_test.exs ├── compact_signature_test.exs ├── real_world_cases │ ├── ox_protocol_test.exs │ └── seaport_protocol_test.exs ├── signed_type_test.exs └── ex_web3_ec_recover_test.exs ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── js_reference ├── tsconfig.json ├── src │ ├── index.ts │ ├── examples.ts │ └── logged_typed_data_encoder.ts ├── package.json ├── .gitignore └── package-lock.json ├── .gitignore ├── lib ├── ex_web3_ec_recover │ ├── signature.ex │ ├── compact_signature.ex │ ├── signed_type │ │ ├── encoder │ │ │ ├── binary_encoder.ex │ │ │ └── hexstring_encoder.ex │ │ └── message.ex │ ├── personal_type.ex │ ├── recover_signature.ex │ └── signed_type.ex └── ex_web3_ec_recover.ex ├── LICENSE ├── .pre-commit-config.yaml ├── mix.exs ├── README.md ├── mix.lock └── .credo.exs /.tool-versions: -------------------------------------------------------------------------------- 1 | rust 1.57 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/personal_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.PersonalTypeTest do 2 | use ExUnit.Case 3 | 4 | doctest ExWeb3EcRecover.PersonalType 5 | end 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "mix" 4 | directory: "/" 5 | open-pull-requests-limit: 30 6 | schedule: 7 | interval: "daily" 8 | time: "03:37" # UTC 9 | -------------------------------------------------------------------------------- /js_reference/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "module": "nodenext", 6 | "esModuleInterop": true, 7 | }, 8 | "include": ["src"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /js_reference/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, providers } from "ethers" 2 | import { allExamples, basicString } from "./examples"; 3 | const devWallet = new Wallet("73c05f0c50ad607da9ce8dac4c39bead67f58cf52f3fcaf5e59befddb152ac74") 4 | const referencePublicAddress = "0x5FF3cb18d8866541C66e4A346767a10480c4278D" 5 | 6 | 7 | 8 | console.log(allExamples(devWallet)) -------------------------------------------------------------------------------- /js_reference/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsreference", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "dependencies": { 7 | "ethers": "^5.7.2" 8 | }, 9 | "devDependencies": { 10 | "@tsconfig/node16": "^1.0.3", 11 | "@types/node": "^18.11.11", 12 | "ts-node": "^10.9.1", 13 | "typescript": "^4.9.3" 14 | }, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /js_reference/.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist/**/* 39 | 40 | # ignore yarn.lock 41 | yarn.lock 42 | -------------------------------------------------------------------------------- /.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 | ex_web3_ec_recover-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/signature.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.Signature do 2 | @moduledoc false 3 | @enforce_keys [:r, :s, :v_num] 4 | defstruct @enforce_keys 5 | 6 | @type t :: %__MODULE__{ 7 | r: binary(), 8 | s: binary(), 9 | v_num: 0 | 1 10 | } 11 | 12 | def from_hexstring("0x" <> signature) when byte_size(signature) == 130 do 13 | # 65 bytes of data, each byte takes two bytes in hexstring 14 | sig_binary = Base.decode16!(signature, case: :lower) 15 | 16 | r = binary_part(sig_binary, 0, 32) 17 | s = binary_part(sig_binary, 32, 32) 18 | 19 | v_num = 20 | binary_part(sig_binary, 64, 1) 21 | |> :binary.decode_unsigned() 22 | 23 | v_num = if v_num >= 27, do: v_num - 27, else: v_num 24 | 25 | %__MODULE__{ 26 | r: r, 27 | s: s, 28 | v_num: v_num 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hawku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/compact_signature.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.CompactSignature do 2 | @moduledoc false 3 | @enforce_keys [:r, :y_parity_and_s] 4 | defstruct @enforce_keys 5 | 6 | @type t :: %__MODULE__{ 7 | r: binary(), 8 | y_parity_and_s: binary() 9 | } 10 | 11 | @spec from_hexstring(binary()) :: ExWeb3EcRecover.CompactSignature.t() 12 | def from_hexstring("0x" <> signature) when byte_size(signature) == 64 do 13 | <> = Base.decode16!(signature) 14 | 15 | %__MODULE__{ 16 | r: r, 17 | y_parity_and_s: y_parity_and_s 18 | } 19 | end 20 | 21 | @spec from_canonical(ExWeb3EcRecover.Signature.t()) :: ExWeb3EcRecover.CompactSignature.t() 22 | def from_canonical(%ExWeb3EcRecover.Signature{} = sig) do 23 | y_parity = sig.v_num 24 | <> = sig.s 25 | y_parity_and_s = Bitwise.bor(Bitwise.bsl(y_parity, 255), s) 26 | 27 | %__MODULE__{ 28 | r: sig.r, 29 | y_parity_and_s: <> 30 | } 31 | end 32 | 33 | @spec serialize(__MODULE__.t()) :: binary() 34 | def serialize(%__MODULE__{} = cs) do 35 | cs.r <> cs.y_parity_and_s 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/signed_type/encoder/binary_encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.SignedType.BinaryEncoder do 2 | @moduledoc """ 3 | This module defines an encoder that expects all binaries to be 4 | to be parsed Elixir binaries and not strings. 5 | """ 6 | 7 | @behaviour ExWeb3EcRecover.SignedType.Encoder 8 | 9 | def encode_value(type, value) when type in ["bytes", "string"], do: ExKeccak.hash_256(value) 10 | 11 | def encode_value("int" <> bytes_length, value), 12 | do: encode_value_atomic("int", bytes_length, value) 13 | 14 | def encode_value("uint" <> bytes_length, value), 15 | do: encode_value_atomic("uint", bytes_length, value) 16 | 17 | def encode_value("bytes" <> bytes_length, value), 18 | do: encode_value_atomic("bytes", bytes_length, value) 19 | 20 | def encode_value("bool", value), 21 | do: ABI.TypeEncoder.encode_raw([value], [:bool]) 22 | 23 | def encode_value("address", value), 24 | do: ABI.TypeEncoder.encode_raw([value], [:address]) 25 | 26 | def encode_value_atomic(type, bytes_length, value) do 27 | case Integer.parse(bytes_length) do 28 | {number, ""} -> 29 | ABI.TypeEncoder.encode_raw([value], [{String.to_existing_atom(type), number}]) 30 | 31 | :error -> 32 | raise "Malformed type `#{type}` in types, with value #{value}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | ### START BACKEND pre-commit ### 12 | - repo: local 13 | hooks: 14 | - id: check-deps 15 | name: Check mix.lock for divergences 16 | always_run: false 17 | pass_filenames: false 18 | language: system 19 | entry: "mix deps.get" 20 | - id: mix-format 21 | name: Ensure the code is properly formatted 22 | always_run: false 23 | pass_filenames: false 24 | language: system 25 | files: \.exs?$ 26 | entry: "mix format" 27 | - id: mix-compile 28 | name: Check whether compiler emits no warnings 29 | always_run: false 30 | pass_filenames: false 31 | language: system 32 | entry: "mix compile --force --warnings-as-errors" 33 | - id: mix-credo 34 | name: Checks for credo warnings 35 | always_run: false 36 | pass_filenames: false 37 | language: system 38 | entry: "mix credo --strict" 39 | ### END BACKEND pre-commit ### 40 | -------------------------------------------------------------------------------- /test/compact_signature_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.CompactSignatureTest do 2 | @moduledoc false 3 | # Test data provided by https://eips.ethereum.org/EIPS/eip-2098#example-implementation-in-python 4 | use ExUnit.Case, async: true 5 | 6 | test "parity 0" do 7 | sig = 8 | "0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b907e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea520641b" 9 | 10 | canon = ExWeb3EcRecover.Signature.from_hexstring(sig) 11 | result = ExWeb3EcRecover.CompactSignature.from_canonical(canon) 12 | 13 | assert Base.encode16(result.r, case: :lower) == 14 | "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90" 15 | 16 | assert Base.encode16(result.y_parity_and_s, case: :lower) == 17 | "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064" 18 | end 19 | 20 | test "parity 1" do 21 | sig = 22 | "0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f5507931c" 23 | 24 | canon = ExWeb3EcRecover.Signature.from_hexstring(sig) 25 | result = ExWeb3EcRecover.CompactSignature.from_canonical(canon) 26 | 27 | assert Base.encode16(result.r, case: :lower) == 28 | "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76" 29 | 30 | assert Base.encode16(result.y_parity_and_s, case: :lower) == 31 | "939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | name: "ex_web3_ec_recover", 7 | source_url: "https://github.com/hawku-com/ex_web3_ec_recover", 8 | docs: [ 9 | # The main page in the docs 10 | main: "ExWeb3EcRecover", 11 | extras: ["README.md"] 12 | ], 13 | app: :ex_web3_ec_recover, 14 | version: "0.6.0", 15 | elixir: "~> 1.12", 16 | start_permanent: Mix.env() == :prod, 17 | description: description(), 18 | package: package(), 19 | deps: deps() 20 | ] 21 | end 22 | 23 | # Run "mix help compile.app" to learn about applications. 24 | def application do 25 | [ 26 | extra_applications: [:logger] 27 | ] 28 | end 29 | 30 | defp description() do 31 | "Library for recovering web3 ETH signatures." 32 | end 33 | 34 | defp package() do 35 | [ 36 | name: "ex_web3_ec_recover", 37 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 38 | maintainers: ["Hawku, Inc,", "Charlie Graham", "Hajto"], 39 | licenses: ["MIT"], 40 | links: %{"GitHub" => "https://github.com/hawku-com/ex_web3_ec_recover"} 41 | ] 42 | end 43 | 44 | # Run "mix help deps" to learn about dependencies. 45 | defp deps do 46 | [ 47 | {:ex_secp256k1, "~> 0.3"}, 48 | {:ex_keccak, "~> 0.3"}, 49 | {:ex_doc, "~> 0.24", only: :dev, runtime: false}, 50 | {:ex_abi, "~> 0.5"}, 51 | {:credo, "~> 1.6.1", only: [:dev, :test], runtime: false}, 52 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/personal_type.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.PersonalType do 2 | @moduledoc """ 3 | This modules implements behaviour described in 4 | [EIP 191](https://eips.ethereum.org/EIPS/eip-191) 5 | 6 | Check `create_hash_from_personal_message/1` for details. 7 | """ 8 | 9 | @doc """ 10 | Creates a has of personal message hash. 11 | 12 | If `message` is a hex string it will parse the string and if it is a binary it 13 | it will be encoded directly. 14 | 15 | ## Examples 16 | 17 | iex> ExWeb3EcRecover.PersonalType.create_hash_from_personal_message( 18 | ...> "hello world" 19 | ...> ) 20 | <<217, 235, 161, 110, 208, 236, 174, 67, 43, 113, 254, 0, 140, 152, 204, 135, 21 | 43, 180, 204, 33, 77, 50, 32, 163, 111, 54, 83, 38, 207, 128, 125, 104>> 22 | 23 | iex> ExWeb3EcRecover.PersonalType.create_hash_from_personal_message( 24 | ...> "0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f" 25 | ...> ) 26 | <<69, 174, 138, 44, 5, 119, 42, 3, 176, 234, 146, 150, 249, 229, 84, 19, 27 | 196,150, 121, 92, 232, 51, 41, 58, 31, 82, 183, 223, 101, 32, 68, 206>> 28 | 29 | """ 30 | @spec create_hash_from_personal_message(String.t() | binary()) :: binary() 31 | def create_hash_from_personal_message(message) 32 | 33 | def create_hash_from_personal_message("0x" <> message) do 34 | message 35 | |> Base.decode16!(case: :mixed) 36 | |> create_hash_from_personal_message() 37 | end 38 | 39 | def create_hash_from_personal_message(orig_message) do 40 | ("\u0019Ethereum Signed Message:\n#{byte_size(orig_message)}" <> orig_message) 41 | |> ExKeccak.hash_256() 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | env: 12 | MIX_ENV: test 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - pair: 18 | elixir: '1.13' 19 | otp: '24.3' 20 | lint: lint 21 | - pair: 22 | elixir: '1.13' 23 | otp: '25.0' 24 | lint: lint 25 | 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - uses: erlef/setup-beam@v1 32 | with: 33 | otp-version: ${{matrix.pair.otp}} 34 | elixir-version: ${{matrix.pair.elixir}} 35 | 36 | - uses: actions/cache@v2 37 | with: 38 | path: | 39 | deps 40 | _build 41 | key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}- 44 | 45 | - name: Run mix deps.get 46 | run: mix deps.get --only test 47 | 48 | - name: Run mix format 49 | run: mix format --check-formatted 50 | if: ${{ matrix.lint }} 51 | 52 | - name: Run mix deps.compile 53 | run: mix deps.compile 54 | 55 | - name: Run mix compile 56 | run: mix compile --warnings-as-errors 57 | if: ${{ matrix.lint }} 58 | 59 | - name: Run credo 60 | run: mix credo --strict 61 | if: ${{ matrix.lint }} 62 | 63 | - name: Run mix test 64 | run: mix test 65 | 66 | - name: Run dialyzer 67 | run: mix dialyzer 68 | if: ${{ matrix.lint }} 69 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/signed_type/encoder/hexstring_encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.SignedType.HexStringEncoder do 2 | @moduledoc """ 3 | This module defines an encoder that expects all binaries to be 4 | to be hexstrings. 5 | """ 6 | 7 | @behaviour ExWeb3EcRecover.SignedType.Encoder 8 | 9 | def encode_value("string", value), do: ExKeccak.hash_256(value) 10 | 11 | def encode_value("bytes", value) do 12 | value 13 | |> ExWeb3EcRecover.parse_hex() 14 | |> ExKeccak.hash_256() 15 | end 16 | 17 | def encode_value("int" <> bytes_length, value) when is_number(value), 18 | do: encode_value_atomic("int", bytes_length, value) 19 | 20 | def encode_value("uint" <> bytes_length, value) when is_number(value), 21 | do: encode_value_atomic("uint", bytes_length, value) 22 | 23 | def encode_value("int" <> bytes_length, value) when is_binary(value), 24 | do: encode_value_atomic("int", bytes_length, String.to_integer(value)) 25 | 26 | def encode_value("uint" <> bytes_length, value) when is_binary(value), 27 | do: encode_value_atomic("uint", bytes_length, String.to_integer(value)) 28 | 29 | def encode_value("bytes" <> bytes_length, value) do 30 | value = ExWeb3EcRecover.parse_hex(value) 31 | encode_value_atomic("bytes", bytes_length, value) 32 | end 33 | 34 | def encode_value("bool", value), 35 | do: ABI.TypeEncoder.encode_raw([value], [:bool]) 36 | 37 | def encode_value("address", value) do 38 | value = ExWeb3EcRecover.parse_hex(value) 39 | ABI.TypeEncoder.encode_raw([value], [:address]) 40 | end 41 | 42 | def encode_value_atomic(type, bytes_length, value) do 43 | case Integer.parse(bytes_length) do 44 | {number, ""} -> 45 | ABI.TypeEncoder.encode_raw([value], [{String.to_existing_atom(type), number}]) 46 | 47 | :error -> 48 | raise "Malformed type `#{type}` in types, with value #{value}" 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExWeb3EcRecover 2 | 3 | Library for recovering and verifying public keys from signatures, 4 | 5 | ## Installation 6 | 7 | This package relies on `ExSecp256k1` which uses Rust. 8 | Please visit [rusts website](https://www.rust-lang.org/tools/install) and install it. 9 | 10 | The package can be installed 11 | by adding `ex_web3_ec_recover` to your list of dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:ex_web3_ec_recover, "~> 0.2.0"} 17 | ] 18 | end 19 | ``` 20 | 21 | 22 | ## Usage 23 | 24 | 25 | ### Personal Sign 26 | ```elixir 27 | iex> ExWeb3EcRecover.recover_personal_signature("hello world", "0x1dd3657c91d95f350ab25f17ee7cbcdbccd3f5bc52976bfd4dd03bd6bc29d2ac23e656bee509ca33b921e0e6b53eb64082be1bb3c69c3a4adccd993b1d667f8d1b") 28 | "0xb117a8bc3ecf2c3f006b89da6826e49b4193977a" 29 | 30 | ``` 31 | 32 | ### Typed Sign (Types 3 and 4) 33 | 34 | 35 | ``` 36 | 37 | iex> types: %{ 38 | "Message" => [%{"name" => "data", "type" => "string"}], 39 | "EIP712Domain" => [ 40 | %{ 41 | "name" => "name", 42 | "type" => "string" 43 | }, 44 | %{ 45 | "name" => "version", 46 | "type" => "string" 47 | }, 48 | %{ 49 | "name" => "chainId", 50 | "type" => "uint256" 51 | }, 52 | %{ 53 | "name" => "verifyingContract", 54 | "type" => "address" 55 | } 56 | ] 57 | }, 58 | primary_type: "Message", 59 | message: %{ 60 | "data" => "test" 61 | }, 62 | domain: %{ 63 | "name" => "example.metamask.io", 64 | "version" => "3", 65 | "chainId" => 1, 66 | "verifyingContract" => "0x" 67 | } 68 | } 69 | iex> ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 70 | "0x29c76e6ad8f28bb1004902578fb108c507be341b" 71 | 72 | ``` 73 | 74 | 75 | ## Documentation 76 | Hosted on [https://hexdocs.pm/ex_web3_ec_recover/ExWeb3EcRecover.html](https://hexdocs.pm/ex_web3_ec_recover/ExWeb3EcRecover.html) 77 | 78 | ## Authors 79 | Charlie Graham & Jakub Hajto (Hawku, Inc) 80 | 81 | ## Releases 82 | 0.2 Support for Typed Signatures (EIP 712) and signatures using Ledger devices 83 |
0.1 Initial Release 84 | 85 | 86 | ExWeb3EcRecover is released under the [MIT License](https://github.com/appcues/exsentry/blob/master/LICENSE.txt). 87 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover do 2 | @moduledoc """ 3 | Documentation for `ExWeb3RecoverSignature`. 4 | """ 5 | 6 | @doc """ 7 | Returns the address that created the signature for a personal signed message on the ETH network. 8 | Useful for checking metamask signatures. Raises an error if sig is invalid. 9 | 10 | ## Examples 11 | 12 | iex> ExWeb3EcRecover.recover_personal_signature( 13 | ...> "hello world", 14 | ...> "0x1dd3657c91d95f350ab25f17ee7cbcdbccd3f5bc52976bfd4dd03bd6bc29d2" <> 15 | ...> "ac23e656bee509ca33b921e0e6b53eb64082be1bb3c69c3a4adccd993b1d667f" <> 16 | ...> "8d1b" 17 | ...> ) 18 | "0xb117a8bc3ecf2c3f006b89da6826e49b4193977a" 19 | 20 | iex> ExWeb3EcRecover.recover_personal_signature( 21 | ...> "0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f", 22 | ...> "0x9ff8350cc7354b80740a3580d0e0fd4f1f02062040bc06b893d70906f872" <> 23 | ...> "8bb5163837fd376bf77ce03b55e9bd092b32af60e86abce48f7b8d3539988e" <> 24 | ...> "e5a9be1c" 25 | ...> ) 26 | "0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb" 27 | 28 | 29 | """ 30 | require Logger 31 | 32 | def recover_personal_signature(message, sig_hexstring) do 33 | ExWeb3EcRecover.RecoverSignature.recover_personal_signature(message, sig_hexstring) 34 | end 35 | 36 | @doc """ 37 | Returns the address that created the signature for a typed structured data signed message on the 38 | ETH network. Useful for checking metamask signatures. Raises an error if sig is invalid. 39 | 40 | ## Examples 41 | 42 | ``` 43 | sig = "0xf75d91c136214ad9d73b4117109982ac905d0e90b5fff7c69ba59dba0669e56"<> 44 | "922cc936feb67993627b56542d138e151de0e196962e38aabf834b002b01592211c" 45 | 46 | message = ExWeb3EcRecover.Message.from_map(raw_message) 47 | 48 | ExWeb3EcRecover.recover_typed_signature( 49 | message, 50 | sig, 51 | :v4 52 | ) 53 | ``` 54 | """ 55 | defdelegate recover_typed_signature(message, sig, version), 56 | to: ExWeb3EcRecover.RecoverSignature 57 | 58 | @doc """ 59 | This function transforms 0x prefixed hex string into an elixir binary 60 | 61 | ## Examples 62 | 63 | iex> ExWeb3EcRecover.parse_hex("0x00") 64 | <<0>> 65 | """ 66 | def parse_hex(hex_string) do 67 | hex_string 68 | |> String.trim_leading("0x") 69 | |> Base.decode16!(case: :mixed) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/recover_signature.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.RecoverSignature do 2 | @moduledoc false 3 | 4 | @prefix_1901 Base.decode16!("1901") 5 | @eip712 "EIP712Domain" 6 | @domain_type %{ 7 | "EIP712Domain" => [ 8 | %{"name" => "name", "type" => "string"}, 9 | %{"name" => "version", "type" => "string"}, 10 | %{"name" => "chainId", "type" => "uint256"}, 11 | %{"name" => "verifyingContract", "type" => "address"} 12 | ] 13 | } 14 | 15 | @allowed_versions [:v4] 16 | 17 | alias ExWeb3EcRecover.PersonalType 18 | alias ExWeb3EcRecover.Signature 19 | alias ExWeb3EcRecover.SignedType 20 | alias ExWeb3EcRecover.SignedType.Message 21 | 22 | defguard is_valid_signature?(signature) 23 | when byte_size(signature) == 132 and binary_part(signature, 0, 2) == "0x" 24 | 25 | def encode_eip712(message) do 26 | domain_separator = 27 | case message.domain do 28 | "0x" <> domain_separator -> 29 | Base.decode16!(domain_separator, case: :mixed) 30 | 31 | domain_data when is_map(domain_data) -> 32 | SignedType.hash_message(message.domain, @domain_type, @eip712) 33 | end 34 | 35 | message_hash = SignedType.hash_message(message.message, message.types, message.primary_type) 36 | 37 | [ 38 | @prefix_1901, 39 | domain_separator, 40 | message_hash 41 | ] 42 | |> :erlang.iolist_to_binary() 43 | end 44 | 45 | def hash_eip712(message) do 46 | message 47 | |> encode_eip712() 48 | |> ExKeccak.hash_256() 49 | end 50 | 51 | def recover_typed_signature(message, sig, version) 52 | 53 | def recover_typed_signature(%Message{} = message, sig, version) 54 | when version in @allowed_versions and is_valid_signature?(sig) do 55 | hash_eip712(message) 56 | |> do_recover_sig(sig) 57 | end 58 | 59 | def recover_typed_signature(_message, _sig, version) when version not in @allowed_versions, 60 | do: {:error, :unsupported_version} 61 | 62 | def recover_typed_signature(_message, _sig, _version), do: {:error, :invalid_signature} 63 | 64 | def recover_personal_signature(message, sig) 65 | when is_valid_signature?(sig) do 66 | message 67 | |> PersonalType.create_hash_from_personal_message() 68 | |> do_recover_sig(sig) 69 | end 70 | 71 | def recover_personal_signature(_message, _sig), do: {:error, :invalid_signature} 72 | 73 | defp do_recover_sig(hash, sig_hexstring) do 74 | sig = Signature.from_hexstring(sig_hexstring) 75 | 76 | hash 77 | |> ExSecp256k1.recover(sig.r, sig.s, sig.v_num) 78 | |> case do 79 | {:ok, recovered_key} -> get_address_from_recovered_key(recovered_key) 80 | {:error, _reason} = error -> error 81 | end 82 | end 83 | 84 | defp get_address_from_recovered_key(key) do 85 | address = 86 | key 87 | |> binary_part(1, 64) 88 | |> ExKeccak.hash_256() 89 | |> Base.encode16(case: :lower) 90 | |> String.slice(-40..-1) 91 | 92 | "0x#{address}" 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/signed_type/message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.SignedType.Message do 2 | @moduledoc """ 3 | This module represents a message data structure that is used 4 | to sign and recover, based on EIP 712. 5 | 6 | ## Domain 7 | For details look at this [website](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). 8 | 9 | ``` 10 | - string name the user readable name of signing domain, i.e. the name of the DApp or the protocol. 11 | - string version the current major version of the signing domain. Signatures from different versions are not compatible. 12 | - uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain. 13 | - address verifyingContract the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention. 14 | - bytes32 salt an disambiguating salt for the protocol. This can be used as a domain separator of last resort. 15 | ``` 16 | Source: https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator 17 | 18 | ## Types 19 | This field contains types available to be used by `message` field. 20 | 21 | It should contain a list of maps containing `name` and `type` values. 22 | For list of available types consult [EIP website](https://eips.ethereum.org/EIPS/eip-712#specification). 23 | 24 | ### Example 25 | 26 | ``` 27 | %{ 28 | "Message" => [ 29 | %{"name" => "data", "type" => "Child"}, 30 | %{"name" => "intData", "type" => "int8"}, 31 | %{"name" => "uintData", "type" => "uint8"}, 32 | %{"name" => "bytesData", "type" => "bytes3"}, 33 | %{"name" => "boolData", "type" => "bool"}, 34 | %{"name" => "addressData", "type" => "address"} 35 | ], 36 | "Child" => [%{"name" => "data", "type" => "GrandChild"}], 37 | "GrandChild" => [%{"name" => "data", "type" => "string"}] 38 | } 39 | ``` 40 | 41 | ## Primary type 42 | A string designating the root type. The root of the message is expected to be of the `primary_type`. 43 | 44 | ## Message 45 | Message is map with the data that will be used to build data structure to be encoded. 46 | """ 47 | 48 | @enforce_keys [:domain, :message, :types, :primary_type] 49 | defstruct @enforce_keys 50 | 51 | @type t :: %__MODULE__{ 52 | domain: map() | binary(), 53 | message: map(), 54 | types: map(), 55 | primary_type: String.t() 56 | } 57 | 58 | @doc """ 59 | This function generates a struct representing a message. 60 | 61 | Check module doc for details. 62 | """ 63 | @spec from_map(message :: map()) :: {:ok, t()} | :error 64 | def from_map(%{ 65 | "domain" => domain, 66 | "message" => message, 67 | "types" => types, 68 | "primaryType" => primary_type 69 | }) do 70 | data = %__MODULE__{ 71 | domain: domain, 72 | message: message, 73 | types: types, 74 | primary_type: primary_type 75 | } 76 | 77 | {:ok, data} 78 | end 79 | 80 | def from_map(_), do: :error 81 | end 82 | -------------------------------------------------------------------------------- /test/real_world_cases/ox_protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.RealWorldCases.OxProtocolTest do 2 | @moduledoc """ 3 | This module contains tests that validate the project against 4 | 0x Protocol. 5 | 6 | https://github.com/0xProject/0x-protocol-specification/blob/master/v3/v3-specification.md 7 | """ 8 | use ExUnit.Case, async: true 9 | 10 | alias ExWeb3EcRecover.SignedType.Message 11 | 12 | # https://github.com/0xProject/0x-protocol-specification/blob/master/v3/v3-specification.md#order-message-format 13 | test "Order message support" do 14 | message = %Message{ 15 | types: %{ 16 | "EIP712Domain" => [ 17 | %{"name" => "name", "type" => "string"}, 18 | %{"name" => "version", "type" => "string"}, 19 | %{"name" => "chainId", "type" => "uint256"}, 20 | %{"name" => "verifyingContract", "type" => "address"} 21 | ], 22 | "Order" => [ 23 | %{"name" => "makerAddress", "type" => "address"}, 24 | %{"name" => "takerAddress", "type" => "address"}, 25 | %{"name" => "feeRecipientAddress", "type" => "address"}, 26 | %{"name" => "senderAddress", "type" => "address"}, 27 | %{"name" => "makerAssetAmount", "type" => "uint256"}, 28 | %{"name" => "takerAssetAmount", "type" => "uint256"}, 29 | %{"name" => "makerFee", "type" => "uint256"}, 30 | %{"name" => "takerFee", "type" => "uint256"}, 31 | %{"name" => "expirationTimeSeconds", "type" => "uint256"}, 32 | %{"name" => "salt", "type" => "uint256"}, 33 | %{"name" => "makerAssetData", "type" => "bytes"}, 34 | %{"name" => "takerAssetData", "type" => "bytes"}, 35 | %{"name" => "makerFeeAssetData", "type" => "bytes"}, 36 | %{"name" => "takerFeeAssetData", "type" => "bytes"} 37 | ] 38 | }, 39 | primary_type: "Order", 40 | domain: %{ 41 | "name" => "0x Protocol", 42 | "version" => "3.0.0", 43 | "chainId" => "137", 44 | "verifyingContract" => "0xfede379e48c873c75f3cc0c81f7c784ad730a8f7" 45 | }, 46 | message: %{ 47 | "makerAddress" => "0x1bbeb0a1a075d870bed8c21dfbe49a37015e4124", 48 | "takerAddress" => "0x0000000000000000000000000000000000000000", 49 | "senderAddress" => "0x0000000000000000000000000000000000000000", 50 | "feeRecipientAddress" => "0x0000000000000000000000000000000000000000", 51 | "expirationTimeSeconds" => "1641635545", 52 | "salt" => "1", 53 | "makerAssetAmount" => "1", 54 | "takerAssetAmount" => "50000000000000000", 55 | "makerAssetData" => 56 | "02571792000000000000000000000000a5f1ea7df861952863df2e8d1312f7305d" <> 57 | "abf215000000000000000000000000000000000000000000000000000000000000" <> 58 | "2b5b", 59 | "takerAssetData" => 60 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 61 | "takerFeeAssetData" => "0x", 62 | "makerFeeAssetData" => "0x", 63 | "takerFee" => "0", 64 | "makerFee" => "0" 65 | } 66 | } 67 | 68 | sig = 69 | "0x16818763816e1aae13ee603e677cfc79e50909518bf0941ff9ed5a8e74b7b4ee50" <> 70 | "820810b3598f6d5bd90db7dd43e8992a628c1b003d13c86c0b2a3a2cde67531b" 71 | 72 | target = "0x29c76e6ad8f28bb1004902578fb108c507be341b" 73 | assert target == ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /js_reference/src/examples.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "ethers" 2 | import {TypedDataEncoder} from "./logged_typed_data_encoder" 3 | 4 | const defaultDomain = { 5 | "name": "example.metamask.io", 6 | "version": "4", 7 | "chainId": 1, 8 | "verifyingContract": "0x0000000000000000000000000000000000000000" 9 | } 10 | 11 | export const basicString = async (wallet: Wallet) =>{ 12 | const types = { 13 | "Message": [{"name": "data", "type": "string"}] 14 | } 15 | 16 | return wallet._signTypedData(defaultDomain, types, {data: "test"}); 17 | } 18 | 19 | export const zeroX = async (wallet: Wallet) => { 20 | const types = { 21 | "Order": [ 22 | {"name": "makerAddress", "type": "address"}, 23 | {"name": "takerAddress", "type": "address"}, 24 | {"name": "feeRecipientAddress", "type": "address"}, 25 | {"name": "senderAddress", "type": "address"}, 26 | {"name": "makerAssetAmount", "type": "uint256"}, 27 | {"name": "takerAssetAmount", "type": "uint256"}, 28 | {"name": "makerFee", "type": "uint256"}, 29 | {"name": "takerFee", "type": "uint256"}, 30 | {"name": "expirationTimeSeconds", "type": "uint256"}, 31 | {"name": "salt", "type": "uint256"}, 32 | {"name": "makerAssetData", "type": "bytes"}, 33 | {"name": "takerAssetData", "type": "bytes"}, 34 | {"name": "makerFeeAssetData", "type": "bytes"}, 35 | {"name": "takerFeeAssetData", "type": "bytes"}, 36 | ] 37 | } 38 | const data = { 39 | "makerAddress": "0x1bbeb0a1a075d870bed8c21dfbe49a37015e4124", 40 | "takerAddress": "0x0000000000000000000000000000000000000000", 41 | "senderAddress": "0x0000000000000000000000000000000000000000", 42 | "feeRecipientAddress": "0x0000000000000000000000000000000000000000", 43 | "expirationTimeSeconds": "1641635545", 44 | "salt": "1", 45 | "makerAssetAmount": "1", 46 | "takerAssetAmount": "50000000000000000", 47 | "makerAssetData": 48 | "0x02571792000000000000000000000000a5f1ea7df861952863df2e8d1312f7305dabf2150000000000000000000000000000000000000000000000000000000000002b5b", 49 | "takerAssetData": 50 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 51 | "takerFeeAssetData": "0x", 52 | "makerFeeAssetData": "0x", 53 | "takerFee": "0", 54 | "makerFee": "0" 55 | } 56 | const domain = { 57 | "name": "0x Protocol", 58 | "version": "3.0.0", 59 | "chainId": "137", 60 | "verifyingContract": "0xfede379e48c873c75f3cc0c81f7c784ad730a8f7" 61 | } 62 | return wallet._signTypedData(domain, types, data); 63 | } 64 | 65 | export const encodeBasicDataTypes = () => { 66 | const data = { 67 | "data": "test", 68 | "data1": "2", 69 | "data2": "3", 70 | "data3": Uint8Array.from(Buffer.from("c3f426ae", 'hex')), 71 | "data4": false, 72 | "data5": "0x5FF3cb18d8866541C66e4A346767a10480c4278D" 73 | } 74 | const types = { 75 | "Message": [ 76 | {"name": "data", "type": "string"}, 77 | {"name": "data1", "type": "int8"}, 78 | {"name": "data2", "type": "uint8"}, 79 | {"name": "data3", "type": "bytes4"}, 80 | {"name": "data4", "type": "bool"}, 81 | {"name": "data5", "type": "address"} 82 | ] 83 | } 84 | console.log(TypedDataEncoder.encode({},types, data)) 85 | } 86 | 87 | export const allExamples = async (wallet: Wallet) => { 88 | encodeBasicDataTypes(); 89 | console.log( await Promise.all([ 90 | basicString(wallet), 91 | zeroX(wallet) 92 | ])) 93 | } -------------------------------------------------------------------------------- /test/real_world_cases/seaport_protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.RealWorldCases.SeaportProtocolTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | 5 | alias ExWeb3EcRecover.SignedType.Message 6 | 7 | test "Order message support" do 8 | message = %Message{ 9 | types: %{ 10 | "ConsiderationItem" => [ 11 | %{"name" => "itemType", "type" => "uint8"}, 12 | %{"name" => "token", "type" => "address"}, 13 | %{"name" => "identifierOrCriteria", "type" => "uint256"}, 14 | %{"name" => "startAmount", "type" => "uint256"}, 15 | %{"name" => "endAmount", "type" => "uint256"}, 16 | %{"name" => "recipient", "type" => "address"} 17 | ], 18 | "OfferItem" => [ 19 | %{"name" => "itemType", "type" => "uint8"}, 20 | %{"name" => "token", "type" => "address"}, 21 | %{"name" => "identifierOrCriteria", "type" => "uint256"}, 22 | %{"name" => "startAmount", "type" => "uint256"}, 23 | %{"name" => "endAmount", "type" => "uint256"} 24 | ], 25 | "OrderComponents" => [ 26 | %{"name" => "offerer", "type" => "address"}, 27 | %{"name" => "zone", "type" => "address"}, 28 | %{"name" => "offer", "type" => "OfferItem[]"}, 29 | %{"name" => "consideration", "type" => "ConsiderationItem[]"}, 30 | %{"name" => "orderType", "type" => "uint8"}, 31 | %{"name" => "startTime", "type" => "uint256"}, 32 | %{"name" => "endTime", "type" => "uint256"}, 33 | %{"name" => "zoneHash", "type" => "bytes32"}, 34 | %{"name" => "salt", "type" => "uint256"}, 35 | %{"name" => "conduitKey", "type" => "bytes32"}, 36 | %{"name" => "counter", "type" => "uint256"} 37 | ] 38 | }, 39 | primary_type: "OrderComponents", 40 | domain: %{ 41 | "chainId" => 137, 42 | "name" => "Seaport", 43 | "verifyingContract" => "0x00000000006c3852cbef3e08e8df289169ede581", 44 | "version" => "1.1" 45 | }, 46 | message: %{ 47 | "conduitKey" => "0x0000000000000000000000000000000000000000000000000000000000000000", 48 | "consideration" => [ 49 | %{ 50 | "endAmount" => 10_000_000_000_000_000, 51 | "identifierOrCriteria" => 0, 52 | "itemType" => 0, 53 | "recipient" => "0x0ca98211D33567153d8e7316ddfa34359f9a40F9", 54 | "startAmount" => 10_000_000_000_000_000, 55 | "token" => "0x0000000000000000000000000000000000000000" 56 | } 57 | ], 58 | "counter" => 0, 59 | "endTime" => 60 | 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935, 61 | "offer" => [ 62 | %{ 63 | "endAmount" => 1, 64 | "identifierOrCriteria" => 423_875, 65 | "itemType" => 2, 66 | "startAmount" => 1, 67 | "token" => "0x67f4732266c7300cca593c814d46bee72e40659f" 68 | } 69 | ], 70 | "offerer" => "0x0ca98211D33567153d8e7316ddfa34359f9a40F9", 71 | "orderType" => 0, 72 | "salt" => 9_602_137_361_397_918_250, 73 | "startTime" => 1_673_439_577, 74 | "zone" => "0x0000000000000000000000000000000000000000", 75 | "zoneHash" => "0x3000000000000000000000000000000000000000000000000000000000000000" 76 | } 77 | } 78 | 79 | # Generated by the metamask, with Domain separator chopped off 80 | assert ExWeb3EcRecover.RecoverSignature.encode_eip712(message) |> Base.encode16(case: :lower) == 81 | "19019b0651fb24301f8cb24693ccaf43dc1cb69134121e8741c4159b3e1dd40c457cbc25b1f9454d96c5c5313be1a7414f48d92b0e37927bd115b9734fc7c3f426ae" 82 | 83 | assert ExWeb3EcRecover.RecoverSignature.hash_eip712(message) |> Base.encode16(case: :lower) == 84 | "c2411dfff82a30b43480466e11b75e98a947e2bbc0c1f689fa31fa7c7c40e607" 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/ex_web3_ec_recover/signed_type.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.SignedType do 2 | @moduledoc """ 3 | This module was written based on nomenclature 4 | and algorithm specified in the [EIP-712](https://eips.ethereum.org/EIPS/eip-712#specification) 5 | """ 6 | 7 | defmodule Encoder do 8 | @moduledoc false 9 | @callback encode_value(value :: any(), type :: String.t()) :: binary() 10 | end 11 | 12 | @default_encoder __MODULE__.HexStringEncoder 13 | 14 | @max_depth 5 15 | 16 | @typedoc """ 17 | The map shape of this field must conform to: 18 | ``` 19 | %{ 20 | "name" => String.t(), 21 | "type" => String.t() 22 | } 23 | ``` 24 | """ 25 | @type field :: %{String.t() => String.t()} 26 | 27 | @type types :: %{String.t() => [field()]} 28 | 29 | @doc """ 30 | Returns a hash of a message. 31 | """ 32 | @spec hash_message(map(), types(), String.t(), Keyword.t()) :: hash :: binary() 33 | def hash_message(message, types, primary_type, opts \\ []) do 34 | encode(message, types, primary_type, opts) 35 | |> ExKeccak.hash_256() 36 | end 37 | 38 | def encode(message, opts \\ []) do 39 | encode(message.message, message.types, message.primary_type, opts) 40 | end 41 | 42 | @doc """ 43 | Encodes a message according to EIP-712 44 | """ 45 | @spec encode(map(), [field()], String.t(), Keyword.t()) :: binary() 46 | def encode(message, types, primary_type, opts \\ []) do 47 | encoder = Keyword.get(opts, :encoder, @default_encoder) 48 | 49 | array? = is_array_type?(primary_type) 50 | primary_type = String.trim_trailing(primary_type, "[]") 51 | 52 | if array? do 53 | encode_array(message, primary_type, types, opts) 54 | else 55 | [ 56 | encode_types(types, primary_type), 57 | encode_type(message, primary_type, types, encoder) 58 | ] 59 | |> :erlang.iolist_to_binary() 60 | end 61 | end 62 | 63 | def encode_array(data, primary_type, types, opts) do 64 | Enum.map_join(data, fn single_struct -> 65 | hash_message(single_struct, types, primary_type, opts) 66 | end) 67 | end 68 | 69 | @spec encode_type(map(), String.t(), types(), module()) :: binary() 70 | def encode_type(data, primary_type, types, encoder) do 71 | types[String.trim_trailing(primary_type, "[]")] 72 | |> Enum.map_join(fn %{"name" => name, "type" => type} -> 73 | value = data[name] 74 | 75 | if custom_type?(types, type) do 76 | hash_message(value, types, type) 77 | else 78 | encoder.encode_value(type, value) 79 | end 80 | end) 81 | end 82 | 83 | def encode_types(types, primary_type) do 84 | sorted_deps = 85 | types 86 | |> find_deps(primary_type) 87 | |> MapSet.to_list() 88 | |> Enum.sort() 89 | 90 | [primary_type | sorted_deps] 91 | |> Enum.map(&format_dep(&1, types)) 92 | |> :erlang.iolist_to_binary() 93 | |> ExKeccak.hash_256() 94 | end 95 | 96 | defp find_deps(types, primary_types, acc \\ MapSet.new(), depth \\ @max_depth) do 97 | types[primary_types] 98 | |> Enum.reduce(acc, fn %{"type" => type}, acc -> 99 | if custom_type?(types, type) do 100 | type = String.trim_trailing(type, "[]") 101 | acc = MapSet.put(acc, type) 102 | find_deps(types, type, acc, depth - 1) 103 | else 104 | acc 105 | end 106 | end) 107 | end 108 | 109 | defp custom_type?(types, type) do 110 | # TODO verify not a builtin type 111 | Map.has_key?(types, String.trim_trailing(type, "[]")) 112 | end 113 | 114 | defp is_array_type?(type), do: String.ends_with?(type, "[]") 115 | 116 | defp format_dep(dep, types) do 117 | arguments = 118 | types[dep] 119 | |> Enum.map(fn %{"name" => name, "type" => type} -> [type, " ", name] end) 120 | |> Enum.intersperse(",") 121 | 122 | [dep, "(", arguments, ")"] 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "1.6.2", "2f82b29a47c0bb7b72f023bf3a34d151624f1cbe1e6c4e52303b05a11166a701", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ae9dc112bc368e7b145c547bec2ed257ef88955851c15057c7835251a17211c6"}, 4 | "dep_from_hexpm": {:hex, :dep_from_hexpm, "0.3.0", "e820a753364b715e84457836317107c340d3bdcaa21b469272da79f29ef5f5cb", [:mix], [], "hexpm", "55b0c9db6c5666a4358e1d8e799f43f3fa091ef036dc0d09bf5ee9f091f07b6d"}, 5 | "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"}, 7 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 8 | "ex_abi": {:hex, :ex_abi, "0.5.9", "afdef4279eb24a36bef1d4a83df8a34997c2dddf3ffac48866bb1113f28ae55b", [:mix], [{:ex_keccak, "~> 0.3.0", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230f6dada9995336f1d1b739194d99307a1d079e045548bbca0b622c8c4dbf5b"}, 9 | "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, 10 | "ex_keccak": {:hex, :ex_keccak, "0.3.0", "8c286f8c44c63c5f95e84053603f09cbef52d96314f2b751e56cf8836ceaea63", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "18352c5873ee4938363ae9802038b6a157177a080db40b6e4ab842255ae44b9b"}, 11 | "ex_secp256k1": {:hex, :ex_secp256k1, "0.3.0", "d2e81dd23764e28e26f07007cbbbe70ddb670aa2de9171991e44ea9c1ee20b82", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "483afaf64a5000c4181257e0a66659c6d40f06b97e39e861f65d3f72bba1fef7"}, 12 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 13 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 14 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 15 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 16 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, 18 | "rustler": {:hex, :rustler, "0.23.0", "87162ffdf5a46b6aa03d624a77367070ff1263961ae35332c059225e136c4a87", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f5ab6f0ec564f5569009c0f5685b0e5b379fd72655e82a8dc5a3c24f9fdda36a"}, 19 | "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm", "f1e3dabef71fb510d015fad18c0e05e7c57281001141504c6b69d94e99750a07"}, 20 | } 21 | -------------------------------------------------------------------------------- /test/signed_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecover.SignedTypeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ExWeb3EcRecover.SignedType 5 | 6 | describe "Encodes a message" do 7 | test "with single basic property" do 8 | types = %{"Message" => [%{"name" => "data", "type" => "string"}]} 9 | primary_type = "Message" 10 | 11 | message = %{ 12 | "data" => "test" 13 | } 14 | 15 | # This was generated with metamask 16 | target = 17 | ("cddf41b07426e1a761f3da57e35474ae3deaa5b596306531f651c6dc1321e4fd9c22ff5f" <> 18 | "21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") 19 | |> String.upcase() 20 | |> Base.decode16!() 21 | 22 | assert target == SignedType.encode(message, types, primary_type) 23 | end 24 | 25 | test "with all dynamic and atomic values" do 26 | types = %{ 27 | "Message" => [ 28 | %{"name" => "data", "type" => "string"}, 29 | %{"name" => "data1", "type" => "int8"}, 30 | %{"name" => "data2", "type" => "uint8"}, 31 | %{"name" => "data3", "type" => "bytes4"}, 32 | %{"name" => "data4", "type" => "bool"}, 33 | %{"name" => "data5", "type" => "address"} 34 | ] 35 | } 36 | 37 | primary_type = "Message" 38 | 39 | message = %{ 40 | "data" => "test", 41 | "data1" => 2, 42 | "data2" => 3, 43 | "data3" => "c3f426ae", 44 | "data4" => false, 45 | "data5" => "0x5FF3cb18d8866541C66e4A346767a10480c4278D" 46 | } 47 | 48 | # This was generated with metamask 49 | target = 50 | "77cf5d045714d6093f70690f1206690fca190fba3e645ede4725917151b7aaee" 51 | |> String.downcase() 52 | 53 | assert target == 54 | SignedType.hash_message(message, types, primary_type) 55 | |> Base.encode16(case: :lower) 56 | end 57 | 58 | test "with integers as strings or numbers" do 59 | types = %{ 60 | "Message" => [ 61 | %{"name" => "data1", "type" => "int256"}, 62 | %{"name" => "data2", "type" => "uint256"} 63 | ] 64 | } 65 | 66 | primary_type = "Message" 67 | 68 | message_as_strings = %{ 69 | "data1" => "1709", 70 | "data2" => "2023" 71 | } 72 | 73 | message_as_numbers = %{ 74 | "data1" => 1709, 75 | "data2" => 2023 76 | } 77 | 78 | # This was generated with metamask 79 | target = 80 | "e52790096313e8f1f8c819063bbc1dd12e680e58d2d8b725cb474c0e7c0cbb70" 81 | |> String.downcase() 82 | 83 | strings_signed = SignedType.hash_message(message_as_strings, types, primary_type) 84 | numbers_signed = SignedType.hash_message(message_as_numbers, types, primary_type) 85 | assert strings_signed == numbers_signed 86 | assert Base.encode16(strings_signed, case: :lower) == target 87 | end 88 | 89 | test "containing references" do 90 | types = %{ 91 | "Message" => [ 92 | %{"name" => "data", "type" => "Test"} 93 | ], 94 | "Test" => [%{"name" => "data", "type" => "ATet"}], 95 | "ATet" => [%{"name" => "data", "type" => "string"}] 96 | } 97 | 98 | message = %{ 99 | "data" => %{ 100 | "data" => %{ 101 | "data" => "test" 102 | } 103 | } 104 | } 105 | 106 | target = 107 | "d5d5d0183e58ac8883e666be66a547e0b76ecb4b4411c92695934e91ca7158ec92eec4cf0549942d6a26ace5f0728db40fa7eb3908a5120f0b88cf5472947781" 108 | |> String.upcase() 109 | |> Base.decode16!() 110 | 111 | assert target == SignedType.encode(message, types, "Message") 112 | end 113 | end 114 | 115 | describe "Encodes a type" do 116 | test "with only a basic property" do 117 | types = %{"Message" => [%{"name" => "data", "type" => "string"}]} 118 | 119 | target = 120 | <<205, 223, 65, 176, 116, 38, 225, 167, 97, 243, 218, 87, 227, 84, 116, 174, 61, 234, 165, 121 | 181, 150, 48, 101, 49, 246, 81, 198, 220, 19, 33, 228, 253>> 122 | 123 | assert target == SignedType.encode_types(types, "Message") 124 | end 125 | 126 | test "with nested references" do 127 | types = %{ 128 | "Message" => [ 129 | %{"name" => "data", "type" => "string"}, 130 | %{"name" => "data", "type" => "Test"} 131 | ], 132 | "Test" => [%{"name" => "data", "type" => "ATet"}], 133 | "ATet" => [%{"name" => "data", "type" => "string"}] 134 | } 135 | 136 | target = 137 | "7031b1a85f03f91f16cb21cc404beb084c0901db5d25302c97760c35738c61f9" 138 | |> String.upcase() 139 | |> Base.decode16!() 140 | 141 | assert target == SignedType.encode_types(types, "Message") 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | # {Credo.Check.Design.TagTODO, [exit_status: 2]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterFilter, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 137 | 138 | # 139 | ## Warnings 140 | # 141 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 142 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 143 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 144 | {Credo.Check.Warning.IExPry, []}, 145 | {Credo.Check.Warning.IoInspect, []}, 146 | {Credo.Check.Warning.OperationOnSameValues, []}, 147 | {Credo.Check.Warning.OperationWithConstantResult, []}, 148 | {Credo.Check.Warning.RaiseInsideRescue, []}, 149 | {Credo.Check.Warning.SpecWithStruct, []}, 150 | {Credo.Check.Warning.WrongTestFileExtension, []}, 151 | {Credo.Check.Warning.UnusedEnumOperation, []}, 152 | {Credo.Check.Warning.UnusedFileOperation, []}, 153 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 154 | {Credo.Check.Warning.UnusedListOperation, []}, 155 | {Credo.Check.Warning.UnusedPathOperation, []}, 156 | {Credo.Check.Warning.UnusedRegexOperation, []}, 157 | {Credo.Check.Warning.UnusedStringOperation, []}, 158 | {Credo.Check.Warning.UnusedTupleOperation, []}, 159 | {Credo.Check.Warning.UnsafeExec, []} 160 | ], 161 | disabled: [ 162 | # 163 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 164 | 165 | # 166 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 167 | # and be sure to use `mix credo --strict` to see low priority checks) 168 | # 169 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 170 | {Credo.Check.Consistency.UnusedVariableNames, []}, 171 | {Credo.Check.Design.DuplicatedCode, []}, 172 | {Credo.Check.Design.SkipTestWithoutComment, []}, 173 | {Credo.Check.Readability.AliasAs, []}, 174 | {Credo.Check.Readability.BlockPipe, []}, 175 | {Credo.Check.Readability.ImplTrue, []}, 176 | {Credo.Check.Readability.MultiAlias, []}, 177 | {Credo.Check.Readability.NestedFunctionCalls, []}, 178 | {Credo.Check.Readability.SeparateAliasRequire, []}, 179 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 180 | {Credo.Check.Readability.SinglePipe, []}, 181 | {Credo.Check.Readability.Specs, []}, 182 | {Credo.Check.Readability.StrictModuleLayout, []}, 183 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 184 | {Credo.Check.Refactor.ABCSize, []}, 185 | {Credo.Check.Refactor.AppendSingleItem, []}, 186 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 187 | {Credo.Check.Refactor.FilterReject, []}, 188 | {Credo.Check.Refactor.IoPuts, []}, 189 | {Credo.Check.Refactor.MapMap, []}, 190 | {Credo.Check.Refactor.ModuleDependencies, []}, 191 | {Credo.Check.Refactor.NegatedIsNil, []}, 192 | {Credo.Check.Refactor.PipeChainStart, []}, 193 | {Credo.Check.Refactor.RejectFilter, []}, 194 | {Credo.Check.Refactor.VariableRebinding, []}, 195 | {Credo.Check.Warning.LazyLogging, []}, 196 | {Credo.Check.Warning.LeakyEnvironment, []}, 197 | {Credo.Check.Warning.MapGetUnsafePass, []}, 198 | {Credo.Check.Warning.MixEnv, []}, 199 | {Credo.Check.Warning.UnsafeToAtom, []} 200 | 201 | # {Credo.Check.Refactor.MapInto, []}, 202 | 203 | # 204 | # Custom checks can be created using `mix credo.gen.check`. 205 | # 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /test/ex_web3_ec_recover_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWeb3EcRecoverTest do 2 | use ExUnit.Case 3 | 4 | doctest ExWeb3EcRecover 5 | 6 | alias ExWeb3EcRecover.SignedType.Message 7 | 8 | @domain %{ 9 | "name" => "example.metamask.io", 10 | "version" => "4", 11 | "chainId" => 1, 12 | "verifyingContract" => "0x0000000000000000000000000000000000000000" 13 | } 14 | 15 | @expected_address "0x5ff3cb18d8866541c66e4a346767a10480c4278d" 16 | 17 | describe "recover_typed_signature/3" do 18 | test "Recovers address from a signature and the message" do 19 | # This sig was genarated using Meta Mask 20 | sig = 21 | "0x97ffd15a08cbaebf4cbf2cd40f704bb5b79e3e3a47e29c90f0d8b5360ef312ba0382885309a88c99832082241675b402bfc631e24554079cbee2d8b70a3caeb71b" 22 | 23 | message = %Message{ 24 | types: %{ 25 | "Message" => [%{"name" => "data", "type" => "string"}] 26 | }, 27 | primary_type: "Message", 28 | message: %{ 29 | "data" => "test" 30 | }, 31 | domain: @domain 32 | } 33 | 34 | assert @expected_address == 35 | ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 36 | end 37 | 38 | test "Recovers address from a signature and the message with precalculated domain" do 39 | # This sig was genarated using Meta Mask 40 | sig = 41 | "0x97ffd15a08cbaebf4cbf2cd40f704bb5b79e3e3a47e29c90f0d8b5360ef312ba0382885309a88c99832082241675b402bfc631e24554079cbee2d8b70a3caeb71b" 42 | 43 | message = %Message{ 44 | types: %{ 45 | "Message" => [%{"name" => "data", "type" => "string"}] 46 | }, 47 | primary_type: "Message", 48 | message: %{ 49 | "data" => "test" 50 | }, 51 | domain: "0x60b65550349ac7d938f53ce6675638066d55afa9f7dd6db452a10139fca6d0a2" 52 | } 53 | 54 | assert @expected_address == 55 | ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 56 | end 57 | 58 | test "Order message support" do 59 | message = %Message{ 60 | types: %{ 61 | "Order" => [ 62 | %{"name" => "makerAddress", "type" => "address"}, 63 | %{"name" => "takerAddress", "type" => "address"}, 64 | %{"name" => "feeRecipientAddress", "type" => "address"}, 65 | %{"name" => "senderAddress", "type" => "address"}, 66 | %{"name" => "makerAssetAmount", "type" => "uint256"}, 67 | %{"name" => "takerAssetAmount", "type" => "uint256"}, 68 | %{"name" => "makerFee", "type" => "uint256"}, 69 | %{"name" => "takerFee", "type" => "uint256"}, 70 | %{"name" => "expirationTimeSeconds", "type" => "uint256"}, 71 | %{"name" => "salt", "type" => "uint256"}, 72 | %{"name" => "makerAssetData", "type" => "bytes"}, 73 | %{"name" => "takerAssetData", "type" => "bytes"}, 74 | %{"name" => "makerFeeAssetData", "type" => "bytes"}, 75 | %{"name" => "takerFeeAssetData", "type" => "bytes"} 76 | ] 77 | }, 78 | primary_type: "Order", 79 | domain: %{ 80 | "name" => "0x Protocol", 81 | "version" => "3.0.0", 82 | "chainId" => 137, 83 | "verifyingContract" => "0xfede379e48c873c75f3cc0c81f7c784ad730a8f7" 84 | }, 85 | message: %{ 86 | "makerAddress" => "0x1bbeb0a1a075d870bed8c21dfbe49a37015e4124", 87 | "takerAddress" => "0x0000000000000000000000000000000000000000", 88 | "senderAddress" => "0x0000000000000000000000000000000000000000", 89 | "feeRecipientAddress" => "0x0000000000000000000000000000000000000000", 90 | "expirationTimeSeconds" => 1_641_635_545, 91 | "salt" => 1, 92 | "makerAssetAmount" => 1, 93 | "takerAssetAmount" => 50_000_000_000_000_000, 94 | "makerAssetData" => 95 | "0x02571792000000000000000000000000a5f1ea7df861952863df2e8d1312f7305dabf2150000000000000000000000000000000000000000000000000000000000002b5b", 96 | "takerAssetData" => 97 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 98 | "takerFeeAssetData" => "0x", 99 | "makerFeeAssetData" => "0x", 100 | "takerFee" => 0, 101 | "makerFee" => 0 102 | } 103 | } 104 | 105 | sig = 106 | "0xe1170c9a9da6b19f579e6d9dce8b577ab577bc73bd247658b77a9846c2b4d3e51e882c9c7364b1e8bcf98b865b72ef835fd4dfe6b883ab6deb41fabe5252cc931c" 107 | 108 | assert @expected_address == ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 109 | end 110 | 111 | test "tests hash message" do 112 | # This sig was genarated using Meta Mask 113 | 114 | msg = %Message{ 115 | types: %{ 116 | "Order" => [ 117 | %{"name" => "makerAddress", "type" => "address"}, 118 | %{"name" => "takerAddress", "type" => "address"}, 119 | %{"name" => "feeRecipientAddress", "type" => "address"}, 120 | %{"name" => "senderAddress", "type" => "address"}, 121 | %{"name" => "makerAssetAmount", "type" => "uint256"}, 122 | %{"name" => "takerAssetAmount", "type" => "uint256"}, 123 | %{"name" => "makerFee", "type" => "uint256"}, 124 | %{"name" => "takerFee", "type" => "uint256"}, 125 | %{"name" => "expirationTimeSeconds", "type" => "uint256"}, 126 | %{"name" => "salt", "type" => "uint256"}, 127 | %{"name" => "makerAssetData", "type" => "bytes"}, 128 | %{"name" => "takerAssetData", "type" => "bytes"}, 129 | %{"name" => "makerFeeAssetData", "type" => "bytes"}, 130 | %{"name" => "takerFeeAssetData", "type" => "bytes"} 131 | ] 132 | }, 133 | primary_type: "Order", 134 | domain: %{ 135 | "name" => "0x Protocol", 136 | "version" => "3.0.0", 137 | "chainId" => 137, 138 | "verifyingContract" => "0xfede379e48c873c75f3cc0c81f7c784ad730a8f7" 139 | }, 140 | message: %{ 141 | "makerAddress" => "0x1bbeb0a1a075d870bed8c21dfbe49a37015e4124", 142 | "takerAddress" => "0x0000000000000000000000000000000000000000", 143 | "senderAddress" => "0x0000000000000000000000000000000000000000", 144 | "feeRecipientAddress" => "0x0000000000000000000000000000000000000000", 145 | "expirationTimeSeconds" => 1_641_627_054, 146 | "salt" => 1, 147 | "makerAssetAmount" => 1, 148 | "takerAssetAmount" => 50_000_000_000_000_000, 149 | "makerAssetData" => 150 | "0x02571792000000000000000000000000a5f1ea7df861952863df2e8d1312f7305dabf2150000000000000000000000000000000000000000000000000000000000002b5b", 151 | "takerAssetData" => 152 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 153 | "takerFeeAssetData" => 154 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 155 | "makerFeeAssetData" => 156 | "0xf47261b00000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619", 157 | "takerFee" => 0, 158 | "makerFee" => 0 159 | } 160 | } 161 | 162 | encrypted = 163 | ExWeb3EcRecover.RecoverSignature.hash_eip712(msg) 164 | |> Base.encode16(case: :lower) 165 | 166 | assert "0x493c8aaeec442571358fb4f5c39284a0f3ca40443c2e5ba693eea4615349fcf4" == 167 | "0x" <> encrypted 168 | end 169 | 170 | test "Return {:error, :unsupported_version} when version is invalid" do 171 | # This sig was genarated using Meta Mask 172 | sig = 173 | "0xf6cda8eaf5137e8cc15d48d03a002b0512446e2a7acbc576c01cfbe40ad" <> 174 | "9345663ccda8884520d98dece9a8bfe38102851bdae7f69b3d8612b9808e6" <> 175 | "337801601b" 176 | 177 | message = %Message{ 178 | types: %{ 179 | "Message" => [%{"name" => "data", "type" => "string"}] 180 | }, 181 | primary_type: "Message", 182 | message: %{ 183 | "data" => "test" 184 | }, 185 | domain: %{} 186 | } 187 | 188 | assert {:error, :unsupported_version} == 189 | ExWeb3EcRecover.recover_typed_signature(message, sig, :v5) 190 | end 191 | 192 | test "Return {:error, :invalid_signature} when signature is invalid" do 193 | sig = "invalid_sig" 194 | 195 | message = %Message{ 196 | types: %{ 197 | "Message" => [%{"name" => "data", "type" => "string"}], 198 | "EIP712Domain" => [] 199 | }, 200 | primary_type: "Message", 201 | message: %{ 202 | "data" => "test" 203 | }, 204 | domain: %{} 205 | } 206 | 207 | assert {:error, :invalid_signature} == 208 | ExWeb3EcRecover.recover_typed_signature(message, sig, :v4) 209 | end 210 | end 211 | 212 | describe "recover_personal_signature/2" do 213 | test "Recover address from signature when signature is valid" do 214 | signature = 215 | "0xaa69ef02d4c01b5014187a5838a00a94176505c4efb9d814d7c2179c090efc361" <> 216 | "c219e3849d0b996064bd28732faeefa8e303e85787171e18489cb97b1d75fd01b" 217 | 218 | message = "some message" 219 | 220 | expected_address = "0x2ff0416047e1a6c06dd2eb0195c984c787adf735" 221 | 222 | assert expected_address == ExWeb3EcRecover.recover_personal_signature(message, signature) 223 | end 224 | 225 | test "Return {:error, :invalid_signature} when signature is invalid" do 226 | signature = "some invalid signature" 227 | 228 | message = "some message" 229 | 230 | assert {:error, :invalid_signature} == 231 | ExWeb3EcRecover.recover_personal_signature(message, signature) 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /js_reference/src/logged_typed_data_encoder.ts: -------------------------------------------------------------------------------- 1 | // 👇️ ts-nocheck ignores all ts errors in the file 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-nocheck 4 | 5 | 6 | import { TypedDataDomain, TypedDataField } from "@ethersproject/abstract-signer"; 7 | import { getAddress } from "@ethersproject/address"; 8 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 9 | import { arrayify, BytesLike, hexConcat, hexlify, hexZeroPad, isHexString } from "@ethersproject/bytes"; 10 | import { keccak256 } from "@ethersproject/keccak256"; 11 | import { deepCopy, defineReadOnly, shallowCopy } from "@ethersproject/properties"; 12 | 13 | import { Logger } from "@ethersproject/logger"; 14 | import { version } from "@ethersproject/hash/lib/_version"; 15 | const logger = new Logger(version); 16 | 17 | import { id } from "@ethersproject/hash/lib/id" 18 | 19 | const padding = new Uint8Array(32); 20 | padding.fill(0); 21 | 22 | const NegativeOne: BigNumber = BigNumber.from(-1); 23 | const Zero: BigNumber = BigNumber.from(0); 24 | const One: BigNumber = BigNumber.from(1); 25 | const MaxUint256: BigNumber = BigNumber.from("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); 26 | 27 | function hexPadRight(value: BytesLike) { 28 | const bytes = arrayify(value); 29 | const padOffset = bytes.length % 32 30 | if (padOffset) { 31 | return hexConcat([ bytes, padding.slice(padOffset) ]); 32 | } 33 | return hexlify(bytes); 34 | } 35 | 36 | const hexTrue = hexZeroPad(One.toHexString(), 32); 37 | const hexFalse = hexZeroPad(Zero.toHexString(), 32); 38 | 39 | const domainFieldTypes: Record = { 40 | name: "string", 41 | version: "string", 42 | chainId: "uint256", 43 | verifyingContract: "address", 44 | salt: "bytes32" 45 | }; 46 | 47 | const domainFieldNames: Array = [ 48 | "name", "version", "chainId", "verifyingContract", "salt" 49 | ]; 50 | 51 | function checkString(key: string): (value: any) => string { 52 | return function (value: any){ 53 | if (typeof(value) !== "string") { 54 | logger.throwArgumentError(`invalid domain value for ${ JSON.stringify(key) }`, `domain.${ key }`, value); 55 | } 56 | return value; 57 | } 58 | } 59 | 60 | const domainChecks: Record any> = { 61 | name: checkString("name"), 62 | version: checkString("version"), 63 | chainId: function(value: any) { 64 | try { 65 | return BigNumber.from(value).toString() 66 | } catch (error) { } 67 | return logger.throwArgumentError(`invalid domain value for "chainId"`, "domain.chainId", value); 68 | }, 69 | verifyingContract: function(value: any) { 70 | try { 71 | return getAddress(value).toLowerCase(); 72 | } catch (error) { } 73 | return logger.throwArgumentError(`invalid domain value "verifyingContract"`, "domain.verifyingContract", value); 74 | }, 75 | salt: function(value: any) { 76 | try { 77 | const bytes = arrayify(value); 78 | if (bytes.length !== 32) { throw new Error("bad length"); } 79 | return hexlify(bytes); 80 | } catch (error) { } 81 | return logger.throwArgumentError(`invalid domain value "salt"`, "domain.salt", value); 82 | } 83 | } 84 | 85 | function getBaseEncoder(type: string): (value: any) => string { 86 | // intXX and uintXX 87 | { 88 | const match = type.match(/^(u?)int(\d*)$/); 89 | if (match) { 90 | const signed = (match[1] === ""); 91 | 92 | const width = parseInt(match[2] || "256"); 93 | if (width % 8 !== 0 || width > 256 || (match[2] && match[2] !== String(width))) { 94 | logger.throwArgumentError("invalid numeric width", "type", type); 95 | } 96 | 97 | const boundsUpper = MaxUint256.mask(signed ? (width - 1): width); 98 | const boundsLower = signed ? boundsUpper.add(One).mul(NegativeOne): Zero; 99 | 100 | return function(value: BigNumberish) { 101 | const v = BigNumber.from(value); 102 | 103 | if (v.lt(boundsLower) || v.gt(boundsUpper)) { 104 | logger.throwArgumentError(`value out-of-bounds for ${ type }`, "value", value); 105 | } 106 | 107 | return hexZeroPad(v.toTwos(256).toHexString(), 32); 108 | }; 109 | } 110 | } 111 | 112 | // bytesXX 113 | { 114 | const match = type.match(/^bytes(\d+)$/); 115 | if (match) { 116 | const width = parseInt(match[1]); 117 | if (width === 0 || width > 32 || match[1] !== String(width)) { 118 | logger.throwArgumentError("invalid bytes width", "type", type); 119 | } 120 | 121 | return function(value: BytesLike) { 122 | const bytes = arrayify(value); 123 | if (bytes.length !== width) { 124 | logger.throwArgumentError(`invalid length for ${ type }`, "value", value); 125 | } 126 | return hexPadRight(value); 127 | }; 128 | } 129 | } 130 | 131 | switch (type) { 132 | case "address": return function(value: string) { 133 | return hexZeroPad(getAddress(value), 32); 134 | }; 135 | case "bool": return function(value: boolean) { 136 | return ((!value) ? hexFalse: hexTrue); 137 | }; 138 | case "bytes": return function(value: BytesLike) { 139 | return keccak256(value); 140 | }; 141 | case "string": return function(value: string) { 142 | return id(value); 143 | }; 144 | } 145 | 146 | return null; 147 | } 148 | 149 | function encodeType(name: string, fields: Array): string { 150 | return `${ name }(${ fields.map(({ name, type }) => (type + " " + name)).join(",") })`; 151 | } 152 | 153 | export class TypedDataEncoder { 154 | readonly primaryType: string; 155 | readonly types: Record>; 156 | 157 | readonly _encoderCache: Record string>; 158 | readonly _types: Record; 159 | 160 | constructor(types: Record>) { 161 | defineReadOnly(this, "types", Object.freeze(deepCopy(types))); 162 | 163 | defineReadOnly(this, "_encoderCache", { }); 164 | defineReadOnly(this, "_types", { }); 165 | 166 | // Link struct types to their direct child structs 167 | const links: Record> = { }; 168 | 169 | // Link structs to structs which contain them as a child 170 | const parents: Record> = { }; 171 | 172 | // Link all subtypes within a given struct 173 | const subtypes: Record> = { }; 174 | 175 | Object.keys(types).forEach((type) => { 176 | links[type] = { }; 177 | parents[type] = [ ]; 178 | subtypes[type] = { } 179 | }); 180 | 181 | for (const name in types) { 182 | 183 | const uniqueNames: Record = { }; 184 | 185 | types[name].forEach((field) => { 186 | 187 | // Check each field has a unique name 188 | if (uniqueNames[field.name]) { 189 | logger.throwArgumentError(`duplicate variable name ${ JSON.stringify(field.name) } in ${ JSON.stringify(name) }`, "types", types); 190 | } 191 | uniqueNames[field.name] = true; 192 | 193 | // Get the base type (drop any array specifiers) 194 | const baseType = field.type.match(/^([^\x5b]*)(\x5b|$)/)[1]; 195 | if (baseType === name) { 196 | logger.throwArgumentError(`circular type reference to ${ JSON.stringify(baseType) }`, "types", types); 197 | } 198 | 199 | // Is this a base encoding type? 200 | const encoder = getBaseEncoder(baseType); 201 | if (encoder) { return ;} 202 | 203 | if (!parents[baseType]) { 204 | logger.throwArgumentError(`unknown type ${ JSON.stringify(baseType) }`, "types", types); 205 | } 206 | 207 | // Add linkage 208 | parents[baseType].push(name); 209 | links[name][baseType] = true; 210 | }); 211 | } 212 | 213 | // Deduce the primary type 214 | const primaryTypes = Object.keys(parents).filter((n) => (parents[n].length === 0)); 215 | 216 | if (primaryTypes.length === 0) { 217 | logger.throwArgumentError("missing primary type", "types", types); 218 | } else if (primaryTypes.length > 1) { 219 | logger.throwArgumentError(`ambiguous primary types or unused types: ${ primaryTypes.map((t) => (JSON.stringify(t))).join(", ") }`, "types", types); 220 | } 221 | 222 | defineReadOnly(this, "primaryType", primaryTypes[0]); 223 | 224 | // Check for circular type references 225 | function checkCircular(type: string, found: Record) { 226 | if (found[type]) { 227 | logger.throwArgumentError(`circular type reference to ${ JSON.stringify(type) }`, "types", types); 228 | } 229 | 230 | found[type] = true; 231 | 232 | Object.keys(links[type]).forEach((child) => { 233 | if (!parents[child]) { return; } 234 | 235 | // Recursively check children 236 | checkCircular(child, found); 237 | 238 | // Mark all ancestors as having this decendant 239 | Object.keys(found).forEach((subtype) => { 240 | subtypes[subtype][child] = true; 241 | }); 242 | }); 243 | 244 | delete found[type]; 245 | } 246 | checkCircular(this.primaryType, { }); 247 | 248 | // Compute each fully describe type 249 | for (const name in subtypes) { 250 | const st = Object.keys(subtypes[name]); 251 | st.sort(); 252 | this._types[name] = encodeType(name, types[name]) + st.map((t) => encodeType(t, types[t])).join(""); 253 | } 254 | } 255 | 256 | getEncoder(type: string): (value: any) => string { 257 | let encoder = this._encoderCache[type]; 258 | if (!encoder) { 259 | encoder = this._encoderCache[type] = this._getEncoder(type); 260 | } 261 | return encoder; 262 | } 263 | 264 | _getEncoder(type: string): (value: any) => string { 265 | 266 | // Basic encoder type (address, bool, uint256, etc) 267 | { 268 | const encoder = getBaseEncoder(type); 269 | if (encoder) { return encoder; } 270 | } 271 | 272 | // Array 273 | const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); 274 | if (match) { 275 | const subtype = match[1]; 276 | const subEncoder = this.getEncoder(subtype); 277 | const length = parseInt(match[3]); 278 | return (value: Array) => { 279 | if (length >= 0 && value.length !== length) { 280 | logger.throwArgumentError("array length mismatch; expected length ${ arrayLength }", "value", value); 281 | } 282 | 283 | let result = value.map(subEncoder); 284 | if (this._types[subtype]) { 285 | result = result.map(keccak256); 286 | } 287 | 288 | return keccak256(hexConcat(result)); 289 | }; 290 | } 291 | 292 | // Struct 293 | const fields = this.types[type]; 294 | if (fields) { 295 | const encodedType = id(this._types[type]); 296 | return (value: Record) => { 297 | const values = fields.map(({ name, type }) => { 298 | const result = this.getEncoder(type)(value[name]); 299 | if (this._types[type]) { return keccak256(result); } 300 | return result; 301 | }); 302 | values.unshift(encodedType); 303 | const result = hexConcat(values); 304 | console.log("Type", encodedType, result) 305 | return result; 306 | } 307 | } 308 | 309 | return logger.throwArgumentError(`unknown type: ${ type }`, "type", type); 310 | } 311 | 312 | encodeType(name: string): string { 313 | const result = this._types[name]; 314 | if (!result) { 315 | logger.throwArgumentError(`unknown type: ${ JSON.stringify(name) }`, "name", name); 316 | } 317 | return result; 318 | } 319 | 320 | encodeData(type: string, value: any): string { 321 | return this.getEncoder(type)(value); 322 | } 323 | 324 | hashStruct(name: string, value: Record): string { 325 | return keccak256(this.encodeData(name, value)); 326 | } 327 | 328 | encode(value: Record): string { 329 | const result = this.encodeData(this.primaryType, value); 330 | console.log("Main type encode", result) 331 | return result; 332 | } 333 | 334 | hash(value: Record): string { 335 | return this.hashStruct(this.primaryType, value); 336 | } 337 | 338 | _visit(type: string, value: any, callback: (type: string, data: any) => any): any { 339 | // Basic encoder type (address, bool, uint256, etc) 340 | { 341 | const encoder = getBaseEncoder(type); 342 | if (encoder) { return callback(type, value); } 343 | } 344 | 345 | // Array 346 | const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); 347 | if (match) { 348 | const subtype = match[1]; 349 | const length = parseInt(match[3]); 350 | if (length >= 0 && value.length !== length) { 351 | logger.throwArgumentError("array length mismatch; expected length ${ arrayLength }", "value", value); 352 | } 353 | return value.map((v: any) => this._visit(subtype, v, callback)); 354 | } 355 | 356 | // Struct 357 | const fields = this.types[type]; 358 | if (fields) { 359 | return fields.reduce((accum, { name, type }) => { 360 | accum[name] = this._visit(type, value[name], callback); 361 | return accum; 362 | }, >{}); 363 | } 364 | 365 | return logger.throwArgumentError(`unknown type: ${ type }`, "type", type); 366 | } 367 | 368 | visit(value: Record, callback: (type: string, data: any) => any): any { 369 | return this._visit(this.primaryType, value, callback); 370 | } 371 | 372 | static from(types: Record>): TypedDataEncoder { 373 | return new TypedDataEncoder(types); 374 | } 375 | 376 | static getPrimaryType(types: Record>): string { 377 | return TypedDataEncoder.from(types).primaryType; 378 | } 379 | 380 | static hashStruct(name: string, types: Record>, value: Record): string { 381 | return TypedDataEncoder.from(types).hashStruct(name, value); 382 | } 383 | 384 | static hashDomain(domain: TypedDataDomain): string { 385 | const domainFields: Array = [ ]; 386 | for (const name in domain) { 387 | const type = domainFieldTypes[name]; 388 | if (!type) { 389 | logger.throwArgumentError(`invalid typed-data domain key: ${ JSON.stringify(name) }`, "domain", domain); 390 | } 391 | domainFields.push({ name, type }); 392 | } 393 | 394 | domainFields.sort((a, b) => { 395 | return domainFieldNames.indexOf(a.name) - domainFieldNames.indexOf(b.name); 396 | }); 397 | 398 | return TypedDataEncoder.hashStruct("EIP712Domain", { EIP712Domain: domainFields }, domain); 399 | } 400 | 401 | static encode(domain: TypedDataDomain, types: Record>, value: Record): string { 402 | return hexConcat([ 403 | "0x1901", 404 | TypedDataEncoder.hashDomain(domain), 405 | TypedDataEncoder.from(types).hash(value) 406 | ]); 407 | } 408 | 409 | static hash(domain: TypedDataDomain, types: Record>, value: Record): string { 410 | return keccak256(TypedDataEncoder.encode(domain, types, value)); 411 | } 412 | 413 | // Replaces all address types with ENS names with their looked up address 414 | static async resolveNames(domain: TypedDataDomain, types: Record>, value: Record, resolveName: (name: string) => Promise): Promise<{ domain: TypedDataDomain, value: any }> { 415 | // Make a copy to isolate it from the object passed in 416 | domain = shallowCopy(domain); 417 | 418 | // Look up all ENS names 419 | const ensCache: Record = { }; 420 | 421 | // Do we need to look up the domain's verifyingContract? 422 | if (domain.verifyingContract && !isHexString(domain.verifyingContract, 20)) { 423 | ensCache[domain.verifyingContract] = "0x"; 424 | } 425 | 426 | // We are going to use the encoder to visit all the base values 427 | const encoder = TypedDataEncoder.from(types); 428 | 429 | // Get a list of all the addresses 430 | encoder.visit(value, (type: string, value: any) => { 431 | if (type === "address" && !isHexString(value, 20)) { 432 | ensCache[value] = "0x"; 433 | } 434 | return value; 435 | }); 436 | 437 | // Lookup each name 438 | for (const name in ensCache) { 439 | ensCache[name] = await resolveName(name); 440 | } 441 | 442 | // Replace the domain verifyingContract if needed 443 | if (domain.verifyingContract && ensCache[domain.verifyingContract]) { 444 | domain.verifyingContract = ensCache[domain.verifyingContract]; 445 | } 446 | 447 | // Replace all ENS names with their address 448 | value = encoder.visit(value, (type: string, value: any) => { 449 | if (type === "address" && ensCache[value]) { return ensCache[value]; } 450 | return value; 451 | }); 452 | 453 | return { domain, value }; 454 | } 455 | 456 | static getPayload(domain: TypedDataDomain, types: Record>, value: Record): any { 457 | // Validate the domain fields 458 | TypedDataEncoder.hashDomain(domain); 459 | 460 | // Derive the EIP712Domain Struct reference type 461 | const domainValues: Record = { }; 462 | const domainTypes: Array<{ name: string, type:string }> = [ ]; 463 | 464 | domainFieldNames.forEach((name) => { 465 | const value = (domain)[name]; 466 | if (value == null) { return; } 467 | domainValues[name] = domainChecks[name](value); 468 | domainTypes.push({ name, type: domainFieldTypes[name] }); 469 | }); 470 | 471 | const encoder = TypedDataEncoder.from(types); 472 | 473 | const typesWithDomain = shallowCopy(types); 474 | if (typesWithDomain.EIP712Domain) { 475 | logger.throwArgumentError("types must not contain EIP712Domain type", "types.EIP712Domain", types); 476 | } else { 477 | typesWithDomain.EIP712Domain = domainTypes; 478 | } 479 | 480 | // Validate the data structures and types 481 | encoder.encode(value); 482 | 483 | return { 484 | types: typesWithDomain, 485 | domain: domainValues, 486 | primaryType: encoder.primaryType, 487 | message: encoder.visit(value, (type: string, value: any) => { 488 | 489 | // bytes 490 | if (type.match(/^bytes(\d*)/)) { 491 | return hexlify(arrayify(value)); 492 | } 493 | 494 | // uint or int 495 | if (type.match(/^u?int/)) { 496 | return BigNumber.from(value).toString(); 497 | } 498 | 499 | switch (type) { 500 | case "address": 501 | return value.toLowerCase(); 502 | case "bool": 503 | return !!value; 504 | case "string": 505 | if (typeof(value) !== "string") { 506 | logger.throwArgumentError(`invalid string`, "value", value); 507 | } 508 | return value; 509 | } 510 | 511 | return logger.throwArgumentError("unsupported type", "type", type); 512 | }) 513 | }; 514 | } 515 | } 516 | 517 | -------------------------------------------------------------------------------- /js_reference/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsreference", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@cspotcode/source-map-support": { 8 | "version": "0.8.1", 9 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 10 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 11 | "dev": true, 12 | "requires": { 13 | "@jridgewell/trace-mapping": "0.3.9" 14 | } 15 | }, 16 | "@ethersproject/abi": { 17 | "version": "5.7.0", 18 | "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", 19 | "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", 20 | "requires": { 21 | "@ethersproject/address": "^5.7.0", 22 | "@ethersproject/bignumber": "^5.7.0", 23 | "@ethersproject/bytes": "^5.7.0", 24 | "@ethersproject/constants": "^5.7.0", 25 | "@ethersproject/hash": "^5.7.0", 26 | "@ethersproject/keccak256": "^5.7.0", 27 | "@ethersproject/logger": "^5.7.0", 28 | "@ethersproject/properties": "^5.7.0", 29 | "@ethersproject/strings": "^5.7.0" 30 | } 31 | }, 32 | "@ethersproject/abstract-provider": { 33 | "version": "5.7.0", 34 | "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", 35 | "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", 36 | "requires": { 37 | "@ethersproject/bignumber": "^5.7.0", 38 | "@ethersproject/bytes": "^5.7.0", 39 | "@ethersproject/logger": "^5.7.0", 40 | "@ethersproject/networks": "^5.7.0", 41 | "@ethersproject/properties": "^5.7.0", 42 | "@ethersproject/transactions": "^5.7.0", 43 | "@ethersproject/web": "^5.7.0" 44 | } 45 | }, 46 | "@ethersproject/abstract-signer": { 47 | "version": "5.7.0", 48 | "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", 49 | "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", 50 | "requires": { 51 | "@ethersproject/abstract-provider": "^5.7.0", 52 | "@ethersproject/bignumber": "^5.7.0", 53 | "@ethersproject/bytes": "^5.7.0", 54 | "@ethersproject/logger": "^5.7.0", 55 | "@ethersproject/properties": "^5.7.0" 56 | } 57 | }, 58 | "@ethersproject/address": { 59 | "version": "5.7.0", 60 | "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", 61 | "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", 62 | "requires": { 63 | "@ethersproject/bignumber": "^5.7.0", 64 | "@ethersproject/bytes": "^5.7.0", 65 | "@ethersproject/keccak256": "^5.7.0", 66 | "@ethersproject/logger": "^5.7.0", 67 | "@ethersproject/rlp": "^5.7.0" 68 | } 69 | }, 70 | "@ethersproject/base64": { 71 | "version": "5.7.0", 72 | "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", 73 | "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", 74 | "requires": { 75 | "@ethersproject/bytes": "^5.7.0" 76 | } 77 | }, 78 | "@ethersproject/basex": { 79 | "version": "5.7.0", 80 | "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", 81 | "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", 82 | "requires": { 83 | "@ethersproject/bytes": "^5.7.0", 84 | "@ethersproject/properties": "^5.7.0" 85 | } 86 | }, 87 | "@ethersproject/bignumber": { 88 | "version": "5.7.0", 89 | "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", 90 | "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", 91 | "requires": { 92 | "@ethersproject/bytes": "^5.7.0", 93 | "@ethersproject/logger": "^5.7.0", 94 | "bn.js": "^5.2.1" 95 | } 96 | }, 97 | "@ethersproject/bytes": { 98 | "version": "5.7.0", 99 | "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", 100 | "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", 101 | "requires": { 102 | "@ethersproject/logger": "^5.7.0" 103 | } 104 | }, 105 | "@ethersproject/constants": { 106 | "version": "5.7.0", 107 | "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", 108 | "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", 109 | "requires": { 110 | "@ethersproject/bignumber": "^5.7.0" 111 | } 112 | }, 113 | "@ethersproject/contracts": { 114 | "version": "5.7.0", 115 | "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", 116 | "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", 117 | "requires": { 118 | "@ethersproject/abi": "^5.7.0", 119 | "@ethersproject/abstract-provider": "^5.7.0", 120 | "@ethersproject/abstract-signer": "^5.7.0", 121 | "@ethersproject/address": "^5.7.0", 122 | "@ethersproject/bignumber": "^5.7.0", 123 | "@ethersproject/bytes": "^5.7.0", 124 | "@ethersproject/constants": "^5.7.0", 125 | "@ethersproject/logger": "^5.7.0", 126 | "@ethersproject/properties": "^5.7.0", 127 | "@ethersproject/transactions": "^5.7.0" 128 | } 129 | }, 130 | "@ethersproject/hash": { 131 | "version": "5.7.0", 132 | "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", 133 | "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", 134 | "requires": { 135 | "@ethersproject/abstract-signer": "^5.7.0", 136 | "@ethersproject/address": "^5.7.0", 137 | "@ethersproject/base64": "^5.7.0", 138 | "@ethersproject/bignumber": "^5.7.0", 139 | "@ethersproject/bytes": "^5.7.0", 140 | "@ethersproject/keccak256": "^5.7.0", 141 | "@ethersproject/logger": "^5.7.0", 142 | "@ethersproject/properties": "^5.7.0", 143 | "@ethersproject/strings": "^5.7.0" 144 | } 145 | }, 146 | "@ethersproject/hdnode": { 147 | "version": "5.7.0", 148 | "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", 149 | "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", 150 | "requires": { 151 | "@ethersproject/abstract-signer": "^5.7.0", 152 | "@ethersproject/basex": "^5.7.0", 153 | "@ethersproject/bignumber": "^5.7.0", 154 | "@ethersproject/bytes": "^5.7.0", 155 | "@ethersproject/logger": "^5.7.0", 156 | "@ethersproject/pbkdf2": "^5.7.0", 157 | "@ethersproject/properties": "^5.7.0", 158 | "@ethersproject/sha2": "^5.7.0", 159 | "@ethersproject/signing-key": "^5.7.0", 160 | "@ethersproject/strings": "^5.7.0", 161 | "@ethersproject/transactions": "^5.7.0", 162 | "@ethersproject/wordlists": "^5.7.0" 163 | } 164 | }, 165 | "@ethersproject/json-wallets": { 166 | "version": "5.7.0", 167 | "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", 168 | "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", 169 | "requires": { 170 | "@ethersproject/abstract-signer": "^5.7.0", 171 | "@ethersproject/address": "^5.7.0", 172 | "@ethersproject/bytes": "^5.7.0", 173 | "@ethersproject/hdnode": "^5.7.0", 174 | "@ethersproject/keccak256": "^5.7.0", 175 | "@ethersproject/logger": "^5.7.0", 176 | "@ethersproject/pbkdf2": "^5.7.0", 177 | "@ethersproject/properties": "^5.7.0", 178 | "@ethersproject/random": "^5.7.0", 179 | "@ethersproject/strings": "^5.7.0", 180 | "@ethersproject/transactions": "^5.7.0", 181 | "aes-js": "3.0.0", 182 | "scrypt-js": "3.0.1" 183 | } 184 | }, 185 | "@ethersproject/keccak256": { 186 | "version": "5.7.0", 187 | "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", 188 | "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", 189 | "requires": { 190 | "@ethersproject/bytes": "^5.7.0", 191 | "js-sha3": "0.8.0" 192 | } 193 | }, 194 | "@ethersproject/logger": { 195 | "version": "5.7.0", 196 | "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", 197 | "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==" 198 | }, 199 | "@ethersproject/networks": { 200 | "version": "5.7.1", 201 | "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", 202 | "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", 203 | "requires": { 204 | "@ethersproject/logger": "^5.7.0" 205 | } 206 | }, 207 | "@ethersproject/pbkdf2": { 208 | "version": "5.7.0", 209 | "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", 210 | "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", 211 | "requires": { 212 | "@ethersproject/bytes": "^5.7.0", 213 | "@ethersproject/sha2": "^5.7.0" 214 | } 215 | }, 216 | "@ethersproject/properties": { 217 | "version": "5.7.0", 218 | "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", 219 | "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", 220 | "requires": { 221 | "@ethersproject/logger": "^5.7.0" 222 | } 223 | }, 224 | "@ethersproject/providers": { 225 | "version": "5.7.2", 226 | "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", 227 | "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", 228 | "requires": { 229 | "@ethersproject/abstract-provider": "^5.7.0", 230 | "@ethersproject/abstract-signer": "^5.7.0", 231 | "@ethersproject/address": "^5.7.0", 232 | "@ethersproject/base64": "^5.7.0", 233 | "@ethersproject/basex": "^5.7.0", 234 | "@ethersproject/bignumber": "^5.7.0", 235 | "@ethersproject/bytes": "^5.7.0", 236 | "@ethersproject/constants": "^5.7.0", 237 | "@ethersproject/hash": "^5.7.0", 238 | "@ethersproject/logger": "^5.7.0", 239 | "@ethersproject/networks": "^5.7.0", 240 | "@ethersproject/properties": "^5.7.0", 241 | "@ethersproject/random": "^5.7.0", 242 | "@ethersproject/rlp": "^5.7.0", 243 | "@ethersproject/sha2": "^5.7.0", 244 | "@ethersproject/strings": "^5.7.0", 245 | "@ethersproject/transactions": "^5.7.0", 246 | "@ethersproject/web": "^5.7.0", 247 | "bech32": "1.1.4", 248 | "ws": "7.4.6" 249 | } 250 | }, 251 | "@ethersproject/random": { 252 | "version": "5.7.0", 253 | "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", 254 | "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", 255 | "requires": { 256 | "@ethersproject/bytes": "^5.7.0", 257 | "@ethersproject/logger": "^5.7.0" 258 | } 259 | }, 260 | "@ethersproject/rlp": { 261 | "version": "5.7.0", 262 | "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", 263 | "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", 264 | "requires": { 265 | "@ethersproject/bytes": "^5.7.0", 266 | "@ethersproject/logger": "^5.7.0" 267 | } 268 | }, 269 | "@ethersproject/sha2": { 270 | "version": "5.7.0", 271 | "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", 272 | "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", 273 | "requires": { 274 | "@ethersproject/bytes": "^5.7.0", 275 | "@ethersproject/logger": "^5.7.0", 276 | "hash.js": "1.1.7" 277 | } 278 | }, 279 | "@ethersproject/signing-key": { 280 | "version": "5.7.0", 281 | "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", 282 | "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", 283 | "requires": { 284 | "@ethersproject/bytes": "^5.7.0", 285 | "@ethersproject/logger": "^5.7.0", 286 | "@ethersproject/properties": "^5.7.0", 287 | "bn.js": "^5.2.1", 288 | "elliptic": "6.5.4", 289 | "hash.js": "1.1.7" 290 | } 291 | }, 292 | "@ethersproject/solidity": { 293 | "version": "5.7.0", 294 | "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", 295 | "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", 296 | "requires": { 297 | "@ethersproject/bignumber": "^5.7.0", 298 | "@ethersproject/bytes": "^5.7.0", 299 | "@ethersproject/keccak256": "^5.7.0", 300 | "@ethersproject/logger": "^5.7.0", 301 | "@ethersproject/sha2": "^5.7.0", 302 | "@ethersproject/strings": "^5.7.0" 303 | } 304 | }, 305 | "@ethersproject/strings": { 306 | "version": "5.7.0", 307 | "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", 308 | "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", 309 | "requires": { 310 | "@ethersproject/bytes": "^5.7.0", 311 | "@ethersproject/constants": "^5.7.0", 312 | "@ethersproject/logger": "^5.7.0" 313 | } 314 | }, 315 | "@ethersproject/transactions": { 316 | "version": "5.7.0", 317 | "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", 318 | "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", 319 | "requires": { 320 | "@ethersproject/address": "^5.7.0", 321 | "@ethersproject/bignumber": "^5.7.0", 322 | "@ethersproject/bytes": "^5.7.0", 323 | "@ethersproject/constants": "^5.7.0", 324 | "@ethersproject/keccak256": "^5.7.0", 325 | "@ethersproject/logger": "^5.7.0", 326 | "@ethersproject/properties": "^5.7.0", 327 | "@ethersproject/rlp": "^5.7.0", 328 | "@ethersproject/signing-key": "^5.7.0" 329 | } 330 | }, 331 | "@ethersproject/units": { 332 | "version": "5.7.0", 333 | "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", 334 | "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", 335 | "requires": { 336 | "@ethersproject/bignumber": "^5.7.0", 337 | "@ethersproject/constants": "^5.7.0", 338 | "@ethersproject/logger": "^5.7.0" 339 | } 340 | }, 341 | "@ethersproject/wallet": { 342 | "version": "5.7.0", 343 | "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", 344 | "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", 345 | "requires": { 346 | "@ethersproject/abstract-provider": "^5.7.0", 347 | "@ethersproject/abstract-signer": "^5.7.0", 348 | "@ethersproject/address": "^5.7.0", 349 | "@ethersproject/bignumber": "^5.7.0", 350 | "@ethersproject/bytes": "^5.7.0", 351 | "@ethersproject/hash": "^5.7.0", 352 | "@ethersproject/hdnode": "^5.7.0", 353 | "@ethersproject/json-wallets": "^5.7.0", 354 | "@ethersproject/keccak256": "^5.7.0", 355 | "@ethersproject/logger": "^5.7.0", 356 | "@ethersproject/properties": "^5.7.0", 357 | "@ethersproject/random": "^5.7.0", 358 | "@ethersproject/signing-key": "^5.7.0", 359 | "@ethersproject/transactions": "^5.7.0", 360 | "@ethersproject/wordlists": "^5.7.0" 361 | } 362 | }, 363 | "@ethersproject/web": { 364 | "version": "5.7.1", 365 | "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", 366 | "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", 367 | "requires": { 368 | "@ethersproject/base64": "^5.7.0", 369 | "@ethersproject/bytes": "^5.7.0", 370 | "@ethersproject/logger": "^5.7.0", 371 | "@ethersproject/properties": "^5.7.0", 372 | "@ethersproject/strings": "^5.7.0" 373 | } 374 | }, 375 | "@ethersproject/wordlists": { 376 | "version": "5.7.0", 377 | "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", 378 | "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", 379 | "requires": { 380 | "@ethersproject/bytes": "^5.7.0", 381 | "@ethersproject/hash": "^5.7.0", 382 | "@ethersproject/logger": "^5.7.0", 383 | "@ethersproject/properties": "^5.7.0", 384 | "@ethersproject/strings": "^5.7.0" 385 | } 386 | }, 387 | "@jridgewell/resolve-uri": { 388 | "version": "3.1.0", 389 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 390 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 391 | "dev": true 392 | }, 393 | "@jridgewell/sourcemap-codec": { 394 | "version": "1.4.14", 395 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 396 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", 397 | "dev": true 398 | }, 399 | "@jridgewell/trace-mapping": { 400 | "version": "0.3.9", 401 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 402 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 403 | "dev": true, 404 | "requires": { 405 | "@jridgewell/resolve-uri": "^3.0.3", 406 | "@jridgewell/sourcemap-codec": "^1.4.10" 407 | } 408 | }, 409 | "@tsconfig/node10": { 410 | "version": "1.0.9", 411 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 412 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 413 | "dev": true 414 | }, 415 | "@tsconfig/node12": { 416 | "version": "1.0.11", 417 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 418 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 419 | "dev": true 420 | }, 421 | "@tsconfig/node14": { 422 | "version": "1.0.3", 423 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 424 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 425 | "dev": true 426 | }, 427 | "@tsconfig/node16": { 428 | "version": "1.0.3", 429 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", 430 | "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", 431 | "dev": true 432 | }, 433 | "@types/node": { 434 | "version": "18.11.11", 435 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz", 436 | "integrity": "sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g==", 437 | "dev": true 438 | }, 439 | "acorn": { 440 | "version": "8.8.1", 441 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", 442 | "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", 443 | "dev": true 444 | }, 445 | "acorn-walk": { 446 | "version": "8.2.0", 447 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 448 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 449 | "dev": true 450 | }, 451 | "aes-js": { 452 | "version": "3.0.0", 453 | "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", 454 | "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" 455 | }, 456 | "arg": { 457 | "version": "4.1.3", 458 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 459 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 460 | "dev": true 461 | }, 462 | "bech32": { 463 | "version": "1.1.4", 464 | "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", 465 | "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" 466 | }, 467 | "bn.js": { 468 | "version": "5.2.1", 469 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", 470 | "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" 471 | }, 472 | "brorand": { 473 | "version": "1.1.0", 474 | "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", 475 | "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" 476 | }, 477 | "create-require": { 478 | "version": "1.1.1", 479 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 480 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 481 | "dev": true 482 | }, 483 | "diff": { 484 | "version": "4.0.2", 485 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 486 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 487 | "dev": true 488 | }, 489 | "elliptic": { 490 | "version": "6.5.4", 491 | "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", 492 | "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", 493 | "requires": { 494 | "bn.js": "^4.11.9", 495 | "brorand": "^1.1.0", 496 | "hash.js": "^1.0.0", 497 | "hmac-drbg": "^1.0.1", 498 | "inherits": "^2.0.4", 499 | "minimalistic-assert": "^1.0.1", 500 | "minimalistic-crypto-utils": "^1.0.1" 501 | }, 502 | "dependencies": { 503 | "bn.js": { 504 | "version": "4.12.0", 505 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", 506 | "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" 507 | } 508 | } 509 | }, 510 | "ethers": { 511 | "version": "5.7.2", 512 | "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", 513 | "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", 514 | "requires": { 515 | "@ethersproject/abi": "5.7.0", 516 | "@ethersproject/abstract-provider": "5.7.0", 517 | "@ethersproject/abstract-signer": "5.7.0", 518 | "@ethersproject/address": "5.7.0", 519 | "@ethersproject/base64": "5.7.0", 520 | "@ethersproject/basex": "5.7.0", 521 | "@ethersproject/bignumber": "5.7.0", 522 | "@ethersproject/bytes": "5.7.0", 523 | "@ethersproject/constants": "5.7.0", 524 | "@ethersproject/contracts": "5.7.0", 525 | "@ethersproject/hash": "5.7.0", 526 | "@ethersproject/hdnode": "5.7.0", 527 | "@ethersproject/json-wallets": "5.7.0", 528 | "@ethersproject/keccak256": "5.7.0", 529 | "@ethersproject/logger": "5.7.0", 530 | "@ethersproject/networks": "5.7.1", 531 | "@ethersproject/pbkdf2": "5.7.0", 532 | "@ethersproject/properties": "5.7.0", 533 | "@ethersproject/providers": "5.7.2", 534 | "@ethersproject/random": "5.7.0", 535 | "@ethersproject/rlp": "5.7.0", 536 | "@ethersproject/sha2": "5.7.0", 537 | "@ethersproject/signing-key": "5.7.0", 538 | "@ethersproject/solidity": "5.7.0", 539 | "@ethersproject/strings": "5.7.0", 540 | "@ethersproject/transactions": "5.7.0", 541 | "@ethersproject/units": "5.7.0", 542 | "@ethersproject/wallet": "5.7.0", 543 | "@ethersproject/web": "5.7.1", 544 | "@ethersproject/wordlists": "5.7.0" 545 | } 546 | }, 547 | "hash.js": { 548 | "version": "1.1.7", 549 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 550 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 551 | "requires": { 552 | "inherits": "^2.0.3", 553 | "minimalistic-assert": "^1.0.1" 554 | } 555 | }, 556 | "hmac-drbg": { 557 | "version": "1.0.1", 558 | "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", 559 | "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", 560 | "requires": { 561 | "hash.js": "^1.0.3", 562 | "minimalistic-assert": "^1.0.0", 563 | "minimalistic-crypto-utils": "^1.0.1" 564 | } 565 | }, 566 | "inherits": { 567 | "version": "2.0.4", 568 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 569 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 570 | }, 571 | "js-sha3": { 572 | "version": "0.8.0", 573 | "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", 574 | "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" 575 | }, 576 | "make-error": { 577 | "version": "1.3.6", 578 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 579 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 580 | "dev": true 581 | }, 582 | "minimalistic-assert": { 583 | "version": "1.0.1", 584 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 585 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 586 | }, 587 | "minimalistic-crypto-utils": { 588 | "version": "1.0.1", 589 | "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", 590 | "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" 591 | }, 592 | "scrypt-js": { 593 | "version": "3.0.1", 594 | "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", 595 | "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" 596 | }, 597 | "ts-node": { 598 | "version": "10.9.1", 599 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 600 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 601 | "dev": true, 602 | "requires": { 603 | "@cspotcode/source-map-support": "^0.8.0", 604 | "@tsconfig/node10": "^1.0.7", 605 | "@tsconfig/node12": "^1.0.7", 606 | "@tsconfig/node14": "^1.0.0", 607 | "@tsconfig/node16": "^1.0.2", 608 | "acorn": "^8.4.1", 609 | "acorn-walk": "^8.1.1", 610 | "arg": "^4.1.0", 611 | "create-require": "^1.1.0", 612 | "diff": "^4.0.1", 613 | "make-error": "^1.1.1", 614 | "v8-compile-cache-lib": "^3.0.1", 615 | "yn": "3.1.1" 616 | } 617 | }, 618 | "typescript": { 619 | "version": "4.9.3", 620 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 621 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 622 | "dev": true 623 | }, 624 | "v8-compile-cache-lib": { 625 | "version": "3.0.1", 626 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 627 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 628 | "dev": true 629 | }, 630 | "ws": { 631 | "version": "7.4.6", 632 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 633 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" 634 | }, 635 | "yn": { 636 | "version": "3.1.1", 637 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 638 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 639 | "dev": true 640 | } 641 | } 642 | } 643 | --------------------------------------------------------------------------------