diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96d937489f..2984fa9149 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -84,3 +84,26 @@ jobs: - name: Run Unit Tests run: yarn test + + lint-testapp: + name: Lint Testapp + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./examples/testapp + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Checkout node action + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache-dependency-path: 'yarn.lock' + + - name: Install NPM dependencies + run: yarn install --immutable + + - name: eslint + run: yarn lint diff --git a/examples/testapp/.eslintrc.js b/examples/testapp/.eslintrc.js new file mode 100644 index 0000000000..b681ca8163 --- /dev/null +++ b/examples/testapp/.eslintrc.js @@ -0,0 +1,87 @@ +module.exports = { + root: true, + extends: [ + 'preact', + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + settings: { + react: { + pragma: 'h', + }, + }, + env: { + browser: true, + es2021: true, + node: true, + commonjs: true, + }, + plugins: ['@typescript-eslint', 'simple-import-sort', 'unused-imports', 'prettier'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'simple-import-sort/imports': [ + 'error', + { + groups: [['^\\u0000'], ['^@?\\w'], ['^src(/.*|$)']], + }, + ], + 'simple-import-sort/exports': 'error', + 'no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'error', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + 'no-console': [ + 'error', + { + allow: ['warn', 'error', 'info'], + }, + ], + 'prettier/prettier': [ + 'error', + { + arrowParens: 'always', + bracketSpacing: true, + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + printWidth: 100, + quoteProps: 'as-needed', + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + }, + ], + // TODO: change this back to error + '@typescript-eslint/no-explicit-any': 'warn', + 'no-useless-constructor': 'off', + 'no-restricted-globals': [ + 'error', + { + name: 'parseInt', + message: 'Use Number.parseInt instead of parseInt.', + }, + ], + }, + overrides: [ + { + files: ['**/*.test.*'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + ], +}; diff --git a/examples/testapp/src/components/Layout.tsx b/examples/testapp/src/components/Layout.tsx index 5fe74e5e30..5ff0f98189 100644 --- a/examples/testapp/src/components/Layout.tsx +++ b/examples/testapp/src/components/Layout.tsx @@ -22,7 +22,6 @@ import { useMemo } from 'react'; import { options, scwUrls, sdkVersions, useCBWSDK } from '../context/CBWSDKReactContextProvider'; - type LayoutProps = { children: React.ReactNode; }; diff --git a/examples/testapp/src/components/MethodsSection/MethodsSection.tsx b/examples/testapp/src/components/MethodsSection/MethodsSection.tsx index fc0350dd4b..9085d00c86 100644 --- a/examples/testapp/src/components/MethodsSection/MethodsSection.tsx +++ b/examples/testapp/src/components/MethodsSection/MethodsSection.tsx @@ -1,9 +1,9 @@ -import { Box, Grid, GridItem, Heading } from "@chakra-ui/react"; -import React from "react"; +import { Box, Grid, GridItem, Heading } from '@chakra-ui/react'; +import React from 'react'; -import { RpcRequestInput } from "../RpcMethods/method/RpcRequestInput"; -import { RpcMethodCard } from "../RpcMethods/RpcMethodCard"; -import { ShortcutType } from "../RpcMethods/shortcut/ShortcutType"; +import { RpcRequestInput } from '../RpcMethods/method/RpcRequestInput'; +import { RpcMethodCard } from '../RpcMethods/RpcMethodCard'; +import { ShortcutType } from '../RpcMethods/shortcut/ShortcutType'; export function MethodsSection({ title, @@ -20,9 +20,9 @@ export function MethodsSection({ diff --git a/examples/testapp/src/components/RpcMethods/RpcMethodCard.tsx b/examples/testapp/src/components/RpcMethods/RpcMethodCard.tsx index 4eddbb50d1..2d59b154a8 100644 --- a/examples/testapp/src/components/RpcMethods/RpcMethodCard.tsx +++ b/examples/testapp/src/components/RpcMethods/RpcMethodCard.tsx @@ -62,7 +62,7 @@ export function RpcMethodCard({ format, method, params, shortcuts }) { return; } }, - [provider] + [method, provider] ); const submit = useCallback( @@ -101,7 +101,7 @@ export function RpcMethodCard({ format, method, params, shortcuts }) { setError({ code, message, data }); } }, - [provider] + [format, method, provider, verify] ); return ( @@ -116,57 +116,55 @@ export function RpcMethodCard({ format, method, params, shortcuts }) { {params?.length > 0 && ( - <> - + + + + + Params + + + + + + {params.map((param) => { + const err = errors[param.key]; + return ( + + + {param.key} + + + {err?.message as string} + + ); + })} + + + + {shortcuts?.length > 0 && ( - Params + Shortcuts - - {params.map((param) => { - const err = errors[param.key]; - return ( - - - {param.key} - - - {err?.message as string} - - ); - })} - + + {shortcuts.map((shortcut) => ( + + ))} + - {shortcuts?.length > 0 && ( - - - - Shortcuts - - - - - - {shortcuts.map((shortcut) => ( - - ))} - - - - )} - - + )} + )} {response && ( diff --git a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts index 8f8e9fd6d5..c591647f6e 100644 --- a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts +++ b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts @@ -89,9 +89,8 @@ export const verifySignMsg = async ({ }); if (valid) { return `SigUtil Successfully verified signer as ${from}`; - } else { - return `SigUtil Failed to verify signer`; } + return 'SigUtil Failed to verify signer'; } case 'eth_signTypedData_v1': case 'eth_signTypedData_v3': @@ -104,16 +103,17 @@ export const verifySignMsg = async ({ const valid = await client.verifyTypedData({ address: from as `0x${string}`, domain: message['domain'] as TypedDataDomain, + // eslint-disable-next-line @typescript-eslint/no-explicit-any types: message['types'] as any, primaryType: message['primaryType'] as string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any message: message['message'] as any, signature: sign as `0x${string}`, }); if (valid) { return `SigUtil Successfully verified signer as ${from}`; - } else { - return `SigUtil Failed to verify signer`; } + return 'SigUtil Failed to verify signer'; } default: return null; diff --git a/examples/testapp/src/components/SDKConfig/SDKConfig.tsx b/examples/testapp/src/components/SDKConfig/SDKConfig.tsx index 7d20b2610c..692e3aaf3a 100644 --- a/examples/testapp/src/components/SDKConfig/SDKConfig.tsx +++ b/examples/testapp/src/components/SDKConfig/SDKConfig.tsx @@ -7,17 +7,14 @@ import { FormControl, Heading, Input, - Text, Switch, -} from "@chakra-ui/react"; -import React, { useCallback, useMemo, useState } from "react"; -import { useCBWSDK } from "../../context/CBWSDKReactContextProvider"; -import { Preference } from "@coinbase/wallet-sdk/dist/core/provider/interface"; -import { keccak256, slice, toHex } from "viem"; + Text, +} from '@chakra-ui/react'; +import { Preference } from '@coinbase/wallet-sdk/dist/core/provider/interface'; +import React, { useCallback, useMemo, useState } from 'react'; +import { keccak256, slice, toHex } from 'viem'; -function is0xString(value: string): value is `0x${string}` { - return value.startsWith("0x"); -} +import { useCBWSDK } from '../../context/CBWSDKReactContextProvider'; function computeDataSuffix(value: string): string { return slice(keccak256(toHex(value)), 0, 16); @@ -37,7 +34,7 @@ export function SDKConfig() { }; setConfig(config_); }, - [config] + [config, setConfig] ); const handleSetDataSuffix = useCallback( @@ -51,7 +48,7 @@ export function SDKConfig() { }, })); }, - [] + [setConfig] ); const sixteenByteHex = useMemo( @@ -60,7 +57,7 @@ export function SDKConfig() { ); const attributionAuto = useMemo(() => { - return "auto" in config.attribution && config.attribution?.auto; + return 'auto' in config.attribution && config.attribution?.auto; }, [config.attribution]); return ( @@ -76,10 +73,7 @@ export function SDKConfig() { - + @@ -87,13 +81,13 @@ export function SDKConfig() { @@ -102,9 +96,8 @@ export function SDKConfig() { attribution.dataSuffix - First 16 bytes of a unique string to identify your onchain - activity. Update the text box below to have your data suffix - applied + First 16 bytes of a unique string to identify your onchain activity. Update the text + box below to have your data suffix applied { - const communicator = (window.ethereum as any).communicator; + const communicator = window.ethereum.communicator; if (communicator) { communicator.url = new URL(url); } @@ -52,12 +43,8 @@ if (typeof window !== "undefined") { } export function CBWSDKReactContextProvider({ children }: CBWSDKProviderProps) { - const [version, setVersion] = React.useState( - undefined - ); - const [option, setOption] = React.useState( - undefined - ); + const [version, setVersion] = React.useState(undefined); + const [option, setOption] = React.useState(undefined); const [config, setConfig] = React.useState({ options: option, attribution: { @@ -70,13 +57,9 @@ export function CBWSDKReactContextProvider({ children }: CBWSDKProviderProps) { useEffect(() => { if (version === undefined) { - const savedVersion = localStorage.getItem( - SELECTED_SDK_KEY - ) as SDKVersionType; + const savedVersion = localStorage.getItem(SELECTED_SDK_KEY) as SDKVersionType; setVersion( - sdkVersions.includes(savedVersion) - ? (savedVersion as SDKVersionType) - : sdkVersions[0] + sdkVersions.includes(savedVersion) ? (savedVersion as SDKVersionType) : sdkVersions[0] ); } }, [version]); @@ -84,52 +67,47 @@ export function CBWSDKReactContextProvider({ children }: CBWSDKProviderProps) { useEffect(() => { if (option === undefined) { const option = localStorage.getItem(OPTIONS_KEY) as OptionsType; - setOption(options.includes(option) ? (option as OptionsType) : "all"); + setOption(options.includes(option) ? (option as OptionsType) : 'all'); } }, [option]); useEffect(() => { if (scwUrl === undefined) { - const savedScwUrl = localStorage.getItem( - SELECTED_SCW_URL_KEY - ) as ScwUrlType; - setScwUrl( - scwUrls.includes(savedScwUrl) ? (savedScwUrl as ScwUrlType) : scwUrls[0] - ); + const savedScwUrl = localStorage.getItem(SELECTED_SCW_URL_KEY) as ScwUrlType; + setScwUrl(scwUrls.includes(savedScwUrl) ? (savedScwUrl as ScwUrlType) : scwUrls[0]); } }, [scwUrl]); useEffect(() => { + // biome-ignore lint/suspicious/noImplicitAnyLet: let cbwsdk; let preference: Preference | string; - if (version === "HEAD" || version === latestPkgJson.version) { - const SDK = - version === "HEAD" ? CoinbaseWalletSDKHEAD : CoinbaseWalletSDKLatest; + if (version === 'HEAD' || version === latestPkgJson.version) { + const SDK = version === 'HEAD' ? CoinbaseWalletSDKHEAD : CoinbaseWalletSDKLatest; cbwsdk = new SDK({ - appName: "SDK Playground", + appName: 'SDK Playground', appChainIds: [84532, 8452], }); - if (version === "HEAD") { + if (version === 'HEAD') { preference = { options: option, attribution: config.attribution }; } else { preference = { options: option }; } setSdk(cbwsdk); - } else if (version === "3.9.3" || version === "3.7.2") { - const SDK = - version === "3.9.3" ? CoinbaseWalletSDK393 : CoinbaseWalletSDK372; + } else if (version === '3.9.3' || version === '3.7.2') { + const SDK = version === '3.9.3' ? CoinbaseWalletSDK393 : CoinbaseWalletSDK372; cbwsdk = new SDK({ - appName: "Test App", + appName: 'Test App', enableMobileWalletLink: true, }); - preference = "jsonRpcUrlMock"; + preference = 'jsonRpcUrlMock'; setSdk(cbwsdk); } if (!cbwsdk) { return; } const cbwprovider = cbwsdk.makeWeb3Provider(preference); - cbwprovider.on("disconnect", () => { + cbwprovider.on('disconnect', () => { location.reload(); }); window.ethereum = cbwprovider; @@ -137,25 +115,25 @@ export function CBWSDKReactContextProvider({ children }: CBWSDKProviderProps) { }, [version, option, config]); useEffect(() => { - if (version === "HEAD" || version === latestPkgJson.version) { + if (version === 'HEAD' || version === latestPkgJson.version) { if (scwUrl) window.setPopupUrl?.(scwUrl); } - }, [version, scwUrl, sdk]); + }, [version, scwUrl]); - const setPreference = (option: OptionsType) => { + const setPreference = useCallback((option: OptionsType) => { localStorage.setItem(OPTIONS_KEY, option); setOption(option); - }; + }, []); - const setSDKVersion = (version: SDKVersionType) => { + const setSDKVersion = useCallback((version: SDKVersionType) => { localStorage.setItem(SELECTED_SDK_KEY, version); setVersion(version); - }; + }, []); - const setScwUrlAndSave = (url: ScwUrlType) => { + const setScwUrlAndSave = useCallback((url: ScwUrlType) => { localStorage.setItem(SELECTED_SCW_URL_KEY, url); setScwUrl(url); - }; + }, []); const ctx = useMemo( () => ({ @@ -184,17 +162,13 @@ export function CBWSDKReactContextProvider({ children }: CBWSDKProviderProps) { ] ); - return ( - - {children} - - ); + return {children}; } export function useCBWSDK() { const context = React.useContext(CBWSDKReactContext); if (context === undefined) { - throw new Error("useCBWSDK must be used within a CBWSDKProvider"); + throw new Error('useCBWSDK must be used within a CBWSDKProvider'); } return context; } diff --git a/examples/testapp/src/pages/index.tsx b/examples/testapp/src/pages/index.tsx index 1d7aef09bf..bdba68ef31 100644 --- a/examples/testapp/src/pages/index.tsx +++ b/examples/testapp/src/pages/index.tsx @@ -1,28 +1,26 @@ -import { Box, Container, Grid, Heading } from "@chakra-ui/react"; -import React, { useEffect } from "react"; +import { Box, Container, Grid, Heading } from '@chakra-ui/react'; +import React, { useEffect } from 'react'; -import { EventListenersCard } from "../components/EventListeners/EventListenersCard"; -import { WIDTH_2XL } from "../components/Layout"; -import { connectionMethods } from "../components/RpcMethods/method/connectionMethods"; -import { multiChainMethods } from "../components/RpcMethods/method/multiChainMethods"; -import { readonlyJsonRpcMethods } from "../components/RpcMethods/method/readonlyJsonRpcMethods"; -import { sendMethods } from "../components/RpcMethods/method/sendMethods"; -import { signMessageMethods } from "../components/RpcMethods/method/signMessageMethods"; -import { walletTxMethods } from "../components/RpcMethods/method/walletTxMethods"; -import { multiChainShortcutsMap } from "../components/RpcMethods/shortcut/multipleChainShortcuts"; -import { readonlyJsonRpcShortcutsMap } from "../components/RpcMethods/shortcut/readonlyJsonRpcShortcuts"; -import { sendShortcutsMap } from "../components/RpcMethods/shortcut/sendShortcuts"; -import { signMessageShortcutsMap } from "../components/RpcMethods/shortcut/signMessageShortcuts"; -import { walletTxShortcutsMap } from "../components/RpcMethods/shortcut/walletTxShortcuts"; -import { useCBWSDK } from "../context/CBWSDKReactContextProvider"; -import { MethodsSection } from "../components/MethodsSection/MethodsSection"; -import { SDKConfig } from "../components/SDKConfig/SDKConfig"; +import { EventListenersCard } from '../components/EventListeners/EventListenersCard'; +import { WIDTH_2XL } from '../components/Layout'; +import { MethodsSection } from '../components/MethodsSection/MethodsSection'; +import { connectionMethods } from '../components/RpcMethods/method/connectionMethods'; +import { multiChainMethods } from '../components/RpcMethods/method/multiChainMethods'; +import { readonlyJsonRpcMethods } from '../components/RpcMethods/method/readonlyJsonRpcMethods'; +import { sendMethods } from '../components/RpcMethods/method/sendMethods'; +import { signMessageMethods } from '../components/RpcMethods/method/signMessageMethods'; +import { walletTxMethods } from '../components/RpcMethods/method/walletTxMethods'; +import { multiChainShortcutsMap } from '../components/RpcMethods/shortcut/multipleChainShortcuts'; +import { readonlyJsonRpcShortcutsMap } from '../components/RpcMethods/shortcut/readonlyJsonRpcShortcuts'; +import { sendShortcutsMap } from '../components/RpcMethods/shortcut/sendShortcuts'; +import { signMessageShortcutsMap } from '../components/RpcMethods/shortcut/signMessageShortcuts'; +import { walletTxShortcutsMap } from '../components/RpcMethods/shortcut/walletTxShortcuts'; +import { SDKConfig } from '../components/SDKConfig/SDKConfig'; +import { useCBWSDK } from '../context/CBWSDKReactContextProvider'; export default function Home() { const { provider, sdkVersion } = useCBWSDK(); - const [connected, setConnected] = React.useState( - Boolean(provider?.connected) - ); + const [connected, setConnected] = React.useState(Boolean(provider?.connected)); const [chainId, setChainId] = React.useState(undefined); // This is for Extension compatibility, Extension with SDK3.9 does not emit connect event // correctly, so we manually check if the extension is connected, and set the connected state @@ -33,36 +31,35 @@ export default function Home() { }, []); useEffect(() => { - provider?.on("connect", () => { + provider?.on('connect', () => { setConnected(true); }); - provider?.on("chainChanged", (newChainId) => { + provider?.on('chainChanged', (newChainId) => { setChainId(newChainId); }); }, [provider]); useEffect(() => { if (connected) { - provider?.request({ method: "eth_chainId" }).then((chainId) => { + provider?.request({ method: 'eth_chainId' }).then((chainId) => { setChainId(Number.parseInt(chainId, 16)); }); } }, [connected, provider]); // There's a bug in 3.9.3 where it does not emit a 'connect' event for walletlink connections - const shouldShowMethodsRequiringConnection = - connected || sdkVersion === "3.9.3"; + const shouldShowMethodsRequiringConnection = connected || sdkVersion === '3.9.3'; return ( Event Listeners - + {/* TODO: once published have this include latest */} - {sdkVersion === "HEAD" && ( + {sdkVersion === 'HEAD' && ( <> SDK Configuration (Optional) @@ -85,11 +82,7 @@ export default function Home() { methods={signMessageMethods} shortcutsMap={signMessageShortcutsMap(chainId)} /> - +