/* eslint-ignore */ /* prettier-ignore */ /** * (C) 2017 SatoshiLabs * * GPLv3 */ var VERSION = 4; if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } var HD_HARDENED = 0x80000000; // react sometimes adds some other parameters that should not be there function _fwStrFix(obj, fw) { if (typeof fw === 'string') { obj.requiredFirmware = fw; } return obj; } 'use strict'; var chrome = window.chrome; var IS_CHROME_APP = chrome && chrome.app && chrome.app.window; var ERR_TIMED_OUT = 'Loading timed out'; var ERR_WINDOW_CLOSED = 'Window closed'; var ERR_WINDOW_BLOCKED = 'Window blocked'; var ERR_ALREADY_WAITING = 'Already waiting for a response'; var ERR_CHROME_NOT_CONNECTED = 'Internal Chrome popup is not responding.'; var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false; var CHROME_URL = window.TREZOR_CHROME_URL || './chrome/wrapper.html'; var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || 'https://connect.trezor.io'; var POPUP_PATH = window.TREZOR_POPUP_PATH || POPUP_ORIGIN + '/' + VERSION; if (window.location.hostname === 'localhost' && !window.TREZOR_POPUP_ORIGIN) { // development settings //POPUP_ORIGIN = window.location.origin; //POPUP_PATH = POPUP_ORIGIN; } var POPUP_URL = window.TREZOR_POPUP_URL || POPUP_PATH + '/popup/popup.html'; var POPUP_INIT_TIMEOUT = 15000; /** * Public API. */ function TrezorConnect() { var manager = new PopupManager(); /** * Popup errors. */ this.ERR_TIMED_OUT = ERR_TIMED_OUT; this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED; this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED; /** * Open the popup for further communication. All API functions open the * popup automatically, but if you need to generate some parameters * asynchronously, use `open` first to avoid popup blockers. * @param {function(?Error)} callback */ this.open = function (callback) { var onchannel = function (result) { if (result instanceof Error) { callback(result); } else { callback(); } }; manager.waitForChannel(onchannel); }; /** * Close the opened popup, if any. */ this.close = function () { manager.close(); }; /** * Enable or disable closing the opened popup after a successful call. * @param {boolean} value */ this.closeAfterSuccess = function (value) { manager.closeAfterSuccess = value; }; /** * Enable or disable closing the opened popup after a failed call. * @param {boolean} value */ this.closeAfterFailure = function (value) { manager.closeAfterFailure = value; }; /** * Set bitcore server * @param {string|Array} value */ this.setBitcoreURLS = function(value) { if (typeof value === 'string') { manager.bitcoreURLS = [ value ]; }else if (value instanceof Array) { manager.bitcoreURLS = value; } } /** * Set currency. Human readable coin name * @param {string|Array} value */ this.setCurrency = function(value) { if (typeof value === 'string') { manager.currency = value; } } /** * Set currency units (mBTC, BTC) * @param {string|Array} value */ this.setCurrencyUnits = function(value) { if (typeof value === 'string') { manager.currencyUnits = value; } } /** * Set coin info json url * @param {string|Array} value */ this.setCoinInfoURL = function(value) { if (typeof value === 'string') { manager.coinInfoURL = value; } } /** * Set max. limit for account discovery * @param {number} value */ this.setAccountDiscoveryLimit = function(value) { if(!isNaN(value)) manager.accountDiscoveryLimit = value; } /** * Set max. gap for account discovery * @param {number} value */ this.setAccountDiscoveryGapLength = function(value) { if(!isNaN(value)) manager.accountDiscoveryGapLength = value; } /** * Set discovery BIP44 coin type * @param {number} value */ this.setAccountDiscoveryBip44CoinType = function(value) { if(!isNaN(value)) manager.accountDiscoveryBip44CoinType = value; } /** * @typedef XPubKeyResult * @param {boolean} success * @param {?string} error * @param {?string} xpubkey serialized extended public key * @param {?string} path BIP32 serializd path of the key */ /** * Load BIP32 extended public key by path. * * Path can be specified either in the string form ("m/44'/1/0") or as * raw integer array. In case you omit the path, user is asked to select * a BIP32 account to export, and the result contains m/44'/0'/x' node * of the account. * * @param {?(string|array)} path * @param {function(XPubKeyResult)} callback * @param {?(string|array)} requiredFirmware */ this.getXPubKey = function (path, callback, requiredFirmware) { if (typeof path === 'string') { path = parseHDPath(path); } manager.sendWithChannel(_fwStrFix({ type: 'xpubkey', path: path }, requiredFirmware), callback); }; this.getFreshAddress = function (callback, requiredFirmware) { var wrapperCallback = function (result) { if (result.success) { callback({success: true, address: result.freshAddress}); } else { callback(result); } } manager.sendWithChannel(_fwStrFix({ type: 'accountinfo' }, requiredFirmware), wrapperCallback); } this.getAccountInfo = function (input, callback, requiredFirmware) { try { manager.sendWithChannel(_fwStrFix({ type: 'accountinfo', description: input }, requiredFirmware), callback); } catch(e) { callback({success: false, error: e}); } } this.getAllAccountsInfo = function(callback, requiredFirmware){ try { manager.sendWithChannel(_fwStrFix({ type: 'allaccountsinfo', description: 'all' }, requiredFirmware), callback); } catch(e) { callback({success: false, error: e}); } } this.getBalance = function (callback, requiredFirmware) { manager.sendWithChannel(_fwStrFix({ type: 'accountinfo' }, requiredFirmware), callback) } /** * @typedef SignTxResult * @param {boolean} success * @param {?string} error * @param {?string} serialized_tx serialized tx, in hex, including signatures * @param {?array} signatures array of input signatures, in hex */ /** * Sign a transaction in the device and return both serialized * transaction and the signatures. * * @param {array} inputs * @param {array} outputs * @param {function(SignTxResult)} callback * @param {?(string|array)} requiredFirmware * * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto */ this.signTx = function (inputs, outputs, callback, requiredFirmware, coin) { manager.sendWithChannel(_fwStrFix({ type: 'signtx', inputs: inputs, outputs: outputs, coin: coin }, requiredFirmware), callback); }; // new implementation with ethereum at beginnig this.ethereumSignTx = function() { this.signEthereumTx.apply(this, arguments); } // old fallback this.signEthereumTx = function ( address_n, nonce, gas_price, gas_limit, to, value, data, chain_id, callback, requiredFirmware ) { if (requiredFirmware == null) { requiredFirmware = '1.4.0'; // first firmware that supports ethereum } if (typeof address_n === 'string') { address_n = parseHDPath(address_n); } manager.sendWithChannel(_fwStrFix({ type: 'signethtx', address_n: address_n, nonce: nonce, gas_price: gas_price, gas_limit: gas_limit, to: to, value: value, data: data, chain_id: chain_id, }, requiredFirmware), callback); }; /** * @typedef TxRecipient * @param {number} amount the amount to send, in satoshis * @param {string} address the address of the recipient */ /** * Compose a transaction by doing BIP-0044 discovery, letting the user * select an account, and picking UTXO by internal preferences. * Transaction is then signed and returned in the same format as * `signTx`. Only supports BIP-0044 accounts (single-signature). * * @param {array} recipients * @param {function(SignTxResult)} callback * @param {?(string|array)} requiredFirmware */ this.composeAndSignTx = function (recipients, callback, requiredFirmware) { manager.sendWithChannel(_fwStrFix({ type: 'composetx', recipients: recipients }, requiredFirmware), callback); }; /** * @typedef RequestLoginResult * @param {boolean} success * @param {?string} error * @param {?string} public_key public key used for signing, in hex * @param {?string} signature signature, in hex */ /** * Sign a login challenge for active origin. * * @param {?string} hosticon * @param {string} challenge_hidden * @param {string} challenge_visual * @param {string|function(RequestLoginResult)} callback * @param {?(string|array)} requiredFirmware * * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto */ this.requestLogin = function ( hosticon, challenge_hidden, challenge_visual, callback, requiredFirmware ) { if (typeof callback === 'string') { // special case for a login through button. // `callback` is name of global var callback = window[callback]; } if (!callback) { throw new TypeError('TrezorConnect: login callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'login', icon: hosticon, challenge_hidden: challenge_hidden, challenge_visual: challenge_visual }, requiredFirmware), callback); }; /** * @typedef SignMessageResult * @param {boolean} success * @param {?string} error * @param {?string} address address (in base58check) * @param {?string} signature signature, in base64 */ /** * Sign a message * * @param {string|array} path * @param {string} message to sign (ascii) * @param {string|function(SignMessageResult)} callback * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) * @param {?(string|array)} requiredFirmware * */ this.signMessage = function ( path, message, callback, opt_coin, requiredFirmware ) { if (typeof path === 'string') { path = parseHDPath(path); } if (!opt_coin) { opt_coin = 'Bitcoin'; } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'signmsg', path: path, message: message, coin: opt_coin, }, requiredFirmware), callback); }; /** * Sign an Ethereum message * * @param {string|array} path * @param {string} message to sign (ascii) * @param {string|function(SignMessageResult)} callback * @param {?(string|array)} requiredFirmware * */ this.ethereumSignMessage = function ( path, message, callback, requiredFirmware ) { if (typeof path === 'string') { path = parseHDPath(path); } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'signethmsg', path: path, message: message, }, requiredFirmware), callback); }; /** * Verify message * * @param {string} address * @param {string} signature (base64) * @param {string} message (string) * @param {string|function()} callback * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) * @param {?(string|array)} requiredFirmware * */ this.verifyMessage = function ( address, signature, message, callback, opt_coin, requiredFirmware ) { if (!opt_coin) { opt_coin = 'Bitcoin'; } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'verifymsg', address: address, signature: signature, message: message, coin: {coin_name: opt_coin}, }, requiredFirmware), callback); }; /** * Verify ethereum message * * @param {string} address * @param {string} signature (base64) * @param {string} message (string) * @param {string|function()} callback * @param {?(string|array)} requiredFirmware * */ this.ethereumVerifyMessage = function ( address, signature, message, callback, requiredFirmware ) { if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'verifyethmsg', address: address, signature: signature, message: message, }, requiredFirmware), callback); }; /** * Symmetric key-value encryption * * @param {string|array} path * @param {string} key to show on device display * @param {string} value hexadecimal value, length a multiple of 16 bytes * @param {boolean} encrypt / decrypt direction * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) * @param {string|function()} callback * @param {?(string|array)} requiredFirmware * */ this.cipherKeyValue = function ( path, key, value, encrypt, ask_on_encrypt, ask_on_decrypt, callback, requiredFirmware ) { if (typeof path === 'string') { path = parseHDPath(path); } if (typeof value !== 'string') { throw new TypeError('TrezorConnect: Value must be a string'); } if (!(/^[0-9A-Fa-f]*$/.test(value))) { throw new TypeError('TrezorConnect: Value must be hexadecimal'); } if (value.length % 32 !== 0) { // 1 byte == 2 hex strings throw new TypeError('TrezorConnect: Value length must be multiple of 16 bytes'); } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel(_fwStrFix({ type: 'cipherkeyvalue', path: path, key: key, value: value, encrypt: !!encrypt, ask_on_encrypt: !!ask_on_encrypt, ask_on_decrypt: !!ask_on_decrypt }, requiredFirmware), callback); }; this.nemGetAddress = function ( address_n, network, callback, requiredFirmware ) { if (requiredFirmware == null) { requiredFirmware = '1.6.0'; // first firmware that supports NEM } if (typeof address_n === 'string') { address_n = parseHDPath(address_n); } manager.sendWithChannel(_fwStrFix({ type: 'nemGetAddress', address_n: address_n, network: network, }, requiredFirmware), callback); } this.nemSignTx = function ( address_n, transaction, callback, requiredFirmware ) { if (requiredFirmware == null) { requiredFirmware = '1.6.0'; // first firmware that supports NEM } if (typeof address_n === 'string') { address_n = parseHDPath(address_n); } manager.sendWithChannel(_fwStrFix({ type: 'nemSignTx', address_n: address_n, transaction: transaction }, requiredFirmware), callback); } this.pushTransaction = function ( rawTx, callback ) { if (!(/^[0-9A-Fa-f]*$/.test(rawTx))) { throw new TypeError('TrezorConnect: Transaction must be hexadecimal'); } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } manager.sendWithChannel({ type: 'pushtx', rawTx: rawTx, }, callback); } /** * Display address on device * * @param {array} address * @param {string} coin * @param {boolean} segwit * @param {?(string|array)} requiredFirmware * */ this.getAddress = function (address, coin, segwit, callback, requiredFirmware) { if (typeof address === 'string') { address = parseHDPath(address); } manager.sendWithChannel(_fwStrFix({ type: 'getaddress', address_n: address, coin: coin, segwit: segwit }, requiredFirmware), callback); } /** * Display ethereum address on device * * @param {array} address * @param {?(string|array)} requiredFirmware * */ this.ethereumGetAddress = function (address, callback, requiredFirmware) { if (typeof address === 'string') { address = parseHDPath(address); } manager.sendWithChannel(_fwStrFix({ type: 'ethgetaddress', address_n: address, }, requiredFirmware), callback); } var LOGIN_CSS = ''; var LOGIN_ONCLICK = 'TrezorConnect.requestLogin(' + "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" + ')'; var LOGIN_HTML = '
' + ' ' + ' ' + ' @text@' + ' ' + ' ' + ' What is TREZOR?' + ' ' + '
'; /** * Find elements and replace them with login buttons. * It's not required to use these special elements, feel free to call * `TrezorConnect.requestLogin` directly. */ this.renderLoginButtons = function () { var elements = document.getElementsByTagName('trezor:login'); for (var i = 0; i < elements.length; i++) { var e = elements[i]; var text = e.getAttribute('text') || 'Sign in with TREZOR'; var callback = e.getAttribute('callback') || ''; var hosticon = e.getAttribute('icon') || ''; var challenge_hidden = e.getAttribute('challenge_hidden') || ''; var challenge_visual = e.getAttribute('challenge_visual') || ''; // it's not valid to put markup into attributes, so let users // supply a raw text and make TREZOR bold text = text.replace('TREZOR', 'TREZOR'); e.outerHTML = (LOGIN_CSS + LOGIN_HTML) .replace('@text@', text) .replace('@callback@', callback) .replace('@hosticon@', hosticon) .replace('@challenge_hidden@', challenge_hidden) .replace('@challenge_visual@', challenge_visual) .replace('@connect_path@', POPUP_PATH); } }; } /* * `getXPubKey()` */ function parseHDPath(string) { return string .toLowerCase() .split('/') .filter(function (p) { return p !== 'm'; }) .map(function (p) { var hardened = false; if (p[p.length - 1] === "'") { hardened = true; p = p.substr(0, p.length - 1); } if (isNaN(p)) { throw new Error('Not a valid path.'); } var n = parseInt(p); if (hardened) { // hardened index n = (n | 0x80000000) >>> 0; } return n; }); } /* * Popup management */ function ChromePopup(url, name, width, height) { var left = (screen.width - width) / 2; var top = (screen.height - height) / 2; var opts = { id: name, innerBounds: { width: width, height: height, left: left, top: top } }; var closed = function () { if (this.onclose) { this.onclose(false); // never report as blocked } }.bind(this); var opened = function (w) { this.window = w; this.window.onClosed.addListener(closed); }.bind(this); chrome.app.window.create(url, opts, opened); this.name = name; this.window = null; this.onclose = null; } function ChromeChannel(popup, waiting) { var port = null; var respond = function (data) { if (waiting) { var w = waiting; waiting = null; w(data); } }; var setup = function (p) { if (p.name === popup.name) { port = p; port.onMessage.addListener(respond); chrome.runtime.onConnect.removeListener(setup); } }; chrome.runtime.onConnect.addListener(setup); this.respond = respond; this.close = function () { chrome.runtime.onConnect.removeListener(setup); port.onMessage.removeListener(respond); port.disconnect(); port = null; }; this.send = function (value, callback) { if (waiting === null) { waiting = callback; if (port) { port.postMessage(value); } else { throw new Error(ERR_CHROME_NOT_CONNECTED); } } else { throw new Error(ERR_ALREADY_WAITING); } }; } function Popup(url, origin, name, width, height) { var left = (screen.width - width) / 2; var top = (screen.height - height) / 2; var opts = 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',menubar=no' + ',toolbar=no' + ',location=no' + ',personalbar=no' + ',status=no'; var w = window.open(url, name, opts); var interval; var blocked = w.closed; var iterate = function () { if (w.closed) { clearInterval(interval); if (this.onclose) { this.onclose(blocked); } } }.bind(this); interval = setInterval(iterate, 100); this.window = w; this.origin = origin; this.onclose = null; } function Channel(popup, waiting) { var respond = function (data) { if (waiting) { var w = waiting; waiting = null; w(data); } }; var receive = function (event) { var org1 = event.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; var org2 = popup.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; //if (event.source === popup.window && event.origin === popup.origin) { if (event.source === popup.window && org1 === org2) { respond(event.data); } }; window.addEventListener('message', receive); this.respond = respond; this.close = function () { window.removeEventListener('message', receive); }; this.send = function (value, callback) { if (waiting === null) { waiting = callback; popup.window.postMessage(value, popup.origin); } else { throw new Error(ERR_ALREADY_WAITING); } }; } function ConnectedChannel(p) { var ready = function () { clearTimeout(this.timeout); this.popup.onclose = null; this.ready = true; this.onready(); }.bind(this); var closed = function (blocked) { clearTimeout(this.timeout); this.channel.close(); if (blocked) { this.onerror(new Error(ERR_WINDOW_BLOCKED)); } else { this.onerror(new Error(ERR_WINDOW_CLOSED)); } }.bind(this); var timedout = function () { this.popup.onclose = null; if (this.popup.window) { this.popup.window.close(); } this.channel.close(); this.onerror(new Error(ERR_TIMED_OUT)); }.bind(this); if (IS_CHROME_APP) { this.popup = new ChromePopup(p.chromeUrl, p.name, p.width, p.height); this.channel = new ChromeChannel(this.popup, ready); } else { this.popup = new Popup(p.url, p.origin, p.name, p.width, p.height); this.channel = new Channel(this.popup, ready); } this.timeout = setTimeout(timedout, POPUP_INIT_TIMEOUT); this.popup.onclose = closed; this.ready = false; this.onready = null; this.onerror = null; } function PopupManager() { var cc = null; var closed = function () { cc.channel.respond(new Error(ERR_WINDOW_CLOSED)); cc.channel.close(); cc = null; }; var open = function (callback) { cc = new ConnectedChannel({ name: 'trezor-connect', width: 600, height: 500, origin: POPUP_ORIGIN, path: POPUP_PATH, url: POPUP_URL, chromeUrl: CHROME_URL }); cc.onready = function () { cc.popup.onclose = closed; callback(cc.channel); }; cc.onerror = function (error) { cc = null; callback(error); }; }.bind(this); this.closeAfterSuccess = true; this.closeAfterFailure = true; this.close = function () { if (cc && cc.popup.window) { cc.popup.window.close(); } }; this.waitForChannel = function (callback) { if (cc) { if (cc.ready) { callback(cc.channel); } else { callback(new Error(ERR_ALREADY_WAITING)); } } else { try { open(callback); } catch (e) { callback(new Error(ERR_WINDOW_BLOCKED)); } } }; this.sendWithChannel = function (message, callback) { message.bitcoreURLS = this.bitcoreURLS || null; message.accountDiscoveryLimit = this.accountDiscoveryLimit || null; message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null; message.accountDiscoveryBip44CoinType = this.accountDiscoveryBip44CoinType || null; var respond = function (response) { var succ = response.success && this.closeAfterSuccess; var fail = !response.success && this.closeAfterFailure; if (succ || fail) { this.close(); } callback(response); }.bind(this); var onresponse = function (response) { if (response instanceof Error) { var error = response; respond({ success: false, error: error.message }); } else { respond(response); } }; var onchannel = function (channel) { if (channel instanceof Error) { var error = channel; respond({ success: false, error: error.message }); } else { channel.send(message, onresponse); } }; this.waitForChannel(onchannel); }; } var connect = new TrezorConnect(); module.exports = connect;