diff --git a/CHANGELOG.md b/CHANGELOG.md index 833417f..2ff9622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.2] - 2025-07-24 + +### Added Changes + +- Added `getEtherspotProvider` to access directly the Etherspot Provider. +- Added `getTransactionHash` to get a transaction hash with a userOp Hash and chain id. +- Update of the `modular-sdk` version to 6.1.1. + ## [2.0.1] - 2025-07-17 ### Documentation diff --git a/__tests__/EtherspotTransactionKit.test.ts b/__tests__/EtherspotTransactionKit.test.ts index e58bb41..137e854 100644 --- a/__tests__/EtherspotTransactionKit.test.ts +++ b/__tests__/EtherspotTransactionKit.test.ts @@ -104,7 +104,7 @@ describe('EtherspotTransactionKit', () => { describe('Constructor', () => { it('should initialize with config', () => { expect(EtherspotProvider).toHaveBeenCalledWith(mockConfig); - expect(transactionKit.getProvider()).toBe(mockProvider); + expect(transactionKit.getEtherspotProvider()).toBe(mockProvider); }); it('should set debug mode from config', () => { @@ -796,7 +796,7 @@ describe('EtherspotTransactionKit', () => { describe('getProvider', () => { it('should return etherspot provider', () => { - const provider = transactionKit.getProvider(); + const provider = transactionKit.getEtherspotProvider(); expect(provider).toBe(mockProvider); }); @@ -963,7 +963,9 @@ describe('Batch operations', () => { beforeEach(() => { transactionKit = new EtherspotTransactionKit(mockConfig); // Re-mock SDK for each test - transactionKit.getProvider().getSdk = jest.fn().mockResolvedValue(mockSdk); + transactionKit.getEtherspotProvider().getSdk = jest + .fn() + .mockResolvedValue(mockSdk); mockSdk.clearUserOpsFromBatch.mockReset(); mockSdk.addUserOpsToBatch.mockReset(); mockSdk.estimate.mockReset(); @@ -1096,7 +1098,7 @@ describe('Batch operations', () => { it('should throw if no provider in estimateBatches', async () => { // @ts-ignore - transactionKit.getProvider().getProvider.mockReturnValue(null); + transactionKit.getProvider = jest.fn().mockReturnValue(null); await expect(transactionKit.estimateBatches()).rejects.toThrow( 'estimateBatches(): No Web3 provider available. This is a critical configuration error.' ); @@ -1104,9 +1106,87 @@ describe('Batch operations', () => { it('should throw if no provider in sendBatches', async () => { // @ts-ignore - transactionKit.getProvider().getProvider.mockReturnValue(null); + transactionKit.getProvider = jest.fn().mockReturnValue(null); await expect(transactionKit.sendBatches()).rejects.toThrow( 'sendBatches(): No Web3 provider available. This is a critical configuration error.' ); }); }); + +describe('getTransactionHash', () => { + let transactionKit: EtherspotTransactionKit; + let mockSdk: jest.Mocked; + + const userOpHash = '0xuserOpHash'; + const txChainId = 1; + + beforeEach(() => { + transactionKit = new EtherspotTransactionKit({ + provider: {} as any, + chainId: txChainId, + bundlerApiKey: 'test-bundler-key', + debugMode: false, + }); + mockSdk = { + getUserOpReceipt: jest.fn(), + } as any; + // Override getSdk to always return our mockSdk + transactionKit.getSdk = jest.fn().mockResolvedValue(mockSdk); + }); + + it('returns transaction hash when available immediately', async () => { + (mockSdk.getUserOpReceipt as jest.Mock).mockResolvedValue('0xhash'); + const result = await transactionKit.getTransactionHash( + userOpHash, + txChainId, + 1000, + 10 + ); + expect(result).toBe('0xhash'); + expect(mockSdk.getUserOpReceipt).toHaveBeenCalledWith(userOpHash); + }); + + it('returns transaction hash after several polls', async () => { + let callCount = 0; + (mockSdk.getUserOpReceipt as jest.Mock).mockImplementation(() => { + callCount++; + return callCount === 3 ? '0xhash' : null; + }); + const result = await transactionKit.getTransactionHash( + userOpHash, + txChainId, + 100, + 1 + ); + expect(result).toBe('0xhash'); + expect(callCount).toBeGreaterThan(1); + }); + + it('returns null on timeout', async () => { + (mockSdk.getUserOpReceipt as jest.Mock).mockResolvedValue(null); + const result = await transactionKit.getTransactionHash( + userOpHash, + txChainId, + 50, + 10 + ); + expect(result).toBeNull(); + }); + + it('handles SDK errors gracefully and continues polling', async () => { + let callCount = 0; + (mockSdk.getUserOpReceipt as jest.Mock).mockImplementation(() => { + callCount++; + if (callCount < 2) throw new Error('Temporary error'); + return callCount === 3 ? '0xhash' : null; + }); + const result = await transactionKit.getTransactionHash( + userOpHash, + txChainId, + 100, + 1 + ); + expect(result).toBe('0xhash'); + expect(callCount).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/example/src/App.tsx b/example/src/App.tsx index 9a1fb15..4d506f6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -9,6 +9,10 @@ import { createWalletClient, custom } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { polygon } from 'viem/chains'; +function bigIntReplacer(key: string, value: any) { + return typeof value === 'bigint' ? value.toString() : value; +} + const account = privateKeyToAccount( `0x${process.env.REACT_APP_DEMO_WALLET_PK}` as `0x${string}` ); @@ -133,7 +137,9 @@ const App = () => { transactionName: 'tx1', }) as INamedTransaction; const result = await named.estimate(); - logAndUpdateState('Estimate single tx1: ' + JSON.stringify(result)); + logAndUpdateState( + 'Estimate single tx1: ' + JSON.stringify(result, bigIntReplacer) + ); } catch (e) { logAndUpdateState('Error: ' + (e as Error).message); } @@ -144,7 +150,9 @@ const App = () => { action: async (logAndUpdateState: (msg: string) => void) => { try { const result = await kit.estimateBatches(); - logAndUpdateState('Estimate batches: ' + JSON.stringify(result)); + logAndUpdateState( + 'Estimate batches: ' + JSON.stringify(result, bigIntReplacer) + ); } catch (e) { logAndUpdateState('Error: ' + (e as Error).message); } @@ -161,12 +169,13 @@ const App = () => { if (result.isSentSuccessfully) { logAndUpdateState( 'Send single tx1: ' + - JSON.stringify(result) + + JSON.stringify(result, bigIntReplacer) + ' (tx1 removed from state)' ); } else { logAndUpdateState( - 'Send single tx1 failed: ' + JSON.stringify(result) + 'Send single tx1 failed: ' + + JSON.stringify(result, bigIntReplacer) ); } } catch (e) { @@ -179,7 +188,7 @@ const App = () => { action: async (logAndUpdateState: (msg: string) => void) => { try { const result = await kit.sendBatches(); - let msg = 'Send batches: ' + JSON.stringify(result); + let msg = 'Send batches: ' + JSON.stringify(result, bigIntReplacer); // Check which batches were removed if (result && result.batches) { const removedBatches = Object.entries(result.batches) @@ -476,7 +485,9 @@ const App = () => { const result = await kit.estimateBatches({ onlyBatchNames: ['batchD'], }); - logAndUpdateState('Estimate empty batchD: ' + JSON.stringify(result)); + logAndUpdateState( + 'Estimate empty batchD: ' + JSON.stringify(result, bigIntReplacer) + ); } catch (e) { logAndUpdateState('Error: ' + (e as Error).message); } @@ -487,7 +498,9 @@ const App = () => { action: async (logAndUpdateState: (msg: string) => void) => { try { const result = await kit.sendBatches({ onlyBatchNames: ['batchD'] }); - logAndUpdateState('Send empty batchD: ' + JSON.stringify(result)); + logAndUpdateState( + 'Send empty batchD: ' + JSON.stringify(result, bigIntReplacer) + ); } catch (e) { logAndUpdateState('Error: ' + (e as Error).message); } @@ -662,7 +675,8 @@ const App = () => { onlyBatchNames: ['doesnotexistbatch'], }); logAndUpdateState( - 'EstimateBatches with nonexistent batch: ' + JSON.stringify(result) + 'EstimateBatches with nonexistent batch: ' + + JSON.stringify(result, bigIntReplacer) ); } catch (e) { logAndUpdateState('Error: ' + (e as Error).message); @@ -677,7 +691,8 @@ const App = () => { onlyBatchNames: ['doesnotexistbatch'], }); let msg = - 'SendBatches with nonexistent batch: ' + JSON.stringify(result); + 'SendBatches with nonexistent batch: ' + + JSON.stringify(result, bigIntReplacer); if ( result && result.batches && @@ -698,7 +713,7 @@ const App = () => { const logAndUpdateState = (msg: string) => { setLogs((prev) => [ msg, - 'State: ' + JSON.stringify(kit.getState(), null, 2), + 'State: ' + JSON.stringify(kit.getState(), bigIntReplacer, 2), ...prev, ]); setCurrentState(kit.getState()); @@ -742,7 +757,7 @@ const App = () => {

