@keplr-ewallet/ewallet-sdk-eth
Version:
497 lines • 20.6 kB
JavaScript
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