import { Bip39, generateKeys } from "@celo/utils/lib/account"; import invariant from "tiny-invariant"; import warning from "tiny-warning"; import Web3 from "web3"; import { EncryptedKeystoreV3Json, Sign, SignedTransaction, TransactionConfig, TransactionReceipt, } from "web3-core"; import { Address, ChainId, chainsSupportingAlternateGas, TransactionOptions, } from "../constants"; import { Wallet, WalletConfig } from "./Wallet"; export type SignTransactionFunctionType = ( transactionConfig: TransactionConfig & { feeCurrency?: Address }, callback?: ((signTransaction: SignedTransaction) => void) | undefined ) => Promise; type Web3SignFunction = Promise; const curriedGetSigner = (web3: Web3, { getMnemonic, getPrivateKey }: WalletConfig) => async () => { let pk = getPrivateKey ? await getPrivateKey() : undefined; if (!pk) { const phrase = getMnemonic ? (await getMnemonic()) ?? " " : ""; const words = phrase?.split(" "); invariant( words.length === 12 || words.length === 24, `Mnemonic is of length ${words.length}, expected 12 or 24` ); const { privateKey } = await generateKeys(phrase.trim()); pk = privateKey; } const account = web3.eth.accounts.privateKeyToAccount(pk); account.sign; return account.signTransaction; }; /** * Representation of an EOA (Externally Owned Account) as a wallet */ export class EOA extends Wallet { public signTransaction: SignTransactionFunctionType | undefined; public encrypt?: (password: string) => EncryptedKeystoreV3Json; private sign?: (data: string) => Sign; /** * * @param config Config file specifying how to retrieve the mnemonic or private key * @param skipRegistration flag to skip the registration check * @returns Instantiated Wallet object */ public async _loadWallet( config: WalletConfig, skipRegistration?: boolean ): Promise { if (config.privateKey || config.getPrivateKey) await this._loadPK(config); else if (config.mnemonic || config.getMnemonic) await this._loadMnemonic(config); else invariant(false, "Credentials not provided. Wallet will not be loaded"); if (!skipRegistration) await this.fetchFromWalletService(); return this; } private async _loadMnemonic({ mnemonic, getMnemonic, bip39, }: { mnemonic?: string; getMnemonic?: () => Promise; bip39?: Bip39; }) { const phrase = mnemonic ?? (getMnemonic ? await getMnemonic() : undefined); if (!phrase) { warning( false, `Provided seed phrase is falsey, you will be unable to sign transactions or messages. Given: ${phrase}` ); return; } const words = phrase.split(" "); invariant( words.length === 12 || words.length === 24, `Mnemonic is of length ${words.length}, expected 12 or 24` ); const { privateKey } = await generateKeys( phrase, undefined, undefined, undefined, bip39 ); return await this._loadPK({ privateKey }); } private async _loadPK({ privateKey, getPrivateKey, }: { privateKey?: string; getPrivateKey?: () => Promise; }) { invariant( privateKey || getPrivateKey, "privateKey or getPrivateGey required" ); const pk = privateKey ?? (getPrivateKey ? await getPrivateKey() : undefined); if (!pk) return; const account = this.web3.eth.accounts.privateKeyToAccount(pk); invariant( !this.address || this.address === account.address, `PK mismatch for account. Expected: ${this.address}, Received: ${account.address}` ); this.address = account.address; this.signTransaction = account.signTransaction; this.sign = account.sign; this.encrypt = account.encrypt; if ( this.homeChain === ChainId.Alfajores || this.homeChain === ChainId.Celo ) { this.connection?.addAccount(pk); } } private async signAndSendSingle( transaction: TransactionConfig, opts?: TransactionOptions ): Promise { const sign = this.signTransaction; if (!sign) { throw new Error("No signing method provided"); } let response; if (chainsSupportingAlternateGas.has(this.homeChain) && this.connection) { const resp = await this.connection.sendTransaction({ ...transaction, from: this.address, feeCurrency: this.feeCurrency, ...opts, }); response = await resp.waitReceipt(); } else { const signedTxn = await sign({ ...transaction, gas: transaction.gas ?? "800000", from: this.address, feeCurrency: this.feeCurrency, }); invariant( signedTxn.rawTransaction, "Signing failed! rawTransaction not present" ); response = await this.web3.eth.sendSignedTransaction( signedTxn.rawTransaction ); } return response; } public async signAndSendTransaction( transactions: TransactionConfig[], opts?: TransactionOptions ): Promise { invariant( !!this.signTransaction, "No method supplied to sign transactions." ); const signAndSendSingle = ( txn: TransactionConfig, opts?: TransactionOptions ) => this.signAndSendSingle(txn, opts); // This is required to preserve scoping. Typescript is annoying if (transactions.length === 1) { return await signAndSendSingle(transactions[0], opts); } const receipts = await Promise.all( transactions.map((el) => signAndSendSingle(el, opts)) ); return receipts[receipts.length - 1]; } signMessage(data: string): Sign { invariant( this.sign, "Credentials for signing messages not provided, missing private key or mnemonic" ); return this.sign(data); } }