diff --git a/.changeset/README.md b/.changeset/README.md
index e5b6d8d6a6..4f3b76b096 100644
--- a/.changeset/README.md
+++ b/.changeset/README.md
@@ -5,4 +5,4 @@ with multi-package repos, or single-package repos to help you version and publis
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
-[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+[our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
index 4f8345f464..6a5a5ae11d 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -1,11 +1,19 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
- "changelog": "@changesets/cli/changelog",
+ "changelog": [
+ "@changesets/cli/changelog",
+ { "repo": "0xsequence/sequence.js" }
+ ],
"commit": false,
- "fixed": [],
- "linked": [],
- "access": "restricted",
- "baseBranch": "master",
- "updateInternalDependencies": "patch",
- "ignore": ["@0xsequence/wallet-primitives-cli", "docs", "web"]
+ "linked": [
+ [
+ "@0xsequence/*"
+ ]
+ ],
+ "access": "public",
+ "baseBranch": "main",
+ "ignore": [],
+ "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
+ "updateInternalDependents": "always"
+ }
}
diff --git a/.changeset/new-elephants-travel.md b/.changeset/new-elephants-travel.md
new file mode 100644
index 0000000000..ddfb37374a
--- /dev/null
+++ b/.changeset/new-elephants-travel.md
@@ -0,0 +1,5 @@
+---
+"@wagmi/cli": patch
+---
+
+Updated block explorer chains.
diff --git a/.changeset/nice-pandas-clap.md b/.changeset/nice-pandas-clap.md
new file mode 100644
index 0000000000..7f4af53010
--- /dev/null
+++ b/.changeset/nice-pandas-clap.md
@@ -0,0 +1,5 @@
+---
+
+---
+
+Circleci project setup
diff --git a/.changeset/quick-hairs-scream.md b/.changeset/quick-hairs-scream.md
new file mode 100644
index 0000000000..206e94e246
--- /dev/null
+++ b/.changeset/quick-hairs-scream.md
@@ -0,0 +1,6 @@
+---
+"wagmi": patch
+"@wagmi/core": patch
+---
+
+Added `chainId` parameter to `getCapabilities`/`useCapabilities`.
diff --git a/.changeset/spicy-bats-juggle.md b/.changeset/spicy-bats-juggle.md
new file mode 100644
index 0000000000..cf7a154229
--- /dev/null
+++ b/.changeset/spicy-bats-juggle.md
@@ -0,0 +1,6 @@
+---
+"@wagmi/cli": patch
+"site": patch
+---
+
+Circleci project setup
diff --git a/.changeset/tall-fans-mate.md b/.changeset/tall-fans-mate.md
new file mode 100644
index 0000000000..cf7a154229
--- /dev/null
+++ b/.changeset/tall-fans-mate.md
@@ -0,0 +1,6 @@
+---
+"@wagmi/cli": patch
+"site": patch
+---
+
+Circleci project setup
diff --git a/.changeset/tiny-laws-dream.md b/.changeset/tiny-laws-dream.md
new file mode 100644
index 0000000000..c39a3d68b9
--- /dev/null
+++ b/.changeset/tiny-laws-dream.md
@@ -0,0 +1,5 @@
+---
+"@fake-scope/fake-pkg": patch
+---
+
+Circleci project setup
diff --git a/.changeset/young-guests-care.md b/.changeset/young-guests-care.md
new file mode 100644
index 0000000000..8de2292dde
--- /dev/null
+++ b/.changeset/young-guests-care.md
@@ -0,0 +1,5 @@
+---
+"site": patch
+---
+
+docs(readme): fix typo
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000..2ef62819f0
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,22 @@
+version: 2.1
+
+jobs:
+ test:
+ docker:
+ - image: ghcr.io/foundry-rs/foundry:latest
+ steps:
+ - checkout
+ - run:
+ name: Install submodules
+ command: git submodule update --init --recursive
+ - run:
+ name: Build
+ command: forge build
+ - run:
+ name: Test
+ command: forge test -vvv
+
+workflows:
+ main:
+ jobs:
+ - test
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000..849dc677d2
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+.eslintrc.js
+packages/**/dist
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7f34c7a889..12451d4bc9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1,5 @@
-* @0xsequence/disable-codeowners-notifications @0xsequence/core
+@tmm @jxom
+
+/packages/connectors/src/metaMask @ecp4224 @omridan159 @abretonc7s @elefantel @BjornGunnarsson @EdouardBougon
+/packages/connectors/src/safe @DaniSomoza @dasanra @mikhailxyz @yagopv
+/packages/connectors/src/walletConnect @ganchoradkov @glitch-txs @ignaciosantise @tomiir
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000000..d3ab387e17
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1 @@
+[View Contributing Guide on wagmi.sh](https://wagmi.sh/dev/contributing)
\ No newline at end of file
diff --git a/.github/DISCUSSION_TEMPLATE/connector-request.yml b/.github/DISCUSSION_TEMPLATE/connector-request.yml
new file mode 100644
index 0000000000..c1e31b1b6b
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/connector-request.yml
@@ -0,0 +1,51 @@
+title: '[Connector Request] '
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for your interest in contributing a new Connector to the Wagmi! If you haven't already, please read the [Contributing Guidelines](https://wagmi.sh/dev/contributing). Once you submit the form, the Wagmi team will follow up in the discussion thread to discuss next steps.
+
+ Please note that in order for connector requests to be accepted, the team creating the Connector must [sponsor Wagmi](https://github.com/sponsors/wevm). It takes time and effort to maintain third-party connectors. Wagmi is an OSS project that depends on sponsors and grants to continue our work. Please get in touch via [dev@wevm.dev](mailto:dev@wevm.dev) if you have questions about sponsoring.
+
+ - type: textarea
+ attributes:
+ label: What **novel use-case** does the Connector provide?
+ description: |
+ A novel use-case is likely one that is not already covered by or not easily extended from another Connector (such as the `injected` or `walletConnect`).
+
+ Examples of **novel** use-cases could be a connector that integrates with:
+
+ - the injected `window.ethereum` provider (a la `injected`)
+ - a series of wallets via QR Codes or Mobile Deep Links (a la `walletConnect`)
+ - a wallet with it's own SDK (a la `coinbaseWallet`)
+ - hardware wallet(s) via Web USB/Bluetooth
+ - an Externally Owned Account via a private key or some other method
+
+ Examples of **nonnovel** use-cases would be a connector that:
+
+ - extends another connector (e.g. `walletConnect`) with no significant differences in functionality other than branding, etc.
+ placeholder: Info on what makes this connector different.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Are the Connector's integrations production-ready and generally available?
+ description: Connectors are intended to be used by consumers in production as part of Wagmi. As such, the Connector and all dependencies must be production-ready and generally available. This means your connector should not rely on non-production software or be restricted to a limited group of users. For example, if your connector requires a wallet that has a closed beta, it is not ready for inclusion in Wagmi.
+ placeholder: Info about the Connector and any dependencies (e.g. browser extension, wallet app, npm package).
+ validations:
+ required: true
+
+ - type: checkboxes
+ attributes:
+ label: Are you committed to actively maintaining the Connector?
+ description: It is critical connectors are updated in a timely manner and actively maintained so that users of Wagmi can rely on them in production settings. The Wagmi core team will provide as much assistance as possible to keep connectors up-to-date with breaking changes from Wagmi, but it is your responsibility to ensure that any dependencies and issues/discussions related to the Connector are handled in a timely manner. If this is not done, the Connector could be removed from the future versions.
+ options:
+ - label: Yes, my team is or I am committed to actively maintaining the Connector.
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Feel free to jot down any additional info you think might be helpful.
+ placeholder: Additional comments, questions, feedback.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..8a561abba1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,88 @@
+name: Bug Report
+description: Report bugs or issues.
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report! The more info you provide, the more we can help you.
+
+ If you are a [Wagmi Sponsor](https://github.com/sponsors/wevm?metadata_campaign=gh_issue), your issues are prioritized.
+
+ - type: checkboxes
+ attributes:
+ label: Check existing issues
+ description: By submitting this issue, you checked there isn't [already an issue](https://github.com/wevm/wagmi/issues) for this bug.
+ options:
+ - label: I checked there isn't [already an issue](https://github.com/wevm/wagmi/issues) for the bug I encountered.
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe the bug
+ description: Clear and concise description of the bug. If you intend to submit a PR for this issue, tell us in the description. Thanks!
+ placeholder: I am doing… What I expect is… What is actually happening…
+ validations:
+ required: true
+
+ - type: input
+ id: reproduction
+ attributes:
+ label: Link to Minimal Reproducible Example
+ description: "Please provide a link that can reproduce the problem: [new.wagmi.sh](https://new.wagmi.sh) for runtime issues or [TypeScript Playground](https://www.typescriptlang.org/play) for type issues. For most issues, you will likely get asked to provide a minimal reproducible example so why not add one now :) If a report is vague (e.g. just snippets, generic error message, screenshot, etc.) and has no reproduction, it will receive a \"Needs Reproduction\" label and be auto-closed."
+ placeholder: https://new.wagmi.sh
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps or code snippets to reproduce the behavior.
+ validations:
+ required: false
+
+ - type: dropdown
+ attributes:
+ label: What Wagmi package(s) are you using?
+ multiple: true
+ options:
+ - 'wagmi'
+ - '@wagmi/cli'
+ - '@wagmi/connectors'
+ - '@wagmi/core'
+ - '@wagmi/vue'
+ - 'create-wagmi'
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Wagmi Package(s) Version(s)
+ description: What version of the Wagmi packages you selected above are you using? If using multiple, separate with comma (e.g. `wagmi@x.y.z, @wagmi/cli@x.y.z`).
+ placeholder: x.y.z (do not write `latest`)
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Viem Version
+ description: What version of [Viem](https://viem.sh) are you using?
+ placeholder: x.y.z (do not write `latest`)
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: TypeScript Version
+ description: What version of TypeScript are you using? Wagmi requires `typescript@>=5`.
+ placeholder: x.y.z (do not write `latest`)
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Anything else?
+ description: Anything that will give us more context about the issue you are encountering. Framework version (e.g. React, Vue), app framework (e.g. Next.js, Nuxt), bundler, etc.
+ validations:
+ required: false
+
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..fc8027c871
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,14 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Get Help
+ url: https://github.com/wevm/wagmi/discussions/new?category=q-a
+ about: Ask a question and discuss with other community members.
+
+ - name: Feature Request
+ url: https://github.com/wevm/wagmi/discussions/new?category=ideas
+ about: Request features or brainstorm ideas for new functionality.
+
+ - name: Connector Request
+ url: https://github.com/wevm/wagmi/discussions/new?category=connector-request
+ about: Kick off a request for a new connector
+
diff --git a/.github/ISSUE_TEMPLATE/docs_issue.yml b/.github/ISSUE_TEMPLATE/docs_issue.yml
new file mode 100644
index 0000000000..f2d53b8a98
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/docs_issue.yml
@@ -0,0 +1,34 @@
+name: Documentation Issue
+description: Tell us about missing or incorrect documentation.
+labels: ['Area: Docs']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for submitting a documentation request. It helps make Wagmi better.
+
+ If it's a small change, like misspelling or example that needs updating, feel free to submit a PR instead of creating this issue.
+
+ - type: dropdown
+ attributes:
+ label: What is the type of issue?
+ multiple: true
+ options:
+ - Documentation is missing
+ - Documentation is incorrect
+ - Documentation is confusing
+ - Example code is not working
+ - Something else
+
+ - type: textarea
+ attributes:
+ label: What is the issue?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Where did you find it?
+ description: Please provide the URL(s) where you found this issue.
+ validations:
+ required: true
diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 0000000000..6b5f336419
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reactive primitives for Ethereum apps
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+## Documentation
+
+For documentation and guides, visit [wagmi.sh](https://wagmi.sh).
+
+## Community
+
+For help, discussion about best practices, or any other conversation that would benefit from being searchable:
+
+[Discuss Wagmi on GitHub](https://github.com/wevm/wagmi/discussions)
+
+For casual chit-chat with others using the framework:
+
+[Join the Wagmi Discord](https://discord.gg/SghfWBKexF)
+
+## Contributing
+
+Contributions to Wagmi are greatly appreciated! If you're interested in contributing to Wagmi, please read the [Contributing Guide](https://wagmi.sh/dev/contributing) **before submitting a pull request**.
+
+## Sponsors
+
+If you find Wagmi useful or use it for work, please consider [sponsoring Wagmi](https://github.com/sponsors/wevm?metadata_campaign=gh_readme_support). Thank you 🙏
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[Sponsor Wagmi](https://github.com/sponsors/wevm?metadata_campaign=gh_readme_support_bottom)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000000..54f40f38df
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,6 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Contact [dev@wevm.dev](mailto:dev@wevm.dev).
+
diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml
deleted file mode 100644
index ca81d1a40a..0000000000
--- a/.github/actions/install-dependencies/action.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Setup Node and PNPM dependencies
-
-runs:
- using: 'composite'
-
- steps:
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Setup PNPM
- uses: pnpm/action-setup@v3
- with:
- version: 10
- run_install: false
-
- - name: Get pnpm store directory
- id: pnpm-cache
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
-
- - name: Setup pnpm cache
- uses: actions/cache@v4
- with:
- path: |
- ${{ steps.pnpm-cache.outputs.STORE_PATH }}
- node_modules
- packages/*/node_modules
- ~/.cache/puppeteer
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install dependencies
- shell: bash
- run: pnpm install --frozen-lockfile
- if: ${{ steps.pnpm-cache.outputs.cache-hit != 'true' }}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..bc63aca35b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: 'monthly'
diff --git a/.github/logo-dark.svg b/.github/logo-dark.svg
new file mode 100644
index 0000000000..5d47cce337
--- /dev/null
+++ b/.github/logo-dark.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/.github/logo-light.svg b/.github/logo-light.svg
new file mode 100644
index 0000000000..4e28590c36
--- /dev/null
+++ b/.github/logo-light.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..602a32d0a8
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+
+
+
diff --git a/.github/workflows/Vercel Preview Deployment.yml b/.github/workflows/Vercel Preview Deployment.yml
new file mode 100644
index 0000000000..ca7ca97005
--- /dev/null
+++ b/.github/workflows/Vercel Preview Deployment.yml
@@ -0,0 +1,22 @@
+name: Playwright Tests
+
+on:
+ repository_dispatch:
+ types:
+ - 'vercel.deployment.success'
+permissions:
+ contents: read
+jobs:
+ run-e2es:
+ if: github.event_name == 'repository_dispatch'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.client_payload.git.sha }}
+ - name: Install dependencies
+ run: npm ci && npx playwright install --with-deps
+ - name: Run tests
+ run: npx playwright test
+ env:
+ BASE_URL: ${{ github.event.client_payload.url }}
diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml
new file mode 100644
index 0000000000..0ebce2b479
--- /dev/null
+++ b/.github/workflows/changesets.yml
@@ -0,0 +1,62 @@
+name: Changesets
+on:
+ push:
+ branches: [main]
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ name: Verify
+ uses: ./.github/workflows/verify.yml
+ secrets: inherit
+
+ changesets:
+ name: Publish
+ needs: verify
+ permissions:
+ contents: write
+ id-token: write
+ pull-requests: write
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ with:
+ # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
+ fetch-depth: 0
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: PR or publish
+ uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba
+ with:
+ title: 'chore: version packages'
+ commit: 'chore: version packages'
+ createGithubReleases: ${{ github.ref == 'refs/heads/main' }}
+ publish: pnpm changeset:publish
+ version: pnpm changeset:version
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Publish prerelease
+ if: steps.changesets.outputs.published != 'true'
+ continue-on-error: true
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ npm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
+ git reset --hard origin/main
+ pnpm clean
+ pnpm changeset version --no-git-tag --snapshot canary
+ pnpm changeset:prepublish
+ pnpm changeset publish --no-git-tag --snapshot canary --tag canary
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000000..d19e21b798
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,39 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request,
+# surfacing known-vulnerable versions of the packages declared or updated in the PR.
+# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable
+# packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+name: 'Dependency review'
+on:
+ pull_request:
+ branches: [ "main" ]
+
+# If using a dependency submission action in this workflow this permission will need to be set to:
+#
+# permissions:
+# contents: write
+#
+# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api
+permissions:
+ contents: read
+ # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option
+ pull-requests: write
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout repository'
+ uses: actions/checkout@v4
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@v4
+ # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options.
+ with:
+ comment-summary-in-pr: always
+ # fail-on-severity: moderate
+ # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later
+ # retry-on-snapshot-warnings: true
diff --git a/.github/workflows/fortify.yml b/.github/workflows/fortify.yml
new file mode 100644
index 0000000000..e8a93615c1
--- /dev/null
+++ b/.github/workflows/fortify.yml
@@ -0,0 +1,84 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+################################################################################################################################################
+# Fortify Application Security provides your team with solutions to empower DevSecOps practices, enable cloud transformation, and secure your #
+# software supply chain. To learn more about Fortify, start a free trial or contact our sales team, visit fortify.com. #
+# #
+# Use this starter workflow as a basis for integrating Fortify Application Security Testing into your GitHub workflows. This template #
+# demonstrates the steps to package the code+dependencies, initiate a scan, and optionally import SAST vulnerabilities into GitHub Security #
+# Code Scanning Alerts. Additional information is available in the workflow comments and the Fortify AST Action / fcli / Fortify product #
+# documentation. If you need additional assistance, please contact Fortify support. #
+################################################################################################################################################
+
+name: Fortify AST Scan
+
+# Customize trigger events based on your DevSecOps process and/or policy
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "master" ]
+ schedule:
+ - cron: '31 12 * * 4'
+ workflow_dispatch:
+
+jobs:
+ Fortify-AST-Scan:
+ # Use the appropriate runner for building your source code. Ensure dev tools required to build your code are present and configured appropriately (MSBuild, Python, etc).
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ steps:
+ # Check out source code
+ - name: Check Out Source Code
+ uses: actions/checkout@v4
+
+ # Java is required to run the various Fortify utilities. Ensuring proper version is installed on the runner.
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ # Perform SAST and optionally SCA scan via Fortify on Demand/Fortify Hosted/Software Security Center, then
+ # optionally export SAST results to the GitHub code scanning dashboard. In case further customization is
+ # required, you can use sub-actions like fortify/github-action/setup@v1 to set up the various Fortify tools
+ # and run them directly from within your pipeline; see https://github.com/fortify/github-action#readme for
+ # details.
+ - name: Run FoD SAST Scan
+ uses: fortify/github-action@a92347297e02391b857e7015792cd1926a4cd418
+ with:
+ sast-scan: true
+ env:
+ ### Required configuration when integrating with Fortify on Demand
+ FOD_URL: https://ams.fortify.com
+ FOD_TENANT: ${{secrets.FOD_TENANT}}
+ FOD_USER: ${{secrets.FOD_USER}}
+ FOD_PASSWORD: ${{secrets.FOD_PAT}}
+ ### Optional configuration when integrating with Fortify on Demand
+ # EXTRA_PACKAGE_OPTS: -oss # Extra 'scancentral package' options, like '-oss'' if
+ # Debricked SCA scan is enabled on Fortify on Demand
+ # EXTRA_FOD_LOGIN_OPTS: --socket-timeout=60s # Extra 'fcli fod session login' options
+ # FOD_RELEASE: MyApp:MyRelease # FoD release name, default: /:; may
+ # replace app+release name with numeric release ID
+ # DO_WAIT: true # Wait for scan completion, implied if 'DO_EXPORT: true'
+ # DO_EXPORT: true # Export SAST results to GitHub code scanning dashboard
+ ### Required configuration when integrating with Fortify Hosted / Software Security Center & ScanCentral
+ # SSC_URL: ${{secrets.SSC_URL}} # SSC URL
+ # SSC_TOKEN: ${{secrets.SSC_TOKEN}} # SSC CIToken or AutomationToken
+ # SC_SAST_TOKEN: ${{secrets.SC_SAST_TOKEN}} # ScanCentral SAST client auth token
+ # SC_SAST_SENSOR_VERSION: ${{vars.SC_SAST_SENSOR_VERSION}} # Sensor version on which to run the scan;
+ # usually defined as organization or repo variable
+ ### Optional configuration when integrating with Fortify Hosted / Software Security Center & ScanCentral
+ # EXTRA_SC_SAST_LOGIN_OPTS: --socket-timeout=60s # Extra 'fcli sc-sast session login' options
+ # SSC_APPVERSION: MyApp:MyVersion # SSC application version, default: /:
+ # EXTRA_PACKAGE_OPTS: -bv myCustomPom.xml # Extra 'scancentral package' options
+ # DO_WAIT: true # Wait for scan completion, implied if 'DO_EXPORT: true'
+ # DO_EXPORT: true # Export SAST results to GitHub code scanning dashboard
diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml
new file mode 100644
index 0000000000..9a386806e0
--- /dev/null
+++ b/.github/workflows/issue-labeled.yml
@@ -0,0 +1,23 @@
+name: Issue Labeled
+
+on:
+ issues:
+ types: [labeled]
+
+permissions:
+ contents: read
+ issues: write
+
+jobs:
+ issue-labeled:
+ if: ${{ github.repository_owner == 'wevm' }}
+ uses: wevm/actions/.github/workflows/issue-labeled.yml@main
+ with:
+ needs-reproduction-body: |
+ Hello @${{ github.event.issue.user.login }}.
+
+ Please provide a [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) using [StackBlitz](https://new.wagmi.sh), [TypeScript Playground](https://www.typescriptlang.org/play) (for type issues), or a separate minimal GitHub repository.
+
+ [Minimal reproductions are required](https://antfu.me/posts/why-reproductions-are-required) as they save us a lot of time reproducing your config/environment and issue, and allow us to help you faster.
+
+ Once a minimal reproduction is added, a team member will confirm it works, then re-open the issue.
diff --git a/.github/workflows/jekyll-docker.yml b/.github/workflows/jekyll-docker.yml
new file mode 100644
index 0000000000..c88a4430c3
--- /dev/null
+++ b/.github/workflows/jekyll-docker.yml
@@ -0,0 +1,23 @@
+name: Jekyll site CI
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build the site in the jekyll/builder container
+ run: |
+ docker run \
+ -v ${{ github.workspace }}:/srv/jekyll -v ${{ github.workspace }}/_site:/srv/jekyll/_site \
+ jekyll/builder:latest /bin/bash -c "chmod -R 777 /srv/jekyll && jekyll build --future"
diff --git a/.github/workflows/lock-issue.yml b/.github/workflows/lock-issue.yml
new file mode 100644
index 0000000000..279452d223
--- /dev/null
+++ b/.github/workflows/lock-issue.yml
@@ -0,0 +1,16 @@
+name: Lock Issue
+
+on:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ lock-issue:
+ if: ${{ github.repository_owner == 'wevm' }}
+ uses: wevm/actions/.github/workflows/lock-issue.yml@main
+ with:
+ issue-comment: |
+ This issue has been locked since it has been closed for more than 14 days.
+
+ If you found a concrete bug or regression related to it, please open a new [bug report](https://github.com/wevm/wagmi/issues/new?template=bug_report.yml) with a reproduction against the latest Wagmi version. If you have any questions or comments you can create a new [discussion thread](https://github.com/wevm/wagmi/discussions).
+
diff --git a/.github/workflows/octopusdeploy.yml b/.github/workflows/octopusdeploy.yml
new file mode 100644
index 0000000000..9c4403d554
--- /dev/null
+++ b/.github/workflows/octopusdeploy.yml
@@ -0,0 +1,112 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by separate terms of service,
+# privacy policy, and support documentation.
+#
+# This workflow will build and publish a Docker container which is then deployed through Octopus Deploy.
+#
+# The build job in this workflow currently assumes that there is a Dockerfile that generates the relevant application image.
+# If required, this job can be modified to generate whatever alternative build artifact is required for your deployment.
+#
+# This workflow assumes you have already created a Project in Octopus Deploy.
+# For instructions see https://octopus.com/docs/projects/setting-up-projects
+#
+# To configure this workflow:
+#
+# 1. Decide where you are going to host your image.
+# This template uses the GitHub Registry for simplicity but if required you can update the relevant DOCKER_REGISTRY variables below.
+#
+# 2. Create and configure an OIDC credential for a service account in Octopus.
+# This allows for passwordless authentication to your Octopus instance through a trust relationship configured between Octopus, GitHub and your GitHub Repository.
+# https://octopus.com/docs/octopus-rest-api/openid-connect/github-actions
+#
+# 3. Configure your Octopus project details below:
+# OCTOPUS_URL: update to your Octopus Instance Url
+# OCTOPUS_SERVICE_ACCOUNT: update to your service account Id
+# OCTOPUS_SPACE: update to the name of the space your project is configured in
+# OCTOPUS_PROJECT: update to the name of your Octopus project
+# OCTOPUS_ENVIRONMENT: update to the name of the environment to recieve the first deployment
+
+
+name: 'Build and Deploy to Octopus Deploy'
+
+on:
+ push:
+ branches:
+ - '"main"'
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ contents: read
+ env:
+ DOCKER_REGISTRY: ghcr.io # TODO: Update to your docker registry uri
+ DOCKER_REGISTRY_USERNAME: ${{ github.actor }} # TODO: Update to your docker registry username
+ DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} # TODO: Update to your docker registry password
+ outputs:
+ image_tag: ${{ steps.meta.outputs.version }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ env.DOCKER_REGISTRY_USERNAME }}
+ password: ${{ env.DOCKER_REGISTRY_PASSWORD }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
+ with:
+ images: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }}
+ tags: type=semver,pattern={{version}},value=v1.0.0-{{sha}}
+
+ - name: Build and push Docker image
+ id: push
+ uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ deploy:
+ name: Deploy
+ permissions:
+ id-token: write
+ runs-on: ubuntu-latest
+ needs: [ build ]
+ env:
+ OCTOPUS_URL: 'https://your-octopus-url' # TODO: update to your Octopus Instance url
+ OCTOPUS_SERVICE_ACCOUNT: 'your-service-account-id' # TODO: update to your service account Id
+ OCTOPUS_SPACE: 'your-space' # TODO: update to the name of the space your project is configured in
+ OCTOPUS_PROJECT: 'your-project' # TODO: update to the name of your Octopus project
+ OCTOPUS_ENVIRONMENT: 'your-environment' # TODO: update to the name of the environment to recieve the first deployment
+
+ steps:
+ - name: Log in to Octopus Deploy
+ uses: OctopusDeploy/login@e485a40e4b47a154bdf59cc79e57894b0769a760 #v1.0.3
+ with:
+ server: '${{ env.OCTOPUS_URL }}'
+ service_account_id: '${{ env.OCTOPUS_SERVICE_ACCOUNT }}'
+
+ - name: Create Release
+ id: create_release
+ uses: OctopusDeploy/create-release-action@fea7e7b45c38c021b6bc5a14bd7eaa2ed5269214 #v3.2.2
+ with:
+ project: '${{ env.OCTOPUS_PROJECT }}'
+ space: '${{ env.OCTOPUS_SPACE }}'
+ packages: '*:${{ needs.build.outputs.image_tag }}'
+
+ - name: Deploy Release
+ uses: OctopusDeploy/deploy-release-action@b10a606c903b0a5bce24102af9d066638ab429ac #v3.2.1
+ with:
+ project: '${{ env.OCTOPUS_PROJECT }}'
+ space: '${{ env.OCTOPUS_SPACE }}'
+ release_number: '${{ steps.create_release.outputs.release_number }}'
+ environments: ${{ env.OCTOPUS_ENVIRONMENT }}
diff --git a/.github/workflows/on_pr_pnpm-format-label.yml b/.github/workflows/on_pr_pnpm-format-label.yml
deleted file mode 100644
index 84fb27cb3e..0000000000
--- a/.github/workflows/on_pr_pnpm-format-label.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: pnpm-format-label
-
-on:
- pull_request:
- types: [labeled]
-
-jobs:
- proto:
- if: ${{ github.event.label.name == 'pnpm format' }}
- uses: ./.github/workflows/pnpm-format.yml
- secrets: inherit
-
- rm:
- if: ${{ github.event.label.name == 'pnpm format' }}
- runs-on: ubuntu-latest
- steps:
- - name: Remove the label
- run: |
- LABEL=$(echo "${{ github.event.label.name }}" | sed 's/ /%20/g')
- curl -X DELETE \
- -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
- -H "Accept: application/vnd.github.v3+json" \
- https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/$LABEL
diff --git a/.github/workflows/pnpm-format.yml b/.github/workflows/pnpm-format.yml
deleted file mode 100644
index 1be36e1a6b..0000000000
--- a/.github/workflows/pnpm-format.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: pnpm format
-
-on:
- workflow_call:
-
-jobs:
- run:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ github.head_ref }}
- fetch-depth: 20
-
- - uses: ./.github/actions/install-dependencies
-
- - run: pnpm format
-
- - name: Commit back
- uses: 0xsequence/actions/git-commit@v0.0.4
- env:
- API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN_GIT_COMMIT }}
- with:
- files: './'
- branch: ${{ github.head_ref }}
- commit_message: '[AUTOMATED] pnpm format'
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000000..dfc963ef8d
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,38 @@
+name: Pull Request
+on:
+ pull_request:
+ types: [opened, reopened, synchronize, ready_for_review]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ name: Verify
+ uses: ./.github/workflows/verify.yml
+ secrets: inherit
+
+ size:
+ name: Size
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ pull-requests: write
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: Report build size
+ uses: preactjs/compressed-size-action@v2
+ with:
+ pattern: 'packages/**/dist/**'
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..297c74f635
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,44 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v3
+
+ - name: Setup Node.js 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+
+ - name: Install Dependencies
+ run: pnpm install
+
+ - name: Create Release Pull Request or Publish to npm
+ id: changesets
+ uses: changesets/action@v1
+ with:
+ # This expects you to have a script called release which does a build for your packages and calls changeset publish
+ publish: pnpm release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Send a Slack notification if a publish happens
+ if: steps.changesets.outputs.published == 'true'
+ # You can do something when a publish happens.
+ run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!"
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
new file mode 100644
index 0000000000..39683bb684
--- /dev/null
+++ b/.github/workflows/snapshot.yml
@@ -0,0 +1,32 @@
+name: Snapshot
+on:
+ workflow_dispatch:
+
+jobs:
+ snapshot:
+ name: Release snapshot version
+ permissions:
+ contents: write
+ id-token: write
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: Publish Snapshots
+ continue-on-error: true
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ snapshot=$(git branch --show-current | tr -cs '[:alnum:]-' '-' | tr '[:upper:]' '[:lower:]' | sed 's/-$//')
+ npm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
+ pnpm clean
+ pnpm changeset version --no-git-tag --snapshot $snapshot
+ pnpm changeset:prepublish
+ pnpm changeset publish --no-git-tag --snapshot $snapshot --tag $snapshot
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index bb22f4c721..0000000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-on: [push]
-
-name: tests
-
-jobs:
- install:
- name: Install dependencies
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: ./.github/actions/install-dependencies
-
- build:
- name: Run build
- runs-on: ubuntu-latest
- needs: [install]
- steps:
- - uses: actions/checkout@v4
- - uses: ./.github/actions/install-dependencies
- - run: pnpm clean
- - run: pnpm build
- - run: pnpm typecheck
- - run: pnpm lint
-
- tests:
- name: Run all tests
- runs-on: ubuntu-latest
- needs: [build]
- steps:
- - uses: actions/checkout@v4
- - uses: ./.github/actions/install-dependencies
- - name: Install Foundry
- uses: foundry-rs/foundry-toolchain@v1
- with:
- version: v1.5.0
- - name: Start Anvil in background
- run: anvil --fork-url https://nodes.sequence.app/arbitrum &
- - run: pnpm build
- - run: pnpm test
-
- # NOTE: if you'd like to see example of how to run
- # tests per package in parallel, see 'v2' branch
- # .github/workflows/tests.yml
-
- # coverage:
- # name: Run coverage
- # runs-on: ubuntu-latest
- # needs: [install]
- # steps:
- # - uses: actions/checkout@v4
- # - uses: actions/setup-node@v4
- # with:
- # node-version: 20
- # - uses: actions/cache@v4
- # id: pnpm-cache
- # with:
- # path: |
- # node_modules
- # */*/node_modules
- # key: ${{ runner.os }}-install-${{ hashFiles('**/package.json', '**/pnpm.lock') }}
- # - run: pnpm dev && (pnpm coverage || true)
- # - uses: codecov/codecov-action@v1
- # with:
- # fail_ci_if_error: true
- # verbose: true
- # directory: ./coverage
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000000..ee13ea7099
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,135 @@
+name: Verify
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ check:
+ name: Check
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GH_PTOKEN }}
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: Check repo
+ run: pnpm check:repo
+
+ - name: Check code
+ run: pnpm check
+
+ - name: Update package versions
+ run: pnpm version:update
+
+ - uses: stefanzweifel/git-auto-commit-action@v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ commit_message: 'chore: format'
+ commit_user_name: 'github-actions[bot]'
+ commit_user_email: 'github-actions[bot]@users.noreply.github.com'
+
+ build:
+ name: Build
+ permissions:
+ contents: read
+ needs: check
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: Build
+ run: pnpm build
+
+ - name: Publint
+ run: pnpm test:build
+
+ - name: Check for unused files, dependencies, and exports
+ run: pnpm knip --production
+
+ types:
+ name: Types
+ permissions:
+ contents: read
+ needs: check
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ strategy:
+ matrix:
+ typescript-version: ['5.7.3', '5.8.3', 'latest']
+ viem-version: ['2.29.2', 'latest']
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - run: pnpm add -D -w typescript@${{ matrix.typescript-version }} viem@${{ matrix.viem-version }}
+
+ - name: Link packages
+ run: pnpm preconstruct
+
+ - name: Check types
+ run: pnpm check:types
+
+ # Redundant with `pnpm check:types`
+ # If Vitest adds special features in the future, e.g. type coverage, can add this back!
+ # - name: Test types
+ permissions:
+ contents: read
+ # run: pnpm test:typecheck
+
+ test:
+ name: Test
+ permissions:
+ contents: read
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ strategy:
+ max-parallel: 3
+ matrix:
+ shard: [1, 2, 3]
+ total-shards: [3]
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ uses: wevm/actions/.github/actions/pnpm@main
+
+ - name: Set up foundry
+ uses: foundry-rs/foundry-toolchain@v1
+ with:
+ version: nightly
+
+ - name: Run tests
+ uses: nick-fields/retry@v3
+ with:
+ command: CI=true pnpm test:cov --shard=${{ matrix.shard }}/${{ matrix.total-shards }} --retry=3 --bail=1
+ max_attempts: 3
+ timeout_minutes: 5
+ env:
+ VITE_MAINNET_FORK_URL: ${{ secrets.VITE_MAINNET_FORK_URL }}
+ VITE_OPTIMISM_FORK_URL: ${{ secrets.VITE_OPTIMISM_FORK_URL }}
+
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index e70ecd7f00..8c1467da71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,41 +1,26 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+# See https://help.github.com/ignore-files/ for more about ignoring files.
-# Dependencies
-node_modules
-.pnp
-.pnp.js
+node_modules/
+cache/
+build/
+dist/
-# Local env files
-.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+test_chain/
-# Testing
-coverage
-
-# Turbo
-.turbo
-
-# Vercel
-.vercel
-
-# Build Outputs
-.next/
-out/
-build
-dist
+*.js.map
+PROD.env
+.DS_Store
+.vscode
+.idea
+*.iml
+.cache
+package-lock.json
+coverage
+.rts2_cache*
-# Debug
-npm-debug.log*
yarn-debug.log*
yarn-error.log*
+lerna-debug.log*
-# Misc
-.DS_Store
-*.pem
-
-# Husky
-.husky/
\ No newline at end of file
+.nyc_output/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..6131d73996
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/signals-implicit-mode"]
+ path = lib/signals-implicit-mode
+ url = https://github.com/0xsequence/signals-implicit-mode
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000..26d33521af
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
new file mode 100644
index 0000000000..cc4b430a73
--- /dev/null
+++ b/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,1698 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000000..4bec4ea8ae
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000000..a55e7a179b
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml
new file mode 100644
index 0000000000..1f2ea11e7f
--- /dev/null
+++ b/.idea/copilot.data.migration.ask2agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000000..c61ea3346e
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000..5691f15403
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sequence.js.iml b/.idea/sequence.js.iml
new file mode 100644
index 0000000000..d6ebd48059
--- /dev/null
+++ b/.idea/sequence.js.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000..35eb1ddfbb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000000..dccb2021ed
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,15139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1770787760033
+
+
+ 1770787760033
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..47687565bf
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,5 @@
+auto-install-peers=false
+enable-pre-post-scripts=true
+link-workspace-packages=deep
+provenance=true
+strict-peer-dependencies=false
diff --git a/.nycrc b/.nycrc
new file mode 100644
index 0000000000..9b547ac2db
--- /dev/null
+++ b/.nycrc
@@ -0,0 +1,26 @@
+{
+ "include": [
+ "packages/**/*.ts"
+ ],
+ "exclude": [
+ "**/*.d.ts",
+ "**/dist/*",
+ "**/tests/*",
+ "**/0xsequence/*"
+ ],
+ "extension": [
+ ".ts"
+ ],
+ "require": [
+ "ts-node/register",
+ "babel-core/register"
+ ],
+ "reporter": [
+ "html",
+ "text",
+ "lcov"
+ ],
+ "sourceMap": true,
+ "instrument": true,
+ "all": true
+}
diff --git a/.prettierrc b/.prettierrc
index cbe842acd7..421afa9791 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,5 +1,9 @@
{
- "printWidth": 120,
+ "tabWidth": 2,
+ "useTabs": false,
"semi": false,
- "singleQuote": true
+ "singleQuote": true,
+ "trailingComma": "none",
+ "arrowParens": "avoid",
+ "printWidth": 130
}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000000..9cb435094a
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "biomejs.biome",
+ "orta.vscode-twoslash-queries",
+ "Vue.volar"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index dc22920a87..0000000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Launch primitives-cli server",
- "type": "node",
- "request": "launch",
- "program": "${workspaceFolder}/packages/wallet/primitives-cli/dist/index.js",
- "args": ["server"],
- "runtimeArgs": ["--enable-source-maps"],
- "cwd": "${workspaceFolder}",
- "console": "integratedTerminal",
- "sourceMaps": true,
- "outFiles": [
- "${workspaceFolder}/packages/wallet/primitives-cli/dist/**/*.js",
- "${workspaceFolder}/packages/wallet/core/dist/**/*.js",
- "${workspaceFolder}/packages/wallet/primitives/dist/**/*.js",
- "${workspaceFolder}/packages/wallet/wdk/dist/**/*.js"
- ],
- "sourceMapPathOverrides": {
- "../packages/wallet/primitives-cli/src/*": "${workspaceFolder}/packages/wallet/primitives-cli/src/*",
- "../packages/wallet/core/src/*": "${workspaceFolder}/packages/wallet/core/src/*",
- "../packages/wallet/primitives/src/*": "${workspaceFolder}/packages/wallet/primitives/src/*",
- "../packages/wallet/wdk/src/*": "${workspaceFolder}/packages/wallet/wdk/src/*"
- }
- }
- ]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 44a73ec3a9..0000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "eslint.workingDirectories": [
- {
- "mode": "auto"
- }
- ]
-}
diff --git a/.vscode/workspace.code-workspace b/.vscode/workspace.code-workspace
new file mode 100644
index 0000000000..0d626129da
--- /dev/null
+++ b/.vscode/workspace.code-workspace
@@ -0,0 +1,16 @@
+{
+ "folders": [
+ {
+ "name": "docs",
+ "path": "../docs"
+ },
+ {
+ "name": "packages",
+ "path": "../packages"
+ },
+ {
+ "name": "playgrounds",
+ "path": "../playgrounds"
+ }
+ ]
+}
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000000..1889d9bae7
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+sequence.app
diff --git a/FUNDING.json b/FUNDING.json
new file mode 100644
index 0000000000..5e01254162
--- /dev/null
+++ b/FUNDING.json
@@ -0,0 +1,10 @@
+{
+ "drips": {
+ "ethereum": {
+ "ownedBy": "0xd2135CfB216b74109775236E36d4b433F1DF507B"
+ }
+ },
+ "opRetro": {
+ "projectId": "0xc0615947773148cbc340b175fb9afc98dbb4e0acd31d018b1ee41a5538785abf"
+ }
+}
diff --git a/LICENSE b/LICENSE
index d645695673..bf69ef4b95 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,20 @@
+ Copyright (c) 2017-present Horizon Blockchain Games Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ ------------------------------------------------------------------------
+
Apache License
Version 2.0, January 2004
diff --git a/README.md b/README.md
index ae41ffdbd5..15f6f79541 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1 @@
-## sequence.js v3 core libraries and SDK
-
-**NOTE: please see [v2](https://github.com/0xsequence/sequence.js/tree/v2) branch for sequence.js 2.x.x**
-
----
-
-Sequence v3 core libraries and [wallet-contracts-v3](https://github.com/0xsequence/wallet-contracts-v3) SDK.
-
-## Packages
-
-- `@0xsequence/wallet-primitives`: stateless low-level utilities specifically for interacting directly with sequence wallet's smart contracts
-- `@0xsequence/wallet-core`: higher level utilities for creating and using sequence wallets
-- `@0xsequence/wallet-wdk`: all-in-one wallet development kit for building a sequence wallet product
-
-## Development
-
-### Getting Started
-
-1. Install dependencies:
- `pnpm install`
-
-2. Build all packages:
- `pnpm build`
-
-### Development Workflow
-
-- Run development mode across all packages:
- `pnpm dev`
-
-- Run tests:
- `pnpm test`
-
- > **Note:** Tests require [anvil](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) and [forge](https://github.com/foundry-rs/foundry) to be installed. You can run a local anvil instance using `pnpm run test:anvil`.
-
-- Linting and formatting is enforced via git hooks
-
-## License
-
-Apache-2.0
+This is a [Vite](https://vitejs.dev) project bootstrapped with [`create-wagmi`](https://github.com/wevm/wagmi/tree/main/packages/create-wagmi).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..5b42dc2ef3
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,19 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 5.1.x | :white_check_mark: |
+| 5.0.x | :x: |
+| 4.0.x | :white_check_mark: |
+| < 4.0 | :x: |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+To report a vulnerability, please email us at [dev@wevm.dev](mailto:dev@wevm.dev). We aim to provide an initial response within 48 hours and will keep you updated on the status of the reported vulnerability.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000000..676233afaf
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,21 @@
+# Node.js
+# Build a general Node.js project with npm.
+# Add steps that analyze code, save build artifacts, deploy, and more:
+# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
+
+trigger:
+- master
+
+pool:
+ vmImage: ubuntu-latest
+
+steps:
+- task: NodeTool@0
+ inputs:
+ versionSpec: '10.x'
+ displayName: 'Install Node.js'
+
+- script: |
+ npm install
+ npm run build
+ displayName: 'npm install and build'
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000000..226b59df34
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,19 @@
+module.exports = {
+ presets: [
+ ['@babel/preset-env', {
+ targets: {
+ esmodules: true
+ },
+ bugfixes: true,
+ loose: true,
+ exclude: [
+ '@babel/plugin-transform-async-to-generator',
+ '@babel/plugin-transform-regenerator'
+ ]
+ }],
+ '@babel/preset-typescript'
+ ],
+ plugins: [
+ ['@babel/plugin-transform-class-properties', { loose: true }]
+ ]
+}
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000000..ce99662cb0
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,89 @@
+{
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+ "files": {
+ "ignore": ["CHANGELOG.md", "pnpm-lock.yaml", "tsconfig.base.json"]
+ },
+ "formatter": {
+ "enabled": true,
+ "formatWithErrors": false,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 80
+ },
+ "linter": {
+ "ignore": ["packages/create-wagmi/templates/*"],
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "a11y": {
+ "useButtonType": "off"
+ },
+ "correctness": {
+ "noUnusedVariables": "error",
+ "useExhaustiveDependencies": "error"
+ },
+ "performance": {
+ "noBarrelFile": "error",
+ "noReExportAll": "error",
+ "noDelete": "off"
+ },
+ "style": {
+ "noNonNullAssertion": "off",
+ "useShorthandArrayType": "error"
+ },
+ "suspicious": {
+ "noArrayIndexKey": "off",
+ "noConfusingVoidType": "off",
+ "noConsoleLog": "error",
+ "noExplicitAny": "off"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "trailingCommas": "all",
+ "semicolons": "asNeeded"
+ }
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "overrides": [
+ {
+ "include": ["*.vue"],
+ "linter": {
+ "rules": {
+ "correctness": {
+ "noUnusedVariables": "off"
+ }
+ }
+ }
+ },
+ {
+ "include": ["./scripts/**/*.ts"],
+ "linter": {
+ "rules": {
+ "suspicious": {
+ "noConsoleLog": "off"
+ }
+ }
+ }
+ },
+ {
+ "include": ["./playgrounds/**"],
+ "linter": {
+ "rules": {
+ "style": {
+ "useNodejsImportProtocol": "off"
+ }
+ }
+ }
+ }
+ ],
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ }
+}
diff --git a/corepack.tgz b/corepack.tgz
new file mode 100644
index 0000000000..afa76b295c
Binary files /dev/null and b/corepack.tgz differ
diff --git a/extras/docs/.gitignore b/extras/docs/.gitignore
deleted file mode 100644
index f886745c52..0000000000
--- a/extras/docs/.gitignore
+++ /dev/null
@@ -1,36 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# env files (can opt-in for commiting if needed)
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
diff --git a/extras/docs/README.md b/extras/docs/README.md
deleted file mode 100644
index a98bfa8140..0000000000
--- a/extras/docs/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/extras/docs/app/favicon.ico b/extras/docs/app/favicon.ico
deleted file mode 100644
index 718d6fea48..0000000000
Binary files a/extras/docs/app/favicon.ico and /dev/null differ
diff --git a/extras/docs/app/fonts/GeistMonoVF.woff b/extras/docs/app/fonts/GeistMonoVF.woff
deleted file mode 100644
index f2ae185cbf..0000000000
Binary files a/extras/docs/app/fonts/GeistMonoVF.woff and /dev/null differ
diff --git a/extras/docs/app/fonts/GeistVF.woff b/extras/docs/app/fonts/GeistVF.woff
deleted file mode 100644
index 1b62daacff..0000000000
Binary files a/extras/docs/app/fonts/GeistVF.woff and /dev/null differ
diff --git a/extras/docs/app/globals.css b/extras/docs/app/globals.css
deleted file mode 100644
index 6af7ecbbb8..0000000000
--- a/extras/docs/app/globals.css
+++ /dev/null
@@ -1,50 +0,0 @@
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- color: var(--foreground);
- background: var(--background);
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-.imgDark {
- display: none;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-
- .imgLight {
- display: none;
- }
- .imgDark {
- display: unset;
- }
-}
diff --git a/extras/docs/app/layout.tsx b/extras/docs/app/layout.tsx
deleted file mode 100644
index 2e5719345e..0000000000
--- a/extras/docs/app/layout.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Metadata } from 'next'
-import localFont from 'next/font/local'
-import './globals.css'
-
-const geistSans = localFont({
- src: './fonts/GeistVF.woff',
- variable: '--font-geist-sans',
-})
-const geistMono = localFont({
- src: './fonts/GeistMonoVF.woff',
- variable: '--font-geist-mono',
-})
-
-export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
-}
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode
-}>) {
- return (
-
- {children}
-
- )
-}
diff --git a/extras/docs/app/page.module.css b/extras/docs/app/page.module.css
deleted file mode 100644
index 3630662c6f..0000000000
--- a/extras/docs/app/page.module.css
+++ /dev/null
@@ -1,188 +0,0 @@
-.page {
- --gray-rgb: 0, 0, 0;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
-
- --button-primary-hover: #383838;
- --button-secondary-hover: #f2f2f2;
-
- display: grid;
- grid-template-rows: 20px 1fr 20px;
- align-items: center;
- justify-items: center;
- min-height: 100svh;
- padding: 80px;
- gap: 64px;
- font-synthesis: none;
-}
-
-@media (prefers-color-scheme: dark) {
- .page {
- --gray-rgb: 255, 255, 255;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
-
- --button-primary-hover: #ccc;
- --button-secondary-hover: #1a1a1a;
- }
-}
-
-.main {
- display: flex;
- flex-direction: column;
- gap: 32px;
- grid-row-start: 2;
-}
-
-.main ol {
- font-family: var(--font-geist-mono);
- padding-left: 0;
- margin: 0;
- font-size: 14px;
- line-height: 24px;
- letter-spacing: -0.01em;
- list-style-position: inside;
-}
-
-.main li:not(:last-of-type) {
- margin-bottom: 8px;
-}
-
-.main code {
- font-family: inherit;
- background: var(--gray-alpha-100);
- padding: 2px 4px;
- border-radius: 4px;
- font-weight: 600;
-}
-
-.ctas {
- display: flex;
- gap: 16px;
-}
-
-.ctas a {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
-}
-
-a.primary {
- background: var(--foreground);
- color: var(--background);
- gap: 8px;
-}
-
-a.secondary {
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-button.secondary {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
- background: transparent;
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-.footer {
- font-family: var(--font-geist-sans);
- grid-row-start: 3;
- display: flex;
- gap: 24px;
-}
-
-.footer a {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.footer img {
- flex-shrink: 0;
-}
-
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- a.primary:hover {
- background: var(--button-primary-hover);
- border-color: transparent;
- }
-
- a.secondary:hover {
- background: var(--button-secondary-hover);
- border-color: transparent;
- }
-
- .footer a:hover {
- text-decoration: underline;
- text-underline-offset: 4px;
- }
-}
-
-@media (max-width: 600px) {
- .page {
- padding: 32px;
- padding-bottom: 80px;
- }
-
- .main {
- align-items: center;
- }
-
- .main ol {
- text-align: center;
- }
-
- .ctas {
- flex-direction: column;
- }
-
- .ctas a {
- font-size: 14px;
- height: 40px;
- padding: 0 16px;
- }
-
- a.secondary {
- min-width: auto;
- }
-
- .footer {
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .logo {
- filter: invert();
- }
-}
diff --git a/extras/docs/app/page.tsx b/extras/docs/app/page.tsx
deleted file mode 100644
index 980bd5ff3e..0000000000
--- a/extras/docs/app/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import Image, { type ImageProps } from 'next/image'
-import { Button } from '@repo/ui/button'
-import styles from './page.module.css'
-
-type Props = Omit & {
- srcLight: string
- srcDark: string
-}
-
-const ThemeImage = (props: Props) => {
- const { srcLight, srcDark, ...rest } = props
-
- return (
- <>
-
-
- >
- )
-}
-
-export default function Home() {
- return (
-
-
-
-
-
- Get started by editing apps/docs/app/page.tsx
-
- Save and see your changes instantly.
-
-
-
-
- Open alert
-
-
-
-
- )
-}
diff --git a/extras/docs/eslint.config.js b/extras/docs/eslint.config.js
deleted file mode 100644
index 0fbeffd979..0000000000
--- a/extras/docs/eslint.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { nextJsConfig } from '@repo/eslint-config/next-js'
-
-/** @type {import("eslint").Linter.Config} */
-export default [
- ...nextJsConfig,
- {
- ignores: ['next-env.d.ts'],
- },
-]
diff --git a/extras/docs/next.config.js b/extras/docs/next.config.js
deleted file mode 100644
index 2963459c42..0000000000
--- a/extras/docs/next.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const workspaceRoot = path.join(__dirname, '..', '..')
-
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- // Anchor output tracing to the monorepo root so Next.js doesn't pick up
- // sibling lockfiles and mis-detect the workspace boundary during lint/build.
- outputFileTracingRoot: workspaceRoot,
-}
-
-export default nextConfig
diff --git a/extras/docs/package.json b/extras/docs/package.json
deleted file mode 100644
index 5b275db5ba..0000000000
--- a/extras/docs/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "docs",
- "version": "0.1.0",
- "type": "module",
- "private": true,
- "scripts": {
- "dev": "next dev --turbopack --port 3001",
- "build": "next build",
- "start": "next start",
- "lint": "eslint . --max-warnings 0",
- "typecheck": "tsc --noEmit",
- "clean": "rimraf .next"
- },
- "dependencies": {
- "@repo/ui": "workspace:^",
- "next": "^15.5.16",
- "react": "^19.2.3",
- "react-dom": "^19.2.3"
- },
- "devDependencies": {
- "@repo/eslint-config": "workspace:^",
- "@repo/typescript-config": "workspace:^",
- "@types/node": "^25.3.0",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "eslint": "^9.39.2",
- "typescript": "^6.0.3"
- }
-}
diff --git a/extras/docs/public/file-text.svg b/extras/docs/public/file-text.svg
deleted file mode 100644
index 9cfb3c9867..0000000000
--- a/extras/docs/public/file-text.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/extras/docs/public/globe.svg b/extras/docs/public/globe.svg
deleted file mode 100644
index 4230a3d207..0000000000
--- a/extras/docs/public/globe.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/docs/public/next.svg b/extras/docs/public/next.svg
deleted file mode 100644
index 5174b28c56..0000000000
--- a/extras/docs/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extras/docs/public/turborepo-dark.svg b/extras/docs/public/turborepo-dark.svg
deleted file mode 100644
index dae38fed54..0000000000
--- a/extras/docs/public/turborepo-dark.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/docs/public/turborepo-light.svg b/extras/docs/public/turborepo-light.svg
deleted file mode 100644
index ddea915815..0000000000
--- a/extras/docs/public/turborepo-light.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/docs/public/vercel.svg b/extras/docs/public/vercel.svg
deleted file mode 100644
index 0164ddc5ad..0000000000
--- a/extras/docs/public/vercel.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/docs/public/window.svg b/extras/docs/public/window.svg
deleted file mode 100644
index bbc780069c..0000000000
--- a/extras/docs/public/window.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/extras/docs/tsconfig.json b/extras/docs/tsconfig.json
deleted file mode 100644
index 7b98032678..0000000000
--- a/extras/docs/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "extends": "@repo/typescript-config/nextjs.json",
- "compilerOptions": {
- "plugins": [
- {
- "name": "next"
- }
- ]
- },
- "include": [
- "**/*.ts",
- "**/*.tsx",
- "../../repo/typescript-config/next-css-side-effect.d.ts",
- "next-env.d.ts",
- "next.config.js",
- ".next/types/**/*.ts"
- ],
- "exclude": ["node_modules"]
-}
diff --git a/extras/web/.gitignore b/extras/web/.gitignore
deleted file mode 100644
index f886745c52..0000000000
--- a/extras/web/.gitignore
+++ /dev/null
@@ -1,36 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# env files (can opt-in for commiting if needed)
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
diff --git a/extras/web/README.md b/extras/web/README.md
deleted file mode 100644
index a98bfa8140..0000000000
--- a/extras/web/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/extras/web/app/favicon.ico b/extras/web/app/favicon.ico
deleted file mode 100644
index 718d6fea48..0000000000
Binary files a/extras/web/app/favicon.ico and /dev/null differ
diff --git a/extras/web/app/fonts/GeistMonoVF.woff b/extras/web/app/fonts/GeistMonoVF.woff
deleted file mode 100644
index f2ae185cbf..0000000000
Binary files a/extras/web/app/fonts/GeistMonoVF.woff and /dev/null differ
diff --git a/extras/web/app/fonts/GeistVF.woff b/extras/web/app/fonts/GeistVF.woff
deleted file mode 100644
index 1b62daacff..0000000000
Binary files a/extras/web/app/fonts/GeistVF.woff and /dev/null differ
diff --git a/extras/web/app/globals.css b/extras/web/app/globals.css
deleted file mode 100644
index 6af7ecbbb8..0000000000
--- a/extras/web/app/globals.css
+++ /dev/null
@@ -1,50 +0,0 @@
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- color: var(--foreground);
- background: var(--background);
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-.imgDark {
- display: none;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-
- .imgLight {
- display: none;
- }
- .imgDark {
- display: unset;
- }
-}
diff --git a/extras/web/app/layout.tsx b/extras/web/app/layout.tsx
deleted file mode 100644
index 2e5719345e..0000000000
--- a/extras/web/app/layout.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Metadata } from 'next'
-import localFont from 'next/font/local'
-import './globals.css'
-
-const geistSans = localFont({
- src: './fonts/GeistVF.woff',
- variable: '--font-geist-sans',
-})
-const geistMono = localFont({
- src: './fonts/GeistMonoVF.woff',
- variable: '--font-geist-mono',
-})
-
-export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
-}
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode
-}>) {
- return (
-
- {children}
-
- )
-}
diff --git a/extras/web/app/page.module.css b/extras/web/app/page.module.css
deleted file mode 100644
index 3630662c6f..0000000000
--- a/extras/web/app/page.module.css
+++ /dev/null
@@ -1,188 +0,0 @@
-.page {
- --gray-rgb: 0, 0, 0;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
-
- --button-primary-hover: #383838;
- --button-secondary-hover: #f2f2f2;
-
- display: grid;
- grid-template-rows: 20px 1fr 20px;
- align-items: center;
- justify-items: center;
- min-height: 100svh;
- padding: 80px;
- gap: 64px;
- font-synthesis: none;
-}
-
-@media (prefers-color-scheme: dark) {
- .page {
- --gray-rgb: 255, 255, 255;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
-
- --button-primary-hover: #ccc;
- --button-secondary-hover: #1a1a1a;
- }
-}
-
-.main {
- display: flex;
- flex-direction: column;
- gap: 32px;
- grid-row-start: 2;
-}
-
-.main ol {
- font-family: var(--font-geist-mono);
- padding-left: 0;
- margin: 0;
- font-size: 14px;
- line-height: 24px;
- letter-spacing: -0.01em;
- list-style-position: inside;
-}
-
-.main li:not(:last-of-type) {
- margin-bottom: 8px;
-}
-
-.main code {
- font-family: inherit;
- background: var(--gray-alpha-100);
- padding: 2px 4px;
- border-radius: 4px;
- font-weight: 600;
-}
-
-.ctas {
- display: flex;
- gap: 16px;
-}
-
-.ctas a {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
-}
-
-a.primary {
- background: var(--foreground);
- color: var(--background);
- gap: 8px;
-}
-
-a.secondary {
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-button.secondary {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
- background: transparent;
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-.footer {
- font-family: var(--font-geist-sans);
- grid-row-start: 3;
- display: flex;
- gap: 24px;
-}
-
-.footer a {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.footer img {
- flex-shrink: 0;
-}
-
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- a.primary:hover {
- background: var(--button-primary-hover);
- border-color: transparent;
- }
-
- a.secondary:hover {
- background: var(--button-secondary-hover);
- border-color: transparent;
- }
-
- .footer a:hover {
- text-decoration: underline;
- text-underline-offset: 4px;
- }
-}
-
-@media (max-width: 600px) {
- .page {
- padding: 32px;
- padding-bottom: 80px;
- }
-
- .main {
- align-items: center;
- }
-
- .main ol {
- text-align: center;
- }
-
- .ctas {
- flex-direction: column;
- }
-
- .ctas a {
- font-size: 14px;
- height: 40px;
- padding: 0 16px;
- }
-
- a.secondary {
- min-width: auto;
- }
-
- .footer {
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .logo {
- filter: invert();
- }
-}
diff --git a/extras/web/app/page.tsx b/extras/web/app/page.tsx
deleted file mode 100644
index 4db7245678..0000000000
--- a/extras/web/app/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import Image, { type ImageProps } from 'next/image'
-import { Button } from '@repo/ui/button'
-import styles from './page.module.css'
-
-type Props = Omit & {
- srcLight: string
- srcDark: string
-}
-
-const ThemeImage = (props: Props) => {
- const { srcLight, srcDark, ...rest } = props
-
- return (
- <>
-
-
- >
- )
-}
-
-export default function Home() {
- return (
-
-
-
-
-
- Get started by editing apps/web/app/page.tsx
-
- Save and see your changes instantly.
-
-
-
-
- Open alert
-
-
-
-
- )
-}
diff --git a/extras/web/eslint.config.js b/extras/web/eslint.config.js
deleted file mode 100644
index 0fbeffd979..0000000000
--- a/extras/web/eslint.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { nextJsConfig } from '@repo/eslint-config/next-js'
-
-/** @type {import("eslint").Linter.Config} */
-export default [
- ...nextJsConfig,
- {
- ignores: ['next-env.d.ts'],
- },
-]
diff --git a/extras/web/next.config.js b/extras/web/next.config.js
deleted file mode 100644
index 2963459c42..0000000000
--- a/extras/web/next.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const workspaceRoot = path.join(__dirname, '..', '..')
-
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- // Anchor output tracing to the monorepo root so Next.js doesn't pick up
- // sibling lockfiles and mis-detect the workspace boundary during lint/build.
- outputFileTracingRoot: workspaceRoot,
-}
-
-export default nextConfig
diff --git a/extras/web/package.json b/extras/web/package.json
deleted file mode 100644
index e042bc8e64..0000000000
--- a/extras/web/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "web",
- "version": "0.1.0",
- "type": "module",
- "private": true,
- "scripts": {
- "dev": "next dev --turbopack --port 3000",
- "build": "next build",
- "start": "next start",
- "lint": "eslint . --max-warnings 0",
- "typecheck": "tsc --noEmit",
- "clean": "rimraf .next"
- },
- "dependencies": {
- "@repo/ui": "workspace:^",
- "next": "^15.5.16",
- "react": "^19.2.3",
- "react-dom": "^19.2.3"
- },
- "devDependencies": {
- "@repo/eslint-config": "workspace:^",
- "@repo/typescript-config": "workspace:^",
- "@types/node": "^25.3.0",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "eslint": "^9.39.2",
- "typescript": "^6.0.3"
- }
-}
diff --git a/extras/web/public/file-text.svg b/extras/web/public/file-text.svg
deleted file mode 100644
index 9cfb3c9867..0000000000
--- a/extras/web/public/file-text.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/extras/web/public/globe.svg b/extras/web/public/globe.svg
deleted file mode 100644
index 4230a3d207..0000000000
--- a/extras/web/public/globe.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/web/public/next.svg b/extras/web/public/next.svg
deleted file mode 100644
index 5174b28c56..0000000000
--- a/extras/web/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extras/web/public/turborepo-dark.svg b/extras/web/public/turborepo-dark.svg
deleted file mode 100644
index dae38fed54..0000000000
--- a/extras/web/public/turborepo-dark.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/web/public/turborepo-light.svg b/extras/web/public/turborepo-light.svg
deleted file mode 100644
index ddea915815..0000000000
--- a/extras/web/public/turborepo-light.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/web/public/vercel.svg b/extras/web/public/vercel.svg
deleted file mode 100644
index 0164ddc5ad..0000000000
--- a/extras/web/public/vercel.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/extras/web/public/window.svg b/extras/web/public/window.svg
deleted file mode 100644
index bbc780069c..0000000000
--- a/extras/web/public/window.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/extras/web/tsconfig.json b/extras/web/tsconfig.json
deleted file mode 100644
index 7b98032678..0000000000
--- a/extras/web/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "extends": "@repo/typescript-config/nextjs.json",
- "compilerOptions": {
- "plugins": [
- {
- "name": "next"
- }
- ]
- },
- "include": [
- "**/*.ts",
- "**/*.tsx",
- "../../repo/typescript-config/next-css-side-effect.d.ts",
- "next-env.d.ts",
- "next.config.js",
- ".next/types/**/*.ts"
- ],
- "exclude": ["node_modules"]
-}
diff --git a/foundry.lock b/foundry.lock
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/foundry.lock
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000..f519ce85a7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Create Wagmi
+
+
+
+
+
+
diff --git a/lefthook.yml b/lefthook.yml
deleted file mode 100644
index 5402d7dc7d..0000000000
--- a/lefthook.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-pre-commit:
- commands:
- prettier:
- glob: '**/*.{js,jsx,ts,tsx,json,md,yml,yaml}'
- run: pnpm prettier --write {staged_files} && git add {staged_files}
- lint:
- run: pnpm lint
- typecheck:
- run: pnpm typecheck
- syncpack:
- glob:
- - "package.json"
- - "packages/**/package.json"
- run: pnpm deps:lint
-
-pre-push:
- commands:
- build:
- run: pnpm build:packages
- test:
- run: pnpm test
diff --git a/packages/0xsequence/README.md b/packages/0xsequence/README.md
new file mode 100644
index 0000000000..1b4b9e6704
--- /dev/null
+++ b/packages/0xsequence/README.md
@@ -0,0 +1,67 @@
+0xsequence
+==========
+
+## Install
+
+`npm install 0xsequence ethers`
+
+or
+
+`pnpm install 0xsequence ethers`
+
+or
+
+`yarn add 0xsequence ethers`
+
+
+## Development Workflow
+
+Sequence is a critical piece of software and any change should be delivered via a TDD (test-driven development)
+workflow.
+
+As well, sequence.js's monorepo tooling is setup with preconstruct, which links all sub-packages together
+so it feels like a single program and is easy to work with. Please run `pnpm dev` in the root of `sequence.js/`
+folder to ensure the monorepo is in 'dev-mode'.
+
+Second, you can run the test suite directly from console with a single `pnpm test`, or you can boot up the Typescript
+compiling server (`pnpm test:server`) and ethereum test node (`pnpm start:hardhat` and `pnpm start:hardhat2`) manually
+in separate terminals, and then run a specific test directly from your browser instance. We recommend running the
+test stack separately and running specific browser tests manually during development. See [here for recommended setup](./#from-browser).
+
+
+## Running E2E Tests
+
+This 0xsequence top-level package contains e2e tests which run in a headless chrome browser.
+
+You can view tests running directly from the browser directly, or from the cli which will communicate
+to the headless browser behind the scenes. See below. Please note, for an improved development workflow
+we highly recommend to view your tests running from the browser as its more clear and better experience.
+
+
+### From Browser
+
+1. `pnpm test:server` -- in one terminal, to start the webpack server compiling typescript
+2. `pnpm start:hardhat` -- in a second terminal, to start hardhat local ethereum test node
+3. `pnpm start:hardhat2` -- (2nd chain) in a third terminal, to start hardhat2 local ethereum test node
+4. open browser to `http://localhost:9999/{browser-test-dir}/{test-filename}.test.html` for example,
+ http://localhost:9999/wallet-provider/dapp.test.html
+5. open your browser console so you can see the tests running and their results.
+
+Finally, if you'd like to run only a specific test case, either add a temporary "return" statement
+following the last test case, so you will preempt the runner after a certain test case.
+
+As well, since you have all the services running in terminals, you can also execute commands via
+the cli by calling `test:run`, which is similar to step 4 above, but executing all tests from the terminal.
+There is also the `test:only` command if you'd like to execute a specific test from ./tests/browser/*.spec.ts
+file, ie. `pnpm test:only window-transport`.
+
+
+### From CLI
+
+With a single command, you can spin up the testing stack and execute tests:
+
+`pnpm test`
+
+This is useful for a sanity check to ensure tests pass, or using it with the CI. However, if you're
+developing on sequence.js, its highly recommended you follow the [development workflow instructions](./#development-workflow).
+
diff --git a/packages/0xsequence/hardhat.config.js b/packages/0xsequence/hardhat.config.js
new file mode 100644
index 0000000000..88c1e3f0a6
--- /dev/null
+++ b/packages/0xsequence/hardhat.config.js
@@ -0,0 +1,21 @@
+/**
+ * @type import('hardhat/config').HardhatUserConfig
+ */
+module.exports = {
+ solidity: '0.7.6',
+
+ networks: {
+ hardhat: {
+ // gas: 10000000000000,
+ // blockGasLimit: 10000000000000,
+ // gasPrice: 2,
+ initialBaseFeePerGas: 1,
+ chainId: 31337,
+ accounts: {
+ mnemonic: 'ripple axis someone ridge uniform wrist prosper there frog rate olympic knee'
+ },
+ // loggingEnabled: true
+ // verbose: true
+ },
+ }
+}
diff --git a/packages/0xsequence/hardhat2.config.js b/packages/0xsequence/hardhat2.config.js
new file mode 100644
index 0000000000..4ec2897be8
--- /dev/null
+++ b/packages/0xsequence/hardhat2.config.js
@@ -0,0 +1,21 @@
+/**
+ * @type import('hardhat/config').HardhatUserConfig
+ */
+module.exports = {
+ solidity: '0.7.6',
+
+ networks: {
+ hardhat: {
+ // gas: 10000000000000,
+ // blockGasLimit: 10000000000000,
+ // gasPrice: 2,
+ initialBaseFeePerGas: 1,
+ chainId: 31338,
+ accounts: {
+ mnemonic: 'ripple axis someone ridge uniform wrist prosper there frog rate olympic knee'
+ },
+ // loggingEnabled: true
+ // verbose: true
+ },
+ }
+}
diff --git a/packages/0xsequence/src/abi.ts b/packages/0xsequence/src/abi.ts
new file mode 100644
index 0000000000..56f239636f
--- /dev/null
+++ b/packages/0xsequence/src/abi.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/abi'
diff --git a/packages/0xsequence/src/account.ts b/packages/0xsequence/src/account.ts
new file mode 100644
index 0000000000..5378d52938
--- /dev/null
+++ b/packages/0xsequence/src/account.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/account'
diff --git a/packages/0xsequence/src/api.ts b/packages/0xsequence/src/api.ts
new file mode 100644
index 0000000000..157694d571
--- /dev/null
+++ b/packages/0xsequence/src/api.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/api'
diff --git a/packages/0xsequence/src/auth.ts b/packages/0xsequence/src/auth.ts
new file mode 100644
index 0000000000..5ea89b7eae
--- /dev/null
+++ b/packages/0xsequence/src/auth.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/auth'
diff --git a/packages/0xsequence/src/core.ts b/packages/0xsequence/src/core.ts
new file mode 100644
index 0000000000..c9df6528a9
--- /dev/null
+++ b/packages/0xsequence/src/core.ts
@@ -0,0 +1,6 @@
+import { commons } from '@0xsequence/core'
+
+export * from '@0xsequence/core'
+
+export type Config = commons.config.Config
+export type WalletContext = commons.context.WalletContext
diff --git a/packages/0xsequence/src/guard.ts b/packages/0xsequence/src/guard.ts
new file mode 100644
index 0000000000..d91cdc9030
--- /dev/null
+++ b/packages/0xsequence/src/guard.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/guard'
diff --git a/packages/0xsequence/src/index.ts b/packages/0xsequence/src/index.ts
new file mode 100644
index 0000000000..e8f182e4c3
--- /dev/null
+++ b/packages/0xsequence/src/index.ts
@@ -0,0 +1,3 @@
+export * as sequence from './sequence'
+
+export { initWallet } from '@0xsequence/provider'
diff --git a/packages/0xsequence/src/indexer.ts b/packages/0xsequence/src/indexer.ts
new file mode 100644
index 0000000000..e59cb5bcef
--- /dev/null
+++ b/packages/0xsequence/src/indexer.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/indexer'
diff --git a/packages/0xsequence/src/metadata.ts b/packages/0xsequence/src/metadata.ts
new file mode 100644
index 0000000000..cb9f181988
--- /dev/null
+++ b/packages/0xsequence/src/metadata.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/metadata'
diff --git a/packages/0xsequence/src/migration.ts b/packages/0xsequence/src/migration.ts
new file mode 100644
index 0000000000..029dfd7095
--- /dev/null
+++ b/packages/0xsequence/src/migration.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/migration'
diff --git a/packages/0xsequence/src/network.ts b/packages/0xsequence/src/network.ts
new file mode 100644
index 0000000000..1d9b96b92e
--- /dev/null
+++ b/packages/0xsequence/src/network.ts
@@ -0,0 +1,17 @@
+export * from '@0xsequence/network'
+
+export type {
+ JsonRpcRequest,
+ JsonRpcResponse,
+ JsonRpcResponseCallback,
+ JsonRpcHandler,
+ JsonRpcSender,
+ EIP1193Provider,
+ EIP1193ProviderFunc,
+ JsonRpcSendFunc,
+ JsonRpcSendAsyncFunc,
+ JsonRpcMiddleware,
+ JsonRpcMiddlewareHandler,
+ NetworkConfig,
+ ChainIdLike
+} from '@0xsequence/network'
diff --git a/packages/0xsequence/src/provider.ts b/packages/0xsequence/src/provider.ts
new file mode 100644
index 0000000000..65262e5f43
--- /dev/null
+++ b/packages/0xsequence/src/provider.ts
@@ -0,0 +1,29 @@
+export * from '@0xsequence/provider'
+
+export type {
+ SequenceProvider,
+ ProviderConfig,
+ WalletSignInOptions,
+ ProviderTransport,
+ WalletTransport,
+ ProviderMessage,
+ ProviderMessageRequest,
+ ProviderMessageResponse,
+ ProviderMessageResponseCallback,
+ ProviderMessageRequestHandler,
+ ProviderMessageTransport,
+ WalletEventTypes,
+ ProviderEventTypes,
+ EventType,
+ WalletSession,
+ OpenState,
+ ConnectOptions,
+ ConnectDetails,
+ PromptConnectDetails,
+ OpenWalletIntent,
+ ETHAuthProof,
+ ProviderError,
+ MessageToSign,
+ ProviderRpcError,
+ ErrSignedInRequired
+} from '@0xsequence/provider'
diff --git a/packages/0xsequence/src/relayer.ts b/packages/0xsequence/src/relayer.ts
new file mode 100644
index 0000000000..92995de5f8
--- /dev/null
+++ b/packages/0xsequence/src/relayer.ts
@@ -0,0 +1,3 @@
+export * from '@0xsequence/relayer'
+
+export type { Relayer, RpcRelayerProto, RelayerTxReceipt } from '@0xsequence/relayer'
diff --git a/packages/0xsequence/src/sequence.ts b/packages/0xsequence/src/sequence.ts
new file mode 100644
index 0000000000..fac5905f24
--- /dev/null
+++ b/packages/0xsequence/src/sequence.ts
@@ -0,0 +1,20 @@
+export * as abi from './abi'
+export * as api from './api'
+export * as auth from './auth'
+export * as guard from './guard'
+export * as indexer from './indexer'
+export * as metadata from './metadata'
+export * as network from './network'
+export * as provider from './provider'
+export * as relayer from './relayer'
+export * as transactions from './transactions'
+export * as utils from './utils'
+export * as core from './core'
+export * as signhub from './signhub'
+export * as sessions from './sessions'
+export * as migration from './migration'
+export * as account from './account'
+
+export { initWallet, getWallet, unregisterWallet, SequenceProvider, SequenceClient, SequenceSigner } from '@0xsequence/provider'
+
+export type { ProviderConfig, WalletSession } from '@0xsequence/provider'
diff --git a/packages/0xsequence/src/sessions.ts b/packages/0xsequence/src/sessions.ts
new file mode 100644
index 0000000000..9a4eebe7c0
--- /dev/null
+++ b/packages/0xsequence/src/sessions.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/sessions'
diff --git a/packages/0xsequence/src/signhub.ts b/packages/0xsequence/src/signhub.ts
new file mode 100644
index 0000000000..6c49ae506d
--- /dev/null
+++ b/packages/0xsequence/src/signhub.ts
@@ -0,0 +1 @@
+export * from '@0xsequence/signhub'
diff --git a/packages/0xsequence/src/transactions.ts b/packages/0xsequence/src/transactions.ts
new file mode 100644
index 0000000000..f2b08c462f
--- /dev/null
+++ b/packages/0xsequence/src/transactions.ts
@@ -0,0 +1,10 @@
+import { commons } from '@0xsequence/core'
+
+export const transactions = commons.transaction
+
+export type Transaction = commons.transaction.Transaction
+export type TransactionEncoded = commons.transaction.TransactionEncoded
+export type TransactionResponse = commons.transaction.TransactionResponse
+export type Transactionish = commons.transaction.Transactionish
+export type SignedTransactionBundle = commons.transaction.SignedTransactionBundle
+export type RelayReadyTransactionBundle = commons.transaction.RelayReadyTransactionBundle
diff --git a/packages/0xsequence/src/utils.ts b/packages/0xsequence/src/utils.ts
new file mode 100644
index 0000000000..40b856986b
--- /dev/null
+++ b/packages/0xsequence/src/utils.ts
@@ -0,0 +1,5 @@
+export * from '@0xsequence/utils'
+
+export { isValidSignature, isValidMessageSignature, isValidTypedDataSignature, isWalletUpToDate } from '@0xsequence/provider'
+
+export type { TypedData, TypedDataDomain, TypedDataField, LogLevel, LoggerConfig } from '@0xsequence/utils'
diff --git a/packages/0xsequence/tests/browser/json-rpc-provider/rpc.test.ts b/packages/0xsequence/tests/browser/json-rpc-provider/rpc.test.ts
new file mode 100644
index 0000000000..4e4f2e5746
--- /dev/null
+++ b/packages/0xsequence/tests/browser/json-rpc-provider/rpc.test.ts
@@ -0,0 +1,37 @@
+import { test, assert } from '../../utils/assert'
+
+import { configureLogger } from '@0xsequence/utils'
+import { JsonRpcProvider } from '@0xsequence/network'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+export const tests = async () => {
+ const provider = new JsonRpcProvider('http://localhost:8545', { chainId: 31337 }, { cacheTimeout: -1 })
+
+ await test('sending a json-rpc request', async () => {
+ {
+ const network = await provider.getNetwork()
+ console.log('network?', network)
+ }
+ {
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(BigInt(chainId), 31337n)
+ }
+ {
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(BigInt(chainId), 31337n)
+ }
+ {
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(BigInt(chainId), 31337n)
+ }
+ {
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(BigInt(chainId), 31337n)
+ }
+ {
+ const netVersion = await provider.send('net_version', [])
+ assert.equal(netVersion, '31337')
+ }
+ })
+}
diff --git a/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts b/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts
new file mode 100644
index 0000000000..38e761aeba
--- /dev/null
+++ b/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts
@@ -0,0 +1,120 @@
+import { ethers } from 'ethers'
+import { WalletRequestHandler, WindowMessageHandler } from '@0xsequence/provider'
+import { Account } from '@0xsequence/account'
+import { NetworkConfig } from '@0xsequence/network'
+import { LocalRelayer } from '@0xsequence/relayer'
+import { configureLogger } from '@0xsequence/utils'
+
+import { testAccounts, getEOAWallet } from '../testutils'
+import { test, assert } from '../../utils/assert'
+import * as utils from '@0xsequence/tests'
+import { Orchestrator } from '@0xsequence/signhub'
+import { trackers } from '@0xsequence/sessions'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+//
+// Wallet, a test wallet
+//
+
+const main = async () => {
+ //
+ // Providers
+ //
+ const provider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+ const provider2 = new ethers.JsonRpcProvider('http://localhost:9545', undefined, { cacheTimeout: -1 })
+
+ //
+ // Deploy Sequence WalletContext (deterministic)
+ //
+ const deployedWalletContext = await utils.context.deploySequenceContexts(await provider.getSigner())
+ await utils.context.deploySequenceContexts(await provider2.getSigner())
+
+ // Generate a new wallet every time, otherwise tests will fail
+ // due to EIP-6492 being used only sometimes (some tests deploy the wallet)
+ const owner = ethers.Wallet.createRandom()
+
+ const relayer = new LocalRelayer(getEOAWallet(testAccounts[5].privateKey))
+ const relayer2 = new LocalRelayer(getEOAWallet(testAccounts[5].privateKey, provider2))
+
+ // Network available list
+ const networks: NetworkConfig[] = [
+ {
+ name: 'hardhat',
+ chainId: 31337,
+ rpcUrl: provider._getConnection().url,
+ provider: provider,
+ relayer: relayer,
+ isDefaultChain: true,
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ },
+ {
+ name: 'hardhat2',
+ chainId: 31338,
+ rpcUrl: provider2._getConnection().url,
+ provider: provider2,
+ relayer: relayer2,
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ }
+ ]
+
+ // Account for managing multi-network wallets
+ // TODO: make this a 3-key multisig with threshold of 2
+ // const account = new Account(
+ // {
+ // initialConfig: wallet.config,
+ // networks,
+ // context: deployedWalletContext
+ // },
+ // owner
+ // )
+ const account = await Account.new({
+ config: {
+ threshold: 2,
+ checkpoint: 0,
+ signers: [
+ {
+ address: owner.address,
+ weight: 2
+ }
+ ]
+ },
+ networks,
+ contexts: deployedWalletContext,
+ orchestrator: new Orchestrator([owner]),
+ tracker: new trackers.local.LocalConfigTracker(provider)
+ })
+
+ // the json-rpc signer via the wallet
+ const walletRequestHandler = new WalletRequestHandler(undefined, null, networks)
+
+ // fake/force an async wallet initialization for the wallet-request handler. This is the behaviour
+ // of the wallet-webapp, so lets ensure the mock wallet does the same thing too.
+ setTimeout(() => {
+ walletRequestHandler.signIn(account)
+ }, 1000)
+
+ // setup and register window message transport
+ const windowHandler = new WindowMessageHandler(walletRequestHandler)
+ windowHandler.register()
+}
+
+main()
+
+export const tests = async () => {
+ // TODO: add tests() method to verify some wallet functionality such a login
+ // and adding / removing keys, etc..
+ // + mock in a RemoteSigner as well.
+
+ await test('stub', async () => {
+ assert.true(true, 'ok')
+ })
+}
diff --git a/packages/0xsequence/tests/browser/mux-transport/mux.test.ts b/packages/0xsequence/tests/browser/mux-transport/mux.test.ts
new file mode 100644
index 0000000000..dcacdccedc
--- /dev/null
+++ b/packages/0xsequence/tests/browser/mux-transport/mux.test.ts
@@ -0,0 +1,177 @@
+import {
+ WalletRequestHandler,
+ ProxyMessageChannel,
+ ProxyMessageHandler,
+ WindowMessageHandler,
+ SequenceClient,
+ MemoryItemStore
+} from '@0xsequence/provider'
+import { ethers } from 'ethers'
+import { test, assert } from '../../utils/assert'
+import { NetworkConfig } from '@0xsequence/network'
+import { LocalRelayer } from '@0xsequence/relayer'
+import { configureLogger } from '@0xsequence/utils'
+import { testAccounts, getEOAWallet } from '../testutils'
+import * as utils from '@0xsequence/tests'
+import { Account } from '@0xsequence/account'
+import { Orchestrator } from '@0xsequence/signhub'
+import { trackers } from '@0xsequence/sessions'
+import { commons } from '@0xsequence/core'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+// Tests simulates a multi-message provider environment by having a wallet available via the
+// proxy channel and wallet window.
+export const tests = async () => {
+ //
+ // Providers
+ //
+ const provider1 = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+ const provider2 = new ethers.JsonRpcProvider('http://localhost:9545', undefined, { cacheTimeout: -1 })
+
+ //
+ // Deploy Sequence WalletContext (deterministic).
+ //
+ const deployedWalletContext = await utils.context.deploySequenceContexts(await provider1.getSigner())
+ await utils.context.deploySequenceContexts(await provider2.getSigner())
+ console.log('walletContext:', deployedWalletContext)
+
+ //
+ // Proxy Channel (normally would be out-of-band)
+ //
+ const ch = new ProxyMessageChannel()
+
+ //
+ // Wallet Handler (local mock wallet, same a mock-wallet tests)
+ //
+
+ // owner account address: 0x4e37E14f5d5AAC4DF1151C6E8DF78B7541680853
+ const owner = getEOAWallet(testAccounts[0].privateKey)
+
+ // relayers, account address: 0x3631d4d374c3710c3456d6b1de1ee8745fbff8ba
+ // const relayerAccount = getEOAWallet(testAccounts[5].privateKey)
+ const relayer1 = new LocalRelayer(getEOAWallet(testAccounts[5].privateKey))
+ const relayer2 = new LocalRelayer(getEOAWallet(testAccounts[5].privateKey, provider2))
+
+ // Network available list
+ const networks: NetworkConfig[] = [
+ // @ts-ignore
+ {
+ name: 'hardhat',
+ chainId: 31337,
+ rpcUrl: provider1._getConnection().url,
+ provider: provider1,
+ relayer: relayer1,
+ isDefaultChain: true
+ },
+ // @ts-ignore
+ {
+ name: 'hardhat2',
+ chainId: 31338,
+ rpcUrl: provider2._getConnection().url,
+ provider: provider2,
+ relayer: relayer2
+ }
+ ]
+
+ // Account for managing multi-network wallets
+ const saccount = await Account.new({
+ networks,
+ contexts: deployedWalletContext,
+ config: {
+ threshold: 1,
+ checkpoint: 0,
+ signers: [
+ {
+ address: owner.address,
+ weight: 1
+ }
+ ]
+ },
+ orchestrator: new Orchestrator([owner]),
+ tracker: new trackers.local.LocalConfigTracker(provider1)
+ })
+
+ // the rpc signer via the wallet
+ const walletRequestHandler = new WalletRequestHandler(saccount, null, networks)
+
+ // register wallet message handler, in this case using the ProxyMessage transport.
+ const proxyHandler = new ProxyMessageHandler(walletRequestHandler, ch.wallet)
+ proxyHandler.register()
+
+ // register window message transport
+ const windowHandler = new WindowMessageHandler(walletRequestHandler)
+ windowHandler.register()
+
+ //
+ // Dapp, wallet provider and dapp tests
+ //
+
+ // wallet client with multiple message provider transports enabled
+ const client = new SequenceClient(
+ {
+ windowTransport: { enabled: true },
+ proxyTransport: { enabled: true, appPort: ch.app }
+ },
+ new MemoryItemStore(),
+ {
+ defaultChainId: 31337
+ }
+ )
+
+ // provider + signer, by default if a chainId is not specified it will direct
+ // requests to the defaultChain
+ // const provider = wallet.getProvider()
+ // const signer = wallet.getSigner()
+
+ // clear it in case we're testing in browser session
+ client.disconnect()
+
+ await test('is disconnected / logged out', async () => {
+ assert.false(client.isConnected(), 'is logged out')
+ })
+
+ await test('is closed', async () => {
+ assert.false(client.isOpened(), 'is closed')
+ })
+
+ await test('connect', async () => {
+ const { connected } = await client.connect({
+ app: 'test',
+ keepWalletOpened: true
+ })
+
+ assert.true(connected, 'is connected')
+ })
+
+ await test('isOpened', async () => {
+ assert.true(client.isOpened(), 'is opened')
+ })
+
+ await test('isConnected', async () => {
+ assert.true(client.isConnected(), 'is connected')
+ })
+
+ await test('open wallet while its already opened', async () => {
+ // its already opened, but lets do it again
+ const opened = await client.openWallet()
+ assert.true(opened, 'wallet is opened')
+ })
+
+ let walletContext: commons.context.VersionedContext
+ await test('getWalletContext', async () => {
+ walletContext = await client.getWalletContext()
+ assert.equal(walletContext[2].factory, deployedWalletContext[2].factory, 'wallet context factory')
+ assert.equal(walletContext[2].guestModule, deployedWalletContext[2].guestModule, 'wallet context guestModule')
+ })
+
+ await test('getChainId', async () => {
+ const chainId = client.getChainId()
+ assert.equal(chainId, 31337, 'chainId is correct')
+ })
+
+ await test('switch chains', async () => {
+ client.setDefaultChainId(31338)
+ assert.equal(client.getChainId(), 31338, 'chainId of other chain is 31338')
+ })
+}
diff --git a/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts b/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts
new file mode 100644
index 0000000000..2a9e2dd66c
--- /dev/null
+++ b/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts
@@ -0,0 +1,174 @@
+import {
+ SequenceClient,
+ ProxyMessageProvider,
+ WalletRequestHandler,
+ ProxyMessageChannel,
+ ProxyMessageHandler,
+ prefixEIP191Message,
+ MemoryItemStore
+} from '@0xsequence/provider'
+import { ethers } from 'ethers'
+import { test, assert } from '../../utils/assert'
+import { LocalRelayer } from '@0xsequence/relayer'
+import { configureLogger, encodeMessageDigest } from '@0xsequence/utils'
+import { testAccounts, getEOAWallet } from '../testutils'
+import { Account } from '@0xsequence/account'
+import * as utils from '@0xsequence/tests'
+import { Orchestrator } from '@0xsequence/signhub'
+import { trackers } from '@0xsequence/sessions'
+import { commons } from '@0xsequence/core'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+export const tests = async () => {
+ // ProxyMessageChannel object is to be instantiated by the app coordinating
+ // the channel, ie. such as the mobile application itself.
+ //
+ // `ch.app` (port) will be injected into the app, and `ch.wallet` (port) will be injected into the wallet.
+ //
+ // Sending messages to the app port will go through channel and get received by the wallet.
+ // Sending messages to the wallet port will go through channel and get received by the app.
+ const ch = new ProxyMessageChannel()
+
+ ch.app.on('open', openInfo => {
+ console.log('app, wallet opened.', openInfo)
+ })
+ ch.app.on('close', () => {
+ console.log('app, wallet closed.')
+ })
+ ch.app.on('connect', () => {
+ console.log('app, wallet connected.')
+ })
+ ch.app.on('disconnect', () => {
+ console.log('app, wallet disconnected.')
+ })
+ // ch.wallet.on('open', () => {
+ // console.log('wallet, wallet opened.')
+ // })
+ // ch.wallet.on('close', () => {
+ // console.log('wallet, wallet closed.')
+ // })
+ // ch.wallet.on('connect', () => {
+ // console.log('wallet, wallet connected.')
+ // })
+ // ch.wallet.on('disconnect', () => {
+ // console.log('wallet, wallet disconnected.')
+ // })
+
+ //
+ // Wallet Handler
+ //
+
+ // owner account address: 0x4e37E14f5d5AAC4DF1151C6E8DF78B7541680853
+ const owner = getEOAWallet(testAccounts[0].privateKey)
+
+ // relayer account is same as owner here
+ const relayer = new LocalRelayer(owner)
+ const rpcProvider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+ const contexts = await utils.context.deploySequenceContexts(await rpcProvider.getSigner())
+
+ const networks = [
+ {
+ name: 'hardhat',
+ chainId: 31337,
+ rpcUrl: rpcProvider._getConnection().url,
+ provider: rpcProvider,
+ relayer: relayer,
+ isDefaultChain: true,
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ }
+ ]
+
+ // wallet account address: 0x91A858FbBa42E7EE200b4303b1A8B2F0BD139663 based on the chainId
+ const account = await Account.new({
+ config: {
+ threshold: 1,
+ checkpoint: 1674142220,
+ signers: [
+ {
+ address: owner.address,
+ weight: 1
+ }
+ ]
+ },
+ networks,
+ contexts,
+ orchestrator: new Orchestrator([owner]),
+ tracker: new trackers.local.LocalConfigTracker(rpcProvider)
+ })
+
+ // the rpc signer via the wallet
+ const walletRequestHandler = new WalletRequestHandler(undefined, null, networks)
+
+ // register wallet message handler, in this case using the ProxyMessage transport.
+ const proxyHandler = new ProxyMessageHandler(walletRequestHandler, ch.wallet)
+ proxyHandler.register()
+
+ //
+ // App Provider
+ //
+ const walletProvider = new ProxyMessageProvider(ch.app)
+ walletProvider.register()
+
+ // setup web3 provider
+ const client = new SequenceClient(walletProvider, new MemoryItemStore(), { defaultChainId: 31337 })
+ const connectPromise = client.connect({ app: 'proxy-transport-channel test', keepWalletOpened: true })
+
+ // fake/force an async wallet initialization for the wallet-request handler. This is the behaviour
+ // of the wallet-webapp, so lets ensure the mock wallet does the same thing too.
+ walletRequestHandler.signIn(account, { connect: true })
+
+ await connectPromise
+
+ const address = client.getAddress()
+
+ await test('verifying getAddress result', async () => {
+ assert.equal(address, ethers.getAddress('0x91A858FbBa42E7EE200b4303b1A8B2F0BD139663'), 'wallet address')
+ })
+
+ await test('sending a json-rpc request', async () => {
+ const result = await walletProvider.request({ method: 'eth_accounts', params: [] })
+ assert.equal(result[0], address, 'response address check')
+ })
+
+ await test('get chain id', async () => {
+ const chainIdClient = client.getChainId()
+ assert.equal(chainIdClient, 31337, 'chain id match')
+
+ const netVersion = await client.request({ method: 'net_version' })
+ assert.equal(netVersion, '31337', 'net_version check')
+
+ const chainId = await client.request({ method: 'eth_chainId' })
+ assert.equal(chainId, '0x7a69', 'eth_chainId check')
+ })
+
+ await test('sign a message and validate/recover', async () => {
+ const message = ethers.toUtf8Bytes('hihi')
+
+ //
+ // Sign the message
+ //
+ const sig = await client.signMessage(message)
+ assert.equal(
+ sig,
+ '0x000163c9620c0001045ea593a25d0053816f2cfb0239eb04c30cc08fd26193927bf6cf68f7f31a8239ecbcbd1365f18a6bf2bf3b13d544c91d85e35503696a28fcb96a4078a7556a1c02',
+ 'signature match'
+ )
+
+ const reader = new commons.reader.OnChainReader(rpcProvider)
+
+ //
+ // Verify the message signature
+ //
+ await account.doBootstrap(31337)
+ const messageDigest = encodeMessageDigest(prefixEIP191Message(message))
+ const isValid = await reader.isValidSignature(address, messageDigest, sig)
+ assert.true(isValid, 'signature is valid - 1')
+ })
+
+ walletProvider.closeWallet()
+}
diff --git a/packages/0xsequence/tests/browser/testutils/accounts.ts b/packages/0xsequence/tests/browser/testutils/accounts.ts
new file mode 100644
index 0000000000..0b84bfe09a
--- /dev/null
+++ b/packages/0xsequence/tests/browser/testutils/accounts.ts
@@ -0,0 +1,44 @@
+import { ethers } from 'ethers'
+
+// testAccounts with 10000 ETH each
+export const testAccounts = [
+ {
+ address: '0x4e37e14f5d5aac4df1151c6e8df78b7541680853',
+ privateKey: '0xcd0434442164a4a6ef9bb677da8dc326fddf412cad4df65e1a3f2555aee5e2b3'
+ },
+ {
+ address: '0x8a6e090a13d2dc04f87a127699952ce2d4428cd9',
+ privateKey: '0x15d476cba8e6a981e77a00fa22a06ce7f418b80dbb3cb2860f67ea811da9b108'
+ },
+ {
+ address: '0xf1fc4872058b066578008519970b7e789eea5040',
+ privateKey: '0x5b7ce9d034f2d2d8cc5667fcd5986db6e4c1e73b51bc84d61fa0b197068e381a'
+ },
+ {
+ address: '0x4875692d103162f4e29ccdd5678806043d3f16c7',
+ privateKey: '0x02173b01073b895fa3f92335658b4b1bbb3686c06193069b5c5914157f6a360a'
+ },
+ {
+ address: '0xf4b294d1fce145a73ce91b860b871e77573957e5',
+ privateKey: '0xbbbf16b45613564ad7bff353d4cb9e249f5a6d6ac2ef27a256ffafb9afaf8d58'
+ },
+ {
+ address: '0x3631d4d374c3710c3456d6b1de1ee8745fbff8ba',
+ privateKey: '0x2c527b40d4db8eff67de1b6b583b5e15037d0e02f88143668e5626039199da48'
+ }
+]
+
+export const getEOAWallet = (privateKey: string, provider?: string | ethers.Provider): ethers.Wallet => {
+ // defaults
+ if (!provider) {
+ provider = 'http://localhost:8545'
+ }
+
+ const wallet = new ethers.Wallet(privateKey)
+
+ if (typeof provider === 'string') {
+ return wallet.connect(new ethers.JsonRpcProvider(provider, undefined, { cacheTimeout: -1 }))
+ } else {
+ return wallet.connect(provider)
+ }
+}
diff --git a/packages/0xsequence/tests/browser/testutils/deploy-wallet-context.ts b/packages/0xsequence/tests/browser/testutils/deploy-wallet-context.ts
new file mode 100644
index 0000000000..fc3805e167
--- /dev/null
+++ b/packages/0xsequence/tests/browser/testutils/deploy-wallet-context.ts
@@ -0,0 +1,79 @@
+// import { ethers } from 'ethers'
+// import { UniversalDeployer } from '@0xsequence/deployer'
+// import { WalletContext } from '@0xsequence/network'
+// import { testAccounts, getEOAWallet } from './accounts'
+
+// // TODO/NOTE: it should be possible to import below from just '@0xsequence/wallet-contracts'
+// // however, experiencing a strange JS packaging/module resolution issue which leads to:
+// //
+// // mock-wallet.test.js:70822 Uncaught (in promise) TypeError: Class constructor ContractFactory cannot be invoked without 'new'
+// //
+// // by importing from '@0xsequence/wallet-contracts/gen/typechain', this issue goes away
+
+// import {
+// Factory__factory,
+// MainModule__factory,
+// MainModuleUpgradable__factory,
+// GuestModule__factory,
+// SequenceUtils__factory,
+// RequireFreshSigner__factory,
+// } from '@0xsequence/wallet-contracts'
+
+// const deployWalletContextCache: WalletContext[] = []
+
+// // deployWalletContext will deploy the Sequence WalletContext via the UniversalDeployer
+// // which will return deterministic contract addresses between calls.
+// export const deployWalletContext = async (...providers: ethers.JsonRpcProvider[]): Promise => {
+// if (!providers || providers.length === 0) {
+// providers.push(new ethers.JsonRpcProvider('http://localhost:8545'))
+// }
+
+// // Memoize the result. Even though its universal/deterministic, caching the result
+// // offers greater efficiency between calls
+// if (deployWalletContextCache.length === providers.length) {
+// return deployWalletContextCache[0]
+// }
+
+// await Promise.all(providers.map(async provider => {
+// // Deploying test accounts with the first test account
+// const wallet = getEOAWallet(testAccounts[0].privateKey, provider)
+
+// // Universal deployer for deterministic contract addresses
+// const universalDeployer = new UniversalDeployer('local', wallet.provider as ethers.JsonRpcProvider)
+// const txParams = { gasLimit: 8000000, gasPrice: 10n.pow(9).mul(10) }
+
+// const walletFactory = await universalDeployer.deploy('WalletFactory', Factory__factory as any, txParams)
+// const mainModule = await universalDeployer.deploy('MainModule', MainModule__factory as any, txParams, 0, walletFactory.address)
+
+// await universalDeployer.deploy('MainModuleUpgradable', MainModuleUpgradable__factory as any, txParams)
+// await universalDeployer.deploy('GuestModule', GuestModule__factory as any, txParams)
+
+// const sequenceUtils = await universalDeployer.deploy('SequenceUtils', SequenceUtils__factory as any, txParams, 0, walletFactory.address, mainModule.address)
+// await universalDeployer.deploy('RequireFreshSignerLib', RequireFreshSigner__factory as any, txParams, 0, sequenceUtils.address)
+
+// const deployment = universalDeployer.getDeployment()
+
+// deployWalletContextCache.push({
+// factory: deployment['WalletFactory'].address,
+// mainModule: deployment['MainModule'].address,
+// mainModuleUpgradable: deployment['MainModuleUpgradable'].address,
+// guestModule: deployment['GuestModule'].address,
+// sequenceUtils: deployment['SequenceUtils'].address,
+// libs: {
+// requireFreshSigner: deployment['RequireFreshSignerLib'].address
+// }
+// })
+// }))
+
+// return deployWalletContextCache[0]
+// }
+
+// // testWalletContext is determined by the `deployWalletContext` method above. We can use this
+// // across instances, but, we must ensure the contracts are deployed by the mock-wallet at least.
+// export const testWalletContext: WalletContext = {
+// factory: "0xf9D09D634Fb818b05149329C1dcCFAeA53639d96",
+// guestModule: "0x02390F3E6E5FD1C6786CB78FD3027C117a9955A7",
+// mainModule: "0xd01F11855bCcb95f88D7A48492F66410d4637313",
+// mainModuleUpgradable: "0x7EFE6cE415956c5f80C6530cC6cc81b4808F6118",
+// sequenceUtils: "0xd130B43062D875a4B7aF3f8fc036Bc6e9D3E1B3E"
+// }
diff --git a/packages/0xsequence/tests/browser/testutils/index.ts b/packages/0xsequence/tests/browser/testutils/index.ts
new file mode 100644
index 0000000000..63f7cc82aa
--- /dev/null
+++ b/packages/0xsequence/tests/browser/testutils/index.ts
@@ -0,0 +1,3 @@
+export * from './accounts'
+// export * from './deploy-wallet-context'
+export * from './wallet'
diff --git a/packages/0xsequence/tests/browser/testutils/wallet.ts b/packages/0xsequence/tests/browser/testutils/wallet.ts
new file mode 100644
index 0000000000..8f74d08413
--- /dev/null
+++ b/packages/0xsequence/tests/browser/testutils/wallet.ts
@@ -0,0 +1,13 @@
+import { ethers } from 'ethers'
+import { toHexString } from '@0xsequence/utils'
+
+export const sendETH = (eoaWallet: ethers.Wallet, toAddress: string, amount: bigint): Promise => {
+ const tx = {
+ gasPrice: '0x55555',
+ gasLimit: '0x55555',
+ to: toAddress,
+ value: toHexString(amount),
+ data: '0x'
+ }
+ return eoaWallet.sendTransaction(tx)
+}
diff --git a/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts b/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts
new file mode 100644
index 0000000000..d3ccb8713e
--- /dev/null
+++ b/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts
@@ -0,0 +1,527 @@
+import { commons, v2 } from '@0xsequence/core'
+import { SequenceClient, SequenceProvider, DefaultProviderConfig, MemoryItemStore } from '@0xsequence/provider'
+import { context } from '@0xsequence/tests'
+import { configureLogger, parseEther } from '@0xsequence/utils'
+import { ethers } from 'ethers'
+import { test, assert } from '../../utils/assert'
+import { testAccounts, getEOAWallet, sendETH } from '../testutils'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+export const tests = async () => {
+ //
+ // Setup
+ //
+ const transportsConfig = {
+ ...DefaultProviderConfig.transports,
+ walletAppURL: 'http://localhost:9999/mock-wallet/mock-wallet.test.html'
+ }
+
+ //
+ // Deploy Sequence WalletContext (deterministic).
+ //
+ const deployedWalletContext = await (async () => {
+ const provider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+ const signer = await provider.getSigner()
+ return context.deploySequenceContexts(signer)
+ })()
+
+ const hardhatProvider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+
+ const client = new SequenceClient(transportsConfig, new MemoryItemStore(), { defaultChainId: 31337 })
+ const wallet = new SequenceProvider(client, chainId => {
+ if (chainId === 31337) {
+ return hardhatProvider
+ }
+
+ if (chainId === 31338) {
+ return new ethers.JsonRpcProvider('http://localhost:9545', undefined, { cacheTimeout: -1 })
+ }
+
+ throw new Error(`No provider for chainId ${chainId}`)
+ })
+
+ // provider + signer, by default if a chainId is not specified it will direct
+ // requests to the defaultChain
+ const provider = wallet.getProvider()
+ const signer = wallet.getSigner()
+
+ // clear it in case we're testing in browser session
+ await wallet.disconnect()
+
+ await test('is disconnected / logged out', async () => {
+ assert.false(wallet.isConnected(), 'is connected')
+ })
+
+ await test('is closed', async () => {
+ assert.false(wallet.isOpened(), 'is closed')
+ })
+
+ await test('is disconnected', async () => {
+ assert.false(wallet.isConnected(), 'is disconnnected')
+ })
+
+ await test('connect', async () => {
+ const { connected } = await wallet.connect({
+ app: 'test',
+ keepWalletOpened: true
+ })
+ assert.true(connected, 'is connected')
+ })
+
+ await test('isOpened', async () => {
+ assert.true(wallet.isOpened(), 'is opened')
+ })
+
+ await test('isConnected', async () => {
+ assert.true(wallet.isConnected(), 'is connected')
+ })
+
+ let walletContext: commons.context.VersionedContext
+ await test('getWalletContext', async () => {
+ walletContext = await wallet.getWalletContext()
+ assert.equal(walletContext[1].factory, deployedWalletContext[1].factory, 'wallet context factory')
+ assert.equal(walletContext[1].guestModule, deployedWalletContext[1].guestModule, 'wallet context guestModule')
+ assert.equal(walletContext[2].factory, deployedWalletContext[2].factory, 'wallet context factory')
+ assert.equal(walletContext[2].guestModule, deployedWalletContext[2].guestModule, 'wallet context guestModule')
+ })
+
+ await test('getChainId', async () => {
+ const chainId = wallet.getChainId()
+ assert.equal(chainId, 31337, 'chainId is correct')
+ })
+
+ await test('networks', async () => {
+ const networks = await wallet.getNetworks()
+
+ assert.equal(networks.length, 2, '2 networks')
+ assert.true(networks[0].isDefaultChain!, '1st network is DefaultChain')
+ assert.true(!networks[1].isDefaultChain, '1st network is not DefaultChain')
+ assert.equal(networks[1].chainId, 31338, 'authChainId is correct')
+
+ const authProvider = wallet.getProvider(31338)!
+ assert.equal(authProvider.getChainId(), 31338, 'authProvider chainId is 31338')
+
+ assert.equal(provider.getChainId(), 31337, 'provider chainId is 31337')
+ })
+
+ await test('getAddress', async () => {
+ const address = wallet.getAddress()
+ assert.true(ethers.isAddress(address), 'wallet address is valid')
+ })
+
+ await test('getWalletConfig', async () => {
+ const allWalletConfigs = await wallet.getWalletConfig()
+
+ const config = allWalletConfigs as v2.config.WalletConfig
+ assert.equal(config.version, 2, 'wallet config version is correct')
+ assert.equal(BigInt(config.threshold), 2n, 'config, 2 threshold')
+ assert.equal(BigInt(config.checkpoint), 0n, 'config, 0 checkpoint')
+ assert.true(v2.config.isSignerLeaf(config.tree), 'config, isSignerLeaf')
+ assert.true(ethers.isAddress((config.tree as v2.config.SignerLeaf).address), 'config, signer address')
+ assert.equal(BigInt((config.tree as v2.config.SignerLeaf).weight), 2n, 'config, signer weight')
+ })
+
+ await test('multiple networks', async () => {
+ // chainId 31337
+ {
+ assert.equal(provider.getChainId(), 31337, 'provider chainId is 31337')
+
+ const network = await provider.getNetwork()
+ assert.equal(network.chainId, 31337n, 'chain id match')
+
+ const netVersion = await provider.send('net_version', [])
+ assert.equal(netVersion, '31337', 'net_version check')
+
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(chainId, ethers.toQuantity(31337), 'eth_chainId check')
+
+ const chainId2 = await signer.getChainId()
+ assert.equal(chainId2, 31337, 'chainId check')
+ }
+
+ // chainId 31338
+ {
+ const provider2 = wallet.getProvider(31338)
+ assert.equal(provider2.getChainId(), 31338, '2nd chain, chainId is 31338 - 2')
+
+ const network = await provider2.getNetwork()
+ assert.equal(network.chainId, 31338n, '2nd chain, chain id match - 3')
+
+ const netVersion = await provider2.send('net_version', [])
+ assert.equal(netVersion, '31338', '2nd chain, net_version check - 4')
+
+ const chainId = await provider2.send('eth_chainId', [])
+ assert.equal(chainId, ethers.toQuantity(31338), '2nd chain, eth_chainId check - 5')
+
+ const chainId2 = await provider2.getSigner().getChainId()
+ assert.equal(chainId2, 31338, '2nd chain, chainId check - 6')
+ }
+ })
+
+ await test('listAccounts', async () => {
+ const signers = provider.listAccounts()
+ assert.equal(signers.length, 1, 'signers, single owner')
+ assert.equal(signers[0], wallet.getAddress(), 'signers, check address')
+ })
+
+ await test('signMessage on defaultChain', async () => {
+ const address = wallet.getAddress()
+ const chainId = wallet.getChainId()
+
+ const message = 'hihi'
+ const message2 = ethers.toUtf8Bytes(message)
+
+ // Sign the message
+ const sigs = await Promise.all(
+ [message, message2].map(async m => {
+ assert.equal(await signer.getChainId(), 31337, 'signer chainId is 31337')
+
+ // NOTE: below line is equivalent to `signer.signMessage(m)` call
+ // const sig = await wallet.utils.signMessage(m)
+ const sig = await signer.signMessage(m, { eip6492: true })
+
+ // Non-deployed wallet (with EIP6492) should return a signature
+ // that ends with the EIP-6492 magic bytes
+ const suffix = '6492649264926492649264926492649264926492649264926492649264926492'
+ assert.true(sig.endsWith(suffix), 'signature ends with EIP-6492 magic bytes')
+
+ return sig
+ })
+ )
+
+ assert.equal(sigs[0], sigs[1], 'signatures should match even if message type is different')
+
+ const sig = sigs[0]
+
+ // Verify the signature
+ const isValid = await wallet.utils.isValidMessageSignature(address, message, sig, chainId)
+ assert.true(isValid, 'signature is valid - 2')
+ })
+
+ await test('signTypedData on defaultChain', async () => {
+ const address = wallet.getAddress()
+ const chainId = wallet.getChainId()
+
+ const domain: ethers.TypedDataDomain = {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: chainId,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
+ }
+
+ const types: { [key: string]: ethers.TypedDataField[] } = {
+ Person: [
+ { name: 'name', type: 'string' },
+ { name: 'wallet', type: 'address' }
+ ]
+ }
+
+ const message = {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
+ }
+
+ const sig = await signer.signTypedData(domain, types, message)
+
+ // Verify typed data
+ const isValid = await wallet.utils.isValidTypedDataSignature(address, { domain, types, message }, sig, chainId)
+ assert.true(isValid, 'signature is valid - 3')
+ })
+
+ await test('signAuthMessage', async () => {
+ const address = wallet.getAddress()
+ const chainId = 31337
+ const authProvider = wallet.getProvider(chainId)!
+
+ assert.equal(chainId, 31337, 'chainId is 31337 (authChain)')
+ assert.equal(authProvider.getChainId(), 31337, 'authProvider chainId is 31337')
+ assert.equal(authProvider.getChainId(), await authProvider.getSigner().getChainId(), 'authProvider signer chainId is 31337')
+
+ // Sign the message
+ const message = 'hihi'
+ const sig = await signer.signMessage(message, { chainId })
+
+ // confirm that authSigner, the chain-bound provider, derived from the authProvider returns the same signature
+ const authSigner = authProvider.getSigner()
+ const sigChk = await authSigner.signMessage(message, { chainId })
+ assert.equal(sigChk, sig, 'authSigner.signMessage returns the same sig')
+
+ // Verify the signature
+ const isValid = await wallet.utils.isValidMessageSignature(address, message, sig, chainId)
+ assert.true(isValid, 'signAuthMessage, signature is valid')
+ })
+
+ await test('getBalance', async () => {
+ // technically, the mock-wallet's single signer owner has some ETH..
+ const balanceSigner1 = await provider.getBalance('0x4e37E14f5d5AAC4DF1151C6E8DF78B7541680853')
+ assert.true(balanceSigner1 > 0n, 'signer1 balance > 0')
+ })
+
+ await test('fund sequence wallet', async () => {
+ // fund Sequence wallet with some ETH from test seed account
+ const testAccount = getEOAWallet(testAccounts[0].privateKey)
+ const walletBalanceBefore = await signer.getBalance()
+
+ const ethAmount = parseEther('10.1234')
+ const txResp = await sendETH(testAccount, wallet.getAddress(), ethAmount)
+ const txReceipt = await provider.getTransactionReceipt(txResp.hash)
+ assert.equal(txReceipt?.status, 1, 'eth sent from signer1')
+
+ const walletBalanceAfter = await signer.getBalance()
+ assert.equal(walletBalanceAfter - walletBalanceBefore, ethAmount, `wallet received ${ethAmount} eth`)
+ })
+
+ const testSendETH = async (
+ title: string,
+ opts: {
+ gasLimit?: string
+ } = {}
+ ) =>
+ test(title, async () => {
+ // sequence wallet to now send some eth back to another seed account
+ // via the relayer
+ {
+ const walletAddress = wallet.getAddress()
+ const walletBalanceBefore = await signer.getBalance()
+
+ // send eth from sequence smart wallet to another test account
+ const toAddress = testAccounts[1].address
+ const toBalanceBefore = await provider.getBalance(toAddress)
+
+ const ethAmount = parseEther('1.4242')
+
+ // NOTE: when a wallet is undeployed (counterfactual), and although the txn contents are to send from our
+ // sequence wallet to the test account, the transaction by the Sequence Wallet instance will be sent `to` the
+ // `GuestModule` smart contract address of the Sequence context `from` the Sequence Relayer (local) account.
+ //
+ // However, when a wallet is deployed on-chain, and the txn object is to send from our sequence wallet to the
+ // test account, the transaction will be sent `to` the smart wallet contract address of the sender by
+ // the relayer. The transaction will then be delegated through the Smart Wallet and transfer will occur
+ // as an internal transaction on-chain.
+ //
+ // Also note, the gasLimit and gasPrice can be estimated by the relayer, or optionally may be specified.
+
+ //--
+
+ // Record wallet deployed state before, so we can check the receipt.to below. We have to do this
+ // because a wallet will automatically get bundled for deployment when it sends a transaction.
+ const beforeWalletDeployed = (await hardhatProvider.getCode(wallet.getAddress())) !== '0x'
+
+ // NOTE/TODO: gasPrice even if set will be set again by the LocalRelayer, we should allow it to be overridden
+ const tx: ethers.TransactionRequest = {
+ from: walletAddress,
+ to: toAddress,
+ value: ethAmount
+ }
+
+ // specifying gasLimit manually
+ if (opts.gasLimit) {
+ tx.gasLimit = opts.gasLimit
+ }
+
+ const txResp = await signer.sendTransaction(tx)
+ const txReceipt = await txResp.wait()
+
+ assert.equal(txReceipt?.status, 1, 'txn sent successfully')
+ assert.true(
+ (await hardhatProvider.getCode(wallet.getAddress())) !== '0x',
+ 'wallet must be in deployed state after the txn'
+ )
+
+ // transaction is sent to the deployed wallet, if the wallet is deployed.. otherwise its sent to guestModule
+ if (beforeWalletDeployed) {
+ assert.equal(txReceipt?.to, wallet.getAddress(), 'recipient is correct')
+ } else {
+ assert.equal(txReceipt?.to, walletContext[2].guestModule, 'recipient is correct')
+ }
+
+ // Ensure fromAddress sent their eth
+ const walletBalanceAfter = await signer.getBalance()
+ const sent = (walletBalanceAfter - walletBalanceBefore) * -1n
+
+ assert.equal(sent, ethAmount, `wallet sent ${sent} eth while expected ${ethAmount}`)
+
+ // Ensure toAddress received their eth
+ const toBalanceAfter = await provider.getBalance(toAddress)
+ const received = toBalanceAfter - toBalanceBefore
+ assert.equal(received, ethAmount, `toAddress received ${received} eth while expected ${ethAmount}`)
+
+ // Extra checks
+ if (opts.gasLimit) {
+ // In our test, we are passing a high gas limit for an internal transaction, so overall
+ // transaction must be higher than this value if it used our value correctly
+ assert.true(txResp.gasLimit >= BigInt(opts.gasLimit), 'sendETH, using higher gasLimit')
+ }
+ }
+ })
+
+ await testSendETH('sendETH (defaultChain)')
+
+ // NOTE: this will pass, as we set the gasLimit low on the txn, but the LocalRelayer will re-estimate
+ // the entire transaction to have it pass.
+ await testSendETH('sendETH with high gasLimit override (defaultChain)', { gasLimit: '0x55555' })
+
+ await test('sendTransaction batch', async () => {
+ const testAccount = getEOAWallet(testAccounts[1].privateKey)
+
+ const ethAmount1 = parseEther('1.234')
+ const ethAmount2 = parseEther('0.456')
+
+ const tx1: ethers.TransactionRequest = {
+ to: testAccount.address,
+ value: ethAmount1
+ }
+ const tx2: ethers.TransactionRequest = {
+ to: testAccount.address,
+ value: ethAmount2
+ }
+
+ const toBalanceBefore = await provider.getBalance(testAccount.address)
+ const txnResp = await signer.sendTransaction([tx1, tx2])
+
+ await txnResp.wait()
+
+ const toBalanceAfter = await provider.getBalance(testAccount.address)
+ const sent = toBalanceAfter - toBalanceBefore
+ const expected = ethAmount1 + ethAmount2
+ assert.equal(sent, expected, `wallet sent ${sent} eth while expected ${expected} (${ethAmount1} + ${ethAmount2})`)
+ })
+
+ await test('sendTransaction batch format 2', async () => {
+ const testAccount = getEOAWallet(testAccounts[1].privateKey)
+
+ const ethAmount1 = parseEther('1.234')
+ const ethAmount2 = parseEther('0.456')
+
+ const tx1: ethers.TransactionRequest = {
+ to: testAccount.address,
+ value: ethAmount1
+ }
+
+ const tx2: ethers.TransactionRequest = {
+ to: testAccount.address,
+ value: ethAmount2
+ }
+
+ const toBalanceBefore = await provider.getBalance(testAccount.address)
+ const txnResp = await signer.sendTransaction([tx1, tx2])
+
+ await txnResp.wait()
+
+ const toBalanceAfter = await provider.getBalance(testAccount.address)
+ const sent = toBalanceAfter - toBalanceBefore
+ const expected = ethAmount1 + ethAmount2
+ assert.equal(sent, expected, `wallet sent ${sent} eth while expected ${expected} (${ethAmount1} + ${ethAmount2})`)
+ })
+
+ await test('sendTransaction batch format 3', async () => {
+ const testAccount = getEOAWallet(testAccounts[1].privateKey)
+
+ const ethAmount1 = parseEther('1.234')
+ const ethAmount2 = parseEther('0.456')
+
+ const tx1: commons.transaction.Transaction = {
+ to: testAccount.address,
+ value: ethAmount1
+ }
+
+ const tx2: commons.transaction.Transaction = {
+ to: testAccount.address,
+ value: ethAmount2
+ }
+
+ const toBalanceBefore = await provider.getBalance(testAccount.address)
+
+ const txnResp = await signer.sendTransaction([tx1, tx2])
+ await txnResp.wait()
+
+ const toBalanceAfter = await provider.getBalance(testAccount.address)
+ const sent = toBalanceAfter - toBalanceBefore
+ const expected = ethAmount1 + ethAmount2
+ assert.equal(sent, expected, `wallet sent ${sent} eth while expected ${expected} (${ethAmount1} + ${ethAmount2})`)
+ })
+
+ await test('sendETH from the sequence smart wallet (authChain)', async () => {
+ // multi-chain to send eth on an alternative chain, in this case the authChain
+ //
+ // NOTE: the account addresses are both chains have been seeded with the same private key
+ // so we can have overlapping addresses and keys for ease of use duringtesting
+
+ // get provider of the 2nd chain
+ const provider2 = wallet.getProvider('hardhat2')!
+
+ assert.equal(provider2.getChainId(), 31338, 'provider is the 2nd chain - 1')
+ assert.equal(provider2.getChainId(), wallet.getProvider(31338)!.getChainId(), 'provider2 code path check')
+
+ const signer2 = provider2.getSigner()
+
+ // confirm all account addresses are the same and correct
+ {
+ assert.equal(wallet.getAddress(), await signer.getAddress(), 'wallet and signer address match')
+ assert.equal(wallet.getAddress(), await signer2.getAddress(), 'wallet and signer2 address match')
+ assert.true(wallet.getAddress() !== testAccounts[0].address, 'wallet is not subkey address')
+ }
+
+ // initial balances
+ {
+ const testAccount = getEOAWallet(testAccounts[0].privateKey, provider2)
+ const walletBalanceBefore = await provider2.getBalance(await testAccount.getAddress())
+
+ const mainTestAccount = getEOAWallet(testAccounts[0].privateKey, wallet.getProvider())
+ const mainWalletBalanceBefore = await provider.getBalance(await mainTestAccount.getAddress())
+
+ assert.true(walletBalanceBefore !== mainWalletBalanceBefore, 'balances across networks do not match')
+ }
+
+ // first, lets move some ETH info the wallet from teh testnet seed account
+ {
+ const testAccount = getEOAWallet(testAccounts[0].privateKey, provider2)
+ const walletBalanceBefore = await signer2.getBalance()
+
+ const ethAmount = parseEther('4.2')
+
+ // const txResp = await sendETH(testAccount, await wallet.getAddress(), ethAmount)
+ // const txReceipt = await provider2.getTransactionReceipt(txResp.hash)
+
+ const txReceipt = await (await sendETH(testAccount, wallet.getAddress(), ethAmount)).wait()
+ assert.equal(txReceipt?.status, 1, 'eth sent')
+
+ const walletBalanceAfter = await signer2.getBalance()
+ assert.equal(walletBalanceAfter - walletBalanceBefore, ethAmount, `wallet received ${ethAmount} eth`)
+ }
+
+ // using sequence wallet on the authChain, send eth back to anotehr seed account via
+ // the authChain relayer
+ {
+ const walletAddress = wallet.getAddress()
+ const walletBalanceBefore = await signer2.getBalance()
+
+ // send eth from sequence smart wallet to another test account
+ const toAddress = testAccounts[1].address
+ const toBalanceBefore = await provider2.getBalance(toAddress)
+
+ const ethAmount = parseEther('1.1234')
+
+ const tx = {
+ from: walletAddress,
+ to: toAddress,
+ value: ethAmount
+ }
+ const txReceipt = await (await signer2.sendTransaction(tx)).wait()
+
+ assert.equal(txReceipt?.status, 1, 'txn sent successfully')
+ assert.true((await hardhatProvider.getCode(walletAddress)) !== '0x', 'wallet must be in deployed state after the txn')
+
+ // Ensure fromAddress sent their eth
+ const walletBalanceAfter = await signer2.getBalance()
+ const sent = (walletBalanceAfter - walletBalanceBefore) * -1n
+
+ assert.equal(sent, ethAmount, `wallet sent ${ethAmount} eth`)
+
+ // Ensure toAddress received their eth
+ const toBalanceAfter = await provider2.getBalance(toAddress)
+ assert.equal(toBalanceAfter - toBalanceBefore, ethAmount, `toAddress received ${ethAmount} eth`)
+ }
+ })
+}
diff --git a/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts b/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts
new file mode 100644
index 0000000000..e4839b67f1
--- /dev/null
+++ b/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts
@@ -0,0 +1,116 @@
+import { DefaultProviderConfig, MemoryItemStore, SequenceClient, SequenceProvider } from '@0xsequence/provider'
+import { configureLogger } from '@0xsequence/utils'
+import { ethers } from 'ethers'
+import { test, assert } from '../../utils/assert'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+export const tests = async () => {
+ //
+ // Setup
+ //
+ const transportsConfig = {
+ ...DefaultProviderConfig.transports,
+ walletAppURL: 'http://localhost:9999/mock-wallet/mock-wallet.test.html'
+ }
+
+ const hardhatProvider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+
+ const client = new SequenceClient(transportsConfig, new MemoryItemStore(), { defaultChainId: 31338 })
+ const provider = new SequenceProvider(client, chainId => {
+ if (chainId === 31337) {
+ return hardhatProvider
+ }
+
+ if (chainId === 31338) {
+ return new ethers.JsonRpcProvider('http://localhost:9545', undefined, { cacheTimeout: -1 })
+ }
+
+ throw new Error(`No provider for chainId ${chainId}`)
+ })
+
+ // clear it in case we're testing in browser session
+ provider.disconnect()
+
+ await test('is logged out', async () => {
+ assert.false(provider.isConnected(), 'is logged out')
+ })
+
+ await test('is disconnected', async () => {
+ assert.false(provider.isConnected(), 'is disconnnected')
+ })
+
+ await test('connect / login', async () => {
+ const { connected } = await provider.connect({
+ app: 'test',
+ keepWalletOpened: true
+ })
+
+ assert.true(connected, 'is connected')
+ })
+
+ await test('isConnected', async () => {
+ assert.true(provider.isConnected(), 'is connected')
+ })
+
+ await test('check defaultNetwork is 31338', async () => {
+ assert.equal(provider.getChainId(), 31338, 'provider chainId is 31338')
+
+ const network = await provider.getNetwork()
+ assert.equal(network.chainId, 31338n, 'chain id match')
+ })
+
+ await test('getNetworks()', async () => {
+ const networks = await provider.getNetworks()
+ console.log('=> networks', networks)
+
+ // There should be two chains, hardhat and hardhat2
+ assert.equal(networks.length, 2, 'networks length is 2')
+ assert.equal(networks[0].chainId, 31337, 'chain id match')
+ assert.equal(networks[1].chainId, 31338, 'chain id match')
+ })
+
+ await test('signMessage with our custom defaultChain', async () => {
+ console.log('signing message...')
+ const signer = provider.getSigner()
+
+ const message = 'Hi there! Please sign this message, 123456789, thanks.'
+
+ // sign
+ const sig = await signer.signMessage(message)
+
+ // validate
+ const isValid = await provider.utils.isValidMessageSignature(provider.getAddress(), message, sig, await signer.getChainId())
+ assert.true(isValid, 'signMessage sig is valid')
+ })
+
+ await test('signTypedData on defaultChain (in this case, hardhat2)', async () => {
+ const address = provider.getAddress()
+ const chainId = provider.getChainId()
+
+ const domain: ethers.TypedDataDomain = {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: chainId,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
+ }
+
+ const types: { [key: string]: ethers.TypedDataField[] } = {
+ Person: [
+ { name: 'name', type: 'string' },
+ { name: 'wallet', type: 'address' }
+ ]
+ }
+
+ const message = {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
+ }
+
+ const sig = await provider.getSigner().signTypedData(domain, types, message)
+
+ // Verify typed data
+ const isValid = await provider.utils.isValidTypedDataSignature(address, { domain, types, message }, sig, chainId)
+ assert.true(isValid, 'signature is valid - 4')
+ })
+}
diff --git a/packages/0xsequence/tests/browser/window-transport/dapp.test.ts b/packages/0xsequence/tests/browser/window-transport/dapp.test.ts
new file mode 100644
index 0000000000..12c6bd9f3d
--- /dev/null
+++ b/packages/0xsequence/tests/browser/window-transport/dapp.test.ts
@@ -0,0 +1,130 @@
+import { isValidSignature, prefixEIP191Message, WindowMessageProvider } from '@0xsequence/provider'
+import { context } from '@0xsequence/tests'
+import { configureLogger, encodeMessageDigest, packMessageData } from '@0xsequence/utils'
+import { ethers } from 'ethers'
+import { test, assert } from '../../utils/assert'
+
+configureLogger({ logLevel: 'DEBUG', silence: false })
+
+const walletProvider = new WindowMessageProvider('http://localhost:9999/mock-wallet/mock-wallet.test.html')
+walletProvider.register()
+
+// ;(window as any).walletProvider = walletProvider
+
+export const tests = async () => {
+ await (async () => {
+ const provider = new ethers.JsonRpcProvider('http://localhost:8545', undefined, { cacheTimeout: -1 })
+ const signer = await provider.getSigner()
+ return context.deploySequenceContexts(signer)
+ })()
+
+ walletProvider.openWallet()
+
+ await test('provider opened the wallet', async () => {
+ const opened = await walletProvider.waitUntilOpened()
+ assert.true(!!opened, 'opened is true')
+ })
+
+ // TODO: try this again, but turn off hardhat, to ensure our error reponses are working correctly..
+ // ..
+ const provider = new ethers.BrowserProvider(walletProvider, undefined, { cacheTimeout: -1 })
+ const signer = await provider.getSigner()
+
+ const address = await signer.getAddress()
+ const { chainId } = await provider.getNetwork()
+
+ await test('getAddress', async () => {
+ assert.true(ethers.isAddress(address), 'wallet address')
+ })
+
+ await test('sending a json-rpc request', async () => {
+ const result = await walletProvider.request({ method: 'eth_accounts', params: [] })
+ assert.equal(result[0], address, 'response address check')
+
+ const resp = await provider.send('eth_accounts', [])
+ assert.true(!!resp, 'response successful')
+ assert.equal(resp[0], address, 'response address check')
+ })
+
+ await test('get chain id', async () => {
+ const network = await provider.getNetwork()
+ assert.equal(network.chainId, 31337n, 'chain id match')
+
+ const netVersion = await provider.send('net_version', [])
+ assert.equal(netVersion, '31337', 'net_version check')
+
+ const chainId = await provider.send('eth_chainId', [])
+ assert.equal(chainId, '0x7a69', 'eth_chainId check')
+ })
+
+ // NOTE: when a dapp wants to verify SmartWallet signed messages, they will need to verify against EIP-1271
+ await test('sign a message and validate/recover', async () => {
+ const message = ethers.toUtf8Bytes('hihi')
+
+ // TODO: signer should be a Sequence signer, and should be able to specify the chainId
+ // however, for a single wallet, it can check the chainId and throw if doesnt match, for multi-wallet it will select
+
+ // Deploy the wallet (by sending a random tx)
+ // (this step is performed by wallet-webapp when signing without EIP-6492 support)
+ await signer.sendTransaction({ to: ethers.Wallet.createRandom().address })
+
+ //
+ // Sign the message
+ //
+ const sig = await signer.signMessage(message)
+
+ //
+ // Verify the message signature
+ //
+ const messageDigest = encodeMessageDigest(prefixEIP191Message(message))
+ const isValid = await isValidSignature(address, messageDigest, sig, provider)
+ assert.true(isValid, 'signature is valid - 5')
+
+ // also compute the subDigest of the message, to be provided to the end-user
+ // in order to recover the config properly, the subDigest + sig is required.
+ const subDigest = packMessageData(address, chainId, messageDigest)
+ })
+
+ await test('sign EIP712 typed data and validate/recover', async () => {
+ const typedData = {
+ types: {
+ Person: [
+ { name: 'name', type: 'string' },
+ { name: 'wallet', type: 'address' }
+ ]
+ },
+ primaryType: 'Person' as const,
+ domain: {
+ name: 'Ether Mail',
+ version: '1',
+ chainId: 31337,
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
+ },
+ message: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
+ }
+ }
+
+ //
+ // Sign the message
+ //
+ const sig = await provider.send('eth_signTypedData', [address, typedData])
+
+ // NOTE: verification of message below is identical to verifying a message with eth_sign,
+ // the difference is we have to provide 'message' as the typedData digest format
+
+ //
+ // Verify the message signature
+ //
+
+ const messageHash = ethers.TypedDataEncoder.hash(typedData.domain, typedData.types, typedData.message)
+ const messageDigest = ethers.getBytes(messageHash)
+ const isValid = await isValidSignature(address, messageDigest, sig, provider)
+ assert.true(isValid, 'signature is valid - 6')
+
+ // also compute the subDigest of the message, to be provided to the end-user
+ // in order to recover the config properly, the subDigest + sig is required.
+ const subDigest = packMessageData(address, chainId, messageDigest)
+ })
+}
diff --git a/packages/0xsequence/tests/json-rpc-provider.spec.ts b/packages/0xsequence/tests/json-rpc-provider.spec.ts
new file mode 100644
index 0000000000..3171949494
--- /dev/null
+++ b/packages/0xsequence/tests/json-rpc-provider.spec.ts
@@ -0,0 +1,3 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('json-rpc-provider', 'json-rpc-provider/rpc.test.html')
diff --git a/packages/0xsequence/tests/mock-wallet.spec.ts b/packages/0xsequence/tests/mock-wallet.spec.ts
new file mode 100644
index 0000000000..62f770985e
--- /dev/null
+++ b/packages/0xsequence/tests/mock-wallet.spec.ts
@@ -0,0 +1,3 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('mock-wallet', 'mock-wallet/mock-wallet.test.html')
diff --git a/packages/0xsequence/tests/mux-transport.spec.ts b/packages/0xsequence/tests/mux-transport.spec.ts
new file mode 100644
index 0000000000..814f019ec3
--- /dev/null
+++ b/packages/0xsequence/tests/mux-transport.spec.ts
@@ -0,0 +1,3 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('mux-transport', 'mux-transport/mux.test.html')
diff --git a/packages/0xsequence/tests/proxy-transport.spec.ts b/packages/0xsequence/tests/proxy-transport.spec.ts
new file mode 100644
index 0000000000..338fb0fc30
--- /dev/null
+++ b/packages/0xsequence/tests/proxy-transport.spec.ts
@@ -0,0 +1,3 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('proxy-transport-channel', 'proxy-transport/channel.test.html')
diff --git a/packages/0xsequence/tests/utils/assert.ts b/packages/0xsequence/tests/utils/assert.ts
new file mode 100644
index 0000000000..6af2b776c2
--- /dev/null
+++ b/packages/0xsequence/tests/utils/assert.ts
@@ -0,0 +1,97 @@
+interface Entry {
+ title: string
+ pass: boolean | null
+ startTime: number
+ error: string | null
+ stack: string | null
+}
+
+declare global {
+ interface Window {
+ __testResults: Entry[]
+ }
+}
+
+const testResults: Entry[] = []
+
+window.__testResults = testResults
+
+export const test = async (title: string, run: () => void) => {
+ console.log(`\n
+╔══════════════════════════════════════════════════════════════════════════════╗
+║ ║
+║ ${title}${' '.repeat(77 - title.length)}║
+║ ║
+╚══════════════════════════════════════════════════════════════════════════════╝\n`)
+
+ const entry: Entry = {
+ title: title,
+ pass: null,
+ startTime: performance.now(),
+ error: null,
+ stack: null
+ }
+ testResults.push(entry)
+
+ try {
+ await run()
+ entry.pass = true
+ } catch (err) {
+ entry.error = err.message
+ entry.stack = err.stack
+ // throw new Error(`case '${title}' failed due to ${err.message}`)
+ // throw err
+ err.message = `case '${title}' failed due to ${err.message}`
+ throw err
+ }
+}
+
+export const assert = {
+ true: function (cond: boolean, msg?: string) {
+ if (cond !== true) {
+ if (msg) {
+ throw new Error(`invalid condition, '${msg}'`)
+ } else {
+ throw new Error(`invalid condition`)
+ }
+ }
+ },
+
+ false: function (cond: boolean, msg?: string) {
+ return assert.true(!cond, msg)
+ },
+
+ equal: function (actual: any, expected: any, msg?: string) {
+ if (actual !== expected) {
+ if (msg) {
+ throw new Error(`expected '${expected}' but got '${actual}', '${msg}'`)
+ } else {
+ throw new Error(`expected '${expected}' but got '${actual}'`)
+ }
+ }
+ },
+
+ rejected: async function (promise: Promise, msg?: string) {
+ let wasRejected = false
+
+ try {
+ await promise
+ } catch {
+ wasRejected = true
+ }
+
+ if (!wasRejected) {
+ if (msg) {
+ throw new Error(`expected to be rejected`)
+ } else {
+ throw new Error(`expected to be rejected, ${msg}`)
+ }
+ }
+ }
+}
+
+export const sleep = (time: number) => {
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, time)
+ })
+}
diff --git a/packages/0xsequence/tests/utils/browser-test-runner.ts b/packages/0xsequence/tests/utils/browser-test-runner.ts
new file mode 100644
index 0000000000..4a709a5702
--- /dev/null
+++ b/packages/0xsequence/tests/utils/browser-test-runner.ts
@@ -0,0 +1,89 @@
+import test from 'ava'
+import * as puppeteer from 'puppeteer'
+
+export const runBrowserTests = async (title: string, path: string) => {
+ test.serial(title, browserContext, async (t, page: puppeteer.Page) => {
+ await page.goto('http://localhost:9999/' + path, {
+ waitUntil: 'networkidle0',
+ timeout: 30000
+ })
+
+ // confirm
+ t.true((await page.title()) === 'test')
+
+ // debugging
+ page.on('console', msg => console.log(`console: ${msg.text()}`))
+
+ // catch uncaught errors
+ page.on('pageerror', err => {
+ page.close()
+ t.fail(`${err}`)
+ })
+
+ // run the test
+ try {
+ const timeout = setTimeout(() => {
+ throw `Test runner timed out after 60s!`
+ }, 60000) // 60 seconds to run the tests
+
+ const testResults = await page.evaluate(async () => {
+ // @ts-ignore
+ await lib.tests()
+
+ // @ts-ignore
+ return window.__testResults
+ })
+
+ clearTimeout(timeout)
+
+ for (let i = 0; i < testResults.length; i++) {
+ const result = testResults[i]
+ if (result.pass === true) {
+ t.log(`${result.title}: \x1b[32mPASS\x1b[0m`)
+ } else {
+ t.log(`${result.title}: \x1b[31mFAIL\x1b[0m`)
+ if (result.error) {
+ t.fail(`WHOOPS! case '${result.title}' failed due to ${result.error} !`)
+ } else {
+ t.fail(`WHOOPS! case '${result.title}' failed !`)
+ }
+ }
+ }
+ } catch (err) {
+ t.fail(`${err}`)
+ }
+ })
+}
+
+export const browserContext = async (t, run) => {
+ const browser = await puppeteer.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ })
+ const page = await browser.newPage()
+ try {
+ await run(t, page)
+ } finally {
+ await page.close()
+ await browser.close()
+ }
+}
+
+// const getChromePath = (): string | undefined => {
+// if (process.env['NIX_PATH']) {
+// // nixos users are unable to use the chrome bin packaged with puppeteer,
+// // so instead we use the locally installed chrome or chromium binary.
+// for (const bin of ['google-chrome-stable', 'chromium']) {
+// const out = spawnSync('which', [bin])
+// if (out.status === 0) {
+// const executablePath = out.stdout.toString().trim()
+// return executablePath
+// }
+// }
+// console.error('Unable to find `google-chrome-stable` or `chromium` binary on your NixOS system.')
+// process.exit(1)
+// } else {
+// // undefined will use the chrome version packaged with puppeteer npm package
+// return undefined
+// }
+// }
diff --git a/packages/0xsequence/tests/utils/webpack-test-server.ts b/packages/0xsequence/tests/utils/webpack-test-server.ts
new file mode 100644
index 0000000000..8b4a050d4c
--- /dev/null
+++ b/packages/0xsequence/tests/utils/webpack-test-server.ts
@@ -0,0 +1,31 @@
+import webpack from 'webpack'
+import WebpackDevServer from 'webpack-dev-server'
+import webpackTestConfig from '../webpack.config'
+
+export const DEFAULT_PORT = 9999
+
+// NOTE: currently not in use, instead we run the server as a separate process via `pnpm test:server`
+
+export const createWebpackTestServer = async (port = DEFAULT_PORT) => {
+ const testServer = new WebpackDevServer(
+ // @ts-ignore
+ webpack(webpackTestConfig),
+ {
+ clientLogLevel: 'silent',
+ open: false,
+ host: '0.0.0.0',
+ historyApiFallback: true,
+ stats: 'errors-only',
+ disableHostCheck: true,
+ publicPath: '/',
+ inline: false,
+ hot: false
+ }
+ )
+
+ await testServer.listen(port, '0.0.0.0', function (err) {
+ if (err) {
+ console.error(err)
+ }
+ })
+}
diff --git a/packages/0xsequence/tests/wallet-provider.spec.ts b/packages/0xsequence/tests/wallet-provider.spec.ts
new file mode 100644
index 0000000000..418cefd9fb
--- /dev/null
+++ b/packages/0xsequence/tests/wallet-provider.spec.ts
@@ -0,0 +1,4 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('wallet-provider/dapp', 'wallet-provider/dapp.test.html')
+runBrowserTests('wallet-provider/dapp2', 'wallet-provider/dapp2.test.html')
diff --git a/packages/0xsequence/tests/webpack.config.js b/packages/0xsequence/tests/webpack.config.js
new file mode 100644
index 0000000000..9b02970794
--- /dev/null
+++ b/packages/0xsequence/tests/webpack.config.js
@@ -0,0 +1,165 @@
+const path = require('path')
+const fs = require('fs')
+const webpack = require('webpack')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+
+const port = process.env['PORT'] || 9999
+
+const appDirectory = fs.realpathSync(process.cwd())
+const resolveCwd = (relativePath) => path.resolve(appDirectory, relativePath)
+
+const resolvePackages = () => {
+ const pkgs = path.resolve(fs.realpathSync(process.cwd()), '..')
+ return fs.readdirSync(pkgs).reduce((list, dir) => {
+ const p = path.join(pkgs, dir, 'src')
+ if (fs.existsSync(p)) {
+ list.push(p)
+ }
+ return list
+ }, [])
+}
+
+// Include extra sources for compilation.
+//
+// NOTE: if you experience an error in your webpack builder such as,
+// Module parse failed: Unexpected token (11:20)
+// You may need an appropriate loader to handle this file type, currently no loaders are
+// configured to process this file. See https://webpack.js.org/concepts#loaders
+//
+// The above error is due to not passing the TypeScript files to the module.rules for
+// babel below. The solution is to include the path to the source files below, and
+// the error will go away.
+const resolveExtras = [
+ // resolveCwd('../wallet/tests/utils'),
+ resolveCwd('../../node_modules/@0xsequence/wallet-contracts/gen')
+]
+
+const resolveTestEntries = (location) => {
+ return fs.readdirSync(location).reduce((list, f) => {
+ const n = path.join(location, f)
+ if (fs.lstatSync(n).isDirectory()) {
+ list.push(...resolveTestEntries(n))
+ } else {
+ if (n.endsWith(".test.ts") > 0) list.push(n)
+ }
+ return list
+ }, [])
+}
+
+const resolveEntry = () => {
+ const browserTestRoot = fs.realpathSync(path.join(process.cwd(), 'tests', 'browser'))
+ const entry = { 'lib': './src/index.ts' }
+ const testEntries = resolveTestEntries(browserTestRoot)
+ testEntries.forEach(v => entry[v.slice(browserTestRoot.length+1, v.length-3)] = v)
+ return entry
+}
+
+const resolveHtmlPlugins = (entry) => {
+ const plugins = []
+ for (let k in entry) {
+ if (k === 'lib') continue
+ plugins.push(new HtmlWebpackPlugin({
+ inject: false,
+ filename: `${k}.html`,
+ templateContent: htmlTemplate(k)
+ }))
+ }
+ return plugins
+}
+
+const htmlTemplate = (k) => `
+
+
+
+ test
+
+
+ ${k}
+
+
+ TEST
+
+
+
+
+
+
+
+`
+
+const entry = resolveEntry()
+
+module.exports = {
+ mode: 'none',
+ context: process.cwd(),
+ entry: entry,
+ output: {
+ library: 'lib',
+ libraryTarget: 'umd'
+ },
+ watch: false,
+ plugins: [...resolveHtmlPlugins(entry)],
+ module: {
+ rules: [
+ {
+ test: /\.(js|mjs|ts)$/,
+ include: [...resolvePackages(), resolveCwd('./tests'), ...resolveExtras],
+ loader: require.resolve('babel-loader'),
+ options: {
+ presets: ['@babel/preset-typescript'],
+ plugins: [
+ [require.resolve('@babel/plugin-transform-class-properties'), { loose: true }]
+ ],
+ cacheCompression: false,
+ compact: false,
+ },
+ },
+ {
+ test: /\.(jpe?g|png|gif|svg)$/i,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 8192000
+ }
+ }
+ ]
+ }
+ ]
+ },
+ resolve: {
+ modules: ['node_modules', resolveCwd('node_modules')],
+ extensions: ['.ts', '.js', '.png', '.jpg', '.d.ts'],
+ alias: {},
+ fallback: {
+ fs: false,
+ stream: false,
+ readline: false,
+ assert: false
+ }
+ },
+ devServer: {
+ clientLogLevel: 'silent',
+ open: false,
+ host: '0.0.0.0',
+ port: port,
+ historyApiFallback: true,
+ stats: 'errors-only',
+ disableHostCheck: true,
+ contentBase: path.resolve(process.cwd(), 'tests/browser'),
+ publicPath: '/',
+ inline: false,
+ hot: false
+ }
+}
diff --git a/packages/0xsequence/tests/window-transport.spec.ts b/packages/0xsequence/tests/window-transport.spec.ts
new file mode 100644
index 0000000000..d56374379b
--- /dev/null
+++ b/packages/0xsequence/tests/window-transport.spec.ts
@@ -0,0 +1,3 @@
+import { runBrowserTests } from './utils/browser-test-runner'
+
+runBrowserTests('window-transport', 'window-transport/dapp.test.html')
diff --git a/packages/abi/src/index.ts b/packages/abi/src/index.ts
new file mode 100644
index 0000000000..6537ee23d2
--- /dev/null
+++ b/packages/abi/src/index.ts
@@ -0,0 +1 @@
+export { walletContracts } from './wallet'
diff --git a/packages/abi/src/tokens/erc1155.ts b/packages/abi/src/tokens/erc1155.ts
new file mode 100644
index 0000000000..1e20ce4050
--- /dev/null
+++ b/packages/abi/src/tokens/erc1155.ts
@@ -0,0 +1,3 @@
+export const abi = []
+
+export const returns = {}
diff --git a/packages/abi/src/tokens/erc20.ts b/packages/abi/src/tokens/erc20.ts
new file mode 100644
index 0000000000..1e20ce4050
--- /dev/null
+++ b/packages/abi/src/tokens/erc20.ts
@@ -0,0 +1,3 @@
+export const abi = []
+
+export const returns = {}
diff --git a/packages/abi/src/tokens/erc721.ts b/packages/abi/src/tokens/erc721.ts
new file mode 100644
index 0000000000..1e20ce4050
--- /dev/null
+++ b/packages/abi/src/tokens/erc721.ts
@@ -0,0 +1,3 @@
+export const abi = []
+
+export const returns = {}
diff --git a/packages/account/hardhat.config.js b/packages/account/hardhat.config.js
new file mode 100644
index 0000000000..9e73336b07
--- /dev/null
+++ b/packages/account/hardhat.config.js
@@ -0,0 +1,12 @@
+
+module.exports = {
+ networks: {
+ hardhat: {
+ chainId: 31337,
+ port: 7146,
+ accounts: {
+ mnemonic: 'ripple axis someone ridge uniform wrist prosper there frog rate olympic knee'
+ },
+ },
+ }
+}
diff --git a/packages/account/hardhat2.config.js b/packages/account/hardhat2.config.js
new file mode 100644
index 0000000000..e984fc2e79
--- /dev/null
+++ b/packages/account/hardhat2.config.js
@@ -0,0 +1,11 @@
+
+module.exports = {
+ networks: {
+ hardhat: {
+ chainId: 31338,
+ accounts: {
+ mnemonic: 'ripple axis someone ridge uniform wrist prosper there frog rate olympic knee'
+ }
+ }
+ }
+}
diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts
new file mode 100644
index 0000000000..a327d4bff8
--- /dev/null
+++ b/packages/account/src/account.ts
@@ -0,0 +1,1198 @@
+import { walletContracts } from '@0xsequence/abi'
+import { commons, universal } from '@0xsequence/core'
+import { migrator, defaults, version } from '@0xsequence/migration'
+import { ChainId, NetworkConfig } from '@0xsequence/network'
+import { type FeeOption, type FeeQuote, isRelayer, type Relayer, RpcRelayer } from '@0xsequence/relayer'
+import type { tracker } from '@0xsequence/sessions'
+import type { SignatureOrchestrator } from '@0xsequence/signhub'
+import { encodeTypedDataDigest, getFetchRequest } from '@0xsequence/utils'
+import { Wallet } from '@0xsequence/wallet'
+import { ethers, MessagePrefix } from 'ethers'
+import { AccountSigner, AccountSignerOptions } from './signer'
+
+export type AccountStatus = {
+ original: {
+ version: number
+ imageHash: string
+ context: commons.context.WalletContext
+ }
+ onChain: {
+ imageHash: string
+ config: commons.config.Config
+ version: number
+ deployed: boolean
+ }
+ fullyMigrated: boolean
+ signedMigrations: migrator.SignedMigration[]
+ version: number
+ presignedConfigurations: tracker.PresignedConfigLink[]
+ imageHash: string
+ config: commons.config.Config
+ checkpoint: ethers.BigNumberish
+ canOnchainValidate: boolean
+}
+
+export type AccountOptions = {
+ // The only unique identifier for a wallet is the address
+ address: string
+
+ // The config tracker keeps track of chained configs,
+ // counterfactual addresses and reverse lookups for configurations
+ // it must implement both the ConfigTracker and MigrationTracker
+ tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+
+ // Versioned contexts contains the context information for each Sequence version
+ contexts: commons.context.VersionedContext
+
+ // Optional list of migrations, if not provided, the default migrations will be used
+ // NOTICE: the last vestion is considered the "current" version for the account
+ migrations?: migrator.Migrations
+
+ // Orchestrator manages signing messages and transactions
+ orchestrator: SignatureOrchestrator
+
+ // Networks information and providers
+ networks: NetworkConfig[]
+
+ // Jwt
+ jwt?: string
+
+ // Project access key
+ projectAccessKey?: string
+}
+
+export interface PreparedTransactions {
+ transactions: commons.transaction.SimulatedTransaction[]
+ flatDecorated: commons.transaction.Transaction[]
+ feeOptions: FeeOption[]
+ feeQuote?: FeeQuote
+}
+
+class Chain0Reader implements commons.reader.Reader {
+ async isDeployed(_wallet: string): Promise {
+ return false
+ }
+
+ async implementation(_wallet: string): Promise {
+ return undefined
+ }
+
+ async imageHash(_wallet: string): Promise {
+ return undefined
+ }
+
+ async nonce(_wallet: string, _space: ethers.BigNumberish): Promise {
+ return 0n
+ }
+
+ async isValidSignature(_wallet: string, _digest: ethers.BytesLike, _signature: ethers.BytesLike): Promise {
+ throw new Error('Method not supported.')
+ }
+}
+
+export class Account {
+ public readonly address: string
+
+ public readonly networks: NetworkConfig[]
+ public readonly tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+ public readonly contexts: commons.context.VersionedContext
+
+ public readonly migrator: migrator.Migrator
+ public readonly migrations: migrator.Migrations
+
+ private orchestrator: SignatureOrchestrator
+
+ private jwt?: string
+
+ private projectAccessKey?: string
+
+ constructor(options: AccountOptions) {
+ this.address = ethers.getAddress(options.address)
+
+ this.contexts = options.contexts
+ this.tracker = options.tracker
+ this.networks = options.networks
+ this.orchestrator = options.orchestrator
+ this.jwt = options.jwt
+ this.projectAccessKey = options.projectAccessKey
+
+ this.migrations = options.migrations || defaults.DefaultMigrations
+ this.migrator = new migrator.Migrator(options.tracker, this.migrations, this.contexts)
+ }
+
+ getSigner(chainId: ChainId, options?: AccountSignerOptions): AccountSigner {
+ return new AccountSigner(this, chainId, options)
+ }
+
+ static async new(options: {
+ config: commons.config.SimpleConfig
+ tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+ contexts: commons.context.VersionedContext
+ orchestrator: SignatureOrchestrator
+ networks: NetworkConfig[]
+ migrations?: migrator.Migrations
+ projectAccessKey?: string
+ }): Promise {
+ const mig = new migrator.Migrator(options.tracker, options.migrations ?? defaults.DefaultMigrations, options.contexts)
+
+ const lastMigration = mig.lastMigration()
+ const lastCoder = lastMigration.configCoder
+
+ const config = lastCoder.fromSimple(options.config)
+ const imageHash = lastCoder.imageHashOf(config)
+ const context = options.contexts[lastMigration.version]
+ const address = commons.context.addressOf(context, imageHash)
+
+ await options.tracker.saveCounterfactualWallet({ config, context: Object.values(options.contexts) })
+
+ return new Account({
+ address,
+ tracker: options.tracker,
+ contexts: options.contexts,
+ networks: options.networks,
+ orchestrator: options.orchestrator,
+ migrations: options.migrations,
+ projectAccessKey: options.projectAccessKey
+ })
+ }
+
+ getAddress(): Promise {
+ return Promise.resolve(this.address)
+ }
+
+ get version(): number {
+ return this.migrator.lastMigration().version
+ }
+
+ get coders(): {
+ signature: commons.signature.SignatureCoder
+ config: commons.config.ConfigCoder
+ } {
+ const lastMigration = this.migrator.lastMigration()
+
+ return {
+ signature: lastMigration.signatureCoder,
+ config: lastMigration.configCoder
+ }
+ }
+
+ network(chainId: ethers.BigNumberish): NetworkConfig {
+ const tcid = BigInt(chainId)
+ const found = this.networks.find(n => tcid === BigInt(n.chainId))
+ if (!found) throw new Error(`Network not found for chainId ${chainId}`)
+ return found
+ }
+
+ providerFor(chainId: ethers.BigNumberish): ethers.Provider {
+ const found = this.network(chainId)
+ if (!found.provider && !found.rpcUrl) {
+ throw new Error(`Provider not found for chainId ${chainId}`)
+ }
+
+ const network = new ethers.Network(found.name, found.chainId)
+
+ return (
+ found.provider ||
+ new ethers.JsonRpcProvider(getFetchRequest(found.rpcUrl, this.projectAccessKey, this.jwt), network, {
+ staticNetwork: network
+ })
+ )
+ }
+
+ reader(chainId: ethers.BigNumberish): commons.reader.Reader {
+ if (BigInt(chainId) === 0n) {
+ return new Chain0Reader()
+ }
+
+ // TODO: Networks should be able to provide a reader directly
+ // and we should default to the on-chain reader
+ return new commons.reader.OnChainReader(this.providerFor(chainId))
+ }
+
+ relayer(chainId: ethers.BigNumberish): Relayer {
+ const found = this.network(chainId)
+ if (!found.relayer) throw new Error(`Relayer not found for chainId ${chainId}`)
+ if (isRelayer(found.relayer)) return found.relayer
+ return new RpcRelayer({
+ ...found.relayer,
+ // we pass both projectAccessKey and jwtAuth because the projectAccessKey is
+ // used either for unauthenticated access, or gas sponsorship even if the jwtAuth is provided,
+ ...{ projectAccessKey: this.projectAccessKey, jwtAuth: this.jwt }
+ })
+ }
+
+ setOrchestrator(orchestrator: SignatureOrchestrator) {
+ this.orchestrator = orchestrator
+ }
+
+ setJwt(jwt: string) {
+ this.jwt = jwt
+ }
+
+ contextFor(version: number): commons.context.WalletContext {
+ const ctx = this.contexts[version]
+ if (!ctx) throw new Error(`Context not found for version ${version}`)
+ return ctx
+ }
+
+ walletForStatus(chainId: ethers.BigNumberish, status: Pick & Pick): Wallet {
+ const coder = universal.coderFor(status.version)
+ return this.walletFor(chainId, this.contextFor(status.version), status.config, coder)
+ }
+
+ walletFor(
+ chainId: ethers.BigNumberish,
+ context: commons.context.WalletContext,
+ config: commons.config.Config,
+ coders: typeof this.coders
+ ): Wallet {
+ const isNetworkZero = BigInt(chainId) === 0n
+ return new Wallet({
+ config,
+ context,
+ chainId,
+ coders,
+ relayer: isNetworkZero ? undefined : this.relayer(chainId),
+ address: this.address,
+ orchestrator: this.orchestrator,
+ reader: this.reader(chainId)
+ })
+ }
+
+ // Get the status of the account on a given network
+ // this does the following process:
+ // 1. Get the current on-chain status of the wallet (version + imageHash)
+ // 2. Get any pending migrations that have been signed by the wallet
+ // 3. Get any pending configuration updates that have been signed by the wallet
+ // 4. Fetch reverse lookups for both on-chain and pending configurations
+ async status(chainId: ethers.BigNumberish, longestPath: boolean = false): Promise {
+ const isDeployedPromise = this.reader(chainId).isDeployed(this.address)
+
+ const counterfactualImageHashPromise = this.tracker
+ .imageHashOfCounterfactualWallet({
+ wallet: this.address
+ })
+ .then(r => {
+ if (!r) throw new Error(`Counterfactual imageHash not found for wallet ${this.address}`)
+ return r
+ })
+
+ const counterFactualVersionPromise = counterfactualImageHashPromise.then(r => {
+ return version.counterfactualVersion(this.address, r.imageHash, Object.values(this.contexts))
+ })
+
+ const onChainVersionPromise = (async () => {
+ const isDeployed = await isDeployedPromise
+ if (!isDeployed) return counterFactualVersionPromise
+
+ const implementation = await this.reader(chainId).implementation(this.address)
+ if (!implementation) throw new Error(`Implementation not found for wallet ${this.address}`)
+
+ const versions = Object.values(this.contexts)
+ for (let i = 0; i < versions.length; i++) {
+ if (versions[i].mainModule === implementation || versions[i].mainModuleUpgradable === implementation) {
+ return versions[i].version
+ }
+ }
+
+ throw new Error(`Version not found for implementation ${implementation}`)
+ })()
+
+ const onChainImageHashPromise = (async () => {
+ const deployedImageHash = await this.reader(chainId).imageHash(this.address)
+ if (deployedImageHash) return deployedImageHash
+ const counterfactualImageHash = await counterfactualImageHashPromise
+ if (counterfactualImageHash) return counterfactualImageHash.imageHash
+ throw new Error(`On-chain imageHash not found for wallet ${this.address}`)
+ })()
+
+ const onChainConfigPromise = (async () => {
+ const onChainImageHash = await onChainImageHashPromise
+ const onChainConfig = await this.tracker.configOfImageHash({ imageHash: onChainImageHash })
+ if (onChainConfig) return onChainConfig
+ throw new Error(`On-chain config not found for imageHash ${onChainImageHash}`)
+ })()
+
+ const onChainVersion = await onChainVersionPromise
+ const onChainImageHash = await onChainImageHashPromise
+
+ let fromImageHash = onChainImageHash
+ let lastVersion = onChainVersion
+ let signedMigrations: migrator.SignedMigration[] = []
+
+ if (onChainVersion !== this.version) {
+ // We either need to use the presigned configuration updates, or we haven't performed
+ // any updates yet, so we can only use the on-chain imageHash as-is
+ const presignedMigrate = await this.migrator.getAllMigratePresignedTransaction({
+ address: this.address,
+ fromImageHash: onChainImageHash,
+ fromVersion: onChainVersion,
+ chainId
+ })
+
+ // The migrator returns the original version and imageHash
+ // if no presigned migration is found, so no need to check here
+ fromImageHash = presignedMigrate.lastImageHash
+ lastVersion = presignedMigrate.lastVersion
+
+ signedMigrations = presignedMigrate.signedMigrations
+ }
+
+ const presigned = await this.tracker.loadPresignedConfiguration({
+ wallet: this.address,
+ fromImageHash: fromImageHash,
+ longestPath
+ })
+
+ const imageHash = presigned && presigned.length > 0 ? presigned[presigned.length - 1].nextImageHash : fromImageHash
+ const config = await this.tracker.configOfImageHash({ imageHash })
+ if (!config) {
+ throw new Error(`Config not found for imageHash ${imageHash}`)
+ }
+
+ const isDeployed = await isDeployedPromise
+ const counterfactualImageHash = await counterfactualImageHashPromise
+ const checkpoint = universal.coderFor(lastVersion).config.checkpointOf(config as any)
+
+ return {
+ original: {
+ ...counterfactualImageHash,
+ version: await counterFactualVersionPromise
+ },
+ onChain: {
+ imageHash: onChainImageHash,
+ config: await onChainConfigPromise,
+ version: onChainVersion,
+ deployed: isDeployed
+ },
+ fullyMigrated: lastVersion === this.version,
+ signedMigrations,
+ version: lastVersion,
+ presignedConfigurations: presigned,
+ imageHash,
+ config,
+ checkpoint,
+ canOnchainValidate: onChainVersion === this.version && isDeployed
+ }
+ }
+
+ private mustBeFullyMigrated(status: AccountStatus) {
+ if (!status.fullyMigrated) {
+ throw new Error(`Wallet ${this.address} is not fully migrated`)
+ }
+ }
+
+ async predecorateSignedTransactions(
+ status: AccountStatus,
+ chainId: ethers.BigNumberish
+ ): Promise {
+ // Request signed predecorate transactions from child wallets
+ const bundles = await this.orchestrator.predecorateSignedTransactions({ chainId })
+ // Get signed predecorate transaction
+ const predecorated = await this.predecorateTransactions([], status, chainId)
+ if (commons.transaction.fromTransactionish(this.address, predecorated).length > 0) {
+ // Sign it
+ bundles.push(await this.signTransactions(predecorated, chainId))
+ }
+ return bundles
+ }
+
+ async predecorateTransactions(
+ txs: commons.transaction.Transactionish,
+ status: AccountStatus,
+ chainId: ethers.BigNumberish
+ ): Promise {
+ txs = Array.isArray(txs) ? txs : [txs]
+ // if onchain wallet config is not up to date
+ // then we should append an extra transaction that updates it
+ // to the latest "lazy" state
+ if (status.onChain.imageHash !== status.imageHash) {
+ const wallet = this.walletForStatus(chainId, status)
+ const updateConfig = await wallet.buildUpdateConfigurationTransaction(status.config)
+ txs = [...txs, ...updateConfig.transactions]
+ }
+
+ // On immutable chains, we add the WalletProxyHook
+ const { proxyImplementationHook } = this.contexts[status.config.version]
+ if (proxyImplementationHook && (chainId === ChainId.IMMUTABLE_ZKEVM || chainId === ChainId.IMMUTABLE_ZKEVM_TESTNET)) {
+ const provider = this.providerFor(chainId)
+ if (provider) {
+ const hook = new ethers.Contract(this.address, walletContracts.walletProxyHook.abi, provider)
+ let implementation
+ try {
+ implementation = await hook.PROXY_getImplementation()
+ } catch (e) {
+ // Handle below
+ console.log('Error getting implementation address', e)
+ }
+ if (!implementation || implementation === ethers.ZeroAddress) {
+ console.log('Adding wallet proxy hook')
+ const hooksInterface = new ethers.Interface(walletContracts.moduleHooks.abi)
+ const tx: commons.transaction.Transaction = {
+ to: this.address,
+ data: hooksInterface.encodeFunctionData(hooksInterface.getFunction('addHook')!, [
+ '0x90611127',
+ proxyImplementationHook
+ ]),
+ gasLimit: 50000, // Expected ~28k gas. Buffer added
+ delegateCall: false,
+ revertOnError: false,
+ value: 0
+ }
+ txs = [tx, ...txs]
+ }
+ }
+ }
+
+ return txs
+ }
+
+ async decorateTransactions(
+ bundles: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[],
+ status: AccountStatus,
+ chainId?: ethers.BigNumberish
+ ): Promise {
+ if (!Array.isArray(bundles)) {
+ // Recurse with array
+ return this.decorateTransactions([bundles], status, chainId)
+ }
+
+ // Default to chainId of first bundle when not supplied
+ chainId = chainId ?? bundles[0].chainId
+
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId)
+ const hasBootstrapTxs = bootstrapBundle.transactions.length > 0
+
+ if (!hasBootstrapTxs && bundles.length === 1) {
+ return bundles[0]
+ }
+
+ // Intent defaults to first bundle when no bootstrap transaction
+ const { entrypoint } = hasBootstrapTxs ? bootstrapBundle : bundles[0]
+
+ const decoratedBundle = {
+ entrypoint,
+ chainId,
+ // Intent of the first bundle is used
+ intent: bundles[0]?.intent,
+ transactions: [
+ ...bootstrapBundle.transactions,
+ ...bundles.map(
+ (bundle): commons.transaction.Transaction => ({
+ to: bundle.entrypoint,
+ data: commons.transaction.encodeBundleExecData(bundle),
+ gasLimit: 0,
+ delegateCall: false,
+ revertOnError: true,
+ value: 0
+ })
+ )
+ ]
+ }
+
+ // Re-compute the meta-transaction id to use the guest module subdigest
+ if (!status.onChain.deployed) {
+ const id = commons.transaction.subdigestOfGuestModuleTransactions(
+ this.contexts[this.version].guestModule,
+ chainId,
+ decoratedBundle.transactions
+ )
+
+ if (decoratedBundle.intent === undefined) {
+ decoratedBundle.intent = { id, wallet: this.address }
+ } else {
+ decoratedBundle.intent.id = id
+ }
+ }
+
+ return decoratedBundle
+ }
+
+ async decorateSignature(
+ signature: T,
+ status: Partial>
+ ): Promise {
+ if (!status.presignedConfigurations || status.presignedConfigurations.length === 0) {
+ return signature
+ }
+
+ const coder = this.coders.signature
+
+ const chain = status.presignedConfigurations.map(c => c.signature)
+ const chainedSignature = coder.chainSignatures(signature, chain)
+ return coder.trim(chainedSignature)
+ }
+
+ async publishWitnessFor(signers: string[], chainId: ethers.BigNumberish = 0): Promise {
+ const digest = ethers.id(`This is a Sequence account woo! ${Date.now()}`)
+
+ const status = await this.status(chainId)
+ const allOfAll = this.coders.config.fromSimple({
+ threshold: signers.length,
+ checkpoint: 0,
+ signers: signers.map(s => ({
+ address: s,
+ weight: 1
+ }))
+ })
+
+ const wallet = this.walletFor(chainId, status.original.context, allOfAll, this.coders)
+ const signature = await wallet.signDigest(digest)
+
+ const decoded = this.coders.signature.decode(signature)
+ const signatures = this.coders.signature.signaturesOfDecoded(decoded)
+
+ if (signatures.length === 0) {
+ throw new Error('No signatures found')
+ }
+
+ return this.tracker.saveWitnesses({ wallet: this.address, digest, chainId, signatures })
+ }
+
+ async publishWitness(): Promise {
+ const digest = ethers.id(`This is a Sequence account woo! ${Date.now()}`)
+ const signature = await this.signDigest(digest, 0, false)
+ const decoded = this.coders.signature.decode(signature)
+ const signatures = this.coders.signature.signaturesOfDecoded(decoded)
+ return this.tracker.saveWitnesses({ wallet: this.address, digest, chainId: 0, signatures })
+ }
+
+ async signDigest(
+ digest: ethers.BytesLike,
+ chainId: ethers.BigNumberish,
+ decorate: boolean = true,
+ cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore',
+ metadata?: object
+ ): Promise {
+ // If we are signing a digest for chainId zero then we can never be fully migrated
+ // because Sequence v1 doesn't allow for signing a message on "all chains"
+
+ // So we ignore the state on "chain zero" and instead use one of the states of the networks
+ // wallet-webapp should ensure the wallet is as migrated as possible, trying to mimic
+ // the behaviour of being migrated on all chains
+ const chainRef = BigInt(chainId) === 0n ? this.networks[0].chainId : chainId
+ const status = await this.status(chainRef)
+ this.mustBeFullyMigrated(status)
+
+ // Check if we can validate onchain and what to do if we can't
+ // revert early, since there is no point in signing a digest now
+ if (!status.canOnchainValidate && cantValidateBehavior === 'throw') {
+ throw new Error('Wallet cannot validate onchain')
+ }
+
+ const wallet = this.walletForStatus(chainId, status)
+ const signature = await wallet.signDigest(digest, metadata)
+
+ const decorated = decorate ? this.decorateSignature(signature, status) : signature
+
+ // If the wallet can't validate onchain then we
+ // need to prefix the decorated signature with all deployments and migrations
+ // aka doing a bootstrap using EIP-6492
+ if (!status.canOnchainValidate) {
+ switch (cantValidateBehavior) {
+ // NOTICE: We covered this case before signing the digest
+ // case 'throw':
+ // throw new Error('Wallet cannot validate on-chain')
+ case 'ignore':
+ return decorated
+
+ case 'eip6492':
+ return this.buildEIP6492Signature(await decorated, status, chainId)
+ }
+ }
+
+ return decorated
+ }
+
+ buildOnChainSignature(digest: ethers.BytesLike): { bundle: commons.transaction.TransactionBundle; signature: string } {
+ const subdigest = commons.signature.subdigestOf({
+ digest: ethers.hexlify(digest),
+ chainId: 0,
+ address: this.address
+ })
+ const hexSubdigest = ethers.hexlify(subdigest)
+ const config = this.coders.config.fromSimple({
+ // Threshold *only* needs to be > 0, this is not a magic number
+ // we only use 2 ** 15 because it may lead to lower gas costs in some chains
+ threshold: 32768,
+ checkpoint: 0,
+ signers: [],
+ subdigests: [hexSubdigest]
+ })
+
+ const walletInterface = new ethers.Interface(walletContracts.mainModule.abi)
+ const bundle: commons.transaction.TransactionBundle = {
+ entrypoint: this.address,
+ transactions: [
+ {
+ to: this.address,
+ data: walletInterface.encodeFunctionData(
+ // *NEVER* use updateImageHash here, as it would effectively destroy the wallet
+ // setExtraImageHash sets an additional imageHash, without changing the current one
+ 'setExtraImageHash',
+ [
+ this.coders.config.imageHashOf(config),
+ // 2 ** 255 instead of max uint256, to have more zeros in the calldata
+ '57896044618658097711785492504343953926634992332820282019728792003956564819968'
+ ]
+ ),
+ // Conservative gas limit, used because the current relayer
+ // has trouble estimating gas for this transaction
+ gasLimit: 250000
+ }
+ ]
+ }
+
+ // Fire and forget request to save the config
+ this.tracker.saveWalletConfig({ config })
+
+ // Encode a signature proof for the given subdigest
+ // use `chainId = 0` to make it simpler, as this signature is only a proof
+ const signature = this.coders.signature.encodeSigners(config, new Map(), [hexSubdigest], 0).encoded
+ return { bundle, signature }
+ }
+
+ private async buildEIP6492Signature(signature: string, status: AccountStatus, chainId: ethers.BigNumberish): Promise {
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId)
+ if (bootstrapBundle.transactions.length === 0) {
+ throw new Error('Cannot build EIP-6492 signature without bootstrap transactions')
+ }
+
+ const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
+ ['address', 'bytes', 'bytes'],
+ [bootstrapBundle.entrypoint, commons.transaction.encodeBundleExecData(bootstrapBundle), signature]
+ )
+
+ return ethers.solidityPacked(['bytes', 'bytes32'], [encoded, commons.EIP6492.EIP_6492_SUFFIX])
+ }
+
+ async editConfig(changes: {
+ add?: commons.config.SimpleSigner[]
+ remove?: string[]
+ threshold?: ethers.BigNumberish
+ }): Promise {
+ const currentConfig = await this.status(0).then(s => s.config)
+ const newConfig = this.coders.config.editConfig(currentConfig, {
+ ...changes,
+ checkpoint: this.coders.config.checkpointOf(currentConfig) + 1n
+ })
+
+ return this.updateConfig(newConfig)
+ }
+
+ async updateConfig(config: commons.config.Config): Promise {
+ // config should be for the current version of the wallet
+ if (!this.coders.config.isWalletConfig(config)) {
+ throw new Error(`Invalid config for wallet ${this.address}`)
+ }
+
+ const nextImageHash = this.coders.config.imageHashOf(config)
+
+ // sign an update config struct
+ const updateStruct = this.coders.signature.hashSetImageHash(nextImageHash)
+
+ // sign the update struct, using chain id 0
+ const signature = await this.signDigest(updateStruct, 0, false)
+
+ // save the presigned transaction to the sessions tracker
+ await this.tracker.savePresignedConfiguration({
+ wallet: this.address,
+ nextConfig: config,
+ signature,
+ referenceChainId: 1
+ })
+
+ // safety check, tracker should have a reverse lookup for the imageHash
+ // outside of the local cache
+ const reverseConfig = await this.tracker.configOfImageHash({
+ imageHash: nextImageHash,
+ noCache: true
+ })
+
+ if (!reverseConfig || this.coders.config.imageHashOf(reverseConfig) !== nextImageHash) {
+ throw Error(`Reverse lookup failed for imageHash ${nextImageHash}`)
+ }
+ }
+
+ /**
+ * This method is used to bootstrap the wallet on a given chain.
+ * this deploys the wallets and executes all the necessary transactions
+ * for that wallet to start working with the given version.
+ *
+ * This usually involves: (a) deploying the wallet, (b) executing migrations
+ *
+ * Notice: It should NOT explicitly include chained signatures. Unless internally used
+ * by any of the migrations.
+ *
+ */
+ async buildBootstrapTransactions(
+ status: AccountStatus,
+ chainId: ethers.BigNumberish
+ ): Promise {
+ const bundle = await this.orchestrator.buildDeployTransaction({ chainId })
+ const transactions: commons.transaction.Transaction[] = bundle?.transactions ?? []
+
+ // Add wallet deployment if needed
+ if (!status.onChain.deployed) {
+ let gasLimit: bigint | undefined
+ switch (BigInt(chainId)) {
+ case BigInt(ChainId.SKALE_NEBULA):
+ gasLimit = 10000000n
+ break
+ case BigInt(ChainId.SOMNIA_TESTNET):
+ gasLimit = 10000000n
+ break
+ case BigInt(ChainId.SOMNIA):
+ gasLimit = 10000000n
+ break
+ }
+
+ // Wallet deployment will vary depending on the version
+ // so we need to use the context to get the correct deployment
+ const deployTransaction = Wallet.buildDeployTransaction(status.original.context, status.original.imageHash, gasLimit)
+
+ transactions.push(...deployTransaction.transactions)
+ }
+
+ // Get pending migrations
+ transactions.push(
+ ...status.signedMigrations.map(m => ({
+ to: m.tx.entrypoint,
+ data: commons.transaction.encodeBundleExecData(m.tx),
+ value: 0,
+ gasLimit: 0,
+ revertOnError: true,
+ delegateCall: false
+ }))
+ )
+
+ // Build the transaction intent, if the transaction has migrations
+ // then we should use one of the intents of the migrations (anyone will do)
+ // if it doesn't, then the only intent we could use if the GuestModule one
+ // ... but this may fail if the relayer uses a different GuestModule
+ const id =
+ status.signedMigrations.length > 0
+ ? status.signedMigrations[0].tx.intent.id
+ : commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, chainId, transactions)
+
+ // Everything is encoded as a bundle
+ // using the GuestModule of the account version
+ const { guestModule } = this.contextFor(status.version)
+ return { entrypoint: guestModule, transactions, chainId, intent: { id, wallet: this.address } }
+ }
+
+ async bootstrapTransactions(
+ chainId: ethers.BigNumberish,
+ prestatus?: AccountStatus
+ ): Promise> {
+ const status = prestatus || (await this.status(chainId))
+ return this.buildBootstrapTransactions(status, chainId)
+ }
+
+ async doBootstrap(chainId: ethers.BigNumberish, feeQuote?: FeeQuote, prestatus?: AccountStatus) {
+ const bootstrapTxs = await this.bootstrapTransactions(chainId, prestatus)
+ return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }, feeQuote)
+ }
+
+ /**
+ * Signs a message.
+ *
+ * This method will sign the message using the account associated with this signer
+ * and the specified chain ID. If the message is already prefixed with the EIP-191
+ * prefix, it will be hashed directly. Otherwise, it will be prefixed before hashing.
+ *
+ * @param message - The message to sign. Can be a string or BytesLike.
+ * @param chainId - The chain ID to use for signing
+ * @param cantValidateBehavior - Behavior when the wallet cannot validate on-chain
+ * @returns A Promise that resolves to the signature as a hexadecimal string
+ */
+ signMessage(
+ message: ethers.BytesLike,
+ chainId: ethers.BigNumberish,
+ cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore'
+ ): Promise {
+ const messageHex = ethers.hexlify(message)
+ const prefixHex = ethers.hexlify(ethers.toUtf8Bytes(MessagePrefix))
+
+ let digest: string
+
+ // We check if the message is already prefixed with EIP-191
+ // This will avoid breaking changes for codebases where the message is already prefixed
+ if (messageHex.substring(2).startsWith(prefixHex.substring(2))) {
+ digest = ethers.keccak256(message)
+ } else {
+ digest = ethers.hashMessage(message)
+ }
+
+ return this.signDigest(digest, chainId, true, cantValidateBehavior)
+ }
+
+ async signTransactions(
+ txs: commons.transaction.Transactionish,
+ chainId: ethers.BigNumberish,
+ pstatus?: AccountStatus,
+ options?: {
+ nonceSpace?: ethers.BigNumberish
+ serial?: boolean
+ }
+ ): Promise {
+ const status = pstatus || (await this.status(chainId))
+ this.mustBeFullyMigrated(status)
+
+ const wallet = this.walletForStatus(chainId, status)
+
+ const metadata: commons.WalletSignRequestMetadata = {
+ address: this.address,
+ digest: '', // Set in wallet.signTransactions
+ chainId,
+ config: { version: this.version },
+ decorate: true,
+ cantValidateBehavior: 'ignore'
+ }
+
+ const nonceOptions = options?.serial
+ ? { serial: true }
+ : options?.nonceSpace !== undefined
+ ? { space: options.nonceSpace }
+ : undefined
+
+ const signed = await wallet.signTransactions(txs, nonceOptions, metadata)
+
+ return {
+ ...signed,
+ signature: await this.decorateSignature(signed.signature, status)
+ }
+ }
+
+ async signMigrations(
+ chainId: ethers.BigNumberish,
+ editConfig: (prevConfig: commons.config.Config) => commons.config.Config
+ ): Promise {
+ const status = await this.status(chainId)
+ if (status.fullyMigrated) return false
+
+ const wallet = this.walletForStatus(chainId, status)
+ const nextConfig = editConfig(wallet.config)
+ const signed = await this.migrator.signNextMigration(this.address, status.version, wallet, nextConfig)
+ if (!signed) return false
+
+ // Make sure the tracker has a copy of the config
+ // before attempting to save the migration
+ // otherwise if this second step fails the tracker could end up
+ // with a migration to an unknown config
+ await this.tracker.saveWalletConfig({ config: nextConfig })
+ const nextCoder = universal.coderFor(nextConfig.version).config
+ const nextImageHash = nextCoder.imageHashOf(nextConfig as any)
+ const reverseConfig = await this.tracker.configOfImageHash({ imageHash: nextImageHash, noCache: true })
+ if (!reverseConfig || nextCoder.imageHashOf(reverseConfig as any) !== nextImageHash) {
+ throw Error(`Reverse lookup failed for imageHash ${nextImageHash}`)
+ }
+
+ await this.tracker.saveMigration(this.address, signed, this.contexts)
+
+ return true
+ }
+
+ async signAllMigrations(
+ editConfig: (prevConfig: commons.config.Config) => commons.config.Config
+ ): Promise<{ signedMigrations: Array; failedChains: number[] }> {
+ const failedChains: number[] = []
+ const signedMigrations = await Promise.all(
+ this.networks.map(async n => {
+ try {
+ // Signing migrations for each chain
+ return await this.signMigrations(n.chainId, editConfig)
+ } catch (error) {
+ console.warn(`Failed to sign migrations for chain ${n.chainId}`, error)
+
+ // Adding failed chainId to the failedChains array
+ failedChains.push(n.chainId)
+ // Using null as a placeholder for failed chains
+ return null
+ }
+ })
+ )
+
+ // Filter out null values to get only the successful signed migrations
+ const successfulSignedMigrations = signedMigrations.filter(migration => migration !== null)
+
+ return { signedMigrations: successfulSignedMigrations, failedChains }
+ }
+
+ async isMigratedAllChains(): Promise<{ migratedAllChains: boolean; failedChains: number[] }> {
+ const failedChains: number[] = []
+ const statuses = await Promise.all(
+ this.networks.map(async n => {
+ try {
+ return await this.status(n.chainId)
+ } catch (error) {
+ failedChains.push(n.chainId)
+
+ console.warn(`Failed to get status for chain ${n.chainId}`, error)
+
+ // default to true for failed chains
+ return { fullyMigrated: true }
+ }
+ })
+ )
+
+ const migratedAllChains = statuses.every(s => s.fullyMigrated)
+ return { migratedAllChains, failedChains }
+ }
+
+ async sendSignedTransactions(
+ signedBundle: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[],
+ chainId: ethers.BigNumberish,
+ quote?: FeeQuote,
+ pstatus?: AccountStatus,
+ callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void,
+ projectAccessKey?: string,
+ waitForReceipt?: boolean
+ ): Promise {
+ if (!Array.isArray(signedBundle)) {
+ return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback, projectAccessKey)
+ }
+ const status = pstatus || (await this.status(chainId))
+ this.mustBeFullyMigrated(status)
+
+ const decoratedBundle = await this.decorateTransactions(signedBundle, status, chainId)
+ callback?.(decoratedBundle)
+
+ return this.relayer(chainId).relay(decoratedBundle, quote, waitForReceipt, projectAccessKey)
+ }
+
+ async fillGasLimits(
+ txs: commons.transaction.Transactionish,
+ chainId: ethers.BigNumberish,
+ status?: AccountStatus
+ ): Promise {
+ const wallet = this.walletForStatus(chainId, status || (await this.status(chainId)))
+ return wallet.fillGasLimits(txs)
+ }
+
+ async gasRefundQuotes(
+ txs: commons.transaction.Transactionish,
+ chainId: ethers.BigNumberish,
+ stubSignatureOverrides: Map,
+ status?: AccountStatus,
+ options?: {
+ simulate?: boolean
+ projectAccessKey?: string
+ }
+ ): Promise<{
+ options: FeeOption[]
+ quote?: FeeQuote
+ decorated: commons.transaction.IntendedTransactionBundle
+ }> {
+ const wstatus = status || (await this.status(chainId))
+ const wallet = this.walletForStatus(chainId, wstatus)
+
+ const predecorated = await this.predecorateTransactions(txs, wstatus, chainId)
+ const transactions = commons.transaction.fromTransactionish(this.address, predecorated)
+
+ // We can't sign the transactions (because we don't want to bother the user)
+ // so we use the latest configuration to build a "stub" signature, the relayer
+ // knows to ignore the wallet signatures
+ const stubSignature = wallet.coders.config.buildStubSignature(wallet.config, stubSignatureOverrides)
+
+ // Now we can decorate the transactions as always, but we need to manually build the signed bundle
+ const intentId = ethers.hexlify(ethers.randomBytes(32))
+ const signedBundle: commons.transaction.SignedTransactionBundle = {
+ chainId,
+ intent: {
+ id: intentId,
+ wallet: this.address
+ },
+ signature: stubSignature,
+ transactions,
+ entrypoint: this.address,
+ nonce: 0 // The relayer also ignored the nonce
+ }
+
+ const decoratedBundle = await this.decorateTransactions(signedBundle, wstatus)
+ const data = commons.transaction.encodeBundleExecData(decoratedBundle)
+ const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data, options)
+ return { ...res, decorated: decoratedBundle }
+ }
+
+ async prepareTransactions(args: {
+ txs: commons.transaction.Transactionish
+ chainId: ethers.BigNumberish
+ stubSignatureOverrides: Map
+ simulateForFeeOptions?: boolean
+ projectAccessKey?: string
+ }): Promise {
+ const status = await this.status(args.chainId)
+
+ const transactions = await this.fillGasLimits(args.txs, args.chainId, status)
+ const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status, {
+ simulate: args.simulateForFeeOptions,
+ projectAccessKey: args.projectAccessKey
+ })
+ const flatDecorated = commons.transaction.unwind(this.address, gasRefundQuote.decorated.transactions)
+
+ return {
+ transactions,
+ flatDecorated,
+ feeOptions: gasRefundQuote.options,
+ feeQuote: gasRefundQuote.quote
+ }
+ }
+
+ async sendTransaction(
+ txs: commons.transaction.Transactionish,
+ chainId: ethers.BigNumberish,
+ quote?: FeeQuote,
+ skipPreDecorate: boolean = false,
+ callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void,
+ options?: {
+ nonceSpace?: ethers.BigNumberish
+ serial?: boolean
+ projectAccessKey?: string,
+ waitForReceipt?: boolean
+ }
+ ): Promise {
+ const status = await this.status(chainId)
+
+ const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId)
+ const hasTxs = commons.transaction.fromTransactionish(this.address, predecorated).length > 0
+ const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined
+
+ const childBundles = await this.orchestrator.predecorateSignedTransactions({ chainId })
+
+ const bundles: commons.transaction.SignedTransactionBundle[] = []
+ if (signed !== undefined && signed.transactions.length > 0) {
+ bundles.push(signed)
+ }
+ bundles.push(...childBundles.filter(b => b.transactions.length > 0))
+
+ return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback, options?.projectAccessKey, options?.waitForReceipt)
+ }
+
+ async signTypedData(
+ domain: ethers.TypedDataDomain,
+ types: Record>,
+ message: Record,
+ chainId: ethers.BigNumberish,
+ cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore'
+ ): Promise {
+ const digest = encodeTypedDataDigest({ domain, types, message })
+ return this.signDigest(digest, chainId, true, cantValidateBehavior)
+ }
+
+ async getSigners(): Promise> {
+ const last = (ts: T[]): T | undefined => (ts.length ? ts[ts.length - 1] : undefined)
+
+ return (
+ await Promise.all(
+ this.networks.map(async ({ chainId, name }) => {
+ try {
+ const status = await this.status(chainId)
+
+ let latestImageHash = last(status.presignedConfigurations)?.nextImageHash
+ if (!latestImageHash) {
+ if (status.onChain.version !== status.version) {
+ const migration = last(status.signedMigrations)
+ if (migration) {
+ const { toVersion, toConfig } = migration
+ const coder = universal.genericCoderFor(toVersion)
+ latestImageHash = coder.config.imageHashOf(toConfig)
+ }
+ }
+ }
+ if (!latestImageHash) {
+ latestImageHash = status.onChain.imageHash
+ }
+
+ const latestConfig = await this.tracker.configOfImageHash({ imageHash: latestImageHash })
+ if (!latestConfig) {
+ throw new Error(`unable to find config for image hash ${latestImageHash}`)
+ }
+
+ const coder = universal.genericCoderFor(latestConfig.version)
+ const signers = coder.config.signersOf(latestConfig)
+
+ return signers.map(signer => ({ ...signer, network: chainId }))
+ } catch (error) {
+ console.warn(`unable to get signers on network ${chainId} ${name}`, error)
+ return []
+ }
+ })
+ )
+ ).flat()
+ }
+
+ async getAllSigners(): Promise<
+ {
+ address: string
+ weight: number
+ network: number
+ flaggedForRemoval: boolean
+ }[]
+ > {
+ const allSigners: {
+ address: string
+ weight: number
+ network: number
+ flaggedForRemoval: boolean
+ }[] = []
+
+ // We need to get the signers for each status
+ await Promise.all(
+ this.networks.map(async network => {
+ const chainId = network.chainId
+
+ // Getting the status with `longestPath` set to true will give us all the possible configurations
+ // between the current onChain config and the latest config, including the ones "flagged for removal"
+ const status = await this.status(chainId, true)
+
+ const fullChain = [
+ status.onChain.imageHash,
+ ...(status.onChain.version !== status.version
+ ? status.signedMigrations.map(m => universal.coderFor(m.toVersion).config.imageHashOf(m.toConfig as any))
+ : []),
+ ...status.presignedConfigurations.map(update => update.nextImageHash)
+ ]
+
+ return Promise.all(
+ fullChain.map(async (nextImageHash, iconf) => {
+ const isLast = iconf === fullChain.length - 1
+ const config = await this.tracker.configOfImageHash({ imageHash: nextImageHash })
+
+ if (!config) {
+ console.warn(`AllSigners may be incomplete, config not found for imageHash ${nextImageHash}`)
+ return
+ }
+
+ const coder = universal.genericCoderFor(config.version)
+ const signers = coder.config.signersOf(config)
+
+ signers.forEach(signer => {
+ const exists = allSigners.find(s => s.address === signer.address && s.network === chainId)
+
+ if (exists && isLast && exists.flaggedForRemoval) {
+ exists.flaggedForRemoval = false
+ return
+ }
+
+ if (exists) return
+
+ allSigners.push({
+ address: signer.address,
+ weight: signer.weight,
+ network: chainId,
+ flaggedForRemoval: !isLast
+ })
+ })
+ })
+ )
+ })
+ )
+
+ return allSigners
+ }
+}
+
+export function isAccount(value: any): value is Account {
+ return value instanceof Account
+}
diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts
new file mode 100644
index 0000000000..8a695b2e25
--- /dev/null
+++ b/packages/account/src/index.ts
@@ -0,0 +1 @@
+export * from './account'
diff --git a/packages/account/src/orchestrator/wrapper.ts b/packages/account/src/orchestrator/wrapper.ts
new file mode 100644
index 0000000000..9efa34af17
--- /dev/null
+++ b/packages/account/src/orchestrator/wrapper.ts
@@ -0,0 +1,69 @@
+import { commons } from '@0xsequence/core'
+import { signers, Status } from '@0xsequence/signhub'
+import { ethers } from 'ethers'
+import { Account } from '../account'
+
+export type MetadataWithChainId = {
+ chainId: ethers.BigNumberish
+}
+
+// Implements a wrapper for using Sequence accounts as nested signers in the signhub orchestrator.
+export class AccountOrchestratorWrapper implements signers.SapientSigner {
+ constructor(public account: Account) {}
+
+ async getAddress(): Promise {
+ return this.account.address
+ }
+
+ getChainIdFromMetadata(metadata: object): bigint {
+ try {
+ const { chainId } = metadata as MetadataWithChainId
+ return BigInt(chainId)
+ } catch (err) {
+ // Invalid metadata object
+ throw new Error('AccountOrchestratorWrapper only supports metadata with chain id')
+ }
+ }
+
+ async buildDeployTransaction(metadata: object): Promise {
+ const chainId = this.getChainIdFromMetadata(metadata)
+ const status = await this.account.status(chainId)
+ return this.account.buildBootstrapTransactions(status, chainId)
+ }
+
+ async predecorateSignedTransactions(metadata: object): Promise {
+ const chainId = this.getChainIdFromMetadata(metadata)
+ const status = await this.account.status(chainId)
+ return this.account.predecorateSignedTransactions(status, chainId)
+ }
+
+ async decorateTransactions(
+ bundle: commons.transaction.IntendedTransactionBundle,
+ metadata: object
+ ): Promise {
+ const chainId = this.getChainIdFromMetadata(metadata)
+ const status = await this.account.status(chainId)
+ return this.account.decorateTransactions(bundle, status)
+ }
+
+ sign(message: ethers.BytesLike, metadata: object): Promise {
+ if (!commons.isWalletSignRequestMetadata(metadata)) {
+ throw new Error('AccountOrchestratorWrapper only supports wallet metadata requests')
+ }
+
+ const { chainId, decorate } = metadata
+ // EIP-6492 not supported on nested signatures
+ // Default to throw instead of ignore. Ignoring should be explicit
+ const cantValidateBehavior = metadata.cantValidateBehavior ?? 'throw'
+
+ // For Sequence nested signatures we must use `signDigest` and not `signMessage`
+ // otherwise the account will hash the digest and the signature will be invalid.
+ return this.account.signDigest(message, chainId, decorate, cantValidateBehavior, metadata)
+ }
+
+ notifyStatusChange(_i: string, _s: Status, _m: object): void {}
+
+ suffix(): ethers.BytesLike {
+ return new Uint8Array([3])
+ }
+}
diff --git a/packages/account/src/signer.ts b/packages/account/src/signer.ts
new file mode 100644
index 0000000000..21aee242a7
--- /dev/null
+++ b/packages/account/src/signer.ts
@@ -0,0 +1,243 @@
+import { ChainId } from '@0xsequence/network'
+import { Account } from './account'
+import { ethers } from 'ethers'
+import { commons } from '@0xsequence/core'
+import { FeeOption, proto } from '@0xsequence/relayer'
+import { toHexString } from '@0xsequence/utils'
+
+export type AccountSignerOptions = {
+ nonceSpace?: ethers.BigNumberish
+ cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'
+ stubSignatureOverrides?: Map
+ selectFee?: (
+ txs: ethers.TransactionRequest | commons.transaction.Transactionish,
+ options: FeeOption[]
+ ) => Promise
+}
+
+function encodeGasRefundTransaction(option?: FeeOption) {
+ if (!option) return []
+
+ const value = BigInt(option.value)
+
+ switch (option.token.type) {
+ case proto.FeeTokenType.UNKNOWN:
+ return [
+ {
+ delegateCall: false,
+ revertOnError: true,
+ gasLimit: option.gasLimit,
+ to: option.to,
+ value: toHexString(value),
+ data: '0x'
+ }
+ ]
+
+ case proto.FeeTokenType.ERC20_TOKEN:
+ if (!option.token.contractAddress) {
+ throw new Error(`No contract address for ERC-20 fee option`)
+ }
+
+ return [
+ {
+ delegateCall: false,
+ revertOnError: true,
+ gasLimit: option.gasLimit,
+ to: option.token.contractAddress,
+ value: 0,
+ data: new ethers.Interface([
+ {
+ constant: false,
+ inputs: [{ type: 'address' }, { type: 'uint256' }],
+ name: 'transfer',
+ outputs: [],
+ type: 'function'
+ }
+ ]).encodeFunctionData('transfer', [option.to, toHexString(value)])
+ }
+ ]
+
+ default:
+ throw new Error(`Unhandled fee token type ${option.token.type}`)
+ }
+}
+
+export class AccountSigner implements ethers.AbstractSigner {
+ constructor(
+ public account: Account,
+ public chainId: ChainId,
+ public readonly options?: AccountSignerOptions
+ ) {}
+
+ get provider() {
+ return this.account.providerFor(this.chainId)
+ }
+
+ async getAddress(): Promise {
+ return this.account.address
+ }
+
+ /**
+ * Signs a message.
+ *
+ * This method will sign the message using the account associated with this signer
+ * and the specified chain ID. The message is already being prefixed with the EIP-191 prefix.
+ *
+ * @param message - The message to sign. Can be a string or BytesLike.
+ * @returns A Promise that resolves to the signature as a hexadecimal string
+ *
+ * @example
+ * ```typescript
+ * const signer = account.getSigner(chainId)
+ *
+ * const message = "Hello, Sequence!";
+ * const signature = await signer.signMessage(message);
+ * console.log(signature);
+ * // => "0x123abc..." (hexadecimal signature)
+ */
+ signMessage(message: string | ethers.BytesLike): Promise {
+ return this.account.signMessage(message, this.chainId, this.options?.cantValidateBehavior ?? 'throw')
+ }
+
+ signTypedData(
+ domain: ethers.TypedDataDomain,
+ types: Record>,
+ value: Record
+ ): Promise {
+ return this.account.signTypedData(domain, types, value, this.chainId, this.options?.cantValidateBehavior ?? 'throw')
+ }
+
+ private async defaultSelectFee(_txs: commons.transaction.Transactionish, options: FeeOption[]): Promise {
+ // If no options, return undefined
+ if (options.length === 0) return undefined
+
+ // If there are multiple options, try them one by one
+ // until we find one that satisfies the balance requirement
+ const balanceOfAbi = [
+ {
+ constant: true,
+ inputs: [{ type: 'address' }],
+ name: 'balanceOf',
+ outputs: [{ type: 'uint256' }],
+ type: 'function'
+ }
+ ]
+
+ for (const option of options) {
+ if (option.token.type === proto.FeeTokenType.UNKNOWN) {
+ // Native token
+ const balance = await this.getBalance()
+ if (balance >= BigInt(option.value)) {
+ return option
+ }
+ } else if (option.token.contractAddress && option.token.type === proto.FeeTokenType.ERC20_TOKEN) {
+ // ERC20 token
+ const token = new ethers.Contract(option.token.contractAddress, balanceOfAbi, this.provider)
+ const balance = await token.balanceOf(this.account.address)
+ if (balance >= BigInt(option.value)) {
+ return option
+ }
+ } else {
+ // Unsupported token type
+ }
+ }
+
+ throw new Error('No fee option available - not enough balance')
+ }
+
+ async sendTransaction(
+ txs: commons.transaction.Transactionish,
+ options?: {
+ simulateForFeeOptions?: boolean
+ projectAccessKey?: string
+ waitForReceipt?: boolean
+ }
+ ): Promise {
+ const prepare = await this.account.prepareTransactions({
+ txs,
+ chainId: this.chainId,
+ stubSignatureOverrides: this.options?.stubSignatureOverrides ?? new Map(),
+ simulateForFeeOptions: options?.simulateForFeeOptions
+ })
+
+ const selectMethod = this.options?.selectFee ?? this.defaultSelectFee.bind(this)
+ const feeOption = await selectMethod(txs, prepare.feeOptions)
+
+ const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)]
+
+ return this.account.sendTransaction(
+ finalTransactions,
+ this.chainId,
+ prepare.feeQuote,
+ undefined,
+ undefined,
+ {
+ nonceSpace: this.options?.nonceSpace,
+ projectAccessKey: options?.projectAccessKey,
+ waitForReceipt: options?.waitForReceipt
+ }
+ ) as Promise // Will always have a transaction response
+ }
+
+ getBalance(blockTag?: ethers.BlockTag | undefined): Promise {
+ return this.provider.getBalance(this.account.address, blockTag)
+ }
+
+ call(transaction: ethers.TransactionRequest, blockTag?: ethers.BlockTag): Promise {
+ return this.provider.call({ ...transaction, blockTag })
+ }
+
+ async resolveName(name: string): Promise {
+ const res = await this.provider.resolveName(name)
+ if (!res) throw new Error(`Could not resolve name ${name}`)
+ return res
+ }
+
+ connect(_provider: ethers.Provider): ethers.Signer {
+ throw new Error('Method not implemented.')
+ }
+
+ signTransaction(transaction: ethers.TransactionRequest): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ getTransactionCount(blockTag?: ethers.BlockTag | undefined): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ estimateGas(transaction: ethers.TransactionRequest): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ getChainId(): Promise {
+ return Promise.resolve(Number(this.chainId))
+ }
+
+ getGasPrice(): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ getFeeData(): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ getNonce(blockTag?: ethers.BlockTag): Promise {
+ throw new Error('Method not implemented.')
+ }
+
+ populateCall(tx: ethers.TransactionRequest): Promise> {
+ throw new Error('Method not implemented.')
+ }
+
+ checkTransaction(transaction: ethers.TransactionRequest): ethers.TransactionRequest {
+ throw new Error('Method not implemented.')
+ }
+
+ async populateTransaction(tx: ethers.TransactionRequest): Promise> {
+ throw new Error('Method not implemented.')
+ }
+
+ _checkProvider(operation?: string | undefined): void {
+ throw new Error('Method not implemented.')
+ }
+}
diff --git a/packages/account/tests/account.spec.ts b/packages/account/tests/account.spec.ts
new file mode 100644
index 0000000000..a79acec88d
--- /dev/null
+++ b/packages/account/tests/account.spec.ts
@@ -0,0 +1,1557 @@
+import { walletContracts } from '@0xsequence/abi'
+import { commons, v1, v2 } from '@0xsequence/core'
+import type { migrator } from '@0xsequence/migration'
+import type { NetworkConfig } from '@0xsequence/network'
+import { LocalRelayer, Relayer } from '@0xsequence/relayer'
+import { tracker, trackers } from '@0xsequence/sessions'
+import { Orchestrator } from '@0xsequence/signhub'
+import * as utils from '@0xsequence/tests'
+import { Wallet } from '@0xsequence/wallet'
+import * as chai from 'chai'
+import chaiAsPromised from 'chai-as-promised'
+import { concat, ethers, MessagePrefix, toUtf8Bytes } from 'ethers'
+import hardhat from 'hardhat'
+
+import { Account } from '../src/account'
+import { AccountOrchestratorWrapper } from '../src/orchestrator/wrapper'
+
+const { expect } = chai.use(chaiAsPromised)
+
+const deterministic = false
+
+describe('Account', () => {
+ let provider1: ethers.BrowserProvider
+ let provider2: ethers.JsonRpcProvider
+
+ let signer1: ethers.Signer
+ let signer2: ethers.Signer
+
+ let contexts: commons.context.VersionedContext
+ let networks: NetworkConfig[]
+
+ let tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+
+ let defaultArgs: {
+ contexts: commons.context.VersionedContext
+ networks: NetworkConfig[]
+ tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+ }
+
+ let defaultTx: commons.transaction.Transaction
+
+ const createNestedAccount = async (entropy: string, bootstrapInner = true, bootstrapOuter = true) => {
+ const signer = randomWallet(entropy)
+
+ const configInner = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+ const accountInner = await Account.new({
+ ...defaultArgs,
+ config: configInner,
+ orchestrator: new Orchestrator([signer])
+ })
+ if (bootstrapInner) {
+ await accountInner.doBootstrap(networks[0].chainId)
+ }
+
+ const configOuter = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: accountInner.address, weight: 1 }]
+ }
+ const accountOuter = await Account.new({
+ ...defaultArgs,
+ config: configOuter,
+ orchestrator: new Orchestrator([new AccountOrchestratorWrapper(accountInner)])
+ })
+ if (bootstrapOuter) {
+ await accountOuter.doBootstrap(networks[0].chainId)
+ }
+
+ return { signer, accountInner, accountOuter }
+ }
+
+ const getEth = async (address: string, signer?: ethers.Signer) => {
+ if (signer === undefined) {
+ // Do both networks
+ await getEth(address, signer1)
+ await getEth(address, signer2)
+ return
+ }
+ // Signer sends the address some ETH for defaultTx use
+ const tx = await signer.sendTransaction({
+ to: address,
+ value: 10 // Should be plenty
+ })
+ await tx.wait()
+ }
+
+ before(async () => {
+ provider1 = new ethers.BrowserProvider(hardhat.network.provider as any, undefined, { cacheTimeout: -1 })
+ provider2 = new ethers.JsonRpcProvider('http://127.0.0.1:7048', undefined, { cacheTimeout: -1 })
+
+ // TODO: Implement migrations on local config tracker
+ tracker = new trackers.local.LocalConfigTracker(provider1)
+
+ signer1 = await provider1.getSigner()
+ signer2 = await provider2.getSigner()
+
+ networks = [
+ {
+ chainId: 31337,
+ name: 'hardhat',
+ provider: provider1,
+ rpcUrl: '',
+ relayer: new LocalRelayer(signer1),
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ },
+ {
+ chainId: 31338,
+ name: 'hardhat2',
+ provider: provider2,
+ rpcUrl: 'http://127.0.0.1:7048',
+ relayer: new LocalRelayer(signer2),
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ }
+ ]
+
+ const context1 = utils.context.deploySequenceContexts(signer1)
+ const context2 = utils.context.deploySequenceContexts(signer2)
+ expect(await context1).to.deep.equal(await context2)
+ contexts = await context1
+
+ defaultArgs = {
+ contexts,
+ networks,
+ tracker
+ }
+
+ defaultTx = {
+ to: await signer1.getAddress(),
+ value: 1
+ }
+ })
+
+ describe('New account', () => {
+ it('Should create a new account', async () => {
+ const signer = randomWallet('Should create a new account')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ expect(account).to.be.instanceOf(Account)
+ expect(account.address).to.not.be.undefined
+
+ await getEth(account.address)
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.version).to.equal(2)
+ })
+
+ it('Should create new nested accounts', async () => {
+ const { accountInner, accountOuter } = await createNestedAccount('create new nested accounts', false, false)
+
+ await getEth(accountOuter.address)
+ await accountOuter.sendTransaction([defaultTx], networks[0].chainId)
+
+ const statusOuter = await accountOuter.status(networks[0].chainId)
+
+ expect(statusOuter.fullyMigrated).to.be.true
+ expect(statusOuter.onChain.deployed).to.be.true
+ expect(statusOuter.onChain.version).to.equal(2)
+
+ const statusInner = await accountInner.status(networks[0].chainId)
+ expect(statusInner.fullyMigrated).to.be.true
+ expect(statusInner.onChain.deployed).to.be.true
+ expect(statusInner.onChain.version).to.equal(2)
+ })
+
+ it('Should send tx on nested accounts', async () => {
+ const { accountInner, accountOuter } = await createNestedAccount('sent tx on nested accounts', true, true)
+
+ await getEth(accountOuter.address)
+ await accountOuter.sendTransaction([defaultTx], networks[0].chainId)
+
+ const statusOuter = await accountOuter.status(networks[0].chainId)
+
+ expect(statusOuter.fullyMigrated).to.be.true
+ expect(statusOuter.onChain.deployed).to.be.true
+ expect(statusOuter.onChain.version).to.equal(2)
+
+ const statusInner = await accountInner.status(networks[0].chainId)
+ expect(statusInner.fullyMigrated).to.be.true
+ expect(statusInner.onChain.deployed).to.be.true
+ expect(statusInner.onChain.version).to.equal(2)
+ })
+
+ it('Should send transactions on multiple networks', async () => {
+ const signer = randomWallet('Should send transactions on multiple networks')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ await getEth(account.address)
+ await account.sendTransaction([defaultTx], networks[0].chainId)
+ await account.sendTransaction([defaultTx], networks[1].chainId)
+
+ const status1 = await account.status(networks[0].chainId)
+ const status2 = await account.status(networks[1].chainId)
+
+ expect(status1.fullyMigrated).to.be.true
+ expect(status1.onChain.deployed).to.be.true
+ expect(status1.onChain.version).to.equal(2)
+
+ expect(status2.fullyMigrated).to.be.true
+ expect(status2.onChain.deployed).to.be.true
+ expect(status2.onChain.version).to.equal(2)
+ })
+
+ it('Should create a new account with many signers', async () => {
+ const signers = new Array(24).fill(0).map(() => randomWallet('Should create a new account with many signers'))
+ const config = {
+ threshold: 3,
+ checkpoint: Math.floor(now() / 1000),
+ signers: signers.map(signer => ({
+ address: signer.address,
+ weight: 1
+ }))
+ }
+
+ const rsigners = signers.sort(() => randomFraction('Should create a new account with many signers 2') - 0.5)
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator(rsigners.slice(0, 4))
+ })
+
+ await getEth(account.address)
+ await account.sendTransaction([defaultTx], networks[0].chainId)
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.version).to.equal(2)
+ })
+
+ it('Should sign and validate a message', async () => {
+ const signer = randomWallet('Should sign and validate a message')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ await account.doBootstrap(networks[0].chainId)
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await account.signMessage(msg, networks[0].chainId)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ account.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+
+ it('Should sign and validate a message with nested account', async () => {
+ const { accountOuter } = await createNestedAccount('sign and validate nested')
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await accountOuter.signMessage(msg, networks[0].chainId)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ accountOuter.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+
+ it('Should update account to new configuration', async () => {
+ const signer = randomWallet('Should update account to new configuration')
+ const simpleConfig1 = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+ const config1 = v2.config.ConfigCoder.fromSimple(simpleConfig1)
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config: simpleConfig1,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ const signer2a = randomWallet('Should update account to new configuration 2')
+ const signer2b = randomWallet('Should update account to new configuration 3')
+
+ const simpleConfig2 = {
+ threshold: 4,
+ checkpoint: Math.floor(now() / 1000) + 1,
+ signers: [
+ {
+ address: signer2a.address,
+ weight: 2
+ },
+ {
+ address: signer2b.address,
+ weight: 2
+ }
+ ]
+ }
+
+ const config2 = v2.config.ConfigCoder.fromSimple(simpleConfig2)
+ await account.updateConfig(config2)
+
+ const status2 = await account.status(networks[0].chainId)
+ expect(status2.fullyMigrated).to.be.true
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.version).to.equal(2)
+ expect(status2.onChain.imageHash).to.deep.equal(v2.config.ConfigCoder.imageHashOf(config1))
+ expect(status2.imageHash).to.deep.equal(v2.config.ConfigCoder.imageHashOf(config2))
+ })
+
+ it('Should sign and validate a message without being deployed', async () => {
+ const signer = randomWallet('Should sign and validate a message without being deployed')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')
+
+ const valid = await account.reader(networks[0].chainId).isValidSignature(account.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.true
+ })
+
+ it('Should sign and validate a message without being deployed with nested account', async () => {
+ const { accountOuter } = await createNestedAccount('sign and validate nested undeployed', true, false)
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await accountOuter.signMessage(msg, networks[0].chainId, 'eip6492')
+
+ const valid = await accountOuter
+ .reader(networks[0].chainId)
+ .isValidSignature(accountOuter.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.true
+ })
+
+ it('Should sign and validate a message with undeployed nested account and signer', async () => {
+ // Testing that an undeployed account doesn't error as other signer can satisfy threshold
+ const signerA = randomWallet('Nested account signer A')
+ const signerB = randomWallet('Nested account signer B')
+
+ const configInner = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signerA.address, weight: 1 }]
+ }
+ const accountInner = await Account.new({
+ ...defaultArgs,
+ config: configInner,
+ orchestrator: new Orchestrator([signerA])
+ }) // Undeployed
+
+ const configOuter = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [
+ { address: accountInner.address, weight: 1 },
+ { address: signerB.address, weight: 1 }
+ ]
+ }
+ const accountOuter = await Account.new({
+ ...defaultArgs,
+ config: configOuter,
+ orchestrator: new Orchestrator([new AccountOrchestratorWrapper(accountInner), signerB])
+ })
+ await accountOuter.doBootstrap(networks[0].chainId)
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await accountOuter.signMessage(msg, networks[0].chainId)
+
+ const valid = await accountOuter
+ .reader(networks[0].chainId)
+ .isValidSignature(accountOuter.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.true
+ })
+
+ it('Should refuse to sign when not deployed', async () => {
+ const signer = randomWallet('Should refuse to sign when not deployed')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ const account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = account.signMessage(msg, networks[0].chainId, 'throw')
+
+ expect(sig).to.be.rejected
+ })
+
+ it('Should refuse to sign when not deployed (nested)', async () => {
+ const { accountOuter } = await createNestedAccount('refuse to sign undeployed', false, false)
+
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = accountOuter.signMessage(msg, networks[0].chainId, 'eip6492') // Note EIP-6492 throws when nested not deployed
+
+ expect(sig).to.be.rejected
+ })
+
+ describe('After upgrading', () => {
+ let account: Account
+
+ let signer1: ethers.Wallet
+ let signer2a: ethers.Wallet
+ let signer2b: ethers.Wallet
+ let signerIndex = 1
+
+ beforeEach(async () => {
+ signer1 = randomWallet(`After upgrading ${signerIndex++}`)
+ const simpleConfig1 = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000) + 1,
+ signers: [{ address: signer1.address, weight: 1 }]
+ }
+
+ account = await Account.new({
+ ...defaultArgs,
+ config: simpleConfig1,
+ orchestrator: new Orchestrator([signer1])
+ })
+ await getEth(account.address)
+
+ signer2a = randomWallet(`After upgrading ${signerIndex++}`)
+ signer2b = randomWallet(`After upgrading ${signerIndex++}`)
+
+ const simpleConfig2 = {
+ threshold: 4,
+ checkpoint: await account.status(0).then(s => BigInt(s.checkpoint) + 1n),
+ signers: [
+ {
+ address: signer2a.address,
+ weight: 2
+ },
+ {
+ address: signer2b.address,
+ weight: 2
+ }
+ ]
+ }
+
+ const config2 = v2.config.ConfigCoder.fromSimple(simpleConfig2)
+ await account.updateConfig(config2)
+ account.setOrchestrator(new Orchestrator([signer2a, signer2b]))
+ })
+
+ it('Should send a transaction', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should send a transaction on nested account', async () => {
+ const configOuter = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: account.address, weight: 1 }]
+ }
+ const accountOuter = await Account.new({
+ ...defaultArgs,
+ config: configOuter,
+ orchestrator: new Orchestrator([new AccountOrchestratorWrapper(account)])
+ })
+
+ await accountOuter.doBootstrap(networks[0].chainId)
+
+ const tx = await accountOuter.sendTransaction([], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const statusOuter = await accountOuter.status(networks[0].chainId)
+ expect(statusOuter.fullyMigrated).to.be.true
+ expect(statusOuter.onChain.deployed).to.be.true
+ expect(statusOuter.onChain.imageHash).to.equal(statusOuter.imageHash)
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should send a transaction on undeployed nested account', async () => {
+ const configOuter = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: account.address, weight: 1 }]
+ }
+ const accountOuter = await Account.new({
+ ...defaultArgs,
+ config: configOuter,
+ orchestrator: new Orchestrator([new AccountOrchestratorWrapper(account)])
+ })
+
+ await getEth(accountOuter.address)
+ const tx = await accountOuter.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should sign a message', async () => {
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await account.signMessage(msg, networks[0].chainId)
+
+ const canOnchainValidate = await account.status(networks[0].chainId).then(s => s.canOnchainValidate)
+ expect(canOnchainValidate).to.be.false
+ await account.doBootstrap(networks[0].chainId)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ account.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+
+ it('Should sign a message, already prefixed with EIP-191', async () => {
+ const msg = ethers.toUtf8Bytes('Hello World')
+
+ const prefixedMessage = concat([toUtf8Bytes(MessagePrefix), toUtf8Bytes(String(msg.length)), msg])
+
+ const sig = await account.signMessage(prefixedMessage, networks[0].chainId)
+
+ const canOnchainValidate = await account.status(networks[0].chainId).then(s => s.canOnchainValidate)
+ expect(canOnchainValidate).to.be.false
+ await account.doBootstrap(networks[0].chainId)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ account.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+
+ it('Should fail to use old signer', async () => {
+ account.setOrchestrator(new Orchestrator([signer1]))
+ const tx = account.sendTransaction([defaultTx], networks[0].chainId)
+ await expect(tx).to.be.rejected
+ })
+
+ it('Should send a transaction on a different network', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[1].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[1].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ describe('After reloading the account', () => {
+ beforeEach(async () => {
+ account = new Account({
+ ...defaultArgs,
+ address: account.address,
+ orchestrator: new Orchestrator([signer2a, signer2b])
+ })
+ await getEth(account.address)
+ })
+
+ it('Should send a transaction', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should sign a message', async () => {
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await account.signMessage(msg, networks[0].chainId)
+
+ const canOnchainValidate = await account.status(networks[0].chainId).then(s => s.canOnchainValidate)
+ expect(canOnchainValidate).to.be.false
+ await account.doBootstrap(networks[0].chainId)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ account.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+ })
+
+ describe('After updating the config again', () => {
+ let signer3a: ethers.Wallet
+ let signer3b: ethers.Wallet
+ let signer3c: ethers.Wallet
+ let signerIndex = 1
+
+ let config3: v2.config.WalletConfig
+
+ beforeEach(async () => {
+ signer3a = randomWallet(`After updating the config again ${signerIndex++}`)
+ signer3b = randomWallet(`After updating the config again ${signerIndex++}`)
+ signer3c = randomWallet(`After updating the config again ${signerIndex++}`)
+
+ const simpleConfig3 = {
+ threshold: 5,
+ checkpoint: await account.status(0).then(s => BigInt(s.checkpoint) + 1n),
+ signers: [
+ {
+ address: signer3a.address,
+ weight: 2
+ },
+ {
+ address: signer3b.address,
+ weight: 2
+ },
+ {
+ address: signer3c.address,
+ weight: 1
+ }
+ ]
+ }
+
+ config3 = v2.config.ConfigCoder.fromSimple(simpleConfig3)
+
+ await account.updateConfig(config3)
+ account.setOrchestrator(new Orchestrator([signer3a, signer3b, signer3c]))
+ })
+
+ it('Should update account status', async () => {
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.false
+ expect(status.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(config3))
+ expect(status.presignedConfigurations.length).to.equal(2)
+ })
+
+ it('Should send a transaction', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should sign a message', async () => {
+ const msg = ethers.toUtf8Bytes('Hello World')
+ const sig = await account.signMessage(msg, networks[0].chainId)
+
+ const canOnchainValidate = await account.status(networks[0].chainId).then(s => s.canOnchainValidate)
+ expect(canOnchainValidate).to.be.false
+ await account.doBootstrap(networks[0].chainId)
+
+ const status = await account.status(networks[0].chainId)
+ expect(status.onChain.imageHash).to.not.equal(status.imageHash)
+
+ const valid = await commons.EIP1271.isValidEIP1271Signature(
+ account.address,
+ ethers.hashMessage(msg),
+ sig,
+ networks[0].provider!
+ )
+
+ expect(valid).to.be.true
+ })
+ })
+
+ describe('After sending a transaction', () => {
+ beforeEach(async () => {
+ await account.sendTransaction([defaultTx], networks[0].chainId)
+ })
+
+ it('Should send a transaction in a different network', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[1].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status = await account.status(networks[1].chainId)
+ expect(status.fullyMigrated).to.be.true
+ expect(status.onChain.deployed).to.be.true
+ expect(status.onChain.imageHash).to.equal(status.imageHash)
+ })
+
+ it('Should send a second transaction', async () => {
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+ })
+
+ let signerIndex = 1
+ it('Should update the configuration again', async () => {
+ const signer2a = randomWallet(`Should update the configuration again ${signerIndex++}`)
+ const signer2b = randomWallet(`Should update the configuration again ${signerIndex++}`)
+ const signer2c = randomWallet(`Should update the configuration again ${signerIndex++}`)
+
+ const simpleConfig2 = {
+ threshold: 6,
+ checkpoint: await account.status(0).then(s => BigInt(s.checkpoint) + 1n),
+ signers: [
+ {
+ address: signer2a.address,
+ weight: 3
+ },
+ {
+ address: signer2b.address,
+ weight: 3
+ },
+ {
+ address: signer2c.address,
+ weight: 3
+ }
+ ]
+ }
+
+ const ogOnchainImageHash = await account.status(0).then(s => s.onChain.imageHash)
+ const imageHash1 = await account.status(0).then(s => s.imageHash)
+
+ const config2 = v2.config.ConfigCoder.fromSimple(simpleConfig2)
+ await account.updateConfig(config2)
+
+ const status1 = await account.status(networks[0].chainId)
+ const status2 = await account.status(networks[1].chainId)
+
+ expect(status1.fullyMigrated).to.be.true
+ expect(status1.onChain.deployed).to.be.true
+ expect(status1.onChain.imageHash).to.equal(imageHash1)
+ expect(status1.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(config2))
+ expect(status1.presignedConfigurations.length).to.equal(1)
+
+ expect(status2.fullyMigrated).to.be.true
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.imageHash).to.equal(ogOnchainImageHash)
+ expect(status2.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(config2))
+ expect(status2.presignedConfigurations.length).to.equal(2)
+ })
+ })
+ })
+ })
+
+ describe('Migrated wallet', () => {
+ it('Should migrate undeployed account', async () => {
+ // Old account may be an address that's not even deployed
+ const signer1 = randomWallet('Should migrate undeployed account')
+
+ const simpleConfig: commons.config.SimpleConfig = {
+ threshold: 1,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const config = v1.config.ConfigCoder.fromSimple(simpleConfig)
+ const configv2 = v2.config.ConfigCoder.fromSimple(simpleConfig)
+
+ const imageHash = v1.config.ConfigCoder.imageHashOf(config)
+ const address = commons.context.addressOf(contexts[1], imageHash)
+
+ // Sessions server MUST have information about the old wallet
+ // in production this is retrieved from SequenceUtils contract
+ await tracker.saveCounterfactualWallet({ config, context: [contexts[1]] })
+
+ // Importing the account should work!
+ const account = new Account({ ...defaultArgs, address, orchestrator: new Orchestrator([signer1]) })
+
+ const status = await account.status(0)
+ expect(status.fullyMigrated).to.be.false
+ expect(status.onChain.deployed).to.be.false
+ expect(status.onChain.imageHash).to.equal(imageHash)
+ expect(status.imageHash).to.equal(imageHash)
+ expect(status.version).to.equal(1)
+
+ // Sending a transaction should fail (not fully migrated)
+ await getEth(account.address)
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.rejected
+
+ // Should sign migration using the account
+ await account.signAllMigrations(c => c)
+
+ const status2 = await account.status(networks[0].chainId)
+ expect(status2.fullyMigrated).to.be.true
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.imageHash).to.equal(imageHash)
+ expect(status2.onChain.version).to.equal(1)
+ expect(status2.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status2.version).to.equal(2)
+
+ // Send a transaction
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status3 = await account.status(networks[0].chainId)
+ expect(status3.fullyMigrated).to.be.true
+ expect(status3.onChain.deployed).to.be.true
+ expect(status3.onChain.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status3.onChain.version).to.equal(2)
+ expect(status3.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status3.version).to.equal(2)
+
+ // Send another transaction on another chain
+ const tx2 = await account.sendTransaction([defaultTx], networks[1].chainId)
+ expect(tx2).to.not.be.undefined
+
+ const status4 = await account.status(networks[1].chainId)
+ expect(status4.fullyMigrated).to.be.true
+ expect(status4.onChain.deployed).to.be.true
+ expect(status4.onChain.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status4.onChain.version).to.equal(2)
+ expect(status4.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status4.version).to.equal(2)
+ })
+
+ it('Should migrate a half-deployed account', async () => {
+ // Old account created with 3 signers, and already deployed
+ // in one of the chains
+ const signer1 = randomWallet('Should migrate a half-deployed account')
+ const signer2 = randomWallet('Should migrate a half-deployed account 2')
+ const signer3 = randomWallet('Should migrate a half-deployed account 3')
+
+ const simpleConfig = {
+ threshold: 2,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 1
+ },
+ {
+ address: signer2.address,
+ weight: 1
+ },
+ {
+ address: signer3.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const config = v1.config.ConfigCoder.fromSimple(simpleConfig)
+ const imageHash = v1.config.ConfigCoder.imageHashOf(config)
+ const address = commons.context.addressOf(contexts[1], imageHash)
+
+ // Deploy the wallet on network 0
+ const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
+ await (networks[0].relayer! as Relayer).relay({
+ ...deployTx,
+ chainId: networks[0].chainId,
+ intent: {
+ id: '0x00',
+ wallet: address
+ }
+ })
+
+ // Feed all information to sequence-sessions
+ // (on prod this would be imported from SequenceUtils)
+ await tracker.saveCounterfactualWallet({ config, context: Object.values(contexts) })
+
+ // Importing the account should work!
+ const account = new Account({
+ ...defaultArgs,
+ address,
+ orchestrator: new Orchestrator([signer1, signer3])
+ })
+
+ // Status on network 0 should be deployed, network 1 not
+ // both should not be migrated, and use the original imageHash
+ const status1 = await account.status(networks[0].chainId)
+ expect(status1.fullyMigrated).to.be.false
+ expect(status1.onChain.deployed).to.be.true
+ expect(status1.onChain.imageHash).to.equal(imageHash)
+ expect(status1.onChain.version).to.equal(1)
+ expect(status1.imageHash).to.equal(imageHash)
+ expect(status1.version).to.equal(1)
+
+ const status2 = await account.status(networks[1].chainId)
+ expect(status2.fullyMigrated).to.be.false
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.imageHash).to.equal(imageHash)
+ expect(status2.onChain.version).to.equal(1)
+ expect(status2.imageHash).to.equal(imageHash)
+ expect(status2.version).to.equal(1)
+
+ // Signing transactions (on both networks) and signing messages should fail
+ await getEth(account.address)
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.rejected
+ await expect(account.sendTransaction([defaultTx], networks[1].chainId)).to.be.rejected
+ await expect(account.signMessage('0x00', networks[0].chainId)).to.be.rejected
+ await expect(account.signMessage('0x00', networks[1].chainId)).to.be.rejected
+
+ await account.signAllMigrations(c => c)
+
+ // Sign a transaction on network 0 and network 1, both should work
+ // and should take the wallet on-chain up to speed
+ const configv2 = v2.config.ConfigCoder.fromSimple(simpleConfig)
+
+ const tx1 = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx1).to.not.be.undefined
+ await tx1!.wait()
+
+ const status1b = await account.status(networks[0].chainId)
+ expect(status1b.fullyMigrated).to.be.true
+ expect(status1b.onChain.deployed).to.be.true
+ expect(status1b.onChain.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status1b.onChain.version).to.equal(2)
+ expect(status1b.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status1b.version).to.equal(2)
+
+ const tx2 = await account.sendTransaction([defaultTx], networks[1].chainId)
+ expect(tx2).to.not.be.undefined
+
+ const status2b = await account.status(networks[1].chainId)
+ expect(status2b).to.be.deep.equal(status1b)
+ })
+
+ it('Should migrate an upgraded wallet', async () => {
+ const signer1 = randomWallet('Should migrate an upgraded wallet')
+ const signer2 = randomWallet('Should migrate an upgraded wallet 2')
+ const signer3 = randomWallet('Should migrate an upgraded wallet 3')
+ const signer4 = randomWallet('Should migrate an upgraded wallet 4')
+
+ const simpleConfig1a = {
+ threshold: 3,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 2
+ },
+ {
+ address: signer2.address,
+ weight: 2
+ },
+ {
+ address: signer3.address,
+ weight: 2
+ }
+ ]
+ }
+
+ const config1a = v1.config.ConfigCoder.fromSimple(simpleConfig1a)
+ const imageHash1a = v1.config.ConfigCoder.imageHashOf(config1a)
+ const address = commons.context.addressOf(contexts[1], imageHash1a)
+
+ const simpleConfig1b = {
+ threshold: 3,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 2
+ },
+ {
+ address: signer2.address,
+ weight: 2
+ },
+ {
+ address: signer4.address,
+ weight: 2
+ }
+ ]
+ }
+
+ const config1b = v1.config.ConfigCoder.fromSimple(simpleConfig1b)
+ const imageHash1b = v1.config.ConfigCoder.imageHashOf(config1b)
+
+ // Update wallet to config 1b (on network 0)
+ const wallet = new Wallet({
+ coders: {
+ signature: v1.signature.SignatureCoder,
+ config: v1.config.ConfigCoder
+ },
+ context: contexts[1],
+ config: config1a,
+ chainId: networks[0].chainId,
+ address,
+ orchestrator: new Orchestrator([signer1, signer3]),
+ relayer: (networks[0].relayer as Relayer)!,
+ provider: networks[0].provider!
+ })
+
+ const utx = await wallet.buildUpdateConfigurationTransaction(config1b)
+ const signed = await wallet.signTransactionBundle(utx)
+ const decorated = await wallet.decorateTransactions(signed)
+ await (networks[0].relayer as Relayer).relay(decorated)
+
+ // Importing the account should work!
+ const account = new Account({
+ ...defaultArgs,
+ address,
+ orchestrator: new Orchestrator([signer1, signer3])
+ })
+
+ // Feed the tracker with all the data
+ await tracker.saveCounterfactualWallet({ config: config1a, context: [contexts[1]] })
+ await tracker.saveWalletConfig({ config: config1b })
+
+ // Status on network 0 should be deployed, network 1 not
+ // and the configuration on network 0 should be the B one
+ const status1 = await account.status(networks[0].chainId)
+ expect(status1.fullyMigrated).to.be.false
+ expect(status1.onChain.deployed).to.be.true
+ expect(status1.onChain.imageHash).to.equal(imageHash1b)
+ expect(status1.onChain.version).to.equal(1)
+ expect(status1.imageHash).to.equal(imageHash1b)
+
+ const status2 = await account.status(networks[1].chainId)
+ expect(status2.fullyMigrated).to.be.false
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.imageHash).to.equal(imageHash1a)
+ expect(status2.onChain.version).to.equal(1)
+ expect(status2.imageHash).to.equal(imageHash1a)
+
+ // Signing transactions (on both networks) and signing messages should fail
+ await getEth(account.address)
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.rejected
+ await expect(account.sendTransaction([defaultTx], networks[1].chainId)).to.be.rejected
+ await expect(account.signMessage('0x00', networks[0].chainId)).to.be.rejected
+ await expect(account.signMessage('0x00', networks[1].chainId)).to.be.rejected
+
+ // Sign all migrations should only have signers1 and 2
+ // so the migration should only be available on network 1 (the one not updated)
+ await account.signAllMigrations(c => c)
+
+ const config2a = v2.config.ConfigCoder.fromSimple(simpleConfig1a)
+ const config2b = v2.config.ConfigCoder.fromSimple(simpleConfig1b)
+ const imageHash2a = v2.config.ConfigCoder.imageHashOf(config2a)
+
+ const status1b = await account.status(networks[0].chainId)
+ expect(status1b.fullyMigrated).to.be.false
+ expect(status1b.onChain.deployed).to.be.true
+ expect(status1b.onChain.imageHash).to.equal(imageHash1b)
+ expect(status1b.onChain.version).to.equal(1)
+ expect(status1b.imageHash).to.equal(imageHash1b)
+ expect(status1b.version).to.equal(1)
+
+ const status2b = await account.status(networks[1].chainId)
+ expect(status2b.fullyMigrated).to.be.true
+ expect(status2b.onChain.deployed).to.be.false
+ expect(status2b.onChain.imageHash).to.equal(imageHash1a)
+ expect(status2b.onChain.version).to.equal(1)
+ expect(status2b.imageHash).to.equal(imageHash2a)
+ expect(status2b.version).to.equal(2)
+
+ // Sending a transaction should work for network 1
+ // but fail for network 0, same with signing messages
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.rejected
+ await expect(account.sendTransaction([defaultTx], networks[1].chainId)).to.be.fulfilled
+
+ await expect(account.signMessage('0x00', networks[0].chainId)).to.be.rejected
+ await expect(account.signMessage('0x00', networks[1].chainId)).to.be.fulfilled
+
+ // Signing another migration with signers1 and 2 should put both in sync
+ account.setOrchestrator(new Orchestrator([signer1, signer2]))
+ await account.signAllMigrations(c => c)
+
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.fulfilled
+ await expect(account.sendTransaction([defaultTx], networks[1].chainId)).to.be.fulfilled
+
+ await expect(account.signMessage('0x00', networks[0].chainId)).to.be.fulfilled
+ await expect(account.signMessage('0x00', networks[1].chainId)).to.be.fulfilled
+
+ const status1c = await account.status(networks[0].chainId)
+ const status2c = await account.status(networks[1].chainId)
+
+ expect(status1c.fullyMigrated).to.be.true
+ expect(status2c.fullyMigrated).to.be.true
+
+ // Configs are still different!
+ expect(status1c.imageHash).to.not.equal(status2c.imageHash)
+
+ const simpleConfig4 = {
+ threshold: 2,
+ checkpoint: 1,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 1
+ },
+ {
+ address: signer2.address,
+ weight: 1
+ },
+ {
+ address: signer4.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const config4 = v2.config.ConfigCoder.fromSimple(simpleConfig4)
+
+ await account.updateConfig(config4)
+
+ const status1d = await account.status(networks[0].chainId)
+ const status2d = await account.status(networks[1].chainId)
+
+ // Configs are now the same!
+ expect(status1d.imageHash).to.be.equal(status2d.imageHash)
+ })
+
+ it('Should edit the configuration during the migration', async () => {
+ // Old account may be an address that's not even deployed
+ const signer1 = randomWallet('Should edit the configuration during the migration')
+ const signer2 = randomWallet('Should edit the configuration during the migration 2')
+
+ const simpleConfig1 = {
+ threshold: 1,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const simpleConfig2 = {
+ threshold: 1,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer2.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const config = v1.config.ConfigCoder.fromSimple(simpleConfig1)
+ const configv2 = v2.config.ConfigCoder.fromSimple(simpleConfig2)
+
+ const imageHash = v1.config.ConfigCoder.imageHashOf(config)
+ const address = commons.context.addressOf(contexts[1], imageHash)
+
+ // Sessions server MUST have information about the old wallet
+ // in production this is retrieved from SequenceUtils contract
+ await tracker.saveCounterfactualWallet({ config, context: [contexts[1]] })
+
+ // Importing the account should work!
+ const orchestrator = new Orchestrator([signer1])
+ const account = new Account({ ...defaultArgs, address, orchestrator: orchestrator })
+
+ const status = await account.status(0)
+ expect(status.fullyMigrated).to.be.false
+ expect(status.onChain.deployed).to.be.false
+ expect(status.onChain.imageHash).to.equal(imageHash)
+ expect(status.imageHash).to.equal(imageHash)
+ expect(status.version).to.equal(1)
+
+ // Sending a transaction should fail (not fully migrated)
+ await getEth(account.address)
+ await expect(account.sendTransaction([defaultTx], networks[0].chainId)).to.be.rejected
+
+ // Should sign migration using the account
+ await account.signAllMigrations(c => {
+ expect(v1.config.ConfigCoder.imageHashOf(c as any)).to.equal(v1.config.ConfigCoder.imageHashOf(config))
+ return configv2
+ })
+
+ const status2 = await account.status(networks[0].chainId)
+ expect(status2.fullyMigrated).to.be.true
+ expect(status2.onChain.deployed).to.be.false
+ expect(status2.onChain.imageHash).to.equal(imageHash)
+ expect(status2.onChain.version).to.equal(1)
+ expect(status2.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status2.version).to.equal(2)
+
+ // Send a transaction
+ orchestrator.setSigners([signer2])
+ const tx = await account.sendTransaction([defaultTx], networks[0].chainId)
+ expect(tx).to.not.be.undefined
+
+ const status3 = await account.status(networks[0].chainId)
+ expect(status3.fullyMigrated).to.be.true
+ expect(status3.onChain.deployed).to.be.true
+ expect(status3.onChain.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status3.onChain.version).to.equal(2)
+ expect(status3.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status3.version).to.equal(2)
+
+ // Send another transaction on another chain
+ const tx2 = await account.sendTransaction([defaultTx], networks[1].chainId)
+ expect(tx2).to.not.be.undefined
+
+ const status4 = await account.status(networks[1].chainId)
+ expect(status4.fullyMigrated).to.be.true
+ expect(status4.onChain.deployed).to.be.true
+ expect(status4.onChain.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status4.onChain.version).to.equal(2)
+ expect(status4.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
+ expect(status4.version).to.equal(2)
+ })
+
+ context('Signing messages', async () => {
+ context('After migrating', async () => {
+ let account: Account
+ let imageHash: string
+
+ beforeEach(async () => {
+ // Old account may be an address that's not even deployed
+ const signer1 = randomWallet(
+ // @ts-ignore
+ 'Signing messages - After migrating' + account?.address ?? '' // Append prev address to entropy to avoid collisions
+ )
+
+ const simpleConfig = {
+ threshold: 1,
+ checkpoint: 0,
+ signers: [
+ {
+ address: signer1.address,
+ weight: 1
+ }
+ ]
+ }
+
+ const config = v1.config.ConfigCoder.fromSimple(simpleConfig)
+ imageHash = v1.config.ConfigCoder.imageHashOf(config)
+ const address = commons.context.addressOf(contexts[1], imageHash)
+
+ // Sessions server MUST have information about the old wallet
+ // in production this is retrieved from SequenceUtils contract
+ await tracker.saveCounterfactualWallet({ config, context: [contexts[1]] })
+
+ account = new Account({ ...defaultArgs, address, orchestrator: new Orchestrator([signer1]) })
+
+ // Should sign migration using the account
+ await account.signAllMigrations(c => c)
+ })
+
+ it('Should validate a message signed by undeployed migrated wallet', async () => {
+ const msg = ethers.toUtf8Bytes('I like that you are reading our tests')
+ const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')
+
+ const valid = await account.reader(networks[0].chainId).isValidSignature(account.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.true
+ })
+
+ it('Should reject a message signed by undeployed migrated wallet (if set the throw)', async () => {
+ const msg = ethers.toUtf8Bytes('I do not know what to write here anymore')
+ const sig = account.signMessage(msg, networks[0].chainId, 'throw')
+
+ await expect(sig).to.be.rejected
+ })
+
+ it('Should return an invalid signature by undeployed migrated wallet (if set to ignore)', async () => {
+ const msg = ethers.toUtf8Bytes('Sending a hug')
+ const sig = await account.signMessage(msg, networks[0].chainId, 'ignore')
+
+ const valid = await account.reader(networks[0].chainId).isValidSignature(account.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.false
+ })
+
+ it('Should validate a message signed by deployed migrated wallet (deployed with v1)', async () => {
+ const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
+ await signer1
+ .sendTransaction({
+ to: deployTx.entrypoint,
+ data: commons.transaction.encodeBundleExecData(deployTx)
+ })
+ .then(t => t.wait())
+
+ expect(await networks[0].provider!.getCode(account.address).then(c => ethers.getBytes(c).length)).to.not.equal(0)
+
+ const msg = ethers.toUtf8Bytes('Everything seems to be working fine so far')
+ const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')
+
+ const valid = await account.reader(networks[0].chainId).isValidSignature(account.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.true
+ })
+
+ it('Should fail to sign a message signed by deployed migrated wallet (deployed with v1) if throw', async () => {
+ const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
+ await signer1
+ .sendTransaction({
+ to: deployTx.entrypoint,
+ data: commons.transaction.encodeBundleExecData(deployTx)
+ })
+ .then(tx => tx.wait())
+
+ expect(await networks[0].provider!.getCode(account.address).then(c => ethers.getBytes(c).length)).to.not.equal(0)
+
+ const msg = ethers.toUtf8Bytes('Everything seems to be working fine so far')
+ const sig = account.signMessage(msg, networks[0].chainId, 'throw')
+ expect(sig).to.be.rejected
+ })
+
+ it('Should return an invalid signature by deployed migrated wallet (deployed with v1) if ignore', async () => {
+ const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
+ await signer1
+ .sendTransaction({
+ to: deployTx.entrypoint,
+ data: commons.transaction.encodeBundleExecData(deployTx)
+ })
+ .then(tx => tx.wait())
+
+ expect(await networks[0].provider!.getCode(account.address).then(c => ethers.getBytes(c).length)).to.not.equal(0)
+
+ const msg = ethers.toUtf8Bytes('Everything seems to be working fine so far')
+ const sig = await account.signMessage(msg, networks[0].chainId, 'ignore')
+ const valid = await account.reader(networks[0].chainId).isValidSignature(account.address, ethers.hashMessage(msg), sig)
+
+ expect(valid).to.be.false
+ })
+ })
+ })
+ })
+
+ describe('Nonce selection', async () => {
+ let signer: ethers.Wallet
+ let account: Account
+
+ let getNonce: (response: ethers.TransactionResponse) => { space: bigint; nonce: bigint }
+
+ before(async () => {
+ const mainModule = new ethers.Interface(walletContracts.mainModule.abi)
+
+ getNonce = ({ data }) => {
+ const [_, encoded] = mainModule.decodeFunctionData('execute', data)
+ const [space, nonce] = commons.transaction.decodeNonce(encoded)
+ return { space, nonce }
+ }
+
+ signer = randomWallet('Nonce selection')
+
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+
+ account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([signer])
+ })
+
+ // use a deployed account, otherwise we end up testing the decorated bundle nonce
+ const response = await account.sendTransaction([], networks[0].chainId)
+ await response?.wait()
+
+ await getEth(account.address, signer1)
+ await getEth(account.address, signer2)
+ })
+
+ it('Should use explicitly set nonces', async () => {
+ let response = await account.sendTransaction(
+ { to: await signer1.getAddress(), value: 1 },
+ networks[0].chainId,
+ undefined,
+ undefined,
+ undefined,
+ { nonceSpace: 6492 }
+ )
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ let { space, nonce } = getNonce(response)
+
+ expect(space === 6492n).to.be.true
+ expect(nonce === 0n).to.be.true
+
+ await response.wait()
+
+ response = await account.sendTransaction(
+ { to: await signer1.getAddress(), value: 1 },
+ networks[0].chainId,
+ undefined,
+ undefined,
+ undefined,
+ { nonceSpace: 6492 }
+ )
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ const encoded = getNonce(response)
+ space = encoded.space
+ nonce = encoded.nonce
+
+ expect(space === 6492n).to.be.true
+ expect(nonce === 1n).to.be.true
+ })
+
+ it('Should select random nonces by default', async () => {
+ let response = await account.sendTransaction({ to: await signer1.getAddress(), value: 1 }, networks[0].chainId)
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ const { space: firstSpace, nonce: firstNonce } = getNonce(response)
+
+ expect(firstSpace === 0n).to.be.false
+ expect(firstNonce === 0n).to.be.true
+
+ // not necessary, parallel execution is ok:
+ // await response.wait()
+
+ response = await account.sendTransaction({ to: await signer1.getAddress(), value: 1 }, networks[0].chainId)
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ const { space: secondSpace, nonce: secondNonce } = getNonce(response)
+
+ expect(secondSpace === 0n).to.be.false
+ expect(secondNonce === 0n).to.be.true
+
+ expect(secondSpace === firstSpace).to.be.false
+ })
+
+ it('Should respect the serial option', async () => {
+ let response = await account.sendTransaction(
+ { to: await signer1.getAddress(), value: 1 },
+ networks[0].chainId,
+ undefined,
+ undefined,
+ undefined,
+ { serial: true }
+ )
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ let { space, nonce } = getNonce(response)
+
+ expect(space === 0n).to.be.true
+ expect(nonce === 0n).to.be.true
+
+ await response.wait()
+
+ response = await account.sendTransaction(
+ { to: await signer1.getAddress(), value: 1 },
+ networks[0].chainId,
+ undefined,
+ undefined,
+ undefined,
+ { serial: true }
+ )
+ if (!response) {
+ throw new Error('expected response')
+ }
+
+ const encoded = getNonce(response)
+ space = encoded.space
+ nonce = encoded.nonce
+
+ expect(space === 0n).to.be.true
+ expect(nonce === 1n).to.be.true
+ })
+ })
+})
+
+let nowCalls = 0
+export function now(): number {
+ if (deterministic) {
+ return Date.parse('2023-02-14T00:00:00.000Z') + 1000 * nowCalls++
+ } else {
+ return Date.now()
+ }
+}
+
+export function randomWallet(entropy: number | string): ethers.Wallet {
+ return new ethers.Wallet(ethers.hexlify(randomBytes(32, entropy)))
+}
+
+export function randomFraction(entropy: number | string): number {
+ const bytes = randomBytes(7, entropy)
+ bytes[0] &= 0x1f
+ return bytes.reduce((sum, byte) => 256 * sum + byte) / Number.MAX_SAFE_INTEGER
+}
+
+export function randomBytes(length: number, entropy: number | string): Uint8Array {
+ if (deterministic) {
+ let bytes = ''
+ while (bytes.length < 2 * length) {
+ bytes += ethers.id(`${bytes}${entropy}`).slice(2)
+ }
+ return ethers.getBytes(`0x${bytes.slice(0, 2 * length)}`)
+ } else {
+ return ethers.randomBytes(length)
+ }
+}
diff --git a/packages/account/tests/signer.spec.ts b/packages/account/tests/signer.spec.ts
new file mode 100644
index 0000000000..89c4475dbd
--- /dev/null
+++ b/packages/account/tests/signer.spec.ts
@@ -0,0 +1,896 @@
+import { commons, v1, v2 } from '@0xsequence/core'
+import { migrator } from '@0xsequence/migration'
+import { NetworkConfig } from '@0xsequence/network'
+import { FeeOption, FeeQuote, LocalRelayer, LocalRelayerOptions, Relayer, proto } from '@0xsequence/relayer'
+import { tracker, trackers } from '@0xsequence/sessions'
+import { Orchestrator } from '@0xsequence/signhub'
+import * as utils from '@0xsequence/tests'
+import { Wallet } from '@0xsequence/wallet'
+import * as chai from 'chai'
+import chaiAsPromised from 'chai-as-promised'
+import { ethers } from 'ethers'
+import hardhat from 'hardhat'
+
+import { Account } from '../src/account'
+import { now, randomWallet } from './account.spec'
+import { createERC20 } from '@0xsequence/tests/src/tokens/erc20'
+import { parseEther } from '@0xsequence/utils'
+
+const { expect } = chai.use(chaiAsPromised)
+
+describe('Account signer', () => {
+ let provider1: ethers.BrowserProvider
+ let provider2: ethers.JsonRpcProvider
+
+ let signer1: ethers.Signer
+ let signer2: ethers.Signer
+
+ let contexts: commons.context.VersionedContext
+ let networks: NetworkConfig[]
+
+ let tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+
+ let defaultArgs: {
+ contexts: commons.context.VersionedContext
+ networks: NetworkConfig[]
+ tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+ }
+
+ before(async () => {
+ provider1 = new ethers.BrowserProvider(hardhat.network.provider as any, undefined, { cacheTimeout: -1 })
+ provider2 = new ethers.JsonRpcProvider('http://127.0.0.1:7048', undefined, { cacheTimeout: -1 })
+
+ signer1 = await provider1.getSigner()
+ signer2 = await provider2.getSigner()
+
+ // TODO: Implement migrations on local config tracker
+ tracker = new trackers.local.LocalConfigTracker(provider1) as any
+
+ networks = [
+ {
+ chainId: 31337,
+ name: 'hardhat',
+ provider: provider1,
+ rpcUrl: '',
+ relayer: new LocalRelayer(signer1),
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ },
+ {
+ chainId: 31338,
+ name: 'hardhat2',
+ provider: provider2,
+ rpcUrl: 'http://127.0.0.1:7048',
+ relayer: new LocalRelayer(signer2),
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ }
+ ]
+
+ contexts = await utils.context.deploySequenceContexts(signer1)
+ const context2 = await utils.context.deploySequenceContexts(signer2)
+
+ expect(contexts).to.deep.equal(context2)
+
+ defaultArgs = {
+ contexts,
+ networks,
+ tracker
+ }
+ })
+
+ describe('with new account', () => {
+ let account: Account
+ let config: any
+ let accountSigner: ethers.Wallet
+
+ beforeEach(async () => {
+ accountSigner = randomWallet('Should create a new account')
+ config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: accountSigner.address, weight: 1 }]
+ }
+
+ account = await Account.new({
+ ...defaultArgs,
+ config,
+ orchestrator: new Orchestrator([accountSigner])
+ })
+ })
+ ;[31337, 31338].map((chainId: number) => {
+ context(`for chain ${chainId}`, () => {
+ it('should send transaction', async () => {
+ const signer = account.getSigner(chainId)
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ })
+
+ it('should send batch transaction', async () => {
+ const signer = account.getSigner(chainId)
+
+ const res = await signer.sendTransaction([
+ {
+ to: ethers.Wallet.createRandom().address
+ },
+ {
+ to: ethers.Wallet.createRandom().address
+ }
+ ])
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ })
+
+ it('should send two transactions (one has deploy)', async () => {
+ const signer = account.getSigner(chainId)
+
+ expect(await signer.provider.getCode(account.address)).to.equal('0x')
+
+ await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(await signer.provider.getCode(account.address)).to.not.equal('0x')
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ })
+
+ it('should fail to sign message because not deployed', async () => {
+ const signer = account.getSigner(chainId)
+
+ await expect(signer.signMessage(ethers.randomBytes(32))).to.be.rejectedWith('Wallet cannot validate onchain')
+ })
+
+ it('should sign message after deployment', async () => {
+ const signer = account.getSigner(chainId)
+
+ await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(await signer.provider.getCode(account.address)).to.not.equal('0x')
+
+ const signature = await signer.signMessage(ethers.randomBytes(32))
+ expect(signature).to.exist
+ expect(signature).to.not.equal('0x')
+ })
+
+ it('should sign a message (undeployed) when using EIP6492', async () => {
+ const signer = account.getSigner(chainId, { cantValidateBehavior: 'eip6492' })
+
+ const signature = await signer.signMessage(ethers.randomBytes(32))
+ expect(signature).to.exist
+ expect(signature).to.not.equal('0x')
+ })
+
+ it('should return account address', async () => {
+ expect(account.address).to.equal(await account.getSigner(chainId).getAddress())
+ })
+
+ it('should return chainId', async () => {
+ expect(chainId).to.equal(await account.getSigner(chainId).getChainId())
+ })
+
+ it('should call select fee even if there is no fee', async () => {
+ let callsToSelectFee = 0
+
+ const tx = {
+ to: ethers.Wallet.createRandom().address
+ }
+
+ const signer = account.getSigner(chainId, {
+ selectFee: async (txs: any, options: FeeOption[]) => {
+ callsToSelectFee++
+ expect(txs).to.deep.equal(tx)
+ expect(options).to.deep.equal([])
+ return undefined
+ }
+ })
+
+ const res = await signer.sendTransaction(tx)
+
+ expect(callsToSelectFee).to.equal(1)
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ })
+
+ describe('select fee', () => {
+ let account: never
+ let getAccount: (feeOptions: FeeOption[], feeQuote: FeeQuote) => Promise
+
+ beforeEach(async () => {
+ class LocalRelayerWithFee extends LocalRelayer {
+ public feeOptions: FeeOption[]
+ public quote: FeeQuote
+
+ constructor(
+ options: LocalRelayerOptions | ethers.Signer,
+ feeOptions: FeeOption[],
+ quote: FeeQuote
+ ) {
+ super(options)
+ this.feeOptions = feeOptions
+ this.quote = quote
+ }
+
+ async getFeeOptions(
+ _address: string,
+ ..._transactions: commons.transaction.Transaction[]
+ ): Promise<{ options: FeeOption[] }> {
+ return { options: this.feeOptions, quote: this.quote } as any
+ }
+
+ async getFeeOptionsRaw(
+ _entrypoint: string,
+ _data: ethers.BytesLike,
+ _options?: { simulate?: boolean }
+ ): Promise<{ options: FeeOption[] }> {
+ return { options: this.feeOptions, quote: this.quote } as any
+ }
+
+ async gasRefundOptions(
+ _address: string,
+ ..._transactions: commons.transaction.Transaction[]
+ ): Promise {
+ return this.feeOptions
+ }
+
+ async relay(
+ signedTxs: commons.transaction.IntendedTransactionBundle,
+ quote?: FeeQuote | undefined,
+ waitForReceipt?: boolean | undefined
+ ): Promise> {
+ expect(quote).to.equal(this.quote)
+ return super.relay(signedTxs, quote, waitForReceipt)
+ }
+ }
+
+ getAccount = async (feeOptions: FeeOption[], feeQuote: FeeQuote) => {
+ return Account.new({
+ ...defaultArgs,
+ networks: defaultArgs.networks.map(n => {
+ return {
+ ...n,
+ relayer: new LocalRelayerWithFee(chainId === 31337 ? signer1 : signer2, feeOptions, feeQuote)
+ }
+ }),
+ config,
+ orchestrator: new Orchestrator([accountSigner])
+ })
+ }
+ })
+
+ it('should automatically select native fee', async () => {
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'native',
+ symbol: 'ETH',
+ type: proto.FeeTokenType.UNKNOWN,
+ logoURL: ''
+ },
+ to: ethers.Wallet.createRandom().address,
+ value: '12',
+ gasLimit: 100000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId)
+
+ await (chainId === 31337 ? signer1 : signer2).sendTransaction({
+ to: account.address,
+ value: 12
+ })
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ })
+
+ it('should reject if balance is not enough', async () => {
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'native',
+ symbol: 'ETH',
+ type: proto.FeeTokenType.UNKNOWN,
+ logoURL: ''
+ },
+ to: ethers.Wallet.createRandom().address,
+ value: parseEther('12').toString(),
+ gasLimit: 100000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId)
+
+ await (chainId === 31337 ? signer1 : signer2).sendTransaction({
+ to: account.address,
+ value: 11
+ })
+
+ const res = signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.be.rejectedWith('No fee option available - not enough balance')
+ })
+
+ it('should automatically select ERC20 fee', async () => {
+ const token = await createERC20(chainId === 31337 ? signer1 : signer2, 'Test Token', 'TEST', 18)
+
+ const recipient = ethers.Wallet.createRandom().address
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'TEST',
+ symbol: 'TEST',
+ type: proto.FeeTokenType.ERC20_TOKEN,
+ logoURL: '',
+ contractAddress: await token.getAddress()
+ },
+ to: recipient,
+ value: parseEther('250').toString(),
+ gasLimit: 400000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId)
+
+ await token.getFunction('mint')(account.address, parseEther('6000'))
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ expect(await token.getFunction('balanceOf')(recipient)).to.equal(parseEther('250'))
+ })
+
+ it('should reject ERC20 fee if not enough balance', async () => {
+ const token = await createERC20(chainId === 31337 ? signer1 : signer2, 'Test Token', 'TEST', 18)
+
+ const recipient = ethers.Wallet.createRandom().address
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'TEST',
+ symbol: 'TEST',
+ type: proto.FeeTokenType.ERC20_TOKEN,
+ logoURL: '',
+ contractAddress: await token.getAddress()
+ },
+ to: recipient,
+ value: parseEther('250').toString(),
+ gasLimit: 400000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId)
+
+ const res = signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.be.rejectedWith('No fee option available - not enough balance')
+ })
+
+ it('should automatically select ERC20 fee if user has no ETH', async () => {
+ const token = await createERC20(chainId === 31337 ? signer1 : signer2, 'Test Token', 'TEST', 18)
+
+ const recipient = ethers.Wallet.createRandom().address
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'native',
+ symbol: 'ETH',
+ type: proto.FeeTokenType.UNKNOWN,
+ logoURL: ''
+ },
+ to: recipient,
+ value: parseEther('12').toString(),
+ gasLimit: 100000
+ },
+ {
+ token: {
+ chainId,
+ name: 'TEST',
+ symbol: 'TEST',
+ type: proto.FeeTokenType.ERC20_TOKEN,
+ logoURL: '',
+ contractAddress: await token.getAddress()
+ },
+ to: recipient,
+ value: parseEther('11').toString(),
+ gasLimit: 400000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId)
+
+ await token.getFunction('mint')(account.address, parseEther('11'))
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ expect(await token.getFunction('balanceOf')(recipient)).to.equal(parseEther('11'))
+ })
+
+ it('should select fee using callback (first option)', async () => {
+ const recipient = ethers.Wallet.createRandom().address
+
+ const token = await createERC20(chainId === 31337 ? signer1 : signer2, 'Test Token', 'TEST', 18)
+
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'native',
+ symbol: 'ETH',
+ type: proto.FeeTokenType.UNKNOWN,
+ logoURL: ''
+ },
+ to: recipient,
+ value: '5',
+ gasLimit: 100000
+ },
+ {
+ token: {
+ chainId,
+ name: 'TEST',
+ symbol: 'TEST',
+ type: proto.FeeTokenType.ERC20_TOKEN,
+ logoURL: '',
+ contractAddress: await token.getAddress()
+ },
+ to: recipient,
+ value: parseEther('11').toString(),
+ gasLimit: 400000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId, {
+ selectFee: async (_txs: any, options: FeeOption[]) => {
+ expect(options).to.deep.equal(feeOptions)
+ return options[0]
+ }
+ })
+
+ await (chainId === 31337 ? signer1 : signer2).sendTransaction({
+ to: account.address,
+ value: 5
+ })
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ expect(await signer.provider.getBalance(recipient)).to.equal(5n)
+ expect(await token.getFunction('balanceOf')(recipient)).to.equal(parseEther('0'))
+ })
+
+ it('should select fee using callback (second option)', async () => {
+ const recipient = ethers.Wallet.createRandom().address
+
+ const token = await createERC20(chainId === 31337 ? signer1 : signer2, 'Test Token', 'TEST', 18)
+
+ const feeOptions: FeeOption[] = [
+ {
+ token: {
+ chainId,
+ name: 'native',
+ symbol: 'ETH',
+ type: proto.FeeTokenType.UNKNOWN,
+ logoURL: ''
+ },
+ to: recipient,
+ value: '5',
+ gasLimit: 100000
+ },
+ {
+ token: {
+ chainId,
+ name: 'TEST',
+ symbol: 'TEST',
+ type: proto.FeeTokenType.ERC20_TOKEN,
+ logoURL: '',
+ contractAddress: await token.getAddress()
+ },
+ to: recipient,
+ value: parseEther('11').toString(),
+ gasLimit: 400000
+ }
+ ]
+
+ const feeQuote: FeeQuote = {
+ _tag: 'FeeQuote',
+ _quote: ethers.randomBytes(99)
+ }
+
+ const account = await getAccount(feeOptions, feeQuote)
+ const signer = account.getSigner(chainId, {
+ selectFee: async (_txs: any, options: FeeOption[]) => {
+ expect(options).to.deep.equal(feeOptions)
+ return options[1]
+ }
+ })
+
+ await token.getFunction('mint')(account.address, parseEther('11'))
+
+ const res = await signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ expect(res).to.exist
+ expect(res.hash).to.exist
+
+ expect(await signer.provider.getTransaction(res.hash)).to.exist
+ expect(await signer.provider.getBalance(recipient)).to.equal(0n)
+ expect(await token.getFunction('balanceOf')(recipient)).to.equal(parseEther('11'))
+ })
+ })
+ })
+
+ it('should send transactions on multiple nonce spaces one by one', async () => {
+ const signer1 = account.getSigner(chainId, { nonceSpace: '0x01' })
+ const signer2 = account.getSigner(chainId, { nonceSpace: 2 })
+ const randomSpace = BigInt(ethers.hexlify(ethers.randomBytes(12)))
+ const signer3 = account.getSigner(chainId, {
+ nonceSpace: randomSpace
+ })
+ const signer4 = account.getSigner(chainId, { nonceSpace: '0x04' })
+ const signer5 = account.getSigner(chainId, { nonceSpace: '0xffffffffffffffffffffffffffffffffffffffff' })
+
+ await signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ await signer2.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ await signer3.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ await signer4.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ await signer5.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ // Should have used all spaces
+ const wallet = account.walletForStatus(chainId, await account.status(chainId))
+
+ const nonceSpace1 = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1.toString()).to.equal('1')
+
+ const nonceSpace2 = await wallet.getNonce(2).then(r => BigInt(r))
+ expect(nonceSpace2.toString()).to.equal('1')
+
+ const nonceSpace3 = await wallet.getNonce(randomSpace).then(r => BigInt(r))
+ expect(nonceSpace3.toString()).to.equal('1')
+
+ const nonceSpace4 = await wallet.getNonce('0x04').then(r => BigInt(r))
+ expect(nonceSpace4.toString()).to.equal('1')
+
+ const nonceSpace5 = await wallet.getNonce('0xffffffffffffffffffffffffffffffffffffffff').then(r => BigInt(r))
+ expect(nonceSpace5.toString()).to.equal('1')
+
+ // Unused space should have nonce 0
+ const nonceSpace6 = await wallet.getNonce('0x06').then(r => BigInt(r))
+ expect(nonceSpace6.toString()).to.equal('0')
+
+ // Using a space should consume it
+ await signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ const nonceSpace1b = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1b.toString()).to.equal('2')
+ })
+
+ // Skip if using external network (chainId 31338)
+ // it randomly fails using node 20, it does not seem to be a bug
+ // on sequence.js, instead the external node returns empty data when calling
+ // `getNonce()`, when it should return a value
+ ;(chainId === 31338 ? describe.skip : describe)('multiple nonce spaces', async () => {
+ it('should send transactions on multiple nonce spaces at once', async () => {
+ const signer1 = account.getSigner(chainId, { nonceSpace: '0x01' })
+ const signer2 = account.getSigner(chainId, { nonceSpace: 2 })
+ const randomSpace = BigInt(ethers.hexlify(ethers.randomBytes(12)))
+ const signer3 = account.getSigner(chainId, {
+ nonceSpace: randomSpace
+ })
+ const signer4 = account.getSigner(chainId, { nonceSpace: '0x04' })
+ const signer5 = account.getSigner(chainId, { nonceSpace: '0xffffffffffffffffffffffffffffffffffffffff' })
+
+ const results = await Promise.all([
+ signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer2.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer3.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer4.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer5.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ ])
+
+ expect(results).to.have.lengthOf(5)
+ expect(results[0]).to.exist
+ expect(results[0].hash).to.exist
+ expect(results[1]).to.exist
+ expect(results[1].hash).to.exist
+ expect(results[2]).to.exist
+ expect(results[2].hash).to.exist
+ expect(results[3]).to.exist
+ expect(results[3].hash).to.exist
+ expect(results[4]).to.exist
+ expect(results[4].hash).to.exist
+
+ // hashes should be different
+ for (let i = 0; i < results.length; i++) {
+ for (let j = i + 1; j < results.length; j++) {
+ expect(results[i].hash).to.not.equal(results[j].hash)
+ }
+ }
+
+ // Should have used all spaces
+ const wallet = account.walletForStatus(chainId, await account.status(chainId))
+
+ const nonceSpace1 = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1.toString()).to.equal('1')
+
+ const nonceSpace2 = await wallet.getNonce(2).then(r => BigInt(r))
+ expect(nonceSpace2.toString()).to.equal('1')
+
+ const nonceSpace3 = await wallet.getNonce(randomSpace).then(r => BigInt(r))
+ expect(nonceSpace3.toString()).to.equal('1')
+
+ const nonceSpace4 = await wallet.getNonce('0x04').then(r => BigInt(r))
+ expect(nonceSpace4.toString()).to.equal('1')
+
+ const nonceSpace5 = await wallet.getNonce('0xffffffffffffffffffffffffffffffffffffffff').then(r => BigInt(r))
+ expect(nonceSpace5.toString()).to.equal('1')
+
+ // Unused space should have nonce 0
+ const nonceSpace6 = await wallet.getNonce('0x06').then(r => BigInt(r))
+ expect(nonceSpace6.toString()).to.equal('0')
+
+ // Using a space should consume it
+ await signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ const nonceSpace1b = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1b.toString()).to.equal('2')
+ })
+
+ it('should send 100 parallel transactions using different spaces', async () => {
+ const signers = new Array(100).fill(0).map(() =>
+ account.getSigner(chainId, {
+ nonceSpace: BigInt(ethers.hexlify(ethers.randomBytes(12)))
+ })
+ )
+
+ // Send a random transaction on each one of them
+ await Promise.all(
+ signers.map(signer =>
+ signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ )
+ )
+
+ // Send another
+ await Promise.all(
+ signers.map(signer =>
+ signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ )
+ )
+
+ /// ... and another
+ await Promise.all(
+ signers.map(signer =>
+ signer.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ )
+ )
+ })
+
+ it('should send multiple transactions on multiple nonce spaces at once', async () => {
+ const signer1 = account.getSigner(chainId, { nonceSpace: '0x01' })
+ const signer2 = account.getSigner(chainId, { nonceSpace: 2 })
+ const randomSpace = BigInt(ethers.hexlify(ethers.randomBytes(12)))
+
+ const signer3 = account.getSigner(chainId, {
+ nonceSpace: randomSpace
+ })
+ const signer4 = account.getSigner(chainId, { nonceSpace: '0x04' })
+ const signer5 = account.getSigner(chainId, { nonceSpace: '0xffffffffffffffffffffffffffffffffffffffff' })
+
+ await Promise.all([
+ signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer2.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer3.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer4.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer5.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ ])
+
+ const results = await Promise.all([
+ signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer2.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer3.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer4.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ }),
+ signer5.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+ ])
+
+ expect(results).to.have.lengthOf(5)
+ expect(results[0]).to.exist
+ expect(results[0].hash).to.exist
+ expect(results[1]).to.exist
+ expect(results[1].hash).to.exist
+ expect(results[2]).to.exist
+ expect(results[2].hash).to.exist
+ expect(results[3]).to.exist
+ expect(results[3].hash).to.exist
+ expect(results[4]).to.exist
+ expect(results[4].hash).to.exist
+
+ // hashes should be different
+ for (let i = 0; i < results.length; i++) {
+ for (let j = i + 1; j < results.length; j++) {
+ expect(results[i].hash).to.not.equal(results[j].hash)
+ }
+ }
+
+ // Should have used all spaces
+ const wallet = account.walletForStatus(chainId, await account.status(chainId))
+
+ const nonceSpace2 = await wallet.getNonce(2).then(r => BigInt(r))
+ expect(nonceSpace2.toString()).to.equal('2')
+
+ const nonceSpace1 = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1.toString()).to.equal('2')
+
+ const nonceSpace3 = await wallet.getNonce(randomSpace).then(r => BigInt(r))
+ expect(nonceSpace3.toString()).to.equal('2')
+
+ const nonceSpace4 = await wallet.getNonce('0x04').then(r => BigInt(r))
+ expect(nonceSpace4.toString()).to.equal('2')
+
+ const nonceSpace5 = await wallet.getNonce('0xffffffffffffffffffffffffffffffffffffffff').then(r => BigInt(r))
+ expect(nonceSpace5.toString()).to.equal('2')
+
+ // Unused space should have nonce 0
+ const nonceSpace6 = await wallet.getNonce('0x06').then(r => BigInt(r))
+ expect(nonceSpace6.toString()).to.equal('0')
+
+ // Using a space should consume it
+ await signer1.sendTransaction({
+ to: ethers.Wallet.createRandom().address
+ })
+
+ const nonceSpace1b = await wallet.getNonce('0x01').then(r => BigInt(r))
+ expect(nonceSpace1b.toString()).to.equal('3')
+ })
+ })
+ })
+ })
+})
diff --git a/packages/auth/src/authorization.ts b/packages/auth/src/authorization.ts
new file mode 100644
index 0000000000..1eacdd7a8f
--- /dev/null
+++ b/packages/auth/src/authorization.ts
@@ -0,0 +1,81 @@
+import { ethers } from 'ethers'
+import { ETHAuth, Proof } from '@0xsequence/ethauth'
+import { ChainIdLike, toChainIdNumber } from '@0xsequence/network'
+import { TypedData } from '@0xsequence/utils'
+import { Signer } from '@0xsequence/wallet'
+import { Account } from '@0xsequence/account'
+import { DEFAULT_SESSION_EXPIRATION } from './services'
+
+export interface AuthorizationOptions {
+ // app name string, ie 'Skyweaver'
+ app?: string
+
+ // origin hostname of encoded in the message, ie. 'play.skyweaver.net'
+ origin?: string
+
+ // expiry in seconds encoded in the message
+ expiry?: number
+
+ // nonce for the authorization request
+ nonce?: number
+}
+
+export interface ETHAuthProof {
+ // eip712 typed-data payload for ETHAuth domain as input
+ typedData: TypedData
+
+ // signature encoded in an ETHAuth proof string
+ proofString: string
+}
+
+// signAuthorization will perform an EIP712 typed-data message signing of ETHAuth domain via the provided
+// Signer and authorization options.
+export const signAuthorization = async (
+ signer: Signer | Account,
+ chainId: ChainIdLike,
+ options: AuthorizationOptions
+): Promise => {
+ const address = ethers.getAddress(await signer.getAddress())
+ if (!address || address === '' || address === '0x') {
+ throw ErrAccountIsRequired
+ }
+
+ const proof = new Proof()
+ proof.address = address
+
+ if (!options || !options.app || options.app === '') {
+ throw new AuthError('authorization options requires app to be set')
+ }
+ proof.claims.app = options.app
+ proof.claims.ogn = options.origin
+ proof.claims.n = options.nonce
+
+ proof.setExpiryIn(options.expiry ? Math.max(options.expiry, 200) : DEFAULT_SESSION_EXPIRATION)
+
+ const typedData = proof.messageTypedData()
+
+ const chainIdNumber = toChainIdNumber(chainId)
+
+ proof.signature = await (signer instanceof Account
+ ? // Account can sign EIP-6492 signatures, so it doesn't require deploying the wallet
+ signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber, 'eip6492')
+ : signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber))
+
+ const ethAuth = new ETHAuth()
+ const proofString = await ethAuth.encodeProof(proof, true)
+
+ return {
+ typedData,
+ proofString
+ }
+}
+
+// TODO: review......
+export class AuthError extends Error {
+ constructor(message?: string) {
+ super(message)
+ this.name = 'AuthError'
+ }
+}
+
+export const ErrAccountIsRequired = new AuthError('auth error: account address is empty')
diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts
new file mode 100644
index 0000000000..af64af8df0
--- /dev/null
+++ b/packages/auth/src/index.ts
@@ -0,0 +1,3 @@
+export * from './authorization'
+export * from './session'
+export * from './proof'
diff --git a/packages/auth/src/proof.ts b/packages/auth/src/proof.ts
new file mode 100644
index 0000000000..1753ef5fcd
--- /dev/null
+++ b/packages/auth/src/proof.ts
@@ -0,0 +1,16 @@
+import { commons } from '@0xsequence/core'
+import { Proof, ValidatorFunc } from '@0xsequence/ethauth'
+import { tracker } from '@0xsequence/sessions'
+import { ethers } from 'ethers'
+
+export const ValidateSequenceWalletProof = (
+ readerFor: (chainId: number) => commons.reader.Reader,
+ tracker: tracker.ConfigTracker,
+ context: commons.context.WalletContext
+): ValidatorFunc => {
+ return async (_provider: ethers.JsonRpcProvider, chainId: number, proof: Proof): Promise<{ isValid: boolean }> => {
+ const digest = proof.messageDigest()
+ const isValid = await readerFor(chainId).isValidSignature(proof.address, digest, proof.signature)
+ return { isValid }
+ }
+}
diff --git a/packages/auth/src/services.ts b/packages/auth/src/services.ts
new file mode 100644
index 0000000000..fdc4bc9c92
--- /dev/null
+++ b/packages/auth/src/services.ts
@@ -0,0 +1,349 @@
+import { Account } from '@0xsequence/account'
+import { SequenceAPIClient } from '@0xsequence/api'
+import { ETHAuth, Proof } from '@0xsequence/ethauth'
+import { Indexer, SequenceIndexer, SequenceIndexerGateway } from '@0xsequence/indexer'
+import { SequenceMetadata } from '@0xsequence/metadata'
+import { ChainIdLike, findNetworkConfig } from '@0xsequence/network'
+import { getFetchRequest } from '@0xsequence/utils'
+import { ethers } from 'ethers'
+
+export type SessionMeta = {
+ // name of the app requesting the session, used with ETHAuth
+ name: string
+
+ // expiration in seconds for a session before it expires, used with ETHAuth
+ expiration?: number
+}
+
+export type ServicesSettings = {
+ metadata: SessionMeta
+ sequenceApiUrl: string
+ sequenceApiChainId: ethers.BigNumberish
+ sequenceMetadataUrl: string
+ sequenceIndexerGatewayUrl: string
+}
+
+export type SessionJWT = {
+ token: string
+ expiration: number
+}
+
+export type SessionJWTPromise = {
+ token: Promise
+ expiration: number
+}
+
+export type ProofStringPromise = {
+ proofString: Promise
+ expiration: number
+}
+
+// Default session expiration of ETHAuth token (1 week)
+export const DEFAULT_SESSION_EXPIRATION = 60 * 60 * 24 * 7
+
+// Long session expiration of ETHAuth token (~1 year)
+export const LONG_SESSION_EXPIRATION = 3e7
+
+const EXPIRATION_JWT_MARGIN = 60 // seconds
+
+export class Services {
+ _initialAuthRequest: Promise
+
+ // proof strings are indexed by account address and app name, see getProofStringKey()
+ private readonly proofStrings: Map = new Map()
+
+ private onAuthCallbacks: ((result: PromiseSettledResult) => void)[] = []
+
+ private apiClient: SequenceAPIClient | undefined
+ private metadataClient: SequenceMetadata | undefined
+ private indexerClients: Map = new Map()
+ private indexerGateway: SequenceIndexerGateway | undefined
+
+ private projectAccessKey?: string
+
+ constructor(
+ public readonly account: Account,
+ public readonly settings: ServicesSettings,
+ public readonly status: {
+ jwt?: SessionJWTPromise
+ metadata?: SessionMeta
+ } = {},
+ projectAccessKey?: string
+ ) {
+ this.projectAccessKey = projectAccessKey
+ }
+
+ private now(): number {
+ return Math.floor(Date.now() / 1000)
+ }
+
+ get expiration(): number {
+ return Math.max(this.settings.metadata.expiration ?? DEFAULT_SESSION_EXPIRATION, 120)
+ }
+
+ onAuth(cb: (result: PromiseSettledResult) => void) {
+ this.onAuthCallbacks.push(cb)
+ return () => (this.onAuthCallbacks = this.onAuthCallbacks.filter(c => c !== cb))
+ }
+
+ async dump(): Promise<{
+ jwt?: SessionJWT
+ metadata?: SessionMeta
+ }> {
+ if (!this.status.jwt) return { metadata: this.settings.metadata }
+
+ return {
+ jwt: {
+ token: await this.status.jwt.token,
+ expiration: this.status.jwt.expiration
+ },
+ metadata: this.status.metadata
+ }
+ }
+
+ auth(maxTries: number = 5): Promise {
+ if (this._initialAuthRequest) return this._initialAuthRequest
+
+ this._initialAuthRequest = (async () => {
+ const url = this.settings.sequenceApiUrl
+ if (!url) throw Error('No sequence api url')
+
+ let jwtAuth: string | undefined
+ for (let i = 1; ; i++) {
+ try {
+ jwtAuth = (await this.getJWT(true)).token
+ break
+ } catch (error) {
+ if (i === maxTries) {
+ console.error(`couldn't authenticate after ${maxTries} attempts`, error)
+ throw error
+ }
+ }
+ }
+
+ return new SequenceAPIClient(url, undefined, jwtAuth)
+ })()
+
+ return this._initialAuthRequest
+ }
+
+ private async getJWT(tryAuth: boolean): Promise {
+ const url = this.settings.sequenceApiUrl
+ if (!url) throw Error('No sequence api url')
+
+ // check if we already have or are waiting for a token
+ if (this.status.jwt) {
+ const jwt = this.status.jwt
+ const token = await jwt.token
+
+ if (this.now() < jwt.expiration) {
+ return { token, expiration: jwt.expiration }
+ }
+
+ // token expired, delete it and get a new one
+ this.status.jwt = undefined
+ }
+
+ if (!tryAuth) {
+ throw new Error('no auth token in memory')
+ }
+
+ const proofStringKey = this.getProofStringKey()
+ const { proofString, expiration } = this.getProofString(proofStringKey)
+
+ const jwt = {
+ token: proofString
+ .then(async proofString => {
+ const api = new SequenceAPIClient(url)
+
+ const authResp = await api.getAuthToken({ ewtString: proofString })
+
+ if (authResp?.status === true && authResp.jwtToken.length !== 0) {
+ return authResp.jwtToken
+ } else {
+ if (!(await this.isProofStringValid(proofString))) {
+ this.proofStrings.delete(proofStringKey)
+ }
+ throw new Error('no auth token from server')
+ }
+ })
+ .catch(reason => {
+ this.status.jwt = undefined
+ throw reason
+ }),
+ expiration
+ }
+
+ this.status.jwt = jwt
+
+ jwt.token
+ .then(token => {
+ this.onAuthCallbacks.forEach(cb => {
+ try {
+ cb({ status: 'fulfilled', value: token })
+ } catch {}
+ })
+ })
+ .catch((reason: any) => {
+ this.onAuthCallbacks.forEach(cb => {
+ try {
+ cb({ status: 'rejected', reason })
+ } catch {}
+ })
+ })
+
+ const token = await jwt.token
+ return { token, expiration }
+ }
+
+ private getProofStringKey(): string {
+ return `${this.account.address} - ${this.settings.metadata.name}`
+ }
+
+ private async isProofStringValid(proofString: string): Promise {
+ try {
+ const ethAuth = new ETHAuth()
+ const chainId = BigInt(this.settings.sequenceApiChainId)
+ const found = findNetworkConfig(this.account.networks, chainId)
+ if (!found) {
+ throw Error('No network found')
+ }
+ ethAuth.chainId = Number(chainId)
+
+ const network = new ethers.Network(found.name, chainId)
+
+ // TODO: Modify ETHAuth so it can take a provider instead of a url
+ // -----
+ // Can't pass jwt here since this is used for getting the jwt
+ ethAuth.provider = new ethers.JsonRpcProvider(getFetchRequest(found.rpcUrl, this.projectAccessKey), network, {
+ staticNetwork: network
+ })
+
+ await ethAuth.decodeProof(proofString)
+
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ async getAPIClient(tryAuth: boolean = true): Promise {
+ if (!this.apiClient) {
+ const url = this.settings.sequenceApiUrl
+ if (!url) throw Error('No sequence api url')
+
+ const jwtAuth = (await this.getJWT(tryAuth)).token
+ this.apiClient = new SequenceAPIClient(url, undefined, jwtAuth)
+ }
+
+ return this.apiClient
+ }
+
+ async getMetadataClient(tryAuth: boolean = true): Promise {
+ if (!this.metadataClient) {
+ const jwtAuth = (await this.getJWT(tryAuth)).token
+ this.metadataClient = new SequenceMetadata(this.settings.sequenceMetadataUrl, undefined, jwtAuth)
+ }
+
+ return this.metadataClient
+ }
+
+ async getIndexerClient(chainId: ChainIdLike, tryAuth: boolean = true): Promise {
+ const network = findNetworkConfig(this.account.networks, chainId)
+ if (!network) {
+ throw Error(`No network for chain ${chainId}`)
+ }
+
+ if (!this.indexerClients.has(network.chainId)) {
+ if (network.indexer) {
+ this.indexerClients.set(network.chainId, network.indexer)
+ } else if (network.indexerUrl) {
+ const jwtAuth = (await this.getJWT(tryAuth)).token
+ this.indexerClients.set(network.chainId, new SequenceIndexer(network.indexerUrl, undefined, jwtAuth))
+ } else {
+ throw Error(`No indexer url for chain ${chainId}`)
+ }
+ }
+
+ return this.indexerClients.get(network.chainId)!
+ }
+
+ async getIndexerGateway(tryAuth: boolean = true): Promise {
+ if (!this.indexerGateway) {
+ const jwtAuth = (await this.getJWT(tryAuth)).token
+ this.indexerGateway = new SequenceIndexerGateway(this.settings.sequenceIndexerGatewayUrl, undefined, jwtAuth)
+ }
+
+ return this.indexerGateway
+ }
+
+ private getProofString(key: string): ProofStringPromise {
+ // check if we already have or are waiting for a proof string
+ if (this.proofStrings.has(key)) {
+ const proofString = this.proofStrings.get(key)!
+
+ if (this.now() < proofString.expiration) {
+ return proofString
+ }
+
+ // proof string expired, delete it and make a new one
+ this.proofStrings.delete(key)
+ }
+
+ const proof = new Proof({
+ address: this.account.address
+ })
+
+ proof.claims.app = this.settings.metadata.name
+ if (typeof window === 'object') {
+ proof.claims.ogn = window.location.origin
+ }
+ proof.setExpiryIn(this.expiration)
+
+ const ethAuth = new ETHAuth()
+ const chainId = BigInt(this.settings.sequenceApiChainId)
+ const found = findNetworkConfig(this.account.networks, chainId)
+ if (!found) {
+ throw Error('No network found')
+ }
+ ethAuth.chainId = Number(chainId)
+
+ const network = new ethers.Network(found.name, chainId)
+
+ // TODO: Modify ETHAuth so it can take a provider instead of a url
+ // -----
+ // Can't pass jwt here since this is used for getting the jwt
+ ethAuth.provider = new ethers.JsonRpcProvider(getFetchRequest(found.rpcUrl, this.projectAccessKey), network, {
+ staticNetwork: network
+ })
+
+ const expiration = this.now() + this.expiration - EXPIRATION_JWT_MARGIN
+
+ const proofString = {
+ proofString: Promise.resolve(
+ // NOTICE: TODO: Here we ask the account to sign the message
+ // using whatever configuration we have ON-CHAIN, this means
+ // that the account will still use the v1 wallet, even if the migration
+ // was signed.
+ //
+ // This works for Sequence webapp v1 -> v2 because all v1 configurations share the same formula
+ // (torus + guard), but if we ever decide to allow cross-device login, then it will not work, because
+ // those other signers may not be part of the configuration.
+ //
+ this.account.signDigest(proof.messageDigest(), this.settings.sequenceApiChainId, true, 'eip6492')
+ )
+ .then(s => {
+ proof.signature = s
+ return ethAuth.encodeProof(proof, true)
+ })
+ .catch(reason => {
+ this.proofStrings.delete(key)
+ throw reason
+ }),
+ expiration
+ }
+
+ this.proofStrings.set(key, proofString)
+ return proofString
+ }
+}
diff --git a/packages/auth/src/session.ts b/packages/auth/src/session.ts
new file mode 100644
index 0000000000..18f08a6e16
--- /dev/null
+++ b/packages/auth/src/session.ts
@@ -0,0 +1,400 @@
+import { ChainId, NetworkConfig, allNetworks, findNetworkConfig } from '@0xsequence/network'
+import { jwtDecodeClaims } from '@0xsequence/utils'
+import { Account } from '@0xsequence/account'
+import { ethers } from 'ethers'
+import { tracker, trackers } from '@0xsequence/sessions'
+import { Orchestrator, SignatureOrchestrator, signers } from '@0xsequence/signhub'
+import { migrator } from '@0xsequence/migration'
+import { commons, universal, v1 } from '@0xsequence/core'
+import { Services, ServicesSettings, SessionJWT, SessionMeta } from './services'
+
+export interface SessionDumpV1 {
+ config: Omit & { address?: string }
+ jwt?: SessionJWT
+ metadata: SessionMeta
+}
+
+export interface SessionDumpV2 {
+ version: 2
+ address: string
+ jwt?: SessionJWT
+ metadata?: SessionMeta
+}
+
+export function isSessionDumpV1(obj: any): obj is SessionDumpV1 {
+ return obj.config && obj.metadata && obj.version === undefined
+}
+
+export function isSessionDumpV2(obj: any): obj is SessionDumpV2 {
+ return obj.version === 2 && obj.address
+}
+
+// These chains are always validated for migrations
+// if they are not available, the login will fail
+export const CRITICAL_CHAINS = [1, 137]
+
+export type SessionSettings = {
+ services?: ServicesSettings
+ contexts: commons.context.VersionedContext
+ networks: NetworkConfig[]
+ tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+}
+
+export const SessionSettingsDefault: SessionSettings = {
+ contexts: commons.context.defaultContexts,
+ networks: allNetworks,
+ tracker: new trackers.remote.RemoteConfigTracker('https://sessions.sequence.app')
+}
+
+export class Session {
+ constructor(
+ public networks: NetworkConfig[],
+ public contexts: commons.context.VersionedContext,
+ public account: Account,
+ public services?: Services
+ ) {}
+
+ async dump(): Promise {
+ const base = {
+ version: 2 as const,
+ address: this.account.address
+ }
+
+ if (this.services) {
+ return {
+ ...base,
+ ...(await this.services.dump())
+ }
+ }
+
+ return base
+ }
+
+ static async singleSigner(args: {
+ settings?: Partial
+ signer: ethers.Signer | signers.SapientSigner | string
+ selectWallet?: (wallets: string[]) => Promise
+ onAccountAddress?: (address: string) => void
+ onMigration?: (account: Account) => Promise
+ editConfigOnMigration?: (config: commons.config.Config) => commons.config.Config
+ projectAccessKey: string
+ }): Promise {
+ let { signer } = args
+
+ if (typeof signer === 'string') {
+ signer = new ethers.Wallet(signer)
+ }
+
+ const orchestrator = new Orchestrator([signer])
+ const referenceSigner = await signer.getAddress()
+ const threshold = 1
+ const addSigners = [
+ {
+ weight: 1,
+ address: referenceSigner
+ }
+ ]
+
+ const selectWallet =
+ args.selectWallet ||
+ (async (wallets: string[]) => {
+ if (wallets.length === 0) return undefined
+
+ // Find a wallet that was originally created
+ // as a 1/1 of the reference signer
+ const tracker = args.settings?.tracker ?? SessionSettingsDefault.tracker
+
+ const configs = await Promise.all(
+ wallets.map(async wallet => {
+ const imageHash = await tracker.imageHashOfCounterfactualWallet({ wallet })
+
+ return {
+ wallet,
+ config: imageHash && (await tracker.configOfImageHash({ imageHash: imageHash.imageHash }))
+ }
+ })
+ )
+
+ for (const config of configs) {
+ if (!config.config) {
+ continue
+ }
+
+ const coder = universal.genericCoderFor(config.config.version)
+ const signers = coder.config.signersOf(config.config)
+
+ if (signers.length === 1 && signers[0].address === referenceSigner) {
+ return config.wallet
+ }
+ }
+
+ return undefined
+ })
+
+ return Session.open({
+ ...args,
+ orchestrator,
+ referenceSigner,
+ threshold,
+ addSigners,
+ selectWallet
+ })
+ }
+
+ static async open(args: {
+ settings?: Partial
+ orchestrator: SignatureOrchestrator
+ addSigners?: commons.config.SimpleSigner[]
+ referenceSigner: string
+ threshold?: ethers.BigNumberish
+ selectWallet: (wallets: string[]) => Promise
+ onAccountAddress?: (address: string) => void
+ editConfigOnMigration?: (config: commons.config.Config) => commons.config.Config
+ onMigration?: (account: Account) => Promise
+ projectAccessKey?: string
+ }): Promise {
+ const {
+ referenceSigner,
+ threshold,
+ addSigners,
+ selectWallet,
+ onAccountAddress,
+ settings,
+ editConfigOnMigration,
+ onMigration,
+ orchestrator,
+ projectAccessKey
+ } = args
+
+ const { contexts, networks, tracker, services } = { ...SessionSettingsDefault, ...settings }
+
+ // The reference network is mainnet, if mainnet is not available, we use the first network
+ const referenceChainId =
+ findNetworkConfig(networks, settings?.services?.sequenceApiChainId ?? ChainId.MAINNET)?.chainId ?? networks[0]?.chainId
+ if (!referenceChainId) throw Error('No reference chain found')
+
+ const foundWallets = await tracker.walletsOfSigner({ signer: referenceSigner })
+ const selectedWallet = await selectWallet(foundWallets.map(w => w.wallet))
+
+ let account: Account
+
+ if (selectedWallet) {
+ onAccountAddress?.(selectedWallet)
+
+ // existing account, lets update it
+ account = new Account({
+ address: selectedWallet,
+ tracker,
+ networks,
+ contexts,
+ orchestrator,
+ projectAccessKey
+ })
+
+ // Get the latest configuration of the wallet (on the reference chain)
+ // now this configuration should be of the latest version, so we can start
+ // manipulating it.
+
+ // NOTICE: We are performing the wallet update on a single chain, assuming that
+ // all other networks have the same configuration. This is not always true.
+ if (addSigners && addSigners.length > 0) {
+ // New wallets never need migrations
+ // (because we create them on the latest version)
+ let status = await account.status(referenceChainId)
+
+ // If the wallet was created originally on v2, then we can skip
+ // the migration checks all together.
+ if (status.original.version !== status.version || account.version !== status.version) {
+ // Account may not have been migrated yet, so we need to check
+ // if it has been migrated and if not, migrate it (in all chains)
+ const { migratedAllChains: isFullyMigrated, failedChains } = await account.isMigratedAllChains()
+
+ // Failed chains must not contain mainnet or polygon, otherwise we cannot proceed.
+ if (failedChains.some(c => CRITICAL_CHAINS.includes(c))) {
+ throw Error(`Failed to fetch account status on ${failedChains.join(', ')}`)
+ }
+
+ if (!isFullyMigrated) {
+ // This is an oportunity for whoever is opening the session to
+ // feed the orchestrator with more signers, so that the migration
+ // can be completed.
+ if (onMigration && !(await onMigration(account))) {
+ throw Error('Migration cancelled, cannot open session')
+ }
+
+ const { failedChains } = await account.signAllMigrations(editConfigOnMigration || (c => c))
+ if (failedChains.some(c => CRITICAL_CHAINS.includes(c))) {
+ throw Error(`Failed to sign migrations on ${failedChains.join(', ')}`)
+ }
+
+ // If we are using a dedupped tracker we need to invalidate the cache
+ // otherwise we run the risk of not seeing the signed migrations reflected.
+ if (trackers.isDedupedTracker(tracker)) {
+ tracker.invalidateCache()
+ }
+
+ let isFullyMigrated2: boolean
+ ;[isFullyMigrated2, status] = await Promise.all([
+ account.isMigratedAllChains().then(r => r.migratedAllChains),
+ account.status(referenceChainId)
+ ])
+
+ if (!isFullyMigrated2) throw Error('Failed to migrate account')
+ }
+ }
+
+ // NOTICE: We only need to do this because the API will not be able to
+ // validate the v2 signature (if the account has an onchain version of 1)
+ // we could speed this up by sending the migration alongside the jwt request
+ // and letting the API validate it offchain.
+ if (status.onChain.version !== status.version) {
+ await account.doBootstrap(referenceChainId, undefined, status)
+ }
+
+ const prevConfig = status.config
+ const nextConfig = account.coders.config.editConfig(prevConfig, {
+ add: addSigners,
+ threshold
+ })
+
+ // Only update the onchain config if the imageHash has changed
+ if (account.coders.config.imageHashOf(prevConfig) !== account.coders.config.imageHashOf(nextConfig)) {
+ const newConfig = account.coders.config.editConfig(nextConfig, {
+ checkpoint: account.coders.config.checkpointOf(prevConfig) + 1n
+ })
+
+ await account.updateConfig(newConfig)
+ }
+ }
+ } else {
+ if (!addSigners || addSigners.length === 0) {
+ throw Error('Cannot create new account without signers')
+ }
+
+ if (!threshold) {
+ throw Error('Cannot create new account without threshold')
+ }
+
+ // fresh account
+ account = await Account.new({
+ config: { threshold, checkpoint: 0, signers: addSigners },
+ tracker,
+ contexts,
+ orchestrator,
+ networks,
+ projectAccessKey
+ })
+
+ onAccountAddress?.(account.address)
+
+ // sign a digest and send it to the tracker
+ // otherwise the tracker will not know about this account
+ await account.publishWitness()
+
+ // safety check, the remove tracker should be able to find
+ // this account for the reference signer
+ const foundWallets = await tracker.walletsOfSigner({ signer: referenceSigner, noCache: true })
+ if (!foundWallets.some(w => w.wallet === account.address)) {
+ throw Error('Account not found on tracker')
+ }
+ }
+
+ let servicesObj: Services | undefined
+
+ if (services) {
+ servicesObj = new Services(account, services)
+ servicesObj.auth() // fire and forget
+
+ servicesObj.onAuth(result => {
+ if (result.status === 'fulfilled') {
+ account.setJwt(result.value)
+ }
+ })
+ }
+
+ return new Session(networks, contexts, account, servicesObj)
+ }
+
+ static async load(args: {
+ settings?: Partial
+ orchestrator: SignatureOrchestrator
+ dump: SessionDumpV1 | SessionDumpV2
+ editConfigOnMigration: (config: commons.config.Config) => commons.config.Config
+ onMigration?: (account: Account) => Promise
+ projectAccessKey?: string
+ }): Promise {
+ const { dump, settings, editConfigOnMigration, onMigration, orchestrator, projectAccessKey } = args
+ const { contexts, networks, tracker, services } = { ...SessionSettingsDefault, ...settings }
+
+ let account: Account
+
+ if (isSessionDumpV1(dump)) {
+ // Old configuration format used to also contain an "address" field
+ // but if it doesn't, it means that it was a "counterfactual" account
+ // not yet updated, so we need to compute the address
+ const oldAddress =
+ dump.config.address ||
+ commons.context.addressOf(contexts[1], v1.config.ConfigCoder.imageHashOf({ ...dump.config, version: 1 }))
+
+ const jwtExpired = (dump.jwt?.expiration ?? 0) < Math.floor(Date.now() / 1000)
+
+ account = new Account({
+ address: oldAddress,
+ tracker,
+ networks,
+ contexts,
+ orchestrator,
+ jwt: jwtExpired ? undefined : dump.jwt?.token,
+ projectAccessKey
+ })
+
+ // TODO: This property may not hold if the user adds a new network
+ if (!(await account.isMigratedAllChains().then(r => r.migratedAllChains))) {
+ // This is an oportunity for whoever is opening the session to
+ // feed the orchestrator with more signers, so that the migration
+ // can be completed.
+ if (onMigration && !(await onMigration(account))) {
+ throw Error('Migration cancelled, cannot open session')
+ }
+
+ console.log('Migrating account...')
+ await account.signAllMigrations(editConfigOnMigration)
+ if (!(await account.isMigratedAllChains().then(r => r.migratedAllChains))) throw Error('Failed to migrate account')
+ }
+
+ // We may need to update the JWT if the account has been migrated
+ } else if (isSessionDumpV2(dump)) {
+ const jwtExpired = (dump.jwt?.expiration ?? 0) < Math.floor(Date.now() / 1000)
+
+ account = new Account({
+ address: dump.address,
+ tracker,
+ networks,
+ contexts,
+ orchestrator,
+ jwt: jwtExpired ? undefined : dump.jwt?.token,
+ projectAccessKey
+ })
+ } else {
+ throw Error('Invalid dump format')
+ }
+
+ let servicesObj: Services | undefined
+
+ if (services) {
+ servicesObj = new Services(
+ account,
+ services,
+ dump.jwt && {
+ jwt: {
+ token: Promise.resolve(dump.jwt.token),
+ expiration: dump.jwt.expiration ?? jwtDecodeClaims(dump.jwt.token).exp
+ },
+ metadata: dump.metadata
+ }
+ )
+ }
+
+ return new Session(networks, contexts, account, servicesObj)
+ }
+}
diff --git a/packages/auth/tests/session.spec.ts b/packages/auth/tests/session.spec.ts
new file mode 100644
index 0000000000..bba425b9a8
--- /dev/null
+++ b/packages/auth/tests/session.spec.ts
@@ -0,0 +1,1437 @@
+import { Account } from '@0xsequence/account'
+import { commons, v1, v2 } from '@0xsequence/core'
+import { ETHAuth, Proof } from '@0xsequence/ethauth'
+import { migrator } from '@0xsequence/migration'
+import { NetworkConfig } from '@0xsequence/network'
+import { LocalRelayer } from '@0xsequence/relayer'
+import { tracker, trackers } from '@0xsequence/sessions'
+import { Orchestrator, SignatureOrchestrator } from '@0xsequence/signhub'
+import * as utils from '@0xsequence/tests'
+import { CallReceiverMock, HookCallerMock } from '@0xsequence/wallet-contracts'
+import * as chai from 'chai'
+import chaiAsPromised from 'chai-as-promised'
+import { ethers } from 'ethers'
+import * as mockServer from 'mockttp'
+import { Session, SessionDumpV1, SessionSettings, ValidateSequenceWalletProof } from '../src'
+import { delay, mockDate } from './utils'
+
+const CallReceiverMockArtifact = require('@0xsequence/wallet-contracts/artifacts/contracts/mocks/CallReceiverMock.sol/CallReceiverMock.json')
+const HookCallerMockArtifact = require('@0xsequence/wallet-contracts/artifacts/contracts/mocks/HookCallerMock.sol/HookCallerMock.json')
+
+const { expect } = chai.use(chaiAsPromised)
+
+const deterministic = false
+
+type EthereumInstance = {
+ chainId?: number
+ providerUrl?: string
+ provider?: ethers.JsonRpcProvider
+ signer?: ethers.Signer
+}
+
+class CountingSigner extends ethers.AbstractSigner {
+ private _signingRequests: number = 0
+
+ constructor(private readonly signer: ethers.Signer) {
+ super()
+ }
+
+ get signingRequests(): number {
+ return this._signingRequests
+ }
+
+ getAddress(): Promise {
+ return this.signer.getAddress()
+ }
+
+ signMessage(message: ethers.BytesLike): Promise {
+ this._signingRequests++
+ return this.signer.signMessage(message)
+ }
+
+ signTransaction(transaction: ethers.TransactionRequest): Promise {
+ this._signingRequests++
+ return this.signer.signTransaction(transaction)
+ }
+
+ signTypedData(
+ domain: ethers.TypedDataDomain,
+ types: Record,
+ value: Record
+ ): Promise {
+ this._signingRequests++
+ return this.signer.signTypedData(domain, types, value)
+ }
+
+ connect(provider: ethers.Provider): ethers.Signer {
+ return this.signer.connect(provider)
+ }
+}
+
+describe('Wallet integration', function () {
+ const ethnode: EthereumInstance = {}
+
+ let relayer: LocalRelayer
+ let callReceiver: CallReceiverMock
+ let hookCaller: HookCallerMock
+
+ let contexts: commons.context.VersionedContext
+ let networks: NetworkConfig[]
+
+ let tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
+ let orchestrator: SignatureOrchestrator
+ let simpleSettings: SessionSettings
+
+ before(async () => {
+ // Provider from hardhat without a server instance
+ ethnode.providerUrl = `http://127.0.0.1:9546/`
+ ethnode.provider = new ethers.JsonRpcProvider(ethnode.providerUrl)
+
+ const chainId = (await ethnode.provider.getNetwork()).chainId
+ ethnode.signer = await ethnode.provider.getSigner()
+ ethnode.chainId = Number(chainId)
+
+ // Deploy local relayer
+ relayer = new LocalRelayer(ethnode.signer)
+
+ networks = [
+ {
+ name: 'local',
+ chainId: Number(chainId),
+ provider: ethnode.provider,
+ isDefaultChain: true,
+ relayer,
+ rpcUrl: '',
+ nativeToken: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18
+ }
+ }
+ ] as NetworkConfig[]
+
+ contexts = await utils.context.deploySequenceContexts(ethnode.signer)
+
+ // Deploy call receiver mock
+ callReceiver = (await new ethers.ContractFactory(
+ CallReceiverMockArtifact.abi,
+ CallReceiverMockArtifact.bytecode,
+ ethnode.signer
+ )
+ .deploy()
+ .then(tx => tx.waitForDeployment())) as CallReceiverMock
+
+ // Deploy hook caller mock
+ hookCaller = (await new ethers.ContractFactory(HookCallerMockArtifact.abi, HookCallerMockArtifact.bytecode, ethnode.signer)
+ .deploy()
+ .then(tx => tx.waitForDeployment())) as HookCallerMock
+
+ tracker = new trackers.local.LocalConfigTracker(ethnode.provider!)
+ orchestrator = new Orchestrator([])
+
+ simpleSettings = {
+ contexts,
+ networks,
+ tracker,
+ services: {
+ metadata: {
+ name: 'test'
+ },
+ sequenceApiUrl: '',
+ sequenceApiChainId: chainId,
+ sequenceMetadataUrl: ''
+ }
+ }
+ })
+
+ it('Should open a new session', async () => {
+ const referenceSigner = randomWallet('Should open a new session')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ orchestrator,
+ settings: simpleSettings,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async ws => {
+ expect(ws.length).to.equal(0)
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(session.account.address).to.not.equal(ethers.ZeroAddress)
+
+ const status = await session.account.status(networks[0].chainId)
+
+ expect(v2.config.isWalletConfig(status.config)).to.equal(true)
+ const configv2 = status.config as v2.config.WalletConfig
+
+ expect(BigInt(configv2.threshold)).to.equal(1n)
+ expect(v2.config.isSignerLeaf(configv2.tree)).to.equal(true)
+
+ const leaf = configv2.tree as v2.config.SignerLeaf
+ expect(leaf.address).to.equal(referenceSigner.address)
+ expect(BigInt(leaf.weight)).to.equal(1n)
+
+ await session.account.sendTransaction({ to: referenceSigner.address }, networks[0].chainId)
+ })
+
+ it('Should dump and load a session', async () => {
+ const referenceSigner = randomWallet('Should dump and load a session')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async ws => {
+ expect(ws.length).to.equal(0)
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ const dump = await session.dump()
+
+ const session2 = await Session.load({
+ settings: simpleSettings,
+ orchestrator,
+ dump,
+ editConfigOnMigration: config => config
+ })
+
+ await session.account.sendTransaction({ to: referenceSigner.address }, networks[0].chainId)
+
+ expect(session.account.address).to.equal(session2.account.address)
+ })
+
+ it('Should open an existing session', async () => {
+ const referenceSigner = randomWallet('Should open an existing session')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async ws => ws[0] ?? undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should open an existing session 2')
+ const session2 = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async ws => {
+ expect(ws.length).to.equal(1)
+ return ws[0]
+ },
+ editConfigOnMigration: config => config
+ })
+
+ const newConfig = (await session2.account.status(networks[0].chainId).then(s => s.config)) as v2.config.WalletConfig
+
+ expect(session2.account.address).to.equal(session.account.address)
+ expect(BigInt(newConfig.threshold)).to.equal(2n)
+
+ const newSigners = v2.config.signersOf(newConfig.tree).map(s => s.address)
+ expect(newSigners.length).to.equal(2)
+ expect(newSigners).to.include(newSigner.address)
+ expect(newSigners).to.include(referenceSigner.address)
+ expect(BigInt((newConfig.tree as any).left.weight)).to.equal(1n)
+ expect(BigInt((newConfig.tree as any).right.weight)).to.equal(1n)
+ })
+
+ it('Should create a new account if selectWallet returns undefined', async () => {
+ const referenceSigner = randomWallet('Should create a new account if selectWallet returns undefined')
+ orchestrator.setSigners([referenceSigner])
+
+ const oldSession = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should create a new account if selectWallet returns undefined 2')
+ const newSession = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [
+ { address: referenceSigner.address, weight: 1 },
+ { address: newSigner.address, weight: 1 }
+ ],
+ threshold: 1,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(1)
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.not.equal(oldSession.account.address)
+ })
+
+ it('Should select between two wallets using selectWallet', async () => {
+ const referenceSigner = randomWallet('Should select between two wallets using selectWallet')
+ orchestrator.setSigners([referenceSigner])
+
+ const oldSession1 = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const oldSession2 = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 2 }],
+ threshold: 2,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should select between two wallets using selectWallet 2')
+ const newSession1 = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(2)
+ expect(wallets).to.include(oldSession1.account.address)
+ expect(wallets).to.include(oldSession2.account.address)
+ return oldSession1.account.address
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession1.account.address).to.equal(oldSession1.account.address)
+
+ const newSession2 = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(2)
+ expect(wallets).to.include(oldSession1.account.address)
+ expect(wallets).to.include(oldSession2.account.address)
+ return oldSession2.account.address
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession2.account.address).to.equal(oldSession2.account.address)
+
+ await newSession1.account.sendTransaction([], networks[0].chainId)
+ await newSession2.account.sendTransaction([], networks[0].chainId)
+ })
+
+ it('Should re-open a session after sending a transaction', async () => {
+ const referenceSigner = randomWallet('Should re-open a session after sending a transaction')
+ const signer1 = randomWallet('Should re-open a session after sending a transaction 2')
+ orchestrator.setSigners([referenceSigner, signer1])
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [
+ {
+ address: referenceSigner.address,
+ weight: 1
+ },
+ {
+ address: signer1.address,
+ weight: 1
+ }
+ ],
+ threshold: 2,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.account.sendTransaction([], networks[0].chainId)
+
+ const signer2 = randomWallet('Should re-open a session after sending a transaction 3')
+
+ const newSession = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: signer2.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(1)
+ return wallets[0]
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.equal(session.account.address)
+
+ await newSession.account.sendTransaction([], networks[0].chainId)
+ })
+
+ describe('Migrate sessions', () => {
+ let ogAccount: Account
+ let referenceSigner: ethers.Wallet
+ let referenceSignerIndex = 1
+ let v1SessionDump: SessionDumpV1
+
+ beforeEach(async () => {
+ // Create a wallet using v1
+ referenceSigner = randomWallet(`Migrate sessions ${referenceSignerIndex++}`)
+ orchestrator.setSigners([referenceSigner])
+
+ ogAccount = await Account.new({
+ config: { threshold: 1, checkpoint: 0, signers: [{ address: referenceSigner.address, weight: 1 }] },
+ tracker,
+ contexts: { 1: contexts[1] },
+ orchestrator,
+ networks,
+ migrations: {
+ 0: {
+ version: 1,
+ configCoder: v1.config.ConfigCoder,
+ signatureCoder: v1.signature.SignatureCoder
+ } as any
+ }
+ })
+
+ await ogAccount.publishWitness()
+
+ v1SessionDump = {
+ config: {
+ threshold: 1,
+ signers: [{ address: referenceSigner.address, weight: 1 }]
+ },
+ metadata: {
+ name: 'Test'
+ }
+ }
+ })
+
+ it('Should open and migrate old session, without dump', async () => {
+ const newSigner = randomWallet('Should open and migrate old session, without dump')
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ const newSession = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(1)
+ return wallets[0]
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.equal(ogAccount.address)
+ const status = await newSession.account.status(networks[0].chainId)
+ expect(status.version).to.equal(2)
+ expect(status.fullyMigrated).to.be.true
+
+ await newSession.account.sendTransaction([], networks[0].chainId)
+ })
+
+ it('Should open and migrate dump', async () => {
+ const newSession = await Session.load({
+ settings: simpleSettings,
+ orchestrator,
+ dump: v1SessionDump,
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.equal(ogAccount.address)
+
+ const status = await newSession.account.status(networks[0].chainId)
+ expect(status.version).to.equal(2)
+ expect(status.fullyMigrated).to.be.true
+
+ await newSession.account.sendTransaction([], networks[0].chainId)
+ })
+
+ describe('After updating old wallet', () => {
+ let newSignerIndex = 1
+
+ beforeEach(async () => {
+ const status = await ogAccount.status(networks[0].chainId)
+ const wallet = ogAccount.walletForStatus(networks[0].chainId, status)
+
+ const newSigner = randomWallet(`After updating old wallet ${newSignerIndex++}`)
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ const uptx = await wallet.buildUpdateConfigurationTransaction({
+ threshold: 2,
+ signers: [
+ { address: referenceSigner.address, weight: 1 },
+ { address: newSigner.address, weight: 1 }
+ ]
+ } as v1.config.WalletConfig)
+
+ const suptx = await wallet.signTransactionBundle(uptx)
+ await wallet.relayer?.relay(suptx)
+
+ v1SessionDump = {
+ ...v1SessionDump,
+ config: {
+ ...v1SessionDump.config,
+ address: wallet.address
+ }
+ }
+ })
+
+ it('Should open and migrate old session', async () => {
+ const newSigner2 = randomWallet('Should open and migrate old session')
+
+ const newSession = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner2.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async wallets => {
+ expect(wallets.length).to.equal(1)
+ return wallets[0]
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.equal(ogAccount.address)
+ const status = await newSession.account.status(networks[0].chainId)
+ expect(status.version).to.equal(2)
+ expect(status.fullyMigrated).to.be.true
+
+ orchestrator.setSigners([referenceSigner, newSigner2])
+ await newSession.account.sendTransaction([], networks[0].chainId)
+ })
+
+ it('Should open and migrate dump', async () => {
+ const newSession = await Session.load({
+ settings: simpleSettings,
+ orchestrator,
+ dump: v1SessionDump,
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.account.address).to.equal(ogAccount.address)
+
+ const status = await newSession.account.status(networks[0].chainId)
+ expect(status.version).to.equal(2)
+ expect(status.fullyMigrated).to.be.true
+
+ await newSession.account.sendTransaction([], networks[0].chainId)
+ })
+ })
+ })
+
+ describe('JWT Auth', () => {
+ let server: mockServer.Mockttp
+ let fakeJwt: string
+ let fakeJwtIndex = 1
+ let proofAddress: string
+
+ let delayMs: number = 0
+ let totalCount: number = 0
+ let recoverCount: { [address: string]: number } = {}
+
+ let alwaysFail: boolean = false
+
+ const sequenceApiUrl = 'http://127.0.0.1:8099'
+ let settings: SessionSettings
+
+ beforeEach(() => {
+ settings = {
+ ...simpleSettings,
+ services: {
+ ...simpleSettings.services!,
+ sequenceApiUrl
+ }
+ }
+
+ fakeJwt = ethers.hexlify(randomBytes(64, `JWT Auth ${fakeJwtIndex++}`))
+
+ server = mockServer.getLocal()
+ server.start(8099)
+ server.forPost('/rpc/API/GetAuthToken').thenCallback(async request => {
+ if (delayMs !== 0) await delay(delayMs)
+
+ const validator = ValidateSequenceWalletProof(
+ () => new commons.reader.OnChainReader(networks[0].provider!),
+ tracker,
+ contexts[2]
+ )
+
+ const ethauth = new ETHAuth(validator)
+
+ ethauth.chainId = ethnode.chainId!
+ ethauth.configJsonRpcProvider(ethnode.providerUrl!)
+
+ totalCount++
+
+ if (alwaysFail) return { statusCode: 400 }
+
+ try {
+ const proof = await ethauth.decodeProof((await request.body.getJson())!['ewtString'])
+ proofAddress = ethers.getAddress(proof.address)
+
+ if (recoverCount[proofAddress]) {
+ recoverCount[proofAddress]++
+ } else {
+ recoverCount[proofAddress] = 1
+ }
+
+ return {
+ statusCode: 200,
+ body: JSON.stringify({
+ status: true,
+ jwtToken: fakeJwt
+ })
+ }
+ } catch {
+ if (recoverCount['error']) {
+ recoverCount['error']++
+ } else {
+ recoverCount['error'] = 1
+ }
+
+ return {
+ statusCode: 401
+ }
+ }
+ })
+ })
+
+ afterEach(() => {
+ server.stop()
+ delayMs = 0
+ totalCount = 0
+ recoverCount = {}
+ alwaysFail = false
+ })
+
+ it('Should get JWT token', async () => {
+ const referenceSigner = randomWallet('Should get JWT token')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?.auth()
+ expect(totalCount).to.equal(1)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ expect(proofAddress).to.equal(session.account.address)
+ })
+
+ it('Should get JWT after updating session', async () => {
+ const referenceSigner = randomWallet('Should get JWT after updating session')
+ orchestrator.setSigners([referenceSigner])
+
+ await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should get JWT after updating session 2')
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async ws => ws[0],
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?.auth()
+
+ expect(totalCount).to.equal(1)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ expect(proofAddress).to.equal(session.account.address)
+ })
+
+ it('Should get JWT during first session creation', async () => {
+ const referenceSigner = randomWallet('Should get JWT during first session creation')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+
+ expect(totalCount).to.equal(1)
+ expect(recoverCount[session.account.address]).to.equal(1)
+
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ })
+
+ it('Should get JWT during session opening', async () => {
+ delayMs = 500
+
+ const referenceSigner = randomWallet('Should get JWT during session opening - 1')
+ orchestrator.setSigners([referenceSigner])
+
+ let session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await expect(session.services?._initialAuthRequest).to.be.rejected
+
+ const newSigner = randomWallet('Should get JWT during session opening 2')
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async ws => {
+ expect(ws.length).to.equal(1)
+ return ws[0]
+ },
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+
+ expect(totalCount).to.equal(1)
+ expect(recoverCount[session.account.address]).to.equal(1)
+
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ })
+
+ it('Should get API with lazy JWT during first session creation', async () => {
+ const referenceSigner = randomWallet('Should get API with lazy JWT during first session creation')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const api = await session.services?.getAPIClient()
+
+ expect(totalCount).to.equal(1)
+ expect(recoverCount[session.account.address]).to.equal(1)
+
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+
+ server.forPost('/rpc/API/FriendList').thenCallback(async request => {
+ const hasToken = request.headers['authorization']!.includes(fakeJwt)
+ return { statusCode: hasToken ? 200 : 401, body: JSON.stringify({}) }
+ })
+
+ await api!.friendList({ page: {} })
+ })
+
+ it('Should get API with lazy JWT during session opening', async () => {
+ delayMs = 500
+ const referenceSigner = randomWallet('Should get API with lazy JWT during session opening')
+ orchestrator.setSigners([referenceSigner])
+
+ await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should get API with lazy JWT during session opening 2')
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async ws => ws[0],
+ editConfigOnMigration: config => config
+ })
+
+ const api = await session.services?.getAPIClient()
+
+ expect(totalCount).to.equal(1)
+ expect(recoverCount[session.account.address]).to.equal(1)
+
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+
+ server.forPost('/rpc/API/FriendList').thenCallback(async request => {
+ const hasToken = request.headers['authorization']!.includes(fakeJwt)
+ return { statusCode: hasToken ? 200 : 401, body: JSON.stringify({}) }
+ })
+
+ await api!.friendList({ page: {} })
+ })
+
+ it('Should call callbacks on JWT token', async () => {
+ const referenceSigner = randomWallet('Should call callbacks on JWT token')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ let calledCallback = 0
+ session.services?.onAuth(() => calledCallback++)
+
+ await session.services?._initialAuthRequest
+
+ expect(calledCallback).to.equal(1)
+ })
+
+ it('Should call callbacks on JWT token (on open only once)', async () => {
+ delayMs = 500
+
+ const referenceSigner = randomWallet('Should call callbacks on JWT token (on open only once)')
+ orchestrator.setSigners([referenceSigner])
+
+ await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should call callbacks on JWT token (on open only once) 2')
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [
+ { address: referenceSigner.address, weight: 1 },
+ { address: newSigner.address, weight: 1 }
+ ],
+ threshold: 2,
+ selectWallet: async ws => ws[0],
+ editConfigOnMigration: config => config
+ })
+
+ let calledCallback = 0
+ session.services?.onAuth(() => calledCallback++)
+
+ await session.services?._initialAuthRequest
+
+ expect(calledCallback).to.equal(1)
+ })
+
+ it('Should retry 5 times retrieving the JWT token', async () => {
+ delayMs = 1000
+ const referenceSigner = randomWallet('Should retry 5 times retrieving the JWT token')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ alwaysFail = true
+ await expect(session.services?.auth()).to.be.rejected
+ expect(totalCount).to.equal(5)
+ expect(session.services?.status.jwt).to.be.undefined
+ })
+
+ it('Should get API with JWT already present', async () => {
+ delayMs = 500
+
+ const referenceSigner = randomWallet('Should get API with JWT already present')
+ orchestrator.setSigners([referenceSigner])
+
+ await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const newSigner = randomWallet('Should get API with JWT already present 2')
+ orchestrator.setSigners([referenceSigner, newSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: newSigner.address, weight: 1 }],
+ threshold: 2,
+ selectWallet: async ws => ws[0],
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+ const totalCountBefore = totalCount
+
+ // This should use the already existing JWT
+ const api = await session.services?.getAPIClient()
+
+ expect(totalCount).to.equal(totalCountBefore)
+ expect(recoverCount[session.account.address]).to.equal(1)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+
+ server.forPost('/rpc/API/FriendList').thenCallback(async request => {
+ const hasToken = request.headers['authorization']!.includes(fakeJwt)
+ return { statusCode: hasToken ? 200 : 401, body: JSON.stringify({}) }
+ })
+
+ await api!.friendList({ page: {} })
+ })
+
+ it('Should fail to get API with false tryAuth and no JWT', async () => {
+ const referenceSigner = randomWallet('Should fail to get API with false tryAuth and no JWT')
+ orchestrator.setSigners([referenceSigner])
+
+ alwaysFail = true
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await expect(session.services?._initialAuthRequest).to.be.rejected
+
+ alwaysFail = false
+
+ const apiPromise = session.services?.getAPIClient(false)
+
+ await expect(apiPromise).to.be.rejected
+
+ expect(totalCount).to.equal(0)
+ expect(session.services?.status.jwt).to.be.undefined
+ })
+
+ it('Should fail to get API without api url', async () => {
+ const referenceSigner = randomWallet('Should fail to get API without api url')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ const apiPromise = session.services?.getAPIClient()
+
+ await expect(apiPromise).to.be.rejected
+
+ expect(totalCount).to.equal(0)
+ expect(session.services?.status.jwt?.token).to.be.undefined
+ })
+
+ it('Should fail to get JWT with no api configured', async () => {
+ const referenceSigner = randomWallet('Should fail to get JWT with no api configured')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: simpleSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await expect(session.services?.auth()).to.be.rejected
+
+ expect(totalCount).to.equal(0)
+ expect(session.services?.status.jwt?.token).to.be.undefined
+ })
+
+ it('Should reuse outstanding JWT requests', async () => {
+ const referenceSigner = new CountingSigner(randomWallet('Should reuse outstanding JWT requests'))
+ orchestrator.setSigners([referenceSigner])
+
+ alwaysFail = true
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: await referenceSigner.getAddress(),
+ addSigners: [{ address: await referenceSigner.getAddress(), weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ // 1 signing request is made to publish signers
+ expect(referenceSigner.signingRequests).to.equal(1)
+
+ const signingRequestsBefore = referenceSigner.signingRequests
+
+ await expect(session.services?._initialAuthRequest).to.be.rejected
+
+ alwaysFail = false
+ totalCount = 0
+
+ // Create a bunch of API clients concurrently
+ const requests: any[] = []
+ while (requests.length < 10) {
+ requests.push(session.services?.getAPIClient())
+ }
+ await expect(Promise.all(requests)).to.be.fulfilled
+
+ expect(totalCount).to.equal(1)
+ expect(referenceSigner.signingRequests).to.equal(signingRequestsBefore + 1)
+ })
+
+ it('Should reuse existing proof signatures', async () => {
+ const referenceSigner = new CountingSigner(randomWallet('Should reuse existing proof signatures'))
+ orchestrator.setSigners([referenceSigner])
+
+ alwaysFail = true
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: await referenceSigner.getAddress(),
+ addSigners: [{ address: await referenceSigner.getAddress(), weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ // 1 signing request is made to publish signers
+ expect(referenceSigner.signingRequests).to.equal(1)
+
+ const signingRequestsBefore = referenceSigner.signingRequests
+
+ await expect(session.services?._initialAuthRequest).to.be.rejected
+
+ totalCount = 0
+
+ // Create a bunch of API clients sequentially
+ for (let i = 0; i < 10; i++) {
+ await expect(session.services?.getAPIClient()).to.be.rejected
+ }
+
+ expect(totalCount).to.equal(10)
+ expect(referenceSigner.signingRequests).to.equal(signingRequestsBefore + 1)
+ })
+
+ it('Should neither re-authenticate nor retry if request succeeds', async () => {
+ const referenceSigner = new CountingSigner(randomWallet('Should neither re-authenticate nor retry if request succeeds'))
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings,
+ orchestrator,
+ referenceSigner: await referenceSigner.getAddress(),
+ addSigners: [{ address: await referenceSigner.getAddress(), weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+
+ const api = await session.services?.getAPIClient()
+
+ const okResponses = [true]
+ server.forPost('/rpc/API/FriendList').thenCallback(async () => {
+ return { statusCode: okResponses.shift() ? 200 : 401, body: JSON.stringify({}) }
+ })
+
+ totalCount = 0
+
+ await expect(api!.friendList({ page: {} })).to.be.fulfilled
+
+ // no re-authentication since it succeeded
+ expect(totalCount).to.equal(0)
+ })
+
+ describe('With expiration', () => {
+ let resetDateMock: Function | undefined
+
+ const setDate = (seconds: number) => {
+ if (resetDateMock) resetDateMock()
+ const newMockDate = new Date()
+ newMockDate.setTime(seconds * 1000)
+ resetDateMock = mockDate(newMockDate)
+ }
+
+ afterEach(() => {
+ if (resetDateMock) resetDateMock()
+ })
+
+ it('Should request a new JWT after expiration', async () => {
+ const baseTime = 1613579057
+ setDate(baseTime)
+
+ const referenceSigner = randomWallet('Should request a new JWT after expiration')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: {
+ ...settings,
+ services: {
+ ...settings.services!,
+ metadata: {
+ name: 'Test',
+ expiration: 240
+ }
+ }
+ },
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+
+ expect(totalCount).to.equal(1)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ expect(session.services?.status.jwt?.expiration).to.equal(baseTime + 240 - 60)
+
+ // Force expire (1 hour)
+ const newBaseTime = baseTime + 60 * 60
+ setDate(newBaseTime)
+
+ fakeJwt = ethers.hexlify(randomBytes(96, 'Should request a new JWT after expiration 2'))
+
+ await session.services?.getAPIClient()
+
+ expect(totalCount).to.equal(2)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ expect(session.services?.status.jwt?.expiration).to.equal(newBaseTime + 240 - 60)
+ })
+
+ it('Should force min expiration time', async () => {
+ const baseTime = 1613579057
+ setDate(baseTime)
+
+ const referenceSigner = randomWallet('Should force min expiration time')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: {
+ ...settings,
+ services: {
+ ...settings.services!,
+ metadata: {
+ name: 'Test',
+ expiration: 1
+ }
+ }
+ },
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => undefined,
+ editConfigOnMigration: config => config
+ })
+
+ await session.services?._initialAuthRequest
+
+ expect(totalCount).to.equal(1)
+ expect(await session.services?.status.jwt?.token).to.equal(fakeJwt)
+ expect(session.services?.status.jwt?.expiration).to.equal(baseTime + 120 - 60)
+ })
+ })
+ })
+
+ describe('ETHAuth proof validation', () => {
+ it('Should validate an ETHAuth signature by an undeployed wallet', async () => {
+ const signer = randomWallet('Should validate an ETHAuth signature by an undeployed wallet')
+ const config = {
+ threshold: 1,
+ checkpoint: Math.floor(now() / 1000),
+ signers: [{ address: signer.address, weight: 1 }]
+ }
+ const account = await Account.new({
+ config,
+ tracker,
+ contexts,
+ orchestrator: new Orchestrator([signer]),
+ networks
+ })
+
+ // begin by setting the parameters of the ETHAuth proof
+ const proof = new Proof({ address: account.address })
+ proof.claims.app = 'Should validate an ETHAuth signature by an undeployed wallet'
+ proof.claims.iat = Math.floor(now() / 1000) // seconds since epoch, or better yet, proof.setIssuedAtNow()
+ proof.claims.exp = proof.claims.iat + 3600 // seconds since epoch, or better yet, proof.setExpiryIn(3600)
+
+ // create an EIP-6492-compatible ETHAuth proof signature of the proof's message digest
+ proof.signature = await account.signDigest(proof.messageDigest(), ethnode.chainId!, true, 'eip6492')
+ // an EIP-6492 signature for an undeployed wallet always ends with the EIP-6492 suffix
+ expect(proof.signature.endsWith(commons.EIP6492.EIP_6492_SUFFIX.slice(2))).to.be.true
+
+ // create an EIP-6492-aware ETHAuth proof validator
+ const validator = ValidateSequenceWalletProof(
+ () => new commons.reader.OnChainReader(ethnode.provider!),
+ tracker,
+ contexts[2]
+ )
+ const ethauth = new ETHAuth(validator)
+ await ethauth.configJsonRpcProvider(ethnode.providerUrl!)
+
+ // proofs can be encoded to and decoded from strings like so
+ const proofString = await ethauth.encodeProof(proof)
+ const decodedProof = await ethauth.decodeProof(proofString)
+
+ // decoded proofs can be validated like so
+ expect(ethauth.validateProof(decodedProof)).to.eventually.be.true
+ })
+ })
+ describe('session without services', () => {
+ let noServiceSettings: SessionSettings
+
+ before(() => {
+ noServiceSettings = {
+ ...simpleSettings,
+ services: undefined
+ }
+ })
+
+ it('should open a session without services', async () => {
+ const referenceSigner = randomWallet('should open a session without services')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: noServiceSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => {
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ expect(session.services).to.be.undefined
+ })
+
+ it('should dump a session without services', async () => {
+ const referenceSigner = randomWallet('should dump a session without services')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: noServiceSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => {
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ const dump = await session.dump()
+ expect(dump).to.not.be.undefined
+ expect(dump.jwt).to.be.undefined
+ expect(dump.metadata).to.be.undefined
+ })
+
+ it('should load dump without services', async () => {
+ const referenceSigner = randomWallet('should load dump without services')
+ orchestrator.setSigners([referenceSigner])
+
+ const session = await Session.open({
+ settings: noServiceSettings,
+ orchestrator,
+ referenceSigner: referenceSigner.address,
+ addSigners: [{ address: referenceSigner.address, weight: 1 }],
+ threshold: 1,
+ selectWallet: async () => {
+ return undefined
+ },
+ editConfigOnMigration: config => config
+ })
+
+ const dump = await session.dump()
+ const newSession = await Session.load({
+ orchestrator,
+ settings: noServiceSettings,
+ dump: dump,
+ editConfigOnMigration: config => config
+ })
+
+ expect(newSession.services).to.be.undefined
+ })
+ })
+
+ describe('single signer session', () => {
+ it('should create a new single signer session', async () => {
+ const signer = randomWallet('should create a new single signer session')
+
+ const session = await Session.singleSigner({
+ settings: simpleSettings,
+ signer: signer,
+ projectAccessKey: ''
+ })
+
+ expect(session.account.address).to.not.be.undefined
+
+ const status = await session.account.status(networks[0].chainId)
+ const config = status.config as v2.config.WalletConfig
+
+ expect(config.threshold).to.equal(1)
+ expect(v2.config.isSignerLeaf(config.tree)).to.be.true
+ expect(config.tree as v2.config.SignerLeaf).to.deep.equal({
+ weight: 1,
+ address: signer.address
+ })
+ })
+
+ it('should open same single signer session twice', async () => {
+ const signer = randomWallet('should open same single signer session twice')
+
+ const session1 = await Session.singleSigner({
+ settings: simpleSettings,
+ signer: signer,
+ projectAccessKey: ''
+ })
+
+ const address1 = session1.account.address
+ const status1 = await session1.account.status(networks[0].chainId)
+
+ const session2 = await Session.singleSigner({
+ settings: simpleSettings,
+ signer: signer,
+ projectAccessKey: ''
+ })
+
+ const address2 = session2.account.address
+ const status2 = await session2.account.status(networks[0].chainId)
+
+ expect(address1).to.equal(address2)
+
+ // should not change the config!
+ expect(status1.config).to.deep.equal(status2.config)
+ })
+
+ it('should send a transaction from a single signer session', async () => {
+ const signer = randomWallet('should send a transaction from a single signer session')
+
+ const session = await Session.singleSigner({
+ settings: simpleSettings,
+ signer: signer,
+ projectAccessKey: ''
+ })
+
+ const receipt = await session.account.sendTransaction(
+ {
+ to: ethers.Wallet.createRandom().address
+ },
+ networks[0].chainId
+ )
+
+ expect(receipt?.hash).to.not.be.undefined
+ })
+ })
+})
+
+let nowCalls = 0
+function now(): number {
+ if (deterministic) {
+ return Date.parse('2023-02-14T00:00:00.000Z') + 1000 * nowCalls++
+ } else {
+ return Date.now()
+ }
+}
+
+function randomWallet(entropy: number | string): ethers.Wallet {
+ return new ethers.Wallet(ethers.hexlify(randomBytes(32, entropy)))
+}
+
+function randomBytes(length: number, entropy: number | string): Uint8Array {
+ if (deterministic) {
+ let bytes = ''
+ while (bytes.length < 2 * length) {
+ bytes += ethers.id(`${bytes}${entropy}`).slice(2)
+ }
+ return ethers.getBytes(`0x${bytes.slice(0, 2 * length)}`)
+ } else {
+ return ethers.randomBytes(length)
+ }
+}
diff --git a/packages/auth/tests/utils/index.ts b/packages/auth/tests/utils/index.ts
new file mode 100644
index 0000000000..8c4c6f9990
--- /dev/null
+++ b/packages/auth/tests/utils/index.ts
@@ -0,0 +1,33 @@
+export function delay(time: number): Promise {
+ return new Promise(solve => setTimeout(solve, time))
+}
+
+/**
+ * @param {Date} expected The date to which we want to freeze time
+ * @returns {Function} Call to remove Date mocking
+ */
+export const mockDate = (expected: Date): (() => void) => {
+ const _Date = Date
+
+ // If any Date or number is passed to the constructor
+ // use that instead of our mocked date
+ function MockDate(mockOverride?: Date | number) {
+ return new _Date(mockOverride || expected)
+ }
+
+ MockDate.UTC = _Date.UTC
+ MockDate.parse = _Date.parse
+ MockDate.now = () => expected.getTime()
+ // Give our mock Date has the same prototype as Date
+ // Some libraries rely on this to identify Date objects
+ MockDate.prototype = _Date.prototype
+
+ // Our mock is not a full implementation of Date
+ // Types will not match but it's good enough for our tests
+ global.Date = MockDate as any
+
+ // Callback function to remove the Date mock
+ return () => {
+ global.Date = _Date
+ }
+}
diff --git a/packages/builder/README.md b/packages/builder/README.md
new file mode 100644
index 0000000000..4c74ce6166
--- /dev/null
+++ b/packages/builder/README.md
@@ -0,0 +1,4 @@
+@0xsequence/builder
+===================
+
+See [0xsequence project page](https://github.com/0xsequence/sequence.js).
diff --git a/packages/builder/package.json b/packages/builder/package.json
new file mode 100644
index 0000000000..7473909010
--- /dev/null
+++ b/packages/builder/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@0xsequence/builder",
+ "version": "2.3.43",
+ "description": "builder sub-package for Sequence",
+ "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/builder",
+ "source": "src/index.ts",
+ "main": "dist/0xsequence-builder.cjs.js",
+ "module": "dist/0xsequence-builder.esm.js",
+ "author": "Horizon Blockchain Games",
+ "license": "Apache-2.0",
+ "scripts": {
+ "test": "echo",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {},
+ "peerDependencies": {},
+ "devDependencies": {},
+ "files": [
+ "src",
+ "dist"
+ ]
+}
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
new file mode 100644
index 0000000000..a2911e2304
--- /dev/null
+++ b/packages/cli/CHANGELOG.md
@@ -0,0 +1,449 @@
+# @wagmi/cli
+
+## 2.3.1
+
+### Patch Changes
+
+- [#4655](https://github.com/wevm/wagmi/pull/4655) [`43241c8417f3c342036bb46ec8e507d052ae2691`](https://github.com/wevm/wagmi/commit/43241c8417f3c342036bb46ec8e507d052ae2691) Thanks [@tmm](https://github.com/tmm)! - Bumped internal deps.
+
+## 2.3.0
+
+### Minor Changes
+
+- [#4629](https://github.com/wevm/wagmi/pull/4629) [`66dec7d75d580b3121ebc7e8162c1f9ae37cfd41`](https://github.com/wevm/wagmi/commit/66dec7d75d580b3121ebc7e8162c1f9ae37cfd41) Thanks [@allezxandre](https://github.com/allezxandre)! - Upgraded to Sourcify v2 API in `sourcify` plugin
+
+## 2.2.1
+
+### Patch Changes
+
+- [`7b0dbe3886c1a7c6dbbdab945d7436ec20ad8f93`](https://github.com/wevm/wagmi/commit/7b0dbe3886c1a7c6dbbdab945d7436ec20ad8f93) Thanks [@tmm](https://github.com/tmm)! - Updated block explorer chains.
+
+## 2.2.0
+
+### Minor Changes
+
+- [#4503](https://github.com/wevm/wagmi/pull/4503) [`8fce8a6f97aa2ee5fd1bda6a3ece422b10324b5a`](https://github.com/wevm/wagmi/commit/8fce8a6f97aa2ee5fd1bda6a3ece422b10324b5a) Thanks [@tmm](https://github.com/tmm)! - Updated Etherscan Plugin to use Etherscan API v2.
+
+- [#4507](https://github.com/wevm/wagmi/pull/4507) [`6f09cc57935891e1c67d6df3459f6998985c69dc`](https://github.com/wevm/wagmi/commit/6f09cc57935891e1c67d6df3459f6998985c69dc) Thanks [@tmm](https://github.com/tmm)! - Added `tryFetchProxyImplementation` flag to Etherscan Plugin to enable fetching the implementation ABI instead of the proxy ABI.
+
+## 2.1.22
+
+### Patch Changes
+
+- [#4462](https://github.com/wevm/wagmi/pull/4462) [`0b2238d27cecbcd33aee64fb0e30ddc18b6ddf74`](https://github.com/wevm/wagmi/commit/0b2238d27cecbcd33aee64fb0e30ddc18b6ddf74) Thanks [@groninge01](https://github.com/groninge01)! - Added Sonic to Etherscan plugin.
+
+## 2.1.21
+
+### Patch Changes
+
+- [#4457](https://github.com/wevm/wagmi/pull/4457) [`21ec74da7f93fc13e253d7b35ddeddc23422a6c1`](https://github.com/wevm/wagmi/commit/21ec74da7f93fc13e253d7b35ddeddc23422a6c1) Thanks [@tmm](https://github.com/tmm)! - Removed internal dependency.
+
+## 2.1.20
+
+### Patch Changes
+
+- [#4450](https://github.com/wevm/wagmi/pull/4450) [`7b9a6bb35881b657a00bdd7ccd7edea32660f5bf`](https://github.com/wevm/wagmi/commit/7b9a6bb35881b657a00bdd7ccd7edea32660f5bf) Thanks [@tmm](https://github.com/tmm)! - Removed internal usage of `fs-extra`.
+
+## 2.1.19
+
+### Patch Changes
+
+- [#4449](https://github.com/wevm/wagmi/pull/4449) [`3fa5c238baa13d948e89974b0bb8530f8fa264fd`](https://github.com/wevm/wagmi/commit/3fa5c238baa13d948e89974b0bb8530f8fa264fd) Thanks [@tmm](https://github.com/tmm)! - Removed `ora` for `nanospinner`.
+
+## 2.1.18
+
+### Patch Changes
+
+- [#4399](https://github.com/wevm/wagmi/pull/4399) [`bc18673e4c272e3b60a1b6016934fe3fbeb6d93a`](https://github.com/wevm/wagmi/commit/bc18673e4c272e3b60a1b6016934fe3fbeb6d93a) Thanks [@tmm](https://github.com/tmm)! - Added Polygon Amoy to Sourcify and Etherscan plugins.
+
+## 2.1.17
+
+### Patch Changes
+
+- [#4370](https://github.com/wevm/wagmi/pull/4370) [`cb58b1ea3ad40e77210f24eb598f9d2306db998c`](https://github.com/wevm/wagmi/commit/cb58b1ea3ad40e77210f24eb598f9d2306db998c) Thanks [@talentlessguy](https://github.com/talentlessguy)! - Bumped internal dependencies.
+
+## 2.1.16
+
+### Patch Changes
+
+- [#4224](https://github.com/wevm/wagmi/pull/4224) [`b0eb89c2a0781bb3434996fa53ee7ceb3bb44db9`](https://github.com/wevm/wagmi/commit/b0eb89c2a0781bb3434996fa53ee7ceb3bb44db9) Thanks [@roderik](https://github.com/roderik)! - Fixed package detection for Bun.
+
+## 2.1.15
+
+### Patch Changes
+
+- [`0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e`](https://github.com/wevm/wagmi/commit/0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e) Thanks [@tmm](https://github.com/tmm)! - Improved TypeScript `'exactOptionalPropertyTypes'` support.
+
+## 2.1.14
+
+### Patch Changes
+
+- [#4120](https://github.com/wevm/wagmi/pull/4120) [`59407bf1276a46e6f1f22a370dde71c92524cd0f`](https://github.com/wevm/wagmi/commit/59407bf1276a46e6f1f22a370dde71c92524cd0f) Thanks [@tmm](https://github.com/tmm)! - Fixed an issue where the Foundry and Hardhat plugins' `exclude` option was ignored.
+
+## 2.1.13
+
+### Patch Changes
+
+- [`7264d1f450727f6ba0cbea8aa1c7a83e22a5bf20`](https://github.com/wevm/wagmi/commit/7264d1f450727f6ba0cbea8aa1c7a83e22a5bf20) Thanks [@tmm](https://github.com/tmm)! - Fixed generate not exiting for long-running processes.
+
+## 2.1.12
+
+### Patch Changes
+
+- [`ac038b29623ccb0d2fee40d9f943c8df28138dac`](https://github.com/wevm/wagmi/commit/ac038b29623ccb0d2fee40d9f943c8df28138dac) Thanks [@tmm](https://github.com/tmm)! - Updated Foundry default excludes.
+
+## 2.1.11
+
+### Patch Changes
+
+- [#4084](https://github.com/wevm/wagmi/pull/4084) [`b54203bf8fa911e6f14b9675980cf38fb95d7d3e`](https://github.com/wevm/wagmi/commit/b54203bf8fa911e6f14b9675980cf38fb95d7d3e) Thanks [@tmm](https://github.com/tmm)! - Reduced internal dependencies.
+
+## 2.1.10
+
+### Patch Changes
+
+- [#4051](https://github.com/wevm/wagmi/pull/4051) [`275e78b0e585f0ec9da2f9661ce9990aed18e9f4`](https://github.com/wevm/wagmi/commit/275e78b0e585f0ec9da2f9661ce9990aed18e9f4) Thanks [@tmm](https://github.com/tmm)! - Updated Sourcify plugin internals.
+
+## 2.1.9
+
+### Patch Changes
+
+- [`f9346dbcffaf57a8949cb96e43df111a89d733b1`](https://github.com/wevm/wagmi/commit/f9346dbcffaf57a8949cb96e43df111a89d733b1) Thanks [@tmm](https://github.com/tmm)! - Updated Foundry plugin default excludes.
+
+## 2.1.8
+
+### Patch Changes
+
+- [#3957](https://github.com/wevm/wagmi/pull/3957) [`7d00680f73b090eb34af928ae74277bec1973953`](https://github.com/wevm/wagmi/commit/7d00680f73b090eb34af928ae74277bec1973953) Thanks [@cstoneham](https://github.com/cstoneham)! - Added Blast to Etherscan plugin
+
+## 2.1.7
+
+### Patch Changes
+
+- [`1122678bbad0232590bd4060a73752de2c84982d`](https://github.com/wevm/wagmi/commit/1122678bbad0232590bd4060a73752de2c84982d) Thanks [@tmm](https://github.com/tmm)! - Published unpublished changes in [#3756](https://github.com/wevm/wagmi/pull/3756).
+
+## 2.1.6
+
+### Patch Changes
+
+- [#3756](https://github.com/wevm/wagmi/pull/3756) [`c7d6f467`](https://github.com/wevm/wagmi/commit/c7d6f4679125fd2f6cca5b5ef362abf47e37f934) Thanks [@jrfrantz](https://github.com/jrfrantz)! - Added basescan to etherscan cli plugin
+
+## 2.1.5
+
+### Patch Changes
+
+- [`e1ca4e63`](https://github.com/wevm/wagmi/commit/e1ca4e637ae6cec7f5902b0a2c0e0efc3b751a1d) Thanks [@tmm](https://github.com/tmm)! - Added title to CLI process.
+
+- [#3723](https://github.com/wevm/wagmi/pull/3723) [`d6bc98ca`](https://github.com/wevm/wagmi/commit/d6bc98ca0ce9081f192f62e0b0fcfea3cb07a2bb) Thanks [@leecobaby](https://github.com/leecobaby)! - Broadened TypeScript detection.
+
+## 2.1.4
+
+### Patch Changes
+
+- [#3737](https://github.com/wevm/wagmi/pull/3737) [`11020fed`](https://github.com/wevm/wagmi/commit/11020fedfc68639eace241e328331cff43bf91af) Thanks [@oskarvu](https://github.com/oskarvu)! - Added Gnosis to Etherscan plugin.
+
+## 2.1.3
+
+### Patch Changes
+
+- [#3660](https://github.com/wevm/wagmi/pull/3660) [`11a22a23`](https://github.com/wevm/wagmi/commit/11a22a23d88c025cde9c91610e9ddf62cd4fa650) Thanks [@JazzBashara](https://github.com/JazzBashara)! - Replaced SnowTrace with SnowScan for the Etherscan plugin
+
+## 2.1.2
+
+### Patch Changes
+
+- [#3641](https://github.com/wevm/wagmi/pull/3641) [`0a866403`](https://github.com/wevm/wagmi/commit/0a866403182ea6b8ba7f976c45be294e48fb7de8) Thanks [@cmwhited](https://github.com/cmwhited)! - Added Arbitrum Sepolia testnet to Etherscan plugin
+
+- [#3633](https://github.com/wevm/wagmi/pull/3633) [`a1d3d1ab`](https://github.com/wevm/wagmi/commit/a1d3d1ab2b023c61c0dbb5d7bf867a9fca673630) Thanks [@pegahcarter](https://github.com/pegahcarter)! - Added Fraxtal to Etherscan plugin
+
+- [#3616](https://github.com/wevm/wagmi/pull/3616) [`2a9f4473`](https://github.com/wevm/wagmi/commit/2a9f4473adc5bcdddf388389387ed5459583769e) Thanks [@petermazzocco](https://github.com/petermazzocco)! - Added Holesky Testnet to Etherscan Plugin
+
+## 2.1.1
+
+### Patch Changes
+
+- [#3579](https://github.com/wevm/wagmi/pull/3579) [`a057919c`](https://github.com/wevm/wagmi/commit/a057919ca3942adeed90af2e343403dc5274e84c) Thanks [@FaisalAli19](https://github.com/FaisalAli19)! - Added Optimism Sepolia Etherscan support
+
+## 2.1.0
+
+### Minor Changes
+
+- [#3506](https://github.com/wevm/wagmi/pull/3506) [`134eb4a1`](https://github.com/wevm/wagmi/commit/134eb4a1e0e29aab87bd5c7cdf05b06dfd7c4fc4) Thanks [@vmaark](https://github.com/vmaark)! - Added resolution of TypeScript Wagmi CLI config to determine if TypeScript generated output is allowed.
+
+## 2.0.4
+
+### Patch Changes
+
+- [#3462](https://github.com/wevm/wagmi/pull/3462) [`d25573ea`](https://github.com/wevm/wagmi/commit/d25573ea03358f967953e37c176b220a7b341769) Thanks [@cruzdanilo](https://github.com/cruzdanilo)! - Upgraded dependencies
+
+## 2.0.3
+
+### Patch Changes
+
+- [#3410](https://github.com/wevm/wagmi/pull/3410) [`55e31c3e`](https://github.com/wevm/wagmi/commit/55e31c3e96c2cbd1d9eb44e5a89f4365489c8310) Thanks [@o-az](https://github.com/o-az)! - Fixed actions plugin issue where `functionName` was used instead of `eventName` for generated contract event actions.
+
+## 2.0.2
+
+### Patch Changes
+
+- [#3371](https://github.com/wevm/wagmi/pull/3371) [`8294d9e5`](https://github.com/wevm/wagmi/commit/8294d9e5b358018ba869b2018cd7ed95462e021f) Thanks [@iceanddust](https://github.com/iceanddust)! - Fixed prop name when generating contract event watch hooks
+
+## 2.0.1
+
+### Major Changes
+
+- [#3333](https://github.com/wevm/wagmi/pull/3333) [`b3a0baaa`](https://github.com/wevm/wagmi/commit/b3a0baaaee7decf750d376aab2502cd33ca4825a) Thanks [@tmm](https://github.com/tmm)! - Wagmi CLI 2.0.
+
+ [Breaking Changes & Migration Guide](https://wagmi.sh/cli/guides/migrate-from-v1-to-v2)
+
+## 1.5.2
+
+### Patch Changes
+
+- [#3051](https://github.com/wagmi-dev/wagmi/pull/3051) [`4704d351`](https://github.com/wagmi-dev/wagmi/commit/4704d351164d39704a4e375c06525554fcc8340e) Thanks [@oxSaturn](https://github.com/oxSaturn)! - Fixed ESM require issue for prettier
+
+## 1.5.1
+
+### Patch Changes
+
+- [#3035](https://github.com/wagmi-dev/wagmi/pull/3035) [`187bf96c`](https://github.com/wagmi-dev/wagmi/commit/187bf96c9fd31675b9d17a7cb4d4e24eea3fa777) Thanks [@cruzdanilo](https://github.com/cruzdanilo)! - ignore foundry invariant lib
+
+## 1.5.0
+
+### Minor Changes
+
+- [#2956](https://github.com/wevm/wagmi/pull/2956) [`2abeb285`](https://github.com/wevm/wagmi/commit/2abeb285674af3e539cc2550b1f5027b1eb0c895) Thanks [@tmm](https://github.com/tmm)! - Replaced `@wagmi/chains` with `viem/chains`.
+
+## 1.4.1
+
+### Patch Changes
+
+- [#2962](https://github.com/wevm/wagmi/pull/2962) [`8ac5b572`](https://github.com/wevm/wagmi/commit/8ac5b57254f77eeb0e07dd83f7d49f396d4581d8) Thanks [@tmm](https://github.com/tmm)! - Fixed esbuild version
+
+## 1.4.0
+
+### Minor Changes
+
+- [#2946](https://github.com/wevm/wagmi/pull/2946) [`1c3228bf`](https://github.com/wevm/wagmi/commit/1c3228bf3fe99b0900b2c9a223c9b81c70bdcd90) Thanks [@tomquirk](https://github.com/tomquirk)! - Added default chain ID to generated `useContractRead` hook.
+
+### Patch Changes
+
+- [#2547](https://github.com/wevm/wagmi/pull/2547) [`8c3889fe`](https://github.com/wevm/wagmi/commit/8c3889fe82c5a1ddb29e74e3863ea6f4917b777a) Thanks [@Iamshankhadeep](https://github.com/Iamshankhadeep)! - Deterministic CLI output
+
+- [#2958](https://github.com/wevm/wagmi/pull/2958) [`b31f36d5`](https://github.com/wevm/wagmi/commit/b31f36d522a634f53d44349d6a9ea47f59d84d7a) Thanks [@tmm](https://github.com/tmm)! - Removed generated file header
+
+- [#2960](https://github.com/wevm/wagmi/pull/2960) [`5d4c4592`](https://github.com/wevm/wagmi/commit/5d4c4592009568cd0b096906a424f27469721a42) Thanks [@tmm](https://github.com/tmm)! - Updated esbuild version
+
+## 1.3.0
+
+### Minor Changes
+
+- [#2616](https://github.com/wevm/wagmi/pull/2616) [`c282a8f7`](https://github.com/wevm/wagmi/commit/c282a8f786d57fec77c931fe99dc20220e843bc8) Thanks [@portdeveloper](https://github.com/portdeveloper)! - Added sepolia chain id
+
+## 1.2.1
+
+### Patch Changes
+
+- [#2607](https://github.com/wevm/wagmi/pull/2607) [`79335b4c`](https://github.com/wevm/wagmi/commit/79335b4c0fcd5e8152a2a1d28314c634db9d9cbf) Thanks [@roninjin10](https://github.com/roninjin10)! - Fixed opitmism goerli chain id
+
+## 1.2.0
+
+### Minor Changes
+
+- [#2536](https://github.com/wevm/wagmi/pull/2536) [`85e9760a`](https://github.com/wevm/wagmi/commit/85e9760a140cb169ac6236d9466b96e2105dd193) Thanks [@tmm](https://github.com/tmm)! - Changed `Address` type import from ABIType to viem.
+
+## 1.1.0
+
+### Minor Changes
+
+- [#2482](https://github.com/wevm/wagmi/pull/2482) [`8764b54a`](https://github.com/wevm/wagmi/commit/8764b54aab68020063946112e8fe52aff650c99c) Thanks [@tmm](https://github.com/tmm)! - Bumped minimum TypeScript version to v5.0.4.
+
+### Patch Changes
+
+- [#2484](https://github.com/wevm/wagmi/pull/2484) [`3adf1f4f`](https://github.com/wevm/wagmi/commit/3adf1f4feab863cb7b5d52c81ad46f7e4eb56f09) Thanks [@jxom](https://github.com/jxom)! - Updated `abitype` to 0.8.7
+
+- [#2484](https://github.com/wevm/wagmi/pull/2484) [`3adf1f4f`](https://github.com/wevm/wagmi/commit/3adf1f4feab863cb7b5d52c81ad46f7e4eb56f09) Thanks [@jxom](https://github.com/jxom)! - Updated references.
+
+## 1.0.3
+
+### Patch Changes
+
+- [#2441](https://github.com/wevm/wagmi/pull/2441) [`326edee4`](https://github.com/wevm/wagmi/commit/326edee4bc85db84a7a4e3768e33785849ab8d8e) Thanks [@tmm](https://github.com/tmm)! - Fixed internal type issue
+
+## 1.0.2
+
+### Patch Changes
+
+- [#2430](https://github.com/wevm/wagmi/pull/2430) [`71d92029`](https://github.com/wevm/wagmi/commit/71d92029ee4344842cd41698858a330fee95b6e0) Thanks [@tmm](https://github.com/tmm)! - Added message when command is not found.
+
+## 1.0.1
+
+### Patch Changes
+
+- [`ea651cd7`](https://github.com/wevm/wagmi/commit/ea651cd7fc75b7866272605467db11fd6e1d81af) Thanks [@jxom](https://github.com/jxom)! - Downgraded abitype.
+
+## 1.0.0
+
+### Major Changes
+
+- [#2235](https://github.com/wevm/wagmi/pull/2235) [`5be0655c`](https://github.com/wevm/wagmi/commit/5be0655c8e48b25d38009022461fbf611af54349) Thanks [@jxom](https://github.com/jxom)! - Released v1. Read [Migration Guide](https://next.wagmi.sh/react/migration-guide#1xx-breaking-changes).
+
+## 1.0.0-next.7
+
+### Patch Changes
+
+- Fixed react plugin generic.
+
+## 1.0.0-next.6
+
+### Major Changes
+
+- Updated references.
+
+## 1.0.0-next.5
+
+### Major Changes
+
+- Added `config.setConnectors`
+
+## 1.0.0-next.4
+
+### Major Changes
+
+- Updated viem.
+ Removed `goerli` export from main entrypoint.
+
+## 1.0.0-next.3
+
+### Major Changes
+
+- Updated references.
+
+## 1.0.0-next.2
+
+### Major Changes
+
+- Updated dependencies
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @wagmi/chains@1.0.0-next.0
+
+## 1.0.0-next.1
+
+### Major Changes
+
+- updated viem
+
+## 1.0.0-next.0
+
+### Major Changes
+
+- [`a7dda00c`](https://github.com/wevm/wagmi/commit/a7dda00c5b546f8b2c42b527e4d9ac1b9e9ab1fb) Thanks [@jxom](https://github.com/jxom)! - Released v1.
+
+### Patch Changes
+
+- Updated dependencies [[`a7dda00c`](https://github.com/wevm/wagmi/commit/a7dda00c5b546f8b2c42b527e4d9ac1b9e9ab1fb)]:
+ - @wagmi/core@1.0.0-next.0
+ - wagmi@1.0.0-next.0
+
+## 0.1.15
+
+### Patch Changes
+
+- [#2145](https://github.com/wevm/wagmi/pull/2145) [`2520743c`](https://github.com/wevm/wagmi/commit/2520743c417a158a00d5edca13a9aa92cefb0cfd) Thanks [@tmm](https://github.com/tmm)! - Fixed issue using Hardhat Plugin with npm.
+
+## 0.1.14
+
+### Patch Changes
+
+- [#2039](https://github.com/wevm/wagmi/pull/2039) [`bac893ab`](https://github.com/wevm/wagmi/commit/bac893ab26012d4d8741c4f80e8b8813aee26f0c) Thanks [@tmm](https://github.com/tmm)! - Updated references.
+
+- [#2039](https://github.com/wevm/wagmi/pull/2039) [`bac893ab`](https://github.com/wevm/wagmi/commit/bac893ab26012d4d8741c4f80e8b8813aee26f0c) Thanks [@tmm](https://github.com/tmm)! - Fixed Actions plugin `overridePackageName` option.
+
+## 0.1.13
+
+### Patch Changes
+
+- [#2000](https://github.com/wevm/wagmi/pull/2000) [`01254765`](https://github.com/wevm/wagmi/commit/01254765eb37b77aca26500c00c721f08a260912) Thanks [@tmm](https://github.com/tmm)! - Fixed React plugin name conflict.
+
+## 0.1.12
+
+### Patch Changes
+
+- [#1992](https://github.com/wevm/wagmi/pull/1992) [`efc93cad`](https://github.com/wevm/wagmi/commit/efc93cadacdb9c9960644dabe4ae837d384df52b) Thanks [@tmm](https://github.com/tmm)! - Refactored internals from ethers to viem.
+
+## 0.1.11
+
+### Patch Changes
+
+- [#1916](https://github.com/wevm/wagmi/pull/1916) [`950490fd`](https://github.com/wevm/wagmi/commit/950490fd132b3fb5b3455e77b58d70f134b8e5c9) Thanks [@technophile-04](https://github.com/technophile-04)! - Updated React plugin to use `Address` type instead of hardcoding `` `0x{string}` ``.
+
+## 0.1.10
+
+### Patch Changes
+
+- [#1892](https://github.com/wevm/wagmi/pull/1892) [`d3d6973b`](https://github.com/wevm/wagmi/commit/d3d6973ba9407e490140d2434eb83aad88d6e10d) Thanks [@greg-schrammel](https://github.com/greg-schrammel)! - Fixed generated read hooks `select` type.
+
+## 0.1.9
+
+### Patch Changes
+
+- [#1886](https://github.com/wevm/wagmi/pull/1886) [`36e119c6`](https://github.com/wevm/wagmi/commit/36e119c6d4bc28a7ae15c9602d0c613bc9681356) Thanks [@roninjin10](https://github.com/roninjin10)! - Fixed package detection for yarn^3
+
+## 0.1.8
+
+### Patch Changes
+
+- [#1884](https://github.com/wevm/wagmi/pull/1884) [`cc03bb44`](https://github.com/wevm/wagmi/commit/cc03bb44268874f95203de67f6d32586e34c0857) Thanks [@roninjin10](https://github.com/roninjin10)! - Added better compatibility for yarn@^3 in `@wagmi/cli`.
+
+## 0.1.7
+
+### Patch Changes
+
+- [#1841](https://github.com/wevm/wagmi/pull/1841) [`cb707f01`](https://github.com/wevm/wagmi/commit/cb707f01cbdcc62a70cf5c8a162d77948d6b6a56) Thanks [@tmm](https://github.com/tmm)! - Added [Sourcify](https://sourcify.dev) CLI plugin.
+
+## 0.1.6
+
+### Patch Changes
+
+- [#1803](https://github.com/wevm/wagmi/pull/1803) [`09b13538`](https://github.com/wevm/wagmi/commit/09b13538abcde879034293cae39551c30cc81445) Thanks [@shotaronowhere](https://github.com/shotaronowhere)! - Swapped deprecated Arbitrum Rinkeby for Arbitrum Goerli URL for Etherscan Plugin.
+
+## 0.1.5
+
+### Patch Changes
+
+- [#1788](https://github.com/wevm/wagmi/pull/1788) [`c3e16d82`](https://github.com/wevm/wagmi/commit/c3e16d82c9c39b8b1c2f3c51037e11d642a20cd6) Thanks [@tmm](https://github.com/tmm)! - Fixed CLI import
+
+## 0.1.4
+
+### Patch Changes
+
+- [#1779](https://github.com/wevm/wagmi/pull/1779) [`97346750`](https://github.com/wevm/wagmi/commit/973467505dc2bb46198a3e9fe6072306170d24c0) Thanks [@tmm](https://github.com/tmm)! - Made `project` optional for Foundry plugin
+
+## 0.1.3
+
+### Patch Changes
+
+- [#1754](https://github.com/wevm/wagmi/pull/1754) [`298728b5`](https://github.com/wevm/wagmi/commit/298728b5918fa15b6b5b082597204a268d4b01f1) Thanks [@tmm](https://github.com/tmm)! - Updated project resolution for Foundry and Hardhat plugins.
+
+- [#1738](https://github.com/wevm/wagmi/pull/1738) [`37c221d0`](https://github.com/wevm/wagmi/commit/37c221d0f4d175084e23a6b172d72f177bfa0c81) Thanks [@roninjin10](https://github.com/roninjin10)! - Added automatic Foundry config detection for artifacts directory.
+
+## 0.1.2
+
+### Patch Changes
+
+- [#1743](https://github.com/wevm/wagmi/pull/1743) [`379315fa`](https://github.com/wevm/wagmi/commit/379315fa359c3118b5d200ec50db3812b0cdd984) Thanks [@kyscott18](https://github.com/kyscott18)! - Add celoscan to `etherscan` plugin
+
+## 0.1.1
+
+### Patch Changes
+
+- [#1736](https://github.com/wevm/wagmi/pull/1736) [`7c43e431`](https://github.com/wevm/wagmi/commit/7c43e431e2eb970610cc6490cee6a4093655a683) Thanks [@tmm](https://github.com/tmm)! - Fixed generated address object key type.
+
+## 0.1.0
+
+### Minor Changes
+
+- [#1732](https://github.com/wevm/wagmi/pull/1732) [`01e21897`](https://github.com/wevm/wagmi/commit/01e2189747a5c22dc758c6d719b4145adc2a643c) Thanks [@tmm](https://github.com/tmm)! - Initial release
diff --git a/packages/cli/README.md b/packages/cli/README.md
new file mode 100644
index 0000000000..640acb22d2
--- /dev/null
+++ b/packages/cli/README.md
@@ -0,0 +1,13 @@
+# @wagmi/cli
+
+Manage and generate code from Ethereum ABIs
+
+## Installation
+
+```bash
+pnpm add @wagmi/cli
+```
+
+## Documentation
+
+For documentation and guides, visit [wagmi.sh](https://wagmi.sh).
diff --git a/packages/cli/package.json b/packages/cli/package.json
new file mode 100644
index 0000000000..f3848081e5
--- /dev/null
+++ b/packages/cli/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@wagmi/cli",
+ "description": "Manage and generate code from Ethereum ABIs",
+ "version": "2.3.1",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/wevm/wagmi.git",
+ "directory": "packages/cli"
+ },
+ "scripts": {
+ "build": "pnpm run clean && pnpm run build:esm+types",
+ "build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
+ "check:types": "tsc --noEmit",
+ "clean": "rm -rf dist tsconfig.tsbuildinfo config plugins",
+ "dev": "bun src/cli.ts",
+ "test:build": "publint --strict && attw --pack --ignore-rules cjs-resolves-to-esm"
+ },
+ "files": [
+ "dist/**",
+ "!dist/**/*.tsbuildinfo",
+ "src/**/*.ts",
+ "!src/**/*.test.ts",
+ "!src/**/*.test-d.ts",
+ "/config",
+ "/plugins"
+ ],
+ "bin": {
+ "wagmi": "./dist/esm/cli.js"
+ },
+ "sideEffects": false,
+ "type": "module",
+ "main": "./dist/esm/exports/index.js",
+ "types": "./dist/types/exports/index.d.ts",
+ "typings": "./dist/types/exports/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/exports/index.d.ts",
+ "default": "./dist/esm/exports/index.js"
+ },
+ "./config": {
+ "types": "./dist/types/exports/config.d.ts",
+ "default": "./dist/esm/exports/config.js"
+ },
+ "./plugins": {
+ "types": "./dist/types/exports/plugins.d.ts",
+ "default": "./dist/esm/exports/plugins.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "config": ["./dist/types/exports/config.d.ts"],
+ "plugins": ["./dist/types/exports/plugins.d.ts"]
+ }
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ },
+ "dependencies": {
+ "abitype": "^1.0.4",
+ "bundle-require": "^5.1.0",
+ "cac": "^6.7.14",
+ "change-case": "^5.4.4",
+ "chokidar": "4.0.3",
+ "dedent": "^1.5.3",
+ "dotenv": "^16.3.1",
+ "dotenv-expand": "^10.0.0",
+ "esbuild": "~0.27.0",
+ "escalade": "3.2.0",
+ "fdir": "^6.1.1",
+ "nanospinner": "1.2.2",
+ "pathe": "^2.0.3",
+ "picocolors": "^1.0.0",
+ "picomatch": "^4.0.2",
+ "prettier": "^3.0.3",
+ "viem": "2.x",
+ "zod": "^3.22.3"
+ },
+ "devDependencies": {
+ "@types/dedent": "^0.7.2",
+ "@types/node": "^22.14.0",
+ "fixturez": "^1.1.0",
+ "msw": "^2.4.9"
+ },
+ "contributors": ["awkweb.eth ", "jxom.eth "],
+ "funding": "https://github.com/sponsors/wevm",
+ "keywords": ["wagmi", "eth", "ethereum", "dapps", "wallet", "web3", "cli"]
+}
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
new file mode 100644
index 0000000000..551543bb2b
--- /dev/null
+++ b/packages/cli/src/cli.ts
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+import { cac } from 'cac'
+
+import { type Generate, generate } from './commands/generate.js'
+import { type Init, init } from './commands/init.js'
+import * as logger from './logger.js'
+import { version } from './version.js'
+
+const cli = cac('wagmi')
+
+cli
+ .command('generate', 'generate code based on configuration')
+ .option('-c, --config ', '[string] path to config file')
+ .option('-r, --root ', '[string] root path to resolve config from')
+ .option('-w, --watch', '[boolean] watch for changes')
+ .example((name) => `${name} generate`)
+ .action(async (options: Generate) => {
+ await generate(options)
+ if (!options.watch) process.exit(0)
+ })
+
+cli
+ .command('init', 'create configuration file')
+ .option('-c, --config ', '[string] path to config file')
+ .option('-r, --root ', '[string] root path to resolve config from')
+ .example((name) => `${name} init`)
+ .action(async (options: Init) => {
+ await init(options)
+ process.exit(0)
+ })
+
+cli.help()
+cli.version(version)
+
+void (async () => {
+ try {
+ process.title = 'node (wagmi)'
+ } catch {}
+
+ try {
+ // Parse CLI args without running command
+ cli.parse(process.argv, { run: false })
+ if (!cli.matchedCommand) {
+ if (cli.args.length === 0) {
+ if (!cli.options.help && !cli.options.version) cli.outputHelp()
+ } else throw new Error(`Unknown command: ${cli.args.join(' ')}`)
+ }
+ await cli.runMatchedCommand()
+ } catch (error) {
+ logger.error(`\n${(error as Error).message}`)
+ process.exit(1)
+ }
+})()
diff --git a/packages/cli/src/commands/generate.test.ts b/packages/cli/src/commands/generate.test.ts
new file mode 100644
index 0000000000..91e4265216
--- /dev/null
+++ b/packages/cli/src/commands/generate.test.ts
@@ -0,0 +1,409 @@
+import { readFile } from 'node:fs/promises'
+import dedent from 'dedent'
+import { resolve } from 'pathe'
+import { afterEach, beforeEach, expect, test, vi } from 'vitest'
+
+import { createFixture, typecheck, watchConsole } from '../../test/utils.js'
+import { generate } from './generate.js'
+
+let console: ReturnType
+beforeEach(() => {
+ console = watchConsole()
+ vi.useFakeTimers()
+
+ const date = new Date(2023, 0, 30, 12)
+ vi.setSystemTime(date)
+})
+
+afterEach(() => {
+ vi.restoreAllMocks()
+ vi.useRealTimers()
+})
+
+test('generates output', async () => {
+ const { dir } = await createFixture({
+ files: {
+ tsconfig: true,
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.js',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await generate()
+
+ expect(console.formatted).toMatchInlineSnapshot(`
+ "- Validating plugins
+ √ Validating plugins
+ - Resolving contracts
+ √ Resolving contracts
+ - Running plugins
+ √ Running plugins
+ - Writing to generated.js
+ √ Writing to generated.js"
+ `)
+})
+
+test('generates typescript output', async () => {
+ const { dir, paths } = await createFixture({
+ files: {
+ tsconfig: true,
+ 'wagmi.config.ts': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await generate()
+
+ expect(console.formatted).toMatchInlineSnapshot(`
+ "- Validating plugins
+ √ Validating plugins
+ - Resolving contracts
+ √ Resolving contracts
+ - Running plugins
+ √ Running plugins
+ - Writing to generated.ts
+ √ Writing to generated.ts"
+ `)
+ await expect(typecheck(paths.tsconfig)).resolves.toMatchInlineSnapshot('""')
+})
+
+test('generates output with plugin', async () => {
+ const { dir } = await createFixture({
+ files: {
+ tsconfig: true,
+ 'wagmi.config.ts': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ plugins: [
+ {
+ name: 'Test',
+ async run({ contracts, isTypeScript, outputs }) {
+ return {
+ imports: '/* imports test */',
+ prepend: '/* prepend test */',
+ content: '/* content test */',
+ }
+ },
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await generate()
+
+ expect(console.formatted).toMatchInlineSnapshot(`
+ "- Validating plugins
+ √ Validating plugins
+ - Resolving contracts
+ √ Resolving contracts
+ - Running plugins
+ √ Running plugins
+ - Writing to generated.ts
+ √ Writing to generated.ts"
+ `)
+ /* eslint-disable no-irregular-whitespace */
+ await expect(
+ readFile(resolve(dir, 'generated.ts'), 'utf8'),
+ ).resolves.toMatchInlineSnapshot(`
+ "/* imports test */
+
+ /* prepend test */
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Foo
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ export const fooAbi = [] as const
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Test
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /* content test */
+ "
+ `)
+ /* eslint-enable no-irregular-whitespace */
+})
+
+test('behavior: invalid cli options', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(
+ generate({
+ // @ts-expect-error possible to pass untyped options through from cli
+ config: 1,
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: Invalid option
+ - Expected string, received number at \`config\`]
+ `)
+})
+
+test('behavior: config not found', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: Config not found]',
+ )
+})
+
+test('behavior: config not found for path', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ try {
+ await generate({ config: 'wagmi.config.js' })
+ } catch (error) {
+ expect(
+ (error as Error).message.replace(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot('"Config not found at wagmi.config.js"')
+ }
+})
+
+test('behavior: config out not unique', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default [
+ {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ]
+ },
+ {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ },
+ ]
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: out "generated.ts" must be unique.]`,
+ )
+})
+
+test('behavior: config contract names not unique', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: Contract name "Foo" must be unique.]`,
+ )
+})
+
+test('behavior: displays message if no contracts found', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': "export default { out: 'generated.ts' }",
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await generate()
+
+ expect(console.formatted).toMatchInlineSnapshot(
+ `
+ "- Validating plugins
+ √ Validating plugins
+ - Resolving contracts
+ × Resolving contracts
+ No contracts found."
+ `,
+ )
+})
+
+test('behavior: throws when abi is invalid', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [{
+ type: 'function',
+ name: 'balanceOf',
+ stateMutability: 'view',
+ inputs: [{ type: 'address' }],
+ }],
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: Invalid ABI for contract "Foo"
+ - Invalid input at \`[0]\`]
+ `)
+})
+
+test('behavior: throws when address is invalid', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ address: '0xfoo',
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: Invalid address for contract "Foo"
+ - Invalid address]
+ `)
+})
+
+test('behavior: throws when multichain address is invalid', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ address: {
+ 1: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
+ 5: '0xfoo',
+ },
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(generate()).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: Invalid address for contract "Foo"
+ - Invalid address at \`5\`]
+ `)
+})
+
+test('behavior: displays message if using --watch flag without watchers configured', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.js': dedent`
+ export default {
+ out: 'generated.ts',
+ contracts: [
+ {
+ abi: [],
+ name: 'Foo',
+ },
+ ],
+ }
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await generate({ watch: true })
+
+ expect(console.formatted).toMatchInlineSnapshot(`
+ "- Validating plugins
+ √ Validating plugins
+ - Resolving contracts
+ √ Resolving contracts
+ - Running plugins
+ √ Running plugins
+ - Writing to generated.ts
+ √ Writing to generated.ts
+ Used --watch flag, but no plugins are watching."
+ `)
+})
+
+test.todo('behavior: save config file logs change')
+test.todo('behavior: updates on add file')
+test.todo('behavior: updates on change file')
+test.todo('behavior: updates on unlink file')
+test.todo('behavior: runs watch command')
+test.todo('behavior: shuts down watch on SIGINT/SIGTERM')
diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts
new file mode 100644
index 0000000000..e9242092ee
--- /dev/null
+++ b/packages/cli/src/commands/generate.ts
@@ -0,0 +1,411 @@
+import { mkdir, writeFile } from 'node:fs/promises'
+import { Abi as AbiSchema } from 'abitype/zod'
+import { camelCase } from 'change-case'
+import type { ChokidarOptions, FSWatcher } from 'chokidar'
+import { watch } from 'chokidar'
+import { default as dedent } from 'dedent'
+import { basename, dirname, resolve } from 'pathe'
+import pc from 'picocolors'
+import { type Abi, type Address, getAddress } from 'viem'
+import { z } from 'zod'
+
+import type { Contract, ContractConfig, Plugin, Watch } from '../config.js'
+import { fromZodError } from '../errors.js'
+import * as logger from '../logger.js'
+import { findConfig } from '../utils/findConfig.js'
+import { format } from '../utils/format.js'
+import { getAddressDocString } from '../utils/getAddressDocString.js'
+import { getIsUsingTypeScript } from '../utils/getIsUsingTypeScript.js'
+import { resolveConfig } from '../utils/resolveConfig.js'
+
+const Generate = z.object({
+ /** Path to config file */
+ config: z.string().optional(),
+ /** Directory to search for config file */
+ root: z.string().optional(),
+ /** Watch for file system changes to config and plugins */
+ watch: z.boolean().optional(),
+})
+export type Generate = z.infer
+
+export async function generate(options: Generate = {}) {
+ // Validate command line options
+ try {
+ await Generate.parseAsync(options)
+ } catch (error) {
+ if (error instanceof z.ZodError)
+ throw fromZodError(error, { prefix: 'Invalid option' })
+ throw error
+ }
+
+ // Get cli config file
+ const configPath = await findConfig(options)
+ if (!configPath) {
+ if (options.config)
+ throw new Error(`Config not found at ${pc.gray(options.config)}`)
+ throw new Error('Config not found')
+ }
+
+ const resolvedConfigs = await resolveConfig({ configPath })
+ const isTypeScript = await getIsUsingTypeScript()
+
+ type Watcher = FSWatcher & { config?: Watch }
+ const watchers: Watcher[] = []
+ const watchWriteDelay = 100
+ const watchOptions = {
+ atomic: true,
+ // awaitWriteFinish: true,
+ ignoreInitial: true,
+ persistent: true,
+ } satisfies ChokidarOptions
+
+ const outNames = new Set()
+ const isArrayConfig = Array.isArray(resolvedConfigs)
+ const configs = isArrayConfig ? resolvedConfigs : [resolvedConfigs]
+ for (const config of configs) {
+ if (isArrayConfig)
+ logger.log(`Using config ${pc.gray(basename(configPath))}`)
+ if (!config.out) throw new Error('out is required.')
+ if (outNames.has(config.out))
+ throw new Error(`out "${config.out}" must be unique.`)
+ outNames.add(config.out)
+
+ // Collect contracts and watch configs from plugins
+ const plugins = (config.plugins ?? []).map((x, i) => ({
+ ...x,
+ id: `${x.name}-${i}`,
+ }))
+ const spinner = logger.spinner('Validating plugins')
+ spinner.start()
+ for (const plugin of plugins) {
+ await plugin.validate?.()
+ }
+ spinner.success()
+
+ // Add plugin contracts to config contracts
+ const contractConfigs = config.contracts ?? []
+ const watchConfigs: Watch[] = []
+ spinner.start('Resolving contracts')
+ for (const plugin of plugins) {
+ if (plugin.watch) watchConfigs.push(plugin.watch)
+ if (plugin.contracts) {
+ const contracts = await plugin.contracts()
+ contractConfigs.push(...contracts)
+ }
+ }
+
+ // Get contracts from config
+ const contractNames = new Set()
+ const contractMap = new Map()
+ for (const contractConfig of contractConfigs) {
+ if (contractNames.has(contractConfig.name))
+ throw new Error(
+ `Contract name "${contractConfig.name}" must be unique.`,
+ )
+ const contract = await getContract({ ...contractConfig, isTypeScript })
+ contractMap.set(contract.name, contract)
+
+ contractNames.add(contractConfig.name)
+ }
+
+ // Sort contracts by name Ascending (low to high) as the key is `String`
+ const sortedAscContractMap = new Map([...contractMap].sort((a, b) => a[0].localeCompare(b[0])))
+ const contracts = [...sortedAscContractMap.values()]
+ if (!contracts.length && !options.watch) {
+ spinner.error()
+ logger.warn('No contracts found.')
+ return
+ }
+ spinner.success()
+
+ // Run plugins
+ const imports = []
+ const prepend = []
+ const content = []
+ type Output = {
+ plugin: Pick
+ } & Awaited>>
+ const outputs: Output[] = []
+ spinner.start('Running plugins')
+ for (const plugin of plugins) {
+ if (!plugin.run) continue
+ const result = await plugin.run({
+ contracts,
+ isTypeScript,
+ outputs,
+ })
+ outputs.push({
+ plugin: { name: plugin.name },
+ ...result,
+ })
+ if (!result.imports && !result.prepend && !result.content) continue
+ content.push(getBannerContent({ name: plugin.name }), result.content)
+ result.imports && imports.push(result.imports)
+ result.prepend && prepend.push(result.prepend)
+ }
+ spinner.success()
+
+ // Write output to file
+ spinner.start(`Writing to ${pc.gray(config.out)}`)
+ await writeContracts({
+ content,
+ contracts,
+ imports,
+ prepend,
+ filename: config.out,
+ })
+ spinner.success()
+
+ if (options.watch) {
+ if (!watchConfigs.length) {
+ logger.log(pc.gray('Used --watch flag, but no plugins are watching.'))
+ continue
+ }
+ logger.log()
+ logger.log('Setting up watch process')
+
+ // Watch for changes
+ let timeout: NodeJS.Timeout | null
+ for (const watchConfig of watchConfigs) {
+ const paths =
+ typeof watchConfig.paths === 'function'
+ ? await watchConfig.paths()
+ : watchConfig.paths
+ const watcher = watch(paths, watchOptions)
+ // Watch for changes to files, new files, and deleted files
+ watcher.on('all', async (event, path) => {
+ if (event !== 'change' && event !== 'add' && event !== 'unlink')
+ return
+
+ let needsWrite = false
+ if (event === 'change' || event === 'add') {
+ const eventFn =
+ event === 'change' ? watchConfig.onChange : watchConfig.onAdd
+ const config = await eventFn?.(path)
+ if (!config) return
+ const contract = await getContract({ ...config, isTypeScript })
+ contractMap.set(contract.name, contract)
+ needsWrite = true
+ } else if (event === 'unlink') {
+ const name = await watchConfig.onRemove?.(path)
+ if (!name) return
+ contractMap.delete(name)
+ needsWrite = true
+ }
+
+ // Debounce writes
+ if (needsWrite) {
+ if (timeout) clearTimeout(timeout)
+ timeout = setTimeout(async () => {
+ timeout = null
+ // Sort contracts by name Ascending (low to high) as the key is `String`
+ const sortedAscContractMap = new Map([...contractMap].sort())
+ const contracts = [...sortedAscContractMap.values()]
+ const sortedAscContractMap = new Map(
+ [...contractMap].sort((a, b) => a[0].localeCompare(b[0])),
+ )
+ const prepend = []
+ const content = []
+ const outputs: Output[] = []
+ for (const plugin of plugins) {
+ if (!plugin.run) continue
+ const result = await plugin.run({
+ contracts,
+ isTypeScript,
+ outputs,
+ })
+ outputs.push({
+ plugin: { name: plugin.name },
+ ...result,
+ })
+ if (!result.imports && !result.prepend && !result.content)
+ continue
+ content.push(
+ getBannerContent({ name: plugin.name }),
+ result.content,
+ )
+ result.imports && imports.push(result.imports)
+ result.prepend && prepend.push(result.prepend)
+ }
+
+ const spinner = logger.spinner(
+ `Writing to ${pc.gray(config.out)}`,
+ )
+ spinner.start()
+ await writeContracts({
+ content,
+ contracts,
+ imports,
+ prepend,
+ filename: config.out,
+ })
+ spinner.success()
+ }, watchWriteDelay)
+ needsWrite = false
+ }
+ })
+
+ // Run parallel command on ready
+ if (watchConfig.command)
+ watcher.on('ready', async () => {
+ await watchConfig.command?.()
+ })
+ ;(watcher as Watcher).config = watchConfig
+ watchers.push(watcher)
+ }
+ }
+ }
+
+ if (!watchers.length) return
+
+ // Watch `@wagmi/cli` config file for changes
+ const watcher = watch(configPath).on('change', async (path) => {
+ logger.log(
+ `> Found a change to config ${pc.gray(
+ basename(path),
+ )}. Restart process for changes to take effect.`,
+ )
+ })
+ watchers.push(watcher)
+
+ // Display message and close watchers on exit
+ process.once('SIGINT', shutdown)
+ process.once('SIGTERM', shutdown)
+ async function shutdown() {
+ logger.log()
+ logger.log('Shutting down watch process')
+ const promises = []
+ for (const watcher of watchers) {
+ if (watcher.config?.onClose) promises.push(watcher.config?.onClose?.())
+ promises.push(watcher.close())
+ }
+ await Promise.allSettled(promises)
+ process.exit(0)
+ }
+}
+
+async function getContract({
+ abi,
+ address,
+ name,
+ isTypeScript,
+}: ContractConfig & { isTypeScript: boolean }): Promise {
+ const constAssertion = isTypeScript ? ' as const' : ''
+ const abiName = `${camelCase(name)}Abi`
+ try {
+ abi = (await AbiSchema.parseAsync(abi)) as Abi
+ } catch (error) {
+ if (error instanceof z.ZodError)
+ throw fromZodError(error, {
+ prefix: `Invalid ABI for contract "${name}"`,
+ })
+ throw error
+ }
+ const docString =
+ typeof address === 'object'
+ ? dedent`\n
+ /**
+ ${getAddressDocString({ address })}
+ */
+ `
+ : ''
+ let content = dedent`
+ ${getBannerContent({ name })}
+
+ ${docString}
+ export const ${abiName} = ${JSON.stringify(abi)}${constAssertion}
+ `
+
+ let meta: Contract['meta'] = { abiName }
+ if (address) {
+ let resolvedAddress: Address | Record
+ try {
+ const Address = z
+ .string()
+ .regex(/^0x[a-fA-F0-9]{40}$/, { message: 'Invalid address' })
+ .transform((val) => getAddress(val)) as z.ZodType
+ const MultiChainAddress = z.record(z.string(), Address)
+ const AddressSchema = z.union([Address, MultiChainAddress])
+ resolvedAddress = await AddressSchema.parseAsync(address)
+ } catch (error) {
+ if (error instanceof z.ZodError)
+ throw fromZodError(error, {
+ prefix: `Invalid address for contract "${name}"`,
+ })
+ throw error
+ }
+
+ const addressName = `${camelCase(name)}Address`
+ const configName = `${camelCase(name)}Config`
+ meta = {
+ ...meta,
+ addressName,
+ configName,
+ }
+
+ const addressContent =
+ typeof resolvedAddress === 'string'
+ ? JSON.stringify(resolvedAddress)
+ : // Remove quotes from chain id key
+ JSON.stringify(resolvedAddress, null, 2).replace(/"(\d*)":/gm, '$1:')
+ content = dedent`
+ ${content}
+
+ ${docString}
+ export const ${addressName} = ${addressContent}${constAssertion}
+
+ ${docString}
+ export const ${configName} = { address: ${addressName}, abi: ${abiName} }${constAssertion}
+ `
+ }
+
+ return { abi, address, content, meta, name }
+}
+
+async function writeContracts({
+ content,
+ contracts,
+ imports,
+ prepend,
+ filename,
+}: {
+ content: string[]
+ contracts: Contract[]
+ imports: string[]
+ prepend: string[]
+ filename: string
+}) {
+ // Assemble code
+ let code = dedent`
+ ${imports.join('\n\n') ?? ''}
+
+ ${prepend.join('\n\n') ?? ''}
+ `
+ for (const contract of contracts) {
+ code = dedent`
+ ${code}
+
+ ${contract.content}
+ `
+ }
+ code = dedent`
+ ${code}
+
+ ${content.join('\n\n') ?? ''}
+ `
+
+ // Format and write output
+ const cwd = process.cwd()
+ const outPath = resolve(cwd, filename)
+ await mkdir(dirname(outPath), { recursive: true })
+ const formatted = await format(code)
+ await writeFile(outPath, formatted)
+}
+
+function getBannerContent({ name }: { name: string }) {
+ return dedent`
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ${name}
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ `
+}
diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts
new file mode 100644
index 0000000000..ed4a1d1644
--- /dev/null
+++ b/packages/cli/src/commands/init.test.ts
@@ -0,0 +1,189 @@
+import { existsSync } from 'node:fs'
+import { mkdir, readFile } from 'node:fs/promises'
+import { resolve } from 'pathe'
+import { afterEach, beforeEach, expect, test, vi } from 'vitest'
+
+import { createFixture, watchConsole } from '../../test/utils.js'
+import { defaultConfig } from '../config.js'
+import { init } from './init.js'
+
+let console: ReturnType
+beforeEach(() => {
+ console = watchConsole()
+})
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('creates config file', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ const configFile = await init()
+
+ expect(existsSync(configFile)).toBeTruthy()
+ expect(await readFile(configFile, 'utf-8')).toMatchInlineSnapshot(`
+ "// @ts-check
+
+ /** @type {import('@wagmi/cli').Config} */
+ export default {
+ out: 'src/generated.js',
+ contracts: [],
+ plugins: [],
+ }
+ "
+ `)
+ expect(
+ console.formatted.replaceAll(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot(`
+ "- Creating config
+ √ Creating config
+ Config created at wagmi.config.js"
+ `)
+})
+
+test('parameters: config', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ const configFile = await init({
+ config: 'foo.config.ts',
+ })
+
+ expect(existsSync(configFile)).toBeTruthy()
+ expect(await readFile(configFile, 'utf-8')).toMatchInlineSnapshot(`
+ "// @ts-check
+
+ /** @type {import('@wagmi/cli').Config} */
+ export default {
+ out: 'src/generated.js',
+ contracts: [],
+ plugins: [],
+ }
+ "
+ `)
+ expect(
+ console.formatted.replaceAll(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot(`
+ "- Creating config
+ √ Creating config
+ Config created at foo.config.ts"
+ `)
+})
+
+test('parameters: content', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'tsconfig.json': '{}',
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ const configFile = await init({
+ content: {
+ ...defaultConfig,
+ out: 'foo/bar/baz.ts',
+ },
+ })
+
+ expect(existsSync(configFile)).toBeTruthy()
+ expect(await readFile(configFile, 'utf-8')).toMatchInlineSnapshot(`
+ "import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig({
+ out: 'foo/bar/baz.ts',
+ contracts: [],
+ plugins: [],
+ })
+ "
+ `)
+ expect(
+ console.formatted.replaceAll(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot(`
+ "- Creating config
+ √ Creating config
+ Config created at wagmi.config.ts"
+ `)
+})
+
+test('parameters: root', async () => {
+ const { dir } = await createFixture()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+ mkdir(resolve(dir, 'foo'))
+
+ const configFile = await init({
+ root: 'foo/',
+ })
+
+ expect(existsSync(configFile)).toBeTruthy()
+ expect(await readFile(configFile, 'utf-8')).toMatchInlineSnapshot(`
+ "// @ts-check
+
+ /** @type {import('@wagmi/cli').Config} */
+ export default {
+ out: 'src/generated.js',
+ contracts: [],
+ plugins: [],
+ }
+ "
+ `)
+ expect(
+ console.formatted.replaceAll(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot(`
+ "- Creating config
+ √ Creating config
+ Config created at foo/wagmi.config.js"
+ `)
+})
+
+test('behavior: creates config file in TypeScript format', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'tsconfig.json': '{}',
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ const configFile = await init()
+
+ expect(existsSync(configFile)).toBeTruthy()
+ expect(await readFile(configFile, 'utf-8')).toMatchInlineSnapshot(`
+ "import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig({
+ out: 'src/generated.ts',
+ contracts: [],
+ plugins: [],
+ })
+ "
+ `)
+ expect(
+ console.formatted.replaceAll(dir, 'path/to/project'),
+ ).toMatchInlineSnapshot(`
+ "- Creating config
+ √ Creating config
+ Config created at wagmi.config.ts"
+ `)
+})
+
+test('behavior: displays config file location when config exists', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.ts': '',
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ const configFile = await init()
+
+ expect(
+ console.formatted.replaceAll(configFile, 'path/to/project/wagmi.config.ts'),
+ ).toMatchInlineSnapshot('"Config already exists at wagmi.config.ts"')
+})
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
new file mode 100644
index 0000000000..ce4e5b32d9
--- /dev/null
+++ b/packages/cli/src/commands/init.ts
@@ -0,0 +1,95 @@
+import { writeFile } from 'node:fs/promises'
+import dedent from 'dedent'
+import { relative, resolve } from 'pathe'
+import pc from 'picocolors'
+import { z } from 'zod'
+
+import { type Config, defaultConfig } from '../config.js'
+import { fromZodError } from '../errors.js'
+import * as logger from '../logger.js'
+import { findConfig } from '../utils/findConfig.js'
+import { format } from '../utils/format.js'
+import { getIsUsingTypeScript } from '../utils/getIsUsingTypeScript.js'
+
+export type Init = {
+ /** Path to config file */
+ config?: string
+ /** Watch for file system changes to config and plugins */
+ content?: Config
+ /** Directory to init config file */
+ root?: string
+}
+
+const Init = z.object({
+ config: z.string().optional(),
+ content: z.object({}).optional(),
+ root: z.string().optional(),
+})
+
+export async function init(options: Init = {}) {
+ // Validate command line options
+ try {
+ await Init.parseAsync(options)
+ } catch (error) {
+ if (error instanceof z.ZodError)
+ throw fromZodError(error, { prefix: 'Invalid option' })
+ throw error
+ }
+
+ // Check for existing config file
+ const configPath = await findConfig(options)
+ if (configPath) {
+ logger.info(
+ `Config already exists at ${pc.gray(
+ relative(process.cwd(), configPath),
+ )}`,
+ )
+ return configPath
+ }
+
+ const spinner = logger.spinner('Creating config')
+ spinner.start()
+ // Check if project is using TypeScript
+ const isUsingTypeScript = await getIsUsingTypeScript()
+ const rootDir = resolve(options.root || process.cwd())
+ let outPath: string
+ if (options.config) {
+ outPath = resolve(rootDir, options.config)
+ } else {
+ const extension = isUsingTypeScript ? 'ts' : 'js'
+ outPath = resolve(rootDir, `wagmi.config.${extension}`)
+ }
+
+ let content: string
+ if (isUsingTypeScript) {
+ const config = options.content ?? defaultConfig
+ content = dedent(`
+ import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig(${JSON.stringify(config)})
+ `)
+ } else {
+ const config = options.content ?? {
+ ...defaultConfig,
+ out: defaultConfig.out.replace('.ts', '.js'),
+ }
+ content = dedent(`
+ // @ts-check
+
+ /** @type {import('@wagmi/cli').Config} */
+ export default ${JSON.stringify(config, null, 2).replace(
+ /"(\d*)":/gm,
+ '$1:',
+ )}
+ `)
+ }
+
+ const formatted = await format(content)
+ await writeFile(outPath, formatted)
+ spinner.success()
+ logger.success(
+ `Config created at ${pc.gray(relative(process.cwd(), outPath))}`,
+ )
+
+ return outPath
+}
diff --git a/packages/cli/src/config.test.ts b/packages/cli/src/config.test.ts
new file mode 100644
index 0000000000..f95d7cb6d3
--- /dev/null
+++ b/packages/cli/src/config.test.ts
@@ -0,0 +1,39 @@
+import { expect, test, vi } from 'vitest'
+
+import { type Config, defineConfig } from './config.js'
+
+test('object', () => {
+ const config: Config = {
+ contracts: [],
+ out: 'wagmi.ts',
+ plugins: [],
+ }
+ expect(defineConfig(config)).toEqual(config)
+})
+
+test('array', () => {
+ const config: Config = {
+ contracts: [],
+ out: 'wagmi.ts',
+ plugins: [],
+ }
+ expect(defineConfig([config, config])).toEqual([config, config])
+})
+
+test('function', () => {
+ const config = vi.fn().mockImplementation(() => ({
+ contracts: [],
+ out: 'wagmi.ts',
+ plugins: [],
+ }))
+ expect(defineConfig(config)).toEqual(config)
+})
+
+test('async function', () => {
+ const config = vi.fn().mockImplementation(async () => ({
+ contracts: [],
+ out: 'wagmi.ts',
+ plugins: [],
+ }))
+ expect(defineConfig(config)).toEqual(config)
+})
diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts
new file mode 100644
index 0000000000..146a1e3424
--- /dev/null
+++ b/packages/cli/src/config.ts
@@ -0,0 +1,121 @@
+import type { Abi, Address } from 'viem'
+
+import type { Compute, MaybeArray, MaybePromise } from './types.js'
+
+export type ContractConfig<
+ chainId extends number = number,
+ requiredChainId extends number | undefined = undefined,
+> = {
+ /**
+ * Contract ABI
+ */
+ abi: Abi
+ /**
+ * Contract address or addresses.
+ *
+ * Accepts an object `{ [chainId]: address }` to support multiple chains.
+ *
+ * @example
+ * '0x314159265dd8dbb310642f98f50c066173c1259b'
+ *
+ * @example
+ * {
+ * 1: '0x314159265dd8dbb310642f98f50c066173c1259b',
+ * 5: '0x112234455c3a32fd11230c42e7bccd4a84e02010',
+ * }
+ */
+ address?:
+ | Address
+ | (requiredChainId extends number
+ ? Record & Partial>
+ : Record)
+ | undefined
+ /**
+ * Name of contract.
+ */
+ name: string
+}
+
+export type Contract = Compute<
+ ContractConfig & {
+ /** Generated string content */
+ content: string
+ /** Meta info about contract */
+ meta: {
+ abiName: string
+ addressName?: string | undefined
+ configName?: string | undefined
+ }
+ }
+>
+
+export type Watch = {
+ /** Command to run along with watch process */
+ command?: (() => MaybePromise) | undefined
+ /** Paths to watch for changes. */
+ paths: string[] | (() => MaybePromise)
+ /** Callback that fires when file is added */
+ onAdd?:
+ | ((path: string) => MaybePromise)
+ | undefined
+ /** Callback that fires when file changes */
+ onChange: (path: string) => MaybePromise
+ /** Callback that fires when watcher is shutdown */
+ onClose?: (() => MaybePromise) | undefined
+ /** Callback that fires when file is removed */
+ onRemove?: ((path: string) => MaybePromise) | undefined
+}
+
+export type Plugin = {
+ /** Contracts provided by plugin */
+ contracts?: (() => MaybePromise) | undefined
+ /** Plugin name */
+ name: string
+ /** Run plugin logic */
+ run?:
+ | ((config: {
+ /** All resolved contracts from config and plugins */
+ contracts: Contract[]
+ /** Whether TypeScript is detected in project */
+ isTypeScript: boolean
+ /** Previous plugin outputs */
+ outputs: readonly {
+ plugin: Pick
+ imports?: string
+ prepend?: string
+ content: string
+ }[]
+ }) => MaybePromise<{
+ imports?: string
+ prepend?: string
+ content: string
+ }>)
+ | undefined
+ /**
+ * Validate plugin configuration or other @wagmi/cli settings require for plugin.
+ */
+ validate?: (() => MaybePromise) | undefined
+ /** File system watch config */
+ watch?: Watch | undefined
+}
+
+export type Config = {
+ /** Contracts to use in commands */
+ contracts?: ContractConfig[] | undefined
+ /** Output file path */
+ out: string
+ /** Plugins to run */
+ plugins?: Plugin[] | undefined
+}
+
+export function defineConfig(
+ config: MaybeArray | (() => MaybePromise>),
+) {
+ return config
+}
+
+export const defaultConfig = {
+ out: 'src/generated.ts',
+ contracts: [],
+ plugins: [],
+} satisfies Config
diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts
new file mode 100644
index 0000000000..6ef37093fc
--- /dev/null
+++ b/packages/cli/src/errors.ts
@@ -0,0 +1,57 @@
+import type { z } from 'zod'
+
+class ValidationError extends Error {
+ details: Zod.ZodIssue[]
+
+ constructor(
+ message: string,
+ options: {
+ details: Zod.ZodIssue[]
+ },
+ ) {
+ super(message)
+ this.details = options.details
+ }
+}
+
+// From https://github.com/causaly/zod-validation-error
+export function fromZodError(
+ zError: z.ZodError,
+ {
+ maxIssuesInMessage = 99,
+ issueSeparator = '\n- ',
+ prefixSeparator = '\n- ',
+ prefix = 'Validation Error',
+ }: {
+ maxIssuesInMessage?: number
+ issueSeparator?: string
+ prefixSeparator?: string
+ prefix?: string
+ } = {},
+): ValidationError {
+ function joinPath(arr: Array): string {
+ return arr.reduce((acc, value) => {
+ if (typeof value === 'number') return `${acc}[${value}]`
+ const separator = acc === '' ? '' : '.'
+ return acc + separator + value
+ }, '')
+ }
+
+ const reason = zError.errors
+ // limit max number of issues printed in the reason section
+ .slice(0, maxIssuesInMessage)
+ // format error message
+ .map((issue) => {
+ const { message, path } = issue
+ if (path.length > 0) return `${message} at \`${joinPath(path)}\``
+ return message
+ })
+ // concat as string
+ .join(issueSeparator)
+
+ const message = reason ? [prefix, reason].join(prefixSeparator) : prefix
+
+ return new ValidationError(message, {
+ details: zError.errors,
+ })
+}
diff --git a/packages/cli/src/exports/config.test.ts b/packages/cli/src/exports/config.test.ts
new file mode 100644
index 0000000000..c833780ffc
--- /dev/null
+++ b/packages/cli/src/exports/config.test.ts
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+
+import * as Exports from './config.js'
+
+test('exports', () => {
+ expect(Object.keys(Exports)).toMatchInlineSnapshot(`
+ [
+ "defineConfig",
+ "defaultConfig",
+ ]
+ `)
+})
diff --git a/packages/cli/src/exports/config.ts b/packages/cli/src/exports/config.ts
new file mode 100644
index 0000000000..b3c4a83ba4
--- /dev/null
+++ b/packages/cli/src/exports/config.ts
@@ -0,0 +1,10 @@
+// biome-ignore lint/performance/noBarrelFile: entrypoint module
+export {
+ type ContractConfig,
+ type Contract,
+ type Watch,
+ type Plugin,
+ type Config,
+ defineConfig,
+ defaultConfig,
+} from '../config.js'
diff --git a/packages/cli/src/exports/index.test-d.ts b/packages/cli/src/exports/index.test-d.ts
new file mode 100644
index 0000000000..b056d56358
--- /dev/null
+++ b/packages/cli/src/exports/index.test-d.ts
@@ -0,0 +1,4 @@
+import { expectTypeOf } from 'vitest'
+
+// noop test because vitest typecheck fails unless each workspace project has type test
+expectTypeOf(1).toEqualTypeOf()
diff --git a/packages/cli/src/exports/index.test.ts b/packages/cli/src/exports/index.test.ts
new file mode 100644
index 0000000000..2da78e8da1
--- /dev/null
+++ b/packages/cli/src/exports/index.test.ts
@@ -0,0 +1,14 @@
+import { expect, test } from 'vitest'
+
+import * as Exports from './index.js'
+
+test('exports', () => {
+ expect(Object.keys(Exports)).toMatchInlineSnapshot(`
+ [
+ "defineConfig",
+ "logger",
+ "loadEnv",
+ "version",
+ ]
+ `)
+})
diff --git a/packages/cli/src/exports/index.ts b/packages/cli/src/exports/index.ts
new file mode 100644
index 0000000000..1c5e624df6
--- /dev/null
+++ b/packages/cli/src/exports/index.ts
@@ -0,0 +1,14 @@
+// biome-ignore lint/performance/noBarrelFile: entrypoint module
+export {
+ defineConfig,
+ type Config,
+ type ContractConfig,
+ type Plugin,
+} from '../config.js'
+
+// biome-ignore lint/performance/noReExportAll: entrypoint module
+export * as logger from '../logger.js'
+
+export { loadEnv } from '../utils/loadEnv.js'
+
+export { version } from '../version.js'
diff --git a/packages/cli/src/exports/plugins.test.ts b/packages/cli/src/exports/plugins.test.ts
new file mode 100644
index 0000000000..4d7b5a97cd
--- /dev/null
+++ b/packages/cli/src/exports/plugins.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from 'vitest'
+
+import * as Exports from './plugins.js'
+
+test('exports', () => {
+ expect(Object.keys(Exports)).toMatchInlineSnapshot(`
+ [
+ "actions",
+ "blockExplorer",
+ "etherscan",
+ "fetch",
+ "foundry",
+ "foundryDefaultExcludes",
+ "hardhat",
+ "hardhatDefaultExcludes",
+ "react",
+ "sourcify",
+ ]
+ `)
+})
diff --git a/packages/cli/src/exports/plugins.ts b/packages/cli/src/exports/plugins.ts
new file mode 100644
index 0000000000..a289b5c576
--- /dev/null
+++ b/packages/cli/src/exports/plugins.ts
@@ -0,0 +1,27 @@
+// biome-ignore lint/performance/noBarrelFile: entrypoint module
+export { actions, type ActionsConfig } from '../plugins/actions.js'
+
+export {
+ blockExplorer,
+ type BlockExplorerConfig,
+} from '../plugins/blockExplorer.js'
+
+export { etherscan, type EtherscanConfig } from '../plugins/etherscan.js'
+
+export { fetch, type FetchConfig } from '../plugins/fetch.js'
+
+export {
+ foundry,
+ foundryDefaultExcludes,
+ type FoundryConfig,
+} from '../plugins/foundry.js'
+
+export {
+ hardhat,
+ hardhatDefaultExcludes,
+ type HardhatConfig,
+} from '../plugins/hardhat.js'
+
+export { react, type ReactConfig } from '../plugins/react.js'
+
+export { sourcify, type SourcifyConfig } from '../plugins/sourcify.js'
diff --git a/packages/cli/src/logger.test.ts b/packages/cli/src/logger.test.ts
new file mode 100644
index 0000000000..7338c3bb14
--- /dev/null
+++ b/packages/cli/src/logger.test.ts
@@ -0,0 +1,32 @@
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { watchConsole } from '../test/utils.js'
+
+import * as logger from './logger.js'
+
+const mockLog = vi.fn()
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test.each(['success', 'info', 'log', 'warn', 'error'])('%s()', (level) => {
+ const spy = vi.spyOn(logger, level as any)
+ spy.mockImplementation(mockLog)
+ const loggerFn = (logger as any)[level]
+ loggerFn(level)
+ expect(spy).toHaveBeenCalledWith(level)
+})
+
+test('spinner', () => {
+ const console = watchConsole()
+ const spinner = logger.spinner('start')
+ spinner.start()
+ spinner.success('success')
+ spinner.error('error')
+ expect(console.formatted).toMatchInlineSnapshot(`
+ "- start
+ √ success
+ × error"
+ `)
+})
diff --git a/packages/cli/src/logger.ts b/packages/cli/src/logger.ts
new file mode 100644
index 0000000000..b56fb9728b
--- /dev/null
+++ b/packages/cli/src/logger.ts
@@ -0,0 +1,37 @@
+import { format as utilFormat } from 'node:util'
+import { createSpinner } from 'nanospinner'
+import pc from 'picocolors'
+
+function format(args: any[]) {
+ return utilFormat(...args)
+ .split('\n')
+ .join('\n')
+}
+
+export function success(...args: any[]) {
+ // biome-ignore lint/suspicious/noConsoleLog: console.log is used for logging
+ console.log(pc.green(format(args)))
+}
+
+export function info(...args: any[]) {
+ console.info(pc.blue(format(args)))
+}
+
+export function log(...args: any[]) {
+ // biome-ignore lint/suspicious/noConsoleLog: console.log is used for logging
+ console.log(pc.white(format(args)))
+}
+
+export function warn(...args: any[]) {
+ console.warn(pc.yellow(format(args)))
+}
+
+export function error(...args: any[]) {
+ console.error(pc.red(format(args)))
+}
+
+export function spinner(text: string) {
+ return createSpinner(text, {
+ color: 'yellow',
+ })
+}
diff --git a/packages/cli/src/plugins/__fixtures__/foundry/.gitignore b/packages/cli/src/plugins/__fixtures__/foundry/.gitignore
new file mode 100644
index 0000000000..3269660cc7
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/foundry/.gitignore
@@ -0,0 +1,11 @@
+# Compiler files
+cache/
+out/
+
+# Ignores development broadcast logs
+!/broadcast
+/broadcast/*/31337/
+/broadcast/**/dry-run/
+
+# Dotenv file
+.env
diff --git a/packages/cli/src/plugins/__fixtures__/foundry/foundry.toml b/packages/cli/src/plugins/__fixtures__/foundry/foundry.toml
new file mode 100644
index 0000000000..59374b16cf
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/foundry/foundry.toml
@@ -0,0 +1,7 @@
+[profile.default]
+libs = ['lib']
+out = 'out'
+solc = '0.8.13'
+src = 'src'
+
+# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/packages/cli/src/plugins/__fixtures__/foundry/src/Counter.sol b/packages/cli/src/plugins/__fixtures__/foundry/src/Counter.sol
new file mode 100644
index 0000000000..5242caa433
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/foundry/src/Counter.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+contract Counter {
+ uint256 public number;
+
+ function setNumber(uint256 newNumber) public {
+ number = newNumber;
+ }
+
+ function increment() public {
+ number++;
+ }
+}
diff --git a/packages/cli/src/plugins/__fixtures__/foundry/src/Foo.sol b/packages/cli/src/plugins/__fixtures__/foundry/src/Foo.sol
new file mode 100644
index 0000000000..f478736520
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/foundry/src/Foo.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+contract Foo {
+ string public bar;
+
+ function setFoo(string memory baz) public {
+ bar = baz;
+ }
+}
+
diff --git a/packages/cli/src/plugins/__fixtures__/hardhat/.gitignore b/packages/cli/src/plugins/__fixtures__/hardhat/.gitignore
new file mode 100644
index 0000000000..85d361b914
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/hardhat/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.env
+coverage
+coverage.json
+typechain
+typechain-types
+
+# Hardhat files
+cache
+artifacts
\ No newline at end of file
diff --git a/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Counter.sol b/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Counter.sol
new file mode 100644
index 0000000000..5242caa433
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Counter.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+contract Counter {
+ uint256 public number;
+
+ function setNumber(uint256 newNumber) public {
+ number = newNumber;
+ }
+
+ function increment() public {
+ number++;
+ }
+}
diff --git a/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Foo.sol b/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Foo.sol
new file mode 100644
index 0000000000..699a63ce0f
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/hardhat/contracts/Foo.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+contract Foo {
+ string public bar;
+
+ function setFoo(string memory baz) public {
+ bar = baz;
+ }
+}
diff --git a/packages/cli/src/plugins/__fixtures__/hardhat/hardhat.config.js b/packages/cli/src/plugins/__fixtures__/hardhat/hardhat.config.js
new file mode 100644
index 0000000000..c8126eedfa
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/hardhat/hardhat.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ solidity: '0.8.17',
+}
diff --git a/packages/cli/src/plugins/__fixtures__/hardhat/package.json b/packages/cli/src/plugins/__fixtures__/hardhat/package.json
new file mode 100644
index 0000000000..85c9ffb7bd
--- /dev/null
+++ b/packages/cli/src/plugins/__fixtures__/hardhat/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "hardhat-fixture",
+ "private": true,
+ "devDependencies": {
+ "hardhat": "^2.22.3"
+ }
+}
diff --git a/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap
new file mode 100644
index 0000000000..2abd351741
--- /dev/null
+++ b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap
@@ -0,0 +1,736 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fetches ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ "name": "WagmiMintExample",
+ },
+]
+`;
+
+exports[`fetches ABI with multichain deployment 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "1": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ "10": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ },
+ "name": "WagmiMintExample",
+ },
+]
+`;
diff --git a/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap
new file mode 100644
index 0000000000..e03ee30f81
--- /dev/null
+++ b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap
@@ -0,0 +1,1238 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fetches ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "1": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ },
+ "name": "WagmiMintExample",
+ },
+]
+`;
+
+exports[`fetches ABI with multichain deployment 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "1": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ "10": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ },
+ "name": "WagmiMintExample",
+ },
+]
+`;
+
+exports[`tryFetchProxyImplementation: fetches ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "1": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ },
+ "name": "WagmiMintExample",
+ },
+]
+`;
+
+exports[`tryFetchProxyImplementation: fetches implementation ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "newImplementation",
+ "type": "address",
+ },
+ ],
+ "name": "upgradeTo",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "newImplementation",
+ "type": "address",
+ },
+ {
+ "name": "data",
+ "type": "bytes",
+ },
+ ],
+ "name": "upgradeToAndCall",
+ "outputs": [],
+ "payable": true,
+ "stateMutability": "payable",
+ "type": "function",
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "implementation",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "newAdmin",
+ "type": "address",
+ },
+ ],
+ "name": "changeAdmin",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "admin",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "name": "_implementation",
+ "type": "address",
+ },
+ ],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "payable": true,
+ "stateMutability": "payable",
+ "type": "fallback",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "name": "previousAdmin",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "name": "newAdmin",
+ "type": "address",
+ },
+ ],
+ "name": "AdminChanged",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "name": "implementation",
+ "type": "address",
+ },
+ ],
+ "name": "Upgraded",
+ "type": "event",
+ },
+ ],
+ "address": {
+ "1": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
+ },
+ "name": "FiatToken",
+ },
+]
+`;
diff --git a/packages/cli/src/plugins/__snapshots__/fetch.test.ts.snap b/packages/cli/src/plugins/__snapshots__/fetch.test.ts.snap
new file mode 100644
index 0000000000..83c4e81f53
--- /dev/null
+++ b/packages/cli/src/plugins/__snapshots__/fetch.test.ts.snap
@@ -0,0 +1,367 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fetches ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Approval",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "ApprovalForAll",
+ "type": "event",
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "Transfer",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes",
+ },
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address",
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool",
+ },
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address",
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address",
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256",
+ },
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": "0xaf0326d92b97df1221759476b072abfd8084f9be",
+ "name": "WagmiMintExample",
+ },
+]
+`;
diff --git a/packages/cli/src/plugins/__snapshots__/sourcify.test.ts.snap b/packages/cli/src/plugins/__snapshots__/sourcify.test.ts.snap
new file mode 100644
index 0000000000..77e82fecde
--- /dev/null
+++ b/packages/cli/src/plugins/__snapshots__/sourcify.test.ts.snap
@@ -0,0 +1,214 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fetches ABI 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "inputs": [
+ {
+ "name": "pubkey",
+ "type": "bytes",
+ },
+ {
+ "name": "withdrawal_credentials",
+ "type": "bytes",
+ },
+ {
+ "name": "amount",
+ "type": "bytes",
+ },
+ {
+ "name": "signature",
+ "type": "bytes",
+ },
+ {
+ "name": "index",
+ "type": "bytes",
+ },
+ ],
+ "name": "DepositEvent",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "name": "pubkey",
+ "type": "bytes",
+ },
+ {
+ "name": "withdrawal_credentials",
+ "type": "bytes",
+ },
+ {
+ "name": "signature",
+ "type": "bytes",
+ },
+ {
+ "name": "deposit_data_root",
+ "type": "bytes32",
+ },
+ ],
+ "name": "deposit",
+ "outputs": [],
+ "stateMutability": "payable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "get_deposit_count",
+ "outputs": [
+ {
+ "type": "bytes",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "get_deposit_root",
+ "outputs": [
+ {
+ "type": "bytes32",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "1": "0x00000000219ab540356cbb839cbe05303d7705fa",
+ },
+ "name": "DepositContract",
+ },
+]
+`;
+
+exports[`fetches ABI with multichain deployment 1`] = `
+[
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor",
+ },
+ {
+ "inputs": [
+ {
+ "name": "pubkey",
+ "type": "bytes",
+ },
+ {
+ "name": "withdrawal_credentials",
+ "type": "bytes",
+ },
+ {
+ "name": "amount",
+ "type": "bytes",
+ },
+ {
+ "name": "signature",
+ "type": "bytes",
+ },
+ {
+ "name": "index",
+ "type": "bytes",
+ },
+ ],
+ "name": "DepositEvent",
+ "type": "event",
+ },
+ {
+ "inputs": [
+ {
+ "name": "pubkey",
+ "type": "bytes",
+ },
+ {
+ "name": "withdrawal_credentials",
+ "type": "bytes",
+ },
+ {
+ "name": "signature",
+ "type": "bytes",
+ },
+ {
+ "name": "deposit_data_root",
+ "type": "bytes32",
+ },
+ ],
+ "name": "deposit",
+ "outputs": [],
+ "stateMutability": "payable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "get_deposit_count",
+ "outputs": [
+ {
+ "type": "bytes",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "get_deposit_root",
+ "outputs": [
+ {
+ "type": "bytes32",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "name": "interfaceId",
+ "type": "bytes4",
+ },
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "type": "bool",
+ },
+ ],
+ "stateMutability": "pure",
+ "type": "function",
+ },
+ ],
+ "address": {
+ "100": "0xC4c622862a8F548997699bE24EA4bc504e5cA865",
+ "137": "0xC4c622862a8F548997699bE24EA4bc504e5cA865",
+ },
+ "name": "Community",
+ },
+]
+`;
diff --git a/packages/cli/src/plugins/actions.test.ts b/packages/cli/src/plugins/actions.test.ts
new file mode 100644
index 0000000000..51b445b616
--- /dev/null
+++ b/packages/cli/src/plugins/actions.test.ts
@@ -0,0 +1,359 @@
+import { erc20Abi } from 'viem'
+import { expect, test } from 'vitest'
+
+import { actions } from './actions.js'
+
+test('default', async () => {
+ const result = await actions().run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.imports).toMatchInlineSnapshot(`
+ "import { createReadContract, createWriteContract, createSimulateContract, createWatchContractEvent } from '@wagmi/core/codegen'
+ "
+ `)
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const readErc20 = /*#__PURE__*/ createReadContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const readErc20Allowance = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const readErc20BalanceOf = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const readErc20Decimals = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const readErc20Name = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'name' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const readErc20Symbol = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const readErc20TotalSupply = /*#__PURE__*/ createReadContract({ abi: erc20Abi, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const writeErc20 = /*#__PURE__*/ createWriteContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const writeErc20Approve = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const writeErc20Transfer = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const writeErc20TransferFrom = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const simulateErc20 = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const simulateErc20Approve = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const simulateErc20Transfer = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const simulateErc20TransferFrom = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const watchErc20Event = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const watchErc20ApprovalEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const watchErc20TransferEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, eventName: 'Transfer' })"
+ `)
+})
+
+test('address', async () => {
+ const result = await actions().run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ addressName: 'erc20Address',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const readErc20 = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const readErc20Allowance = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const readErc20BalanceOf = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const readErc20Decimals = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const readErc20Name = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'name' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const readErc20Symbol = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const readErc20TotalSupply = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const writeErc20 = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const writeErc20Approve = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const writeErc20Transfer = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const writeErc20TransferFrom = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const simulateErc20 = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const simulateErc20Approve = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const simulateErc20Transfer = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const simulateErc20TransferFrom = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const watchErc20Event = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const watchErc20ApprovalEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const watchErc20TransferEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Transfer' })"
+ `)
+})
+
+test('legacy hook names', async () => {
+ const result = await actions({ getActionName: 'legacy' }).run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ addressName: 'erc20Address',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const readErc20 = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const readErc20Allowance = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const readErc20BalanceOf = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const readErc20Decimals = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const readErc20Name = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'name' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const readErc20Symbol = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link readContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const readErc20TotalSupply = /*#__PURE__*/ createReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const writeErc20 = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const writeErc20Approve = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const writeErc20Transfer = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link writeContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const writeErc20TransferFrom = /*#__PURE__*/ createWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const prepareWriteErc20 = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const prepareWriteErc20Approve = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const prepareWriteErc20Transfer = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link simulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const prepareWriteErc20TransferFrom = /*#__PURE__*/ createSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const watchErc20Event = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const watchErc20ApprovalEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link watchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const watchErc20TransferEvent = /*#__PURE__*/ createWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Transfer' })"
+ `)
+})
+
+test('override package name', async () => {
+ const result = await actions({ overridePackageName: 'wagmi' }).run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.imports).toMatchInlineSnapshot(`
+ "import { createReadContract, createWriteContract, createSimulateContract, createWatchContractEvent } from 'wagmi/codegen'
+ "
+ `)
+})
diff --git a/packages/cli/src/plugins/actions.ts b/packages/cli/src/plugins/actions.ts
new file mode 100644
index 0000000000..01c804fd91
--- /dev/null
+++ b/packages/cli/src/plugins/actions.ts
@@ -0,0 +1,321 @@
+import { pascalCase } from 'change-case'
+
+import type { Contract, Plugin } from '../config.js'
+import type { Compute, RequiredBy } from '../types.js'
+import { getAddressDocString } from '../utils/getAddressDocString.js'
+import { getIsPackageInstalled } from '../utils/packages.js'
+
+export type ActionsConfig = {
+ getActionName?:
+ | 'legacy' // TODO: Deprecate `'legacy'` option
+ | ((options: {
+ contractName: string
+ itemName?: string | undefined
+ type: 'read' | 'simulate' | 'watch' | 'write'
+ }) => string)
+ overridePackageName?: '@wagmi/core' | 'wagmi' | undefined
+}
+
+type ActionsResult = Compute>
+
+export function actions(config: ActionsConfig = {}): ActionsResult {
+ return {
+ name: 'Action',
+ async run({ contracts }) {
+ const imports = new Set([])
+ const content: string[] = []
+ const pure = '/*#__PURE__*/'
+
+ const actionNames = new Set()
+ for (const contract of contracts) {
+ let hasReadFunction = false
+ let hasWriteFunction = false
+ let hasEvent = false
+ const readItems = []
+ const writeItems = []
+ const eventItems = []
+ for (const item of contract.abi) {
+ if (item.type === 'function')
+ if (
+ item.stateMutability === 'view' ||
+ item.stateMutability === 'pure'
+ ) {
+ hasReadFunction = true
+ readItems.push(item)
+ } else {
+ hasWriteFunction = true
+ writeItems.push(item)
+ }
+ else if (item.type === 'event') {
+ hasEvent = true
+ eventItems.push(item)
+ }
+ }
+
+ let innerContent: string
+ if (contract.meta.addressName)
+ innerContent = `abi: ${contract.meta.abiName}, address: ${contract.meta.addressName}`
+ else innerContent = `abi: ${contract.meta.abiName}`
+
+ if (hasReadFunction) {
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'read',
+ contract.name,
+ )
+ const docString = genDocString('readContract', contract)
+ const functionName = 'createReadContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of readItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'pure' &&
+ item.stateMutability !== 'view'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const hookName = getActionName(
+ config,
+ actionNames,
+ 'read',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('readContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+
+ if (hasWriteFunction) {
+ {
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'write',
+ contract.name,
+ )
+ const docString = genDocString('writeContract', contract)
+ const functionName = 'createWriteContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of writeItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'nonpayable' &&
+ item.stateMutability !== 'payable'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'write',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('writeContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+
+ {
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'simulate',
+ contract.name,
+ )
+ const docString = genDocString('simulateContract', contract)
+ const functionName = 'createSimulateContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of writeItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'nonpayable' &&
+ item.stateMutability !== 'payable'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'simulate',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('simulateContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+ }
+
+ if (hasEvent) {
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'watch',
+ contract.name,
+ )
+ const docString = genDocString('watchContractEvent', contract)
+ const functionName = 'createWatchContractEvent'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of eventItems) {
+ if (item.type !== 'event') continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const actionName = getActionName(
+ config,
+ actionNames,
+ 'watch',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('watchContractEvent', contract, {
+ name: 'eventName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${actionName} = ${pure} ${functionName}({ ${innerContent}, eventName: '${item.name}' })`,
+ )
+ }
+ }
+ }
+
+ const importValues = [...imports.values()]
+
+ let packageName = '@wagmi/core/codegen'
+ if (config.overridePackageName) {
+ switch (config.overridePackageName) {
+ case '@wagmi/core':
+ packageName = '@wagmi/core/codegen'
+ break
+ case 'wagmi':
+ packageName = 'wagmi/codegen'
+ break
+ }
+ } else if (await getIsPackageInstalled({ packageName: 'wagmi' }))
+ packageName = 'wagmi/codegen'
+ else if (await getIsPackageInstalled({ packageName: '@wagmi/core' }))
+ packageName = '@wagmi/core/codegen'
+
+ return {
+ imports: importValues.length
+ ? `import { ${importValues.join(', ')} } from '${packageName}'\n`
+ : '',
+ content: content.join('\n\n'),
+ }
+ },
+ }
+}
+
+function genDocString(
+ actionName: string,
+ contract: Contract,
+ item?: { name: string; value: string },
+) {
+ let description = `Wraps __{@link ${actionName}}__ with \`abi\` set to __{@link ${contract.meta.abiName}}__`
+ if (item) description += ` and \`${item.name}\` set to \`"${item.value}"\``
+
+ const docString = getAddressDocString({ address: contract.address })
+ if (docString)
+ return `/**
+ * ${description}
+ *
+ ${docString}
+ */`
+
+ return `/**
+ * ${description}
+ */`
+}
+
+function getActionName(
+ config: ActionsConfig,
+ actionNames: Set,
+ type: 'read' | 'simulate' | 'watch' | 'write',
+ contractName: string,
+ itemName?: string | undefined,
+) {
+ const ContractName = pascalCase(contractName)
+ const ItemName = itemName ? pascalCase(itemName) : undefined
+
+ let actionName: string
+ if (typeof config.getActionName === 'function')
+ actionName = config.getActionName({
+ type,
+ contractName: ContractName,
+ itemName: ItemName,
+ })
+ else if (typeof config.getActionName === 'string' && type === 'simulate') {
+ actionName = `prepareWrite${ContractName}${ItemName ?? ''}`
+ } else {
+ actionName = `${type}${ContractName}${ItemName ?? ''}`
+ if (type === 'watch') actionName = `${actionName}Event`
+ }
+
+ if (actionNames.has(actionName))
+ throw new Error(
+ `Action name "${actionName}" must be unique for contract "${contractName}". Try using \`getActionName\` to create a unique name.`,
+ )
+
+ actionNames.add(actionName)
+ return actionName
+}
diff --git a/packages/cli/src/plugins/blockExplorer.test.ts b/packages/cli/src/plugins/blockExplorer.test.ts
new file mode 100644
index 0000000000..13372f53ec
--- /dev/null
+++ b/packages/cli/src/plugins/blockExplorer.test.ts
@@ -0,0 +1,53 @@
+import { setupServer } from 'msw/node'
+import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
+
+import {
+ address,
+ apiKey,
+ baseUrl,
+ handlers,
+ unverifiedContractAddress,
+} from '../../test/utils.js'
+import { blockExplorer } from './blockExplorer.js'
+
+const server = setupServer(...handlers)
+
+beforeAll(() => server.listen())
+afterEach(() => server.resetHandlers())
+afterAll(() => server.close())
+
+test('fetches ABI', async () => {
+ await expect(
+ blockExplorer({
+ apiKey,
+ baseUrl,
+ contracts: [{ name: 'WagmiMintExample', address }],
+ }).contracts!(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fetches ABI with multichain deployment', async () => {
+ await expect(
+ blockExplorer({
+ apiKey,
+ baseUrl,
+ contracts: [
+ { name: 'WagmiMintExample', address: { 1: address, 10: address } },
+ ],
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fails to fetch for unverified contract', async () => {
+ await expect(
+ blockExplorer({
+ apiKey,
+ baseUrl,
+ contracts: [
+ { name: 'WagmiMintExample', address: unverifiedContractAddress },
+ ],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: Contract source code not verified]',
+ )
+})
diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts
new file mode 100644
index 0000000000..2518b6e936
--- /dev/null
+++ b/packages/cli/src/plugins/blockExplorer.ts
@@ -0,0 +1,107 @@
+import { camelCase } from 'change-case'
+import type { Address } from 'viem'
+import { z } from 'zod'
+
+import type { ContractConfig } from '../config.js'
+import { fromZodError } from '../errors.js'
+import type { Compute } from '../types.js'
+import { fetch } from './fetch.js'
+
+export type BlockExplorerConfig = {
+ /**
+ * API key for block explorer. Appended to the request URL as query param `&apikey=${apiKey}`.
+ */
+ apiKey?: string | undefined
+ /**
+ * Base URL for block explorer.
+ */
+ baseUrl: string
+ /**
+ * Duration in milliseconds to cache ABIs.
+ *
+ * @default 1_800_000 // 30m in ms
+ */
+ cacheDuration?: number | undefined
+ /**
+ * Chain ID for block explorer. Appended to the request URL as query param `&chainId=${chainId}`.
+ */
+ chainId?: number | undefined
+ /**
+ * Contracts to fetch ABIs for.
+ */
+ contracts: Compute>[]
+ /**
+ * Function to get address from contract config.
+ */
+ getAddress?:
+ | ((config: {
+ address: NonNullable
+ }) => Address)
+ | undefined
+ /**
+ * Name of source.
+ */
+ name?: ContractConfig['name'] | undefined
+}
+
+const BlockExplorerResponse = z.discriminatedUnion('status', [
+ z.object({
+ status: z.literal('1'),
+ message: z.literal('OK'),
+ result: z
+ .string()
+ .transform((val) => JSON.parse(val) as ContractConfig['abi']),
+ }),
+ z.object({
+ status: z.literal('0'),
+ message: z.literal('NOTOK'),
+ result: z.string(),
+ }),
+])
+
+/**
+ * Fetches contract ABIs from block explorers, supporting `?module=contract&action=getabi` requests.
+ */
+export function blockExplorer(config: BlockExplorerConfig) {
+ const {
+ apiKey,
+ baseUrl,
+ cacheDuration,
+ chainId,
+ contracts,
+ getAddress = ({ address }) => {
+ if (typeof address === 'string') return address
+ return Object.values(address)[0]!
+ },
+ name = 'Block Explorer',
+ } = config
+
+ return fetch({
+ cacheDuration,
+ contracts,
+ name,
+ getCacheKey({ contract }) {
+ if (typeof contract.address === 'string')
+ return `${camelCase(name)}:${contract.address}`
+ return `${camelCase(name)}:${JSON.stringify(contract.address)}`
+ },
+ async parse({ response }) {
+ const json = await response.json()
+ const parsed = await BlockExplorerResponse.safeParseAsync(json)
+ if (!parsed.success)
+ throw fromZodError(parsed.error, { prefix: 'Invalid response' })
+ if (parsed.data.status === '0') throw new Error(parsed.data.result)
+ return parsed.data.result
+ },
+ request({ address }) {
+ if (!address) throw new Error('address is required')
+ return {
+ url: `${baseUrl}?${chainId ? `chainId=${chainId}&` : ''}module=contract&action=getabi&address=${getAddress(
+ {
+ address,
+ },
+ )}${apiKey ? `&apikey=${apiKey}` : ''}`,
+ }
+ },
+ })
+}
diff --git a/packages/cli/src/plugins/etherscan.test.ts b/packages/cli/src/plugins/etherscan.test.ts
new file mode 100644
index 0000000000..dc496f4630
--- /dev/null
+++ b/packages/cli/src/plugins/etherscan.test.ts
@@ -0,0 +1,112 @@
+import { mkdir, rm } from 'node:fs/promises'
+import { setupServer } from 'msw/node'
+import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
+
+import {
+ address,
+ apiKey,
+ handlers,
+ invalidApiKey,
+ proxyAddress,
+ timeoutAddress,
+ unverifiedContractAddress,
+} from '../../test/utils.js'
+import { etherscan } from './etherscan.js'
+import { getCacheDir } from './fetch.js'
+
+const server = setupServer(...handlers)
+
+beforeAll(() => server.listen())
+afterEach(() => server.resetHandlers())
+afterAll(() => server.close())
+
+test('fetches ABI', async () => {
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ contracts: [{ name: 'WagmiMintExample', address }],
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fetches ABI with multichain deployment', async () => {
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ contracts: [
+ { name: 'WagmiMintExample', address: { 1: address, 10: address } },
+ ],
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fails to fetch for unverified contract', async () => {
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ contracts: [
+ { name: 'WagmiMintExample', address: unverifiedContractAddress },
+ ],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: Contract source code not verified]',
+ )
+})
+
+test('missing address for chainId', async () => {
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ // @ts-expect-error `chainId` and `keyof typeof contracts[number].address` mismatch
+ contracts: [{ name: 'WagmiMintExample', address: { 10: address } }],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: No address found for chainId "1". Make sure chainId "1" is set as an address.]`,
+ )
+})
+
+test('invalid api key', async () => {
+ await expect(
+ etherscan({
+ apiKey: invalidApiKey,
+ chainId: 1,
+ contracts: [{ name: 'WagmiMintExample', address: timeoutAddress }],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot('[Error: Invalid API Key]')
+})
+
+test('tryFetchProxyImplementation: fetches ABI', async () => {
+ const cacheDir = getCacheDir()
+ await mkdir(cacheDir, { recursive: true })
+
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ contracts: [{ name: 'WagmiMintExample', address }],
+ tryFetchProxyImplementation: true,
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+
+ await rm(cacheDir, { recursive: true })
+})
+
+test('tryFetchProxyImplementation: fetches implementation ABI', async () => {
+ const cacheDir = getCacheDir()
+ await mkdir(cacheDir, { recursive: true })
+
+ await expect(
+ etherscan({
+ apiKey,
+ chainId: 1,
+ contracts: [{ name: 'FiatToken', address: proxyAddress }],
+ tryFetchProxyImplementation: true,
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+
+ await rm(cacheDir, { recursive: true })
+})
diff --git a/packages/cli/src/plugins/etherscan.ts b/packages/cli/src/plugins/etherscan.ts
new file mode 100644
index 0000000000..fda375c240
--- /dev/null
+++ b/packages/cli/src/plugins/etherscan.ts
@@ -0,0 +1,268 @@
+import { mkdir, writeFile } from 'node:fs/promises'
+import { Address as AddressSchema } from 'abitype/zod'
+import { camelCase } from 'change-case'
+import { join } from 'pathe'
+import type { Abi, Address } from 'viem'
+import { z } from 'zod'
+
+import type { ContractConfig } from '../config.js'
+import { fromZodError } from '../errors.js'
+import type { Compute } from '../types.js'
+import { fetch, getCacheDir } from './fetch.js'
+
+export type EtherscanConfig = {
+ /**
+ * Etherscan API key.
+ *
+ * Create or manage keys at https://etherscan.io/myapikey
+ */
+ apiKey: string
+ /**
+ * Duration in milliseconds to cache ABIs.
+ *
+ * @default 1_800_000 // 30m in ms
+ */
+ cacheDuration?: number | undefined
+ /**
+ * Chain ID to use for fetching ABI.
+ *
+ * If `address` is an object, `chainId` is used to select the address.
+ *
+ * View supported chains on the [Etherscan docs](https://docs.etherscan.io/etherscan-v2/getting-started/supported-chains).
+ */
+ chainId: (chainId extends ChainId ? chainId : never) | (ChainId & {})
+ /**
+ * Contracts to fetch ABIs for.
+ */
+ contracts: Compute, 'abi'>>[]
+ /**
+ * Whether to try fetching proxy implementation address of the contract
+ *
+ * @default false
+ */
+ tryFetchProxyImplementation?: boolean | undefined
+}
+
+/**
+ * Fetches contract ABIs from Etherscan.
+ */
+export function etherscan(
+ config: EtherscanConfig,
+) {
+ const {
+ apiKey,
+ cacheDuration = 1_800_000,
+ chainId,
+ tryFetchProxyImplementation = false,
+ } = config
+
+ const contracts = config.contracts.map((x) => ({
+ ...x,
+ address:
+ typeof x.address === 'string' ? { [chainId]: x.address } : x.address,
+ })) as Omit[]
+
+ const name = 'Etherscan'
+
+ const getCacheKey: Parameters[0]['getCacheKey'] = ({
+ contract,
+ }) => {
+ if (typeof contract.address === 'string')
+ return `${camelCase(name)}:${contract.address}`
+ return `${camelCase(name)}:${JSON.stringify(contract.address)}`
+ }
+
+ return fetch({
+ cacheDuration,
+ contracts,
+ name,
+ getCacheKey,
+ async parse({ response }) {
+ const json = await response.json()
+ const parsed = await GetAbiResponse.safeParseAsync(json)
+ if (!parsed.success)
+ throw fromZodError(parsed.error, { prefix: 'Invalid response' })
+ if (parsed.data.status === '0') throw new Error(parsed.data.result)
+ return parsed.data.result
+ },
+ async request(contract) {
+ if (!contract.address) throw new Error('address is required')
+
+ const resolvedAddress = (() => {
+ if (!contract.address) throw new Error('address is required')
+ if (typeof contract.address === 'string') return contract.address
+ const contractAddress = contract.address[chainId]
+ if (!contractAddress)
+ throw new Error(
+ `No address found for chainId "${chainId}". Make sure chainId "${chainId}" is set as an address.`,
+ )
+ return contractAddress
+ })()
+
+ const options = {
+ address: resolvedAddress,
+ apiKey,
+ chainId,
+ }
+
+ let abi: Abi | undefined
+ const implementationAddress = await (async () => {
+ if (!tryFetchProxyImplementation) return
+ const json = await globalThis
+ .fetch(buildUrl({ ...options, action: 'getsourcecode' }))
+ .then((res) => res.json())
+ const parsed = await GetSourceCodeResponse.safeParseAsync(json)
+ if (!parsed.success)
+ throw fromZodError(parsed.error, { prefix: 'Invalid response' })
+ if (parsed.data.status === '0') throw new Error(parsed.data.result)
+ if (!parsed.data.result[0]) return
+ abi = parsed.data.result[0].ABI
+ return parsed.data.result[0].Implementation as Address
+ })()
+
+ if (abi) {
+ const cacheDir = getCacheDir()
+ await mkdir(cacheDir, { recursive: true })
+ const cacheKey = getCacheKey({ contract })
+ const cacheFilePath = join(cacheDir, `${cacheKey}.json`)
+ await writeFile(
+ cacheFilePath,
+ `${JSON.stringify({ abi, timestamp: Date.now() + cacheDuration }, undefined, 2)}\n`,
+ )
+ }
+
+ return {
+ url: buildUrl({
+ ...options,
+ action: 'getabi',
+ address: implementationAddress || resolvedAddress,
+ }),
+ }
+ },
+ })
+}
+
+function buildUrl(options: {
+ action: 'getabi' | 'getsourcecode'
+ address: Address
+ apiKey: string
+ chainId: ChainId | undefined
+}) {
+ const baseUrl = 'https://api.etherscan.io/v2/api'
+ const { action, address, apiKey, chainId } = options
+ return `${baseUrl}?${chainId ? `chainId=${chainId}&` : ''}module=contract&action=${action}&address=${address}${apiKey ? `&apikey=${apiKey}` : ''}`
+}
+
+const GetAbiResponse = z.discriminatedUnion('status', [
+ z.object({
+ status: z.literal('1'),
+ message: z.literal('OK'),
+ result: z.string().transform((val) => JSON.parse(val) as Abi),
+ }),
+ z.object({
+ status: z.literal('0'),
+ message: z.literal('NOTOK'),
+ result: z.string(),
+ }),
+])
+
+const GetSourceCodeResponse = z.discriminatedUnion('status', [
+ z.object({
+ status: z.literal('1'),
+ message: z.literal('OK'),
+ result: z.array(
+ z.discriminatedUnion('Proxy', [
+ z.object({
+ ABI: z.string().transform((val) => JSON.parse(val) as Abi),
+ Implementation: AddressSchema,
+ Proxy: z.literal('1'),
+ }),
+ z.object({
+ ABI: z.string().transform((val) => JSON.parse(val) as Abi),
+ Implementation: z.string(),
+ Proxy: z.literal('0'),
+ }),
+ ]),
+ ),
+ }),
+ z.object({
+ status: z.literal('0'),
+ message: z.literal('NOTOK'),
+ result: z.string(),
+ }),
+])
+
+// Supported chains
+// https://docs.etherscan.io/etherscan-v2/getting-started/supported-chains
+type ChainId =
+ | 1 // Ethereum Mainnet
+ | 11155111 // Sepolia Testnet
+ | 17000 // Holesky Testnet
+ | 560048 // Hoodi Testnet
+ | 56 // BNB Smart Chain Mainnet
+ | 97 // BNB Smart Chain Testnet
+ | 137 // Polygon Mainnet
+ | 80002 // Polygon Amoy Testnet
+ | 1101 // Polygon zkEVM Mainnet
+ | 2442 // Polygon zkEVM Cardona Testnet
+ | 8453 // Base Mainnet
+ | 84532 // Base Sepolia Testnet
+ | 42161 // Arbitrum One Mainnet
+ | 42170 // Arbitrum Nova Mainnet
+ | 421614 // Arbitrum Sepolia Testnet
+ | 59144 // Linea Mainnet
+ | 59141 // Linea Sepolia Testnet
+ | 250 // Fantom Opera Mainnet
+ | 4002 // Fantom Testnet
+ | 81457 // Blast Mainnet
+ | 168587773 // Blast Sepolia Testnet
+ | 10 // OP Mainnet
+ | 11155420 // OP Sepolia Testnet
+ | 43114 // Avalanche C-Chain
+ | 43113 // Avalanche Fuji Testnet
+ | 199 // BitTorrent Chain Mainnet
+ | 1028 // BitTorrent Chain Testnet
+ | 42220 // Celo Mainnet
+ | 44787 // Celo Alfajores Testnet
+ | 25 // Cronos Mainnet
+ | 252 // Fraxtal Mainnet
+ | 2522 // Fraxtal Testnet
+ | 100 // Gnosis
+ | 255 // Kroma Mainnet
+ | 2358 // Kroma Sepolia Testnet
+ | 5000 // Mantle Mainnet
+ | 5003 // Mantle Sepolia Testnet
+ | 1284 // Moonbeam Mainnet
+ | 1285 // Moonriver Mainnet
+ | 1287 // Moonbase Alpha Testnet
+ | 204 // opBNB Mainnet
+ | 5611 // opBNB Testnet
+ | 534352 // Scroll Mainnet
+ | 534351 // Scroll Sepolia Testnet
+ | 167000 // Taiko Mainnet
+ | 167009 // Taiko Hekla L2 Testnet
+ | 1111 // WEMIX3.0 Mainnet
+ | 1112 // WEMIX3.0 Testnet
+ | 324 // zkSync Mainnet
+ | 300 // zkSync Sepolia Testnet
+ | 660279 // Xai Mainnet
+ | 37714555429 // Xai Sepolia Testnet
+ | 50 // XDC Mainnet
+ | 51 // XDC Apothem Testnet
+ | 33139 // ApeChain Mainnet
+ | 33111 // ApeChain Curtis Testnet
+ | 480 // World Mainnet
+ | 4801 // World Sepolia Testnet
+ | 50104 // Sophon Mainnet
+ | 531050104 // Sophon Sepolia Testnet
+ | 146 // Sonic Mainnet
+ | 57054 // Sonic Blaze Testnet
+ | 130 // Unichain Mainnet
+ | 1301 // Unichain Sepolia Testnet
+ | 2741 // Abstract Mainnet
+ | 11124 // Abstract Sepolia Testnet
+ | 80094 // Berachain Mainnet
+ | 80069 // Berachain Bepolia Testnet
+ | 1923 // Swellchain Mainnet
+ | 1924 // Swellchain Testnet
+ | 10143 // Monad Testnet
diff --git a/packages/cli/src/plugins/fetch.test.ts b/packages/cli/src/plugins/fetch.test.ts
new file mode 100644
index 0000000000..600cbfeda7
--- /dev/null
+++ b/packages/cli/src/plugins/fetch.test.ts
@@ -0,0 +1,186 @@
+import { mkdir, rm, writeFile } from 'node:fs/promises'
+import { homedir } from 'node:os'
+import { setupServer } from 'msw/node'
+import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
+
+import {
+ address,
+ apiKey,
+ baseUrl,
+ handlers,
+ timeoutAddress,
+ unverifiedContractAddress,
+} from '../../test/utils.js'
+import { fetch, getCacheDir } from './fetch.js'
+
+const server = setupServer(...handlers)
+
+beforeAll(() => server.listen())
+afterEach(() => server.resetHandlers())
+afterAll(() => server.close())
+
+type Fetch = Parameters[0]
+const request: Fetch['request'] = ({ address }) => {
+ return {
+ url: `${baseUrl}?module=contract&action=getabi&address=${address}&apikey=${apiKey}`,
+ }
+}
+const parse: Fetch['parse'] = async ({ response }) => {
+ const data = (await response.json()) as
+ | { status: '1'; message: 'OK'; result: string }
+ | { status: '0'; message: 'NOTOK'; result: string }
+ if (data.status === '0') throw new Error(data.result)
+ return JSON.parse(data.result)
+}
+
+test('fetches ABI', async () => {
+ await expect(
+ fetch({
+ contracts: [{ name: 'WagmiMintExample', address }],
+ request,
+ parse,
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fails to fetch for unverified contract', async () => {
+ await expect(
+ fetch({
+ contracts: [
+ { name: 'WagmiMintExample', address: unverifiedContractAddress },
+ ],
+ request,
+ parse,
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: Contract source code not verified]',
+ )
+})
+
+test('aborts request', async () => {
+ await expect(
+ fetch({
+ contracts: [{ name: 'WagmiMintExample', address: timeoutAddress }],
+ request,
+ parse,
+ timeoutDuration: 1_000,
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[AbortError: This operation was aborted]',
+ )
+})
+
+test('reads from cache', async () => {
+ const cacheDir = `${homedir}/.wagmi-cli/plugins/fetch/cache`
+ await mkdir(cacheDir, { recursive: true })
+
+ const contract = {
+ name: 'WagmiMintExample',
+ address: timeoutAddress,
+ } as const
+ const cacheKey = JSON.stringify(contract)
+ const cacheFilePath = `${cacheDir}/${cacheKey}.json`
+ await writeFile(
+ cacheFilePath,
+ JSON.stringify(
+ {
+ abi: [
+ {
+ inputs: [],
+ name: 'mint',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ ],
+ timestamp: Date.now() + 30_000,
+ },
+ null,
+ 2,
+ ),
+ )
+
+ await expect(
+ fetch({
+ contracts: [contract],
+ request,
+ parse,
+ }).contracts?.(),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": "0xecb504d39723b0be0e3a9aa33d646642d1051ee1",
+ "name": "WagmiMintExample",
+ },
+ ]
+ `)
+
+ await rm(cacheDir, { recursive: true })
+})
+
+test('fails and reads from cache', async () => {
+ const cacheDir = getCacheDir()
+ await mkdir(cacheDir, { recursive: true })
+
+ const contract = {
+ name: 'WagmiMintExample',
+ address: timeoutAddress,
+ } as const
+ const cacheKey = JSON.stringify(contract)
+ const cacheFilePath = `${cacheDir}/${cacheKey}.json`
+ await writeFile(
+ cacheFilePath,
+ JSON.stringify(
+ {
+ abi: [
+ {
+ inputs: [],
+ name: 'mint',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ ],
+ timestamp: Date.now() - 30_000,
+ },
+ null,
+ 2,
+ ),
+ )
+
+ await expect(
+ fetch({
+ contracts: [contract],
+ request,
+ parse,
+ timeoutDuration: 1,
+ }).contracts?.(),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": "0xecb504d39723b0be0e3a9aa33d646642d1051ee1",
+ "name": "WagmiMintExample",
+ },
+ ]
+ `)
+
+ await rm(cacheDir, { recursive: true })
+})
diff --git a/packages/cli/src/plugins/fetch.ts b/packages/cli/src/plugins/fetch.ts
new file mode 100644
index 0000000000..778d4a8162
--- /dev/null
+++ b/packages/cli/src/plugins/fetch.ts
@@ -0,0 +1,127 @@
+import { mkdir, readFile, writeFile } from 'node:fs/promises'
+import { homedir } from 'node:os'
+import { join } from 'pathe'
+
+import type { Abi } from 'viem'
+import type { ContractConfig, Plugin } from '../config.js'
+import type { Compute, RequiredBy } from '../types.js'
+
+export type FetchConfig = {
+ /**
+ * Duration in milliseconds to cache ABIs from request.
+ *
+ * @default 1_800_000 // 30m in ms
+ */
+ cacheDuration?: number | undefined
+ /**
+ * Contracts to fetch ABIs for.
+ */
+ contracts: Compute>[]
+ /**
+ * Function for creating a cache key for contract.
+ */
+ getCacheKey?:
+ | ((config: { contract: Compute> }) => string)
+ | undefined
+ /**
+ * Name of source.
+ */
+ name?: ContractConfig['name'] | undefined
+ /**
+ * Function for parsing ABI from fetch response.
+ *
+ * @default ({ response }) => response.json()
+ */
+ parse?:
+ | ((config: {
+ response: Response
+ }) => ContractConfig['abi'] | Promise)
+ | undefined
+ /**
+ * Function for returning a request to fetch ABI from.
+ */
+ request: (config: {
+ address?: ContractConfig['address'] | undefined
+ name: ContractConfig['name']
+ }) =>
+ | { url: RequestInfo; init?: RequestInit | undefined }
+ | Promise<{ url: RequestInfo; init?: RequestInit | undefined }>
+ /**
+ * Duration in milliseconds before request times out.
+ *
+ * @default 5_000 // 5s in ms
+ */
+ timeoutDuration?: number | undefined
+}
+
+type FetchResult = Compute>
+
+/** Fetches and parses contract ABIs from network resource with `fetch`. */
+export function fetch(config: FetchConfig): FetchResult {
+ const {
+ cacheDuration = 1_800_000,
+ contracts: contractConfigs,
+ getCacheKey = ({ contract }) => JSON.stringify(contract),
+ name = 'Fetch',
+ parse = ({ response }) => response.json(),
+ request,
+ timeoutDuration = 5_000,
+ } = config
+
+ return {
+ async contracts() {
+ const cacheDir = getCacheDir()
+ await mkdir(cacheDir, { recursive: true })
+
+ const timestamp = Date.now() + cacheDuration
+ const contracts = []
+ for (const contract of contractConfigs) {
+ const cacheKey = getCacheKey({ contract })
+ const cacheFilePath = join(cacheDir, `${cacheKey}.json`)
+ const cachedFile = JSON.parse(
+ await readFile(cacheFilePath, 'utf8').catch(() => 'null'),
+ )
+
+ let abi: Abi | undefined
+ if (cachedFile?.timestamp > Date.now()) abi = cachedFile.abi
+ else {
+ try {
+ const controller = new globalThis.AbortController()
+ const timeout = setTimeout(
+ () => controller.abort(),
+ timeoutDuration,
+ )
+
+ const { url, init } = await request(contract)
+ const response = await globalThis.fetch(url, {
+ ...init,
+ signal: controller.signal,
+ })
+ clearTimeout(timeout)
+
+ abi = await parse({ response })
+ await writeFile(
+ cacheFilePath,
+ `${JSON.stringify({ abi, timestamp }, undefined, 2)}\n`,
+ )
+ } catch (error) {
+ try {
+ // Attempt to read from cache if fetch fails.
+ abi = JSON.parse(await readFile(cacheFilePath, 'utf8')).abi
+ } catch {}
+ if (!abi) throw error
+ }
+ }
+
+ if (!abi) throw Error('Failed to fetch ABI for contract.')
+ contracts.push({ abi, address: contract.address, name: contract.name })
+ }
+ return contracts
+ },
+ name,
+ }
+}
+
+export function getCacheDir() {
+ return join(homedir(), '.wagmi-cli/plugins/fetch/cache')
+}
diff --git a/packages/cli/src/plugins/foundry.test.ts b/packages/cli/src/plugins/foundry.test.ts
new file mode 100644
index 0000000000..75e5ec73ee
--- /dev/null
+++ b/packages/cli/src/plugins/foundry.test.ts
@@ -0,0 +1,153 @@
+import fixtures from 'fixturez'
+import { dirname, resolve } from 'pathe'
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { foundry } from './foundry.js'
+
+const f = fixtures(__dirname)
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('forge not installed', async () => {
+ const dir = f.temp()
+ expect(
+ foundry({
+ project: dir,
+ forge: {
+ path: '/path/to/forge',
+ },
+ }).validate?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: forge must be installed to use Foundry plugin.
+ To install, follow the instructions at https://book.getfoundry.sh/getting-started/installation]
+ `)
+})
+
+test('project does not exist', async () => {
+ const dir = f.temp()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ try {
+ await foundry({ project: '../path/to/project' }).validate?.()
+ } catch (error) {
+ expect(
+ (error as Error).message.replace(dirname(dir), '..'),
+ ).toMatchInlineSnapshot('"Foundry project ../path/to/project not found."')
+ }
+})
+
+test('validates without project', async () => {
+ const dir = resolve(__dirname, '__fixtures__/foundry/')
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(foundry().validate?.()).resolves.toBeUndefined()
+})
+
+test('contracts', async () => {
+ await expect(
+ foundry({
+ project: resolve(__dirname, '__fixtures__/foundry/'),
+ exclude: ['Foo.sol/**'],
+ }).contracts?.(),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "name": "increment",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "number",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "newNumber",
+ "type": "uint256",
+ },
+ ],
+ "name": "setNumber",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": undefined,
+ "name": "Counter",
+ },
+ ]
+ `)
+})
+
+test('contracts without project', async () => {
+ const dir = resolve(__dirname, '__fixtures__/foundry/')
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(
+ foundry({
+ exclude: ['Foo.sol/**'],
+ }).contracts?.(),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "name": "increment",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "number",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "newNumber",
+ "type": "uint256",
+ },
+ ],
+ "name": "setNumber",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": undefined,
+ "name": "Counter",
+ },
+ ]
+ `)
+})
diff --git a/packages/cli/src/plugins/foundry.ts b/packages/cli/src/plugins/foundry.ts
new file mode 100644
index 0000000000..bb5b302f42
--- /dev/null
+++ b/packages/cli/src/plugins/foundry.ts
@@ -0,0 +1,263 @@
+import { execSync, spawn, spawnSync } from 'node:child_process'
+import { existsSync } from 'node:fs'
+import { readFile } from 'node:fs/promises'
+import dedent from 'dedent'
+import { fdir } from 'fdir'
+import { basename, extname, join, resolve } from 'pathe'
+import pc from 'picocolors'
+import { z } from 'zod'
+
+import type { ContractConfig, Plugin } from '../config.js'
+import * as logger from '../logger.js'
+import type { Compute, RequiredBy } from '../types.js'
+
+export const foundryDefaultExcludes = [
+ 'Base.sol/**',
+ 'Common.sol/**',
+ 'Components.sol/**',
+ 'IERC165.sol/**',
+ 'IERC20.sol/**',
+ 'IERC721.sol/**',
+ 'IMulticall2.sol/**',
+ 'MockERC20.sol/**',
+ 'MockERC721.sol/**',
+ 'Script.sol/**',
+ 'StdAssertions.sol/**',
+ 'StdChains.sol/**',
+ 'StdCheats.sol/**',
+ 'StdError.sol/**',
+ 'StdInvariant.sol/**',
+ 'StdJson.sol/**',
+ 'StdMath.sol/**',
+ 'StdStorage.sol/**',
+ 'StdStyle.sol/**',
+ 'StdToml.sol/**',
+ 'StdUtils.sol/**',
+ 'Test.sol/**',
+ 'Vm.sol/**',
+ 'build-info/**',
+ 'console.sol/**',
+ 'console2.sol/**',
+ 'safeconsole.sol/**',
+ '**.s.sol/*.json',
+ '**.t.sol/*.json',
+]
+
+export type FoundryConfig = {
+ /**
+ * Project's artifacts directory.
+ *
+ * Same as your project's `--out` (`-o`) option.
+ *
+ * @default foundry.config#out | 'out'
+ */
+ artifacts?: string | undefined
+ /** Mapping of addresses to attach to artifacts. */
+ deployments?: { [key: string]: ContractConfig['address'] } | undefined
+ /** Artifact files to exclude. */
+ exclude?: string[] | undefined
+ /** [Forge](https://book.getfoundry.sh/forge) configuration */
+ forge?:
+ | {
+ /**
+ * Remove build artifacts and cache directories on start up.
+ *
+ * @default false
+ */
+ clean?: boolean | undefined
+ /**
+ * Build Foundry project before fetching artifacts.
+ *
+ * @default true
+ */
+ build?: boolean | undefined
+ /**
+ * Path to `forge` executable command
+ *
+ * @default "forge"
+ */
+ path?: string | undefined
+ /**
+ * Rebuild every time a watched file or directory is changed.
+ *
+ * @default true
+ */
+ rebuild?: boolean | undefined
+ }
+ | undefined
+ /** Artifact files to include. */
+ include?: string[] | undefined
+ /** Optional prefix to prepend to artifact names. */
+ namePrefix?: string | undefined
+ /** Path to foundry project. */
+ project?: string | undefined
+}
+
+type FoundryResult = Compute<
+ RequiredBy
+>
+
+const FoundryConfigSchema = z.object({
+ out: z.string().default('out'),
+ src: z.string().default('src'),
+})
+
+/** Resolves ABIs from [Foundry](https://github.com/foundry-rs/foundry) project. */
+export function foundry(config: FoundryConfig = {}): FoundryResult {
+ const {
+ artifacts,
+ deployments = {},
+ exclude = foundryDefaultExcludes,
+ forge: {
+ clean = false,
+ build = true,
+ path: forgeExecutable = 'forge',
+ rebuild = true,
+ } = {},
+ include = ['*.json'],
+ namePrefix = '',
+ } = config
+
+ function getContractName(artifactPath: string, usePrefix = true) {
+ const filename = basename(artifactPath)
+ const extension = extname(artifactPath)
+ return `${usePrefix ? namePrefix : ''}${filename.replace(extension, '')}`
+ }
+
+ async function getContract(artifactPath: string) {
+ const artifact = await JSON.parse(await readFile(artifactPath, 'utf8'))
+ return {
+ abi: artifact.abi,
+ address: (deployments as Record)[
+ getContractName(artifactPath, false)
+ ],
+ name: getContractName(artifactPath),
+ }
+ }
+
+ function getArtifactPaths(artifactsDirectory: string) {
+ const crawler = new fdir().withBasePath().globWithOptions(
+ include.map((x) => `${artifactsDirectory}/**/${x}`),
+ {
+ dot: true,
+ ignore: exclude.map((x) => `${artifactsDirectory}/**/${x}`),
+ },
+ )
+ return crawler.crawl(artifactsDirectory).withPromise()
+ }
+
+ const project = resolve(process.cwd(), config.project ?? '')
+
+ let foundryConfig: z.infer = {
+ out: 'out',
+ src: 'src',
+ }
+ try {
+ const result = spawnSync(
+ forgeExecutable,
+ ['config', '--json', '--root', project],
+ {
+ encoding: 'utf-8',
+ shell: true,
+ },
+ )
+ if (result.error) throw result.error
+ if (result.status !== 0)
+ throw new Error(`Failed with code ${result.status}`)
+ if (result.signal) throw new Error('Process terminated by signal')
+ foundryConfig = FoundryConfigSchema.parse(JSON.parse(result.stdout))
+ } catch {
+ } finally {
+ foundryConfig = {
+ ...foundryConfig,
+ out: artifacts ?? foundryConfig.out,
+ }
+ }
+
+ const artifactsDirectory = join(project, foundryConfig.out)
+
+ return {
+ async contracts() {
+ if (clean)
+ spawnSync(forgeExecutable, ['clean', '--root', project], {
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ if (build)
+ spawnSync(forgeExecutable, ['build', '--root', project], {
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ if (!existsSync(artifactsDirectory))
+ throw new Error('Artifacts not found.')
+
+ const artifactPaths = await getArtifactPaths(artifactsDirectory)
+ const contracts = []
+ for (const artifactPath of artifactPaths) {
+ const contract = await getContract(artifactPath)
+ if (!contract.abi?.length) continue
+ contracts.push(contract)
+ }
+ return contracts
+ },
+ name: 'Foundry',
+ async validate() {
+ // Check that project directory exists
+ if (!existsSync(project))
+ throw new Error(`Foundry project ${pc.gray(config.project)} not found.`)
+
+ // Ensure forge is installed
+ if (clean || build || rebuild)
+ try {
+ execSync(`${forgeExecutable} --version`, {
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ } catch (_error) {
+ throw new Error(dedent`
+ forge must be installed to use Foundry plugin.
+ To install, follow the instructions at https://book.getfoundry.sh/getting-started/installation
+ `)
+ }
+ },
+ watch: {
+ command: rebuild
+ ? async () => {
+ logger.log(
+ `${pc.magenta('Foundry')} Watching project at ${pc.gray(
+ project,
+ )}`,
+ )
+ const subprocess = spawn(forgeExecutable, [
+ 'build',
+ '--watch',
+ '--root',
+ project,
+ ])
+ subprocess.stdout?.on('data', (data) => {
+ process.stdout.write(`${pc.magenta('Foundry')} ${data}`)
+ })
+
+ process.once('SIGINT', shutdown)
+ process.once('SIGTERM', shutdown)
+ function shutdown() {
+ subprocess?.kill()
+ }
+ }
+ : undefined,
+ paths: [
+ ...include.map((x) => `${artifactsDirectory}/**/${x}`),
+ ...exclude.map((x) => `!${artifactsDirectory}/**/${x}`),
+ ],
+ async onAdd(path) {
+ return getContract(path)
+ },
+ async onChange(path) {
+ return getContract(path)
+ },
+ async onRemove(path) {
+ return getContractName(path)
+ },
+ },
+ }
+}
diff --git a/packages/cli/src/plugins/hardhat.test.ts b/packages/cli/src/plugins/hardhat.test.ts
new file mode 100644
index 0000000000..efb416c5e6
--- /dev/null
+++ b/packages/cli/src/plugins/hardhat.test.ts
@@ -0,0 +1,85 @@
+import fixtures from 'fixturez'
+import { dirname, resolve } from 'pathe'
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { hardhat } from './hardhat.js'
+
+const f = fixtures(__dirname)
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('validate', async () => {
+ const temp = f.temp()
+ expect(
+ hardhat({ project: temp }).validate?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: hardhat must be installed to use Hardhat plugin.]',
+ )
+})
+
+test('project does not exist', async () => {
+ const dir = f.temp()
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ try {
+ await hardhat({ project: '../path/to/project' }).validate?.()
+ } catch (error) {
+ expect(
+ (error as Error).message.replace(dirname(dir), '..'),
+ ).toMatchInlineSnapshot('"Hardhat project ../path/to/project not found."')
+ }
+})
+
+test('contracts', async () => {
+ expect(
+ hardhat({
+ project: resolve(__dirname, '__fixtures__/hardhat/'),
+ exclude: ['Foo.sol/**'],
+ }).contracts?.(),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "abi": [
+ {
+ "inputs": [],
+ "name": "increment",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ {
+ "inputs": [],
+ "name": "number",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256",
+ },
+ ],
+ "stateMutability": "view",
+ "type": "function",
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "newNumber",
+ "type": "uint256",
+ },
+ ],
+ "name": "setNumber",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function",
+ },
+ ],
+ "address": undefined,
+ "name": "Counter",
+ },
+ ]
+ `)
+}, 10_000)
diff --git a/packages/cli/src/plugins/hardhat.ts b/packages/cli/src/plugins/hardhat.ts
new file mode 100644
index 0000000000..a4feb6efdf
--- /dev/null
+++ b/packages/cli/src/plugins/hardhat.ts
@@ -0,0 +1,235 @@
+import { execSync, spawn } from 'node:child_process'
+import { existsSync } from 'node:fs'
+import { readFile } from 'node:fs/promises'
+import { fdir } from 'fdir'
+import { basename, extname, join, resolve } from 'pathe'
+import pc from 'picocolors'
+
+import type { ContractConfig, Plugin } from '../config.js'
+import * as logger from '../logger.js'
+import type { Compute, RequiredBy } from '../types.js'
+import { getIsPackageInstalled, getPackageManager } from '../utils/packages.js'
+
+export const hardhatDefaultExcludes = ['build-info/**', '*.dbg.json']
+
+export type HardhatConfig = {
+ /**
+ * Project's artifacts directory.
+ *
+ * Same as your project's `artifacts` [path configuration](https://hardhat.org/hardhat-runner/docs/config#path-configuration) option.
+ *
+ * @default 'artifacts/'
+ */
+ artifacts?: string | undefined
+ /** Mapping of addresses to attach to artifacts. */
+ deployments?: { [key: string]: ContractConfig['address'] } | undefined
+ /** Artifact files to exclude. */
+ exclude?: string[] | undefined
+ /** Commands to run */
+ commands?:
+ | {
+ /**
+ * Remove build artifacts and cache directories on start up.
+ *
+ * @default `${packageManger} hardhat clean`
+ */
+ clean?: string | boolean | undefined
+ /**
+ * Build Hardhat project before fetching artifacts.
+ *
+ * @default `${packageManger} hardhat compile`
+ */
+ build?: string | boolean | undefined
+ /**
+ * Command to run when watched file or directory is changed.
+ *
+ * @default `${packageManger} hardhat compile`
+ */
+ rebuild?: string | boolean | undefined
+ }
+ | undefined
+ /** Artifact files to include. */
+ include?: string[] | undefined
+ /** Optional prefix to prepend to artifact names. */
+ namePrefix?: string | undefined
+ /** Path to Hardhat project. */
+ project: string
+ /**
+ * Project's artifacts directory.
+ *
+ * Same as your project's `sources` [path configuration](https://hardhat.org/hardhat-runner/docs/config#path-configuration) option.
+ *
+ * @default 'contracts/'
+ */
+ sources?: string | undefined
+}
+
+type HardhatResult = Compute<
+ RequiredBy
+>
+
+/** Resolves ABIs from [Hardhat](https://github.com/NomicFoundation/hardhat) project. */
+export function hardhat(config: HardhatConfig): HardhatResult {
+ const {
+ artifacts = 'artifacts',
+ deployments = {},
+ exclude = hardhatDefaultExcludes,
+ commands = {},
+ include = ['*.json'],
+ namePrefix = '',
+ sources = 'contracts',
+ } = config
+
+ function getContractName(artifact: { contractName: string }) {
+ return `${namePrefix}${artifact.contractName}`
+ }
+
+ async function getContract(artifactPath: string) {
+ const artifact = await JSON.parse(await readFile(artifactPath, 'utf8'))
+ return {
+ abi: artifact.abi,
+ address: deployments[artifact.contractName],
+ name: getContractName(artifact),
+ }
+ }
+
+ function getArtifactPaths(artifactsDirectory: string) {
+ const crawler = new fdir().withBasePath().globWithOptions(
+ include.map((x) => `${artifactsDirectory}/**/${x}`),
+ {
+ dot: true,
+ ignore: exclude.map((x) => `${artifactsDirectory}/**/${x}`),
+ },
+ )
+ return crawler.crawl(artifactsDirectory).withPromise()
+ }
+
+ const project = resolve(process.cwd(), config.project)
+ const artifactsDirectory = join(project, artifacts)
+ const sourcesDirectory = join(project, sources)
+
+ const { build = true, clean = false, rebuild = true } = commands
+ return {
+ async contracts() {
+ if (clean) {
+ const packageManager = await getPackageManager(true)
+ const [command, ...options] = (
+ typeof clean === 'boolean' ? `${packageManager} hardhat clean` : clean
+ ).split(' ')
+ execSync(`${command!} ${options.join(' ')}`, {
+ cwd: project,
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ }
+ if (build) {
+ const packageManager = await getPackageManager(true)
+ const [command, ...options] = (
+ typeof build === 'boolean'
+ ? `${packageManager} hardhat compile`
+ : build
+ ).split(' ')
+ execSync(`${command!} ${options.join(' ')}`, {
+ cwd: project,
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ }
+ if (!existsSync(artifactsDirectory))
+ throw new Error('Artifacts not found.')
+
+ const artifactPaths = await getArtifactPaths(artifactsDirectory)
+ const contracts = []
+ for (const artifactPath of artifactPaths) {
+ const contract = await getContract(artifactPath)
+ if (!contract.abi?.length) continue
+ contracts.push(contract)
+ }
+ return contracts
+ },
+ name: 'Hardhat',
+ async validate() {
+ // Check that project directory exists
+ if (!existsSync(project))
+ throw new Error(`Hardhat project ${pc.gray(project)} not found.`)
+
+ // Check that `hardhat` is installed
+ const packageName = 'hardhat'
+ const isPackageInstalled = await getIsPackageInstalled({
+ packageName,
+ cwd: project,
+ })
+ if (isPackageInstalled) return
+ throw new Error(`${packageName} must be installed to use Hardhat plugin.`)
+ },
+ watch: {
+ command: rebuild
+ ? async () => {
+ logger.log(
+ `${pc.blue('Hardhat')} Watching project at ${pc.gray(project)}`,
+ )
+
+ const [command, ...options] = (
+ typeof rebuild === 'boolean'
+ ? `${await getPackageManager(true)} hardhat compile`
+ : rebuild
+ ).split(' ')
+
+ const { watch } = await import('chokidar')
+ const watcher = watch(sourcesDirectory, {
+ atomic: true,
+ awaitWriteFinish: true,
+ ignoreInitial: true,
+ persistent: true,
+ })
+ watcher.on('all', async (event, path) => {
+ if (event !== 'change' && event !== 'add' && event !== 'unlink')
+ return
+ logger.log(
+ `${pc.blue('Hardhat')} Detected ${event} at ${basename(path)}`,
+ )
+ const subprocess = spawn(command!, options, {
+ cwd: project,
+ })
+ subprocess.stdout?.on('data', (data) => {
+ process.stdout.write(`${pc.blue('Hardhat')} ${data}`)
+ })
+ })
+
+ process.once('SIGINT', shutdown)
+ process.once('SIGTERM', shutdown)
+ async function shutdown() {
+ await watcher.close()
+ }
+ }
+ : undefined,
+ paths: [
+ artifactsDirectory,
+ ...include.map((x) => `${artifactsDirectory}/**/${x}`),
+ ...exclude.map((x) => `!${artifactsDirectory}/**/${x}`),
+ ],
+ async onAdd(path) {
+ return getContract(path)
+ },
+ async onChange(path) {
+ return getContract(path)
+ },
+ async onRemove(path) {
+ const filename = basename(path)
+ const extension = extname(path)
+ // Since we can't use `getContractName`, guess from path
+ const removedContractName = `${namePrefix}${filename.replace(
+ extension,
+ '',
+ )}`
+ const artifactPaths = await getArtifactPaths(artifactsDirectory)
+ for (const artifactPath of artifactPaths) {
+ const contract = await getContract(artifactPath)
+ // If contract with same name exists, don't remove
+ if (contract.name === removedContractName) return
+ }
+ return removedContractName
+ },
+ },
+ }
+}
diff --git a/packages/cli/src/plugins/react.test.ts b/packages/cli/src/plugins/react.test.ts
new file mode 100644
index 0000000000..939a5299ae
--- /dev/null
+++ b/packages/cli/src/plugins/react.test.ts
@@ -0,0 +1,337 @@
+import { erc20Abi } from 'viem'
+import { expect, test } from 'vitest'
+
+import { react } from './react.js'
+
+test('default', async () => {
+ const result = await react().run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.imports).toMatchInlineSnapshot(`
+ "import { createUseReadContract, createUseWriteContract, createUseSimulateContract, createUseWatchContractEvent } from 'wagmi/codegen'
+ "
+ `)
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useReadErc20 = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const useReadErc20Allowance = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const useReadErc20BalanceOf = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const useReadErc20Decimals = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const useReadErc20Name = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'name' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const useReadErc20Symbol = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const useReadErc20TotalSupply = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useWriteErc20 = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const useWriteErc20Approve = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const useWriteErc20Transfer = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const useWriteErc20TransferFrom = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useSimulateErc20 = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const useSimulateErc20Approve = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const useSimulateErc20Transfer = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const useSimulateErc20TransferFrom = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useWatchErc20Event = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const useWatchErc20ApprovalEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const useWatchErc20TransferEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, eventName: 'Transfer' })"
+ `)
+})
+
+test('address', async () => {
+ const result = await react().run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ addressName: 'erc20Address',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useReadErc20 = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const useReadErc20Allowance = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const useReadErc20BalanceOf = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const useReadErc20Decimals = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const useReadErc20Name = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'name' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const useReadErc20Symbol = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const useReadErc20TotalSupply = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useWriteErc20 = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const useWriteErc20Approve = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const useWriteErc20Transfer = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const useWriteErc20TransferFrom = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useSimulateErc20 = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const useSimulateErc20Approve = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const useSimulateErc20Transfer = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const useSimulateErc20TransferFrom = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useWatchErc20Event = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const useWatchErc20ApprovalEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const useWatchErc20TransferEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Transfer' })"
+ `)
+})
+
+test('legacy hook names', async () => {
+ const result = await react({ getHookName: 'legacy' }).run?.({
+ contracts: [
+ {
+ name: 'erc20',
+ abi: erc20Abi,
+ content: '',
+ meta: {
+ abiName: 'erc20Abi',
+ addressName: 'erc20Address',
+ },
+ },
+ ],
+ isTypeScript: true,
+ outputs: [],
+ })
+
+ expect(result?.content).toMatchInlineSnapshot(`
+ "/**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useErc20Read = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"allowance"\`
+ */
+ export const useErc20Allowance = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'allowance' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"balanceOf"\`
+ */
+ export const useErc20BalanceOf = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'balanceOf' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"decimals"\`
+ */
+ export const useErc20Decimals = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'decimals' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"name"\`
+ */
+ export const useErc20Name = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'name' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"symbol"\`
+ */
+ export const useErc20Symbol = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'symbol' })
+
+ /**
+ * Wraps __{@link useReadContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"totalSupply"\`
+ */
+ export const useErc20TotalSupply = /*#__PURE__*/ createUseReadContract({ abi: erc20Abi, address: erc20Address, functionName: 'totalSupply' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useErc20Write = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const useErc20Approve = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const useErc20Transfer = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useWriteContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const useErc20TransferFrom = /*#__PURE__*/ createUseWriteContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const usePrepareErc20Write = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"approve"\`
+ */
+ export const usePrepareErc20Approve = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'approve' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transfer"\`
+ */
+ export const usePrepareErc20Transfer = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transfer' })
+
+ /**
+ * Wraps __{@link useSimulateContract}__ with \`abi\` set to __{@link erc20Abi}__ and \`functionName\` set to \`"transferFrom"\`
+ */
+ export const usePrepareErc20TransferFrom = /*#__PURE__*/ createUseSimulateContract({ abi: erc20Abi, address: erc20Address, functionName: 'transferFrom' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__
+ */
+ export const useErc20Event = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Approval"\`
+ */
+ export const useErc20ApprovalEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Approval' })
+
+ /**
+ * Wraps __{@link useWatchContractEvent}__ with \`abi\` set to __{@link erc20Abi}__ and \`eventName\` set to \`"Transfer"\`
+ */
+ export const useErc20TransferEvent = /*#__PURE__*/ createUseWatchContractEvent({ abi: erc20Abi, address: erc20Address, eventName: 'Transfer' })"
+ `)
+})
diff --git a/packages/cli/src/plugins/react.ts b/packages/cli/src/plugins/react.ts
new file mode 100644
index 0000000000..b76ea006a5
--- /dev/null
+++ b/packages/cli/src/plugins/react.ts
@@ -0,0 +1,312 @@
+import { pascalCase } from 'change-case'
+
+import type { Contract, Plugin } from '../config.js'
+import type { Compute, RequiredBy } from '../types.js'
+import { getAddressDocString } from '../utils/getAddressDocString.js'
+
+export type ReactConfig = {
+ getHookName?:
+ | 'legacy' // TODO: Deprecate `'legacy'` option
+ | ((options: {
+ contractName: string
+ itemName?: string | undefined
+ type: 'read' | 'simulate' | 'watch' | 'write'
+ }) => `use${string}`)
+}
+
+type ReactResult = Compute>
+
+export function react(config: ReactConfig = {}): ReactResult {
+ return {
+ name: 'React',
+ async run({ contracts }) {
+ const imports = new Set([])
+ const content: string[] = []
+ const pure = '/*#__PURE__*/'
+
+ const hookNames = new Set()
+ for (const contract of contracts) {
+ let hasReadFunction = false
+ let hasWriteFunction = false
+ let hasEvent = false
+ const readItems = []
+ const writeItems = []
+ const eventItems = []
+ for (const item of contract.abi) {
+ if (item.type === 'function')
+ if (
+ item.stateMutability === 'view' ||
+ item.stateMutability === 'pure'
+ ) {
+ hasReadFunction = true
+ readItems.push(item)
+ } else {
+ hasWriteFunction = true
+ writeItems.push(item)
+ }
+ else if (item.type === 'event') {
+ hasEvent = true
+ eventItems.push(item)
+ }
+ }
+
+ let innerContent: string
+ if (contract.meta.addressName)
+ innerContent = `abi: ${contract.meta.abiName}, address: ${contract.meta.addressName}`
+ else innerContent = `abi: ${contract.meta.abiName}`
+
+ if (hasReadFunction) {
+ const hookName = getHookName(config, hookNames, 'read', contract.name)
+ const docString = genDocString('useReadContract', contract)
+ const functionName = 'createUseReadContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of readItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'pure' &&
+ item.stateMutability !== 'view'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'read',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('useReadContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+
+ if (hasWriteFunction) {
+ {
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'write',
+ contract.name,
+ )
+ const docString = genDocString('useWriteContract', contract)
+ const functionName = 'createUseWriteContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of writeItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'nonpayable' &&
+ item.stateMutability !== 'payable'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'write',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('useWriteContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+
+ {
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'simulate',
+ contract.name,
+ )
+ const docString = genDocString('useSimulateContract', contract)
+ const functionName = 'createUseSimulateContract'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of writeItems) {
+ if (item.type !== 'function') continue
+ if (
+ item.stateMutability !== 'nonpayable' &&
+ item.stateMutability !== 'payable'
+ )
+ continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'simulate',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('useSimulateContract', contract, {
+ name: 'functionName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent}, functionName: '${item.name}' })`,
+ )
+ }
+ }
+ }
+
+ if (hasEvent) {
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'watch',
+ contract.name,
+ )
+ const docString = genDocString('useWatchContractEvent', contract)
+ const functionName = 'createUseWatchContractEvent'
+ imports.add(functionName)
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent} })`,
+ )
+
+ const names = new Set()
+ for (const item of eventItems) {
+ if (item.type !== 'event') continue
+
+ // Skip overrides since they are captured by same hook
+ if (names.has(item.name)) continue
+ names.add(item.name)
+
+ const hookName = getHookName(
+ config,
+ hookNames,
+ 'watch',
+ contract.name,
+ item.name,
+ )
+ const docString = genDocString('useWatchContractEvent', contract, {
+ name: 'eventName',
+ value: item.name,
+ })
+ content.push(
+ `${docString}
+export const ${hookName} = ${pure} ${functionName}({ ${innerContent}, eventName: '${item.name}' })`,
+ )
+ }
+ }
+ }
+
+ const importValues = [...imports.values()]
+
+ return {
+ imports: importValues.length
+ ? `import { ${importValues.join(', ')} } from 'wagmi/codegen'\n`
+ : '',
+ content: content.join('\n\n'),
+ }
+ },
+ }
+}
+
+function genDocString(
+ hookName: string,
+ contract: Contract,
+ item?: { name: string; value: string },
+) {
+ let description = `Wraps __{@link ${hookName}}__ with \`abi\` set to __{@link ${contract.meta.abiName}}__`
+ if (item) description += ` and \`${item.name}\` set to \`"${item.value}"\``
+
+ const docString = getAddressDocString({ address: contract.address })
+ if (docString)
+ return `/**
+ * ${description}
+ *
+ ${docString}
+ */`
+
+ return `/**
+ * ${description}
+ */`
+}
+
+function getHookName(
+ config: ReactConfig,
+ hookNames: Set,
+ type: 'read' | 'simulate' | 'watch' | 'write',
+ contractName: string,
+ itemName?: string | undefined,
+) {
+ const ContractName = pascalCase(contractName)
+ const ItemName = itemName ? pascalCase(itemName) : undefined
+
+ let hookName: string
+ if (typeof config.getHookName === 'function')
+ hookName = config.getHookName({
+ type,
+ contractName: ContractName,
+ itemName: ItemName,
+ })
+ else if (typeof config.getHookName === 'string') {
+ switch (type) {
+ case 'read':
+ hookName = `use${ContractName}${ItemName ?? 'Read'}`
+ break
+ case 'simulate':
+ hookName = `usePrepare${ContractName}${ItemName ?? 'Write'}`
+ break
+ case 'watch':
+ hookName = `use${ContractName}${ItemName ?? ''}Event`
+ break
+ case 'write':
+ hookName = `use${ContractName}${ItemName ?? 'Write'}`
+ break
+ }
+ } else {
+ hookName = `use${pascalCase(type)}${ContractName}${ItemName ?? ''}`
+ if (type === 'watch') hookName = `${hookName}Event`
+ }
+
+ if (hookNames.has(hookName))
+ throw new Error(
+ `Hook name "${hookName}" must be unique for contract "${contractName}". Try using \`getHookName\` to create a unique name.`,
+ )
+
+ hookNames.add(hookName)
+ return hookName
+}
diff --git a/packages/cli/src/plugins/sourcify.test.ts b/packages/cli/src/plugins/sourcify.test.ts
new file mode 100644
index 0000000000..842a291147
--- /dev/null
+++ b/packages/cli/src/plugins/sourcify.test.ts
@@ -0,0 +1,83 @@
+import { http, HttpResponse } from 'msw'
+import { setupServer } from 'msw/node'
+import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
+
+import { depositAbi } from '../../test/constants.js'
+import { sourcify } from './sourcify.js'
+
+const baseUrl = 'https://sourcify.dev/server/v2/contract'
+const address = '0x00000000219ab540356cbb839cbe05303d7705fa'
+const chainId = 1
+const multichainAddress = '0xC4c622862a8F548997699bE24EA4bc504e5cA865'
+const multichainIdGnosis = 100
+const multichainIdPolygon = 137
+const successJson = {
+ abi: depositAbi,
+}
+
+const handlers = [
+ http.get(`${baseUrl}/${chainId}/${address}`, () =>
+ HttpResponse.json(successJson),
+ ),
+ http.get(`${baseUrl}/${multichainIdGnosis}/${address}`, () =>
+ HttpResponse.json({}, { status: 404 }),
+ ),
+ http.get(`${baseUrl}/${multichainIdGnosis}/${multichainAddress}`, () =>
+ HttpResponse.json(successJson),
+ ),
+ http.get(`${baseUrl}/${multichainIdPolygon}/${multichainAddress}`, () =>
+ HttpResponse.json(successJson),
+ ),
+]
+
+const server = setupServer(...handlers)
+
+beforeAll(() => server.listen())
+afterEach(() => server.resetHandlers())
+afterAll(() => server.close())
+
+test('fetches ABI', () => {
+ expect(
+ sourcify({
+ chainId: chainId,
+ contracts: [{ name: 'DepositContract', address }],
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fetches ABI with multichain deployment', () => {
+ expect(
+ sourcify({
+ chainId: 100,
+ contracts: [
+ {
+ name: 'Community',
+ address: { 100: multichainAddress, 137: multichainAddress },
+ },
+ ],
+ }).contracts?.(),
+ ).resolves.toMatchSnapshot()
+})
+
+test('fails to fetch for unverified contract', () => {
+ expect(
+ sourcify({
+ chainId: 100,
+ contracts: [{ name: 'DepositContract', address }],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ '[Error: Contract not found in Sourcify repository.]',
+ )
+})
+
+test('missing address for chainId', () => {
+ expect(
+ sourcify({
+ chainId: 1,
+ // @ts-expect-error `chainId` and `keyof typeof contracts[number].address` mismatch
+ contracts: [{ name: 'DepositContract', address: { 10: address } }],
+ }).contracts?.(),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: No address found for chainId "1". Make sure chainId "1" is set as an address.]`,
+ )
+})
diff --git a/packages/cli/src/plugins/sourcify.ts b/packages/cli/src/plugins/sourcify.ts
new file mode 100644
index 0000000000..0d452046ca
--- /dev/null
+++ b/packages/cli/src/plugins/sourcify.ts
@@ -0,0 +1,312 @@
+import { Abi as AbiSchema } from 'abitype/zod'
+import type { Address } from 'viem'
+import { z } from 'zod'
+
+import type { ContractConfig } from '../config.js'
+import { fromZodError } from '../errors.js'
+import type { Compute } from '../types.js'
+import { fetch } from './fetch.js'
+
+export type SourcifyConfig = {
+ /**
+ * Duration in milliseconds to cache ABIs.
+ *
+ * @default 1_800_000 // 30m in ms
+ */
+ cacheDuration?: number | undefined
+ /**
+ * Chain id to use for fetching ABI.
+ *
+ * If `address` is an object, `chainId` is used to select the address.
+ *
+ * See https://docs.sourcify.dev/docs/chains for supported chains.
+ */
+ chainId: (chainId extends ChainId ? chainId : never) | (ChainId & {})
+ /**
+ * Contracts to fetch ABIs for.
+ */
+ contracts: Compute, 'abi'>>[]
+}
+
+const SourcifyResponse = z.object({
+ abi: AbiSchema,
+})
+
+/** Fetches contract ABIs from Sourcify. */
+export function sourcify(
+ config: SourcifyConfig,
+) {
+ const { cacheDuration, chainId, contracts: contracts_ } = config
+
+ const contracts = contracts_.map((x) => ({
+ ...x,
+ address:
+ typeof x.address === 'string' ? { [chainId]: x.address } : x.address,
+ })) as Omit[]
+
+ return fetch({
+ cacheDuration,
+ contracts,
+ async parse({ response }) {
+ if (response.status === 404)
+ throw new Error('Contract not found in Sourcify repository.')
+
+ const json = await response.json()
+ const parsed = await SourcifyResponse.safeParseAsync(json)
+ if (!parsed.success)
+ throw fromZodError(parsed.error, { prefix: 'Invalid response' })
+
+ if (parsed.data.abi) return parsed.data.abi as ContractConfig['abi']
+ throw new Error('contract not found')
+ },
+ request({ address }) {
+ if (!address) throw new Error('address is required')
+
+ let contractAddress: Address | undefined
+ if (typeof address === 'string') contractAddress = address
+ else if (typeof address === 'object') contractAddress = address[chainId]
+
+ if (!contractAddress)
+ throw new Error(
+ `No address found for chainId "${chainId}". Make sure chainId "${chainId}" is set as an address.`,
+ )
+ return {
+ url: `https://sourcify.dev/server/v2/contract/${chainId}/${contractAddress}?fields=abi`,
+ }
+ },
+ })
+}
+
+// Supported chains
+// https://docs.sourcify.dev/docs/chains
+type ChainId =
+ | 1 // Ethereum Mainnet
+ | 17000 // Ethereum Testnet Holesky
+ | 5 // Ethereum Testnet Goerli
+ | 11155111 // Ethereum Testnet Sepolia
+ | 3 // Ethereum Testnet Ropsten
+ | 4 // Ethereum Testnet Rinkeby
+ | 10 // OP Mainnet
+ | 100 // Gnosis
+ | 100009 // VeChain
+ | 100010 // VeChain Testnet
+ | 1001 // Kaia Kairos Testnet
+ | 10200 // Gnosis Chiado Testnet
+ | 10242 // Arthera Mainnet
+ | 10243 // Arthera Testnet
+ | 1030 // Conflux eSpace
+ | 103090 // Crystaleum
+ | 105105 // Stratis Mainnet
+ | 106 // Velas EVM Mainnet
+ | 10849 // Lamina1
+ | 10850 // Lamina1 Identity
+ | 1088 // Metis Andromeda Mainnet
+ | 1101 // Polygon zkEVM
+ | 111000 // Siberium Test Network
+ | 11111 // WAGMI
+ | 1114 // Core Blockchain Testnet2
+ | 1115 // Core Blockchain Testnet
+ | 11155420 // OP Sepolia Testnet
+ | 1116 // Core Blockchain Mainnet
+ | 11235 // Haqq Network
+ | 1127469 // Tiltyard Subnet
+ | 11297108099 // Palm Testnet
+ | 11297108109 // Palm
+ | 1149 // Symplexia Smart Chain
+ | 122 // Fuse Mainnet
+ | 1284 // Moonbeam
+ | 1285 // Moonriver
+ | 1287 // Moonbase Alpha
+ | 12898 // PlayFair Testnet Subnet
+ | 1291 // Swisstronik Testnet
+ | 1313161554 // Aurora Mainnet
+ | 1313161555 // Aurora Testnet
+ | 13337 // Beam Testnet
+ | 13381 // Phoenix Mainnet
+ | 1339 // Elysium Mainnet
+ | 137 // Polygon Mainnet
+ | 14 // Flare Mainnet
+ | 1433 // Rikeza Network Mainnet
+ | 1516 // Story Odyssey Testnet
+ | 16180 // PLYR PHI
+ | 16350 // Incentiv Devnet
+ | 167005 // Taiko Grimsvotn L2
+ | 167006 // Taiko Eldfell L3
+ | 17069 // Garnet Holesky
+ | 180 // AME Chain Mainnet
+ | 1890 // Lightlink Phoenix Mainnet
+ | 1891 // Lightlink Pegasus Testnet
+ | 19 // Songbird Canary-Network
+ | 19011 // HOME Verse Mainnet
+ | 192837465 // Gather Mainnet Network
+ | 2000 // Dogechain Mainnet
+ | 200810 // Bitlayer Testnet
+ | 200901 // Bitlayer Mainnet
+ | 2017 // Adiri
+ | 2020 // Ronin Mainnet
+ | 2021 // Edgeware EdgeEVM Mainnet
+ | 202401 // YMTECH-BESU Testnet
+ | 2037 // Kiwi Subnet
+ | 2038 // Shrapnel Testnet
+ | 2044 // Shrapnel Subnet
+ | 2047 // Stratos Testnet
+ | 2048 // Stratos
+ | 205205 // Auroria Testnet
+ | 212 // MAPO Makalu
+ | 216 // Happychain Testnet
+ | 222000222 // Kanazawa
+ | 2221 // Kava Testnet
+ | 2222 // Kava
+ | 223 // B2 Mainnet
+ | 22776 // MAP Protocol
+ | 23294 // Oasis Sapphire
+ | 23295 // Oasis Sapphire Testnet
+ | 2358 // Kroma Sepolia
+ | 2442 // Polygon zkEVM Cardona Testnet
+ | 246 // Energy Web Chain
+ | 25 // Cronos Mainnet
+ | 250 // Fantom Opera
+ | 252 // Fraxtal
+ | 2522 // Fraxtal Testnet
+ | 255 // Kroma
+ | 25925 // KUB Testnet
+ | 26100 // Ferrum Quantum Portal Network
+ | 28 // Boba Network Rinkeby Testnet
+ | 28528 // Optimism Bedrock (Goerli Alpha Testnet)
+ | 288 // Boba Network
+ | 295 // Hedera Mainnet
+ | 30 // Rootstock Mainnet
+ | 300 // zkSync Sepolia Testnet
+ | 311752642 // OneLedger Mainnet
+ | 314 // Filecoin - Mainnet
+ | 314159 // Filecoin - Calibration testnet
+ | 32769 // Zilliqa EVM
+ | 32770 // Zilliqa 2 EVM proto-mainnet
+ | 33101 // Zilliqa EVM Testnet
+ | 33103 // Zilliqa 2 EVM proto-testnet
+ | 33111 // Curtis
+ | 333000333 // Meld
+ | 335 // DFK Chain Test
+ | 336 // Shiden
+ | 34443 // Mode
+ | 35441 // Q Mainnet
+ | 35443 // Q Testnet
+ | 356256156 // Gather Testnet Network
+ | 369 // PulseChain
+ | 3737 // Crossbell
+ | 37714555429 // Xai Testnet v2
+ | 383414847825 // Zeniq
+ | 39797 // Energi Mainnet
+ | 40 // Telos EVM Mainnet
+ | 4000 // Ozone Chain Mainnet
+ | 41 // Telos EVM Testnet
+ | 4157 // CrossFi Testnet
+ | 420 // Optimism Goerli Testnet
+ | 4200 // Merlin Mainnet
+ | 420420 // Kekchain
+ | 420666 // Kekchain (kektest)
+ | 42161 // Arbitrum One
+ | 421611 // Arbitrum Rinkeby
+ | 421613 // Arbitrum Goerli
+ | 4216137055 // OneLedger Testnet Frankenstein
+ | 421614 // Arbitrum Sepolia
+ | 42170 // Arbitrum Nova
+ | 42220 // Celo Mainnet
+ | 42261 // Oasis Emerald Testnet
+ | 42262 // Oasis Emerald
+ | 42766 // ZKFair Mainnet
+ | 43 // Darwinia Pangolin Testnet
+ | 43113 // Avalanche Fuji Testnet
+ | 43114 // Avalanche C-Chain
+ | 432201 // Dexalot Subnet Testnet
+ | 432204 // Dexalot Subnet
+ | 4337 // Beam
+ | 44 // Crab Network
+ | 44787 // Celo Alfajores Testnet
+ | 46 // Darwinia Network
+ | 486217935 // Gather Devnet Network
+ | 48898 // Zircuit Garfield Testnet
+ | 48899 // Zircuit Testnet
+ | 48900 // Zircuit Mainnet
+ | 49797 // Energi Testnet
+ | 50 // XDC Network
+ | 5000 // Mantle
+ | 5003 // Mantle Sepolia Testnet
+ | 51 // XDC Apothem Network
+ | 5115 // Citrea Testnet
+ | 534 // Candle
+ | 534351 // Scroll Sepolia Testnet
+ | 534352 // Scroll
+ | 53935 // DFK Chain
+ | 54211 // Haqq Chain Testnet
+ | 56 // BNB Smart Chain Mainnet
+ | 560048 // Hoodi testnet
+ | 57 // Syscoin Mainnet
+ | 570 // Rollux Mainnet
+ | 5700 // Syscoin Tanenbaum Testnet
+ | 57000 // Rollux Testnet
+ | 5845 // Tangle
+ | 59141 // Linea Sepolia
+ | 59144 // Linea
+ | 592 // Astar
+ | 59902 // Metis Sepolia Testnet
+ | 61 // Ethereum Classic
+ | 6119 // UPTN
+ | 62320 // Celo Baklava Testnet
+ | 62621 // MultiVAC Mainnet
+ | 62831 // PLYR TAU Testnet
+ | 6321 // Aura Euphoria Testnet
+ | 6322 // Aura Mainnet
+ | 641230 // Bear Network Chain Mainnet
+ | 648 // Endurance Smart Chain Mainnet
+ | 660279 // Xai Mainnet
+ | 666666666 // Degen Chain
+ | 69 // Optimism Kovan
+ | 690 // Redstone
+ | 7000 // ZetaChain Mainnet
+ | 7001 // ZetaChain Testnet
+ | 7078815900 // Mekong
+ | 710420 // Tiltyard Mainnet Subnet
+ | 71401 // Godwoken Testnet v1
+ | 71402 // Godwoken Mainnet
+ | 7171 // Bitrock Mainnet
+ | 7200 // exSat Mainnet
+ | 723107 // TixChain Testnet
+ | 73799 // Energy Web Volta Testnet
+ | 764984 // Lamina1 Testnet
+ | 7668 // The Root Network - Mainnet
+ | 7672 // The Root Network - Porcini Testnet
+ | 767368 // Lamina1 Identity Testnet
+ | 77 // POA Network Sokol
+ | 7700 // Canto
+ | 7701 // Canto Tesnet
+ | 7771 // Bitrock Testnet
+ | 7777777 // Zora
+ | 78430 // Amplify Subnet
+ | 78431 // Bulletin Subnet
+ | 78432 // Conduit Subnet
+ | 8 // Ubiq
+ | 80001 // Mumbai
+ | 80002 // Amoy
+ | 82 // Meter Mainnet
+ | 8217 // Kaia Mainnet
+ | 83 // Meter Testnet
+ | 839999 // exSat Testnet
+ | 841 // Taraxa Mainnet
+ | 842 // Taraxa Testnet
+ | 8453 // Base
+ | 84531 // Base Goerli Testnet
+ | 84532 // Base Sepolia Testnet
+ | 888 // Wanchain
+ | 9000 // Evmos Testnet
+ | 9001 // Evmos
+ | 919 // Mode Testnet
+ | 957 // Lyra Chain
+ | 96 // KUB Mainnet
+ | 97 // BNB Smart Chain Testnet
+ | 970 // Oort Mainnet
+ | 99 // POA Network Core
+ | 9977 // Mind Smart Chain Testnet
+ | 999 // Wanchain Testnet
+ | 9996 // Mind Smart Chain Mainnet
+ | 999999999 // Zora Sepolia Testnet
diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts
new file mode 100644
index 0000000000..adb2889348
--- /dev/null
+++ b/packages/cli/src/types.ts
@@ -0,0 +1,10 @@
+export type Compute = { [key in keyof type]: type[key] } & unknown
+
+export type MaybeArray = T | T[]
+
+export type MaybePromise = T | Promise
+
+export type RequiredBy = Required<
+ Pick
+> &
+ Omit
diff --git a/packages/cli/src/utils/findConfig.test.ts b/packages/cli/src/utils/findConfig.test.ts
new file mode 100644
index 0000000000..52c2066773
--- /dev/null
+++ b/packages/cli/src/utils/findConfig.test.ts
@@ -0,0 +1,42 @@
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { createFixture } from '../../test/utils.js'
+import { findConfig } from './findConfig.js'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('finds config file', async () => {
+ const { dir, paths } = await createFixture({
+ files: { 'wagmi.config.ts': '' },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(findConfig()).resolves.toBe(paths['wagmi.config.ts'])
+})
+
+test('finds config file at location', async () => {
+ const { dir, paths } = await createFixture({
+ files: { 'wagmi.config.ts': '' },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(findConfig({ config: paths['wagmi.config.ts'] })).resolves.toBe(
+ paths['wagmi.config.ts'],
+ )
+})
+
+test('finds config file at root', async () => {
+ const { dir, paths } = await createFixture({
+ files: { 'wagmi.config.ts': '' },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(findConfig({ root: dir })).resolves.toBe(
+ paths['wagmi.config.ts'],
+ )
+})
diff --git a/packages/cli/src/utils/findConfig.ts b/packages/cli/src/utils/findConfig.ts
new file mode 100644
index 0000000000..2e94e2d5a4
--- /dev/null
+++ b/packages/cli/src/utils/findConfig.ts
@@ -0,0 +1,39 @@
+import { existsSync } from 'node:fs'
+import escalade from 'escalade'
+import { resolve } from 'pathe'
+
+// Do not reorder
+// In order of preference files are checked
+const configFiles = [
+ 'wagmi.config.ts',
+ 'wagmi.config.js',
+ 'wagmi.config.mjs',
+ 'wagmi.config.mts',
+]
+
+type FindConfigParameters = {
+ /** Config file name */
+ config?: string | undefined
+ /** Config file directory */
+ root?: string | undefined
+}
+
+/**
+ * Resolves path to wagmi CLI config file.
+ */
+export async function findConfig(parameters: FindConfigParameters = {}) {
+ const { config, root } = parameters
+ const rootDir = resolve(root || process.cwd())
+ if (config) {
+ const path = resolve(rootDir, config)
+ if (existsSync(path)) return path
+ return
+ }
+ const configPath = await escalade(rootDir, (_dir, names) => {
+ for (const name of names) {
+ if (configFiles.includes(name)) return name
+ }
+ return undefined
+ })
+ return configPath
+}
diff --git a/packages/cli/src/utils/format.test.ts b/packages/cli/src/utils/format.test.ts
new file mode 100644
index 0000000000..6d04eb5465
--- /dev/null
+++ b/packages/cli/src/utils/format.test.ts
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+
+import { format } from './format.js'
+
+test('formats code', async () => {
+ await expect(
+ format(`const foo = "bar"`),
+ ).resolves.toMatchInlineSnapshot(`
+ "const foo = 'bar'
+ "
+ `)
+})
diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts
new file mode 100644
index 0000000000..e4fd289c69
--- /dev/null
+++ b/packages/cli/src/utils/format.ts
@@ -0,0 +1,16 @@
+import prettier from 'prettier'
+
+export async function format(content: string) {
+ const config = await prettier.resolveConfig(process.cwd())
+ return prettier.format(content, {
+ arrowParens: 'always',
+ endOfLine: 'lf',
+ parser: 'typescript',
+ printWidth: 80,
+ semi: false,
+ singleQuote: true,
+ tabWidth: 2,
+ trailingComma: 'all',
+ ...config,
+ })
+}
diff --git a/packages/cli/src/utils/getAddressDocString.test.ts b/packages/cli/src/utils/getAddressDocString.test.ts
new file mode 100644
index 0000000000..798e6e14e5
--- /dev/null
+++ b/packages/cli/src/utils/getAddressDocString.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from 'vitest'
+
+import { getAddressDocString } from './getAddressDocString.js'
+
+test('address', async () => {
+ expect(
+ getAddressDocString({
+ address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ }),
+ ).toMatchInlineSnapshot('""')
+})
+
+test('multichain address with known chain ids', async () => {
+ expect(
+ getAddressDocString({
+ address: {
+ 1: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ 5: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ 10: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ },
+ }),
+ ).toMatchInlineSnapshot(`
+ "* - [__View Contract on Ethereum Etherscan__](https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e)
+ * - [__View Contract on Goerli Etherscan__](https://goerli.etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e)
+ * - [__View Contract on Op Mainnet Optimism Explorer__](https://optimistic.etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e)"
+ `)
+})
+
+test('multichain address with unknown chain id', async () => {
+ expect(
+ getAddressDocString({
+ address: {
+ 1: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ 2: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ },
+ }),
+ ).toMatchInlineSnapshot(
+ '"* [__View Contract on Ethereum Etherscan__](https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e)"',
+ )
+})
diff --git a/packages/cli/src/utils/getAddressDocString.ts b/packages/cli/src/utils/getAddressDocString.ts
new file mode 100644
index 0000000000..d0e137928c
--- /dev/null
+++ b/packages/cli/src/utils/getAddressDocString.ts
@@ -0,0 +1,53 @@
+import { capitalCase } from 'change-case'
+import dedent from 'dedent'
+import * as allChains from 'viem/chains'
+
+import type { Contract } from '../config.js'
+
+const chainMap: Record = {}
+for (const chain of Object.values(allChains)) {
+ if (typeof chain !== 'object') continue
+ if (!('id' in chain)) continue
+ chainMap[chain.id] = chain
+}
+
+export function getAddressDocString(parameters: {
+ address: Contract['address']
+}) {
+ const { address } = parameters
+ if (!address || typeof address === 'string') return ''
+
+ if (Object.keys(address).length === 1)
+ return `* ${getLink({
+ address: address[Number.parseInt(Object.keys(address)[0]!)]!,
+ chainId: Number.parseInt(Object.keys(address)[0]!),
+ })}`
+
+ const addresses = Object.entries(address).filter(
+ (x) => chainMap[Number.parseInt(x[0])],
+ )
+ if (addresses.length === 0) return ''
+ if (addresses.length === 1 && addresses[0])
+ return `* ${getLink({
+ address: addresses[0][1],
+ chainId: Number.parseInt(addresses[0][0])!,
+ })}`
+
+ return dedent`
+ ${addresses.reduce((prev, curr) => {
+ const chainId = Number.parseInt(curr[0])
+ const address = curr[1]
+ return `${prev}\n* - ${getLink({ address, chainId })}`
+ }, '')}
+ `
+}
+
+function getLink({ address, chainId }: { address: string; chainId: number }) {
+ const chain = chainMap[chainId]
+ if (!chain) return ''
+ const blockExplorer = chain.blockExplorers?.default
+ if (!blockExplorer) return ''
+ return `[__View Contract on ${capitalCase(chain.name)} ${capitalCase(
+ blockExplorer.name,
+ )}__](${blockExplorer.url}/address/${address})`
+}
diff --git a/packages/cli/src/utils/getIsUsingTypeScript.test.ts b/packages/cli/src/utils/getIsUsingTypeScript.test.ts
new file mode 100644
index 0000000000..3ba7c86a65
--- /dev/null
+++ b/packages/cli/src/utils/getIsUsingTypeScript.test.ts
@@ -0,0 +1,43 @@
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { createFixture } from '../../test/utils.js'
+import { getIsUsingTypeScript } from './getIsUsingTypeScript.js'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('true if has tsconfig', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'tsconfig.json': '',
+ },
+ })
+
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(getIsUsingTypeScript()).resolves.toBe(true)
+})
+
+test('true if has wagmi.config', async () => {
+ const { dir } = await createFixture({
+ files: {
+ 'wagmi.config.ts': '',
+ },
+ })
+
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(getIsUsingTypeScript()).resolves.toBe(true)
+})
+
+test('false', async () => {
+ const { dir } = await createFixture()
+
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ await expect(getIsUsingTypeScript()).resolves.toBe(false)
+})
diff --git a/packages/cli/src/utils/getIsUsingTypeScript.ts b/packages/cli/src/utils/getIsUsingTypeScript.ts
new file mode 100644
index 0000000000..bd2383b0eb
--- /dev/null
+++ b/packages/cli/src/utils/getIsUsingTypeScript.ts
@@ -0,0 +1,33 @@
+import escalade from 'escalade'
+
+export async function getIsUsingTypeScript() {
+ try {
+ const cwd = process.cwd()
+ const tsconfig = await escalade(cwd, (_dir, names) => {
+ const files = [
+ 'tsconfig.json',
+ 'tsconfig.base.json',
+ 'tsconfig.lib.json',
+ 'tsconfig.node.json',
+ ]
+ for (const name of names) {
+ if (files.includes(name)) return name
+ }
+ return undefined
+ })
+ if (tsconfig) return true
+
+ const wagmiConfig = await escalade(cwd, (_dir, names) => {
+ const files = ['wagmi.config.ts', 'wagmi.config.mts']
+ for (const name of names) {
+ if (files.includes(name)) return name
+ }
+ return undefined
+ })
+ if (wagmiConfig) return true
+
+ return false
+ } catch {
+ return false
+ }
+}
diff --git a/packages/cli/src/utils/loadEnv.test.ts b/packages/cli/src/utils/loadEnv.test.ts
new file mode 100644
index 0000000000..d577778eb0
--- /dev/null
+++ b/packages/cli/src/utils/loadEnv.test.ts
@@ -0,0 +1,77 @@
+import { afterEach, expect, test, vi } from 'vitest'
+
+import { createFixture } from '../../test/utils.js'
+import { loadEnv } from './loadEnv.js'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+test('loads env', async () => {
+ const { dir } = await createFixture({
+ files: {
+ '.env': `
+ FOO=bar
+ SOME_ENV_VAR=1
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ expect(loadEnv()).toMatchInlineSnapshot(`
+ {
+ "FOO": "bar",
+ "SOME_ENV_VAR": "1",
+ }
+ `)
+})
+
+test('loads env from envDir', async () => {
+ const { dir } = await createFixture({
+ files: {
+ '.env': `
+ FOO=bar
+ SOME_ENV_VAR=1
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ expect(loadEnv({ envDir: dir })).toMatchInlineSnapshot(`
+ {
+ "FOO": "bar",
+ "SOME_ENV_VAR": "1",
+ }
+ `)
+})
+
+test('loads env with mode', async () => {
+ const mode = 'dev'
+ const { dir } = await createFixture({
+ files: {
+ [`.env.${mode}`]: `
+ FOO=bar
+ SOME_ENV_VAR=1
+ `,
+ },
+ })
+ const spy = vi.spyOn(process, 'cwd')
+ spy.mockImplementation(() => dir)
+
+ expect(loadEnv({ mode })).toMatchInlineSnapshot(`
+ {
+ "FOO": "bar",
+ "SOME_ENV_VAR": "1",
+ }
+ `)
+})
+
+test('throws error when mode is "local"', async () => {
+ expect(() => {
+ loadEnv({ mode: 'local' })
+ }).toThrowErrorMatchingInlineSnapshot(
+ `[Error: "local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.]`,
+ )
+})
diff --git a/packages/cli/src/utils/loadEnv.ts b/packages/cli/src/utils/loadEnv.ts
new file mode 100644
index 0000000000..d7ffa99919
--- /dev/null
+++ b/packages/cli/src/utils/loadEnv.ts
@@ -0,0 +1,90 @@
+import { parse } from 'dotenv'
+import { expand } from 'dotenv-expand'
+
+import { existsSync, readFileSync, statSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+
+// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/env.ts#L7
+export function loadEnv(
+ config: {
+ mode?: string
+ envDir?: string
+ } = {},
+): Record {
+ const mode = config.mode
+ if (mode === 'local') {
+ throw new Error(
+ `"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`,
+ )
+ }
+
+ const envFiles = [
+ /** default file */ '.env',
+ /** local file */ '.env.local',
+ ...(mode
+ ? [
+ /** mode file */ `.env.${mode}`,
+ /** mode local file */ `.env.${mode}.local`,
+ ]
+ : []),
+ ]
+
+ const envDir = config.envDir ?? process.cwd()
+ const parsed = Object.fromEntries(
+ envFiles.flatMap((file) => {
+ const path = lookupFile(envDir, [file], {
+ pathOnly: true,
+ rootDir: envDir,
+ })
+ if (!path) return []
+ return Object.entries(parse(readFileSync(path)))
+ }),
+ )
+
+ try {
+ // let environment variables use each other
+ expand({ parsed })
+ } catch (error) {
+ // custom error handling until https://github.com/motdotla/dotenv-expand/issues/65 is fixed upstream
+ // check for message "TypeError: Cannot read properties of undefined (reading 'split')"
+ if ((error as Error).message.includes('split')) {
+ throw new Error(
+ 'dotenv-expand failed to expand env vars. Maybe you need to escape `$`?',
+ )
+ }
+ throw error
+ }
+
+ return parsed
+}
+
+function lookupFile(
+ dir: string,
+ formats: string[],
+ options?: {
+ pathOnly?: boolean
+ rootDir?: string
+ predicate?: (file: string) => boolean
+ },
+): string | undefined {
+ for (const format of formats) {
+ const fullPath = join(dir, format)
+ if (existsSync(fullPath) && statSync(fullPath).isFile()) {
+ const result = options?.pathOnly
+ ? fullPath
+ : readFileSync(fullPath, 'utf-8')
+ if (!options?.predicate || options.predicate(result)) {
+ return result
+ }
+ }
+ }
+
+ const parentDir = dirname(dir)
+ if (
+ parentDir !== dir &&
+ (!options?.rootDir || parentDir.startsWith(options?.rootDir))
+ )
+ return lookupFile(parentDir, formats, options)
+
+ return undefined
+}
diff --git a/packages/cli/src/utils/packages.test.ts b/packages/cli/src/utils/packages.test.ts
new file mode 100644
index 0000000000..96ea2e26ef
--- /dev/null
+++ b/packages/cli/src/utils/packages.test.ts
@@ -0,0 +1,19 @@
+import { expect, test } from 'vitest'
+
+import { getIsPackageInstalled, getPackageManager } from './packages.js'
+
+test('getIsPackageInstalled: true', async () => {
+ await expect(getIsPackageInstalled({ packageName: 'vitest' })).resolves.toBe(
+ true,
+ )
+})
+
+test('getIsPackageInstalled: false', async () => {
+ await expect(
+ getIsPackageInstalled({ packageName: 'vitest-unknown' }),
+ ).resolves.toBe(false)
+})
+
+test('getPackageManager', async () => {
+ await expect(getPackageManager()).resolves.toMatchInlineSnapshot('"pnpm"')
+})
diff --git a/packages/cli/src/utils/packages.ts b/packages/cli/src/utils/packages.ts
new file mode 100644
index 0000000000..1abf394a12
--- /dev/null
+++ b/packages/cli/src/utils/packages.ts
@@ -0,0 +1,124 @@
+import { execSync, spawnSync } from 'node:child_process'
+import { promises as fs } from 'node:fs'
+import { resolve } from 'node:path'
+
+export async function getIsPackageInstalled(parameters: {
+ packageName: string
+ cwd?: string
+}) {
+ const { packageName, cwd = process.cwd() } = parameters
+ try {
+ const packageManager = await getPackageManager()
+ const command = (() => {
+ switch (packageManager) {
+ case 'yarn':
+ return ['why', packageName]
+ case 'bun':
+ return ['pm', 'ls', '--all']
+ default:
+ return ['ls', packageName]
+ }
+ })()
+
+ const result = execSync(`${packageManager} ${command.join(' ')}`, {
+ cwd,
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+
+ // For Bun, we need to check if the package name is in the output
+ if (packageManager === 'bun') return result.includes(packageName)
+
+ return result !== ''
+ } catch (_error) {
+ return false
+ }
+}
+
+export async function getPackageManager(executable?: boolean | undefined) {
+ const userAgent = process.env.npm_config_user_agent
+ if (userAgent) {
+ if (userAgent.includes('pnpm')) return 'pnpm'
+ // The yarn@^3 user agent includes npm, so yarn must be checked first.
+ if (userAgent.includes('yarn')) return 'yarn'
+ if (userAgent.includes('npm')) return executable ? 'npx' : 'npm'
+ if (userAgent.includes('bun')) return executable ? 'bunx' : 'bun'
+ }
+
+ const packageManager = await detect()
+ if (packageManager === 'npm' && executable) return 'npx'
+ return packageManager
+}
+
+type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun'
+
+async function detect(
+ parameters: { cwd?: string; includeGlobalBun?: boolean } = {},
+) {
+ const { cwd, includeGlobalBun } = parameters
+ const type = await getTypeofLockFile(cwd)
+ if (type) {
+ return type
+ }
+ const [hasYarn, hasPnpm, hasBun] = await Promise.all([
+ hasGlobalInstallation('yarn'),
+ hasGlobalInstallation('pnpm'),
+ includeGlobalBun && hasGlobalInstallation('bun'),
+ ])
+ if (hasYarn) return 'yarn'
+ if (hasPnpm) return 'pnpm'
+ if (hasBun) return 'bun'
+ return 'npm'
+}
+
+const cache = new Map()
+
+function hasGlobalInstallation(pm: PackageManager): boolean {
+ const key = `has_global_${pm}`
+ if (cache.has(key)) return cache.get(key)
+
+ try {
+ const result = execSync(`${pm} --version`, {
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ })
+ const isGlobal = /^\d+.\d+.\d+$/.test(result)
+ cache.set(key, isGlobal)
+ return isGlobal
+ } catch {
+ return false
+ }
+}
+
+function getTypeofLockFile(cwd = '.'): Promise {
+ const key = `lockfile_${cwd}`
+ if (cache.has(key)) {
+ return Promise.resolve(cache.get(key))
+ }
+
+ return Promise.all([
+ pathExists(resolve(cwd, 'yarn.lock')),
+ pathExists(resolve(cwd, 'package-lock.json')),
+ pathExists(resolve(cwd, 'pnpm-lock.yaml')),
+ pathExists(resolve(cwd, 'bun.lockb')),
+ ]).then(([isYarn, isNpm, isPnpm, isBun]) => {
+ let value: PackageManager | null = null
+
+ if (isYarn) value = 'yarn'
+ else if (isPnpm) value = 'pnpm'
+ else if (isBun) value = 'bun'
+ else if (isNpm) value = 'npm'
+
+ cache.set(key, value)
+ return value
+ })
+}
+
+async function pathExists(p: string) {
+ try {
+ await fs.access(p)
+ return true
+ } catch {
+ return false
+ }
+}
diff --git a/packages/cli/src/utils/resolveConfig.test.ts b/packages/cli/src/utils/resolveConfig.test.ts
new file mode 100644
index 0000000000..6669edc9b6
--- /dev/null
+++ b/packages/cli/src/utils/resolveConfig.test.ts
@@ -0,0 +1,83 @@
+import { expect, test } from 'vitest'
+
+import { createFixture } from '../../test/utils.js'
+import { defaultConfig } from '../config.js'
+import { findConfig } from './findConfig.js'
+import { resolveConfig } from './resolveConfig.js'
+
+test.skip('resolves config', async () => {
+ const { paths } = await createFixture({
+ files: {
+ 'wagmi.config.ts': `
+ import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig(${JSON.stringify(defaultConfig)})
+ `,
+ },
+ })
+
+ const configPath = await findConfig({
+ config: paths['wagmi.config.ts'],
+ })
+ await expect(
+ resolveConfig({ configPath: configPath! }),
+ ).resolves.toMatchInlineSnapshot(`
+ {
+ "contracts": [],
+ "out": "src/generated.ts",
+ "plugins": [],
+ }
+ `)
+})
+
+test.skip('resolves function config', async () => {
+ const { paths } = await createFixture({
+ files: {
+ 'wagmi.config.ts': `
+ import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig(() => (${JSON.stringify(defaultConfig)}))
+ `,
+ },
+ })
+
+ const configPath = await findConfig({
+ config: paths['wagmi.config.ts'],
+ })
+ await expect(
+ resolveConfig({ configPath: configPath! }),
+ ).resolves.toMatchInlineSnapshot(`
+ {
+ "contracts": [],
+ "out": "src/generated.ts",
+ "plugins": [],
+ }
+ `)
+})
+
+test.skip('resolves array config', async () => {
+ const { paths } = await createFixture({
+ files: {
+ 'wagmi.config.ts': `
+ import { defineConfig } from '@wagmi/cli'
+
+ export default defineConfig([${JSON.stringify(defaultConfig)}])
+ `,
+ },
+ })
+
+ const configPath = await findConfig({
+ config: paths['wagmi.config.ts'],
+ })
+ await expect(
+ resolveConfig({ configPath: configPath! }),
+ ).resolves.toMatchInlineSnapshot(`
+ [
+ {
+ "contracts": [],
+ "out": "src/generated.ts",
+ "plugins": [],
+ },
+ ]
+ `)
+})
diff --git a/packages/cli/src/utils/resolveConfig.ts b/packages/cli/src/utils/resolveConfig.ts
new file mode 100644
index 0000000000..048b1c337f
--- /dev/null
+++ b/packages/cli/src/utils/resolveConfig.ts
@@ -0,0 +1,21 @@
+import { bundleRequire } from 'bundle-require'
+
+import type { Config } from '../config.js'
+import type { MaybeArray } from '../types.js'
+
+type ResolveConfigParameters = {
+ /** Path to config file */
+ configPath: string
+}
+
+/** Bundles and returns wagmi config object from path. */
+export async function resolveConfig(
+ parameters: ResolveConfigParameters,
+): Promise> {
+ const { configPath } = parameters
+ const res = await bundleRequire({ filepath: configPath })
+ let config = res.mod.default
+ if (config.default) config = config.default
+ if (typeof config !== 'function') return config
+ return await config()
+}
diff --git a/packages/cli/src/version.ts b/packages/cli/src/version.ts
new file mode 100644
index 0000000000..166b988ccb
--- /dev/null
+++ b/packages/cli/src/version.ts
@@ -0,0 +1 @@
+export const version = '2.3.1'
diff --git a/packages/cli/test/constants.ts b/packages/cli/test/constants.ts
new file mode 100644
index 0000000000..9b84c5bb1b
--- /dev/null
+++ b/packages/cli/test/constants.ts
@@ -0,0 +1,32 @@
+import { parseAbi } from 'viem'
+
+export const wagmiAbi = parseAbi([
+ 'constructor()',
+ 'event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)',
+ 'event ApprovalForAll(address indexed owner, address indexed operator, bool approved)',
+ 'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
+ 'function approve(address to, uint256 tokenId)',
+ 'function balanceOf(address owner) view returns (uint256)',
+ 'function getApproved(uint256 tokenId) view returns (address)',
+ 'function isApprovedForAll(address owner, address operator) view returns (bool)',
+ 'function mint()',
+ 'function name() view returns (string)',
+ 'function ownerOf(uint256 tokenId) view returns (address)',
+ 'function safeTransferFrom(address from, address to, uint256 tokenId)',
+ 'function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data)',
+ 'function setApprovalForAll(address operator, bool approved)',
+ 'function supportsInterface(bytes4 interfaceId) view returns (bool)',
+ 'function symbol() view returns (string)',
+ 'function tokenURI(uint256 tokenId) pure returns (string)',
+ 'function totalSupply() view returns (uint256)',
+ 'function transferFrom(address from, address to, uint256 tokenId)',
+])
+
+export const depositAbi = parseAbi([
+ 'constructor()',
+ 'event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index)',
+ 'function deposit(bytes pubkey, bytes withdrawal_credentials, bytes signature, bytes32 deposit_data_root) payable',
+ 'function get_deposit_count() view returns (bytes)',
+ 'function get_deposit_root() view returns (bytes32)',
+ 'function supportsInterface(bytes4 interfaceId) pure returns (bool)',
+])
diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts
new file mode 100644
index 0000000000..d2eed4a5e5
--- /dev/null
+++ b/packages/cli/test/setup.ts
@@ -0,0 +1,57 @@
+import { mkdir } from 'node:fs/promises'
+import { homedir } from 'node:os'
+import type { createSpinner as nanospinner_createSpinner } from 'nanospinner'
+import { join } from 'pathe'
+import { vi } from 'vitest'
+
+const cacheDir = join(homedir(), '.wagmi-cli/plugins/fetch/cache')
+await mkdir(cacheDir, { recursive: true })
+
+vi.mock('nanospinner', async (importOriginal) => {
+ const mod = await importOriginal<{
+ createSpinner: typeof nanospinner_createSpinner
+ }>()
+
+ function createSpinner(
+ initialText: string,
+ opts: Parameters[1],
+ ) {
+ let currentText = ''
+ const spinner = mod.createSpinner(initialText, opts)
+ return {
+ ...spinner,
+ start(text = initialText) {
+ // biome-ignore lint/suspicious/noConsoleLog: console.log is used for logging
+ console.log(`- ${text}`)
+ spinner.start(text)
+ currentText = text
+ },
+ success(text = currentText) {
+ // biome-ignore lint/suspicious/noConsoleLog: console.log is used for logging
+ console.log(`√ ${text}`)
+ spinner.success(text)
+ },
+ error(text = currentText) {
+ console.error(`× ${text}`)
+ spinner.error(text)
+ },
+ }
+ }
+ return { createSpinner }
+})
+
+vi.mock('picocolors', async () => {
+ function pass(input: string | number | null | undefined) {
+ return input
+ }
+ return {
+ default: {
+ blue: pass,
+ gray: pass,
+ green: pass,
+ red: pass,
+ white: pass,
+ yellow: pass,
+ },
+ }
+})
diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts
new file mode 100644
index 0000000000..4ea6c6051e
--- /dev/null
+++ b/packages/cli/test/utils.ts
@@ -0,0 +1,292 @@
+import { spawnSync } from 'node:child_process'
+import { cp, mkdir, symlink, writeFile } from 'node:fs/promises'
+import fixtures from 'fixturez'
+import { http, HttpResponse } from 'msw'
+import * as path from 'pathe'
+import { vi } from 'vitest'
+
+const f = fixtures(__dirname)
+
+type Json =
+ | string
+ | number
+ | boolean
+ | null
+ | { [property: string]: Json }
+ | Json[]
+
+export async function createFixture<
+ TFiles extends { [filename: string]: string | Json } & {
+ tsconfig?: true
+ },
+>(
+ config: {
+ copyNodeModules?: boolean
+ dir?: string
+ files?: TFiles
+ } = {},
+) {
+ const dir = config.dir ?? f.temp()
+ await mkdir(dir, { recursive: true })
+
+ // Create test files
+ const paths: { [_ in keyof TFiles]: string } = {} as any
+ await Promise.all(
+ (Object.keys(config.files ?? {}) as (keyof TFiles)[]).map(
+ async (filename_) => {
+ let file: Json | true | undefined
+ let filename = filename_
+ if (filename === 'tsconfig') {
+ filename = 'tsconfig.json'
+ file = getTsConfig(dir)
+ } else file = config.files![filename]
+
+ const filePath = path.join(dir, filename.toString())
+ await mkdir(path.dirname(filePath), { recursive: true })
+
+ await writeFile(
+ filePath,
+ typeof file === 'string' ? file : JSON.stringify(file, null, 2),
+ )
+ paths[filename === 'tsconfig.json' ? 'tsconfig' : filename] = filePath
+ },
+ ),
+ )
+
+ if (config.copyNodeModules) {
+ await symlink(
+ path.join(__dirname, '../node_modules'),
+ path.join(dir, 'node_modules'),
+ 'dir',
+ )
+ await cp(
+ path.join(__dirname, '../package.json'),
+ path.join(dir, 'package.json'),
+ )
+ }
+
+ return {
+ dir,
+ paths: paths,
+ }
+}
+
+type TsConfig = {
+ compilerOptions: { [property: string]: any }
+ exclude: string[]
+ include: string[]
+}
+function getTsConfig(baseUrl: string) {
+ return {
+ compilerOptions: {
+ allowJs: true,
+ baseUrl: '.',
+ esModuleInterop: true,
+ forceConsistentCasingInFileNames: true,
+ incremental: true,
+ isolatedModules: true,
+ jsx: 'preserve',
+ lib: ['dom', 'dom.iterable', 'esnext'],
+ module: 'esnext',
+ moduleResolution: 'node',
+ noEmit: true,
+ paths: {
+ '@wagmi/cli': [path.relative(baseUrl, 'packages/cli/src')],
+ '@wagmi/cli/*': [path.relative(baseUrl, 'packages/cli/src/*')],
+ '@wagmi/connectors': [
+ path.relative(baseUrl, 'packages/connectors/src'),
+ ],
+ '@wagmi/connectors/*': [
+ path.relative(baseUrl, 'packages/connectors/src/*'),
+ ],
+ '@wagmi/core': [path.relative(baseUrl, 'packages/core/src')],
+ '@wagmi/core/*': [path.relative(baseUrl, 'packages/core/src/*')],
+ wagmi: [path.relative(baseUrl, 'packages/react/src')],
+ 'wagmi/*': [path.relative(baseUrl, 'packages/react/src/*')],
+ },
+ resolveJsonModule: true,
+ skipLibCheck: true,
+ strict: true,
+ target: 'es6',
+ },
+ include: [`${baseUrl}/**/*.ts`, `${baseUrl}/**/*.tsx`],
+ exclude: ['node_modules'],
+ } as TsConfig
+}
+
+export function watchConsole() {
+ type Console = 'info' | 'log' | 'warn' | 'error'
+ const output: { [_ in Console | 'all']: string[] } = {
+ info: [],
+ log: [],
+ warn: [],
+ error: [],
+ all: [],
+ }
+ function handleOutput(method: Console) {
+ return (message: string) => {
+ output[method].push(message)
+ output.all.push(message)
+ }
+ }
+ return {
+ debug: console.debug,
+ info: vi.spyOn(console, 'info').mockImplementation(handleOutput('info')),
+ log: vi.spyOn(console, 'log').mockImplementation(handleOutput('log')),
+ warn: vi.spyOn(console, 'warn').mockImplementation(handleOutput('warn')),
+ error: vi.spyOn(console, 'error').mockImplementation(handleOutput('error')),
+ output,
+ get formatted() {
+ return output.all.join('\n')
+ },
+ }
+}
+
+export async function typecheck(project: string) {
+ try {
+ const result = spawnSync(
+ 'tsc',
+ ['--noEmit', '--target', 'es2021', '--pretty', 'false', '-p', project],
+ {
+ encoding: 'utf-8',
+ stdio: 'pipe',
+ },
+ )
+ if (result.error) throw result.error
+ if (result.status !== 0)
+ throw new Error(`Failed with code ${result.status}`)
+ if (result.signal) throw new Error('Process terminated by signal')
+ return result.stdout
+ } catch (error) {
+ throw new Error(
+ (error as Error).message.replaceAll(
+ path.dirname(project),
+ '/path/to/project',
+ ),
+ )
+ }
+}
+
+export const baseUrl = 'https://api.etherscan.io/v2/api'
+export const apiKey = 'abc'
+export const invalidApiKey = 'xyz'
+export const address = '0xaf0326d92b97df1221759476b072abfd8084f9be'
+export const proxyAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
+export const implementationAddress =
+ '0x43506849d7c04f9138d1a2050bbf3a0c054402dd'
+export const unverifiedContractAddress =
+ '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
+export const timeoutAddress = '0xecb504d39723b0be0e3a9aa33d646642d1051ee1'
+
+export const handlers = [
+ http.get(baseUrl, async ({ request }) => {
+ const url = new URL(request.url)
+ const search = url.search.replace(/^\?chainId=\d&/, '?')
+
+ if (
+ search ===
+ `?module=contract&action=getabi&address=${unverifiedContractAddress}&apikey=${apiKey}`
+ )
+ return HttpResponse.json({
+ status: '0',
+ message: 'NOTOK',
+ result: 'Contract source code not verified',
+ })
+
+ if (
+ search ===
+ `?module=contract&action=getabi&address=${timeoutAddress}&apikey=${invalidApiKey}`
+ )
+ return HttpResponse.json({
+ status: '0',
+ message: 'NOTOK',
+ result: 'Invalid API Key',
+ })
+
+ if (
+ search ===
+ `?module=contract&action=getabi&address=${address}&apikey=${apiKey}`
+ )
+ return HttpResponse.json({
+ status: '1',
+ message: 'OK',
+ result:
+ '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]',
+ })
+
+ if (
+ search ===
+ `?module=contract&action=getabi&address=${timeoutAddress}&apikey=${apiKey}`
+ ) {
+ await new Promise((resolve) => setTimeout(resolve, 10_000))
+ return HttpResponse.json({})
+ }
+
+ if (
+ search ===
+ `?module=contract&action=getabi&address=${implementationAddress}&apikey=${apiKey}`
+ )
+ return HttpResponse.json({
+ status: '1',
+ message: 'OK',
+ result:
+ '[{"constant":false,"inputs":[{"name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newImplementation","type":"address"},{"name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"implementation","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newAdmin","type":"address"}],"name":"changeAdmin","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"admin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_implementation","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"previousAdmin","type":"address"},{"indexed":false,"name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"implementation","type":"address"}],"name":"Upgraded","type":"event"}]',
+ })
+
+ if (
+ search ===
+ `?module=contract&action=getsourcecode&address=${proxyAddress}&apikey=${apiKey}`
+ )
+ return HttpResponse.json({
+ status: '1',
+ message: 'OK',
+ result: [
+ {
+ SourceCode: '...',
+ ABI: '[{"constant":false,"inputs":[{"name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newImplementation","type":"address"},{"name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"implementation","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newAdmin","type":"address"}],"name":"changeAdmin","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"admin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_implementation","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"previousAdmin","type":"address"},{"indexed":false,"name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"implementation","type":"address"}],"name":"Upgraded","type":"event"}]',
+ ContractName: 'FiatTokenProxy',
+ CompilerVersion: 'v0.4.24+commit.e67f0147',
+ OptimizationUsed: '0',
+ Runs: '200',
+ ConstructorArguments:
+ '0000000000000000000000000882477e7895bdc5cea7cb1552ed914ab157fe56',
+ EVMVersion: 'Default',
+ Library: '',
+ LicenseType: '',
+ Proxy: '1',
+ Implementation: '0x43506849d7c04f9138d1a2050bbf3a0c054402dd',
+ SwarmSource:
+ 'bzzr://a4a547cfc7202c5acaaae74d428e988bc62ad5024eb0165532d3a8f91db4ed24',
+ },
+ ],
+ })
+
+ if (
+ search ===
+ `?module=contract&action=getsourcecode&address=${address}&apikey=${apiKey}`
+ )
+ return HttpResponse.json({
+ status: '1',
+ message: 'OK',
+ result: [
+ {
+ SourceCode: '...',
+ ABI: '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]',
+ ContractName: 'WagmiMintExample',
+ CompilerVersion: 'v0.8.11+commit.d7f03943',
+ OptimizationUsed: '1',
+ Runs: '10000',
+ ConstructorArguments: '',
+ EVMVersion: 'Default',
+ Library: '',
+ LicenseType: '',
+ Proxy: '0',
+ Implementation: '',
+ SwarmSource: '',
+ },
+ ],
+ })
+
+ throw new Error(`Unhandled request: ${search}`)
+ }),
+]
diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json
new file mode 100644
index 0000000000..3a046d812b
--- /dev/null
+++ b/packages/cli/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.test.ts"],
+ "compilerOptions": {
+ "sourceMap": true,
+ "types": ["@types/node"]
+ }
+}
diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json
new file mode 100644
index 0000000000..4054dee118
--- /dev/null
+++ b/packages/cli/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.build.json",
+ "include": ["src/**/*.ts", "test/**/*.ts", "types/**/*.d.ts"],
+ "exclude": []
+}
diff --git a/packages/cli/types/fixturez.d.ts b/packages/cli/types/fixturez.d.ts
new file mode 100644
index 0000000000..f96282756f
--- /dev/null
+++ b/packages/cli/types/fixturez.d.ts
@@ -0,0 +1,18 @@
+declare module 'fixturez' {
+ interface FixturezOpts {
+ glob?: string
+ cleanup?: boolean
+ root?: string
+ }
+
+ interface Fixturez {
+ find(basename: string): string
+ copy(basename: string): string
+ temp(): string
+ cleanup(): void
+ }
+
+ function fixturez(dirname: string, opts?: FixturezOpts): Fixturez
+
+ export = fixturez
+}
diff --git a/packages/connectors/CHANGELOG.md b/packages/connectors/CHANGELOG.md
new file mode 100644
index 0000000000..c67e25dcb0
--- /dev/null
+++ b/packages/connectors/CHANGELOG.md
@@ -0,0 +1,1640 @@
+# @wagmi/connectors
+
+## 5.8.3
+
+### Patch Changes
+
+- [#4660](https://github.com/wevm/wagmi/pull/4660) [`42b1fed58e9ac09da0f8ebf3e9271f98a707aaac`](https://github.com/wevm/wagmi/commit/42b1fed58e9ac09da0f8ebf3e9271f98a707aaac) Thanks [@ganchoradkov](https://github.com/ganchoradkov)! - Updated `@walletconnect/ethereum-provider` version to `2.20.2`
+
+## 5.8.2
+
+### Patch Changes
+
+- Updated dependencies [[`29297a48af72b537173d948ccd2fe37d39914c66`](https://github.com/wevm/wagmi/commit/29297a48af72b537173d948ccd2fe37d39914c66), [`07370106d5fb6b8fe300992d93abf25b3d0eaf57`](https://github.com/wevm/wagmi/commit/07370106d5fb6b8fe300992d93abf25b3d0eaf57)]:
+ - @wagmi/core@2.17.2
+
+## 5.8.1
+
+### Patch Changes
+
+- Updated dependencies [[`01f64e64fa4f85cdd30023903f972f4f9023681f`](https://github.com/wevm/wagmi/commit/01f64e64fa4f85cdd30023903f972f4f9023681f)]:
+ - @wagmi/core@2.17.1
+
+## 5.8.0
+
+### Minor Changes
+
+- [#4644](https://github.com/wevm/wagmi/pull/4644) [`cc5517ff6880bb630f1b201930acc20dd1a0b451`](https://github.com/wevm/wagmi/commit/cc5517ff6880bb630f1b201930acc20dd1a0b451) Thanks [@lukaisailovic](https://github.com/lukaisailovic)! - Updated `@walletconnect/etherereum-provider` to `2.20.0`.
+
+## 5.7.13
+
+### Patch Changes
+
+- [#4622](https://github.com/wevm/wagmi/pull/4622) [`88427b2bcd13ec375ef519e9ad1ccffef9f02a7b`](https://github.com/wevm/wagmi/commit/88427b2bcd13ec375ef519e9ad1ccffef9f02a7b) Thanks [@dan1kov](https://github.com/dan1kov)! - Added `rdns` property to Coinbase Wallet v3 connector
+
+- [#4605](https://github.com/wevm/wagmi/pull/4605) [`3f8b2edc4f237cccff1009bcef03d51ca27a7324`](https://github.com/wevm/wagmi/commit/3f8b2edc4f237cccff1009bcef03d51ca27a7324) Thanks [@chybisov](https://github.com/chybisov)! - Bumped `@safe-global/safe-apps-provider` version to `0.18.6`.
+
+- Updated dependencies [[`799ee4d4b23c2ecd64e3f3668e67634e81939719`](https://github.com/wevm/wagmi/commit/799ee4d4b23c2ecd64e3f3668e67634e81939719)]:
+ - @wagmi/core@2.17.0
+
+## 5.7.12
+
+### Patch Changes
+
+- [#4608](https://github.com/wevm/wagmi/pull/4608) [`b59c024b23c69f5459b17390531207cfdf126ce4`](https://github.com/wevm/wagmi/commit/b59c024b23c69f5459b17390531207cfdf126ce4) Thanks [@jxom](https://github.com/jxom)! - Updated `@walletconnect/ethereum-provider`.
+
+## 5.7.11
+
+### Patch Changes
+
+- Updated dependencies [[`a4bd0623eed28e3761a27295831a60ad835f0ee0`](https://github.com/wevm/wagmi/commit/a4bd0623eed28e3761a27295831a60ad835f0ee0)]:
+ - @wagmi/core@2.16.7
+
+## 5.7.10
+
+### Patch Changes
+
+- [#4573](https://github.com/wevm/wagmi/pull/4573) [`e944812ebc234a72c1417b77cff341166f5e0fef`](https://github.com/wevm/wagmi/commit/e944812ebc234a72c1417b77cff341166f5e0fef) Thanks [@ganchoradkov](https://github.com/ganchoradkov)! - updated `@walletconnect/ethereum-provider` to `2.19.1`
+
+- Updated dependencies [[`edf47477b2f6385a1c3ae01d36a8498c47f30a0b`](https://github.com/wevm/wagmi/commit/edf47477b2f6385a1c3ae01d36a8498c47f30a0b)]:
+ - @wagmi/core@2.16.6
+
+## 5.7.9
+
+### Patch Changes
+
+- [#4571](https://github.com/wevm/wagmi/pull/4571) [`5b7101fddb61df56e34b2e02b46bc409e496eaf9`](https://github.com/wevm/wagmi/commit/5b7101fddb61df56e34b2e02b46bc409e496eaf9) Thanks [@ganchoradkov](https://github.com/ganchoradkov)! - Updated `@walletconnect/ethereum-provider` to `2.19.0`
+
+## 5.7.8
+
+### Patch Changes
+
+- Updated dependencies [[`d0c9a86921a4e939373cc6e763284e53f2a2e93c`](https://github.com/wevm/wagmi/commit/d0c9a86921a4e939373cc6e763284e53f2a2e93c)]:
+ - @wagmi/core@2.16.5
+
+## 5.7.7
+
+### Patch Changes
+
+- [`507f864d91238bfd423d0e36d3619eb9f6e52eec`](https://github.com/wevm/wagmi/commit/507f864d91238bfd423d0e36d3619eb9f6e52eec) Thanks [@jxom](https://github.com/jxom)! - Updated `@coinbase/wallet-sdk`.
+
+- Updated dependencies [[`507f864d91238bfd423d0e36d3619eb9f6e52eec`](https://github.com/wevm/wagmi/commit/507f864d91238bfd423d0e36d3619eb9f6e52eec)]:
+ - @wagmi/core@2.16.4
+
+## 5.7.6
+
+### Patch Changes
+
+- [#4524](https://github.com/wevm/wagmi/pull/4524) [`639952c97f0fe3927106f42d3c9f7f366cdf7f7a`](https://github.com/wevm/wagmi/commit/639952c97f0fe3927106f42d3c9f7f366cdf7f7a) Thanks [@chakra-guy](https://github.com/chakra-guy)! - Updated MetaMask SDK.
+
+- [#4525](https://github.com/wevm/wagmi/pull/4525) [`5aa2c095f7bfb6dfcf91c6945c3e1f9c9dd05766`](https://github.com/wevm/wagmi/commit/5aa2c095f7bfb6dfcf91c6945c3e1f9c9dd05766) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Added Phantom flag to Injected Connector.
+
+## 5.7.5
+
+### Patch Changes
+
+- [#4512](https://github.com/wevm/wagmi/pull/4512) [`a257e8d4f97431a4af872cda1817b4ae17c7bbed`](https://github.com/wevm/wagmi/commit/a257e8d4f97431a4af872cda1817b4ae17c7bbed) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Fixed MetaMask switchChain/addChain handling.
+
+## 5.7.4
+
+### Patch Changes
+
+- [#4505](https://github.com/wevm/wagmi/pull/4505) [`c8a257e0f6d2ece013b873895c35769a8a804fdc`](https://github.com/wevm/wagmi/commit/c8a257e0f6d2ece013b873895c35769a8a804fdc) Thanks [@chakra-guy](https://github.com/chakra-guy)! - Bumped Metamask SDK Version (changes include [bug fixes and minor changes](https://github.com/MetaMask/metamask-sdk/pull/1194)).
+
+## 5.7.3
+
+### Patch Changes
+
+- [#4480](https://github.com/wevm/wagmi/pull/4480) [`384a1d91597622eb59e1c05dc13ce25017c5b6d8`](https://github.com/wevm/wagmi/commit/384a1d91597622eb59e1c05dc13ce25017c5b6d8) Thanks [@RodeRickIsWatching](https://github.com/RodeRickIsWatching)! - Fixed invocation of default storage.
+
+- Updated dependencies [[`384a1d91597622eb59e1c05dc13ce25017c5b6d8`](https://github.com/wevm/wagmi/commit/384a1d91597622eb59e1c05dc13ce25017c5b6d8)]:
+ - @wagmi/core@2.16.3
+
+## 5.7.2
+
+### Patch Changes
+
+- [`012907032b532a438fce48f407470250cbc8f0c6`](https://github.com/wevm/wagmi/commit/012907032b532a438fce48f407470250cbc8f0c6) Thanks [@jxom](https://github.com/jxom)! - Fixed assignment in `getDefaultStorage`.
+
+- Updated dependencies [[`012907032b532a438fce48f407470250cbc8f0c6`](https://github.com/wevm/wagmi/commit/012907032b532a438fce48f407470250cbc8f0c6)]:
+ - @wagmi/core@2.16.2
+
+## 5.7.1
+
+### Patch Changes
+
+- [#4471](https://github.com/wevm/wagmi/pull/4471) [`9c8c35a3b829f2c58edcd3a29e2dcd99974d7470`](https://github.com/wevm/wagmi/commit/9c8c35a3b829f2c58edcd3a29e2dcd99974d7470) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Improved MetaMask chain switching behavior.
+
+- Updated dependencies [[`3892ebd21c06beef4b28ece4e70d2a38807bce6f`](https://github.com/wevm/wagmi/commit/3892ebd21c06beef4b28ece4e70d2a38807bce6f)]:
+ - @wagmi/core@2.16.1
+
+## 5.7.0
+
+### Minor Changes
+
+- [#4440](https://github.com/wevm/wagmi/pull/4440) [`e3f63a02c1f7d80481804584f262bc98dab0400d`](https://github.com/wevm/wagmi/commit/e3f63a02c1f7d80481804584f262bc98dab0400d) Thanks [@johanneskares](https://github.com/johanneskares)! - Added Coinbase Smart Wallet "Instant Onboarding" mode to `coinbaseWallet`.
+
+## 5.6.2
+
+### Patch Changes
+
+- [#4437](https://github.com/wevm/wagmi/pull/4437) [`adf2253b10c6d4fc583e4bc9f01a8ef5ca267c85`](https://github.com/wevm/wagmi/commit/adf2253b10c6d4fc583e4bc9f01a8ef5ca267c85) Thanks [@chybisov](https://github.com/chybisov)! - Bumped `@safe-global/safe-apps-provider` version to `0.18.5`.
+
+## 5.6.1
+
+### Patch Changes
+
+- [#4458](https://github.com/wevm/wagmi/pull/4458) [`987404f590c1d29ebb3cb68928f5e54aa032793d`](https://github.com/wevm/wagmi/commit/987404f590c1d29ebb3cb68928f5e54aa032793d) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Fixed MetaMask internal metadata handling.
+
+## 5.6.0
+
+### Minor Changes
+
+- [#4453](https://github.com/wevm/wagmi/pull/4453) [`070e48480194c8d7f45bda1d7dd1346e6f5d7227`](https://github.com/wevm/wagmi/commit/070e48480194c8d7f45bda1d7dd1346e6f5d7227) Thanks [@tmm](https://github.com/tmm)! - Added narrowing to `config.connectors`.
+
+### Patch Changes
+
+- [#4456](https://github.com/wevm/wagmi/pull/4456) [`8b0726c1106fce88b782e676498eabf0718b2619`](https://github.com/wevm/wagmi/commit/8b0726c1106fce88b782e676498eabf0718b2619) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Bumped MetaMask SDK and fixed internal metadata handling.
+- Updated dependencies [[`afea6b67822a7a2b96901ec851441d27ee0f7a52`](https://github.com/wevm/wagmi/commit/afea6b67822a7a2b96901ec851441d27ee0f7a52), [`070e48480194c8d7f45bda1d7dd1346e6f5d7227`](https://github.com/wevm/wagmi/commit/070e48480194c8d7f45bda1d7dd1346e6f5d7227)]:
+ - @wagmi/core@2.16.0
+
+## 5.5.3
+
+### Patch Changes
+
+- [#4433](https://github.com/wevm/wagmi/pull/4433) [`06e186cd679b27fe195309110e766fcf46d4efbc`](https://github.com/wevm/wagmi/commit/06e186cd679b27fe195309110e766fcf46d4efbc) Thanks [@Aerilym](https://github.com/Aerilym)! - Bumped Metamask SDK version to `0.31.1`.
+
+- Updated dependencies [[`06e186cd679b27fe195309110e766fcf46d4efbc`](https://github.com/wevm/wagmi/commit/06e186cd679b27fe195309110e766fcf46d4efbc)]:
+ - @wagmi/core@2.15.2
+
+## 5.5.2
+
+### Patch Changes
+
+- [#4422](https://github.com/wevm/wagmi/pull/4422) [`e563ef69130a511fd6f3f72ed4cd4fbe1390541f`](https://github.com/wevm/wagmi/commit/e563ef69130a511fd6f3f72ed4cd4fbe1390541f) Thanks [@abretonc7s](https://github.com/abretonc7s)! - Bumped MetaMask SDK.
+
+## 5.5.1
+
+### Patch Changes
+
+- Updated dependencies [[`b8bbb409f4934538e3dd6cac5aaf7346292d0693`](https://github.com/wevm/wagmi/commit/b8bbb409f4934538e3dd6cac5aaf7346292d0693)]:
+ - @wagmi/core@2.15.1
+
+## 5.5.0
+
+### Minor Changes
+
+- [#4417](https://github.com/wevm/wagmi/pull/4417) [`42e65ea4fea99c639817088bba915e0933d17141`](https://github.com/wevm/wagmi/commit/42e65ea4fea99c639817088bba915e0933d17141) Thanks [@jxom](https://github.com/jxom)! - Removed simulation in `writeContract` & `sendTransaction`.
+
+### Patch Changes
+
+- Updated dependencies [[`42e65ea4fea99c639817088bba915e0933d17141`](https://github.com/wevm/wagmi/commit/42e65ea4fea99c639817088bba915e0933d17141)]:
+ - @wagmi/core@2.15.0
+
+## 5.4.0
+
+### Minor Changes
+
+- [#4409](https://github.com/wevm/wagmi/pull/4409) [`7ca62b44cd997d48f92c2b81343726a5908aa00b`](https://github.com/wevm/wagmi/commit/7ca62b44cd997d48f92c2b81343726a5908aa00b) Thanks [@fan-zhang-sv](https://github.com/fan-zhang-sv)! - Added `preference` object for Coinbase Wallet connector.
+
+## 5.3.10
+
+### Patch Changes
+
+- [#4406](https://github.com/wevm/wagmi/pull/4406) [`a13aa8d7c38eb3cc8171a02d6302e6d12cf6bcb3`](https://github.com/wevm/wagmi/commit/a13aa8d7c38eb3cc8171a02d6302e6d12cf6bcb3) Thanks [@tmm](https://github.com/tmm)! - Added additional RDNS to MetaMask Connector.
+
+- Updated dependencies [[`a13aa8d7c38eb3cc8171a02d6302e6d12cf6bcb3`](https://github.com/wevm/wagmi/commit/a13aa8d7c38eb3cc8171a02d6302e6d12cf6bcb3)]:
+ - @wagmi/core@2.14.6
+
+## 5.3.9
+
+### Patch Changes
+
+- [`b12a04eeec985c48d2feac94b011d41fb29ca23e`](https://github.com/wevm/wagmi/commit/b12a04eeec985c48d2feac94b011d41fb29ca23e) Thanks [@tmm](https://github.com/tmm)! - Bumped Coinbase Wallet SDK version.
+
+## 5.3.8
+
+### Patch Changes
+
+- [#4390](https://github.com/wevm/wagmi/pull/4390) [`dac62dc99a0679fa632a0fae49873d6053d06b35`](https://github.com/wevm/wagmi/commit/dac62dc99a0679fa632a0fae49873d6053d06b35) Thanks [@chybisov](https://github.com/chybisov)! - Bumped Safe Apps Provider version.
+
+- Updated dependencies [[`6b9bbacdc7bffd44fc2165362a5e65fd434e7646`](https://github.com/wevm/wagmi/commit/6b9bbacdc7bffd44fc2165362a5e65fd434e7646)]:
+ - @wagmi/core@2.14.5
+
+## 5.3.7
+
+### Patch Changes
+
+- Updated dependencies [[`e08681c81fbdf475213e2d0f4c5517d0abf4e743`](https://github.com/wevm/wagmi/commit/e08681c81fbdf475213e2d0f4c5517d0abf4e743)]:
+ - @wagmi/core@2.14.4
+
+## 5.3.6
+
+### Patch Changes
+
+- [#4385](https://github.com/wevm/wagmi/pull/4385) [`7558ff3133c11bc4c49473d08ee9a47eaa12df5b`](https://github.com/wevm/wagmi/commit/7558ff3133c11bc4c49473d08ee9a47eaa12df5b) Thanks [@cb-jake](https://github.com/cb-jake)! - Bumped Coinbase Wallet SDK version.
+
+## 5.3.5
+
+### Patch Changes
+
+- [`7fe78f2d09778fc01fd0cffe85ba198e64999275`](https://github.com/wevm/wagmi/commit/7fe78f2d09778fc01fd0cffe85ba198e64999275) Thanks [@tmm](https://github.com/tmm)! - Fixed MetaMask connector not returning provider in some cases.
+
+- Updated dependencies [[`cb7dd2ebb871d0be8f1a11a8cd8ce592cd74b7c7`](https://github.com/wevm/wagmi/commit/cb7dd2ebb871d0be8f1a11a8cd8ce592cd74b7c7)]:
+ - @wagmi/core@2.14.3
+
+## 5.3.4
+
+### Patch Changes
+
+- [#4371](https://github.com/wevm/wagmi/pull/4371) [`b6861a4c378dab78d8751ae0ac2aa425f3c24b8f`](https://github.com/wevm/wagmi/commit/b6861a4c378dab78d8751ae0ac2aa425f3c24b8f) Thanks [@iceanddust](https://github.com/iceanddust)! - Fixed Safe connector not working in some Vite apps
+
+- Updated dependencies [[`d0d0963bb5904a15cf0355862d62dd141ce0c31c`](https://github.com/wevm/wagmi/commit/d0d0963bb5904a15cf0355862d62dd141ce0c31c), [`ecac0ba36243d94c9199d0bd21937104c835d9a0`](https://github.com/wevm/wagmi/commit/ecac0ba36243d94c9199d0bd21937104c835d9a0)]:
+ - @wagmi/core@2.14.2
+
+## 5.3.3
+
+### Patch Changes
+
+- [#4362](https://github.com/wevm/wagmi/pull/4362) [`83c6d16b7d6dddfa6bda036e04f00ec313c6248c`](https://github.com/wevm/wagmi/commit/83c6d16b7d6dddfa6bda036e04f00ec313c6248c) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Fixed MetaMask connector internal logic.
+
+## 5.3.2
+
+### Patch Changes
+
+- [#4357](https://github.com/wevm/wagmi/pull/4357) [`8970cc51398e1ac713435533096215c6d31ffdf9`](https://github.com/wevm/wagmi/commit/8970cc51398e1ac713435533096215c6d31ffdf9) Thanks [@tmm](https://github.com/tmm)! - Bumped dependencies.
+
+## 5.3.1
+
+### Patch Changes
+
+- Updated dependencies [[`052e72e1f8c1c14fcbdce04a9f8fa7ec28d83702`](https://github.com/wevm/wagmi/commit/052e72e1f8c1c14fcbdce04a9f8fa7ec28d83702), [`b250fc21ee577b2a75c5a34ff684f62fb4ad771a`](https://github.com/wevm/wagmi/commit/b250fc21ee577b2a75c5a34ff684f62fb4ad771a)]:
+ - @wagmi/core@2.14.1
+
+## 5.3.0
+
+### Minor Changes
+
+- [#4343](https://github.com/wevm/wagmi/pull/4343) [`f43e074f473820b208a6295d7c97f847332f1a1d`](https://github.com/wevm/wagmi/commit/f43e074f473820b208a6295d7c97f847332f1a1d) Thanks [@tmm](https://github.com/tmm)! - Added `rdns` property to connector interface. This is used to filter out duplicate [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) injected providers when [`createConfig#multiInjectedProviderDiscovery`](https://wagmi.sh/core/api/createConfig#multiinjectedproviderdiscovery) is enabled and `createConfig#connectors` already matches EIP-6963 providers' `rdns` property.
+
+### Patch Changes
+
+- Updated dependencies [[`f43e074f473820b208a6295d7c97f847332f1a1d`](https://github.com/wevm/wagmi/commit/f43e074f473820b208a6295d7c97f847332f1a1d)]:
+ - @wagmi/core@2.14.0
+
+## 5.2.2
+
+### Patch Changes
+
+- [#4347](https://github.com/wevm/wagmi/pull/4347) [`5ae49af590ff168426c9c283d54c34ae5148fcd9`](https://github.com/wevm/wagmi/commit/5ae49af590ff168426c9c283d54c34ae5148fcd9) Thanks [@EdouardBougon](https://github.com/EdouardBougon)! - Added workaround for MetaMask mobile sometimes disconnecting.
+
+- [#4350](https://github.com/wevm/wagmi/pull/4350) [`f3182b22e6e454d9bd74f1b940ef34431fd9555d`](https://github.com/wevm/wagmi/commit/f3182b22e6e454d9bd74f1b940ef34431fd9555d) Thanks [@abretonc7s](https://github.com/abretonc7s)! - Updated MetaMask SDK.
+
+- Updated dependencies [[`c05caabc20c3ced9682cfc7ba1f3f7dcfece0703`](https://github.com/wevm/wagmi/commit/c05caabc20c3ced9682cfc7ba1f3f7dcfece0703)]:
+ - @wagmi/core@2.13.9
+
+## 5.2.1
+
+### Patch Changes
+
+- [#4345](https://github.com/wevm/wagmi/pull/4345) [`91a40f2db08e3a91db421b8732a5511a1e6c88fd`](https://github.com/wevm/wagmi/commit/91a40f2db08e3a91db421b8732a5511a1e6c88fd) Thanks [@tmm](https://github.com/tmm)! - Bumped MetaMask SDK.
+
+## 5.2.0
+
+### Minor Changes
+
+- [#4337](https://github.com/wevm/wagmi/pull/4337) [`34a0c3b7eea778aee7c27f7ace5e4b2be4e8a0a4`](https://github.com/wevm/wagmi/commit/34a0c3b7eea778aee7c27f7ace5e4b2be4e8a0a4) Thanks [@tmm](https://github.com/tmm)! - Added "Connect and Sign" behavior to MetaMask Connector.
+
+## 5.1.15
+
+### Patch Changes
+
+- [`3b2123664b7ac66848390739e855c3b9702ab60c`](https://github.com/wevm/wagmi/commit/3b2123664b7ac66848390739e855c3b9702ab60c) Thanks [@tmm](https://github.com/tmm)! - Bumped WalletConnect Provider.
+
+## 5.1.14
+
+### Patch Changes
+
+- [#4207](https://github.com/wevm/wagmi/pull/4207) [`56f2482508f2ba71bd6b0295c70c6abca7101e57`](https://github.com/wevm/wagmi/commit/56f2482508f2ba71bd6b0295c70c6abca7101e57) Thanks [@Smert](https://github.com/Smert)! - Updated chain switch listener for `injected` and `metaMask` to be more robust.
+
+- Updated dependencies [[`56f2482508f2ba71bd6b0295c70c6abca7101e57`](https://github.com/wevm/wagmi/commit/56f2482508f2ba71bd6b0295c70c6abca7101e57)]:
+ - @wagmi/core@2.13.8
+
+## 5.1.13
+
+### Patch Changes
+
+- Updated dependencies [[`be75c2d4ef636d7362420ab0a106bfdf63f5d1e6`](https://github.com/wevm/wagmi/commit/be75c2d4ef636d7362420ab0a106bfdf63f5d1e6)]:
+ - @wagmi/core@2.13.7
+
+## 5.1.12
+
+### Patch Changes
+
+- Updated dependencies [[`edcbf5d6fbe92f639bead800502edda9e0aa39f1`](https://github.com/wevm/wagmi/commit/edcbf5d6fbe92f639bead800502edda9e0aa39f1)]:
+ - @wagmi/core@2.13.6
+
+## 5.1.11
+
+### Patch Changes
+
+- [#4271](https://github.com/wevm/wagmi/pull/4271) [`82404c960e04c83e0bae6e1e12459ef9debf9554`](https://github.com/wevm/wagmi/commit/82404c960e04c83e0bae6e1e12459ef9debf9554) Thanks [@omridan159](https://github.com/omridan159)! - Bumped MetaMask SDK.
+
+- [#4227](https://github.com/wevm/wagmi/pull/4227) [`d07ad7f63a018256908a673d078aaf79e47ac703`](https://github.com/wevm/wagmi/commit/d07ad7f63a018256908a673d078aaf79e47ac703) Thanks [@xianchenxc](https://github.com/xianchenxc)! - Fixed MetaMask Connector throwing error after switching to a chain that was just added via `'wallet_addEthereumChain'`.
+
+## 5.1.10
+
+### Patch Changes
+
+- [#4255](https://github.com/wevm/wagmi/pull/4255) [`81de006e66121a18c61945c1f9b8426c83a5713c`](https://github.com/wevm/wagmi/commit/81de006e66121a18c61945c1f9b8426c83a5713c) Thanks [@tomiir](https://github.com/tomiir)! - Bumped `@walletconnect/ethereum-provider` from version `2.15.3` to version `2.16.1`.
+
+- Updated dependencies [[`f47ce8f6d263e49fdff90b8edb3190142d2657bb`](https://github.com/wevm/wagmi/commit/f47ce8f6d263e49fdff90b8edb3190142d2657bb)]:
+ - @wagmi/core@2.13.5
+
+## 5.1.9
+
+### Patch Changes
+
+- [#4243](https://github.com/wevm/wagmi/pull/4243) [`21bd0e473d374cbbd7a01bececa6022d529026ba`](https://github.com/wevm/wagmi/commit/21bd0e473d374cbbd7a01bececa6022d529026ba) Thanks [@tomiir](https://github.com/tomiir)! - Bumped `@walletconnect/ethereum-provider` from version `2.15.2` to version `2.15.3`
+
+- [#4251](https://github.com/wevm/wagmi/pull/4251) [`5c89c6853e616437a3be2b019db895451fecfb3c`](https://github.com/wevm/wagmi/commit/5c89c6853e616437a3be2b019db895451fecfb3c) Thanks [@tmm](https://github.com/tmm)! - Bumped MM SDK.
+
+## 5.1.8
+
+### Patch Changes
+
+- [`b580ad4edff1721e0b9d138cf5ae2ec74d2374c7`](https://github.com/wevm/wagmi/commit/b580ad4edff1721e0b9d138cf5ae2ec74d2374c7) Thanks [@tmm](https://github.com/tmm)! - Bumped WalletConnect Provider.
+
+## 5.1.7
+
+### Patch Changes
+
+- [#4213](https://github.com/wevm/wagmi/pull/4213) [`91fd81a068789c5020e891f539bcad8f54a7a52f`](https://github.com/wevm/wagmi/commit/91fd81a068789c5020e891f539bcad8f54a7a52f) Thanks [@tomiir](https://github.com/tomiir)! - Updated `@walletconnect/ethereum-provider` from version `2.15.0` to version `2.15.1`.
+
+## 5.1.6
+
+### Patch Changes
+
+- [#4208](https://github.com/wevm/wagmi/pull/4208) [`3168616298cbb6135d0ffda771cba4126e83eba8`](https://github.com/wevm/wagmi/commit/3168616298cbb6135d0ffda771cba4126e83eba8) Thanks [@tomiir](https://github.com/tomiir)! - Updated WalletConnect Ethereum Provider version from `2.14.0` to `2.15.0`.
+
+- [#4211](https://github.com/wevm/wagmi/pull/4211) [`d7608ef9a79459465dc8c06a2ab740465c881907`](https://github.com/wevm/wagmi/commit/d7608ef9a79459465dc8c06a2ab740465c881907) Thanks [@tmm](https://github.com/tmm)! - Added default name for MetaMask Connector.
+
+## 5.1.5
+
+### Patch Changes
+
+- Updated dependencies [[`b4c8971788c70b09479946ecfa998cff2f1b3953`](https://github.com/wevm/wagmi/commit/b4c8971788c70b09479946ecfa998cff2f1b3953)]:
+ - @wagmi/core@2.13.4
+
+## 5.1.4
+
+### Patch Changes
+
+- Updated dependencies [[`871dbdbfe59ac8ad01d1ec6150ea7b091b7b7de4`](https://github.com/wevm/wagmi/commit/871dbdbfe59ac8ad01d1ec6150ea7b091b7b7de4)]:
+ - @wagmi/core@2.13.3
+
+## 5.1.3
+
+### Patch Changes
+
+- Updated dependencies [[`1b9b523fa9b9dfe839aecdf4b40caa9547d7e594`](https://github.com/wevm/wagmi/commit/1b9b523fa9b9dfe839aecdf4b40caa9547d7e594)]:
+ - @wagmi/core@2.13.2
+
+## 5.1.2
+
+### Patch Changes
+
+- [`abb490dac4f0f02f46cb0878e7ca9a0db6aada56`](https://github.com/wevm/wagmi/commit/abb490dac4f0f02f46cb0878e7ca9a0db6aada56) Thanks [@tmm](https://github.com/tmm)! - Bumped MetaMask SDK version.
+
+- [`28e0e5c9a4f856583f9d36a807502bd51a0c6ec2`](https://github.com/wevm/wagmi/commit/28e0e5c9a4f856583f9d36a807502bd51a0c6ec2) Thanks [@tmm](https://github.com/tmm)! - Bumped WalletConnect Ethereum Provider version.
+
+## 5.1.1
+
+### Patch Changes
+
+- Updated dependencies [[`07c1227f306d0efb9421d4bb77a774f92f5fcf45`](https://github.com/wevm/wagmi/commit/07c1227f306d0efb9421d4bb77a774f92f5fcf45)]:
+ - @wagmi/core@2.13.1
+
+## 5.1.0
+
+### Minor Changes
+
+- [#4162](https://github.com/wevm/wagmi/pull/4162) [`a73a7737b756886b388f120ae423e72cca53e8a0`](https://github.com/wevm/wagmi/commit/a73a7737b756886b388f120ae423e72cca53e8a0) Thanks [@jxom](https://github.com/jxom)! - Added functionality for consumer-defined RPC URLs (`config.transports`) to be propagated to the WalletConnect & MetaMask Connectors.
+
+### Patch Changes
+
+- Updated dependencies [[`a73a7737b756886b388f120ae423e72cca53e8a0`](https://github.com/wevm/wagmi/commit/a73a7737b756886b388f120ae423e72cca53e8a0)]:
+ - @wagmi/core@2.13.0
+
+## 5.0.26
+
+### Patch Changes
+
+- [`8d81df5cc884d0a210dedd3c1ea0e2e9e52b83c5`](https://github.com/wevm/wagmi/commit/8d81df5cc884d0a210dedd3c1ea0e2e9e52b83c5) Thanks [@tmm](https://github.com/tmm)! - Fixed `metaMask` connector switch chain issue.
+
+- Updated dependencies [[`5bc8c8877810b2eec24a829df87dce40a51e6f20`](https://github.com/wevm/wagmi/commit/5bc8c8877810b2eec24a829df87dce40a51e6f20)]:
+ - @wagmi/core@2.12.2
+
+## 5.0.25
+
+### Patch Changes
+
+- [#4146](https://github.com/wevm/wagmi/pull/4146) [`cc996e08e930c9e88cf753a1e874652059e81a3b`](https://github.com/wevm/wagmi/commit/cc996e08e930c9e88cf753a1e874652059e81a3b) Thanks [@jxom](https://github.com/jxom)! - Updated `@safe-global/safe-apps-sdk` + `@safe-global/safe-apps-provider` dependencies.
+
+- Updated dependencies [[`cc996e08e930c9e88cf753a1e874652059e81a3b`](https://github.com/wevm/wagmi/commit/cc996e08e930c9e88cf753a1e874652059e81a3b)]:
+ - @wagmi/core@2.12.1
+
+## 5.0.24
+
+### Patch Changes
+
+- Updated dependencies [[`5581a810ef70308e99c6f8b630cd4bca59f64afc`](https://github.com/wevm/wagmi/commit/5581a810ef70308e99c6f8b630cd4bca59f64afc)]:
+ - @wagmi/core@2.12.0
+
+## 5.0.23
+
+### Patch Changes
+
+- [`d3814ab4b88f9f0e052b53bc3d458df87b43f01d`](https://github.com/wevm/wagmi/commit/d3814ab4b88f9f0e052b53bc3d458df87b43f01d) Thanks [@jxom](https://github.com/jxom)! - Updated `mipd` dependency.
+
+- Updated dependencies [[`b08013eaa9ce97c02f8a7128ea400e3da7ef74bb`](https://github.com/wevm/wagmi/commit/b08013eaa9ce97c02f8a7128ea400e3da7ef74bb), [`d3814ab4b88f9f0e052b53bc3d458df87b43f01d`](https://github.com/wevm/wagmi/commit/d3814ab4b88f9f0e052b53bc3d458df87b43f01d)]:
+ - @wagmi/core@2.11.8
+
+## 5.0.22
+
+### Patch Changes
+
+- [`0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e`](https://github.com/wevm/wagmi/commit/0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e) Thanks [@tmm](https://github.com/tmm)! - Improved TypeScript `'exactOptionalPropertyTypes'` support.
+
+- Updated dependencies [[`0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e`](https://github.com/wevm/wagmi/commit/0bb8b562ae04ecfeb2d6b2f1b980ebae31dc127e)]:
+ - @wagmi/core@2.11.7
+
+## 5.0.21
+
+### Patch Changes
+
+- [#4094](https://github.com/wevm/wagmi/pull/4094) [`ff0760b5900114bcfdf420a9fba3cc278ac95afe`](https://github.com/wevm/wagmi/commit/ff0760b5900114bcfdf420a9fba3cc278ac95afe) Thanks [@omridan159](https://github.com/omridan159)! - Bumped MetaMask SDK to fix `metaMask` connector error bubbling.
+
+- Updated dependencies [[`95965c1f19d480b97f2b297a077a9e607dee32ad`](https://github.com/wevm/wagmi/commit/95965c1f19d480b97f2b297a077a9e607dee32ad)]:
+ - @wagmi/core@2.11.6
+
+## 5.0.20
+
+### Patch Changes
+
+- [`43fa971d34cac57fa5a2898ad4d839b95d7af37c`](https://github.com/wevm/wagmi/commit/43fa971d34cac57fa5a2898ad4d839b95d7af37c) Thanks [@tmm](https://github.com/tmm)! - Bumped Coinbase Wallet SDK and fixed `metaMask` connector hang on mobile.
+
+## 5.0.19
+
+### Patch Changes
+
+- [#4083](https://github.com/wevm/wagmi/pull/4083) [`b7ad208030d9f2e3f89912ff76b16cdbd848feda`](https://github.com/wevm/wagmi/commit/b7ad208030d9f2e3f89912ff76b16cdbd848feda) Thanks [@omridan159](https://github.com/omridan159)! - Bumped MetaMask SDK
+
+## 5.0.18
+
+### Patch Changes
+
+- [#4081](https://github.com/wevm/wagmi/pull/4081) [`44d24620c9e3957f3245d14d6a042736371df70b`](https://github.com/wevm/wagmi/commit/44d24620c9e3957f3245d14d6a042736371df70b) Thanks [@tmm](https://github.com/tmm)! - Bumped MetaMask SDK
+
+## 5.0.17
+
+### Patch Changes
+
+- Updated dependencies [[`04f2b846b113f3d300d82c9fa75212f1805817c5`](https://github.com/wevm/wagmi/commit/04f2b846b113f3d300d82c9fa75212f1805817c5)]:
+ - @wagmi/core@2.11.5
+
+## 5.0.16
+
+### Patch Changes
+
+- [#4071](https://github.com/wevm/wagmi/pull/4071) [`02c38c28d1aa0ad7a61c33775de603ed974c5c1b`](https://github.com/wevm/wagmi/commit/02c38c28d1aa0ad7a61c33775de603ed974c5c1b) Thanks [@omridan159](https://github.com/omridan159)! - Bumped MetaMask SDK
+
+- Updated dependencies [[`9e8345cd56186b997b5e56deaa2cfc69b30d15f6`](https://github.com/wevm/wagmi/commit/9e8345cd56186b997b5e56deaa2cfc69b30d15f6)]:
+ - @wagmi/core@2.11.4
+
+## 5.0.15
+
+### Patch Changes
+
+- Updated dependencies [[`8974e6269bb5d7bfaa90db0246bc7d13e8bff798`](https://github.com/wevm/wagmi/commit/8974e6269bb5d7bfaa90db0246bc7d13e8bff798)]:
+ - @wagmi/core@2.11.3
+
+## 5.0.14
+
+### Patch Changes
+
+- Updated dependencies [[`b4d9ef79deb554ee20fed6666a474be5e7cdd522`](https://github.com/wevm/wagmi/commit/b4d9ef79deb554ee20fed6666a474be5e7cdd522)]:
+ - @wagmi/core@2.11.2
+
+## 5.0.13
+
+### Patch Changes
+
+- [`9c862d8d63e3d692a22cef2a90782b74a9103f17`](https://github.com/wevm/wagmi/commit/9c862d8d63e3d692a22cef2a90782b74a9103f17) Thanks [@tmm](https://github.com/tmm)! - Reverted internal module loading utility.
+
+- Updated dependencies [[`9c862d8d63e3d692a22cef2a90782b74a9103f17`](https://github.com/wevm/wagmi/commit/9c862d8d63e3d692a22cef2a90782b74a9103f17)]:
+ - @wagmi/core@2.11.1
+
+## 5.0.12
+
+### Patch Changes
+
+- Updated dependencies [[`06bb598a7f04c7b167f5b7ff6d46bd15886a6a14`](https://github.com/wevm/wagmi/commit/06bb598a7f04c7b167f5b7ff6d46bd15886a6a14), [`24a45b269bd0214a29d6f82a84ac66ef8c3f3822`](https://github.com/wevm/wagmi/commit/24a45b269bd0214a29d6f82a84ac66ef8c3f3822)]:
+ - @wagmi/core@2.11.0
+
+## 5.0.11
+
+### Patch Changes
+
+- [#4020](https://github.com/wevm/wagmi/pull/4020) [`e3b124ce414b8fd1b2214e2c5a28dc72158a13d1`](https://github.com/wevm/wagmi/commit/e3b124ce414b8fd1b2214e2c5a28dc72158a13d1) Thanks [@tmm](https://github.com/tmm)! - Added reconnection support to `metaMask` on mobile and use deeplinks by default.
+
+- Updated dependencies [[`f2a7cefab96691ebed8b8e45ffde071c47b58dbe`](https://github.com/wevm/wagmi/commit/f2a7cefab96691ebed8b8e45ffde071c47b58dbe), [`f0ea0b2a7fe193dadfeb49a4c8031ee451c638b5`](https://github.com/wevm/wagmi/commit/f0ea0b2a7fe193dadfeb49a4c8031ee451c638b5)]:
+ - @wagmi/core@2.10.6
+
+## 5.0.10
+
+### Patch Changes
+
+- [`560952acd4bfe33db6c7c07b35c613cef278677c`](https://github.com/wevm/wagmi/commit/560952acd4bfe33db6c7c07b35c613cef278677c) Thanks [@tmm](https://github.com/tmm)! - Captured Coinbase Smart Wallet error when closing window as EIP-1193 `4001` error.
+
+## 5.0.9
+
+### Patch Changes
+
+- [`32cdd7b7dc5aff916c040628519562c3a99d418d`](https://github.com/wevm/wagmi/commit/32cdd7b7dc5aff916c040628519562c3a99d418d) Thanks [@tmm](https://github.com/tmm)! - Bumped `@metamask/sdk` to remove peer dependency install warning.
+
+## 5.0.8
+
+### Patch Changes
+
+- [#3997](https://github.com/wevm/wagmi/pull/3997) [`c1952d1ff7f0a491dc88595a49159451b07b5621`](https://github.com/wevm/wagmi/commit/c1952d1ff7f0a491dc88595a49159451b07b5621) Thanks [@nateReiners](https://github.com/nateReiners)! - Bumped Coinbase Wallet SDK.
+
+## 5.0.7
+
+### Patch Changes
+
+- Updated dependencies [[`030c7c2cb380dfd67a2182f62e2aa7a6e1601898`](https://github.com/wevm/wagmi/commit/030c7c2cb380dfd67a2182f62e2aa7a6e1601898)]:
+ - @wagmi/core@2.10.5
+
+## 5.0.6
+
+### Patch Changes
+
+- Updated dependencies [[`51fde8a0433b4fff357c1a8d7e08b41b4c86c968`](https://github.com/wevm/wagmi/commit/51fde8a0433b4fff357c1a8d7e08b41b4c86c968)]:
+ - @wagmi/core@2.10.4
+
+## 5.0.5
+
+### Patch Changes
+
+- [#3979](https://github.com/wevm/wagmi/pull/3979) [`70dd28669dd8d2ce08217cd02e29a8fbba7a08d4`](https://github.com/wevm/wagmi/commit/70dd28669dd8d2ce08217cd02e29a8fbba7a08d4) Thanks [@tmm](https://github.com/tmm)! - Fixed `walletConnect` connector.
+
+## 5.0.4
+
+### Patch Changes
+
+- [#3972](https://github.com/wevm/wagmi/pull/3972) [`be9e1b8a9818b92eb0654a20d9471e9e39329e7e`](https://github.com/wevm/wagmi/commit/be9e1b8a9818b92eb0654a20d9471e9e39329e7e) Thanks [@nateReiners](https://github.com/nateReiners)! - Bumped Coinbase Wallet SDK.
+
+## 5.0.3
+
+### Patch Changes
+
+- [#3962](https://github.com/wevm/wagmi/pull/3962) [`2804a8a583b1874271154898b4bae38756ef581c`](https://github.com/wevm/wagmi/commit/2804a8a583b1874271154898b4bae38756ef581c) Thanks [@tmm](https://github.com/tmm)! - Added timeout to `getInfo` called in `safe` connector since [non-Safe App iFrames cause it to not resolve](https://github.com/safe-global/safe-apps-sdk/issues/263#issuecomment-1029835840).
+
+- Updated dependencies [[`2804a8a583b1874271154898b4bae38756ef581c`](https://github.com/wevm/wagmi/commit/2804a8a583b1874271154898b4bae38756ef581c)]:
+ - @wagmi/core@2.10.3
+
+## 5.0.2
+
+### Patch Changes
+
+- [#3940](https://github.com/wevm/wagmi/pull/3940) [`a5071f581dfdfb961718873643a2fc629101c72a`](https://github.com/wevm/wagmi/commit/a5071f581dfdfb961718873643a2fc629101c72a) Thanks [@jxom](https://github.com/jxom)! - Fixed usage of `metaMask` connector in Vite environments.
+
+- Updated dependencies [[`a5071f581dfdfb961718873643a2fc629101c72a`](https://github.com/wevm/wagmi/commit/a5071f581dfdfb961718873643a2fc629101c72a)]:
+ - @wagmi/core@2.10.2
+
+## 5.0.1
+
+### Patch Changes
+
+- Bumped versions.
+
+- Updated dependencies []:
+ - @wagmi/core@2.10.1
+
+## 5.0.0
+
+### Major Changes
+
+- [#3928](https://github.com/wevm/wagmi/pull/3928) [`3117e71825f9c58a0d718f3d1686f1a191fa9cb1`](https://github.com/wevm/wagmi/commit/3117e71825f9c58a0d718f3d1686f1a191fa9cb1) Thanks [@tmm](https://github.com/tmm)! - **Breaking:** Updated default Coinbase SDK in `coinbaseWallet` Connector to v4.x.
+
+ Added a `version` property (defaults to `'4'`) to the `coinbaseWallet` Connector to target a version of the Coinbase SDK:
+
+ ```diff
+ coinbaseWallet({
+ + version: '3' | '4',
+ })
+ ```
+
+ If `headlessMode` property is set to `true`, then the Connector will target v3 of the Coinbase SDK.
+
+ The following properties are removed in v4 of the `coinbaseWallet` Connector:
+
+ - `chainId`
+ - `darkMode`
+ - `diagnosticLogger`
+ - `enableMobileDeepLink`
+ - `jsonRpcUrl`
+ - `linkApiUrl`
+ - `overrideIsCoinbaseBrowser`
+ - `overrideIsCoinbaseWallet`
+ - `overrideIsMetaMask`
+ - `reloadOnDisconnect`
+ - `uiConstructor`
+
+ Consumers can still use the above properties in v3 by passing `version: '3'` to the Connector. However, please note that v3 of the Coinbase SDK is deprecated and will be removed in a future release.
+
+### Patch Changes
+
+- Updated dependencies [[`3117e71825f9c58a0d718f3d1686f1a191fa9cb1`](https://github.com/wevm/wagmi/commit/3117e71825f9c58a0d718f3d1686f1a191fa9cb1)]:
+ - @wagmi/core@2.10.0
+
+## 4.3.10
+
+### Patch Changes
+
+- [#3906](https://github.com/wevm/wagmi/pull/3906) [`32fcb4a31dde6b0206961d8ffe9c651f8a459c67`](https://github.com/wevm/wagmi/commit/32fcb4a31dde6b0206961d8ffe9c651f8a459c67) Thanks [@tmm](https://github.com/tmm)! - Added support for Vue.
+
+- Updated dependencies [[`32fcb4a31dde6b0206961d8ffe9c651f8a459c67`](https://github.com/wevm/wagmi/commit/32fcb4a31dde6b0206961d8ffe9c651f8a459c67)]:
+ - @wagmi/core@2.9.8
+
+## 4.3.9
+
+### Patch Changes
+
+- [#3924](https://github.com/wevm/wagmi/pull/3924) [`1f58734f88458e0f6adb05c99f0c90f36ab286b8`](https://github.com/wevm/wagmi/commit/1f58734f88458e0f6adb05c99f0c90f36ab286b8) Thanks [@jxom](https://github.com/jxom)! - Refactored `isChainsStale` logic in `walletConnect` connector.
+
+- Updated dependencies [[`1f58734f88458e0f6adb05c99f0c90f36ab286b8`](https://github.com/wevm/wagmi/commit/1f58734f88458e0f6adb05c99f0c90f36ab286b8)]:
+ - @wagmi/core@2.9.7
+
+## 4.3.8
+
+### Patch Changes
+
+- [#3917](https://github.com/wevm/wagmi/pull/3917) [`05948fdad5bb4a56b08916d45b3dec2cb1e5f55b`](https://github.com/wevm/wagmi/commit/05948fdad5bb4a56b08916d45b3dec2cb1e5f55b) Thanks [@jxom](https://github.com/jxom)! - Updated `@metamask/sdk`.
+
+- Updated dependencies [[`05948fdad5bb4a56b08916d45b3dec2cb1e5f55b`](https://github.com/wevm/wagmi/commit/05948fdad5bb4a56b08916d45b3dec2cb1e5f55b)]:
+ - @wagmi/core@2.9.6
+
+## 4.3.7
+
+### Patch Changes
+
+- Updated dependencies [[`4fecbbb66d0aacd03b8c62a6455d11a33cde8f85`](https://github.com/wevm/wagmi/commit/4fecbbb66d0aacd03b8c62a6455d11a33cde8f85)]:
+ - @wagmi/core@2.9.5
+
+## 4.3.6
+
+### Patch Changes
+
+- Updated dependencies [[`e6139a97c4b8804d734b1547b5e3921ce01fbe24`](https://github.com/wevm/wagmi/commit/e6139a97c4b8804d734b1547b5e3921ce01fbe24)]:
+ - @wagmi/core@2.9.4
+
+## 4.3.5
+
+### Patch Changes
+
+- [#3904](https://github.com/wevm/wagmi/pull/3904) [`addca28ebc20f1a4367c35fe9ef786decff9c87e`](https://github.com/wevm/wagmi/commit/addca28ebc20f1a4367c35fe9ef786decff9c87e) Thanks [@jxom](https://github.com/jxom)! - Updated `@walletconnect/ethereum-provider`.
+
+- Updated dependencies [[`addca28ebc20f1a4367c35fe9ef786decff9c87e`](https://github.com/wevm/wagmi/commit/addca28ebc20f1a4367c35fe9ef786decff9c87e)]:
+ - @wagmi/core@2.9.3
+
+## 4.3.4
+
+### Patch Changes
+
+- [#3902](https://github.com/wevm/wagmi/pull/3902) [`204b7b624612405500ec098fb9e35facd3f74ca4`](https://github.com/wevm/wagmi/commit/204b7b624612405500ec098fb9e35facd3f74ca4) Thanks [@jxom](https://github.com/jxom)! - Made third-party SDK imports type-only.
+
+- Updated dependencies [[`204b7b624612405500ec098fb9e35facd3f74ca4`](https://github.com/wevm/wagmi/commit/204b7b624612405500ec098fb9e35facd3f74ca4)]:
+ - @wagmi/core@2.9.2
+
+## 4.3.3
+
+### Patch Changes
+
+- Updated dependencies [[`cda6a5d5`](https://github.com/wevm/wagmi/commit/cda6a5d56328330fbde050b4ef40b01c58d2519a)]:
+ - @wagmi/core@2.9.1
+
+## 4.3.2
+
+### Patch Changes
+
+- Updated dependencies [[`017828fc`](https://github.com/wevm/wagmi/commit/017828fc027c7a84b54ea9d627e9389f4d60d6c2)]:
+ - @wagmi/core@2.9.0
+
+## 4.3.1
+
+### Patch Changes
+
+- Updated dependencies [[`d4a78eb0`](https://github.com/wevm/wagmi/commit/d4a78eb07119d2e5617e52481ac7d6c6d1583ddc)]:
+ - @wagmi/core@2.8.1
+
+## 4.3.0
+
+### Minor Changes
+
+- [#3868](https://github.com/wevm/wagmi/pull/3868) [`c2af20b8`](https://github.com/wevm/wagmi/commit/c2af20b88cf16970d087faaec10b463357a5836e) Thanks [@jxom](https://github.com/jxom)! - Added `supportsSimulation` property to connectors that indicates if the connector's wallet supports contract simulation.
+
+### Patch Changes
+
+- Updated dependencies [[`0d141f17`](https://github.com/wevm/wagmi/commit/0d141f171d6ec44bcbfc9c876565b5e2fb8af6de), [`c2af20b8`](https://github.com/wevm/wagmi/commit/c2af20b88cf16970d087faaec10b463357a5836e)]:
+ - @wagmi/core@2.8.0
+
+## 4.2.0
+
+### Minor Changes
+
+- [#3857](https://github.com/wevm/wagmi/pull/3857) [`d4274c03`](https://github.com/wevm/wagmi/commit/d4274c03a6af5f2d26d31432016ebc14950a330e) Thanks [@tmm](https://github.com/tmm)! - Added `addEthereumChainParameter` to `switchChain`-related methods.
+
+### Patch Changes
+
+- Updated dependencies [[`d4274c03`](https://github.com/wevm/wagmi/commit/d4274c03a6af5f2d26d31432016ebc14950a330e), [`4781a405`](https://github.com/wevm/wagmi/commit/4781a4056d4ffc2c74f96a75429e9b2cd2417ad8), [`400c960b`](https://github.com/wevm/wagmi/commit/400c960b30d701c134850c695ae903a382c29b5b)]:
+ - @wagmi/core@2.7.0
+
+## 4.1.28
+
+### Patch Changes
+
+- [`e3c832a1`](https://github.com/wevm/wagmi/commit/e3c832a12c301f9b0ee129d877b3101d220ba8b2) Thanks [@jxom](https://github.com/jxom)! - Fixed undefined `navigator` issue in MetaMask connector.
+
+- Updated dependencies [[`e3c832a1`](https://github.com/wevm/wagmi/commit/e3c832a12c301f9b0ee129d877b3101d220ba8b2)]:
+ - @wagmi/core@2.6.19
+
+## 4.1.27
+
+### Patch Changes
+
+- [#3848](https://github.com/wevm/wagmi/pull/3848) [`dd40a41c`](https://github.com/wevm/wagmi/commit/dd40a41c526ab60a288aff2250ed8dba92a27b16) Thanks [@jxom](https://github.com/jxom)! - Updated MetaMask SDK.
+
+- Updated dependencies [[`dd40a41c`](https://github.com/wevm/wagmi/commit/dd40a41c526ab60a288aff2250ed8dba92a27b16)]:
+ - @wagmi/core@2.6.18
+
+## 4.1.26
+
+### Patch Changes
+
+- Updated dependencies [[`a97bfbae`](https://github.com/wevm/wagmi/commit/a97bfbaeb615cfef04665e5e7348d85d17f960f0)]:
+ - @wagmi/core@2.6.17
+
+## 4.1.25
+
+### Patch Changes
+
+- [#3788](https://github.com/wevm/wagmi/pull/3788) [`42ad380d`](https://github.com/wevm/wagmi/commit/42ad380d9a5d8bc0f61d73612142dea9d098de5e) Thanks [@tmm](https://github.com/tmm)! - Refactored connectors to remove unnecessarily event listeners.
+
+- Updated dependencies [[`42ad380d`](https://github.com/wevm/wagmi/commit/42ad380d9a5d8bc0f61d73612142dea9d098de5e)]:
+ - @wagmi/core@2.6.16
+
+## 4.1.24
+
+### Patch Changes
+
+- Updated dependencies [[`b907d5ac`](https://github.com/wevm/wagmi/commit/b907d5ac3a746bcbccc06d1fe78c5bd8f9a7d685)]:
+ - @wagmi/core@2.6.15
+
+## 4.1.23
+
+### Patch Changes
+
+- Updated dependencies [[`b3b54ef1`](https://github.com/wevm/wagmi/commit/b3b54ef179c5fa0d1694d38d4b808549a0550409), [`3da20bb8`](https://github.com/wevm/wagmi/commit/3da20bb80e7c3efeef8227ced66ad615370fc242), [`a3d1858f`](https://github.com/wevm/wagmi/commit/a3d1858fce448d2b70e36ee692ef1589b74e9d3f)]:
+ - @wagmi/core@2.6.14
+
+## 4.1.22
+
+### Patch Changes
+
+- Updated dependencies [[`b80236dc`](https://github.com/wevm/wagmi/commit/b80236dc623095fe8f1e1d10957d7776fb6ab48b)]:
+ - @wagmi/core@2.6.13
+
+## 4.1.21
+
+### Patch Changes
+
+- Updated dependencies [[`a59069e9`](https://github.com/wevm/wagmi/commit/a59069e9fab45dd606bb89a7f829fe94c51a5494), [`0acd3132`](https://github.com/wevm/wagmi/commit/0acd31320f534993af566be5490c2978b6184f66)]:
+ - @wagmi/core@2.6.12
+
+## 4.1.20
+
+### Patch Changes
+
+- [`e1ca4e63`](https://github.com/wevm/wagmi/commit/e1ca4e637ae6cec7f5902b0a2c0e0efc3b751a1d) Thanks [@tmm](https://github.com/tmm)! - Deprecated `normalizeChainId`. Use `Number` instead.
+
+- Updated dependencies [[`e1ca4e63`](https://github.com/wevm/wagmi/commit/e1ca4e637ae6cec7f5902b0a2c0e0efc3b751a1d)]:
+ - @wagmi/core@2.6.11
+
+## 4.1.19
+
+### Patch Changes
+
+- Updated dependencies [[`dbdca8fd`](https://github.com/wevm/wagmi/commit/dbdca8fd14b90c166222a66a373c1b33c06ce019)]:
+ - @wagmi/core@2.6.10
+
+## 4.1.18
+
+### Patch Changes
+
+- Updated dependencies [[`d56edf4f`](https://github.com/wevm/wagmi/commit/d56edf4f27c52acc7a0f57114454b0d3e22cacd6)]:
+ - @wagmi/core@2.6.9
+
+## 4.1.17
+
+### Patch Changes
+
+- Updated dependencies [[`e46bcd47`](https://github.com/wevm/wagmi/commit/e46bcd4738a18da15b53f6612b614379c1985374)]:
+ - @wagmi/core@2.6.8
+
+## 4.1.16
+
+### Patch Changes
+
+- [`1c1fee6a`](https://github.com/wevm/wagmi/commit/1c1fee6ab8f01f7734ac6ce05093fa8e388beb3e) Thanks [@jxom](https://github.com/jxom)! - Updated `@walletconnect/ethereum-provider`.
+
+- [#3653](https://github.com/wevm/wagmi/pull/3653) [`88a2d744`](https://github.com/wevm/wagmi/commit/88a2d744a1315908c9e54156026df3ad2435ad44) Thanks [@tash-2s](https://github.com/tash-2s)! - Fixed error occurring when adding chains without explorers to MetaMask.
+
+- Updated dependencies [[`b479b5e8`](https://github.com/wevm/wagmi/commit/b479b5e8a5866cba792862f22e6352c4fb566137), [`f5648dd2`](https://github.com/wevm/wagmi/commit/f5648dd28b3576b628f57732b89287f55acbb1c1), [`1c1fee6a`](https://github.com/wevm/wagmi/commit/1c1fee6ab8f01f7734ac6ce05093fa8e388beb3e), [`88a2d744`](https://github.com/wevm/wagmi/commit/88a2d744a1315908c9e54156026df3ad2435ad44)]:
+ - @wagmi/core@2.6.7
+
+## 4.1.15
+
+### Patch Changes
+
+- Updated dependencies [[`a91c0b64`](https://github.com/wevm/wagmi/commit/a91c0b64ba8b3e6537a560e69724eb601f26af27)]:
+ - @wagmi/core@2.6.6
+
+## 4.1.14
+
+### Patch Changes
+
+- [#3591](https://github.com/wevm/wagmi/pull/3591) [`ca5decdb`](https://github.com/wevm/wagmi/commit/ca5decdb712f81e3f5dab933a94b967bca5b6af4) Thanks [@tmm](https://github.com/tmm)! - Fixed Coinbase Wallet import.
+
+- Updated dependencies [[`c677dcd2`](https://github.com/wevm/wagmi/commit/c677dcd245dccdf69289a3d66dded237b09570a2)]:
+ - @wagmi/core@2.6.5
+
+## 4.1.13
+
+### Patch Changes
+
+- [#3569](https://github.com/wevm/wagmi/pull/3569) [`fa25b448`](https://github.com/wevm/wagmi/commit/fa25b4482504b4d9729a5687ea6d6dc959265bc0) Thanks [@svenvoskamp](https://github.com/svenvoskamp)! - Updated dependencies.
+
+- [#3558](https://github.com/wevm/wagmi/pull/3558) [`895f28e8`](https://github.com/wevm/wagmi/commit/895f28e873af7c8eda5ca85734ff67c8979fd950) Thanks [@tmm](https://github.com/tmm)! - Fixed connector warnings.
+
+- Updated dependencies [[`7c6618e6`](https://github.com/wevm/wagmi/commit/7c6618e6a0eb1ff39cf8f66b34d3ddc14be538fe), [`895f28e8`](https://github.com/wevm/wagmi/commit/895f28e873af7c8eda5ca85734ff67c8979fd950)]:
+ - @wagmi/core@2.6.4
+
+## 4.1.12
+
+### Patch Changes
+
+- Updated dependencies [[`9c3b85dd`](https://github.com/wevm/wagmi/commit/9c3b85dd0a9a4a593e1d7e029345275735330e32), [`2a72214a`](https://github.com/wevm/wagmi/commit/2a72214a2901d6b6ddd39f80238aa0bd4db670a7)]:
+ - @wagmi/core@2.6.3
+
+## 4.1.11
+
+### Patch Changes
+
+- [#3518](https://github.com/wevm/wagmi/pull/3518) [`338e857d`](https://github.com/wevm/wagmi/commit/338e857d8cb2fe85e13d9207bef14cada1c1962d) Thanks [@tmm](https://github.com/tmm)! - Bumped dependencies.
+
+- Updated dependencies [[`414eb048`](https://github.com/wevm/wagmi/commit/414eb048af492caac70c0e874dfc87c30702804a), [`338e857d`](https://github.com/wevm/wagmi/commit/338e857d8cb2fe85e13d9207bef14cada1c1962d)]:
+ - @wagmi/core@2.6.2
+
+## 4.1.10
+
+### Patch Changes
+
+- [#3510](https://github.com/wevm/wagmi/pull/3510) [`660ff80d`](https://github.com/wevm/wagmi/commit/660ff80d5b046967a446eba43ee54b8359a37d0d) Thanks [@tmm](https://github.com/tmm)! - Fixed issue where connectors returning multiple addresses didn't checksum correctly.
+
+- Updated dependencies [[`660ff80d`](https://github.com/wevm/wagmi/commit/660ff80d5b046967a446eba43ee54b8359a37d0d), [`101a7dd1`](https://github.com/wevm/wagmi/commit/101a7dd131b0cae2dc25579ecab9044290efd37b)]:
+ - @wagmi/core@2.6.1
+
+## 4.1.9
+
+### Patch Changes
+
+- [#3496](https://github.com/wevm/wagmi/pull/3496) [`ba7f8a75`](https://github.com/wevm/wagmi/commit/ba7f8a758efb07664c6e401b5e7e325e7c62341b) Thanks [@tmm](https://github.com/tmm)! - Bumped dependencies.
+
+- Updated dependencies [[`ba7f8a75`](https://github.com/wevm/wagmi/commit/ba7f8a758efb07664c6e401b5e7e325e7c62341b)]:
+ - @wagmi/core@2.6.0
+
+## 4.1.8
+
+### Patch Changes
+
+- Updated dependencies [[`ca98041d`](https://github.com/wevm/wagmi/commit/ca98041d1b39893d90246929485f4db0d1c6f9f7)]:
+ - @wagmi/core@2.5.0
+
+## 4.1.7
+
+### Patch Changes
+
+- [#3427](https://github.com/wevm/wagmi/pull/3427) [`370f1b4a`](https://github.com/wevm/wagmi/commit/370f1b4a3f154d181acf381c31c2e7862e22c0e4) Thanks [@marthendalnunes](https://github.com/marthendalnunes)! - Bumped dependencies.
+
+- Updated dependencies [[`370f1b4a`](https://github.com/wevm/wagmi/commit/370f1b4a3f154d181acf381c31c2e7862e22c0e4)]:
+ - @wagmi/core@2.4.0
+
+## 4.1.6
+
+### Patch Changes
+
+- Updated dependencies [[`3be5bb7b`](https://github.com/wevm/wagmi/commit/3be5bb7b0b38646e12e6da5c762ef74dff66bcc2)]:
+ - @wagmi/core@2.3.1
+
+## 4.1.5
+
+### Patch Changes
+
+- [#3459](https://github.com/wevm/wagmi/pull/3459) [`d950b666`](https://github.com/wevm/wagmi/commit/d950b666b56700ca039ce16cdfdf34564991e7f5) Thanks [@marthendalnunes](https://github.com/marthendalnunes)! - Bumped dependencies
+
+- [`1cfb6e5a`](https://github.com/wevm/wagmi/commit/1cfb6e5a875e707abcee00dd5739e87da05e8c90) Thanks [@jxom](https://github.com/jxom)! - Bumped listener limit on WalletConnect connector.
+
+- Updated dependencies [[`d950b666`](https://github.com/wevm/wagmi/commit/d950b666b56700ca039ce16cdfdf34564991e7f5), [`90ef39bb`](https://github.com/wevm/wagmi/commit/90ef39bb0f4ecb3c914d317875348e35ba0f4524), [`1cfb6e5a`](https://github.com/wevm/wagmi/commit/1cfb6e5a875e707abcee00dd5739e87da05e8c90)]:
+ - @wagmi/core@2.3.0
+
+## 4.1.4
+
+### Patch Changes
+
+- [#3443](https://github.com/wevm/wagmi/pull/3443) [`007024a6`](https://github.com/wevm/wagmi/commit/007024a684ddbecf924cdc06dd6a8854fc3d5eeb) Thanks [@jmrossy](https://github.com/jmrossy)! - Bumped dependencies.
+
+- [#3447](https://github.com/wevm/wagmi/pull/3447) [`a02a26ad`](https://github.com/wevm/wagmi/commit/a02a26ad030d3afb78f744377d61b5c60b65d97a) Thanks [@tmm](https://github.com/tmm)! - Bumped dependencies.
+
+- Updated dependencies [[`a02a26ad`](https://github.com/wevm/wagmi/commit/a02a26ad030d3afb78f744377d61b5c60b65d97a), [`007024a6`](https://github.com/wevm/wagmi/commit/007024a684ddbecf924cdc06dd6a8854fc3d5eeb)]:
+ - @wagmi/core@2.2.1
+
+## 4.1.3
+
+### Patch Changes
+
+- Updated dependencies [[`00bf10a4`](https://github.com/wevm/wagmi/commit/00bf10a428b0d1c5dac35ebf25b19571e033ac26), [`64c073f6`](https://github.com/wevm/wagmi/commit/64c073f6c2720961e2d6aff986670b73dbfab9c3), [`fb6c4148`](https://github.com/wevm/wagmi/commit/fb6c4148d9e9e2fccfbe74c8f343b444dc68dec5)]:
+ - @wagmi/core@2.2.0
+
+## 4.1.2
+
+### Patch Changes
+
+- Updated dependencies [[`e00b8205`](https://github.com/wevm/wagmi/commit/e00b82058685751637edfa9a6b2d196a12549fe7)]:
+ - @wagmi/core@2.1.2
+
+## 4.1.1
+
+### Patch Changes
+
+- [`ec0d8b41`](https://github.com/wevm/wagmi/commit/ec0d8b4112181fefb11025e436a94a6114761d37) Thanks [@tmm](https://github.com/tmm)! - Added note to `metaMask` connector.
+
+- Updated dependencies [[`64b82282`](https://github.com/wevm/wagmi/commit/64b82282c1e57e77c25aa0814673780e4d11edd4), [`ec0d8b41`](https://github.com/wevm/wagmi/commit/ec0d8b4112181fefb11025e436a94a6114761d37)]:
+ - @wagmi/core@2.1.1
+
+## 4.1.0
+
+### Minor Changes
+
+- Updated dependencies [[`c9cd302e`](https://github.com/wevm/wagmi/commit/c9cd302e1c65c980deaee2e12567c2a8ec08b399)]:
+ - @wagmi/core@2.1.0
+
+## 4.0.2
+
+### Patch Changes
+
+- [#3384](https://github.com/wevm/wagmi/pull/3384) [`ee868c33`](https://github.com/wevm/wagmi/commit/ee868c3385dae511230b6ddcb5627c1293cc1844) Thanks [@tmm](https://github.com/tmm)! - Fixed connectors not bubbling error when connecting with `chainId` and subsequent user rejection.
+
+- Updated dependencies [[`ee868c33`](https://github.com/wevm/wagmi/commit/ee868c3385dae511230b6ddcb5627c1293cc1844)]:
+ - @wagmi/core@2.0.2
+
+## 4.0.1
+
+### Major Changes
+
+- [#3333](https://github.com/wevm/wagmi/pull/3333) [`b3a0baaa`](https://github.com/wevm/wagmi/commit/b3a0baaaee7decf750d376aab2502cd33ca4825a) Thanks [@tmm](https://github.com/tmm)! - Added support for Wagmi 2.0.
+
+### Patch Changes
+
+- Updated dependencies [[`b3a0baaa`](https://github.com/wevm/wagmi/commit/b3a0baaaee7decf750d376aab2502cd33ca4825a)]:
+ - @wagmi/core@2.0.0
+
+## 3.1.11
+
+### Patch Changes
+
+- [#3361](https://github.com/wevm/wagmi/pull/3361) [`bbbbf587`](https://github.com/wevm/wagmi/commit/bbbbf587e41bae12b072b7a7c897d580fc07cd2b) Thanks [@0xAsimetriq](https://github.com/0xAsimetriq)! - Updated WalletConnect connector dependencies
+
+## 3.1.10
+
+### Patch Changes
+
+- [`53ca1f7e`](https://github.com/wevm/wagmi/commit/53ca1f7eb411d912e11fcce7e03bd61ed067959c) Thanks [@tmm](https://github.com/tmm)! - Removed LedgerConnector due to security vulnerability
+
+## 3.1.9
+
+### Patch Changes
+
+- [#3114](https://github.com/wevm/wagmi/pull/3114) [`51eca0fb`](https://github.com/wevm/wagmi/commit/51eca0fbaea6932f31a5b8b4213f0252280053e2) Thanks [@akathecoder](https://github.com/akathecoder)! - Added Okto Wallet to Injected Wallets Connector
+
+- [#3299](https://github.com/wevm/wagmi/pull/3299) [`b02020b3`](https://github.com/wevm/wagmi/commit/b02020b3724e0228198f35817611bb063295906e) Thanks [@dasanra](https://github.com/dasanra)! - Fixed issue with [Safe SDK](https://github.com/wevm/viem/issues/579) by bumping `@safe-global/safe-apps-provider@0.18.1`
+
+## 3.1.8
+
+### Patch Changes
+
+- [#3197](https://github.com/wevm/wagmi/pull/3197) [`e8f7bcbc`](https://github.com/wevm/wagmi/commit/e8f7bcbcd9c038a901c29e71769682c088efe2ac) Thanks [@ByteZhang1024](https://github.com/ByteZhang1024)! - Added OneKey Wallet to injected connector flags.
+
+## 3.1.7
+
+### Patch Changes
+
+- [#3276](https://github.com/wevm/wagmi/pull/3276) [`83223a06`](https://github.com/wevm/wagmi/commit/83223a0659e2f675d897a1d3374c7af752c16abf) Thanks [@glitch-txs](https://github.com/glitch-txs)! - Removed required namespaces from WalletConnect connector
+
+## 3.1.6
+
+### Patch Changes
+
+- [#3236](https://github.com/wevm/wagmi/pull/3236) [`cc7e18f2`](https://github.com/wevm/wagmi/commit/cc7e18f2e7f6b8b989f60f0b05aee70e996a9975) Thanks [@0xAsimetriq](https://github.com/0xAsimetriq)! - Updated @walletconnect/ethereum-provider
+
+- [#3236](https://github.com/wevm/wagmi/pull/3236) [`cc7e18f2`](https://github.com/wevm/wagmi/commit/cc7e18f2e7f6b8b989f60f0b05aee70e996a9975) Thanks [@0xAsimetriq](https://github.com/0xAsimetriq)! - Updated @walletconnect/ethereum-provider
+
+## 3.1.5
+
+### Patch Changes
+
+- [#3220](https://github.com/wagmi-dev/wagmi/pull/3220) [`a1950449`](https://github.com/wagmi-dev/wagmi/commit/a1950449127ddf72fff8ecd1fc34c3690befbb05) Thanks [@rkalis](https://github.com/rkalis)! - Fixed a bug where injected walets with an empty providers array could not connect
+
+## 3.1.4
+
+### Patch Changes
+
+- [#3115](https://github.com/wagmi-dev/wagmi/pull/3115) [`4e6ec415`](https://github.com/wagmi-dev/wagmi/commit/4e6ec4151baece94e940e227e0e3711c7f8534d9) Thanks [@bifot](https://github.com/bifot)! - Added SafePal injected name mapping.
+
+## 3.1.3
+
+### Patch Changes
+
+- [#3141](https://github.com/wagmi-dev/wagmi/pull/3141) [`e78aa337`](https://github.com/wagmi-dev/wagmi/commit/e78aa337c454f04b41a3cbd381d25270dd4a0afd) Thanks [@einaralex](https://github.com/einaralex)! - Updated WalletConnect libraries.
+
+## 3.1.2
+
+### Patch Changes
+
+- [#3009](https://github.com/wagmi-dev/wagmi/pull/3009) [`3aaba328`](https://github.com/wagmi-dev/wagmi/commit/3aaba32808ddb4035ec885f96992c91078056715) Thanks [@0xAsimetriq](https://github.com/0xAsimetriq)! - Update WalletConnect dependencies
+
+## 3.1.1
+
+### Patch Changes
+
+- [#2973](https://github.com/wevm/wagmi/pull/2973) [`bf831bb3`](https://github.com/wevm/wagmi/commit/bf831bb30df8037cc4312342d0fe3c045408c2fe) Thanks [@masm](https://github.com/masm)! - Added Zeal wallet
+
+## 3.1.0
+
+### Minor Changes
+
+- [#2956](https://github.com/wevm/wagmi/pull/2956) [`2abeb285`](https://github.com/wevm/wagmi/commit/2abeb285674af3e539cc2550b1f5027b1eb0c895) Thanks [@tmm](https://github.com/tmm)! - Replaced `@wagmi/chains` with `viem/chains`.
+
+## 3.0.0
+
+### Patch Changes
+
+- 0306383: Updated WalletConnect dependencies
+- Updated dependencies [d1ef9b4]
+- Updated dependencies [484c846]
+ - @wagmi/chains@1.8.0
+
+## 2.7.0
+
+### Minor Changes
+
+- a270cb9: Updated WalletConnect dependencies.
+
+### Patch Changes
+
+- 06cc1b4: Add SubWallet injected flags
+- 131a337: Added Desig Wallet name mapping.
+- e089d7d: Added Fordefi Wallet name mapping.
+- ce84d0a: Added Coin98 Wallet injected flags.
+- Updated dependencies [8fdacd8]
+- Updated dependencies [2e9283a]
+- Updated dependencies [a432a2b]
+- Updated dependencies [408740a]
+- Updated dependencies [6794a61]
+- Updated dependencies [0c5a32b]
+- Updated dependencies [ebc85ec]
+- Updated dependencies [5683df2]
+- Updated dependencies [414ff36]
+- Updated dependencies [4f514c6]
+- Updated dependencies [1cf72bc]
+- Updated dependencies [cd68471]
+- Updated dependencies [baf3143]
+- Updated dependencies [9737f24]
+- Updated dependencies [7797238]
+- Updated dependencies [3846811]
+- Updated dependencies [0ea344c]
+ - @wagmi/chains@1.7.0
+
+## 2.6.6
+
+### Patch Changes
+
+- 56c127d: Updated WalletConnect dependencies.
+- Updated dependencies [4b411d2]
+- Updated dependencies [df697ac]
+- Updated dependencies [186f5a7]
+- Updated dependencies [a96b514]
+- Updated dependencies [0a6e6da]
+ - @wagmi/chains@1.5.0
+
+## 2.6.5
+
+### Patch Changes
+
+- 51e346e: Updated WalletConnectConnector logic to handle individual namespaces like eip155:\*
+
+## 2.6.4
+
+### Patch Changes
+
+- 0a57de2: Added conditional for WalletConnectConnector optionalChains
+
+## 2.6.3
+
+### Patch Changes
+
+- f2d532d: Updated WalletConnect dependencies, exposed `relayUrl` option for `WalletConnectConnector`
+- ff53857: Fixed issue importing `EthereumProvider` in Vite environments.
+- Updated dependencies [d642e1d]
+- Updated dependencies [3027d7b]
+- Updated dependencies [97dbd44]
+ - @wagmi/chains@1.4.0
+
+## 2.6.2
+
+### Patch Changes
+
+- 27bb1b3: Added explicit type annotations for the `getWalletClient()` method.
+
+## 2.6.1
+
+### Patch Changes
+
+- a3507a9: Updated @walletconnect/ethereum-provider dependency
+
+## 2.6.0
+
+### Minor Changes
+
+- 32dc317: Updated @walletconnect/ethereum-provider and @walletconnect/modal dependencies
+
+## 2.5.0
+
+### Minor Changes
+
+- 57e674e: Updated `@safe-global/safe-apps-sdk` & `@safe-global/safe-apps-provider`
+
+## 2.4.0
+
+### Patch Changes
+
+- f21c8e0: Added WalletConnect v2 support to Ledger connector.
+- 27482bb: Add HAQQ Wallet detection
+- 7d6aa43: Exported `normalizeChainId`.
+- Updated dependencies [62b8209]
+- Updated dependencies [106ac13]
+- Updated dependencies [8b3f5e5]
+ - @wagmi/chains@1.3.0
+
+## 2.3.0
+
+### Minor Changes
+
+- 28219ae: Added metadata property to WalletConnect init function
+- 6fef949: Updated @walletconnect/modal and @walletconnect/ethereum-provider deps
+
+### Patch Changes
+
+- 72f6465: Added `TTWallet` to `getInjectedName` list
+- Updated dependencies [a7cbd04]
+- Updated dependencies [f6ee133]
+ - @wagmi/chains@1.2.0
+
+## 2.2.0
+
+### Minor Changes
+
+- 6c841d4: Changed `Address` type import from ABIType to viem.
+
+### Patch Changes
+
+- 09c83f8: Update @walletconnect/ethereum-provider, Replace @web3modal/standalone with @walletconnect/modal, Fix issue with wallet_addEthereumChain method in WalletConnectConnector
+
+## 2.1.1
+
+### Patch Changes
+
+- c24de75: Updated `@walletconnect/ethereum-provider` and `@web3modal/standalone` dependencies.
+- 605c422: Bumped `viem` peer dependency.
+- dc1c546: Throw ResourceUnavailableError on -30002 errors.
+
+## 2.1.0
+
+### Minor Changes
+
+- b001569: Bumped minimum TypeScript version to v5.0.4.
+
+### Patch Changes
+
+- 0f05b2b: Updated `abitype` to `0.8.7`.
+- 6aea7ee: Fixed internal types.
+- b187cb0: Added `isNovaWallet` injected flag.
+- 5e44429: Added Edgeware mainnet and testnet
+- b18b314: Updated @walletconnect/ethereum-provider and @web3modal/standalone dependencies
+- Updated dependencies [b62a199]
+- Updated dependencies [b001569]
+- Updated dependencies [260ab59]
+- Updated dependencies [6aea7ee]
+- Updated dependencies [5e44429]
+ - @wagmi/chains@1.0.0
+
+## 2.0.0
+
+### Patch Changes
+
+- Updated dependencies [36c14b2]
+ - @wagmi/chains@0.3.0
+
+## 1.0.5
+
+### Patch Changes
+
+- fa61dfe: Updated viem.
+- Updated dependencies [577d2a0]
+ - @wagmi/chains@0.2.25
+
+## 1.0.4
+
+### Patch Changes
+
+- bbbd11b: Corrected Rabby Wallet name
+- Updated dependencies [0639a1f]
+ - @wagmi/chains@0.2.24
+
+## 1.0.3
+
+### Patch Changes
+
+- 64dfe61: Update @web3modal/standalone to v2.4.1, Update @walletconnect/ethereum-provider to 2.7.4
+- bab7ad8: Added Defiant to injected connector flags
+- 44cde07: Added Talisman wallet flag
+
+## 1.0.2
+
+### Patch Changes
+
+- bce5a0c: Removed chain fallback when instantiating a Wallet Client.
+
+## 1.0.1
+
+### Patch Changes
+
+- [`ea651cd7`](https://github.com/wevm/wagmi/commit/ea651cd7fc75b7866272605467db11fd6e1d81af) Thanks [@jxom](https://github.com/jxom)! - Downgraded abitype.
+
+## 1.0.0
+
+### Major Changes
+
+- 7e274f5: Released v1.
+
+### Patch Changes
+
+- 0966bf7: Changed Kucoin Wallet name mapping to Halo Wallet
+
+## 1.0.0-next.5
+
+### Major Changes
+
+- Updated references.
+
+## 1.0.0-next.4
+
+### Major Changes
+
+- Updated references.
+
+## 1.0.0-next.3
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @wagmi/chains@1.0.0-next.0
+
+## 1.0.0-next.2
+
+### Major Changes
+
+- updated viem
+
+## 1.0.0-next.1
+
+### Major Changes
+
+- [`a7dda00c`](https://github.com/wevm/wagmi/commit/a7dda00c5b546f8b2c42b527e4d9ac1b9e9ab1fb) Thanks [@jxom](https://github.com/jxom)! - Released v1.
+
+## 1.0.0-next.0
+
+### Major Changes
+
+- 33488cf: Released v1.
+
+## 0.3.19
+
+### Patch Changes
+
+- 274eef3: - Updated @web3modal/standalone to 2.3.7
+ - Updated @walletconnect/ethereum-provider to 2.7.1
+- 41697df: Updated @walletconnect/ethereum-provider version to 2.7.2
+- 82dcb72: Added Enkrypt extension detection
+
+## 0.3.18
+
+### Patch Changes
+
+- f66e065: Added BlockWallet to injected connector flags.
+
+## 0.3.17
+
+### Patch Changes
+
+- 12ab5d1: Updated @coinbase/wallet-sdk to 3.6.6
+
+## 0.3.16
+
+### Patch Changes
+
+- c1e3ddf: Reverted ABIType version change.
+
+## 0.3.15
+
+### Patch Changes
+
+- d4825e6: Fixed ABIType version to match downstream packages.
+
+## 0.3.14
+
+### Patch Changes
+
+- c25ac82: Added more flags to `MetaMaskConnector` `getProvider` check.
+- b19a932: Updated @web3modal/standalone to 2.3.0, @walletconnect/ethereum-provider to 2.7.0
+- cdc387e: Added `ImToken` to `getInjectedName` list
+
+## 0.3.13
+
+### Patch Changes
+
+- 2a21d27: Updated `@coinbase/wallet-sdk` to `3.6.4`
+
+## 0.3.12
+
+### Patch Changes
+
+- 9bb22b6: Updated `@walletconnect/ethereum-provider` to `2.6.2`, relaxed `@web3modal/standalone` version requirement
+- 0d7625b: Added Rabby to injected connector flags
+- f63d7fd: Added correct error to switch network cause.
+
+## 0.3.11
+
+### Patch Changes
+
+- 0778abc: Renamed `isTally` injected provider to `Taho`
+
+## 0.3.10
+
+### Patch Changes
+
+- 4267020: Added `qrModalOptions` option to `WalletConnectConnector`
+- e78fb0a: Pinned WalletConnect dependencies
+
+## 0.3.9
+
+### Patch Changes
+
+- 5cd0afc: Added `isZerion` to `InjectedProviderFlags` and `getInjectedName`
+- be4825e: Added GameStop Wallet to injected connector flags
+
+## 0.3.8
+
+### Patch Changes
+
+- 11f3fe2: Fixed issue where `UNSTABLE_shimOnConnectSelectAccount` would not bubble up error for MetaMask if request to connect was already active.
+
+## 0.3.7
+
+### Patch Changes
+
+- 04c0e47: Fixed issue switching chain after adding to MetaMask.
+
+## 0.3.6
+
+### Patch Changes
+
+- 85330c1: Removed `InjectedConnector` `shimChainChangedDisconnect` shim (no longer necessary).
+
+## 0.3.5
+
+### Patch Changes
+
+- 8b1a526: Added Dawn wallet flag
+
+## 0.3.4
+
+### Patch Changes
+
+- 6b15d6f: Updated `@walletconnect/ethereum-provider` to `2.5.1`.
+- 1f452e7: Added OKX Wallet to injected connector flags.
+- a4d9083: Added Backpack wallet to injected connector flags.
+- 6a4af48: Enabled support for programmatic chain switching on `LedgerConnector` & added `"ledger"` to the switch chain regex on `WalletConnectLegacyConnector`.
+
+## 0.3.3
+
+### Patch Changes
+
+- f24ce0c: Updated @walletconnect/ethereum-provider to 2.4.8
+- e3a3fee: Added "uniswap wallet" to the regex that determines wallets allowed to switch chains in the WalletConnect legacy connector
+- 641af48: Added name mapping for Bifrost Wallet
+- 4d2c90a: Added name mapping for Phantom
+- 3d276d0: Added Status as the name of the injected connector for the Status App
+
+## 0.3.2
+
+### Patch Changes
+
+- 13a6a07: Updated `@walletconnect/ethereum-provider` to `2.4.7`.
+
+## 0.3.1
+
+### Patch Changes
+
+- a23c40f: Added name mapping for [Frontier](https://frontier.xyz) Wallet
+- d779fb3: Added name mapping for HyperPay.
+
+## 0.3.0
+
+### Minor Changes
+
+- c4d5bb5: **Breaking:** Removed the `version` config option for `WalletConnectConnector`.
+
+ `WalletConnectConnector` now uses WalletConnect v2 by default. WalletConnect v1 is now `WalletConnectLegacyConnector`.
+
+ ### WalletConnect v2
+
+ ```diff
+ import { WalletConnectConnector } from '@wagmi/connectors/walletConnect'
+
+ const connector = new WalletConnectConnector({
+ options: {
+ - version: '2',
+ projectId: 'abc',
+ },
+ })
+ ```
+
+ ### WalletConnect v1
+
+ ```diff
+ -import { WalletConnectConnector } from '@wagmi/connectors/walletConnect'
+ +import { WalletConnectLegacyConnector } from '@wagmi/connectors/walletConnectLegacy'
+
+ -const connector = new WalletConnectConnector({
+ +const connector = new WalletConnectLegacyConnector({
+ options: {
+ qrcode: true,
+ },
+ })
+ ```
+
+## 0.2.7
+
+### Patch Changes
+
+- 57f1226: Added name mapping for XDEFI
+
+## 0.2.6
+
+### Patch Changes
+
+- bb1b88c: Added name mapping for Bitski injected wallet
+- fcb5595: Fixed shim disconnect key to read from defined Connector ID.
+- 49f8853: Fixed `SafeConnector` import type error that existed for specific build environments.
+
+## 0.2.5
+
+### Patch Changes
+
+- 5d121f2: Added `isApexWallet` to injected `window.ethereum` flags.
+- e3566eb: Updated `@web3modal/standalone` to `2.1.1` for WalletConnectConnector.
+
+## 0.2.4
+
+### Patch Changes
+
+- a4f31bc: Added Connector for [Safe](https://safe.global) wallet
+- d5e25d9: Locked ethers peer dependency version to >=5.5.1 <6
+
+## 0.2.3
+
+### Patch Changes
+
+- 6fa74dd: Updated `@walletconnect/universal-provider`
+ Added more signable methods to WC v2.
+
+## 0.2.2
+
+### Patch Changes
+
+- 6b0725b: Fixed race condition between `switchNetwork` and mutation Hooks that use `chainId` (e.g. `sendTransaction`).
+
+## 0.2.1
+
+### Patch Changes
+
+- 942fcde: Updated `@walletconnect/universal-provider` and `@web3modal/standalone` packages for WalletConnectConnector (v2).
+
+ Improved initialization flow for `@walletconnect/universal-provider` for WalletConnectConnector (v2).
+
+## 0.2.0
+
+### Minor Changes
+
+- be33c7d: Chains are now narrowed to their most specific type using the TypeScript [`satisfies`](https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#the-satisfies-operator) operator.
+
+## 0.1.10
+
+### Patch Changes
+
+- d75e8d2: Fixed ABIType version mismatch between packages.
+
+## 0.1.9
+
+### Patch Changes
+
+- 8c3fc00: Added public RPC URL to Connector fallback chains
+
+## 0.1.8
+
+### Patch Changes
+
+- 5e6dc30: Replaced legacy qrcodemodal with web3modal for WalletConnect v2.
+
+## 0.1.7
+
+### Patch Changes
+
+- be4add2: Added `isRainbow` flag to `InjectedConnector`.
+
+## 0.1.6
+
+### Patch Changes
+
+- 3dfc558: Add `switchSigner` method to `MockProvider`.
+
+## 0.1.5
+
+### Patch Changes
+
+- 7dce4b5: Bumped WalletConnect Universal Provider version.
+
+## 0.1.4
+
+### Patch Changes
+
+- 4cec598: Added CJS escape hatch bundle under the "cjs" tag.
+
+## 0.1.3
+
+### Patch Changes
+
+- 822bc88: The `WalletConnectConnector` now supports WalletConnect v2.
+
+ It can be enabled by setting `version` to `'2'` and supplying a [WalletConnect Cloud `projectId`](https://cloud.walletconnect.com/sign-in).
+
+## 0.1.2
+
+### Patch Changes
+
+- 5e5f37f: Fixed issue where connecting to MetaMask may return with a stale address
+
+## 0.1.1
+
+### Patch Changes
+
+- 919790c: Updated `@ledgerhq/connect-kit-loader` to `1.0.1`
+
+## 0.1.0
+
+### Minor Changes
+
+- 5db7cba: Added `LedgerConnector`
+- 55a0ca2: Initial release of the `@wagmi/connectors` package – a collection of Connectors for wagmi.
diff --git a/packages/connectors/README.md b/packages/connectors/README.md
new file mode 100644
index 0000000000..05418ec15c
--- /dev/null
+++ b/packages/connectors/README.md
@@ -0,0 +1,13 @@
+# @wagmi/connectors
+
+Collection of connectors for Wagmi
+
+## Installation
+
+```bash
+pnpm add @wagmi/connectors @wagmi/core viem
+```
+
+## Documentation
+
+For documentation and guides, visit [wagmi.sh](https://wagmi.sh).
diff --git a/packages/connectors/package.json b/packages/connectors/package.json
new file mode 100644
index 0000000000..5ddcfaa107
--- /dev/null
+++ b/packages/connectors/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@wagmi/connectors",
+ "description": "Collection of connectors for Wagmi",
+ "version": "5.8.3",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/wevm/wagmi.git",
+ "directory": "packages/connectors"
+ },
+ "scripts": {
+ "build": "pnpm run clean && pnpm run build:esm+types",
+ "build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
+ "check:types": "tsc --noEmit",
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
+ "test:build": "publint --strict && attw --pack --ignore-rules cjs-resolves-to-esm"
+ },
+ "files": [
+ "dist/**",
+ "!dist/**/*.tsbuildinfo",
+ "src/**/*.ts",
+ "!src/**/*.test.ts",
+ "!src/**/*.test-d.ts"
+ ],
+ "sideEffects": false,
+ "type": "module",
+ "main": "./dist/esm/exports/index.js",
+ "types": "./dist/types/exports/index.d.ts",
+ "typings": "./dist/types/exports/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/exports/index.d.ts",
+ "default": "./dist/esm/exports/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "peerDependencies": {
+ "@wagmi/core": "workspace:*",
+ "typescript": ">=5.0.4",
+ "viem": "2.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ },
+ "dependencies": {
+ "@coinbase/wallet-sdk": "4.3.0",
+ "@metamask/sdk": "0.33.1",
+ "@safe-global/safe-apps-provider": "0.18.6",
+ "@safe-global/safe-apps-sdk": "9.1.0",
+ "@walletconnect/ethereum-provider": "2.20.2",
+ "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3"
+ },
+ "devDependencies": {
+ "@wagmi/core": "workspace:*",
+ "msw": "^2.4.9"
+ },
+ "contributors": ["awkweb.eth ", "jxom.eth "],
+ "funding": "https://github.com/sponsors/wevm",
+ "keywords": [
+ "react",
+ "hooks",
+ "eth",
+ "ethereum",
+ "dapps",
+ "wallet",
+ "web3",
+ "abi"
+ ]
+}
diff --git a/packages/connectors/src/coinbaseWallet.test.ts b/packages/connectors/src/coinbaseWallet.test.ts
new file mode 100644
index 0000000000..99b141e49b
--- /dev/null
+++ b/packages/connectors/src/coinbaseWallet.test.ts
@@ -0,0 +1,17 @@
+import { config } from '@wagmi/test'
+import { expect, expectTypeOf, test } from 'vitest'
+
+import { coinbaseWallet } from './coinbaseWallet.js'
+
+test('setup', () => {
+ const connectorFn = coinbaseWallet({ appName: 'wagmi', version: '4' })
+ const connector = config._internal.connectors.setup(connectorFn)
+ expect(connector.name).toEqual('Coinbase Wallet')
+
+ type ConnectFnParameters = NonNullable<
+ Parameters<(typeof connector)['connect']>[0]
+ >
+ expectTypeOf().toMatchTypeOf<
+ boolean | undefined
+ >()
+})
diff --git a/packages/connectors/src/coinbaseWallet.ts b/packages/connectors/src/coinbaseWallet.ts
new file mode 100644
index 0000000000..630c91261e
--- /dev/null
+++ b/packages/connectors/src/coinbaseWallet.ts
@@ -0,0 +1,546 @@
+import type {
+ Preference,
+ ProviderInterface,
+ createCoinbaseWalletSDK,
+} from '@coinbase/wallet-sdk'
+import {
+ ChainNotConfiguredError,
+ type Connector,
+ createConnector,
+} from '@wagmi/core'
+import type { Compute, Mutable, Omit } from '@wagmi/core/internal'
+import type {
+ CoinbaseWalletProvider as CBW_Provider,
+ CoinbaseWalletSDK as CBW_SDK,
+} from 'cbw-sdk'
+import {
+ type AddEthereumChainParameter,
+ type Address,
+ type Hex,
+ type ProviderRpcError,
+ SwitchChainError,
+ UserRejectedRequestError,
+ getAddress,
+ numberToHex,
+} from 'viem'
+
+type Version = '3' | '4'
+
+export type CoinbaseWalletParameters =
+ version extends '4'
+ ? Compute<
+ {
+ headlessMode?: false | undefined
+ /** Coinbase Wallet SDK version */
+ version?: version | '3' | undefined
+ } & Version4Parameters
+ >
+ : Compute<
+ {
+ /**
+ * @deprecated `headlessMode` will be removed in the next major version. Upgrade to `version: '4'`.
+ */
+ headlessMode?: true | undefined
+ /**
+ * Coinbase Wallet SDK version
+ * @deprecated Version 3 will be removed in the next major version. Upgrade to `version: '4'`.
+ * @default '4'
+ */
+ version?: version | '4' | undefined
+ } & Version3Parameters
+ >
+
+coinbaseWallet.type = 'coinbaseWallet' as const
+export function coinbaseWallet(
+ parameters: CoinbaseWalletParameters = {} as any,
+): version extends '4'
+ ? ReturnType
+ : ReturnType {
+ if (parameters.version === '3' || parameters.headlessMode)
+ return version3(parameters as Version3Parameters) as any
+ return version4(parameters as Version4Parameters) as any
+}
+
+type Version4Parameters = Mutable<
+ Omit<
+ Parameters[0],
+ | 'appChainIds' // set via wagmi config
+ | 'preference'
+ > & {
+ // TODO(v3): Remove `Preference['options']`
+ /**
+ * Preference for the type of wallet to display.
+ * @default 'all'
+ */
+ preference?: Preference['options'] | Compute | undefined
+ }
+>
+
+function version4(parameters: Version4Parameters) {
+ type Provider = ProviderInterface & {
+ // for backwards compatibility
+ close?(): void
+ }
+ type Properties = {
+ connect(parameters?: {
+ chainId?: number | undefined
+ instantOnboarding?: boolean | undefined
+ isReconnecting?: boolean | undefined
+ }): Promise<{
+ accounts: readonly Address[]
+ chainId: number
+ }>
+ }
+
+ let walletProvider: Provider | undefined
+
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
+ let chainChanged: Connector['onChainChanged'] | undefined
+ let disconnect: Connector['onDisconnect'] | undefined
+
+ return createConnector((config) => ({
+ id: 'coinbaseWalletSDK',
+ name: 'Coinbase Wallet',
+ rdns: 'com.coinbase.wallet',
+ type: coinbaseWallet.type,
+ async connect({ chainId, ...rest } = {}) {
+ try {
+ const provider = await this.getProvider()
+ const accounts = (
+ (await provider.request({
+ method: 'eth_requestAccounts',
+ params:
+ 'instantOnboarding' in rest && rest.instantOnboarding
+ ? [{ onboarding: 'instant' }]
+ : [],
+ })) as string[]
+ ).map((x) => getAddress(x))
+
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged)
+ }
+ if (!chainChanged) {
+ chainChanged = this.onChainChanged.bind(this)
+ provider.on('chainChanged', chainChanged)
+ }
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect)
+ }
+
+ // Switch to chain if provided
+ let currentChainId = await this.getChainId()
+ if (chainId && currentChainId !== chainId) {
+ const chain = await this.switchChain!({ chainId }).catch((error) => {
+ if (error.code === UserRejectedRequestError.code) throw error
+ return { id: currentChainId }
+ })
+ currentChainId = chain?.id ?? currentChainId
+ }
+
+ return { accounts, chainId: currentChainId }
+ } catch (error) {
+ if (
+ /(user closed modal|accounts received is empty|user denied account|request rejected)/i.test(
+ (error as Error).message,
+ )
+ )
+ throw new UserRejectedRequestError(error as Error)
+ throw error
+ }
+ },
+ async disconnect() {
+ const provider = await this.getProvider()
+
+ if (accountsChanged) {
+ provider.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+
+ provider.disconnect()
+ provider.close?.()
+ },
+ async getAccounts() {
+ const provider = await this.getProvider()
+ return (
+ (await provider.request({
+ method: 'eth_accounts',
+ })) as string[]
+ ).map((x) => getAddress(x))
+ },
+ async getChainId() {
+ const provider = await this.getProvider()
+ const chainId = (await provider.request({
+ method: 'eth_chainId',
+ })) as Hex
+ return Number(chainId)
+ },
+ async getProvider() {
+ if (!walletProvider) {
+ const preference = (() => {
+ if (typeof parameters.preference === 'string')
+ return { options: parameters.preference }
+ return {
+ ...parameters.preference,
+ options: parameters.preference?.options ?? 'all',
+ }
+ })()
+
+ const { createCoinbaseWalletSDK } = await import('@coinbase/wallet-sdk')
+ const sdk = createCoinbaseWalletSDK({
+ ...parameters,
+ appChainIds: config.chains.map((x) => x.id),
+ preference,
+ })
+
+ walletProvider = sdk.getProvider()
+ }
+
+ return walletProvider
+ },
+ async isAuthorized() {
+ try {
+ const accounts = await this.getAccounts()
+ return !!accounts.length
+ } catch {
+ return false
+ }
+ },
+ async switchChain({ addEthereumChainParameter, chainId }) {
+ const chain = config.chains.find((chain) => chain.id === chainId)
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
+
+ const provider = await this.getProvider()
+
+ try {
+ await provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: numberToHex(chain.id) }],
+ })
+ return chain
+ } catch (error) {
+ // Indicates chain is not added to provider
+ if ((error as ProviderRpcError).code === 4902) {
+ try {
+ let blockExplorerUrls: string[] | undefined
+ if (addEthereumChainParameter?.blockExplorerUrls)
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
+ else
+ blockExplorerUrls = chain.blockExplorers?.default.url
+ ? [chain.blockExplorers?.default.url]
+ : []
+
+ let rpcUrls: readonly string[]
+ if (addEthereumChainParameter?.rpcUrls?.length)
+ rpcUrls = addEthereumChainParameter.rpcUrls
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
+
+ const addEthereumChain = {
+ blockExplorerUrls,
+ chainId: numberToHex(chainId),
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
+ iconUrls: addEthereumChainParameter?.iconUrls,
+ nativeCurrency:
+ addEthereumChainParameter?.nativeCurrency ??
+ chain.nativeCurrency,
+ rpcUrls,
+ } satisfies AddEthereumChainParameter
+
+ await provider.request({
+ method: 'wallet_addEthereumChain',
+ params: [addEthereumChain],
+ })
+
+ return chain
+ } catch (error) {
+ throw new UserRejectedRequestError(error as Error)
+ }
+ }
+
+ throw new SwitchChainError(error as Error)
+ }
+ },
+ onAccountsChanged(accounts) {
+ if (accounts.length === 0) this.onDisconnect()
+ else
+ config.emitter.emit('change', {
+ accounts: accounts.map((x) => getAddress(x)),
+ })
+ },
+ onChainChanged(chain) {
+ const chainId = Number(chain)
+ config.emitter.emit('change', { chainId })
+ },
+ async onDisconnect(_error) {
+ config.emitter.emit('disconnect')
+
+ const provider = await this.getProvider()
+ if (accountsChanged) {
+ provider.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ },
+ }))
+}
+
+type Version3Parameters = Mutable<
+ Omit<
+ ConstructorParameters[0],
+ 'reloadOnDisconnect' // remove property since TSDoc says default is `true`
+ >
+> & {
+ /**
+ * Fallback Ethereum JSON RPC URL
+ * @default ""
+ */
+ jsonRpcUrl?: string | undefined
+ /**
+ * Fallback Ethereum Chain ID
+ * @default 1
+ */
+ chainId?: number | undefined
+ /**
+ * Whether or not to reload dapp automatically after disconnect.
+ * @default false
+ */
+ reloadOnDisconnect?: boolean | undefined
+}
+
+function version3(parameters: Version3Parameters) {
+ const reloadOnDisconnect = false
+
+ type Provider = CBW_Provider
+
+ let sdk: CBW_SDK | undefined
+ let walletProvider: Provider | undefined
+
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
+ let chainChanged: Connector['onChainChanged'] | undefined
+ let disconnect: Connector['onDisconnect'] | undefined
+
+ return createConnector((config) => ({
+ id: 'coinbaseWalletSDK',
+ name: 'Coinbase Wallet',
+ rdns: 'com.coinbase.wallet',
+ type: coinbaseWallet.type,
+ async connect({ chainId } = {}) {
+ try {
+ const provider = await this.getProvider()
+ const accounts = (
+ (await provider.request({
+ method: 'eth_requestAccounts',
+ })) as string[]
+ ).map((x) => getAddress(x))
+
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged)
+ }
+ if (!chainChanged) {
+ chainChanged = this.onChainChanged.bind(this)
+ provider.on('chainChanged', chainChanged)
+ }
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect)
+ }
+
+ // Switch to chain if provided
+ let currentChainId = await this.getChainId()
+ if (chainId && currentChainId !== chainId) {
+ const chain = await this.switchChain!({ chainId }).catch((error) => {
+ if (error.code === UserRejectedRequestError.code) throw error
+ return { id: currentChainId }
+ })
+ currentChainId = chain?.id ?? currentChainId
+ }
+
+ return { accounts, chainId: currentChainId }
+ } catch (error) {
+ if (
+ /(user closed modal|accounts received is empty|user denied account)/i.test(
+ (error as Error).message,
+ )
+ )
+ throw new UserRejectedRequestError(error as Error)
+ throw error
+ }
+ },
+ async disconnect() {
+ const provider = await this.getProvider()
+
+ if (accountsChanged) {
+ provider.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+
+ provider.disconnect()
+ provider.close()
+ },
+ async getAccounts() {
+ const provider = await this.getProvider()
+ return (
+ await provider.request({
+ method: 'eth_accounts',
+ })
+ ).map((x) => getAddress(x))
+ },
+ async getChainId() {
+ const provider = await this.getProvider()
+ const chainId = await provider.request({
+ method: 'eth_chainId',
+ })
+ return Number(chainId)
+ },
+ async getProvider() {
+ if (!walletProvider) {
+ // Unwrapping import for Vite compatibility.
+ // See: https://github.com/vitejs/vite/issues/9703
+ const CoinbaseWalletSDK = await (async () => {
+ const { default: SDK } = await import('cbw-sdk')
+ if (typeof SDK !== 'function' && typeof SDK.default === 'function')
+ return SDK.default
+ return SDK as unknown as typeof SDK.default
+ })()
+
+ sdk = new CoinbaseWalletSDK({ ...parameters, reloadOnDisconnect })
+
+ // Force types to retrieve private `walletExtension` method from the Coinbase Wallet SDK.
+ const walletExtensionChainId = (
+ sdk as unknown as {
+ get walletExtension(): { getChainId(): number } | undefined
+ }
+ ).walletExtension?.getChainId()
+
+ const chain =
+ config.chains.find((chain) =>
+ parameters.chainId
+ ? chain.id === parameters.chainId
+ : chain.id === walletExtensionChainId,
+ ) || config.chains[0]
+ const chainId = parameters.chainId || chain?.id
+ const jsonRpcUrl =
+ parameters.jsonRpcUrl || chain?.rpcUrls.default.http[0]
+
+ walletProvider = sdk.makeWeb3Provider(jsonRpcUrl, chainId)
+ }
+
+ return walletProvider
+ },
+ async isAuthorized() {
+ try {
+ const accounts = await this.getAccounts()
+ return !!accounts.length
+ } catch {
+ return false
+ }
+ },
+ async switchChain({ addEthereumChainParameter, chainId }) {
+ const chain = config.chains.find((chain) => chain.id === chainId)
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
+
+ const provider = await this.getProvider()
+
+ try {
+ await provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: numberToHex(chain.id) }],
+ })
+ return chain
+ } catch (error) {
+ // Indicates chain is not added to provider
+ if ((error as ProviderRpcError).code === 4902) {
+ try {
+ let blockExplorerUrls: string[] | undefined
+ if (addEthereumChainParameter?.blockExplorerUrls)
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
+ else
+ blockExplorerUrls = chain.blockExplorers?.default.url
+ ? [chain.blockExplorers?.default.url]
+ : []
+
+ let rpcUrls: readonly string[]
+ if (addEthereumChainParameter?.rpcUrls?.length)
+ rpcUrls = addEthereumChainParameter.rpcUrls
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
+
+ const addEthereumChain = {
+ blockExplorerUrls,
+ chainId: numberToHex(chainId),
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
+ iconUrls: addEthereumChainParameter?.iconUrls,
+ nativeCurrency:
+ addEthereumChainParameter?.nativeCurrency ??
+ chain.nativeCurrency,
+ rpcUrls,
+ } satisfies AddEthereumChainParameter
+
+ await provider.request({
+ method: 'wallet_addEthereumChain',
+ params: [addEthereumChain],
+ })
+
+ return chain
+ } catch (error) {
+ throw new UserRejectedRequestError(error as Error)
+ }
+ }
+
+ throw new SwitchChainError(error as Error)
+ }
+ },
+ onAccountsChanged(accounts) {
+ if (accounts.length === 0) this.onDisconnect()
+ else
+ config.emitter.emit('change', {
+ accounts: accounts.map((x) => getAddress(x)),
+ })
+ },
+ onChainChanged(chain) {
+ const chainId = Number(chain)
+ config.emitter.emit('change', { chainId })
+ },
+ async onDisconnect(_error) {
+ config.emitter.emit('disconnect')
+
+ const provider = await this.getProvider()
+ if (accountsChanged) {
+ provider.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ },
+ }))
+}
diff --git a/packages/connectors/src/exports/index.test.ts b/packages/connectors/src/exports/index.test.ts
new file mode 100644
index 0000000000..bcf100cb61
--- /dev/null
+++ b/packages/connectors/src/exports/index.test.ts
@@ -0,0 +1,17 @@
+import { expect, test } from 'vitest'
+
+import * as connectors from './index.js'
+
+test('exports', () => {
+ expect(Object.keys(connectors)).toMatchInlineSnapshot(`
+ [
+ "injected",
+ "mock",
+ "coinbaseWallet",
+ "metaMask",
+ "safe",
+ "walletConnect",
+ "version",
+ ]
+ `)
+})
diff --git a/packages/connectors/src/exports/index.ts b/packages/connectors/src/exports/index.ts
new file mode 100644
index 0000000000..bac0975956
--- /dev/null
+++ b/packages/connectors/src/exports/index.ts
@@ -0,0 +1,23 @@
+// biome-ignore lint/performance/noBarrelFile: entrypoint module
+export {
+ type InjectedParameters,
+ injected,
+ type MockParameters,
+ mock,
+} from '@wagmi/core'
+
+export {
+ type CoinbaseWalletParameters,
+ coinbaseWallet,
+} from '../coinbaseWallet.js'
+
+export { type MetaMaskParameters, metaMask } from '../metaMask.js'
+
+export { type SafeParameters, safe } from '../safe.js'
+
+export {
+ type WalletConnectParameters,
+ walletConnect,
+} from '../walletConnect.js'
+
+export { version } from '../version.js'
diff --git a/packages/connectors/src/metaMask.test.ts b/packages/connectors/src/metaMask.test.ts
new file mode 100644
index 0000000000..40c3f0f7f9
--- /dev/null
+++ b/packages/connectors/src/metaMask.test.ts
@@ -0,0 +1,10 @@
+import { config } from '@wagmi/test'
+import { expect, test } from 'vitest'
+
+import { metaMask } from './metaMask.js'
+
+test('setup', () => {
+ const connectorFn = metaMask()
+ const connector = config._internal.connectors.setup(connectorFn)
+ expect(connector.name).toEqual('MetaMask')
+})
diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts
new file mode 100644
index 0000000000..02ab4c3fb3
--- /dev/null
+++ b/packages/connectors/src/metaMask.ts
@@ -0,0 +1,505 @@
+import type {
+ MetaMaskSDK,
+ MetaMaskSDKOptions,
+ RPC_URLS_MAP,
+ SDKProvider,
+} from '@metamask/sdk'
+import {
+ ChainNotConfiguredError,
+ type Connector,
+ ProviderNotFoundError,
+ createConnector,
+ extractRpcUrls,
+} from '@wagmi/core'
+import type {
+ Compute,
+ ExactPartial,
+ OneOf,
+ RemoveUndefined,
+ UnionCompute,
+} from '@wagmi/core/internal'
+import {
+ type AddEthereumChainParameter,
+ type Address,
+ type Hex,
+ type ProviderConnectInfo,
+ type ProviderRpcError,
+ ResourceUnavailableRpcError,
+ type RpcError,
+ SwitchChainError,
+ UserRejectedRequestError,
+ getAddress,
+ hexToNumber,
+ numberToHex,
+ withRetry,
+ withTimeout,
+} from 'viem'
+
+export type MetaMaskParameters = UnionCompute<
+ WagmiMetaMaskSDKOptions &
+ OneOf<
+ | {
+ /* Shortcut to connect and sign a message */
+ connectAndSign?: string | undefined
+ }
+ | {
+ // TODO: Strongly type `method` and `params`
+ /* Allow `connectWith` any rpc method */
+ connectWith?: { method: string; params: unknown[] } | undefined
+ }
+ >
+>
+
+type WagmiMetaMaskSDKOptions = Compute<
+ ExactPartial<
+ Omit<
+ MetaMaskSDKOptions,
+ | '_source'
+ | 'forceDeleteProvider'
+ | 'forceInjectProvider'
+ | 'injectProvider'
+ | 'useDeeplink'
+ | 'readonlyRPCMap'
+ >
+ > & {
+ /** @deprecated */
+ forceDeleteProvider?: MetaMaskSDKOptions['forceDeleteProvider']
+ /** @deprecated */
+ forceInjectProvider?: MetaMaskSDKOptions['forceInjectProvider']
+ /** @deprecated */
+ injectProvider?: MetaMaskSDKOptions['injectProvider']
+ /** @deprecated */
+ useDeeplink?: MetaMaskSDKOptions['useDeeplink']
+ }
+>
+
+metaMask.type = 'metaMask' as const
+export function metaMask(parameters: MetaMaskParameters = {}) {
+ type Provider = SDKProvider
+ type Properties = {
+ onConnect(connectInfo: ProviderConnectInfo): void
+ onDisplayUri(uri: string): void
+ }
+ type Listener = Parameters[1]
+
+ let sdk: MetaMaskSDK
+ let provider: Provider | undefined
+ let providerPromise: Promise
+
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
+ let chainChanged: Connector['onChainChanged'] | undefined
+ let connect: Connector['onConnect'] | undefined
+ let displayUri: ((uri: string) => void) | undefined
+ let disconnect: Connector['onDisconnect'] | undefined
+
+ return createConnector((config) => ({
+ id: 'metaMaskSDK',
+ name: 'MetaMask',
+ rdns: ['io.metamask', 'io.metamask.mobile'],
+ type: metaMask.type,
+ async setup() {
+ const provider = await this.getProvider()
+ if (provider?.on) {
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider.on('connect', connect as Listener)
+ }
+
+ // We shouldn't need to listen for `'accountsChanged'` here since the `'connect'` event should suffice (and wallet shouldn't be connected yet).
+ // Some wallets, like MetaMask, do not implement the `'connect'` event and overload `'accountsChanged'` instead.
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged as Listener)
+ }
+ }
+ },
+ async connect({ chainId, isReconnecting } = {}) {
+ const provider = await this.getProvider()
+ if (!displayUri) {
+ displayUri = this.onDisplayUri
+ provider.on('display_uri', displayUri as Listener)
+ }
+
+ let accounts: readonly Address[] = []
+ if (isReconnecting) accounts = await this.getAccounts().catch(() => [])
+
+ try {
+ let signResponse: string | undefined
+ let connectWithResponse: unknown | undefined
+ if (!accounts?.length) {
+ if (parameters.connectAndSign || parameters.connectWith) {
+ if (parameters.connectAndSign)
+ signResponse = await sdk.connectAndSign({
+ msg: parameters.connectAndSign,
+ })
+ else if (parameters.connectWith)
+ connectWithResponse = await sdk.connectWith({
+ method: parameters.connectWith.method,
+ params: parameters.connectWith.params,
+ })
+
+ accounts = await this.getAccounts()
+ } else {
+ const requestedAccounts = (await sdk.connect()) as string[]
+ accounts = requestedAccounts.map((x) => getAddress(x))
+ }
+ }
+ // Switch to chain if provided
+ let currentChainId = (await this.getChainId()) as number
+ if (chainId && currentChainId !== chainId) {
+ const chain = await this.switchChain!({ chainId }).catch((error) => {
+ if (error.code === UserRejectedRequestError.code) throw error
+ return { id: currentChainId }
+ })
+ currentChainId = chain?.id ?? currentChainId
+ }
+
+ if (displayUri) {
+ provider.removeListener('display_uri', displayUri)
+ displayUri = undefined
+ }
+
+ if (signResponse)
+ provider.emit('connectAndSign', {
+ accounts,
+ chainId: currentChainId,
+ signResponse,
+ })
+ else if (connectWithResponse)
+ provider.emit('connectWith', {
+ accounts,
+ chainId: currentChainId,
+ connectWithResponse,
+ })
+
+ // Manage EIP-1193 event listeners
+ // https://eips.ethereum.org/EIPS/eip-1193#events
+ if (connect) {
+ provider.removeListener('connect', connect)
+ connect = undefined
+ }
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged as Listener)
+ }
+ if (!chainChanged) {
+ chainChanged = this.onChainChanged.bind(this)
+ provider.on('chainChanged', chainChanged as Listener)
+ }
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect as Listener)
+ }
+
+ return { accounts, chainId: currentChainId }
+ } catch (err) {
+ const error = err as RpcError
+ if (error.code === UserRejectedRequestError.code)
+ throw new UserRejectedRequestError(error)
+ if (error.code === ResourceUnavailableRpcError.code)
+ throw new ResourceUnavailableRpcError(error)
+ throw error
+ }
+ },
+ async disconnect() {
+ const provider = await this.getProvider()
+
+ // Manage EIP-1193 event listeners
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider.on('connect', connect as Listener)
+ }
+
+ await sdk.terminate()
+ },
+ async getAccounts() {
+ const provider = await this.getProvider()
+ const accounts = (await provider.request({
+ method: 'eth_accounts',
+ })) as string[]
+ return accounts.map((x) => getAddress(x))
+ },
+ async getChainId() {
+ const provider = await this.getProvider()
+ const chainId =
+ provider.getChainId() ||
+ (await provider?.request({ method: 'eth_chainId' }))
+ return Number(chainId)
+ },
+ async getProvider() {
+ async function initProvider() {
+ // Unwrapping import for Vite compatibility.
+ // See: https://github.com/vitejs/vite/issues/9703
+ const MetaMaskSDK = await (async () => {
+ const { default: SDK } = await import('@metamask/sdk')
+ if (typeof SDK !== 'function' && typeof SDK.default === 'function')
+ return SDK.default
+ return SDK as unknown as typeof SDK.default
+ })()
+
+ const readonlyRPCMap: RPC_URLS_MAP = {}
+ for (const chain of config.chains)
+ readonlyRPCMap[numberToHex(chain.id)] = extractRpcUrls({
+ chain,
+ transports: config.transports,
+ })?.[0]
+
+ sdk = new MetaMaskSDK({
+ _source: 'wagmi',
+ forceDeleteProvider: false,
+ forceInjectProvider: false,
+ injectProvider: false,
+ // Workaround cast since MetaMask SDK does not support `'exactOptionalPropertyTypes'`
+ ...(parameters as RemoveUndefined),
+ readonlyRPCMap,
+ dappMetadata: {
+ ...parameters.dappMetadata,
+ // Test if name and url are set AND not empty
+ name: parameters.dappMetadata?.name
+ ? parameters.dappMetadata?.name
+ : 'wagmi',
+ url: parameters.dappMetadata?.url
+ ? parameters.dappMetadata?.url
+ : typeof window !== 'undefined'
+ ? window.location.origin
+ : 'https://wagmi.sh',
+ },
+ useDeeplink: parameters.useDeeplink ?? true,
+ })
+ const result = await sdk.init()
+ // On initial load, sometimes `sdk.getProvider` does not return provider.
+ // https://github.com/wevm/wagmi/issues/4367
+ // Use result of `init` call if available.
+ const provider = (() => {
+ if (result?.activeProvider) return result.activeProvider
+ return sdk.getProvider()
+ })()
+ if (!provider) throw new ProviderNotFoundError()
+ return provider
+ }
+
+ if (!provider) {
+ if (!providerPromise) providerPromise = initProvider()
+ provider = await providerPromise
+ }
+ return provider!
+ },
+ async isAuthorized() {
+ try {
+ // MetaMask mobile provider sometimes fails to immediately resolve
+ // JSON-RPC requests on page load
+ const timeout = 200
+ const accounts = await withRetry(
+ () => withTimeout(() => this.getAccounts(), { timeout }),
+ {
+ delay: timeout + 1,
+ retryCount: 3,
+ },
+ )
+ return !!accounts.length
+ } catch {
+ return false
+ }
+ },
+ async switchChain({ addEthereumChainParameter, chainId }) {
+ const provider = await this.getProvider()
+
+ const chain = config.chains.find((x) => x.id === chainId)
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
+
+ try {
+ await provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: numberToHex(chainId) }],
+ })
+
+ // During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain.
+ // If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain.
+ // To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via
+ // this callback or an externally emitted `'chainChanged'` event.
+ // https://github.com/MetaMask/metamask-extension/issues/24247
+ await waitForChainIdToSync()
+ await sendAndWaitForChangeEvent(chainId)
+
+ return chain
+ } catch (err) {
+ const error = err as RpcError
+
+ if (error.code === UserRejectedRequestError.code)
+ throw new UserRejectedRequestError(error)
+
+ // Indicates chain is not added to provider
+ if (
+ error.code === 4902 ||
+ // Unwrapping for MetaMask Mobile
+ // https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719
+ (error as ProviderRpcError<{ originalError?: { code: number } }>)
+ ?.data?.originalError?.code === 4902
+ ) {
+ try {
+ await provider.request({
+ method: 'wallet_addEthereumChain',
+ params: [
+ {
+ blockExplorerUrls: (() => {
+ const { default: blockExplorer, ...blockExplorers } =
+ chain.blockExplorers ?? {}
+ if (addEthereumChainParameter?.blockExplorerUrls)
+ return addEthereumChainParameter.blockExplorerUrls
+ if (blockExplorer)
+ return [
+ blockExplorer.url,
+ ...Object.values(blockExplorers).map((x) => x.url),
+ ]
+ return
+ })(),
+ chainId: numberToHex(chainId),
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
+ iconUrls: addEthereumChainParameter?.iconUrls,
+ nativeCurrency:
+ addEthereumChainParameter?.nativeCurrency ??
+ chain.nativeCurrency,
+ rpcUrls: (() => {
+ if (addEthereumChainParameter?.rpcUrls?.length)
+ return addEthereumChainParameter.rpcUrls
+ return [chain.rpcUrls.default?.http[0] ?? '']
+ })(),
+ } satisfies AddEthereumChainParameter,
+ ],
+ })
+
+ await waitForChainIdToSync()
+ await sendAndWaitForChangeEvent(chainId)
+
+ return chain
+ } catch (err) {
+ const error = err as RpcError
+ if (error.code === UserRejectedRequestError.code)
+ throw new UserRejectedRequestError(error)
+ throw new SwitchChainError(error)
+ }
+ }
+
+ throw new SwitchChainError(error)
+ }
+
+ async function waitForChainIdToSync() {
+ // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`.
+ // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop.
+ await withRetry(
+ async () => {
+ const value = hexToNumber(
+ // `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks
+ (await provider.request({ method: 'eth_chainId' })) as Hex,
+ )
+ // `value` doesn't match expected `chainId`, throw to trigger retry
+ if (value !== chainId)
+ throw new Error('User rejected switch after adding network.')
+ return value
+ },
+ {
+ delay: 50,
+ retryCount: 20, // android device encryption is slower
+ },
+ )
+ }
+
+ async function sendAndWaitForChangeEvent(chainId: number) {
+ await new Promise((resolve) => {
+ const listener = ((data) => {
+ if ('chainId' in data && data.chainId === chainId) {
+ config.emitter.off('change', listener)
+ resolve()
+ }
+ }) satisfies Parameters[1]
+ config.emitter.on('change', listener)
+ config.emitter.emit('change', { chainId })
+ })
+ }
+ },
+ async onAccountsChanged(accounts) {
+ // Disconnect if there are no accounts
+ if (accounts.length === 0) {
+ // ... and using browser extension
+ if (sdk.isExtensionActive()) this.onDisconnect()
+ // FIXME(upstream): Mobile app sometimes emits invalid `accountsChanged` event with empty accounts array
+ else return
+ }
+ // Connect if emitter is listening for connect event (e.g. is disconnected and connects through wallet interface)
+ else if (config.emitter.listenerCount('connect')) {
+ const chainId = (await this.getChainId()).toString()
+ this.onConnect({ chainId })
+ }
+ // Regular change event
+ else
+ config.emitter.emit('change', {
+ accounts: accounts.map((x) => getAddress(x)),
+ })
+ },
+ onChainChanged(chain) {
+ const chainId = Number(chain)
+ config.emitter.emit('change', { chainId })
+ },
+ async onConnect(connectInfo) {
+ const accounts = await this.getAccounts()
+ if (accounts.length === 0) return
+
+ const chainId = Number(connectInfo.chainId)
+ config.emitter.emit('connect', { accounts, chainId })
+
+ const provider = await this.getProvider()
+ if (connect) {
+ provider.removeListener('connect', connect)
+ connect = undefined
+ }
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged as Listener)
+ }
+ if (!chainChanged) {
+ chainChanged = this.onChainChanged.bind(this)
+ provider.on('chainChanged', chainChanged as Listener)
+ }
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect as Listener)
+ }
+ },
+ async onDisconnect(error) {
+ const provider = await this.getProvider()
+
+ // If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting
+ // https://github.com/MetaMask/providers/pull/120
+ if (error && (error as RpcError<1013>).code === 1013) {
+ if (provider && !!(await this.getAccounts()).length) return
+ }
+
+ config.emitter.emit('disconnect')
+
+ // Manage EIP-1193 event listeners
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider.on('connect', connect as Listener)
+ }
+ },
+ onDisplayUri(uri) {
+ config.emitter.emit('message', { type: 'display_uri', data: uri })
+ },
+ }))
+}
diff --git a/packages/connectors/src/safe.test.ts b/packages/connectors/src/safe.test.ts
new file mode 100644
index 0000000000..0571115f36
--- /dev/null
+++ b/packages/connectors/src/safe.test.ts
@@ -0,0 +1,23 @@
+import { config } from '@wagmi/test'
+import { expect, test } from 'vitest'
+
+import { safe } from './safe.js'
+
+/*
+ * To manually test the Safe connector:
+ *
+ * 1. Run the wagmi playground app (`pnpm dev`)
+ * 2. Add a custom Safe App with App URL set to `http://localhost:5173` (make sure there is a `manifest.json` file served by the playground)
+ * 3. Open the playground app at `https://app.safe.global/eth:0x4557B18E779944BFE9d78A672452331C186a9f48/apps?appUrl=http%3A%2F%2Flocalhost%3A5173`
+ *
+ * See https://docs.gnosis-safe.io/learn/safe-tools/sdks/safe-apps/releasing-your-safe-app for more info.
+ */
+
+test('setup', () => {
+ const connectorFn = safe({
+ allowedDomains: [/gnosis-safe.io$/, /app.safe.global$/],
+ debug: false,
+ })
+ const connector = config._internal.connectors.setup(connectorFn)
+ expect(connector.name).toEqual('Safe')
+})
diff --git a/packages/connectors/src/safe.ts b/packages/connectors/src/safe.ts
new file mode 100644
index 0000000000..13153e106f
--- /dev/null
+++ b/packages/connectors/src/safe.ts
@@ -0,0 +1,145 @@
+import type { SafeAppProvider } from '@safe-global/safe-apps-provider'
+import type { Opts } from '@safe-global/safe-apps-sdk'
+import {
+ type Connector,
+ ProviderNotFoundError,
+ createConnector,
+} from '@wagmi/core'
+import type { Compute } from '@wagmi/core/internal'
+import { getAddress, withTimeout } from 'viem'
+
+export type SafeParameters = Compute<
+ Opts & {
+ /**
+ * Connector automatically connects when used as Safe App.
+ *
+ * This flag simulates the disconnect behavior by keeping track of connection status in storage
+ * and only autoconnecting when previously connected by user action (e.g. explicitly choosing to connect).
+ *
+ * @default false
+ */
+ shimDisconnect?: boolean | undefined
+ /**
+ * Timeout in milliseconds for `getInfo` (from the Safe SDK) to resolve.
+ *
+ * `getInfo` does not resolve when not used in Safe App iFrame. This allows the connector to force a timeout.
+ * @default 10
+ */
+ unstable_getInfoTimeout?: number | undefined
+ }
+>
+
+safe.type = 'safe' as const
+export function safe(parameters: SafeParameters = {}) {
+ const { shimDisconnect = false } = parameters
+
+ type Provider = SafeAppProvider | undefined
+ type Properties = Record
+ type StorageItem = { 'safe.disconnected': true }
+
+ let provider_: Provider | undefined
+
+ let disconnect: Connector['onDisconnect'] | undefined
+
+ return createConnector((config) => ({
+ id: 'safe',
+ name: 'Safe',
+ type: safe.type,
+ async connect() {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+
+ const accounts = await this.getAccounts()
+ const chainId = await this.getChainId()
+
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect)
+ }
+
+ // Remove disconnected shim if it exists
+ if (shimDisconnect) await config.storage?.removeItem('safe.disconnected')
+
+ return { accounts, chainId }
+ },
+ async disconnect() {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+
+ // Add shim signalling connector is disconnected
+ if (shimDisconnect)
+ await config.storage?.setItem('safe.disconnected', true)
+ },
+ async getAccounts() {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+ return (await provider.request({ method: 'eth_accounts' })).map(
+ getAddress,
+ )
+ },
+ async getProvider() {
+ // Only allowed in iframe context
+ const isIframe =
+ typeof window !== 'undefined' && window?.parent !== window
+ if (!isIframe) return
+
+ if (!provider_) {
+ const { default: SDK } = await import('@safe-global/safe-apps-sdk')
+ const sdk = new SDK(parameters)
+
+ // `getInfo` hangs when not used in Safe App iFrame
+ // https://github.com/safe-global/safe-apps-sdk/issues/263#issuecomment-1029835840
+ const safe = await withTimeout(() => sdk.safe.getInfo(), {
+ timeout: parameters.unstable_getInfoTimeout ?? 10,
+ })
+ if (!safe) throw new Error('Could not load Safe information')
+ // Unwrapping import for Vite compatibility.
+ // See: https://github.com/vitejs/vite/issues/9703
+ const SafeAppProvider = await (async () => {
+ const Provider = await import('@safe-global/safe-apps-provider')
+ if (
+ typeof Provider.SafeAppProvider !== 'function' &&
+ typeof Provider.default.SafeAppProvider === 'function'
+ )
+ return Provider.default.SafeAppProvider
+ return Provider.SafeAppProvider
+ })()
+ provider_ = new SafeAppProvider(safe, sdk)
+ }
+ return provider_
+ },
+ async getChainId() {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+ return Number(provider.chainId)
+ },
+ async isAuthorized() {
+ try {
+ const isDisconnected =
+ shimDisconnect &&
+ // If shim exists in storage, connector is disconnected
+ (await config.storage?.getItem('safe.disconnected'))
+ if (isDisconnected) return false
+
+ const accounts = await this.getAccounts()
+ return !!accounts.length
+ } catch {
+ return false
+ }
+ },
+ onAccountsChanged() {
+ // Not relevant for Safe because changing account requires app reload.
+ },
+ onChainChanged() {
+ // Not relevant for Safe because Safe smart contract wallets only exist on single chain.
+ },
+ onDisconnect() {
+ config.emitter.emit('disconnect')
+ },
+ }))
+}
diff --git a/packages/connectors/src/version.ts b/packages/connectors/src/version.ts
new file mode 100644
index 0000000000..11f81d1c34
--- /dev/null
+++ b/packages/connectors/src/version.ts
@@ -0,0 +1 @@
+export const version = '5.8.3'
diff --git a/packages/connectors/src/walletConnect.test.ts b/packages/connectors/src/walletConnect.test.ts
new file mode 100644
index 0000000000..4e8a74ebfe
--- /dev/null
+++ b/packages/connectors/src/walletConnect.test.ts
@@ -0,0 +1,67 @@
+import { config, walletConnectProjectId } from '@wagmi/test'
+import { http, HttpResponse } from 'msw'
+import { setupServer } from 'msw/node'
+import {
+ afterAll,
+ afterEach,
+ beforeAll,
+ expect,
+ expectTypeOf,
+ test,
+ vi,
+} from 'vitest'
+
+import { walletConnect } from './walletConnect.js'
+
+const handlers = [
+ http.get('https://relay.walletconnect.com', async () =>
+ HttpResponse.json(
+ {
+ topic: '222781e3-3fad-4184-acde-077796bf0d3d',
+ type: 'sub',
+ payload: '',
+ silent: true,
+ },
+ { status: 200 },
+ ),
+ ),
+]
+
+const server = setupServer(...handlers)
+
+beforeAll(() => {
+ server.listen({
+ onUnhandledRequest: 'warn',
+ })
+
+ const matchMedia = vi.fn().mockImplementation((query) => {
+ return {
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // deprecated
+ removeListener: vi.fn(), // deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ }
+ })
+ vi.stubGlobal('matchMedia', matchMedia)
+})
+
+afterEach(() => server.resetHandlers())
+
+afterAll(() => server.close())
+
+test('setup', () => {
+ const connectorFn = walletConnect({ projectId: walletConnectProjectId })
+ const connector = config._internal.connectors.setup(connectorFn)
+ expect(connector.name).toEqual('WalletConnect')
+
+ type ConnectFnParameters = NonNullable<
+ Parameters<(typeof connector)['connect']>[0]
+ >
+ expectTypeOf().toMatchTypeOf<
+ string | undefined
+ >()
+})
diff --git a/packages/connectors/src/walletConnect.ts b/packages/connectors/src/walletConnect.ts
new file mode 100644
index 0000000000..fc4f794c1f
--- /dev/null
+++ b/packages/connectors/src/walletConnect.ts
@@ -0,0 +1,468 @@
+import {
+ ChainNotConfiguredError,
+ type Connector,
+ ProviderNotFoundError,
+ createConnector,
+ extractRpcUrls,
+} from '@wagmi/core'
+import type { Compute, ExactPartial, Omit } from '@wagmi/core/internal'
+import type { EthereumProvider } from '@walletconnect/ethereum-provider'
+import {
+ type AddEthereumChainParameter,
+ type Address,
+ type ProviderConnectInfo,
+ type ProviderRpcError,
+ type RpcError,
+ SwitchChainError,
+ UserRejectedRequestError,
+ getAddress,
+ numberToHex,
+} from 'viem'
+
+type WalletConnectConnector = Connector & {
+ onDisplayUri(uri: string): void
+ onSessionDelete(data: { topic: string }): void
+}
+
+type EthereumProviderOptions = Parameters<(typeof EthereumProvider)['init']>[0]
+
+export type WalletConnectParameters = Compute<
+ {
+ /**
+ * If a new chain is added to a previously existing configured connector `chains`, this flag
+ * will determine if that chain should be considered as stale. A stale chain is a chain that
+ * WalletConnect has yet to establish a relationship with (e.g. the user has not approved or
+ * rejected the chain).
+ *
+ * This flag mainly affects the behavior when a wallet does not support dynamic chain authorization
+ * with WalletConnect v2.
+ *
+ * If `true` (default), the new chain will be treated as a stale chain. If the user
+ * has yet to establish a relationship (approved/rejected) with this chain in their WalletConnect
+ * session, the connector will disconnect upon the dapp auto-connecting, and the user will have to
+ * reconnect to the dapp (revalidate the chain) in order to approve the newly added chain.
+ * This is the default behavior to avoid an unexpected error upon switching chains which may
+ * be a confusing user experience (e.g. the user will not know they have to reconnect
+ * unless the dapp handles these types of errors).
+ *
+ * If `false`, the new chain will be treated as a potentially valid chain. This means that if the user
+ * has yet to establish a relationship with the chain in their WalletConnect session, wagmi will successfully
+ * auto-connect the user. This comes with the trade-off that the connector will throw an error
+ * when attempting to switch to the unapproved chain if the wallet does not support dynamic session updates.
+ * This may be useful in cases where a dapp constantly
+ * modifies their configured chains, and they do not want to disconnect the user upon
+ * auto-connecting. If the user decides to switch to the unapproved chain, it is important that the
+ * dapp handles this error and prompts the user to reconnect to the dapp in order to approve
+ * the newly added chain.
+ *
+ * @default true
+ */
+ isNewChainsStale?: boolean
+ } & Omit<
+ EthereumProviderOptions,
+ | 'chains'
+ | 'events'
+ | 'optionalChains'
+ | 'optionalEvents'
+ | 'optionalMethods'
+ | 'methods'
+ | 'rpcMap'
+ | 'showQrModal'
+ > &
+ ExactPartial>
+>
+
+walletConnect.type = 'walletConnect' as const
+export function walletConnect(parameters: WalletConnectParameters) {
+ const isNewChainsStale = parameters.isNewChainsStale ?? true
+
+ type Provider = Awaited>
+ type Properties = {
+ connect(parameters?: {
+ chainId?: number | undefined
+ isReconnecting?: boolean | undefined
+ pairingTopic?: string | undefined
+ }): Promise<{
+ accounts: readonly Address[]
+ chainId: number
+ }>
+ getNamespaceChainsIds(): number[]
+ getRequestedChainsIds(): Promise
+ isChainsStale(): Promise
+ onConnect(connectInfo: ProviderConnectInfo): void
+ onDisplayUri(uri: string): void
+ onSessionDelete(data: { topic: string }): void
+ setRequestedChainsIds(chains: number[]): void
+ requestedChainsStorageKey: `${string}.requestedChains`
+ }
+ type StorageItem = {
+ [_ in Properties['requestedChainsStorageKey']]: number[]
+ }
+
+ let provider_: Provider | undefined
+ let providerPromise: Promise
+ const NAMESPACE = 'eip155'
+
+ let accountsChanged: WalletConnectConnector['onAccountsChanged'] | undefined
+ let chainChanged: WalletConnectConnector['onChainChanged'] | undefined
+ let connect: WalletConnectConnector['onConnect'] | undefined
+ let displayUri: WalletConnectConnector['onDisplayUri'] | undefined
+ let sessionDelete: WalletConnectConnector['onSessionDelete'] | undefined
+ let disconnect: WalletConnectConnector['onDisconnect'] | undefined
+
+ return createConnector((config) => ({
+ id: 'walletConnect',
+ name: 'WalletConnect',
+ type: walletConnect.type,
+ async setup() {
+ const provider = await this.getProvider().catch(() => null)
+ if (!provider) return
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider.on('connect', connect)
+ }
+ if (!sessionDelete) {
+ sessionDelete = this.onSessionDelete.bind(this)
+ provider.on('session_delete', sessionDelete)
+ }
+ },
+ async connect({ chainId, ...rest } = {}) {
+ try {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+ if (!displayUri) {
+ displayUri = this.onDisplayUri
+ provider.on('display_uri', displayUri)
+ }
+
+ let targetChainId = chainId
+ if (!targetChainId) {
+ const state = (await config.storage?.getItem('state')) ?? {}
+ const isChainSupported = config.chains.some(
+ (x) => x.id === state.chainId,
+ )
+ if (isChainSupported) targetChainId = state.chainId
+ else targetChainId = config.chains[0]?.id
+ }
+ if (!targetChainId) throw new Error('No chains found on connector.')
+
+ const isChainsStale = await this.isChainsStale()
+ // If there is an active session with stale chains, disconnect current session.
+ if (provider.session && isChainsStale) await provider.disconnect()
+
+ // If there isn't an active session or chains are stale, connect.
+ if (!provider.session || isChainsStale) {
+ const optionalChains = config.chains
+ .filter((chain) => chain.id !== targetChainId)
+ .map((optionalChain) => optionalChain.id)
+ await provider.connect({
+ optionalChains: [targetChainId, ...optionalChains],
+ ...('pairingTopic' in rest
+ ? { pairingTopic: rest.pairingTopic }
+ : {}),
+ })
+
+ this.setRequestedChainsIds(config.chains.map((x) => x.id))
+ }
+
+ // If session exists and chains are authorized, enable provider for required chain
+ const accounts = (await provider.enable()).map((x) => getAddress(x))
+ const currentChainId = await this.getChainId()
+
+ if (displayUri) {
+ provider.removeListener('display_uri', displayUri)
+ displayUri = undefined
+ }
+ if (connect) {
+ provider.removeListener('connect', connect)
+ connect = undefined
+ }
+ if (!accountsChanged) {
+ accountsChanged = this.onAccountsChanged.bind(this)
+ provider.on('accountsChanged', accountsChanged)
+ }
+ if (!chainChanged) {
+ chainChanged = this.onChainChanged.bind(this)
+ provider.on('chainChanged', chainChanged)
+ }
+ if (!disconnect) {
+ disconnect = this.onDisconnect.bind(this)
+ provider.on('disconnect', disconnect)
+ }
+ if (!sessionDelete) {
+ sessionDelete = this.onSessionDelete.bind(this)
+ provider.on('session_delete', sessionDelete)
+ }
+
+ return { accounts, chainId: currentChainId }
+ } catch (error) {
+ if (
+ /(user rejected|connection request reset)/i.test(
+ (error as ProviderRpcError)?.message,
+ )
+ ) {
+ throw new UserRejectedRequestError(error as Error)
+ }
+ throw error
+ }
+ },
+ async disconnect() {
+ const provider = await this.getProvider()
+ try {
+ await provider?.disconnect()
+ } catch (error) {
+ if (!/No matching key/i.test((error as Error).message)) throw error
+ } finally {
+ if (chainChanged) {
+ provider?.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider?.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider?.on('connect', connect)
+ }
+ if (accountsChanged) {
+ provider?.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (sessionDelete) {
+ provider?.removeListener('session_delete', sessionDelete)
+ sessionDelete = undefined
+ }
+
+ this.setRequestedChainsIds([])
+ }
+ },
+ async getAccounts() {
+ const provider = await this.getProvider()
+ return provider.accounts.map((x) => getAddress(x))
+ },
+ async getProvider({ chainId } = {}) {
+ async function initProvider() {
+ const optionalChains = config.chains.map((x) => x.id) as [number]
+ if (!optionalChains.length) return
+ const { EthereumProvider } = await import(
+ '@walletconnect/ethereum-provider'
+ )
+ return await EthereumProvider.init({
+ ...parameters,
+ disableProviderPing: true,
+ optionalChains,
+ projectId: parameters.projectId,
+ rpcMap: Object.fromEntries(
+ config.chains.map((chain) => {
+ const [url] = extractRpcUrls({
+ chain,
+ transports: config.transports,
+ })
+ return [chain.id, url]
+ }),
+ ),
+ showQrModal: parameters.showQrModal ?? true,
+ })
+ }
+
+ if (!provider_) {
+ if (!providerPromise) providerPromise = initProvider()
+ provider_ = await providerPromise
+ provider_?.events.setMaxListeners(Number.POSITIVE_INFINITY)
+ }
+ if (chainId) await this.switchChain?.({ chainId })
+ return provider_!
+ },
+ async getChainId() {
+ const provider = await this.getProvider()
+ return provider.chainId
+ },
+ async isAuthorized() {
+ try {
+ const [accounts, provider] = await Promise.all([
+ this.getAccounts(),
+ this.getProvider(),
+ ])
+
+ // If an account does not exist on the session, then the connector is unauthorized.
+ if (!accounts.length) return false
+
+ // If the chains are stale on the session, then the connector is unauthorized.
+ const isChainsStale = await this.isChainsStale()
+ if (isChainsStale && provider.session) {
+ await provider.disconnect().catch(() => {})
+ return false
+ }
+ return true
+ } catch {
+ return false
+ }
+ },
+ async switchChain({ addEthereumChainParameter, chainId }) {
+ const provider = await this.getProvider()
+ if (!provider) throw new ProviderNotFoundError()
+
+ const chain = config.chains.find((x) => x.id === chainId)
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
+
+ try {
+ await Promise.all([
+ new Promise((resolve) => {
+ const listener = ({
+ chainId: currentChainId,
+ }: { chainId?: number | undefined }) => {
+ if (currentChainId === chainId) {
+ config.emitter.off('change', listener)
+ resolve()
+ }
+ }
+ config.emitter.on('change', listener)
+ }),
+ provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: numberToHex(chainId) }],
+ }),
+ ])
+
+ const requestedChains = await this.getRequestedChainsIds()
+ this.setRequestedChainsIds([...requestedChains, chainId])
+
+ return chain
+ } catch (err) {
+ const error = err as RpcError
+
+ if (/(user rejected)/i.test(error.message))
+ throw new UserRejectedRequestError(error)
+
+ // Indicates chain is not added to provider
+ try {
+ let blockExplorerUrls: string[] | undefined
+ if (addEthereumChainParameter?.blockExplorerUrls)
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
+ else
+ blockExplorerUrls = chain.blockExplorers?.default.url
+ ? [chain.blockExplorers?.default.url]
+ : []
+
+ let rpcUrls: readonly string[]
+ if (addEthereumChainParameter?.rpcUrls?.length)
+ rpcUrls = addEthereumChainParameter.rpcUrls
+ else rpcUrls = [...chain.rpcUrls.default.http]
+
+ const addEthereumChain = {
+ blockExplorerUrls,
+ chainId: numberToHex(chainId),
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
+ iconUrls: addEthereumChainParameter?.iconUrls,
+ nativeCurrency:
+ addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency,
+ rpcUrls,
+ } satisfies AddEthereumChainParameter
+
+ await provider.request({
+ method: 'wallet_addEthereumChain',
+ params: [addEthereumChain],
+ })
+
+ const requestedChains = await this.getRequestedChainsIds()
+ this.setRequestedChainsIds([...requestedChains, chainId])
+ return chain
+ } catch (error) {
+ throw new UserRejectedRequestError(error as Error)
+ }
+ }
+ },
+ onAccountsChanged(accounts) {
+ if (accounts.length === 0) this.onDisconnect()
+ else
+ config.emitter.emit('change', {
+ accounts: accounts.map((x) => getAddress(x)),
+ })
+ },
+ onChainChanged(chain) {
+ const chainId = Number(chain)
+ config.emitter.emit('change', { chainId })
+ },
+ async onConnect(connectInfo) {
+ const chainId = Number(connectInfo.chainId)
+ const accounts = await this.getAccounts()
+ config.emitter.emit('connect', { accounts, chainId })
+ },
+ async onDisconnect(_error) {
+ this.setRequestedChainsIds([])
+ config.emitter.emit('disconnect')
+
+ const provider = await this.getProvider()
+ if (accountsChanged) {
+ provider.removeListener('accountsChanged', accountsChanged)
+ accountsChanged = undefined
+ }
+ if (chainChanged) {
+ provider.removeListener('chainChanged', chainChanged)
+ chainChanged = undefined
+ }
+ if (disconnect) {
+ provider.removeListener('disconnect', disconnect)
+ disconnect = undefined
+ }
+ if (sessionDelete) {
+ provider.removeListener('session_delete', sessionDelete)
+ sessionDelete = undefined
+ }
+ if (!connect) {
+ connect = this.onConnect.bind(this)
+ provider.on('connect', connect)
+ }
+ },
+ onDisplayUri(uri) {
+ config.emitter.emit('message', { type: 'display_uri', data: uri })
+ },
+ onSessionDelete() {
+ this.onDisconnect()
+ },
+ getNamespaceChainsIds() {
+ if (!provider_) return []
+ const chainIds = provider_.session?.namespaces[NAMESPACE]?.accounts?.map(
+ (account) => Number.parseInt(account.split(':')[1] || ''),
+ )
+ return chainIds ?? []
+ },
+ async getRequestedChainsIds() {
+ return (
+ (await config.storage?.getItem(this.requestedChainsStorageKey)) ?? []
+ )
+ },
+ /**
+ * Checks if the target chains match the chains that were
+ * initially requested by the connector for the WalletConnect session.
+ * If there is a mismatch, this means that the chains on the connector
+ * are considered stale, and need to be revalidated at a later point (via
+ * connection).
+ *
+ * There may be a scenario where a dapp adds a chain to the
+ * connector later on, however, this chain will not have been approved or rejected
+ * by the wallet. In this case, the chain is considered stale.
+ */
+ async isChainsStale() {
+ if (!isNewChainsStale) return false
+
+ const connectorChains = config.chains.map((x) => x.id)
+ const namespaceChains = this.getNamespaceChainsIds()
+ if (
+ namespaceChains.length &&
+ !namespaceChains.some((id) => connectorChains.includes(id))
+ )
+ return false
+
+ const requestedChains = await this.getRequestedChainsIds()
+ return !connectorChains.every((id) => requestedChains.includes(id))
+ },
+ async setRequestedChainsIds(chains) {
+ await config.storage?.setItem(this.requestedChainsStorageKey, chains)
+ },
+ get requestedChainsStorageKey() {
+ return `${this.id}.requestedChains` as Properties['requestedChainsStorageKey']
+ },
+ }))
+}
diff --git a/packages/connectors/tsconfig.build.json b/packages/connectors/tsconfig.build.json
new file mode 100644
index 0000000000..fbed2b1036
--- /dev/null
+++ b/packages/connectors/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.test.ts", "src/**/*.test-d.ts"],
+ "compilerOptions": {
+ "sourceMap": true
+ }
+}
diff --git a/packages/connectors/tsconfig.json b/packages/connectors/tsconfig.json
new file mode 100644
index 0000000000..bd33919ac3
--- /dev/null
+++ b/packages/connectors/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.build.json",
+ "include": ["src/**/*.ts"],
+ "exclude": []
+}
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
new file mode 100644
index 0000000000..dcc8dbc8c0
--- /dev/null
+++ b/packages/core/CHANGELOG.md
@@ -0,0 +1,1913 @@
+# @0xsequence/core
+
+## 2.3.43
+
+### Patch Changes
+
+- Remove cognito dependency from waas
+- Updated dependencies
+ - @0xsequence/utils@2.3.43
+ - @0xsequence/abi@2.3.43
+
+## 2.3.42
+
+### Patch Changes
+
+- Disable deprecated chains
+- Updated dependencies
+ - @0xsequence/utils@2.3.42
+ - @0xsequence/abi@2.3.42
+
+## 2.3.41
+
+### Patch Changes
+
+- Add new chains, indexer upgrade
+- Updated dependencies
+ - @0xsequence/utils@2.3.41
+ - @0xsequence/abi@2.3.41
+
+## 2.3.40
+
+### Patch Changes
+
+- Remove legacy etherlink testnet
+- Updated dependencies
+ - @0xsequence/utils@2.3.40
+ - @0xsequence/abi@2.3.40
+
+## 2.3.39
+
+### Patch Changes
+
+- Add incentiv mainnet
+- Updated dependencies
+ - @0xsequence/utils@2.3.39
+ - @0xsequence/abi@2.3.39
+
+## 2.3.38
+
+### Patch Changes
+
+- Add Etherlink Shadownet
+- Updated dependencies
+ - @0xsequence/utils@2.3.38
+ - @0xsequence/abi@2.3.38
+
+## 2.3.37
+
+### Patch Changes
+
+- API updates
+- Updated dependencies
+ - @0xsequence/utils@2.3.37
+ - @0xsequence/abi@2.3.37
+
+## 2.3.36
+
+### Patch Changes
+
+- API interface updates, new chains
+- Updated dependencies
+ - @0xsequence/utils@2.3.36
+ - @0xsequence/abi@2.3.36
+
+## 2.3.35
+
+### Patch Changes
+
+- Network and API updates
+- Updated dependencies
+ - @0xsequence/utils@2.3.35
+ - @0xsequence/abi@2.3.35
+
+## 2.3.34
+
+### Patch Changes
+
+- API updates, remove Sei
+- Updated dependencies
+ - @0xsequence/utils@2.3.34
+ - @0xsequence/abi@2.3.34
+
+## 2.3.33
+
+### Patch Changes
+
+- Arc Testnet fixes
+- Updated dependencies
+ - @0xsequence/utils@2.3.33
+ - @0xsequence/abi@2.3.33
+
+## 2.3.32
+
+### Patch Changes
+
+- Remove LAOS and TRN
+- Updated dependencies
+ - @0xsequence/utils@2.3.32
+ - @0xsequence/abi@2.3.32
+
+## 2.3.31
+
+### Patch Changes
+
+- Update marketplace API
+- Updated dependencies
+ - @0xsequence/utils@2.3.31
+ - @0xsequence/abi@2.3.31
+
+## 2.3.30
+
+### Patch Changes
+
+- Add Monad mainnet
+- Updated dependencies
+ - @0xsequence/utils@2.3.30
+ - @0xsequence/abi@2.3.30
+
+## 2.3.29
+
+### Patch Changes
+
+- Update relayer and api interfaces
+- Updated dependencies
+ - @0xsequence/utils@2.3.29
+ - @0xsequence/abi@2.3.29
+
+## 2.3.28
+
+### Patch Changes
+
+- Deprecate Incentiv Testnet v1
+- Updated dependencies
+ - @0xsequence/utils@2.3.28
+ - @0xsequence/abi@2.3.28
+
+## 2.3.27
+
+### Patch Changes
+
+- Minor fix for return types in relay
+- Updated dependencies
+ - @0xsequence/utils@2.3.27
+ - @0xsequence/abi@2.3.27
+
+## 2.3.26
+
+### Patch Changes
+
+- Expose waitForReceipt for AccountSigner
+- Updated dependencies
+ - @0xsequence/utils@2.3.26
+ - @0xsequence/abi@2.3.26
+
+## 2.3.25
+
+### Patch Changes
+
+- Add Katana, Sandbox Testnet
+- Updated dependencies
+ - @0xsequence/utils@2.3.25
+ - @0xsequence/abi@2.3.25
+
+## 2.3.24
+
+### Patch Changes
+
+- Add Incentiv Testnet v2
+- Updated dependencies
+ - @0xsequence/utils@2.3.24
+ - @0xsequence/abi@2.3.24
+
+## 2.3.23
+
+### Patch Changes
+
+- Networks update
+- Updated dependencies
+ - @0xsequence/utils@2.3.23
+ - @0xsequence/abi@2.3.23
+
+## 2.3.22
+
+### Patch Changes
+
+- Add Sei and Somnia
+- Updated dependencies
+ - @0xsequence/utils@2.3.22
+ - @0xsequence/abi@2.3.22
+
+## 2.3.21
+
+### Patch Changes
+
+- waas: x (twitter) authentication
+- Updated dependencies
+ - @0xsequence/utils@2.3.21
+ - @0xsequence/abi@2.3.21
+
+## 2.3.20
+
+### Patch Changes
+
+- Release fix
+- Updated dependencies
+ - @0xsequence/utils@2.3.20
+ - @0xsequence/abi@2.3.20
+
+## 2.3.19
+
+### Patch Changes
+
+- Downgrade pnpm to 10.11.0
+- Updated dependencies
+ - @0xsequence/utils@2.3.19
+ - @0xsequence/abi@2.3.19
+
+## 2.3.18
+
+### Patch Changes
+
+- Marketplace API update
+- Updated dependencies
+ - @0xsequence/utils@2.3.18
+ - @0xsequence/abi@2.3.18
+
+## 2.3.17
+
+### Patch Changes
+
+- Add Incentiv Testnet, remove Frequency
+- Updated dependencies
+ - @0xsequence/utils@2.3.17
+ - @0xsequence/abi@2.3.17
+
+## 2.3.16
+
+### Patch Changes
+
+- somnia-testnet: wallet deployment 10M gas limit
+- Updated dependencies
+ - @0xsequence/utils@2.3.16
+ - @0xsequence/abi@2.3.16
+
+## 2.3.15
+
+### Patch Changes
+
+- somnia-testnet: wallet deployment 1M gas limit
+- Updated dependencies
+ - @0xsequence/utils@2.3.15
+ - @0xsequence/abi@2.3.15
+
+## 2.3.14
+
+### Patch Changes
+
+- Update stack api rpc
+- Updated dependencies
+ - @0xsequence/utils@2.3.14
+ - @0xsequence/abi@2.3.14
+
+## 2.3.13
+
+### Patch Changes
+
+- - Improvements to geoblock check
+ - Updated Somnia explorer url
+- Updated dependencies
+ - @0xsequence/utils@2.3.13
+ - @0xsequence/abi@2.3.13
+
+## 2.3.12
+
+### Patch Changes
+
+- Stack API updates
+- Updated dependencies
+ - @0xsequence/utils@2.3.12
+ - @0xsequence/abi@2.3.12
+
+## 2.3.11
+
+### Patch Changes
+
+- Deprecate XR1
+- Updated dependencies
+ - @0xsequence/utils@2.3.11
+ - @0xsequence/abi@2.3.11
+
+## 2.3.10
+
+### Patch Changes
+
+- RPC API updates
+- Updated dependencies
+ - @0xsequence/utils@2.3.10
+ - @0xsequence/abi@2.3.10
+
+## 2.3.9
+
+### Patch Changes
+
+- update indexer rpc client
+- Updated dependencies
+ - @0xsequence/abi@2.3.9
+ - @0xsequence/utils@2.3.9
+
+## 2.3.8
+
+### Patch Changes
+
+- indexer: update clients
+- Updated dependencies
+ - @0xsequence/abi@2.3.8
+ - @0xsequence/utils@2.3.8
+
+## 2.3.7
+
+### Patch Changes
+
+- Metadata updates
+- Updated dependencies
+ - @0xsequence/abi@2.3.7
+ - @0xsequence/utils@2.3.7
+
+## 2.3.6
+
+### Patch Changes
+
+- New chains
+- Updated dependencies
+ - @0xsequence/abi@2.3.6
+ - @0xsequence/utils@2.3.6
+
+## 2.3.5
+
+### Patch Changes
+
+- Add Frequency Testnet
+- Updated dependencies
+ - @0xsequence/abi@2.3.5
+ - @0xsequence/utils@2.3.5
+
+## 2.3.4
+
+### Patch Changes
+
+- metadata: exclude deprecated methods on rpc client
+- Updated dependencies
+ - @0xsequence/abi@2.3.4
+ - @0xsequence/utils@2.3.4
+
+## 2.3.3
+
+### Patch Changes
+
+- metadata: client update
+- Updated dependencies
+ - @0xsequence/abi@2.3.3
+ - @0xsequence/utils@2.3.3
+
+## 2.3.2
+
+### Patch Changes
+
+- metadata: update rpc client
+- Updated dependencies
+ - @0xsequence/abi@2.3.2
+ - @0xsequence/utils@2.3.2
+
+## 2.3.1
+
+### Patch Changes
+
+- indexer: update rpc client
+- Updated dependencies
+ - @0xsequence/abi@2.3.1
+ - @0xsequence/utils@2.3.1
+
+## 2.3.0
+
+### Minor Changes
+
+- update metadata rpc client
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@2.3.0
+ - @0xsequence/utils@2.3.0
+
+## 2.2.15
+
+### Patch Changes
+
+- API updates
+- Updated dependencies
+ - @0xsequence/abi@2.2.15
+ - @0xsequence/utils@2.2.15
+
+## 2.2.14
+
+### Patch Changes
+
+- Somnia Testnet and Monad Testnet
+- Updated dependencies
+ - @0xsequence/abi@2.2.14
+ - @0xsequence/utils@2.2.14
+
+## 2.2.13
+
+### Patch Changes
+
+- Add XR1 to all networks
+- Updated dependencies
+ - @0xsequence/abi@2.2.13
+ - @0xsequence/utils@2.2.13
+
+## 2.2.12
+
+### Patch Changes
+
+- Add XR1
+- Updated dependencies
+ - @0xsequence/abi@2.2.12
+ - @0xsequence/utils@2.2.12
+
+## 2.2.11
+
+### Patch Changes
+
+- Relayer updates
+- Updated dependencies
+ - @0xsequence/abi@2.2.11
+ - @0xsequence/utils@2.2.11
+
+## 2.2.10
+
+### Patch Changes
+
+- Etherlink support
+- Updated dependencies
+ - @0xsequence/abi@2.2.10
+ - @0xsequence/utils@2.2.10
+
+## 2.2.9
+
+### Patch Changes
+
+- Indexer gateway native token balances
+- Updated dependencies
+ - @0xsequence/abi@2.2.9
+ - @0xsequence/utils@2.2.9
+
+## 2.2.8
+
+### Patch Changes
+
+- Add Moonbeam and Moonbase Alpha
+- Updated dependencies
+ - @0xsequence/abi@2.2.8
+ - @0xsequence/utils@2.2.8
+
+## 2.2.7
+
+### Patch Changes
+
+- Update Builder package
+- Updated dependencies
+ - @0xsequence/abi@2.2.7
+ - @0xsequence/utils@2.2.7
+
+## 2.2.6
+
+### Patch Changes
+
+- Update relayer package
+- Updated dependencies
+ - @0xsequence/abi@2.2.6
+ - @0xsequence/utils@2.2.6
+
+## 2.2.5
+
+### Patch Changes
+
+- auth: fix sequence indexer gateway url
+- account: immutable wallet proxy hook
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.2.5
+ - @0xsequence/utils@2.2.5
+
+## 2.2.4
+
+### Patch Changes
+
+- network: update soneium mainnet block explorer url
+- waas: signTypedData intent support
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.2.4
+ - @0xsequence/utils@2.2.4
+
+## 2.2.3
+
+### Patch Changes
+
+- provider: updating initWallet to use connected network configs if they exist
+- Updated dependencies
+ - @0xsequence/abi@2.2.3
+ - @0xsequence/utils@2.2.3
+
+## 2.2.2
+
+### Patch Changes
+
+- pass projectAccessKey to relayer at all times
+- Updated dependencies
+ - @0xsequence/abi@2.2.2
+ - @0xsequence/utils@2.2.2
+
+## 2.2.1
+
+### Patch Changes
+
+- waas-ethers: sign typed data
+- Updated dependencies
+ - @0xsequence/abi@2.2.1
+ - @0xsequence/utils@2.2.1
+
+## 2.2.0
+
+### Minor Changes
+
+- indexer: gateway client
+- @0xsequence/builder
+- upgrade puppeteer to v23.10.3
+
+### Patch Changes
+
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.2.0
+ - @0xsequence/utils@2.2.0
+
+## 2.1.8
+
+### Patch Changes
+
+- Add Soneium Mainnet
+- Updated dependencies
+ - @0xsequence/abi@2.1.8
+ - @0xsequence/utils@2.1.8
+
+## 2.1.7
+
+### Patch Changes
+
+- guard: pass project access key to guard requests
+- Updated dependencies
+ - @0xsequence/abi@2.1.7
+ - @0xsequence/utils@2.1.7
+
+## 2.1.6
+
+### Patch Changes
+
+- Add LAOS and Telos Testnet chains
+- Updated dependencies
+ - @0xsequence/abi@2.1.6
+ - @0xsequence/utils@2.1.6
+
+## 2.1.5
+
+### Patch Changes
+
+- account: save presigned configuration with reference chain id 1
+- Updated dependencies
+ - @0xsequence/abi@2.1.5
+ - @0xsequence/utils@2.1.5
+
+## 2.1.4
+
+### Patch Changes
+
+- provider: pass projectAccessKey into MuxMessageProvider
+- Updated dependencies
+ - @0xsequence/abi@2.1.4
+ - @0xsequence/utils@2.1.4
+
+## 2.1.3
+
+### Patch Changes
+
+- waas: time drift date fix due to strange browser quirk
+- Updated dependencies
+ - @0xsequence/abi@2.1.3
+ - @0xsequence/utils@2.1.3
+
+## 2.1.2
+
+### Patch Changes
+
+- provider: export analytics correctly
+- Updated dependencies
+ - @0xsequence/abi@2.1.2
+ - @0xsequence/utils@2.1.2
+
+## 2.1.1
+
+### Patch Changes
+
+- Add LAOS chain support
+- Updated dependencies
+ - @0xsequence/abi@2.1.1
+ - @0xsequence/utils@2.1.1
+
+## 2.1.0
+
+### Minor Changes
+
+- account: forward project access key when estimating fees and sending transactions
+
+### Patch Changes
+
+- sessions: save signatures with reference chain id
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.1.0
+ - @0xsequence/utils@2.1.0
+
+## 2.0.26
+
+### Patch Changes
+
+- account: fix chain id comparison
+- Updated dependencies
+ - @0xsequence/abi@2.0.26
+ - @0xsequence/utils@2.0.26
+
+## 2.0.25
+
+### Patch Changes
+
+- skale-nebula: deploy gas limit = 10m
+- Updated dependencies
+ - @0xsequence/abi@2.0.25
+ - @0xsequence/utils@2.0.25
+
+## 2.0.24
+
+### Patch Changes
+
+- sessions: arweave: configurable gateway url
+- waas: use /status to get time drift before sending any intents
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.0.24
+ - @0xsequence/utils@2.0.24
+
+## 2.0.23
+
+### Patch Changes
+
+- Add The Root Network support
+- Updated dependencies
+ - @0xsequence/abi@2.0.23
+ - @0xsequence/utils@2.0.23
+
+## 2.0.22
+
+### Patch Changes
+
+- Add SKALE Nebula Mainnet support
+- Updated dependencies
+ - @0xsequence/abi@2.0.22
+ - @0xsequence/utils@2.0.22
+
+## 2.0.21
+
+### Patch Changes
+
+- account: add publishWitnessFor
+- Updated dependencies
+ - @0xsequence/abi@2.0.21
+ - @0xsequence/utils@2.0.21
+
+## 2.0.20
+
+### Patch Changes
+
+- upgrade deps, and improve waas session status handling
+- Updated dependencies
+ - @0xsequence/abi@2.0.20
+ - @0xsequence/utils@2.0.20
+
+## 2.0.19
+
+### Patch Changes
+
+- Add Immutable zkEVM support
+- Updated dependencies
+ - @0xsequence/abi@2.0.19
+ - @0xsequence/utils@2.0.19
+
+## 2.0.18
+
+### Patch Changes
+
+- waas: new contractCall transaction type
+- sessions: add arweave owner
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.0.18
+ - @0xsequence/utils@2.0.18
+
+## 2.0.17
+
+### Patch Changes
+
+- update waas auth to clear session before signIn
+- Updated dependencies
+ - @0xsequence/abi@2.0.17
+ - @0xsequence/utils@2.0.17
+
+## 2.0.16
+
+### Patch Changes
+
+- Removed Astar chains
+- Updated dependencies
+ - @0xsequence/abi@2.0.16
+ - @0xsequence/utils@2.0.16
+
+## 2.0.15
+
+### Patch Changes
+
+- indexer: update bindings with token balance additions
+- Updated dependencies
+ - @0xsequence/abi@2.0.15
+ - @0xsequence/utils@2.0.15
+
+## 2.0.14
+
+### Patch Changes
+
+- sessions: arweave config reader
+- network: add b3 and apechain mainnet configs
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.0.14
+ - @0xsequence/utils@2.0.14
+
+## 2.0.13
+
+### Patch Changes
+
+- network: toy-testnet
+- Updated dependencies
+ - @0xsequence/abi@2.0.13
+ - @0xsequence/utils@2.0.13
+
+## 2.0.12
+
+### Patch Changes
+
+- api: update bindings
+- Updated dependencies
+ - @0xsequence/abi@2.0.12
+ - @0xsequence/utils@2.0.12
+
+## 2.0.11
+
+### Patch Changes
+
+- waas: intents test fix
+- api: update bindings
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.0.11
+ - @0xsequence/utils@2.0.11
+
+## 2.0.10
+
+### Patch Changes
+
+- network: soneium minato testnet
+- Updated dependencies
+ - @0xsequence/abi@2.0.10
+ - @0xsequence/utils@2.0.10
+
+## 2.0.9
+
+### Patch Changes
+
+- network: fix SKALE network name
+- Updated dependencies
+ - @0xsequence/abi@2.0.9
+ - @0xsequence/utils@2.0.9
+
+## 2.0.8
+
+### Patch Changes
+
+- metadata: update bindings
+- Updated dependencies
+ - @0xsequence/abi@2.0.8
+ - @0xsequence/utils@2.0.8
+
+## 2.0.7
+
+### Patch Changes
+
+- wallet request handler fix
+- Updated dependencies
+ - @0xsequence/abi@2.0.7
+ - @0xsequence/utils@2.0.7
+
+## 2.0.6
+
+### Patch Changes
+
+- network: matic -> pol
+- Updated dependencies
+ - @0xsequence/abi@2.0.6
+ - @0xsequence/utils@2.0.6
+
+## 2.0.5
+
+### Patch Changes
+
+- provider: update databeat to 0.9.2
+- Updated dependencies
+ - @0xsequence/abi@2.0.5
+ - @0xsequence/utils@2.0.5
+
+## 2.0.4
+
+### Patch Changes
+
+- network: add skale-nebula-testnet
+- Updated dependencies
+ - @0xsequence/abi@2.0.4
+ - @0xsequence/utils@2.0.4
+
+## 2.0.3
+
+### Patch Changes
+
+- waas: check session status in SequenceWaaS.isSignedIn()
+- Updated dependencies
+ - @0xsequence/abi@2.0.3
+ - @0xsequence/utils@2.0.3
+
+## 2.0.2
+
+### Patch Changes
+
+- sessions: property convert serialized bignumber hex value to bigint
+- Updated dependencies
+ - @0xsequence/abi@2.0.2
+ - @0xsequence/utils@2.0.2
+
+## 2.0.1
+
+### Patch Changes
+
+- waas: http signature check for authenticator requests
+- provider: unwrap legacy json rpc responses
+- use json replacer and reviver for bigints
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@2.0.1
+ - @0xsequence/utils@2.0.1
+
+## 2.0.0
+
+### Major Changes
+
+- ethers v6
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@2.0.0
+ - @0xsequence/utils@2.0.0
+
+## 1.10.15
+
+### Patch Changes
+
+- utils: extractProjectIdFromAccessKey
+- Updated dependencies
+ - @0xsequence/abi@1.10.15
+
+## 1.10.14
+
+### Patch Changes
+
+- network: add borne-testnet to allNetworks
+- Updated dependencies
+ - @0xsequence/abi@1.10.14
+
+## 1.10.13
+
+### Patch Changes
+
+- network: add borne testnet
+- Updated dependencies
+ - @0xsequence/abi@1.10.13
+
+## 1.10.12
+
+### Patch Changes
+
+- api: update bindings
+- global/window -> globalThis
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.10.12
+
+## 1.10.11
+
+### Patch Changes
+
+- waas: updated intent.gen without webrpc types, errors exported from authenticator.gen
+- Updated dependencies
+ - @0xsequence/abi@1.10.11
+
+## 1.10.10
+
+### Patch Changes
+
+- metadata: update bindings with new contract collections api
+- Updated dependencies
+ - @0xsequence/abi@1.10.10
+
+## 1.10.9
+
+### Patch Changes
+
+- waas minor update
+- Updated dependencies
+ - @0xsequence/abi@1.10.9
+
+## 1.10.8
+
+### Patch Changes
+
+- update metadata bindings
+- Updated dependencies
+ - @0xsequence/abi@1.10.8
+
+## 1.10.7
+
+### Patch Changes
+
+- minor fixes to waas client
+- Updated dependencies
+ - @0xsequence/abi@1.10.7
+
+## 1.10.6
+
+### Patch Changes
+
+- metadata: update bindings
+- Updated dependencies
+ - @0xsequence/abi@1.10.6
+
+## 1.10.5
+
+### Patch Changes
+
+- network: ape-chain-testnet -> apechain-testnet
+- Updated dependencies
+ - @0xsequence/abi@1.10.5
+
+## 1.10.4
+
+### Patch Changes
+
+- network: add b3-sepolia, ape-chain-testnet, blast, blast-sepolia
+- Updated dependencies
+ - @0xsequence/abi@1.10.4
+
+## 1.10.3
+
+### Patch Changes
+
+- typing fix
+- Updated dependencies
+ - @0xsequence/abi@1.10.3
+
+## 1.10.2
+
+### Patch Changes
+
+- - waas: add getIdToken method
+ - indexer: update api client
+- Updated dependencies
+ - @0xsequence/abi@1.10.2
+
+## 1.10.1
+
+### Patch Changes
+
+- metadata: update bindings
+- Updated dependencies
+ - @0xsequence/abi@1.10.1
+
+## 1.10.0
+
+### Minor Changes
+
+- waas release v1.3.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.10.0
+
+## 1.9.37
+
+### Patch Changes
+
+- network: adds nativeToken data to NetworkMetadata constants
+- Updated dependencies
+ - @0xsequence/abi@1.9.37
+
+## 1.9.36
+
+### Patch Changes
+
+- guard: export client
+- Updated dependencies
+ - @0xsequence/abi@1.9.36
+
+## 1.9.35
+
+### Patch Changes
+
+- guard: update bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.35
+
+## 1.9.34
+
+### Patch Changes
+
+- waas: always use lowercase email
+- Updated dependencies
+ - @0xsequence/abi@1.9.34
+
+## 1.9.33
+
+### Patch Changes
+
+- waas: umd build
+- Updated dependencies
+ - @0xsequence/abi@1.9.33
+
+## 1.9.32
+
+### Patch Changes
+
+- indexer: update bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.32
+
+## 1.9.31
+
+### Patch Changes
+
+- metadata: token directory changes
+- Updated dependencies
+ - @0xsequence/abi@1.9.31
+
+## 1.9.30
+
+### Patch Changes
+
+- update
+- Updated dependencies
+ - @0xsequence/abi@1.9.30
+
+## 1.9.29
+
+### Patch Changes
+
+- disable gnosis chain
+- Updated dependencies
+ - @0xsequence/abi@1.9.29
+
+## 1.9.28
+
+### Patch Changes
+
+- add utils/merkletree
+- Updated dependencies
+ - @0xsequence/abi@1.9.28
+
+## 1.9.27
+
+### Patch Changes
+
+- network: optimistic -> optimism
+- waas: remove defaults
+- api, sessions: update bindings
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.9.27
+
+## 1.9.26
+
+### Patch Changes
+
+- - add backend interfaces for pluggable interfaces
+ - introduce @0xsequence/react-native
+ - update pnpm to lockfile v9
+- Updated dependencies
+ - @0xsequence/abi@1.9.26
+
+## 1.9.25
+
+### Patch Changes
+
+- update webrpc clients with new error types
+- Updated dependencies
+ - @0xsequence/abi@1.9.25
+
+## 1.9.24
+
+### Patch Changes
+
+- waas: add memoryStore backend to localStore
+- Updated dependencies
+ - @0xsequence/abi@1.9.24
+
+## 1.9.23
+
+### Patch Changes
+
+- update api client bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.23
+
+## 1.9.22
+
+### Patch Changes
+
+- update metadata client bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.22
+
+## 1.9.21
+
+### Patch Changes
+
+- api client bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.21
+
+## 1.9.20
+
+### Patch Changes
+
+- api client bindings update
+- Updated dependencies
+ - @0xsequence/abi@1.9.20
+
+## 1.9.19
+
+### Patch Changes
+
+- waas update
+- Updated dependencies
+ - @0xsequence/abi@1.9.19
+
+## 1.9.18
+
+### Patch Changes
+
+- provider: prohibit dangerous functions
+- Updated dependencies
+ - @0xsequence/abi@1.9.18
+
+## 1.9.17
+
+### Patch Changes
+
+- network: add xr-sepolia
+- Updated dependencies
+ - @0xsequence/abi@1.9.17
+
+## 1.9.16
+
+### Patch Changes
+
+- waas: sequence.feeOptions
+- Updated dependencies
+ - @0xsequence/abi@1.9.16
+
+## 1.9.15
+
+### Patch Changes
+
+- metadata: collection external_link field name fix
+- Updated dependencies
+ - @0xsequence/abi@1.9.15
+
+## 1.9.14
+
+### Patch Changes
+
+- network: astar-zkatana -> astar-zkyoto
+- network: deprecate polygon mumbai network
+- network: add xai and polygon amoy
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.9.14
+
+## 1.9.13
+
+### Patch Changes
+
+- waas: fix @0xsequence/network dependency
+- Updated dependencies
+ - @0xsequence/abi@1.9.13
+
+## 1.9.12
+
+### Patch Changes
+
+- indexer: update rpc bindings
+- provider: signMessage: Serialize the BytesLike or string message into hexstring before sending
+- waas: SessionAuthProof
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.9.12
+
+## 1.9.11
+
+### Patch Changes
+
+- metdata, update rpc bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.11
+
+## 1.9.10
+
+### Patch Changes
+
+- update metadata rpc bindings
+- Updated dependencies
+ - @0xsequence/abi@1.9.10
+
+## 1.9.9
+
+### Patch Changes
+
+- metadata, add SequenceCollections rpc client
+- Updated dependencies
+ - @0xsequence/abi@1.9.9
+
+## 1.9.8
+
+### Patch Changes
+
+- waas client update
+- Updated dependencies
+ - @0xsequence/abi@1.9.8
+
+## 1.9.7
+
+### Patch Changes
+
+- update rpc client bindings for api, metadata and relayer
+- Updated dependencies
+ - @0xsequence/abi@1.9.7
+
+## 1.9.6
+
+### Patch Changes
+
+- waas package update
+- Updated dependencies
+ - @0xsequence/abi@1.9.6
+
+## 1.9.5
+
+### Patch Changes
+
+- RpcRelayer prioritize project access key
+- Updated dependencies
+ - @0xsequence/abi@1.9.5
+
+## 1.9.4
+
+### Patch Changes
+
+- waas: fix network dependency
+- Updated dependencies
+ - @0xsequence/abi@1.9.4
+
+## 1.9.3
+
+### Patch Changes
+
+- provider: don't append access key to RPC url if user has already provided it
+- Updated dependencies
+ - @0xsequence/abi@1.9.3
+
+## 1.9.2
+
+### Patch Changes
+
+- network: add xai-sepolia
+- Updated dependencies
+ - @0xsequence/abi@1.9.2
+
+## 1.9.1
+
+### Patch Changes
+
+- analytics fix
+- Updated dependencies
+ - @0xsequence/abi@1.9.1
+
+## 1.9.0
+
+### Minor Changes
+
+- waas release
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.9.0
+
+## 1.8.8
+
+### Patch Changes
+
+- update metadata bindings
+- Updated dependencies
+ - @0xsequence/abi@1.8.8
+
+## 1.8.7
+
+### Patch Changes
+
+- provider: update databeat to 0.9.1
+- Updated dependencies
+ - @0xsequence/abi@1.8.7
+
+## 1.8.6
+
+### Patch Changes
+
+- guard: SignedOwnershipProof
+- Updated dependencies
+ - @0xsequence/abi@1.8.6
+
+## 1.8.5
+
+### Patch Changes
+
+- guard: signOwnershipProof and isSignedOwnershipProof
+- Updated dependencies
+ - @0xsequence/abi@1.8.5
+
+## 1.8.4
+
+### Patch Changes
+
+- network: add homeverse to networks list
+- Updated dependencies
+ - @0xsequence/abi@1.8.4
+
+## 1.8.3
+
+### Patch Changes
+
+- api: introduce basic linked wallet support
+- Updated dependencies
+ - @0xsequence/abi@1.8.3
+
+## 1.8.2
+
+### Patch Changes
+
+- provider: don't initialize analytics unless explicitly requested
+- Updated dependencies
+ - @0xsequence/abi@1.8.2
+
+## 1.8.1
+
+### Patch Changes
+
+- update to analytics provider
+- Updated dependencies
+ - @0xsequence/abi@1.8.1
+
+## 1.8.0
+
+### Minor Changes
+
+- provider: project analytics
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.8.0
+
+## 1.7.2
+
+### Patch Changes
+
+- 0xsequence: ChainId should not be exported as a type
+- account, wallet: fix nonce selection
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.7.2
+
+## 1.7.1
+
+### Patch Changes
+
+- network: add missing avalanche logoURI
+- Updated dependencies
+ - @0xsequence/abi@1.7.1
+
+## 1.7.0
+
+### Minor Changes
+
+- provider: projectAccessKey is now required
+
+### Patch Changes
+
+- network: add NetworkMetadata.logoURI property for all networks
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.7.0
+
+## 1.6.3
+
+### Patch Changes
+
+- network list update
+- Updated dependencies
+ - @0xsequence/abi@1.6.3
+
+## 1.6.2
+
+### Patch Changes
+
+- auth: projectAccessKey option
+- wallet: use 12 bytes for random space
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.6.2
+
+## 1.6.1
+
+### Patch Changes
+
+- core: add simple config from subdigest support
+- core: fix encode tree with subdigest
+- account: implement buildOnChainSignature on Account
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.6.1
+
+## 1.6.0
+
+### Minor Changes
+
+- account, wallet: parallel transactions by default
+
+### Patch Changes
+
+- provider: emit disconnect on sign out
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.6.0
+
+## 1.5.0
+
+### Minor Changes
+
+- signhub: add 'signing' signer status
+
+### Patch Changes
+
+- auth: Session.open: onAccountAddress callback
+- account: allow empty transaction bundles
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.5.0
+
+## 1.4.9
+
+### Patch Changes
+
+- rename SequenceMetadataClient to SequenceMetadata
+- Updated dependencies
+ - @0xsequence/abi@1.4.9
+
+## 1.4.8
+
+### Patch Changes
+
+- account: Account.getSigners
+- Updated dependencies
+ - @0xsequence/abi@1.4.8
+
+## 1.4.7
+
+### Patch Changes
+
+- update indexer client bindings
+- Updated dependencies
+ - @0xsequence/abi@1.4.7
+
+## 1.4.6
+
+### Patch Changes
+
+- - add sepolia networks, mark goerli as deprecated
+ - update indexer client bindings
+- Updated dependencies
+ - @0xsequence/abi@1.4.6
+
+## 1.4.5
+
+### Patch Changes
+
+- indexer/metadata: update client bindings
+- auth: selectWallet with new address
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.4.5
+
+## 1.4.4
+
+### Patch Changes
+
+- indexer: update bindings
+- auth: handle jwt expiry
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.4.4
+
+## 1.4.3
+
+### Patch Changes
+
+- guard: return active status from GuardSigner.getAuthMethods
+- Updated dependencies
+ - @0xsequence/abi@1.4.3
+
+## 1.4.2
+
+### Patch Changes
+
+- guard: update bindings
+- Updated dependencies
+ - @0xsequence/abi@1.4.2
+
+## 1.4.1
+
+### Patch Changes
+
+- network: remove unused networks
+- signhub: orchestrator interface
+- guard: auth methods interface
+- guard: update bindings for pin and totp
+- guard: no more retry logic
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.4.1
+
+## 1.4.0
+
+### Minor Changes
+
+- project access key support
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.4.0
+
+## 1.3.0
+
+### Minor Changes
+
+- signhub: account children
+
+### Patch Changes
+
+- guard: do not throw when building deploy transaction
+- network: snowtrace.io -> subnets.avax.network/c-chain
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.3.0
+
+## 1.2.9
+
+### Patch Changes
+
+- account: AccountSigner.sendTransaction simulateForFeeOptions
+- relayer: update bindings
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.2.9
+
+## 1.2.8
+
+### Patch Changes
+
+- rename X-Sequence-Token-Key header to X-Access-Key
+- Updated dependencies
+ - @0xsequence/abi@1.2.8
+
+## 1.2.7
+
+### Patch Changes
+
+- add x-sequence-token-key to clients
+- Updated dependencies
+ - @0xsequence/abi@1.2.7
+
+## 1.2.6
+
+### Patch Changes
+
+- Fix bind multicall provider
+- Updated dependencies
+ - @0xsequence/abi@1.2.6
+
+## 1.2.5
+
+### Patch Changes
+
+- Multicall default configuration fixes
+- Updated dependencies
+ - @0xsequence/abi@1.2.5
+
+## 1.2.4
+
+### Patch Changes
+
+- provider: Adding missing payment provider types to PaymentProviderOption
+- provider: WalletRequestHandler.notifyChainChanged
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.2.4
+
+## 1.2.3
+
+### Patch Changes
+
+- auth, provider: connect to accept optional authorizeNonce
+- Updated dependencies
+ - @0xsequence/abi@1.2.3
+
+## 1.2.2
+
+### Patch Changes
+
+- provider: allow createContract calls
+- core: check for explicit zero address in contract deployments
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.2.2
+
+## 1.2.1
+
+### Patch Changes
+
+- auth: use sequence api chain id as reference chain id if available
+- Updated dependencies
+ - @0xsequence/abi@1.2.1
+
+## 1.2.0
+
+### Minor Changes
+
+- split services from session, better local support
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.2.0
+
+## 1.1.15
+
+### Patch Changes
+
+- guard: remove error filtering
+- Updated dependencies
+ - @0xsequence/abi@1.1.15
+
+## 1.1.14
+
+### Patch Changes
+
+- guard: add GuardSigner.onError
+- Updated dependencies
+ - @0xsequence/abi@1.1.14
+
+## 1.1.13
+
+### Patch Changes
+
+- provider: pass client version with connect options
+- provider: removing large from BannerSize
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.1.13
+
+## 1.1.12
+
+### Patch Changes
+
+- provider: adding bannerSize to ConnectOptions
+- Updated dependencies
+ - @0xsequence/abi@1.1.12
+
+## 1.1.11
+
+### Patch Changes
+
+- add homeverse configs
+- Updated dependencies
+ - @0xsequence/abi@1.1.11
+
+## 1.1.10
+
+### Patch Changes
+
+- handle default EIP6492 on send
+- Updated dependencies
+ - @0xsequence/abi@1.1.10
+
+## 1.1.9
+
+### Patch Changes
+
+- Custom default EIP6492 on client
+- Updated dependencies
+ - @0xsequence/abi@1.1.9
+
+## 1.1.8
+
+### Patch Changes
+
+- metadata: searchMetadata: add types filter
+- Updated dependencies
+ - @0xsequence/abi@1.1.8
+
+## 1.1.7
+
+### Patch Changes
+
+- adding signInWith connect settings option to allow dapps to automatically login their users with a certain provider optimizing the normal authentication flow
+- Updated dependencies
+ - @0xsequence/abi@1.1.7
+
+## 1.1.6
+
+### Patch Changes
+
+- metadata: searchMetadata: add chainID and excludeTokenMetadata filters
+- Updated dependencies
+ - @0xsequence/abi@1.1.6
+
+## 1.1.5
+
+### Patch Changes
+
+- account: re-compute meta-transaction id for wallet deployment transactions
+- Updated dependencies
+ - @0xsequence/abi@1.1.5
+
+## 1.1.4
+
+### Patch Changes
+
+- network: rename base-mainnet to base
+- provider: override isDefaultChain with ConnectOptions.networkId if provided
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.1.4
+
+## 1.1.3
+
+### Patch Changes
+
+- provider: use network id from transport session
+- provider: sign authorization using ConnectOptions.networkId if provided
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.1.3
+
+## 1.1.2
+
+### Patch Changes
+
+- provider: jsonrpc chain id fixes
+- Updated dependencies
+ - @0xsequence/abi@1.1.2
+
+## 1.1.1
+
+### Patch Changes
+
+- network: add base mainnet and sepolia
+- provider: reject toxic transaction requests
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.1.1
+
+## 1.1.0
+
+### Minor Changes
+
+- Refactor dapp facing provider
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.1.0
+
+## 1.0.5
+
+### Patch Changes
+
+- network: export network constants
+- guard: use the correct global for fetch
+- network: nova-explorer.arbitrum.io -> nova.arbiscan.io
+- Updated dependencies
+- Updated dependencies
+- Updated dependencies
+ - @0xsequence/abi@1.0.5
+
+## 1.0.4
+
+### Patch Changes
+
+- provider: accept name or number for networkId
+- Updated dependencies
+ - @0xsequence/abi@1.0.4
+
+## 1.0.3
+
+### Patch Changes
+
+- Simpler isValidSignature helpers
+- Updated dependencies
+ - @0xsequence/abi@1.0.3
+
+## 1.0.2
+
+### Patch Changes
+
+- add extra signature validation utils methods
+- Updated dependencies
+ - @0xsequence/abi@1.0.2
+
+## 1.0.1
+
+### Patch Changes
+
+- add homeverse testnet
+- Updated dependencies
+ - @0xsequence/abi@1.0.1
+
+## 1.0.0
+
+### Major Changes
+
+- https://sequence.xyz/blog/sequence-wallet-light-state-sync-full-merkle-wallets
+
+### Patch Changes
+
+- Updated dependencies
+ - @0xsequence/abi@1.0.0
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 0000000000..b46e39c559
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,13 @@
+# @wagmi/core
+
+VanillaJS library for Ethereum
+
+## Installation
+
+```bash
+pnpm add @wagmi/core viem
+```
+
+## Documentation
+
+For documentation and guides, visit [wagmi.sh](https://wagmi.sh).
diff --git a/packages/core/package.json b/packages/core/package.json
new file mode 100644
index 0000000000..a8c7b23afd
--- /dev/null
+++ b/packages/core/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@0xsequence/core",
+ "version": "2.3.43",
+ "description": "core primitives for interacting with the sequence wallet contracts",
+ "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/core",
+ "source": "src/index.ts",
+ "main": "dist/0xsequence-core.cjs.js",
+ "module": "dist/0xsequence-core.esm.js",
+ "author": "Horizon Blockchain Games",
+ "license": "Apache-2.0",
+ "scripts": {
+ "test": "pnpm test:file tests/**/*.spec.ts",
+ "test:file": "TS_NODE_PROJECT=../../tsconfig.test.json mocha -r ts-node/register --timeout 30000",
+ "test:coverage": "nyc pnpm test"
+ },
+ "peerDependencies": {
+ "ethers": ">=6"
+ },
+ "devDependencies": {
+ "@istanbuljs/nyc-config-typescript": "^1.0.2",
+ "ethers": "6.13.4",
+ "nyc": "^15.1.0"
+ },
+ "files": [
+ "src",
+ "dist"
+ ],
+ "dependencies": {
+ "@0xsequence/utils": "workspace:*",
+ "@0xsequence/abi": "workspace:*"
+ }
+}
diff --git a/packages/core/src/actions/call.test.ts b/packages/core/src/actions/call.test.ts
new file mode 100644
index 0000000000..2ef01160da
--- /dev/null
+++ b/packages/core/src/actions/call.test.ts
@@ -0,0 +1,149 @@
+import { accounts, address, config } from '@wagmi/test'
+import { parseEther, parseGwei } from 'viem'
+import { expect, test } from 'vitest'
+
+import { call } from './call.js'
+
+const name4bytes = '0x06fdde03'
+const mint4bytes = '0x1249c58b'
+const mintWithParams4bytes = '0xa0712d68'
+const fourTwenty =
+ '00000000000000000000000000000000000000000000000000000000000001a4'
+
+const account = accounts[0]
+
+test('default', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: name4bytes,
+ to: address.wagmiMintExample,
+ }),
+ ).resolves.toMatchInlineSnapshot(`
+ {
+ "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000",
+ }
+ `)
+})
+
+test('zero data', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: mint4bytes,
+ to: address.wagmiMintExample,
+ }),
+ ).resolves.toMatchInlineSnapshot(`
+ {
+ "data": undefined,
+ }
+ `)
+})
+
+// TODO: Re-enable
+test.skip('parameters: blockNumber', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: name4bytes,
+ to: address.wagmiMintExample,
+ blockNumber: 16280770n,
+ }),
+ ).resolves.toMatchInlineSnapshot(`
+ {
+ "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000",
+ }
+ `)
+})
+
+test('insufficient funds', async () => {
+ await expect(
+ call(config, {
+ account,
+ to: accounts[1],
+ value: parseEther('100000'),
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [CallExecutionError: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account.
+
+ This error could arise when the account does not have enough funds to:
+ - pay for the total gas fee,
+ - pay for the value to send.
+
+ The cost of the transaction is calculated as \`gas * gas fee + value\`, where:
+ - \`gas\` is the amount of gas needed for transaction to execute,
+ - \`gas fee\` is the gas fee,
+ - \`value\` is the amount of ether to send to the recipient.
+
+ Raw Call Arguments:
+ from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+ to: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
+ value: 100000 ETH
+
+ Details: Insufficient funds for gas * price + value
+ Version: viem@2.29.2]
+ `)
+})
+
+test('maxFeePerGas less than maxPriorityFeePerGas', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: name4bytes,
+ to: address.wagmiMintExample,
+ maxFeePerGas: parseGwei('20'),
+ maxPriorityFeePerGas: parseGwei('22'),
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [CallExecutionError: The provided tip (\`maxPriorityFeePerGas\` = 22 gwei) cannot be higher than the fee cap (\`maxFeePerGas\` = 20 gwei).
+
+ Raw Call Arguments:
+ from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+ to: 0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2
+ data: 0x06fdde03
+ maxFeePerGas: 20 gwei
+ maxPriorityFeePerGas: 22 gwei
+
+ Version: viem@2.29.2]
+ `)
+})
+
+test('contract revert (contract error)', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: `${mintWithParams4bytes}${fourTwenty}`,
+ to: address.wagmiMintExample,
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [CallExecutionError: Execution reverted with reason: Token ID is taken.
+
+ Raw Call Arguments:
+ from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+ to: 0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2
+ data: 0xa0712d6800000000000000000000000000000000000000000000000000000000000001a4
+
+ Details: execution reverted: Token ID is taken
+ Version: viem@2.29.2]
+ `)
+})
+
+test('contract revert (insufficient params)', async () => {
+ await expect(
+ call(config, {
+ account,
+ data: mintWithParams4bytes,
+ to: address.wagmiMintExample,
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [CallExecutionError: Execution reverted for an unknown reason.
+
+ Raw Call Arguments:
+ from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+ to: 0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2
+ data: 0xa0712d68
+
+ Details: execution reverted
+ Version: viem@2.29.2]
+ `)
+})
diff --git a/packages/core/src/actions/call.ts b/packages/core/src/actions/call.ts
new file mode 100644
index 0000000000..90e6c1ae20
--- /dev/null
+++ b/packages/core/src/actions/call.ts
@@ -0,0 +1,27 @@
+import type {
+ CallErrorType as viem_CallErrorType,
+ CallParameters as viem_CallParameters,
+ CallReturnType as viem_CallReturnType,
+} from 'viem'
+import { call as viem_call } from 'viem/actions'
+
+import type { Config } from '../createConfig.js'
+import type { ChainIdParameter } from '../types/properties.js'
+import { getAction } from '../utils/getAction.js'
+
+export type CallParameters =
+ viem_CallParameters & ChainIdParameter
+
+export type CallReturnType = viem_CallReturnType
+
+export type CallErrorType = viem_CallErrorType
+
+export async function call(
+ config: config,
+ parameters: CallParameters,
+): Promise {
+ const { chainId, ...rest } = parameters
+ const client = config.getClient({ chainId })
+ const action = getAction(client, viem_call, 'call')
+ return action(rest)
+}
diff --git a/packages/core/src/actions/codegen/createReadContract.test-d.ts b/packages/core/src/actions/codegen/createReadContract.test-d.ts
new file mode 100644
index 0000000000..f5c9dd302b
--- /dev/null
+++ b/packages/core/src/actions/codegen/createReadContract.test-d.ts
@@ -0,0 +1,130 @@
+import { abi, config, mainnet, optimism } from '@wagmi/test'
+import { assertType, expectTypeOf, test } from 'vitest'
+
+import { createReadContract } from './createReadContract.js'
+
+test('default', async () => {
+ const readErc20 = createReadContract({
+ abi: abi.erc20,
+ address: '0x',
+ })
+
+ const result = await readErc20(config, {
+ functionName: 'balanceOf',
+ args: ['0x'],
+ chainId: 1,
+ })
+ expectTypeOf(result).toEqualTypeOf()
+})
+
+test('multichain address', async () => {
+ const readErc20 = createReadContract({
+ abi: abi.erc20,
+ address: {
+ [mainnet.id]: '0x',
+ [optimism.id]: '0x',
+ },
+ })
+
+ const result = await readErc20(config, {
+ functionName: 'balanceOf',
+ args: ['0x'],
+ chainId: mainnet.id,
+ // ^?
+ })
+ assertType(result)
+
+ readErc20(config, {
+ functionName: 'balanceOf',
+ args: ['0x'],
+ // @ts-expect-error chain id must match address keys
+ chainId: 420,
+ })
+
+ readErc20(config, {
+ functionName: 'balanceOf',
+ args: ['0x'],
+ // @ts-expect-error address not allowed
+ address: '0x',
+ })
+})
+
+test('overloads', async () => {
+ const readViewOverloads = createReadContract({
+ abi: abi.viewOverloads,
+ address: '0x',
+ })
+
+ const result1 = await readViewOverloads(config, {
+ functionName: 'foo',
+ })
+ assertType(result1)
+
+ const result2 = await readViewOverloads(config, {
+ functionName: 'foo',
+ args: [],
+ })
+ assertType(result2)
+
+ const result3 = await readViewOverloads(config, {
+ functionName: 'foo',
+ args: ['0x'],
+ })
+ // @ts-ignore – TODO: Fix https://github.com/wevm/viem/issues/1916
+ assertType(result3)
+
+ const result4 = await readViewOverloads(config, {
+ functionName: 'foo',
+ args: ['0x', '0x'],
+ })
+ assertType<{
+ foo: `0x${string}`
+ bar: `0x${string}`
+ // @ts-ignore – TODO: Fix https://github.com/wevm/viem/issues/1916
+ }>(result4)
+})
+
+test('functionName', async () => {
+ const readErc20BalanceOf = createReadContract({
+ abi: abi.erc20,
+ address: '0x',
+ functionName: 'balanceOf',
+ })
+
+ const result = await readErc20BalanceOf(config, {
+ args: ['0x'],
+ chainId: 1,
+ })
+ expectTypeOf(result).toEqualTypeOf()
+})
+
+test('functionName with overloads', async () => {
+ const readViewOverloads = createReadContract({
+ abi: abi.viewOverloads,
+ address: '0x',
+ functionName: 'foo',
+ })
+
+ const result1 = await readViewOverloads(config, {})
+ assertType(result1)
+
+ const result2 = await readViewOverloads(config, {
+ args: [],
+ })
+ assertType(result2)
+
+ const result3 = await readViewOverloads(config, {
+ args: ['0x'],
+ })
+ // @ts-ignore – TODO: Fix https://github.com/wevm/viem/issues/1916
+ assertType(result3)
+
+ const result4 = await readViewOverloads(config, {
+ args: ['0x', '0x'],
+ })
+ assertType<{
+ foo: `0x${string}`
+ bar: `0x${string}`
+ // @ts-ignore – TODO: Fix https://github.com/wevm/viem/issues/1916
+ }>(result4)
+})
diff --git a/packages/core/src/actions/codegen/createReadContract.test.ts b/packages/core/src/actions/codegen/createReadContract.test.ts
new file mode 100644
index 0000000000..9de7ae718f
--- /dev/null
+++ b/packages/core/src/actions/codegen/createReadContract.test.ts
@@ -0,0 +1,50 @@
+import { abi, address, chain, config } from '@wagmi/test'
+import { expect, test } from 'vitest'
+
+import { createReadContract } from './createReadContract.js'
+
+test('default', async () => {
+ const readWagmiMintExample = createReadContract({
+ address: address.wagmiMintExample,
+ abi: abi.wagmiMintExample,
+ })
+
+ await expect(
+ readWagmiMintExample(config, {
+ functionName: 'balanceOf',
+ args: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'],
+ }),
+ ).resolves.toMatchInlineSnapshot('4n')
+})
+
+test('multichain', async () => {
+ const readWagmiMintExample = createReadContract({
+ address: {
+ [chain.mainnet.id]: address.wagmiMintExample,
+ [chain.mainnet2.id]: address.wagmiMintExample,
+ },
+ abi: abi.wagmiMintExample,
+ })
+
+ await expect(
+ readWagmiMintExample(config, {
+ functionName: 'balanceOf',
+ args: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'],
+ chainId: chain.mainnet2.id,
+ }),
+ ).resolves.toMatchInlineSnapshot('4n')
+})
+
+test('functionName', async () => {
+ const readWagmiMintExampleBalanceOf = createReadContract({
+ address: address.wagmiMintExample,
+ abi: abi.wagmiMintExample,
+ functionName: 'balanceOf',
+ })
+
+ await expect(
+ readWagmiMintExampleBalanceOf(config, {
+ args: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'],
+ }),
+ ).resolves.toMatchInlineSnapshot('4n')
+})
diff --git a/packages/core/src/actions/codegen/createReadContract.ts b/packages/core/src/actions/codegen/createReadContract.ts
new file mode 100644
index 0000000000..f7dbfbed45
--- /dev/null
+++ b/packages/core/src/actions/codegen/createReadContract.ts
@@ -0,0 +1,100 @@
+import type {
+ Abi,
+ Address,
+ ContractFunctionArgs,
+ ContractFunctionName,
+} from 'viem'
+
+import type { Config } from '../../createConfig.js'
+import type { UnionCompute, UnionStrictOmit } from '../../types/utils.js'
+import { getAccount } from '../getAccount.js'
+import { getChainId } from '../getChainId.js'
+import {
+ type ReadContractParameters,
+ type ReadContractReturnType,
+ readContract,
+} from '../readContract.js'
+
+type stateMutability = 'pure' | 'view'
+
+export type CreateReadContractParameters<
+ abi extends Abi | readonly unknown[],
+ address extends Address | Record | undefined = undefined,
+ functionName extends
+ | ContractFunctionName
+ | undefined = undefined,
+> = {
+ abi: abi | Abi | readonly unknown[]
+ address?: address | Address | Record | undefined
+ functionName?:
+ | functionName
+ | ContractFunctionName
+ | undefined
+}
+
+export type CreateReadContractReturnType<
+ abi extends Abi | readonly unknown[],
+ address extends Address | Record | undefined,
+ functionName extends ContractFunctionName | undefined,
+ ///
+ omittedProperties extends 'abi' | 'address' | 'chainId' | 'functionName' =
+ | 'abi'
+ | (address extends undefined ? never : 'address')
+ | (address extends Record ? 'chainId' : never)
+ | (functionName extends undefined ? never : 'functionName'),
+> = <
+ config extends Config,
+ name extends functionName extends ContractFunctionName
+ ? functionName
+ : ContractFunctionName