@etherspot/modular-sdk
Version:
Etherspot Modular SDK - build with ERC-7579 smart accounts modules
345 lines • 15.5 kB
JavaScript
import { concat, encodeAbiParameters, encodeFunctionData, getAddress, isAddress, isBytes, pad, parseAbi, parseAbiParameters, toHex } from 'viem';
import { accountAbi, bootstrapAbi, entryPointAbi, factoryAbi } from '../common/abis.js';
import { getInstalledModules } from '../common/getInstalledModules.js';
import { CALL_TYPE, EXEC_TYPE, MODULE_TYPE, getExecuteMode } from '../common/index.js';
import { getViemAddress } from '../common/utils/viem-utils.js';
import { DEFAULT_BOOTSTRAP_ADDRESS, DEFAULT_QUERY_PAGE_SIZE, Networks } from '../network/constants.js';
import { BigNumber } from '../types/bignumber.js';
import { BaseAccountAPI } from './BaseAccountAPI.js';
import { _makeBootstrapConfig, makeBootstrapConfig } from './Bootstrap.js';
// Creating a constant for the sentinel address using viem
const SENTINEL_ADDRESS = getAddress("0x0000000000000000000000000000000000000001");
const ADDRESS_ZERO = getAddress("0x0000000000000000000000000000000000000000");
/**
* An implementation of the BaseAccountAPI using the EtherspotWallet contract.
* - contract deployer gets "entrypoint", "owner" addresses and "index" nonce
* - owner signs requests using normal "Ethereum Signed Message" (ether's signer.signMessage())
* - nonce method is "nonce()"
* - execute method is "execFromEntryPoint()"
*/
export class EtherspotWalletAPI extends BaseAccountAPI {
constructor(params) {
super(params);
this.index = params.index ?? 0;
this.predefinedAccountAddress = params.predefinedAccountAddress ?? null;
if (params?.optionsLike) {
this.bootstrapAddress = params.optionsLike?.bootstrapAddress ?? Networks[params.optionsLike.chainId]?.contracts?.bootstrap ?? DEFAULT_BOOTSTRAP_ADDRESS;
}
else {
this.bootstrapAddress = DEFAULT_BOOTSTRAP_ADDRESS;
}
}
getEOAAddress() {
return this.services.walletService.EOAAddress ?? '0x';
}
async isModuleInstalled(moduleTypeId, module, initData = '0x') {
const accountAddress = await this.getAccountAddress();
if (!accountAddress)
throw new Error('Account address not found');
const response = await this.publicClient.readContract({
address: accountAddress,
abi: parseAbi(accountAbi),
functionName: 'isModuleInstalled',
args: [moduleTypeId, module, initData]
});
return response;
}
async isModuleInitialised(moduleTypeId, module, initData = '0x') {
const accountAddress = await this.getAccountAddress();
if (!accountAddress)
throw new Error('Account address not found');
const response = await this.publicClient.readContract({
address: accountAddress,
abi: parseAbi(accountAbi),
functionName: 'isModuleInstalled',
args: [moduleTypeId, module, initData]
});
return response;
}
async installModule(moduleTypeId, module, initData = '0x') {
const accountAddress = await this.getAccountAddress();
if (!accountAddress)
throw new Error('Account address not found');
if (await this.isModuleInstalled(moduleTypeId, module, initData)) {
throw new Error('the module is already installed');
}
return encodeFunctionData({
functionName: 'installModule',
abi: parseAbi(accountAbi),
args: [moduleTypeId, module, initData],
});
}
async uninstallModule(moduleTypeId, module, deinitData) {
const isModuleInstalled = await this.isModuleInstalled(moduleTypeId, module, deinitData);
if (!isModuleInstalled) {
throw new Error('he module is not installed in the wallet');
}
// if this is uninstall on validator or executor, we need to check if there is more than 1 module
// we cant delete all modules when moduletypeid is validator or executor
if (moduleTypeId === MODULE_TYPE.EXECUTOR || moduleTypeId === MODULE_TYPE.VALIDATOR) {
const installedModules = moduleTypeId === MODULE_TYPE.EXECUTOR ? await this.getAllExecutors() : await this.getAllValidators();
if (installedModules.length === 1) {
throw new Error('Cannot uninstall the only module');
}
}
return encodeFunctionData({
functionName: 'uninstallModule',
abi: parseAbi(accountAbi),
args: [moduleTypeId, module, deinitData],
});
}
async getAllExecutors(pageSize = DEFAULT_QUERY_PAGE_SIZE) {
if (!this.accountAddress) {
throw new Error('Account address not found');
}
return await getInstalledModules({ client: this.publicClient, moduleAddress: getViemAddress(this.accountAddress), moduleTypes: ['executor'], pageSize: pageSize });
}
async getPreviousAddress(targetAddress, moduleTypeId) {
if (moduleTypeId !== MODULE_TYPE.EXECUTOR && moduleTypeId !== MODULE_TYPE.VALIDATOR) {
throw new Error("Unsupported module type");
}
const insalledModules = moduleTypeId === MODULE_TYPE.EXECUTOR ? await this.getAllExecutors() : await this.getAllValidators();
const index = insalledModules.indexOf(targetAddress);
if (index === 0) {
return SENTINEL_ADDRESS;
}
else if (index > 0) {
return insalledModules[index - 1];
}
else {
throw new Error(`Module ${targetAddress} not found in installed modules`);
}
}
// here its users responsibility to prepare deInit Data
// deinitData is prepared as bytes data made of the previous node address and the deinit data
// the deinit data is the data that is passed to the module to be uninstalled
async generateModuleDeInitData(moduleTypeId, module, deinitDataBase) {
// this is applicable only for Executor and Validator modules
// if the module type is not Executor or Validator, throw an error
if (moduleTypeId !== MODULE_TYPE.EXECUTOR && moduleTypeId !== MODULE_TYPE.VALIDATOR) {
throw new Error("Unsupported module type");
}
// Get the previous address in the list
const previousAddress = await this.getPreviousAddress(module, moduleTypeId);
// Prepare the deinit data
const deInitDataGenerated = encodeAbiParameters(parseAbiParameters('address, bytes'), [previousAddress, deinitDataBase]);
return deInitDataGenerated;
}
// function to get validators
async getAllValidators(pageSize = DEFAULT_QUERY_PAGE_SIZE) {
if (!this.accountAddress) {
throw new Error('Account address not found');
}
return await getInstalledModules({ client: this.publicClient, moduleAddress: getViemAddress(this.accountAddress), moduleTypes: ['validator'], pageSize: pageSize });
}
// function to get active hook
async getActiveHook() {
const activeHook = await this.publicClient.readContract({
address: this.accountAddress,
abi: parseAbi(accountAbi),
functionName: 'getActiveHook',
});
return activeHook;
}
async getFallbacks() {
return [];
}
// function to club the response of getAllExecutors, getAllValidators and getActiveHook
// return should be a wrapper of tis way
// prepare a schema like above and return the response
async getAllModules(pageSize = DEFAULT_QUERY_PAGE_SIZE) {
const validators = await this.getAllValidators(pageSize) || [];
const executors = await this.getAllExecutors(pageSize) || [];
const hook = await this.getActiveHook() || "";
const fallbacks = await this.getFallbacks() || [];
return {
validators,
executors,
hook,
fallbacks
};
}
async checkAccountAddress(address) {
const eoaAddress = await this.getEOAAddress();
const isOwner = await this.publicClient.readContract({
address: address,
abi: parseAbi(accountAbi),
functionName: 'isOwner',
args: [eoaAddress]
});
if (!isOwner) {
throw new Error('the specified accountAddress does not belong to the given EOA provider');
}
else {
this.accountAddress = address;
}
}
async getInitCodeData() {
if (!this.validatorAddress) {
throw new Error('Validator address not found');
}
const validators = makeBootstrapConfig(this.validatorAddress, '0x');
const executors = makeBootstrapConfig(ADDRESS_ZERO, '0x');
const hook = _makeBootstrapConfig(ADDRESS_ZERO, '0x');
const fallbacks = makeBootstrapConfig(ADDRESS_ZERO, '0x');
const initMSAData = encodeFunctionData({
functionName: 'initMSA',
abi: parseAbi(bootstrapAbi),
args: [validators, executors, hook, fallbacks],
});
const eoaAddress = await this.getEOAAddress();
const initCode = encodeAbiParameters(parseAbiParameters('address, address, bytes'), [eoaAddress, this.bootstrapAddress, initMSAData]);
return initCode;
}
/**
* return the value to put into the "initCode" field, if the account is not yet deployed.
* this value holds the "factory" address, followed by this account's information
*/
async getAccountInitCode() {
if (this.factoryAddress == null || this.factoryAddress == '') {
throw new Error('no factory to get initCode');
}
const initCode = await this.getInitCodeData();
const salt = pad(toHex(this.index), { size: 32 });
const functionData = encodeFunctionData({
functionName: 'createAccount',
abi: parseAbi(factoryAbi),
args: [
salt,
initCode,
],
});
return concat([
this.factoryAddress,
functionData,
]);
}
async getCounterFactualAddress() {
if (this.predefinedAccountAddress) {
await this.checkAccountAddress(this.predefinedAccountAddress);
}
const salt = pad(toHex(this.index), { size: 32 });
const initCode = await this.getInitCodeData();
if (!this.accountAddress) {
this.accountAddress = (await this.publicClient.readContract({
address: this.factoryAddress,
abi: parseAbi(factoryAbi),
functionName: 'getAddress',
args: [salt, initCode]
}));
}
return this.accountAddress;
}
async getNonce(key = BigNumber.from(0)) {
const accountAddress = await this.getAccountAddress();
const nonceKey = key.eq(0) ? this.validatorAddress : key.toHexString();
if (!nonceKey) {
throw new Error('nonce key not defined');
}
if (!this.checkAccountPhantom()) {
let isAddressIndicator = false;
try {
isAddressIndicator = isAddress(getAddress(nonceKey), { strict: true });
if (!isAddressIndicator) {
throw new Error(`Invalid Validator Address: ${nonceKey}`);
}
else {
const isModuleInstalled = await this.isModuleInstalled(MODULE_TYPE.VALIDATOR, nonceKey);
if (!isModuleInstalled) {
throw new Error(`Validator: ${nonceKey} is not installed in the wallet`);
}
}
}
catch (e) {
console.error(`Error caught : ${e}`);
throw new Error(`Invalid Validator Address: ${nonceKey}`);
}
}
const dummyKey = getAddress(nonceKey) + "00000000";
const nonceResponse = await this.publicClient.readContract({
address: this.entryPointAddress,
abi: parseAbi(entryPointAbi),
functionName: 'getNonce',
args: [accountAddress, BigInt(dummyKey)]
});
return nonceResponse;
}
/**
* encode a method call from entryPoint to our contract
* @param target
* @param value
* @param data
*/
async encodeExecute(target, value, data) {
const executeMode = getExecuteMode({
callType: CALL_TYPE.SINGLE,
execType: EXEC_TYPE.DEFAULT
});
// Assuming toHex is a function that accepts string | number | bigint | boolean | Uint8Array
// Convert BigNumberish to a string if it's a BigNumber
// Convert BigNumberish or Bytes to a compatible type
let valueToProcess;
if (BigNumber.isBigNumber(value)) {
valueToProcess = value.toString(); // Convert BigNumber to string
}
else if (isBytes(value)) {
valueToProcess = new Uint8Array(value); // Convert Bytes to Uint8Array
}
else {
// Here, TypeScript is unsure about the type of `value`
// You need to ensure `value` is of a type compatible with `valueToProcess`
// If `value` can only be string, number, bigint, boolean, or Uint8Array, this assignment is safe
// If `value` can be of other types (like Bytes), you need an explicit conversion or handling here
// For example, if there's a chance `value` is still `Bytes`, you could handle it like so:
if (typeof value === 'object' && value !== null && 'length' in value) {
// Assuming this condition is sufficient to identify Bytes-like objects
// Convert it to Uint8Array
valueToProcess = new Uint8Array(Object.values(value));
}
else {
valueToProcess = value;
}
}
const calldata = concat([
target,
pad(toHex(valueToProcess), { size: 32 }),
data
]);
return encodeFunctionData({
functionName: 'execute',
abi: parseAbi(accountAbi),
args: [executeMode, calldata],
});
}
async signUserOpHash(userOpHash) {
return await this.services.walletService.signUserOp(userOpHash);
}
async encodeBatch(targets, values, datas) {
const executeMode = getExecuteMode({
callType: CALL_TYPE.BATCH,
execType: EXEC_TYPE.DEFAULT
});
const result = targets.map((target, index) => ({
target: target,
value: values[index],
callData: datas[index]
}));
const convertedResult = result.map(item => ({
...item,
// Convert `value` from BigNumberish to bigint
value: typeof item.value === 'bigint' ? item.value : BigNumber.from(item.value.toString()).toBigInt(),
}));
//TODO-Test-LibraryFix identify the syntax for viem to pass array of tuple
// const calldata = ethers.utils.defaultAbiCoder.encode(
// ["tuple(address target,uint256 value,bytes callData)[]"],
// [result]
// );
const calldata = encodeAbiParameters(parseAbiParameters('(address target,uint256 value,bytes callData)[]'), [convertedResult]);
return encodeFunctionData({
functionName: 'execute',
abi: parseAbi(accountAbi),
args: [executeMode, calldata],
});
}
}
//# sourceMappingURL=EtherspotWalletAPI.js.map