eth-onekey-bridge-keyring
Version:
A MetaMask compatible keyring, for onekey hardware wallets
517 lines • 23.6 kB
JavaScript
;
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