sparrow-controllers
Version:
Collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets
636 lines • 29.2 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _KeyringController_keyring;
Object.defineProperty(exports, "__esModule", { value: true });
exports.KeyringController = exports.SignTypedDataVersion = exports.AccountImportStrategy = exports.KeyringTypes = void 0;
const ethereumjs_util_1 = require("ethereumjs-util");
const eth_sig_util_1 = require("eth-sig-util");
const ethereumjs_wallet_1 = __importStar(require("ethereumjs-wallet"));
const eth_keyring_controller_1 = __importDefault(require("eth-keyring-controller"));
const async_mutex_1 = require("async-mutex");
const BaseController_1 = require("../BaseController");
const util_1 = require("../util");
/**
* Available keyring types
*/
var KeyringTypes;
(function (KeyringTypes) {
KeyringTypes["simple"] = "Simple Key Pair";
KeyringTypes["hd"] = "HD Key Tree";
KeyringTypes["qr"] = "QR Hardware Wallet Device";
})(KeyringTypes = exports.KeyringTypes || (exports.KeyringTypes = {}));
/**
* A strategy for importing an account
*/
var AccountImportStrategy;
(function (AccountImportStrategy) {
AccountImportStrategy["privateKey"] = "privateKey";
AccountImportStrategy["json"] = "json";
})(AccountImportStrategy = exports.AccountImportStrategy || (exports.AccountImportStrategy = {}));
/**
* The `signTypedMessage` version
*
* @see https://docs.metamask.io/guide/signing-data.html
*/
var SignTypedDataVersion;
(function (SignTypedDataVersion) {
SignTypedDataVersion["V1"] = "V1";
SignTypedDataVersion["V3"] = "V3";
SignTypedDataVersion["V4"] = "V4";
})(SignTypedDataVersion = exports.SignTypedDataVersion || (exports.SignTypedDataVersion = {}));
/**
* Controller responsible for establishing and managing user identity
*/
class KeyringController extends BaseController_1.BaseController {
/**
* Creates a KeyringController instance.
*
* @param options - The controller options.
* @param options.removeIdentity - Remove the identity with the given address.
* @param options.syncIdentities - Sync identities with the given list of addresses.
* @param options.updateIdentities - Generate an identity for each address given that doesn't already have an identity.
* @param options.setSelectedAddress - Set the selected address.
* @param options.setAccountLabel - Set a new name for account.
* @param config - Initial options used to configure this controller.
* @param state - Initial state to set on this controller.
*/
constructor({ removeIdentity, syncIdentities, updateIdentities, setSelectedAddress, setAccountLabel, }, config, state) {
super(config, state);
this.mutex = new async_mutex_1.Mutex();
/**
* Name of this controller used during composition
*/
this.name = 'KeyringController';
_KeyringController_keyring.set(this, void 0);
__classPrivateFieldSet(this, _KeyringController_keyring, new eth_keyring_controller_1.default(Object.assign({ initState: state }, config)), "f");
this.defaultState = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _KeyringController_keyring, "f").store.getState()), { keyrings: [] });
this.removeIdentity = removeIdentity;
this.syncIdentities = syncIdentities;
this.updateIdentities = updateIdentities;
this.setSelectedAddress = setSelectedAddress;
this.setAccountLabel = setAccountLabel;
this.initialize();
this.fullUpdate();
}
/**
* Adds a new account to the default (first) HD seed phrase keyring.
*
* @returns Promise resolving to current state when the account is added.
*/
addNewAccount() {
return __awaiter(this, void 0, void 0, function* () {
const primaryKeyring = __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringsByType('HD Key Tree')[0];
/* istanbul ignore if */
if (!primaryKeyring) {
throw new Error('No HD keyring found');
}
const oldAccounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").addNewAccount(primaryKeyring);
const newAccounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
yield this.verifySeedPhrase();
this.updateIdentities(newAccounts);
newAccounts.forEach((selectedAddress) => {
if (!oldAccounts.includes(selectedAddress)) {
this.setSelectedAddress(selectedAddress);
}
});
return this.fullUpdate();
});
}
/**
* Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences.
*
* @returns Promise resolving to current state when the account is added.
*/
addNewAccountWithoutUpdate() {
return __awaiter(this, void 0, void 0, function* () {
const primaryKeyring = __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringsByType('HD Key Tree')[0];
/* istanbul ignore if */
if (!primaryKeyring) {
throw new Error('No HD keyring found');
}
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").addNewAccount(primaryKeyring);
yield this.verifySeedPhrase();
return this.fullUpdate();
});
}
/**
* Effectively the same as creating a new keychain then populating it
* using the given seed phrase.
*
* @param password - Password to unlock keychain.
* @param seed - A BIP39-compliant seed phrase,
* either as a string or an array of UTF-8 bytes that represent the string.
* @returns Promise resolving to the restored keychain object.
*/
createNewVaultAndRestore(password, seed) {
return __awaiter(this, void 0, void 0, function* () {
const releaseLock = yield this.mutex.acquire();
if (!password || !password.length) {
throw new Error('Invalid password');
}
try {
this.updateIdentities([]);
const vault = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").createNewVaultAndRestore(password, seed);
this.updateIdentities(yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts());
this.fullUpdate();
return vault;
}
finally {
releaseLock();
}
});
}
/**
* Create a new primary keychain and wipe any previous keychains.
*
* @param password - Password to unlock the new vault.
* @returns Newly-created keychain object.
*/
createNewVaultAndKeychain(password) {
return __awaiter(this, void 0, void 0, function* () {
const releaseLock = yield this.mutex.acquire();
try {
const vault = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").createNewVaultAndKeychain(password);
this.updateIdentities(yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts());
this.fullUpdate();
return vault;
}
finally {
releaseLock();
}
});
}
/**
* Returns the status of the vault.
*
* @returns Boolean returning true if the vault is unlocked.
*/
isUnlocked() {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").memStore.getState().isUnlocked;
}
/**
* Gets the seed phrase of the HD keyring.
*
* @param password - Password of the keyring.
* @returns Promise resolving to the seed phrase.
*/
exportSeedPhrase(password) {
if (__classPrivateFieldGet(this, _KeyringController_keyring, "f").password === password) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").keyrings[0].mnemonic;
}
throw new Error('Invalid password');
}
/**
* Gets the private key from the keyring controlling an address.
*
* @param password - Password of the keyring.
* @param address - Address to export.
* @returns Promise resolving to the private key for an address.
*/
exportAccount(password, address) {
if (__classPrivateFieldGet(this, _KeyringController_keyring, "f").password === password) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").exportAccount(address);
}
throw new Error('Invalid password');
}
/**
* Returns the public addresses of all accounts for the current keyring.
*
* @returns A promise resolving to an array of addresses.
*/
getAccounts() {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
}
/**
* Imports an account with the specified import strategy.
*
* @param strategy - Import strategy name.
* @param args - Array of arguments to pass to the underlying stategy.
* @throws Will throw when passed an unrecognized strategy.
* @returns Promise resolving to current state when the import is complete.
*/
importAccountWithStrategy(strategy, args) {
return __awaiter(this, void 0, void 0, function* () {
let privateKey;
switch (strategy) {
case 'privateKey':
const [importedKey] = args;
if (!importedKey) {
throw new Error('Cannot import an empty key.');
}
const prefixed = (0, ethereumjs_util_1.addHexPrefix)(importedKey);
let bufferedPrivateKey;
try {
bufferedPrivateKey = (0, ethereumjs_util_1.toBuffer)(prefixed);
}
catch (_a) {
throw new Error('Cannot import invalid private key.');
}
/* istanbul ignore if */
if (!(0, ethereumjs_util_1.isValidPrivate)(bufferedPrivateKey)) {
throw new Error('Cannot import invalid private key.');
}
privateKey = (0, ethereumjs_util_1.stripHexPrefix)(prefixed);
break;
case 'json':
let wallet;
const [input, password] = args;
try {
wallet = ethereumjs_wallet_1.thirdparty.fromEtherWallet(input, password);
}
catch (e) {
wallet = wallet || (yield ethereumjs_wallet_1.default.fromV3(input, password, true));
}
privateKey = (0, ethereumjs_util_1.bufferToHex)(wallet.getPrivateKey());
break;
default:
throw new Error(`Unexpected import strategy: '${strategy}'`);
}
const newKeyring = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").addNewKeyring(KeyringTypes.simple, [
privateKey,
]);
const accounts = yield newKeyring.getAccounts();
const allAccounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
this.updateIdentities(allAccounts);
this.setSelectedAddress(accounts[0]);
return this.fullUpdate();
});
}
/**
* Removes an account from keyring state.
*
* @param address - Address of the account to remove.
* @returns Promise resolving current state when this account removal completes.
*/
removeAccount(address) {
return __awaiter(this, void 0, void 0, function* () {
this.removeIdentity(address);
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").removeAccount(address);
return this.fullUpdate();
});
}
/**
* Deallocates all secrets and locks the wallet.
*
* @returns Promise resolving to current state.
*/
setLocked() {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").setLocked();
}
/**
* Signs message by calling down into a specific keyring.
*
* @param messageParams - PersonalMessageParams object to sign.
* @returns Promise resolving to a signed message string.
*/
signMessage(messageParams) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").signMessage(messageParams);
}
/**
* Signs personal message by calling down into a specific keyring.
*
* @param messageParams - PersonalMessageParams object to sign.
* @returns Promise resolving to a signed message string.
*/
signPersonalMessage(messageParams) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").signPersonalMessage(messageParams);
}
/**
* Signs typed message by calling down into a specific keyring.
*
* @param messageParams - TypedMessageParams object to sign.
* @param version - Compatibility version EIP712.
* @throws Will throw when passed an unrecognized version.
* @returns Promise resolving to a signed message string or an error if any.
*/
signTypedMessage(messageParams, version) {
return __awaiter(this, void 0, void 0, function* () {
try {
const address = (0, eth_sig_util_1.normalize)(messageParams.from);
const qrKeyring = yield this.getOrAddQRKeyring();
const qrAccounts = yield qrKeyring.getAccounts();
if (qrAccounts.findIndex((qrAddress) => qrAddress.toLowerCase() === address.toLowerCase()) !== -1) {
const messageParamsClone = Object.assign({}, messageParams);
if (version !== SignTypedDataVersion.V1 &&
typeof messageParamsClone.data === 'string') {
messageParamsClone.data = JSON.parse(messageParamsClone.data);
}
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").signTypedMessage(messageParamsClone, { version });
}
const { password } = __classPrivateFieldGet(this, _KeyringController_keyring, "f");
const privateKey = yield this.exportAccount(password, address);
const privateKeyBuffer = (0, ethereumjs_util_1.toBuffer)((0, ethereumjs_util_1.addHexPrefix)(privateKey));
switch (version) {
case SignTypedDataVersion.V1:
// signTypedDataLegacy will throw if the data is invalid.
return (0, eth_sig_util_1.signTypedDataLegacy)(privateKeyBuffer, {
data: messageParams.data,
});
case SignTypedDataVersion.V3:
return (0, eth_sig_util_1.signTypedData)(privateKeyBuffer, {
data: JSON.parse(messageParams.data),
});
case SignTypedDataVersion.V4:
return (0, eth_sig_util_1.signTypedData_v4)(privateKeyBuffer, {
data: JSON.parse(messageParams.data),
});
default:
throw new Error(`Unexpected signTypedMessage version: '${version}'`);
}
}
catch (error) {
throw new Error(`Keyring Controller signTypedMessage: ${error}`);
}
});
}
/**
* Signs a transaction by calling down into a specific keyring.
*
* @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance.
* @param from - Address to sign from, should be in keychain.
* @returns Promise resolving to a signed transaction string.
*/
signTransaction(transaction, from) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").signTransaction(transaction, from);
}
/**
* Attempts to decrypt the current vault and load its keyrings.
*
* @param password - Password to unlock the keychain.
* @returns Promise resolving to the current state.
*/
submitPassword(password) {
return __awaiter(this, void 0, void 0, function* () {
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").submitPassword(password);
const accounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
yield this.syncIdentities(accounts);
return this.fullUpdate();
});
}
/**
* Adds new listener to be notified of state changes.
*
* @param listener - Callback triggered when state changes.
*/
subscribe(listener) {
__classPrivateFieldGet(this, _KeyringController_keyring, "f").store.subscribe(listener);
}
/**
* Removes existing listener from receiving state changes.
*
* @param listener - Callback to remove.
* @returns True if a listener is found and unsubscribed.
*/
unsubscribe(listener) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").store.unsubscribe(listener);
}
/**
* Adds new listener to be notified when the wallet is locked.
*
* @param listener - Callback triggered when wallet is locked.
* @returns EventEmitter if listener added.
*/
onLock(listener) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").on('lock', listener);
}
/**
* Adds new listener to be notified when the wallet is unlocked.
*
* @param listener - Callback triggered when wallet is unlocked.
* @returns EventEmitter if listener added.
*/
onUnlock(listener) {
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").on('unlock', listener);
}
/**
* Verifies the that the seed phrase restores the current keychain's accounts.
*
* @returns Whether the verification succeeds.
*/
verifySeedPhrase() {
return __awaiter(this, void 0, void 0, function* () {
const primaryKeyring = __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringsByType(KeyringTypes.hd)[0];
/* istanbul ignore if */
if (!primaryKeyring) {
throw new Error('No HD keyring found.');
}
const seedWords = (yield primaryKeyring.serialize()).mnemonic;
const accounts = yield primaryKeyring.getAccounts();
/* istanbul ignore if */
if (accounts.length === 0) {
throw new Error('Cannot verify an empty keyring.');
}
const TestKeyringClass = __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringClassForType(KeyringTypes.hd);
const testKeyring = new TestKeyringClass({
mnemonic: seedWords,
numberOfAccounts: accounts.length,
});
const testAccounts = yield testKeyring.getAccounts();
/* istanbul ignore if */
if (testAccounts.length !== accounts.length) {
throw new Error('Seed phrase imported incorrect number of accounts.');
}
testAccounts.forEach((account, i) => {
/* istanbul ignore if */
if (account.toLowerCase() !== accounts[i].toLowerCase()) {
throw new Error('Seed phrase imported different accounts.');
}
});
return seedWords;
});
}
/**
* Update keyrings in state and calls KeyringController fullUpdate method returning current state.
*
* @returns The current state.
*/
fullUpdate() {
return __awaiter(this, void 0, void 0, function* () {
const keyrings = yield Promise.all(__classPrivateFieldGet(this, _KeyringController_keyring, "f").keyrings.map((keyring, index) => __awaiter(this, void 0, void 0, function* () {
const keyringAccounts = yield keyring.getAccounts();
const accounts = Array.isArray(keyringAccounts)
? keyringAccounts.map((address) => (0, util_1.toChecksumHexAddress)(address))
: /* istanbul ignore next */ [];
return {
accounts,
index,
type: keyring.type,
};
})));
this.update({ keyrings: [...keyrings] });
return __classPrivateFieldGet(this, _KeyringController_keyring, "f").fullUpdate();
});
}
// QR Hardware related methods
/**
* Add qr hardware keyring.
*
* @returns The added keyring
*/
addQRKeyring() {
return __awaiter(this, void 0, void 0, function* () {
const keyring = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").addNewKeyring(KeyringTypes.qr);
yield this.fullUpdate();
return keyring;
});
}
/**
* Get qr hardware keyring.
*
* @returns The added keyring
*/
getOrAddQRKeyring() {
return __awaiter(this, void 0, void 0, function* () {
const keyring = __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringsByType(KeyringTypes.qr)[0];
return keyring || (yield this.addQRKeyring());
});
}
restoreQRKeyring(serialized) {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).deserialize(serialized);
this.updateIdentities(yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts());
yield this.fullUpdate();
});
}
resetQRKeyringState() {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).resetStore();
});
}
getQRKeyringState() {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getOrAddQRKeyring()).getMemStore();
});
}
submitQRCryptoHDKey(cryptoHDKey) {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey);
});
}
submitQRCryptoAccount(cryptoAccount) {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount);
});
}
submitQRSignature(requestId, ethSignature) {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature);
});
}
cancelQRSignRequest() {
return __awaiter(this, void 0, void 0, function* () {
(yield this.getOrAddQRKeyring()).cancelSignRequest();
});
}
connectQRHardware(page) {
return __awaiter(this, void 0, void 0, function* () {
try {
const keyring = yield this.getOrAddQRKeyring();
let accounts;
switch (page) {
case -1:
accounts = yield keyring.getPreviousPage();
break;
case 1:
accounts = yield keyring.getNextPage();
break;
default:
accounts = yield keyring.getFirstPage();
}
return accounts.map((account) => {
return Object.assign(Object.assign({}, account), { balance: '0x0' });
});
}
catch (e) {
throw new Error(`Unspecified error when connect QR Hardware, ${e}`);
}
});
}
unlockQRHardwareWalletAccount(index) {
return __awaiter(this, void 0, void 0, function* () {
const keyring = yield this.getOrAddQRKeyring();
keyring.setAccountToUnlock(index);
const oldAccounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").addNewAccount(keyring);
const newAccounts = yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts();
this.updateIdentities(newAccounts);
newAccounts.forEach((address) => {
if (!oldAccounts.includes(address)) {
if (this.setAccountLabel) {
this.setAccountLabel(address, `${keyring.getName()} ${index}`);
}
this.setSelectedAddress(address);
}
});
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").persistAllKeyrings();
yield this.fullUpdate();
});
}
getAccountKeyringType(account) {
return __awaiter(this, void 0, void 0, function* () {
return (yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringForAccount(account)).type;
});
}
forgetQRDevice() {
return __awaiter(this, void 0, void 0, function* () {
const keyring = yield this.getOrAddQRKeyring();
keyring.forgetDevice();
const accounts = (yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getAccounts());
accounts.forEach((account) => {
this.setSelectedAddress(account);
});
yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").persistAllKeyrings();
yield this.fullUpdate();
});
}
}
exports.KeyringController = KeyringController;
_KeyringController_keyring = new WeakMap();
exports.default = KeyringController;
//# sourceMappingURL=KeyringController.js.map