UNPKG

@thirdweb-dev/wallets

Version:

<p align="center"> <br /> <a href="https://thirdweb.com"><img src="https://github.com/thirdweb-dev/js/blob/main/legacy_packages/sdk/logo.svg?raw=true" width="200" alt=""/></a> <br /> </p> <h1 align="center">thirdweb Wallet SDK</h1> <p align="center"> <a h

1,333 lines (1,273 loc) • 52.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var connector = require('../../../../dist/connector-a63dd9e7.cjs.dev.js'); var httpRpcClient = require('../../../../dist/http-rpc-client-d27d3692.cjs.dev.js'); var url = require('../../../../dist/url-4b641c31.cjs.dev.js'); var headers = require('../../../../dist/headers-2be346e5.cjs.dev.js'); var contracts = require('@account-abstraction/contracts'); var ethers = require('ethers'); var defineProperty = require('../../../../dist/defineProperty-9051a5d3.cjs.dev.js'); var sdk = require('@thirdweb-dev/sdk'); var evm_wallets_abstract_dist_thirdwebDevWalletsEvmWalletsAbstract = require('../../../wallets/abstract/dist/thirdweb-dev-wallets-evm-wallets-abstract.cjs.dev.js'); var utils = require('../../../../dist/utils-2aaa1386.cjs.dev.js'); var chains = require('@thirdweb-dev/chains'); require('eventemitter3'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } /** * @internal */ /** * @internal */ /** * an API to external a UserOperation with paymaster info */ class PaymasterAPI {} class VerifyingPaymasterAPI extends PaymasterAPI { constructor(paymasterUrl, entryPoint, clientId, secretKey) { super(); this.paymasterUrl = paymasterUrl; this.entryPoint = entryPoint; this.clientId = clientId; this.secretKey = secretKey; } async getPaymasterAndData(userOp) { const headers$1 = { "Content-Type": "application/json" }; if (url.isTwUrl(this.paymasterUrl)) { if (this.secretKey && this.clientId) { throw new Error("Cannot use both secret key and client ID. Please use secretKey for server-side applications and clientId for client-side applications."); } if (this.secretKey) { headers$1["x-secret-key"] = this.secretKey; } else if (this.clientId) { headers$1["x-client-id"] = this.clientId; const bundleId = typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis ? globalThis.APP_BUNDLE_ID : undefined; if (bundleId) { headers$1["x-bundle-id"] = bundleId; } } // Dashboard token. if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { headers$1["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; } // CLI token. if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { headers$1["authorization"] = `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`; headers$1["x-authorize-wallet"] = "true"; } headers.setAnalyticsHeaders(headers$1); } // Ask the paymaster to sign the transaction and return a valid paymasterAndData value. const response = await fetch(this.paymasterUrl, { method: "POST", headers: headers$1, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "pm_sponsorUserOperation", params: [await httpRpcClient.hexlifyUserOp(userOp), this.entryPoint] }) }); const res = await response.json(); if (!response.ok) { const error = res.error || response.statusText; const code = res.code || "UNKNOWN"; throw new Error(`Paymaster error: ${error} Status: ${response.status} Code: ${code}`); } if (httpRpcClient.DEBUG) { console.debug("Paymaster result:", res); } if (res.result) { // some paymasters return a string, some return an object with more data if (typeof res.result === "string") { return { paymasterAndData: res.result }; } else { return res.result; } } else { const error = res.error?.message || res.error || response.statusText || "unknown error"; throw new Error(`Paymaster error from ${this.paymasterUrl}: ${error}`); } } } const getVerifyingPaymaster = (paymasterUrl, entryPoint, clientId, secretKey) => new VerifyingPaymasterAPI(paymasterUrl, entryPoint, clientId, secretKey); /** * This class encapsulates Ethers.js listener function and necessary UserOperation details to * discover a TransactionReceipt for the operation. * * TODO refactor this to a simple event listener on the entry point */ class UserOperationEventListener { constructor(resolve, reject, entryPoint, sender, userOpHash, nonce, timeout) { this.resolve = resolve; this.reject = reject; this.entryPoint = entryPoint; this.sender = sender; this.userOpHash = userOpHash; this.nonce = nonce; this.timeout = timeout; defineProperty._defineProperty(this, "resolved", false); // eslint-disable-next-line @typescript-eslint/no-misused-promises this.boundLisener = this.listenerCallback.bind(this); } start() { // eslint-disable-next-line @typescript-eslint/no-misused-promises const filter = this.entryPoint.filters.UserOperationEvent(this.userOpHash); // listener takes time... first query directly: // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { const res = await this.entryPoint.queryFilter(filter, -10); // look at last 10 blocks if (res.length > 0) { void this.listenerCallback(res[0]); } else { this.entryPoint.once(filter, this.boundLisener); } }, 100); } stop() { // eslint-disable-next-line @typescript-eslint/no-misused-promises this.entryPoint.off("UserOperationEvent", this.boundLisener); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async listenerCallback() { for (var _len = arguments.length, param = new Array(_len), _key = 0; _key < _len; _key++) { param[_key] = arguments[_key]; } // TODO clean this up.. // eslint-disable-next-line prefer-rest-params const event = arguments[arguments.length - 1]; if (!event.args) { console.error("got event without args", event); return; } // TODO: can this happen? we register to event by userOpHash.. if (event.args.userOpHash !== this.userOpHash) { console.log(`== event with wrong userOpHash: sender/nonce: event.${event.args.sender}@${event.args.nonce.toString()}!= userOp.${this.sender}@${parseInt(this.nonce?.toString())}`); return; } const transactionReceipt = await event.getTransactionReceipt(); // before returning the receipt, update the status from the event. // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!event.args.success) { await this.extractFailureReason(transactionReceipt); } this.stop(); this.resolve(transactionReceipt); this.resolved = true; } async extractFailureReason(receipt) { receipt.status = 0; const revertReasonEvents = await this.entryPoint.queryFilter(this.entryPoint.filters.UserOperationRevertReason(this.userOpHash, this.sender), receipt.blockHash); if (revertReasonEvents[0]) { let message = revertReasonEvents[0].args.revertReason; if (message.startsWith("0x08c379a0")) { // Error(string) message = ethers.utils.defaultAbiCoder.decode(["string"], "0x" + message.substring(10)).toString(); } this.reject(new Error(`UserOp failed with reason: ${message}`)); } } } class ERC4337EthersSigner extends ethers.Signer { // TODO: we have 'erc4337provider', remove shared dependencies or avoid two-way reference constructor(config, originalSigner, erc4337provider, httpRpcClient, smartAccountAPI) { super(); ethers.utils.defineReadOnly(this, "provider", erc4337provider); this.config = config; this.originalSigner = originalSigner; this.erc4337provider = erc4337provider; this.httpRpcClient = httpRpcClient; this.smartAccountAPI = smartAccountAPI; this.approving = false; } // This one is called by Contract. It signs the request and passes in to Provider to be sent. async sendTransaction(transaction, options) { if (!this.approving) { this.approving = true; const tx = await this.smartAccountAPI.createApproveTx(); if (tx) { await (await this.sendTransaction(tx)).wait(); } this.approving = false; } const tx = await ethers.ethers.utils.resolveProperties(transaction); await this.verifyAllNecessaryFields(tx); const multidimensionalNonce = httpRpcClient.randomNonce(); const unsigned = await this.smartAccountAPI.createUnsignedUserOp(this.httpRpcClient, { target: tx.to || "", data: tx.data?.toString() || "0x", value: tx.value, gasLimit: tx.gasLimit, nonce: multidimensionalNonce, maxFeePerGas: tx.maxFeePerGas, maxPriorityFeePerGas: tx.maxPriorityFeePerGas }, options); const userOperation = await this.smartAccountAPI.signUserOp(unsigned); const transactionResponse = await this.erc4337provider.constructUserOpTransactionResponse(userOperation); try { await this.httpRpcClient.sendUserOpToBundler(userOperation); } catch (error) { throw this.unwrapError(error); } // TODO: handle errors - transaction that is "rejected" by bundler is _not likely_ to ever resolve its "wait()" return transactionResponse; } unwrapError(errorIn) { try { let errorMsg = "Unknown Error"; if (errorIn.error) { errorMsg = `The bundler has failed to include UserOperation in a batch: ${errorIn.error}`; } else if (errorIn.body && typeof errorIn.body === "string") { const errorBody = JSON.parse(errorIn.body); const errorStatus = errorIn.status || "UNKNOWN"; const errorCode = errorBody?.code || "UNKNOWN"; let failedOpMessage = errorBody?.error?.message || errorBody?.error?.data || errorBody?.error || errorIn.reason; if (failedOpMessage?.includes("FailedOp")) { let paymasterInfo = ""; // TODO: better error extraction methods will be needed const matched = failedOpMessage.match(/FailedOp\((.*)\)/); if (matched) { const split = matched[1].split(","); paymasterInfo = `(paymaster address: ${split[1]})`; failedOpMessage = split[2]; } errorMsg = `The bundler has failed to include UserOperation in a batch: ${failedOpMessage} ${paymasterInfo}`; } else { errorMsg = `RPC error: ${failedOpMessage} Status: ${errorStatus} Code: ${errorCode}`; } } const error = new Error(errorMsg); error.stack = errorIn.stack; return error; } catch (error) {} return errorIn; } async verifyAllNecessaryFields(transactionRequest) { if (!transactionRequest.to) { throw new Error("Missing call target"); } if (!transactionRequest.data && !transactionRequest.value) { // TBD: banning no-op UserOps seems to make sense on provider level throw new Error("Missing call data or value"); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars connect(provider) { throw new Error("changing providers is not supported"); } async getAddress() { if (!this.address) { this.address = await this.erc4337provider.getSenderAccountAddress(); } return this.address; } /** * Sign a message and return the signature */ async signMessage(message) { // Deploy smart wallet if needed const isNotDeployed = await this.smartAccountAPI.checkAccountPhantom(); if (isNotDeployed) { console.log("Account contract not deployed yet. Deploying account before signing message"); const tx = await this.sendTransaction({ to: await this.getAddress(), data: "0x" }); await tx.wait(); } const [chainId, address] = await Promise.all([this.getChainId(), this.getAddress()]); const originalMsgHash = ethers.utils.hashMessage(message); let factorySupports712; let signature; const rpcUrl = evm_wallets_abstract_dist_thirdwebDevWalletsEvmWalletsAbstract.chainIdToThirdwebRpc(chainId, this.config.clientId); const headers$1 = {}; if (url.isTwUrl(rpcUrl)) { const bundleId = typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis ? globalThis.APP_BUNDLE_ID : undefined; if (this.config.secretKey) { headers$1["x-secret-key"] = this.config.secretKey; } else if (this.config.clientId) { headers$1["x-client-id"] = this.config.clientId; if (bundleId) { headers$1["x-bundle-id"] = bundleId; } } // Dashboard token if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { headers$1["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; } // CLI token if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { headers$1["authorization"] = `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`; headers$1["x-authorize-wallet"] = "true"; } headers.setAnalyticsHeaders(headers$1); } try { const provider = new ethers.providers.StaticJsonRpcProvider({ url: rpcUrl, headers: headers$1 }, chainId); const walletContract = new ethers.Contract(address, ["function getMessageHash(bytes32 _hash) public view returns (bytes32)"], provider); // if this fails it's a pre 712 factory await walletContract.getMessageHash(originalMsgHash); factorySupports712 = true; } catch { factorySupports712 = false; } if (factorySupports712) { const result = await sdk.signTypedDataInternal(this, { name: "Account", version: "1", chainId, verifyingContract: address }, { AccountMessage: [{ name: "message", type: "bytes" }] }, { message: ethers.utils.defaultAbiCoder.encode(["bytes32"], [originalMsgHash]) }); signature = result.signature; } else { signature = await this.originalSigner.signMessage(message); } const isValid = await utils.checkContractWalletSignature(message, signature, address, chainId, this.config.clientId, this.config.secretKey); if (isValid) { return signature; } else { throw new Error("Unable to verify signature on smart account, please make sure the smart account is deployed and the signature is valid."); } } async signTransaction(transaction, options) { const tx = await ethers.ethers.utils.resolveProperties(transaction); await this.verifyAllNecessaryFields(tx); const multidimensionalNonce = httpRpcClient.randomNonce(); const unsigned = await this.smartAccountAPI.createUnsignedUserOp(this.httpRpcClient, { target: tx.to || "", data: tx.data?.toString() || "0x", value: tx.value, gasLimit: tx.gasLimit, nonce: multidimensionalNonce }, options); const userOperation = await this.smartAccountAPI.signUserOp(unsigned); const userOpString = JSON.stringify(await httpRpcClient.hexlifyUserOp(userOperation)); return userOpString; } } class ERC4337EthersProvider extends ethers.providers.BaseProvider { constructor(chainId, config, originalSigner, originalProvider, httpRpcClient, entryPoint, smartAccountAPI) { super({ name: "ERC-4337 Custom Network", chainId }); this.chainId = chainId; this.config = config; this.originalSigner = originalSigner; this.originalProvider = originalProvider; this.httpRpcClient = httpRpcClient; this.entryPoint = entryPoint; this.smartAccountAPI = smartAccountAPI; this.signer = new ERC4337EthersSigner(config, originalSigner, this, httpRpcClient, smartAccountAPI); } getSigner() { return this.signer; } async perform(method, params) { if (method === "sendTransaction" || method === "getTransactionReceipt") { // TODO: do we need 'perform' method to be available at all? // there is nobody out there to use it for ERC-4337 methods yet, we have nothing to override in fact. throw new Error("Should not get here. Investigate."); } if (method === "estimateGas") { // gas estimation does nothing at this layer, sendTransaction will do the gas estimation for the userOp return ethers.BigNumber.from(500000); } return await this.originalProvider.perform(method, params); } async getTransaction(transactionHash) { // TODO return await super.getTransaction(transactionHash); } async getTransactionReceipt(transactionHash) { const userOpHash = await transactionHash; const sender = await this.getSenderAccountAddress(); return await new Promise((resolve, reject) => { new UserOperationEventListener(resolve, reject, this.entryPoint, sender, userOpHash).start(); }); } async getSenderAccountAddress() { return await this.smartAccountAPI.getAccountAddress(); } async waitForTransaction(transactionHash, confirmations, timeout) { const sender = await this.getSenderAccountAddress(); return await new Promise((resolve, reject) => { const listener = new UserOperationEventListener(resolve, reject, this.entryPoint, sender, transactionHash, undefined, timeout); listener.start(); }); } // fabricate a response in a format usable by ethers users... async constructUserOpTransactionResponse(userOp1) { const userOp = await ethers.utils.resolveProperties(userOp1); const userOpHash = await this.smartAccountAPI.getUserOpHash(userOp); return { hash: userOpHash, confirmations: 0, from: userOp.sender, nonce: 0, // not the real nonce, but good enough for this purpose gasLimit: ethers.BigNumber.from(userOp.callGasLimit), // ?? value: ethers.BigNumber.from(0), data: ethers.utils.hexValue(userOp.callData), // should extract the actual called method from this "execFromEntryPoint()" call chainId: this.chainId, wait: async confirmations => { const transactionReceipt = await this.smartAccountAPI.getUserOpReceipt(this.httpRpcClient, userOpHash); if (userOp.initCode.length !== 0) { // checking if the wallet has been deployed by the transaction; it must be if we are here await this.smartAccountAPI.checkAccountPhantom(); } return transactionReceipt; } }; } async detectNetwork() { return this.originalProvider.detectNetwork(); } } /** * wrap an existing provider to tunnel requests through Account Abstraction. * @param originalProvider - The normal provider * @param config - see {@link ClientConfig} for more info * @param originalSigner - use this signer as the owner. of this wallet. By default, use the provider's signer */ function create4337Provider(config, accountApi, originalProvider, chainId) { const entryPoint = contracts.EntryPoint__factory.connect(config.entryPointAddress, originalProvider); const httpRpcClient$1 = new httpRpcClient.HttpRpcClient(config.bundlerUrl, config.entryPointAddress, chainId, config.clientId, config.secretKey); return new ERC4337EthersProvider(chainId, config, config.localSigner, originalProvider, httpRpcClient$1, entryPoint, accountApi); } const DUMMY_SIGNATURE = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; /** * Base class for all Smart Wallet ERC-4337 Clients to implement. * Subclass should inherit 5 methods to support a specific wallet contract: * * - getAccountInitCode - return the value to put into the "initCode" field, if the account is not yet deployed. should create the account instance using a factory contract. * - getNonce - return current account's nonce value * - encodeExecute - encode the call from entryPoint through our account to the target contract. * - signUserOpHash - sign the hash of a UserOp. * * The user can use the following APIs: * - createUnsignedUserOp - given "target" and "calldata", fill userOp to perform that operation from the account. * - createSignedUserOp - helper to call the above createUnsignedUserOp, and then extract the userOpHash and sign it */ class BaseAccountAPI { // entryPoint connected to "zero" address. allowed to make static calls (e.g. to getSenderAddress) /** * base constructor. * subclass SHOULD add parameters that define the owner (signer) of this wallet */ constructor(params) { defineProperty._defineProperty(this, "isPhantom", true); this.provider = params.provider; this.entryPointAddress = params.entryPointAddress; this.accountAddress = params.accountAddress; this.paymasterAPI = params.paymasterAPI; this.gasless = params.gasless; this.erc20PaymasterAddress = params.erc20PaymasterAddress; this.erc20TokenAddress = params.erc20TokenAddress; // factory "connect" define the contract address. the contract "connect" defines the "from" address. this.entryPointView = contracts.EntryPoint__factory.connect(params.entryPointAddress, params.provider).connect(ethers.ethers.constants.AddressZero); } /** * return the value to put into the "initCode" field, if the contract is not yet deployed. * this value holds the "factory" address, followed by this account's information */ /** * return current account's nonce. */ /** * encode the call from entryPoint through our account to the target contract. * @param target - The target contract address * @param value - The value to send to the target contract * @param data - The calldata to send to the target contract */ /** * sign a userOp's hash (userOpHash). * @param userOpHash - The hash to sign */ /** * calculate the account address even before it is deployed */ /** * check if the contract is already deployed. */ async checkAccountPhantom() { if (!this.isPhantom) { // already deployed. no need to check anymore. return this.isPhantom; } const senderAddressCode = await this.provider.getCode(this.getAccountAddress()); if (senderAddressCode.length > 2) { this.isPhantom = false; } return this.isPhantom; } /** * return initCode value to into the UserOp. * (either deployment code, or empty hex if contract already deployed) */ async getInitCode() { if (await this.checkAccountPhantom()) { return await this.getAccountInitCode(); } return "0x"; } /** * return maximum gas used for verification. * NOTE: createUnsignedUserOp will add to this value the cost of creation, if the contract is not yet created. */ async getVerificationGasLimit() { return 100000; } /** * return userOpHash for signing. * This value matches entryPoint.getUserOpHash (calculated off-chain, to avoid a view call) * @param userOp - userOperation, (signature field ignored) */ async getUserOpHash(userOp) { const chainId = await this.provider.getNetwork().then(net => net.chainId); return httpRpcClient.getUserOpHashV06(userOp, this.entryPointAddress, chainId); } /** * return the account's address. * this value is valid even before deploying the contract. */ async getAccountAddress() { if (!this.senderAddress) { if (this.accountAddress) { this.senderAddress = this.accountAddress; } else { this.senderAddress = await this.getCounterFactualAddress(); } } return this.senderAddress; } async estimateCreationGas(initCode) { if (!initCode || initCode === "0x") { return 0; } const deployerAddress = initCode.substring(0, 42); const deployerCallData = "0x" + initCode.substring(42); return await this.provider.estimateGas({ to: deployerAddress, data: deployerCallData }); } async createUnsignedUserOp(httpRpcClient, info, options) { let { maxFeePerGas, maxPriorityFeePerGas } = info; // get fees from bundler if available if (url.isTwUrl(httpRpcClient.bundlerUrl)) { const bundlerFeeData = await httpRpcClient.getUserOperationGasPrice(); maxFeePerGas = ethers.BigNumber.from(bundlerFeeData.maxFeePerGas); maxPriorityFeePerGas = ethers.BigNumber.from(bundlerFeeData.maxPriorityFeePerGas); } else { // if bundler is not available, try to get fees from the network if not passed explicitly if (!maxFeePerGas || !maxPriorityFeePerGas) { const feeData = await sdk.getDynamicFeeData(this.provider); if (!maxPriorityFeePerGas) { maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined; } if (!maxFeePerGas) { maxFeePerGas = feeData.maxFeePerGas ?? undefined; const network = await this.provider.getNetwork(); const chainId = network.chainId; if (chainId === chains.Celo.chainId || chainId === chains.CeloAlfajoresTestnet.chainId || chainId === chains.CeloBaklavaTestnet.chainId) { maxPriorityFeePerGas = maxFeePerGas; } } } } if (!maxFeePerGas || !maxPriorityFeePerGas) { throw new Error("maxFeePerGas or maxPriorityFeePerGas could not be calculated, please pass them explicitely"); } const [sender, nonce] = await Promise.all([this.getAccountAddress(), info.nonce ? Promise.resolve(info.nonce) : this.getNonce()]); const initCode = await this.getInitCode(); const value = parseNumber(info.value) ?? ethers.BigNumber.from(0); const callData = options?.batchData ? info.data : await this.prepareExecute(info.target, value, info.data).then(async tx => { if (!info.gasLimit) { // estimate gas on the inner transactions to simulate // bundler would not revert otherwise await this.provider.estimateGas({ from: sender, to: info.target, data: info.data, value: value }); } return tx.encode(); }); const partialOp = { sender, nonce, initCode, callData, maxFeePerGas, maxPriorityFeePerGas, callGasLimit: ethers.BigNumber.from(1000000), verificationGasLimit: ethers.BigNumber.from(1000000), preVerificationGas: ethers.BigNumber.from(1000000), paymasterAndData: "0x", signature: DUMMY_SIGNATURE }; // paymaster data + maybe used for estimation as well const gasless = options?.gasless !== undefined ? options.gasless : this.gasless; const useErc20Paymaster = this.erc20PaymasterAddress && this.erc20TokenAddress && (await this.isAccountApproved()); if (useErc20Paymaster) { partialOp.paymasterAndData = this.erc20PaymasterAddress; let estimates; try { estimates = await httpRpcClient.estimateUserOpGas(partialOp); } catch (error) { throw this.unwrapBundlerError(error); } partialOp.callGasLimit = estimates.callGasLimit; partialOp.verificationGasLimit = estimates.verificationGasLimit; partialOp.preVerificationGas = estimates.preVerificationGas; } else if (gasless) { const paymasterResult = await this.paymasterAPI.getPaymasterAndData(partialOp); const paymasterAndData = paymasterResult.paymasterAndData; if (paymasterAndData && paymasterAndData !== "0x") { partialOp.paymasterAndData = paymasterAndData; } // paymaster can have the gas limits in the response if (paymasterResult.callGasLimit && paymasterResult.verificationGasLimit && paymasterResult.preVerificationGas) { partialOp.callGasLimit = ethers.BigNumber.from(paymasterResult.callGasLimit); partialOp.verificationGasLimit = ethers.BigNumber.from(paymasterResult.verificationGasLimit); partialOp.preVerificationGas = ethers.BigNumber.from(paymasterResult.preVerificationGas); } else { // otherwise fallback to bundler for gas limits let estimates; try { estimates = await httpRpcClient.estimateUserOpGas(partialOp); } catch (error) { throw this.unwrapBundlerError(error); } partialOp.callGasLimit = estimates.callGasLimit; partialOp.verificationGasLimit = estimates.verificationGasLimit; partialOp.preVerificationGas = estimates.preVerificationGas; // need paymaster to re-sign after estimates if (paymasterAndData && paymasterAndData !== "0x") { const paymasterResult2 = await this.paymasterAPI.getPaymasterAndData(partialOp); if (paymasterResult2.paymasterAndData && paymasterResult2.paymasterAndData !== "0x") { partialOp.paymasterAndData = paymasterResult2.paymasterAndData; } } } } else { // query bundler for gas limits let estimates; try { estimates = await httpRpcClient.estimateUserOpGas(partialOp); } catch (error) { throw this.unwrapBundlerError(error); } partialOp.callGasLimit = estimates.callGasLimit; partialOp.verificationGasLimit = estimates.verificationGasLimit; partialOp.preVerificationGas = estimates.preVerificationGas; } return { ...partialOp, signature: "" }; } /** * Sign the filled userOp. * @param userOp - The UserOperation to sign (with signature field ignored) */ async signUserOp(userOp) { const userOpHash = await this.getUserOpHash(userOp); const signature = await this.signUserOpHash(userOpHash); return { ...userOp, signature }; } /** * get the transaction that has this userOpHash mined, or throws if not found * @param userOpHash - returned by sendUserOpToBundler (or by getUserOpHash..) * @param timeout - stop waiting after this timeout * @param interval - time to wait between polls. * @returns The transaction receipt, or an error if timed out. */ async getUserOpReceipt(httpRpcClient, userOpHash) { let timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 120000; let interval = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1000; const endtime = Date.now() + timeout; while (Date.now() < endtime) { const userOpReceipt = await httpRpcClient.getUserOperationReceipt(userOpHash); if (userOpReceipt) { // avoid desync with current provider state return await this.provider.waitForTransaction(userOpReceipt.receipt.transactionHash); } await new Promise(resolve => setTimeout(resolve, interval)); } throw new Error("Timeout waiting for userOp to be mined"); } unwrapBundlerError(error) { const message = error?.error?.message || error.error || error.message || error; return new Error(message); } } function parseNumber(a) { if (!a || a === "") { return null; } return ethers.BigNumber.from(a.toString()); } class AccountAPI extends BaseAccountAPI { constructor(params, originalProvider) { super({ ...params, provider: originalProvider }); this.params = params; // Technically dont need the signer here, but we need to encode/estimate gas with it so a signer is required // We don't want to use the localSigner directly since it might be connected to another chain // so we just use the public hardhat pkey instead this.sdk = sdk.ThirdwebSDK.fromPrivateKey(sdk.LOCAL_NODE_PKEY, params.chain, { clientId: params.clientId, secretKey: params.secretKey, // @ts-expect-error expected chain type error supportedChains: typeof params.chain === "object" ? [params.chain] : undefined }); } async getChainId() { return await this.provider.getNetwork().then(n => n.chainId); } async getAccountContract() { if (!this.accountContract) { if (this.params.accountInfo?.abi) { this.accountContract = await this.sdk.getContract(await this.getAccountAddress(), this.params.accountInfo.abi); } else { this.accountContract = await this.sdk.getContract(await this.getAccountAddress(), utils.ACCOUNT_CORE_ABI); } } return this.accountContract; } async getAccountInitCode() { const factory = await this.getFactoryContract(); const localSigner = await this.params.localSigner.getAddress(); const tx = await this.params.factoryInfo.createAccount(factory, localSigner); return ethers.utils.hexConcat([factory.getAddress(), tx.encode()]); } async getFactoryContract() { if (this.factoryContract) { return this.factoryContract; } if (this.params.factoryInfo?.abi) { this.factoryContract = await this.sdk.getContract(this.params.factoryAddress, this.params.factoryInfo.abi); } else { this.factoryContract = await this.sdk.getContract(this.params.factoryAddress); } return this.factoryContract; } async getCounterFactualAddress() { if (this.params.accountAddress) { return this.params.accountAddress; } const factory = await this.getFactoryContract(); const localSigner = await this.params.localSigner.getAddress(); return this.params.factoryInfo.getAccountAddress(factory, localSigner); } async getNonce() { if (await this.checkAccountPhantom()) { return ethers.BigNumber.from(0); } const accountContract = await this.getAccountContract(); return this.params.accountInfo.getNonce(accountContract); } async prepareExecute(target, value, data) { const accountContract = await this.getAccountContract(); return this.params.accountInfo.execute(accountContract, target, value, data); } async prepareExecuteBatch(targets, values, datas) { const accountContract = await this.getAccountContract(); return accountContract.prepare("executeBatch", [targets, values, datas]); } async signUserOpHash(userOpHash) { return await this.params.localSigner.signMessage(ethers.utils.arrayify(userOpHash)); } async isAcountDeployed() { return !(await this.checkAccountPhantom()); } async isAccountApproved() { if (!this.params.erc20PaymasterAddress || !this.params.erc20TokenAddress) { return true; } const swAddress = await this.getCounterFactualAddress(); const ERC20Abi = (await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@thirdweb-dev/contracts-js/dist/abis/IERC20.json')); })).default; const erc20Token = await this.sdk.getContract(this.params.erc20TokenAddress, ERC20Abi); const allowance = await erc20Token.call("allowance", [swAddress, this.params.erc20PaymasterAddress]); return allowance.gte(ethers.BigNumber.from(2).pow(96).sub(1)); } async createApproveTx() { if (await this.isAccountApproved()) { return undefined; } const amountToApprove = ethers.BigNumber.from(2).pow(96).sub(1); const ethersSigner = new ethers.ethers.Wallet(sdk.LOCAL_NODE_PKEY, this.provider); const erc20Contract = new ethers.Contract(this.params.erc20TokenAddress, ["function approve(address spender, uint256 amount) public returns (bool)"], ethersSigner); const tx = { to: this.params.erc20TokenAddress, from: await this.getAccountAddress(), value: 0, data: erc20Contract.interface.encodeFunctionData("approve", [this.params.erc20PaymasterAddress, amountToApprove]) }; return tx; } } class SmartWalletConnector extends connector.Connector { constructor(config) { super(); this.config = config; } async initialize(params) { const config = this.config; const originalProvider = sdk.getChainProvider(config.chain, { clientId: config.clientId, secretKey: config.secretKey }); this.chainId = (await originalProvider.getNetwork()).chainId; const bundlerUrl = this.config.bundlerUrl || `https://${this.chainId}.bundler.thirdweb.com`; const paymasterUrl = this.config.paymasterUrl || `https://${this.chainId}.bundler.thirdweb.com`; const entryPointAddress = config.entryPointAddress || utils.ENTRYPOINT_ADDRESS; const localSigner = await params.personalWallet.getSigner(); const providerConfig = { chain: config.chain, localSigner, entryPointAddress, bundlerUrl, paymasterAPI: this.config.paymasterAPI ? this.config.paymasterAPI : getVerifyingPaymaster(paymasterUrl, entryPointAddress, this.config.clientId, this.config.secretKey), gasless: config.gasless, factoryAddress: config.factoryAddress || utils.DEFAULT_FACTORY_ADDRESS, accountAddress: params.accountAddress, factoryInfo: { createAccount: config.factoryInfo?.createAccount || this.defaultFactoryInfo().createAccount, getAccountAddress: config.factoryInfo?.getAccountAddress || this.defaultFactoryInfo().getAccountAddress, abi: config.factoryInfo?.abi }, accountInfo: { execute: config.accountInfo?.execute || this.defaultAccountInfo().execute, getNonce: config.accountInfo?.getNonce || this.defaultAccountInfo().getNonce, abi: config.accountInfo?.abi }, clientId: config.clientId, secretKey: config.secretKey, erc20PaymasterAddress: config.erc20PaymasterAddress, erc20TokenAddress: config.erc20TokenAddress }; this.personalWallet = params.personalWallet; const accountApi = new AccountAPI(providerConfig, originalProvider); this.aaProvider = create4337Provider(providerConfig, accountApi, originalProvider, this.chainId); this.accountApi = accountApi; } async connect(connectionArgs) { await this.initialize(connectionArgs); return await this.getAddress(); } getProvider() { if (!this.aaProvider) { throw new Error("Personal wallet not connected"); } return Promise.resolve(this.aaProvider); } async getSigner() { if (!this.aaProvider) { throw new Error("Personal wallet not connected"); } return Promise.resolve(this.aaProvider.getSigner()); } async getAddress() { const signer = await this.getSigner(); return signer.getAddress(); } async isConnected() { try { const address = await this.getAddress(); return !!address; } catch (e) { return false; } } async disconnect() { this.personalWallet = undefined; this.aaProvider = undefined; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async switchChain(chainId) { const provider = await this.getProvider(); const currentChainId = (await provider.getNetwork()).chainId; if (currentChainId !== chainId) { // only throw if actually trying to switch chains throw new Error("Not supported."); } } setupListeners() { return Promise.resolve(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars updateChains(chains) {} /** * Check whether the connected signer can execute a given transaction using the smart wallet. * @param transaction - The transaction to execute using the smart wallet. * @returns whether the connected signer can execute the transaction using the smart wallet. */ async hasPermissionToExecute(transaction) { const accountContract = await this.getAccountContract(); const signer = await this.getSigner(); const signerAddress = await signer.getAddress(); const restrictions = (await accountContract.account.getAllSigners()).filter(item => ethers.ethers.utils.getAddress(item.signer) === ethers.ethers.utils.getAddress(signerAddress))[0]?.permissions; if (!restrictions) { return false; } return restrictions.approvedCallTargets.includes(transaction.getTarget()); } /// PREPARED TRANSACTIONS /** * Send a single transaction without waiting for confirmations * @param transaction - the transaction to send * @param config - optional the transaction configuration * @returns The awaitable transaction */ async send(transaction, options) { const signer = await this.getSigner(); return signer.sendTransaction({ to: transaction.getTarget(), data: transaction.encode(), value: await transaction.getValue() }, options); } /** * Execute a single transaction (waiting for confirmations) * @param transaction - The transaction to execute * @returns The transaction receipt */ async execute(transaction, options) { const tx = await this.send(transaction, options); const receipt = await tx.wait(); return { receipt }; } async sendBatch(transactions, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const signer = await this.getSigner(); const { tx, batchData } = await this.prepareBatchTx(transactions); return await signer.sendTransaction({ to: await signer.getAddress(), data: tx.encode(), value: 0 }, { ...options, batchData }); } /** * Execute multiple transactions in a single batch * @param transactions - The transactions to execute * @returns The transaction receipt */ async executeBatch(transactions, options) { const tx = await this.sendBatch(transactions, options); const receipt = await tx.wait(); return { receipt }; } /// RAW TRANSACTIONS async sendRaw(transaction, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const signer = await this.getSigner(); return signer.sendTransaction(transaction, options); } async executeRaw(transaction, options) { const tx = await this.sendRaw(transaction, options); const receipt = await tx.wait(); return { receipt }; } async sendBatchRaw(transactions, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const signer = await this.getSigner(); const batch = await this.prepareBatchRaw(transactions); return signer.sendTransaction({ to: await signer.getAddress(), data: batch.tx.encode(), value: 0 }, { ...options, batchData: batch.batchData // batched tx flag }); } async executeBatchRaw(transactions, options) { const tx = await this.sendBatchRaw(transactions, options); const receipt = await tx.wait(); return { receipt }; } /// ESTIMATION async estimate(transaction, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } return this.estimateTx({ target: transaction.getTarget(), data: transaction.encode(), value: await transaction.getValue(), gasLimit: await transaction.getOverrides().gasLimit, maxFeePerGas: await transaction.getOverrides().maxFeePerGas, maxPriorityFeePerGas: await transaction.getOverrides().maxPriorityFeePerGas, nonce: await transaction.getOverrides().nonce }, options); } async estimateRaw(transaction, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const tx = await ethers.ethers.utils.resolveProperties(transaction); return this.estimateTx({ target: tx.to || ethers.constants.AddressZero, data: tx.data?.toString() || "", value: tx.value || ethers.BigNumber.from(0), gasLimit: tx.gasLimit, maxFeePerGas: tx.maxFeePerGas, maxPriorityFeePerGas: tx.maxPriorityFeePerGas, nonce: tx.nonce }, options); } async estimateBatch(transactions, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const { tx, batchData } = await this.prepareBatchTx(transactions); return this.estimateTx({ target: tx.getTarget(), data: tx.encode(), value: await tx.getValue(), gasLimit: await tx.getOverrides().gasLimit, maxFeePerGas: await tx.getOverrides().maxFeePerGas, maxPriorityFeePerGas: await tx.getOverrides().maxPriorityFeePerGas, nonce: await tx.getOverrides().nonce }, { ...options, batchData }); } async estimateBatchRaw(transactions, options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const { tx, batchData } = await this.prepareBatchRaw(transactions); return this.estimateTx({ target: tx.getTarget(), data: tx.encode(), value: await tx.getValue(), gasLimit: await tx.getOverrides().gasLimit, maxFeePerGas: await tx.getOverrides().maxFeePerGas, maxPriorityFeePerGas: await tx.getOverrides().maxPriorityFeePerGas, nonce: await tx.getOverrides().nonce }, { ...options, batchData }); } //// DEPLOYMENT /** * Manually deploy the smart wallet contract. If already deployed this will throw an error. * Note that this is not necessary as the smart wallet will be deployed automatically on the first transaction the user makes. * @returns The transaction receipt */ async deploy(options) { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } const signer = await this.getSigner(); const tx = await signer.sendTransaction({ to: await signer.getAddress(), data: "0x" }, { ...options, batchData: { targets: [], data: [], values: [] } // batched tx flag to avoid hitting the Router fallback method }); const receipt = await tx.wait(); return { receipt }; } /** * Check if the smart wallet contract is deployed * @returns true if the smart wallet contract is deployed */ async isDeployed() { if (!this.accountApi) { throw new Error("Personal wallet not connected"); } return await this.accountApi.isAcountDeployed(); } async deployIfNeeded(options) { const isDeployed = await this.isDeployed(); if (!isDeployed) { await this.deploy(options); } } //// PERMISSIONS async grantPermissions(target, permissions) { const accountContract = await this.getAccountContract(); return accountContract.account.grantPermissions(target, permissions); } async revokePermissions(target) { const accountContract = await this.getAccountContract(); return accountContract.account.revokeAccess(target); } async addAdmin(target) { const accountContract = await this.getAccountContract(); return accountContract.account.grantAdminPermissions(target); } async removeAdmin(target) { const accountContract = await this.getAccountContract(); return accountContract.account.revokeAdminPermissions(target); } async getAllActiveSigners() { const isDeployed = await this.isDeployed(); if (isDeployed) { const accountContract = await this.getAccountContract(); return accountContract.account.getAllAdminsAndSigners(); } else { const personalWallet = await this.personalWallet?.getSigner(); if (!personalWallet) { throw new Error("Personal wallet not connected"); } return [{ isAdmin: true, signer: await personalWallet.getAddress(), permissions: { startDate: new Date(0), expirationDate: new Date(0), nativeTokenLimitPerTransaction: ethers.BigNumber.from(0), approvedCallTargets: [] } }]; } } /** * Get the underlying account contract of the smart wallet. * @returns The account contract of the smart wallet. */ async getAccountContract() { // getting a new instance everytime // to avoid caching issues pre/post deployment const sdk$1 = sdk.ThirdwebSDK.fromSigner(await this.getSigner(), this.config.chain, { clientId: this.config.clientId, secretKey: this.config.secretKey }); if (this.config.accountInfo?.abi) { return sdk$1.getContract(await this.getAddress(), this.config.accountInfo.abi); } else { return sdk$1.getContract(await this.getAddress(), utils.ACCOUNT_CORE_ABI); } } /** * Get the underlying account factory contract of the smart wallet. * @returns The account factory contract. */ async getFactoryContract() { if (!this.config.factoryAddress) { throw new Error("Factory address not set!"); } const sdk$1 = sdk.ThirdwebSDK.fromSigner(await this.getSigner(), this.config.chain, { clientId: this.config.clientId, secretKey: this.config.secretKey }); if (this.config.factoryInfo?.abi) { return sdk$1.getContract(this.config.factoryAddress, this.config.factoryInfo.abi); } return sdk$1.getContract(this.config.factoryAddress); } defaultFactoryInfo() { return { createAccount: async (factory, owner) => { return factory.prepare("createAccount", [owner, ethers.ethers.utils.toUtf8Bytes("")]); }, getAccountAddress: async (factory, owner) => { return await factory.call("getAddress", [owner, ethers.ethers.utils.toUtf8Bytes("")]); } }; } defaultAccountInfo() { return { execute: async (account, target, value, data) => { return account.prepare("execute", [target, value, data]); }, getNonce: async account => { return account.call("getNonce", []); } }; } /// PRIVATE METHODS async estimateTx(tx, options) { if (!this.accountApi || !this.aaProvider) { throw new Error("Personal wallet not connected"); } let deployGasLimit = ethers.BigNumber.from(0); const [provider, isDeployed] = await Promise.all([this.getProvider(), this.isDeployed()]); if (!isDeployed) { deployGasLimit = await this.estimateDeploymentGasLimit();