UNPKG

@keplr-ewallet/ewallet-sdk-eth

Version:
497 lines 20.6 kB
import { hexToString, isAddress, isAddressEqual, serializeTypedData, } from "viem"; import { v4 as uuidv4 } from "uuid"; import { ErrorCodes, standardError } from "../errors"; import { PUBLIC_RPC_METHODS } from "../rpc"; import { parseTypedData, isValidChainId, toSignableTransaction, validateChain, } from "../utils"; import { ProviderEventEmitter, VERSION } from "../provider"; export class EWalletEIP1193Provider extends ProviderEventEmitter { constructor(options) { super(); this.isEWallet = true; this.version = VERSION; this.name = "EWalletEIP1193Provider"; this.ready = null; this.isInitialized = false; this.lastConnectedEmittedEvent = null; this.addedChains = options.chains.map((chain) => (Object.assign(Object.assign({}, chain), { validationStatus: "pending", connected: false }))); this.activeChain = this.addedChains[0]; this.ready = this._init(options); this.request = this.request.bind(this); this.on = this.on.bind(this); this.removeListener = this.removeListener.bind(this); } get chainId() { return this.activeChain.chainId; } get isConnected() { return this.addedChains.some((chain) => chain.connected); } async request(args) { this.validateRequestArgs(args); try { const result = await this.handleRequest(args); if (this.activeChain) { this._handleConnected(true, { chainId: this.activeChain.chainId }); } return result; } catch (error) { if (this.isConnectionError(error)) { const rpcError = standardError.rpc.resourceUnavailable({ message: (error === null || error === void 0 ? void 0 : error.message) || "Resource unavailable", data: error, }); this._handleConnected(false, rpcError); throw rpcError; } throw error; } } async handleRequest(args) { if (PUBLIC_RPC_METHODS.has(args.method)) { return this.handlePublicRpcRequest(args); } return this.handleWalletRpcRequest(args); } async handlePublicRpcRequest(args) { switch (args.method) { case "web3_clientVersion": return `${this.name}/${this.version}`; default: await this._validateActiveChain(); const { rpcUrls: [rpcUrl], } = this.activeChain; if (!rpcUrl) { throw standardError.provider.chainDisconnected({ message: "No RPC URL for the active chain", }); } const requestBody = Object.assign(Object.assign({}, args), { jsonrpc: "2.0", id: uuidv4() }); const res = await fetch(rpcUrl, { method: "POST", body: JSON.stringify(requestBody), mode: "cors", headers: { "Content-Type": "application/json", }, }); const data = (await res.json()); if ("error" in data) { throw data.error; } return data.result; } } async handleWalletRpcRequest(args) { var _a, _b, _c, _d, _e, _f; switch (args.method) { case "wallet_addEthereumChain": { const [newChain] = args.params; let chainIdx = this.addedChains.findIndex((c) => c.chainId === newChain.chainId); let chain; const isNew = chainIdx === -1; const wasActive = this.activeChain && this.activeChain.chainId === newChain.chainId; let original = undefined; if (isNew) { chain = Object.assign(Object.assign({}, newChain), { validationStatus: "pending", connected: false }); this.addedChains.push(chain); chainIdx = this.addedChains.length - 1; } else { chain = this.addedChains[chainIdx]; original = Object.assign({}, chain); Object.assign(chain, newChain); chain.validationStatus = "pending"; chain.connected = false; } let validationSucceeded = false; try { await this._manageChain(chain, true, true, false); validationSucceeded = true; chain.validationStatus = "valid"; if (wasActive) chain.connected = true; } catch (err) { if (isNew) { this.addedChains.splice(chainIdx, 1); } else if (original) { Object.assign(chain, original); if (wasActive) this.activeChain = original; } } if (!validationSucceeded) { throw standardError.rpc.invalidParams({ message: "Chain validation failed.", data: { chainId: newChain.chainId }, }); } return null; } case "wallet_switchEthereumChain": { const [{ chainId: chainIdToSwitch }] = args.params; const chain = this.addedChains.find((chain) => chain.chainId === chainIdToSwitch); if (!chain) { throw standardError.provider.unsupportedChain({ message: "Chain not found", data: { chainId: chainIdToSwitch, }, }); } const prevChainId = (_a = this.activeChain) === null || _a === void 0 ? void 0 : _a.chainId; this.activeChain = chain; if (prevChainId !== chainIdToSwitch) { this._handleChainChanged(chainIdToSwitch); } this._handleConnected(true, { chainId: chainIdToSwitch }); return null; } default: break; } await this._validateActiveChain(); switch (args.method) { case "eth_accounts": case "eth_requestAccounts": this._handleConnected(true, { chainId: (_b = this.activeChain) === null || _b === void 0 ? void 0 : _b.chainId }); try { const { address } = await this._getAuthenticatedSigner(args.method); return [address]; } catch (error) { return []; } case "eth_sendTransaction": const [tx] = args.params; const signedTx = await this.request({ method: "eth_signTransaction", params: [tx], }); const txHash = await this.request({ method: "eth_sendRawTransaction", params: [signedTx], }); this._handleConnected(true, { chainId: (_c = this.activeChain) === null || _c === void 0 ? void 0 : _c.chainId }); return txHash; case "eth_signTransaction": { const [tx] = args.params; const signableTx = toSignableTransaction(tx); const { signer, address } = await this._getAuthenticatedSigner(args.method); const { signedTransaction } = await signer.sign({ type: "sign_transaction", data: { address, transaction: signableTx, }, }); this._handleConnected(true, { chainId: (_d = this.activeChain) === null || _d === void 0 ? void 0 : _d.chainId }); return signedTransaction; } case "eth_signTypedData_v4": { const [signWith, rawTypedData] = args.params; const { signer, address } = await this._getAuthenticatedSigner(args.method); if (!isAddressEqual(signWith, address)) { throw standardError.rpc.invalidInput({ message: "Signer address mismatch", data: { signWith, signerAddress: address, }, }); } const typedData = typeof rawTypedData === "string" ? parseTypedData(rawTypedData) : rawTypedData; const { signature } = await signer.sign({ type: "sign_typedData_v4", data: { address, serializedTypedData: serializeTypedData(typedData), }, }); this._handleConnected(true, { chainId: (_e = this.activeChain) === null || _e === void 0 ? void 0 : _e.chainId }); return signature; } case "personal_sign": { const [message, signWith] = args.params; const { signer, address } = await this._getAuthenticatedSigner(args.method); if (!isAddressEqual(signWith, address)) { throw standardError.rpc.invalidInput({ message: "Signer address mismatch", data: { signWith, signerAddress: address, }, }); } const originalMessage = message.startsWith("0x") ? hexToString(message) : message; const { signature } = await signer.sign({ type: "personal_sign", data: { address, message: originalMessage, }, }); this._handleConnected(true, { chainId: (_f = this.activeChain) === null || _f === void 0 ? void 0 : _f.chainId }); return signature; } default: throw standardError.provider.unsupportedMethod({ message: "Method not supported", data: args.method, }); } } async _validateActiveChain() { if (!this.activeChain) { throw standardError.rpc.invalidRequest({}); } const activeChainStatus = this.addedChains.find((chain) => chain.chainId === this.activeChain.chainId); if ((activeChainStatus === null || activeChainStatus === void 0 ? void 0 : activeChainStatus.validationStatus) !== "valid") { throw standardError.rpc.invalidRequest({ message: "Active chain is not valid.", data: { chainId: this.activeChain.chainId }, }); } } async _getAuthenticatedSigner(calledMethod) { const signer = this.signer; if (!signer) { throw standardError.provider.unsupportedMethod({ message: "Signer is required for wallet RPC methods", data: calledMethod, }); } try { const address = await signer.getAddress(); if (!address) { throw new Error("Signer address is not available"); } return { signer, address }; } catch (error) { throw standardError.provider.unsupportedMethod({ message: "No authenticated signer for wallet RPC methods", data: calledMethod, }); } } async _manageChain(chain, skipDuplicateCheck = false, validationOnly = false, shouldSwitch = true) { const chainsToCheck = validationOnly ? [] : this.addedChains; const result = validateChain(chain, chainsToCheck); if (!result.isValid) { throw new Error(result.error || "Chain validation failed"); } const { rpcUrls: [rpcUrl], } = chain; if (!rpcUrl) { throw new Error("No RPC URL for the chain"); } const requestBody = { method: "eth_chainId", jsonrpc: "2.0", id: uuidv4(), }; const res = await fetch(rpcUrl, { method: "POST", body: JSON.stringify(requestBody), mode: "cors", headers: { "Content-Type": "application/json", }, }); const data = await res.json(); if (data.error) { throw new Error(`RPC Error: ${data.error.message}`); } const rpcChainId = data.result; if (rpcChainId !== chain.chainId) { throw new Error(`Chain ID mismatch: expected ${chain.chainId}, got ${rpcChainId}`); } if (validationOnly) { const existingChain = this.addedChains.find((c) => c.chainId === chain.chainId); if (existingChain) { existingChain.validationStatus = "valid"; } return; } const prevActiveChain = this.activeChain; if (shouldSwitch) { this.activeChain = chain; } try { if (!skipDuplicateCheck) { const existingChain = this.addedChains.find((c) => c.chainId === chain.chainId); if (!existingChain) { this.addedChains.push(Object.assign(Object.assign({}, chain), { connected: false, validationStatus: "valid" })); } else { existingChain.validationStatus = "valid"; } } const prevChainId = prevActiveChain === null || prevActiveChain === void 0 ? void 0 : prevActiveChain.chainId; if (shouldSwitch && prevChainId !== rpcChainId && this.isInitialized) { this._handleChainChanged(rpcChainId); this._handleConnected(true, { chainId: rpcChainId }); } } catch (error) { if (shouldSwitch) { this.activeChain = prevActiveChain; } throw error; } } async _init(options) { const { signer } = options; let signerAddresses = []; if (signer) { try { const signerAddress = await signer.getAddress(); if (!isAddress(signerAddress)) { throw new Error("Invalid signer address"); } if (typeof signer.sign !== "function") { throw new Error("Invalid signer"); } signerAddresses = [signerAddress]; } catch (error) { signerAddresses = []; } this.signer = signer; } if (options.skipChainValidation) { this.addedChains.forEach((chain, idx) => { chain.validationStatus = "valid"; chain.connected = idx === 0; }); this.activeChain = this.addedChains[0]; this.isInitialized = true; this.emit("_initialized", {}); this._handleChainChanged(this.activeChain.chainId); this._handleConnected(true, { chainId: this.activeChain.chainId }); if (signerAddresses.length > 0) { this._handleAccountsChanged(signerAddresses); } return; } await Promise.all(this.addedChains.map(async (chain) => { try { await this._manageChain(chain, true, true, false); } catch (err) { chain.validationStatus = "invalid"; chain.connected = false; } })); const firstValid = this.addedChains.find((c) => c.validationStatus === "valid"); if (firstValid) { this.activeChain = firstValid; this.isInitialized = true; this.emit("_initialized", {}); this._handleChainChanged(this.activeChain.chainId); this._handleConnected(true, { chainId: this.activeChain.chainId }); if (signerAddresses.length > 0) { this._handleAccountsChanged(signerAddresses); } } else { throw new Error("No valid chains found during provider initialization"); } } validateRequestArgs(args) { if (!args || typeof args !== "object" || Array.isArray(args)) { throw standardError.rpc.invalidParams({ message: "Expected a single, non-array, object argument.", data: args, }); } const { method, params } = args; if (typeof method !== "string" || method.length === 0) { throw standardError.rpc.invalidParams({ message: "Expected a non-empty string for method.", data: args, }); } if (typeof params !== "undefined" && typeof params !== "object") { throw standardError.rpc.invalidParams({ message: "Expected a single, non-array, object argument.", data: args, }); } if (params !== undefined && !Array.isArray(params) && (typeof params !== "object" || params === null)) { throw standardError.rpc.invalidParams({ message: "Expected a single, non-array, object argument.", data: args, }); } } _handleConnected(connected, data) { if (!this.isInitialized) { return; } if (this.activeChain) { const activeChainId = this.activeChain.chainId; this.addedChains.forEach((chain) => { chain.connected = chain.chainId === activeChainId ? connected : false; }); } if (connected && this.lastConnectedEmittedEvent !== "connect") { this.emit("connect", data); this.lastConnectedEmittedEvent = "connect"; } else if (!connected && this.addedChains.every(({ connected }) => !connected) && this.lastConnectedEmittedEvent !== "disconnect") { this.emit("disconnect", data); this.lastConnectedEmittedEvent = "disconnect"; } } isConnectionError(error) { var _a, _b; if ((error === null || error === void 0 ? void 0 : error.name) === "TypeError" || error instanceof TypeError) { const message = ((_a = error.message) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || ""; if (message.includes("fetch failed") || message.includes("failed to fetch") || message.includes("network error") || message.includes("load failed") || message.includes("networkerror when attempting to fetch")) { return true; } } if (error instanceof SyntaxError && ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("JSON"))) { return true; } if ((error === null || error === void 0 ? void 0 : error.code) === ErrorCodes.provider.disconnected || (error === null || error === void 0 ? void 0 : error.code) === ErrorCodes.provider.chainDisconnected) { return true; } if ((error === null || error === void 0 ? void 0 : error.name) === "AbortError") { return true; } return false; } _handleChainChanged(chainId) { if (!isValidChainId(chainId)) { return; } if (this.isInitialized) { this.emit("chainChanged", chainId); } } _handleAccountsChanged(newAddress) { this.emit("accountsChanged", newAddress); } } export async function initEWalletEIP1193Provider(options) { const provider = new EWalletEIP1193Provider(options); await provider.ready; return provider; } //# sourceMappingURL=base.js.map