/* eslint-disable @typescript-eslint/no-unused-vars */ import type { TransactionReceipt } from '@ethersproject/abstract-provider'; import { TransactionRequest, TransactionResponse } from '@ethersproject/abstract-provider'; import { Signer as Abstractsigner, TypedDataDomain, TypedDataField, TypedDataSigner } from '@ethersproject/abstract-signer'; import { getAddress } from '@ethersproject/address'; import { BigNumber } from '@ethersproject/bignumber'; import { Bytes, concat, joinSignature } from '@ethersproject/bytes'; import { Logger } from '@ethersproject/logger'; import { Deferrable, defineReadOnly } from '@ethersproject/properties'; import { toUtf8Bytes } from '@ethersproject/strings'; import { SubmittableResult } from '@polkadot/api'; import { SubmittableExtrinsic } from '@polkadot/api/types'; import { u8aConcat, u8aEq, u8aToHex, isNumber } from '@polkadot/util'; import { blake2AsU8a, decodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; import { Provider } from './Provider'; import { SigningKey } from './SigningKey'; import { dataToString, handleTxResponse, toBN } from './utils'; const logger = new Logger('evm-provider'); export class Signer extends Abstractsigner implements TypedDataSigner { // @ts-ignore readonly provider: Provider; // @ts-ignore readonly signingKey: SigningKey; // @ts-ignore readonly _substrateAddress: string; constructor(provider: Provider, address: string, signingKey: SigningKey) { super(); defineReadOnly(this, 'provider', provider); defineReadOnly(this, 'signingKey', signingKey); // @ts-ignore this.provider.api.setSigner(signingKey); if (typeof address === 'string' && isEthereumAddress(address)) { logger.throwError('expect substrate address'); } else { try { decodeAddress(address); defineReadOnly(this, '_substrateAddress', address); } catch { logger.throwArgumentError('invalid address', 'address', address); } } } connect(provider: Provider): Signer { return logger.throwError( 'cannot alter JSON-RPC Signer connection', Logger.errors.UNSUPPORTED_OPERATION, { operation: 'connect' } ); } /** * * @param evmAddress The EVM address to check * @returns A promise that resolves to true if the EVM address is claimed * or false if the address is not claimed */ async isClaimed(evmAddress?: string): Promise<boolean> { const rpcEvmAddress = await this.queryEvmAddress(); if (!rpcEvmAddress) return false; if (!evmAddress) return true; if (rpcEvmAddress === evmAddress) { return true; } return logger.throwError( 'An evm account already exists to bind to this account' ); } /** * Get the signer's EVM address, and claim an EVM address if it has not claimed one. * @returns A promise resolving to the EVM address of the signer's substrate * address */ async getAddress(): Promise<string> { const address = await this.queryEvmAddress(); if (address) { return address; } else { // default address return this.computeDefaultEvmAddress(); } } /** * Get the signers EVM address if it has claimed one. * @returns A promise resolving to the EVM address of the signer's substrate * address or an empty string if the EVM address isn't claimed */ async queryEvmAddress(): Promise<string> { const address = await this.provider.api.query.evmAccounts.evmAddresses( this._substrateAddress ); if (!address.isEmpty) { const evmAddress = getAddress(address.toString()); return evmAddress; } return ''; } /** * * @returns The default EVM address generated for the signer's substrate address */ computeDefaultEvmAddress(): string { const address = this._substrateAddress; const publicKey = decodeAddress(address); const isStartWithEvm = u8aEq('evm:', publicKey.slice(0, 4)); if (isStartWithEvm) { return getAddress(u8aToHex(publicKey.slice(4, 24))); } return getAddress( u8aToHex(blake2AsU8a(u8aConcat('evm:', publicKey), 256).slice(0, 20)) ); } /** * * @returns The substrate account stored in this Signer */ async getSubstrateAddress(): Promise<string> { return this._substrateAddress; } async claimEvmAccount(evmAddress: string): Promise<void> { const isConnented = await this.isClaimed(evmAddress); if (isConnented) return; const publicKey = decodeAddress(this._substrateAddress); const data = 'Reef evm:' + Buffer.from(publicKey).toString('hex'); const signature = await this._signMessage(evmAddress, data); const extrinsic = this.provider.api.tx.evmAccounts.claimAccount( evmAddress, signature ); await extrinsic.signAsync(this._substrateAddress); await new Promise<void>((resolve, reject) => { extrinsic .send((result: SubmittableResult) => { handleTxResponse(result, this.provider.api) .then(() => { resolve(); }) .catch(({ message, result }) => { if (message === 'evmAccounts.AccountIdHasMapped') { resolve(); } reject(message); }); }) .catch((error) => { reject(error && error.message); }); }); } /** * Claims a default EVM address for this signer's substrate address */ async claimDefaultAccount(): Promise<void> { const extrinsic = this.provider.api.tx.evmAccounts.claimDefaultAccount(); await extrinsic.signAsync(this._substrateAddress); await new Promise<void>((resolve, reject) => { extrinsic .send((result: SubmittableResult) => { handleTxResponse(result, this.provider.api) .then(() => { resolve(); }) .catch(({ message, result }) => { if (message === 'evmAccounts.AccountIdHasMapped') { resolve(); } reject(message); }); }) .catch((error) => { reject(error && error.message); }); }); } signTransaction( transaction: Deferrable<TransactionRequest> ): Promise<string> { return logger.throwError( 'signing transactions is unsupported', Logger.errors.UNSUPPORTED_OPERATION, { operation: 'signTransaction' } ); } /** * * @param transaction * @returns A promise that resolves to the transaction's response */ async sendTransaction( _transaction: Deferrable<TransactionRequest> ): Promise<TransactionResponse> { this._checkProvider('sendTransaction'); const signerAddress = await this.getSubstrateAddress(); const evmAddress = await this.getAddress(); // estimateResources requires the from parameter. // However, when creating the contract, there is no from parameter in the tx const transaction = { from: evmAddress, ..._transaction }; const resources = await this.provider.estimateResources(transaction); // Multiply by 3.1 const gasLimit: BigNumber = resources.gas.mul(31).div(10); let storageLimit: BigNumber; // If the storage limit is supplied, override it from the estimateResources if (transaction.customData) { if ('storageLimit' in transaction.customData) { storageLimit = transaction.customData.storageLimit; if (isNumber(storageLimit)) { storageLimit = BigNumber.from(storageLimit); } } } else { storageLimit = resources.storage.mul(31).div(10); } let totalLimit = await transaction.gasLimit; if (totalLimit === null || totalLimit === undefined) { totalLimit = gasLimit.add(storageLimit); } transaction.gasLimit = totalLimit; const tx = await this.populateTransaction(transaction); const data = tx.data; const from = tx.from; if (!data) { return logger.throwError('Request data not found'); } if (!from) { return logger.throwError('Request from not found'); } let extrinsic: SubmittableExtrinsic<'promise'>; // @TODO create contract if (!tx.to) { extrinsic = this.provider.api.tx.evm.create( tx.data, toBN(tx.value), toBN(gasLimit), toBN(storageLimit.isNegative() ? 0 : storageLimit) ); } else { extrinsic = this.provider.api.tx.evm.call( tx.to, tx.data, toBN(tx.value), toBN(gasLimit), toBN(storageLimit.isNegative() ? 0 : storageLimit) ); } await extrinsic.signAsync(signerAddress); return new Promise((resolve, reject) => { extrinsic .send((result: SubmittableResult) => { handleTxResponse(result, this.provider.api) .then(() => { resolve({ hash: extrinsic.hash.toHex(), from: from || '', confirmations: 0, nonce: toBN(tx.nonce).toNumber(), gasLimit: BigNumber.from(tx.gasLimit || '0'), gasPrice: BigNumber.from(0), data: dataToString(data), value: BigNumber.from(tx.value || '0'), chainId: 13939, wait: (confirmations?: number): Promise<TransactionReceipt> => { return this.provider._resolveTransactionReceipt( extrinsic.hash.toHex(), result.status.asInBlock.toHex(), from ); } }); }) .catch(({ message, result }) => { reject(message); }); }) .catch((error) => { reject(error && error.message); }); }); } /** * * @param message The message to sign * @returns A promise that resolves to the signed hash of the message */ async signMessage(message: Bytes | string): Promise<string> { const evmAddress = await this.queryEvmAddress(); return this._signMessage(evmAddress, message); } async _signMessage( evmAddress: string, message: Bytes | string ): Promise<string> { if (!evmAddress) { return logger.throwError('No binding evm address'); } const messagePrefix = '\x19Ethereum Signed Message:\n'; if (typeof message === 'string') { message = toUtf8Bytes(message); } const msg = u8aToHex( concat([ toUtf8Bytes(messagePrefix), toUtf8Bytes(String(message.length)), message ]) ); if (!this.signingKey.signRaw) { return logger.throwError('Need to implement signRaw method'); } const result = await this.signingKey.signRaw({ address: evmAddress, data: msg, type: 'bytes' }); return joinSignature(result.signature); } async _signTypedData( domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: Record<string, any> ): Promise<string> { return logger.throwError( '_signTypedData is unsupported', Logger.errors.UNSUPPORTED_OPERATION, { operation: '_signTypedData' } ); } }