@ducatus/ducatus-wallet-client-rev
Version:
Client for @ducatus/ducatus-wallet-service-rev
421 lines (346 loc) • 11.6 kB
text/typescript
'use strict';
var $ = require('preconditions').singleton();
import * as _ from 'lodash';
import { Constants, Utils } from './common';
import { Credentials } from './credentials';
import { BitcoreLib, Deriver, Transactions } from '@ducatus/ducatus-crypto-wallet-core-rev';
var Bitcore = BitcoreLib;
var Mnemonic = require('bitcore-mnemonic');
var sjcl = require('sjcl');
var log = require('./log');
const async = require('async');
const Uuid = require('uuid');
var Errors = require('./errors');
const wordsForLang: any = {
en: Mnemonic.Words.ENGLISH,
es: Mnemonic.Words.SPANISH,
ja: Mnemonic.Words.JAPANESE,
zh: Mnemonic.Words.CHINESE,
fr: Mnemonic.Words.FRENCH,
it: Mnemonic.Words.ITALIAN
};
// we always set 'livenet' for xprivs. it has not consecuences
// other than the serialization
const NETWORK: string = 'livenet';
export class Key {
version: number;
use0forBCH: boolean;
use44forMultisig: boolean;
compliantDerivation: boolean;
id: any;
static FIELDS = [
'xPrivKey', // obsolte
'xPrivKeyEncrypted', // obsolte
'mnemonic',
'mnemonicEncrypted',
'mnemonicHasPassphrase',
'fingerPrint', // BIP32 32bit fingerprint
'compliantDerivation',
'BIP45',
// data for derived credentials.
'use0forBCH', // use the 0 coin' path element in BCH (legacy)
'use44forMultisig', // use the purpose 44' for multisig wallts (legacy)
'version',
'id'
];
constructor() {
this.version = 1;
this.use0forBCH = false;
this.use44forMultisig = false;
this.compliantDerivation = true;
this.id = Uuid.v4();
}
static match(a, b) {
return a.id == b.id;
}
static create = function(opts) {
opts = opts || {};
if (opts.language && !wordsForLang[opts.language]) throw new Error('Unsupported language');
var m = new Mnemonic(wordsForLang[opts.language]);
while (!Mnemonic.isValid(m.toString())) {
m = new Mnemonic(wordsForLang[opts.language]);
}
let x: any = new Key();
let xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK);
x.xPrivKey = xpriv.toString();
x.fingerPrint = xpriv.fingerPrint.toString('hex');
x.mnemonic = m.phrase;
x.mnemonicHasPassphrase = !!opts.passphrase;
// bug backwards compatibility flags
x.use0forBCH = opts.useLegacyCoinType;
x.use44forMultisig = opts.useLegacyPurpose;
x.compliantDerivation = !opts.nonCompliantDerivation;
return x;
};
static fromMnemonic = function(words, opts) {
$.checkArgument(words);
if (opts) $.shouldBeObject(opts);
opts = opts || {};
var m = new Mnemonic(words);
var x: any = new Key();
let xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK);
x.xPrivKey = xpriv.toString();
x.fingerPrint = xpriv.fingerPrint.toString('hex');
x.mnemonic = words;
x.mnemonicHasPassphrase = !!opts.passphrase;
x.use0forBCH = opts.useLegacyCoinType;
x.use44forMultisig = opts.useLegacyPurpose;
x.compliantDerivation = !opts.nonCompliantDerivation;
return x;
};
static fromExtendedPrivateKey = function(xPriv, opts) {
$.checkArgument(xPriv);
opts = opts || {};
let xpriv;
try {
xpriv = new Bitcore.HDPrivateKey(xPriv);
} catch (e) {
throw new Error('Invalid argument');
}
var x: any = new Key();
x.xPrivKey = xpriv.toString();
x.fingerPrint = xpriv.fingerPrint.toString('hex');
x.mnemonic = null;
x.mnemonicHasPassphrase = null;
x.use44forMultisig = opts.useLegacyPurpose;
x.use0forBCH = opts.useLegacyCoinType;
x.compliantDerivation = !opts.nonCompliantDerivation;
return x;
};
static fromObj = function(obj) {
$.shouldBeObject(obj);
var x: any = new Key();
if (obj.version != x.version) {
throw new Error('Bad Key version');
}
_.each(Key.FIELDS, function(k) {
x[k] = obj[k];
});
$.checkState(x.xPrivKey || x.xPrivKeyEncrypted, 'invalid input');
return x;
};
toObj = function() {
var self = this;
var x = {};
_.each(Key.FIELDS, function(k) {
x[k] = self[k];
});
return x;
};
isPrivKeyEncrypted = function() {
return !!this.xPrivKeyEncrypted && !this.xPrivKey;
};
checkPassword = function(password) {
if (this.isPrivKeyEncrypted()) {
try {
sjcl.decrypt(password, this.xPrivKeyEncrypted);
} catch (ex) {
return false;
}
return true;
}
return null;
};
get = function(password) {
var keys: any = {};
let fingerPrintUpdated = false;
if (this.isPrivKeyEncrypted()) {
$.checkArgument(password, 'Private keys are encrypted, a password is needed');
try {
keys.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted);
// update fingerPrint if not set.
if (!this.fingerPrint) {
let xpriv = new Bitcore.HDPrivateKey(keys.xPrivKey);
this.fingerPrint = xpriv.fingerPrint.toString('hex');
fingerPrintUpdated = true;
}
if (this.mnemonicEncrypted) {
keys.mnemonic = sjcl.decrypt(password, this.mnemonicEncrypted);
}
} catch (ex) {
throw new Error('Could not decrypt');
}
} else {
keys.xPrivKey = this.xPrivKey;
keys.mnemonic = this.mnemonic;
if (fingerPrintUpdated) {
keys.fingerPrintUpdated = true;
}
}
return keys;
};
encrypt = function(password, opts) {
if (this.xPrivKeyEncrypted) throw new Error('Private key already encrypted');
if (!this.xPrivKey) throw new Error('No private key to encrypt');
this.xPrivKeyEncrypted = sjcl.encrypt(password, this.xPrivKey, opts);
if (!this.xPrivKeyEncrypted) throw new Error('Could not encrypt');
if (this.mnemonic) this.mnemonicEncrypted = sjcl.encrypt(password, this.mnemonic, opts);
delete this.xPrivKey;
delete this.mnemonic;
};
decrypt = function(password) {
if (!this.xPrivKeyEncrypted) throw new Error('Private key is not encrypted');
try {
this.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted);
if (this.mnemonicEncrypted) {
this.mnemonic = sjcl.decrypt(password, this.mnemonicEncrypted);
}
delete this.xPrivKeyEncrypted;
delete this.mnemonicEncrypted;
} catch (ex) {
log.error('error decrypting:', ex);
throw new Error('Could not decrypt');
}
};
derive = function(password, path) {
$.checkArgument(path, 'no path at derive()');
var xPrivKey = new Bitcore.HDPrivateKey(this.get(password).xPrivKey, NETWORK);
var deriveFn = this.compliantDerivation
? _.bind(xPrivKey.deriveChild, xPrivKey)
: _.bind(xPrivKey.deriveNonCompliantChild, xPrivKey);
return deriveFn(path);
};
_checkCoin(coin) {
if (!_.includes(Constants.COINS, coin)) throw new Error('Invalid coin');
}
_checkNetwork(network) {
if (!_.includes(['livenet', 'testnet'], network)) throw new Error('Invalid network');
}
/*
* This is only used on "create"
* no need to include/support
* BIP45
*/
getBaseAddressDerivationPath(opts) {
$.checkArgument(opts, 'Need to provide options');
$.checkArgument(opts.n >= 1, 'n need to be >=1');
let purpose = opts.n == 1 || this.use44forMultisig ? '44' : '48';
var coinCode = '0';
if (opts.network == 'testnet' && Constants.UTXO_COINS.includes(opts.coin)) {
coinCode = '1';
} else if (opts.coin == 'bch') {
if (this.use0forBCH) {
coinCode = '1025';
} else {
coinCode = '145';
}
} else if (opts.coin == 'btc') {
coinCode = '1025';
} else if (opts.coin == 'duc') {
coinCode = '0';
} else if (opts.coin == 'eth') {
coinCode = '60';
} else if (opts.coin == 'xrp') {
coinCode = '144';
} else if (opts.coin == 'ducx') {
coinCode = '1060';
} else {
throw new Error('unknown coin: ' + opts.coin);
}
return 'm/' + purpose + "'/" + coinCode + "'/" + opts.account + "'";
}
/*
* opts.coin
* opts.network
* opts.account
* opts.n
*/
createCredentials = function(password, opts) {
opts = opts || {};
if (password) $.shouldBeString(password, 'provide password');
this._checkCoin(opts.coin);
this._checkNetwork(opts.network);
$.shouldBeNumber(opts.account, 'Invalid account');
$.shouldBeNumber(opts.n, 'Invalid n');
$.shouldBeUndefined(opts.useLegacyCoinType);
$.shouldBeUndefined(opts.useLegacyPurpose);
let path = this.getBaseAddressDerivationPath(opts);
let xPrivKey = this.derive(password, path);
let requestPrivKey = this.derive(password, Constants.PATHS.REQUEST_KEY).privateKey.toString();
if (opts.network == 'testnet') {
// Hacky: BTC/BCH xPriv depends on network: This code is to
// convert a livenet xPriv to a testnet xPriv
let x = xPrivKey.toObject();
x.network = 'testnet';
delete x.xprivkey;
delete x.checksum;
x.privateKey = _.padStart(x.privateKey, 64, '0');
xPrivKey = new Bitcore.HDPrivateKey(x);
}
return Credentials.fromDerivedKey({
xPubKey: xPrivKey.hdPublicKey.toString(),
coin: opts.coin,
network: opts.network,
account: opts.account,
n: opts.n,
rootPath: path,
keyId: this.id,
requestPrivKey,
addressType: opts.addressType,
walletPrivKey: opts.walletPrivKey
});
};
/*
* opts
* opts.path
* opts.requestPrivKey
*/
createAccess = function(password, opts) {
opts = opts || {};
$.shouldBeString(opts.path);
var requestPrivKey = new Bitcore.PrivateKey(opts.requestPrivKey || null);
var requestPubKey = requestPrivKey.toPublicKey().toString();
var xPriv = this.derive(password, opts.path);
var signature = Utils.signRequestPubKey(requestPubKey, xPriv);
requestPrivKey = requestPrivKey.toString();
return {
signature,
requestPrivKey
};
};
sign = function(rootPath, txp, password, cb) {
$.shouldBeString(rootPath);
if (this.isPrivKeyEncrypted() && !password) {
return cb(new Errors.ENCRYPTED_PRIVATE_KEY());
}
var privs = [];
var derived: any = {};
var derived = this.derive(password, rootPath);
var xpriv = new Bitcore.HDPrivateKey(derived);
var t = Utils.buildTx(txp);
if (Constants.UTXO_COINS.includes(txp.coin)) {
_.each(txp.inputs, function(i) {
$.checkState(i.path, 'Input derivation path not available (signing transaction)');
if (!derived[i.path]) {
derived[i.path] = xpriv.deriveChild(i.path).privateKey;
privs.push(derived[i.path]);
}
});
var signatures = _.map(privs, function(priv, i) {
return t.getSignatures(priv);
});
signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) {
return s.signature.toDER().toString('hex');
});
return signatures;
} else {
let tx = t.uncheckedSerialize();
tx = typeof tx === 'string' ? [tx] : tx;
const chain = Utils.getChain(txp.coin);
const txArray = _.isArray(tx) ? tx : [tx];
const isChange = false;
const addressIndex = 0;
const { privKey, pubKey } = Deriver.derivePrivateKey(chain, txp.network, derived, addressIndex, isChange);
let signatures = [];
for (const rawTx of txArray) {
const signed = Transactions.getSignature({
chain,
tx: rawTx,
key: { privKey, pubKey }
});
signatures.push(signed);
}
return signatures;
}
};
}