mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 19:10:22 +01:00
1138 lines
27 KiB
JavaScript
1138 lines
27 KiB
JavaScript
/* eslint-disable */
|
||
/* prettier-ignore */
|
||
|
||
/**
|
||
* (C) 2017 SatoshiLabs
|
||
*
|
||
* GPLv3
|
||
*/
|
||
var TREZOR_CONNECT_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 + "/" + TREZOR_CONNECT_VERSION;
|
||
var POPUP_URL =
|
||
window.TREZOR_POPUP_URL ||
|
||
POPUP_PATH + "/popup/popup.html?v=" + new Date().getTime();
|
||
|
||
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.currency = this.currency || null;
|
||
message.currencyUnits = this.currencyUnits || null;
|
||
message.coinInfoURL = this.coinInfoURL || 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);
|
||
};
|
||
}
|
||
|
||
const connect = new TrezorConnect();
|
||
module.exports = connect; |