UNPKG

@hethers/wallet

Version:

Classes for managing, encrypting and decrypting ECDSA private keys as a Signer for hethers.

304 lines (257 loc) 10.3 kB
import { Account, AccountLike, getAccountFromAddress, getAddress, getAddressFromAccount } from "@hethers/address"; import { Provider, TransactionRequest, TransactionResponse } from "@hethers/abstract-provider"; import { ExternallyOwnedAccount, Signer, TypedDataDomain, TypedDataField, TypedDataSigner } from "@hethers/abstract-signer"; import { arrayify, Bytes, BytesLike, concat, hexDataSlice, hexlify, isHexString, joinSignature, SignatureLike } from "@ethersproject/bytes"; import { hashMessage } from "@ethersproject/hash"; import { defaultPath, entropyToMnemonic, initializeSigningKey, HDNode, Mnemonic } from "@hethers/hdnode"; import { keccak256 } from "@ethersproject/keccak256"; import { defineReadOnly } from "@ethersproject/properties"; import { randomBytes } from "@ethersproject/random"; import { recoverPublicKey, SigningKey, SigningKeyED } from "@hethers/signing-key"; import { decryptJsonWallet, decryptJsonWalletSync, encryptKeystore, ProgressCallback } from "@hethers/json-wallets"; import { computeAlias, serializeHederaTransaction, UnsignedTransaction } from "@hethers/transactions"; import { Wordlist } from "@ethersproject/wordlists"; import { Logger } from "@hethers/logger"; import { version } from "./_version"; import { PrivateKey as HederaPrivKey, PublicKey as HederaPubKey } from "@hashgraph/sdk"; const logger = new Logger(version); function isAccount(value: any): value is ExternallyOwnedAccount { if (!value || !value.privateKey) return false; let privKeyCopy = HederaPrivKey.fromString(value.privateKey).toStringRaw(); if (!privKeyCopy.startsWith('0x')) { privKeyCopy = '0x' + privKeyCopy } return isHexString(privKeyCopy, 32); } function hasMnemonic(value: any): value is { mnemonic: Mnemonic } { const mnemonic = value.mnemonic; return (mnemonic && mnemonic.phrase); } function hasAlias(value: any): value is ExternallyOwnedAccount { return isAccount(value) && value.alias != null; } function prepend0x(value: string): string { if (value.match(/^[0-9a-f]*$/i) && value.length === 64) { return `0x${value}`; } return value; } export class Wallet extends Signer implements ExternallyOwnedAccount, TypedDataSigner { // EVM Address format readonly address?: string; // Hedera Account format readonly account?: Account; // Hedera alias readonly alias?: string; readonly provider: Provider; readonly isED25519Type?: boolean; // Wrapping the _signingKey and _mnemonic in a getter function prevents // leaking the private key in console.log; still, be careful! :) readonly _signingKey: () => SigningKey; readonly _mnemonic: () => Mnemonic; constructor(identity: BytesLike | ExternallyOwnedAccount | SigningKey, provider?: Provider) { logger.checkNew(new.target, Wallet); super(); if (isAccount(identity) && !SigningKey.isSigningKey(identity)) { defineReadOnly(this, "isED25519Type", !!identity.isED25519Type); // removes DER header if presented in the private key let privKey = HederaPrivKey.fromString(identity.privateKey).toStringRaw(); // A lot of common tools do not prefix private keys with a 0x (see: #1166) if (typeof (privKey) === "string") { privKey = prepend0x(privKey); } const signingKey = initializeSigningKey(privKey, this.isED25519Type); defineReadOnly(this, "_signingKey", () => signingKey); if (identity.address || identity.account) { defineReadOnly(this, "address", identity.address ? getAddress(identity.address) : getAddressFromAccount(identity.account)); defineReadOnly(this, "account", identity.account ? identity.account : getAccountFromAddress(identity.address)); } if (hasAlias(identity)) { defineReadOnly(this, "alias", identity.alias); if (this.alias !== computeAlias(signingKey.privateKey, this.isED25519Type)) { logger.throwArgumentError("privateKey/alias mismatch", "privateKey", "[REDACTED]"); } } if (hasMnemonic(identity)) { const srcMnemonic = identity.mnemonic; defineReadOnly(this, "_mnemonic", () => ( { phrase: srcMnemonic.phrase, path: srcMnemonic.path || defaultPath, locale: srcMnemonic.locale || "en" } )); const mnemonic = this.mnemonic; const node = HDNode.fromMnemonic(mnemonic.phrase, null, mnemonic.locale, this.isED25519Type).derivePath(mnemonic.path); if (node.privateKey !== this._signingKey().privateKey) { logger.throwArgumentError("mnemonic/privateKey mismatch", "privateKey", "[REDACTED]"); } } else { defineReadOnly(this, "_mnemonic", (): Mnemonic => null); } } else { if (SigningKey.isSigningKey(identity)) { /* istanbul ignore if */ if (identity.curve !== "secp256k1" && identity.curve !== "ed25519") { logger.throwArgumentError("unsupported curve; must be secp256k1 or ed25519", "privateKey", "[REDACTED]"); } defineReadOnly(this, "_signingKey", () => (<SigningKey | SigningKeyED>identity)); defineReadOnly(this, "isED25519Type", identity.curve === "ed25519"); } else { // A lot of common tools do not prefix private keys with a 0x (see: #1166) if (typeof (identity) === "string") { identity = prepend0x(HederaPrivKey.fromString(identity).toStringRaw()); } const signingKey = new SigningKey(identity); defineReadOnly(this, "_signingKey", () => signingKey); defineReadOnly(this, "isED25519Type", false); } defineReadOnly(this, "_mnemonic", (): Mnemonic => null); defineReadOnly(this, "alias", computeAlias(this._signingKey().privateKey)); } /* istanbul ignore if */ if (provider && !Provider.isProvider(provider)) { logger.throwArgumentError("invalid provider", "provider", provider); } defineReadOnly(this, "provider", provider || null); } get mnemonic(): Mnemonic { return this._mnemonic(); } get privateKey(): string { return this._signingKey().privateKey; } get publicKey(): string { return this._signingKey().publicKey; } getAddress(): Promise<string> { return Promise.resolve(this.address); } getAccount(): Promise<Account> { return Promise.resolve(this.account); } getAlias(): Promise<string> { return Promise.resolve(this.alias); } getEvmAddress(): Promise<string> { return Promise.resolve(this.provider.getEvmAddress(this.address)); } connect(provider: Provider): Wallet { return new Wallet(this, provider); } connectAccount(accountLike: AccountLike): Wallet { const eoa = { privateKey: this._signingKey().privateKey, address: getAddressFromAccount(accountLike), alias: this.alias, isED25519Type: this.isED25519Type, mnemonic: this._mnemonic() }; return new Wallet(eoa, this.provider); } signTransaction(transaction: TransactionRequest): Promise<string> { this._checkAddress('signTransaction'); let tx = this.checkTransaction(transaction); return this.populateTransaction(tx).then(async readyTx => { const pubKey = HederaPubKey.fromString(this._signingKey().compressedPublicKey); const tx = serializeHederaTransaction(<UnsignedTransaction>readyTx, pubKey); const privKey = this.isED25519Type ? HederaPrivKey.fromStringED25519(this._signingKey().privateKey) : HederaPrivKey.fromStringECDSA(this._signingKey().privateKey); const signed = await tx.sign(privKey); return hexlify(signed.toBytes()); }); } async signMessage(message: Bytes | string): Promise<string> { return joinSignature(this._signingKey().signDigest(hashMessage(message))); } async _signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> { return logger.throwError("_signTypedData not supported", Logger.errors.UNSUPPORTED_OPERATION, { operation: '_signTypedData' }); } encrypt(password: Bytes | string, options?: any, progressCallback?: ProgressCallback): Promise<string> { if (typeof (options) === "function" && !progressCallback) { progressCallback = options; options = {}; } if (progressCallback && typeof (progressCallback) !== "function") { throw new Error("invalid callback"); } if (!options) { options = {}; } return encryptKeystore(this, password, options, progressCallback); } /** * Performs a contract local call (ContractCallQuery) against the given contract in the provider's network. * In the future, this method should automatically perform getCost and apply the results for gasLimit/txFee. * TODO: utilize getCost when implemented * * @param txRequest - the call request to be submitted */ /** * Static methods to create Wallet instances. */ static createRandom(options?: any): Wallet { let entropy: Uint8Array = randomBytes(16); if (!options) { options = {}; } if (options.extraEntropy) { entropy = arrayify(hexDataSlice(keccak256(concat([entropy, options.extraEntropy])), 0, 16)); } const mnemonic = entropyToMnemonic(entropy, options.locale); return Wallet.fromMnemonic(mnemonic, options.path, options.locale, options.isED25519Type); } async createAccount(pubKey: BytesLike, initialBalance?: BigInt): Promise<TransactionResponse> { if (!initialBalance) initialBalance = BigInt(0); const signed = await this.signTransaction({ customData: { publicKey: pubKey, initialBalance } }); return this.provider.sendTransaction(signed); }; static fromEncryptedJson(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise<Wallet> { return decryptJsonWallet(json, password, progressCallback).then((account) => { return new Wallet(account); }); } static fromEncryptedJsonSync(json: string, password: Bytes | string): Wallet { return new Wallet(decryptJsonWalletSync(json, password)); } static fromMnemonic(mnemonic: string, path?: string, wordlist?: Wordlist, isED25519Type?: boolean): Wallet { if (!path) { path = defaultPath; } return new Wallet(HDNode.fromMnemonic(mnemonic, null, wordlist, isED25519Type).derivePath(path)); } _checkAddress(operation?: string): void { if (!this.address) { logger.throwError("missing address", Logger.errors.UNSUPPORTED_OPERATION, { operation: (operation || "_checkAddress") }); } } } export function verifyMessage(message: Bytes | string, signature: SignatureLike, isED25519Type?: boolean): string { return recoverPublicKey(arrayify(hashMessage(message)), signature, isED25519Type); }