├── .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 |
43 |
{children}
44 |
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 | --------------------------------------------------------------------------------