Last active
October 27, 2023 06:34
-
-
Save XuNeal/3ccb3ce3fb37d152e4e7c69a40e1d03d to your computer and use it in GitHub Desktop.
imKey Connector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Chain, | |
ProviderRpcError, | |
RpcError, | |
SwitchChainError, | |
UserRejectedRequestError, | |
normalizeChainId, | |
getClient, | |
} from "@wagmi/core"; | |
import { providers } from "ethers"; | |
import { getAddress, hexValue } from "ethers/lib/utils.js"; | |
import type { ConnectorData } from "@wagmi/connectors"; | |
import { Connector } from "@wagmi/connectors"; | |
// import ImKeyProvider from "imkey-web3-provider"; | |
import ImKeyProvider from "@imkey/web3-provider"; | |
import { AddChainError, ChainNotConfiguredError } from "wagmi"; | |
type ImKeyConnectorOptions = { | |
rpcUrl?: string; | |
infuraId?: string; | |
chainId?: number; | |
headers?: Record<string, string>; | |
symbol?: string; | |
decimals?: number; | |
language?: string; | |
shimDisconnect?: boolean; | |
}; | |
type ImKeySigner = providers.JsonRpcSigner; | |
export class ImKeyConnector extends Connector< | |
ImKeyProvider, | |
ImKeyConnectorOptions, | |
ImKeySigner | |
> { | |
readonly id = "imkey"; | |
readonly name = "imKey"; | |
readonly ready = true; | |
#provider?: ImKeyProvider; | |
protected shimDisconnectKey = `${this.id}.shimDisconnect`; | |
constructor({ | |
chains, | |
options = { shimDisconnect: true }, | |
}: { | |
chains?: Chain[]; | |
options?: ImKeyConnectorOptions; | |
} = {}) { | |
super({ chains, options }); | |
} | |
async connect({ chainId }: { chainId?: number } = {}): Promise< | |
Required<ConnectorData> | |
> { | |
try { | |
const provider = await this.getProvider({ chainId }); | |
this.emit("message", { type: "connecting" }); | |
const accounts = (await provider.request({ | |
method: "eth_requestAccounts", | |
})) as string[]; | |
const account = getAddress(accounts[0] as string); | |
const id = await this.getChainId(); | |
const unsupported = this.isChainUnsupported(id); | |
// Enable support for programmatic chain switching | |
this.switchChain = this.#switchChain; | |
if (this.options.shimDisconnect) | |
getClient().storage?.setItem(this.shimDisconnectKey, true); | |
return { | |
account, | |
chain: { id, unsupported }, | |
provider: new providers.Web3Provider( | |
provider as providers.ExternalProvider | |
), | |
}; | |
} catch (error) { | |
if ((error as ProviderRpcError).code === 4001) { | |
throw new UserRejectedRequestError(error); | |
} | |
if ((error as RpcError).code === -32002) { | |
throw error instanceof Error ? error : new Error(String(error)); | |
} | |
throw error; | |
} | |
} | |
async disconnect() { | |
const provider = await this.getProvider(); | |
if (provider) { | |
await provider.stop(); | |
provider.off("accountsChanged", this.onAccountsChanged); | |
provider.off("chainChanged", this.onChainChanged); | |
provider.off("disconnect", this.onDisconnect); | |
} | |
if (this.options.shimDisconnect) | |
getClient().storage?.removeItem(this.shimDisconnectKey); | |
} | |
async getAccount() { | |
const provider = await this.getProvider(); | |
const accounts = (await provider.request({ | |
method: "eth_accounts", | |
})) as string[]; | |
const account = getAddress(accounts[0] as string); | |
return account; | |
} | |
async getChainId() { | |
const provider = await this.getProvider(); | |
const chainId = (await provider.request({ | |
method: "eth_chainId", | |
})) as number; | |
return normalizeChainId(chainId); | |
} | |
async getProvider({ chainId }: { chainId?: number } = {}) { | |
let chain: Chain = this.chains[0]; | |
let lastChainIdStr = this.getLastChainId(); | |
if (lastChainIdStr) { | |
let lastChainId = parseInt(lastChainIdStr); | |
chain = this.chains.find((x) => x.id === lastChainId) ?? this.chains[0]; | |
} | |
if (chainId) { | |
chain = this.chains.find((x) => x.id === chainId) ?? this.chains[0]; | |
} | |
if (!this.#provider) { | |
this.#provider = await this.createProvider(chain); | |
return this.#provider; | |
} | |
if (chainId) { | |
const connectedChainId = (await this.#provider.request({ | |
method: "eth_chainId", | |
})) as number; | |
if (chainId === connectedChainId) { | |
return this.#provider; | |
} else { | |
this.#provider = await this.createProvider(chain); | |
return this.#provider; | |
} | |
} | |
return this.#provider; | |
} | |
async createProvider(chain: Chain) { | |
const provider = new ImKeyProvider({ | |
rpcUrl: chain?.rpcUrls.default.http[0], | |
chainId: chain?.id, | |
decimals: chain?.nativeCurrency.decimals, | |
symbol: chain?.nativeCurrency.symbol, | |
}); | |
await provider.enable(); | |
if (provider.on) { | |
provider.on("accountsChanged", this.onAccountsChanged); | |
provider.on("chainChanged", this.onChainChanged); | |
provider.on("disconnect", this.onDisconnect); | |
} | |
this.saveLastChainId(chain); | |
return provider; | |
} | |
async getSigner({ chainId }: { chainId?: number } = {}) { | |
const provider = await this.getProvider({ chainId }); | |
const account = await this.getAccount(); | |
return new providers.Web3Provider( | |
provider as providers.ExternalProvider, | |
chainId | |
).getSigner(account); | |
} | |
async isAuthorized() { | |
try { | |
if ( | |
this.options.shimDisconnect && | |
// If shim does not exist in storage, wallet is disconnected | |
!getClient().storage?.getItem(this.shimDisconnectKey) | |
) | |
return false; | |
const account = await this.getAccount(); | |
return !!account; | |
} catch { | |
return false; | |
} | |
} | |
async #switchChain(chainId: number) { | |
const provider = await this.getProvider(); | |
const id = hexValue(chainId); | |
try { | |
await Promise.all([ | |
provider.request({ | |
method: "wallet_switchEthereumChain", | |
params: [{ chainId: id }], | |
}), | |
new Promise<void>((res) => | |
this.on("change", ({ chain }) => { | |
if (chain?.id === chainId) res(); | |
}) | |
), | |
]); | |
return ( | |
this.chains.find((x) => x.id === chainId) ?? | |
({ | |
id: chainId, | |
name: `Chain ${id}`, | |
network: `${id}`, | |
nativeCurrency: { name: "Ether", decimals: 18, symbol: "ETH" }, | |
rpcUrls: { default: { http: [""] }, public: { http: [""] } }, | |
} as Chain) | |
); | |
} catch (error) { | |
const chain = this.chains.find((x) => x.id === chainId); | |
if (!chain) | |
throw new ChainNotConfiguredError({ chainId, connectorId: this.id }); | |
// Indicates chain is not added to provider | |
if ( | |
(error as ProviderRpcError).code === 4902 || | |
// Unwrapping for MetaMask Mobile | |
// https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719 | |
(error as RpcError<{ originalError?: { code: number } }>)?.data | |
?.originalError?.code === 4902 | |
) { | |
try { | |
await Promise.all([ | |
provider.request({ | |
method: "wallet_addEthereumChain", | |
params: [ | |
{ | |
chainId: id, | |
chainName: chain.name, | |
nativeCurrency: chain.nativeCurrency, | |
rpcUrls: [chain.rpcUrls.public?.http[0] ?? ""], | |
blockExplorerUrls: this.getBlockExplorerUrls(chain), | |
}, | |
], | |
}), | |
new Promise<void>((res) => | |
this.on("change", ({ chain }) => { | |
if (chain?.id === chainId) res(); | |
}) | |
), | |
]); | |
const currentChainId = await this.getChainId(); | |
if (currentChainId !== chainId) | |
throw new ProviderRpcError( | |
"User rejected switch after adding network.", | |
{ code: 4001 } | |
); | |
this.saveLastChainId(chain); | |
return chain; | |
} catch (addError) { | |
if ((addError as ProviderRpcError).code === 4001) | |
throw new UserRejectedRequestError(addError); | |
throw new AddChainError(); | |
} | |
} | |
const message = | |
typeof error === "string" | |
? error | |
: (error as ProviderRpcError)?.message; | |
if (/user rejected request/i.test(message)) | |
throw new UserRejectedRequestError(error); | |
throw new SwitchChainError(error); | |
} | |
} | |
protected onAccountsChanged = (accounts: string[]) => { | |
if (accounts.length === 0) this.emit("disconnect"); | |
else this.emit("change", { account: getAddress(accounts[0] as string) }); | |
}; | |
protected onChainChanged = (chainId: number | string) => { | |
const id = normalizeChainId(chainId); | |
const unsupported = this.isChainUnsupported(id); | |
this.emit("change", { chain: { id, unsupported } }); | |
}; | |
protected onDisconnect = () => { | |
this.emit("disconnect"); | |
if (this.options.shimDisconnect) | |
getClient().storage?.removeItem(this.shimDisconnectKey); | |
}; | |
protected saveLastChainId(chain: Chain) { | |
var d = new Date(); | |
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000); | |
var expires = "expires=" + d.toUTCString(); | |
document.cookie = | |
"imkey_last_chain_id=" + chain?.id.toString() ?? "" + "; " + expires; | |
} | |
protected getLastChainId() { | |
const name = "imkey_last_chain_id="; | |
const cookie = document.cookie.split(";"); | |
for (let i = 0; i < cookie.length; i++) { | |
const item = cookie[i].trim(); | |
if (item.indexOf(name) == 0) | |
return item.substring(name.length, item.length); | |
} | |
return ""; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment