UNPKG

bitcore-wallet-service

Version:
1,200 lines (1,199 loc) 176 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WalletService = void 0; exports.checkRequired = checkRequired; const async = __importStar(require("async")); const crypto_wallet_core_1 = require("crypto-wallet-core"); const _ = __importStar(require("lodash")); const moralis_1 = __importDefault(require("moralis")); require("source-map-support/register"); const config_1 = __importDefault(require("../config")); const logger_1 = __importDefault(require("./logger")); const deprecated_serverMessages_1 = require("../deprecated-serverMessages"); const banxa_1 = require("../externalServices/banxa"); const changelly_1 = require("../externalServices/changelly"); const moonpay_1 = require("../externalServices/moonpay"); const oneInch_1 = require("../externalServices/oneInch"); const ramp_1 = require("../externalServices/ramp"); const sardine_1 = require("../externalServices/sardine"); const simplex_1 = require("../externalServices/simplex"); const thorswap_1 = require("../externalServices/thorswap"); const transak_1 = require("../externalServices/transak"); const wyre_1 = require("../externalServices/wyre"); const serverMessages_1 = require("../serverMessages"); const bchaddresstranslator_1 = require("./bchaddresstranslator"); const blockchainexplorer_1 = require("./blockchainexplorer"); const index_1 = require("./chain/index"); const common_1 = require("./common"); const clienterror_1 = require("./errors/clienterror"); const errordefinitions_1 = require("./errors/errordefinitions"); const fiatrateservice_1 = require("./fiatrateservice"); const lock_1 = require("./lock"); const messagebroker_1 = require("./messagebroker"); const model_1 = require("./model"); const storage_1 = require("./storage"); const Uuid = require('uuid'); const $ = require('preconditions').singleton(); const EmailValidator = require('email-validator'); const Bitcore = require('bitcore-lib'); const Bitcore_ = { btc: Bitcore, bch: require('bitcore-lib-cash'), eth: Bitcore, matic: Bitcore, arb: Bitcore, base: Bitcore, op: Bitcore, xrp: Bitcore, doge: require('bitcore-lib-doge'), ltc: require('bitcore-lib-ltc'), sol: Bitcore, }; const Utils = common_1.Common.Utils; const Constants = common_1.Common.Constants; const Defaults = common_1.Common.Defaults; const Services = common_1.Common.Services; let request = require('request'); let initialized = false; let doNotCheckV8 = false; let isMoralisInitialized = false; let lock; let storage; let blockchainExplorer; let blockchainExplorerOpts; let messageBroker; let fiatRateService; let serviceVersion; function boolToNum(x) { return x ? 1 : 0; } class WalletService { constructor() { if (!initialized) { throw new Error('Server not initialized'); } this.lock = lock; this.storage = storage; this.blockchainExplorer = blockchainExplorer; this.blockchainExplorerOpts = blockchainExplorerOpts; this.messageBroker = messageBroker; this.fiatRateService = fiatRateService; this.notifyTicker = 0; this.request = request; this.externalServices = { banxa: new banxa_1.BanxaService(), changelly: new changelly_1.ChangellyService(), moonpay: new moonpay_1.MoonpayService(), oneInch: new oneInch_1.OneInchService(this.storage), ramp: new ramp_1.RampService(), sardine: new sardine_1.SardineService(), simplex: new simplex_1.SimplexService(), thorswap: new thorswap_1.ThorswapService(), transak: new transak_1.TransakService(), wyre: new wyre_1.WyreService(), }; } static getServiceVersion() { if (!serviceVersion) { serviceVersion = 'bws-' + require('../../package').version; } return serviceVersion; } static initialize(opts, cb) { $.shouldBeFunction(cb, ''); opts = opts || {}; blockchainExplorer = opts.blockchainExplorer; blockchainExplorerOpts = opts.blockchainExplorerOpts; doNotCheckV8 = opts.doNotCheckV8; if (opts.request) { request = opts.request; } const initStorage = cb => { if (opts.storage) { storage = opts.storage; return cb(); } else { const newStorage = new storage_1.Storage(); newStorage.connect(opts.storageOpts, err => { if (err) { return cb(err); } storage = newStorage; return cb(); }); } }; const initMessageBroker = cb => { messageBroker = opts.messageBroker || new messagebroker_1.MessageBroker(opts.messageBrokerOpts); if (messageBroker) { messageBroker.onMessage(WalletService.handleIncomingNotifications); } return cb(); }; const initFiatRateService = cb => { if (opts.fiatRateService) { fiatRateService = opts.fiatRateService; return cb(); } else { const newFiatRateService = new fiatrateservice_1.FiatRateService(); const opts2 = opts.fiatRateServiceOpts || {}; opts2.storage = storage; newFiatRateService.init(opts2, err => { if (err) { return cb(err); } fiatRateService = newFiatRateService; return cb(); }); } }; const initMoralis = async (cb) => { if (!config_1.default.moralis || !config_1.default.moralis.apiKey) { logger_1.default.warn('Moralis missing credentials'); return cb(); } if (!isMoralisInitialized) { try { logger_1.default.info('Initializing Moralis...'); const API_KEY = config_1.default.moralis.apiKey; await moralis_1.default.start({ apiKey: API_KEY, }); logger_1.default.info('Moralis initialized successfully!'); isMoralisInitialized = true; return cb(); } catch (err) { logger_1.default.error('Error initializing Moralis: ', err); isMoralisInitialized = false; return cb(); } } else { return cb(); } }; async.series([ next => { initStorage(next); }, next => { initMessageBroker(next); }, next => { initFiatRateService(next); }, next => { initMoralis(next); } ], err => { lock = opts.lock || new lock_1.Lock(storage); if (err) { logger_1.default.error('Could not initialize: %o', err); throw err; } initialized = true; return cb(); }); } static handleIncomingNotifications(notification, cb) { cb = cb || function () { }; return cb(); } static shutDown(cb) { if (!initialized) { return cb(); } storage.disconnect(err => { if (err) { return cb(err); } initialized = false; return cb(); }); } static getInstance(opts) { opts = opts || {}; const version = Utils.parseVersion(opts.clientVersion); if (version && version.agent === 'bwc') { if (version.major === 0 || (version.major === 1 && version.minor < 2)) { throw new clienterror_1.ClientError(errordefinitions_1.Errors.codes.UPGRADE_NEEDED, 'BWC clients < 1.2 are no longer supported.'); } } const server = new WalletService(); server._setClientVersion(opts.clientVersion); server._setAppVersion(opts.userAgent); server.userAgent = opts.userAgent; return server; } static getInstanceWithAuth(opts, cb) { const withSignature = cb => { if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) { return; } let server; try { server = WalletService.getInstance(opts); } catch (ex) { return cb(ex); } server.storage.fetchCopayerLookup(opts.copayerId, (err, copayer) => { if (err) { return cb(err); } if (!copayer) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.NOT_AUTHORIZED, 'Copayer not found')); } const isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys); if (!isValid) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.NOT_AUTHORIZED, 'Invalid signature')); } server.walletId = copayer.walletId; if (copayer.isSupportStaff) { server.walletId = opts.walletId || copayer.walletId; server.copayerIsSupportStaff = true; } if (copayer.isMarketingStaff) { server.copayerIsMarketingStaff = true; } server.copayerId = opts.copayerId; return cb(null, server); }); }; const withSession = cb => { if (!checkRequired(opts, ['copayerId', 'session'], cb)) { return; } let server; try { server = WalletService.getInstance(opts); } catch (ex) { return cb(ex); } server.storage.getSession(opts.copayerId, (err, s) => { if (err) { return cb(err); } const isValid = s && s.id === opts.session && s.isValid(); if (!isValid) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.NOT_AUTHORIZED, 'Session expired')); } server.storage.fetchCopayerLookup(opts.copayerId, (err, copayer) => { if (err) { return cb(err); } if (!copayer) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.NOT_AUTHORIZED, 'Copayer not found')); } server.copayerId = opts.copayerId; server.walletId = copayer.walletId; return cb(null, server); }); }); }; const authFn = opts.session ? withSession : withSignature; return authFn(cb); } _runLocked(cb, task, waitTime) { $.checkState(this.walletId, 'Failed state: this.walletId undefined at <_runLocked()>'); this.lock.runLocked(this.walletId, { waitTime }, cb, task); } logi(message, ...args) { if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; } } if (!this || !this.walletId) { return logger_1.default.warn(message, ...args); } message = '<' + this.walletId + '>' + message; return logger_1.default.info(message, ...args); } logw(message, ...args) { if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; args[i] = args[i]?.stack || args[i]?.message || args[i]; } } if (!this || !this.walletId) { return logger_1.default.warn(message, ...args); } message = '<' + this.walletId + '>' + message; return logger_1.default.warn(message, ...args); } logd(message, ...args) { if (typeof message === 'string' && args.length > 0 && !message.endsWith('%o')) { for (let i = 0; i < args.length; i++) { message += ' %o'; } } if (!this || !this.walletId) { return logger_1.default.verbose(message, ...args); } message = '<' + this.walletId + '>' + message; return logger_1.default.verbose(message, ...args); } login(opts, cb) { let session; async.series([ next => { this.storage.getSession(this.copayerId, (err, s) => { if (err) { return next(err); } session = s; next(); }); }, next => { if (!session || !session.isValid()) { session = model_1.Session.create({ copayerId: this.copayerId, walletId: this.walletId }); } else { session.touch(); } next(); }, next => { this.storage.storeSession(session, next); } ], err => { if (err) { return cb(err); } if (!session) { return cb(new Error('Could not get current session for this copayer')); } return cb(null, session.id); }); } logout(opts, cb) { } createWallet(opts, cb) { let pubKey; opts.coin = opts.coin || Defaults.COIN; if (!opts.chain) { opts.chain = opts.coin; } if (opts.chain === 'bch' && opts.n > 1) { const version = Utils.parseVersion(this.clientVersion); if (version && version.agent === 'bwc') { if (version.major < 8 || (version.major === 8 && version.minor < 3)) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.UPGRADE_NEEDED, 'BWC clients < 8.3 are no longer supported for multisig BCH wallets.')); } } } if (!checkRequired(opts, ['name', 'm', 'n', 'pubKey'], cb)) { return; } if (!opts.name) { return cb(new clienterror_1.ClientError('Invalid wallet name')); } if (!model_1.Wallet.verifyCopayerLimits(opts.m, opts.n)) { return cb(new clienterror_1.ClientError('Invalid combination of required copayers / total copayers')); } if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) { return cb(new clienterror_1.ClientError('Invalid chain')); } opts.network = Utils.getNetworkName(opts.chain, opts.network) || 'livenet'; if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS[opts.chain])) { return cb(new clienterror_1.ClientError('Invalid network')); } if (opts.network === 'regtest' && !config_1.default.allowRegtest) { return cb(new clienterror_1.ClientError('Regtest is not allowed for this environment')); } const derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP44; let addressType = opts.n === 1 ? Constants.SCRIPT_TYPES.P2PKH : Constants.SCRIPT_TYPES.P2SH; if (opts.useNativeSegwit && Utils.checkValueInCollection(opts.chain, Constants.NATIVE_SEGWIT_CHAINS)) { switch (Number(opts.segwitVersion)) { case 0: default: addressType = opts.n === 1 ? Constants.SCRIPT_TYPES.P2WPKH : Constants.SCRIPT_TYPES.P2WSH; break; case 1: if (!Utils.checkValueInCollection(opts.chain, Constants.TAPROOT_CHAINS)) { return cb(new clienterror_1.ClientError('Invalid chain for P2TR')); } addressType = Constants.SCRIPT_TYPES.P2TR; break; } } try { pubKey = new Bitcore.PublicKey.fromString(opts.pubKey); } catch (ex) { return cb(new clienterror_1.ClientError('Invalid public key')); } if (opts.n > 1 && !index_1.ChainService.supportsMultisig(opts.chain)) { return cb(new clienterror_1.ClientError('Multisig wallets are not supported for this coin')); } if (index_1.ChainService.isSingleAddress(opts.chain)) { opts.singleAddress = true; } let newWallet; async.series([ acb => { if (!opts.id) { return acb(); } this.storage.fetchWallet(opts.id, (err, wallet) => { if (wallet) { return acb(errordefinitions_1.Errors.WALLET_ALREADY_EXISTS); } return acb(err); }); }, acb => { const wallet = model_1.Wallet.create({ id: opts.id, name: opts.name, m: opts.m, n: opts.n, coin: opts.coin, chain: opts.chain, network: opts.network, pubKey: pubKey.toString(), singleAddress: !!opts.singleAddress, derivationStrategy, addressType, nativeCashAddr: opts.nativeCashAddr, usePurpose48: opts.n > 1 && !!opts.usePurpose48, hardwareSourcePublicKey: opts.hardwareSourcePublicKey, clientDerivedPublicKey: opts.clientDerivedPublicKey }); this.storage.storeWallet(wallet, err => { this.logd('Wallet created', wallet.id, opts.network); newWallet = wallet; return acb(err); }); } ], err => { return cb(err, newWallet ? newWallet.id : null); }); } getWallet(opts, cb) { this.storage.fetchWallet(this.walletId, (err, wallet) => { if (err) return cb(err); if (!wallet) return cb(errordefinitions_1.Errors.WALLET_NOT_FOUND); if (wallet.coin != 'bch' || wallet.nativeCashAddr) return cb(null, wallet); if (opts.doNotMigrate) return cb(null, wallet); if (!wallet.chain) wallet.chain = index_1.ChainService.getChain(wallet.coin); logger_1.default.info(`Migrating wallet ${wallet.id} to cashAddr`); this.storage.migrateToCashAddr(this.walletId, e => { if (e) return cb(e); wallet.nativeCashAddr = true; return this.storage.storeWallet(wallet, e => { if (e) return cb(e); return cb(e, wallet); }); }); }); } getWalletFromIdentifier(opts, cb) { if (!opts.identifier) return cb(); const end = (err, ret) => { if (opts.walletCheck && !err && ret) { return this.syncWallet(ret, cb); } else { return cb(err, ret); } }; let walletId; async.parallel([ done => { this.storage.fetchWallet(opts.identifier, (err, wallet) => { if (wallet) walletId = wallet.id; return done(err); }); }, done => { this.storage.fetchAddressByChain(Defaults.CHAIN, opts.identifier, (err, address) => { if (address) walletId = address.walletId; return done(err); }); }, done => { this.storage.fetchTxByHash(opts.identifier, (err, tx) => { if (tx) walletId = tx.walletId; return done(err); }); } ], err => { if (err) return cb(err); if (walletId) { return this.storage.fetchWallet(walletId, end); } return cb(); }); } getStatus(opts, cb) { opts = opts || {}; const status = {}; async.parallel([ next => { this.getWallet({}, (err, wallet) => { if (err) return next(err); const walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager']; const copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData']; wallet.copayers = (wallet.copayers || []).map(copayer => { if (copayer.id == this.copayerId) return copayer; return _.omit(copayer, 'customData'); }); if (!opts.includeExtendedInfo) { wallet = _.omit(wallet, walletExtendedKeys); wallet.copayers = (wallet.copayers || []).map(copayer => { return _.omit(copayer, copayerExtendedKeys); }); } status.wallet = wallet; if (opts.includeServerMessages) { status.serverMessages = (0, serverMessages_1.serverMessages)(wallet, this.appName, this.appVersion); } else { status.serverMessage = (0, deprecated_serverMessages_1.serverMessages)(wallet, this.appName, this.appVersion); } next(); }); }, next => { opts.wallet = status.wallet; this.getBalance(opts, (err, balance) => { if (opts.includeExtendedInfo) { if (err && err.code != 'WALLET_NEED_SCAN') { return next(err); } } else if (err) { return next(err); } status.balance = balance; next(); }); }, next => { this.getPendingTxs(opts, (err, pendingTxps) => { if (err) return next(err); status.pendingTxps = pendingTxps; next(); }); }, next => { this.getPreferences({}, (err, preferences) => { if (err) return next(err); status.preferences = preferences; next(); }); } ], err => { if (err) return cb(err); return cb(null, status); }); } _verifySignature(text, signature, pubkey) { return Utils.verifyMessage(text, signature, pubkey); } _verifyRequestPubKey(requestPubKey, signature, xPubKey) { const pub = new Bitcore.HDPublicKey(xPubKey).deriveChild(Constants.PATHS.REQUEST_KEY_AUTH).publicKey; return Utils.verifyMessage(requestPubKey, signature, pub.toString()); } _getSigningKey(text, signature, pubKeys) { return pubKeys.find(item => { return this._verifySignature(text, signature, item.key); }); } _notify(type, data, opts, cb) { if (typeof opts === 'function') { cb = opts; opts = {}; } opts = opts || {}; cb = cb || function () { }; const walletId = this.walletId || data.walletId; const copayerId = this.copayerId || data.copayerId; $.checkState(walletId, 'Failed state: walletId undefined at <_notify()>'); const notification = model_1.Notification.create({ type, data, ticker: this.notifyTicker++, creatorId: opts.isGlobal ? null : copayerId, walletId }); this.storage.storeNotification(walletId, notification, () => { this.messageBroker.send(notification); return cb(); }); } _notifyTxProposalAction(type, txp, extraArgs, cb) { if (typeof extraArgs === 'function') { cb = extraArgs; extraArgs = {}; } const data = Object.assign({ txProposalId: txp.id, creatorId: txp.creatorId, amount: txp.getTotalAmount(), message: txp.message, tokenAddress: txp.tokenAddress, multisigContractAddress: txp.multisigContractAddress }, extraArgs); this._notify(type, data, {}, cb); } _addCopayerToWallet(wallet, opts, cb) { const copayer = model_1.Copayer.create({ coin: wallet.coin, chain: wallet.chain, name: opts.name, copayerIndex: wallet.copayers.length, xPubKey: opts.xPubKey, hardwareSourcePublicKey: opts.hardwareSourcePublicKey, clientDerivedPublicKey: opts.clientDerivedPublicKey, requestPubKey: opts.requestPubKey, signature: opts.copayerSignature, customData: opts.customData, derivationStrategy: wallet.derivationStrategy }); this.storage.fetchCopayerLookup(copayer.id, (err, res) => { if (err) return cb(err); if (res) return cb(errordefinitions_1.Errors.COPAYER_REGISTERED); if (opts.dryRun) return cb(null, { copayerId: null, wallet }); wallet.addCopayer(copayer); this.storage.storeWalletAndUpdateCopayersLookup(wallet, err => { if (err) return cb(err); async.series([ next => { this._notify('NewCopayer', { walletId: opts.walletId, copayerId: copayer.id, copayerName: copayer.name }, {}, next); }, next => { if (wallet.isComplete() && wallet.isShared()) { this._notify('WalletComplete', { walletId: opts.walletId }, { isGlobal: true }, next); } else { next(); } } ], () => { return cb(null, { copayerId: copayer.id, wallet }); }); }); }); } _addKeyToCopayer(wallet, copayer, opts, cb) { wallet.addCopayerRequestKey(copayer.copayerId, opts.requestPubKey, opts.signature, opts.restrictions, opts.name); this.storage.storeWalletAndUpdateCopayersLookup(wallet, err => { if (err) return cb(err); return cb(null, { copayerId: copayer.id, wallet }); }); } addAccess(opts, cb) { if (!checkRequired(opts, ['copayerId', 'requestPubKey', 'signature'], cb)) return; this.storage.fetchCopayerLookup(opts.copayerId, (err, copayer) => { if (err) return cb(err); if (!copayer) return cb(errordefinitions_1.Errors.NOT_AUTHORIZED); this.storage.fetchWallet(copayer.walletId, (err, wallet) => { if (err) return cb(err); if (!wallet) return cb(errordefinitions_1.Errors.NOT_AUTHORIZED); const xPubKey = wallet.copayers.find(c => c.id === opts.copayerId).xPubKey; if (!this._verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) { return cb(errordefinitions_1.Errors.NOT_AUTHORIZED); } if (copayer.requestPubKeys.length > Defaults.MAX_KEYS) return cb(errordefinitions_1.Errors.TOO_MANY_KEYS); this._addKeyToCopayer(wallet, copayer, opts, cb); }); }); } _setClientVersion(version) { delete this.parsedClientVersion; this.clientVersion = version; } _setAppVersion(userAgent) { const parsed = Utils.parseAppVersion(userAgent); if (!parsed) { this.appName = this.appVersion = null; } else { this.appName = parsed.app; this.appVersion = parsed; } } _parseClientVersion() { if (this.parsedClientVersion == null) { this.parsedClientVersion = Utils.parseVersion(this.clientVersion); } return this.parsedClientVersion; } _clientSupportsPayProRefund() { const version = this._parseClientVersion(); if (!version) return false; if (version.agent != 'bwc') return true; if (version.major < 1 || (version.major == 1 && version.minor < 2)) return false; return true; } static _getCopayerHash(name, xPubKey, requestPubKey) { return [name, xPubKey, requestPubKey].join('|'); } joinWallet(opts, cb) { if (!checkRequired(opts, ['walletId', 'name', 'requestPubKey', 'copayerSignature'], cb)) return; if (!opts.name) return cb(new clienterror_1.ClientError('Invalid copayer name')); opts.coin = opts.coin || Defaults.COIN; if (!opts.chain) { opts.chain = opts.coin; } if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new clienterror_1.ClientError('Invalid coin')); let xPubKey; if (!opts.hardwareSourcePublicKey && !opts.clientDerivedPublicKey) { if (!checkRequired(opts, ['xPubKey'], cb)) return; try { xPubKey = Bitcore_[opts.chain].HDPublicKey(opts.xPubKey); } catch (ex) { return cb(new clienterror_1.ClientError('Invalid extended public key')); } if (xPubKey.network == null) { return cb(new clienterror_1.ClientError('Invalid extended public key')); } } this.walletId = opts.walletId; this._runLocked(cb, cb => { this.storage.fetchWallet(opts.walletId, (err, wallet) => { if (err) return cb(err); if (!wallet) return cb(errordefinitions_1.Errors.WALLET_NOT_FOUND); if (opts.hardwareSourcePublicKey || opts.clientDerivedPublicKey) { this._addCopayerToWallet(wallet, opts, cb); return; } if (opts.chain === 'bch' && wallet.n > 1) { const version = Utils.parseVersion(this.clientVersion); if (version && version.agent === 'bwc') { if (version.major < 8 || (version.major === 8 && version.minor < 3)) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.UPGRADE_NEEDED, 'BWC clients < 8.3 are no longer supported for multisig BCH wallets.')); } } } if (wallet.n > 1 && wallet.usePurpose48) { const version = Utils.parseVersion(this.clientVersion); if (version && version.agent === 'bwc') { if (version.major < 8 || (version.major === 8 && version.minor < 4)) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.UPGRADE_NEEDED, 'Please upgrade your client to join this multisig wallet')); } } } if (wallet.n > 1 && wallet.addressType === 'P2WSH') { const version = Utils.parseVersion(this.clientVersion); if (version && version.agent === 'bwc') { if (version.major < 8 || (version.major === 8 && version.minor < 17)) { return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.UPGRADE_NEEDED, 'Please upgrade your client to join this multisig wallet')); } } } if (opts.chain != wallet.chain) { return cb(new clienterror_1.ClientError('The wallet you are trying to join was created for a different chain')); } if (!Utils.compareNetworks(wallet.network, xPubKey.network.name, wallet.chain)) { return cb(new clienterror_1.ClientError('The wallet you are trying to join was created for a different network')); } if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) { return cb(new clienterror_1.ClientError('The wallet you are trying to join was created with an older version of the client app.')); } const hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); if (!this._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) { return cb(new clienterror_1.ClientError()); } if (wallet.copayers?.find(c => c.xPubKey === opts.xPubKey)) return cb(errordefinitions_1.Errors.COPAYER_IN_WALLET); if (wallet.copayers.length == wallet.n) return cb(errordefinitions_1.Errors.WALLET_FULL); this._addCopayerToWallet(wallet, opts, cb); }); }); } savePreferences(opts, cb) { opts = opts || {}; const preferences = [ { name: 'email', isValid(value) { return EmailValidator.validate(value); } }, { name: 'language', isValid(value) { return typeof value === 'string' && value.length == 2; } }, { name: 'unit', isValid(value) { return typeof value === 'string' && ['btc', 'bit'].includes(value.toLowerCase()); } }, { name: 'tokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('eth', 'mainnet', x)); } }, { name: 'multisigEthInfo', isValid(value) { return (Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('eth', 'mainnet', x.multisigContractAddress))); } }, { name: 'maticTokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('matic', 'mainnet', x)); } }, { name: 'multisigMaticInfo', isValid(value) { return (Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('matic', 'mainnet', x.multisigContractAddress))); } }, { name: 'opTokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('op', 'mainnet', x)); } }, { name: 'baseTokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('base', 'mainnet', x)); } }, { name: 'arbTokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('arb', 'mainnet', x)); } }, { name: 'solTokenAddresses', isValid(value) { return Array.isArray(value) && value.every(x => crypto_wallet_core_1.Validation.validateAddress('sol', 'mainnet', x)); } }, ]; opts = _.pick(opts, preferences.map(p => p.name)); try { for (const preference of preferences) { const value = opts[preference.name]; if (!value) continue; if (!preference.isValid(value)) { throw new Error('Invalid ' + preference.name); } } } catch (ex) { return cb(new clienterror_1.ClientError(ex)); } this.getWallet({}, (err, wallet) => { if (err) return cb(err); if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) { opts.tokenAddresses = null; opts.multisigEthInfo = null; } if (wallet.coin != 'matic') { opts.maticTokenAddresses = null; opts.multisigMaticInfo = null; } this._runLocked(cb, cb => { this.storage.fetchPreferences(this.walletId, this.copayerId, (err, oldPref) => { if (err) return cb(err); const newPref = model_1.Preferences.create({ walletId: this.walletId, copayerId: this.copayerId }); const preferences = model_1.Preferences.fromObj(_.defaults(newPref, opts, oldPref)); if (opts.tokenAddresses) { oldPref = oldPref || {}; oldPref.tokenAddresses = oldPref.tokenAddresses || []; preferences.tokenAddresses = _.uniq(oldPref.tokenAddresses.concat(opts.tokenAddresses)); } if (opts.multisigEthInfo) { oldPref = oldPref || {}; oldPref.multisigEthInfo = oldPref.multisigEthInfo || []; preferences.multisigEthInfo = _.uniq(oldPref.multisigEthInfo.concat(opts.multisigEthInfo).reduce((x, y) => { let exists = false; for (let e of x) { if (e.multisigContractAddress === y.multisigContractAddress) { e.tokenAddresses = e.tokenAddresses || []; y.tokenAddresses = _.uniq(e.tokenAddresses.concat(y.tokenAddresses)); e = Object.assign(e, y); exists = true; } } return exists ? x : [...x, y]; }, [])); } if (opts.maticTokenAddresses) { oldPref = oldPref || {}; oldPref.maticTokenAddresses = oldPref.maticTokenAddresses || []; preferences.maticTokenAddresses = _.uniq(oldPref.maticTokenAddresses.concat(opts.maticTokenAddresses)); } if (opts.opTokenAddresses) { oldPref = oldPref || {}; oldPref.opTokenAddresses = oldPref.opTokenAddresses || []; preferences.opTokenAddresses = _.uniq(oldPref.opTokenAddresses.concat(opts.opTokenAddresses)); } if (opts.baseTokenAddresses) { oldPref = oldPref || {}; oldPref.baseTokenAddresses = oldPref.baseTokenAddresses || []; preferences.baseTokenAddresses = _.uniq(oldPref.baseTokenAddresses.concat(opts.baseTokenAddresses)); } if (opts.arbTokenAddresses) { oldPref = oldPref || {}; oldPref.arbTokenAddresses = oldPref.arbTokenAddresses || []; preferences.arbTokenAddresses = _.uniq(oldPref.arbTokenAddresses.concat(opts.arbTokenAddresses)); } if (opts.solTokenAddresses) { oldPref = oldPref || {}; oldPref.solTokenAddresses = oldPref.solTokenAddresses || []; preferences.solTokenAddresses = _.uniq(oldPref.solTokenAddresses.concat(opts.solTokenAddresses)); } if (opts.multisigMaticInfo) { oldPref = oldPref || {}; oldPref.multisigMaticInfo = oldPref.multisigMaticInfo || []; preferences.multisigMaticInfo = _.uniq(oldPref.multisigMaticInfo.concat(opts.multisigMaticInfo).reduce((x, y) => { let exists = false; for (let e of x) { if (e.multisigContractAddress === y.multisigContractAddress) { e.maticTokenAddresses = e.maticTokenAddresses || []; y.maticTokenAddresses = _.uniq(e.maticTokenAddresses.concat(y.maticTokenAddresses)); e = Object.assign(e, y); exists = true; } } return exists ? x : [...x, y]; }, [])); } this.storage.storePreferences(preferences, err => { return cb(err); }); }); }); }); } getPreferences(opts, cb) { this.storage.fetchPreferences(this.walletId, this.copayerId, (err, preferences) => { if (err) return cb(err); return cb(null, preferences || {}); }); } _canCreateAddress(ignoreMaxGap, cb) { if (ignoreMaxGap) return cb(null, true); this.storage.fetchAddresses(this.walletId, (err, addresses) => { if (err) return cb(err); const latestAddresses = addresses.filter(x => !x.isChange).slice(-Defaults.MAX_MAIN_ADDRESS_GAP); if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || latestAddresses.some(a => a.hasActivity)) return cb(null, true); const bc = this._getBlockchainExplorer(latestAddresses[0].coin, latestAddresses[0].network); if (!bc) return cb(new Error('Could not get blockchain explorer instance')); let activityFound = false; let i = latestAddresses.length; async.whilst(() => { return i > 0 && !activityFound; }, next => { bc.getAddressActivity(latestAddresses[--i].address, (err, res) => { if (err) return next(err); activityFound = !!res; return next(); }); }, err => { if (err) return cb(err); if (!activityFound) return cb(null, false); const address = latestAddresses[i]; address.hasActivity = true; this.storage.storeAddress(address, err => { return cb(err, true); }); }); }); } _store(wallet, address, cb, checkSync = false, forceSync = false) { let stoAddress = _.clone(address); index_1.ChainService.addressToStorageTransform(wallet.chain, wallet.network, stoAddress); this.storage.storeAddressAndWallet(wallet, stoAddress, (err, isDuplicate) => { if (err) return cb(err); this.syncWallet(wallet, err2 => { if (err2) { this.logw('Error syncing v8 addresses: ', err2); } return cb(null, isDuplicate); }, !checkSync, null, forceSync); }); } createAddress(opts, cb) { opts = opts || {}; const createNewAddress = (wallet, cb) => { let address; try { address = wallet.createAddress(!!opts.isChange); } catch (e) { this.logw('Error creating address', e); return cb('Bad xPub'); } this._store(wallet, address, (err, duplicate) => { if (err) return cb(err); if (duplicate) return cb(null, address); if (wallet.chain == 'bch' && opts.noCashAddr) { address = _.cloneDeep(address); address.address = bchaddresstranslator_1.BCHAddressTranslator.translate(address.address, 'copay'); } this._notify('NewAddress', { address: address.address }, () => { return cb(null, address); }); }, true); }; const getFirstAddress = (wallet, cb) => { this.storage.fetchAddresses(this.walletId, (err, addresses) => { if (err) return cb(err); if (addresses?.length) { const x = addresses[0]; index_1.ChainService.addressFromStorageTransform(wallet.chain, wallet.network, x); return cb(null, x); } return createNewAddress(wallet, cb); }); }; this.getWallet({ doNotMigrate: opts.doNotMigrate }, (err, wallet) => { if (err) return cb(err); if (index_1.ChainService.isSingleAddress(wallet.chain)) { opts.ignoreMaxGap = true; opts.singleAddress = true; } this._canCreateAddress(opts.ignoreMaxGap || opts.singleAddress || wallet.singleAddress, (err, canCreate) => { if (err) return cb(err); if (!canCreate) return cb(errordefinitions_1.Errors.MAIN_ADDRESS_GAP_REACHED); this._runLocked(cb, cb => { this.getWallet({ doNotMigrate: opts.doNotMigrate }, (err, wallet) => { if (err) return cb(err); if (!wallet.isComplete()) return cb(errordefinitions_1.Errors.WALLET_NOT_COMPLETE); if (wallet.scanStatus == 'error')