import { promisify } from 'es6-promisify'; import Web3 from 'web3'; import { AbstractProvider } from 'web3-core'; import { JsonRpcPayload, JsonRpcResponse, } from 'web3-core-helpers'; import { SignatureTypes, SigningMethod, } from '../types'; import { createTypedSignature, stripHexPrefix } from './helpers'; export abstract class Signer { protected readonly web3: Web3; // ============ Constructor ============ constructor( web3: Web3, ) { this.web3 = web3; } // ============ Functions ============ /** * Returns a signable EIP712 Hash of a struct */ public getEIP712Hash( structHash: string, ): string { const hash: string | null = Web3.utils.soliditySha3( { t: 'bytes2', v: '0x1901' }, { t: 'bytes32', v: this.getDomainHash() as string }, { t: 'bytes32', v: structHash }, ); // Non-null assertion operator is safe, hash is null only on empty input. return hash!; } /** * Returns the EIP712 domain separator hash. */ public abstract getDomainHash(): string; protected async ethSignTypedDataInternal( signer: string, data: {}, signingMethod: SigningMethod, ): Promise<string> { let rpcMethod: string; let rpcData: {}; let provider = this.web3.currentProvider; if (provider === null) { throw new Error('Cannot sign since Web3 currentProvider is null'); } if (typeof provider === 'string') { throw new Error('Cannot sign since Web3 currentProvider is a string'); } provider = provider as AbstractProvider; let sendAsync: (param: JsonRpcPayload) => Promise<JsonRpcResponse>; switch (signingMethod) { case SigningMethod.TypedData: sendAsync = promisify(provider.send!).bind(provider); rpcMethod = 'eth_signTypedData'; rpcData = data; break; case SigningMethod.MetaMask: sendAsync = promisify(provider.sendAsync).bind(provider); rpcMethod = 'eth_signTypedData_v3'; rpcData = JSON.stringify(data); break; case SigningMethod.MetaMaskLatest: sendAsync = promisify(provider.sendAsync).bind(provider); rpcMethod = 'eth_signTypedData_v4'; rpcData = JSON.stringify(data); break; case SigningMethod.CoinbaseWallet: sendAsync = promisify(provider.sendAsync).bind(provider); rpcMethod = 'eth_signTypedData_v4'; rpcData = data; break; default: throw new Error(`Invalid signing method ${signingMethod}`); } const response = await sendAsync({ method: rpcMethod, params: [signer, rpcData], jsonrpc: '2.0', id: Date.now(), }); if (response.error) { throw new Error((response.error as unknown as { message: string }).message); } return `0x${stripHexPrefix(response.result)}0${SignatureTypes.NO_PREPEND}`; } /** * Sign a message with `personal_sign`. */ protected async ethSignPersonalInternal( signer: string, message: string, ): Promise<string> { let provider = this.web3.currentProvider; if (provider === null) { throw new Error('Cannot sign since Web3 currentProvider is null'); } if (typeof provider === 'string') { throw new Error('Cannot sign since Web3 currentProvider is a string'); } provider = provider as AbstractProvider; const sendAsync: (param: JsonRpcPayload) => Promise<JsonRpcResponse> = ( promisify(provider.sendAsync || provider.send).bind(provider) ); const rpcMethod = 'personal_sign'; const response = await sendAsync({ method: rpcMethod, params: [signer, message], jsonrpc: '2.0', id: Date.now(), }); if (response.error) { throw new Error((response.error as unknown as { message: string }).message); } // Note: Using createTypedSignature() fixes the signature `v` value. return createTypedSignature(response.result, SignatureTypes.PERSONAL); } }