Files
2024-07-26 15:54:51 +10:00

609 lines
24 KiB
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 __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LedgerHDPathType = void 0;
const events_1 = require("events");
const ethUtil = __importStar(require("ethereumjs-util"));
const tx_1 = require("@ethereumjs/tx");
const hdkey_1 = __importDefault(require("hdkey"));
const connect_plugin_ethereum_1 = __importDefault(require("@trezor/connect-plugin-ethereum"));
const hdPathString = `m/44'/60'/0'/0`;
const SLIP0044TestnetPath = `m/44'/1'/0'/0`;
const ALLOWED_HD_PATHS = {
[hdPathString]: true,
[SLIP0044TestnetPath]: true,
};
const keyringType = 'Trezor Hardware';
const pathBase = 'm';
const MAX_INDEX = 1000;
const DELAY_BETWEEN_POPUPS = 1000;
const TREZOR_CONNECT_MANIFEST = {
email: '[email protected]/',
appUrl: 'https://debank.com/',
};
const isSameAddress = (a, b) => {
return a.toLowerCase() === b.toLowerCase();
};
var LedgerHDPathType;
(function (LedgerHDPathType) {
LedgerHDPathType["LedgerLive"] = "LedgerLive";
LedgerHDPathType["Legacy"] = "Legacy";
LedgerHDPathType["BIP44"] = "BIP44";
})(LedgerHDPathType || (exports.LedgerHDPathType = LedgerHDPathType = {}));
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @typedef {import('@ethereumjs/tx').TypedTransaction} TypedTransaction
* @typedef {InstanceType<import("ethereumjs-tx")>} OldEthJsTransaction
*/
/**
* 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 {TypedTransaction | OldEthJsTransaction} tx
* @returns {tx is OldEthJsTransaction} Returns `true` if tx is an old-style ethereumjs-tx transaction.
*/
function isOldStyleEthereumjsTx(tx) {
return typeof tx.getChainId === 'function';
}
class TrezorKeyring extends events_1.EventEmitter {
constructor(opts = {}) {
super();
this.type = keyringType;
this.accounts = [];
this.hdk = new hdkey_1.default();
this.page = 0;
this.perPage = 5;
this.unlockedAccount = 0;
this.paths = {};
this.hdPath = '';
if (!opts.bridge) {
throw new Error('Bridge is required');
}
this.bridge = opts.bridge;
this.type = keyringType;
this.accounts = [];
this.hdk = new hdkey_1.default();
this.page = 0;
this.perPage = 5;
this.unlockedAccount = 0;
this.paths = {};
this.deserialize(opts);
this.accountDetails = {};
this.init();
}
init() {
this.bridge.init({
manifest: TREZOR_CONNECT_MANIFEST,
lazyLoad: true,
});
this.bridge.event.on('cleanUp', this.cleanUp);
}
/**
* Gets the model, if known.
* This may be `undefined` if the model hasn't been loaded yet.
*
* @returns {"T" | "1" | undefined}
*/
getModel() {
return this.bridge.model;
}
dispose() {
// This removes the Trezor Connect iframe from the DOM
// This method is not well documented, but the code it calls can be seen
// here: https://github.com/trezor/connect/blob/dec4a56af8a65a6059fb5f63fa3c6690d2c37e00/src/js/iframe/builder.js#L181
this.bridge.dispose();
}
cleanUp(force = false) {
if (!this.hdk) {
return;
}
if (force || this.bridge.connectDevices.size > 1) {
this.hdk = new hdkey_1.default();
}
}
serialize() {
return Promise.resolve({
hdPath: this.hdPath,
accounts: this.accounts,
page: this.page,
paths: this.paths,
perPage: this.perPage,
unlockedAccount: this.unlockedAccount,
accountDetails: this.accountDetails,
});
}
deserialize(opts = {}) {
this.hdPath = opts.hdPath || hdPathString;
this.accounts = opts.accounts || [];
this.page = opts.page || 0;
this.perPage = opts.perPage || 5;
this.accountDetails = opts.accountDetails || {};
return Promise.resolve();
}
isUnlocked() {
return Boolean(this.hdk && this.hdk.publicKey);
}
unlock() {
if (this.isUnlocked()) {
return Promise.resolve('already unlocked');
}
return new Promise((resolve, reject) => {
this.bridge
.getPublicKey({
path: this.hdPath,
coin: 'ETH',
})
.then((response) => {
if (response.success) {
this.hdk.publicKey = Buffer.from(response.payload.publicKey, 'hex');
this.hdk.chainCode = Buffer.from(response.payload.chainCode, 'hex');
resolve('just unlocked');
}
else {
reject(new Error((response.payload && response.payload.error) || 'Unknown error'));
}
})
.catch((e) => {
reject(new Error((e && e.toString()) || 'Unknown error'));
});
});
}
setAccountToUnlock(index) {
this.unlockedAccount = parseInt(index, 10);
}
addAccounts(n = 1) {
return new Promise((resolve, reject) => {
this.unlock()
.then((_) => {
const from = this.unlockedAccount;
const to = from + n;
for (let i = from; i < to; i++) {
const address = this._addressFromIndex(pathBase, i);
if (!this.accounts.includes(address)) {
this.accounts.push(address);
this.accountDetails[ethUtil.toChecksumAddress(address)] = {
hdPath: this._pathFromAddress(address),
hdPathType: LedgerHDPathType.BIP44,
hdPathBasePublicKey: this.getPathBasePublicKey(),
index: i,
};
}
this.page = 0;
}
resolve(this.accounts);
})
.catch((e) => {
reject(e);
});
});
}
getFirstPage() {
this.page = 0;
return this.__getPage(1);
}
getNextPage() {
return this.__getPage(1);
}
getPreviousPage() {
return this.__getPage(-1);
}
getAddresses(start, end) {
return new Promise((resolve, reject) => {
this.unlock()
.then((_) => {
const from = start;
const to = end;
const accounts = [];
for (let i = from; i < to; i++) {
const address = this._addressFromIndex(pathBase, i);
accounts.push({
address,
balance: null,
index: i + 1,
});
this.paths[ethUtil.toChecksumAddress(address)] = i;
}
resolve(accounts);
})
.catch((e) => {
reject(e);
});
});
}
__getPage(increment) {
this.page += increment;
if (this.page <= 0) {
this.page = 1;
}
return new Promise((resolve, reject) => {
this.unlock()
.then((_) => {
const from = (this.page - 1) * this.perPage;
const to = from + this.perPage;
const accounts = [];
for (let i = from; i < to; i++) {
const address = this._addressFromIndex(pathBase, i);
accounts.push({
address,
balance: null,
index: i + 1,
});
this.paths[ethUtil.toChecksumAddress(address)] = i;
}
resolve(accounts);
})
.catch((e) => {
reject(e);
});
});
}
getAccounts() {
return Promise.resolve(this.accounts.slice());
}
removeAccount(address) {
if (!this.accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase())) {
throw new Error(`Address ${address} not found in this keyring`);
}
this.accounts = this.accounts.filter((a) => a.toLowerCase() !== address.toLowerCase());
const checksummedAddress = ethUtil.toChecksumAddress(address);
delete this.accountDetails[checksummedAddress];
delete this.paths[checksummedAddress];
}
/**
* Signs a transaction using Trezor.
*
* Accepts either an ethereumjs-tx or @ethereumjs/tx transaction, and returns
* the same type.
*
* @template {TypedTransaction | OldEthJsTransaction} Transaction
* @param {string} address - Hex string address.
* @param {Transaction} tx - Instance of either new-style or old-style ethereumjs transaction.
* @returns {Promise<Transaction>} The signed transaction, an instance of either new-style or old-style
* ethereumjs transaction.
*/
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 this._signTransaction(address, tx.getChainId(), tx, (payload) => {
tx.v = Buffer.from(payload.v, 'hex');
tx.r = Buffer.from(payload.r, 'hex');
tx.s = Buffer.from(payload.s, 'hex');
return tx;
});
}
return this._signTransaction(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),
});
});
}
/**
*
* @template {TypedTransaction | OldEthJsTransaction} Transaction
* @param {string} address - Hex string address.
* @param {number} chainId - Chain ID
* @param {Transaction} tx - Instance of either new-style or old-style ethereumjs transaction.
* @param {(import('trezor-connect').EthereumSignedTx) => Transaction} handleSigning - Converts signed transaction
* to the same new-style or old-style ethereumjs-tx.
* @returns {Promise<Transaction>} The signed transaction, an instance of either new-style or old-style
* ethereumjs transaction.
*/
_signTransaction(address, chainId, tx, handleSigning) {
return __awaiter(this, void 0, void 0, function* () {
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: this._normalize(tx.to),
value: this._normalize(tx.value),
data: this._normalize(tx.data),
chainId,
nonce: this._normalize(tx.nonce),
gasLimit: this._normalize(tx.gasLimit),
gasPrice: this._normalize(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: this._normalize(tx.to) });
}
try {
const status = yield this.unlock();
yield wait(status === 'just unlocked' ? DELAY_BETWEEN_POPUPS : 0);
const response = yield this.bridge.ethereumSignTransaction({
path: this._pathFromAddress(address),
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((response.payload && response.payload.error) || 'Unknown error');
}
catch (e) {
throw new Error((e && e.toString()) || 'Unknown error');
}
});
}
signMessage(withAccount, data) {
return this.signPersonalMessage(withAccount, data);
}
// For personal_sign, we need to prefix the message:
signPersonalMessage(withAccount, message) {
return new Promise((resolve, reject) => {
this.unlock()
.then((status) => {
setTimeout((_) => {
this.bridge
.ethereumSignMessage({
path: this._pathFromAddress(withAccount),
message: ethUtil.stripHexPrefix(message),
hex: true,
})
.then((response) => {
if (response.success) {
if (response.payload.address !==
ethUtil.toChecksumAddress(withAccount)) {
reject(new Error('signature doesnt match the right address'));
}
const signature = `0x${response.payload.signature}`;
resolve(signature);
}
else {
reject(new Error((response.payload && response.payload.error) ||
'Unknown error'));
}
})
.catch((e) => {
reject(new Error((e && e.toString()) || 'Unknown error'));
});
// This is necessary to avoid popup collision
// between the unlock & sign trezor popups
}, status === 'just unlocked' ? DELAY_BETWEEN_POPUPS : 0);
})
.catch((e) => {
reject(new Error((e && e.toString()) || 'Unknown error'));
});
});
}
/**
* EIP-712 Sign Typed Data
*/
signTypedData(address, data, { version }) {
return __awaiter(this, void 0, void 0, function* () {
yield wait(500);
const dataWithHashes = (0, connect_plugin_ethereum_1.default)(data, version === 'V4');
// set default values for signTypedData
// Trezor is stricter than @metamask/eth-sig-util in what it accepts
const _a = dataWithHashes.types, _b = _a === void 0 ? {} : _a, { EIP712Domain = [] } = _b, otherTypes = __rest(_b, ["EIP712Domain"]), { message = {}, domain = {}, primaryType,
// snake_case since Trezor uses Protobuf naming conventions here
domain_separator_hash, // eslint-disable-line camelcase
message_hash } = dataWithHashes;
// This is necessary to avoid popup collision
// between the unlock & sign trezor popups
const status = yield this.unlock();
yield wait(status === 'just unlocked' ? DELAY_BETWEEN_POPUPS : 0);
const response = yield this.bridge.ethereumSignTypedData({
path: this._pathFromAddress(address),
data: {
types: Object.assign({ EIP712Domain }, otherTypes),
message,
domain,
primaryType,
},
metamask_v4_compat: true,
// Trezor 1 only supports blindly signing hashes
domain_separator_hash,
message_hash,
});
if (response.success) {
if (ethUtil.toChecksumAddress(address) !== response.payload.address) {
throw new Error('signature doesnt match the right address');
}
return response.payload.signature;
}
throw new Error((response.payload && response.payload.error) || 'Unknown error');
});
}
exportAccount() {
return Promise.reject(new Error('Not supported on this device'));
}
forgetDevice() {
this.accounts = [];
this.hdk = new hdkey_1.default();
this.page = 0;
this.unlockedAccount = 0;
this.paths = {};
}
/**
* Set the HD path to be used by the keyring. Only known supported HD paths are allowed.
*
* If the given HD path is already the current HD path, nothing happens. Otherwise the new HD
* path is set, and the wallet state is completely reset.
*
* @throws {Error] Throws if the HD path is not supported.
*
* @param {string} hdPath - The HD path to set.
*/
setHdPath(hdPath) {
if (!ALLOWED_HD_PATHS[hdPath]) {
throw new Error(`The setHdPath method does not support setting HD Path to ${hdPath}`);
}
// Reset HDKey if the path changes
if (this.hdPath !== hdPath) {
this.hdk = new hdkey_1.default();
this.accounts = [];
this.page = 0;
this.perPage = 5;
this.unlockedAccount = 0;
this.paths = {};
}
this.hdPath = hdPath;
}
/* PRIVATE METHODS */
_normalize(buf) {
return ethUtil.bufferToHex(buf).toString();
}
// eslint-disable-next-line no-shadow
_addressFromIndex(pathBase, i) {
const dkey = this.hdk.derive(`${pathBase}/${i}`);
const address = ethUtil
.publicToAddress(dkey.publicKey, true)
.toString('hex');
return ethUtil.toChecksumAddress(`0x${address}`);
}
_pathFromAddress(address) {
return `${this.hdPath}/${this.indexFromAddress(address)}`;
}
indexFromAddress(address) {
var _a;
const checksummedAddress = ethUtil.toChecksumAddress(address);
let index = this.paths[checksummedAddress] ||
((_a = this.accountDetails[checksummedAddress]) === null || _a === void 0 ? void 0 : _a.index);
if (typeof index === 'undefined') {
for (let i = 0; i < MAX_INDEX; i++) {
if (checksummedAddress === this._addressFromIndex(pathBase, i)) {
index = i;
break;
}
}
}
if (typeof index === 'undefined') {
throw new Error('Unknown address');
}
return index;
}
getCurrentAccounts() {
return __awaiter(this, void 0, void 0, function* () {
yield this.unlock();
const addresses = yield this.getAccounts();
const currentPublicKey = this.getPathBasePublicKey();
const accounts = [];
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i];
yield this._fixAccountDetail(address);
const detail = this.accountDetails[ethUtil.toChecksumAddress(address)];
if ((detail === null || detail === void 0 ? void 0 : detail.hdPathBasePublicKey) !== currentPublicKey) {
continue;
}
try {
const account = {
address,
index: this.indexFromAddress(address) + 1,
};
accounts.push(account);
}
catch (e) {
console.log('address not found', address);
}
}
return accounts;
});
}
getPathBasePublicKey() {
return this.hdk.publicKey.toString('hex');
}
_fixAccountDetail(address) {
return __awaiter(this, void 0, void 0, function* () {
const checksummedAddress = ethUtil.toChecksumAddress(address);
const detail = this.accountDetails[checksummedAddress];
// The detail is already fixed
if ((detail === null || detail === void 0 ? void 0 : detail.hdPathBasePublicKey) && detail.hdPath) {
return;
}
let addressInDevice;
let index;
try {
index = this.indexFromAddress(address);
addressInDevice = this._addressFromIndex(pathBase, index);
}
catch (e) {
console.log('address not found', address);
}
if (!addressInDevice || !isSameAddress(address, addressInDevice)) {
return;
}
this.accountDetails[checksummedAddress] = Object.assign(Object.assign({}, detail), { index, hdPath: this._pathFromAddress(address), hdPathType: LedgerHDPathType.BIP44, hdPathBasePublicKey: this.getPathBasePublicKey() });
});
}
}
TrezorKeyring.type = keyringType;
exports.default = TrezorKeyring;