├── .changeset ├── README.md └── config.json ├── .github ├── actions │ └── install-dependencies │ │ └── action.yml ├── dependabot.yml ├── mipd-dark-hug.svg ├── mipd-light-hug.svg └── workflows │ ├── on-pull-request.yml │ ├── on-push-to-main.yml │ ├── snapshot.yml │ └── verify.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── workspace.code-workspace ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── playgrounds ├── vite-react │ ├── index.html │ ├── package.json │ ├── src │ │ ├── main.tsx │ │ ├── vite-env.d.ts │ │ └── wallet.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── vite-svelte │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.svelte │ │ ├── main.ts │ │ ├── vite-env.d.ts │ │ └── wallet.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vite-vue │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.vue │ ├── main.ts │ ├── vite-env.d.ts │ └── wallet.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rome.json ├── scripts └── prepublishOnly.ts ├── src ├── index.test.ts ├── index.ts ├── register.ts ├── store.test.ts ├── store.ts ├── types.test-d.ts ├── types.ts ├── utils.test.ts ├── utils.ts └── window.ts ├── tsconfig.base.json ├── tsconfig.build.json ├── tsconfig.json └── tsconfig.node.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "wagmi-dev/mipd" }], 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "patch", 8 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 9 | "onlyUpdatePeerDependentsWhenOutOfRange": true 10 | }, 11 | "ignore": ["vite-react", "vite-svelte", "vite-vue"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/actions/install-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: "Install dependencies" 2 | description: "Prepare repository and all dependencies" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Set up pnpm 8 | uses: pnpm/action-setup@v2 9 | 10 | - name: Set up node 11 | uses: actions/setup-node@v3 12 | with: 13 | cache: pnpm 14 | node-version: 18 15 | 16 | - name: Install dependencies 17 | shell: bash 18 | run: pnpm install --ignore-scripts -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | -------------------------------------------------------------------------------- /.github/mipd-dark-hug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/mipd-light-hug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/on-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, ready_for_review] 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | verify: 12 | name: Verify 13 | uses: ./.github/workflows/verify.yml 14 | secrets: inherit 15 | 16 | size: 17 | name: Size 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Install dependencies 26 | uses: ./.github/actions/install-dependencies 27 | 28 | - name: Report build size 29 | uses: preactjs/compressed-size-action@v2 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/on-push-to-main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | verify: 12 | name: Verify 13 | uses: ./.github/workflows/verify.yml 14 | secrets: inherit 15 | 16 | canary: 17 | name: Release canary 18 | needs: verify 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 5 21 | 22 | steps: 23 | - name: Clone repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install dependencies 27 | uses: ./.github/actions/install-dependencies 28 | 29 | - name: Setup .npmrc file 30 | uses: actions/setup-node@v4 31 | with: 32 | registry-url: 'https://registry.npmjs.org' 33 | 34 | - name: Set version 35 | run: | 36 | npm --no-git-tag-version version minor 37 | npm --no-git-tag-version version $(npm pkg get version | sed 's/"//g')-canary.$(date +'%Y%m%dT%H%M%S') 38 | 39 | - name: Build 40 | run: pnpm build 41 | 42 | - name: Publish to npm 43 | run: npm publish --tag canary 44 | env: 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | 47 | changesets: 48 | name: Changesets 49 | needs: verify 50 | runs-on: ubuntu-latest 51 | timeout-minutes: 5 52 | 53 | steps: 54 | - name: Clone repository 55 | uses: actions/checkout@v4 56 | with: 57 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 58 | fetch-depth: 0 59 | 60 | - name: Install dependencies 61 | uses: ./.github/actions/install-dependencies 62 | 63 | - name: Create version pull request or publish to npm 64 | uses: changesets/action@v1 65 | with: 66 | title: 'chore: version packages' 67 | commit: 'chore: version packages' 68 | publish: pnpm changeset:release 69 | version: pnpm changeset:version 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | snapshot: 7 | name: Publish Snapshot 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Clone repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Install dependencies 14 | uses: ./.github/actions/install-dependencies 15 | 16 | - name: Publish snapshots 17 | uses: seek-oss/changesets-snapshot@v0 18 | with: 19 | pre-publish: pnpm build 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 5 10 | 11 | steps: 12 | - name: Clone repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Install dependencies 16 | uses: ./.github/actions/install-dependencies 17 | 18 | - name: Lint code 19 | run: pnpm format && pnpm lint:fix 20 | 21 | - uses: stefanzweifel/git-auto-commit-action@v5 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | commit_message: 'chore: format' 26 | commit_user_name: 'github-actions[bot]' 27 | commit_user_email: 'github-actions[bot]@users.noreply.github.com' 28 | 29 | build: 30 | name: Build 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 5 33 | 34 | steps: 35 | - name: Clone repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Install dependencies 39 | uses: ./.github/actions/install-dependencies 40 | 41 | - name: Build 42 | run: pnpm build 43 | 44 | types: 45 | name: Types 46 | runs-on: ubuntu-latest 47 | timeout-minutes: 5 48 | 49 | steps: 50 | - name: Clone repository 51 | uses: actions/checkout@v4 52 | 53 | - name: Install dependencies 54 | uses: ./.github/actions/install-dependencies 55 | 56 | - name: Build 57 | run: pnpm build 58 | 59 | - name: Check types 60 | run: pnpm typecheck 61 | 62 | - name: Test types 63 | run: pnpm test:typecheck 64 | 65 | test: 66 | name: Test 67 | runs-on: ubuntu-latest 68 | timeout-minutes: 5 69 | 70 | steps: 71 | - name: Clone repository 72 | uses: actions/checkout@v4 73 | 74 | - name: Install dependencies 75 | uses: ./.github/actions/install-dependencies 76 | 77 | - name: Run tests 78 | run: pnpm test:cov 79 | 80 | - name: Upload coverage reports to Codecov 81 | uses: codecov/codecov-action@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local 2 | .DS_Store 3 | .eslintcache 4 | .next 5 | .pnpm-debug.log* 6 | bench 7 | cache 8 | coverage 9 | node_modules 10 | tsconfig*.tsbuildinfo 11 | dist 12 | 13 | # local env files 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .envrc 20 | 21 | # @wagmi/cli 22 | generated.ts 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "rome.rome", 4 | "orta.vscode-twoslash-queries", 5 | "svelte.svelte-vscode", 6 | "Vue.volar", 7 | "Vue.vscode-typescript-vue-plugin" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rome.rome", 3 | "editor.formatOnSave": true, 4 | "javascript.validate.enable": true, 5 | "js/ts.implicitProjectConfig.checkJs": true, 6 | "typescript.tsdk": "node_modules/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports.rome": true 10 | }, 11 | "[json]": { 12 | "editor.defaultFormatter": "rome.rome" 13 | }, 14 | "[javascript]": { 15 | "editor.defaultFormatter": "rome.rome" 16 | }, 17 | "[javascriptreact]": { 18 | "editor.defaultFormatter": "rome.rome" 19 | }, 20 | "[typescript]": { 21 | "editor.defaultFormatter": "rome.rome" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "mipd", 5 | "path": "../" 6 | }, 7 | { 8 | "name": "playgrounds", 9 | "path": "../playgrounds" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # mipd 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - [`1f71eef`](https://github.com/wevm/mipd/commit/1f71eef452f3c8d38b4f7485eb76fbe7475d2977) Thanks [@tmm](https://github.com/tmm)! - Bumped Viem version 8 | 9 | ## 0.0.6 10 | 11 | ### Patch Changes 12 | 13 | - [#7](https://github.com/wevm/mipd/pull/7) [`0d6999c`](https://github.com/wevm/mipd/commit/0d6999c375f817eb67228e84faf449eb5f8e3fef) Thanks [@DanielSinclair](https://github.com/DanielSinclair)! - Enforcing `icon` string type to be a RFC-2397 compliant data URI 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present weth, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | 6 | mipd logo 7 | 8 |

