diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d258f58..e0c2853a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## [1.9.2] - 2024-12-21 +### New +- Added support for signTypedData along with Eip-6942 + ## [1.9.1] - 2024-12-13 ### Bug Fix - Added `0x` before sending userOp to paymaster diff --git a/examples/26-sign-typed-data.ts b/examples/26-sign-typed-data.ts new file mode 100644 index 00000000..0b082149 --- /dev/null +++ b/examples/26-sign-typed-data.ts @@ -0,0 +1,70 @@ +import { ethers } from "ethers"; +import { EtherspotBundler, MessagePayload, PrimeSdk } from "../src"; +import * as dotenv from 'dotenv'; +dotenv.config(); + +async function main() { + + const bundlerApiKey = "eyJvcmciOiI2NTIzZjY5MzUwOTBmNzAwMDFiYjJkZWIiLCJpZCI6IjMxMDZiOGY2NTRhZTRhZTM4MGVjYjJiN2Q2NDMzMjM4IiwiaCI6Im11cm11cjEyOCJ9"; + + const primeSdk = new PrimeSdk({ privateKey: process.env.WALLET_PRIVATE_KEY }, { + chainId: Number(process.env.CHAIN_ID), + bundlerProvider: new EtherspotBundler(Number(process.env.CHAIN_ID), bundlerApiKey) + }); + + const address: string = await primeSdk.getCounterFactualAddress(); + console.log('\x1b[33m%s\x1b[0m', `EtherspotWallet address: ${address}`); + + const types = { + Mail: [ + {name: 'from', type: 'Person'}, + {name: 'to', type: 'Person'}, + {name: 'contents', type: 'string'}, + ], + Person: [ + {name: 'name', type: 'string'}, + {name: 'wallet', type: 'address'} + ], + } + + const domainSeparator = { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + } + + const typedData: MessagePayload = { + domain: domainSeparator, + primaryType: 'Person', + types + }; + + const message = { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + contents: "Hello, Bob!" + } + + const signature = await primeSdk.signTypedData(typedData, message); + console.log('signature:: ', signature); + + // will work only if wallet is deployed already. + const signer = ethers.utils.verifyTypedData( + domainSeparator, + types, + message, + signature + ); + console.log('signer:: ', signer); +} + +main() + .catch(console.error) + .finally(() => process.exit()); \ No newline at end of file diff --git a/package.json b/package.json index b681d637..56dc1ce1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@etherspot/prime-sdk", - "version": "1.9.1", + "version": "1.9.2", "description": "Etherspot Prime (Account Abstraction) SDK", "keywords": [ "ether", @@ -43,6 +43,7 @@ "21-get-multiple-accounts": "./node_modules/.bin/ts-node ./examples/21-get-multiple-accounts", "22-concurrent-userops": "./node_modules/.bin/ts-node ./examples/22-concurrent-userops", "25-get-quotes": "./node_modules/.bin/ts-node ./examples/25-get-quotes", + "26-sign-typed-data": "./node_modules/.bin/ts-node ./examples/26-sign-typed-data", "format": "prettier --write \"{src,test,examples}/**/*.ts\"", "lint": "eslint \"{src,test,examples}/**/*.ts\"", "lint-fix": "npm run lint -- --fix", diff --git a/src/sdk/base/BaseAccountAPI.ts b/src/sdk/base/BaseAccountAPI.ts index e43448f6..9a53cabf 100644 --- a/src/sdk/base/BaseAccountAPI.ts +++ b/src/sdk/base/BaseAccountAPI.ts @@ -1,4 +1,4 @@ -import { ethers, BigNumber, BigNumberish, TypedDataField } from 'ethers'; +import { ethers, BigNumber, BigNumberish } from 'ethers'; import { BehaviorSubject } from 'rxjs'; import { Provider } from '@ethersproject/providers'; import { EntryPoint, EntryPoint__factory, INonceManager, INonceManager__factory } from '../contracts'; @@ -8,7 +8,7 @@ import { resolveProperties } from 'ethers/lib/utils'; import { PaymasterAPI } from './PaymasterAPI'; import { ErrorSubject, Exception, getUserOpHash, NotPromise, packUserOp } from '../common'; import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas'; -import { Factory, isWalletProvider, Network, NetworkNames, NetworkService, SdkOptions, SignMessageDto, State, StateService, validateDto, WalletProviderLike, WalletService } from '..'; +import { Factory, isWalletProvider, MessagePayload, Network, NetworkNames, NetworkService, SdkOptions, SignMessageDto, State, StateService, validateDto, WalletProviderLike, WalletService } from '..'; import { Context } from '../context'; import { PaymasterResponse } from './VerifyingPaymasterAPI'; @@ -17,6 +17,7 @@ export interface BaseApiParams { entryPointAddress: string; accountAddress?: string; overheads?: Partial; + factoryAddress?: string; walletProvider: WalletProviderLike, optionsLike?: SdkOptions } @@ -57,6 +58,7 @@ export abstract class BaseAccountAPI { accountAddress?: string; paymasterAPI?: PaymasterAPI; factoryUsed: Factory; + factoryAddress?: string; /** * base constructor. @@ -97,6 +99,7 @@ export abstract class BaseAccountAPI { this.overheads = params.overheads; this.entryPointAddress = params.entryPointAddress; this.accountAddress = params.accountAddress; + this.factoryAddress = params.factoryAddress; // factory "connect" define the contract address. the contract "connect" defines the "from" address. this.entryPointView = EntryPoint__factory.connect(params.entryPointAddress, params.provider).connect( @@ -509,7 +512,8 @@ export abstract class BaseAccountAPI { return null; } - async signTypedData(types: TypedDataField[], message: any) { - return this.services.walletService.signTypedData(types, message, this.accountAddress); + async signTypedData(types: MessagePayload, message: any) { + const initCode = await this.getInitCode(); + return this.services.walletService.signTypedData(types, message, this.factoryAddress, initCode); } } diff --git a/src/sdk/sdk.ts b/src/sdk/sdk.ts index 968cfb3d..eaf03ba6 100644 --- a/src/sdk/sdk.ts +++ b/src/sdk/sdk.ts @@ -4,13 +4,14 @@ import { EthereumProvider, isWalletConnectProvider, isWalletProvider, + MessagePayload, WalletConnect2WalletProvider, WalletProviderLike } from './wallet'; import { Factory, PaymasterApi, SdkOptions } from './interfaces'; import { Network } from "./network"; import { BatchUserOpsRequest, Exception, getGasFee, onRampApiKey, openUrl, UserOpsRequest } from "./common"; -import { BigNumber, BigNumberish, ethers, providers, TypedDataField } from 'ethers'; +import { BigNumber, BigNumberish, ethers, providers } from 'ethers'; import { Networks, onRamperAllNetworks } from './network/constants'; import { UserOperationStruct } from './contracts/account-abstraction/contracts/core/BaseAccount'; import { EtherspotWalletAPI, HttpRpcClient, VerifyingPaymasterAPI } from './base'; @@ -245,7 +246,7 @@ export class PrimeSdk { } async signTypedData( - DataFields: TypedDataField[], + DataFields: MessagePayload, message: any ) { return this.etherspotWallet.signTypedData(DataFields, message); diff --git a/src/sdk/wallet/providers/dynamic.wallet-provider.ts b/src/sdk/wallet/providers/dynamic.wallet-provider.ts index 4c73eed9..236956b2 100644 --- a/src/sdk/wallet/providers/dynamic.wallet-provider.ts +++ b/src/sdk/wallet/providers/dynamic.wallet-provider.ts @@ -1,7 +1,6 @@ import { NetworkNames, prepareNetworkName } from '../../network'; import { prepareAddress, UniqueSubject } from '../../common'; -import { WalletProvider } from './interfaces'; -import { TypedDataField } from 'ethers'; +import { MessagePayload, WalletProvider } from './interfaces'; export abstract class DynamicWalletProvider implements WalletProvider { readonly address$ = new UniqueSubject(); @@ -21,7 +20,7 @@ export abstract class DynamicWalletProvider implements WalletProvider { abstract signMessage(message: any): Promise; - abstract signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise; + abstract signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise; protected setAddress(address: string): void { this.address$.next(prepareAddress(address)); diff --git a/src/sdk/wallet/providers/interfaces.ts b/src/sdk/wallet/providers/interfaces.ts index 5d01514c..77a0987b 100644 --- a/src/sdk/wallet/providers/interfaces.ts +++ b/src/sdk/wallet/providers/interfaces.ts @@ -1,8 +1,14 @@ -import { BytesLike, TypedDataField, Wallet } from 'ethers'; +import { BytesLike, TypedDataField, Wallet, TypedDataDomain } from 'ethers'; import type UniversalProvider from '@walletconnect/universal-provider'; import { UniqueSubject } from '../../common'; import { NetworkNames } from '../../network'; +export type MessagePayload = { + domain: TypedDataDomain; + types: Record; + primaryType: string; +}; + export interface WalletProvider { readonly type?: string; readonly wallet?: Wallet; @@ -12,7 +18,7 @@ export interface WalletProvider { readonly networkName$?: UniqueSubject; signMessage(message: BytesLike): Promise; - signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise; + signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise; } export interface Web3Provider { @@ -31,6 +37,7 @@ export interface WalletConnectConnector { accounts: string[]; chainId: number; signPersonalMessage(params: any[]): Promise; + request(args: RequestArguments): Promise; on(event: string, callback: (error: Error | null, payload: any | null) => void): void; } diff --git a/src/sdk/wallet/providers/key.wallet-provider.ts b/src/sdk/wallet/providers/key.wallet-provider.ts index 5c56cd5a..82a749ed 100644 --- a/src/sdk/wallet/providers/key.wallet-provider.ts +++ b/src/sdk/wallet/providers/key.wallet-provider.ts @@ -1,5 +1,5 @@ -import { Wallet, BytesLike, TypedDataField } from 'ethers'; -import { WalletProvider } from './interfaces'; +import { Wallet, BytesLike, utils } from 'ethers'; +import { MessagePayload, WalletProvider } from './interfaces'; export class KeyWalletProvider implements WalletProvider { readonly type = 'Key'; @@ -19,7 +19,27 @@ export class KeyWalletProvider implements WalletProvider { return this.wallet.signMessage(message); } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - throw new Error('Not supported in this connectedProvider'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const {domain, types} = typedData; + + // EIP Domain has to be removed because ethers will add it using `domain` + if(types["EIP712Domain"]) { + delete typedData.types["EIP712Domain"]; + } + const signature = await this.wallet._signTypedData( + domain, + types, + message + ); + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } + return signature; } } diff --git a/src/sdk/wallet/providers/meta-mask.wallet-provider.ts b/src/sdk/wallet/providers/meta-mask.wallet-provider.ts index 6c981141..990e85d1 100644 --- a/src/sdk/wallet/providers/meta-mask.wallet-provider.ts +++ b/src/sdk/wallet/providers/meta-mask.wallet-provider.ts @@ -1,6 +1,7 @@ -import { BytesLike, TypedDataField } from 'ethers'; +import { BytesLike, utils } from 'ethers'; import { toHex } from '../../common'; import { DynamicWalletProvider } from './dynamic.wallet-provider'; +import { MessagePayload } from './interfaces'; declare const window: Window & { ethereum: { @@ -57,43 +58,27 @@ export class MetaMaskWalletProvider extends DynamicWalletProvider { ]); } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - const chainId = await this.sendRequest('eth_chainId'); - const domainSeparator = { - name: "EtherspotWallet", - version: "2.0.0", - chainId: chainId, - verifyingContract: accountAddress - }; - let signature = await this.sendRequest('eth_signTypedData_v4', [ + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const {domain, types, primaryType} = typedData; + + const msgParams = JSON.stringify({ + domain, + message, + primaryType, + types + }); + const signature = await this.sendRequest('eth_signTypedData_v4', [ this.address, - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "message": typedData - }, - "primaryType": "message", - "domain": domainSeparator, - "message": message - } - ]) + msgParams + ]); + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } return signature; } diff --git a/src/sdk/wallet/providers/wallet-connect-2.wallet-provider.ts b/src/sdk/wallet/providers/wallet-connect-2.wallet-provider.ts index f6809b7b..632f6d2f 100644 --- a/src/sdk/wallet/providers/wallet-connect-2.wallet-provider.ts +++ b/src/sdk/wallet/providers/wallet-connect-2.wallet-provider.ts @@ -1,7 +1,7 @@ -import { BytesLike, TypedDataField } from 'ethers'; +import { BytesLike, utils } from 'ethers'; import { toHex } from '../../common'; import { DynamicWalletProvider } from './dynamic.wallet-provider'; -import { EthereumProvider } from './interfaces'; +import { EthereumProvider, MessagePayload } from './interfaces'; export class WalletConnect2WalletProvider extends DynamicWalletProvider { constructor(readonly provider: EthereumProvider) { @@ -38,47 +38,31 @@ export class WalletConnect2WalletProvider extends DynamicWalletProvider { return typeof response === 'string' ? response : null; } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - - const domainSeparator = { - name: "EtherspotWallet", - version: "2.0.0", - chainId: this.provider.chainId, - verifyingContract: accountAddress - }; - const signature = await this.provider.signer.request({ - method: 'eth_signTypedData_v4', + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const {domain, types, primaryType} = typedData; + + const msgParams = JSON.stringify({ + domain, + message, + primaryType, + types + }); + const signature: string = await this.provider.signer.request({ + method: 'eth_signTypedData_v4', params: [ this.address, - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "message": typedData - }, - "primaryType": "message", - "domain": domainSeparator, - "message": message - } + msgParams ] }) - return typeof signature === 'string' ? signature : null; + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } + return signature; } protected updateSessionHandler(error: Error, payload: { params: { accounts: string[]; chainId: number } }): void { diff --git a/src/sdk/wallet/providers/wallet-connect.wallet-provider.ts b/src/sdk/wallet/providers/wallet-connect.wallet-provider.ts index d184e533..0839ed8a 100644 --- a/src/sdk/wallet/providers/wallet-connect.wallet-provider.ts +++ b/src/sdk/wallet/providers/wallet-connect.wallet-provider.ts @@ -1,7 +1,7 @@ -import { BytesLike, TypedDataField } from 'ethers'; +import { BytesLike, utils } from 'ethers'; import { toHex } from '../../common'; import { DynamicWalletProvider } from './dynamic.wallet-provider'; -import { WalletConnectConnector } from './interfaces'; +import { MessagePayload, WalletConnectConnector } from './interfaces'; export class WalletConnectWalletProvider extends DynamicWalletProvider { static connect(connector: WalletConnectConnector): WalletConnectWalletProvider { @@ -42,8 +42,28 @@ export class WalletConnectWalletProvider extends DynamicWalletProvider { return response || null; } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - throw new Error('Not supported on this provider') + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const signature: string = await this.connector.request({ + method: 'eth_signTypedData_v4', + params: [ + this.address, + { + domain: typedData.domain, + message, + primaryType: typedData.primaryType, + types: typedData.types, + } + ] + }); + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } + return signature; } protected updateSessionHandler(error: Error, payload: { params: { accounts: string[]; chainId: number } }): void { diff --git a/src/sdk/wallet/providers/web3.wallet-provider.ts b/src/sdk/wallet/providers/web3.wallet-provider.ts index ae8a4a67..35d57449 100644 --- a/src/sdk/wallet/providers/web3.wallet-provider.ts +++ b/src/sdk/wallet/providers/web3.wallet-provider.ts @@ -1,7 +1,7 @@ -import { BytesLike, TypedDataField } from 'ethers'; +import { BytesLike, utils } from 'ethers'; import { prepareAddress, toHex } from '../../common'; import { NetworkNames, prepareNetworkName } from '../../network'; -import { Web3Provider } from './interfaces'; +import { MessagePayload, Web3Provider } from './interfaces'; import { DynamicWalletProvider } from './dynamic.wallet-provider'; export class Web3WalletProvider extends DynamicWalletProvider { @@ -58,43 +58,27 @@ export class Web3WalletProvider extends DynamicWalletProvider { ); } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - const chainId = await this.sendRequest('eth_chainId'); - const domainSeparator = { - name: "EtherspotWallet", - version: "2.0.0", - chainId: chainId, - verifyingContract: accountAddress - }; - let signature = await this.sendRequest('eth_signTypedData_v4', [ + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const {domain, types, primaryType} = typedData; + + const msgParams = JSON.stringify({ + domain, + message, + primaryType, + types + }); + const signature = await this.sendRequest('eth_signTypedData_v4', [ this.address, - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "message": typedData - }, - "primaryType": "message", - "domain": domainSeparator, - "message": message - } - ]) + msgParams + ]); + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } return signature; } diff --git a/src/sdk/wallet/providers/web3eip1193.wallet-provider.ts b/src/sdk/wallet/providers/web3eip1193.wallet-provider.ts index 3a31f139..d74226cc 100644 --- a/src/sdk/wallet/providers/web3eip1193.wallet-provider.ts +++ b/src/sdk/wallet/providers/web3eip1193.wallet-provider.ts @@ -1,7 +1,7 @@ -import { BytesLike, TypedDataField } from 'ethers'; +import { BytesLike, utils } from 'ethers'; import { prepareAddress, toHex } from '../../common'; import { NetworkNames, prepareNetworkName } from '../../network'; -import { Web3eip1193Provider } from './interfaces'; +import { MessagePayload, Web3eip1193Provider } from './interfaces'; import { DynamicWalletProvider } from './dynamic.wallet-provider'; export class Web3eip1193WalletProvider extends DynamicWalletProvider { @@ -50,43 +50,27 @@ export class Web3eip1193WalletProvider extends DynamicWalletProvider { return this.sendRequest('personal_sign', [toHex(message), this.address]); } - async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise { - const chainId = await this.sendRequest('eth_chainId'); - const domainSeparator = { - name: "EtherspotWallet", - version: "2.0.0", - chainId: chainId, - verifyingContract: accountAddress - }; - let signature = await this.sendRequest('eth_signTypedData_v4', [ + async signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + const {domain, types, primaryType} = typedData; + + const msgParams = JSON.stringify({ + domain, + message, + primaryType, + types + }); + const signature = await this.sendRequest('eth_signTypedData_v4', [ this.address, - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "message": typedData - }, - "primaryType": "message", - "domain": domainSeparator, - "message": message - } - ]) + msgParams + ]); + + if (initCode !== '0x') { + const abiCoderResult = utils.defaultAbiCoder.encode( + ['address', 'bytes', 'bytes'], + [factoryAddress, initCode, signature] + ); + return abiCoderResult + '6492649264926492649264926492649264926492649264926492649264926492'; //magicBytes + } return signature; } diff --git a/src/sdk/wallet/wallet.service.ts b/src/sdk/wallet/wallet.service.ts index 86423381..b00a8ba6 100644 --- a/src/sdk/wallet/wallet.service.ts +++ b/src/sdk/wallet/wallet.service.ts @@ -1,8 +1,8 @@ import { Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; -import { BytesLike, ethers, providers, Wallet as EtherWallet, TypedDataField } from 'ethers'; +import { BytesLike, ethers, providers, Wallet as EtherWallet } from 'ethers'; import { Service, ObjectSubject } from '../common'; -import { WalletProvider, WalletProviderLike, KeyWalletProvider, WalletLike } from './providers'; +import { WalletProvider, WalletProviderLike, KeyWalletProvider, WalletLike, MessagePayload } from './providers'; import { Wallet, WalletOptions } from './interfaces'; export class WalletService extends Service { @@ -45,8 +45,8 @@ export class WalletService extends Service { return this.provider ? this.provider.signMessage(message) : null; } - async signTypedData(types: TypedDataField[], message: any, accountAddress: string): Promise { - return this.provider ? this.provider.signTypedData(types, message, accountAddress) : null; + async signTypedData(types: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise { + return this.provider ? this.provider.signTypedData(types, message, factoryAddress, initCode) : null; } protected switchWalletProvider(providerLike: WalletProviderLike): void {