diff --git a/example/components/Card.tsx b/example/components/Card.tsx index 64582b5c6..d81954a03 100644 --- a/example/components/Card.tsx +++ b/example/components/Card.tsx @@ -5,6 +5,7 @@ import type { MetaMask } from '@web3-react/metamask' import type { Network } from '@web3-react/network' import type { WalletConnect } from '@web3-react/walletconnect' import type { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' +import type { TrustWallet } from '../../packages/trust/dist/index' import { getName } from '../utils' import { Accounts } from './Accounts' @@ -13,7 +14,7 @@ import { ConnectWithSelect } from './ConnectWithSelect' import { Status } from './Status' interface Props { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe | TrustWallet activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ConnectWithSelect.tsx b/example/components/ConnectWithSelect.tsx index 3568c6574..6502335b7 100644 --- a/example/components/ConnectWithSelect.tsx +++ b/example/components/ConnectWithSelect.tsx @@ -6,6 +6,7 @@ import { Network } from '@web3-react/network' import { WalletConnect } from '@web3-react/walletconnect' import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' import { useCallback, useEffect, useState } from 'react' +import { TrustWallet } from '../../packages/trust/dist/index' import { CHAINS, getAddChainParameters } from '../chains' @@ -48,7 +49,7 @@ export function ConnectWithSelect({ error, setError, }: { - connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe + connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe | TrustWallet activeChainId: ReturnType chainIds?: ReturnType[] isActivating: ReturnType diff --git a/example/components/ProviderExample.tsx b/example/components/ProviderExample.tsx index 8721d3318..42f74a3ef 100644 --- a/example/components/ProviderExample.tsx +++ b/example/components/ProviderExample.tsx @@ -4,20 +4,23 @@ import type { MetaMask } from '@web3-react/metamask' import type { Network } from '@web3-react/network' import type { WalletConnect } from '@web3-react/walletconnect' import type { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' +import type { TrustWallet } from '../../packages/trust/dist/index' import { coinbaseWallet, hooks as coinbaseWalletHooks } from '../connectors/coinbaseWallet' import { hooks as metaMaskHooks, metaMask } from '../connectors/metaMask' import { hooks as networkHooks, network } from '../connectors/network' import { hooks as walletConnectHooks, walletConnect } from '../connectors/walletConnect' import { hooks as walletConnectV2Hooks, walletConnectV2 } from '../connectors/walletConnectV2' +import { hooks as trustHooks, trustWallet } from '../connectors/trustWallet' import { getName } from '../utils' -const connectors: [MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [ +const connectors: [MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | TrustWallet , Web3ReactHooks][] = [ [metaMask, metaMaskHooks], [walletConnect, walletConnectHooks], [walletConnectV2, walletConnectV2Hooks], [coinbaseWallet, coinbaseWalletHooks], [network, networkHooks], + [trustWallet, trustHooks], ] function Child() { diff --git a/example/components/connectorCards/TrustWalletCard.tsx b/example/components/connectorCards/TrustWalletCard.tsx new file mode 100644 index 000000000..686e621cb --- /dev/null +++ b/example/components/connectorCards/TrustWalletCard.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' +import { hooks, trustWallet } from '../../connectors/trustWallet' +import { Card } from '../Card' + +const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider, useENSNames } = hooks + +export default function TrustWalletCard() { + const chainId = useChainId() + const accounts = useAccounts() + const isActivating = useIsActivating() + + const isActive = useIsActive() + + const provider = useProvider() + const ENSNames = useENSNames(provider) + + const [error, setError] = useState(undefined) + + useEffect(() => { + trustWallet.connectEagerly().catch((error) => { + console.debug('Failed to connect eagerly to OKX Wallet', error) + }) + }, []) + + return ( + + ) +} \ No newline at end of file diff --git a/example/connectors/trustWallet.ts b/example/connectors/trustWallet.ts new file mode 100644 index 000000000..9217802e0 --- /dev/null +++ b/example/connectors/trustWallet.ts @@ -0,0 +1,5 @@ +import { initializeConnector } from '@web3-react/core' + +import { TrustWallet } from '../../packages/trust/dist/index' + +export const [trustWallet, hooks] = initializeConnector((actions) => new TrustWallet({ actions })) \ No newline at end of file diff --git a/example/pages/index.tsx b/example/pages/index.tsx index baefa90fd..4cb7cd9e8 100644 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -4,6 +4,7 @@ import MetaMaskCard from '../components/connectorCards/MetaMaskCard' import NetworkCard from '../components/connectorCards/NetworkCard' import WalletConnectV2Card from '../components/connectorCards/WalletConnectV2Card' import ProviderExample from '../components/ProviderExample' +import TrustWalletCard from '../components/connectorCards/TrustWalletCard' export default function Home() { return ( @@ -15,6 +16,7 @@ export default function Home() { + ) diff --git a/example/utils.ts b/example/utils.ts index 68e79460a..e4686e8d2 100644 --- a/example/utils.ts +++ b/example/utils.ts @@ -5,6 +5,7 @@ import { Network } from '@web3-react/network' import type { Connector } from '@web3-react/types' import { WalletConnect as WalletConnect } from '@web3-react/walletconnect' import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' +import { TrustWallet } from '../packages/trust/dist/index' export function getName(connector: Connector) { if (connector instanceof MetaMask) return 'MetaMask' @@ -13,5 +14,6 @@ export function getName(connector: Connector) { if (connector instanceof CoinbaseWallet) return 'Coinbase Wallet' if (connector instanceof Network) return 'Network' if (connector instanceof GnosisSafe) return 'Gnosis Safe' + if (connector instanceof TrustWallet) return 'Trust Wallet' return 'Unknown' } diff --git a/packages/trust/README.md b/packages/trust/README.md new file mode 100644 index 000000000..565c67822 --- /dev/null +++ b/packages/trust/README.md @@ -0,0 +1 @@ +# @web3-react/trust diff --git a/packages/trust/package.json b/packages/trust/package.json new file mode 100644 index 000000000..6e9135ef6 --- /dev/null +++ b/packages/trust/package.json @@ -0,0 +1,32 @@ +{ + "name": "@web3-react/trust", + "keywords": [ + "web3-react", + "trust" + ], + "author": "Noah Zinsmeister ", + "license": "GPL-3.0-or-later", + "repository": "github:Uniswap/web3-react", + "publishConfig": { + "access": "public" + }, + "version": "8.2.2", + "files": [ + "dist/*" + ], + "type": "commonjs", + "types": "./dist/index.d.ts", + "main": "./dist/index.js", + "exports": "./dist/index.js", + "scripts": { + "prebuild": "rm -rf dist", + "build": "tsc", + "start": "tsc --watch" + }, + "dependencies": { + "@web3-react/types": "^8.2.2" + }, + "devDependencies": { + "@web3-react/store": "^8.2.2" + } +} diff --git a/packages/trust/src/index.spec.ts b/packages/trust/src/index.spec.ts new file mode 100644 index 000000000..3b9a2d444 --- /dev/null +++ b/packages/trust/src/index.spec.ts @@ -0,0 +1,66 @@ +import { createWeb3ReactStoreAndActions } from '@web3-react/store' +import type { Actions, Web3ReactStore } from '@web3-react/types' +import { TrustWallet } from '.' +import { MockEIP1193Provider } from '@web3-react/core' + +const chainId = '0x1' +const accounts: string[] = ['0x0000000000000000000000000000000000000000'] + +describe('TrustWallet', () => { + let mockProvider: MockEIP1193Provider + + beforeEach(() => { + mockProvider = new MockEIP1193Provider() + }) + + beforeEach(() => { + ;(window as any).ethereum = mockProvider + }) + + let store: Web3ReactStore + let connector: TrustWallet + + beforeEach(() => { + let actions: Actions + ;[store, actions] = createWeb3ReactStoreAndActions() + connector = new TrustWallet({ actions }) + }) + + test('#connectEagerly', async () => { + mockProvider.chainId = chainId + mockProvider.accounts = accounts + + await connector.connectEagerly() + + expect(mockProvider.eth_requestAccounts).not.toHaveBeenCalled() + expect(mockProvider.eth_accounts).toHaveBeenCalled() + expect(mockProvider.eth_chainId).toHaveBeenCalled() + expect(mockProvider.eth_chainId.mock.invocationCallOrder[0]) + .toBeGreaterThan(mockProvider.eth_accounts.mock.invocationCallOrder[0]) + + expect(store.getState()).toEqual({ + chainId: Number.parseInt(chainId, 16), + accounts, + activating: false, + }) + }) + + test('#activate', async () => { + mockProvider.chainId = chainId + mockProvider.accounts = accounts + + await connector.activate() + + expect(mockProvider.eth_requestAccounts).toHaveBeenCalled() + expect(mockProvider.eth_accounts).not.toHaveBeenCalled() + expect(mockProvider.eth_chainId).toHaveBeenCalled() + expect(mockProvider.eth_chainId.mock.invocationCallOrder[0]) + .toBeGreaterThan(mockProvider.eth_requestAccounts.mock.invocationCallOrder[0]) + + expect(store.getState()).toEqual({ + chainId: Number.parseInt(chainId, 16), + accounts, + activating: false, + }) + }) +}) \ No newline at end of file diff --git a/packages/trust/src/index.ts b/packages/trust/src/index.ts new file mode 100644 index 000000000..1ca9f31e7 --- /dev/null +++ b/packages/trust/src/index.ts @@ -0,0 +1,186 @@ +import type { + Actions, + AddEthereumChainParameter, + Provider, + ProviderConnectInfo, + ProviderRpcError, + WatchAssetParameters, +} from '@web3-react/types' +import { Connector } from '@web3-react/types' + +type TrustWalletProvider = Provider & { + __trust?: boolean + isConnected?: () => boolean + providers?: TrustWalletProvider[] +} + +export class NoTrustWalletError extends Error { + public constructor() { + super('Trust Wallet not installed') + this.name = NoTrustWalletError.name + Object.setPrototypeOf(this, NoTrustWalletError.prototype) + } +} + +function parseChainId(chainId: string) { + return Number.parseInt(chainId, 16) +} + + +export interface TrustWalletConstructorArgs { + actions: Actions + options?: Parameters[0] + onError?: (error: Error) => void +} + +export class TrustWallet extends Connector { + /** {@inheritdoc Connector.provider} */ + public provider?: TrustWalletProvider + + private readonly options?: Parameters[0] + private eagerConnection?: Promise + + constructor({ actions, options, onError }: TrustWalletConstructorArgs) { + super(actions, onError) + this.options = options + } + + private async isomorphicInitialize(): Promise { + if (this.eagerConnection) return + + const windowObj: any = window + const provider = windowObj.ethereum as any + if (provider) { + this.provider = provider as TrustWalletProvider + + if (this.provider.providers?.length) { + this.provider = this.provider.providers.find((p) => p.__trust) ?? this.provider.providers[0] + } + + this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) + + this.provider.on('disconnect', (error: ProviderRpcError): void => { + if (error.code === 1013) { + console.debug('Trust Wallet logged connection error 1013: "Try again later"') + return + } + this.actions.resetState() + this.onError?.(error) + }) + + this.provider.on('chainChanged', (chainId: string): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) + + this.provider.on('accountsChanged', (accounts: string[]): void => { + if (accounts.length === 0) { + this.actions.resetState() + } else { + this.actions.update({ accounts }) + } + }) + } + } + + /** {@inheritdoc Connector.connectEagerly} */ + public async connectEagerly(): Promise { + const cancelActivation = this.actions.startActivation() + + try { + await this.isomorphicInitialize() + if (!this.provider) return cancelActivation() + + const accounts = (await this.provider.request({ method: 'eth_accounts' })) as string[] + if (!accounts.length) throw new Error('No accounts returned') + const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string + this.actions.update({ chainId: parseChainId(chainId), accounts }) + } catch (error) { + console.debug('Could not connect eagerly', error) + this.actions.resetState() + } + } + + /** + * Initiates a connection. + * + * @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is + * already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch + * to the chain, if one of two conditions is met: either they already have it added in their extension, or the + * argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the + * specified parameters first, before being prompted to switch. + */ + public async activate(desiredChainIdOrChainParameters?: number | AddEthereumChainParameter): Promise { + let cancelActivation: () => void + if (!this.provider?.isConnected?.()) cancelActivation = this.actions.startActivation() + + return this.isomorphicInitialize() + .then(async () => { + if (!this.provider) throw new NoTrustWalletError() + + const accounts = (await this.provider.request({ method: 'eth_requestAccounts' })) as string[] + const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string + const receivedChainId = parseChainId(chainId) + const desiredChainId = + typeof desiredChainIdOrChainParameters === 'number' + ? desiredChainIdOrChainParameters + : desiredChainIdOrChainParameters?.chainId + + // if there's no desired chain, or it's equal to the received, update + if (!desiredChainId || receivedChainId === desiredChainId) + return this.actions.update({ chainId: receivedChainId, accounts }) + + const desiredChainIdHex = `0x${desiredChainId.toString(16)}` + + // if we're here, we can try to switch networks + return this.provider + .request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: desiredChainIdHex }], + }) + .catch((error: ProviderRpcError) => { + const errorCode = (error.data as any)?.originalError?.code || error.code + + // 4902 indicates that the chain has not been added to TrustWallet and wallet_addEthereumChain needs to be called + if (errorCode === 4902 && typeof desiredChainIdOrChainParameters !== 'number') { + if (!this.provider) throw new Error('No provider') + // if we're here, we can try to add a new network + return this.provider.request({ + method: 'wallet_addEthereumChain', + params: [{ ...desiredChainIdOrChainParameters, chainId: desiredChainIdHex }], + }) + } + + throw error + }) + .then(() => this.activate(desiredChainId)) + }) + .catch((error) => { + cancelActivation?.() + throw error + }) + } + + public async watchAsset({ address, symbol, decimals, image }: WatchAssetParameters): Promise { + if (!this.provider) throw new Error('No provider') + + return this.provider + .request({ + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options: { + address, + symbol, + decimals, + image, + }, + }, + }) + .then((success) => { + if (!success) throw new Error('Rejected') + return true + }) + } +} diff --git a/packages/trust/tsconfig.json b/packages/trust/tsconfig.json new file mode 100644 index 000000000..67531bbb0 --- /dev/null +++ b/packages/trust/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "outDir": "./dist" + } +}