2019-05-14 18:25:46 -04:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
import { getAddress } from "@ethersproject/address";
|
|
|
|
import { Provider, TransactionRequest } from "@ethersproject/abstract-provider";
|
2020-10-18 21:52:25 -04:00
|
|
|
import { ExternallyOwnedAccount, Signer, TypedDataDomain, TypedDataField, TypedDataSigner } from "@ethersproject/abstract-signer";
|
2019-05-14 18:25:46 -04:00
|
|
|
import { arrayify, Bytes, BytesLike, concat, hexDataSlice, isHexString, joinSignature, SignatureLike } from "@ethersproject/bytes";
|
2020-10-18 21:52:25 -04:00
|
|
|
import { hashMessage, _TypedDataEncoder } from "@ethersproject/hash";
|
2020-01-18 21:09:02 -05:00
|
|
|
import { defaultPath, HDNode, entropyToMnemonic, Mnemonic } from "@ethersproject/hdnode";
|
2019-05-14 18:25:46 -04:00
|
|
|
import { keccak256 } from "@ethersproject/keccak256";
|
2019-06-10 22:25:46 -04:00
|
|
|
import { defineReadOnly, resolveProperties } from "@ethersproject/properties";
|
2019-05-14 18:25:46 -04:00
|
|
|
import { randomBytes } from "@ethersproject/random";
|
|
|
|
import { SigningKey } from "@ethersproject/signing-key";
|
2020-02-27 18:42:59 +00:00
|
|
|
import { decryptJsonWallet, decryptJsonWalletSync, encryptKeystore, ProgressCallback } from "@ethersproject/json-wallets";
|
2020-02-06 18:21:34 -05:00
|
|
|
import { computeAddress, recoverAddress, serialize, UnsignedTransaction } from "@ethersproject/transactions";
|
2019-08-25 02:39:20 -04:00
|
|
|
import { Wordlist } from "@ethersproject/wordlists";
|
2019-05-14 18:25:46 -04:00
|
|
|
|
2019-08-01 18:04:06 -04:00
|
|
|
import { Logger } from "@ethersproject/logger";
|
|
|
|
import { version } from "./_version";
|
|
|
|
const logger = new Logger(version);
|
|
|
|
|
2019-05-14 18:25:46 -04:00
|
|
|
function isAccount(value: any): value is ExternallyOwnedAccount {
|
|
|
|
return (value != null && isHexString(value.privateKey, 32) && value.address != null);
|
|
|
|
}
|
|
|
|
|
2020-01-18 21:09:02 -05:00
|
|
|
function hasMnemonic(value: any): value is { mnemonic: Mnemonic } {
|
|
|
|
const mnemonic = value.mnemonic;
|
|
|
|
return (mnemonic && mnemonic.phrase);
|
|
|
|
}
|
|
|
|
|
2020-10-18 21:52:25 -04:00
|
|
|
export class Wallet extends Signer implements ExternallyOwnedAccount, TypedDataSigner {
|
2019-05-14 18:25:46 -04:00
|
|
|
|
|
|
|
readonly address: string;
|
|
|
|
readonly provider: Provider;
|
|
|
|
|
|
|
|
// Wrapping the _signingKey and _mnemonic in a getter function prevents
|
|
|
|
// leaking the private key in console.log; still, be careful! :)
|
|
|
|
readonly _signingKey: () => SigningKey;
|
2020-01-18 21:09:02 -05:00
|
|
|
readonly _mnemonic: () => Mnemonic;
|
2019-05-14 18:25:46 -04:00
|
|
|
|
|
|
|
constructor(privateKey: BytesLike | ExternallyOwnedAccount | SigningKey, provider?: Provider) {
|
2019-08-01 18:04:06 -04:00
|
|
|
logger.checkNew(new.target, Wallet);
|
2019-05-14 18:25:46 -04:00
|
|
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
if (isAccount(privateKey)) {
|
2019-11-01 23:33:51 +09:00
|
|
|
const signingKey = new SigningKey(privateKey.privateKey);
|
2019-05-14 18:25:46 -04:00
|
|
|
defineReadOnly(this, "_signingKey", () => signingKey);
|
|
|
|
defineReadOnly(this, "address", computeAddress(this.publicKey));
|
|
|
|
|
|
|
|
if (this.address !== getAddress(privateKey.address)) {
|
2020-04-22 02:42:25 -04:00
|
|
|
logger.throwArgumentError("privateKey/address mismatch", "privateKey", "[REDACTED]");
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
|
2020-01-18 21:09:02 -05:00
|
|
|
if (hasMnemonic(privateKey)) {
|
|
|
|
const srcMnemonic = privateKey.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).derivePath(mnemonic.path);
|
2019-05-14 18:25:46 -04:00
|
|
|
if (computeAddress(node.privateKey) !== this.address) {
|
2020-04-22 02:42:25 -04:00
|
|
|
logger.throwArgumentError("mnemonic/address mismatch", "privateKey", "[REDACTED]");
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
} else {
|
2020-01-18 21:09:02 -05:00
|
|
|
defineReadOnly(this, "_mnemonic", (): Mnemonic => null);
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
2019-06-10 22:25:46 -04:00
|
|
|
if (SigningKey.isSigningKey(privateKey)) {
|
2020-07-13 06:48:33 -04:00
|
|
|
/* istanbul ignore if */
|
2019-06-10 22:25:46 -04:00
|
|
|
if (privateKey.curve !== "secp256k1") {
|
2019-08-01 18:04:06 -04:00
|
|
|
logger.throwArgumentError("unsupported curve; must be secp256k1", "privateKey", "[REDACTED]");
|
2019-06-10 22:25:46 -04:00
|
|
|
}
|
2020-11-20 17:30:36 -05:00
|
|
|
defineReadOnly(this, "_signingKey", () => (<SigningKey>privateKey));
|
|
|
|
|
2019-05-14 18:25:46 -04:00
|
|
|
} else {
|
2020-11-20 17:30:36 -05:00
|
|
|
// A lot of common tools do not prefix private keys with a 0x (see: #1166)
|
|
|
|
if (typeof(privateKey) === "string") {
|
|
|
|
if (privateKey.match(/^[0-9a-f]*$/i) && privateKey.length === 64) {
|
|
|
|
privateKey = "0x" + privateKey;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-01 23:33:51 +09:00
|
|
|
const signingKey = new SigningKey(privateKey);
|
2019-05-14 18:25:46 -04:00
|
|
|
defineReadOnly(this, "_signingKey", () => signingKey);
|
|
|
|
}
|
2020-11-20 17:30:36 -05:00
|
|
|
|
2020-01-18 21:09:02 -05:00
|
|
|
defineReadOnly(this, "_mnemonic", (): Mnemonic => null);
|
2019-05-14 18:25:46 -04:00
|
|
|
defineReadOnly(this, "address", computeAddress(this.publicKey));
|
|
|
|
}
|
|
|
|
|
2020-07-13 06:48:33 -04:00
|
|
|
/* istanbul ignore if */
|
2019-06-10 22:25:46 -04:00
|
|
|
if (provider && !Provider.isProvider(provider)) {
|
2019-08-01 18:04:06 -04:00
|
|
|
logger.throwArgumentError("invalid provider", "provider", provider);
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
defineReadOnly(this, "provider", provider || null);
|
|
|
|
}
|
|
|
|
|
2020-01-18 21:09:02 -05:00
|
|
|
get mnemonic(): Mnemonic { return this._mnemonic(); }
|
2019-05-14 18:25:46 -04:00
|
|
|
get privateKey(): string { return this._signingKey().privateKey; }
|
|
|
|
get publicKey(): string { return this._signingKey().publicKey; }
|
|
|
|
|
|
|
|
getAddress(): Promise<string> {
|
|
|
|
return Promise.resolve(this.address);
|
|
|
|
}
|
|
|
|
|
|
|
|
connect(provider: Provider): Wallet {
|
|
|
|
return new Wallet(this, provider);
|
|
|
|
}
|
|
|
|
|
|
|
|
signTransaction(transaction: TransactionRequest): Promise<string> {
|
|
|
|
return resolveProperties(transaction).then((tx) => {
|
|
|
|
if (tx.from != null) {
|
|
|
|
if (getAddress(tx.from) !== this.address) {
|
2020-07-13 06:48:33 -04:00
|
|
|
logger.throwArgumentError("transaction from address mismatch", "transaction.from", transaction.from);
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
delete tx.from;
|
|
|
|
}
|
|
|
|
|
2020-02-06 18:21:34 -05:00
|
|
|
const signature = this._signingKey().signDigest(keccak256(serialize(<UnsignedTransaction>tx)));
|
|
|
|
return serialize(<UnsignedTransaction>tx, signature);
|
2019-05-14 18:25:46 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-18 21:52:25 -04:00
|
|
|
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> {
|
|
|
|
// Populate any ENS names
|
|
|
|
const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => {
|
|
|
|
if (this.provider == null) {
|
|
|
|
logger.throwError("cannot resolve ENS names without a provider", Logger.errors.UNSUPPORTED_OPERATION, {
|
2020-11-14 17:42:36 -05:00
|
|
|
operation: "resolveName",
|
|
|
|
value: name
|
2020-10-18 21:52:25 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return this.provider.resolveName(name);
|
|
|
|
});
|
|
|
|
|
|
|
|
return joinSignature(this._signingKey().signDigest(_TypedDataEncoder.hash(populated.domain, types, populated.value)));
|
2019-05-14 18:25:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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));
|
|
|
|
}
|
|
|
|
|
2019-11-01 23:33:51 +09:00
|
|
|
const mnemonic = entropyToMnemonic(entropy, options.locale);
|
2019-05-14 18:25:46 -04:00
|
|
|
return Wallet.fromMnemonic(mnemonic, options.path, options.locale);
|
|
|
|
}
|
|
|
|
|
|
|
|
static fromEncryptedJson(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise<Wallet> {
|
|
|
|
return decryptJsonWallet(json, password, progressCallback).then((account) => {
|
|
|
|
return new Wallet(account);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-02-27 18:42:59 +00:00
|
|
|
static fromEncryptedJsonSync(json: string, password: Bytes | string): Wallet {
|
|
|
|
return new Wallet(decryptJsonWalletSync(json, password));
|
|
|
|
}
|
|
|
|
|
2019-05-14 18:25:46 -04:00
|
|
|
static fromMnemonic(mnemonic: string, path?: string, wordlist?: Wordlist): Wallet {
|
|
|
|
if (!path) { path = defaultPath; }
|
|
|
|
return new Wallet(HDNode.fromMnemonic(mnemonic, null, wordlist).derivePath(path));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function verifyMessage(message: Bytes | string, signature: SignatureLike): string {
|
|
|
|
return recoverAddress(hashMessage(message), signature);
|
|
|
|
}
|
2020-10-20 00:15:55 -04:00
|
|
|
|
|
|
|
export function verifyTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>, signature: SignatureLike): string {
|
|
|
|
return recoverAddress(_TypedDataEncoder.hash(domain, types, value), signature);
|
|
|
|
}
|