@turnkey/eip-1193-provider
Version:
EIP-1193 Provider for Turnkey.
265 lines (261 loc) • 12.7 kB
JavaScript
'use strict';
var http = require('@turnkey/http');
var viem = require('viem');
var utils = require('viem/utils');
var EventEmitter = require('events');
var utils$1 = require('./utils.js');
var turnkey = require('./turnkey.js');
var errors = require('./errors.js');
var version = require('./version.js');
const createEIP1193Provider = async (options) => {
const { turnkeyClient, organizationId, walletId, chains } = options;
// Used for public RPC requests
let id = 0;
// `activeChain` holds the current Ethereum chain that the provider is operating on to.
// It is set when the provider successfully switches to a new chain via `wallet_switchEthereumChain`
// or adds a new chain with `wallet_addEthereumChain`. This variable is crucial for ensuring that
// the provider operates with the correct chain context, including chain ID and RPC URLs.
let activeChain;
let addedChains = [];
const accounts = new Set();
// Initialize eventEmitter with a Proxy directly
const eventEmitter = new EventEmitter();
// `isInitialized` indicates that the provider is setup and ready to use.
// Used to skip setting connected for the initial RPC requests.
let isInitialized = false;
let lastEmittedEvent;
function setConnected(connected, data) {
if (!isInitialized)
return;
// Find the currently selected chain and update its connected status
addedChains = addedChains.map((chain) => chain.chainId === activeChain.chainId ? { ...chain, connected } : chain);
if (connected && lastEmittedEvent !== "connect" && isInitialized) {
// Emit 'connect' event when the provider becomes connected as per EIP-1193
// See https://eips.ethereum.org/EIPS/eip-1193#connect
eventEmitter.emit("connect", data);
lastEmittedEvent = "connect";
}
else if (addedChains.every(({ connected }) => !connected) &&
lastEmittedEvent !== "disconnect") {
// Emit 'disconnect' event when disconnected from all chains
// See https://eips.ethereum.org/EIPS/eip-1193#disconnect
const providerDisconnectedError = new viem.ProviderDisconnectedError(data);
eventEmitter.emit("disconnect", providerDisconnectedError);
// Reset 'connect' emitted flag on disconnect
lastEmittedEvent = "disconnect";
throw providerDisconnectedError;
}
else if (!connected) {
// Provider is disconnected from currentChain but connected to at least 1 other chain
// Provider is still considered 'connected' & we don't emit unless all chains disconnected
// See https://eips.ethereum.org/EIPS/eip-1193#provider-errors
throw new viem.ChainDisconnectedError(data);
}
}
const request = async ({ method, params, }) => {
try {
switch (method) {
case "web3_clientVersion": {
return version.VERSION;
}
/**
* Requests that the user provide an Ethereum address to be identified by.
* This method is specified by [EIP-1102](https://eips.ethereum.org/EIPS/eip-1102)
* This method must be called first to establish the connectivity of the client.
* @returns {Promise<Address[]>} An array of addresses after user authorization.
*/
case "eth_requestAccounts": {
// Note: In the future we should add a way for developers to surface a UI
// for user to select their accounts. For now it just returns all accounts
// for the provided walletId
const walletAccounts = await turnkeyClient.getWalletAccounts({
organizationId,
walletId,
});
walletAccounts.accounts.map(({ address }) => {
accounts.add(address);
});
setConnected(true, { chainId: activeChain.chainId });
return [...accounts];
}
/**
* Returns a list of addresses owned by the user.
* @returns {Promise<Address[]>} An array of addresses owned by the user.
*/
case "eth_accounts":
setConnected(true, { chainId: activeChain.chainId });
return [...accounts];
case "personal_sign": {
const [message, signWith] = params;
const signedMessage = await turnkey.signMessage({
organizationId,
message,
signWith: utils.getAddress(signWith),
client: turnkeyClient,
});
setConnected(true, { chainId: activeChain.chainId });
return signedMessage;
}
case "eth_sign": {
const [signWith, message] = params;
const signedMessage = await turnkey.signMessage({
organizationId,
message,
signWith: utils.getAddress(signWith),
client: turnkeyClient,
});
setConnected(true, { chainId: activeChain.chainId });
return signedMessage;
}
case "eth_signTypedData_v4": {
const [signWith, typedData] = params;
const message = utils.hashTypedData(typedData);
const signedMessage = await turnkey.signMessage({
organizationId,
message,
signWith: utils.getAddress(signWith),
client: turnkeyClient,
});
setConnected(true, { chainId: activeChain.chainId });
return signedMessage;
}
case "eth_signTransaction": {
const [transaction] = params;
const unsignedTransaction = utils$1.preprocessTransaction({ ...transaction });
const signedTransaction = await turnkey.signTransaction({
organizationId,
unsignedTransaction,
signWith: utils.getAddress(transaction.from),
client: turnkeyClient,
});
setConnected(true, { chainId: activeChain.chainId });
return `0x${signedTransaction}`;
}
case "wallet_addEthereumChain": {
const [chain] = params;
// Validate the to be added
utils$1.validateChain(chain, addedChains);
// Store the current connected chain for potential rollback
const previousActiveChain = activeChain;
// Update the connected chain to the new chain
activeChain = chain;
// Verify the specified chain ID matches the return value of eth_chainId from the endpoint
const rpcChainId = await request({ method: "eth_chainId" });
if (activeChain.chainId !== rpcChainId) {
// Revert to the previous connected chain or to undefined if no other chain connected
activeChain = previousActiveChain;
throw new errors.ChainIdMismatchError(chain.chainId, rpcChainId);
}
addedChains.push({ ...chain, connected: true });
return null;
}
case "wallet_switchEthereumChain": {
const [targetChainId] = params;
const targetChain = addedChains.find((chain) => chain.chainId === targetChainId);
if (!targetChain) {
throw new errors.UnrecognizedChainError(targetChainId);
}
activeChain = targetChain;
eventEmitter.emit("chainChanged", { chainId: activeChain.chainId });
return null;
}
// @ts-expect-error fall through expected
case "eth_sendTransaction": {
const [transaction] = params;
const signedTransaction = await request({
method: "eth_signTransaction",
params: [transaction],
});
// Change the method to 'eth_sendRawTransaction' and pass the signed transaction
method = "eth_sendRawTransaction";
params = [signedTransaction];
// Fall through to 'eth_sendRawTransaction' case
}
case "eth_sendRawTransaction":
case "eth_chainId":
case "eth_subscribe":
case "eth_unsubscribe":
case "eth_blobBaseFee":
case "eth_blockNumber":
case "eth_call":
case "eth_coinbase":
case "eth_estimateGas":
case "eth_feeHistory":
case "eth_gasPrice":
case "eth_getBalance":
case "eth_getBlockByHash":
case "eth_getBlockByNumber":
case "eth_getBlockReceipts":
case "eth_getBlockTransactionCountByHash":
case "eth_getBlockTransactionCountByNumber":
case "eth_getCode":
case "eth_getFilterChanges":
case "eth_getFilterLogs":
case "eth_getLogs":
case "eth_getProof":
case "eth_getStorageAt":
case "eth_getTransactionByBlockHashAndIndex":
case "eth_getTransactionByBlockNumberAndIndex":
case "eth_getTransactionByHash":
case "eth_getTransactionCount":
case "eth_getTransactionReceipt":
case "eth_getUncleCountByBlockHash":
case "eth_getUncleCountByBlockNumber":
case "eth_maxPriorityFeePerGas":
case "eth_newBlockFilter":
case "eth_newFilter":
case "eth_newPendingTransactionFilter":
case "eth_syncing":
// @ts-expect-error fall through expected
case "eth_uninstallFilter":
const { rpcUrls: [rpcUrl], } = activeChain;
if (rpcUrl) {
const rpcClient = utils.getHttpRpcClient(rpcUrl);
let response = await rpcClient.request({
body: {
method,
params,
id: id++,
},
});
if (response.error) {
throw new viem.RpcRequestError({
body: { method, params },
error: response.error,
url: rpcUrl,
});
}
// Set connected status upon successful Ethereum RPC request
setConnected(true, { chainId: activeChain.chainId });
return response.result;
}
default:
throw new viem.MethodNotSupportedRpcError(new Error(`Invalid method: ${method}`));
}
}
catch (error) {
if ((error.name === "HttpRequestError" &&
error.details === "fetch failed") ||
(error instanceof http.TurnkeyRequestError && turnkey.turnkeyIsDisconnected(error))) {
setConnected(false, error);
}
throw error;
}
};
if (Array.isArray(chains) && chains.length > 0) {
for (const chain of chains) {
await request({
method: "wallet_addEthereumChain",
params: [chain],
});
}
}
isInitialized = true;
return {
on: eventEmitter.on.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
request,
};
};
exports.createEIP1193Provider = createEIP1193Provider;
//# sourceMappingURL=index.js.map