MyCrypto/common/vendor/trezor-connect.js

1017 lines
29 KiB
JavaScript
Raw Normal View History

/* 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<string>} 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<string>} value
*/
this.setCurrency = function(value) {
if (typeof value === 'string') {
manager.currency = value;
}
}
/**
* Set currency units (mBTC, BTC)
* @param {string|Array<string>} value
*/
this.setCurrencyUnits = function(value) {
if (typeof value === 'string') {
manager.currencyUnits = value;
}
}
/**
* Set coin info json url
* @param {string|Array<string>} 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<number>)} path
* @param {function(XPubKeyResult)} callback
* @param {?(string|array<number>)} 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<string>} signatures array of input signatures, in hex
*/
/**
* Sign a transaction in the device and return both serialized
* transaction and the signatures.
*
* @param {array<TxInputType>} inputs
* @param {array<TxOutputType>} outputs
* @param {function(SignTxResult)} callback
* @param {?(string|array<number>)} 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<TxRecipient>} recipients
* @param {function(SignTxResult)} callback
* @param {?(string|array<number>)} 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<number>)} 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 <trezor:login> 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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 =
'<style>@import url("@connect_path@/login_buttons.css")</style>';
var LOGIN_ONCLICK =
'TrezorConnect.requestLogin('
+ "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'"
+ ')';
var LOGIN_HTML =
'<div id="trezorconnect-wrapper">'
+ ' <a id="trezorconnect-button" onclick="' + LOGIN_ONCLICK + '">'
+ ' <span id="trezorconnect-icon"></span>'
+ ' <span id="trezorconnect-text">@text@</span>'
+ ' </a>'
+ ' <span id="trezorconnect-info">'
+ ' <a id="trezorconnect-infolink" href="https://www.buytrezor.com/"'
+ ' target="_blank">What is TREZOR?</a>'
+ ' </span>'
+ '</div>';
/**
* Find <trezor:login> 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', '<strong>TREZOR</strong>');
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;