Skip to content

SDK upgrade with the ModularEtherspotWalletFactory contracts (on all chains) with latest IHook #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [6.1.0] - 2025-05-28
### Breaking Changes
- Changed contract address for Wallet Factory from `0x2A40091f044e48DEB5C0FCbc442E443F3341B451` to `0x38CC0EDdD3a944CA17981e0A19470d2298B8d43a`.
- Changed contract address for Bootstrap from `0x0D5154d7751b6e2fDaa06F0cC9B400549394C8AA` to `0xCF2808eA7d131d96E5C73Eb0eCD8Dc84D33905C7`.
- Changed contract address for Multiple Owner ECDSA Validator from `0x0740Ed7c11b9da33d9C80Bd76b826e4E90CC1906` to `0x0eA25BF9F313344d422B513e1af679484338518E`.
- Added support for HookMultiPlexer at address `0xDcA918dd23456d321282DF9507F6C09A50522136`.
- Results in a change of precomputed modular account address.

## [6.0.1] - 2025-05-28
### Fix
- revert 6.0.0 to the older version of the sdk
Expand Down
47 changes: 22 additions & 25 deletions examples/basics/custom-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,39 @@ const bundlerApiKey = 'etherspot_public_key';
const bundlerUrl = 'https://testnet-rpc.etherspot.io/v2/84532'; // bundler url
const chainId = 84532; // chain id
const entryPointAddress = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'; // entry point address
const walletFactoryAddress = '0x2A40091f044e48DEB5C0FCbc442E443F3341B451'; // wallet factory address
const bootstrapAddress = '0x0D5154d7751b6e2fDaa06F0cC9B400549394C8AA'; // bootstrap address
const multipleOwnerECDSAValidatorAddress = '0x0740Ed7c11b9da33d9C80Bd76b826e4E90CC1906'; // multi owner ECDSA validator factory address
const walletFactoryAddress = '0x38CC0EDdD3a944CA17981e0A19470d2298B8d43a'; // wallet factory address
const bootstrapAddress = '0xCF2808eA7d131d96E5C73Eb0eCD8Dc84D33905C7'; // bootstrap address
const multipleOwnerECDSAValidatorAddress = '0x0eA25BF9F313344d422B513e1af679484338518E'; // multi owner ECDSA validator factory address

// tsx examples/basics/custom-chain.ts
async function main() {
// for custom chains, you can use the following code to create a chain object
const chain = defineChain({
id: chainId,
name: "Base sepolia Testnet",
name: 'Base sepolia Testnet',
nativeCurrency: {
decimals: 18,
name: 'ETH',
symbol: 'ETH'
symbol: 'ETH',
},
rpcUrls: {
default: {
http: ['https://sepolia.base.org'] // RPC URL
}
}
})
http: ['https://sepolia.base.org'], // RPC URL
},
},
});
// initializating sdk...
const modularSdk = new ModularSdk(
process.env.WALLET_PRIVATE_KEY as string,
{
chain: chain,
chainId: chainId,
bundlerProvider: new EtherspotBundler(chainId, bundlerApiKey, bundlerUrl),
index: 0,
entryPointAddress,
walletFactoryAddress,
bootstrapAddress,
multipleOwnerECDSAValidatorAddress,
rpcProviderUrl: bundlerUrl,
})

const modularSdk = new ModularSdk(process.env.WALLET_PRIVATE_KEY as string, {
chain: chain,
chainId: chainId,
bundlerProvider: new EtherspotBundler(chainId, bundlerApiKey, bundlerUrl),
index: 0,
entryPointAddress,
walletFactoryAddress,
bootstrapAddress,
multipleOwnerECDSAValidatorAddress,
rpcProviderUrl: bundlerUrl,
});

