542 lines
15 KiB
JavaScript
542 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
angular.module('copayApp.services')
|
|
.factory('ledger', function($log, bwcService, gettext, hwWallet, platformInfo) {
|
|
var root = {};
|
|
var LEDGER_CHROME_ID = "kkdpmhnladdopljabkgpacgpliggeeaf";
|
|
|
|
root.description = {
|
|
supported: platformInfo.supportsLedger,
|
|
id: 'ledger',
|
|
name: 'Ledger',
|
|
longName: 'Ledger Hardware Wallet',
|
|
isEmbeddedHardware: false,
|
|
supportsTestnet: false
|
|
};
|
|
|
|
root.callbacks = {};
|
|
root.hasSession = function() {
|
|
root._message({
|
|
command: "has_session"
|
|
});
|
|
}
|
|
|
|
root.getEntropySource = function(isMultisig, account, callback) {
|
|
root.getXPubKey(hwWallet.getEntropyPath(root.description.id, isMultisig, account), function(data) {
|
|
if (!data.success)
|
|
return callback(hwWallet._err(data));
|
|
|
|
return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey));
|
|
});
|
|
};
|
|
|
|
root.getXPubKey = function(path, callback) {
|
|
$log.debug('Ledger deriving xPub path:', path);
|
|
root.callbacks["get_xpubkey"] = callback;
|
|
root._messageAfterSession({
|
|
command: "get_xpubkey",
|
|
path: path
|
|
});
|
|
};
|
|
|
|
root.initSource = function(opts, callback) {
|
|
// No initialization for this hardware source.
|
|
return callback(opts);
|
|
};
|
|
|
|
root.getInfoForNewWallet = function(isMultisig, account, networkName, callback) {
|
|
// networkName not used for this hardware (always livenet)
|
|
root.getEntropySource(isMultisig, account, function(err, entropySource) {
|
|
if (err) return callback(err);
|
|
|
|
var opts = {};
|
|
opts.entropySource = entropySource;
|
|
root.getXPubKey(hwWallet.getAddressPath(root.description.id, isMultisig, account), function(data) {
|
|
if (!data.success) {
|
|
$log.warn(data.message);
|
|
return callback(data);
|
|
}
|
|
opts.extendedPublicKey = data.xpubkey;
|
|
opts.externalSource = root.description.id;
|
|
|
|
// Old ledger compat
|
|
opts.derivationStrategy = opts.account ? 'BIP48' : 'BIP44';
|
|
return callback(null, opts);
|
|
});
|
|
});
|
|
};
|
|
|
|
root._signP2SH = function(txp, account, isMultisig, callback) {
|
|
root.callbacks["sign_p2sh"] = callback;
|
|
var redeemScripts = [];
|
|
var paths = [];
|
|
var tx = bwcService.getUtils().buildTx(txp);
|
|
for (var i = 0; i < tx.inputs.length; i++) {
|
|
redeemScripts.push(new ByteString(tx.inputs[i].redeemScript.toBuffer().toString('hex'), GP.HEX).toString());
|
|
paths.push(hwWallet.getAddressPath(root.description.id, isMultisig, account) + txp.inputs[i].path.substring(1));
|
|
}
|
|
var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX));
|
|
var inputs = [];
|
|
for (var i = 0; i < splitTransaction.inputs.length; i++) {
|
|
var input = splitTransaction.inputs[i];
|
|
inputs.push([
|
|
root._reverseBytestring(input.prevout.bytes(0, 32)).toString(),
|
|
root._reverseBytestring(input.prevout.bytes(32)).toString()
|
|
]);
|
|
}
|
|
$log.debug('Ledger signing paths:', paths);
|
|
root._messageAfterSession({
|
|
command: "sign_p2sh",
|
|
inputs: inputs,
|
|
scripts: redeemScripts,
|
|
outputs_number: splitTransaction.outputs.length,
|
|
outputs_script: splitTransaction.outputScript.toString(),
|
|
paths: paths
|
|
});
|
|
};
|
|
|
|
root.signTx = function(txp, account, callback) {
|
|
|
|
// TODO Compat
|
|
var isMultisig = true;
|
|
if (txp.addressType == 'P2PKH') {
|
|
var msg = 'P2PKH wallets are not supported with ledger';
|
|
$log.error(msg);
|
|
return callback(msg);
|
|
} else {
|
|
root._signP2SH(txp, account, isMultisig, callback);
|
|
}
|
|
}
|
|
|
|
root._message = function(data) {
|
|
chrome.runtime.sendMessage(
|
|
LEDGER_CHROME_ID, {
|
|
request: data
|
|
},
|
|
function(response) {
|
|
root._callback(response);
|
|
}
|
|
);
|
|
}
|
|
|
|
root._messageAfterSession = function(data) {
|
|
root._after_session = data;
|
|
root._message({
|
|
command: "launch"
|
|
});
|
|
root._should_poll_session = true;
|
|
root._do_poll_session();
|
|
}
|
|
|
|
root._do_poll_session = function() {
|
|
root.hasSession();
|
|
if (root._should_poll_session) {
|
|
setTimeout(root._do_poll_session, 500);
|
|
}
|
|
}
|
|
|
|
root._callback = function(data) {
|
|
if (typeof data == "object") {
|
|
if (data.command == "has_session" && data.success) {
|
|
root._message(root._after_session);
|
|
root._after_session = null;
|
|
root._should_poll_session = false;
|
|
} else if (typeof root.callbacks[data.command] == "function") {
|
|
root.callbacks[data.command](data);
|
|
} else {}
|
|
} else {
|
|
root._should_poll_session = false;
|
|
Object.keys(root.callbacks).forEach(function(key) {
|
|
root.callbacks[key]({
|
|
success: false,
|
|
message: gettext("The Ledger Chrome application is not installed"),
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
root._splitTransaction = function(transaction) {
|
|
var result = {};
|
|
var inputs = [];
|
|
var outputs = [];
|
|
var offset = 0;
|
|
var version = transaction.bytes(offset, 4);
|
|
offset += 4;
|
|
var varint = root._getVarint(transaction, offset);
|
|
var numberInputs = varint[0];
|
|
offset += varint[1];
|
|
for (var i = 0; i < numberInputs; i++) {
|
|
var input = {};
|
|
input['prevout'] = transaction.bytes(offset, 36);
|
|
offset += 36;
|
|
varint = root._getVarint(transaction, offset);
|
|
offset += varint[1];
|
|
input['script'] = transaction.bytes(offset, varint[0]);
|
|
offset += varint[0];
|
|
input['sequence'] = transaction.bytes(offset, 4);
|
|
offset += 4;
|
|
inputs.push(input);
|
|
}
|
|
varint = root._getVarint(transaction, offset);
|
|
var numberOutputs = varint[0];
|
|
offset += varint[1];
|
|
var outputStartOffset = offset;
|
|
for (var i = 0; i < numberOutputs; i++) {
|
|
var output = {};
|
|
output['amount'] = transaction.bytes(offset, 8);
|
|
offset += 8;
|
|
varint = root._getVarint(transaction, offset);
|
|
offset += varint[1];
|
|
output['script'] = transaction.bytes(offset, varint[0]);
|
|
offset += varint[0];
|
|
outputs.push(output);
|
|
}
|
|
var locktime = transaction.bytes(offset, 4);
|
|
result['version'] = version;
|
|
result['inputs'] = inputs;
|
|
result['outputs'] = outputs;
|
|
result['locktime'] = locktime;
|
|
result['outputScript'] = transaction.bytes(outputStartOffset, offset - outputStartOffset);
|
|
return result;
|
|
}
|
|
|
|
root._getVarint = function(data, offset) {
|
|
if (data.byteAt(offset) < 0xfd) {
|
|
return [data.byteAt(offset), 1];
|
|
}
|
|
if (data.byteAt(offset) == 0xfd) {
|
|
return [((data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 3];
|
|
}
|
|
if (data.byteAt(offset) == 0xfe) {
|
|
return [((data.byteAt(offset + 4) << 24) + (data.byteAt(offset + 3) << 16) +
|
|
(data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 5];
|
|
}
|
|
}
|
|
|
|
root._reverseBytestring = function(x) {
|
|
var res = "";
|
|
for (var i = x.length - 1; i >= 0; i--) {
|
|
res += Convert.toHexByte(x.byteAt(i));
|
|
}
|
|
return new ByteString(res, GP.HEX);
|
|
}
|
|
|
|
return root;
|
|
});
|
|
|
|
var Convert = {};
|
|
|
|
/**
|
|
* Convert a binary string to his hexadecimal representation
|
|
* @param {String} src binary string
|
|
* @static
|
|
* @returns {String} hexadecimal representation
|
|
*/
|
|
Convert.stringToHex = function(src) {
|
|
var r = "";
|
|
var hexes = new Array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
|
|
for (var i = 0; i < src.length; i++) {
|
|
r += hexes[src.charCodeAt(i) >> 4] + hexes[src.charCodeAt(i) & 0xf];
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* Convert an hexadecimal string to its binary representation
|
|
* @param {String} src hexadecimal string
|
|
* @static
|
|
* @return {Array} byte array
|
|
* @throws {InvalidString} if the string isn't properly formatted
|
|
*/
|
|
Convert.hexToBin = function(src) {
|
|
var result = "";
|
|
var digits = "0123456789ABCDEF";
|
|
if ((src.length % 2) != 0) {
|
|
throw "Invalid string";
|
|
}
|
|
src = src.toUpperCase();
|
|
for (var i = 0; i < src.length; i += 2) {
|
|
var x1 = digits.indexOf(src.charAt(i));
|
|
if (x1 < 0) {
|
|
return "";
|
|
}
|
|
var x2 = digits.indexOf(src.charAt(i + 1));
|
|
if (x2 < 0) {
|
|
return "";
|
|
}
|
|
result += String.fromCharCode((x1 << 4) + x2);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Convert a double digit hexadecimal number to an integer
|
|
* @static
|
|
* @param {String} data buffer containing the digit to parse
|
|
* @param {Number} offset offset to the digit (default is 0)
|
|
* @returns {Number} converted digit
|
|
*/
|
|
Convert.readHexDigit = function(data, offset) {
|
|
var digits = '0123456789ABCDEF';
|
|
if (typeof offset == "undefined") {
|
|
offset = 0;
|
|
}
|
|
return (digits.indexOf(data.substring(offset, offset + 1).toUpperCase()) << 4) + (digits.indexOf(data.substring(offset + 1, offset + 2).toUpperCase()));
|
|
}
|
|
|
|
/**
|
|
* Convert a number to a two digits hexadecimal string (deprecated)
|
|
* @static
|
|
* @param {Number} number number to convert
|
|
* @returns {String} converted number
|
|
*/
|
|
Convert.toHexDigit = function(number) {
|
|
var digits = '0123456789abcdef';
|
|
return digits.charAt(number >> 4) + digits.charAt(number & 0x0F);
|
|
}
|
|
|
|
/**
|
|
* Convert a number to a two digits hexadecimal string (similar to toHexDigit)
|
|
* @static
|
|
* @param {Number} number number to convert
|
|
* @returns {String} converted number
|
|
*/
|
|
Convert.toHexByte = function(number) {
|
|
return Convert.toHexDigit(number);
|
|
}
|
|
|
|
/**
|
|
* Convert a BCD number to a two digits hexadecimal string
|
|
* @static
|
|
* @param {Number} number number to convert
|
|
* @returns {String} converted number
|
|
*/
|
|
Convert.toHexByteBCD = function(numberBCD) {
|
|
var number = ((numberBCD / 10) * 16) + (numberBCD % 10);
|
|
return Convert.toHexDigit(number);
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert a number to an hexadecimal short number
|
|
* @static
|
|
* @param {Number} number number to convert
|
|
* @returns {String} converted number
|
|
*/
|
|
Convert.toHexShort = function(number) {
|
|
return Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
|
|
}
|
|
|
|
/**
|
|
* Convert a number to an hexadecimal int number
|
|
* @static
|
|
* @param {Number} number number to convert
|
|
* @returns {String} converted number
|
|
*/
|
|
Convert.toHexInt = function(number) {
|
|
return Convert.toHexDigit((number >> 24) & 0xff) + Convert.toHexDigit((number >> 16) & 0xff) +
|
|
Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
|
|
}
|
|
|
|
|
|
var GP = {};
|
|
GP.ASCII = 1;
|
|
GP.HEX = 5;
|
|
|
|
/**
|
|
* @class GPScript ByteString implementation
|
|
* @param {String} value initial value
|
|
* @param {HEX|ASCII} encoding encoding to use
|
|
* @property {Number} length length of the ByteString
|
|
* @constructs
|
|
*/
|
|
var ByteString = function(value, encoding) {
|
|
this.encoding = encoding;
|
|
this.hasBuffer = (typeof Buffer != 'undefined');
|
|
if (this.hasBuffer && (value instanceof Buffer)) {
|
|
this.value = value;
|
|
this.encoding = GP.HEX;
|
|
} else {
|
|
switch (encoding) {
|
|
case GP.HEX:
|
|
if (!this.hasBuffer) {
|
|
this.value = Convert.hexToBin(value);
|
|
} else {
|
|
this.value = new Buffer(value, 'hex');
|
|
}
|
|
break;
|
|
|
|
case GP.ASCII:
|
|
if (!this.hasBuffer) {
|
|
this.value = value;
|
|
} else {
|
|
this.value = new Buffer(value, 'ascii');
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw "Invalid arguments";
|
|
}
|
|
}
|
|
this.length = this.value.length;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the byte value at the given index
|
|
* @param {Number} index index
|
|
* @returns {Number} byte value
|
|
*/
|
|
ByteString.prototype.byteAt = function(index) {
|
|
if (arguments.length < 1) {
|
|
throw "Argument missing";
|
|
}
|
|
if (typeof index != "number") {
|
|
throw "Invalid index";
|
|
}
|
|
if ((index < 0) || (index >= this.value.length)) {
|
|
throw "Invalid index offset";
|
|
}
|
|
if (!this.hasBuffer) {
|
|
return Convert.readHexDigit(Convert.stringToHex(this.value.substring(index, index + 1)));
|
|
} else {
|
|
return this.value[index];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve a subset of the ByteString
|
|
* @param {Number} offset offset to start at
|
|
* @param {Number} [count] size of the target ByteString (default : use the remaining length)
|
|
* @returns {ByteString} subset of the original ByteString
|
|
*/
|
|
ByteString.prototype.bytes = function(offset, count) {
|
|
var result;
|
|
if (arguments.length < 1) {
|
|
throw "Argument missing";
|
|
}
|
|
if (typeof offset != "number") {
|
|
throw "Invalid offset";
|
|
}
|
|
//if ((offset < 0) || (offset >= this.value.length)) {
|
|
if (offset < 0) {
|
|
throw "Invalid offset";
|
|
}
|
|
if (typeof count == "number") {
|
|
if (count < 0) {
|
|
throw "Invalid count";
|
|
}
|
|
if (!this.hasBuffer) {
|
|
result = new ByteString(this.value.substring(offset, offset + count), GP.ASCII);
|
|
} else {
|
|
result = new Buffer(count);
|
|
this.value.copy(result, 0, offset, offset + count);
|
|
}
|
|
} else
|
|
if (typeof count == "undefined") {
|
|
if (!this.hasBuffer) {
|
|
result = new ByteString(this.value.substring(offset), GP.ASCII);
|
|
} else {
|
|
result = new Buffer(this.value.length - offset);
|
|
this.value.copy(result, 0, offset, this.value.length);
|
|
}
|
|
} else {
|
|
throw "Invalid count";
|
|
}
|
|
if (!this.hasBuffer) {
|
|
result.encoding = this.encoding;
|
|
return result;
|
|
} else {
|
|
return new ByteString(result, GP.HEX);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends two ByteString
|
|
* @param {ByteString} target ByteString to append
|
|
* @returns {ByteString} result of the concatenation
|
|
*/
|
|
ByteString.prototype.concat = function(target) {
|
|
if (arguments.length < 1) {
|
|
throw "Not enough arguments";
|
|
}
|
|
if (!(target instanceof ByteString)) {
|
|
throw "Invalid argument";
|
|
}
|
|
if (!this.hasBuffer) {
|
|
var result = this.value + target.value;
|
|
var x = new ByteString(result, GP.ASCII);
|
|
x.encoding = this.encoding;
|
|
return x;
|
|
} else {
|
|
var result = Buffer.concat([this.value, target.value]);
|
|
return new ByteString(result, GP.HEX);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if two ByteString are equal
|
|
* @param {ByteString} target ByteString to check against
|
|
* @returns {Boolean} true if the two ByteString are equal
|
|
*/
|
|
ByteString.prototype.equals = function(target) {
|
|
if (arguments.length < 1) {
|
|
throw "Not enough arguments";
|
|
}
|
|
if (!(target instanceof ByteString)) {
|
|
throw "Invalid argument";
|
|
}
|
|
if (!this.hasBuffer) {
|
|
return (this.value == target.value);
|
|
} else {
|
|
return Buffer.equals(this.value, target.value);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert the ByteString to a String using the given encoding
|
|
* @param {HEX|ASCII|UTF8|BASE64|CN} encoding encoding to use
|
|
* @return {String} converted content
|
|
*/
|
|
ByteString.prototype.toString = function(encoding) {
|
|
var targetEncoding = this.encoding;
|
|
if (arguments.length >= 1) {
|
|
if (typeof encoding != "number") {
|
|
throw "Invalid encoding";
|
|
}
|
|
switch (encoding) {
|
|
case GP.HEX:
|
|
case GP.ASCII:
|
|
targetEncoding = encoding;
|
|
break;
|
|
|
|
default:
|
|
throw "Unsupported arguments";
|
|
}
|
|
targetEncoding = encoding;
|
|
}
|
|
switch (targetEncoding) {
|
|
case GP.HEX:
|
|
if (!this.hasBuffer) {
|
|
return Convert.stringToHex(this.value);
|
|
} else {
|
|
return this.value.toString('hex');
|
|
}
|
|
case GP.ASCII:
|
|
if (!this.hasBuffer) {
|
|
return this.value;
|
|
} else {
|
|
return this.value.toString();
|
|
}
|
|
default:
|
|
throw "Unsupported";
|
|
}
|
|
}
|
|
|
|
ByteString.prototype.toStringIE = function(encoding) {
|
|
return this.toString(encoding);
|
|
}
|
|
|
|
ByteString.prototype.toBuffer = function() {
|
|
return this.value;
|
|
}
|