diff --git a/.changeset/smooth-drinks-walk.md b/.changeset/smooth-drinks-walk.md new file mode 100644 index 00000000..1036148d --- /dev/null +++ b/.changeset/smooth-drinks-walk.md @@ -0,0 +1,5 @@ +--- +'@totejs/walletkit': patch +--- + +Support more wallets. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3774ea3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,217 @@ +# WalletKit Contribution Guide + +Thanks for your interest in contributing to WalletKit! Please take a moment to review this document +before submitting a pull request. + +## Prerequisites + +This project relies on [nodejs](https://nodejs.org/en), and uses [`pnpm`](https://pnpm.io) as a +package manager, make sure you have them installed: + +- [node.js](https://nodejs.org/en/) v16 or higher +- [npm](https://pnpm.io) v8 or higher + +Then simply clone the repository and enter the directory: + +```sh +git clone https://github.com/node-real/walletkit.git +git cd walletkit +``` + +## Development environment + +Install the dependencies and start the local development environment: + +```sh +pnpm install +pnpm dev +``` + +In default, this will run the [test example](./examples/test/), you can use this example for +development and debugging. Any changes in `packages/walletkit` will trigger a refresh. + +## Coding standards + +We use `eslint` and our code formatting rules are defined in [.eslintrc.cjs](./.eslintrc.cjs), you +can check your code by running: + +```sh +pnpm lint +``` + +Besides, before committing, git hook will automatically run eslint to check and fix errors. + +## Tests + +Any changes need a test, please make sure all your changes are tested before committing. + +## Reporting a bug + +Just submit an issue though [github issue page](https://github.com/node-real/walletkit/issues). + +## Adding a new wallet + +Before adding a new wallet, you need to collect some information: + +| item | description | e.g. | required | +| ------------------ | --------------------------------------------------------------------------------------------------- | ----------------------------------- | -------- | +| wallet name | - | Trust Wallet | yes | +| short name | If display space is insufficient, the short name will be displayed. | Trust | optional | +| wallet logo | logo in svg format. | - | yes | +| download url | - | https://trustwallet.com/download | yes | +| deeplink | After clicking deeplink in the system browser, we can directly open dapp in the app's dapp browser. | trust://open_url?coin_id=60&url=xxx | yes | +| WalletConnect link | If your app supports WalletConnect, please provides the WalletConnect uri. | trust://wc?uri=xxx | optional | + +Then you can add it to project by following steps: + +1. Create a new folder named with wallet name to the `src/wallets` directory, e.g. trustWallet +2. Create an icon file `icon.tsx`: + +```tsx +export const TrustWalletLightIcon = (props: React.SVGProps) => { + return ( + + ... + + ) +``` + +3. Create wallet configuration file `index.tsx`: + +```tsx +import { Chain } from 'wagmi'; + +import { + TrustWalletDarkIcon, + TrustWalletLightIcon, + TrustWalletMobileDarkIcon, + TrustWalletMobileLightIcon, +} from './icon'; +import { PartialWalletProps, WalletProps } from '../types'; +import { TrustWalletConnector, TrustWalletConnectorOptions } from '../trustWallet/connector'; +import { hasInjectedProvider } from '../utils'; + +export const TRUST_WALLET_ID = 'trust'; + +export interface TrustWalletProps extends PartialWalletProps { + connectorOptions?: TrustWalletConnectorOptions; +} + +export function trustWallet(props: TrustWalletProps = {}): WalletProps { + const { connectorOptions, ...restProps } = props; + + return { + id: TRUST_WALLET_ID, + name: 'Trust Wallet', + logos: { + default: { + light: , + dark: , + }, + mobile: { + light: , + dark: , + }, + }, + downloadUrls: { + default: 'https://trustwallet.com/', + }, + spinnerColor: '#1098FC', + installed: isTrustWallet(), + createConnector: (chains: Chain[]) => { + return new TrustWalletConnector({ + chains, + options: { + shimDisconnect: true, + ...connectorOptions, + }, + }); + }, + getUri: () => { + const dappPath = `https://link.trustwallet.com/open_url?coin_id=60&url=${encodeURIComponent( + window.location.href, + )}`; + return dappPath; + }, + ...restProps, + }; +} + +export function isTrustWallet() { + if (typeof window === 'undefined') return false; + + return !!( + hasInjectedProvider('isTrust') || + window?.trustwallet?.isTrust || + window?.trustWallet?.isTrust + ); +} +``` + +and the configuration type definition as follow, have a look: + +```tsx +export interface WalletProps { + id: string; + name: string; + logos: { + default: ReactElement | { [x in ColorMode]: ReactElement }; + mobile?: ReactElement | { [x in ColorMode]: ReactElement }; + }; + downloadUrls: { + default: string | undefined; + }; + spinnerColor?: string; + installed: boolean | undefined; + createConnector: (chains: Chain[]) => Connector; + getUri: () => string | undefined; +} +``` + +4. Export the new wallet in `src/wallets/index.ts` + +```tsx +export * from './trustWallet'; +``` + +5. Open `examples/test/pages/_app.tsx` to test the new wallet + +```tsx +import { + trustWallet, // import new wallet + metaMask, + walletConnect, +} from '@totejs/walletkit/wallets'; +import { useState } from 'react'; + +const config = createConfig( + getDefaultConfig({ + autoConnect: false, + appName: 'WalletKit', + + // WalletConnect 2.0 requires a projectId which you can create quickly + // and easily for free over at WalletConnect Cloud https://cloud.walletconnect.com/sign-in + walletConnectProjectId: 'xxx', + + chains, + connectors: [ + trustWallet(), // Add to wallet list + metaMask(), + walletConnect(), + ], + }), +); +``` + +## Release notes + +A complete development workflow like following: + +1. Create a new branch out of `main` branch +2. Make some changes, fix bugs or add new features +3. Run `pnpm changeset` to create a new changeset +4. Commit the code, code review is required, after code review, we can merge the code to `main` + branch +5. Then [github action](https://github.com/node-real/walletkit/actions) will automatically execute + and create a new [release PR](https://github.com/node-real/walletkit/pulls), merge this PR, a new + version will be released diff --git a/README.md b/README.md index 5fad8a2c..824c3018 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,11 @@ export default function App() { ); } ``` + +## Contributing + +Please follow our [WalletKit Contribution Guide](./CONTRIBUTING.md). + +## License + +See [LICENSE](./LICENSE) for more information. diff --git a/examples/test/package.json b/examples/test/package.json index 0b7c593c..a5c2351a 100644 --- a/examples/test/package.json +++ b/examples/test/package.json @@ -10,17 +10,15 @@ "lint": "next lint" }, "dependencies": { - "react": "^18", - "react-dom": "^18", - "next": "^13", "@totejs/walletkit": "workspace:*", - "wagmi": "^0", - "ethers": "^5" + "next": "^13", + "react": "^18", + "react-dom": "^18" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", - "@types/react-dom": "^18" + "@types/react-dom": "^18", + "typescript": "^5" } } diff --git a/examples/test/pages/_app.tsx b/examples/test/pages/_app.tsx index 0b2e17f2..d5be91da 100644 --- a/examples/test/pages/_app.tsx +++ b/examples/test/pages/_app.tsx @@ -13,12 +13,20 @@ import { useModal, } from '@totejs/walletkit'; -import { trustWallet, metaMask, walletConnect } from '@totejs/walletkit/wallets'; +import { + trustWallet, + metaMask, + walletConnect, + okxWallet, + mathWallet, + binanceWeb3Wallet, + coinbaseWallet, +} from '@totejs/walletkit/wallets'; import { useState } from 'react'; const client = createClient( getDefaultConfig({ - autoConnect: true, + autoConnect: false, appName: 'WalletKit', // WalletConnect 2.0 requires a projectId which you can create quickly @@ -26,15 +34,20 @@ const client = createClient( walletConnectProjectId: 'e68a1816d39726c2afabf05661a32767', chains, - connectors: [trustWallet(), metaMask(), walletConnect()], + connectors: [ + trustWallet(), + metaMask(), + okxWallet(), + mathWallet(), + binanceWeb3Wallet(), + coinbaseWallet(), + walletConnect(), + ], }), ); const options: WalletKitOptions = { initialChainId: 56, - closeModalAfterConnected: false, - closeModalOnEsc: true, - closeModalOnOverlayClick: false, }; export default function App({ Component, pageProps }: AppProps) { diff --git a/examples/vite/package.json b/examples/vite/package.json index ce223f52..2d93fbc2 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -5,7 +5,7 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host 0.0.0.0", "build": "tsc && vite build", "preview": "vite preview" }, @@ -14,7 +14,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "wagmi": "^0", - "ethers": "^5" + "ethers": "^5", + "vconsole": "^3.15.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index edf6c027..8ca012ec 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -8,7 +8,11 @@ import { WalletKitOptions, SwitchNetworkModal, } from '@totejs/walletkit'; -import { metaMask, trustWallet, walletConnect } from '@totejs/walletkit/wallets'; +import { trustWallet, metaMask, walletConnect } from '@totejs/walletkit/wallets'; + +import VConsole from 'vconsole'; + +new VConsole(); const client = createClient( getDefaultConfig({ @@ -20,7 +24,22 @@ const client = createClient( walletConnectProjectId: 'e68a1816d39726c2afabf05661a32767', chains, - connectors: [trustWallet(), metaMask(), walletConnect()], + connectors: [ + trustWallet(), + metaMask(), + walletConnect({ + connectorOptions: { + showQrModal: true, + qrModalOptions: { + explorerRecommendedWalletIds: [ + '8a0ee50d1f22f6651afcae7eb4253e52a3310b90af5daef78a8c4929a9bb99d4', + 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', + '4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0', + ], + }, + }, + }), + ], }), ); @@ -31,9 +50,8 @@ const options: WalletKitOptions = { export default function App() { return ( - + - {/* 👇 Here's the SwitchNetworkModal If the user switches to a network that is not supported by our dapp, diff --git a/package.json b/package.json index d4e06517..aac48eea 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "license": "MIT", "scripts": { "prepare": "husky install", + "lint": "pnpm eslint .", "dev": "pnpm --recursive --parallel --filter example-test --filter @totejs/walletkit dev", "ci:version": "pnpm changeset version && pnpm install && cp README.md packages/walletkit/README.md", "ci:publish": "pnpm publish -r --publish-branch 0.x" diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index 0db2e20e..891f018f 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -30,7 +30,7 @@ "./wallets": "./dist/wallets/index.js" }, "scripts": { - "dev": "vite build --watch", + "dev": "vite build --watch --emptyOutDir=false", "build": "vite build" }, "peerDependencies": { diff --git a/packages/walletkit/src/defaultConfig/getDefaultConfig.ts b/packages/walletkit/src/defaultConfig/getDefaultConfig.ts index e18df9be..35647029 100644 --- a/packages/walletkit/src/defaultConfig/getDefaultConfig.ts +++ b/packages/walletkit/src/defaultConfig/getDefaultConfig.ts @@ -8,7 +8,6 @@ import { WalletProps } from '../wallets/types'; import { WALLET_CONNECT_PROJECT_ID } from '../constants/common'; import { setGlobalData } from '../globalData'; import { getDefaultWallets } from './getDefaultWallets'; -import { getWalletById, isMetaMaskConnector } from '../wallets'; export interface DefaultConfigProps { appName: string; @@ -22,7 +21,7 @@ export interface DefaultConfigProps { infuraId?: string; chains?: Chain[]; - connectors?: Array; + connectors?: WalletProps[]; autoConnect?: boolean; provider?: any; @@ -109,61 +108,11 @@ export const getDefaultConfig = (props: DefaultConfigProps) => { }; }; -// 1. if item is a connector object, add the `_wallet` field directly -// 2. if item is a wallet config object, calling `createConnector` to get a new connector -// and keeping the wallet config object to the `_wallet` field -function createConnectors(input: Array = [], chains: Chain[]) { - const connectors = input.map((w: any) => { - if (w.createConnector) { - const c = w.createConnector(chains); - c._wallet = w; - return withHackHandler(c); - } else { - w._wallet = getWalletById(w.id); - return withHackHandler(w); - } +function createConnectors(input: WalletProps[], chains: Chain[]) { + const connectors = input.map((w) => { + const c = w.createConnector(chains); + c._wallet = w; + return c; }); return connectors; } - -// !!!hack -// sometimes provider isn't ready, requests will be pending and no responses, -function withHackHandler(c: Connector) { - return c; - - const provider = c?.options?.getProvider?.(); - - if (provider && !provider.__hasWrappedRequest && isMetaMaskConnector(c)) { - provider.__hasWrappedRequest = true; - - const originalReq = provider.request; - - const run = (duration = 0, timerArr: any = [], ...params: any) => { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - originalReq - .call(provider, ...params) - .then((res: any) => { - // recovery - provider.request = originalReq; - timerArr.forEach((item: any) => { - clearTimeout(item); - }); - resolve(res); - }) - .catch(reject); - }, duration); - - timerArr.push(timer); - }); - }; - - provider.request = async function (...params: any) { - const durationArr = [0, 500, 1000, 1500, 2000, 3000]; - const timerArr: any = []; - return Promise.race(durationArr.map((t) => run(t, timerArr, ...params))); - }; - } - - return c; -} diff --git a/packages/walletkit/src/pages/Connecting/ConnectSpinner/index.tsx b/packages/walletkit/src/pages/Connecting/ConnectSpinner/index.tsx index c45b0933..04671c9b 100644 --- a/packages/walletkit/src/pages/Connecting/ConnectSpinner/index.tsx +++ b/packages/walletkit/src/pages/Connecting/ConnectSpinner/index.tsx @@ -1,5 +1,5 @@ import { cx } from '../../../base/utils/css'; -import { clsContainer, clsChildren, clsErrorCircle, clsLoading } from './styles.css'; +import { clsContainer, clsLogo, clsErrorCircle, clsLoading } from './styles.css'; import { Box, BoxProps } from '../../../base/components/Box'; import { CircleLoadingIcon } from '../../../base/icons/CircleLoadingIcon'; @@ -28,7 +28,7 @@ export function ConnectSpinner(props: ConnectSpinnerProps) { )} {isError && } - {children} + {children} ); } diff --git a/packages/walletkit/src/pages/Connecting/ConnectSpinner/styles.css.ts b/packages/walletkit/src/pages/Connecting/ConnectSpinner/styles.css.ts index 69858b16..fd595f9d 100644 --- a/packages/walletkit/src/pages/Connecting/ConnectSpinner/styles.css.ts +++ b/packages/walletkit/src/pages/Connecting/ConnectSpinner/styles.css.ts @@ -1,4 +1,4 @@ -import { keyframes, style } from '@vanilla-extract/css'; +import { globalStyle, keyframes, style } from '@vanilla-extract/css'; import { cssVar } from '../../../base/utils/css'; export const clsContainer = style({ @@ -10,9 +10,17 @@ export const clsContainer = style({ justifyContent: 'center', }); -export const clsChildren = style({ +export const clsLogo = style({ borderRadius: '50%', overflow: 'hidden', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +globalStyle(`${clsLogo} > *`, { + width: 80, + height: 80, }); export const clsErrorCircle = style({ diff --git a/packages/walletkit/src/pages/Connecting/index.tsx b/packages/walletkit/src/pages/Connecting/index.tsx index eb5503a4..ced0aa10 100644 --- a/packages/walletkit/src/pages/Connecting/index.tsx +++ b/packages/walletkit/src/pages/Connecting/index.tsx @@ -11,7 +11,7 @@ import { Description } from './Content/Description'; import { InfoTitle } from './Content/InfoTitle'; import { UnsupportedContent } from './UnsupportedContent'; import { ConnectSpinner } from './ConnectSpinner'; -import { clsContent, clsGap, clsLogoWrapper, clsButton, clsFooter } from './styles.css'; +import { clsContent, clsGap, clsButton, clsFooter } from './styles.css'; import { ModalBody } from '../../base/components/Modal/ModalBody'; import { ModalFooter } from '../../base/components/Modal/ModalFooter'; import { Button } from '../../base/components/Button'; @@ -116,7 +116,7 @@ export function ConnectingPage() { - {logos.default} + {logos.default} diff --git a/packages/walletkit/src/pages/Connecting/styles.css.ts b/packages/walletkit/src/pages/Connecting/styles.css.ts index b4fef201..c1e336ae 100644 --- a/packages/walletkit/src/pages/Connecting/styles.css.ts +++ b/packages/walletkit/src/pages/Connecting/styles.css.ts @@ -1,21 +1,10 @@ -import { style, globalStyle } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; import { cssVar } from '../..'; export const clsContent = style({ overflowY: 'auto', }); -export const clsLogoWrapper = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}); - -globalStyle(`${clsLogoWrapper} > svg`, { - width: 80, - height: 80, -}); - export const clsGap = style({ marginTop: 32, }); diff --git a/packages/walletkit/src/typings.d.ts b/packages/walletkit/src/typings.d.ts index 48b34505..79e81685 100644 --- a/packages/walletkit/src/typings.d.ts +++ b/packages/walletkit/src/typings.d.ts @@ -1,4 +1,5 @@ import { Connector as WagmiConnector } from 'wagmi'; +import type { WindowProvider } from 'wagmi/window'; import { WalletProps } from './wallets/types'; @@ -8,6 +9,7 @@ declare global { trustWallet: any; trustwallet: any; tokenpocket: any; + okexchain: any; } } diff --git a/packages/walletkit/src/wallets/binanceWeb3Wallet/icon.tsx b/packages/walletkit/src/wallets/binanceWeb3Wallet/icon.tsx new file mode 100644 index 00000000..4c9ae87b --- /dev/null +++ b/packages/walletkit/src/wallets/binanceWeb3Wallet/icon.tsx @@ -0,0 +1,13 @@ +export const BinanceWeb3WalletIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + ); +}; diff --git a/packages/walletkit/src/wallets/binanceWeb3Wallet/index.tsx b/packages/walletkit/src/wallets/binanceWeb3Wallet/index.tsx new file mode 100644 index 00000000..03b7aa15 --- /dev/null +++ b/packages/walletkit/src/wallets/binanceWeb3Wallet/index.tsx @@ -0,0 +1,53 @@ +import { Chain } from 'wagmi'; + +import { WalletProps } from '../types'; +import { BinanceWeb3WalletIcon } from './icon'; +import { CustomConnector } from '../custom/connector'; +import { getInjectedProvider } from '../utils'; +import { PartialCustomProps } from '../custom'; + +export const BINANCE_WEB3_WALLET_ID = 'binanceWeb3Wallet'; + +export function binanceWeb3Wallet(props: PartialCustomProps = {}): WalletProps { + const { connectorOptions, ...restProps } = props; + + return { + id: BINANCE_WEB3_WALLET_ID, + name: 'Binance Web3 Wallet', + logos: { + default: , + }, + downloadUrls: { + default: 'https://www.binance.com/en/web3wallet', + }, + spinnerColor: undefined, + installed: isBinanceWeb3Wallet(), + createConnector: (chains: Chain[]) => { + return new CustomConnector({ + id: BINANCE_WEB3_WALLET_ID, + chains, + options: { + name: 'Binance Web3 Wallet', + shimDisconnect: true, + getProvider() { + if (typeof window === 'undefined') return; + + const provider = getInjectedProvider('isOkxWallet') ?? window.okexchain; + return provider; + }, + ...connectorOptions, + }, + }); + }, + getUri: () => { + return undefined; + }, + ...restProps, + }; +} + +export function isBinanceWeb3Wallet() { + if (typeof window === 'undefined') return false; + + return false; +} diff --git a/packages/walletkit/src/wallets/coinbaseWallet/icon.tsx b/packages/walletkit/src/wallets/coinbaseWallet/icon.tsx new file mode 100644 index 00000000..ce7e86f7 --- /dev/null +++ b/packages/walletkit/src/wallets/coinbaseWallet/icon.tsx @@ -0,0 +1,13 @@ +export const CoinbaseWalletIcon = (props: React.SVGProps) => { + return ( + + + + + ); +}; diff --git a/packages/walletkit/src/wallets/coinbaseWallet/index.tsx b/packages/walletkit/src/wallets/coinbaseWallet/index.tsx new file mode 100644 index 00000000..77bcdc19 --- /dev/null +++ b/packages/walletkit/src/wallets/coinbaseWallet/index.tsx @@ -0,0 +1,57 @@ +import { Chain } from 'wagmi'; + +import { PartialWalletProps, WalletProps } from '../types'; +import { CoinbaseWalletIcon } from './icon'; +import { hasInjectedProvider } from '../utils'; +import { getGlobalData } from '../../globalData'; +import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'; + +export const COINBASE_WALLET_ID = 'coinbaseWallet'; + +export type CoinbaseWalletConnectorOptions = Required< + ConstructorParameters +>[0]['options']; + +export interface CoinbaseWalletProps extends PartialWalletProps { + connectorOptions?: CoinbaseWalletConnectorOptions; +} + +export function coinbaseWallet(props: CoinbaseWalletProps = {}): WalletProps { + const { connectorOptions, ...restProps } = props; + + return { + id: COINBASE_WALLET_ID, + name: 'Coinbase Wallet', + logos: { + default: , + }, + downloadUrls: { + default: 'https://coinbase.com/wallet', + }, + spinnerColor: undefined, + installed: isCoinbaseWallet(), + createConnector: (chains: Chain[]) => { + const { walletConnectDefaultOptions } = getGlobalData(); + const { appName } = walletConnectDefaultOptions; + + return new CoinbaseWalletConnector({ + chains, + options: { + appName, + headlessMode: true, + ...connectorOptions, + }, + }); + }, + getUri: () => { + return `https://go.cb-w.com/dapp?cb_url=${encodeURIComponent(window.location.href)}`; + }, + ...restProps, + }; +} + +export function isCoinbaseWallet() { + if (typeof window === 'undefined') return false; + + return hasInjectedProvider('isCoinbaseWallet'); +} diff --git a/packages/walletkit/src/wallets/custom/connector.ts b/packages/walletkit/src/wallets/custom/connector.ts new file mode 100644 index 00000000..f2eda2f7 --- /dev/null +++ b/packages/walletkit/src/wallets/custom/connector.ts @@ -0,0 +1,29 @@ +import { Chain } from 'wagmi'; +import { InjectedConnector } from 'wagmi/connectors/injected'; + +export type CustomConnectorOptions = Required< + ConstructorParameters +>[0]['options']; + +export interface CustomConstructorParams { + id: string; + chains?: Chain[]; + options?: CustomConnectorOptions; +} + +export class CustomConnector extends InjectedConnector { + public id: string; + public shimDisconnectKey; + + constructor(props: CustomConstructorParams) { + const { id, chains, options } = props ?? {}; + + super({ + chains, + options, + }); + + this.id = id; + this.shimDisconnectKey = `${this.id}.shimDisconnect`; + } +} diff --git a/packages/walletkit/src/wallets/custom/index.tsx b/packages/walletkit/src/wallets/custom/index.tsx new file mode 100644 index 00000000..d10a271f --- /dev/null +++ b/packages/walletkit/src/wallets/custom/index.tsx @@ -0,0 +1,18 @@ +import { PartialWalletProps, WalletProps } from '../types'; +import { CustomConnectorOptions } from './connector'; + +export interface PartialCustomProps extends PartialWalletProps { + connectorOptions?: CustomConnectorOptions; +} + +export interface CustomProps extends WalletProps { + connectorOptions?: CustomConnectorOptions; +} + +export function custom(props: CustomProps): WalletProps { + const { ...restProps } = props ?? {}; + + return { + ...restProps, + }; +} diff --git a/packages/walletkit/src/wallets/index.ts b/packages/walletkit/src/wallets/index.ts index 8c389049..28dd0538 100644 --- a/packages/walletkit/src/wallets/index.ts +++ b/packages/walletkit/src/wallets/index.ts @@ -1,16 +1,17 @@ +// types +export * from './types'; + // wallets +export * from './custom'; export * from './injected'; export * from './metaMask'; export * from './safe'; export * from './tokenPocket'; -export * from './tokenPocket/connector'; export * from './trustWallet'; export * from './trustWallet/connector'; export * from './walletConnect'; export * from './walletConnect/connector'; - -// utils -export * from './utils'; - -// types -export * from './types'; +// export * from './okxWallet'; +// export * from './mathWallet'; +// export * from './binanceWeb3Wallet'; +// export * from './coinbaseWallet'; diff --git a/packages/walletkit/src/wallets/injected/index.tsx b/packages/walletkit/src/wallets/injected/index.tsx index a26c23d0..c8b15545 100644 --- a/packages/walletkit/src/wallets/injected/index.tsx +++ b/packages/walletkit/src/wallets/injected/index.tsx @@ -1,5 +1,5 @@ import { InjectedConnector } from 'wagmi/connectors/injected'; -import { Chain, Connector } from 'wagmi'; +import { Chain } from 'wagmi'; import { PartialWalletProps, WalletProps } from '../types'; import { InjectedIcon } from './icon'; @@ -45,7 +45,3 @@ export function injected(props: InjectedProps = {}): WalletProps { export function isInjected() { return typeof window !== 'undefined' && Boolean(window.ethereum); } - -export function isInjectedConnector(connector?: Connector) { - return connector?.id === INJECTED_ID; -} diff --git a/packages/walletkit/src/wallets/mathWallet/icon.tsx b/packages/walletkit/src/wallets/mathWallet/icon.tsx new file mode 100644 index 00000000..50995612 --- /dev/null +++ b/packages/walletkit/src/wallets/mathWallet/icon.tsx @@ -0,0 +1,36 @@ +export const MathWalletLightIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + + ); +}; + +export const MathWalletDarkIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/packages/walletkit/src/wallets/mathWallet/index.tsx b/packages/walletkit/src/wallets/mathWallet/index.tsx new file mode 100644 index 00000000..9e87beb6 --- /dev/null +++ b/packages/walletkit/src/wallets/mathWallet/index.tsx @@ -0,0 +1,56 @@ +import { Chain } from 'wagmi'; + +import { WalletProps } from '../types'; +import { MathWalletDarkIcon, MathWalletLightIcon } from './icon'; +import { CustomConnector } from '../custom/connector'; +import { getInjectedProvider, hasInjectedProvider } from '../utils'; +import { PartialCustomProps } from '../custom'; + +export const MATH_WALLET_ID = 'mathWallet'; + +export function mathWallet(props: PartialCustomProps = {}): WalletProps { + const { connectorOptions, ...restProps } = props; + + return { + id: MATH_WALLET_ID, + name: 'Math Wallet', + logos: { + default: { + light: , + dark: , + }, + }, + downloadUrls: { + default: 'https://mathwallet.org', + }, + spinnerColor: undefined, + installed: isMathWallet(), + createConnector: (chains: Chain[]) => { + return new CustomConnector({ + id: MATH_WALLET_ID, + chains, + options: { + name: 'Math Wallet', + shimDisconnect: true, + getProvider() { + if (typeof window === 'undefined') return; + + const provider = getInjectedProvider('isMathWallet'); + return provider; + }, + ...connectorOptions, + }, + }); + }, + getUri: () => { + return `mathwallet://mathwallet.org?action=link&value=${window.location.href}`; + }, + ...restProps, + }; +} + +export function isMathWallet() { + if (typeof window === 'undefined') return false; + + return hasInjectedProvider('isMathWallet'); +} diff --git a/packages/walletkit/src/wallets/okxWallet/icon.tsx b/packages/walletkit/src/wallets/okxWallet/icon.tsx new file mode 100644 index 00000000..a6a02db2 --- /dev/null +++ b/packages/walletkit/src/wallets/okxWallet/icon.tsx @@ -0,0 +1,13 @@ +export const OkxWalletIcon = (props: React.SVGProps) => { + return ( + + + + + ); +}; diff --git a/packages/walletkit/src/wallets/okxWallet/index.tsx b/packages/walletkit/src/wallets/okxWallet/index.tsx new file mode 100644 index 00000000..0b64c8b6 --- /dev/null +++ b/packages/walletkit/src/wallets/okxWallet/index.tsx @@ -0,0 +1,53 @@ +import { Chain } from 'wagmi'; + +import { WalletProps } from '../types'; +import { OkxWalletIcon } from './icon'; +import { CustomConnector } from '../custom/connector'; +import { getInjectedProvider, hasInjectedProvider } from '../utils'; +import { PartialCustomProps } from '../custom'; + +export const OKX_WALLET_ID = 'okxWallet'; + +export function okxWallet(props: PartialCustomProps = {}): WalletProps { + const { connectorOptions, ...restProps } = props; + + return { + id: OKX_WALLET_ID, + name: 'OKX Wallet', + logos: { + default: , + }, + downloadUrls: { + default: 'https://www.okx.com/web3', + }, + spinnerColor: undefined, + installed: isOkxWallet(), + createConnector: (chains: Chain[]) => { + return new CustomConnector({ + id: OKX_WALLET_ID, + chains, + options: { + name: 'OKX Wallet', + shimDisconnect: true, + getProvider() { + if (typeof window === 'undefined') return; + + const provider = getInjectedProvider('isOkxWallet') ?? window.okexchain; + return provider; + }, + ...connectorOptions, + }, + }); + }, + getUri: () => { + return `okx://wallet/dapp/details?dappUrl=${window.location.href}`; + }, + ...restProps, + }; +} + +export function isOkxWallet() { + if (typeof window === 'undefined') return false; + + return !!(hasInjectedProvider('isOkxWallet') || window.okexchain?.isOkxWallet); +} diff --git a/packages/walletkit/src/wallets/tokenPocket/connector.tsx b/packages/walletkit/src/wallets/tokenPocket/connector.tsx deleted file mode 100644 index ebf097a6..00000000 --- a/packages/walletkit/src/wallets/tokenPocket/connector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Chain } from 'wagmi'; -import { InjectedConnector } from 'wagmi/connectors/injected'; -import { TOKEN_POCKET_ID } from '.'; - -export type TokenPocketConnectorOptions = { - shimDisconnect?: boolean; -}; - -export class TokenPocketConnector extends InjectedConnector { - readonly id: any = TOKEN_POCKET_ID; - protected shimDisconnectKey = `${this.id}.shimDisconnect`; - - constructor({ - chains, - options: _options, - }: { - chains?: Chain[]; - options?: TokenPocketConnectorOptions; - } = {}) { - const options = { - name: 'Token Pocket', - shimDisconnect: true, - getProvider: getTokenPocketProvider, - ..._options, - }; - - super({ - chains, - options, - }); - } -} - -function getTokenPocketProvider() { - if (typeof window === 'undefined') return; - - if (window?.ethereum?.isTokenPocket) { - return window.ethereum; - } - - return window?.tokenpocket; -} diff --git a/packages/walletkit/src/wallets/tokenPocket/index.tsx b/packages/walletkit/src/wallets/tokenPocket/index.tsx index 524604fd..09c962b3 100644 --- a/packages/walletkit/src/wallets/tokenPocket/index.tsx +++ b/packages/walletkit/src/wallets/tokenPocket/index.tsx @@ -1,16 +1,14 @@ -import { Chain, Connector } from 'wagmi'; +import { Chain } from 'wagmi'; -import { PartialWalletProps, WalletProps } from '../types'; +import { WalletProps } from '../types'; import { TokenPocketIcon } from './icon'; -import { TokenPocketConnector, TokenPocketConnectorOptions } from '../tokenPocket/connector'; +import { getInjectedProvider, hasInjectedProvider } from '../utils'; +import { CustomConnector } from '../custom/connector'; +import { PartialCustomProps } from '../custom'; export const TOKEN_POCKET_ID = 'tokenPocket'; -export interface TokenPocketProps extends PartialWalletProps { - connectorOptions?: TokenPocketConnectorOptions; -} - -export function tokenPocket(props: TokenPocketProps = {}): WalletProps { +export function tokenPocket(props: PartialCustomProps = {}): WalletProps { const { connectorOptions, ...restProps } = props; return { @@ -25,19 +23,27 @@ export function tokenPocket(props: TokenPocketProps = {}): WalletProps { spinnerColor: '#2980FE', installed: isTokenPocket(), createConnector: (chains: Chain[]) => { - return new TokenPocketConnector({ + return new CustomConnector({ + id: TOKEN_POCKET_ID, chains, options: { + name: 'TokenPocket', shimDisconnect: true, + getProvider() { + if (typeof window === 'undefined') return; + + const provider = getInjectedProvider('isTokenPocket') ?? window.tokenpocket; + return provider; + }, ...connectorOptions, }, }); }, getUri: () => { - const tpParams = { + const params = { url: window.location.href, }; - return `tpdapp://open?params=${encodeURIComponent(JSON.stringify(tpParams))}`; + return `tpdapp://open?params=${encodeURIComponent(JSON.stringify(params))}`; }, ...restProps, }; @@ -45,9 +51,6 @@ export function tokenPocket(props: TokenPocketProps = {}): WalletProps { export function isTokenPocket() { if (typeof window === 'undefined') return false; - return window?.ethereum?.isTokenPocket ?? !!window.tokenpocket; -} -export function isTokenPocketConnector(connector?: Connector) { - return connector?.id === TOKEN_POCKET_ID; + return !!(hasInjectedProvider('isTokenPocket') || window.tokenpocket); } diff --git a/packages/walletkit/src/wallets/trustWallet/connector.ts b/packages/walletkit/src/wallets/trustWallet/connector.ts index 937f7065..902d4599 100644 --- a/packages/walletkit/src/wallets/trustWallet/connector.ts +++ b/packages/walletkit/src/wallets/trustWallet/connector.ts @@ -1,6 +1,7 @@ import { Chain } from 'wagmi'; import { MetaMaskConnector } from 'wagmi/connectors/metaMask'; import { TRUST_WALLET_ID } from '.'; +import { getInjectedProvider } from '../utils'; export type TrustWalletConnectorOptions = { shimDisconnect?: boolean; @@ -20,7 +21,7 @@ export class TrustWalletConnector extends MetaMaskConnector { const options = { name: 'Trust Wallet', shimDisconnect: true, - getProvider: getTrustWalletProvider, + getProvider, ..._options, }; @@ -31,29 +32,14 @@ export class TrustWalletConnector extends MetaMaskConnector { } } -function getTrustWalletProvider() { - const isTrustWallet = (ethereum: any) => { - return !!ethereum.isTrust; - }; +function getProvider() { + if (typeof window === 'undefined') return; - const injectedProviderExist = - typeof window !== 'undefined' && typeof window.ethereum !== 'undefined'; + const provider = getInjectedProvider('isTrust') ?? window.trustwallet ?? window.trustWallet; - if (!injectedProviderExist) { - return; + if (provider && provider.removeListener === undefined) { + provider.removeListener = provider.off; } - if (isTrustWallet(window.ethereum)) { - return window.ethereum; - } - - if (window.ethereum?.providers) { - return window.ethereum.providers.find(isTrustWallet); - } - - if (window.trustwallet && window.trustwallet.removeListener === undefined) { - window.trustwallet.removeListener = window.trustwallet.off; - } - - return window.trustwallet; + return provider; } diff --git a/packages/walletkit/src/wallets/trustWallet/index.tsx b/packages/walletkit/src/wallets/trustWallet/index.tsx index b1d26a7a..e5a8af31 100644 --- a/packages/walletkit/src/wallets/trustWallet/index.tsx +++ b/packages/walletkit/src/wallets/trustWallet/index.tsx @@ -8,7 +8,7 @@ import { } from './icon'; import { PartialWalletProps, WalletProps } from '../types'; import { TrustWalletConnector, TrustWalletConnectorOptions } from '../trustWallet/connector'; -import { Connector } from 'wagmi/connectors'; +import { hasInjectedProvider } from '../utils'; export const TRUST_WALLET_ID = 'trust'; @@ -58,16 +58,10 @@ export function trustWallet(props: TrustWalletProps = {}): WalletProps { export function isTrustWallet() { if (typeof window === 'undefined') return false; - const { ethereum } = window; return !!( - ethereum?.isTrust || - (ethereum?.providers && ethereum?.providers.find((provider: any) => provider.isTrust)) || + hasInjectedProvider('isTrust') || window?.trustwallet?.isTrust || window?.trustWallet?.isTrust ); } - -export function isTrustWalletConnector(connector?: Connector) { - return connector?.id === TRUST_WALLET_ID; -} diff --git a/packages/walletkit/src/wallets/utils.ts b/packages/walletkit/src/wallets/utils.ts index 884e8e83..cdf87a36 100644 --- a/packages/walletkit/src/wallets/utils.ts +++ b/packages/walletkit/src/wallets/utils.ts @@ -1,26 +1,16 @@ -import { INJECTED_ID, injected } from './injected'; -import { META_MASK_ID, metaMask } from './metaMask'; -import { SAFE_ID, safe } from './safe'; -import { TOKEN_POCKET_ID, tokenPocket } from './tokenPocket'; -import { TRUST_WALLET_ID, trustWallet } from './trustWallet'; -import { WalletProps } from './types'; -import { WALLET_CONNECT_ID, walletConnect } from './walletConnect'; +import type { InjectedProviderFlags, WindowProvider } from 'wagmi/window'; -export function getWalletById(id: string, config?: WalletProps) { - switch (id) { - case INJECTED_ID: - return injected(config); - case META_MASK_ID: - return metaMask(config); - case SAFE_ID: - return safe(config); - case TOKEN_POCKET_ID: - return tokenPocket(config); - case TRUST_WALLET_ID: - return trustWallet(config); - case WALLET_CONNECT_ID: - return walletConnect(config); - default: - return injected(config); - } +export function getInjectedProvider(flag: keyof InjectedProviderFlags): WindowProvider | undefined { + if (typeof window === 'undefined' || typeof window.ethereum === 'undefined') return; + const providers = window.ethereum.providers; + + return providers + ? providers.find((provider: WindowProvider) => provider[flag]) + : window.ethereum[flag] + ? window.ethereum + : undefined; +} + +export function hasInjectedProvider(flag: keyof InjectedProviderFlags): boolean { + return Boolean(getInjectedProvider(flag)); } diff --git a/packages/walletkit/src/wallets/walletConnect/index.tsx b/packages/walletkit/src/wallets/walletConnect/index.tsx index af9ab367..d46f385e 100644 --- a/packages/walletkit/src/wallets/walletConnect/index.tsx +++ b/packages/walletkit/src/wallets/walletConnect/index.tsx @@ -9,9 +9,9 @@ import { isMobile } from '../..'; export const WALLET_CONNECT_ID = 'walletConnect'; -export type WalletConnectConnectorOptions = Required< - ConstructorParameters ->[0]['options']; +export type WalletConnectConnectorOptions = Partial< + Required>[0]['options'] +>; export interface WalletConnectProps extends PartialWalletProps { connectorOptions?: WalletConnectConnectorOptions; @@ -58,9 +58,6 @@ export function walletConnect(props: WalletConnectProps = {}): WalletProps { qrModalOptions: { explorerRecommendedWalletIds: [ '8a0ee50d1f22f6651afcae7eb4253e52a3310b90af5daef78a8c4929a9bb99d4', - '971e689d0a5be527bac79629b4ee9b925e82208e5168b733496a09c0faed0709', - 'fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa', - '7674bb4e353bf52886768a3ddc2a4562ce2f4191c80831291218ebd90f5f5e26', 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', '4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0', ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 040d2b59..993faa48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,9 +81,6 @@ importers: '@totejs/walletkit': specifier: workspace:* version: link:../../packages/walletkit - ethers: - specifier: ^5 - version: 5.7.2 next: specifier: ^13 version: 13.5.6(react-dom@18.2.0)(react@18.2.0) @@ -93,9 +90,6 @@ importers: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) - wagmi: - specifier: ^0 - version: 0.12.19(@types/react@18.2.37)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) devDependencies: '@types/node': specifier: ^20 @@ -124,6 +118,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + vconsole: + specifier: ^3.15.1 + version: 3.15.1 wagmi: specifier: ^0 version: 0.12.19(@types/react@18.2.37)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) @@ -140,9 +137,6 @@ importers: typescript: specifier: ^5.0.2 version: 5.2.2 - vconsole: - specifier: ^3.15.1 - version: 3.15.1 vite: specifier: ^4.4.5 version: 4.5.0(@types/node@18.18.9) @@ -3956,6 +3950,7 @@ packages: /copy-text-to-clipboard@3.2.0: resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} engines: {node: '>=12'} + dev: false /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -3965,6 +3960,7 @@ packages: /core-js@3.33.2: resolution: {integrity: sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==} requiresBuild: true + dev: false /cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.9)(cosmiconfig@8.3.6)(typescript@5.2.2): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} @@ -6450,6 +6446,7 @@ packages: /mutation-observer@1.0.3: resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==} + dev: false /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -8138,6 +8135,7 @@ packages: copy-text-to-clipboard: 3.2.0 core-js: 3.33.2 mutation-observer: 1.0.3 + dev: false /vfile-message@3.1.4: resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} diff --git a/website/src/pages/index.mdx b/website/src/pages/index.mdx index 3a27b0bd..a4649d56 100644 --- a/website/src/pages/index.mdx +++ b/website/src/pages/index.mdx @@ -183,6 +183,45 @@ const client = createClient( ``` +## Customizing walletConnect recommended wallets + +Sometimes, we need to customize walletConnect's wallets, which we can configure as follows. The +wallet id can be obtained from [walletConnect explorer](https://walletconnect.com/explorer). + +Other supported configurations are also available from +[walletConnect's official documentation](https://docs.walletconnect.com/advanced/walletconnectmodal/options). + +```tsx live=false +const config = createConfig( + getDefaultConfig({ + autoConnect: true, + appName: 'WalletKit', + + // WalletConnect 2.0 requires a projectId which you can create quickly + // and easily for free over at WalletConnect Cloud https://cloud.walletconnect.com/sign-in + walletConnectProjectId: 'xxx', + + chains, + connectors: [ + trustWallet(), + metaMask(), + walletConnect({ + connectorOptions: { + showQrModal: true, // Open walletConnect's modal directly rather than displaying the QR code. + qrModalOptions: { + explorerRecommendedWalletIds: [ + '8a0ee50d1f22f6651afcae7eb4253e52a3310b90af5daef78a8c4929a9bb99d4', + 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', + '4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0', + ], + }, + }, + }), + ], + }), +); +``` + `useModal` ```tsx live=false