import { Signer } from "@ethersproject/abstract-signer"; import { BigNumber } from "@ethersproject/bignumber"; import { Bytes } from "@ethersproject/bytes"; import { _TypedDataEncoder } from "@ethersproject/hash"; import { Deferrable } from "@ethersproject/properties"; import type { BlockTag, FeeData, JsonRpcProvider, Provider, TransactionRequest, TransactionResponse, } from "@ethersproject/providers"; import { isJsonRpcProvider, TypedDataDomain, TypedDataSigner, TypedDataTypes, } from "./types/ethers"; /** * Wrapper around a TypedDataSigner Signer object that implements `_signTypedData` using * `eth_signTypedData_v3` instead of `eth_signTypedData_v4`. * * Takes a Signer instance on creation. * All other Signer methods are proxied to initial instance. */ export class TypedDataV3Signer implements TypedDataSigner { signer: Signer; provider: JsonRpcProvider; _isSigner = true; constructor(signer: Signer) { this.signer = signer; if (!signer.provider) { throw new Error("Signer does not have a provider set"); } if (!isJsonRpcProvider(signer.provider)) { throw new Error("Provider must be of type JsonRpcProvider"); } this.provider = signer.provider; } async _signTypedData( domain: TypedDataDomain, types: TypedDataTypes, data: Record, ): Promise { const populated = await _TypedDataEncoder.resolveNames( domain, types, data, (name: string) => this.resolveName(name), ); const payload = _TypedDataEncoder.getPayload( populated.domain, types, populated.value, ); const msg = JSON.stringify(payload); const address = await this.getAddress(); // Actual signing return (await this.provider.send("eth_signTypedData_v3", [ address.toLowerCase(), msg, ])) as string; } // --- start boilerplate proxy methods --- getAddress(): Promise { return this.signer.getAddress(); } signMessage(message: string | Bytes): Promise { return this.signer.signMessage(message); } signTransaction( transaction: Deferrable, ): Promise { return this.signer.signTransaction(transaction); } connect(provider: Provider): Signer { return this.signer.connect(provider); } getBalance(blockTag?: BlockTag): Promise { return this.signer.getBalance(blockTag); } getTransactionCount(blockTag?: BlockTag): Promise { return this.signer.getTransactionCount(blockTag); } estimateGas(transaction: Deferrable): Promise { return this.signer.estimateGas(transaction); } call( transaction: Deferrable, blockTag?: BlockTag, ): Promise { return this.signer.call(transaction, blockTag); } sendTransaction( transaction: Deferrable, ): Promise { return this.signer.sendTransaction(transaction); } getChainId(): Promise { return this.signer.getChainId(); } getGasPrice(): Promise { return this.signer.getGasPrice(); } getFeeData(): Promise { return this.signer.getFeeData(); } resolveName(name: string): Promise { return this.signer.resolveName(name); } checkTransaction( transaction: Deferrable, ): Deferrable { return this.signer.checkTransaction(transaction); } populateTransaction( transaction: Deferrable, ): Promise { return this.signer.populateTransaction(transaction); } _checkProvider(operation?: string): void { return this.signer._checkProvider(operation); } // --- end boilerplate proxy methods --- } /** * Wrapper around a TypedDataSigner Signer object that implements `_signTypedData` using * `eth_signTypedData_v4` as usual. * The difference here is that the domain `chainId` is transformed to a `number`. * That's done to circumvent a bug introduced in the latest Metamask version (9.6.0) * that no longer accepts a string for domain `chainId`. * See for more details https://github.com/MetaMask/metamask-extension/issues/11308. * * Takes a Signer instance on creation. * All other Signer methods are proxied to initial instance. */ export class IntChainIdTypedDataV4Signer implements TypedDataSigner { signer: Signer; provider: JsonRpcProvider; _isSigner = true; constructor(signer: Signer) { this.signer = signer; if (!signer.provider) { throw new Error("Signer does not have a provider set"); } if (!isJsonRpcProvider(signer.provider)) { throw new Error("Provider must be of type JsonRpcProvider"); } this.provider = signer.provider; } async _signTypedData( domain: TypedDataDomain, types: TypedDataTypes, data: Record, ): Promise { const populated = await _TypedDataEncoder.resolveNames( domain, types, data, (name: string) => this.resolveName(name), ); const payload = _TypedDataEncoder.getPayload( populated.domain, types, populated.value, ); // Making `chainId` an int since Latest Metamask version (9.6.0) breaks otherwise payload.domain.chainId = parseInt(payload.domain.chainId, 10); const msg = JSON.stringify(payload); const address = await this.getAddress(); // Actual signing return (await this.provider.send("eth_signTypedData_v4", [ address.toLowerCase(), msg, ])) as string; } // --- start boilerplate proxy methods --- getAddress(): Promise { return this.signer.getAddress(); } signMessage(message: string | Bytes): Promise { return this.signer.signMessage(message); } signTransaction( transaction: Deferrable, ): Promise { return this.signer.signTransaction(transaction); } connect(provider: Provider): Signer { return this.signer.connect(provider); } getBalance(blockTag?: BlockTag): Promise { return this.signer.getBalance(blockTag); } getTransactionCount(blockTag?: BlockTag): Promise { return this.signer.getTransactionCount(blockTag); } estimateGas(transaction: Deferrable): Promise { return this.signer.estimateGas(transaction); } call( transaction: Deferrable, blockTag?: BlockTag, ): Promise { return this.signer.call(transaction, blockTag); } sendTransaction( transaction: Deferrable, ): Promise { return this.signer.sendTransaction(transaction); } getChainId(): Promise { return this.signer.getChainId(); } getGasPrice(): Promise { return this.signer.getGasPrice(); } getFeeData(): Promise { return this.signer.getFeeData(); } resolveName(name: string): Promise { return this.signer.resolveName(name); } checkTransaction( transaction: Deferrable, ): Deferrable { return this.signer.checkTransaction(transaction); } populateTransaction( transaction: Deferrable, ): Promise { return this.signer.populateTransaction(transaction); } _checkProvider(operation?: string): void { return this.signer._checkProvider(operation); } // --- end boilerplate proxy methods --- }