├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── actions
│ └── install-dependencies
│ │ └── action.yml
├── assets
│ ├── interface-diagram-dark.png
│ ├── interface-diagram-light.png
│ ├── messaging-dark.png
│ ├── messaging-light.png
│ └── setup.md
└── workflows
│ ├── artifacts.yml
│ ├── on-pull-request.yml
│ ├── on-push-to-main.yml
│ ├── prerelease.yml
│ ├── release.yml
│ └── verify.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── biome.json
├── bun.lockb
├── dom.d.ts
├── index.html
├── manifest.config.ts
├── package.json
├── patches
└── @samrum+vite-plugin-web-extension+5.1.0.patch
├── public
├── fonts
│ ├── SFMono-Medium.woff2
│ ├── SFMono-Regular.woff2
│ ├── SFMono-Semibold.woff2
│ ├── SFPro-Bold.woff2
│ ├── SFPro-Heavy.woff2
│ ├── SFPro-Light.woff2
│ ├── SFPro-Medium.woff2
│ ├── SFPro-Regular.woff2
│ └── SFPro-Semibold.woff2
├── icons
│ ├── icon-dev@128w.png
│ ├── icon-dev@16w.png
│ ├── icon-dev@32w.png
│ ├── icon-dev@48w.png
│ ├── icon@128w.png
│ ├── icon@16w.png
│ ├── icon@32w.png
│ └── icon@48w.png
├── sources
│ ├── SF-Pro-Text-Black.otf
│ ├── SF-Pro-Text-Bold.otf
│ ├── SF-Pro-Text-Heavy.otf
│ ├── SF-Pro-Text-Light.otf
│ ├── SF-Pro-Text-Medium.otf
│ ├── SF-Pro-Text-Regular.otf
│ ├── SF-Pro-Text-Semibold.otf
│ ├── SF-Pro-Text-Thin.otf
│ ├── SF-Pro-Text-Ultralight.otf
│ ├── SF-Pro.ttf
│ ├── chars.txt
│ └── names.txt
└── vite.svg
├── scripts
├── deploy-contracts.ts
├── generate-typed-artifacts.ts
├── preinstall.ts
└── symbols.ts
├── src
├── actions
│ ├── connect.ts
│ ├── disconnect.ts
│ ├── getAccountTokens.ts
│ ├── getContracts.ts
│ └── index.ts
├── app.tsx
├── components
│ ├── Container.tsx
│ ├── FormPopover.tsx
│ ├── Header.css.ts
│ ├── Header.tsx
│ ├── LabelledContent.tsx
│ ├── LoadMore.tsx
│ ├── NetworkOfflineDialog.css.ts
│ ├── NetworkOfflineDialog.tsx
│ ├── OnboardingContainer.tsx
│ ├── Progress.tsx
│ ├── Toaster.css.ts
│ ├── Toaster.tsx
│ ├── Tooltip.tsx
│ ├── VirtualList.tsx
│ ├── _playground
│ │ ├── constants.ts
│ │ ├── index.html
│ │ └── index.tsx
│ ├── abi
│ │ ├── AbiFunctionsAccordion.css.ts
│ │ ├── AbiFunctionsAccordion.tsx
│ │ ├── AbiParametersInputs.playground.tsx
│ │ ├── AbiParametersInputs.tsx
│ │ ├── DecodedAbiParameters.css.ts
│ │ ├── DecodedAbiParameters.tsx
│ │ ├── DecodedCalldata.tsx
│ │ ├── FormattedAbiFunctionName.tsx
│ │ ├── FormattedAbiItem.tsx
│ │ └── index.ts
│ ├── form
│ │ ├── CheckboxField.tsx
│ │ ├── InputField.tsx
│ │ ├── Root.tsx
│ │ ├── SelectField.tsx
│ │ └── index.ts
│ ├── index.ts
│ ├── logs
│ │ ├── DecodedLogs.css.ts
│ │ ├── DecodedLogs.tsx
│ │ └── index.ts
│ ├── svgs
│ │ ├── BrandIcon.tsx
│ │ ├── Cog.css.ts
│ │ ├── Cog.tsx
│ │ ├── Cogs.css.ts
│ │ ├── Cogs.tsx
│ │ ├── Spinner.tsx
│ │ ├── Wallet.tsx
│ │ └── index.ts
│ └── tabs
│ │ ├── TabsContent.tsx
│ │ ├── TabsList.css.ts
│ │ ├── TabsList.tsx
│ │ └── index.ts
├── constants
│ ├── abi.ts
│ ├── etherscan.ts
│ └── index.ts
├── contexts
│ ├── AppMeta.ts
│ └── index.ts
├── design-system
│ ├── AccentColorProvider.tsx
│ ├── ColorSchemeProvider.tsx
│ ├── ThemeProvider.tsx
│ ├── _playground
│ │ ├── index.html
│ │ └── index.tsx
│ ├── components
│ │ ├── Bleed.tsx
│ │ ├── Box.css.ts
│ │ ├── Box.tsx
│ │ ├── Button.css.ts
│ │ ├── Button.tsx
│ │ ├── ButtonCopy.tsx
│ │ ├── ButtonSymbol.css.ts
│ │ ├── ButtonSymbol.tsx
│ │ ├── ButtonText.tsx
│ │ ├── Columns.css.ts
│ │ ├── Columns.tsx
│ │ ├── Inline.tsx
│ │ ├── Input.css.ts
│ │ ├── Input.tsx
│ │ ├── Inset.tsx
│ │ ├── Link.tsx
│ │ ├── Rows.css.ts
│ │ ├── Rows.tsx
│ │ ├── SFSymbol.tsx
│ │ ├── Select.css.ts
│ │ ├── Select.tsx
│ │ ├── Separator.css.ts
│ │ ├── Separator.tsx
│ │ ├── Stack.tsx
│ │ ├── Text.css.ts
│ │ └── Text.tsx
│ ├── index.ts
│ ├── styles
│ │ ├── global.css.ts
│ │ ├── reset.css.ts
│ │ └── theme.css.ts
│ ├── symbols
│ │ └── generated
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ ├── tokens.ts
│ ├── utils
│ │ ├── initializeTheme.critical.ts
│ │ ├── initializeTheme.ts
│ │ ├── theme.ts
│ │ └── toRgb.ts
│ └── vite.config.ts
├── entries
│ ├── background
│ │ ├── commands.ts
│ │ ├── context-menu.ts
│ │ ├── extension-id.ts
│ │ ├── index.ts
│ │ ├── inpage.ts
│ │ ├── intercept-requests.ts
│ │ ├── rpc.ts
│ │ └── wallet-sidebar.ts
│ ├── content
│ │ └── index.ts
│ ├── iframe
│ │ ├── index.html
│ │ └── index.ts
│ └── inpage
│ │ ├── index.ts
│ │ └── injectProvider.ts
├── errors
│ ├── base.ts
│ ├── index.ts
│ └── rpc.ts
├── hmr.ts
├── hooks
│ ├── useAccountTokens.ts
│ ├── useAccounts.ts
│ ├── useAutoloadAbi.ts
│ ├── useBalance.ts
│ ├── useBlock.ts
│ ├── useBytecode.ts
│ ├── useCalldataAbi.ts
│ ├── useClient.ts
│ ├── useContracts.ts
│ ├── useDebounce.ts
│ ├── useErc20Balance.ts
│ ├── useErc20Metadata.ts
│ ├── useGetAutomine.ts
│ ├── useGetLogs.ts
│ ├── useHost.ts
│ ├── useImpersonate.ts
│ ├── useInfiniteBlockTransactions.ts
│ ├── useInfiniteBlocks.ts
│ ├── useLookupSignature.ts
│ ├── useMine.ts
│ ├── useNetworkStatus.ts
│ ├── useNonce.ts
│ ├── usePendingBlock.ts
│ ├── usePendingTransactions.ts
│ ├── usePrepareTransactionRequest.ts
│ ├── usePrevious.ts
│ ├── useReadContract.ts
│ ├── useRevert.ts
│ ├── useSearchParamsState.ts
│ ├── useSetAccount.ts
│ ├── useSetAutomine.ts
│ ├── useSetBalance.ts
│ ├── useSetErc20Balance.ts
│ ├── useSetIntervalMining.ts
│ ├── useSetNonce.ts
│ ├── useSnapshot.ts
│ ├── useStopImpersonate.ts
│ ├── useSyncExternalStoreWithTracked.ts
│ ├── useTransaction.ts
│ ├── useTransactionConfirmations.ts
│ ├── useTransactionReceipt.ts
│ ├── useTxpool.ts
│ └── useWriteContract.ts
├── index.html
├── index.ts
├── messengers
│ ├── getMessenger.ts
│ ├── index.ts
│ ├── schema.ts
│ └── transports
│ │ ├── bridge.ts
│ │ ├── extension.ts
│ │ ├── tab.ts
│ │ ├── types.ts
│ │ ├── utils.ts
│ │ └── window.ts
├── provider.ts
├── react-query.tsx
├── reset.d.ts
├── screens
│ ├── _layout.tsx
│ ├── account-details.tsx
│ ├── block-config.tsx
│ ├── block-details.tsx
│ ├── contract-details.tsx
│ ├── index.css.ts
│ ├── index.tsx
│ ├── network-config.tsx
│ ├── networks.tsx
│ ├── onboarding
│ │ ├── configure.tsx
│ │ ├── download.tsx
│ │ ├── run.tsx
│ │ └── start.tsx
│ ├── pending-request.css.ts
│ ├── pending-request.tsx
│ ├── session.tsx
│ ├── settings.tsx
│ └── transaction-details.tsx
├── storage
│ ├── index.ts
│ ├── utils.ts
│ ├── webext.ts
│ └── window.ts
├── types
│ ├── rpc.ts
│ └── utils.ts
├── utils
│ ├── abi.ts
│ ├── capitalize.ts
│ ├── deepEqual.ts
│ ├── index.ts
│ ├── isDomain.ts
│ ├── normalizeAbiParametersValues.ts
│ ├── truncate.ts
│ └── uid.ts
├── viem.ts
├── vite-env.d.ts
└── zustand
│ ├── _template.ts
│ ├── account.ts
│ ├── batch-calls.ts
│ ├── contracts.ts
│ ├── index.ts
│ ├── network.ts
│ ├── pending-requests.ts
│ ├── scroll-position.ts
│ ├── sessions.ts
│ ├── settings.ts
│ ├── tokens.ts
│ └── utils.ts
├── test
├── contracts
│ ├── foundry.toml
│ ├── generated.ts
│ └── src
│ │ ├── MockERC20.sol
│ │ ├── MockERC721.sol
│ │ └── Playground.sol
└── dapp
│ ├── App.tsx
│ └── main.tsx
├── tsconfig.json
├── vite.config.inpage.ts
├── vite.config.ts
└── yarn.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | contact_links:
2 | - name: Ask Question
3 | url: https://github.com/paradigmxyz/rivet/discussions/new?category=q-a
4 | about: Ask questions and discuss with other community members
5 | - name: Request Feature
6 | url: https://github.com/paradigmxyz/rivet/discussions/new?category=ideas
7 | about: Requests features or ideas for new functionality
8 |
--------------------------------------------------------------------------------
/.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 Bun
8 | uses: oven-sh/setup-bun@v1
9 |
10 | - name: Set up foundry
11 | uses: foundry-rs/foundry-toolchain@v1
12 |
13 | - name: Install dependencies
14 | shell: bash
15 | run: bun install --yarn --ignore-scripts
--------------------------------------------------------------------------------
/.github/assets/interface-diagram-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/.github/assets/interface-diagram-dark.png
--------------------------------------------------------------------------------
/.github/assets/interface-diagram-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/.github/assets/interface-diagram-light.png
--------------------------------------------------------------------------------
/.github/assets/messaging-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/.github/assets/messaging-dark.png
--------------------------------------------------------------------------------
/.github/assets/messaging-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/.github/assets/messaging-light.png
--------------------------------------------------------------------------------
/.github/assets/setup.md:
--------------------------------------------------------------------------------
1 | ### Setup Instructions
2 |
3 | 1. Download the `extension.zip` asset from the link below
4 | 2. Unzip the downloaded file
5 | 3. Open your chromium browser and navigate to `chrome://extensions`
6 | 4. Enable `Developer Mode` in the top right corner
7 | 5. Click `Load Unpacked` in the top left corner
8 | 6. Select the unzipped folder
9 | 7. Done! You should now see the Rivet extension in your browser
--------------------------------------------------------------------------------
/.github/workflows/artifacts.yml:
--------------------------------------------------------------------------------
1 | name: Artifacts
2 | on:
3 | workflow_call:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | artifacts:
8 | name: Artifacts
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Clone repository
13 | uses: actions/checkout@v3
14 |
15 | - name: Install dependencies
16 | uses: ./.github/actions/install-dependencies
17 |
18 | - name: Build
19 | run: bun run build
20 |
21 | - name: Upload Artifact
22 | uses: actions/upload-artifact@v3
23 | with:
24 | name: extension
25 | path: dist/build
26 |
27 | - name: Comment
28 | uses: tonyhallett/artifacts-url-comments@v1.1.0
29 | if: ${{ github.event_name == 'pull_request' }}
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | with:
33 | prefix: "Here is the extension:"
34 | suffix: "Have a nice day. 🫡"
35 | format: name
36 | addTo: pull
37 |
--------------------------------------------------------------------------------
/.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 | artifacts:
17 | name: Artifacts
18 | uses: ./.github/workflows/artifacts.yml
19 |
--------------------------------------------------------------------------------
/.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 | prerelease:
17 | name: Prerelease
18 | uses: ./.github/workflows/prerelease.yml
19 | secrets: inherit
--------------------------------------------------------------------------------
/.github/workflows/prerelease.yml:
--------------------------------------------------------------------------------
1 | name: Prerelease
2 | on:
3 | workflow_call:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | prerelease:
8 | name: Prerelease
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Clone repository
13 | uses: actions/checkout@v3
14 |
15 | - name: Install dependencies
16 | uses: ./.github/actions/install-dependencies
17 |
18 | - name: Get commit SHA
19 | id: sha
20 | run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
21 |
22 | - name: Build
23 | run: bun run build
24 |
25 | - name: Set version
26 | run: |
27 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
28 | git config --global user.name "github-actions[bot]"
29 | npm version 0.0.0-nightly.${{ steps.sha.outputs.sha_short }}
30 | git push --tags
31 |
32 | - name: Zip
33 | run: bun run zip
34 |
35 | - name: Get NPM version
36 | id: package-version
37 | uses: martinbeentjes/npm-get-version-action@v1.3.1
38 |
39 | - name: Release
40 | uses: softprops/action-gh-release@v1
41 | with:
42 | body_path: ${{ github.workspace }}/.github/assets/setup.md
43 | files: dist/extension.zip
44 | tag_name: 'v${{ steps.package-version.outputs.current-version }}'
45 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | workflow_call:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Clone repository
13 | uses: actions/checkout@v3
14 |
15 | - name: Install dependencies
16 | uses: ./.github/actions/install-dependencies
17 |
18 | - name: Set version
19 | run: |
20 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
21 | git config --global user.name "github-actions[bot]"
22 | npm version patch -m "chore: submit %s to web store"
23 | git push --tags
24 |
25 | - name: Build
26 | run: bun run build
27 |
28 | - name: Zip
29 | run: bun run zip
30 |
31 | - name: Submit to Chrome Web Store
32 | uses: PlasmoHQ/bpp@v3.5.0
33 | with:
34 | artifact: ./dist/extension.zip
35 | keys: ${{ secrets.BPP_KEYS }}
36 |
37 | - name: Get NPM version
38 | id: package-version
39 | uses: martinbeentjes/npm-get-version-action@v1.3.1
40 |
41 | - name: Release
42 | uses: softprops/action-gh-release@v1
43 | with:
44 | files: dist/extension.zip
45 | tag_name: 'v${{ steps.package-version.outputs.current-version }}'
46 |
47 | - name: Push
48 | uses: ad-m/github-push-action@master
49 | with:
50 | github_token: ${{ secrets.GITHUB_TOKEN }}
51 | branch: ${{ github.ref }}
52 |
53 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify
2 | on:
3 | workflow_call:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | lint:
8 | name: Lint
9 | runs-on: ubuntu-latest
10 | timeout-minutes: 5
11 |
12 | steps:
13 | - name: Clone repository
14 | uses: actions/checkout@v3
15 |
16 | - name: Install dependencies
17 | uses: ./.github/actions/install-dependencies
18 |
19 | - name: Lint code
20 | run: bun run format && bun run lint:fix
21 |
22 | - uses: stefanzweifel/git-auto-commit-action@v4
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | with:
26 | commit_message: 'chore: format'
27 | commit_user_name: 'github-actions[bot]'
28 | commit_user_email: 'github-actions[bot]@users.noreply.github.com'
29 |
30 | build:
31 | name: Build
32 | needs: lint
33 | runs-on: ubuntu-latest
34 | timeout-minutes: 5
35 |
36 | steps:
37 | - name: Clone repository
38 | uses: actions/checkout@v3
39 |
40 | - name: Install dependencies
41 | uses: ./.github/actions/install-dependencies
42 |
43 | - name: Build
44 | run: bun run build
45 |
46 | types:
47 | name: Types
48 | needs: lint
49 | runs-on: ubuntu-latest
50 | timeout-minutes: 5
51 |
52 | steps:
53 | - name: Clone repository
54 | uses: actions/checkout@v3
55 |
56 | - name: Install dependencies
57 | uses: ./.github/actions/install-dependencies
58 |
59 | - name: Check types
60 | run: bun run typecheck
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | /logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | public/initializeTheme.*
16 | keys.json
17 |
18 | # Editor directories and files
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
27 | # Foundry
28 | /cache
29 | /out
30 | test/contracts/cache
31 | test/contracts/out
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["biomejs.biome"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "editor.formatOnSave": true,
4 | "typescript.tsdk": "node_modules/typescript/lib",
5 | "typescript.enablePromptUseWorkspaceTsdk": true,
6 | "editor.codeActionsOnSave": {
7 | "source.organizeImports.biome": "explicit"
8 | },
9 | "[json]": {
10 | "editor.defaultFormatter": "biomejs.biome"
11 | },
12 | "[javascript]": {
13 | "editor.defaultFormatter": "biomejs.biome"
14 | },
15 | "[javascriptreact]": {
16 | "editor.defaultFormatter": "biomejs.biome"
17 | },
18 | "[typescript]": {
19 | "editor.defaultFormatter": "biomejs.biome"
20 | },
21 | "[typescriptreact]": {
22 | "editor.defaultFormatter": "biomejs.biome"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present Rivet contributors
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 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
3 | "files": {
4 | "ignore": [
5 | "node_modules",
6 | "public",
7 | "dist",
8 | "contracts/cache",
9 | "contracts/out",
10 | "contracts/generated.ts",
11 | "keys.json",
12 | "symbols/generated"
13 | ]
14 | },
15 | "formatter": {
16 | "enabled": true,
17 | "formatWithErrors": false,
18 | "indentStyle": "space",
19 | "indentWidth": 2,
20 | "lineWidth": 80
21 | },
22 | "linter": {
23 | "enabled": true,
24 | "rules": {
25 | "recommended": true,
26 | "complexity": {
27 | "noForEach": "off"
28 | },
29 | "correctness": {
30 | "noUnusedVariables": "error",
31 | "useExhaustiveDependencies": "off"
32 | },
33 | "performance": {
34 | "noAccumulatingSpread": "off",
35 | "noDelete": "off"
36 | },
37 | "style": {
38 | "noNonNullAssertion": "off",
39 | "useShorthandArrayType": "error"
40 | },
41 | "suspicious": {
42 | "noArrayIndexKey": "off",
43 | "noAssignInExpressions": "off",
44 | "noConfusingVoidType": "off",
45 | "noExplicitAny": "off"
46 | }
47 | }
48 | },
49 | "javascript": {
50 | "formatter": {
51 | "quoteStyle": "single",
52 | "trailingComma": "all",
53 | "semicolons": "asNeeded"
54 | }
55 | },
56 | "organizeImports": {
57 | "enabled": true
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/bun.lockb
--------------------------------------------------------------------------------
/dom.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test Dapp
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/manifest.config.ts:
--------------------------------------------------------------------------------
1 | import pkg from './package.json'
2 |
3 | export const getManifest = ({ dev }: { dev?: boolean }) =>
4 | ({
5 | name: `${pkg.extension.name}${dev ? ' (dev)' : ''}`,
6 | description: pkg.extension.description,
7 | version: pkg.version,
8 | manifest_version: 3,
9 | action: {
10 | default_icon: {
11 | '16': `icons/icon${dev ? '-dev' : ''}@16w.png`,
12 | '32': `icons/icon${dev ? '-dev' : ''}@32w.png`,
13 | '48': `icons/icon${dev ? '-dev' : ''}@48w.png`,
14 | '128': `icons/icon${dev ? '-dev' : ''}@128w.png`,
15 | },
16 | },
17 | background: {
18 | service_worker: 'src/entries/background/index.ts',
19 | },
20 | content_scripts: [
21 | {
22 | matches: ['*://*/*'],
23 | js: ['src/entries/content/index.ts'],
24 | run_at: 'document_start',
25 | all_frames: true,
26 | },
27 | ],
28 | side_panel: {
29 | default_path: 'src/entries/iframe/index.html',
30 | },
31 | icons: {
32 | '16': `icons/icon${dev ? '-dev' : ''}@16w.png`,
33 | '32': `icons/icon${dev ? '-dev' : ''}@32w.png`,
34 | '48': `icons/icon${dev ? '-dev' : ''}@48w.png`,
35 | '128': `icons/icon${dev ? '-dev' : ''}@128w.png`,
36 | },
37 | permissions: [
38 | 'activeTab',
39 | 'contextMenus',
40 | 'declarativeNetRequest',
41 | 'scripting',
42 | 'sidePanel',
43 | 'storage',
44 | 'tabs',
45 | 'unlimitedStorage',
46 | 'webRequest',
47 | ],
48 | host_permissions: ['*://*/*'],
49 | web_accessible_resources: [
50 | {
51 | resources: ['*.woff2'],
52 | matches: [''],
53 | },
54 | {
55 | resources: ['inpage.js'],
56 | matches: ['*://*/*'],
57 | },
58 | ],
59 | commands: {
60 | 'toggle-theme': {
61 | suggested_key: 'Ctrl+Shift+Y' as any,
62 | description: 'Toggle Theme',
63 | },
64 | },
65 | }) satisfies chrome.runtime.Manifest
66 |
--------------------------------------------------------------------------------
/patches/@samrum+vite-plugin-web-extension+5.1.0.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@samrum/vite-plugin-web-extension/dist/index.cjs b/node_modules/@samrum/vite-plugin-web-extension/dist/index.cjs
2 | index b3007a2..69ef681 100644
3 | --- a/node_modules/@samrum/vite-plugin-web-extension/dist/index.cjs
4 | +++ b/node_modules/@samrum/vite-plugin-web-extension/dist/index.cjs
5 | @@ -156,7 +156,7 @@ class DevBuilder {
6 | }) {
7 | this.hmrServerOrigin = this.getHmrServerOrigin(devServerPort);
8 | this.hmrViteClientUrl = `${this.hmrServerOrigin}/@vite/client`;
9 | - await fsExtra.emptyDir(this.outDir);
10 | + // await fsExtra.emptyDir(this.outDir);
11 | const publicDir = path__default.resolve(
12 | process.cwd(),
13 | this.viteConfig.root,
14 | diff --git a/node_modules/@samrum/vite-plugin-web-extension/dist/index.mjs b/node_modules/@samrum/vite-plugin-web-extension/dist/index.mjs
15 | index dc8132c..0f57806 100644
16 | --- a/node_modules/@samrum/vite-plugin-web-extension/dist/index.mjs
17 | +++ b/node_modules/@samrum/vite-plugin-web-extension/dist/index.mjs
18 | @@ -147,7 +147,7 @@ class DevBuilder {
19 | }) {
20 | this.hmrServerOrigin = this.getHmrServerOrigin(devServerPort);
21 | this.hmrViteClientUrl = `${this.hmrServerOrigin}/@vite/client`;
22 | - await emptyDir(this.outDir);
23 | + // await emptyDir(this.outDir);
24 | const publicDir = path.resolve(
25 | process.cwd(),
26 | this.viteConfig.root,
27 |
--------------------------------------------------------------------------------
/public/fonts/SFMono-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFMono-Medium.woff2
--------------------------------------------------------------------------------
/public/fonts/SFMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFMono-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/SFMono-Semibold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFMono-Semibold.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Bold.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Heavy.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Heavy.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Light.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Medium.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/SFPro-Semibold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/fonts/SFPro-Semibold.woff2
--------------------------------------------------------------------------------
/public/icons/icon-dev@128w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon-dev@128w.png
--------------------------------------------------------------------------------
/public/icons/icon-dev@16w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon-dev@16w.png
--------------------------------------------------------------------------------
/public/icons/icon-dev@32w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon-dev@32w.png
--------------------------------------------------------------------------------
/public/icons/icon-dev@48w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon-dev@48w.png
--------------------------------------------------------------------------------
/public/icons/icon@128w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon@128w.png
--------------------------------------------------------------------------------
/public/icons/icon@16w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon@16w.png
--------------------------------------------------------------------------------
/public/icons/icon@32w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon@32w.png
--------------------------------------------------------------------------------
/public/icons/icon@48w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/icons/icon@48w.png
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Black.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Black.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Bold.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Heavy.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Heavy.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Light.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Medium.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Regular.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Semibold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Semibold.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Thin.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Thin.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro-Text-Ultralight.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro-Text-Ultralight.otf
--------------------------------------------------------------------------------
/public/sources/SF-Pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paradigmxyz/rivet/fd94089ba4bec65bbf3fa288efbeab7306cb1537/public/sources/SF-Pro.ttf
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/deploy-contracts.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path'
2 | import { globby } from 'globby'
3 | ;(async () => {
4 | const rpcUrl = process.argv[2] || 'http://127.0.0.1:8545'
5 | const privateKey =
6 | process.argv[3] ||
7 | '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' // anvil private key before you ask
8 |
9 | const contractsPaths = await globby(
10 | join(__dirname, '../test/contracts/src/**/*.sol'),
11 | )
12 |
13 | contractsPaths.forEach((contractPath) => {
14 | const contract = contractPath.split('/')
15 | const contractName = contract[contract.length - 1].replace('.sol', '')
16 | console.log('Deploying: ', contractName)
17 | Bun.spawnSync(
18 | [
19 | 'forge',
20 | 'create',
21 | '--rpc-url',
22 | rpcUrl,
23 | '--private-key',
24 | privateKey,
25 | `${contractPath}:${contractName}`,
26 | ],
27 | { stdout: 'inherit' },
28 | )
29 | })
30 | console.log('Deployed contracts.')
31 | })()
32 |
--------------------------------------------------------------------------------
/scripts/generate-typed-artifacts.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path'
2 | import { globby } from 'globby'
3 |
4 | const generatedPath = join(import.meta.dir, '../test/contracts/generated.ts')
5 | Bun.write(generatedPath, '')
6 |
7 | const generated = Bun.file(generatedPath)
8 | const writer = generated.writer()
9 |
10 | const paths = await globby([
11 | join(import.meta.dir, '../test/contracts/out/**/*.json'),
12 | ])
13 |
14 | await Promise.all(
15 | paths.map(async (path) => {
16 | const fileName = path.split('/').pop()?.replace('.json', '')
17 | const json = await Bun.file(path, { type: 'application/json' }).json()
18 | writer.write(
19 | `export const ${fileName} = ${JSON.stringify(
20 | json,
21 | null,
22 | 2,
23 | )} as const;\n\n`,
24 | )
25 | }),
26 | )
27 |
28 | writer.end()
29 |
--------------------------------------------------------------------------------
/scripts/preinstall.ts:
--------------------------------------------------------------------------------
1 | import whichPmRuns from 'which-pm-runs'
2 |
3 | const pm = whichPmRuns()
4 | if (pm?.name !== 'bun')
5 | throw new Error(
6 | `\`${pm?.name} install\` is not supported. Please run \`bun install\``,
7 | )
8 |
--------------------------------------------------------------------------------
/src/actions/connect.ts:
--------------------------------------------------------------------------------
1 | import { numberToHex } from 'viem'
2 | import type { Messenger } from '~/messengers'
3 | import { networkStore, sessionsStore } from '~/zustand'
4 |
5 | export async function connect({
6 | host,
7 | messenger,
8 | }: { host: string; messenger: Messenger }) {
9 | const { network } = networkStore.getState()
10 |
11 | const { addSession } = sessionsStore.getState()
12 | addSession({ session: { host } })
13 |
14 | await messenger.send('connect', { chainId: numberToHex(network.chainId) })
15 | }
16 |
--------------------------------------------------------------------------------
/src/actions/disconnect.ts:
--------------------------------------------------------------------------------
1 | import type { Messenger } from '~/messengers'
2 | import { sessionsStore } from '~/zustand'
3 |
4 | export async function disconnect({
5 | host,
6 | messenger,
7 | }: { host: string; messenger: Messenger }) {
8 | const { removeSession } = sessionsStore.getState()
9 | removeSession({ host })
10 |
11 | await messenger.send('disconnect', undefined)
12 | }
13 |
--------------------------------------------------------------------------------
/src/actions/getAccountTokens.ts:
--------------------------------------------------------------------------------
1 | import { type Address, parseAbiItem } from 'abitype'
2 | import type { GetLogsParameters } from 'viem'
3 |
4 | import { getLogsQueryOptions } from '~/hooks/useGetLogs'
5 | import { queryClient } from '~/react-query'
6 | import type { Client } from '~/viem'
7 |
8 | export async function getAccountTokens(
9 | client: Client,
10 | {
11 | address,
12 | fromBlock,
13 | toBlock,
14 | }: {
15 | address: Address
16 | fromBlock: GetLogsParameters['fromBlock']
17 | toBlock: GetLogsParameters['toBlock']
18 | },
19 | ) {
20 | const [transfersFrom, transfersTo] = await Promise.all([
21 | queryClient.fetchQuery(
22 | getLogsQueryOptions(client, {
23 | event: parseAbiItem(
24 | 'event Transfer(address indexed from, address indexed to, uint256)',
25 | ),
26 | args: {
27 | from: address,
28 | },
29 | fromBlock,
30 | toBlock,
31 | }),
32 | ),
33 | queryClient.fetchQuery(
34 | getLogsQueryOptions(client, {
35 | event: parseAbiItem(
36 | 'event Transfer(address indexed from, address indexed to, uint256)',
37 | ),
38 | args: {
39 | to: address,
40 | },
41 | fromBlock,
42 | toBlock,
43 | }),
44 | ),
45 | ])
46 |
47 | // TODO: Check if log addresses are ERC20 tokens.
48 |
49 | return [
50 | ...new Set([
51 | ...(transfersFrom?.map((t) => t.address) || []),
52 | ...(transfersTo?.map((t) => t.address) || []),
53 | ]),
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/src/actions/getContracts.ts:
--------------------------------------------------------------------------------
1 | import type { Client } from '~/viem'
2 | import {
3 | type UseBlockParameters,
4 | getBlockQueryOptions,
5 | } from '../hooks/useBlock'
6 | import { getBytecodeQueryOptions } from '../hooks/useBytecode'
7 | import { getTransactionReceiptQueryOptions } from '../hooks/useTransactionReceipt'
8 | import { queryClient } from '../react-query'
9 |
10 | export async function getContracts(
11 | client: Client,
12 | {
13 | fromBlock,
14 | toBlock = 'latest',
15 | }: {
16 | fromBlock: NonNullable<
17 | | UseBlockParameters['blockNumber']
18 | | Exclude
19 | >
20 | toBlock?:
21 | | UseBlockParameters['blockNumber']
22 | | Exclude
23 | },
24 | ) {
25 | const [fromBlockNumber, toBlockNumber] = await Promise.all([
26 | (async () => {
27 | if (typeof fromBlock === 'bigint') return fromBlock
28 | const block = await queryClient.fetchQuery(
29 | getBlockQueryOptions(client, { blockTag: fromBlock }),
30 | )
31 | return block.number
32 | })(),
33 | (async () => {
34 | if (typeof toBlock === 'bigint') return toBlock
35 | const block = await queryClient.fetchQuery(
36 | getBlockQueryOptions(client, { blockTag: toBlock }),
37 | )
38 | return block.number
39 | })(),
40 | ])
41 |
42 | const contracts = (
43 | await Promise.all(
44 | [...Array(Number(toBlockNumber - fromBlockNumber) + 1)].map(
45 | async (_, i) => {
46 | const blockNumber = fromBlockNumber + BigInt(i)
47 | const block = await queryClient.fetchQuery(
48 | getBlockQueryOptions(client, { blockNumber }),
49 | )
50 |
51 | const receipts = await Promise.all(
52 | block.transactions.map(async (transaction) => {
53 | const receipt = await queryClient.fetchQuery(
54 | getTransactionReceiptQueryOptions(client, {
55 | hash: transaction,
56 | }),
57 | )
58 | const address = receipt.contractAddress
59 | if (!address) return null
60 |
61 | const bytecode = await queryClient.fetchQuery(
62 | getBytecodeQueryOptions(client, {
63 | address,
64 | }),
65 | )
66 |
67 | return {
68 | address,
69 | bytecode,
70 | receipt,
71 | }
72 | }),
73 | )
74 | return receipts.filter(Boolean)
75 | },
76 | ),
77 | )
78 | ).flat()
79 |
80 | return contracts
81 | }
82 |
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | export { connect } from './connect'
2 | export { disconnect } from './disconnect'
3 | export { getAccountTokens } from './getAccountTokens'
4 |
--------------------------------------------------------------------------------
/src/components/FormPopover.tsx:
--------------------------------------------------------------------------------
1 | import * as Popover from '@radix-ui/react-popover'
2 | import { type ReactNode, useState } from 'react'
3 |
4 | import { Box, Button, Stack } from '~/design-system'
5 | import * as Form from './form'
6 |
7 | export function FormPopover({
8 | children,
9 | disabled,
10 | onSubmit,
11 | }: {
12 | children: ReactNode
13 | disabled: boolean
14 | onSubmit: (e: React.FormEvent) => void
15 | }) {
16 | const [open, setOpen] = useState(false)
17 |
18 | return (
19 |
20 |
21 |
22 | setOpen(true)}
26 | symbol="square.and.pencil"
27 | variant="ghost primary"
28 | />
29 |
30 |
31 |
32 | setOpen(false)}
34 | onPointerDownOutside={() => setOpen(false)}
35 | style={{ zIndex: 1 }}
36 | >
37 |
44 | {
46 | onSubmit(e)
47 | setOpen(false)
48 | }}
49 | >
50 |
51 | {children}
52 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/Header.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 | import { keyframes } from '@vanilla-extract/css'
3 |
4 | import { foregroundColorVars } from '../design-system/styles/theme.css'
5 |
6 | const mineAnimation = keyframes({
7 | '0%': {
8 | color: 'white',
9 | transform: 'rotate(0deg)',
10 | },
11 | '50%': {
12 | color: 'white',
13 | transform: 'rotate(10deg)',
14 | },
15 | '100%': {
16 | color: `rgb(${foregroundColorVars['text/tertiary']})`,
17 | transform: 'rotate(0deg)',
18 | },
19 | })
20 |
21 | export const mineSymbol = style({
22 | animationName: mineAnimation,
23 | animationDuration: '0.2s',
24 | animationTimingFunction: 'linear',
25 | })
26 |
--------------------------------------------------------------------------------
/src/components/LabelledContent.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react'
2 |
3 | import { Box, Stack, Text } from '~/design-system'
4 |
5 | import type { StackProps } from '../design-system/components/Stack'
6 | import type { TextStyles } from '../design-system/components/Text.css'
7 |
8 | export function LabelledContent({
9 | children,
10 | label,
11 | labelRight,
12 | labelColor = 'text/tertiary',
13 | width,
14 | }: {
15 | children: ReactNode
16 | label: string | ReactNode
17 | labelRight?: ReactNode
18 | labelColor?: TextStyles['color']
19 | width?: StackProps['width']
20 | }) {
21 | return (
22 |
23 |
30 | {typeof label === 'string' ? (
31 |
32 | {label.toUpperCase()}
33 |
34 | ) : (
35 | label
36 | )}
37 | {labelRight}
38 |
39 | {children}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/LoadMore.tsx:
--------------------------------------------------------------------------------
1 | import type { UseInfiniteQueryResult } from '@tanstack/react-query'
2 | import { type ReactNode, useEffect } from 'react'
3 | import { useInView } from 'react-intersection-observer'
4 |
5 | import { Box } from '~/design-system'
6 |
7 | export function LoadMore({
8 | children,
9 | query,
10 | }: { children: ReactNode; query: UseInfiniteQueryResult }) {
11 | const { fetchNextPage, isFetching, isFetchingNextPage } = query
12 |
13 | const { ref, inView } = useInView()
14 | useEffect(() => {
15 | if (isFetching) return
16 | if (isFetchingNextPage) return
17 | if (inView) fetchNextPage()
18 | }, [fetchNextPage, inView, isFetching, isFetchingNextPage])
19 |
20 | return {(isFetching || isFetchingNextPage) && children}
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/NetworkOfflineDialog.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 |
3 | export const content = style({
4 | selectors: {
5 | '&:focus': {
6 | outline: 'unset',
7 | },
8 | },
9 | })
10 |
--------------------------------------------------------------------------------
/src/components/NetworkOfflineDialog.tsx:
--------------------------------------------------------------------------------
1 | import * as Dialog from '@radix-ui/react-dialog'
2 | import { Box, Inset, Stack, Text } from '~/design-system'
3 | import * as styles from './NetworkOfflineDialog.css'
4 |
5 | export function NetworkOfflineDialog() {
6 | return (
7 |
8 |
9 |
19 |
20 |
21 |
32 |
46 |
47 |
48 | Anvil is disconnected.
49 |
50 | Once Anvil is reconnected, DevTools will automatically
51 | reconnect.
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/OnboardingContainer.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { Container } from '~/components'
4 | import { Box, Inset, Stack, Text } from '~/design-system'
5 |
6 | export function OnboardingContainer({
7 | children,
8 | footer,
9 | title,
10 | }: { children: ReactNode; footer?: ReactNode; title: string }) {
11 | const navigate = useNavigate()
12 | return (
13 |
16 |
17 |
18 | Setup
19 |
20 | {title}
21 |
22 |
23 | }
24 | footer={
25 |
26 | {footer}
27 | navigate(-1)}
31 | paddingTop="4px"
32 | paddingBottom="12px"
33 | >
34 | Back
35 |
36 |
37 | }
38 | >
39 | {children}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Progress.tsx:
--------------------------------------------------------------------------------
1 | import * as Progress_ from '@radix-ui/react-progress'
2 | import { Box } from '~/design-system'
3 |
4 | export function Progress({
5 | height,
6 | progress,
7 | }: { height: number; progress: number }) {
8 | return (
9 |
10 |
18 |
19 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Toaster.css.ts:
--------------------------------------------------------------------------------
1 | import { globalStyle } from '@vanilla-extract/css'
2 |
3 | import {
4 | backgroundColorVars,
5 | foregroundColorVars,
6 | textColorForBackgroundColorVars,
7 | } from '~/design-system/styles/theme.css'
8 | import { fontFamily, fontSize, fontWeight } from '~/design-system/tokens'
9 |
10 | globalStyle(':root [data-sonner-toaster]', {
11 | fontFamily: fontFamily.default,
12 | vars: {
13 | '--border-radius': '0',
14 | '--initial-height': '60px',
15 | '--normal-bg': `rgb(${backgroundColorVars['surface/secondary/elevated']})`,
16 | '--normal-border': `rgb(${backgroundColorVars['surface/fill']})`,
17 | '--normal-text': `rgb(${foregroundColorVars['text/primary']})`,
18 | '--success-bg': `rgb(${backgroundColorVars['surface/greenTint']})`,
19 | '--success-border': `rgb(${backgroundColorVars['surface/greenTint']})`,
20 | '--success-text': `rgb(${textColorForBackgroundColorVars['surface/greenTint']})`,
21 | '--error-bg': `rgb(${backgroundColorVars['surface/redTint']})`,
22 | '--error-border': `rgb(${backgroundColorVars['surface/redTint']})`,
23 | '--error-text': `rgb(${textColorForBackgroundColorVars['surface/redTint']})`,
24 | },
25 | })
26 |
27 | globalStyle(':root [data-sonner-toaster] [data-styled=true]', {
28 | padding: '8px',
29 | })
30 |
31 | globalStyle(':root [data-sonner-toaster] [data-title]', {
32 | fontWeight: fontWeight.regular,
33 | ...fontSize(true)['12px'],
34 | })
35 |
36 | globalStyle(':root [data-sonner-toaster] [data-icon]', {
37 | display: 'none',
38 | })
39 |
--------------------------------------------------------------------------------
/src/components/Toaster.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as Toaster_ } from 'sonner'
2 |
3 | import './Toaster.css'
4 |
5 | export function Toaster() {
6 | return (
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Content,
3 | Portal,
4 | Provider,
5 | Root,
6 | Trigger,
7 | } from '@radix-ui/react-tooltip'
8 | import type { ReactNode } from 'react'
9 | import { Box, Text } from '~/design-system'
10 |
11 | export type TooltipProps = {
12 | children: ReactNode
13 | enabled?: boolean
14 | height?: 'fit' | 'full'
15 | label: string | ReactNode
16 | side?: 'top' | 'bottom' | 'left' | 'right'
17 | width?: 'fit' | 'full'
18 | }
19 |
20 | export function Tooltip({
21 | children,
22 | enabled,
23 | height,
24 | label,
25 | side,
26 | width,
27 | }: TooltipProps) {
28 | return (
29 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 |
38 | {
43 | e.stopPropagation
44 | e.preventDefault()
45 | }}
46 | style={{ cursor: 'text', pointerEvents: 'visible' }}
47 | >
48 | {typeof label !== 'string' ? (
49 | label
50 | ) : (
51 | {label}
52 | )}
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/_playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Component Playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/abi/FormattedAbiFunctionName.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { type Hex, parseAbiItem, slice } from 'viem'
3 |
4 | import { Text } from '~/design-system'
5 | import { useLookupSignature } from '../../hooks/useLookupSignature'
6 |
7 | import { FormattedAbiItem } from './FormattedAbiItem'
8 |
9 | export function FormattedAbiFunctionName({ data }: { data: Hex }) {
10 | const selector = slice(data, 0, 4)
11 | const { data: signature } = useLookupSignature({
12 | selector,
13 | })
14 |
15 | const abiItem = useMemo(() => {
16 | if (!signature) return
17 | const abiItem = parseAbiItem(`function ${signature}`)
18 | if (abiItem.type !== 'function') return
19 | return abiItem
20 | }, [signature])
21 |
22 | return (
23 |
24 | {abiItem ? (
25 |
32 | ) : (
33 | selector
34 | )}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/abi/index.ts:
--------------------------------------------------------------------------------
1 | export { AbiFunctionsAccordion } from './AbiFunctionsAccordion'
2 | export { AbiParametersInputs } from './AbiParametersInputs'
3 | export { DecodedAbiParameters } from './DecodedAbiParameters'
4 | export { DecodedCalldata } from './DecodedCalldata'
5 | export { FormattedAbiItem } from './FormattedAbiItem'
6 |
--------------------------------------------------------------------------------
/src/components/form/CheckboxField.tsx:
--------------------------------------------------------------------------------
1 | import * as Form from '@radix-ui/react-form'
2 | import type { UseFormRegister } from 'react-hook-form'
3 |
4 | import { Box, Inline, Inset, Text } from '~/design-system'
5 |
6 | export type CheckboxFieldProps = {
7 | label: string
8 | register: ReturnType>
9 | }
10 |
11 | export function CheckboxField({ label, register }: CheckboxFieldProps) {
12 | return (
13 |
14 |
15 |
16 |
17 | {/** TODO: component */}
18 |
19 |
20 |
21 |
22 | {label}
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/form/Root.tsx:
--------------------------------------------------------------------------------
1 | export { Root } from '@radix-ui/react-form'
2 |
--------------------------------------------------------------------------------
/src/components/form/SelectField.tsx:
--------------------------------------------------------------------------------
1 | import * as Form from '@radix-ui/react-form'
2 | import type { ReactNode } from 'react'
3 | import type { UseFormRegister } from 'react-hook-form'
4 |
5 | import { Select, Stack, Text } from '~/design-system'
6 | import type { InputProps } from '~/design-system/components/Input'
7 |
8 | export type SelectFieldProps = {
9 | children: ReactNode
10 | height?: InputProps['height']
11 | hideLabel?: boolean
12 | label: string
13 | register: ReturnType>
14 | }
15 |
16 | export function SelectField({
17 | children,
18 | height,
19 | hideLabel,
20 | label,
21 | register,
22 | }: SelectFieldProps) {
23 | return (
24 |
25 |
26 | {hideLabel && (
27 |
28 | {label}
29 |
30 | )}
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/form/index.ts:
--------------------------------------------------------------------------------
1 | export { CheckboxField } from './CheckboxField'
2 | export { InputField } from './InputField'
3 | export { Root } from './Root'
4 | export { SelectField } from './SelectField'
5 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | AbiFunctionsAccordion,
3 | AbiParametersInputs,
4 | DecodedCalldata,
5 | DecodedAbiParameters,
6 | FormattedAbiItem,
7 | } from './abi'
8 | export { DecodedLogs } from './logs'
9 | export { TabsContent, TabsList } from './tabs'
10 |
11 | export { Container } from './Container'
12 | export { FormPopover } from './FormPopover'
13 | export { Header } from './Header'
14 | export { LabelledContent } from './LabelledContent'
15 | export { LoadMore } from './LoadMore'
16 | export { NetworkOfflineDialog } from './NetworkOfflineDialog'
17 | export { OnboardingContainer } from './OnboardingContainer'
18 | export { Progress } from './Progress'
19 | export { Toaster } from './Toaster'
20 | export { Tooltip } from './Tooltip'
21 | export { useVirtualList } from './VirtualList'
22 |
--------------------------------------------------------------------------------
/src/components/logs/index.ts:
--------------------------------------------------------------------------------
1 | export { DecodedLogs } from './DecodedLogs'
2 |
--------------------------------------------------------------------------------
/src/components/svgs/BrandIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '~/design-system'
2 | import { backgroundColorVars } from '~/design-system/styles/theme.css'
3 |
4 | export function BrandIcon({ size }: { size: `${number}px` }) {
5 | return (
6 |
15 |
20 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/svgs/Cog.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 | import { keyframes } from '@vanilla-extract/css'
3 |
4 | const circularAnimation = keyframes({
5 | from: {
6 | transform: 'rotate(0deg)',
7 | },
8 | to: {
9 | transform: 'rotate(360deg)',
10 | },
11 | })
12 |
13 | const transformProperties = style({
14 | transformBox: 'fill-box',
15 | transformOrigin: 'center',
16 | })
17 |
18 | export const spin = style([
19 | transformProperties,
20 | {
21 | animationName: circularAnimation,
22 | animationDuration: '3s',
23 | animationIterationCount: 'infinite',
24 | animationTimingFunction: 'linear',
25 | },
26 | ])
27 |
--------------------------------------------------------------------------------
/src/components/svgs/Cog.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '~/design-system'
2 | import { backgroundColorVars } from '~/design-system/styles/theme.css'
3 |
4 | import * as styles from './Cog.css'
5 |
6 | export function Cog({ size }: { size: string }) {
7 | return (
8 |
19 |
24 |
30 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/svgs/Cogs.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 | import { keyframes } from '@vanilla-extract/css'
3 |
4 | const circularAnimation = keyframes({
5 | from: {
6 | transform: 'rotate(0deg)',
7 | },
8 | to: {
9 | transform: 'rotate(360deg)',
10 | },
11 | })
12 |
13 | const floatAnimation = keyframes({
14 | '0%': {
15 | transform: 'rotate(0deg) translateX(1px) rotate(0deg)',
16 | },
17 | '100%': {
18 | transform: 'rotate(360deg) translateX(1px) rotate(-360deg)',
19 | },
20 | })
21 |
22 | const transformProperties = style({
23 | transformBox: 'fill-box',
24 | transformOrigin: 'center',
25 | })
26 |
27 | export const spin = style([
28 | transformProperties,
29 | {
30 | animationName: circularAnimation,
31 | animationDuration: '30s',
32 | animationIterationCount: 'infinite',
33 | animationTimingFunction: 'linear',
34 | },
35 | ])
36 |
37 | export const float = style([
38 | transformProperties,
39 | {
40 | animationName: floatAnimation,
41 | animationDuration: '5s',
42 | animationIterationCount: 'infinite',
43 | animationTimingFunction: 'linear',
44 | },
45 | ])
46 |
--------------------------------------------------------------------------------
/src/components/svgs/Spinner.tsx:
--------------------------------------------------------------------------------
1 | // TODO: Put in ~/design-system
2 | import { Box } from '~/design-system'
3 |
4 | export function Spinner({ size }: { size: string }) {
5 | return (
6 |
16 |
23 |
24 |
25 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/svgs/Wallet.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '~/design-system'
2 | import { backgroundColorVars } from '~/design-system/styles/theme.css'
3 |
4 | export function Wallet({ size }: { size: `${number}px` }) {
5 | return (
6 |
15 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/svgs/index.ts:
--------------------------------------------------------------------------------
1 | export { BrandIcon } from './BrandIcon'
2 | export { Cogs } from './Cogs'
3 | export { Cog } from './Cog'
4 | export { Spinner } from './Spinner'
5 | export { Wallet } from './Wallet'
6 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsContent.tsx:
--------------------------------------------------------------------------------
1 | import * as Tabs_ from '@radix-ui/react-tabs'
2 | import type { ReactNode } from 'react'
3 |
4 | import { Box } from '~/design-system'
5 |
6 | type TabsContentProps = {
7 | children: ReactNode
8 | inset?: boolean
9 | scrollable?: boolean | 'auto'
10 | value: string
11 | }
12 |
13 | export function TabsContent({
14 | children,
15 | inset = true,
16 | scrollable = true,
17 | value,
18 | }: TabsContentProps) {
19 | return (
20 |
21 |
29 |
34 | {children}
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsList.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 |
3 | import { foregroundColorVars } from '~/design-system/styles/theme.css'
4 |
5 | export const tabTrigger = style({
6 | transition: 'unset',
7 | selectors: {
8 | '&[data-state="active"]': {
9 | boxShadow: `0px -1.5px 0px rgb(${foregroundColorVars['text/primary']}) inset`,
10 | },
11 | '&[data-state="inactive"]': {
12 | color: `rgb(${foregroundColorVars['text/tertiary']})`,
13 | },
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsList.tsx:
--------------------------------------------------------------------------------
1 | import * as Tabs_ from '@radix-ui/react-tabs'
2 |
3 | import { Bleed, Box, Inline, Separator, Text } from '~/design-system'
4 |
5 | import * as styles from './TabsList.css'
6 |
7 | type TabItem = { label: string; value: string }
8 |
9 | type TabsListProps = {
10 | items: TabItem[]
11 | onSelect?: (item: TabItem) => void
12 | }
13 |
14 | export function TabsList({ items, onSelect }: TabsListProps) {
15 | return (
16 | <>
17 |
18 |
19 | {items.map((item) => (
20 |
26 | onSelect?.(item)}
32 | style={{ height: '36px' }}
33 | >
34 | {item.label}
35 |
36 |
37 | ))}
38 |
39 |
40 |
41 |
42 |
43 | >
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/tabs/index.ts:
--------------------------------------------------------------------------------
1 | export { TabsContent } from './TabsContent'
2 | export { TabsList } from './TabsList'
3 |
--------------------------------------------------------------------------------
/src/constants/etherscan.ts:
--------------------------------------------------------------------------------
1 | export const etherscanApiUrls = {
2 | [1]: 'https://api.etherscan.io/api',
3 | [5]: 'https://api-goerli.etherscan.io/api',
4 | [11155111]: 'https://api-sepolia.etherscan.io/api',
5 | [10]: 'https://api-optimistic.etherscan.io/api',
6 | [420]: 'https://api-goerli-optimistic.etherscan.io/api',
7 | [137]: 'https://api.polygonscan.com/api',
8 | [80_001]: 'https://api-testnet.polygonscan.com/api',
9 | [42_161]: 'https://api.arbiscan.io/api',
10 | [421_613]: 'https://api-goerli.arbiscan.io/api',
11 | [56]: 'https://api.bscscan.com/api',
12 | [97]: 'https://api-testnet.bscscan.com/api',
13 | [128]: 'https://api.hecoinfo.com/api',
14 | [256]: 'https://api-testnet.hecoinfo.com/api',
15 | [250]: 'https://api.ftmscan.com/api',
16 | [4002]: 'https://api-testnet.ftmscan.com/api',
17 | [43114]: 'https://api.snowtrace.io/api',
18 | [43113]: 'https://api-testnet.snowtrace.io/api',
19 | [42220]: 'https://api.celoscan.io/api',
20 | [44787]: 'https://api-alfajores.celoscan.io/api',
21 | [8543]: 'https://api.basescan.org/api',
22 | [85431]: 'https://api-goerli.basescan.org/api',
23 | [7777777]: 'https://api.basescan.org/api',
24 | } as const
25 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export { erc20Abi } from './abi'
2 |
--------------------------------------------------------------------------------
/src/contexts/AppMeta.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react'
2 |
3 | export type AppMeta = {
4 | type: 'embedded' | 'standalone'
5 | }
6 |
7 | export const AppMetaContext = createContext({ type: 'standalone' })
8 |
9 | export function useAppMeta() {
10 | return useContext(AppMetaContext)
11 | }
12 |
--------------------------------------------------------------------------------
/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | export { type AppMeta, AppMetaContext, useAppMeta } from './AppMeta'
2 |
--------------------------------------------------------------------------------
/src/design-system/AccentColorProvider.tsx:
--------------------------------------------------------------------------------
1 | import { assignInlineVars } from '@vanilla-extract/dynamic'
2 | import chroma from 'chroma-js'
3 | import {
4 | type CSSProperties,
5 | type ReactNode,
6 | createContext,
7 | useContext,
8 | useMemo,
9 | } from 'react'
10 |
11 | import { inheritedColorVars } from './styles/theme.css'
12 | import { type ColorScheme, defaultInheritedColor } from './tokens'
13 | import { toRgb } from './utils/toRgb'
14 |
15 | const AccentColorContext = createContext<{
16 | scheme: ColorScheme
17 | foregroundStyle: CSSProperties
18 | style: CSSProperties
19 | }>({
20 | scheme: 'light',
21 | foregroundStyle: {},
22 | style: {},
23 | })
24 |
25 | export function AccentColorProvider({
26 | children,
27 | color,
28 | }: {
29 | children: ReactNode
30 | color: string
31 | }) {
32 | const scheme: ColorScheme = useMemo(
33 | () => (chroma.contrast(color, '#fff') > 2.125 ? 'dark' : 'light'),
34 | [color],
35 | )
36 | const foregroundStyle = useMemo(
37 | () => ({ color: defaultInheritedColor.accent[scheme] }),
38 | [scheme],
39 | )
40 | const style = useMemo(
41 | () => assignInlineVars({ [inheritedColorVars.accent]: toRgb(color) }),
42 | [color],
43 | )
44 |
45 | return (
46 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | export function useAccentColor() {
53 | return useContext(AccentColorContext)
54 | }
55 |
--------------------------------------------------------------------------------
/src/design-system/ColorSchemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, createContext, useContext, useMemo } from 'react'
2 |
3 | import { useAccentColor } from './AccentColorProvider'
4 | import {
5 | type BackgroundColor,
6 | type ColorScheme,
7 | backgroundColor,
8 | } from './tokens'
9 |
10 | type ColorSchemeProviderProps = {
11 | color: 'accent' | BackgroundColor
12 | children: ReactNode
13 | }
14 |
15 | export const ColorSchemeContext = createContext<{
16 | light: ColorScheme
17 | dark: ColorScheme
18 | }>({
19 | light: 'light',
20 | dark: 'dark',
21 | })
22 |
23 | export function ColorSchemeProvider({
24 | color,
25 | children,
26 | }: ColorSchemeProviderProps) {
27 | const { scheme } = useAccentColor()
28 | const parentScheme = useColorScheme()
29 |
30 | const lightScheme =
31 | color === 'accent'
32 | ? scheme
33 | : backgroundColor[color][parentScheme.light].scheme
34 |
35 | const darkScheme =
36 | color === 'accent'
37 | ? scheme
38 | : backgroundColor[color][parentScheme.dark].scheme
39 |
40 | const value = useMemo(
41 | () => ({ light: lightScheme, dark: darkScheme }),
42 | [darkScheme, lightScheme],
43 | )
44 |
45 | return (
46 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | export function useColorScheme() {
53 | return useContext(ColorSchemeContext)
54 | }
55 |
--------------------------------------------------------------------------------
/src/design-system/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, useMemo } from 'react'
2 |
3 | import { ColorSchemeContext } from './ColorSchemeProvider'
4 | import {
5 | colorModeProviderStyle,
6 | colorSchemeForThemeClass,
7 | } from './styles/theme.css'
8 | import type { Theme } from './tokens'
9 |
10 | interface ThemeProviderProps {
11 | children: ReactNode | ((args: { className: string }) => ReactNode)
12 | theme: Theme
13 | }
14 |
15 | const colorSchemeForTheme = {
16 | light: 'light',
17 | dark: 'dark',
18 | } as const
19 |
20 | const schemeClasses = {
21 | light: `${colorSchemeForThemeClass.light.light} ${colorSchemeForThemeClass.dark.light}`,
22 | dark: `${colorSchemeForThemeClass.light.dark} ${colorSchemeForThemeClass.dark.dark}`,
23 | }
24 |
25 | export function ThemeProvider({ children, theme }: ThemeProviderProps) {
26 | const colorScheme = colorSchemeForTheme[theme]
27 | const className = schemeClasses[colorScheme]
28 |
29 | return (
30 | ({
33 | light: colorScheme,
34 | dark: colorScheme,
35 | }),
36 | [colorScheme],
37 | )}
38 | >
39 | {typeof children === 'function' ? (
40 | children({ className })
41 | ) : (
42 |
45 | )}
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/design-system/_playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Design System
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/design-system/components/Bleed.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, forwardRef } from 'react'
2 |
3 | import type { NegatedSpacing } from '../tokens'
4 | import { Box } from './Box'
5 |
6 | type BleedProps = {
7 | bottom?: NegatedSpacing
8 | children?: ReactNode
9 | horizontal?: NegatedSpacing
10 | left?: NegatedSpacing
11 | right?: NegatedSpacing
12 | space?: NegatedSpacing
13 | top?: NegatedSpacing
14 | vertical?: NegatedSpacing
15 | }
16 |
17 | export const Bleed = forwardRef(function Bleed(
18 | {
19 | bottom,
20 | children,
21 | horizontal,
22 | left,
23 | right,
24 | space,
25 | top,
26 | vertical,
27 | }: BleedProps,
28 | ref,
29 | ) {
30 | return (
31 |
38 | {children}
39 |
40 | )
41 | })
42 |
--------------------------------------------------------------------------------
/src/design-system/components/Button.css.ts:
--------------------------------------------------------------------------------
1 | import { styleVariants } from '@vanilla-extract/css'
2 |
3 | export const buttonHeight = {
4 | '16px': 16,
5 | '18px': 18,
6 | '20px': 20,
7 | '24px': 24,
8 | '36px': 36,
9 | '44px': 44,
10 | } as const
11 | export type ButtonHeight = keyof typeof buttonHeight
12 |
13 | export const buttonHeightStyles = styleVariants(buttonHeight, (height) => [
14 | { height },
15 | ])
16 |
17 | export type ButtonKind = 'ghost' | 'solid' | 'stroked' | 'tint'
18 |
19 | export const buttonVariants = [
20 | 'ghost primary',
21 | 'ghost blue',
22 | 'ghost red',
23 | 'ghost green',
24 | 'solid invert',
25 | 'solid primary',
26 | 'solid fill',
27 | 'solid blue',
28 | 'solid red',
29 | 'solid green',
30 | 'stroked fill',
31 | 'stroked invert',
32 | 'stroked blue',
33 | 'stroked red',
34 | 'stroked green',
35 | 'tint blue',
36 | 'tint green',
37 | 'tint red',
38 | ] as const satisfies readonly `${ButtonKind} ${string}`[]
39 | export type ButtonVariant = (typeof buttonVariants)[number]
40 |
--------------------------------------------------------------------------------
/src/design-system/components/ButtonCopy.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, useEffect, useState } from 'react'
2 |
3 | import type { UnionOmit } from '~/types/utils'
4 |
5 | import { ButtonRoot, type ButtonRootProps } from './Button'
6 | import type { ButtonHeight } from './Button.css'
7 | import { symbolStylesForHeight, symbolStylesForVariant } from './ButtonSymbol'
8 | import { widthForHeight } from './ButtonSymbol.css'
9 | import { SFSymbol, type SFSymbolProps } from './SFSymbol'
10 |
11 | type ButtonCopyProps = UnionOmit<
12 | ButtonRootProps,
13 | 'children' | 'width' | 'onClick'
14 | > & {
15 | text: string
16 | }
17 |
18 | export const checkmarkStylesForHeight = {
19 | '16px': {
20 | size: '9px',
21 | },
22 | '18px': {
23 | size: '9px',
24 | },
25 | '20px': {
26 | size: '9px',
27 | },
28 | '24px': {
29 | size: '11px',
30 | },
31 | '36px': {
32 | size: '12px',
33 | },
34 | '44px': {
35 | size: '15px',
36 | },
37 | } satisfies Record
38 |
39 | export const ButtonCopy = forwardRef(
40 | (props: ButtonCopyProps, ref) => {
41 | const { height = '36px', variant = 'solid invert', text } = props
42 |
43 | const [copied, setCopied] = useState(false)
44 | useEffect(() => {
45 | if (copied) {
46 | navigator.clipboard.writeText((text || '').replace(/^"|"$/g, ''))
47 | setTimeout(() => setCopied(false), 1000)
48 | }
49 | }, [copied, text])
50 |
51 | return (
52 | {
57 | e.preventDefault()
58 | setCopied(true)
59 | }}
60 | >
61 | {copied ? (
62 |
67 | ) : (
68 |
73 | )}
74 |
75 | )
76 | },
77 | )
78 |
--------------------------------------------------------------------------------
/src/design-system/components/ButtonSymbol.css.ts:
--------------------------------------------------------------------------------
1 | import { styleVariants } from '@vanilla-extract/css'
2 |
3 | import { buttonHeight } from './Button.css'
4 |
5 | export const widthForHeight = styleVariants(buttonHeight, (height) => [
6 | { width: height },
7 | ])
8 |
--------------------------------------------------------------------------------
/src/design-system/components/ButtonSymbol.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react'
2 |
3 | import type { UnionOmit } from '~/types/utils'
4 |
5 | import { Tooltip } from '../../components'
6 | import type { SymbolName } from '../tokens'
7 | import { ButtonRoot, type ButtonRootProps } from './Button'
8 | import type { ButtonHeight, ButtonVariant } from './Button.css'
9 | import { widthForHeight } from './ButtonSymbol.css'
10 | import { SFSymbol } from './SFSymbol'
11 | import type { SFSymbolProps } from './SFSymbol'
12 |
13 | type ButtonSymbolProps = UnionOmit & {
14 | label: string
15 | symbol: SymbolName
16 | symbolProps?: Partial
17 | }
18 |
19 | export const symbolStylesForHeight = {
20 | '16px': {
21 | size: '9px',
22 | },
23 | '18px': {
24 | size: '11px',
25 | },
26 | '20px': {
27 | size: '11px',
28 | },
29 | '24px': {
30 | size: '12px',
31 | },
32 | '36px': {
33 | size: '15px',
34 | },
35 | '44px': {
36 | size: '18px',
37 | },
38 | } satisfies Record
39 |
40 | export const symbolStylesForVariant = {
41 | 'ghost primary': {},
42 | 'ghost blue': {
43 | color: 'surface/blue',
44 | },
45 | 'ghost green': {
46 | color: 'surface/green',
47 | },
48 | 'ghost red': {
49 | color: 'surface/red',
50 | },
51 | 'solid invert': {},
52 | 'solid primary': {},
53 | 'solid fill': {},
54 | 'solid blue': {},
55 | 'solid green': {},
56 | 'solid red': {},
57 | 'stroked fill': {},
58 | 'stroked invert': {},
59 | 'stroked blue': {
60 | color: 'surface/blue',
61 | },
62 | 'stroked red': {
63 | color: 'surface/red',
64 | },
65 | 'stroked green': {
66 | color: 'surface/green',
67 | },
68 | 'tint blue': {},
69 | 'tint green': {},
70 | 'tint red': {},
71 | } satisfies Record
72 |
73 | export const ButtonSymbol = forwardRef(
74 | (
75 | { label, symbol, symbolProps, width, ...rootProps }: ButtonSymbolProps,
76 | ref,
77 | ) => {
78 | const { height = '36px', variant = 'solid invert' } = rootProps
79 | return (
80 |
81 |
88 |
94 |
95 |
96 | )
97 | },
98 | )
99 |
--------------------------------------------------------------------------------
/src/design-system/components/ButtonText.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react'
2 |
3 | import { Box } from './Box'
4 | import type { BoxStyles } from './Box.css'
5 | import { ButtonRoot, type ButtonRootProps } from './Button'
6 | import type { ButtonHeight, ButtonVariant } from './Button.css'
7 | import { Text, type TextProps } from './Text'
8 |
9 | type ButtonTextProps = ButtonRootProps
10 |
11 | const stylesForHeight = {
12 | '16px': {
13 | paddingHorizontal: '4px',
14 | },
15 | '18px': {
16 | paddingHorizontal: '6px',
17 | },
18 | '20px': {
19 | paddingHorizontal: '6px',
20 | },
21 | '24px': {
22 | paddingHorizontal: '6px',
23 | },
24 | '36px': {
25 | paddingHorizontal: '12px',
26 | },
27 | '44px': {
28 | paddingHorizontal: '16px',
29 | },
30 | } satisfies Record
31 |
32 | const textStylesForHeight = {
33 | '16px': {
34 | size: '9px',
35 | },
36 | '18px': {
37 | size: '11px',
38 | },
39 | '20px': {
40 | size: '11px',
41 | },
42 | '24px': {
43 | size: '11px',
44 | },
45 | '36px': {
46 | size: '15px',
47 | },
48 | '44px': {
49 | size: '18px',
50 | },
51 | } satisfies Record
52 |
53 | const textStylesForVariant = {
54 | 'ghost primary': {},
55 | 'ghost blue': {
56 | color: 'surface/blue',
57 | },
58 | 'ghost green': {
59 | color: 'surface/green',
60 | },
61 | 'ghost red': {
62 | color: 'surface/red',
63 | },
64 | 'solid invert': {},
65 | 'solid primary': {},
66 | 'solid fill': {},
67 | 'solid blue': {},
68 | 'solid green': {},
69 | 'solid red': {},
70 | 'stroked fill': {},
71 | 'stroked invert': {},
72 | 'stroked blue': {
73 | color: 'surface/blue',
74 | },
75 | 'stroked red': {
76 | color: 'surface/red',
77 | },
78 | 'stroked green': {
79 | color: 'surface/green',
80 | },
81 | 'tint blue': {},
82 | 'tint green': {},
83 | 'tint red': {},
84 | } satisfies Record
85 |
86 | export const ButtonText = forwardRef(
87 | ({ children, width = 'full', ...props }: ButtonTextProps, ref) => {
88 | const { height = '36px', variant = 'solid invert' } = props
89 | return (
90 |
91 |
92 |
96 | {children}
97 |
98 |
99 |
100 | )
101 | },
102 | )
103 |
--------------------------------------------------------------------------------
/src/design-system/components/Columns.css.ts:
--------------------------------------------------------------------------------
1 | import { fallbackVar, styleVariants } from '@vanilla-extract/css'
2 | import { calc } from '@vanilla-extract/css-utils'
3 |
4 | import { gapVar } from './Box.css'
5 |
6 | const columnWidths = {
7 | '0': [0, 1],
8 | '1/2': [1, 2],
9 | '1/3': [1, 3],
10 | '1/4': [1, 4],
11 | '1/5': [1, 5],
12 | '2/3': [2, 3],
13 | '2/5': [2, 5],
14 | '3/4': [3, 4],
15 | '3/5': [3, 5],
16 | '4/5': [4, 5],
17 | } as const
18 |
19 | export const width = styleVariants(columnWidths, ([numerator, denominator]) => {
20 | const gapOffset = calc.subtract(
21 | fallbackVar(gapVar, '0px'),
22 | calc(fallbackVar(gapVar, '0px')).divide(denominator).multiply(numerator),
23 | )
24 |
25 | return {
26 | width: calc.subtract(`${(numerator * 100) / denominator}%`, gapOffset),
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/src/design-system/components/Inline.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, forwardRef } from 'react'
2 |
3 | import type { Spacing } from '../tokens'
4 | import { Box } from './Box'
5 | import type { BoxStyles } from './Box.css'
6 |
7 | const alignHorizontalToJustifyContent = {
8 | center: 'center',
9 | justify: 'space-between',
10 | left: 'flex-start',
11 | right: 'flex-end',
12 | } as const
13 | type AlignHorizontal = keyof typeof alignHorizontalToJustifyContent
14 |
15 | const alignVerticalToAlignItems = {
16 | bottom: 'flex-end',
17 | center: 'center',
18 | top: 'flex-start',
19 | } as const
20 | type AlignVertical = keyof typeof alignVerticalToAlignItems
21 |
22 | type InlineProps = {
23 | alignHorizontal?: AlignHorizontal
24 | alignVertical?: AlignVertical
25 | children?: ReactNode
26 | gap?: Spacing
27 | height?: BoxStyles['height']
28 | wrap?: boolean
29 | }
30 |
31 | export const Inline = forwardRef(
32 | (
33 | {
34 | alignHorizontal = 'left',
35 | alignVertical,
36 | children,
37 | gap,
38 | height,
39 | wrap = true,
40 | },
41 | ref,
42 | ) => {
43 | return (
44 |
55 | {children}
56 |
57 | )
58 | },
59 | )
60 |
--------------------------------------------------------------------------------
/src/design-system/components/Input.css.ts:
--------------------------------------------------------------------------------
1 | import { style, styleVariants } from '@vanilla-extract/css'
2 |
3 | import { backgroundColorVars, inheritedColorVars } from '../styles/theme.css'
4 |
5 | export const backgroundStyle = style({
6 | transition: 'border-color 100ms ease',
7 | })
8 |
9 | export const inputHeights = {
10 | '24px': 24,
11 | '36px': 36,
12 | } as const
13 | export type InputHeight = keyof typeof inputHeights
14 |
15 | export type InputState = 'warning' | 'error'
16 |
17 | export type InputKind = 'solid'
18 | export const inputVariants = [
19 | 'solid',
20 | ] as const satisfies readonly `${InputKind}`[]
21 | export type InputVariant = (typeof inputVariants)[number]
22 |
23 | export const heightStyles = styleVariants(inputHeights, (height) => [
24 | { height },
25 | ])
26 |
27 | export const placeholderStyle = style({
28 | '::placeholder': {
29 | color: `rgb(${inheritedColorVars.text} / 0.4)`,
30 | },
31 | })
32 |
33 | export const disabledStyle = style({
34 | selectors: {
35 | '&[disabled]': {
36 | pointerEvents: 'none',
37 | },
38 | },
39 | })
40 |
41 | export const invalidStyle = style({
42 | selectors: {
43 | '&[data-invalid="true"]': {
44 | borderColor: `rgb(${backgroundColorVars['surface/red']})`,
45 | },
46 | },
47 | })
48 |
--------------------------------------------------------------------------------
/src/design-system/components/Inset.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, forwardRef } from 'react'
2 |
3 | import type { Spacing } from '../tokens'
4 | import { Box } from './Box'
5 | import type { BoxStyles } from './Box.css'
6 |
7 | type InsetProps = {
8 | bottom?: Spacing
9 | children?: ReactNode
10 | height?: BoxStyles['height']
11 | horizontal?: Spacing
12 | left?: Spacing
13 | right?: Spacing
14 | space?: Spacing
15 | top?: Spacing
16 | vertical?: Spacing
17 | }
18 |
19 | export const Inset = forwardRef(
20 | (
21 | {
22 | bottom,
23 | children,
24 | height,
25 | horizontal,
26 | left,
27 | right,
28 | space,
29 | top,
30 | vertical,
31 | }: InsetProps,
32 | ref,
33 | ) => {
34 | return (
35 |
44 | {children}
45 |
46 | )
47 | },
48 | )
49 |
--------------------------------------------------------------------------------
/src/design-system/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { forwardRef, useContext } from 'react'
3 | import { Link as RouterLink } from 'react-router-dom'
4 |
5 | import { Box } from './Box'
6 | import type { BoxStyles } from './Box.css'
7 | import { TextContext } from './Text'
8 | import type { TextStyles } from './Text.css'
9 | import * as styles from './Text.css'
10 |
11 | export type LinkProps = {
12 | align?: TextStyles['textAlign']
13 | children: React.ReactNode
14 | color?: TextStyles['color']
15 | size?: TextStyles['fontSize']
16 | style?: React.CSSProperties
17 | weight?: TextStyles['fontWeight']
18 | width?: BoxStyles['width']
19 | testId?: string
20 | } & (
21 | | {
22 | external: true
23 | href: string
24 | to?: never
25 | }
26 | | {
27 | external?: false
28 | href?: never
29 | to?: string
30 | }
31 | )
32 |
33 | export const Link = forwardRef(
34 | (
35 | {
36 | align,
37 | children,
38 | color = 'text',
39 | external,
40 | href,
41 | size: size_,
42 | style,
43 | weight = 'regular',
44 | testId,
45 | to,
46 | }: LinkProps,
47 | ref,
48 | ) => {
49 | const { root } = useContext(TextContext)
50 | const inline = !root
51 | const size = size_ || (inline ? undefined : '15px')
52 | const textStyle = inline ? styles.inlineText : styles.capsizedText
53 | const wrap = (children: React.ReactElement) =>
54 | external || !to ? children : {children}
55 | return (
56 |
57 | {wrap(
58 |
74 | {children}
75 | ,
76 | )}
77 |
78 | )
79 | },
80 | )
81 |
--------------------------------------------------------------------------------
/src/design-system/components/Rows.css.ts:
--------------------------------------------------------------------------------
1 | import { fallbackVar, styleVariants } from '@vanilla-extract/css'
2 | import { calc } from '@vanilla-extract/css-utils'
3 |
4 | import { gapVar } from './Box.css'
5 |
6 | const rowHeights = {
7 | '1/2': [1, 2],
8 | '1/3': [1, 3],
9 | '1/4': [1, 4],
10 | '1/5': [1, 5],
11 | '2/3': [2, 3],
12 | '2/5': [2, 5],
13 | '3/4': [3, 4],
14 | '3/5': [3, 5],
15 | '4/5': [4, 5],
16 | } as const
17 |
18 | export const height = styleVariants(rowHeights, ([numerator, denominator]) => {
19 | const gapOffset = calc.subtract(
20 | fallbackVar(gapVar, '0px'),
21 | calc(fallbackVar(gapVar, '0px')).divide(denominator).multiply(numerator),
22 | )
23 |
24 | return {
25 | height: calc.subtract(`${(numerator * 100) / denominator}%`, gapOffset),
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/src/design-system/components/SFSymbol.tsx:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx'
2 | import { forwardRef } from 'react'
3 |
4 | import symbols from '../symbols/generated'
5 | import type { FontSize, FontWeight, SymbolName } from '../tokens'
6 | import { Box } from './Box'
7 | import type { BoxStyles } from './Box.css'
8 |
9 | export type SFSymbolProps = {
10 | className?: ClassValue
11 | color?: BoxStyles['color']
12 | weight?: FontWeight
13 | symbol: SymbolName
14 | size?: FontSize
15 | }
16 |
17 | export const SFSymbol = forwardRef(
18 | (
19 | {
20 | className,
21 | color = 'text',
22 | symbol: name,
23 | weight = 'regular',
24 | size = '16px',
25 | }: SFSymbolProps,
26 | ref,
27 | ) => {
28 | const symbol = symbols[name as keyof typeof symbols][weight]
29 | return (
30 |
43 |
44 |
45 | )
46 | },
47 | )
48 |
--------------------------------------------------------------------------------
/src/design-system/components/Select.css.ts:
--------------------------------------------------------------------------------
1 | import { style, styleVariants } from '@vanilla-extract/css'
2 |
3 | import { backgroundColorVars } from '../styles/theme.css'
4 |
5 | export const backgroundStyle = style({
6 | transition: 'border-color 100ms ease',
7 | })
8 |
9 | export const selectHeights = {
10 | '24px': 24,
11 | '36px': 36,
12 | } as const
13 | export type SelectHeight = keyof typeof selectHeights
14 |
15 | export type SelectKind = 'solid'
16 | export const selectVariants = [
17 | 'solid',
18 | ] as const satisfies readonly `${SelectKind}`[]
19 | export type SelectVariant = (typeof selectVariants)[number]
20 |
21 | export const heightStyles = styleVariants(selectHeights, (height) => [
22 | { height },
23 | ])
24 |
25 | export const disabledStyle = style({
26 | selectors: {
27 | '&[disabled]': {
28 | pointerEvents: 'none',
29 | },
30 | },
31 | })
32 |
33 | export const invalidStyle = style({
34 | selectors: {
35 | '&[data-invalid="true"]': {
36 | borderColor: `rgb(${backgroundColorVars['surface/red']})`,
37 | },
38 | },
39 | })
40 |
--------------------------------------------------------------------------------
/src/design-system/components/Select.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, type SelectHTMLAttributes, forwardRef } from 'react'
2 |
3 | import { Box } from './Box'
4 | import type { BoxStyles } from './Box.css'
5 | import {
6 | type SelectHeight,
7 | type SelectVariant,
8 | backgroundStyle,
9 | disabledStyle,
10 | heightStyles,
11 | invalidStyle,
12 | } from './Select.css'
13 | import type { TextStyles } from './Text.css'
14 | import * as styles from './Text.css'
15 |
16 | export type SelectProps = Omit<
17 | SelectHTMLAttributes,
18 | keyof BoxStyles
19 | > & {
20 | children: ReactNode
21 | height?: SelectHeight
22 | placeholder?: string
23 | testId?: string
24 | variant?: SelectVariant
25 | }
26 |
27 | export const stylesForVariant = {
28 | solid: {
29 | backgroundColor: {
30 | default: 'surface/primary/elevated',
31 | disabled: 'surface/secondary/elevated',
32 | },
33 | borderColor: {
34 | default: 'surface/invert@0.2',
35 | hover: 'surface/invert@0.3',
36 | focus: 'surface/invert@0.7',
37 | hoverfocus: 'surface/invert@0.7',
38 | },
39 | },
40 | } satisfies Record
41 |
42 | export const stylesForHeight = {
43 | '24px': {
44 | paddingHorizontal: '2px',
45 | },
46 | '36px': {
47 | paddingHorizontal: '8px',
48 | },
49 | } satisfies Record
50 |
51 | export const textStylesForHeight = {
52 | '24px': {
53 | fontSize: '11px',
54 | },
55 | '36px': {
56 | fontSize: '15px',
57 | },
58 | } satisfies Record
59 |
60 | export const Select = forwardRef(
61 | (
62 | { placeholder, height = '36px', variant = 'solid', testId, ...selectProps },
63 | ref,
64 | ) => {
65 | return (
66 |
86 | )
87 | },
88 | )
89 |
--------------------------------------------------------------------------------
/src/design-system/components/Separator.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, styleVariants } from '@vanilla-extract/css'
2 |
3 | export const strokeWeightVar = createVar()
4 |
5 | export const orientation = styleVariants({
6 | horizontal: {
7 | height: strokeWeightVar,
8 | width: '100%',
9 | },
10 | vertical: {
11 | height: '100%',
12 | width: strokeWeightVar,
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/src/design-system/components/Separator.tsx:
--------------------------------------------------------------------------------
1 | import { assignInlineVars } from '@vanilla-extract/dynamic'
2 | import { forwardRef } from 'react'
3 |
4 | import type { StrokeWeight } from '../tokens'
5 | import { Box } from './Box'
6 | import type { BoxStyles } from './Box.css'
7 | import * as styles from './Separator.css'
8 |
9 | export type SeparatorProps = {
10 | color?: BoxStyles['backgroundColor']
11 | orientation?: 'horizontal' | 'vertical'
12 | strokeWeight?: StrokeWeight
13 | }
14 |
15 | export const Separator = forwardRef(
16 | (
17 | {
18 | color = 'separator/tertiary',
19 | orientation = 'horizontal',
20 | strokeWeight = '1px',
21 | }: SeparatorProps,
22 | ref,
23 | ) => {
24 | return (
25 |
34 | )
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/src/design-system/components/Stack.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, forwardRef } from 'react'
2 |
3 | import type { Spacing } from '../tokens'
4 | import { Box } from './Box'
5 | import type { BoxStyles } from './Box.css'
6 |
7 | const alignHorizontalToAlignItems = {
8 | center: 'center',
9 | left: 'flex-start',
10 | right: 'flex-end',
11 | stretch: 'stretch',
12 | } as const
13 | type AlignHorizontal = keyof typeof alignHorizontalToAlignItems
14 |
15 | export type StackProps = {
16 | alignHorizontal?: AlignHorizontal
17 | gap?: Spacing
18 | children: ReactNode
19 | width?: BoxStyles['width']
20 | }
21 |
22 | export const Stack = forwardRef(function Stack(
23 | { alignHorizontal, children, gap, width = 'full' }: StackProps,
24 | ref,
25 | ) {
26 | return (
27 |
37 | {children}
38 |
39 | )
40 | })
41 |
--------------------------------------------------------------------------------
/src/design-system/components/Text.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 | import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles'
3 | import { mapValues } from 'remeda'
4 |
5 | import {
6 | backgroundColorVars,
7 | foregroundColorVars,
8 | inheritedColorVars,
9 | } from '../styles/theme.css'
10 | import { fontFamily, fontSize, fontWeight } from '../tokens'
11 |
12 | const textProperties = (inline: boolean) =>
13 | defineProperties({
14 | properties: {
15 | color: {
16 | accent: `rgb(${inheritedColorVars.accent})`,
17 | text: `rgb(${inheritedColorVars.text})`,
18 | ...mapValues(backgroundColorVars, (colorVar) => `rgb(${colorVar})`),
19 | ...mapValues(foregroundColorVars, (colorVar) => `rgb(${colorVar})`),
20 | },
21 | fontFamily,
22 | fontSize: fontSize(inline),
23 | fontWeight,
24 | overflowWrap: ['anywhere', 'break-word'],
25 | textAlign: ['left', 'center', 'right'],
26 | textDecoration: ['underline'],
27 | textUnderlineOffset: ['2px'],
28 | whiteSpace: ['nowrap'],
29 | },
30 | })
31 |
32 | export const inlineText = createSprinkles(textProperties(true))
33 | export const capsizedText = createSprinkles(textProperties(false))
34 | export type TextStyles = Parameters[0]
35 |
36 | export const tabular = style({
37 | fontVariant: 'tabular-nums',
38 | letterSpacing: '0px',
39 | })
40 |
41 | export const ellipsis = style({
42 | textOverflow: 'ellipsis',
43 | overflow: 'hidden',
44 | whiteSpace: 'nowrap',
45 | })
46 |
47 | export const overflow = style({
48 | overflow: 'visible',
49 | })
50 |
--------------------------------------------------------------------------------
/src/design-system/index.ts:
--------------------------------------------------------------------------------
1 | export { AccentColorProvider } from './AccentColorProvider'
2 | export { ThemeProvider } from './ThemeProvider'
3 | export type { Theme } from './tokens'
4 |
5 | export { Bleed } from './components/Bleed'
6 | export { Box } from './components/Box'
7 | export { Button } from './components/Button'
8 | export { Column, Columns } from './components/Columns'
9 | export { Inline } from './components/Inline'
10 | export { Input } from './components/Input'
11 | export { Inset } from './components/Inset'
12 | export { Link } from './components/Link'
13 | export { Rows, Row } from './components/Rows'
14 | export { Select } from './components/Select'
15 | export { Separator } from './components/Separator'
16 | export { SFSymbol } from './components/SFSymbol'
17 | export { Stack } from './components/Stack'
18 | export { Text } from './components/Text'
19 |
20 | export { initializeTheme } from './utils/initializeTheme'
21 | export { getTheme, setTheme } from './utils/theme'
22 |
--------------------------------------------------------------------------------
/src/design-system/styles/global.css.ts:
--------------------------------------------------------------------------------
1 | import { globalFontFace, globalStyle } from '@vanilla-extract/css'
2 |
3 | import { fontFamily } from '../tokens'
4 | import { backgroundColorVars } from './theme.css'
5 |
6 | const fonts = {
7 | SFPro: [
8 | ['SFPro-Light', 300],
9 | ['SFPro-Regular', 400],
10 | ['SFPro-Medium', 500],
11 | ['SFPro-Semibold', 600],
12 | ['SFPro-Bold', 700],
13 | ['SFPro-Heavy', 800],
14 | ],
15 | SFMono: [
16 | ['SFMono-Regular', 400],
17 | ['SFMono-Medium', 500],
18 | ['SFMono-Semibold', 600],
19 | ],
20 | }
21 |
22 | Object.entries(fonts).forEach(([familyName, sets]) => {
23 | sets.forEach(([name, fontWeight]) => {
24 | globalFontFace(familyName, {
25 | src: `url('/fonts/${name}.woff2') format('woff2')`,
26 | fontWeight,
27 | fontStyle: 'normal',
28 | fontDisplay: 'auto',
29 | })
30 | })
31 | })
32 |
33 | globalStyle('html, body', {
34 | backgroundColor: `rgb(${backgroundColorVars['surface/primary']})`,
35 | fontFamily: fontFamily.default,
36 | fontFeatureSettings: '"rlig" 1, "calt" 1',
37 | fontSize: '16px',
38 | margin: 0,
39 | padding: 0,
40 | border: 0,
41 | boxSizing: 'border-box',
42 | })
43 |
44 | globalStyle('code', {
45 | fontFamily: fontFamily.mono,
46 | })
47 |
48 | globalStyle('pre', {
49 | margin: 0,
50 | })
51 |
--------------------------------------------------------------------------------
/src/design-system/styles/reset.css.ts:
--------------------------------------------------------------------------------
1 | import { globalStyle, layer, style } from '@vanilla-extract/css'
2 |
3 | const reset = layer('reset')
4 |
5 | globalStyle('a', {
6 | textDecoration: 'none',
7 | color: 'inherit',
8 | })
9 |
10 | export const resetBase = style({
11 | '@layer': {
12 | [reset]: {
13 | margin: 0,
14 | padding: 0,
15 | border: 0,
16 | boxSizing: 'border-box',
17 | fontSize: '100%',
18 | font: 'inherit',
19 | verticalAlign: 'baseline',
20 | },
21 | },
22 | })
23 |
24 | const list = style({
25 | '@layer': { [reset]: { listStyle: 'none' } },
26 | })
27 |
28 | const table = style({
29 | '@layer': { [reset]: { borderCollapse: 'collapse', borderSpacing: 0 } },
30 | })
31 |
32 | const appearanceNone = style({
33 | '@layer': { [reset]: { appearance: 'none' } },
34 | })
35 |
36 | const backgroundTransparent = style({
37 | '@layer': { [reset]: { backgroundColor: 'transparent' } },
38 | })
39 |
40 | const button = style([
41 | backgroundTransparent,
42 | {
43 | '@layer': {
44 | [reset]: {
45 | color: 'unset',
46 | cursor: 'default',
47 | display: 'block',
48 | textAlign: 'unset',
49 | },
50 | },
51 | },
52 | ])
53 |
54 | const field = [appearanceNone, backgroundTransparent]
55 |
56 | const quotes = style({
57 | '@layer': {
58 | [reset]: {
59 | quotes: 'none',
60 | selectors: {
61 | '&:before, &:after': {
62 | content: ["''", 'none'],
63 | },
64 | },
65 | },
66 | },
67 | })
68 |
69 | const select = style([
70 | field,
71 | {
72 | '@layer': {
73 | [reset]: {
74 | backgroundColor: 'unset',
75 | ':disabled': {
76 | opacity: 1,
77 | },
78 | selectors: {
79 | '&:focus-visible': {
80 | outline: 'none',
81 | },
82 | '&::-ms-expand': {
83 | display: 'none',
84 | },
85 | },
86 | },
87 | },
88 | },
89 | ])
90 |
91 | const input = style([
92 | field,
93 | style({
94 | '@layer': {
95 | [reset]: {
96 | selectors: {
97 | '&:focus-visible': {
98 | outline: 'none',
99 | },
100 | '&::-ms-clear': {
101 | display: 'none',
102 | },
103 | '&::-webkit-search-cancel-button': {
104 | WebkitAppearance: 'none',
105 | },
106 | },
107 | },
108 | },
109 | }),
110 | ])
111 |
112 | export const resetElements = {
113 | blockquote: quotes,
114 | button,
115 | input,
116 | ol: list,
117 | q: quotes,
118 | select,
119 | table,
120 | ul: list,
121 | }
122 |
--------------------------------------------------------------------------------
/src/design-system/utils/initializeTheme.critical.ts:
--------------------------------------------------------------------------------
1 | import { getTheme } from './theme'
2 |
3 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
4 |
5 | const { storageTheme, systemTheme } = getTheme()
6 | const theme = storageTheme || systemTheme || 'dark'
7 |
8 | document.documentElement.dataset.theme = theme
9 |
10 | if (!storageTheme) {
11 | // Update the theme if the user changes their OS preference
12 | darkModeMediaQuery.addEventListener('change', ({ matches: isDark }) => {
13 | document.documentElement.dataset.theme = isDark ? 'dark' : 'light'
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/design-system/utils/initializeTheme.ts:
--------------------------------------------------------------------------------
1 | import { colorSchemeForThemeClass } from '../styles/theme.css'
2 |
3 | export function initializeTheme() {
4 | // Set the initial color contexts to match their respective themes
5 | document.body.classList.add(
6 | colorSchemeForThemeClass.light.light,
7 | colorSchemeForThemeClass.dark.dark,
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/design-system/utils/theme.ts:
--------------------------------------------------------------------------------
1 | import type { Theme } from '../tokens'
2 |
3 | export function getTheme(): {
4 | storageTheme: Theme | null
5 | systemTheme: Theme | null
6 | } {
7 | const storageTheme =
8 | typeof localStorage !== 'undefined'
9 | ? (localStorage.getItem('theme') as Theme)
10 | : null
11 | const systemTheme =
12 | // eslint-disable-next-line no-nested-ternary
13 | typeof window !== 'undefined'
14 | ? window.matchMedia('(prefers-color-scheme: light)').matches
15 | ? 'light'
16 | : 'dark'
17 | : null
18 | return { storageTheme, systemTheme }
19 | }
20 |
21 | export function setTheme(theme: Theme) {
22 | localStorage.setItem('theme', theme)
23 | document.documentElement.dataset.theme = theme
24 | }
25 |
--------------------------------------------------------------------------------
/src/design-system/utils/toRgb.ts:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js'
2 |
3 | export function toRgb(color: string) {
4 | const [r, g, b, a] = chroma(color).rgba()
5 | return `${r} ${g} ${b}${a !== 1 ? ` / ${a}` : ''}`
6 | }
7 |
--------------------------------------------------------------------------------
/src/design-system/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path'
2 | import { defineConfig } from 'vite'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | build: {
7 | lib: {
8 | formats: ['iife'],
9 | name: 'theme',
10 | entry: [join(__dirname, './utils/initializeTheme.critical.ts')],
11 | },
12 | copyPublicDir: true,
13 | minify: true,
14 | outDir: 'public',
15 | emptyOutDir: false,
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/src/entries/background/commands.ts:
--------------------------------------------------------------------------------
1 | import { getMessenger } from '~/messengers'
2 |
3 | const walletMessenger = getMessenger('background:wallet')
4 |
5 | export function handleCommands() {
6 | chrome.commands.onCommand.addListener((command) => {
7 | if (command === 'toggle-theme')
8 | walletMessenger.send('toggleTheme', undefined)
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/entries/background/context-menu.ts:
--------------------------------------------------------------------------------
1 | export function setupContextMenu() {
2 | chrome.sidePanel
3 | .setPanelBehavior({ openPanelOnActionClick: true })
4 | .catch((error) => console.error(error))
5 |
6 | // TODO: Only create context menu if selected text is "openable" in Rivet.
7 | // chrome.contextMenus.create({
8 | // id: 'open',
9 | // title: 'Open in Rivet',
10 | // contexts: ['selection'],
11 | // })
12 |
13 | chrome.contextMenus.create({
14 | id: 'open-wallet-tab',
15 | title: 'Open Wallet in a New Tab',
16 | type: 'normal',
17 | contexts: ['action'],
18 | })
19 |
20 | if (process.env.NODE_ENV === 'development') {
21 | chrome.contextMenus.create({
22 | id: 'open-design-system',
23 | title: 'Open Design System',
24 | type: 'normal',
25 | contexts: ['action'],
26 | })
27 | chrome.contextMenus.create({
28 | id: 'open-components',
29 | title: 'Open Component Playground',
30 | type: 'normal',
31 | contexts: ['action'],
32 | })
33 | chrome.contextMenus.create({
34 | id: 'open-test-dapp',
35 | title: 'Open Test Dapp',
36 | type: 'normal',
37 | contexts: ['action'],
38 | })
39 | }
40 |
41 | chrome.contextMenus.onClicked.addListener(({ menuItemId }) => {
42 | if (menuItemId === 'open-wallet-tab') {
43 | chrome.tabs.create({
44 | url: `chrome-extension://${chrome.runtime.id}/src/index.html`,
45 | })
46 | } else if (menuItemId === 'open-design-system') {
47 | chrome.tabs.create({
48 | url: `chrome-extension://${chrome.runtime.id}/src/design-system/_playground/index.html`,
49 | })
50 | } else if (menuItemId === 'open-components') {
51 | chrome.tabs.create({
52 | url: `chrome-extension://${chrome.runtime.id}/src/components/_playground/index.html`,
53 | })
54 | } else if (menuItemId === 'open-test-dapp') {
55 | chrome.tabs.create({
56 | url: 'http://localhost:5173',
57 | })
58 | }
59 | // TODO: Match selected text.
60 | // } else if (menuItemId === 'open') {
61 | // inpageMessenger.send('toggleWallet', {
62 | // open: true,
63 | // route: `/transaction/${selectionText}`,
64 | // })
65 | // }
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/src/entries/background/extension-id.ts:
--------------------------------------------------------------------------------
1 | import { getMessenger } from '~/messengers'
2 |
3 | export function setupExtensionId() {
4 | const messenger = getMessenger('background:contentScript')
5 | messenger.reply('extensionId', async () => chrome.runtime.id)
6 | }
7 |
--------------------------------------------------------------------------------
/src/entries/background/index.ts:
--------------------------------------------------------------------------------
1 | import { syncStores } from '~/zustand'
2 |
3 | import { getMessenger } from '../../messengers'
4 | import { handleCommands } from './commands'
5 | import { setupContextMenu } from './context-menu'
6 | import { setupExtensionId } from './extension-id'
7 | import { setupInpage } from './inpage'
8 | import { interceptJsonRpcRequests } from './intercept-requests'
9 | import { setupRpcHandler } from './rpc'
10 | import { setupWalletSidebarHandler } from './wallet-sidebar'
11 |
12 | const contentMessenger = getMessenger('background:contentScript')
13 | const inpageMessenger = getMessenger('background:inpage')
14 | const walletMessenger = getMessenger('background:wallet')
15 |
16 | contentMessenger.reply('ping', async () => 'pong')
17 |
18 | handleCommands()
19 | interceptJsonRpcRequests()
20 | setupContextMenu()
21 | setupExtensionId()
22 | setupInpage()
23 | setupRpcHandler({ messenger: inpageMessenger })
24 | setupRpcHandler({ messenger: walletMessenger })
25 | setupWalletSidebarHandler()
26 | syncStores()
27 |
--------------------------------------------------------------------------------
/src/entries/background/inpage.ts:
--------------------------------------------------------------------------------
1 | export function setupInpage() {
2 | chrome.scripting.registerContentScripts([
3 | {
4 | id: 'inpage',
5 | matches: ['file://*/*', 'http://*/*', 'https://*/*'],
6 | js: ['inpage.js'],
7 | runAt: 'document_start',
8 | world: 'MAIN',
9 | },
10 | ])
11 | }
12 |
--------------------------------------------------------------------------------
/src/entries/background/wallet-sidebar.ts:
--------------------------------------------------------------------------------
1 | import { settingsStore } from '../../zustand'
2 |
3 | export function setupWalletSidebarHandler() {
4 | chrome.runtime.onMessage.addListener((message, sender) => {
5 | if (message.type === 'openWallet') {
6 | const { bypassSignatureAuth, bypassTransactionAuth } =
7 | settingsStore.getState()
8 | const { method } = message.payload
9 | if (
10 | method === 'eth_requestAccounts' ||
11 | method === 'wallet_showCallsStatus' ||
12 | (method === 'eth_sendTransaction' && !bypassTransactionAuth) ||
13 | (method === 'eth_sign' && !bypassSignatureAuth) ||
14 | (method === 'eth_signTypedData_v4' && !bypassSignatureAuth) ||
15 | (method === 'personal_sign' && !bypassSignatureAuth) ||
16 | (method === 'wallet_sendCalls' && !bypassTransactionAuth)
17 | ) {
18 | chrome.sidePanel.open({ tabId: sender.tab!.id! })
19 | }
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/entries/content/index.ts:
--------------------------------------------------------------------------------
1 | import { getMessenger } from '~/messengers'
2 | import { setupBridgeTransportRelay } from '~/messengers/transports/bridge'
3 |
4 | setupBridgeTransportRelay()
5 |
6 | const backgroundMessenger = getMessenger('background:contentScript')
7 | backgroundMessenger.send('ping', undefined)
8 | setInterval(() => {
9 | backgroundMessenger.send('ping', undefined)
10 | }, 5000)
11 |
12 | window.addEventListener('message', ({ data }) => {
13 | if (data.type === 'openWallet') chrome.runtime.sendMessage(data)
14 | })
15 |
--------------------------------------------------------------------------------
/src/entries/iframe/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rivet
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/entries/iframe/index.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../../app'
2 |
3 | init({ type: 'embedded' })
4 |
--------------------------------------------------------------------------------
/src/entries/inpage/index.ts:
--------------------------------------------------------------------------------
1 | import { injectProvider } from './injectProvider'
2 |
3 | injectProvider()
4 |
--------------------------------------------------------------------------------
/src/entries/inpage/injectProvider.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from '@lukeed/uuid'
2 | import { type EIP1193Provider, announceProvider } from 'mipd'
3 |
4 | import { getMessenger } from '~/messengers'
5 | import { getProvider } from '~/provider'
6 |
7 | const backgroundMessenger = getMessenger('background:inpage')
8 | const walletMessenger = getMessenger('wallet:inpage')
9 |
10 | export function injectProvider() {
11 | const provider = getProvider({
12 | host: window.location.host,
13 | eventMessenger: [walletMessenger, backgroundMessenger],
14 | requestMessenger: backgroundMessenger,
15 | })
16 |
17 | // Inject provider directly onto window
18 | window.ethereum = provider
19 | window.dispatchEvent(new Event('ethereum#initialized'))
20 |
21 | // Re-inject provider on demand
22 | walletMessenger.reply('injectProvider', async () => {
23 | window.ethereum = provider
24 | })
25 |
26 | // Announce provider
27 | announceProvider({
28 | info: {
29 | icon: 'data:image/svg+xml,',
30 | name: 'Rivet',
31 | rdns: 'et.riv',
32 | uuid: uuidv4(),
33 | },
34 | provider: provider as EIP1193Provider,
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/errors/base.ts:
--------------------------------------------------------------------------------
1 | type BaseErrorParameters = {
2 | metaMessages?: string[]
3 | } & (
4 | | {
5 | cause?: never
6 | details?: string
7 | }
8 | | {
9 | cause: BaseError | Error
10 | details?: never
11 | }
12 | )
13 |
14 | export class BaseError extends Error {
15 | details: string
16 | metaMessages?: string[]
17 | shortMessage: string
18 |
19 | override name = 'DevWalletError'
20 | version = 'dev-wallet@0.0.0'
21 |
22 | constructor(shortMessage: string, args: BaseErrorParameters = {}) {
23 | super()
24 |
25 | const details =
26 | args.cause instanceof BaseError
27 | ? args.cause.details
28 | : args.cause?.message
29 | ? args.cause.message
30 | : args.details!
31 |
32 | this.message = [
33 | shortMessage || 'An error occurred.',
34 | '',
35 | ...(args.metaMessages ? [...args.metaMessages, ''] : []),
36 | ...(details ? [`Details: ${details}`] : []),
37 | `Version: ${this.version}`,
38 | ].join('\n')
39 |
40 | if (args.cause) this.cause = args.cause
41 | this.details = details
42 | this.metaMessages = args.metaMessages
43 | this.shortMessage = shortMessage
44 | }
45 |
46 | walk(fn?: (err: unknown) => boolean) {
47 | return this.#walk(this, fn)
48 | }
49 |
50 | #walk(err: unknown, fn?: (err: unknown) => boolean): unknown {
51 | if (fn?.(err)) return err
52 | if ((err as Error).cause) return this.#walk((err as Error).cause, fn)
53 | return err
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export { BaseError } from './base'
2 |
3 | export {
4 | type ProviderRpcErrorCode,
5 | ChainDisconnectedError,
6 | ProviderDisconnectedError,
7 | ProviderRpcError,
8 | SwitchChainError,
9 | UnauthorizedProviderError,
10 | UnknownRpcError,
11 | UnsupportedProviderMethodError,
12 | UserRejectedRequestError,
13 | } from './rpc'
14 |
--------------------------------------------------------------------------------
/src/hmr.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import RefreshRuntime from '/@react-refresh'
3 |
4 | if (import.meta.hot) {
5 | RefreshRuntime.injectIntoGlobalHook(window)
6 | window.$RefreshReg$ = () => {}
7 | window.$RefreshSig$ = () => (type) => type
8 | window.__vite_plugin_react_preamble_installed__ = true
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useAccounts.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useAccountStore, useNetworkStore } from '~/zustand'
3 |
4 | export function useAccounts() {
5 | const { accounts, getAccounts } = useAccountStore()
6 | const { network } = useNetworkStore()
7 |
8 | return useMemo(
9 | () => getAccounts({ rpcUrl: network.rpcUrl }),
10 | [accounts, network.rpcUrl],
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useAutoloadAbi.ts:
--------------------------------------------------------------------------------
1 | import { loaders, whatsabi } from '@shazow/whatsabi'
2 | import { queryOptions, useQuery } from '@tanstack/react-query'
3 | import type { Address, Client } from 'viem'
4 | import { createQueryKey } from '~/react-query'
5 | import { etherscanApiUrls } from '../constants/etherscan'
6 | import { useClient } from './useClient'
7 |
8 | type AutoloadAbiParameters = {
9 | address?: Address | null
10 | enabled?: boolean
11 | }
12 |
13 | export const autoloadAbiQueryKey = createQueryKey<
14 | 'autoload-abi',
15 | [key: Client['key'], address: Address]
16 | >('autoload-abi')
17 |
18 | export function useAutoloadAbiQueryOptions({
19 | address,
20 | enabled,
21 | }: AutoloadAbiParameters) {
22 | const client = useClient()
23 | return queryOptions({
24 | enabled: enabled && Boolean(address),
25 | gcTime: Number.POSITIVE_INFINITY,
26 | staleTime: Number.POSITIVE_INFINITY,
27 | queryKey: autoloadAbiQueryKey([client.key, address!]),
28 | async queryFn() {
29 | if (!address) throw new Error('address is required')
30 | if (!client) throw new Error('client is required')
31 | const result = await whatsabi.autoload(address, {
32 | provider: client,
33 | followProxies: true,
34 | abiLoader: new loaders.MultiABILoader([
35 | new loaders.SourcifyABILoader({
36 | chainId: client.chain.id,
37 | }),
38 | new loaders.EtherscanABILoader({
39 | baseURL:
40 | (etherscanApiUrls as any)[client.chain.id] || etherscanApiUrls[1],
41 | }),
42 | ]),
43 | })
44 | if (!result.abi.some((item) => (item as { name?: string }).name))
45 | return null
46 | return result.abi.map((abiItem) => ({
47 | ...abiItem,
48 | outputs: 'outputs' in abiItem && abiItem.outputs ? abiItem.outputs : [],
49 | }))
50 | },
51 | })
52 | }
53 |
54 | export function useAutoloadAbi(args: AutoloadAbiParameters) {
55 | const queryOptions = useAutoloadAbiQueryOptions(args)
56 | return useQuery(queryOptions)
57 | }
58 |
--------------------------------------------------------------------------------
/src/hooks/useBalance.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type { Client, GetBalanceParameters } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 |
8 | type UseBalanceParameters = {
9 | address?: GetBalanceParameters['address']
10 | }
11 |
12 | export const getBalanceQueryKey = createQueryKey<
13 | 'balance',
14 | [key: Client['key'], args: UseBalanceParameters]
15 | >('balance')
16 |
17 | export function useBalanceQueryOptions({ address }: UseBalanceParameters) {
18 | const client = useClient()
19 | return queryOptions({
20 | enabled: Boolean(address),
21 | queryKey: getBalanceQueryKey([client.key, { address }]),
22 | async queryFn() {
23 | return (await client.getBalance({ address: address! })) || null
24 | },
25 | })
26 | }
27 |
28 | export function useBalance({ address }: UseBalanceParameters) {
29 | const queryOptions = useBalanceQueryOptions({ address })
30 | return useQuery(queryOptions)
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/useBlock.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import { type BlockTag, type GetBlockParameters, stringify } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 | import type { Client } from '~/viem'
6 |
7 | import { useClient } from './useClient'
8 |
9 | export type UseBlockParameters<
10 | TIncludeTransactions extends boolean = false,
11 | TBlockTag extends BlockTag = 'latest',
12 | > = GetBlockParameters & {
13 | gcTime?: number
14 | }
15 |
16 | export const getBlockQueryKey = createQueryKey<
17 | 'block',
18 | [key: Client['key'], block: BlockTag | (string & {}), deps: string]
19 | >('block')
20 |
21 | export function getBlockQueryOptions<
22 | TIncludeTransactions extends boolean = false,
23 | TBlockTag extends BlockTag = 'latest',
24 | >(
25 | client: Client,
26 | args: UseBlockParameters = {},
27 | ) {
28 | return queryOptions({
29 | gcTime: args.gcTime,
30 | queryKey: getBlockQueryKey([
31 | client.key,
32 | args.blockHash ||
33 | args.blockNumber?.toString() ||
34 | args.blockTag ||
35 | 'latest',
36 | stringify(args),
37 | ]),
38 | async queryFn() {
39 | return (await client.getBlock(args)) || null
40 | },
41 | })
42 | }
43 |
44 | export function useBlockQueryOptions<
45 | TIncludeTransactions extends boolean = false,
46 | TBlockTag extends BlockTag = 'latest',
47 | >(args: UseBlockParameters = {}) {
48 | const client = useClient()
49 | return getBlockQueryOptions(client, args)
50 | }
51 |
52 | export function useBlock<
53 | TIncludeTransactions extends boolean = false,
54 | TBlockTag extends BlockTag = 'latest',
55 | >(args: UseBlockParameters = {}) {
56 | const queryOptions = useBlockQueryOptions(args)
57 | return useQuery(queryOptions)
58 | }
59 |
--------------------------------------------------------------------------------
/src/hooks/useBytecode.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type { GetBytecodeParameters } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 | import type { Client } from '~/viem'
6 |
7 | import { useClient } from './useClient'
8 |
9 | type UseBytecodeParameters = Partial
10 |
11 | export const getBytecodeQueryKey = createQueryKey<
12 | 'bytecode',
13 | [key: Client['key'], args: UseBytecodeParameters]
14 | >('bytecode')
15 |
16 | export function getBytecodeQueryOptions(
17 | client: Client,
18 | { address }: UseBytecodeParameters,
19 | ) {
20 | return queryOptions({
21 | enabled: Boolean(address),
22 | queryKey: getBytecodeQueryKey([client.key, { address }]),
23 | async queryFn() {
24 | return (await client.getBytecode({ address: address! })) || null
25 | },
26 | })
27 | }
28 |
29 | export function useBytecodeQueryOptions(args: UseBytecodeParameters) {
30 | const client = useClient()
31 | return getBytecodeQueryOptions(client, args)
32 | }
33 |
34 | export function useBytecode(args: UseBytecodeParameters) {
35 | const queryOptions = useBytecodeQueryOptions(args)
36 | return useQuery(queryOptions)
37 | }
38 |
--------------------------------------------------------------------------------
/src/hooks/useCalldataAbi.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import type { Abi, Hex } from 'viem'
3 |
4 | import { guessAbiItem } from '~/utils'
5 |
6 | export function useCalldataAbi({ data }: { data?: Hex; enabled?: boolean }) {
7 | return useMemo(() => {
8 | if (!data) return null
9 | try {
10 | const abiItem = guessAbiItem(data)
11 | return [
12 | {
13 | ...abiItem,
14 | name:
15 | 'name' in abiItem
16 | ? `0x${abiItem.name.replace('guessed_', '')}`
17 | : 'guessed',
18 | },
19 | ] as Abi
20 | } catch {}
21 | }, [data])
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useClient.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 |
3 | import { getClient } from '~/viem'
4 | import { useNetworkStore } from '~/zustand'
5 |
6 | export function useClient({ rpcUrl }: { rpcUrl?: string } = {}) {
7 | const { network } = useNetworkStore()
8 | return useMemo(
9 | () => getClient({ rpcUrl: rpcUrl || network.rpcUrl }),
10 | [network.rpcUrl, rpcUrl],
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export function useDebounce(value: T, delay?: number): T {
4 | const [debouncedValue, setDebouncedValue] = useState(value)
5 |
6 | useEffect(() => {
7 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500)
8 | return () => clearTimeout(timer)
9 | }, [value, delay])
10 |
11 | return debouncedValue
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useErc20Balance.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import { type Address, type Client, getContract } from 'viem'
3 |
4 | import { erc20Abi } from '~/constants'
5 | import { createQueryKey } from '~/react-query'
6 |
7 | import { useClient } from './useClient'
8 |
9 | type UseErc20BalanceParameters = {
10 | address: Address
11 | tokenAddress: Address
12 | }
13 |
14 | export const getErc20BalanceQueryKey = createQueryKey<
15 | 'erc20-balance',
16 | [key: Client['key'], args: UseErc20BalanceParameters]
17 | >('erc20-balance')
18 |
19 | export function useErc20BalanceQueryOptions({
20 | address,
21 | tokenAddress,
22 | }: UseErc20BalanceParameters) {
23 | const client = useClient()
24 |
25 | return queryOptions({
26 | enabled: Boolean(address),
27 | queryKey: getErc20BalanceQueryKey([client.key, { tokenAddress, address }]),
28 | async queryFn() {
29 | const contract = getContract({
30 | address: tokenAddress,
31 | abi: erc20Abi,
32 | client,
33 | })
34 | return contract.read.balanceOf([address])
35 | },
36 | })
37 | }
38 |
39 | export function useErc20Balance({
40 | tokenAddress,
41 | address,
42 | }: UseErc20BalanceParameters) {
43 | const queryOptions = useErc20BalanceQueryOptions({ tokenAddress, address })
44 | return useQuery(queryOptions)
45 | }
46 |
--------------------------------------------------------------------------------
/src/hooks/useErc20Metadata.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import { type Address, type Client, getContract } from 'viem'
3 |
4 | import { erc20Abi } from '~/constants'
5 | import { createQueryKey } from '~/react-query'
6 |
7 | import { useClient } from './useClient'
8 |
9 | type UseErc20MetadataParameters = {
10 | tokenAddress: Address
11 | }
12 |
13 | export const getErcBalanceQueryKey = createQueryKey<
14 | 'erc20-metadata',
15 | [key: Client['key'], args: UseErc20MetadataParameters]
16 | >('erc20-metadata')
17 |
18 | export function useErc20MetadataQueryOptions({
19 | tokenAddress,
20 | }: UseErc20MetadataParameters) {
21 | const client = useClient()
22 |
23 | return queryOptions({
24 | enabled: Boolean(tokenAddress),
25 | queryKey: getErcBalanceQueryKey([client.key, { tokenAddress }]),
26 | async queryFn() {
27 | const contract = getContract({
28 | address: tokenAddress,
29 | abi: erc20Abi,
30 | client,
31 | })
32 | const [name, symbol, decimals, totalSupply] = await Promise.all([
33 | contract.read.name(),
34 | contract.read.symbol(),
35 | contract.read.decimals(),
36 | contract.read.totalSupply(),
37 | ])
38 | return {
39 | decimals,
40 | name,
41 | symbol,
42 | totalSupply,
43 | }
44 | },
45 | })
46 | }
47 |
48 | export function useErc20Metadata({ tokenAddress }: UseErc20MetadataParameters) {
49 | const queryOptions = useErc20MetadataQueryOptions({
50 | tokenAddress,
51 | })
52 | return useQuery(queryOptions)
53 | }
54 |
--------------------------------------------------------------------------------
/src/hooks/useGetAutomine.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 | import type { Client } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 |
8 | export const getAutomineQueryKey = createQueryKey<
9 | 'automining',
10 | [key: Client['key']]
11 | >('automining')
12 |
13 | export function useGetAutomineQueryOptions() {
14 | const client = useClient()
15 |
16 | return {
17 | queryKey: getAutomineQueryKey([client.key]),
18 | async queryFn() {
19 | return client.getAutomine()
20 | },
21 | }
22 | }
23 |
24 | export function useGetAutomine() {
25 | const queryOptions = useGetAutomineQueryOptions()
26 | return useQuery(queryOptions)
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks/useGetLogs.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import {
3 | type BlockNumber,
4 | type BlockTag,
5 | type GetLogsParameters,
6 | stringify,
7 | } from 'viem'
8 |
9 | import { createQueryKey } from '~/react-query'
10 |
11 | import type { AbiEvent } from 'abitype'
12 | import type { Client } from '../viem'
13 | import { useClient } from './useClient'
14 |
15 | type UseLogsParameters<
16 | TAbiEvent extends AbiEvent,
17 | TFromBlock extends BlockNumber | BlockTag,
18 | TToBlock extends BlockNumber | BlockTag,
19 | > = GetLogsParameters<
20 | TAbiEvent,
21 | TAbiEvent[],
22 | undefined,
23 | TFromBlock,
24 | TToBlock
25 | > & {
26 | enabled?: boolean
27 | }
28 |
29 | export const getLogsQueryKey = createQueryKey<
30 | 'get-logs',
31 | [key: Client['key'], args: string]
32 | >('get-logs')
33 |
34 | export function getLogsQueryOptions<
35 | TAbiEvent extends AbiEvent,
36 | TFromBlock extends BlockNumber | BlockTag,
37 | TToBlock extends BlockNumber | BlockTag,
38 | >(client: Client, args: UseLogsParameters) {
39 | const { enabled = true, fromBlock, toBlock } = args
40 |
41 | return queryOptions({
42 | enabled,
43 | gcTime:
44 | typeof fromBlock === 'bigint' && typeof toBlock === 'bigint'
45 | ? Number.POSITIVE_INFINITY
46 | : undefined,
47 | staleTime:
48 | typeof fromBlock === 'bigint' && typeof toBlock === 'bigint'
49 | ? Number.POSITIVE_INFINITY
50 | : undefined,
51 | queryKey: getLogsQueryKey([client.key, stringify(args)]),
52 | async queryFn() {
53 | return await client.getLogs(args)
54 | },
55 | })
56 | }
57 |
58 | export function useGetLogsQueryOptions<
59 | TAbiEvent extends AbiEvent,
60 | TFromBlock extends BlockNumber | BlockTag,
61 | TToBlock extends BlockNumber | BlockTag,
62 | >(args: UseLogsParameters) {
63 | const client = useClient()
64 | return getLogsQueryOptions(client, args)
65 | }
66 |
67 | export function useGetLogs<
68 | TAbiEvent extends AbiEvent,
69 | TFromBlock extends BlockNumber | BlockTag,
70 | TToBlock extends BlockNumber | BlockTag,
71 | >(args: UseLogsParameters) {
72 | const queryOptions = useGetLogsQueryOptions(args)
73 | return useQuery(queryOptions)
74 | }
75 |
--------------------------------------------------------------------------------
/src/hooks/useHost.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | export function useHost() {
4 | return useQuery({
5 | queryKey: ['host'],
6 | async queryFn() {
7 | const [tab] = await chrome.tabs.query({
8 | active: true,
9 | lastFocusedWindow: true,
10 | })
11 | if (!tab.url) return null
12 | return new URL(tab.url).host.replace('www.', '')
13 | },
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/hooks/useImpersonate.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { ImpersonateAccountParameters } from 'viem'
3 |
4 | import { useClient } from './useClient'
5 |
6 | export function useImpersonate() {
7 | const client = useClient()
8 |
9 | return useMutation({
10 | mutationFn: async ({ address }: ImpersonateAccountParameters) => {
11 | await client.impersonateAccount({ address })
12 | },
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/useInfiniteBlockTransactions.ts:
--------------------------------------------------------------------------------
1 | import { type InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
2 | import type { Client, Transaction } from 'viem'
3 |
4 | import { createQueryKey, queryClient } from '~/react-query'
5 |
6 | import { useBlock } from './useBlock'
7 | import { useClient } from './useClient'
8 |
9 | export const getInfiniteBlockTransactionsQueryKey = createQueryKey<
10 | 'block-transactions',
11 | [key: Client['key']]
12 | >('block-transactions')
13 |
14 | export function useInfiniteBlockTransactionsQueryOptions() {
15 | const { data: block } = useBlock({ gcTime: 0 })
16 | const client = useClient()
17 | const limit_ = 10
18 |
19 | return {
20 | enabled: Boolean(block?.number),
21 | initialPageParam: 0,
22 | getNextPageParam: (lastPage: unknown[], pages: unknown[]) => {
23 | if (lastPage.length < limit_) return null
24 | return pages.length
25 | },
26 | queryKey: getInfiniteBlockTransactionsQueryKey([client.key]),
27 | async queryFn({ pageParam }: { pageParam: number }) {
28 | let blockNumber = block?.number!
29 | let limit = limit_
30 |
31 | const prevInfiniteTransactions = queryClient.getQueryData([
32 | 'block-transactions',
33 | client.key,
34 | ]) as InfiniteData
35 | if (prevInfiniteTransactions) {
36 | if (pageParam > 0) {
37 | const transactions = prevInfiniteTransactions.pages[pageParam - 1]
38 | blockNumber = transactions[transactions.length - 1].blockNumber! - 1n
39 | } else {
40 | limit = prevInfiniteTransactions.pages[0].length || limit
41 | }
42 | }
43 |
44 | let count = 0
45 | let transactions: Transaction[] = []
46 | while (transactions.length < limit && count < 10 && blockNumber > 0n) {
47 | const block_ = await client.getBlock({
48 | blockNumber,
49 | includeTransactions: true,
50 | })
51 | transactions = [...transactions, ...block_.transactions]
52 | blockNumber--
53 | count++
54 | }
55 | return transactions
56 | },
57 | }
58 | }
59 |
60 | export function useInfiniteBlockTransactions() {
61 | const queryOptions = useInfiniteBlockTransactionsQueryOptions()
62 | return useInfiniteQuery(queryOptions)
63 | }
64 |
--------------------------------------------------------------------------------
/src/hooks/useInfiniteBlocks.ts:
--------------------------------------------------------------------------------
1 | import { type InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
2 | import type { Block, Client } from 'viem'
3 | import { createQueryKey, queryClient } from '~/react-query'
4 |
5 | import { useBlock } from './useBlock'
6 | import { useClient } from './useClient'
7 |
8 | export const getInfiniteBlocksQueryKey = createQueryKey<
9 | 'blocks',
10 | [key: Client['key']]
11 | >('blocks')
12 |
13 | export function useInfiniteBlocksQueryOptions() {
14 | const limit_ = 10
15 |
16 | const { data: block } = useBlock({ gcTime: 0 })
17 | const client = useClient()
18 | return {
19 | enabled: Boolean(block?.number),
20 | initialPageParam: 0,
21 | getNextPageParam: (_: unknown, pages: unknown[]) => pages.length,
22 | queryKey: getInfiniteBlocksQueryKey([client.key]),
23 | async queryFn({ pageParam }: { pageParam: number }) {
24 | let blockNumber = block?.number!
25 | let limit = limit_
26 |
27 | const prevInfiniteBlocks = queryClient.getQueryData([
28 | 'blocks',
29 | client.key,
30 | ]) as InfiniteData
31 | if (prevInfiniteBlocks) {
32 | if (pageParam > 0) {
33 | const block = prevInfiniteBlocks.pages[pageParam - 1]
34 | blockNumber = block[block.length - 1].number! - 1n
35 | } else {
36 | limit = prevInfiniteBlocks.pages[0].length || limit
37 | }
38 | }
39 |
40 | return (
41 | await Promise.all(
42 | [...Array(limit)].map(async (_, i) => {
43 | const blockNumber_ = blockNumber - BigInt(i)
44 | if (blockNumber_ < 0n) return
45 | return client.getBlock({ blockNumber: blockNumber_ })
46 | }),
47 | )
48 | ).filter(Boolean)
49 | },
50 | }
51 | }
52 |
53 | export function useInfiniteBlocks() {
54 | const queryOptions = useInfiniteBlocksQueryOptions()
55 | return useInfiniteQuery(queryOptions)
56 | }
57 |
--------------------------------------------------------------------------------
/src/hooks/useLookupSignature.ts:
--------------------------------------------------------------------------------
1 | import { loaders } from '@shazow/whatsabi'
2 | import { queryOptions, useQuery } from '@tanstack/react-query'
3 | import type { Client, Hex } from 'viem'
4 | import { createQueryKey } from '~/react-query'
5 | import { useClient } from './useClient'
6 |
7 | type LookupSignatureParameters = {
8 | enabled?: boolean
9 | selector?: Hex
10 | }
11 |
12 | export const lookupSignatureQueryKey = createQueryKey<
13 | 'lookup-signature',
14 | [key: Client['key'], selector: Hex]
15 | >('lookup-signature')
16 |
17 | export function useLookupSignatureQueryOptions({
18 | enabled,
19 | selector,
20 | }: LookupSignatureParameters) {
21 | const client = useClient()
22 | return queryOptions({
23 | enabled: enabled && Boolean(selector),
24 | gcTime: Number.POSITIVE_INFINITY,
25 | staleTime: Number.POSITIVE_INFINITY,
26 | queryKey: lookupSignatureQueryKey([client.key, selector!]),
27 | async queryFn() {
28 | if (!selector) throw new Error('selector is required')
29 | if (!client) throw new Error('client is required')
30 | const signature =
31 | selector.length === 10
32 | ? await loaders.defaultSignatureLookup.loadFunctions(selector)
33 | : await loaders.defaultSignatureLookup.loadEvents(selector)
34 | return signature[0] ?? null
35 | },
36 | })
37 | }
38 |
39 | export function useLookupSignature(args: LookupSignatureParameters) {
40 | const queryOptions = useLookupSignatureQueryOptions(args)
41 | return useQuery(queryOptions)
42 | }
43 |
--------------------------------------------------------------------------------
/src/hooks/useMine.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { MineParameters } from 'viem'
3 |
4 | import { queryClient } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 | import { usePendingBlockQueryOptions } from './usePendingBlock'
8 |
9 | export function useMine() {
10 | const client = useClient()
11 | const pendingBlockQueryOptions = usePendingBlockQueryOptions()
12 |
13 | return useMutation({
14 | mutationFn: async ({ blocks, interval = 0 }: MineParameters) => {
15 | await client.mine({
16 | blocks,
17 | interval,
18 | })
19 | if (Number(interval) === 0)
20 | queryClient.invalidateQueries(pendingBlockQueryOptions)
21 | },
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/useNetworkStatus.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 | import type { Client } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useNetworkStore } from '../zustand'
7 | import { useClient } from './useClient'
8 |
9 | type UseNetworkStatusParameters = {
10 | enabled?: boolean
11 | refetchInterval?: number
12 | retry?: number
13 | retryDelay?: number
14 | rpcUrl?: string
15 | }
16 |
17 | export const getNetworkStatusQueryKey = createQueryKey<
18 | 'listening',
19 | [key: Client['key'], string | undefined]
20 | >('listening')
21 |
22 | export function useNetworkStatus({
23 | enabled = true,
24 | refetchInterval,
25 | retry = 5,
26 | retryDelay,
27 | rpcUrl,
28 | }: UseNetworkStatusParameters = {}) {
29 | const client = useClient({ rpcUrl })
30 | const { networks, upsertNetwork } = useNetworkStore()
31 |
32 | return useQuery({
33 | enabled,
34 | queryKey: getNetworkStatusQueryKey([client.key, rpcUrl]),
35 | async queryFn() {
36 | try {
37 | const chainId = await client.getChainId()
38 | const network = networks.find((x) => x.rpcUrl === client.key)
39 |
40 | if (network) {
41 | // If chain becomes out of sync, update to the new chain.
42 | if (
43 | chainId &&
44 | network.rpcUrl === client.key &&
45 | network.chainId !== chainId
46 | ) {
47 | upsertNetwork({
48 | network: { chainId },
49 | rpcUrl: client.key,
50 | })
51 | }
52 |
53 | // If there is no fork block number, update to the current block number.
54 | if (typeof network.forkBlockNumber !== 'bigint') {
55 | ;(async () => {
56 | upsertNetwork({
57 | network: { forkBlockNumber: await client.getBlockNumber() },
58 | rpcUrl: client.key,
59 | })
60 | })()
61 | }
62 | }
63 |
64 | return chainId
65 | } catch {
66 | return false
67 | }
68 | },
69 | refetchInterval: refetchInterval ?? client.pollingInterval,
70 | retry,
71 | retryDelay,
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/src/hooks/useNonce.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 | import type { Client, GetTransactionCountParameters } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 |
8 | type UseNonceParameters = {
9 | address?: GetTransactionCountParameters['address']
10 | }
11 |
12 | export const getNonceQueryKey = createQueryKey<
13 | 'nonce',
14 | [key: Client['key'], deps: UseNonceParameters]
15 | >('nonce')
16 |
17 | export function useNonceQueryOptions({ address }: UseNonceParameters) {
18 | const client = useClient()
19 | return {
20 | enabled: Boolean(address),
21 | queryKey: getNonceQueryKey([client.key, { address }]),
22 | async queryFn() {
23 | return (await client.getTransactionCount({ address: address! })) ?? 0
24 | },
25 | }
26 | }
27 |
28 | export function useNonce({ address }: UseNonceParameters) {
29 | const queryOptions = useNonceQueryOptions({ address })
30 | return useQuery(queryOptions)
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/usePendingTransactions.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type { Client, Transaction } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 |
8 | export const getPendingTransactionsQueryKey = createQueryKey<
9 | 'pending-transactions',
10 | [key: Client['key']]
11 | >('pending-transactions')
12 |
13 | export function usePendingTransactionsQueryOptions() {
14 | const client = useClient()
15 | return queryOptions({
16 | queryKey: getPendingTransactionsQueryKey([client.key]),
17 | async queryFn() {
18 | const block = await client.getBlock({
19 | blockTag: 'pending',
20 | includeTransactions: true,
21 | })
22 | return [...(block.transactions as Transaction[])].reverse()
23 | },
24 | })
25 | }
26 |
27 | export function usePendingTransactions() {
28 | const queryOptions = usePendingTransactionsQueryOptions()
29 | return useQuery(queryOptions)
30 | }
31 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | export function usePrevious(newValue: T) {
4 | const previousRef = useRef()
5 |
6 | useEffect(() => {
7 | previousRef.current = newValue
8 | })
9 |
10 | return previousRef.current
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useRevert.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { RevertParameters } from 'viem'
3 |
4 | import { queryClient } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 | import { usePendingBlockQueryOptions } from './usePendingBlock'
8 |
9 | export function useRevert() {
10 | const client = useClient()
11 | const pendingBlockQueryOptions = usePendingBlockQueryOptions()
12 |
13 | return useMutation({
14 | mutationFn: async ({ id }: RevertParameters) => {
15 | await client.revert({
16 | id,
17 | })
18 | queryClient.invalidateQueries(pendingBlockQueryOptions)
19 | },
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useSearchParamsState.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useSearchParams } from 'react-router-dom'
3 |
4 | type UseSearchParamsStateReturnType = readonly [
5 | state: value,
6 | setState: (newState: value) => void,
7 | ]
8 |
9 | export function useSearchParamsState(
10 | name: string,
11 | defaultValue: value,
12 | ): UseSearchParamsStateReturnType {
13 | const [searchParams, setSearchParams] = useSearchParams()
14 |
15 | const state = useMemo(() => {
16 | const searchParam = searchParams.get(name)
17 | if (!searchParam) return defaultValue
18 | try {
19 | return JSON.parse(searchParam)
20 | } catch {
21 | return typeof searchParam === 'string' ? searchParam : defaultValue
22 | }
23 | }, [defaultValue, name, searchParams])
24 |
25 | const setState = (newState: string) => {
26 | setSearchParams({
27 | ...[...searchParams.entries()].reduce(
28 | (acc, [key, value]) => ({
29 | ...acc,
30 | [key]: value,
31 | }),
32 | {},
33 | ),
34 | [name]:
35 | typeof newState !== 'string' ? JSON.stringify(newState) : newState,
36 | })
37 | }
38 |
39 | return [state, setState] as UseSearchParamsStateReturnType
40 | }
41 |
--------------------------------------------------------------------------------
/src/hooks/useSetAccount.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 |
3 | import { type Account, useAccountStore } from '~/zustand/account'
4 |
5 | import { useImpersonate } from './useImpersonate'
6 | import { useStopImpersonate } from './useStopImpersonate'
7 |
8 | export type SetAccountParameters = {
9 | account: Omit
10 | key?: string
11 | setActive?: boolean
12 | }
13 |
14 | export function useSetAccount() {
15 | const {
16 | account: activeAccount,
17 | switchAccount,
18 | upsertAccount,
19 | } = useAccountStore()
20 | const { mutateAsync: stopImpersonate } = useStopImpersonate()
21 | const { mutateAsync: impersonate } = useImpersonate()
22 |
23 | return useMutation({
24 | mutationFn: async ({ account, key, setActive }: SetAccountParameters) => {
25 | if (activeAccount?.impersonate)
26 | await stopImpersonate({
27 | address: activeAccount.address,
28 | })
29 | const key_ = `${account.rpcUrl}.${account.address}`
30 | upsertAccount({
31 | key,
32 | account: {
33 | ...account,
34 | key: key_,
35 | state: 'loaded',
36 | } as Account,
37 | })
38 | if (setActive) switchAccount(key_)
39 | if (account.impersonate) await impersonate({ address: account.address })
40 | },
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/src/hooks/useSetAutomine.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 |
3 | import { queryClient } from '~/react-query'
4 | import { useNetworkStore } from '~/zustand'
5 |
6 | import { useClient } from './useClient'
7 | import { useGetAutomineQueryOptions } from './useGetAutomine'
8 |
9 | export function useSetAutomine() {
10 | const { queryKey } = useGetAutomineQueryOptions()
11 | const { network, upsertNetwork } = useNetworkStore()
12 | const client = useClient()
13 |
14 | return useMutation({
15 | mutationFn: async (nextAutomining: boolean) => {
16 | await client.setAutomine(nextAutomining)
17 | queryClient.setQueryData(queryKey, () => nextAutomining)
18 | if (nextAutomining) {
19 | await upsertNetwork({
20 | rpcUrl: network.rpcUrl,
21 | network: { blockTime: 0 },
22 | })
23 | }
24 | },
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useSetBalance.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { SetBalanceParameters } from 'viem'
3 |
4 | import { queryClient } from '~/react-query'
5 |
6 | import { getBalanceQueryKey } from './useBalance'
7 | import { useClient } from './useClient'
8 |
9 | export function useSetBalance() {
10 | const client = useClient()
11 |
12 | return useMutation({
13 | async mutationFn({ address, value }: SetBalanceParameters) {
14 | await client.setBalance({ address, value })
15 | queryClient.invalidateQueries({
16 | queryKey: getBalanceQueryKey([client.key, { address }]),
17 | })
18 | },
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/useSetIntervalMining.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { SetIntervalMiningParameters } from 'viem'
3 |
4 | import { useNetworkStore } from '~/zustand'
5 |
6 | import { useClient } from './useClient'
7 | import { useSetAutomine } from './useSetAutomine'
8 |
9 | export function useSetIntervalMining() {
10 | const { network, upsertNetwork } = useNetworkStore()
11 | const client = useClient()
12 | const { mutateAsync: setAutomine } = useSetAutomine()
13 |
14 | return useMutation({
15 | mutationFn: async ({ interval }: SetIntervalMiningParameters) => {
16 | await client.setIntervalMining({
17 | interval,
18 | })
19 | await upsertNetwork({
20 | rpcUrl: network.rpcUrl,
21 | network: { blockTime: interval },
22 | })
23 | if (interval > 0) await setAutomine(false)
24 | },
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useSetNonce.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { SetNonceParameters } from 'viem'
3 |
4 | import { queryClient } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 | import { getNonceQueryKey } from './useNonce'
8 |
9 | export function useSetNonce() {
10 | const client = useClient()
11 |
12 | return useMutation({
13 | async mutationFn({ address, nonce }: SetNonceParameters) {
14 | await client.setNonce({ address, nonce })
15 | queryClient.invalidateQueries({
16 | queryKey: getNonceQueryKey([client.key, { address }]),
17 | })
18 | },
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/useSnapshot.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type { Client } from 'viem'
3 |
4 | import { createQueryKey } from '~/react-query'
5 |
6 | import { useClient } from './useClient'
7 |
8 | type UseSnapshotParameters = {
9 | blockNumber?: bigint | null
10 | enabled?: boolean
11 | }
12 |
13 | export const getSnapshotQueryKey = createQueryKey<
14 | 'snapshot',
15 | [key: Client['key'], blockNumber: string]
16 | >('snapshot')
17 |
18 | export function useSnapshotQueryOptions({
19 | blockNumber,
20 | enabled = true,
21 | }: UseSnapshotParameters) {
22 | const client = useClient()
23 | return queryOptions({
24 | gcTime: Number.POSITIVE_INFINITY,
25 | staleTime: 0,
26 | enabled: Boolean(enabled && blockNumber),
27 | queryKey: getSnapshotQueryKey([client.key, (blockNumber || '').toString()]),
28 | async queryFn() {
29 | return (await client.snapshot()) || null
30 | },
31 | })
32 | }
33 |
34 | export function useSnapshot({ blockNumber, enabled }: UseSnapshotParameters) {
35 | const queryOptions = useSnapshotQueryOptions({ blockNumber, enabled })
36 | return useQuery(queryOptions)
37 | }
38 |
--------------------------------------------------------------------------------
/src/hooks/useStopImpersonate.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type { StopImpersonatingAccountParameters } from 'viem'
3 |
4 | import { useClient } from './useClient'
5 |
6 | export function useStopImpersonate() {
7 | const client = useClient()
8 |
9 | return useMutation({
10 | mutationFn: async ({ address }: StopImpersonatingAccountParameters) => {
11 | await client.stopImpersonatingAccount({ address })
12 | },
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/useTransaction.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type {
3 | BlockTag,
4 | Client,
5 | GetTransactionParameters as GetTransactionParameters_viem,
6 | Hash,
7 | } from 'viem'
8 | import { createQueryKey } from '~/react-query'
9 | import { useClient } from './useClient'
10 |
11 | type GetTransactionParameters =
12 | GetTransactionParameters_viem & {
13 | enabled?: boolean
14 | }
15 |
16 | export const getTransactionQueryKey = createQueryKey<
17 | 'transaction',
18 | [key: Client['key'], hash: Hash | (string & {})]
19 | >('transaction')
20 |
21 | export function useTransactionQueryOptions<
22 | TBlockTag extends BlockTag = 'latest',
23 | >(args: GetTransactionParameters) {
24 | const client = useClient()
25 | return queryOptions({
26 | enabled: args.enabled,
27 | queryKey: getTransactionQueryKey([
28 | client.key,
29 | args.blockHash ||
30 | args.blockNumber?.toString() ||
31 | args.blockTag ||
32 | args.hash ||
33 | 'latest',
34 | ]),
35 | async queryFn() {
36 | return (await client.getTransaction(args)) || null
37 | },
38 | })
39 | }
40 |
41 | export function useTransaction(
42 | args: GetTransactionParameters,
43 | ) {
44 | const queryOptions = useTransactionQueryOptions(args)
45 | return useQuery(queryOptions)
46 | }
47 |
--------------------------------------------------------------------------------
/src/hooks/useTransactionConfirmations.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import {
3 | type Client,
4 | type GetTransactionConfirmationsParameters,
5 | type Hash,
6 | stringify,
7 | } from 'viem'
8 | import { createQueryKey } from '~/react-query'
9 | import { useClient } from './useClient'
10 |
11 | export const getTransactionQueryKey = createQueryKey<
12 | 'transaction-confirmations',
13 | [key: Client['key'], hash: Hash | (string & {})]
14 | >('transaction-confirmations')
15 |
16 | export function useTransactionConfirmationsQueryOptions(
17 | args: GetTransactionConfirmationsParameters,
18 | ) {
19 | const client = useClient()
20 | return queryOptions({
21 | queryKey: getTransactionQueryKey([
22 | client.key,
23 | args.hash || stringify(args),
24 | ]),
25 | async queryFn() {
26 | return (await client.getTransactionConfirmations(args)) || null
27 | },
28 | })
29 | }
30 |
31 | export function useTransactionConfirmations(
32 | args: GetTransactionConfirmationsParameters,
33 | ) {
34 | const queryOptions = useTransactionConfirmationsQueryOptions(args)
35 | return useQuery(queryOptions)
36 | }
37 |
--------------------------------------------------------------------------------
/src/hooks/useTransactionReceipt.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions, useQuery } from '@tanstack/react-query'
2 | import type { GetTransactionReceiptParameters, Hash } from 'viem'
3 | import { createQueryKey } from '~/react-query'
4 | import type { Client } from '~/viem'
5 | import { useClient } from './useClient'
6 |
7 | export const getTransactionQueryKey = createQueryKey<
8 | 'transaction-receipt',
9 | [key: Client['key'], hash: Hash | (string & {})]
10 | >('transaction-receipt')
11 |
12 | export function getTransactionReceiptQueryOptions(
13 | client: Client,
14 | args: GetTransactionReceiptParameters,
15 | ) {
16 | return queryOptions({
17 | queryKey: getTransactionQueryKey([client.key, args.hash]),
18 | async queryFn() {
19 | return (await client.getTransactionReceipt(args)) || null
20 | },
21 | })
22 | }
23 |
24 | export function useTransactionReceiptQueryOptions(
25 | args: GetTransactionReceiptParameters,
26 | ) {
27 | const client = useClient()
28 | return getTransactionReceiptQueryOptions(client, args)
29 | }
30 |
31 | export function useTransactionReceipt(args: GetTransactionReceiptParameters) {
32 | const queryOptions = useTransactionReceiptQueryOptions(args)
33 | return useQuery(queryOptions)
34 | }
35 |
--------------------------------------------------------------------------------
/src/hooks/useTxpool.ts:
--------------------------------------------------------------------------------
1 | import { deepmerge } from '@fastify/deepmerge'
2 | import { queryOptions, useQuery } from '@tanstack/react-query'
3 | import { mapValues } from 'remeda'
4 | import type { Client } from 'viem'
5 |
6 | import { createQueryKey } from '~/react-query'
7 |
8 | import { useClient } from './useClient'
9 | import { useNetworkStatus } from './useNetworkStatus'
10 |
11 | export const getTxpoolQueryKey = createQueryKey<'txpool', [key: Client['key']]>(
12 | 'txpool',
13 | )
14 |
15 | export function useTxpoolQueryOptions() {
16 | const { data: chainId } = useNetworkStatus()
17 | const client = useClient()
18 | return queryOptions({
19 | enabled: Boolean(chainId),
20 | queryKey: getTxpoolQueryKey([client.key]),
21 | async queryFn() {
22 | return (await client.getTxpoolContent()) || null
23 | },
24 | select(data) {
25 | const pending = mapValues(data.pending, (x) =>
26 | Object.values(x)
27 | .reverse()
28 | .map(
29 | (transaction) =>
30 | ({
31 | transaction,
32 | type: 'pending',
33 | }) as const,
34 | ),
35 | )
36 | const queued = mapValues(data.queued, (x) =>
37 | Object.values(x)
38 | .reverse()
39 | .map(
40 | (transaction) =>
41 | ({
42 | transaction,
43 | type: 'queued',
44 | }) as const,
45 | ),
46 | )
47 | return Object.entries(deepmerge()(pending, queued))
48 | },
49 | })
50 | }
51 |
52 | export function useTxpool() {
53 | const queryOptions = useTxpoolQueryOptions()
54 | return useQuery(queryOptions)
55 | }
56 |
--------------------------------------------------------------------------------
/src/hooks/useWriteContract.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import type {
3 | Abi,
4 | Account,
5 | Chain,
6 | ContractFunctionArgs,
7 | ContractFunctionName,
8 | WriteContractParameters,
9 | } from 'viem'
10 |
11 | import { useClient } from './useClient'
12 |
13 | type UseWriteContractParameters<
14 | TAbi extends Abi | readonly unknown[],
15 | TFunctionName extends ContractFunctionName,
16 | TArgs extends ContractFunctionArgs<
17 | TAbi,
18 | 'nonpayable' | 'payable',
19 | TFunctionName
20 | > = ContractFunctionArgs,
21 | > = WriteContractParameters
22 |
23 | export function useWriteContract<
24 | const TAbi extends Abi | readonly unknown[],
25 | TFunctionName extends ContractFunctionName,
26 | TArgs extends ContractFunctionArgs<
27 | TAbi,
28 | 'nonpayable' | 'payable',
29 | TFunctionName
30 | >,
31 | >() {
32 | const client = useClient()
33 |
34 | return useMutation({
35 | mutationFn(args: UseWriteContractParameters) {
36 | return client.writeContract({ ...args, chain: null } as any)
37 | },
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rivet
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { init } from './app'
2 |
3 | init()
4 |
--------------------------------------------------------------------------------
/src/messengers/getMessenger.ts:
--------------------------------------------------------------------------------
1 | import type { Schema } from './schema'
2 | import { createBridgeTransport } from './transports/bridge'
3 | import { createExtensionTransport } from './transports/extension'
4 | import { createTabTransport } from './transports/tab'
5 | import type { Transport } from './transports/types'
6 | import { createWindowTransport } from './transports/window'
7 |
8 | const transportsForConnection = {
9 | 'wallet:inpage': createBridgeTransport('wallet:inpage'),
10 | 'background:inpage': createBridgeTransport('background:inpage'),
11 | 'background:wallet': createExtensionTransport('background:wallet'),
12 | 'wallet:contentScript': createTabTransport('wallet:contentScript'),
13 | 'background:contentScript': createTabTransport('background:contentScript'),
14 | 'contentScript:inpage': createWindowTransport('contentScript:inpage'),
15 | } as const
16 | type Connection = keyof typeof transportsForConnection
17 |
18 | export type Messenger = Transport
19 |
20 | export function getMessenger(connection: Connection): Messenger {
21 | return transportsForConnection[connection] as Messenger
22 | }
23 |
--------------------------------------------------------------------------------
/src/messengers/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | getMessenger,
3 | type Messenger,
4 | } from './getMessenger'
5 |
--------------------------------------------------------------------------------
/src/messengers/schema.ts:
--------------------------------------------------------------------------------
1 | import type { Address } from 'viem'
2 |
3 | import type { RpcRequest, RpcResponse } from '~/types/rpc'
4 | import type { SessionsState } from '~/zustand'
5 |
6 | export type Schema = {
7 | accountsChanged: [
8 | payload: { accounts: Address[]; sessions: SessionsState['sessions'] },
9 | response: void,
10 | ]
11 | chainChanged: [
12 | payload: { chainId: string; sessions: SessionsState['sessions'] },
13 | response: void,
14 | ]
15 | connect: [payload: { chainId: string }, response: void]
16 | disconnect: [payload: undefined, response: void]
17 | extensionId: [payload: void, response: string]
18 | injectProvider: [payload: void, response: void]
19 | pendingRequest: [
20 | payload: { request: RpcRequest; status: 'approved' | 'rejected' },
21 | response: void,
22 | ]
23 | ping: [payload: void, response: string]
24 | pushRoute: [payload: string, response: void]
25 | request: [
26 | payload: { request: RpcRequest; rpcUrl?: string },
27 | response: RpcResponse,
28 | ]
29 | toggleTheme: [payload: void, response: void]
30 | transactionExecuted: [payload: void, response: void]
31 | }
32 |
--------------------------------------------------------------------------------
/src/messengers/transports/bridge.ts:
--------------------------------------------------------------------------------
1 | import { createTabTransport } from './tab'
2 | import type { Transport } from './types'
3 | import { createWindowTransport } from './window'
4 |
5 | const windowTransport = createWindowTransport('contentScript:inpage')
6 | const tabTransport = createTabTransport('background:contentScript')
7 |
8 | const transport = tabTransport.available ? tabTransport : windowTransport
9 |
10 | /**
11 | * Creates a "bridge transport" that can be used to communicate between
12 | * scripts where there isn't a direct messaging connection (ie. inpage <-> background).
13 | */
14 | export const createBridgeTransport = (
15 | connection: TConnection,
16 | ): Transport => ({
17 | available: transport.available,
18 | connection,
19 | async send(topic, payload, { id } = {}) {
20 | return transport.send(topic, payload, { connection, id })
21 | },
22 | reply(topic, callback) {
23 | return transport.reply(topic, callback, { connection })
24 | },
25 | })
26 |
27 | export function setupBridgeTransportRelay() {
28 | // inpage -> content script -> background
29 | windowTransport.reply('*', async (payload, { connection, topic, id }) => {
30 | if (!topic) return
31 |
32 | const topic_ = topic.replace('> ', '')
33 | const response = await tabTransport.send(topic_, payload, {
34 | id,
35 | connection,
36 | })
37 | return response
38 | })
39 |
40 | // background -> content script -> inpage
41 | tabTransport.reply('*', async (payload, { connection, topic, id }) => {
42 | if (!topic) return
43 |
44 | const topic_: string = topic.replace('> ', '')
45 | const response = await windowTransport.send(topic_, payload, {
46 | id,
47 | connection,
48 | })
49 | return response
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/src/messengers/transports/types.ts:
--------------------------------------------------------------------------------
1 | export type CallbackOptions = {
2 | /** The connection of the messenger. */
3 | connection: string
4 | /** The sender of the message. */
5 | sender: chrome.runtime.MessageSender
6 | /** The topic provided. */
7 | topic: string
8 | /** An optional scoped identifier. */
9 | id?: number | string
10 | }
11 |
12 | export type CallbackFunction = (
13 | payload: TPayload,
14 | callbackOptions: CallbackOptions,
15 | ) => Promise
16 |
17 | export type Source = 'background' | 'content' | 'inpage' | 'popup'
18 |
19 | export type TransportSchema = Record
20 |
21 | export type Transport<
22 | TConnection extends string,
23 | TSchema extends TransportSchema = TransportSchema,
24 | > = {
25 | /** Whether or not the transport is available in the context. */
26 | available: boolean
27 | /** Connection type. */
28 | connection: TConnection
29 | /** Sends a message to the `reply` handler. */
30 | send: (
31 | /** A scoped topic that the `reply` will listen for. */
32 | topic: TTopic,
33 | /** The payload to send to the `reply` handler. */
34 | payload: TSchema extends TransportSchema ? TSchema[TTopic][0] : unknown,
35 | options?: {
36 | /** The connection of the messenger. */
37 | connection?: string
38 | /** Identify & scope the request via an ID. */
39 | id?: string | number
40 | },
41 | ) => Promise
42 | /** Replies to `send`. */
43 | reply: (
44 | /** A scoped topic that was sent from `send`. */
45 | topic: TTopic,
46 | callback: CallbackFunction<
47 | TSchema extends TransportSchema ? TSchema[TTopic][0] : unknown,
48 | TSchema extends TransportSchema ? TSchema[TTopic][1] : unknown
49 | >,
50 | options?: {
51 | /** The connection of the messenger. */
52 | connection?: string
53 | },
54 | ) => () => void
55 | }
56 |
57 | export type SendMessage = {
58 | connection: string
59 | topic: string
60 | payload: TPayload
61 | id?: number | string
62 | }
63 |
64 | export type ReplyMessage = {
65 | connection: string
66 | topic: string
67 | id: number | string
68 | payload: { response: TResponse; error: Error }
69 | }
70 |
--------------------------------------------------------------------------------
/src/messengers/transports/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ReplyMessage, SendMessage } from './types'
2 |
3 | export function isValidReply({
4 | id,
5 | topic,
6 | message,
7 | }: {
8 | id?: number | string
9 | topic: string
10 | message: ReplyMessage
11 | }) {
12 | if (message.topic !== `< ${topic}`) return false
13 | if (!message.payload) return false
14 | if (typeof id !== 'undefined' && message.id !== id) return false
15 | return true
16 | }
17 |
18 | export function isValidSend({
19 | topic,
20 | message,
21 | options,
22 | }: {
23 | topic: string
24 | message: SendMessage
25 | options?: { connection?: string }
26 | }) {
27 | if (!message.topic) return false
28 | if (
29 | options?.connection &&
30 | message.connection &&
31 | message.connection !== options.connection
32 | )
33 | return false
34 | if (topic !== '*' && message.topic !== `> ${topic}`) return false
35 | if (topic === '*' && message.topic.startsWith('<')) return false
36 | return true
37 | }
38 |
--------------------------------------------------------------------------------
/src/messengers/transports/window.ts:
--------------------------------------------------------------------------------
1 | import type { SendMessage, Transport } from './types'
2 | import { isValidReply, isValidSend } from './utils'
3 |
4 | /**
5 | * Creates a "window transport" that can be used to communicate between
6 | * scripts where `window` is defined.
7 | */
8 | export const createWindowTransport = (
9 | connection: TConnection,
10 | ): Transport => ({
11 | available: typeof window !== 'undefined',
12 | connection,
13 | async send(topic, payload, { connection: connection_, id } = {}) {
14 | window.postMessage(
15 | {
16 | connection: connection_ || connection,
17 | topic: `> ${topic}`,
18 | payload,
19 | id,
20 | },
21 | '*',
22 | )
23 | return new Promise((resolve, reject) => {
24 | const listener = (event: MessageEvent) => {
25 | if (!isValidReply({ id, message: event.data, topic })) return
26 | if (event.source !== window) return
27 |
28 | window.removeEventListener('message', listener)
29 |
30 | const { response, error } = event.data.payload
31 | if (error) reject(new Error(error.message))
32 | resolve(response)
33 | }
34 | window.addEventListener('message', listener)
35 | })
36 | },
37 | reply(topic, callback, options) {
38 | const listener = async (event: MessageEvent>) => {
39 | if (!isValidSend({ message: event.data, options, topic })) return
40 |
41 | const sender = event.source
42 | if (sender !== window) return
43 |
44 | let error: unknown
45 | let response: unknown
46 | try {
47 | response = await callback(event.data.payload, {
48 | connection: event.data.connection,
49 | topic: event.data.topic,
50 | sender,
51 | id: event.data.id,
52 | })
53 | } catch (error_) {
54 | error = error_
55 | }
56 |
57 | const repliedTopic = event.data.topic.replace('>', '<')
58 | window.postMessage({
59 | connection: event.data.connection,
60 | topic: repliedTopic,
61 | payload: { error, response },
62 | id: event.data.id,
63 | })
64 | }
65 | window.addEventListener('message', listener, false)
66 | return () => window.removeEventListener('message', listener)
67 | },
68 | })
69 |
--------------------------------------------------------------------------------
/src/reset.d.ts:
--------------------------------------------------------------------------------
1 | import '@total-typescript/ts-reset'
2 |
--------------------------------------------------------------------------------
/src/screens/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { Outlet, useLocation, useNavigate } from 'react-router-dom'
3 |
4 | import { Header, NetworkOfflineDialog, Toaster } from '~/components'
5 | import { Box } from '~/design-system'
6 | import { useNetworkStatus } from '~/hooks/useNetworkStatus'
7 | import { useNetworkStore, usePendingRequestsStore } from '~/zustand'
8 | import { getMessenger } from '../messengers'
9 | import PendingRequest from './pending-request'
10 |
11 | const headerHeight = '120px'
12 | const networkOfflineBypassPaths = ['networks', 'session']
13 |
14 | const contentMessenger = getMessenger('wallet:contentScript')
15 |
16 | export default function Layout() {
17 | const { network, onboarded } = useNetworkStore()
18 | const location = useLocation()
19 | const { data: online } = useNetworkStatus({ rpcUrl: network.rpcUrl })
20 | const navigate = useNavigate()
21 | const { pendingRequests } = usePendingRequestsStore()
22 | const pendingRequest = pendingRequests[pendingRequests.length - 1]
23 |
24 | const showHeader = onboarded
25 |
26 | const isNetworkOffline = Boolean(network.rpcUrl && onboarded && !online)
27 | useEffect(() => {
28 | contentMessenger.reply('pushRoute', async (route) => {
29 | navigate(route)
30 | return
31 | })
32 | }, [])
33 |
34 | const showNetworkOfflineDialog =
35 | isNetworkOffline &&
36 | !networkOfflineBypassPaths.some((path) => location.pathname.includes(path))
37 |
38 | return (
39 |
50 |
51 | {showHeader && (
52 |
53 |
54 |
55 | )}
56 |
61 | {showNetworkOfflineDialog && }
62 | {pendingRequests.length > 0 && (
63 |
64 | )}
65 |
66 |
67 |
68 |
69 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/src/screens/index.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 | import { keyframes } from '@vanilla-extract/css'
3 |
4 | import { backgroundColorVars } from '../design-system/styles/theme.css'
5 |
6 | const mineAnimation = keyframes({
7 | '0%': {
8 | backgroundColor: `rgb(${backgroundColorVars['surface/fill/secondary']})`,
9 | },
10 | '100%': {
11 | backgroundColor: 'transparent',
12 | },
13 | })
14 |
15 | export const mineBackground = style({
16 | animationName: mineAnimation,
17 | animationDuration: '0.5s',
18 | animationTimingFunction: 'linear',
19 | })
20 |
--------------------------------------------------------------------------------
/src/screens/onboarding/download.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import { OnboardingContainer } from '~/components'
3 | import { Box, Button, Stack, Text } from '~/design-system'
4 |
5 | export default function OnboardingDownload() {
6 | return (
7 |
11 |
12 |
13 | }
14 | >
15 |
16 |
17 | Rivet requires Foundry Anvil to run a local chain.
18 |
19 |
20 | Run the following command in your CLI to install Foundry:
21 |
22 |
35 |
36 | curl -L https://foundry.paradigm.xyz | bash
37 |
38 |
39 |
44 |
45 |
46 |
47 | When installed, you can continue.
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/screens/pending-request.css.ts:
--------------------------------------------------------------------------------
1 | // TODO: Dedupe common styles between all of our accordions
2 |
3 | import { globalStyle, keyframes, style } from '@vanilla-extract/css'
4 | import { backgroundColorVars } from '~/design-system/styles/theme.css'
5 |
6 | const fadeIn = keyframes({
7 | from: {
8 | opacity: 0,
9 | },
10 | to: {
11 | opacity: 1,
12 | },
13 | })
14 |
15 | const fadeOut = keyframes({
16 | from: {
17 | opacity: 1,
18 | },
19 | to: {
20 | opacity: 0,
21 | },
22 | })
23 |
24 | const rotateUp = keyframes({
25 | from: {
26 | transform: 'rotate(0deg)',
27 | },
28 | to: {
29 | transform: 'rotate(-180deg)',
30 | },
31 | })
32 |
33 | const slideDown = keyframes({
34 | from: {
35 | height: '0px',
36 | opacity: 0,
37 | },
38 | to: {
39 | height: 'var(--radix-collapsible-content-height)',
40 | opacity: 1,
41 | },
42 | })
43 |
44 | const slideUp = keyframes({
45 | from: {
46 | height: 'var(--radix-collapsible-content-height)',
47 | opacity: 1,
48 | },
49 | to: {
50 | height: '0px',
51 | opacity: 0,
52 | },
53 | })
54 |
55 | export const item = style({
56 | borderBottomWidth: 1,
57 | borderBottomColor: `rgb(${backgroundColorVars['surface/fill']})`,
58 | borderBottomStyle: 'solid',
59 | })
60 |
61 | export const root = style({
62 | selectors: {
63 | [`${item}[data-state="open"] &`]: {
64 | animationName: fadeIn,
65 | animationDuration: '100ms',
66 | animationTimingFunction: 'linear',
67 | },
68 | [`${item}[data-state="closed"] &`]: {
69 | animationName: fadeOut,
70 | animationDuration: '100ms',
71 | animationTimingFunction: 'linear',
72 | },
73 | },
74 | })
75 |
76 | export const trigger = style({})
77 |
78 | export const content = style({
79 | selectors: {
80 | '&[data-state="open"]': {
81 | animationName: slideDown,
82 | animationDuration: '100ms',
83 | animationTimingFunction: 'cubic-bezier(0.87, 0, 0.13, 1)',
84 | },
85 | '&[data-state="closed"]': {
86 | animationName: slideUp,
87 | animationDuration: '100ms',
88 | animationTimingFunction: 'cubic-bezier(0.87, 0, 0.13, 1)',
89 | },
90 | },
91 | })
92 |
93 | export const chevron = style({
94 | selectors: {
95 | [`${trigger}[data-state="open"] &`]: {
96 | animationName: rotateUp,
97 | animationDuration: '200ms',
98 | animationTimingFunction: 'ease-out',
99 | transform: 'rotate(-180deg)',
100 | },
101 | },
102 | })
103 |
104 | globalStyle(`${root} *`, {
105 | transition: 'opacity 0.1s',
106 | })
107 |
--------------------------------------------------------------------------------
/src/screens/session.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react'
2 | import { connect, disconnect } from '~/actions'
3 | import { Container, LabelledContent } from '~/components'
4 | import { Box, Button, Inline, Separator, Stack, Text } from '~/design-system'
5 | import { useHost } from '~/hooks/useHost'
6 | import { getMessenger } from '~/messengers'
7 | import { useSessionsStore } from '~/zustand'
8 |
9 | const inpageMessenger = getMessenger('wallet:inpage')
10 |
11 | export default function Session() {
12 | const { data: host } = useHost()
13 | const { getSession, sessions } = useSessionsStore()
14 | const isConnected = Boolean(host && getSession({ host }))
15 |
16 | return (
17 | <>
18 |
19 |
20 | {isConnected ? (
21 |
29 | ) : (
30 |
38 | )}
39 |
40 |
41 |
42 |
43 |
44 | {Object.values(sessions).map((session) => {
45 | return (
46 |
47 |
52 |
53 |
54 | {session.host.replace('www.', '')}
55 |
56 |
57 | {
62 | disconnect({
63 | host: session.host,
64 | messenger: inpageMessenger,
65 | })
66 | }}
67 | variant="ghost red"
68 | />
69 |
70 |
71 |
72 | )
73 | })}
74 |
75 |
76 |
77 | >
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/src/storage/index.ts:
--------------------------------------------------------------------------------
1 | export { webextStorage, type WebextStorage } from './webext'
2 | export { windowStorage, type WindowStorage } from './window'
3 |
--------------------------------------------------------------------------------
/src/storage/utils.ts:
--------------------------------------------------------------------------------
1 | export function replacer(_: string, value: unknown) {
2 | if (typeof value === 'bigint') return `#bigint.${value.toString()}`
3 | return value
4 | }
5 |
6 | export function reviver(_: string, value: unknown) {
7 | if (typeof value === 'string' && value.startsWith('#bigint.'))
8 | return BigInt(value.replace('#bigint.', ''))
9 | return value
10 | }
11 |
--------------------------------------------------------------------------------
/src/storage/window.ts:
--------------------------------------------------------------------------------
1 | import { replacer, reviver } from './utils'
2 |
3 | export type WindowStorage = Omit & {
4 | getItem(key: string, defaultValue?: T | null): T | null
5 | setItem(key: string, value: T | null): void
6 | }
7 |
8 | const noopStorage: WindowStorage = {
9 | clear: () => undefined,
10 | getItem: () => null,
11 | key: () => null,
12 | length: 0,
13 | removeItem: () => undefined,
14 | setItem: () => undefined,
15 | }
16 |
17 | /**
18 | * Sync window storage.
19 | *
20 | * Only accessible in environments where `window` is defined.
21 | */
22 | export const windowStorage = {
23 | local: createWindowStorage({ type: 'local' }),
24 | session: createWindowStorage({ type: 'session' }),
25 | } as const
26 |
27 | function createWindowStorage({
28 | key: prefixKey = 'wagmi.wallet',
29 | type,
30 | }: { key?: string; type: 'session' | 'local' }): WindowStorage {
31 | if (typeof window === 'undefined') return noopStorage
32 |
33 | const storage = type === 'local' ? window.localStorage : window.sessionStorage
34 |
35 | const getKey = (key: string) => `${prefixKey}.${key}`
36 |
37 | return {
38 | ...storage,
39 | getItem(key, defaultValue = null) {
40 | const value = storage.getItem(getKey(key))
41 | try {
42 | return value ? (JSON.parse(value, reviver) as any) : defaultValue
43 | } catch (error) {
44 | console.warn(error)
45 | return defaultValue
46 | }
47 | },
48 | setItem(key, value) {
49 | if (value === null) storage.removeItem(getKey(key))
50 |
51 | try {
52 | storage.setItem(getKey(key), JSON.stringify(value, replacer))
53 | } catch (err) {
54 | console.error(err)
55 | }
56 | },
57 | removeItem(key: string) {
58 | storage.remove(getKey(key))
59 | },
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/types/rpc.ts:
--------------------------------------------------------------------------------
1 | import type { EIP1193Parameters, EIP1474Methods } from 'viem'
2 |
3 | type SuccessResult = {
4 | method?: never | undefined
5 | result: T
6 | error?: never | undefined
7 | }
8 | type ErrorResult = {
9 | method?: never | undefined
10 | result?: never | undefined
11 | error: T
12 | }
13 | type Subscription = {
14 | method: 'eth_subscription'
15 | error?: never | undefined
16 | result?: never | undefined
17 | params: {
18 | subscription: string
19 | } & (
20 | | {
21 | result: TResult
22 | error?: never | undefined
23 | }
24 | | {
25 | result?: never | undefined
26 | error: TError
27 | }
28 | )
29 | }
30 | export type RpcResponse = {
31 | jsonrpc: `${number}`
32 | id: number
33 | } & (
34 | | SuccessResult
35 | | ErrorResult
36 | | Subscription
37 | )
38 |
39 | export type RpcRequest = EIP1193Parameters & { id: number }
40 |
--------------------------------------------------------------------------------
/src/types/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Combines members of an intersection into a readable type.
3 | *
4 | * @see {@link https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg}
5 | * @example
6 | * Prettify<{ a: string } & { b: string } & { c: number, d: bigint }>
7 | * => { a: string, b: string, c: number, d: bigint }
8 | */
9 | export type Prettify = {
10 | [K in keyof T]: T[K]
11 | } & {}
12 |
13 | /**
14 | * @description Creates a type that is T with the required keys K.
15 | *
16 | * @example
17 | * RequiredBy<{ a?: string, b: number }, 'a'>
18 | * => { a: string, b: number }
19 | */
20 | export type RequiredBy = Omit &
21 | ExactRequired>
22 |
23 | export type ExactRequired = {
24 | [P in keyof type]-?: Exclude
25 | }
26 |
27 | export type OneOf<
28 | union extends object,
29 | fallback extends object | undefined = undefined,
30 | ///
31 | keys extends KeyofUnion = KeyofUnion,
32 | > = union extends infer Item
33 | ? Prettify<
34 | Item & {
35 | [_K in Exclude]?: fallback extends object
36 | ? // @ts-ignore
37 | fallback[_K]
38 | : undefined
39 | }
40 | >
41 | : never
42 | type KeyofUnion = type extends type ? keyof type : never
43 |
44 | /**
45 | * @description Construct a type with the properties of union type T except for those in type K.
46 | * @example
47 | * type Result = UnionOmit<{ a: string, b: number } | { a: string, b: undefined, c: number }, 'a'>
48 | * => { b: number } | { b: undefined, c: number }
49 | */
50 | export type UnionOmit = type extends any
51 | ? Omit
52 | : never
53 |
--------------------------------------------------------------------------------
/src/utils/abi.ts:
--------------------------------------------------------------------------------
1 | // TODO: rewrite abi-guesser in viem
2 | import { guessFragment } from '@openchainxyz/abi-guesser'
3 | import type { AbiEvent } from 'abitype'
4 | import { FunctionFragment } from 'ethers'
5 | import {
6 | type AbiItem,
7 | type ContractEventName,
8 | type DecodeEventLogParameters,
9 | type Hex,
10 | decodeEventLog,
11 | } from 'viem'
12 |
13 | export function decodeEventLogs_guessed<
14 | const TAbiEvent extends AbiEvent,
15 | TTopics extends Hex[] = Hex[],
16 | TData extends Hex | undefined = undefined,
17 | >({
18 | abiItem,
19 | data,
20 | topics,
21 | }: { abiItem: AbiEvent } & Pick<
22 | DecodeEventLogParameters<
23 | [TAbiEvent],
24 | ContractEventName<[TAbiEvent]>,
25 | TTopics,
26 | TData,
27 | true
28 | >,
29 | 'data' | 'topics'
30 | >) {
31 | const indexedValues = topics.slice(1)
32 |
33 | for (let i = 0; i < indexedValues.length; i++) {
34 | const offset = indexedValues.length - i
35 | for (
36 | let j = 0;
37 | j < abiItem.inputs.length - indexedValues.length + 1 - i;
38 | j++
39 | ) {
40 | const inputs = abiItem.inputs.map((input, index) => ({
41 | ...input,
42 | indexed:
43 | index < offset - 1 ||
44 | index === i + j + offset - 1 ||
45 | index >= abiItem.inputs.length - (indexedValues.length - offset),
46 | }))
47 | const abi = [{ ...abiItem, inputs }]
48 | try {
49 | return decodeEventLog({
50 | abi,
51 | topics,
52 | data,
53 | })
54 | } catch {}
55 | }
56 | }
57 | }
58 |
59 | export function guessAbiItem(data: Hex) {
60 | return JSON.parse(
61 | FunctionFragment.from(guessFragment(data)).format('json'),
62 | ) as AbiItem
63 | }
64 |
--------------------------------------------------------------------------------
/src/utils/capitalize.ts:
--------------------------------------------------------------------------------
1 | export const capitalize = (str: string) =>
2 | `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`
3 |
--------------------------------------------------------------------------------
/src/utils/deepEqual.ts:
--------------------------------------------------------------------------------
1 | /** Forked from https://github.com/epoberezkin/fast-deep-equal */
2 |
3 | export function deepEqual(a: any, b: any) {
4 | if (a === b) return true
5 |
6 | if (a && b && typeof a === 'object' && typeof b === 'object') {
7 | if (a.constructor !== b.constructor) return false
8 |
9 | let length: number
10 | let i: number
11 |
12 | if (Array.isArray(a) && Array.isArray(b)) {
13 | length = a.length
14 | // biome-ignore lint/suspicious/noDoubleEquals:
15 | if (length != b.length) return false
16 | for (i = length; i-- !== 0; ) if (!deepEqual(a[i], b[i])) return false
17 | return true
18 | }
19 |
20 | if (a.valueOf !== Object.prototype.valueOf)
21 | return a.valueOf() === b.valueOf()
22 | if (a.toString !== Object.prototype.toString)
23 | return a.toString() === b.toString()
24 |
25 | const keys = Object.keys(a)
26 | length = keys.length
27 | if (length !== Object.keys(b).length) return false
28 |
29 | for (i = length; i-- !== 0; )
30 | if (!Object.prototype.hasOwnProperty.call(b, keys[i]!)) return false
31 |
32 | for (i = length; i-- !== 0; ) {
33 | const key = keys[i]
34 |
35 | if (key && !deepEqual(a[key], b[key])) return false
36 | }
37 |
38 | return true
39 | }
40 |
41 | // biome-ignore lint/suspicious/noSelfCompare:
42 | return a !== a && b !== b
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { decodeEventLogs_guessed, guessAbiItem } from './abi'
2 | export { capitalize } from './capitalize'
3 | export { deepEqual } from './deepEqual'
4 | export { isDomain } from './isDomain'
5 | export { normalizeAbiParametersValues } from './normalizeAbiParametersValues'
6 | export { truncate } from './truncate'
7 | export { uid } from './uid'
8 |
--------------------------------------------------------------------------------
/src/utils/isDomain.ts:
--------------------------------------------------------------------------------
1 | export function isDomain(value: string): boolean {
2 | return /^.*\.[a-zA-Z]{2,}$/i.test(value)
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/normalizeAbiParametersValues.ts:
--------------------------------------------------------------------------------
1 | import type { AbiParameter } from 'abitype'
2 |
3 | export function normalizeAbiParametersValues({
4 | params,
5 | values,
6 | }: { params: readonly AbiParameter[]; values: Record }) {
7 | const normalizedValues = []
8 | for (let i = 0; i < params.length; i++) {
9 | const param = params[i]
10 | const value = values[param.name || i.toString()]
11 | const normalizedValue = normalize({ param, value })
12 | normalizedValues.push(normalizedValue)
13 | }
14 | return normalizedValues
15 | }
16 |
17 | function normalize({
18 | param,
19 | value: value_,
20 | }: { param: AbiParameter; value: unknown }): unknown {
21 | const value = getInputValue(value_)
22 |
23 | if (Array.isArray(value)) {
24 | const childType = param.type.replace(/\[\d*\]$/, '')
25 | return value.map((v) =>
26 | normalize({
27 | param: {
28 | ...param,
29 | type: childType,
30 | },
31 | value: v,
32 | }),
33 | )
34 | }
35 |
36 | if (typeof value === 'object' && value !== null && 'components' in param) {
37 | const components = param.components ?? []
38 | const hasUnnamedChild =
39 | components.length === 0 || components.some(({ name }) => !name)
40 |
41 | const normalizedValue: any = hasUnnamedChild ? [] : {}
42 | for (let i = 0; i < components.length; i++) {
43 | const param = components[i]
44 | const value_ = (value as any)[param.name ?? i.toString()]
45 | normalizedValue[hasUnnamedChild ? i : param.name!] = normalize({
46 | param,
47 | value: value_,
48 | })
49 | }
50 | return normalizedValue
51 | }
52 |
53 | if (typeof value === 'string') return normalizePrimitive({ param, value })
54 |
55 | throw new Error('Unknown param + value to normalize.')
56 | }
57 |
58 | function normalizePrimitive({
59 | param,
60 | value,
61 | }: { param: AbiParameter; value: string }) {
62 | if (param.type === 'bool') return JSON.parse(value)
63 | if (param.type.startsWith('uint') || param.type.startsWith('int'))
64 | return BigInt(value)
65 | return value
66 | }
67 |
68 | function getInputValue(value: unknown) {
69 | return value &&
70 | typeof value === 'object' &&
71 | '_value' in value &&
72 | value._value !== ''
73 | ? value._value
74 | : value
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/truncate.ts:
--------------------------------------------------------------------------------
1 | export function truncate(
2 | str: string,
3 | { start = 8, end = 6 }: { start?: number; end?: number } = {},
4 | ) {
5 | if (str.length <= start + end) return str
6 | return `${str.slice(0, start)}\u2026${str.slice(-end)}`
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/uid.ts:
--------------------------------------------------------------------------------
1 | const size = 256
2 | let index = size
3 | let buffer: string
4 |
5 | export function uid(length = 11) {
6 | if (!buffer || index + length > size * 2) {
7 | buffer = ''
8 | index = 0
9 | for (let i = 0; i < size; i++) {
10 | buffer += ((256 + Math.random() * 256) | 0).toString(16).substring(1)
11 | }
12 | }
13 | return buffer.substring(index, index++ + length)
14 | }
15 |
--------------------------------------------------------------------------------
/src/viem.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Client as Client_Base,
3 | type EIP1474Methods,
4 | type PublicActions,
5 | type TestActions,
6 | type Transport,
7 | type WalletActions,
8 | createClient,
9 | custom,
10 | publicActions,
11 | testActions,
12 | walletActions,
13 | } from 'viem'
14 | import { type Chain, foundry, mainnet } from 'viem/chains'
15 |
16 | import { getMessenger } from '~/messengers'
17 | import { getProvider } from '~/provider'
18 |
19 | export const defaultChain = {
20 | ...mainnet,
21 | rpcUrls: foundry.rpcUrls,
22 | } as const satisfies Chain
23 |
24 | const messenger = getMessenger('background:wallet')
25 |
26 | export function buildChain({ rpcUrl }: { rpcUrl: string }): Chain {
27 | return {
28 | ...defaultChain,
29 | rpcUrls: {
30 | default: {
31 | http: [rpcUrl],
32 | },
33 | public: {
34 | http: [rpcUrl],
35 | },
36 | },
37 | }
38 | }
39 |
40 | export type Client = Client_Base<
41 | Transport,
42 | Chain,
43 | undefined,
44 | EIP1474Methods,
45 | WalletActions &
46 | PublicActions &
47 | TestActions & { mode: 'anvil'; rpcUrl: string }
48 | >
49 |
50 | const clientCache = new Map()
51 | export function getClient({ rpcUrl }: { rpcUrl: string }): Client {
52 | const cachedClient = clientCache.get(rpcUrl)
53 | if (cachedClient) return cachedClient
54 |
55 | const client = createClient({
56 | key: rpcUrl,
57 | chain: buildChain({ rpcUrl }),
58 | transport: custom(
59 | getProvider({
60 | eventMessenger: messenger,
61 | requestMessenger: messenger,
62 | rpcUrl,
63 | }),
64 | { retryCount: 0 },
65 | ),
66 | })
67 | .extend(() => ({ mode: 'anvil', rpcUrl }))
68 | .extend(testActions({ mode: 'anvil' }))
69 | .extend(publicActions)
70 | .extend(walletActions)
71 | clientCache.set(rpcUrl, client)
72 | return client
73 | }
74 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/src/zustand/_template.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from 'zustand'
2 |
3 | import { createStore } from './utils'
4 |
5 | export type TemplateState = {
6 | foo?: string
7 | }
8 | export type TemplateActions = {
9 | setFoo: (foo?: string) => void
10 | }
11 | export type TemplateStore = TemplateState & TemplateActions
12 |
13 | export const templateStore = createStore(
14 | (set) => ({
15 | foo: undefined,
16 | setFoo(foo) {
17 | set({ foo })
18 | },
19 | }),
20 | {
21 | persist: {
22 | name: 'foo',
23 | version: 0,
24 | },
25 | },
26 | )
27 |
28 | export const useTemplateState = () => useStore(templateStore)
29 |
--------------------------------------------------------------------------------
/src/zustand/batch-calls.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithTracked } from '~/hooks/useSyncExternalStoreWithTracked'
2 |
3 | import type { Hex, RpcTransactionRequest } from 'viem'
4 | import { createStore } from './utils'
5 |
6 | type BatchCalls = {
7 | calls: RpcTransactionRequest[]
8 | transactionHashes: Hex[]
9 | }
10 |
11 | export type BatchCallsState = {
12 | batch: Record
13 | }
14 | export type BatchCallsActions = {
15 | getBatchFromTransactionHash: (hash: Hex) => BatchCalls | undefined
16 | setBatch: (id: string, batch: BatchCalls) => void
17 | }
18 | export type BatchCallsStore = BatchCallsState & BatchCallsActions
19 |
20 | export const batchCallsStore = createStore(
21 | (set, get) => ({
22 | batch: {},
23 | getBatchFromTransactionHash(hash: Hex) {
24 | const value = Object.values(get().batch).find(({ transactionHashes }) =>
25 | transactionHashes.includes(hash),
26 | )
27 | if (!value) return undefined
28 | return value
29 | },
30 | setBatch(id, batch) {
31 | set((state) => ({
32 | ...state,
33 | batch: {
34 | ...state.batch,
35 | [id]: batch,
36 | },
37 | }))
38 | },
39 | }),
40 | {
41 | persist: {
42 | name: 'batch-calls',
43 | version: 0,
44 | },
45 | },
46 | )
47 |
48 | export const useBatchCallsStore = () =>
49 | useSyncExternalStoreWithTracked(
50 | batchCallsStore.subscribe,
51 | batchCallsStore.getState,
52 | )
53 |
--------------------------------------------------------------------------------
/src/zustand/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | type AccountActions,
3 | type AccountState,
4 | type AccountStore,
5 | accountStore,
6 | useAccountStore,
7 | } from './account'
8 |
9 | export {
10 | type BatchCallsActions,
11 | type BatchCallsState,
12 | type BatchCallsStore,
13 | batchCallsStore,
14 | useBatchCallsStore,
15 | } from './batch-calls'
16 |
17 | export {
18 | type ContractsActions,
19 | type ContractsState,
20 | type ContractsStore,
21 | contractsStore,
22 | useContractsStore,
23 | } from './contracts'
24 |
25 | export {
26 | type NetworkActions,
27 | type NetworkState,
28 | type NetworkStore,
29 | networkStore,
30 | useNetworkStore,
31 | } from './network'
32 |
33 | export {
34 | type PendingRequestsActions,
35 | type PendingRequestsState,
36 | type PendingRequestsStore,
37 | pendingRequestsStore,
38 | usePendingRequestsStore,
39 | } from './pending-requests'
40 |
41 | export {
42 | type ScrollPositionActions,
43 | type ScrollPositionState,
44 | type ScrollPositionStore,
45 | scrollPositionStore,
46 | useScrollPositionStore,
47 | } from './scroll-position'
48 |
49 | export {
50 | type SessionsActions,
51 | type SessionsState,
52 | type SessionsStore,
53 | sessionsStore,
54 | useSessionsStore,
55 | } from './sessions'
56 |
57 | export {
58 | type SettingsActions,
59 | type SettingsState,
60 | type SettingsStore,
61 | settingsStore,
62 | useSettingsStore,
63 | } from './settings'
64 |
65 | export {
66 | type TokensActions,
67 | type TokensState,
68 | type TokensStore,
69 | tokensStore,
70 | useTokensStore,
71 | } from './tokens'
72 |
73 | export { syncStores } from './utils'
74 |
--------------------------------------------------------------------------------
/src/zustand/pending-requests.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithTracked } from '~/hooks/useSyncExternalStoreWithTracked'
2 | import type { RpcRequest } from '~/types/rpc'
3 |
4 | import { createStore } from './utils'
5 |
6 | export type PendingRequest = RpcRequest & {
7 | sender?: chrome.runtime.MessageSender
8 | }
9 |
10 | export type PendingRequestsState = {
11 | pendingRequests: PendingRequest[]
12 | }
13 | export type PendingRequestsActions = {
14 | addPendingRequest: (request: PendingRequest) => void
15 | removePendingRequest: (requestId: number) => void
16 | updatePendingRequest: (updatedRequest: PendingRequest) => void
17 | }
18 | export type PendingRequestsStore = PendingRequestsState & PendingRequestsActions
19 |
20 | export const pendingRequestsStore = createStore(
21 | (set) => ({
22 | pendingRequests: [],
23 | addPendingRequest(request) {
24 | set((state) => ({
25 | ...state,
26 | pendingRequests: [...state.pendingRequests, request],
27 | }))
28 | },
29 | removePendingRequest(requestId) {
30 | set((state) => ({
31 | ...state,
32 | pendingRequests: state.pendingRequests.filter(
33 | (request) => request.id !== requestId,
34 | ),
35 | }))
36 | },
37 | updatePendingRequest(updatedRequest) {
38 | set((state) => ({
39 | ...state,
40 | pendingRequests: state.pendingRequests.map((request) =>
41 | request.id !== updatedRequest.id ? request : updatedRequest,
42 | ),
43 | }))
44 | },
45 | }),
46 | {
47 | persist: {
48 | name: 'pending-requests',
49 | version: 0,
50 | },
51 | },
52 | )
53 |
54 | export const usePendingRequestsStore = () =>
55 | useSyncExternalStoreWithTracked(
56 | pendingRequestsStore.subscribe,
57 | pendingRequestsStore.getState,
58 | )
59 |
--------------------------------------------------------------------------------
/src/zustand/scroll-position.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStore } from 'react'
2 |
3 | import { createStore } from './utils'
4 |
5 | export type ScrollPositionState = {
6 | position?: number
7 | }
8 | export type ScrollPositionActions = {
9 | setPosition: (position: number) => void
10 | }
11 | export type ScrollPositionStore = ScrollPositionState & ScrollPositionActions
12 |
13 | export const scrollPositionStore = createStore((set) => ({
14 | position: 0,
15 | setPosition(position) {
16 | set({ position })
17 | },
18 | }))
19 |
20 | export const useScrollPositionStore = () =>
21 | useSyncExternalStore(
22 | scrollPositionStore.subscribe,
23 | scrollPositionStore.getState,
24 | )
25 |
--------------------------------------------------------------------------------
/src/zustand/sessions.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithTracked } from '~/hooks/useSyncExternalStoreWithTracked'
2 |
3 | import { createStore } from './utils'
4 |
5 | type Host = string
6 | type Session = { host: Host; autoApprove?: boolean }
7 |
8 | export type SessionsState = {
9 | sessions: Session[]
10 | }
11 | export type SessionsActions = {
12 | addSession: ({ session }: { session: Session }) => void
13 | getSession: ({ host }: { host: Host }) => Session | undefined
14 | removeSession: ({ host }: { host: Host }) => void
15 | }
16 | export type SessionsStore = SessionsState & SessionsActions
17 |
18 | export const sessionsStore = createStore(
19 | (set, get) => ({
20 | sessions: [],
21 | addSession({ session }) {
22 | if (get().sessions.find((s) => s.host === session.host)) return
23 | set((state) => ({
24 | ...state,
25 | sessions: [...state.sessions, session],
26 | }))
27 | },
28 | getSession({ host }) {
29 | return get().sessions.find((session) => session.host === host)
30 | },
31 | removeSession({ host }) {
32 | set((state) => {
33 | const sessions = state.sessions.filter(
34 | (session) => session.host !== host,
35 | )
36 | return {
37 | ...state,
38 | sessions,
39 | }
40 | })
41 | },
42 | }),
43 | {
44 | persist: {
45 | name: 'sessions',
46 | version: 0,
47 | },
48 | },
49 | )
50 |
51 | export const useSessionsStore = () =>
52 | useSyncExternalStoreWithTracked(
53 | sessionsStore.subscribe,
54 | sessionsStore.getState,
55 | )
56 |
--------------------------------------------------------------------------------
/src/zustand/settings.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithTracked } from '~/hooks/useSyncExternalStoreWithTracked'
2 | import { createStore } from './utils'
3 |
4 | export type SettingsState = {
5 | bypassConnectAuth?: boolean
6 | bypassSignatureAuth?: boolean
7 | bypassTransactionAuth?: boolean
8 | }
9 | export type SettingsActions = {
10 | setBypassConnectAuth: (value?: boolean) => void
11 | setBypassSignatureAuth: (value?: boolean) => void
12 | setBypassTransactionAuth: (value?: boolean) => void
13 | }
14 | export type SettingsStore = SettingsState & SettingsActions
15 |
16 | export const settingsStore = createStore(
17 | (set) => ({
18 | bypassConnectAuth: false,
19 | bypassSignatureAuth: false,
20 | bypassTransactionAuth: false,
21 | setBypassConnectAuth(value) {
22 | set({ bypassConnectAuth: value })
23 | },
24 | setBypassSignatureAuth(value) {
25 | set({ bypassSignatureAuth: value })
26 | },
27 | setBypassTransactionAuth(value) {
28 | set({ bypassTransactionAuth: value })
29 | },
30 | }),
31 | {
32 | persist: {
33 | name: 'settings',
34 | version: 0,
35 | },
36 | },
37 | )
38 |
39 | export const useSettingsStore = () =>
40 | useSyncExternalStoreWithTracked(
41 | settingsStore.subscribe,
42 | settingsStore.getState,
43 | )
44 |
--------------------------------------------------------------------------------
/src/zustand/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type PersistOptions,
3 | type PersistStorage,
4 | persist,
5 | } from 'zustand/middleware'
6 | import {
7 | type Mutate,
8 | type StoreApi,
9 | createStore as create,
10 | } from 'zustand/vanilla'
11 |
12 | import { webextStorage } from '~/storage'
13 |
14 | import * as stores from './index'
15 |
16 | //////////////////////////////////////////////////////////////////
17 | // Zustand Storages
18 |
19 | export const persistStorage: PersistStorage = {
20 | ...webextStorage.local,
21 | getItem: async (key) => {
22 | return (await webextStorage.local.getItem(key)) || null
23 | },
24 | }
25 |
26 | export const noopStorage: PersistStorage = {
27 | getItem: async () => null,
28 | setItem: async () => undefined,
29 | removeItem: async () => undefined,
30 | }
31 |
32 | //////////////////////////////////////////////////////////////////
33 | // Stores
34 |
35 | type Initializer = Parameters>[0]
36 | export type StoreWithPersist = Mutate<
37 | StoreApi,
38 | [['zustand/persist', unknown]]
39 | > & {
40 | initializer: Initializer
41 | }
42 |
43 | export function createStore(
44 | initializer: Initializer,
45 | { persist: persistOptions }: { persist?: PersistOptions } = {},
46 | ) {
47 | const name = `zustand.${persistOptions?.name}`
48 | return Object.assign(
49 | create(
50 | persist(initializer, {
51 | ...persistOptions,
52 | name,
53 | storage: persistOptions ? persistStorage : noopStorage,
54 | }),
55 | ),
56 | { initializer },
57 | )
58 | }
59 |
60 | async function syncStore({ store }: { store: StoreWithPersist }) {
61 | if (!store.persist) return
62 |
63 | const persistOptions = store.persist.getOptions()
64 | const storageName = persistOptions.name || ''
65 |
66 | const listener = async (changedStore: StoreWithPersist) => {
67 | if (changedStore === undefined) {
68 | // Retrieve the default state from the store initializer.
69 | const state = store.initializer(
70 | (x) => x,
71 | () => null,
72 | {} as any,
73 | )
74 | const version = persistOptions.version
75 | await persistOptions.storage?.setItem(storageName, { state, version })
76 | }
77 | store.persist.rehydrate()
78 | }
79 |
80 | webextStorage.local.subscribe(storageName, listener)
81 | }
82 |
83 | export function syncStores() {
84 | Object.values(stores).forEach((store) => {
85 | if ('persist' in store && (store as StoreWithPersist).persist)
86 | syncStore({ store: store as StoreWithPersist })
87 | })
88 | }
89 |
90 | export function getKey(args: string[]): string {
91 | return args.join('-').replace(/\./g, '-')
92 | }
93 |
--------------------------------------------------------------------------------
/test/contracts/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = 'src'
3 | solc_version = "0.8.18"
4 | libs = ['../../node_modules']
5 |
6 | ignored_error_codes = ["license", "unused-param", "unused-var"]
7 |
8 |
9 | [fmt]
10 | line_length = 80
11 |
12 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config
--------------------------------------------------------------------------------
/test/contracts/src/MockERC20.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.18;
3 |
4 | import {ERC20} from "solmate/src/tokens/ERC20.sol";
5 |
6 | contract MockERC20 is ERC20 {
7 | constructor(
8 | string memory _name,
9 | string memory _symbol,
10 | uint8 _decimals
11 | ) ERC20(_name, _symbol, _decimals) {}
12 |
13 | function mint(address to, uint256 value) public virtual {
14 | _mint(to, value);
15 | }
16 |
17 | function burn(address from, uint256 value) public virtual {
18 | _burn(from, value);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/contracts/src/MockERC721.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.18;
3 |
4 | import {ERC721} from "solmate/src/tokens/ERC721.sol";
5 |
6 | contract MockERC721 is ERC721 {
7 | constructor(
8 | string memory _name,
9 | string memory _symbol
10 | ) ERC721(_name, _symbol) {}
11 |
12 | function tokenURI(
13 | uint256
14 | ) public pure virtual override returns (string memory) {}
15 |
16 | function mint(address to, uint256 tokenId) public virtual {
17 | _mint(to, tokenId);
18 | }
19 |
20 | function burn(uint256 tokenId) public virtual {
21 | _burn(tokenId);
22 | }
23 |
24 | function safeMint(address to, uint256 tokenId) public virtual {
25 | _safeMint(to, tokenId);
26 | }
27 |
28 | function safeMint(
29 | address to,
30 | uint256 tokenId,
31 | bytes memory data
32 | ) public virtual {
33 | _safeMint(to, tokenId, data);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/contracts/src/Playground.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.18;
3 |
4 | contract Playground {
5 | event Approve(address indexed spender, uint256 amount);
6 |
7 | event TestEventRivet1(
8 | uint256 indexed a,
9 | bool b,
10 | string c,
11 | uint256 indexed d
12 | );
13 |
14 | struct Foo {
15 | uint x;
16 | bool y;
17 | }
18 |
19 | function test_rivet_1(
20 | uint256 a,
21 | bool b,
22 | Foo memory c,
23 | Foo[] memory d
24 | ) public returns (bool success) {
25 | emit TestEventRivet1(a, b, "rivet", c.x);
26 | return true;
27 | }
28 |
29 | function approve(address spender, uint256 amount) public returns (bool) {
30 | emit Approve(spender, amount);
31 | return true;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/dapp/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 |
3 | import App from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6 | ,
7 | )
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "module": "ESNext",
7 | "types": ["bun-types", "@types/chrome"],
8 | "skipLibCheck": true,
9 | "paths": {
10 | "~/*": ["src/*"]
11 | },
12 |
13 | /* Bundler mode */
14 | "moduleResolution": "bundler",
15 | "allowImportingTsExtensions": true,
16 | "resolveJsonModule": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 | "verbatimModuleSyntax": true,
20 |
21 | /* Linting */
22 | "strict": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "noFallthroughCasesInSwitch": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/vite.config.inpage.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import tsconfigPaths from 'vite-tsconfig-paths'
3 |
4 | import { outDir } from './vite.config'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | build: {
9 | emptyOutDir: false,
10 | modulePreload: false,
11 | outDir,
12 | rollupOptions: {
13 | input: {
14 | inpage: 'src/entries/inpage/index.ts',
15 | },
16 | output: {
17 | entryFileNames: '[name].js',
18 | chunkFileNames: '[name].js',
19 | },
20 | plugins: [
21 | {
22 | name: 'try-catch',
23 | generateBundle(_, context) {
24 | Object.values(context).forEach((bundle: any) => {
25 | bundle.code = `(function(){try{${bundle.code}}catch{}}())`
26 | })
27 | },
28 | },
29 | ],
30 | },
31 | },
32 | plugins: [tsconfigPaths()],
33 | })
34 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import webExtension from '@samrum/vite-plugin-web-extension'
2 | import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'
3 | import react from '@vitejs/plugin-react'
4 | import { defineConfig } from 'vite'
5 | import tsconfigPaths from 'vite-tsconfig-paths'
6 |
7 | import { getManifest } from './manifest.config'
8 |
9 | const dev = process.env.NODE_ENV === 'development'
10 |
11 | export const outDir = dev ? 'dist/dev' : 'dist/build'
12 |
13 | // https://vitejs.dev/config/
14 | export default defineConfig({
15 | build: {
16 | emptyOutDir: false,
17 | outDir,
18 | },
19 | optimizeDeps: {
20 | exclude: ['mipd', 'viem', '@vanilla-extract/css'],
21 | },
22 | plugins: [
23 | tsconfigPaths(),
24 | react(),
25 | vanillaExtractPlugin(),
26 | webExtension({
27 | additionalInputs: {
28 | html: [
29 | 'src/index.html',
30 | 'src/entries/iframe/index.html',
31 | 'src/components/_playground/index.html',
32 | 'src/design-system/_playground/index.html',
33 | ],
34 | },
35 | manifest: getManifest({ dev }),
36 | }),
37 | ],
38 | })
39 |
--------------------------------------------------------------------------------