From 893bc1e66c16c7db10a1b5754906c7773863dcd7 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 20 Nov 2025 10:57:55 +0000 Subject: [PATCH 1/7] fix: Implement custom 6492 compatible signMessage --- __tests__/EtherspotTransactionKit.test.ts | 224 ++++++++++++ example/README.md | 148 ++++++-- example/package-lock.json | 196 ++++++++++- example/package.json | 8 +- example/src/signMessage-example.ts | 402 ++++++++++++++++++++++ lib/TransactionKit.ts | 172 ++++++++- lib/interfaces/index.ts | 4 + 7 files changed, 1113 insertions(+), 41 deletions(-) create mode 100644 example/src/signMessage-example.ts diff --git a/__tests__/EtherspotTransactionKit.test.ts b/__tests__/EtherspotTransactionKit.test.ts index 9c8ad2a..fa05d4a 100644 --- a/__tests__/EtherspotTransactionKit.test.ts +++ b/__tests__/EtherspotTransactionKit.test.ts @@ -33,6 +33,18 @@ jest.mock('viem', () => { ...actual, isAddress: jest.fn(), parseEther: jest.fn(), + toHex: jest.fn((val) => `0x${val.toString(16)}`), + toRlp: jest.fn((val) => `0x${Buffer.from(JSON.stringify(val)).toString('hex')}`), + zeroAddress: '0x0000000000000000000000000000000000000000', + }; +}); + +jest.mock('viem/accounts', () => { + const actual = jest.requireActual('viem/accounts'); + const mockSignMessage = jest.fn().mockResolvedValue('0x' + '1'.repeat(130)); + return { + ...actual, + signMessage: mockSignMessage, }; }); @@ -2335,6 +2347,218 @@ describe('DelegatedEoa Mode Integration', () => { }); }); + describe('signMessage', () => { + const { signMessage: viemSignMessage } = require('viem/accounts'); + const { toRlp } = require('viem'); + + beforeEach(() => { + jest.clearAllMocks(); + (viemSignMessage as jest.Mock).mockClear(); + (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + (toRlp as jest.Mock).mockClear(); + }); + + it('should create EIP-6492 signature when EOA is not yet installed', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), + } as any; + const mockPublicClient = { + getCode: jest + .fn() + .mockResolvedValueOnce('0x') // For isDelegateSmartAccountToEoa check + .mockResolvedValue('0x'), // For other calls + getTransactionCount: jest.fn().mockResolvedValue(5), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + (viemSignMessage as jest.Mock).mockResolvedValue( + '0x' + '1'.repeat(130) // 65 bytes signature (r + s + v) + ); + (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); + + const result = await transactionKit.signMessage('Hello, World!', 1); + + // Should start with EIP-6492 magic prefix + expect(result).toMatch(/^0x6492/); + expect(result.length).toBeGreaterThan(140); // At least magic prefix + signature + deployment data + expect(viemSignMessage).toHaveBeenCalledWith({ + account: mockOwner, + message: 'Hello, World!', + }); + expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); + }); + + it('should create EIP-6492 signature when EOA is already installed', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), + } as any; + const mockPublicClient = { + getCode: jest + .fn() + .mockResolvedValueOnce('0xef01001234') // Already installed + .mockResolvedValue('0xef01001234'), + getTransactionCount: jest.fn().mockResolvedValue(5), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + (viemSignMessage as jest.Mock).mockResolvedValue( + '0x' + '2'.repeat(130) // 65 bytes signature + ); + (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); + + const result = await transactionKit.signMessage('Test message', 1); + + expect(result).toMatch(/^0x6492/); + expect(mockViemSignMessage).toHaveBeenCalled(); + expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); + }); + + it('should throw error for non-delegatedEoa wallet mode', async () => { + mockProvider.getWalletMode.mockReturnValue('modular'); + + await expect( + transactionKit.signMessage('Test message', 1) + ).rejects.toThrow( + "signMessage() is only available in 'delegatedEoa' wallet mode" + ); + }); + + it('should handle authorization creation failure', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockRejectedValue(new Error('Authorization failed')), + } as any; + const mockPublicClient = { + getCode: jest.fn().mockResolvedValue('0x'), // Not installed + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + + // This will fail when trying to delegate + await expect( + transactionKit.signMessage('Test message', 1) + ).rejects.toThrow(); + }); + + it('should handle message signing failure', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), + } as any; + const mockPublicClient = { + getCode: jest.fn().mockResolvedValue('0x'), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + (viemSignMessage as jest.Mock).mockRejectedValue(new Error('Signing failed')); + + await expect( + transactionKit.signMessage('Test message', 1) + ).rejects.toThrow('Signing failed'); + }); + + it('should use default chainId when not provided', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), + } as any; + const mockPublicClient = { + getCode: jest.fn().mockResolvedValue('0x'), + getTransactionCount: jest.fn().mockResolvedValue(5), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + mockProvider.getChainId.mockReturnValue(1); + (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); + + await transactionKit.signMessage('Test message'); + + expect(mockProvider.getOwnerAccount).toHaveBeenCalledWith(1); + expect(mockProvider.getBundlerClient).toHaveBeenCalledWith(1); + }); + + it('should handle hex string messages', async () => { + const mockOwner = { + address: '0xowner123456789012345678901234567890', + } as any; + const mockBundlerClient = { + signAuthorization: jest + .fn() + .mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), + } as any; + const mockPublicClient = { + getCode: jest.fn().mockResolvedValue('0x'), + getTransactionCount: jest.fn().mockResolvedValue(5), + } as any; + + mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); + mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); + mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); + + const result = await transactionKit.signMessage( + '0x48656c6c6f' as `0x${string}`, + 1 + ); + + expect(result).toMatch(/^0x6492/); + expect(viemSignMessage).toHaveBeenCalledWith({ + account: mockOwner, + message: '0x48656c6c6f', + }); + }); + }); + describe('estimate with delegatedEoa mode', () => { it('should estimate transaction in delegatedEoa mode when EOA is designated', async () => { const mockAccount = { diff --git a/example/README.md b/example/README.md index b87cb00..181240e 100644 --- a/example/README.md +++ b/example/README.md @@ -1,46 +1,142 @@ -# Getting Started with Create React App +# signMessage Example + +This example demonstrates how to use the `signMessage` function with EIP-6492 format for EIP-7702 wallets. + +## Features -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +The example includes four scenarios: + +1. **Example 1: Non-Delegated EOA** - Signing messages before EIP-7702 delegation +2. **Example 2: Delegated EOA** - Signing messages after EIP-7702 delegation +3. **Example 3: Compare Signatures** - Comparing signatures from different delegation states +4. **Example 4: Different Message Types** - Signing various message formats (text, hex, special chars, long messages) -## Available Scripts +## Prerequisites -In the project directory, you can run: +1. Node.js 18+ installed +2. TypeScript and ts-node installed +3. A private key for testing (use a test account, never use your main account's private key) +4. A bundler API key (optional, but recommended for delegation) -### `npm start` +## Setup -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. **Install dependencies** (from example directory): + ```bash + npm install + ``` -The page will reload if you make edits.\ -You will also see any lint errors in the console. +2. **Set up environment variables**: + + Create a `.env` file in the `example` directory with: + ```env + REACT_APP_DEMO_WALLET_PK=0x...your-private-key... + REACT_APP_BUNDLER_URL=https://api.etherspot.io/v2 + REACT_APP_ETHERSPOT_BUNDLER_API_KEY=your-api-key + REACT_APP_CHAIN_ID=11155111 + ``` + + Or use these alternative env var names: + ```env + PRIVATE_KEY=0x...your-private-key... + BUNDLER_URL=https://api.etherspot.io/v2 + BUNDLER_API_KEY=your-api-key + CHAIN_ID=11155111 + ``` -### `npm test` +**⚠️ WARNING**: Never commit your `.env` file or share your private key! -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +## Running the Example -### `npm run build` +From the `example` directory: -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +```bash +# Using ts-node +npx ts-node src/signMessage-example.ts -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +# Or if ts-node is installed globally +ts-node src/signMessage-example.ts -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +# Or compile and run +tsc src/signMessage-example.ts --esModuleInterop --module commonjs --target es2020 --outDir dist +node dist/signMessage-example.js +``` -### `npm run eject` +## Expected Output -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +The example will output: -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +- Wallet address (EOA) +- Delegation status +- EIP-6492 signature format verification +- Signature components breakdown +- Full signature hex string -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +Example output: +``` +============================================================ +Example 1: Sign Message with Non-Delegated EOA +============================================================ -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +EOA Address: 0x1234... +Is Delegated: No +Message to Sign: Hello, World! This is a test message. +EIP-6492 Format Valid: ✓ Yes +Magic Prefix: 0x6492 +Signature: 0x1234... +Signature Length: 132 chars +Deployment Data Length: 200+ chars +Total Signature Length: 400+ chars +``` -## Learn More +## Understanding EIP-6492 Signatures -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +EIP-6492 signatures have the following format: -To learn React, check out the [React documentation](https://reactjs.org/). +``` +0x6492 +``` + +Where: +- `0x6492` - Magic prefix (2 bytes) +- `` - 65-byte signature (r, s, v) from EIP-191 personal_sign +- `` - RLP-encoded transaction data for EIP-7702 authorization + +## Use Cases + +1. **Pre-deployment Signatures**: Sign messages before the smart account is activated +2. **Cross-chain Compatibility**: Signatures work across different chains +3. **Future-proof Signatures**: Signatures remain valid even after delegation changes + +## Troubleshooting + +### Error: "signMessage() is only available in 'delegatedEoa' wallet mode" +- Make sure `walletMode: 'delegatedEoa'` is set in the configuration + +### Error: "Failed to create authorization" +- Check your bundler URL and API key +- Ensure the chain ID is correct +- Verify network connectivity + +### Error: "Invalid private key" +- Ensure the private key starts with `0x` +- Check that it's a valid 64-character hex string + +## Notes + +- This example uses Sepolia testnet by default +- Signatures are deterministic for the same message and state +- Deployment data may change between delegation states +- Always use test accounts for experimentation + +## Security + +⚠️ **IMPORTANT SECURITY NOTES**: + +1. Never use your main account's private key +2. Never commit `.env` files to version control +3. Use testnets for development and testing +4. Keep your private keys secure and never share them + +## License + +Same as the main project license. diff --git a/example/package-lock.json b/example/package-lock.json index a2f1f14..558b718 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -23,13 +23,17 @@ "react-icons": "^4.7.1", "react-jsx-parser": "^1.29.0", "react-scripts": "5.0.1", - "react-test-renderer": "18.2.0", - "typescript": "^4.9.5" + "react-test-renderer": "18.2.0" + }, + "devDependencies": { + "dotenv": "^17.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" } }, "..": { "name": "@etherspot/transaction-kit", - "version": "2.1.1", + "version": "2.1.3", "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", @@ -2145,6 +2149,30 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/normalize.css": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", @@ -4717,6 +4745,34 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -7279,6 +7335,13 @@ "node": ">=10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -7963,6 +8026,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -8121,12 +8194,16 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -12771,6 +12848,13 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -15597,6 +15681,15 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, "node_modules/react-shallow-renderer": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", @@ -17827,6 +17920,70 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -18017,16 +18174,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -18233,6 +18390,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -19176,6 +19340,16 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/example/package.json b/example/package.json index cb9beb8..2c27f54 100644 --- a/example/package.json +++ b/example/package.json @@ -14,12 +14,11 @@ "@types/react-dom": "^18.0.10", "ethers": "^5.7.2", "react": "18.2.0", - "react-test-renderer": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.7.1", "react-jsx-parser": "^1.29.0", "react-scripts": "5.0.1", - "typescript": "^4.9.5" + "react-test-renderer": "18.2.0" }, "scripts": { "start": "react-scripts start", @@ -44,5 +43,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "dotenv": "^17.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" } } diff --git a/example/src/signMessage-example.ts b/example/src/signMessage-example.ts new file mode 100644 index 0000000..ab6edb5 --- /dev/null +++ b/example/src/signMessage-example.ts @@ -0,0 +1,402 @@ +/** + * Example: Demonstrating signMessage with EIP-6492 for EIP-7702 Wallets + * + * This example shows how to use the signMessage function with: + * 1. Non-delegated EOA (before EIP-7702 delegation) + * 2. Delegated EOA (after EIP-7702 delegation) + * + * Prerequisites: + * - Set up environment variables (see .env.example) + * - Install dependencies: npm install + * - Run: npx ts-node src/signMessage-example.ts + */ + +import { TransactionKit } from '@etherspot/transaction-kit'; +import { parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; +import * as dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const PRIVATE_KEY = process.env.REACT_APP_DEMO_WALLET_PK || process.env.PRIVATE_KEY || '0x' + '0'.repeat(64); +const BUNDLER_URL = process.env.REACT_APP_BUNDLER_URL || process.env.BUNDLER_URL || 'https://api.etherspot.io/v2'; +const BUNDLER_API_KEY = process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY || process.env.BUNDLER_API_KEY || ''; +const CHAIN_ID = parseInt(process.env.REACT_APP_CHAIN_ID || process.env.CHAIN_ID || '11155111'); // Sepolia + +// Colors for console output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + red: '\x1b[31m', +}; + +function log(message: string, color: keyof typeof colors = 'reset') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logSection(title: string) { + console.log('\n' + '='.repeat(60)); + log(title, 'bright'); + console.log('='.repeat(60) + '\n'); +} + +function logInfo(label: string, value: string | number | boolean) { + log(`${label}:`, 'cyan'); + console.log(` ${value}\n`); +} + +/** + * Verify EIP-6492 signature format + */ +function verifyEIP6492Format(signature: string): boolean { + if (!signature.startsWith('0x6492')) { + return false; + } + if (signature.length < 140) { + // Minimum: 0x6492 (4) + signature (130) + some deployment data + return false; + } + return true; +} + +/** + * Extract signature components from EIP-6492 format + */ +function extractSignatureComponents(eip6492Signature: string) { + const withoutPrefix = eip6492Signature.slice(2); // Remove '0x' + const magicPrefix = withoutPrefix.slice(0, 4); // '6492' + const signature = '0x' + withoutPrefix.slice(4, 134); // 65 bytes = 130 hex chars + const deploymentData = '0x' + withoutPrefix.slice(134); + + return { + magicPrefix, + signature, + deploymentData, + signatureLength: signature.length, + deploymentDataLength: deploymentData.length, + }; +} + +/** + * Example 1: Sign message with non-delegated EOA + */ +async function example1_NonDelegatedEOA() { + logSection('Example 1: Sign Message with Non-Delegated EOA'); + + try { + // Initialize TransactionKit with delegatedEoa mode + const transactionKit = TransactionKit({ + chainId: CHAIN_ID, + walletMode: 'delegatedEoa', + privateKey: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + bundlerApiKey: BUNDLER_API_KEY, + debugMode: true, + }); + + logInfo('Wallet Mode', 'delegatedEoa'); + logInfo('Chain ID', CHAIN_ID); + + // Get wallet address + const walletAddress = await transactionKit.getWalletAddress(); + logInfo('EOA Address', walletAddress || 'N/A'); + + // Check if EOA is already delegated + const isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); + logInfo('Is Delegated', isDelegated ? 'Yes' : 'No'); + + if (isDelegated) { + log('⚠️ EOA is already delegated. This example shows non-delegated behavior.', 'yellow'); + log(' Consider using Example 2 for delegated EOA scenarios.\n', 'yellow'); + } + + // Sign a message + const message = 'Hello, World! This is a test message.'; + logInfo('Message to Sign', message); + + log('Signing message...', 'blue'); + const signature = await transactionKit.signMessage(message, CHAIN_ID); + + // Verify signature format + const isValidFormat = verifyEIP6492Format(signature); + logInfo('EIP-6492 Format Valid', isValidFormat ? '✓ Yes' : '✗ No'); + + if (isValidFormat) { + const components = extractSignatureComponents(signature); + logInfo('Magic Prefix', `0x${components.magicPrefix}`); + logInfo('Signature (first 20 chars)', `${components.signature.slice(0, 22)}...`); + logInfo('Signature Length', `${components.signatureLength} chars`); + logInfo('Deployment Data Length', `${components.deploymentDataLength} chars`); + logInfo('Total Signature Length', `${signature.length} chars`); + } + + logInfo('Full Signature', signature); + + log('✓ Example 1 completed successfully!', 'green'); + return signature; + } catch (error: any) { + log(`✗ Example 1 failed: ${error.message}`, 'red'); + console.error(error); + throw error; + } +} + +/** + * Example 2: Sign message with delegated EOA + */ +async function example2_DelegatedEOA() { + logSection('Example 2: Sign Message with Delegated EOA'); + + try { + // Initialize TransactionKit with delegatedEoa mode + const transactionKit = TransactionKit({ + chainId: CHAIN_ID, + walletMode: 'delegatedEoa', + privateKey: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + bundlerApiKey: BUNDLER_API_KEY, + debugMode: true, + }); + + logInfo('Wallet Mode', 'delegatedEoa'); + logInfo('Chain ID', CHAIN_ID); + + // Get wallet address + const walletAddress = await transactionKit.getWalletAddress(); + logInfo('EOA Address', walletAddress || 'N/A'); + + // Check if EOA is already delegated + let isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); + logInfo('Is Delegated (Before)', isDelegated ? 'Yes' : 'No'); + + // If not delegated, delegate it first + if (!isDelegated) { + log('Delegating EOA to smart account...', 'blue'); + const delegateResult = await transactionKit.delegateSmartAccountToEoa({ + chainId: CHAIN_ID, + delegateImmediately: true, // Execute immediately + }); + + logInfo('Delegation Status', delegateResult.isAlreadyInstalled ? 'Already Installed' : 'Newly Installed'); + logInfo('EOA Address', delegateResult.eoaAddress); + logInfo('Delegate Address', delegateResult.delegateAddress); + + if (delegateResult.userOpHash) { + logInfo('UserOp Hash', delegateResult.userOpHash); + } + + // Wait a bit for the transaction to be mined + log('Waiting for delegation to be confirmed...', 'blue'); + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Check again + isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); + logInfo('Is Delegated (After)', isDelegated ? 'Yes' : 'No'); + } + + // Sign a message with the delegated EOA + const message = 'This message is signed by a delegated EOA!'; + logInfo('Message to Sign', message); + + log('Signing message with delegated EOA...', 'blue'); + const signature = await transactionKit.signMessage(message, CHAIN_ID); + + // Verify signature format + const isValidFormat = verifyEIP6492Format(signature); + logInfo('EIP-6492 Format Valid', isValidFormat ? '✓ Yes' : '✗ No'); + + if (isValidFormat) { + const components = extractSignatureComponents(signature); + logInfo('Magic Prefix', `0x${components.magicPrefix}`); + logInfo('Signature (first 20 chars)', `${components.signature.slice(0, 22)}...`); + logInfo('Signature Length', `${components.signatureLength} chars`); + logInfo('Deployment Data Length', `${components.deploymentDataLength} chars`); + logInfo('Total Signature Length', `${signature.length} chars`); + } + + logInfo('Full Signature', signature); + + log('✓ Example 2 completed successfully!', 'green'); + return signature; + } catch (error: any) { + log(`✗ Example 2 failed: ${error.message}`, 'red'); + console.error(error); + throw error; + } +} + +/** + * Example 3: Compare signatures from different states + */ +async function example3_CompareSignatures() { + logSection('Example 3: Compare Signatures from Different States'); + + try { + const transactionKit = TransactionKit({ + chainId: CHAIN_ID, + walletMode: 'delegatedEoa', + privateKey: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + bundlerApiKey: BUNDLER_API_KEY, + debugMode: false, + }); + + const message = 'Same message, different states'; + logInfo('Message', message); + + // Sign before delegation (if not already delegated) + const isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); + + let signatureBefore: string | null = null; + let signatureAfter: string | null = null; + + if (!isDelegated) { + log('Signing before delegation...', 'blue'); + signatureBefore = await transactionKit.signMessage(message, CHAIN_ID); + if (signatureBefore) { + logInfo('Signature Before Delegation', signatureBefore.slice(0, 50) + '...'); + } + + // Delegate + log('Delegating...', 'blue'); + await transactionKit.delegateSmartAccountToEoa({ + chainId: CHAIN_ID, + delegateImmediately: true, + }); + + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + // Sign after delegation + log('Signing after delegation...', 'blue'); + signatureAfter = await transactionKit.signMessage(message, CHAIN_ID); + if (signatureAfter) { + logInfo('Signature After Delegation', signatureAfter.slice(0, 50) + '...'); + } + + if (signatureBefore !== null && signatureAfter !== null) { + const areSame = signatureBefore === signatureAfter; + logInfo('Signatures Match', areSame ? 'Yes' : 'No'); + + if (!areSame) { + log('Note: Signatures differ because deployment data may change.', 'yellow'); + } + } else { + log('Note: Could not compare signatures - one or both signatures are null.', 'yellow'); + } + + log('✓ Example 3 completed successfully!', 'green'); + } catch (error: any) { + log(`✗ Example 3 failed: ${error.message}`, 'red'); + console.error(error); + throw error; + } +} + +/** + * Example 4: Sign different types of messages + */ +async function example4_DifferentMessageTypes() { + logSection('Example 4: Sign Different Types of Messages'); + + try { + const transactionKit = TransactionKit({ + chainId: CHAIN_ID, + walletMode: 'delegatedEoa', + privateKey: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + bundlerApiKey: BUNDLER_API_KEY, + debugMode: false, + }); + + const messages = [ + 'Simple text message', + '0x48656c6c6f', // Hex string + 'Message with special chars: !@#$%^&*()', + 'Very long message: ' + 'x'.repeat(100), + ]; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + log(`\nSigning message ${i + 1}:`, 'blue'); + logInfo('Message', message.length > 50 ? message.slice(0, 50) + '...' : message); + + const signature = await transactionKit.signMessage(message, CHAIN_ID); + const isValid = verifyEIP6492Format(signature); + + logInfo('Valid EIP-6492', isValid ? '✓ Yes' : '✗ No'); + logInfo('Signature', signature.slice(0, 50) + '...'); + } + + log('\n✓ Example 4 completed successfully!', 'green'); + } catch (error: any) { + log(`✗ Example 4 failed: ${error.message}`, 'red'); + console.error(error); + throw error; + } +} + +/** + * Main function + */ +async function main() { + log('\n' + '='.repeat(60), 'bright'); + log('EIP-6492 signMessage Example for EIP-7702 Wallets', 'bright'); + log('='.repeat(60) + '\n', 'bright'); + + logInfo('Configuration', ''); + logInfo(' Chain ID', CHAIN_ID); + logInfo(' Bundler URL', BUNDLER_URL); + logInfo(' Bundler API Key', BUNDLER_API_KEY ? 'Set' : 'Not Set'); + + // Validate private key + if (PRIVATE_KEY === '0x' + '0'.repeat(64)) { + log('\n⚠️ WARNING: Using default private key. Set PRIVATE_KEY in .env file!', 'yellow'); + } + + try { + // Run examples + await example1_NonDelegatedEOA(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + await example2_DelegatedEOA(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + await example3_CompareSignatures(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + await example4_DifferentMessageTypes(); + + log('\n' + '='.repeat(60), 'bright'); + log('All examples completed successfully!', 'green'); + log('='.repeat(60) + '\n', 'bright'); + } catch (error: any) { + log('\n' + '='.repeat(60), 'red'); + log('Examples failed!', 'red'); + log('='.repeat(60) + '\n', 'red'); + process.exit(1); + } +} + +// Run if executed directly +if (require.main === module) { + main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); + }); +} + +export { + example1_NonDelegatedEOA, + example2_DelegatedEOA, + example3_CompareSignatures, + example4_DifferentMessageTypes, +}; + diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index f9af799..95acc36 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -4,8 +4,13 @@ import { KERNEL_V3_3, KernelVersionToAddressesMap, } from '@zerodev/sdk/constants'; -import { isAddress, zeroAddress } from 'viem'; -import { SignAuthorizationReturnType } from 'viem/accounts'; +import { + isAddress, + toHex, + toRlp, + zeroAddress, +} from 'viem'; +import { SignAuthorizationReturnType, signMessage as viemSignMessage } from 'viem/accounts'; // interfaces import { @@ -656,6 +661,169 @@ export class EtherspotTransactionKit implements IInitial { } } + /** + * Signs a message using EIP-6492 format for EIP-7702 wallets. + * This creates a signature that can be validated before the smart account is deployed/activated. + * + * @param message - The message to sign (string or hex string). + * @param chainId - (Optional) The chain ID to use. If not provided, uses the provider's current chain ID. + * @returns A promise that resolves to the EIP-6492 formatted signature as a hex string. + * @throws {Error} If called in 'modular' wallet mode (only available in 'delegatedEoa' mode). + * @throws {Error} If signing fails or authorization cannot be created. + * + * @remarks + * - Only available in 'delegatedEoa' wallet mode. + * - Creates an EIP-6492 compatible signature that wraps the standard signature with deployment data. + * - The signature format is: `0x6492` + * - If the EOA is not yet designated, this will create the authorization automatically. + * - The signature can be validated by contracts that support EIP-6492, even before the smart account is activated. + */ + async signMessage( + message: string | `0x${string}`, + chainId?: number + ): Promise<`0x${string}`> { + const walletMode = this.#etherspotProvider.getWalletMode(); + const signChainId = chainId || this.#etherspotProvider.getChainId(); + + log( + 'signMessage(): Called', + { message, signChainId }, + this.debugMode + ); + + if (walletMode !== 'delegatedEoa') { + this.throwError( + "signMessage() is only available in 'delegatedEoa' wallet mode. " + + `Current mode: '${walletMode}'. ` + + 'This method creates EIP-6492 compatible signatures for EIP-7702 wallets.' + ); + } + + try { + // Get the owner account (EOA) + const owner = await this.#etherspotProvider.getOwnerAccount(signChainId); + const bundlerClient = + await this.#etherspotProvider.getBundlerClient(signChainId); + + // Sign the message using standard personal_sign (EIP-191) + const signature = await viemSignMessage({ + account: owner, + message: message as string, + }); + + log( + 'signMessage(): Message signed', + { signature: signature.substring(0, 20) + '...' }, + this.debugMode + ); + + // Get or create the authorization + // Check if already installed + const isAlreadyInstalled = + await this.isDelegateSmartAccountToEoa(signChainId); + + let authorization: SignAuthorizationReturnType | undefined; + + if (isAlreadyInstalled) { + // If already installed, we need to get the authorization + // We'll create a new one for the same contract address + const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] + .accountImplementationAddress as `0x${string}`; + + authorization = await bundlerClient.signAuthorization({ + account: owner, + contractAddress: delegateAddress, + }); + + log( + 'signMessage(): Authorization retrieved for installed account', + { delegateAddress }, + this.debugMode + ); + } else { + // Not installed yet, delegate to get authorization + const delegateResult = await this.delegateSmartAccountToEoa({ + chainId: signChainId, + delegateImmediately: false, + }); + + if (!delegateResult.authorization) { + this.throwError( + 'signMessage(): Failed to create authorization for EIP-6492 signature' + ); + } + + authorization = delegateResult.authorization; + + log( + 'signMessage(): Authorization created', + { eoaAddress: delegateResult.eoaAddress }, + this.debugMode + ); + } + + if (!authorization) { + this.throwError( + 'signMessage(): Authorization is required but was not created' + ); + } + + // Encode the authorization as deployment data for EIP-6492 + // For EIP-7702, the deployment data is the RLP-encoded transaction that would set the authorization + // Format: RLP([chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice]) + const publicClient = + await this.#etherspotProvider.getPublicClient(signChainId); + const nonce = await publicClient.getTransactionCount({ + address: owner.address, + }); + + // Get the authorization data + // The authorization.data field contains the encoded authorization + const authorizationData = authorization.data || '0x'; + + // Create deployment transaction data + // EIP-7702 authorization transaction structure: + // [chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice] + const deploymentTx = [ + toHex(signChainId), // chainId + toHex(nonce), // nonce + zeroAddress, // to (zero address for EIP-7702 authorization) + '0x0', // value + authorizationData, // data (authorization bytes) + '0x0', // gasLimit (placeholder, not critical for EIP-6492) + '0x0', // gasPrice (placeholder, not critical for EIP-6492) + ]; + + // RLP encode the deployment transaction + const deploymentData = toRlp(deploymentTx); + + log( + 'signMessage(): Deployment data encoded', + { deploymentDataLength: deploymentData.length }, + this.debugMode + ); + + // Create EIP-6492 format: 0x6492 + signature + deployment_data + // Remove '0x' prefix from signature and deployment data for concatenation + const signatureBytes = signature.slice(2); // Remove '0x' + const deploymentBytes = deploymentData.slice(2); // Remove '0x' + + // EIP-6492 format: magic prefix (0x6492) + signature (65 bytes) + deployment_data + const eip6492Signature = `0x6492${signatureBytes}${deploymentBytes}` as `0x${string}`; + + log( + 'signMessage(): EIP-6492 signature created', + { signatureLength: eip6492Signature.length }, + this.debugMode + ); + + return eip6492Signature; + } catch (error) { + log('signMessage(): Failed', error, this.debugMode); + throw error; + } + } + /** * Specifies or updates the transaction details to be sent. * diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 091c772..ab980e6 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -107,6 +107,10 @@ export interface IInitial { eoaAddress: string; userOpHash?: string; }>; + signMessage( + message: string | `0x${string}`, + chainId?: number + ): Promise<`0x${string}`>; getState(): IInstance; setDebugMode(enabled: boolean): void; getProvider(): WalletProviderLike; From 94051853affa7a3b88d752c2f6e2039338ac3c57 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 20 Nov 2025 11:02:28 +0000 Subject: [PATCH 2/7] chore: Bumped the package version --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d4635..a558574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.2.0] - 2025-11-20 + +### Added Changes + +- **EIP-6492 signMessage Support**: Added `signMessage()` method for creating EIP-6492 compatible signatures for EIP-7702 wallets. This allows signing messages before and after smart account delegation, with signatures that can be validated by EIP-6492 compatible validators. The method wraps standard EIP-191 personal_sign signatures with deployment data in EIP-6492 format (`0x6492`). +- **Example Program**: Added comprehensive example program (`example/src/signMessage-example.ts`) demonstrating signMessage usage with both delegated and non-delegated EOAs. + +### Notes + +- `signMessage()` is only available in `delegatedEoa` wallet mode +- Automatically creates EIP-7702 authorization if EOA is not yet delegated +- Signatures are compatible with EIP-6492 validators + ## [2.1.3] - 2025-01-27 ### Fixes diff --git a/package.json b/package.json index 4f5a58c..f45f422 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@etherspot/transaction-kit", "description": "Framework-agnostic Etherspot Transaction Kit", - "version": "2.1.3", + "version": "2.2.0", "main": "dist/cjs/index.js", "scripts": { "rollup:build": "NODE_OPTIONS=--max-old-space-size=8192 rollup -c --bundleConfigAsCjs", From 106f7dc654240505be5475d78cddb678e9ab78dc Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 20 Nov 2025 11:43:48 +0000 Subject: [PATCH 3/7] fix: Handle an issue when private key is not available --- example/.env.example | 5 ++++ example/validate-example.js | 52 +++++++++++++++++++++++++++++++++++++ lib/TransactionKit.ts | 14 +++++++--- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 example/.env.example create mode 100644 example/validate-example.js diff --git a/example/.env.example b/example/.env.example new file mode 100644 index 0000000..e011436 --- /dev/null +++ b/example/.env.example @@ -0,0 +1,5 @@ +# Environment variables for signMessage example +REACT_APP_DEMO_WALLET_PK=0x0000000000000000000000000000000000000000000000000000000000000000 +REACT_APP_BUNDLER_URL=https://api.etherspot.io/v2 +REACT_APP_ETHERSPOT_BUNDLER_API_KEY=your-api-key-here +REACT_APP_CHAIN_ID=11155111 diff --git a/example/validate-example.js b/example/validate-example.js new file mode 100644 index 0000000..34efd15 --- /dev/null +++ b/example/validate-example.js @@ -0,0 +1,52 @@ +// Simple validation script to demonstrate the example structure +const fs = require('fs'); +const path = require('path'); + +console.log('✓ Validating signMessage-example.ts structure...\n'); + +const exampleFile = path.join(__dirname, 'src', 'signMessage-example.ts'); +const content = fs.readFileSync(exampleFile, 'utf8'); + +// Check for key components +const checks = [ + { name: 'EIP-6492 format verification function', pattern: /function verifyEIP6492Format/ }, + { name: 'Signature component extraction', pattern: /function extractSignatureComponents/ }, + { name: 'Example 1: Non-Delegated EOA', pattern: /async function example1_NonDelegatedEOA/ }, + { name: 'Example 2: Delegated EOA', pattern: /async function example2_DelegatedEOA/ }, + { name: 'Example 3: Compare Signatures', pattern: /async function example3_CompareSignatures/ }, + { name: 'Example 4: Different Message Types', pattern: /async function example4_DifferentMessageTypes/ }, + { name: 'Main function', pattern: /async function main/ }, + { name: 'TransactionKit import', pattern: /import.*TransactionKit.*from.*@etherspot\/transaction-kit/ }, + { name: 'signMessage usage', pattern: /signMessage\(/ }, + { name: 'EIP-6492 magic prefix check', pattern: /0x6492/ }, +]; + +let passed = 0; +let failed = 0; + +checks.forEach(check => { + if (check.pattern.test(content)) { + console.log(`✓ ${check.name}`); + passed++; + } else { + console.log(`✗ ${check.name}`); + failed++; + } +}); + +console.log(`\n${'='.repeat(60)}`); +console.log(`Results: ${passed} passed, ${failed} failed`); +console.log(`${'='.repeat(60)}\n`); + +if (failed === 0) { + console.log('✅ All structure checks passed!'); + console.log('\nThe example file is properly structured and ready to use.'); + console.log('\nTo run the example:'); + console.log(' 1. Install dependencies: npm install'); + console.log(' 2. Set up .env file with your configuration'); + console.log(' 3. Run: npx ts-node src/signMessage-example.ts\n'); + process.exit(0); +} else { + console.log('❌ Some checks failed. Please review the example file.'); + process.exit(1); +} diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index 95acc36..4b01000 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -10,7 +10,7 @@ import { toRlp, zeroAddress, } from 'viem'; -import { SignAuthorizationReturnType, signMessage as viemSignMessage } from 'viem/accounts'; +import { SignAuthorizationReturnType } from 'viem/accounts'; // interfaces import { @@ -677,6 +677,9 @@ export class EtherspotTransactionKit implements IInitial { * - The signature format is: `0x6492` * - If the EOA is not yet designated, this will create the authorization automatically. * - The signature can be validated by contracts that support EIP-6492, even before the smart account is activated. + * - Uses WalletClient.signMessage() which delegates to the underlying provider/transport if the account supports it. + * This allows signing with provider-based accounts (e.g., MetaMask, hardware wallets) when using viemLocalAccount + * that wraps the provider, not just direct private key accounts. */ async signMessage( message: string | `0x${string}`, @@ -700,13 +703,16 @@ export class EtherspotTransactionKit implements IInitial { } try { - // Get the owner account (EOA) + // Get the owner account (EOA) and wallet client const owner = await this.#etherspotProvider.getOwnerAccount(signChainId); const bundlerClient = await this.#etherspotProvider.getBundlerClient(signChainId); + const walletClient = + await this.#etherspotProvider.getWalletClient(signChainId); - // Sign the message using standard personal_sign (EIP-191) - const signature = await viemSignMessage({ + // Sign the message using wallet client (delegates to provider if account supports it) + // This works with both direct private key accounts and provider-based accounts + const signature = await walletClient.signMessage({ account: owner, message: message as string, }); From 6d436f8eda5bef9bb167f8d5221c25376bbe4da6 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 20 Nov 2025 12:29:25 +0000 Subject: [PATCH 4/7] fix: Added a wrapper to intercept calls to signMessage --- __tests__/EtherspotTransactionKit.test.ts | 57 +++-- lib/TransactionKit.ts | 243 ++++++++++++---------- 2 files changed, 168 insertions(+), 132 deletions(-) diff --git a/__tests__/EtherspotTransactionKit.test.ts b/__tests__/EtherspotTransactionKit.test.ts index fa05d4a..86bced1 100644 --- a/__tests__/EtherspotTransactionKit.test.ts +++ b/__tests__/EtherspotTransactionKit.test.ts @@ -2348,13 +2348,10 @@ describe('DelegatedEoa Mode Integration', () => { }); describe('signMessage', () => { - const { signMessage: viemSignMessage } = require('viem/accounts'); const { toRlp } = require('viem'); beforeEach(() => { jest.clearAllMocks(); - (viemSignMessage as jest.Mock).mockClear(); - (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); (toRlp as jest.Mock).mockClear(); }); @@ -2377,13 +2374,16 @@ describe('DelegatedEoa Mode Integration', () => { .mockResolvedValue('0x'), // For other calls getTransactionCount: jest.fn().mockResolvedValue(5), } as any; + const mockWalletClient = { + signMessage: jest + .fn() + .mockResolvedValue('0x' + '1'.repeat(130)), // Standard signature + } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockResolvedValue( - '0x' + '1'.repeat(130) // 65 bytes signature (r + s + v) - ); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); const result = await transactionKit.signMessage('Hello, World!', 1); @@ -2391,10 +2391,8 @@ describe('DelegatedEoa Mode Integration', () => { // Should start with EIP-6492 magic prefix expect(result).toMatch(/^0x6492/); expect(result.length).toBeGreaterThan(140); // At least magic prefix + signature + deployment data - expect(viemSignMessage).toHaveBeenCalledWith({ - account: mockOwner, - message: 'Hello, World!', - }); + // The wrapper account's signMessage will call walletClient.signMessage with the original owner + expect(mockWalletClient.signMessage).toHaveBeenCalled(); expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); }); @@ -2417,19 +2415,22 @@ describe('DelegatedEoa Mode Integration', () => { .mockResolvedValue('0xef01001234'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; + const mockWalletClient = { + signMessage: jest + .fn() + .mockResolvedValue('0x' + '2'.repeat(130)), // Standard signature + } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockResolvedValue( - '0x' + '2'.repeat(130) // 65 bytes signature - ); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); const result = await transactionKit.signMessage('Test message', 1); expect(result).toMatch(/^0x6492/); - expect(mockViemSignMessage).toHaveBeenCalled(); + expect(mockWalletClient.signMessage).toHaveBeenCalled(); expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); }); @@ -2481,12 +2482,18 @@ describe('DelegatedEoa Mode Integration', () => { } as any; const mockPublicClient = { getCode: jest.fn().mockResolvedValue('0x'), + getTransactionCount: jest.fn().mockResolvedValue(5), + } as any; + const mockWalletClient = { + signMessage: jest + .fn() + .mockRejectedValue(new Error('Signing failed')), } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockRejectedValue(new Error('Signing failed')); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); await expect( transactionKit.signMessage('Test message', 1) @@ -2509,18 +2516,24 @@ describe('DelegatedEoa Mode Integration', () => { getCode: jest.fn().mockResolvedValue('0x'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; + const mockWalletClient = { + signMessage: jest + .fn() + .mockResolvedValue('0x' + '1'.repeat(130)), + } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); mockProvider.getChainId.mockReturnValue(1); - (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); await transactionKit.signMessage('Test message'); expect(mockProvider.getOwnerAccount).toHaveBeenCalledWith(1); expect(mockProvider.getBundlerClient).toHaveBeenCalledWith(1); + expect(mockProvider.getWalletClient).toHaveBeenCalledWith(1); }); it('should handle hex string messages', async () => { @@ -2539,11 +2552,16 @@ describe('DelegatedEoa Mode Integration', () => { getCode: jest.fn().mockResolvedValue('0x'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; + const mockWalletClient = { + signMessage: jest + .fn() + .mockResolvedValue('0x' + '1'.repeat(130)), + } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); mockProvider.getBundlerClient.mockResolvedValue(mockBundlerClient); mockProvider.getPublicClient.mockResolvedValue(mockPublicClient); - (viemSignMessage as jest.Mock).mockResolvedValue('0x' + '1'.repeat(130)); + mockProvider.getWalletClient.mockResolvedValue(mockWalletClient); (toRlp as jest.Mock).mockReturnValue('0xdeadbeef1234567890abcdef'); const result = await transactionKit.signMessage( @@ -2552,10 +2570,7 @@ describe('DelegatedEoa Mode Integration', () => { ); expect(result).toMatch(/^0x6492/); - expect(viemSignMessage).toHaveBeenCalledWith({ - account: mockOwner, - message: '0x48656c6c6f', - }); + expect(mockWalletClient.signMessage).toHaveBeenCalled(); }); }); diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index 4b01000..53faf60 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -10,7 +10,8 @@ import { toRlp, zeroAddress, } from 'viem'; -import { SignAuthorizationReturnType } from 'viem/accounts'; +import { SignAuthorizationReturnType, type LocalAccount } from 'viem/accounts'; +import type { Address, Hex } from 'viem'; // interfaces import { @@ -661,6 +662,114 @@ export class EtherspotTransactionKit implements IInitial { } } + /** + * Creates an EIP-6492 enabled account wrapper that intercepts signMessage calls. + * When signMessage is called on this account, it automatically wraps the signature with EIP-6492 format. + * + * @private + */ + /** + * Creates an EIP-6492 enabled account wrapper that intercepts signMessage calls. + * When signMessage is called on this account, it automatically wraps the signature with EIP-6492 format. + * + * @private + */ + private async createEIP6492Account( + owner: LocalAccount, + signChainId: number + ): Promise> { + const bundlerClient = + await this.#etherspotProvider.getBundlerClient(signChainId); + const walletClient = + await this.#etherspotProvider.getWalletClient(signChainId); + + // Capture 'this' context for use in the wrapper + const self = this; + + // Create a wrapper account that intercepts signMessage calls + // When walletClient.signMessage() is called with this account, viem will detect + // that the account has a signMessage method and call it directly. + // This works with ANY provider (WalletConnect, MetaMask, private keys, etc.) because: + // 1. Our wrapper's signMessage is called first + // 2. We then call walletClient.signMessage() with the original owner account + // 3. Viem delegates to the provider/transport if the owner account supports it + const eip6492Account: LocalAccount = { + ...owner, + async signMessage({ message }: { message: string | Hex }) { + // First, get the standard signature from the underlying account + // This will delegate to WalletConnect/MetaMask/provider if the owner account is provider-based + const standardSignature = await walletClient.signMessage({ + account: owner, + message: message as string, + }); + + // Get or create the authorization + const isAlreadyInstalled = + await self.isDelegateSmartAccountToEoa(signChainId); + + let authorization: SignAuthorizationReturnType | undefined; + + if (isAlreadyInstalled) { + const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] + .accountImplementationAddress as `0x${string}`; + + authorization = await bundlerClient.signAuthorization({ + account: owner, + contractAddress: delegateAddress, + }); + } else { + const delegateResult = await self.delegateSmartAccountToEoa({ + chainId: signChainId, + delegateImmediately: false, + }); + + if (!delegateResult.authorization) { + throw new Error( + 'Failed to create authorization for EIP-6492 signature' + ); + } + + authorization = delegateResult.authorization; + } + + if (!authorization) { + throw new Error( + 'Authorization is required but was not created' + ); + } + + // Encode the authorization as deployment data + const publicClient = + await self.#etherspotProvider.getPublicClient(signChainId); + const nonce = await publicClient.getTransactionCount({ + address: owner.address, + }); + + const authorizationData = authorization.data || '0x'; + + const deploymentTx = [ + toHex(signChainId), + toHex(nonce), + zeroAddress, + '0x0', + authorizationData, + '0x0', + '0x0', + ]; + + const deploymentData = toRlp(deploymentTx); + + // Create EIP-6492 format: 0x6492 + signature + deployment_data + const signatureBytes = standardSignature.slice(2); + const deploymentBytes = deploymentData.slice(2); + + return `0x6492${signatureBytes}${deploymentBytes}` as `0x${string}`; + }, + }; + + return eip6492Account; + } + /** * Signs a message using EIP-6492 format for EIP-7702 wallets. * This creates a signature that can be validated before the smart account is deployed/activated. @@ -678,8 +787,12 @@ export class EtherspotTransactionKit implements IInitial { * - If the EOA is not yet designated, this will create the authorization automatically. * - The signature can be validated by contracts that support EIP-6492, even before the smart account is activated. * - Uses WalletClient.signMessage() which delegates to the underlying provider/transport if the account supports it. - * This allows signing with provider-based accounts (e.g., MetaMask, hardware wallets) when using viemLocalAccount - * that wraps the provider, not just direct private key accounts. + * This allows signing with provider-based accounts (e.g., WalletConnect, MetaMask, hardware wallets) when using + * viemLocalAccount that wraps the provider, not just direct private key accounts. + * - The implementation creates an EIP-6492 enabled account wrapper that intercepts signMessage calls, + * allowing walletClient.signMessage() to directly return EIP-6492 formatted signatures. + * - Works with WalletConnect and other providers: The wrapper's signMessage is called first, then it delegates + * to the provider for actual signing, ensuring the signature is wrapped in EIP-6492 format regardless of provider. */ async signMessage( message: string | `0x${string}`, @@ -703,127 +816,35 @@ export class EtherspotTransactionKit implements IInitial { } try { - // Get the owner account (EOA) and wallet client + // Get the owner account (EOA) const owner = await this.#etherspotProvider.getOwnerAccount(signChainId); - const bundlerClient = - await this.#etherspotProvider.getBundlerClient(signChainId); + + // Create EIP-6492 enabled account wrapper + const eip6492Account = await this.createEIP6492Account(owner, signChainId); + + // Get wallet client const walletClient = await this.#etherspotProvider.getWalletClient(signChainId); - // Sign the message using wallet client (delegates to provider if account supports it) - // This works with both direct private key accounts and provider-based accounts + // Sign the message using the EIP-6492 enabled account wrapper + // The wrapper account's signMessage method automatically wraps the signature with EIP-6492 format + // This allows walletClient.signMessage() to directly return EIP-6492 formatted signatures const signature = await walletClient.signMessage({ - account: owner, + account: eip6492Account, message: message as string, }); - log( - 'signMessage(): Message signed', - { signature: signature.substring(0, 20) + '...' }, - this.debugMode - ); - - // Get or create the authorization - // Check if already installed - const isAlreadyInstalled = - await this.isDelegateSmartAccountToEoa(signChainId); - - let authorization: SignAuthorizationReturnType | undefined; - - if (isAlreadyInstalled) { - // If already installed, we need to get the authorization - // We'll create a new one for the same contract address - const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] - .accountImplementationAddress as `0x${string}`; - - authorization = await bundlerClient.signAuthorization({ - account: owner, - contractAddress: delegateAddress, - }); - - log( - 'signMessage(): Authorization retrieved for installed account', - { delegateAddress }, - this.debugMode - ); - } else { - // Not installed yet, delegate to get authorization - const delegateResult = await this.delegateSmartAccountToEoa({ - chainId: signChainId, - delegateImmediately: false, - }); - - if (!delegateResult.authorization) { - this.throwError( - 'signMessage(): Failed to create authorization for EIP-6492 signature' - ); - } - - authorization = delegateResult.authorization; - - log( - 'signMessage(): Authorization created', - { eoaAddress: delegateResult.eoaAddress }, - this.debugMode - ); - } - - if (!authorization) { - this.throwError( - 'signMessage(): Authorization is required but was not created' - ); - } - - // Encode the authorization as deployment data for EIP-6492 - // For EIP-7702, the deployment data is the RLP-encoded transaction that would set the authorization - // Format: RLP([chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice]) - const publicClient = - await this.#etherspotProvider.getPublicClient(signChainId); - const nonce = await publicClient.getTransactionCount({ - address: owner.address, - }); - - // Get the authorization data - // The authorization.data field contains the encoded authorization - const authorizationData = authorization.data || '0x'; - - // Create deployment transaction data - // EIP-7702 authorization transaction structure: - // [chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice] - const deploymentTx = [ - toHex(signChainId), // chainId - toHex(nonce), // nonce - zeroAddress, // to (zero address for EIP-7702 authorization) - '0x0', // value - authorizationData, // data (authorization bytes) - '0x0', // gasLimit (placeholder, not critical for EIP-6492) - '0x0', // gasPrice (placeholder, not critical for EIP-6492) - ]; - - // RLP encode the deployment transaction - const deploymentData = toRlp(deploymentTx); - - log( - 'signMessage(): Deployment data encoded', - { deploymentDataLength: deploymentData.length }, - this.debugMode - ); - - // Create EIP-6492 format: 0x6492 + signature + deployment_data - // Remove '0x' prefix from signature and deployment data for concatenation - const signatureBytes = signature.slice(2); // Remove '0x' - const deploymentBytes = deploymentData.slice(2); // Remove '0x' - - // EIP-6492 format: magic prefix (0x6492) + signature (65 bytes) + deployment_data - const eip6492Signature = `0x6492${signatureBytes}${deploymentBytes}` as `0x${string}`; - log( 'signMessage(): EIP-6492 signature created', - { signatureLength: eip6492Signature.length }, + { + signatureLength: signature.length, + signaturePrefix: signature.substring(0, 6), + startsWith6492: signature.startsWith('0x6492') + }, this.debugMode ); - return eip6492Signature; + return signature; } catch (error) { log('signMessage(): Failed', error, this.debugMode); throw error; From 1fb983cdebeedd0de2f2eb8278b2143599badd53 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 20 Nov 2025 14:00:57 +0000 Subject: [PATCH 5/7] fix: fix build error and properly encode 7702 authorisation --- lib/TransactionKit.ts | 19 +++++++++++++++---- package-lock.json | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index 53faf60..0e96da4 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -11,7 +11,7 @@ import { zeroAddress, } from 'viem'; import { SignAuthorizationReturnType, type LocalAccount } from 'viem/accounts'; -import type { Address, Hex } from 'viem'; +import type { Address, Hex, SignableMessage } from 'viem'; // interfaces import { @@ -695,7 +695,7 @@ export class EtherspotTransactionKit implements IInitial { // 3. Viem delegates to the provider/transport if the owner account supports it const eip6492Account: LocalAccount = { ...owner, - async signMessage({ message }: { message: string | Hex }) { + async signMessage({ message }: { message: SignableMessage }) { // First, get the standard signature from the underlying account // This will delegate to WalletConnect/MetaMask/provider if the owner account is provider-based const standardSignature = await walletClient.signMessage({ @@ -745,14 +745,25 @@ export class EtherspotTransactionKit implements IInitial { address: owner.address, }); - const authorizationData = authorization.data || '0x'; + // Encode authorization for EIP-7702 deployment data + // Authorization structure: [chainId, address, nonce, r, s, v] + // For EIP-6492, we need RLP-encoded transaction: [chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice] + // The authorization itself is RLP-encoded: [chainId, address, nonce, r, s, v] + const authorizationRlp = toRlp([ + toHex(authorization.chainId), + authorization.address, + toHex(authorization.nonce), + authorization.r, + authorization.s, + toHex(authorization.v), + ]); const deploymentTx = [ toHex(signChainId), toHex(nonce), zeroAddress, '0x0', - authorizationData, + authorizationRlp, '0x0', '0x0', ]; diff --git a/package-lock.json b/package-lock.json index b2c935f..578a122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@etherspot/transaction-kit", - "version": "2.1.3", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@etherspot/transaction-kit", - "version": "2.1.3", + "version": "2.2.0", "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", From 5347b3ced45db763147e8c7711281a5e3c945477 Mon Sep 17 00:00:00 2001 From: RanaBug Date: Tue, 9 Dec 2025 16:37:38 +0000 Subject: [PATCH 6/7] fixes signature eip6692 --- CHANGELOG.md | 5 +- __tests__/EtherspotTransactionKit.test.ts | 111 +++--- example/README.md | 149 ++------ example/package-lock.json | 89 +++-- example/package.json | 8 +- example/src/signMessage-example.ts | 402 ---------------------- example/validate-example.js | 52 --- lib/TransactionKit.ts | 143 ++++---- package-lock.json | 4 +- package.json | 2 +- 10 files changed, 205 insertions(+), 760 deletions(-) delete mode 100644 example/src/signMessage-example.ts delete mode 100644 example/validate-example.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a558574..7c13b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,10 @@ # Changelog -## [2.2.0] - 2025-11-20 +## [2.1.4] - 2025-12-09 ### Added Changes -- **EIP-6492 signMessage Support**: Added `signMessage()` method for creating EIP-6492 compatible signatures for EIP-7702 wallets. This allows signing messages before and after smart account delegation, with signatures that can be validated by EIP-6492 compatible validators. The method wraps standard EIP-191 personal_sign signatures with deployment data in EIP-6492 format (`0x6492`). -- **Example Program**: Added comprehensive example program (`example/src/signMessage-example.ts`) demonstrating signMessage usage with both delegated and non-delegated EOAs. +- **EIP-6492 signMessage Support**: Added `signMessage()` method for creating EIP-6492 compatible signatures for EIP-7702 wallets. This allows signing messages before and after smart account delegation, with signatures that can be validated by EIP-6492 compatible validators. The method wraps standard EIP-191 personal_sign signatures with deployment data in EIP-6492 format: `abi.encode((factoryAddress, factoryCalldata, originalSignature)) || magicBytes` where magicBytes is the 32-byte suffix. ### Notes diff --git a/__tests__/EtherspotTransactionKit.test.ts b/__tests__/EtherspotTransactionKit.test.ts index 86bced1..7ee1ed6 100644 --- a/__tests__/EtherspotTransactionKit.test.ts +++ b/__tests__/EtherspotTransactionKit.test.ts @@ -33,8 +33,24 @@ jest.mock('viem', () => { ...actual, isAddress: jest.fn(), parseEther: jest.fn(), - toHex: jest.fn((val) => `0x${val.toString(16)}`), - toRlp: jest.fn((val) => `0x${Buffer.from(JSON.stringify(val)).toString('hex')}`), + toHex: jest.fn((val) => { + if (val === undefined || val === null) return '0x0'; + if (typeof val === 'bigint') return `0x${val.toString(16)}`; + if (typeof val === 'number') return `0x${val.toString(16)}`; + return `0x${val.toString(16)}`; + }), + toRlp: jest.fn( + (val) => `0x${Buffer.from(JSON.stringify(val)).toString('hex')}` + ), + encodeAbiParameters: jest.fn((params, values) => { + // Mock ABI encoding: simple concatenation for testing + // In reality, this would properly ABI encode the tuple + const factoryAddress = values[0]; + const factoryCalldata = values[1]; + const originalSignature = values[2]; + // Simulate ABI encoding by concatenating (this is simplified for tests) + return `0x${factoryAddress.slice(2)}${factoryCalldata.slice(2)}${originalSignature.slice(2)}` as `0x${string}`; + }), zeroAddress: '0x0000000000000000000000000000000000000000', }; }); @@ -48,6 +64,8 @@ jest.mock('viem/accounts', () => { }; }); +const { signMessage: viemSignMessage } = require('viem/accounts'); + // Move mockConfig and mockSdk to a higher scope for batch tests let mockConfig: any; let mockSdk: any; @@ -2360,12 +2378,10 @@ describe('DelegatedEoa Mode Integration', () => { address: '0xowner123456789012345678901234567890', } as any; const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockResolvedValue({ - address: '0xdelegate123456789012345678901234567890', - data: '0xabcdef1234567890abcdef1234567890abcdef12', - }), + signAuthorization: jest.fn().mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), } as any; const mockPublicClient = { getCode: jest @@ -2375,9 +2391,7 @@ describe('DelegatedEoa Mode Integration', () => { getTransactionCount: jest.fn().mockResolvedValue(5), } as any; const mockWalletClient = { - signMessage: jest - .fn() - .mockResolvedValue('0x' + '1'.repeat(130)), // Standard signature + signMessage: jest.fn().mockResolvedValue('0x' + '1'.repeat(130)), // Standard signature } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); @@ -2388,9 +2402,12 @@ describe('DelegatedEoa Mode Integration', () => { const result = await transactionKit.signMessage('Hello, World!', 1); - // Should start with EIP-6492 magic prefix - expect(result).toMatch(/^0x6492/); - expect(result.length).toBeGreaterThan(140); // At least magic prefix + signature + deployment data + // EIP-6492 format: encodedWrapper || magicBytes (32-byte suffix) + // Magic bytes should be at the end: 0x6492649264926492649264926492649264926492649264926492649264926492 + expect(result).toMatch( + /6492649264926492649264926492649264926492649264926492649264926492$/ + ); + expect(result.length).toBeGreaterThan(200); // At least encoded wrapper + 32-byte magic suffix // The wrapper account's signMessage will call walletClient.signMessage with the original owner expect(mockWalletClient.signMessage).toHaveBeenCalled(); expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); @@ -2401,12 +2418,10 @@ describe('DelegatedEoa Mode Integration', () => { address: '0xowner123456789012345678901234567890', } as any; const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockResolvedValue({ - address: '0xdelegate123456789012345678901234567890', - data: '0xabcdef1234567890abcdef1234567890abcdef12', - }), + signAuthorization: jest.fn().mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), } as any; const mockPublicClient = { getCode: jest @@ -2416,9 +2431,7 @@ describe('DelegatedEoa Mode Integration', () => { getTransactionCount: jest.fn().mockResolvedValue(5), } as any; const mockWalletClient = { - signMessage: jest - .fn() - .mockResolvedValue('0x' + '2'.repeat(130)), // Standard signature + signMessage: jest.fn().mockResolvedValue('0x' + '2'.repeat(130)), // Standard signature } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); @@ -2429,7 +2442,10 @@ describe('DelegatedEoa Mode Integration', () => { const result = await transactionKit.signMessage('Test message', 1); - expect(result).toMatch(/^0x6492/); + // EIP-6492 format: encodedWrapper || magicBytes (32-byte suffix at end) + expect(result).toMatch( + /6492649264926492649264926492649264926492649264926492649264926492$/ + ); expect(mockWalletClient.signMessage).toHaveBeenCalled(); expect(mockBundlerClient.signAuthorization).toHaveBeenCalled(); }); @@ -2473,21 +2489,17 @@ describe('DelegatedEoa Mode Integration', () => { address: '0xowner123456789012345678901234567890', } as any; const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockResolvedValue({ - address: '0xdelegate123456789012345678901234567890', - data: '0xabcdef1234567890abcdef1234567890abcdef12', - }), + signAuthorization: jest.fn().mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), } as any; const mockPublicClient = { getCode: jest.fn().mockResolvedValue('0x'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; const mockWalletClient = { - signMessage: jest - .fn() - .mockRejectedValue(new Error('Signing failed')), + signMessage: jest.fn().mockRejectedValue(new Error('Signing failed')), } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); @@ -2505,21 +2517,17 @@ describe('DelegatedEoa Mode Integration', () => { address: '0xowner123456789012345678901234567890', } as any; const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockResolvedValue({ - address: '0xdelegate123456789012345678901234567890', - data: '0xabcdef1234567890abcdef1234567890abcdef12', - }), + signAuthorization: jest.fn().mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), } as any; const mockPublicClient = { getCode: jest.fn().mockResolvedValue('0x'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; const mockWalletClient = { - signMessage: jest - .fn() - .mockResolvedValue('0x' + '1'.repeat(130)), + signMessage: jest.fn().mockResolvedValue('0x' + '1'.repeat(130)), } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); @@ -2541,21 +2549,17 @@ describe('DelegatedEoa Mode Integration', () => { address: '0xowner123456789012345678901234567890', } as any; const mockBundlerClient = { - signAuthorization: jest - .fn() - .mockResolvedValue({ - address: '0xdelegate123456789012345678901234567890', - data: '0xabcdef1234567890abcdef1234567890abcdef12', - }), + signAuthorization: jest.fn().mockResolvedValue({ + address: '0xdelegate123456789012345678901234567890', + data: '0xabcdef1234567890abcdef1234567890abcdef12', + }), } as any; const mockPublicClient = { getCode: jest.fn().mockResolvedValue('0x'), getTransactionCount: jest.fn().mockResolvedValue(5), } as any; const mockWalletClient = { - signMessage: jest - .fn() - .mockResolvedValue('0x' + '1'.repeat(130)), + signMessage: jest.fn().mockResolvedValue('0x' + '1'.repeat(130)), } as any; mockProvider.getOwnerAccount.mockResolvedValue(mockOwner); @@ -2569,7 +2573,10 @@ describe('DelegatedEoa Mode Integration', () => { 1 ); - expect(result).toMatch(/^0x6492/); + // EIP-6492 format: encodedWrapper || magicBytes (32-byte suffix at end) + expect(result).toMatch( + /6492649264926492649264926492649264926492649264926492649264926492$/ + ); expect(mockWalletClient.signMessage).toHaveBeenCalled(); }); }); diff --git a/example/README.md b/example/README.md index 181240e..e23bce0 100644 --- a/example/README.md +++ b/example/README.md @@ -1,142 +1,45 @@ -# signMessage Example - -This example demonstrates how to use the `signMessage` function with EIP-6492 format for EIP-7702 wallets. - -## Features +# Getting Started with Create React App -The example includes four scenarios: - -1. **Example 1: Non-Delegated EOA** - Signing messages before EIP-7702 delegation -2. **Example 2: Delegated EOA** - Signing messages after EIP-7702 delegation -3. **Example 3: Compare Signatures** - Comparing signatures from different delegation states -4. **Example 4: Different Message Types** - Signing various message formats (text, hex, special chars, long messages) +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). -## Prerequisites +## Available Scripts -1. Node.js 18+ installed -2. TypeScript and ts-node installed -3. A private key for testing (use a test account, never use your main account's private key) -4. A bundler API key (optional, but recommended for delegation) +In the project directory, you can run: -## Setup +### `npm start` -1. **Install dependencies** (from example directory): - ```bash - npm install - ``` +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. -2. **Set up environment variables**: - - Create a `.env` file in the `example` directory with: - ```env - REACT_APP_DEMO_WALLET_PK=0x...your-private-key... - REACT_APP_BUNDLER_URL=https://api.etherspot.io/v2 - REACT_APP_ETHERSPOT_BUNDLER_API_KEY=your-api-key - REACT_APP_CHAIN_ID=11155111 - ``` - - Or use these alternative env var names: - ```env - PRIVATE_KEY=0x...your-private-key... - BUNDLER_URL=https://api.etherspot.io/v2 - BUNDLER_API_KEY=your-api-key - CHAIN_ID=11155111 - ``` +The page will reload if you make edits.\ +You will also see any lint errors in the console. -**⚠️ WARNING**: Never commit your `.env` file or share your private key! +### `npm test` -## Running the Example +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. -From the `example` directory: +### `npm run build` -```bash -# Using ts-node -npx ts-node src/signMessage-example.ts +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. -# Or if ts-node is installed globally -ts-node src/signMessage-example.ts +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! -# Or compile and run -tsc src/signMessage-example.ts --esModuleInterop --module commonjs --target es2020 --outDir dist -node dist/signMessage-example.js -``` +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. -## Expected Output +### `npm run eject` -The example will output: +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** -- Wallet address (EOA) -- Delegation status -- EIP-6492 signature format verification -- Signature components breakdown -- Full signature hex string +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. -Example output: -``` -============================================================ -Example 1: Sign Message with Non-Delegated EOA -============================================================ +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. -EOA Address: 0x1234... -Is Delegated: No -Message to Sign: Hello, World! This is a test message. -EIP-6492 Format Valid: ✓ Yes -Magic Prefix: 0x6492 -Signature: 0x1234... -Signature Length: 132 chars -Deployment Data Length: 200+ chars -Total Signature Length: 400+ chars -``` +## Learn More -## Understanding EIP-6492 Signatures +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). -EIP-6492 signatures have the following format: - -``` -0x6492 -``` - -Where: -- `0x6492` - Magic prefix (2 bytes) -- `` - 65-byte signature (r, s, v) from EIP-191 personal_sign -- `` - RLP-encoded transaction data for EIP-7702 authorization - -## Use Cases - -1. **Pre-deployment Signatures**: Sign messages before the smart account is activated -2. **Cross-chain Compatibility**: Signatures work across different chains -3. **Future-proof Signatures**: Signatures remain valid even after delegation changes - -## Troubleshooting - -### Error: "signMessage() is only available in 'delegatedEoa' wallet mode" -- Make sure `walletMode: 'delegatedEoa'` is set in the configuration - -### Error: "Failed to create authorization" -- Check your bundler URL and API key -- Ensure the chain ID is correct -- Verify network connectivity - -### Error: "Invalid private key" -- Ensure the private key starts with `0x` -- Check that it's a valid 64-character hex string - -## Notes - -- This example uses Sepolia testnet by default -- Signatures are deterministic for the same message and state -- Deployment data may change between delegation states -- Always use test accounts for experimentation - -## Security - -⚠️ **IMPORTANT SECURITY NOTES**: - -1. Never use your main account's private key -2. Never commit `.env` files to version control -3. Use testnets for development and testing -4. Keep your private keys secure and never share them - -## License - -Same as the main project license. +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/example/package-lock.json b/example/package-lock.json index 558b718..00db6b6 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -23,17 +23,13 @@ "react-icons": "^4.7.1", "react-jsx-parser": "^1.29.0", "react-scripts": "5.0.1", - "react-test-renderer": "18.2.0" - }, - "devDependencies": { - "dotenv": "^17.2.3", - "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "react-test-renderer": "18.2.0", + "typescript": "^4.9.5" } }, "..": { "name": "@etherspot/transaction-kit", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", @@ -2153,8 +2149,9 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2166,8 +2163,9 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4749,29 +4747,33 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7339,8 +7341,9 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/cross-fetch": { "version": "3.1.8", @@ -8030,8 +8033,9 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -8193,19 +8197,6 @@ "tslib": "^2.0.3" } }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", @@ -12852,8 +12843,9 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -17924,8 +17916,9 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -17968,8 +17961,9 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "acorn": "^8.11.0" }, @@ -17981,8 +17975,9 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -18174,16 +18169,16 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -18394,8 +18389,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -19344,8 +19340,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } diff --git a/example/package.json b/example/package.json index 2c27f54..cb9beb8 100644 --- a/example/package.json +++ b/example/package.json @@ -14,11 +14,12 @@ "@types/react-dom": "^18.0.10", "ethers": "^5.7.2", "react": "18.2.0", + "react-test-renderer": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.7.1", "react-jsx-parser": "^1.29.0", "react-scripts": "5.0.1", - "react-test-renderer": "18.2.0" + "typescript": "^4.9.5" }, "scripts": { "start": "react-scripts start", @@ -43,10 +44,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "dotenv": "^17.2.3", - "ts-node": "^10.9.2", - "typescript": "^5.9.3" } } diff --git a/example/src/signMessage-example.ts b/example/src/signMessage-example.ts deleted file mode 100644 index ab6edb5..0000000 --- a/example/src/signMessage-example.ts +++ /dev/null @@ -1,402 +0,0 @@ -/** - * Example: Demonstrating signMessage with EIP-6492 for EIP-7702 Wallets - * - * This example shows how to use the signMessage function with: - * 1. Non-delegated EOA (before EIP-7702 delegation) - * 2. Delegated EOA (after EIP-7702 delegation) - * - * Prerequisites: - * - Set up environment variables (see .env.example) - * - Install dependencies: npm install - * - Run: npx ts-node src/signMessage-example.ts - */ - -import { TransactionKit } from '@etherspot/transaction-kit'; -import { parseEther } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { sepolia } from 'viem/chains'; -import * as dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - -// Configuration -const PRIVATE_KEY = process.env.REACT_APP_DEMO_WALLET_PK || process.env.PRIVATE_KEY || '0x' + '0'.repeat(64); -const BUNDLER_URL = process.env.REACT_APP_BUNDLER_URL || process.env.BUNDLER_URL || 'https://api.etherspot.io/v2'; -const BUNDLER_API_KEY = process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY || process.env.BUNDLER_API_KEY || ''; -const CHAIN_ID = parseInt(process.env.REACT_APP_CHAIN_ID || process.env.CHAIN_ID || '11155111'); // Sepolia - -// Colors for console output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', - red: '\x1b[31m', -}; - -function log(message: string, color: keyof typeof colors = 'reset') { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -function logSection(title: string) { - console.log('\n' + '='.repeat(60)); - log(title, 'bright'); - console.log('='.repeat(60) + '\n'); -} - -function logInfo(label: string, value: string | number | boolean) { - log(`${label}:`, 'cyan'); - console.log(` ${value}\n`); -} - -/** - * Verify EIP-6492 signature format - */ -function verifyEIP6492Format(signature: string): boolean { - if (!signature.startsWith('0x6492')) { - return false; - } - if (signature.length < 140) { - // Minimum: 0x6492 (4) + signature (130) + some deployment data - return false; - } - return true; -} - -/** - * Extract signature components from EIP-6492 format - */ -function extractSignatureComponents(eip6492Signature: string) { - const withoutPrefix = eip6492Signature.slice(2); // Remove '0x' - const magicPrefix = withoutPrefix.slice(0, 4); // '6492' - const signature = '0x' + withoutPrefix.slice(4, 134); // 65 bytes = 130 hex chars - const deploymentData = '0x' + withoutPrefix.slice(134); - - return { - magicPrefix, - signature, - deploymentData, - signatureLength: signature.length, - deploymentDataLength: deploymentData.length, - }; -} - -/** - * Example 1: Sign message with non-delegated EOA - */ -async function example1_NonDelegatedEOA() { - logSection('Example 1: Sign Message with Non-Delegated EOA'); - - try { - // Initialize TransactionKit with delegatedEoa mode - const transactionKit = TransactionKit({ - chainId: CHAIN_ID, - walletMode: 'delegatedEoa', - privateKey: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - bundlerApiKey: BUNDLER_API_KEY, - debugMode: true, - }); - - logInfo('Wallet Mode', 'delegatedEoa'); - logInfo('Chain ID', CHAIN_ID); - - // Get wallet address - const walletAddress = await transactionKit.getWalletAddress(); - logInfo('EOA Address', walletAddress || 'N/A'); - - // Check if EOA is already delegated - const isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); - logInfo('Is Delegated', isDelegated ? 'Yes' : 'No'); - - if (isDelegated) { - log('⚠️ EOA is already delegated. This example shows non-delegated behavior.', 'yellow'); - log(' Consider using Example 2 for delegated EOA scenarios.\n', 'yellow'); - } - - // Sign a message - const message = 'Hello, World! This is a test message.'; - logInfo('Message to Sign', message); - - log('Signing message...', 'blue'); - const signature = await transactionKit.signMessage(message, CHAIN_ID); - - // Verify signature format - const isValidFormat = verifyEIP6492Format(signature); - logInfo('EIP-6492 Format Valid', isValidFormat ? '✓ Yes' : '✗ No'); - - if (isValidFormat) { - const components = extractSignatureComponents(signature); - logInfo('Magic Prefix', `0x${components.magicPrefix}`); - logInfo('Signature (first 20 chars)', `${components.signature.slice(0, 22)}...`); - logInfo('Signature Length', `${components.signatureLength} chars`); - logInfo('Deployment Data Length', `${components.deploymentDataLength} chars`); - logInfo('Total Signature Length', `${signature.length} chars`); - } - - logInfo('Full Signature', signature); - - log('✓ Example 1 completed successfully!', 'green'); - return signature; - } catch (error: any) { - log(`✗ Example 1 failed: ${error.message}`, 'red'); - console.error(error); - throw error; - } -} - -/** - * Example 2: Sign message with delegated EOA - */ -async function example2_DelegatedEOA() { - logSection('Example 2: Sign Message with Delegated EOA'); - - try { - // Initialize TransactionKit with delegatedEoa mode - const transactionKit = TransactionKit({ - chainId: CHAIN_ID, - walletMode: 'delegatedEoa', - privateKey: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - bundlerApiKey: BUNDLER_API_KEY, - debugMode: true, - }); - - logInfo('Wallet Mode', 'delegatedEoa'); - logInfo('Chain ID', CHAIN_ID); - - // Get wallet address - const walletAddress = await transactionKit.getWalletAddress(); - logInfo('EOA Address', walletAddress || 'N/A'); - - // Check if EOA is already delegated - let isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); - logInfo('Is Delegated (Before)', isDelegated ? 'Yes' : 'No'); - - // If not delegated, delegate it first - if (!isDelegated) { - log('Delegating EOA to smart account...', 'blue'); - const delegateResult = await transactionKit.delegateSmartAccountToEoa({ - chainId: CHAIN_ID, - delegateImmediately: true, // Execute immediately - }); - - logInfo('Delegation Status', delegateResult.isAlreadyInstalled ? 'Already Installed' : 'Newly Installed'); - logInfo('EOA Address', delegateResult.eoaAddress); - logInfo('Delegate Address', delegateResult.delegateAddress); - - if (delegateResult.userOpHash) { - logInfo('UserOp Hash', delegateResult.userOpHash); - } - - // Wait a bit for the transaction to be mined - log('Waiting for delegation to be confirmed...', 'blue'); - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Check again - isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); - logInfo('Is Delegated (After)', isDelegated ? 'Yes' : 'No'); - } - - // Sign a message with the delegated EOA - const message = 'This message is signed by a delegated EOA!'; - logInfo('Message to Sign', message); - - log('Signing message with delegated EOA...', 'blue'); - const signature = await transactionKit.signMessage(message, CHAIN_ID); - - // Verify signature format - const isValidFormat = verifyEIP6492Format(signature); - logInfo('EIP-6492 Format Valid', isValidFormat ? '✓ Yes' : '✗ No'); - - if (isValidFormat) { - const components = extractSignatureComponents(signature); - logInfo('Magic Prefix', `0x${components.magicPrefix}`); - logInfo('Signature (first 20 chars)', `${components.signature.slice(0, 22)}...`); - logInfo('Signature Length', `${components.signatureLength} chars`); - logInfo('Deployment Data Length', `${components.deploymentDataLength} chars`); - logInfo('Total Signature Length', `${signature.length} chars`); - } - - logInfo('Full Signature', signature); - - log('✓ Example 2 completed successfully!', 'green'); - return signature; - } catch (error: any) { - log(`✗ Example 2 failed: ${error.message}`, 'red'); - console.error(error); - throw error; - } -} - -/** - * Example 3: Compare signatures from different states - */ -async function example3_CompareSignatures() { - logSection('Example 3: Compare Signatures from Different States'); - - try { - const transactionKit = TransactionKit({ - chainId: CHAIN_ID, - walletMode: 'delegatedEoa', - privateKey: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - bundlerApiKey: BUNDLER_API_KEY, - debugMode: false, - }); - - const message = 'Same message, different states'; - logInfo('Message', message); - - // Sign before delegation (if not already delegated) - const isDelegated = await transactionKit.isDelegateSmartAccountToEoa(); - - let signatureBefore: string | null = null; - let signatureAfter: string | null = null; - - if (!isDelegated) { - log('Signing before delegation...', 'blue'); - signatureBefore = await transactionKit.signMessage(message, CHAIN_ID); - if (signatureBefore) { - logInfo('Signature Before Delegation', signatureBefore.slice(0, 50) + '...'); - } - - // Delegate - log('Delegating...', 'blue'); - await transactionKit.delegateSmartAccountToEoa({ - chainId: CHAIN_ID, - delegateImmediately: true, - }); - - await new Promise(resolve => setTimeout(resolve, 5000)); - } - - // Sign after delegation - log('Signing after delegation...', 'blue'); - signatureAfter = await transactionKit.signMessage(message, CHAIN_ID); - if (signatureAfter) { - logInfo('Signature After Delegation', signatureAfter.slice(0, 50) + '...'); - } - - if (signatureBefore !== null && signatureAfter !== null) { - const areSame = signatureBefore === signatureAfter; - logInfo('Signatures Match', areSame ? 'Yes' : 'No'); - - if (!areSame) { - log('Note: Signatures differ because deployment data may change.', 'yellow'); - } - } else { - log('Note: Could not compare signatures - one or both signatures are null.', 'yellow'); - } - - log('✓ Example 3 completed successfully!', 'green'); - } catch (error: any) { - log(`✗ Example 3 failed: ${error.message}`, 'red'); - console.error(error); - throw error; - } -} - -/** - * Example 4: Sign different types of messages - */ -async function example4_DifferentMessageTypes() { - logSection('Example 4: Sign Different Types of Messages'); - - try { - const transactionKit = TransactionKit({ - chainId: CHAIN_ID, - walletMode: 'delegatedEoa', - privateKey: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - bundlerApiKey: BUNDLER_API_KEY, - debugMode: false, - }); - - const messages = [ - 'Simple text message', - '0x48656c6c6f', // Hex string - 'Message with special chars: !@#$%^&*()', - 'Very long message: ' + 'x'.repeat(100), - ]; - - for (let i = 0; i < messages.length; i++) { - const message = messages[i]; - log(`\nSigning message ${i + 1}:`, 'blue'); - logInfo('Message', message.length > 50 ? message.slice(0, 50) + '...' : message); - - const signature = await transactionKit.signMessage(message, CHAIN_ID); - const isValid = verifyEIP6492Format(signature); - - logInfo('Valid EIP-6492', isValid ? '✓ Yes' : '✗ No'); - logInfo('Signature', signature.slice(0, 50) + '...'); - } - - log('\n✓ Example 4 completed successfully!', 'green'); - } catch (error: any) { - log(`✗ Example 4 failed: ${error.message}`, 'red'); - console.error(error); - throw error; - } -} - -/** - * Main function - */ -async function main() { - log('\n' + '='.repeat(60), 'bright'); - log('EIP-6492 signMessage Example for EIP-7702 Wallets', 'bright'); - log('='.repeat(60) + '\n', 'bright'); - - logInfo('Configuration', ''); - logInfo(' Chain ID', CHAIN_ID); - logInfo(' Bundler URL', BUNDLER_URL); - logInfo(' Bundler API Key', BUNDLER_API_KEY ? 'Set' : 'Not Set'); - - // Validate private key - if (PRIVATE_KEY === '0x' + '0'.repeat(64)) { - log('\n⚠️ WARNING: Using default private key. Set PRIVATE_KEY in .env file!', 'yellow'); - } - - try { - // Run examples - await example1_NonDelegatedEOA(); - await new Promise(resolve => setTimeout(resolve, 2000)); - - await example2_DelegatedEOA(); - await new Promise(resolve => setTimeout(resolve, 2000)); - - await example3_CompareSignatures(); - await new Promise(resolve => setTimeout(resolve, 2000)); - - await example4_DifferentMessageTypes(); - - log('\n' + '='.repeat(60), 'bright'); - log('All examples completed successfully!', 'green'); - log('='.repeat(60) + '\n', 'bright'); - } catch (error: any) { - log('\n' + '='.repeat(60), 'red'); - log('Examples failed!', 'red'); - log('='.repeat(60) + '\n', 'red'); - process.exit(1); - } -} - -// Run if executed directly -if (require.main === module) { - main().catch((error) => { - console.error('Unhandled error:', error); - process.exit(1); - }); -} - -export { - example1_NonDelegatedEOA, - example2_DelegatedEOA, - example3_CompareSignatures, - example4_DifferentMessageTypes, -}; - diff --git a/example/validate-example.js b/example/validate-example.js deleted file mode 100644 index 34efd15..0000000 --- a/example/validate-example.js +++ /dev/null @@ -1,52 +0,0 @@ -// Simple validation script to demonstrate the example structure -const fs = require('fs'); -const path = require('path'); - -console.log('✓ Validating signMessage-example.ts structure...\n'); - -const exampleFile = path.join(__dirname, 'src', 'signMessage-example.ts'); -const content = fs.readFileSync(exampleFile, 'utf8'); - -// Check for key components -const checks = [ - { name: 'EIP-6492 format verification function', pattern: /function verifyEIP6492Format/ }, - { name: 'Signature component extraction', pattern: /function extractSignatureComponents/ }, - { name: 'Example 1: Non-Delegated EOA', pattern: /async function example1_NonDelegatedEOA/ }, - { name: 'Example 2: Delegated EOA', pattern: /async function example2_DelegatedEOA/ }, - { name: 'Example 3: Compare Signatures', pattern: /async function example3_CompareSignatures/ }, - { name: 'Example 4: Different Message Types', pattern: /async function example4_DifferentMessageTypes/ }, - { name: 'Main function', pattern: /async function main/ }, - { name: 'TransactionKit import', pattern: /import.*TransactionKit.*from.*@etherspot\/transaction-kit/ }, - { name: 'signMessage usage', pattern: /signMessage\(/ }, - { name: 'EIP-6492 magic prefix check', pattern: /0x6492/ }, -]; - -let passed = 0; -let failed = 0; - -checks.forEach(check => { - if (check.pattern.test(content)) { - console.log(`✓ ${check.name}`); - passed++; - } else { - console.log(`✗ ${check.name}`); - failed++; - } -}); - -console.log(`\n${'='.repeat(60)}`); -console.log(`Results: ${passed} passed, ${failed} failed`); -console.log(`${'='.repeat(60)}\n`); - -if (failed === 0) { - console.log('✅ All structure checks passed!'); - console.log('\nThe example file is properly structured and ready to use.'); - console.log('\nTo run the example:'); - console.log(' 1. Install dependencies: npm install'); - console.log(' 2. Set up .env file with your configuration'); - console.log(' 3. Run: npx ts-node src/signMessage-example.ts\n'); - process.exit(0); -} else { - console.log('❌ Some checks failed. Please review the example file.'); - process.exit(1); -} diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index 0e96da4..e93d271 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -4,14 +4,15 @@ import { KERNEL_V3_3, KernelVersionToAddressesMap, } from '@zerodev/sdk/constants'; +import type { Address, SignableMessage } from 'viem'; import { + encodeAbiParameters, isAddress, toHex, toRlp, zeroAddress, } from 'viem'; import { SignAuthorizationReturnType, type LocalAccount } from 'viem/accounts'; -import type { Address, Hex, SignableMessage } from 'viem'; // interfaces import { @@ -662,12 +663,6 @@ export class EtherspotTransactionKit implements IInitial { } } - /** - * Creates an EIP-6492 enabled account wrapper that intercepts signMessage calls. - * When signMessage is called on this account, it automatically wraps the signature with EIP-6492 format. - * - * @private - */ /** * Creates an EIP-6492 enabled account wrapper that intercepts signMessage calls. * When signMessage is called on this account, it automatically wraps the signature with EIP-6492 format. @@ -684,6 +679,7 @@ export class EtherspotTransactionKit implements IInitial { await this.#etherspotProvider.getWalletClient(signChainId); // Capture 'this' context for use in the wrapper + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // Create a wrapper account that intercepts signMessage calls @@ -709,15 +705,12 @@ export class EtherspotTransactionKit implements IInitial { let authorization: SignAuthorizationReturnType | undefined; - if (isAlreadyInstalled) { - const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] - .accountImplementationAddress as `0x${string}`; - - authorization = await bundlerClient.signAuthorization({ - account: owner, - contractAddress: delegateAddress, - }); - } else { + // Get authorization for EIP-6492 wrapper + // We always need authorization data for the EIP-6492 signature format, + // regardless of whether the account is already installed or not + if (!isAlreadyInstalled) { + // When not installed, use delegateSmartAccountToEoa to get authorization + // This ensures proper initialization and state management const delegateResult = await self.delegateSmartAccountToEoa({ chainId: signChainId, delegateImmediately: false, @@ -730,51 +723,63 @@ export class EtherspotTransactionKit implements IInitial { } authorization = delegateResult.authorization; + } else { + // When already installed, sign authorization directly + const delegateAddress = KernelVersionToAddressesMap[KERNEL_V3_3] + .accountImplementationAddress as `0x${string}`; + + authorization = await bundlerClient.signAuthorization({ + account: owner, + contractAddress: delegateAddress, + }); } if (!authorization) { - throw new Error( - 'Authorization is required but was not created' - ); + throw new Error('Authorization is required but was not created'); } - // Encode the authorization as deployment data - const publicClient = - await self.#etherspotProvider.getPublicClient(signChainId); - const nonce = await publicClient.getTransactionCount({ - address: owner.address, - }); + // Encode authorization for EIP-7702 + // Normalize authorization fields and provide safe fallbacks + const authChainIdHex = toHex(authorization.chainId); + const authNonceHex = toHex(authorization.nonce); + const authVHex = toHex(authorization.v ?? 0); + const authR = authorization.r; + const authS = authorization.s; + const authAddress = authorization.address; - // Encode authorization for EIP-7702 deployment data - // Authorization structure: [chainId, address, nonce, r, s, v] - // For EIP-6492, we need RLP-encoded transaction: [chainId, nonce, to (zeroAddress), value (0), data (authorization), gasLimit, gasPrice] - // The authorization itself is RLP-encoded: [chainId, address, nonce, r, s, v] + // Encode authorization as RLP: [chainId, address, nonce, r, s, v] const authorizationRlp = toRlp([ - toHex(authorization.chainId), - authorization.address, - toHex(authorization.nonce), - authorization.r, - authorization.s, - toHex(authorization.v), + authChainIdHex, + authAddress, + authNonceHex, + authR, + authS, + authVHex, ]); - const deploymentTx = [ - toHex(signChainId), - toHex(nonce), - zeroAddress, - '0x0', - authorizationRlp, - '0x0', - '0x0', - ]; - - const deploymentData = toRlp(deploymentTx); + // For EIP-6492, we use the factory variant: + // abi.encode((create2Factory, factoryCalldata, originalERC1271Signature)) + // For EIP-7702, we use zero address as factory (no CREATE2 factory needed) + // and the authorization RLP as the factoryCalldata + const factoryAddress = zeroAddress; // EIP-7702 doesn't use a CREATE2 factory + const factoryCalldata = authorizationRlp; // Authorization data to activate EIP-7702 account + + // ABI encode the tuple: (address factoryAddress, bytes factoryCalldata, bytes originalERC1271Signature) + const encodedWrapper = encodeAbiParameters( + [ + { name: 'factoryAddress', type: 'address' }, + { name: 'factoryCalldata', type: 'bytes' }, + { name: 'originalERC1271Signature', type: 'bytes' }, + ], + [factoryAddress, factoryCalldata, standardSignature] + ); - // Create EIP-6492 format: 0x6492 + signature + deployment_data - const signatureBytes = standardSignature.slice(2); - const deploymentBytes = deploymentData.slice(2); + // EIP-6492 magic bytes: 32-byte suffix (0x6492 repeated 16 times) + const magicBytes = + '0x6492649264926492649264926492649264926492649264926492649264926492' as `0x${string}`; - return `0x6492${signatureBytes}${deploymentBytes}` as `0x${string}`; + // EIP-6492 format: encodedWrapper || magicBytes + return (encodedWrapper + magicBytes.slice(2)) as `0x${string}`; }, }; @@ -794,7 +799,8 @@ export class EtherspotTransactionKit implements IInitial { * @remarks * - Only available in 'delegatedEoa' wallet mode. * - Creates an EIP-6492 compatible signature that wraps the standard signature with deployment data. - * - The signature format is: `0x6492` + * - The signature format follows EIP-6492: `abi.encode((factoryAddress, factoryCalldata, originalSignature)) || magicBytes` + * where magicBytes is the 32-byte suffix `0x6492...` (repeated 16 times) * - If the EOA is not yet designated, this will create the authorization automatically. * - The signature can be validated by contracts that support EIP-6492, even before the smart account is activated. * - Uses WalletClient.signMessage() which delegates to the underlying provider/transport if the account supports it. @@ -812,11 +818,7 @@ export class EtherspotTransactionKit implements IInitial { const walletMode = this.#etherspotProvider.getWalletMode(); const signChainId = chainId || this.#etherspotProvider.getChainId(); - log( - 'signMessage(): Called', - { message, signChainId }, - this.debugMode - ); + log('signMessage(): Called', { message, signChainId }, this.debugMode); if (walletMode !== 'delegatedEoa') { this.throwError( @@ -829,28 +831,23 @@ export class EtherspotTransactionKit implements IInitial { try { // Get the owner account (EOA) const owner = await this.#etherspotProvider.getOwnerAccount(signChainId); - + // Create EIP-6492 enabled account wrapper - const eip6492Account = await this.createEIP6492Account(owner, signChainId); - - // Get wallet client - const walletClient = - await this.#etherspotProvider.getWalletClient(signChainId); - - // Sign the message using the EIP-6492 enabled account wrapper - // The wrapper account's signMessage method automatically wraps the signature with EIP-6492 format - // This allows walletClient.signMessage() to directly return EIP-6492 formatted signatures - const signature = await walletClient.signMessage({ - account: eip6492Account, - message: message as string, + const eip6492Account = await this.createEIP6492Account( + owner, + signChainId + ); + + // Call the wrapper's signMessage directly so the EIP-6492 logic always runs, + // regardless of wallet client behavior or mocks. + const signature = await eip6492Account.signMessage({ + message: message as SignableMessage, }); log( 'signMessage(): EIP-6492 signature created', - { - signatureLength: signature.length, - signaturePrefix: signature.substring(0, 6), - startsWith6492: signature.startsWith('0x6492') + { + signatureLength: signature.length, }, this.debugMode ); diff --git a/package-lock.json b/package-lock.json index 578a122..013f289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@etherspot/transaction-kit", - "version": "2.2.0", + "version": "2.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@etherspot/transaction-kit", - "version": "2.2.0", + "version": "2.1.4", "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", diff --git a/package.json b/package.json index f45f422..affc3ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@etherspot/transaction-kit", "description": "Framework-agnostic Etherspot Transaction Kit", - "version": "2.2.0", + "version": "2.1.4", "main": "dist/cjs/index.js", "scripts": { "rollup:build": "NODE_OPTIONS=--max-old-space-size=8192 rollup -c --bundleConfigAsCjs", From 0677db9e5c4388cec0d4fb1fab2de6cc28f143b1 Mon Sep 17 00:00:00 2001 From: RanaBug Date: Tue, 9 Dec 2025 17:49:00 +0000 Subject: [PATCH 7/7] no authorization error improved --- lib/TransactionKit.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/TransactionKit.ts b/lib/TransactionKit.ts index e93d271..06ea789 100644 --- a/lib/TransactionKit.ts +++ b/lib/TransactionKit.ts @@ -735,7 +735,11 @@ export class EtherspotTransactionKit implements IInitial { } if (!authorization) { - throw new Error('Authorization is required but was not created'); + throw new Error( + 'Failed to create authorization for EIP-6492 signature. ' + + 'This may be due to network issues, bundler API problems, or account configuration. ' + + 'Please check your network connection and bundler API key, or try again later.' + ); } // Encode authorization for EIP-7702