9 | 10 |

11 | TypeScript Utilities for EIP-6963: Multi Injected Provider Discovery 12 |

13 | 14 | ## Contents 15 | 16 | - [Install](#install) 17 | - [Store](#store) 18 | - [Utilities](#utilities) 19 | - [`requestProviders`](#requestproviders) 20 | - [`announceProvider`](#announceprovider) 21 | - [`window` Type Polyfill](#window-polyfill) 22 | - [Types](#types) 23 | - [Configuration](#configuration) 24 | 25 | ## Install 26 | 27 | ```bash 28 | npm i mipd 29 | ``` 30 | 31 | ```bash 32 | pnpm add mipd 33 | ``` 34 | 35 | ```bash 36 | yarn add mipd 37 | ``` 38 | 39 | ## Store 40 | 41 | The MIPD Store stores the Providers that have been emitted by a Wallet (or other source), and provides a way to subscribe to the store and retrieve the Providers. 42 | 43 | ### Overview 44 | 45 | ```ts 46 | import { createStore } from 'mipd' 47 | 48 | // Set up a MIPD Store, and request Providers. 49 | const store = createStore() 50 | 51 | // Subscribe to the MIPD Store. 52 | store.subscribe(providerDetails => { 53 | console.log(providerDetails) 54 | // => [EIP6963ProviderDetail, EIP6963ProviderDetail, ...] 55 | }) 56 | 57 | // Retrieve emitted Providers. 58 | store.getProviders() 59 | // => [EIP6963ProviderDetail, EIP6963ProviderDetail, ...] 60 | 61 | // Find a Provider Detail. 62 | store.findProvider({ rdns: 'com.example' }) 63 | // => EIP6963ProviderDetail | undefined 64 | 65 | // Clear the store, including all Providers. 66 | store.clear() 67 | 68 | // Reset the store, and emit an event to request Providers. 69 | store.reset() 70 | 71 | // Destroy the store, and remove all Providers and event listeners. 72 | store.destroy() 73 | ``` 74 | 75 | ### Usage 76 | 77 | #### Vanilla JS 78 | 79 | ```tsx 80 | import { createStore } from 'mipd' 81 | 82 | const store = createStore() 83 | 84 | let providers = store.getProviders() 85 | store.subscribe(providerDetails => (providers = providerDetails)) 86 | ``` 87 | 88 | #### React 89 | 90 | ```tsx 91 | import { useSyncExternalStore } from 'react' 92 | import { createStore } from 'mipd' 93 | 94 | const store = createStore() 95 | 96 | function Example() { 97 | const providers = useSyncExternalStore(store.subscribe, store.getProviders) 98 | // ... 99 | } 100 | ``` 101 | 102 | #### Svelte 103 | 104 | ```html 105 | 112 | 113 | 114 | ``` 115 | 116 | #### Vue 117 | 118 | ```html 119 | 127 | 128 | 129 | ``` 130 | 131 | ### API 132 | 133 | #### createStore() 134 | 135 | Creates a MIPD Store, and emits an event to request Providers from the Wallet(s). 136 | 137 | ```ts 138 | const store = createStore() 139 | ``` 140 | 141 | #### store.subscribe(listener, args) 142 | 143 | Subscribes to the MIPD Store, and returns a function to unsubscribe. 144 | 145 | ```ts 146 | const unsubscribe = store.subscribe(providers => { 147 | console.log(providers) 148 | // => [EIP6963ProviderDetail, EIP6963ProviderDetail, ...] 149 | }) 150 | ``` 151 | 152 | **Definition** 153 | 154 | ```ts 155 | export type Listener = ( 156 | // The updated Providers store. 157 | providerDetails: EIP6963ProviderDetail[], 158 | meta?: { 159 | // The Providers that were added to the store. 160 | added?: EIP6963ProviderDetail[] 161 | // The Providers that were removed from the store. 162 | removed?: EIP6963ProviderDetail[] 163 | }, 164 | ) => void 165 | 166 | function subscribe( 167 | // The listener function. 168 | listener: Listener, 169 | args?: { 170 | // If `true`, the listener will be called immediately with the stored Providers. 171 | emitImmediately?: boolean 172 | } 173 | ): () => void // Returns an unsubscribe function. 174 | ``` 175 | 176 | #### store.getProviders() 177 | 178 | Returns the current Providers. 179 | 180 | ```ts 181 | const providers = store.getProviders() 182 | // => [EIP6963ProviderDetail, EIP6963ProviderDetail, ...] 183 | ``` 184 | 185 | **Definition** 186 | 187 | ```ts 188 | function getProviders(): EIP6963ProviderDetail[] 189 | ``` 190 | 191 | #### store.findProvider(args) 192 | 193 | Finds a provider detail by its RDNS (Reverse Domain Name Identifier). 194 | 195 | ```ts 196 | const provider = store.findProvider({ rdns: 'com.example' }) 197 | ``` 198 | 199 | **Definition** 200 | 201 | ```ts 202 | function findProvider(args: { 203 | // The RDNS of the Provider Detail to find. 204 | rdns: string 205 | }): EIP6963ProviderDetail | undefined 206 | ``` 207 | 208 | #### store.clear() 209 | 210 | Clears the store, including all Providers. 211 | 212 | ```ts 213 | store.clear() 214 | ``` 215 | 216 | **Definition** 217 | 218 | ```ts 219 | function clear(): void 220 | ``` 221 | 222 | #### store.reset() 223 | 224 | Resets the store, and emits an event to request Providers from the Wallet(s). 225 | 226 | ```ts 227 | store.reset() 228 | ``` 229 | 230 | **Definition** 231 | 232 | ```ts 233 | function reset(): void 234 | ``` 235 | 236 | #### store.destroy() 237 | 238 | Destroys the store, and removes all Providers and event listeners. 239 | 240 | ```ts 241 | store.destroy() 242 | ``` 243 | 244 | **Definition** 245 | 246 | ```ts 247 | function destroy(): void 248 | ``` 249 | 250 | ## Utilities 251 | 252 | ### requestProviders 253 | 254 | The `requestProviders` utility emits an event to request Providers from the Wallet(s). It returns an `unsubscribe` function to clean up event listeners. 255 | 256 | ```ts 257 | import { requestProviders } from 'mipd' 258 | 259 | let providers = [] 260 | 261 | const unsubscribe = requestProviders(providerDetail => providers.push(providerDetail)) 262 | ``` 263 | 264 | **Definition** 265 | 266 | ```ts 267 | function requestProviders( 268 | // The listener. 269 | listener: (providerDetail: EIP6963ProviderDetail) => void 270 | // Unsubscribe function to clean up the listener. 271 | ): () => void 272 | ``` 273 | 274 | ### announceProvider 275 | 276 | The `announceProvider` utility emits an event to announce a Provider to the Wallet(s), and also listen for incoming requests. It returns an `unsubscribe` function to clean up event listeners. 277 | 278 | ```ts 279 | import { announceProvider } from 'mipd' 280 | 281 | const unsubscribe = announceProvider({ 282 | info: { 283 | icon: 'data:image/svg+xml,', 284 | name: 'Example', 285 | rdns: 'com.example', 286 | uuid: '00000000-0000-0000-0000-000000000000' 287 | }, 288 | provider: new EIP1193Provider() 289 | }) 290 | ``` 291 | 292 | **Definition** 293 | 294 | ```ts 295 | function requestProviders( 296 | // The EIP-1193 Provider and Provider Info. 297 | detail: EIP6963ProviderDetail 298 | // Unsubscribe function to clean up the listener. 299 | ): () => void 300 | ``` 301 | 302 | ## `window` Polyfill 303 | 304 | By importing the `mipd/window` Polyfill, the types on `window.addEventListener` will be inferred to include the `EIP6963AnnounceProviderEvent` and `EIP6963RequestProviderEvent` types. 305 | 306 | ```ts 307 | import 'mipd/window' 308 | 309 | window.addEventListener( 310 | 'eip6963:announceProvider' 311 | // ^? 'eip6963:announceProvider' | 'eip6963:requestProvider' | 'click' | ... 312 | 313 | event => { 314 | // ^? EIP6963AnnounceProviderEvent 315 | 316 | event.type 317 | // ^? 'eip6963:announceProvider' 318 | event.detail 319 | // ^? EIP6963ProviderDetail 320 | event.detail.info 321 | // ^? EIP6963ProviderInfo 322 | event.detail.provider 323 | // ^? EIP1193Provider 324 | } 325 | ) 326 | 327 | window.addEventListener( 328 | 'eip6963:requestProvider' 329 | // ^? 'eip6963:announceProvider' | 'eip6963:requestProvider' | 'click' | ... 330 | 331 | event => { 332 | // ^? EIP6963RequestProviderEvent 333 | 334 | event.type 335 | // ^? 'eip6963:requestProvider' 336 | } 337 | ) 338 | ``` 339 | 340 | ## Types 341 | 342 | ### EIP6963ProviderDetail 343 | 344 | Event detail from `eip6963:announceProvider`. 345 | 346 | #### Import 347 | 348 | ```ts 349 | import { type EIP6963ProviderDetail } from 'mipd' 350 | ``` 351 | 352 | #### Definition 353 | 354 | ```ts 355 | export interface EIP6963ProviderDetail { 356 | info: EIP6963ProviderInfo 357 | provider: TProvider 358 | } 359 | ``` 360 | 361 | ### EIP6963ProviderInfo 362 | 363 | Metadata of the EIP-1193 Provider. 364 | 365 | #### Import 366 | 367 | ```ts 368 | import { type EIP6963ProviderInfo } from 'mipd' 369 | ``` 370 | 371 | #### Definition 372 | 373 | ```ts 374 | export interface EIP6963ProviderInfo { 375 | icon: string 376 | name: string 377 | rdns?: ... | (string & {}) 378 | uuid: string 379 | } 380 | ``` 381 | 382 | ### EIP6963AnnounceProviderEvent 383 | 384 | Event type to announce an EIP-1193 Provider. 385 | 386 | #### Import 387 | 388 | ```ts 389 | import { type EIP6963AnnounceProviderEvent } from 'mipd' 390 | ``` 391 | 392 | #### Definition 393 | 394 | ```ts 395 | export interface EIP6963AnnounceProviderEvent 396 | extends CustomEvent> { 397 | type: 'eip6963:announceProvider' 398 | } 399 | ``` 400 | 401 | ### EIP6963RequestProviderEvent 402 | 403 | Event type to request EIP-1193 Providers. 404 | 405 | #### Import 406 | 407 | ```ts 408 | import { type EIP6963RequestProviderEvent } from 'mipd' 409 | ``` 410 | 411 | #### Definition 412 | 413 | ```ts 414 | export interface EIP6963RequestProviderEvent extends Event { 415 | type: 'eip6963:requestProvider' 416 | } 417 | ``` 418 | 419 | ## Configuration 420 | 421 | In some cases you might want to tune the global types (e.g. the `EIP1193Provider`). To do this, the following configuration options are available: 422 | 423 | | Type | Default | Description | 424 | | ------------------- | -------------------------------------------------------------- | ---------------------- | 425 | | `provider` | `import('viem').EIP1193Provider` | The EIP-1193 Provider. | 426 | | `rdns` | `'com.coinbase' \| 'com.enkrypt' \| 'io.metamask' \| 'io.zerion' \| (string & {})` | Deterministic identifier for the Provider in the form of an rDNS (Reverse Domain Name Notation) | 427 | 428 | Configuration options are customizable using [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). Extend the `Register` interface either directly in your code or in a `d.ts` file (e.g. `eip6963.d.ts`): 429 | 430 | ```ts 431 | import { type EIP1193Provider } from './eip1193-provider' 432 | 433 | declare module 'mipd' { 434 | interface Register { 435 | provider: EIP1193Provider 436 | } 437 | } 438 | ``` 439 | 440 | ## Authors 441 | 442 | - [@jxom](https://github.com/jxom) (jxom.eth, [Twitter](https://twitter.com/jakemoxey)) 443 | - [@tmm](https://github.com/tmm) (awkweb.eth, [Twitter](https://twitter.com/awkweb)) 444 | 445 | ## License 446 | 447 | [MIT](/LICENSE) License 448 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mipd", 3 | "description": "TypeScript Utilities for EIP-6963", 4 | "version": "0.0.7", 5 | "license": "MIT", 6 | "repository": "wagmi-dev/mipd", 7 | "scripts": { 8 | "build": "pnpm run clean && pnpm run build:cjs && pnpm run build:esm && pnpm run build:types", 9 | "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", 10 | "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm && echo > ./dist/esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'", 11 | "build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", 12 | "changeset": "changeset", 13 | "changeset:release": "pnpm build && changeset publish", 14 | "changeset:version": "changeset version && pnpm install --lockfile-only", 15 | "clean": "rimraf dist *.tsbuildinfo", 16 | "dev:react": "pnpm build && pnpm --filter vite-react dev", 17 | "dev:svelte": "pnpm build && pnpm --filter vite-svelte dev", 18 | "dev:vue": "pnpm build && pnpm --filter vite-vue dev", 19 | "format": "rome format . --write", 20 | "lint": "rome check .", 21 | "lint:fix": "pnpm lint --apply", 22 | "preinstall": "npx only-allow pnpm", 23 | "prepare": "npx simple-git-hooks", 24 | "prepublishOnly": "bun scripts/prepublishOnly.ts", 25 | "size": "size-limit", 26 | "test": "vitest dev", 27 | "test:cov": "vitest run --coverage", 28 | "test:typecheck": "vitest typecheck", 29 | "typecheck": "tsc --noEmit" 30 | }, 31 | "files": [ 32 | "dist", 33 | "!dist/**/*.tsbuildinfo", 34 | "src/**/*.ts", 35 | "!src/**/*.test.ts", 36 | "!src/**/*.test-d.ts" 37 | ], 38 | "sideEffects": false, 39 | "type": "module", 40 | "main": "./dist/cjs/index.js", 41 | "module": "./dist/esm/index.js", 42 | "types": "./dist/types/index.d.ts", 43 | "typings": "./dist/types/index.d.ts", 44 | "exports": { 45 | ".": { 46 | "types": "./dist/types/index.d.ts", 47 | "import": "./dist/esm/index.js", 48 | "default": "./dist/cjs/index.js" 49 | }, 50 | "./window": { 51 | "types": "./dist/types/window.d.ts", 52 | "import": "./dist/esm/window.js", 53 | "default": "./dist/cjs/window.js" 54 | }, 55 | "./package.json": "./package.json" 56 | }, 57 | "typesVersions": { 58 | "*": { 59 | "window": [ 60 | "./src/window.d.ts" 61 | ] 62 | } 63 | }, 64 | "peerDependencies": { 65 | "typescript": ">=5.0.4" 66 | }, 67 | "peerDependenciesMeta": { 68 | "typescript": { 69 | "optional": true 70 | } 71 | }, 72 | "devDependencies": { 73 | "@changesets/changelog-github": "^0.4.8", 74 | "@changesets/cli": "^2.26.1", 75 | "@size-limit/preset-small-lib": "^8.2.6", 76 | "@types/fs-extra": "^11.0.1", 77 | "@types/node": "^20.3.1", 78 | "@vitest/coverage-v8": "^0.32.2", 79 | "bun": "^0.5.9", 80 | "fs-extra": "^11.1.1", 81 | "jsdom": "^22.1.0", 82 | "rimraf": "^5.0.1", 83 | "rome": "~12.1.3", 84 | "simple-git-hooks": "^2.8.1", 85 | "size-limit": "^8.2.6", 86 | "typescript": "^5.0.4", 87 | "viem": "^2.9.1", 88 | "vite": "^4.3.9", 89 | "vitest": "^0.32.2" 90 | }, 91 | "contributors": [ 92 | "jxom.eth ", 93 | "awkweb.eth " 94 | ], 95 | "funding": [ 96 | { 97 | "type": "github", 98 | "url": "https://github.com/sponsors/wagmi-dev" 99 | } 100 | ], 101 | "keywords": [ 102 | "eth", 103 | "ethereum", 104 | "dapps", 105 | "wallet", 106 | "web3", 107 | "eip", 108 | "6963" 109 | ], 110 | "simple-git-hooks": { 111 | "pre-commit": "pnpm format && pnpm lint:fix" 112 | }, 113 | "packageManager": "pnpm@8.3.1", 114 | "size-limit": [ 115 | { 116 | "name": "import { createStore } from 'mipd'", 117 | "path": "./dist/esm/index.js", 118 | "limit": "400 B", 119 | "import": "{ createStore }" 120 | }, 121 | { 122 | "name": "import { requestProviders } from 'mipd'", 123 | "path": "./dist/esm/index.js", 124 | "limit": "129 B", 125 | "import": "{ requestProviders }" 126 | }, 127 | { 128 | "name": "import { announceProvider } from 'mipd'", 129 | "path": "./dist/esm/index.js", 130 | "limit": "132 B", 131 | "import": "{ announceProvider }" 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /playgrounds/vite-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playgrounds/vite-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "mipd": "workspace:*", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.37", 18 | "@types/react-dom": "^18.0.11", 19 | "@vitejs/plugin-react": "^4.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playgrounds/vite-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'mipd' 2 | import { useSyncExternalStore } from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | 5 | const store = createStore() 6 | 7 | export default function App() { 8 | const providers = useSyncExternalStore(store.subscribe, store.getProviders) 9 | return
{JSON.stringify(providers, null, 2)}
10 | } 11 | 12 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /playgrounds/vite-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playgrounds/vite-react/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import { EIP1193Provider, announceProvider } from 'mipd' 2 | import 'mipd/window' 3 | 4 | announceProvider({ 5 | info: { 6 | icon: 'data:image/svg+xml,', 7 | name: 'Example Wallet', 8 | rdns: 'org.example', 9 | uuid: '350670db-19fa-4704-a166-e52e178b59d1', 10 | }, 11 | provider: '' as unknown as EIP1193Provider, 12 | }) 13 | 14 | announceProvider({ 15 | info: { 16 | icon: 'data:image/svg+xml,', 17 | name: 'Foo Wallet', 18 | rdns: 'org.foo', 19 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 20 | }, 21 | provider: '' as unknown as EIP1193Provider, 22 | }) 23 | 24 | await new Promise((res) => setTimeout(res, 1000)) 25 | 26 | announceProvider({ 27 | info: { 28 | icon: 'data:image/svg+xml,', 29 | name: 'Bar Wallet', 30 | rdns: 'io.bar', 31 | uuid: '350670db-19fa-4704-a166-e52e178b59d3', 32 | }, 33 | provider: '' as unknown as EIP1193Provider, 34 | }) 35 | 36 | await new Promise((res) => setTimeout(res, 1000)) 37 | 38 | announceProvider({ 39 | info: { 40 | icon: 'data:image/svg+xml,', 41 | name: 'Baz Wallet', 42 | rdns: 'com.baz', 43 | uuid: '350670db-19fa-4704-a166-e52e178b59d4', 44 | }, 45 | provider: '' as unknown as EIP1193Provider, 46 | }) 47 | -------------------------------------------------------------------------------- /playgrounds/vite-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /playgrounds/vite-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /playgrounds/vite-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | 20 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 21 | 22 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 23 | 24 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 25 | 26 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 27 | 28 | **Why include `.vscode/extensions.json`?** 29 | 30 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 31 | 32 | **Why enable `allowJs` in the TS template?** 33 | 34 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 35 | 36 | **Why is HMR not preserving my local component state?** 37 | 38 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 39 | 40 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 41 | 42 | ```ts 43 | // store.ts 44 | // An extremely simple external store 45 | import { writable } from 'svelte/store' 46 | export default writable(0) 47 | ``` 48 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Svelte + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-svelte", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "dependencies": { 13 | "mipd": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/vite-plugin-svelte": "^2.0.4", 17 | "@tsconfig/svelte": "^4.0.1", 18 | "svelte": "^3.58.0", 19 | "svelte-check": "^3.3.1", 20 | "tslib": "^2.5.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/src/App.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
{JSON.stringify($providers, null, 2)}
9 | 10 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.getElementById('app'), 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import { EIP1193Provider, announceProvider } from 'mipd' 2 | import 'mipd/window' 3 | 4 | announceProvider({ 5 | info: { 6 | icon: 'data:image/svg+xml,', 7 | name: 'Example Wallet', 8 | rdns: 'org.example', 9 | uuid: '350670db-19fa-4704-a166-e52e178b59d1', 10 | }, 11 | provider: '' as unknown as EIP1193Provider, 12 | }) 13 | 14 | announceProvider({ 15 | info: { 16 | icon: 'data:image/svg+xml,', 17 | name: 'Foo Wallet', 18 | rdns: 'org.foo', 19 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 20 | }, 21 | provider: '' as unknown as EIP1193Provider, 22 | }) 23 | 24 | await new Promise((res) => setTimeout(res, 1000)) 25 | 26 | announceProvider({ 27 | info: { 28 | icon: 'data:image/svg+xml,', 29 | name: 'Bar Wallet', 30 | rdns: 'io.bar', 31 | uuid: '350670db-19fa-4704-a166-e52e178b59d3', 32 | }, 33 | provider: '' as unknown as EIP1193Provider, 34 | }) 35 | 36 | await new Promise((res) => setTimeout(res, 1000)) 37 | 38 | announceProvider({ 39 | info: { 40 | icon: 'data:image/svg+xml,', 41 | name: 'Baz Wallet', 42 | rdns: 'com.baz', 43 | uuid: '350670db-19fa-4704-a166-e52e178b59d4', 44 | }, 45 | provider: '' as unknown as EIP1193Provider, 46 | }) 47 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /playgrounds/vite-svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { svelte } from '@sveltejs/vite-plugin-svelte' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()], 7 | }) 8 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "mipd": "workspace:*", 13 | "vue": "^3.2.47" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^4.1.0", 17 | "vue-tsc": "^1.4.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import { createApp } from 'vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import { EIP1193Provider, announceProvider } from 'mipd' 2 | import 'mipd/window' 3 | 4 | announceProvider({ 5 | info: { 6 | icon: 'data:image/svg+xml,', 7 | name: 'Example Wallet', 8 | rdns: 'org.example', 9 | uuid: '350670db-19fa-4704-a166-e52e178b59d1', 10 | }, 11 | provider: '' as unknown as EIP1193Provider, 12 | }) 13 | 14 | announceProvider({ 15 | info: { 16 | icon: 'data:image/svg+xml,', 17 | name: 'Foo Wallet', 18 | rdns: 'org.foo', 19 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 20 | }, 21 | provider: '' as unknown as EIP1193Provider, 22 | }) 23 | 24 | await new Promise((res) => setTimeout(res, 1000)) 25 | 26 | announceProvider({ 27 | info: { 28 | icon: 'data:image/svg+xml,', 29 | name: 'Bar Wallet', 30 | rdns: 'io.bar', 31 | uuid: '350670db-19fa-4704-a166-e52e178b59d3', 32 | }, 33 | provider: '' as unknown as EIP1193Provider, 34 | }) 35 | 36 | await new Promise((res) => setTimeout(res, 1000)) 37 | 38 | announceProvider({ 39 | info: { 40 | icon: 'data:image/svg+xml,', 41 | name: 'Baz Wallet', 42 | rdns: 'com.baz', 43 | uuid: '350670db-19fa-4704-a166-e52e178b59d4', 44 | }, 45 | provider: '' as unknown as EIP1193Provider, 46 | }) 47 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /playgrounds/vite-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages: true 2 | packages: 3 | - '.' 4 | - 'playgrounds/*' 5 | -------------------------------------------------------------------------------- /rome.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "ignore": [ 4 | "coverage", 5 | "dist", 6 | "tsconfig.json", 7 | "tsconfig.*.json", 8 | "tsconfig.node.json" 9 | ] 10 | }, 11 | "formatter": { 12 | "enabled": true, 13 | "formatWithErrors": false, 14 | "indentStyle": "space", 15 | "indentSize": 2, 16 | "lineWidth": 80 17 | }, 18 | "linter": { 19 | "enabled": true, 20 | "rules": { 21 | "recommended": true, 22 | "correctness": { 23 | "noUnusedVariables": "error" 24 | }, 25 | "performance": { 26 | "noDelete": "off" 27 | }, 28 | "style": { 29 | "noNonNullAssertion": "off", 30 | "useShorthandArrayType": "error" 31 | }, 32 | "suspicious": { 33 | "noArrayIndexKey": "off", 34 | "noExplicitAny": "off" 35 | } 36 | } 37 | }, 38 | "javascript": { 39 | "formatter": { 40 | "quoteStyle": "single", 41 | "trailingComma": "all", 42 | "semicolons": "asNeeded" 43 | } 44 | }, 45 | "organizeImports": { 46 | "enabled": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/prepublishOnly.ts: -------------------------------------------------------------------------------- 1 | import { readJsonSync, writeJsonSync } from 'fs-extra' 2 | import path from 'path' 3 | 4 | // Generates a package.json to be published to NPM with only the necessary fields. 5 | const packageJsonPath = path.join(__dirname, '../package.json') 6 | const tmpPackageJson = readJsonSync(packageJsonPath) 7 | 8 | writeJsonSync(`${packageJsonPath}.tmp`, tmpPackageJson, { spaces: 2 }) 9 | 10 | const { 11 | ['simple-git-hooks']: _sgh, 12 | ['size-limit']: _sL, 13 | devDependencies: _dD, 14 | packageManager: _pM, 15 | scripts: _s, 16 | // NOTE: We explicitly don't want to publish the `type` field. We create a separate package.json for `dist/cjs` and `dist/esm` that has the type field. 17 | type: _t, 18 | ...rest 19 | } = tmpPackageJson 20 | writeJsonSync(packageJsonPath, rest, { spaces: 2 }) 21 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | 3 | import * as Exports from './index.js' 4 | 5 | it('should expose correct exports', () => { 6 | expect(Object.keys(Exports)).toMatchInlineSnapshot(` 7 | [ 8 | "createStore", 9 | "announceProvider", 10 | "requestProviders", 11 | ] 12 | `) 13 | }) 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | DefaultRegister, 3 | EIP1193Provider, 4 | Rdns, 5 | Register, 6 | ResolvedRegister, 7 | } from './register.js' 8 | 9 | export { 10 | createStore, 11 | type Listener, 12 | type Store, 13 | } from './store.js' 14 | 15 | export type { 16 | EIP6963AnnounceProviderEvent, 17 | EIP6963ProviderDetail, 18 | EIP6963ProviderInfo, 19 | EIP6963RequestProviderEvent, 20 | } from './types.js' 21 | 22 | export { 23 | type AnnounceProviderParameters, 24 | type AnnounceProviderReturnType, 25 | announceProvider, 26 | type RequestProvidersParameters, 27 | type RequestProvidersReturnType, 28 | requestProviders, 29 | } from './utils.js' 30 | -------------------------------------------------------------------------------- /src/register.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Override `Register` to customize type options 3 | * 4 | * @example 5 | * import { type EIP1193Provider } from './eip1193' 6 | * 7 | * declare module 'mipd' { 8 | * export interface Register { 9 | * provider: EIP1193Provider 10 | * } 11 | * } 12 | */ 13 | // rome-ignore lint/suspicious/noEmptyInterface: For extending global type 14 | export interface Register {} 15 | 16 | export type DefaultRegister = { 17 | /** The EIP-1193 Provider. */ 18 | provider: import('viem').EIP1193Provider 19 | /** Reverse Domain Name Notation (rDNS) of the Wallet Provider. */ 20 | rdns: 'com.coinbase' | 'com.enkrypt' | 'io.metamask' | 'io.zerion' 21 | } 22 | 23 | export type ResolvedRegister = { 24 | /** The EIP-1193 Provider. */ 25 | provider: Register extends { 26 | provider: infer provider extends DefaultRegister['provider'] 27 | } 28 | ? provider 29 | : DefaultRegister['provider'] 30 | /** Reverse Domain Name Notation (rDNS) of the Wallet Provider. */ 31 | rdns: Register extends { rdns: infer rdns extends string } 32 | ? rdns 33 | : DefaultRegister['rdns'] | (string & {}) // loose autocomplete 34 | } 35 | 36 | export type EIP1193Provider = ResolvedRegister['provider'] 37 | export type Rdns = ResolvedRegister['rdns'] 38 | -------------------------------------------------------------------------------- /src/store.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | import type { EIP1193Provider } from 'viem' 3 | import { describe, expect, test } from 'vitest' 4 | 5 | import { type Listener, createStore } from './store.js' 6 | import type { EIP6963ProviderDetail } from './types.js' 7 | import { announceProvider } from './utils.js' 8 | 9 | const detail_1 = { 10 | info: { 11 | icon: 'data:image/svg+xml,', 12 | name: 'Example Wallet', 13 | rdns: 'org.example', 14 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 15 | }, 16 | provider: '' as unknown as EIP1193Provider, 17 | } as const satisfies EIP6963ProviderDetail 18 | 19 | const detail_2 = { 20 | info: { 21 | icon: 'data:image/svg+xml,', 22 | name: 'Foo Wallet', 23 | rdns: 'org.foo', 24 | uuid: '12345555-19fa-4704-a166-e52e178b59d2', 25 | }, 26 | provider: '' as unknown as EIP1193Provider, 27 | } as const satisfies EIP6963ProviderDetail 28 | 29 | describe('createStore', () => { 30 | test('_listeners', () => { 31 | const store = createStore() 32 | 33 | const unsubscribe_1 = store.subscribe(() => {}) 34 | expect(store._listeners().size).toBe(1) 35 | 36 | const unsubscribe_2 = store.subscribe(() => {}) 37 | store.subscribe(() => {}) 38 | expect(store._listeners().size).toBe(3) 39 | 40 | store.subscribe(() => {}) 41 | expect(store._listeners().size).toBe(4) 42 | 43 | unsubscribe_1() 44 | expect(store._listeners().size).toBe(3) 45 | unsubscribe_2() 46 | expect(store._listeners().size).toBe(2) 47 | 48 | store.destroy() 49 | expect(store._listeners().size).toBe(0) 50 | }) 51 | 52 | test('clear', () => { 53 | const store = createStore() 54 | 55 | store.subscribe(() => {}) 56 | store.subscribe(() => {}) 57 | announceProvider(detail_1)() 58 | announceProvider(detail_2)() 59 | expect(store.getProviders().length).toBe(2) 60 | 61 | store.clear() 62 | expect(store.getProviders().length).toBe(0) 63 | }) 64 | 65 | test('destroy', () => { 66 | const store = createStore() 67 | 68 | // Setting up listeners and announcing some provider details. 69 | store.subscribe(() => {}) 70 | store.subscribe(() => {}) 71 | store.subscribe(() => {}) 72 | store.subscribe(() => {}) 73 | announceProvider(detail_1)() 74 | announceProvider(detail_2)() 75 | expect(store.getProviders().length).toBe(2) 76 | expect(store._listeners().size).toBe(4) 77 | 78 | // Destroying should clear listeners and provider details. 79 | store.destroy() 80 | expect(store.getProviders().length).toBe(0) 81 | expect(store._listeners().size).toBe(0) 82 | 83 | announceProvider(detail_1)() 84 | announceProvider(detail_2)() 85 | 86 | expect(store.getProviders().length).toBe(0) 87 | expect(store._listeners().size).toBe(0) 88 | 89 | // Resetting after destroy should work and emit provider details. 90 | store.reset() 91 | 92 | store.subscribe(() => {}) 93 | store.subscribe(() => {}) 94 | announceProvider(detail_1)() 95 | announceProvider(detail_2)() 96 | 97 | expect(store.getProviders().length).toBe(2) 98 | expect(store._listeners().size).toBe(2) 99 | 100 | store.destroy() 101 | }) 102 | 103 | test('find', () => { 104 | const store = createStore() 105 | 106 | announceProvider(detail_1)() 107 | expect(store.findProvider({ rdns: 'org.example' })).toBe(detail_1) 108 | expect(store.findProvider({ rdns: 'org.foo' })).toBe(undefined) 109 | 110 | announceProvider(detail_2)() 111 | expect(store.findProvider({ rdns: 'org.foo' })).toBe(detail_2) 112 | 113 | store.destroy() 114 | }) 115 | 116 | test('get', async () => { 117 | const store = createStore() 118 | 119 | announceProvider(detail_1)() 120 | expect(store.getProviders()).toMatchInlineSnapshot(` 121 | [ 122 | { 123 | "info": { 124 | "icon": "data:image/svg+xml,", 125 | "name": "Example Wallet", 126 | "rdns": "org.example", 127 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 128 | }, 129 | "provider": "", 130 | }, 131 | ] 132 | `) 133 | 134 | await new Promise((resolve) => setTimeout(resolve, 1000)) 135 | announceProvider(detail_2)() 136 | expect(store.getProviders()).toMatchInlineSnapshot(` 137 | [ 138 | { 139 | "info": { 140 | "icon": "data:image/svg+xml,", 141 | "name": "Example Wallet", 142 | "rdns": "org.example", 143 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 144 | }, 145 | "provider": "", 146 | }, 147 | { 148 | "info": { 149 | "icon": "data:image/svg+xml,", 150 | "name": "Foo Wallet", 151 | "rdns": "org.foo", 152 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 153 | }, 154 | "provider": "", 155 | }, 156 | ] 157 | `) 158 | 159 | // Test duplicate emitted detail 160 | announceProvider(detail_2)() 161 | expect(store.getProviders()).toMatchInlineSnapshot(` 162 | [ 163 | { 164 | "info": { 165 | "icon": "data:image/svg+xml,", 166 | "name": "Example Wallet", 167 | "rdns": "org.example", 168 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 169 | }, 170 | "provider": "", 171 | }, 172 | { 173 | "info": { 174 | "icon": "data:image/svg+xml,", 175 | "name": "Foo Wallet", 176 | "rdns": "org.foo", 177 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 178 | }, 179 | "provider": "", 180 | }, 181 | ] 182 | `) 183 | 184 | return store.destroy() 185 | }) 186 | 187 | test('reset', () => { 188 | const store = createStore() 189 | 190 | announceProvider(detail_1)() 191 | announceProvider(detail_2)() 192 | expect(store.getProviders().length).toBe(2) 193 | 194 | store.reset() 195 | expect(store.getProviders().length).toBe(0) 196 | 197 | store.destroy() 198 | }) 199 | 200 | test('subscribe', async () => { 201 | const store = createStore() 202 | 203 | let results: Parameters[] = [] 204 | const unsubscribe = store.subscribe((providerDetails, providerDetail) => 205 | results.push([providerDetails, providerDetail]), 206 | ) 207 | 208 | announceProvider(detail_1)() 209 | await new Promise((resolve) => setTimeout(resolve, 1000)) 210 | announceProvider(detail_2)() 211 | 212 | // Test duplicate emitted detail 213 | announceProvider(detail_2)() 214 | 215 | expect(results.length).toBe(2) 216 | expect(results[0]).toMatchInlineSnapshot(` 217 | [ 218 | [ 219 | { 220 | "info": { 221 | "icon": "data:image/svg+xml,", 222 | "name": "Example Wallet", 223 | "rdns": "org.example", 224 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 225 | }, 226 | "provider": "", 227 | }, 228 | ], 229 | { 230 | "added": [ 231 | { 232 | "info": { 233 | "icon": "data:image/svg+xml,", 234 | "name": "Example Wallet", 235 | "rdns": "org.example", 236 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 237 | }, 238 | "provider": "", 239 | }, 240 | ], 241 | }, 242 | ] 243 | `) 244 | expect(results[1]).toMatchInlineSnapshot(` 245 | [ 246 | [ 247 | { 248 | "info": { 249 | "icon": "data:image/svg+xml,", 250 | "name": "Example Wallet", 251 | "rdns": "org.example", 252 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 253 | }, 254 | "provider": "", 255 | }, 256 | { 257 | "info": { 258 | "icon": "data:image/svg+xml,", 259 | "name": "Foo Wallet", 260 | "rdns": "org.foo", 261 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 262 | }, 263 | "provider": "", 264 | }, 265 | ], 266 | { 267 | "added": [ 268 | { 269 | "info": { 270 | "icon": "data:image/svg+xml,", 271 | "name": "Foo Wallet", 272 | "rdns": "org.foo", 273 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 274 | }, 275 | "provider": "", 276 | }, 277 | ], 278 | }, 279 | ] 280 | `) 281 | 282 | results = [] 283 | 284 | store.destroy() 285 | expect(results).toMatchInlineSnapshot(` 286 | [ 287 | [ 288 | [], 289 | { 290 | "removed": [ 291 | { 292 | "info": { 293 | "icon": "data:image/svg+xml,", 294 | "name": "Example Wallet", 295 | "rdns": "org.example", 296 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 297 | }, 298 | "provider": "", 299 | }, 300 | { 301 | "info": { 302 | "icon": "data:image/svg+xml,", 303 | "name": "Foo Wallet", 304 | "rdns": "org.foo", 305 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 306 | }, 307 | "provider": "", 308 | }, 309 | ], 310 | }, 311 | ], 312 | ] 313 | `) 314 | 315 | results = [] 316 | 317 | unsubscribe() 318 | 319 | announceProvider(detail_1)() 320 | announceProvider(detail_2)() 321 | 322 | expect(results).toHaveLength(0) 323 | 324 | store.destroy() 325 | }) 326 | 327 | test('subscribe (emitImmediately)', () => { 328 | const store = createStore() 329 | 330 | announceProvider(detail_1)() 331 | announceProvider(detail_2)() 332 | 333 | const results: Parameters[] = [] 334 | store.subscribe((providerDetails, providerDetail) => 335 | results.push([providerDetails, providerDetail]), 336 | ) 337 | 338 | expect(results.length).toBe(0) 339 | 340 | store.subscribe( 341 | (providerDetails, providerDetail) => 342 | results.push([providerDetails, providerDetail]), 343 | { emitImmediately: true }, 344 | ) 345 | 346 | expect(results.length).toBe(1) 347 | expect(results).toMatchInlineSnapshot(` 348 | [ 349 | [ 350 | [ 351 | { 352 | "info": { 353 | "icon": "data:image/svg+xml,", 354 | "name": "Example Wallet", 355 | "rdns": "org.example", 356 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 357 | }, 358 | "provider": "", 359 | }, 360 | { 361 | "info": { 362 | "icon": "data:image/svg+xml,", 363 | "name": "Foo Wallet", 364 | "rdns": "org.foo", 365 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 366 | }, 367 | "provider": "", 368 | }, 369 | ], 370 | { 371 | "added": [ 372 | { 373 | "info": { 374 | "icon": "data:image/svg+xml,", 375 | "name": "Example Wallet", 376 | "rdns": "org.example", 377 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 378 | }, 379 | "provider": "", 380 | }, 381 | { 382 | "info": { 383 | "icon": "data:image/svg+xml,", 384 | "name": "Foo Wallet", 385 | "rdns": "org.foo", 386 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 387 | }, 388 | "provider": "", 389 | }, 390 | ], 391 | }, 392 | ], 393 | ] 394 | `) 395 | }) 396 | }) 397 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import type { Rdns } from './register.js' 2 | import type { EIP6963ProviderDetail } from './types.js' 3 | import { requestProviders } from './utils.js' 4 | 5 | export type Listener = ( 6 | providerDetails: readonly EIP6963ProviderDetail[], 7 | meta?: 8 | | { 9 | added?: readonly EIP6963ProviderDetail[] | undefined 10 | removed?: readonly EIP6963ProviderDetail[] | undefined 11 | } 12 | | undefined, 13 | ) => void 14 | 15 | export type Store = { 16 | /** 17 | * Clears the store, including all provider details. 18 | */ 19 | clear(): void 20 | /** 21 | * Destroys the store, including all provider details and listeners. 22 | */ 23 | destroy(): void 24 | /** 25 | * Finds a provider detail by its RDNS (Reverse Domain Name Identifier). 26 | */ 27 | findProvider(args: { rdns: Rdns }): EIP6963ProviderDetail | undefined 28 | /** 29 | * Returns all provider details that have been emitted. 30 | */ 31 | getProviders(): readonly EIP6963ProviderDetail[] 32 | /** 33 | * Resets the store, and emits an event to request provider details. 34 | */ 35 | reset(): void 36 | /** 37 | * Subscribes to emitted provider details. 38 | */ 39 | subscribe( 40 | listener: Listener, 41 | args?: { emitImmediately?: boolean | undefined } | undefined, 42 | ): () => void 43 | 44 | /** 45 | * @internal 46 | * Current state of listening listeners. 47 | */ 48 | _listeners(): Set 49 | } 50 | 51 | export function createStore(): Store { 52 | const listeners: Set = new Set() 53 | let providerDetails: readonly EIP6963ProviderDetail[] = [] 54 | 55 | const request = () => 56 | requestProviders((providerDetail) => { 57 | if ( 58 | providerDetails.some( 59 | ({ info }) => info.uuid === providerDetail.info.uuid, 60 | ) 61 | ) 62 | return 63 | 64 | providerDetails = [...providerDetails, providerDetail] 65 | listeners.forEach((listener) => 66 | listener(providerDetails, { added: [providerDetail] }), 67 | ) 68 | }) 69 | let unwatch = request() 70 | 71 | return { 72 | _listeners() { 73 | return listeners 74 | }, 75 | clear() { 76 | listeners.forEach((listener) => 77 | listener([], { removed: [...providerDetails] }), 78 | ) 79 | providerDetails = [] 80 | }, 81 | destroy() { 82 | this.clear() 83 | listeners.clear() 84 | unwatch?.() 85 | }, 86 | findProvider({ rdns }) { 87 | return providerDetails.find( 88 | (providerDetail) => providerDetail.info.rdns === rdns, 89 | ) 90 | }, 91 | getProviders() { 92 | return providerDetails 93 | }, 94 | reset() { 95 | this.clear() 96 | unwatch?.() 97 | unwatch = request() 98 | }, 99 | subscribe(listener, { emitImmediately } = {}) { 100 | listeners.add(listener) 101 | if (emitImmediately) listener(providerDetails, { added: providerDetails }) 102 | return () => listeners.delete(listener) 103 | }, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/types.test-d.ts: -------------------------------------------------------------------------------- 1 | import type { EIP1193Provider } from 'viem' 2 | import { expectTypeOf, test } from 'vitest' 3 | 4 | import type { 5 | EIP6963AnnounceProviderEvent, 6 | EIP6963ProviderDetail, 7 | EIP6963ProviderInfo, 8 | EIP6963RequestProviderEvent, 9 | } from './index.js' 10 | 11 | test('EIP6963ProviderInfo', () => { 12 | expectTypeOf().toEqualTypeOf 13 | expectTypeOf().toEqualTypeOf 14 | expectTypeOf().toEqualTypeOf< 15 | undefined | 'com.enkrypt' | 'io.metamask' | (string & {}) 16 | > 17 | expectTypeOf().toEqualTypeOf 18 | 19 | const KnownRdns_1: EIP6963ProviderInfo = { 20 | icon: 'data:image/svg+xml,', 21 | name: 'MetaMask', 22 | rdns: 'io.metamask', 23 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 24 | } 25 | KnownRdns_1 26 | 27 | const UnknownRdns_1: EIP6963ProviderInfo = { 28 | icon: 'data:image/svg+xml,', 29 | name: 'Example Wallet', 30 | rdns: 'org.example', 31 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 32 | } 33 | UnknownRdns_1 34 | }) 35 | 36 | test('EIP6963ProviderDetail', () => { 37 | expectTypeOf() 38 | .toEqualTypeOf 39 | expectTypeOf() 40 | .toEqualTypeOf 41 | 42 | type CustomProvider = { request: () => void } 43 | expectTypeOf['provider']>() 44 | .toEqualTypeOf 45 | }) 46 | 47 | test('EIP6963AnnounceProviderEvent', () => { 48 | expectTypeOf().toEqualTypeOf< 49 | EIP6963ProviderDetail 50 | >() 51 | expectTypeOf< 52 | EIP6963AnnounceProviderEvent['type'] 53 | >().toEqualTypeOf<'eip6963:announceProvider'>() 54 | 55 | type CustomProvider = { request: () => void } 56 | expectTypeOf< 57 | EIP6963AnnounceProviderEvent['detail'] 58 | >().toEqualTypeOf>() 59 | }) 60 | 61 | test('EIP6963RequestProviderEvent', () => { 62 | expectTypeOf< 63 | EIP6963RequestProviderEvent['type'] 64 | >().toEqualTypeOf<'eip6963:requestProvider'>() 65 | }) 66 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EIP1193Provider, Rdns } from './register.js' 2 | 3 | /** 4 | * Event detail from the `"eip6963:announceProvider"` event. 5 | */ 6 | export interface EIP6963ProviderDetail< 7 | TProvider = EIP1193Provider, 8 | TRdns extends string = Rdns, 9 | > { 10 | info: EIP6963ProviderInfo 11 | provider: TProvider 12 | } 13 | 14 | /** 15 | * Metadata of the EIP-1193 Provider. 16 | */ 17 | export interface EIP6963ProviderInfo { 18 | icon: `data:image/${string}` // RFC-2397 19 | name: string 20 | rdns: TRdns 21 | uuid: string 22 | } 23 | 24 | /** 25 | * Event type to announce an EIP-1193 Provider. 26 | */ 27 | export interface EIP6963AnnounceProviderEvent 28 | extends CustomEvent> { 29 | type: 'eip6963:announceProvider' 30 | } 31 | 32 | /** 33 | * Event type to request EIP-1193 Providers. 34 | */ 35 | export interface EIP6963RequestProviderEvent extends Event { 36 | type: 'eip6963:requestProvider' 37 | } 38 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | import type { EIP1193Provider } from 'viem' 3 | import { expect, test } from 'vitest' 4 | 5 | import type { EIP6963ProviderDetail } from './types.js' 6 | import { announceProvider, requestProviders } from './utils.js' 7 | 8 | const detail_1 = { 9 | info: { 10 | icon: 'data:image/svg+xml,', 11 | name: 'Example Wallet', 12 | rdns: 'org.example', 13 | uuid: '350670db-19fa-4704-a166-e52e178b59d2', 14 | }, 15 | provider: '' as unknown as EIP1193Provider, 16 | } as const satisfies EIP6963ProviderDetail 17 | 18 | const detail_2 = { 19 | info: { 20 | icon: 'data:image/svg+xml,', 21 | name: 'Foo Wallet', 22 | rdns: 'org.foo', 23 | uuid: '12345555-19fa-4704-a166-e52e178b59d2', 24 | }, 25 | provider: '' as unknown as EIP1193Provider, 26 | } as const satisfies EIP6963ProviderDetail 27 | 28 | test('requestProviders', async () => { 29 | const results: EIP6963ProviderDetail[] = [] 30 | const unsubscribe = requestProviders((providerDetail) => { 31 | results.push(providerDetail) 32 | }) 33 | 34 | announceProvider(detail_1)() 35 | await new Promise((resolve) => setTimeout(resolve, 1000)) 36 | announceProvider(detail_2)() 37 | 38 | expect(results.length).toBe(2) 39 | expect(results[0]).toMatchInlineSnapshot(` 40 | { 41 | "info": { 42 | "icon": "data:image/svg+xml,", 43 | "name": "Example Wallet", 44 | "rdns": "org.example", 45 | "uuid": "350670db-19fa-4704-a166-e52e178b59d2", 46 | }, 47 | "provider": "", 48 | } 49 | `) 50 | expect(results[1]).toMatchInlineSnapshot(` 51 | { 52 | "info": { 53 | "icon": "data:image/svg+xml,", 54 | "name": "Foo Wallet", 55 | "rdns": "org.foo", 56 | "uuid": "12345555-19fa-4704-a166-e52e178b59d2", 57 | }, 58 | "provider": "", 59 | } 60 | `) 61 | 62 | unsubscribe?.() 63 | }) 64 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { EIP1193Provider } from './register.js' 2 | import type { 3 | EIP6963AnnounceProviderEvent, 4 | EIP6963ProviderDetail, 5 | } from './types.js' 6 | 7 | //////////////////////////////////////////////////////////////////////////// 8 | // Announce Provider 9 | 10 | export type AnnounceProviderParameters = EIP6963ProviderDetail< 11 | EIP1193Provider, 12 | string 13 | > 14 | export type AnnounceProviderReturnType = () => void 15 | 16 | /** 17 | * Announces an EIP-1193 Provider. 18 | */ 19 | export function announceProvider( 20 | detail: AnnounceProviderParameters, 21 | ): AnnounceProviderReturnType { 22 | const event: CustomEvent = new CustomEvent( 23 | 'eip6963:announceProvider', 24 | { detail: Object.freeze(detail) }, 25 | ) 26 | 27 | window.dispatchEvent(event) 28 | 29 | const handler = () => window.dispatchEvent(event) 30 | window.addEventListener('eip6963:requestProvider', handler) 31 | return () => window.removeEventListener('eip6963:requestProvider', handler) 32 | } 33 | 34 | //////////////////////////////////////////////////////////////////////////// 35 | // Request Providers 36 | 37 | export type RequestProvidersParameters = ( 38 | providerDetail: EIP6963ProviderDetail, 39 | ) => void 40 | export type RequestProvidersReturnType = (() => void) | undefined 41 | 42 | /** 43 | * Watches for EIP-1193 Providers to be announced. 44 | */ 45 | export function requestProviders( 46 | listener: RequestProvidersParameters, 47 | ): RequestProvidersReturnType { 48 | if (typeof window === 'undefined') return 49 | const handler = (event: EIP6963AnnounceProviderEvent) => 50 | listener(event.detail) 51 | 52 | window.addEventListener('eip6963:announceProvider', handler) 53 | 54 | window.dispatchEvent(new CustomEvent('eip6963:requestProvider')) 55 | 56 | return () => window.removeEventListener('eip6963:announceProvider', handler) 57 | } 58 | -------------------------------------------------------------------------------- /src/window.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EIP6963AnnounceProviderEvent, 3 | EIP6963RequestProviderEvent, 4 | } from './types.js' 5 | 6 | export {} 7 | 8 | declare global { 9 | interface WindowEventMap { 10 | 'eip6963:announceProvider': EIP6963AnnounceProviderEvent 11 | 'eip6963:requestProvider': EIP6963RequestProviderEvent 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | // This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config. 3 | "include": [], 4 | "compilerOptions": { 5 | // Incremental builds 6 | // NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes. 7 | "incremental": true, 8 | 9 | // Type checking 10 | "strict": true, 11 | "useDefineForClassFields": true, // Not enabled by default in `strict` mode unless we bump `target` to ES2022. 12 | "noFallthroughCasesInSwitch": true, // Not enabled by default in `strict` mode. 13 | "noImplicitReturns": true, // Not enabled by default in `strict` mode. 14 | "useUnknownInCatchVariables": true, // TODO: This would normally be enabled in `strict` mode but would require some adjustments to the codebase. 15 | "noImplicitOverride": true, // Not enabled by default in `strict` mode. 16 | "noUnusedLocals": true, // Not enabled by default in `strict` mode. 17 | "noUnusedParameters": true, // Not enabled by default in `strict` mode. 18 | 19 | // JavaScript support 20 | "allowJs": false, 21 | "checkJs": false, 22 | 23 | // Interop constraints 24 | "esModuleInterop": false, 25 | "allowSyntheticDefaultImports": false, 26 | "forceConsistentCasingInFileNames": true, 27 | "verbatimModuleSyntax": true, 28 | "importHelpers": true, // This is only used for build validation. Since we do not have `tslib` installed, this will fail if we accidentally make use of anything that'd require injection of helpers. 29 | 30 | // Language and environment 31 | "moduleResolution": "NodeNext", 32 | "module": "ESNext", 33 | "target": "ES2021", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping. 34 | "lib": [ 35 | "ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances. 36 | "DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped. 37 | ], 38 | 39 | // Skip type checking for node modules 40 | "skipLibCheck": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is used to compile the for cjs and esm (see package.json build scripts). It should exclude all test files. 3 | "extends": "./tsconfig.base.json", 4 | "include": ["src"], 5 | "exclude": [ 6 | "src/**/*.test.ts", 7 | "src/**/*.test-d.ts" 8 | ], 9 | "compilerOptions": { 10 | "sourceMap": true, 11 | "rootDir": "./src" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This configuration is used for local development and type checking. 3 | "extends": "./tsconfig.base.json", 4 | "include": ["src"], 5 | "exclude": [], 6 | "references": [{ "path": "./tsconfig.node.json" }] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | // This configuration is used for local development and type checking of configuration and script files that are not part of the build. 3 | "include": ["scripts"], 4 | "compilerOptions": { 5 | "strict": true, 6 | "composite": true, 7 | "module": "ESNext", 8 | "moduleResolution": "Node", 9 | "allowSyntheticDefaultImports": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------