Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ft: adding support of sign typed data along with eip 6942 #138

Merged
merged 2 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
70 changes: 70 additions & 0 deletions examples/26-sign-typed-data.ts
Original file line number Diff line number Diff line change
@@ -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());
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@etherspot/prime-sdk",
"version": "1.9.1",
"version": "1.9.2",
"description": "Etherspot Prime (Account Abstraction) SDK",
"keywords": [
"ether",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 8 additions & 4 deletions src/sdk/base/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -17,6 +17,7 @@ export interface BaseApiParams {
entryPointAddress: string;
accountAddress?: string;
overheads?: Partial<GasOverheads>;
factoryAddress?: string;
walletProvider: WalletProviderLike,
optionsLike?: SdkOptions
}
Expand Down Expand Up @@ -57,6 +58,7 @@ export abstract class BaseAccountAPI {
accountAddress?: string;
paymasterAPI?: PaymasterAPI;
factoryUsed: Factory;
factoryAddress?: string;

/**
* base constructor.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
}
5 changes: 3 additions & 2 deletions src/sdk/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -245,7 +246,7 @@ export class PrimeSdk {
}

async signTypedData(
DataFields: TypedDataField[],
DataFields: MessagePayload,
message: any
) {
return this.etherspotWallet.signTypedData(DataFields, message);
Expand Down
5 changes: 2 additions & 3 deletions src/sdk/wallet/providers/dynamic.wallet-provider.ts
Original file line number Diff line number Diff line change
@@ -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<string>();
Expand All @@ -21,7 +20,7 @@ export abstract class DynamicWalletProvider implements WalletProvider {

abstract signMessage(message: any): Promise<string>;

abstract signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise<string>;
abstract signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise<string>;

protected setAddress(address: string): void {
this.address$.next(prepareAddress(address));
Expand Down
11 changes: 9 additions & 2 deletions src/sdk/wallet/providers/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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<string, TypedDataField[]>;
primaryType: string;
};

export interface WalletProvider {
readonly type?: string;
readonly wallet?: Wallet;
Expand All @@ -12,7 +18,7 @@ export interface WalletProvider {
readonly networkName$?: UniqueSubject<NetworkNames>;

signMessage(message: BytesLike): Promise<string>;
signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise<string>;
signTypedData(typedData: MessagePayload, message: any, factoryAddress?: string, initCode?: string): Promise<string>;
}

export interface Web3Provider {
Expand All @@ -31,6 +37,7 @@ export interface WalletConnectConnector {
accounts: string[];
chainId: number;
signPersonalMessage(params: any[]): Promise<any>;
request<T = unknown>(args: RequestArguments): Promise<T>;
on(event: string, callback: (error: Error | null, payload: any | null) => void): void;
}

Expand Down
28 changes: 24 additions & 4 deletions src/sdk/wallet/providers/key.wallet-provider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,7 +19,27 @@ export class KeyWalletProvider implements WalletProvider {
return this.wallet.signMessage(message);
}

async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise<string> {
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<string> {
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;
}
}
59 changes: 22 additions & 37 deletions src/sdk/wallet/providers/meta-mask.wallet-provider.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -57,43 +58,27 @@ export class MetaMaskWalletProvider extends DynamicWalletProvider {
]);
}

async signTypedData(typedData: TypedDataField[], message: any, accountAddress: string): Promise<string> {
const chainId = await this.sendRequest<string>('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<string> {
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;
}

Expand Down
Loading
Loading