// get address of EtherspotWallet...
const address: string = await modularSdk.getCounterFactualAddress();
Expand Down Expand Up @@ -77,7 +74,7 @@ async function main() {
console.log('Waiting for transaction...');
let userOpsReceipt: string | null = null;
const timeout = Date.now() + 1200000; // 1 minute timeout
while ((userOpsReceipt == null) && (Date.now() < timeout)) {
while (userOpsReceipt == null && Date.now() < timeout) {
await sleep(2);
userOpsReceipt = await modularSdk.getUserOpReceipt(uoHash);
}
Expand All @@ -86,4 +83,4 @@ async function main() {

main()
.catch(console.error)
.finally(() => process.exit());
.finally(() => process.exit());
1 change: 0 additions & 1 deletion examples/basics/get-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ dotenv.config();
// tsx examples/basics/get-address.ts
async function main() {
const bundlerApiKey = 'etherspot_public_key';
const customBundlerUrl = '';

// initializating sdk...
const modularSdk = generateModularSDKInstance(
Expand Down
2 changes: 1 addition & 1 deletion examples/basics/transfer-erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dotenv.config();
// add/change these values
const recipient = '0x80a1874E1046B1cc5deFdf4D3153838B72fF94Ac'; // recipient wallet address
const value = '0.1'; // transfer value
const tokenAddress = process.env.TOKEN_ADDRESS as string; // token address
const tokenAddress = '0xDeDf3B40f8c44b1fF195F37F35c6b8199C7ee443'; // token address
const bundlerApiKey = 'etherspot_public_key';

// tsx examples/basics/transfer-erc20.ts
Expand Down
70 changes: 70 additions & 0 deletions examples/modules/install-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { printOp } from '../../src/sdk/common/OperationUtils';
import * as dotenv from 'dotenv';
import { MODULE_TYPE, sleep } from '../../src/sdk/common';
import { encodeAbiParameters, encodeFunctionData, Hex, parseAbi } from 'viem';
import { generateModularSDKInstance } from '../helpers/sdk-helper';
import { getHookMultiPlexerInitData } from '../pulse/utils';
import { accountAbi } from '../../src/sdk/common/abis';

dotenv.config();

const bundlerApiKey = 'etherspot_public_key';
const CHAIN_ID = '1';
const HOOK_MULTIPLEXER_ADDRESS = '0xDcA918dd23456d321282DF9507F6C09A50522136';

// tsx examples/modules/install-hook.ts
async function main() {
// Init SDK
const modularSdk = generateModularSDKInstance(
process.env.WALLET_PRIVATE_KEY as string,
Number(CHAIN_ID),
bundlerApiKey,
);

// Get counterfactual of ModularEtherspotWallet...
const address: Hex = (await modularSdk.getCounterFactualAddress()) as Hex;
console.log('\x1b[33m%s\x1b[0m', `EtherspotWallet address: ${address}`);
// Get native balance
const balance = await modularSdk.getNativeBalance();
console.log('\x1b[33m%s\x1b[0m', `EtherspotWallet native balance: ${balance}`);
// Clear existing UserOps from batch
await modularSdk.clearUserOpsFromBatch();

/*//////////////////////////////////////////////////////////////
INSTALL HOOK MULTIPLEXER WITH CREDIBLE ACCOUNT MODULE - AS HOOK
//////////////////////////////////////////////////////////////*/

//Get HookMultiPlexer init data with CredibleAccountHook as global subhook
let hmpInitData = getHookMultiPlexerInitData([]);
const hmpInstallCalldata = encodeFunctionData({
abi: parseAbi(accountAbi),
functionName: 'installModule',
args: [BigInt(MODULE_TYPE.HOOK), HOOK_MULTIPLEXER_ADDRESS, hmpInitData],
});
// // Add UserOp to batch
await modularSdk.addUserOpsToBatch({ to: address, data: hmpInstallCalldata });

/*//////////////////////////////////////////////////////////////
ESTIMATE/SEND USER OP
//////////////////////////////////////////////////////////////*/

// Estimate UserOp
const op = await modularSdk.estimate();
console.log(`Estimate UserOp: ${await printOp(op)}`);
// Send UserOp
const uoHash = await modularSdk.send(op);
console.log(`UserOpHash: ${uoHash}`);
// Await transaction hash
console.log('Waiting for transaction...');
let userOpsReceipt = null;
const timeout = Date.now() + 1200000; // 1 minute timeout
while (userOpsReceipt == null && Date.now() < timeout) {
await sleep(2);
userOpsReceipt = await modularSdk.getUserOpReceipt(uoHash);
}
console.log('\x1b[33m%s\x1b[0m', `Transaction Receipt: `, userOpsReceipt);
}

main()
.catch(console.error)
.finally(() => process.exit());
31 changes: 31 additions & 0 deletions examples/modules/is-module-initialised.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as dotenv from 'dotenv';
import { MODULE_TYPE } from '../../src/sdk/common';
import { generateModularSDKInstance } from '../helpers/sdk-helper';

dotenv.config();

// tsx examples/modules/is-module-installed.ts
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the filename and comment inconsistency.

There's a mismatch between the filename (is-module-initialised.ts) and the comment (is-module-installed). The script actually calls isModuleInstalled, so the filename should reflect what it's actually doing.

Apply this diff to fix the comment:

-// tsx examples/modules/is-module-installed.ts
+// tsx examples/modules/is-module-initialised.ts

Or consider renaming the file to is-module-installed.ts if that better reflects the intended functionality.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// tsx examples/modules/is-module-installed.ts
// tsx examples/modules/is-module-initialised.ts
🤖 Prompt for AI Agents
In examples/modules/is-module-initialised.ts at line 7, the comment references a
different filename (is-module-installed.ts) which causes inconsistency. Fix this
by updating the comment to match the actual filename or rename the file to
is-module-installed.ts if that better reflects the script's functionality.
Ensure the comment and filename are consistent and accurately describe the
module's purpose.

async function main() {
const bundlerApiKey = 'etherspot_public_key';

console.log(`inside is-module-installed script:`);

// initializating sdk for index 0...
const modularSdk = generateModularSDKInstance(
process.env.WALLET_PRIVATE_KEY,
Number(process.env.CHAIN_ID),
bundlerApiKey
);// Testnets dont need apiKey on bundlerProvider

// get address of EtherspotWallet
const address: string = await modularSdk.getCounterFactualAddress();

console.log('\x1b[33m%s\x1b[0m', `EtherspotWallet address: ${address}`);

const isModuleInstalled = await modularSdk.isModuleInstalled(MODULE_TYPE.VALIDATOR, '0xFE14F6d4e407850b24D160B9ACfBb042D32BE492');
console.log(`isModuleInstalled: ${isModuleInstalled}`);
}

main()
.catch(console.error)
.finally(() => process.exit());
4 changes: 2 additions & 2 deletions examples/modules/uninstall-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function main() {
//generate deinit data...
const deInitData = await modularSdk.generateModuleDeInitData(
MODULE_TYPE.VALIDATOR,
'0xF4CDE8B11500ca9Ea108c5838DD26Ff1a4257a0c',
'0x9509aae8990bfA12BE09130BB822C37F3086863E',
deInitDataDefault);

console.log(`deinitData: ${deInitData}`);
Expand All @@ -36,7 +36,7 @@ async function main() {
// 0x22A55192a663591586241D42E603221eac49ed09
// 0xF4CDE8B11500ca9Ea108c5838DD26Ff1a4257a0c
const uoHash = await modularSdk.uninstallModule(MODULE_TYPE.VALIDATOR,
'0xF4CDE8B11500ca9Ea108c5838DD26Ff1a4257a0c', deInitData);
'0x9509aae8990bfA12BE09130BB822C37F3086863E', deInitData);
console.log(`UserOpHash: ${uoHash}`);

// get transaction hash...
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@etherspot/modular-sdk",
"version": "6.0.1",
"version": "6.1.0",
"description": "Etherspot Modular SDK - build with ERC-7579 smart accounts modules",
"keywords": [
"ether",
Expand Down
4 changes: 3 additions & 1 deletion src/sdk/base/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp.js';
import { PaymasterAPI } from './PaymasterAPI.js';
import { ErrorSubject, Exception, getUserOpHash, NotPromise, packUserOp, UserOperation } from '../common/index.js';
import { AddressZero, ErrorSubject, Exception, getUserOpHash, NotPromise, packUserOp, UserOperation } from '../common/index.js';
import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas.js';
import { Factory, Network, NetworkNames, NetworkService, SdkOptions, SignMessageDto, validateDto } from '../index.js';
import { Context } from '../context.js';
Expand Down Expand Up @@ -56,6 +56,7 @@ export abstract class BaseAccountAPI {
factoryUsed: Factory;
factoryAddress?: string;
validatorAddress?: string;
hookMultiplexerAddress?: string;
wallet: WalletProviderLike;
publicClient: PublicClient;

Expand Down Expand Up @@ -96,6 +97,7 @@ export abstract class BaseAccountAPI {
this.factoryAddress = params.factoryAddress;
this.publicClient = params.publicClient;
this.validatorAddress = params.optionsLike?.multipleOwnerECDSAValidatorAddress ?? Networks[params.optionsLike.chainId]?.contracts?.multipleOwnerECDSAValidator ?? DEFAULT_MULTIPLE_OWNER_ECDSA_VALIDATOR_ADDRESS;
this.hookMultiplexerAddress = Networks[params.optionsLike.chainId]?.contracts?.hookMultiPlexer || AddressZero;
}

get error$(): ErrorSubject {
Expand Down
12 changes: 12 additions & 0 deletions src/sdk/base/EtherspotWalletAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ export class EtherspotWalletAPI extends BaseAccountAPI {
return response as boolean;
}

async isModuleInitialised(moduleTypeId: MODULE_TYPE, module: string, initData = '0x'): Promise<boolean> {
const accountAddress = await this.getAccountAddress();
if (!accountAddress) throw new Error('Account address not found');
const response = await this.publicClient.readContract({
address: accountAddress as Hex,
abi: parseAbi(accountAbi),
functionName: 'isModuleInstalled',
args: [moduleTypeId, module, initData]
});
return response as boolean;
}
Comment on lines +79 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify the purpose of isModuleInitialised method.

This new method appears to be an exact duplicate of the existing isModuleInstalled method. Both call the same contract function isModuleInstalled, which creates confusion about their intended purposes.

Options to resolve:

  1. If this should check module initialization (different from installation), update to call the appropriate contract function:
  async isModuleInitialised(moduleTypeId: MODULE_TYPE, module: string, initData = '0x'): Promise<boolean> {
    const accountAddress = await this.getAccountAddress();
    if (!accountAddress) throw new Error('Account address not found');
    const response = await this.publicClient.readContract({
      address: accountAddress as Hex,
      abi: parseAbi(accountAbi),
-     functionName: 'isModuleInstalled',
+     functionName: 'isModuleInitialised', // or the correct function name
      args: [moduleTypeId, module, initData]
    });
    return response as boolean;
  }
  1. If this is intended as an alias, document it clearly and consider using a simpler implementation:
+ /**
+  * Alias for isModuleInstalled for backward compatibility
+  */
  async isModuleInitialised(moduleTypeId: MODULE_TYPE, module: string, initData = '0x'): Promise<boolean> {
-   const accountAddress = await this.getAccountAddress();
-   if (!accountAddress) throw new Error('Account address not found');
-   const response = await this.publicClient.readContract({
-     address: accountAddress as Hex,
-     abi: parseAbi(accountAbi),
-     functionName: 'isModuleInstalled',
-     args: [moduleTypeId, module, initData]
-   });
-   return response as boolean;
+   return this.isModuleInstalled(moduleTypeId, module, initData);
  }

Check if the contract has a separate isModuleInitialised function:


🏁 Script executed:

#!/bin/bash
# Description: Check if the account ABI contains isModuleInitialised function
# Expected: Should show if this function exists in the contract

echo "=== Checking for isModuleInitialised in account ABI ==="
rg -A 5 -B 5 "isModuleInitialised" src/sdk/common/abis.ts

echo "=== Checking usage of this new method ==="
rg "isModuleInitialised" --type ts -A 3 -B 3

Length of output: 896


Clarify the purpose of isModuleInitialised

The account ABI (src/sdk/common/abis.ts) does not define any isModuleInitialised function, so this new method on lines 79–89 in src/sdk/base/EtherspotWalletAPI.ts is an exact duplicate of isModuleInstalled. You need to resolve the intent:

• If you meant to call a different on‐chain function, update the functionName to the correct ABI entry (e.g. isModuleInitialised or whatever the actual name is).
• If this is merely an alias for backward compatibility, simplify the implementation and document it accordingly:

+ /**
+  * Alias for isModuleInstalled (kept for backward compatibility).
+  */
  async isModuleInitialised(
    moduleTypeId: MODULE_TYPE,
    module: string,
    initData = '0x'
  ): Promise<boolean> {
-   const accountAddress = await this.getAccountAddress();
-   if (!accountAddress) throw new Error('Account address not found');
-   const response = await this.publicClient.readContract({
-     address: accountAddress as Hex,
-     abi: parseAbi(accountAbi),
-     functionName: 'isModuleInstalled',
-     args: [moduleTypeId, module, initData]
-   });
-   return response as boolean;
+   return this.isModuleInstalled(moduleTypeId, module, initData);
  }

Please choose one of these resolutions so the code’s intent matches its implementation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async isModuleInitialised(moduleTypeId: MODULE_TYPE, module: string, initData = '0x'): Promise<boolean> {
const accountAddress = await this.getAccountAddress();
if (!accountAddress) throw new Error('Account address not found');
const response = await this.publicClient.readContract({
address: accountAddress as Hex,
abi: parseAbi(accountAbi),
functionName: 'isModuleInstalled',
args: [moduleTypeId, module, initData]
});
return response as boolean;
}
/**
* Alias for isModuleInstalled (kept for backward compatibility).
*/
async isModuleInitialised(
moduleTypeId: MODULE_TYPE,
module: string,
initData = '0x'
): Promise<boolean> {
return this.isModuleInstalled(moduleTypeId, module, initData);
}
🤖 Prompt for AI Agents
In src/sdk/base/EtherspotWalletAPI.ts between lines 79 and 89, the
isModuleInitialised method duplicates the isModuleInstalled method by calling
the same contract function, causing confusion. To fix this, first verify if the
contract has a distinct function for module initialization; if so, update the
functionName in the readContract call to that specific function. If no such
function exists and this method is intended as an alias, simplify the
implementation to call isModuleInstalled internally and add clear documentation
explaining it is an alias for clarity.


async installModule(moduleTypeId: MODULE_TYPE, module: string, initData = '0x'): Promise<string> {
const accountAddress = await this.getAccountAddress();
if (!accountAddress) throw new Error('Account address not found');
Expand Down
Loading