diff --git a/CHANGELOG.md b/CHANGELOG.md index 51680fc..e71b1e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.13.0] - 2024-06-17 + +### Added Changes +- `getSDK` include a param to choose to instantiate the Prime SDK instead of the Modular SDK +- Added Etherspot Modular SDK `installModule` and `uninstallModule` to hook `useEtherspotModules` +- Added `isModular` to context `EtherspotContextProvider` + +### Breaking Changes +- Etherspot Modular SDK implemented to TransactionKit as the default `accountTemplate` +- Changed the `etherspot-prime` wallet type to `etherspot` wallet type + ## [0.12.1] - 2024-05-22 ### Added Changes diff --git a/__mocks__/@etherspot/modular-sdk.js b/__mocks__/@etherspot/modular-sdk.js new file mode 100644 index 0000000..e9a97b6 --- /dev/null +++ b/__mocks__/@etherspot/modular-sdk.js @@ -0,0 +1,143 @@ +import * as EtherspotModular from '@etherspot/modular-sdk'; +import { ethers } from 'ethers'; + +export const defaultAccountAddressModular = '0x7F30B1960D5556929B03a0339814fE903c55a347'; +export const otherFactoryDefaultAccountAddressModular = '0xe383724e3bDC4753746dEC781809f8CD82010914'; +export const otherAccountAddressModular = '0xAb4C67d8D7B248B2fA6B638C645466065fE8F1F1'; + +export class ModularSdk { + sdkChainId; + userOps = []; + nonce = ethers.BigNumber.from(1); + factoryWallet; + + constructor(provider, config) { + this.sdkChainId = config.chainId; + this.factoryWallet = config.factoryWallet; + } + + getCounterFactualAddress() { + if (this.factoryWallet === 'etherspotModular') { + return defaultAccountAddressModular; + } + return otherFactoryDefaultAccountAddressModular; + } + + async clearUserOpsFromBatch() { + this.userOps = []; + } + + async addUserOpsToBatch(userOp) { + this.userOps.push(userOp); + } + + async estimate({ paymasterDetails: paymaster }) { + let maxFeePerGas = ethers.utils.parseUnits('1', 'gwei'); + let maxPriorityFeePerGas = ethers.utils.parseUnits('1', 'gwei'); + let callGasLimit = ethers.BigNumber.from('50000'); + let signature = '0x004'; + + if (paymaster?.url === 'someUrl') { + maxFeePerGas = ethers.utils.parseUnits('2', 'gwei'); + maxPriorityFeePerGas = ethers.utils.parseUnits('3', 'gwei'); + callGasLimit = ethers.BigNumber.from('75000'); + } + + if (paymaster?.url === 'someUnstableUrl') { + signature = '0x0'; + } + + let finalGasLimit = ethers.BigNumber.from(callGasLimit); + + if (this.sdkChainId === 420) { + throw new Error('Transaction reverted: chain too high'); + } + + this.userOps.forEach((userOp) => { + if (userOp.to === '0xDEADBEEF') { + throw new Error('Transaction reverted: invalid address'); + } + finalGasLimit = finalGasLimit.add(callGasLimit); + if (userOp.data && userOp.data !== '0x0' && userOp.data !== '0xFFF') { + finalGasLimit = finalGasLimit.add(callGasLimit); + } + }); + + return { + sender: defaultAccountAddressModular, + nonce: this.nonce, + initCode: '0x001', + callData: '0x002', + callGasLimit: finalGasLimit, + verificationGasLimit: ethers.BigNumber.from('25000'), + preVerificationGas: ethers.BigNumber.from('75000'), + maxFeePerGas, + maxPriorityFeePerGas, + paymasterAndData: '0x003', + signature, + }; + } + + totalGasEstimated({ callGasLimit, verificationGasLimit, preVerificationGas }) { + return callGasLimit.add(verificationGasLimit).add(preVerificationGas); + } + + async send(userOp) { + if (this.sdkChainId === 696969) { + throw new Error('Transaction reverted: chain too hot'); + } + + if (userOp.signature === '0x0') { + throw new Error('Transaction reverted: invalid signature'); + } + + /** + * provide fake userOp hash by increasing nonce on each send + * and add SDK chain ID to make it more unique per userOp + */ + const userOpHash = this.nonce.add(this.sdkChainId).toHexString(); + this.nonce = this.nonce.add(1); + + return userOpHash; + } + + async installModule(moduleType, module, initData, accountAddress) { + if (!accountAddress && !defaultAccountAddressModular) { + throw new Error('No account address provided!') + } + + if (!moduleType || !module) { + throw new Error('installModule props missing') + } + + if (module === '0x222') { + throw new Error('module is already installed') + } + + return '0x123'; + } + + async uninstallModule(moduleType, module, deinitData, accountAddress) { + if (module === '0x222') { + throw new Error('module is not installed') + } + + if (!accountAddress && !defaultAccountAddressModular) { + throw new Error('No account address provided!') + } + + if (!moduleType || !module || !deinitData) { + throw new Error('uninstallModule props missing') + } + + return '0x456'; + } +} + +export const isWalletProvider = EtherspotModular.isWalletProvider; + +export const Factory = EtherspotModular.Factory; + +export const EtherspotBundler = jest.fn(); + +export default EtherspotModular; diff --git a/__tests__/components/EtherspotTokenTransferTransaction.test.js b/__tests__/components/EtherspotTokenTransferTransaction.test.js index 7f6b05e..433b758 100644 --- a/__tests__/components/EtherspotTokenTransferTransaction.test.js +++ b/__tests__/components/EtherspotTokenTransferTransaction.test.js @@ -1,4 +1,5 @@ -import { renderHook, render, waitFor, act } from '@testing-library/react'; +import { renderHook, render, waitFor } from '@testing-library/react'; +import { act } from 'react'; import { ethers } from 'ethers'; import { useEtherspotTransactions, EtherspotTransactionKit, EtherspotBatches, EtherspotBatch, EtherspotTokenTransferTransaction } from '../../src'; @@ -24,7 +25,7 @@ describe('EtherspotTokenTransferTransaction', () => { it('throws error if wrong receiver address provided', async () => { await expect(async () => { - await act(() => { + await act(() => { render( diff --git a/__tests__/hooks/useEtherspotModules.test.js b/__tests__/hooks/useEtherspotModules.test.js new file mode 100644 index 0000000..40c1ce0 --- /dev/null +++ b/__tests__/hooks/useEtherspotModules.test.js @@ -0,0 +1,95 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { ethers } from 'ethers'; + +// hooks +import { useEtherspotModules, EtherspotTransactionKit } from '../../src'; +import { MODULE_TYPE } from '@etherspot/modular-sdk/dist/sdk/common'; + +const ethersProvider = new ethers.providers.JsonRpcProvider('http://localhost:8545', 'sepolia'); // replace with your node's RPC URL +const provider = new ethers.Wallet.createRandom().connect(ethersProvider); + +const moduleAddress = '0x111'; +const initData = ethers.utils.defaultAbiCoder.encode( + ["address", "bytes"], + ['0x0000000000000000000000000000000000000001', '0x00'] + ); + const deInitData = ethers.utils.defaultAbiCoder.encode( + ["address", "bytes"], + ['0x0000000000000000000000000000000000000001', '0x00'] + ); + +describe('useEtherspotModules()', () => { + it('install one module', async () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(({ chainId }) => useEtherspotModules(chainId), { + initialProps: { chainId: 1 }, + wrapper, + }); + + + // wait for balances to be fetched for chain ID 1 + await waitFor(() => expect(result.current).not.toBeNull()); + + const installModuleMissingProps = await result.current.installModule(MODULE_TYPE.VALIDATOR) + .catch((e) => { + console.error(e); + return `${e}` + }) + expect(installModuleMissingProps).toBe('Error: Failed to install module: Error: installModule props missing'); + + const installModuleAlreadyInstalled = await result.current.installModule(MODULE_TYPE.VALIDATOR, '0x222') + .catch((e) => { + console.error(e); + return `${e}` + }) + + expect(installModuleAlreadyInstalled).toBe('Error: Failed to install module: Error: module is already installed'); + + const installOneModule = await result.current.installModule(MODULE_TYPE.VALIDATOR, moduleAddress, initData); + expect(installOneModule).toBe('0x123') + }); + + it('uninstall one module', async () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(({ chainId }) => useEtherspotModules(chainId), { + initialProps: { chainId: 1 }, + wrapper, + }); + + // wait for balances to be fetched for chain ID 1 + await waitFor(() => expect(result.current).not.toBeNull()); + + const uninstallModuleNotInstalled = await result.current.uninstallModule(MODULE_TYPE.VALIDATOR, '0x222', deInitData) + .catch((e) => { + console.error(e); + return `${e}` + }) + + expect(uninstallModuleNotInstalled).toBe('Error: Failed to uninstall module: Error: module is not installed'); + + const installOneModule = await result.current.installModule(MODULE_TYPE.VALIDATOR, moduleAddress, initData); + expect(installOneModule).toBe('0x123'); + + const uninstallModulePropsMissing = await result.current.uninstallModule(moduleAddress) + .catch((e) => { + console.error(e); + return `${e}` + }) + expect(uninstallModulePropsMissing).toBe('Error: Failed to uninstall module: Error: uninstallModule props missing'); + + + const uninstallOneModule = await result.current.uninstallModule(MODULE_TYPE.VALIDATOR, moduleAddress, deInitData); + expect(uninstallOneModule).toBe('0x456'); + + }); +}) diff --git a/__tests__/hooks/useEtherspotTransactions.test.js b/__tests__/hooks/useEtherspotTransactions.test.js index 28ae131..15a253f 100644 --- a/__tests__/hooks/useEtherspotTransactions.test.js +++ b/__tests__/hooks/useEtherspotTransactions.test.js @@ -1,4 +1,5 @@ -import { renderHook, render, act, waitFor } from '@testing-library/react'; +import { renderHook, render, waitFor } from '@testing-library/react'; +import { act } from 'react'; import { ethers } from 'ethers'; // hooks diff --git a/__tests__/hooks/useWalletAddress.test.js b/__tests__/hooks/useWalletAddress.test.js index 7d134ea..fa26839 100644 --- a/__tests__/hooks/useWalletAddress.test.js +++ b/__tests__/hooks/useWalletAddress.test.js @@ -4,6 +4,9 @@ import { Factory } from '@etherspot/prime-sdk'; // hooks import { EtherspotTransactionKit, useWalletAddress } from '../../src'; +import { + defaultAccountAddressModular, +} from '../../__mocks__/@etherspot/modular-sdk'; import { defaultAccountAddress, otherFactoryDefaultAccountAddress, @@ -17,7 +20,7 @@ const providerWalletAddress = provider.address; describe('useWalletAddress()', () => { it('returns default type wallet address if no provided type', async () => { const wrapper = ({ children }) => ( - + {children} ); @@ -36,15 +39,15 @@ describe('useWalletAddress()', () => { ); const { result, rerender } = renderHook(({ providerType }) => useWalletAddress(providerType), { - initialProps: { providerType: 'etherspot-prime' }, + initialProps: { providerType: 'etherspot' }, wrapper, }); await waitFor(() => expect(result.current).not.toBe(undefined)); - expect(result.current).toEqual(defaultAccountAddress); + expect(result.current).toEqual(defaultAccountAddressModular); rerender({ providerType: 'provider' }); - await waitFor(() => expect(result.current).not.toBe(defaultAccountAddress)); + await waitFor(() => expect(result.current).not.toBe(defaultAccountAddressModular)); expect(result.current).toEqual(providerWalletAddress); }); @@ -56,12 +59,12 @@ describe('useWalletAddress()', () => { ); const { result, rerender } = renderHook(({ providerType }) => useWalletAddress(providerType), { - initialProps: { providerType: 'etherspot-prime' }, + initialProps: { providerType: 'etherspot' }, wrapper, }); await waitFor(() => expect(result.current).not.toBe(undefined)); - expect(result.current).toEqual(defaultAccountAddress); + expect(result.current).toEqual(defaultAccountAddressModular); rerender({ providerType: 'whatever' }); await waitFor(() => expect(result.current).toBe(undefined)); @@ -79,7 +82,7 @@ describe('useWalletAddress()', () => { }); await waitFor(() => expect(resultNoAccountTemplate.current).not.toBe(undefined)); - expect(resultNoAccountTemplate.current).toEqual(defaultAccountAddress); + expect(resultNoAccountTemplate.current).toEqual(defaultAccountAddressModular); const { result: resultWithAccountTemplate } = renderHook(() => useWalletAddress(), { wrapper: createWrapper({ accountTemplate: Factory.SIMPLE_ACCOUNT }), diff --git a/example/src/App.tsx b/example/src/App.tsx index d0e3ea8..7a55607 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,3 +1,4 @@ +import { MODULE_TYPE } from '@etherspot/modular-sdk/dist/sdk/common'; import { EstimatedBatch, EtherspotBatch, @@ -8,6 +9,7 @@ import { useEtherspot, useEtherspotTransactions, useWalletAddress, + useEtherspotModules, } from '@etherspot/transaction-kit'; import TreeItem from '@mui/lab/TreeItem'; import TreeView from '@mui/lab/TreeView'; @@ -27,6 +29,8 @@ const tabs = { MULTIPLE_TRANSACTIONS: 'MULTIPLE_TRANSACTIONS', }; +const testModuleSepoliaTestnet = '0x6a00da4DEEf677Ad854B7c14F17Ed9312c2B5fDf'; + const CodePreview = ({ code }: { code: string }) => (
@@ -79,6 +83,7 @@ const App = () => {
   const { batches, estimate, send } = useEtherspotTransactions();
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const { getDataService, chainId: etherspotChainId } = useEtherspot();
+  const { installModule, uninstallModule } = useEtherspotModules();
   const [balancePerAddress, setBalancePerAddress] = useState({
     [walletAddressByName.Alice]: '',
     [walletAddressByName.Bob]: '',
@@ -154,6 +159,19 @@ const App = () => {
     setExpanded(batchesTreeViewExpandedIds);
   };
 
+  const deInitData = ethers.utils.defaultAbiCoder.encode(
+    ["address", "bytes"],
+    ['0x0000000000000000000000000000000000000001', '0x00']
+  );
+
+  const onInstallModuleClick = async () => {
+    await installModule(MODULE_TYPE.VALIDATOR, testModuleSepoliaTestnet);
+  }
+
+  const onUninstallModuleClick = async () => {
+    await uninstallModule(MODULE_TYPE.VALIDATOR, testModuleSepoliaTestnet, deInitData)
+  }
+
   useEffect(() => {
     let expired = false;
 
@@ -237,6 +255,8 @@ const App = () => {
             
           
         )}
+        
+        
       
       
          setActiveTab(id)}>
@@ -290,7 +310,7 @@ const App = () => {
                     
                       {!!estimatedBatch?.cost && (
                         
-                          Batch estimated: {ethers.utils.formatEther(estimatedBatch.cost)} MATIC
+                          Batch estimated: {ethers.utils.formatEther(estimatedBatch.cost)} ETH
                         
                       )}
                       {!!estimatedBatch?.errorMessage && (
@@ -324,7 +344,7 @@ const App = () => {
                             key={transaction.treeNodeId}
                           >
                             To: {transaction.to}
-                            Value: {transactionValue} MATIC
+                            Value: {transactionValue} ETH
                             Data: {transaction.data ?? 'None'}
                           
                         );
diff --git a/package-lock.json b/package-lock.json
index ab55231..badc4a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
 {
   "name": "@etherspot/transaction-kit",
-  "version": "0.12.0",
+  "version": "0.13.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@etherspot/transaction-kit",
-      "version": "0.12.0",
+      "version": "0.13.0",
       "license": "MIT",
       "dependencies": {
         "@etherspot/eip1271-verification-util": "0.1.2",
+        "@etherspot/modular-sdk": "1.0.1",
         "@etherspot/prime-sdk": "1.8.1",
         "buffer": "^6.0.3",
         "ethers": "^5.6.9",
@@ -2684,6 +2685,25 @@
         }
       }
     },
+    "node_modules/@etherspot/modular-sdk": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@etherspot/modular-sdk/-/modular-sdk-1.0.1.tgz",
+      "integrity": "sha512-NCAgsvJobfYmWhNs4GIzapvVLQGGdtGHvOJVMaFj1OFdIRTMoco1BqDC0H4fNoZK3zQPrGhVBGUUJSc7q1wQEA==",
+      "dependencies": {
+        "@lifi/sdk": "2.5.0",
+        "@thehubbleproject/bls": "0.5.1",
+        "@walletconnect/universal-provider": "2.10.0",
+        "buffer": "^6.0.3",
+        "class-transformer": "0.5.1",
+        "class-validator": "0.14.1",
+        "commander": "10.0.1",
+        "cross-fetch": "3.1.8",
+        "ethers": "5.7.2",
+        "prettier": "2.8.8",
+        "reflect-metadata": "0.1.14",
+        "rxjs": "6.6.7"
+      }
+    },
     "node_modules/@etherspot/prime-sdk": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/@etherspot/prime-sdk/-/prime-sdk-1.8.1.tgz",
diff --git a/package.json b/package.json
index 3a53489..ed88d16 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@etherspot/transaction-kit",
   "description": "React Etherspot Transaction Kit",
-  "version": "0.12.1",
+  "version": "0.13.0",
   "main": "dist/cjs/index.js",
   "scripts": {
     "rollup:build": "NODE_OPTIONS=--max-old-space-size=8192 rollup -c",
@@ -22,6 +22,7 @@
   "homepage": "https://github.com/etherspot/transaction-kit#readme",
   "dependencies": {
     "@etherspot/eip1271-verification-util": "0.1.2",
+    "@etherspot/modular-sdk": "1.0.1",
     "@etherspot/prime-sdk": "1.8.1",
     "buffer": "^6.0.3",
     "ethers": "^5.6.9",
@@ -61,4 +62,4 @@
   "peerDependencies": {
     "react": ">=16.13.0"
   }
-}
\ No newline at end of file
+}
diff --git a/rollup.config.js b/rollup.config.js
index 154ed48..09a8347 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -10,6 +10,7 @@ const packageJson = require('./package.json');
 const external = [
   'etherspot',
   '@etherspot/prime-sdk',
+  '@etherspot/modular-sdk',
   'ethers',
   'react',
   'buffer',
diff --git a/src/components/EtherspotTokenTransferTransaction.tsx b/src/components/EtherspotTokenTransferTransaction.tsx
index cf601f9..abc1a26 100644
--- a/src/components/EtherspotTokenTransferTransaction.tsx
+++ b/src/components/EtherspotTokenTransferTransaction.tsx
@@ -34,7 +34,7 @@ const EtherspotTokenTransferTransaction = ({
 }: EtherspotTokenTransferTransactionProps): JSX.Element => {
   const context = useContext(EtherspotBatchContext);
   const componentId = useId();
-  const senderAddress = useWalletAddress('etherspot-prime', context?.chainId);
+  const senderAddress = useWalletAddress('etherspot', context?.chainId);
 
   if (context === null) {
     throw new Error('No parent ');
diff --git a/src/components/EtherspotTransactionKit.tsx b/src/components/EtherspotTransactionKit.tsx
index 65ba599..3bd9db2 100644
--- a/src/components/EtherspotTransactionKit.tsx
+++ b/src/components/EtherspotTransactionKit.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
-import { WalletProviderLike, Factory } from '@etherspot/prime-sdk';
+import { WalletProviderLike } from '@etherspot/prime-sdk';
+import { WalletProviderLike as WalletProviderLikeModular} from '@etherspot/modular-sdk';
 
 // types
 import { AccountTemplate } from '../types/EtherspotTransactionKit';
@@ -8,9 +9,8 @@ import { AccountTemplate } from '../types/EtherspotTransactionKit';
 import EtherspotTransactionKitContextProvider from '../providers/EtherspotTransactionKitContextProvider';
 import ProviderWalletContextProvider from '../providers/ProviderWalletContextProvider';
 import EtherspotContextProvider from '../providers/EtherspotContextProvider';
-
 interface EtherspotTransactionKitProps extends React.PropsWithChildren {
-  provider: WalletProviderLike;
+  provider: WalletProviderLike | WalletProviderLikeModular;
   chainId?: number;
   accountTemplate?: AccountTemplate;
   dataApiKey?: string;
@@ -21,16 +21,38 @@ const EtherspotTransactionKit = ({
   children,
   provider,
   chainId = 1,
-  accountTemplate = Factory.ETHERSPOT,
+  accountTemplate,
   dataApiKey,
   bundlerApiKey,
-}: EtherspotTransactionKitProps) => (
+}: EtherspotTransactionKitProps) => {
+  let accountTemp: AccountTemplate;
+
+  switch (accountTemplate) {
+    case 'etherspotModular':
+      accountTemp = 'etherspotModular';
+      break;
+    case 'etherspot':
+      accountTemp = 'etherspot';
+      break;
+    case 'simpleAccount':
+      accountTemp = 'simpleAccount';
+      break;
+    case 'zeroDev':
+      accountTemp = 'zeroDev';
+      break;
+    default:
+      accountTemp = 'etherspotModular';
+      break;
+  }
+
+  return (
   
     
       
@@ -38,6 +60,7 @@ const EtherspotTransactionKit = ({
       
     
   
-);
+  )
+};
 
 export default EtherspotTransactionKit;
diff --git a/src/contexts/EtherspotContext.tsx b/src/contexts/EtherspotContext.tsx
index 947bf8c..8137213 100644
--- a/src/contexts/EtherspotContext.tsx
+++ b/src/contexts/EtherspotContext.tsx
@@ -1,12 +1,14 @@
 import { createContext } from 'react';
 import { DataUtils, PrimeSdk, WalletProviderLike } from '@etherspot/prime-sdk';
+import { ModularSdk, WalletProviderLike as WalletProviderLikeModular } from '@etherspot/modular-sdk';
 
 export interface EtherspotContextData {
   data: {
-    getSdk: (chainId?: number, forceNewInstance?: boolean) => Promise;
+    getSdk: (chainId?: number, forceNewInstance?: boolean) => Promise;
     getDataService: () => DataUtils;
-    provider: WalletProviderLike | null | undefined;
+    provider: WalletProviderLike | WalletProviderLikeModular | null | undefined;
     chainId: number;
+    isModular: boolean;
   }
 }
 
diff --git a/src/hooks/useEtherspotModules.ts b/src/hooks/useEtherspotModules.ts
new file mode 100644
index 0000000..b016504
--- /dev/null
+++ b/src/hooks/useEtherspotModules.ts
@@ -0,0 +1,98 @@
+import { useMemo } from 'react';
+
+// hooks
+import useEtherspot from './useEtherspot';
+
+// types
+import { MODULE_TYPE } from '@etherspot/modular-sdk/dist/sdk/common';
+import { ModularSdk } from '@etherspot/modular-sdk';
+
+interface IEtherspotModulesHook {
+    installModule: (moduleType: MODULE_TYPE, module: string, initData?: string, accountAddress?: string, chainId?: number) => Promise;
+    uninstallModule: (moduleType: MODULE_TYPE, module: string, deinitData: string, accountAddress?: string, chainId?: number) => Promise;
+}
+
+/**
+ * Hook to fetch account balances
+ * @param chainId {number | undefined} - Chain ID
+ * @returns {IEtherspotModulesHook} - hook method to fetch Etherspot account balances
+ */
+const useEtherspotModules = (chainId?: number): IEtherspotModulesHook => {
+  const { getSdk, chainId: etherspotChainId, isModular } = useEtherspot();
+
+  const defaultChainId = useMemo(() => {
+    if (chainId) return chainId;
+    return etherspotChainId;
+  }, [chainId, etherspotChainId]);
+
+  const installModule = async (
+    moduleType: MODULE_TYPE,
+    module: string,
+    initData?: string,
+    accountAddress?: string,
+    modulesChainId: number = defaultChainId,
+  ) => {
+    // this hook can only be used is the sdk is using the modular functionality
+    if (!isModular) {
+      throw new Error(`The  component is not using the modular functionality. Please make sure to use the modular functionality to install and uninstall modules.`);
+    }
+
+    const sdkForChainId = await getSdk(modulesChainId) as ModularSdk;
+
+    const modulesForAccount = accountAddress ?? await sdkForChainId.getCounterFactualAddress();
+    if (!modulesForAccount) {
+      throw new Error(`No account address provided!`);
+    }
+
+    try {
+        const getInstallModule = await sdkForChainId.installModule(moduleType, module, initData)
+        return getInstallModule;
+    } catch (e) {
+      console.error(
+        `Sorry, an error occurred whilst trying to install the new module`
+        + ` ${module}`
+        + ` for ${modulesForAccount}. Please try again. Error:`,
+        e,
+      );
+      throw new Error(`Failed to install module: ${e}`)
+    }
+  }
+
+  const uninstallModule = async (
+    moduleType: MODULE_TYPE,
+    module: string,
+    deinitData: string,
+    accountAddress?: string,
+    modulesChainId: number = defaultChainId,
+  ) => {
+    // this hook can only be used is the sdk is using the modular functionality
+    if (!isModular) {
+      throw new Error(`The  component is not using the modular functionality. Please make sure to use the modular functionality to install and uninstall modules.`);
+    }
+
+    const sdkForChainId = await getSdk(modulesChainId) as ModularSdk;
+
+    const modulesForAccount = accountAddress ?? await sdkForChainId.getCounterFactualAddress();
+    if (!modulesForAccount) {
+      throw new Error(`No account address provided!`);
+    }
+
+    try {
+        const getUninstallModule = await sdkForChainId.uninstallModule(moduleType, module, deinitData);
+        return getUninstallModule;
+    } catch (e) {
+      console.error(
+        `Sorry, an error occurred whilst trying to uninstall the module`
+        + ` ${module}`
+        + ` for ${modulesForAccount}. Please try again. Error:`,
+        e,
+      );
+      throw new Error(`Failed to uninstall module: ${e}`)
+    }
+  }
+
+  return { installModule, uninstallModule };
+};
+
+export default useEtherspotModules;
+
diff --git a/src/hooks/useWalletAddress.ts b/src/hooks/useWalletAddress.ts
index 02ece24..485d3bf 100644
--- a/src/hooks/useWalletAddress.ts
+++ b/src/hooks/useWalletAddress.ts
@@ -12,7 +12,7 @@ import useEtherspot from './useEtherspot';
  * @param chainId {number} - Chain ID
  * @returns {string | undefined} - wallet address by its type
  */
-const useWalletAddress = (walletType: IWalletType = 'etherspot-prime', chainId?: number): string | undefined => {
+const useWalletAddress = (walletType: IWalletType = 'etherspot', chainId?: number): string | undefined => {
   const [accountAddress, setAccountAddress] = useState<(string | undefined)>(undefined);
   const { getSdk, chainId: defaultChainId, provider } = useEtherspot();
 
@@ -25,7 +25,7 @@ const useWalletAddress = (walletType: IWalletType = 'etherspot-prime', chainId?:
     let shouldUpdate = true;
 
     const updateAccountAddress = async () => {
-      const etherspotPrimeSdk = await getSdk(walletAddressChainId);
+      const etherspotModularOrPrimeSdk = await getSdk(walletAddressChainId);
 
       let newAccountAddress;
 
@@ -35,17 +35,17 @@ const useWalletAddress = (walletType: IWalletType = 'etherspot-prime', chainId?:
          * Reference – https://github.com/etherspot/etherspot-prime-sdk/blob/master/src/sdk/sdk.ts#L31
          */
         // @ts-ignore
-        newAccountAddress = etherspotPrimeSdk?.etherspotWallet?.accountAddress;
+        newAccountAddress = etherspotModularOrPrimeSdk?.etherspotWallet?.accountAddress;
       } catch (e) {
-        console.warn(`Unable to get wallet address from SDK state for etherspot-prime type for chainId ID ${walletAddressChainId}.`, e);
+        console.warn(`Unable to get wallet address from SDK state for etherspot type for chainId ID ${walletAddressChainId}.`, e);
       }
 
       // if were unable to get wallet address from SDK state, try to get using getCounterFactualAddress
       if (!newAccountAddress) {
         try {
-          newAccountAddress = await etherspotPrimeSdk.getCounterFactualAddress();
+          newAccountAddress = await etherspotModularOrPrimeSdk.getCounterFactualAddress();
         } catch (e) {
-          console.warn(`Unable to get wallet address for etherspot-prime type for chainId ID ${walletAddressChainId}.`, e);
+          console.warn(`Unable to get wallet address for etherspot type for chainId ID ${walletAddressChainId}.`, e);
         }
       }
 
@@ -60,7 +60,7 @@ const useWalletAddress = (walletType: IWalletType = 'etherspot-prime', chainId?:
   }, [getSdk, walletAddressChainId]);
 
   return useMemo(() => {
-    if (walletType === 'etherspot-prime') {
+    if (walletType === 'etherspot') {
       return accountAddress;
     }
 
diff --git a/src/index.ts b/src/index.ts
index c087ac4..dd1df53 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,6 +21,7 @@ export { default as useEtherspotPrices } from './hooks/useEtherspotPrices';
 export { default as useEtherspotSwaps } from './hooks/useEtherspotSwaps';
 export { default as useWalletAddress } from './hooks/useWalletAddress';
 export { default as useEtherspot } from './hooks/useEtherspot';
+export { default as useEtherspotModules } from './hooks/useEtherspotModules';
 export * from './types/EtherspotTransactionKit';
 
 
diff --git a/src/providers/EtherspotContextProvider.tsx b/src/providers/EtherspotContextProvider.tsx
index 28bfed6..2c441bf 100644
--- a/src/providers/EtherspotContextProvider.tsx
+++ b/src/providers/EtherspotContextProvider.tsx
@@ -15,6 +15,7 @@ import React, {
   useMemo,
 } from 'react';
 import isEqual from 'lodash/isEqual';
+import { ModularSdk, WalletProviderLike as WalletProviderLikeModular, isWalletProvider as isWalletProviderModular, Web3WalletProvider as Web3WalletModularProvider, EtherspotBundler as EtherspotBundlerModular, Factory as ModularFactory } from '@etherspot/modular-sdk';
 
 // contexts
 import EtherspotContext from '../contexts/EtherspotContext';
@@ -28,6 +29,9 @@ let prevAccountTemplate: AccountTemplate | undefined;
 
 let dataService: DataUtils;
 
+let sdkPerChainModular: { [chainId: number]: ModularSdk | Promise } = {};
+let prevProviderModular: WalletProviderLikeModular;
+
 const EtherspotContextProvider = ({
   children,
   provider,
@@ -35,13 +39,15 @@ const EtherspotContextProvider = ({
   accountTemplate,
   dataApiKey,
   bundlerApiKey,
+  isModular,
 }: {
   children: ReactNode;
-  provider: WalletProviderLike;
+  provider: WalletProviderLike | WalletProviderLikeModular;
   chainId: number;
   accountTemplate?: AccountTemplate;
   dataApiKey?: string;
   bundlerApiKey?: string;
+  isModular: boolean;
 }) => {
   const context = useContext(EtherspotContext);
 
@@ -53,24 +59,70 @@ const EtherspotContextProvider = ({
     return () => {
       // reset on unmount
       sdkPerChain = {};
+      sdkPerChainModular = {};
     }
   }, []);
 
   const getSdk = useCallback(async (sdkChainId: number = chainId, forceNewInstance: boolean = false) => {
-    const accountTemplateOrProviderChanged = (prevProvider && !isEqual(prevProvider, provider))
+    if (isModular) {
+
+      const accountTemplateOrProviderChanged = (prevProvider && !isEqual(prevProviderModular, provider as WalletProviderLikeModular))
       || (prevAccountTemplate && prevAccountTemplate !== accountTemplate);
+      
+      if (sdkPerChainModular[sdkChainId] && !forceNewInstance && !accountTemplateOrProviderChanged) {
+        return sdkPerChainModular[sdkChainId];
+      }
 
-    if (sdkPerChain[sdkChainId] && !forceNewInstance && !accountTemplateOrProviderChanged) {
-      return sdkPerChain[sdkChainId];
-    }
+      sdkPerChainModular[sdkChainId] = (async () => {
+      let mappedProvider;
+
+      if (!isWalletProviderModular(provider as WalletProviderLikeModular)) {
+        try {
+          // @ts-ignore
+          mappedProvider = new Web3WalletModularProvider(provider as WalletProviderLikeModular);
+          await mappedProvider.refresh();
+        } catch (e) {
+          // no need to log, this is an attempt
+        }
+
+        if (!mappedProvider) {
+          throw new Error('Invalid provider!');
+        }
+      }
 
-    sdkPerChain[sdkChainId] = (async () => {
+      const etherspotModularSdk = new ModularSdk(mappedProvider as Web3WalletModularProvider ?? provider as WalletProviderLikeModular, {
+        chainId: +sdkChainId,
+        bundlerProvider: new EtherspotBundlerModular(+sdkChainId, bundlerApiKey ?? ('__ETHERSPOT_BUNDLER_API_KEY__' || undefined)),
+        factoryWallet: accountTemplate as ModularFactory,
+      });
+
+      // load the address into SDK state
+      await etherspotModularSdk.getCounterFactualAddress();
+
+      prevProviderModular = provider as WalletProviderLikeModular;
+      prevAccountTemplate = accountTemplate;
+
+      return etherspotModularSdk;
+    })();
+
+    return sdkPerChainModular[sdkChainId];
+
+    } else {
+
+      const accountTemplateOrProviderChanged = (prevProvider && !isEqual(prevProvider, provider as WalletProviderLike))
+      || (prevAccountTemplate && prevAccountTemplate !== accountTemplate);
+
+      if (sdkPerChain[sdkChainId] && !forceNewInstance && !accountTemplateOrProviderChanged) {
+        return sdkPerChain[sdkChainId];
+      }
+
+      sdkPerChain[sdkChainId] = (async () => {
       let mappedProvider;
 
-      if (!isWalletProvider(provider)) {
+      if (!isWalletProvider(provider as WalletProviderLike)) {
         try {
           // @ts-ignore
-          mappedProvider = new Web3WalletProvider(provider);
+          mappedProvider = new Web3WalletProvider(provider as WalletProviderLike);
           await mappedProvider.refresh();
         } catch (e) {
           // no need to log, this is an attempt
@@ -81,7 +133,7 @@ const EtherspotContextProvider = ({
         }
       }
 
-      const etherspotPrimeSdk = new PrimeSdk(mappedProvider ?? provider, {
+      const etherspotPrimeSdk = new PrimeSdk(mappedProvider as Web3WalletProvider ?? provider as WalletProviderLike, {
         chainId: +sdkChainId,
         bundlerProvider: new EtherspotBundler(+sdkChainId, bundlerApiKey ?? ('__ETHERSPOT_BUNDLER_API_KEY__' || undefined)),
         factoryWallet: accountTemplate as Factory,
@@ -90,13 +142,14 @@ const EtherspotContextProvider = ({
       // load the address into SDK state
       await etherspotPrimeSdk.getCounterFactualAddress();
 
-      prevProvider = provider;
+      prevProvider = provider as WalletProviderLike;
       prevAccountTemplate = accountTemplate;
 
       return etherspotPrimeSdk;
     })();
 
     return sdkPerChain[sdkChainId];
+    }
   }, [provider, chainId, accountTemplate, bundlerApiKey]);
 
   const getDataService = useCallback(() => {
@@ -110,11 +163,13 @@ const EtherspotContextProvider = ({
     getDataService,
     provider,
     chainId,
+    isModular,
   }), [
     getSdk,
     getDataService,
     provider,
     chainId,
+    isModular,
   ]);
 
   return (
diff --git a/src/providers/EtherspotTransactionKitContextProvider.tsx b/src/providers/EtherspotTransactionKitContextProvider.tsx
index 8950a12..b1012c7 100644
--- a/src/providers/EtherspotTransactionKitContextProvider.tsx
+++ b/src/providers/EtherspotTransactionKitContextProvider.tsx
@@ -65,17 +65,17 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti
         }
 
         // force new instance for each batch to not mix up user ops added to SDK state batch
-        const etherspotPrimeSdk = await getSdk(batchChainId, true);
+        const etherspotModularOrPrimeSdk = await getSdk(batchChainId, true);
 
         try {
-          if (!forSending) await etherspotPrimeSdk.clearUserOpsFromBatch();
+          if (!forSending) await etherspotModularOrPrimeSdk.clearUserOpsFromBatch();
 
           await Promise.all(batch.transactions.map(async ({ to, value, data }) => {
-            await etherspotPrimeSdk.addUserOpsToBatch(({ to, value, data }));
+            await etherspotModularOrPrimeSdk.addUserOpsToBatch(({ to, value, data }));
           }));
 
-          const userOp = await etherspotPrimeSdk.estimate({ paymasterDetails: groupedBatch.paymaster });
-          const totalGas = await etherspotPrimeSdk.totalGasEstimated(userOp);
+          const userOp = await etherspotModularOrPrimeSdk.estimate({ paymasterDetails: groupedBatch.paymaster });
+          const totalGas = await etherspotModularOrPrimeSdk.totalGasEstimated(userOp);
           estimatedBatches.push({ ...batch, cost: totalGas.mul(userOp.maxFeePerGas as BigNumber), userOp });
         } catch (e) {
           const errorMessage = parseEtherspotErrorMessage(e, 'Failed to estimate!');
@@ -110,9 +110,9 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti
     }) => Promise.all(batches.map(async (batch) => {
       const batchChainId = batch.chainId ?? chainId;
 
-      const etherspotPrimeSdk = await getSdk(batchChainId);
+      const etherspotModularOrPrimeSdk = await getSdk(batchChainId);
 
-      await etherspotPrimeSdk.clearUserOpsFromBatch();
+      await etherspotModularOrPrimeSdk.clearUserOpsFromBatch();
     }))));
 
     const estimated = await estimate(batchesIds, true);
@@ -130,7 +130,7 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti
           continue;
         }
 
-        const etherspotPrimeSdk = await getSdk(batchChainId);
+        const etherspotModularOrPrimeSdk = await getSdk(batchChainId);
 
         if (!estimatedBatch.userOp) {
           sentBatches.push({ ...estimatedBatch, errorMessage: 'Failed to get estimated UserOp!' });
@@ -138,7 +138,7 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti
         }
 
         try {
-          const userOpHash = await etherspotPrimeSdk.send(estimatedBatch.userOp);
+          const userOpHash = await etherspotModularOrPrimeSdk.send(estimatedBatch.userOp);
           sentBatches.push({ ...estimatedBatch, userOpHash });
         } catch (e) {
           const errorMessage = parseEtherspotErrorMessage(e, 'Failed to send!');
diff --git a/src/types/EtherspotTransactionKit.ts b/src/types/EtherspotTransactionKit.ts
index ff85e85..b1260d8 100644
--- a/src/types/EtherspotTransactionKit.ts
+++ b/src/types/EtherspotTransactionKit.ts
@@ -4,6 +4,7 @@ import { Route } from '@lifi/types';
 import { PaymasterApi } from '@etherspot/prime-sdk';
 import { ExchangeOffer } from '@etherspot/prime-sdk/dist/sdk/data';
 import { TransactionStatuses } from '@etherspot/prime-sdk/dist/sdk/data/constants';
+import { PaymasterApi as PaymasterApiModular } from '@etherspot/modular-sdk';
 
 export interface ITransaction {
   id?: string;
@@ -43,7 +44,7 @@ export interface IBatches {
   onEstimated?: (estimated: EstimatedBatch[]) => void;
   onSent?: (sent: SentBatch[]) => void;
   skip?: boolean;
-  paymaster?: PaymasterApi,
+  paymaster?: PaymasterApi | PaymasterApiModular,
 }
 
 export type IEstimatedBatches = IBatches & {
@@ -99,7 +100,7 @@ export interface IProviderWalletTransactionSent {
   errorMessage?: string;
 }
 
-export type IWalletType = 'provider' | 'etherspot-prime';
+export type IWalletType = 'provider' | 'etherspot';
 
 type EtherspotPromiseOrValue = T | Promise;
 
@@ -182,4 +183,4 @@ export interface UserOpTransaction {
   nftTransfers?: EtherspotNftTransfersEntity[];
 }
 
-export type AccountTemplate = 'etherspot' | 'zeroDev' | 'simpleAccount';
+export type AccountTemplate = 'etherspot' | 'etherspotModular' | 'zeroDev' | 'simpleAccount';