Skip to content

Instantly share code, notes, and snippets.

@XuNeal
Last active October 27, 2023 06:34
Show Gist options
  • Save XuNeal/3ccb3ce3fb37d152e4e7c69a40e1d03d to your computer and use it in GitHub Desktop.
Save XuNeal/3ccb3ce3fb37d152e4e7c69a40e1d03d to your computer and use it in GitHub Desktop.
imKey Connector
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