Current State

-          {JSON.stringify(currentState, null, 2)}
+          {JSON.stringify(currentState, bigIntReplacer, 2)}
         
diff --git a/lib/EtherspotUtils.ts b/lib/EtherspotUtils.ts index e889bb7..fa3873f 100644 --- a/lib/EtherspotUtils.ts +++ b/lib/EtherspotUtils.ts @@ -82,7 +82,7 @@ export class EtherspotUtils { '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', ]; return zeroAddresses.some((zeroAddress) => - this.addressesEqual(zeroAddress, address) + EtherspotUtils.addressesEqual(zeroAddress, address) ); } diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index f9d23c1..b8ed22a 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -1,4 +1,4 @@ -import { ModularSdk } from '@etherspot/modular-sdk'; +import { ModularSdk, WalletProviderLike } from '@etherspot/modular-sdk'; import { isAddress } from 'viem'; // interfaces @@ -589,7 +589,7 @@ export class EtherspotTransactionKit implements IInitial { } log('estimate(): Getting provider...', undefined, this.debugMode); - const provider = this.etherspotProvider.getProvider(); + const provider = this.getProvider(); log('estimate(): Got provider:', provider, this.debugMode); if (!provider) { log( @@ -817,7 +817,7 @@ export class EtherspotTransactionKit implements IInitial { } log('send(): Getting provider...', undefined, this.debugMode); - const provider = this.etherspotProvider.getProvider(); + const provider = this.getProvider(); log('send(): Got provider:', provider, this.debugMode); if (!provider) { log( @@ -1086,7 +1086,7 @@ export class EtherspotTransactionKit implements IInitial { } // Get the provider - const provider = this.etherspotProvider.getProvider(); + const provider = this.getProvider(); // Validation: if there is no provider, return error if (!provider) { log( @@ -1357,7 +1357,7 @@ export class EtherspotTransactionKit implements IInitial { } // Get the provider - const provider = this.etherspotProvider.getProvider(); + const provider = this.getProvider(); // Validation: if there is no provider, return error if (!provider) { log( @@ -1753,20 +1753,24 @@ export class EtherspotTransactionKit implements IInitial { } /** - * Returns the underlying EtherspotProvider instance used by this kit. + * Returns the underlying raw provider used by this kit (WalletProviderLike). * - * @returns The EtherspotProvider instance. + * @returns The WalletProviderLike instance. * * @remarks - * - Useful for advanced operations or direct provider access. - * - Does not mutate any internal state. + * - This is the provider you should use in your app for web3 interactions. + * - For advanced operations, use getEtherspotProvider(). */ - getProvider(): EtherspotProvider { - log( - 'getProvider(): Returning provider', - this.etherspotProvider, - this.debugMode - ); + getProvider(): WalletProviderLike { + return this.etherspotProvider.getProvider(); + } + + /** + * Returns the EtherspotProvider instance for advanced use. + * + * @returns The EtherspotProvider instance. + */ + getEtherspotProvider(): EtherspotProvider { return this.etherspotProvider; } @@ -1791,6 +1795,47 @@ export class EtherspotTransactionKit implements IInitial { return sdk; } + /** + * Polls for the transaction hash using a user operation hash and chain ID. + * + * @param userOpHash - The user operation hash to query. + * @param txChainId - The chain ID to use for the SDK. + * @param timeout - (Optional) Timeout in ms (default: 60000). + * @param retryInterval - (Optional) Polling interval in ms (default: 2000). + * @returns The transaction hash as a string, or null if not found in time. + */ + public async getTransactionHash( + userOpHash: string, + txChainId: number, + timeout: number = 60 * 1000, + retryInterval: number = 2000 + ): Promise { + const etherspotModulaSdk = await this.getSdk(txChainId); + + let transactionHash: string | null = null; + const timeoutTotal = Date.now() + timeout; + + while (!transactionHash && Date.now() < timeoutTotal) { + await new Promise((resolve) => setTimeout(resolve, retryInterval)); + try { + transactionHash = await etherspotModulaSdk.getUserOpReceipt(userOpHash); + } catch (error) { + console.error( + 'Error fetching transaction hash. Please check if the transaction has gone through, or try to send the transaction again:', + error + ); + } + } + + if (!transactionHash) { + console.warn( + 'Failed to get the transaction hash within time limit. Please try again' + ); + } + + return transactionHash; + } + /** * Resets all internal state, clearing all transactions, batches, and caches. * diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 278a5de..b7cbc76 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -37,9 +37,16 @@ export interface IInitial { getWalletAddress(chainId?: number): Promise; getState(): IInstance; setDebugMode(enabled: boolean): void; - getProvider(): EtherspotProvider; + getProvider(): WalletProviderLike; + getEtherspotProvider(): EtherspotProvider; getSdk(chainId?: number, forceNewInstance?: boolean): Promise; reset(): void; + getTransactionHash( + userOpHash: string, + txChainId: number, + timeout?: number, + retryInterval?: number + ): Promise; } export interface ITransaction { diff --git a/package-lock.json b/package-lock.json index 116b4fc..d9a3688 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", - "@etherspot/modular-sdk": "6.1.0", + "@etherspot/modular-sdk": "^6.1.1", "buffer": "6.0.3", "lodash": "4.17.21", "viem": "2.21.54" @@ -1915,9 +1915,9 @@ } }, "node_modules/@etherspot/modular-sdk": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@etherspot/modular-sdk/-/modular-sdk-6.1.0.tgz", - "integrity": "sha512-3SFM9V9gWxSNfelLlcUBs4MpdeF6kdY6L2VknFxEezhHx8/3Qya9viv3Dfr5pWgjbS2mXrkhjZw3VuAw25yDeQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@etherspot/modular-sdk/-/modular-sdk-6.1.1.tgz", + "integrity": "sha512-dueRoAYtXz+azYci6x/U2SJDTUqAI5fjuZA4ex7XaiCHxHjwCeo2is5ggUZXITTAC3UbsnjtZ8TKaYMeBzmXYw==", "license": "MIT", "dependencies": { "@lifi/sdk": "2.5.0", diff --git a/package.json b/package.json index 98bd4ba..2691ec6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "homepage": "https://github.com/etherspot/transaction-kit#readme", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", - "@etherspot/modular-sdk": "6.1.0", + "@etherspot/modular-sdk": "6.1.1", "buffer": "6.0.3", "lodash": "4.17.21", "viem": "2.21.54" @@ -65,6 +65,5 @@ "rollup": "3.29.5", "rollup-plugin-dts": "6.1.1", "typescript": "5.7.3" - }, - "peerDependencies": {} + } }