UNPKG

@ducatus/ducatus-wallet-client-rev

Version:

Client for @ducatus/ducatus-wallet-service-rev

401 lines (344 loc) 11.7 kB
'use strict'; import { BitcoreLib, BitcoreLibCash, Deriver, DucatuscoreLib, Transactions } from '@ducatus/ducatus-crypto-wallet-core-rev'; import * as _ from 'lodash'; import { Constants } from './constants'; import { Defaults } from './defaults'; var $ = require('preconditions').singleton(); var sjcl = require('sjcl'); var Stringify = require('json-stable-stringify'); var Bitcore = BitcoreLib; var Bitcore_ = { btc: Bitcore, bch: BitcoreLibCash, eth: Bitcore, xrp: Bitcore, duc: DucatuscoreLib, ducx: Bitcore }; var PrivateKey = Bitcore.PrivateKey; var PublicKey = Bitcore.PublicKey; var crypto = Bitcore.crypto; let SJCL = {}; export class Utils { static getChain(coin: string): string { let normalizedChain = coin.toUpperCase(); if (Constants.ERC20.includes(coin)) { normalizedChain = 'ETH'; } if (Constants.DRC20.includes(coin)) { normalizedChain = 'DUCX'; } return normalizedChain; } static encryptMessage(message, encryptingKey) { var key = sjcl.codec.base64.toBits(encryptingKey); return sjcl.encrypt( key, message, _.defaults( { ks: 128, iter: 1 }, SJCL ) ); } // Will throw if it can't decrypt static decryptMessage(cyphertextJson, encryptingKey) { if (!cyphertextJson) return; if (!encryptingKey) throw new Error('No key'); var key = sjcl.codec.base64.toBits(encryptingKey); return sjcl.decrypt(key, cyphertextJson); } static decryptMessageNoThrow(cyphertextJson, encryptingKey) { if (!encryptingKey) return '<ECANNOTDECRYPT>'; if (!cyphertextJson) return ''; // no sjcl encrypted json var r = this.isJsonString(cyphertextJson); if (!r || !r.iv || !r.ct) { return cyphertextJson; } try { return this.decryptMessage(cyphertextJson, encryptingKey); } catch (e) { return '<ECANNOTDECRYPT>'; } } static isJsonString(str) { var r; try { r = JSON.parse(str); } catch (e) { return false; } return r; } /* TODO: It would be nice to be compatible with bitcoind signmessage. How * the hash is calculated there? */ static hashMessage(text) { $.checkArgument(text); var buf = Buffer.from(text); var ret = crypto.Hash.sha256sha256(buf); ret = new Bitcore.encoding.BufferReader(ret).readReverse(); return ret; } static signMessage(message, privKey) { $.checkArgument(message); var priv = new PrivateKey(privKey); const flattenedMessage = _.isArray(message) ? _.join(message) : message; var hash = this.hashMessage(flattenedMessage); return crypto.ECDSA.sign(hash, priv, 'little').toString(); } static verifyMessage(message: Array<string> | string, signature, pubKey) { $.checkArgument(message); $.checkArgument(pubKey); if (!signature) return false; var pub = new PublicKey(pubKey); const flattenedMessage = _.isArray(message) ? _.join(message) : message; const hash = this.hashMessage(flattenedMessage); try { var sig = new crypto.Signature.fromString(signature); return crypto.ECDSA.verify(hash, sig, pub, 'little'); } catch (e) { return false; } } static privateKeyToAESKey(privKey) { $.checkArgument(privKey && _.isString(privKey)); $.checkArgument(Bitcore.PrivateKey.isValid(privKey), 'The private key received is invalid'); var pk = Bitcore.PrivateKey.fromString(privKey); return Bitcore.crypto.Hash.sha256(pk.toBuffer()) .slice(0, 16) .toString('base64'); } static getCopayerHash(name, xPubKey, requestPubKey) { return [name, xPubKey, requestPubKey].join('|'); } static getProposalHash(proposalHeader) { // For backwards compatibility if (arguments.length > 1) { return this.getOldHash.apply(this, arguments); } return Stringify(proposalHeader); } static getOldHash(toAddress, amount, message, payProUrl) { return [toAddress, amount, message || '', payProUrl || ''].join('|'); } static parseDerivationPath(path: string) { const pathIndex = /m\/([0-9]*)\/([0-9]*)/; const [_input, changeIndex, addressIndex] = path.match(pathIndex); const isChange = Number.parseInt(changeIndex) > 0; return { _input, addressIndex, isChange }; } static deriveAddress(scriptType, publicKeyRing, path, m, network, coin) { $.checkArgument(_.includes(_.values(Constants.SCRIPT_TYPES), scriptType)); coin = coin || 'btc'; const chain = this.getChain(coin).toLowerCase(); var bitcore = Bitcore_[chain]; var publicKeys = _.map(publicKeyRing, item => { var xpub = new bitcore.HDPublicKey(item.xPubKey); return xpub.deriveChild(path).publicKey; }); var bitcoreAddress; switch (scriptType) { case Constants.SCRIPT_TYPES.P2WSH: const nestedWitness = false; bitcoreAddress = bitcore.Address.createMultisig(publicKeys, m, network, nestedWitness, 'witnessscripthash'); break; case Constants.SCRIPT_TYPES.P2SH: bitcoreAddress = bitcore.Address.createMultisig(publicKeys, m, network); break; case Constants.SCRIPT_TYPES.P2WPKH: bitcoreAddress = bitcore.Address.fromPublicKey(publicKeys[0], network, 'witnesspubkeyhash'); break; case Constants.SCRIPT_TYPES.P2PKH: $.checkState(_.isArray(publicKeys) && publicKeys.length == 1); if (Constants.UTXO_COINS.includes(coin)) { bitcoreAddress = bitcore.Address.fromPublicKey(publicKeys[0], network); } else { const { addressIndex, isChange } = this.parseDerivationPath(path); const [{ xPubKey }] = publicKeyRing; bitcoreAddress = Deriver.deriveAddress(chain.toUpperCase(), network, xPubKey, addressIndex, isChange); } break; } return { address: bitcoreAddress.toString(true), path, publicKeys: _.invokeMap(publicKeys, 'toString') }; } // note that we use the string version of xpub, // serialized by BITCORE BTC. // testnet xpub starts with t. // livenet xpub starts with x. // no matter WHICH coin static xPubToCopayerId(coin, xpub) { // this was introduced because we allowed coin = 0' wallets for BCH // for the "wallet duplication" feature // now it is effective for all coins. const chain = this.getChain(coin).toLowerCase(); var str = chain == 'btc' ? xpub : chain + xpub; var hash = sjcl.hash.sha256.hash(str); return sjcl.codec.hex.fromBits(hash); } static signRequestPubKey(requestPubKey, xPrivKey) { var priv = new Bitcore.HDPrivateKey(xPrivKey).deriveChild(Constants.PATHS.REQUEST_KEY_AUTH).privateKey; return this.signMessage(requestPubKey, priv); } static verifyRequestPubKey(requestPubKey, signature, xPubKey) { var pub = new Bitcore.HDPublicKey(xPubKey).deriveChild(Constants.PATHS.REQUEST_KEY_AUTH).publicKey; return this.verifyMessage(requestPubKey, signature, pub.toString()); } static formatAmount(satoshis, unit, opts?) { $.shouldBeNumber(satoshis); $.checkArgument(_.includes(_.keys(Constants.UNITS), unit)); var clipDecimals = (number, decimals) => { var x = number.toString().split('.'); var d = (x[1] || '0').substring(0, decimals); return parseFloat(x[0] + '.' + d); }; var addSeparators = (nStr, thousands, decimal, minDecimals) => { nStr = nStr.replace('.', decimal); var x = nStr.split(decimal); var x0 = x[0]; var x1 = x[1]; x1 = _.dropRightWhile(x1, (n, i) => { return n == '0' && i >= minDecimals; }).join(''); var x2 = x.length > 1 ? decimal + x1 : ''; x0 = x0.replace(/\B(?=(\d{3})+(?!\d))/g, thousands); return x0 + x2; }; opts = opts || {}; var u = Constants.UNITS[unit]; var precision = opts.fullPrecision ? 'full' : 'short'; var amount = clipDecimals(satoshis / u.toSatoshis, u[precision].maxDecimals).toFixed(u[precision].maxDecimals); return addSeparators( amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u[precision].minDecimals ); } static buildTx(txp) { var coin = txp.coin || 'btc'; if (Constants.UTXO_COINS.includes(coin)) { var bitcore = Bitcore_[coin]; var t = new bitcore.Transaction(); if (txp.version >= 4) { t.setVersion(2); } else { t.setVersion(1); } $.checkState(_.includes(_.values(Constants.SCRIPT_TYPES), txp.addressType)); switch (txp.addressType) { case Constants.SCRIPT_TYPES.P2WSH: case Constants.SCRIPT_TYPES.P2SH: _.each(txp.inputs, i => { t.from(i, i.publicKeys, txp.requiredSignatures); }); break; case Constants.SCRIPT_TYPES.P2WPKH: case Constants.SCRIPT_TYPES.P2PKH: t.from(txp.inputs); break; } if (txp.toAddress && txp.amount && !txp.outputs) { t.to(txp.toAddress, txp.amount); } else if (txp.outputs) { _.each(txp.outputs, o => { $.checkState(o.script || o.toAddress, 'Output should have either toAddress or script specified'); if (o.script) { t.addOutput( new bitcore.Transaction.Output({ script: o.script, satoshis: o.amount }) ); } else { t.to(o.toAddress, o.amount); } }); } t.fee(txp.fee); t.change(txp.changeAddress.address); // Shuffle outputs for improved privacy if (t.outputs.length > 1) { var outputOrder = _.reject(txp.outputOrder, order => { return order >= t.outputs.length; }); $.checkState(t.outputs.length == outputOrder.length); t.sortOutputs(outputs => { return _.map(outputOrder, i => { return outputs[i]; }); }); } // Validate inputs vs outputs independently of Bitcore var totalInputs = _.reduce( txp.inputs, (memo, i) => { return +i.satoshis + memo; }, 0 ); var totalOutputs = _.reduce( t.outputs, (memo, o) => { return +o.satoshis + memo; }, 0 ); $.checkState(totalInputs - totalOutputs >= 0); $.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE); return t; } else { const { data, destinationTag, outputs, payProUrl, tokenAddress, tokenId } = txp; const recipients = outputs.map(output => { return { amount: output.amount, address: output.toAddress, data: output.data, gasLimit: output.gasLimit }; }); // Backwards compatibility BWC <= 8.9.0 if (data) { recipients[0].data = data; } const unsignedTxs = []; const isERC20 = tokenAddress && !payProUrl; const isERC721 = isERC20 && tokenId; let chain = isERC721 ? 'ERC721' : isERC20 ? this.getChain(coin) === 'DUCX' ? 'DRC20' : 'ERC20' : this.getChain(coin); if (txp.wDucxAddress) { chain = 'TOB'; } for (let index = 0; index < recipients.length; index++) { const rawTx = Transactions.create({ ...txp, ...recipients[index], tag: destinationTag ? Number(destinationTag) : undefined, chain, nonce: Number(txp.nonce) + Number(index), recipients: [recipients[index]] }); unsignedTxs.push(rawTx); } return { uncheckedSerialize: () => unsignedTxs }; } } }