UNPKG

eth-onekey-bridge-keyring

Version:

A MetaMask compatible keyring, for onekey hardware wallets

517 lines 23.6 kB
"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 __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 _OneKeyKeyring_instances, _OneKeyKeyring_normalize, _OneKeyKeyring_signTransaction, _OneKeyKeyring_getPassphraseState, _OneKeyKeyring_getPage, _OneKeyKeyring_batchGetAddress, _OneKeyKeyring_accountDetailsFromAddress, _OneKeyKeyring_getPathForIndex, _OneKeyKeyring_isLedgerLiveHdPath, _OneKeyKeyring_isLedgerLegacyHdPath, _OneKeyKeyring_isStandardBip44HdPath; Object.defineProperty(exports, "__esModule", { value: true }); exports.OneKeyKeyring = void 0; /* eslint-disable jsdoc/match-description */ /* eslint-disable jsdoc/require-param */ /* eslint-disable id-length */ const tx_1 = require("@ethereumjs/tx"); const ethUtil = __importStar(require("@ethereumjs/util")); const eth_sig_util_1 = require("@metamask/eth-sig-util"); // eslint-disable-next-line import/no-nodejs-modules const buffer_1 = require("buffer"); // eslint-disable-next-line import/no-nodejs-modules const events_1 = require("events"); const constants_1 = require("./constants"); const pathBase = 'm'; const defaultHdPath = `${pathBase}/44'/60'/0'/0`; const keyringType = 'OneKey Hardware'; var NetworkApiUrls; (function (NetworkApiUrls) { NetworkApiUrls["Ropsten"] = "https://api-ropsten.etherscan.io"; NetworkApiUrls["Kovan"] = "https://api-kovan.etherscan.io"; NetworkApiUrls["Rinkeby"] = "https://api-rinkeby.etherscan.io"; NetworkApiUrls["Mainnet"] = "https://api.etherscan.io"; })(NetworkApiUrls || (NetworkApiUrls = {})); /** * Check if the given transaction is made with ethereumjs-tx or @ethereumjs/tx * * Transactions built with older versions of ethereumjs-tx have a * getChainId method that newer versions do not. * Older versions are mutable * while newer versions default to being immutable. * Expected shape and type * of data for v, r and s differ (Buffer (old) vs BN (new)). * * @param tx - Transaction to check, instance of either ethereumjs-tx or @ethereumjs/tx. * @returns Returns `true` if tx is an old-style ethereumjs-tx transaction. */ function isOldStyleEthereumjsTx(tx) { return 'getChainId' in tx && typeof tx.getChainId === 'function'; } /** * Check if the given value has a hex prefix. * * @param value - The value to check. * @returns Returns `true` if the value has a hex prefix. */ function hasHexPrefix(value) { return value.startsWith('0x'); } /** * Add a hex prefix to the given value. * * @param value - The value to add a hex prefix to. * @returns Returns the value with a hex prefix. */ function addHexPrefix(value) { if (hasHexPrefix(value)) { return value; } return `0x${value}`; } /** * Check if the passphrase state is empty. * * @param passphraseState - The passphrase state to check. * @returns Returns `true` if the passphrase state is empty. */ function isEmptyPassphrase(passphraseState) { return (passphraseState === null || passphraseState === undefined || passphraseState === ''); } class OneKeyKeyring extends events_1.EventEmitter { constructor({ bridge }) { super(); _OneKeyKeyring_instances.add(this); this.type = keyringType; this.page = 0; this.perPage = 5; this.unlockedAccount = 0; this.accounts = []; this.accountDetails = {}; this.needResetPassphraseState = false; this.hdPath = defaultHdPath; this.network = NetworkApiUrls.Mainnet; this.implementFullBIP44 = false; this.passphraseEnabled = false; if (!bridge) { throw new Error('Bridge is a required dependency for the keyring'); } this.bridge = bridge; this.bridge.on(constants_1.ONEKEY_HARDWARE_UI_EVENT, (_event) => { this.emit(constants_1.ONEKEY_HARDWARE_UI_EVENT, _event); }); } async init(settings) { return this.bridge.init(settings); } async destroy() { this.bridge.off(constants_1.ONEKEY_HARDWARE_UI_EVENT); return this.bridge.dispose(); } async serialize() { return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts, accountDetails: this.accountDetails, page: this.page, }); } async deserialize(opts = {}) { var _a, _b, _c, _d; this.hdPath = (_a = opts.hdPath) !== null && _a !== void 0 ? _a : defaultHdPath; this.accounts = (_b = opts.accounts) !== null && _b !== void 0 ? _b : []; this.accountDetails = (_c = opts.accountDetails) !== null && _c !== void 0 ? _c : {}; this.page = (_d = opts.page) !== null && _d !== void 0 ? _d : 0; return Promise.resolve(); } getModel() { return this.bridge.model; } setAccountToUnlock(index) { this.unlockedAccount = index; } setHdPath(hdPath) { this.hdPath = hdPath; } isUnlocked() { return true; } async unlock() { if (this.isUnlocked()) { return Promise.resolve('already unlocked'); } return Promise.resolve('just unlocked'); } async addAccounts(n = 1) { return new Promise((resolve, reject) => { const from = this.unlockedAccount; const to = from + n; const newAccounts = []; const paths = []; for (let i = from; i < to; i++) { paths.push(__classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPathForIndex).call(this, i)); } __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_batchGetAddress).call(this, paths, this.passphraseState) .then((addresses) => { var _a; if (addresses.length !== paths.length) { throw new Error('Unknown error'); } for (let i = 0; i < paths.length; i++) { const address = addresses[i]; if (typeof address === 'undefined') { throw new Error('Unknown error'); } if (!this.accounts.includes(address)) { this.accounts = [...this.accounts, address]; newAccounts.push(address); } if (!this.accountDetails[address]) { this.accountDetails[address] = { index: i, hdPath: (_a = paths[i]) !== null && _a !== void 0 ? _a : '', passphraseState: this.passphraseState, }; } this.page = 0; } resolve(newAccounts); }) .catch((e) => { reject(e); }); }); } getName() { return keyringType; } async getFirstPage() { this.page = 0; return __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPage).call(this, 1); } async getNextPage() { return __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPage).call(this, 1); } async getPreviousPage() { return __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPage).call(this, -1); } async getAccounts() { return Promise.resolve(this.accounts.slice()); } removeAccount(address) { const filteredAccounts = this.accounts.filter((a) => a.toLowerCase() !== address.toLowerCase()); if (filteredAccounts.length === this.accounts.length) { throw new Error(`Address ${address} not found in this keyring`); } this.accounts = filteredAccounts; delete this.accountDetails[ethUtil.toChecksumAddress(address)]; } async updateTransportMethod(transportType) { return this.bridge.updateTransportMethod(transportType); } /** * Signs a transaction using OneKey. * * Accepts either an ethereumjs-tx or @ethereumjs/tx transaction, and returns * the same type. * * @param address - Hex string address. * @param tx - Instance of either new-style or old-style ethereumjs transaction. * @returns The signed transaction, an instance of either new-style or old-style * ethereumjs transaction. */ async signTransaction(address, tx) { if (isOldStyleEthereumjsTx(tx)) { // In this version of ethereumjs-tx we must add the chainId in hex format // to the initial v value. The chainId must be included in the serialized // transaction which is only communicated to ethereumjs-tx in this // value. In newer versions the chainId is communicated via the 'Common' // object. return __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_signTransaction).call(this, address, // @types/ethereumjs-tx and old ethereumjs-tx versions document // this function return value as Buffer, but the actual // Transaction._chainId will always be a number. // See https://github.com/ethereumjs/ethereumjs-tx/blob/v1.3.7/index.js#L126 tx.getChainId(), tx, (payload) => { tx.v = buffer_1.Buffer.from(payload.v, 'hex'); tx.r = buffer_1.Buffer.from(payload.r, 'hex'); tx.s = buffer_1.Buffer.from(payload.s, 'hex'); return tx; }); } return __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_signTransaction).call(this, address, Number(tx.common.chainId()), tx, (payload) => { // Because tx will be immutable, first get a plain javascript object that // represents the transaction. Using txData here as it aligns with the // nomenclature of ethereumjs/tx. const txData = tx.toJSON(); // The fromTxData utility expects a type to support transactions with a type other than 0 txData.type = tx.type; // The fromTxData utility expects v,r and s to be hex prefixed txData.v = ethUtil.addHexPrefix(payload.v); txData.r = ethUtil.addHexPrefix(payload.r); txData.s = ethUtil.addHexPrefix(payload.s); // Adopt the 'common' option from the original transaction and set the // returned object to be frozen if the original is frozen. return tx_1.TransactionFactory.fromTxData(txData, { common: tx.common, freeze: Object.isFrozen(tx), }); }); } async signMessage(withAccount, data) { return this.signPersonalMessage(withAccount, data); } // For personal_sign, we need to prefix the message: async signPersonalMessage(withAccount, message) { return new Promise((resolve, reject) => { const details = __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_accountDetailsFromAddress).call(this, withAccount); this.bridge .ethereumSignMessage({ path: details.hdPath, passphraseState: details.passphraseState, useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), messageHex: ethUtil.stripHexPrefix(message), }) .then((response) => { var _a; if (response.success) { if (response.payload.address !== ethUtil.toChecksumAddress(withAccount)) { reject(new Error('signature doesnt match the right address')); } const signature = addHexPrefix(response.payload.signature); resolve(signature); } else { reject(new Error(((_a = response.payload) === null || _a === void 0 ? void 0 : _a.error) || 'Unknown error')); } }) .catch((e) => { reject(new Error((e === null || e === void 0 ? void 0 : e.toString()) || 'Unknown error')); }); }); } /** * EIP-712 Sign Typed Data */ async signTypedData(address, data, { version }) { var _a; const useV4 = version === 'V4'; const dataVersion = version === 'V4' ? eth_sig_util_1.SignTypedDataVersion.V4 : eth_sig_util_1.SignTypedDataVersion.V3; const typedData = eth_sig_util_1.TypedDataUtils.sanitizeData(data); const domainHash = eth_sig_util_1.TypedDataUtils.hashStruct('EIP712Domain', typedData.domain, typedData.types, dataVersion).toString('hex'); const messageHash = eth_sig_util_1.TypedDataUtils.hashStruct(typedData.primaryType, typedData.message, typedData.types, dataVersion).toString('hex'); const details = __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_accountDetailsFromAddress).call(this, address); const response = await this.bridge.ethereumSignTypedData({ path: details.hdPath, passphraseState: details.passphraseState, useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), data: data, domainHash, messageHash, metamaskV4Compat: Boolean(useV4), // eslint-disable-line camelcase }); if (response.success) { if (ethUtil.toChecksumAddress(address) !== response.payload.address) { throw new Error('signature doesnt match the right address'); } return addHexPrefix(response.payload.signature); } throw new Error(((_a = response.payload) === null || _a === void 0 ? void 0 : _a.error) || 'Unknown error'); } exportAccount() { throw new Error('Not supported on this device'); } forgetDevice() { this.accounts = []; this.page = 0; this.unlockedAccount = 0; this.accountDetails = {}; this.passphraseState = undefined; this.needResetPassphraseState = false; } async enablePassphrase() { this.passphraseEnabled = true; } async resetPassphraseState() { this.needResetPassphraseState = true; } async getPassphraseState(_index, _hdPath) { // TODO: implement return Promise.resolve(undefined); } } exports.OneKeyKeyring = OneKeyKeyring; _OneKeyKeyring_instances = new WeakSet(), _OneKeyKeyring_normalize = function _OneKeyKeyring_normalize(buffer) { return ethUtil.bufferToHex(buffer).toString(); }, _OneKeyKeyring_signTransaction = async function _OneKeyKeyring_signTransaction(address, chainId, tx, handleSigning) { var _a, _b; let transaction; if (isOldStyleEthereumjsTx(tx)) { // legacy transaction from ethereumjs-tx package has no .toJSON() function, // so we need to convert to hex-strings manually manually transaction = { to: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.to), value: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.value), data: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.data), chainId, nonce: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.nonce), gasLimit: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.gasLimit), gasPrice: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, tx.gasPrice), }; } else { // new-style transaction from @ethereumjs/tx package // we can just copy tx.toJSON() for everything except chainId, which must be a number transaction = Object.assign(Object.assign({}, tx.toJSON()), { chainId, to: __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_normalize).call(this, ethUtil.toBuffer(tx.to)) }); } try { const details = __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_accountDetailsFromAddress).call(this, address); const response = await this.bridge.ethereumSignTransaction({ path: details.hdPath, passphraseState: details.passphraseState, useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), transaction, }); if (response.success) { const newOrMutatedTx = handleSigning(response.payload); const addressSignedWith = ethUtil.toChecksumAddress(ethUtil.addHexPrefix(newOrMutatedTx.getSenderAddress().toString('hex'))); const correctAddress = ethUtil.toChecksumAddress(address); if (addressSignedWith !== correctAddress) { throw new Error("signature doesn't match the right address"); } return newOrMutatedTx; } throw new Error(((_a = response.payload) === null || _a === void 0 ? void 0 : _a.error) || 'Unknown error'); } catch (e) { throw new Error((_b = e === null || e === void 0 ? void 0 : e.toString()) !== null && _b !== void 0 ? _b : 'Unknown error'); } }, _OneKeyKeyring_getPassphraseState = /* PRIVATE METHODS */ async function _OneKeyKeyring_getPassphraseState() { if (!this.passphraseEnabled) { return Promise.resolve(undefined); } if (this.needResetPassphraseState) { this.passphraseState = undefined; this.needResetPassphraseState = false; } if (this.passphraseState) { return Promise.resolve(this.passphraseState); } return this.bridge.getPassphraseState().then((response) => { var _a; if (response.success) { return response.payload; } throw new Error(((_a = response.payload) === null || _a === void 0 ? void 0 : _a.error) || 'Unknown error'); }); }, _OneKeyKeyring_getPage = async function _OneKeyKeyring_getPage(increment) { this.page += increment; if (this.page <= 0) { this.page = 1; } return new Promise((resolve, reject) => { const from = (this.page - 1) * this.perPage; const to = from + this.perPage; const accounts = []; __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPassphraseState).call(this) .then(async (passphraseState) => { const paths = []; for (let i = from; i < to; i++) { paths.push(__classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_getPathForIndex).call(this, i)); } this.passphraseState = passphraseState; const addresses = await __classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_batchGetAddress).call(this, paths, passphraseState); if (addresses.length !== paths.length) { throw new Error('Unknown error'); } for (let i = 0; i < paths.length; i++) { const address = addresses[i]; if (typeof address === 'undefined') { throw new Error('Unknown error'); } accounts.push({ address, balance: null, index: from + i, }); } resolve(accounts); }) .catch((e) => { reject(e); }); }); }, _OneKeyKeyring_batchGetAddress = async function _OneKeyKeyring_batchGetAddress(paths, passphraseState) { var _a; const batchParams = paths.map((path) => ({ path, showOnOneKey: false, })); const response = await this.bridge.batchGetPublicKey({ bundle: batchParams, useBatch: true, passphraseState, useEmptyPassphrase: isEmptyPassphrase(passphraseState), }); if (response.success) { return response.payload.map((item) => { const address = ethUtil .publicToAddress(buffer_1.Buffer.from(item.pub, 'hex'), true) .toString('hex'); return ethUtil.toChecksumAddress(addHexPrefix(address)); }); } throw new Error(((_a = response.payload) === null || _a === void 0 ? void 0 : _a.error) || 'Unknown error'); }, _OneKeyKeyring_accountDetailsFromAddress = function _OneKeyKeyring_accountDetailsFromAddress(address) { const checksummedAddress = ethUtil.toChecksumAddress(address); const accountDetails = this.accountDetails[checksummedAddress]; if (typeof accountDetails === 'undefined') { throw new Error('Unknown address'); } return accountDetails; }, _OneKeyKeyring_getPathForIndex = function _OneKeyKeyring_getPathForIndex(index) { // Check if the path is BIP 44 (Ledger Live) if (__classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_isLedgerLiveHdPath).call(this)) { return `m/44'/60'/${index}'/0/0`; } if (__classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_isLedgerLegacyHdPath).call(this)) { return `m/44'/60'/0'/${index}`; } if (__classPrivateFieldGet(this, _OneKeyKeyring_instances, "m", _OneKeyKeyring_isStandardBip44HdPath).call(this)) { return `m/44'/60'/0'/0/${index}`; } // default path: m/44'/60'/0'/0/x return `${this.hdPath}/${index}`; }, _OneKeyKeyring_isLedgerLiveHdPath = function _OneKeyKeyring_isLedgerLiveHdPath() { return this.hdPath === `m/44'/60'/x'/0/0`; }, _OneKeyKeyring_isLedgerLegacyHdPath = function _OneKeyKeyring_isLedgerLegacyHdPath() { return this.hdPath === `m/44'/60'/0'/x`; }, _OneKeyKeyring_isStandardBip44HdPath = function _OneKeyKeyring_isStandardBip44HdPath() { return (this.hdPath === `m/44'/60'/0'/0/x` || this.hdPath === `m/44'/60'/0'/0`); }; OneKeyKeyring.type = keyringType; //# sourceMappingURL=onekey-keyring.js.map