@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
JavaScript
'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();