diff --git a/packages/dev-frontend/src/App.tsx b/packages/dev-frontend/src/App.tsx index 0fc369058..4650461f9 100644 --- a/packages/dev-frontend/src/App.tsx +++ b/packages/dev-frontend/src/App.tsx @@ -1,10 +1,10 @@ import React from "react"; import { createClient, WagmiConfig } from "wagmi"; import { mainnet, goerli, sepolia, localhost } from "wagmi/chains"; -import { ConnectKitProvider, getDefaultClient } from "connectkit"; +import { ConnectKitProvider } from "connectkit"; import { Flex, Heading, ThemeProvider, Paragraph, Link } from "theme-ui"; -// import { BatchedWebSocketAugmentedWeb3Provider } from "@liquity/providers"; +import getDefaultClient from "./connectkit/defaultClient"; import { LiquityProvider } from "./hooks/LiquityContext"; import { WalletConnector } from "./components/WalletConnector"; import { TransactionProvider } from "./components/Transaction"; diff --git a/packages/dev-frontend/src/connectkit/defaultClient.ts b/packages/dev-frontend/src/connectkit/defaultClient.ts new file mode 100644 index 000000000..da0af9917 --- /dev/null +++ b/packages/dev-frontend/src/connectkit/defaultClient.ts @@ -0,0 +1,224 @@ +// BSD 2-Clause License +// +// Copyright (c) 2022, LFE, Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. + +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { Connector, configureChains, ChainProviderFn } from "wagmi"; +import { Chain, mainnet, polygon, optimism, arbitrum } from "wagmi/chains"; +import { Provider } from "@wagmi/core"; + +import { MetaMaskConnector } from "wagmi/connectors/metaMask"; +import { WalletConnectConnector } from "wagmi/connectors/walletConnect"; +import { WalletConnectLegacyConnector } from "wagmi/connectors/walletConnectLegacy"; +import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet"; +import { SafeConnector } from "wagmi/connectors/safe"; +import { InjectedConnector } from "wagmi/connectors/injected"; + +import { alchemyProvider } from "../wagmi/alchemyProvider"; +import { infuraProvider } from "wagmi/providers/infura"; +import { jsonRpcProvider } from "wagmi/providers/jsonRpc"; +import { publicProvider } from "wagmi/providers/public"; + +let globalAppName: string; +let globalAppIcon: string; + +export const getAppName = () => globalAppName; +export const getAppIcon = () => globalAppIcon; + +const defaultChains = [mainnet, polygon, optimism, arbitrum]; + +type DefaultConnectorsProps = { + chains?: Chain[]; + app: { + name: string; + icon?: string; + description?: string; + url?: string; + }; + walletConnectProjectId?: string; +}; + +type DefaultClientProps = { + appName: string; + appIcon?: string; + appDescription?: string; + appUrl?: string; + autoConnect?: boolean; + alchemyId?: string; + infuraId?: string; + chains?: Chain[]; + connectors?: any; + provider?: any; + webSocketProvider?: any; + enableWebSocketProvider?: boolean; + stallTimeout?: number; + /* WC 2.0 requires a project ID (get one here: https://cloud.walletconnect.com/sign-in) */ + walletConnectProjectId: string; +}; + +type ConnectKitClientProps = { + autoConnect?: boolean; + connectors?: Connector[]; + provider: Provider; + webSocketProvider?: any; +}; + +const getDefaultConnectors = ({ chains, app, walletConnectProjectId }: DefaultConnectorsProps) => { + const hasAllAppData = app.name && app.icon && app.description && app.url; + const shouldUseSafeConnector = !(typeof window === "undefined") && window?.parent !== window; + + let connectors: Connector[] = []; + + // If we're in an iframe, include the SafeConnector + if (shouldUseSafeConnector) { + connectors = [ + ...connectors, + new SafeConnector({ + chains, + options: { + allowedDomains: [/gnosis-safe.io$/, /app.safe.global$/], + debug: false + } + }) + ]; + } + + // Add the rest of the connectors + connectors = [ + ...connectors, + new MetaMaskConnector({ + chains, + options: { + shimDisconnect: true, + UNSTABLE_shimOnConnectSelectAccount: true + } + }), + new CoinbaseWalletConnector({ + chains, + options: { + appName: app.name, + headlessMode: true + } + }), + walletConnectProjectId + ? new WalletConnectConnector({ + chains, + options: { + showQrModal: false, + projectId: walletConnectProjectId, + metadata: hasAllAppData + ? { + name: app.name, + description: app.description!, + url: app.url!, + icons: [app.icon!] + } + : undefined + } + }) + : new WalletConnectLegacyConnector({ + chains, + options: { + qrcode: false + } + }), + new InjectedConnector({ + chains, + options: { + shimDisconnect: true, + name: detectedName => + `Injected (${typeof detectedName === "string" ? detectedName : detectedName.join(", ")})` + } + }) + ]; + + return connectors; +}; + +const defaultClient = ({ + autoConnect = true, + appName = "ConnectKit", + appIcon, + appDescription, + appUrl, + chains = defaultChains, + alchemyId, + infuraId, + connectors, + provider, + stallTimeout, + webSocketProvider, + enableWebSocketProvider, + walletConnectProjectId +}: DefaultClientProps) => { + globalAppName = appName; + if (appIcon) globalAppIcon = appIcon; + + const providers: ChainProviderFn[] = []; + if (alchemyId) { + providers.push(alchemyProvider({ apiKey: alchemyId, stallTimeout })); + } + if (infuraId) { + providers.push(infuraProvider({ apiKey: infuraId, stallTimeout })); + } + providers.push( + jsonRpcProvider({ + rpc: c => { + return { http: c.rpcUrls.default.http[0] }; + }, + stallTimeout + }) + ); + providers.push(publicProvider()); + + const { + provider: configuredProvider, + chains: configuredChains, + webSocketProvider: configuredWebSocketProvider + } = configureChains(chains, providers); + + const connectKitClient: ConnectKitClientProps = { + autoConnect, + connectors: + connectors ?? + getDefaultConnectors({ + chains: configuredChains, + app: { + name: appName, + icon: appIcon, + description: appDescription, + url: appUrl + }, + walletConnectProjectId + }), + provider: provider ?? configuredProvider, + webSocketProvider: enableWebSocketProvider // Removed by default, breaks if used in Next.js – "unhandledRejection: Error: could not detect network" + ? webSocketProvider ?? configuredWebSocketProvider + : undefined + }; + + return { ...connectKitClient }; +}; + +export default defaultClient; diff --git a/packages/dev-frontend/src/providers/AlchemyProviderWithSepoliaSupport.ts b/packages/dev-frontend/src/providers/AlchemyProviderWithSepoliaSupport.ts new file mode 100644 index 000000000..b2a4fbca0 --- /dev/null +++ b/packages/dev-frontend/src/providers/AlchemyProviderWithSepoliaSupport.ts @@ -0,0 +1,57 @@ +import { Network, Networkish } from "@ethersproject/networks"; +import { defineReadOnly } from "@ethersproject/properties"; +import type { ConnectionInfo } from "@ethersproject/web"; + +import { + AlchemyProvider, + WebSocketProvider, + CommunityResourcable, + showThrottleMessage +} from "@ethersproject/providers"; + +const defaultApiKey = "_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC"; + +export class AlchemyWebSocketProviderWithSepoliaSupport + extends WebSocketProvider + implements CommunityResourcable { + readonly apiKey!: string; + + constructor(network?: Networkish, apiKey?: any) { + const provider = new AlchemyProvider(network, apiKey); + + const url = provider.connection.url + .replace(/^http/i, "ws") + .replace(".alchemyapi.", ".ws.alchemyapi."); + + super(url, provider.network); + defineReadOnly(this, "apiKey", provider.apiKey); + } + + isCommunityResource(): boolean { + return this.apiKey === defaultApiKey; + } +} + +export class AlchemyProviderWithSepoliaSupport extends AlchemyProvider { + static getUrl(network: Network, apiKey: string): ConnectionInfo { + let host = null; + switch (network.name) { + case "sepolia": + host = "eth-sepolia.g.alchemy.com/v2/"; + break; + default: + return AlchemyProvider.getUrl(network, apiKey); + } + + return { + allowGzip: true, + url: "https:/" + "/" + host + apiKey, + throttleCallback: () => { + if (apiKey === defaultApiKey) { + showThrottleMessage(); + } + return Promise.resolve(true); + } + }; + } +} diff --git a/packages/dev-frontend/src/wagmi/alchemyProvider.ts b/packages/dev-frontend/src/wagmi/alchemyProvider.ts new file mode 100644 index 000000000..bb35b0414 --- /dev/null +++ b/packages/dev-frontend/src/wagmi/alchemyProvider.ts @@ -0,0 +1,79 @@ +// MIT License + +// Copyright (c) 2023-present weth, LLC + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { Chain } from "@wagmi/chains"; + +import type { ChainProviderFn, FallbackProviderConfig } from "@wagmi/core"; + +import { + AlchemyProviderWithSepoliaSupport, + AlchemyWebSocketProviderWithSepoliaSupport +} from "../providers/AlchemyProviderWithSepoliaSupport"; + +export type AlchemyProviderConfig = FallbackProviderConfig & { + /** Your Alchemy API key from the [Alchemy Dashboard](https://dashboard.alchemyapi.io/). */ + apiKey: string; +}; + +export function alchemyProvider({ + apiKey, + priority, + stallTimeout, + weight +}: AlchemyProviderConfig): ChainProviderFn< + TChain, + AlchemyProviderWithSepoliaSupport, + AlchemyWebSocketProviderWithSepoliaSupport +> { + return function (chain) { + if (!chain.rpcUrls.alchemy?.http[0]) return null; + return { + chain: { + ...chain, + rpcUrls: { + ...chain.rpcUrls, + default: { http: [`${chain.rpcUrls.alchemy?.http[0]}/${apiKey}`] } + } + } as TChain, + provider: () => { + const provider = new AlchemyProviderWithSepoliaSupport( + { + chainId: chain.id, + name: chain.network, + ensAddress: chain.contracts?.ensRegistry?.address + }, + apiKey + ); + return Object.assign(provider, { priority, stallTimeout, weight }); + }, + webSocketProvider: () => + new AlchemyWebSocketProviderWithSepoliaSupport( + { + chainId: chain.id, + name: chain.network, + ensAddress: chain.contracts?.ensRegistry?.address + }, + apiKey + ) + }; + }; +}