UNPKG

melis-api-js

Version:
1,559 lines (1,385 loc) 88.2 kB
require('isomorphic-fetch'); const Q = require('q') const events = require('events') const Stomp = require('webstomp-client') const WebSocketClient = require('ws') const SockJS = require('sockjs-client') const Bitcoin = require('bitcoinjs-lib') const isNode = (require('detect-node') && !('electron' in global.process.versions)) const randomBytes = require('randombytes') const sjcl = require('sjcl-all') const C = require("./cm-constants") const FeeApi = require("./fee-api") const BC_APIS = require("./blockchain-apis") const CoinDrivers = require("./drivers") const logger = require("./logger") const MelisErrorModule = require("./melis-error") const MelisError = MelisErrorModule.MelisError const throwUnexpectedEx = MelisErrorModule.throwUnexpectedEx function walletOpen(target, hd, serverWalletData, isSingleAccount) { if (!hd || !serverWalletData) throwUnexpectedEx("No data opening wallet") const accounts = {}, balances = {}, infos = {}, keys = {} serverWalletData.accounts.forEach((a, i) => { accounts[a.pubId] = a balances[a.pubId] = serverWalletData.balances[i] infos[a.pubId] = serverWalletData.accountInfos[i] if (isSingleAccount) keys[a.num] = hd else keys[a.num] = target.deriveAccountHdKey(hd, a.num, a.coin) }) if (isSingleAccount) target.hdAccount = hd else target.hdWallet = hd target.walletData = { accounts, balances, infos, keys } // Transforms arrays in objects serverWalletData.accounts = accounts serverWalletData.balances = balances serverWalletData.accountInfos = infos emitEvent(target, C.EVENT_WALLET_OPENED, serverWalletData) } function walletClose(target) { target.hdWallet = null target.hdAccount = null target.walletData = null } function updateWalletInfo(target, info) { target.walletData.info = info } function updateAccount(target, accountData, hdWallet) { const account = accountData.account if (!target.walletData.accounts[account.pubId]) target.walletData.accounts[account.pubId] = account target.walletData.balances[account.pubId] = accountData.balance if (accountData.accountInfo) target.walletData.infos[account.pubId] = accountData.accountInfo if (hdWallet) target.walletData.keys[account.num] = target.deriveAccountHdKey(hdWallet, account.num, account.coin) } function updateServerConfig(target, config) { if (config.message) logger.log("Server message status: " + config.message) target.useTestPaths = !(!config.platform || config.platform === "production") target.cmConfiguration = config target.lastBlocks = config.topBlocks target.platform = config.platform } function possiblyIncompleteAccountInfo(info) { return !info || !info.account || info.account.status == C.STATUS_WAITING_COSIGNERS || (info.cosigners && info.cosigners.length > 1 && !info.scriptParams) } function emitEvent(target, event, params) { target.lastReceivedMsgDate = new Date() if (event === C.EVENT_DISCONNECT_REQ) { logger.log("Server requested to disconnect:", params) return handleConnectionLoss(target, true) } if (event === C.EVENT_PING) target.stompClient.send(C.UTILS_PONG, {}, {}) if (event !== C.EVENT_RPC_ACTIVITY_END && event !== C.EVENT_RPC_ACTIVITY_START) logger.log("[CM emitEvent] " + event + " params: " + JSON.stringify(params)) if (event === C.EVENT_CONFIG) updateServerConfig(target, params) if (event === C.EVENT_ACCOUNT_UPDATED) updateAccount(target, params) var listeners = target.listeners(event) if (!listeners.length) { //logger.log("[CM emitEvent] nessun listener per l'evento '" + event + "'") target.emit(C.UNHANDLED_EVENT, { name: event, params: params }) } else { target.emit(event, params) } } function buildMelisErrorFromServerEx(err) { const res = new MelisError(err.ex, err.msg) for (var prop in err) { if (prop !== 'ex' && prop !== 'msg') res[prop] = err[prop] } return res } function buildBadParamEx(paramName, msg) { //return {ex: 'CmBadParamException', param: paramName, msg: msg} const err = new MelisError('CmBadParamException', msg) err.param = paramName return err } function buildInvalidAddressEx(address, msg) { return new MelisError('CmInvalidAddressException', msg) } function buildConnectionFailureEx(msg) { return new MelisError('ConnectionFailureException', msg) } function failPromiseWithEx(ex) { return Q.reject(ex) } function failPromiseWithBadParam(paramName, msg) { return Q.reject(buildBadParamEx(paramName, msg)) } function throwBadParamEx(paramName, msg) { throw buildBadParamEx(paramName, msg) } function throwInvalidSignatureEx(msg) { throw new MelisError('CmInvalidSignatureException', msg) } function throwConnectionEx(msg) { throw buildConnectionFailureEx(msg) } function initializePrivateFields(target) { target.rpcCounter = 0 if (target.waitingReplies) { for (var d in target.waitingReplies) { var deferred = target.waitingReplies[d].deferred deferred.reject(C.EVENT_DISCONNECT) } } target.waitingReplies = {} target.hdWallet = null target.accountExtendedKey = null target.walletData = null target.lastBlocks = {} target.lastOpenParams = null target.cmConfiguration = null // Got from server at connect target.connected = false target.connecting = false target.paused = false target.stompClient = null } function addPagingInfo(pars, pagingInfo) { if (pagingInfo) { pars.page = pagingInfo.page || 0 pars.size = pagingInfo.size || 20 if (pagingInfo.sortField) { pars.sortField = pagingInfo.sortField pars.sortDir = pagingInfo.sortDir } } return pars } function simpleRandomInt(max) { return Math.floor(Math.random() * max) } function handleConnectionLoss(target) { if (!target.connected) return Q() var deferred = Q.defer() target.stompClient.disconnect(function (res) { stompDisconnected(target, res, null) deferred.resolve() }) return deferred.promise } function keepAliveFunction(target) { var nowTime = new Date().getTime() var lastMsgTime = target.lastReceivedMsgDate ? target.lastReceivedMsgDate.getTime() : nowTime - 1 var secsElapsed = (nowTime - lastMsgTime) / 1000 + 1 // console.log("[KEEPALIVE] elapsed from last msg: " + secsElapsed + " minKeepAlive: " + target.maxKeepAliveSeconds) if (secsElapsed >= target.maxKeepAliveSeconds + 20) { logger.logWarning("No response from server since " + secsElapsed + " seconds: DISCONNECTING") handleConnectionLoss(target) } else if (secsElapsed >= target.maxKeepAliveSeconds / 2) { target.ping() } } function rpcReplyHandler(target, res) { // logger.log("[STOMP] Ricevuta risposta RPC: " + res) //var messageId = res.headers.myId target.lastReceivedMsgDate = new Date() var message = JSON.parse(res.body) var messageId = message.id //logger.log("[STOMP] rpcReplyHandler message: " + JSON.stringify(message)) if (messageId) { var rpcData = target.waitingReplies[messageId] delete target.waitingReplies[messageId] if (rpcData) rpcData.deferred.resolve(message.m) else logger.logError("[STOMP] RPC reply con ID: " + messageId + " non trovato in coda") } else { logger.logWarning("[STOMP] RPC reply senza ID:", message) } } function rpcErrorHandler(target, res) { logger.log("[STOMP] RPC Exception:", res) target.lastReceivedMsgDate = new Date() //var messageId = res.headers.myId const message = JSON.parse(res.body) const messageId = message.id if (messageId) { const rpcData = target.waitingReplies[messageId] delete target.waitingReplies[messageId] if (rpcData) { if (message.ex === C.EX_TOO_MANY_REQUESTS && rpcData.numRetries < target.rpcMaxRetries) { var rpcRetryDelay = target.rpcRetryDelay * rpcData.numRetries logger.log("Server requested to slow down requests -- retry #" + rpcData.numRetries + " waiting " + rpcRetryDelay + "ms") setTimeout(function () { logger.log("Preparing new request") target.rpc(rpcData.queue, rpcData.data, rpcData.headers, rpcData.numRetries + 1).then(function (res) { rpcData.deferred.resolve(res) }).catch(function (res) { logger.log("RE-REQUEST FAILED:", res) rpcData.deferred.reject(buildMelisErrorFromServerEx(res)) }) }, rpcRetryDelay) } else rpcData.deferred.reject(buildMelisErrorFromServerEx(message)) } else { logger.logError("[STOMP] RPC Error -- Unable to find request with ID: " + messageId) } } } function CM(config) { if (!config) config = {} if (config.useTestPaths) this.useTestPaths = config.useTestPaths if (config.stompEndpoint || process.env.MELIS_ENDPOINT) this.stompEndpoint = process.env.MELIS_ENDPOINT || config.stompEndpoint this.apiDiscoveryUrl = process.env.MELIS_DISCOVER || config.apiDiscoveryUrl || C.MELIS_DISCOVER this.apiUrls = null this.rpcTimeout = config.rpcTimeout >= 100 ? config.rpcTimeout : 60000 this.rpcRetryDelay = config.rpcRetryDelay >= 10 ? config.rpcRetryDelay : 1500 this.rpcMaxRetries = config.rpcMaxRetries >= 1 ? config.rpcMaxRetries : 10 this.autoReconnectDelay = config.autoReconnectDelay >= 0 ? config.autoReconnectDelay : 30 this.maxKeepAliveSeconds = config.maxKeepAliveSeconds >= 20 ? config.maxKeepAliveSeconds : 60 this.disableKeepAlive = config.disableKeepAlive === true this.connected = false this.autoReconnectFunc = null this.stompClient = null this.externalTxValidator = null this.minutesBetweenNetworkFeesUpdates = 60 this.feeApi = new FeeApi({ melis: this }) initializePrivateFields(this) } CM.prototype = Object.create(events.EventEmitter.prototype) // by default f is console CM.prototype.setLogger = function (f) { if (f) logger.setLogObject(f) else logger.setLogObject({ log: (a, b) => { }, logWarning: (a, b) => { }, logError: (a, b) => { } }) } CM.prototype.getRpcTimeout = function () { return this.rpcTimeout } CM.prototype.setRpcTimeout = function (ms) { if (ms >= 1 && ms <= 1000000) this.rpcTimeout = ms else throwBadParamEx(ms, "Timeout ms must be between 1 and 1000000") } CM.prototype.isProdNet = function () { return !this.useTestPaths } // // Coin dependent functions // function getSupportedCoins() { return Object.keys(CoinDrivers) } function getDriver(coin) { const driver = CoinDrivers[coin] if (!driver) throw new MelisError("Unknown coin: " + coin) return driver } CM.prototype.getDefaultPlatformCoin = function (coin) { return this.isProdNet() ? C.COIN_PROD_BTC : C.COIN_TEST_BTC } CM.prototype.getCoinDriver = function (coin) { return getDriver(coin) } CM.prototype.decodeCoinAddress = function (coin, address) { return getDriver(coin).decodeCoinAddress(address) } CM.prototype.hashForSignature = function (coin, tx, index, redeemScript, amount, hashFlags) { return getDriver(coin).hashForSignature(tx, index, redeemScript, amount, hashFlags) } CM.prototype.isValidAddress = function (coin, address) { return getDriver(coin).isValidAddress(address) } CM.prototype.toScriptSignature = function (coin, signature, hashFlags) { return getDriver(coin).toScriptSignature(signature, hashFlags) } CM.prototype.toOutputScript = function (coin, address) { return getDriver(coin).toOutputScript(address) } CM.prototype.wifToEcPair = function (coin, wif) { return getDriver(coin).wifToEcPair(wif) } CM.prototype.signMessageWithKP = function (coin, keyPair, message) { return getDriver(coin).signMessageWithKP(keyPair, message) } CM.prototype.verifyMessageSignature = function (coin, address, signature, message) { return getDriver(coin).verifyMessageSignature(address, signature, message) } CM.prototype.signMessageWithAA = function (account, aa, message) { if (account.type !== C.TYPE_PLAIN_HD) throw new MelisError('CmBadParamException', 'Only single signature accounts can sign messages') const key = this.deriveAddressKey(account.num, aa.chain, aa.hdindex) return this.signMessageWithKP(account.coin, key.keyPair, message) } CM.prototype.buildAddressFromScript = function (coin, script) { return getDriver(coin).buildAddressFromScript(script) } CM.prototype.pubkeyToAddress = function (coin, key) { return getDriver(coin).pubkeyToAddress(key) } CM.prototype.prepareAddressSignature = function (coin, keyPair, prefix) { return getDriver(coin).prepareAddressSignature(keyPair, prefix) } CM.prototype.extractPubKeyFromOutputScript = function (coin, script) { return getDriver(coin).extractPubKeyFromOutputScript(script) } CM.prototype.calcP2SH = function (coin, accountInfo, chain, hdIndex) { return getDriver(coin).calcP2SH(accountInfo, chain, hdIndex) } CM.prototype.derivePubKeys = function (coin, xpubs, chain, hdIndex) { return getDriver(coin).derivePubKeys(xpubs, chain, hdIndex) } CM.prototype.hdNodeFromHexSeed = function (seed, coin) { if (!coin) coin = this.getDefaultPlatformCoin() return getDriver(coin).hdNodeFromHexSeed(seed) } CM.prototype.hdNodeFromBase58 = function (xpub, coin) { if (!coin) coin = this.getDefaultPlatformCoin() return getDriver(coin).hdNodeFromBase58(xpub) } CM.prototype.hdNodeToBase58Xpub = function (hd, coin) { if (!coin) coin = this.getDefaultPlatformCoin() return getDriver(coin).hdNodeToBase58Xpub(hd) } CM.prototype.exportAccountMasterKeyToBase58 = function (account) { const hd = this.peekAccountMasterKey(account) return getDriver(account.coin).hdNodeToExtendedBase58(hd) } CM.prototype.updateNetworkFees = function (coin) { var self = this if (!self.feeInfos) self.feeInfos = {} if (self.feeInfos[coin] && self.feeInfos[coin].lastUpdated) { const msToLastUpdate = new Date() - self.feeInfos[coin].lastUpdated if (msToLastUpdate > 1000 * 60 * 15) // Update fees not more than once every 15 minutes return self.feeInfos[coin] } const provider = coin.endsWith(C.COIN_PROD_BTC) ? 'melis' : 'hardcoded' return self.feeApi.getFeesByProvider(coin, provider)().then(res => { return self.feeInfos[coin] = res }) } CM.prototype.setAutoReconnectDelay = function (seconds) { if (seconds >= 0) this.autoReconnectDelay = seconds else this.autoReconnectDelay = 0 } CM.prototype.randomBytes = function (n) { return randomBytes(n) } CM.prototype.randomHexBytes = function (n) { return this.randomBytes(n).toString('hex') } CM.prototype.random32HexBytes = function () { return this.randomHexBytes(32) } CM.prototype.isConnected = function () { return this.connected } CM.prototype.isReady = function () { return !(!this.cmConfiguration) } CM.prototype.parseBIP32Path = function (path, radix) { if (!radix) radix = 10 if (path.indexOf("m/") === 0) path = path.substring(2) var result = [] var pathElems = path.split("/") for (var i = 0; i < pathElems.length; i++) { var hardened = false var val = pathElems[i] if (val.charAt(val.length - 1) === '\'') { hardened = true val = val.substring(0, val.length - 1) } val = parseInt(val, radix) if (val >= 0x80000000) throwBadParamEx('path', "Invalid path element: " + val) result.push((hardened ? (0x80000000) | val : val) >>> 0) } return result } CM.prototype.getLoginPath = function () { const product = 31337 // CM const path = this.isProdNet() ? 0 : 1 // Use another path for I2P/TOR? return [ ((0x80000000) | product) >>> 0, ((0x80000000) | path) >>> 0 ] } CM.prototype.deriveKeyFromPath = function (hdnode, path) { if (!path || path.length === 0) return hdnode let key = hdnode for (let i = 0; i < path.length; i++) { const index = path[i] if (index & 0x80000000) { const v = index & 0x7FFFFFFF key = key.deriveHardened(v) } else { key = key.derive(index) } } return key } // BIP44 standard derivation CM.prototype.deriveAccountHdKey = function (hd, accountNum, coin) { const subTree = this.isProdNet() ? 0 : 1 let key = hd.deriveHardened(44) key = key.deriveHardened(subTree) key = key.deriveHardened(accountNum) if (coin) getDriver(coin).fixKeyNetworkParameters(key) return key } CM.prototype.deriveChainIndex = function (accountKey, chain, index) { return accountKey.derive(chain).derive(index) } CM.prototype.deriveAddressKey = function (accountNum, chain, index) { const accountHd = this.walletData.keys[accountNum] return accountHd.derive(chain).derive(index) } CM.prototype.peekAccountMasterKey = function (account) { return this.walletData.keys[account.num] } CM.prototype.exportAddressKeyToWIF = function (account, aa) { const key = this.deriveAddressKey(account.num, aa.chain, aa.hdindex) return key.keyPair.toWIF() } CM.prototype.rpc = function (queue, data, headers, numRetries) { logger.log("[RPC] q: " + queue + (headers ? " h: " + JSON.stringify(headers) : " no headers") + (data ? " data: " + JSON.stringify(data) : " no data")) if (!this.connected) return Q.reject(buildConnectionFailureEx("RPC call without connection")) if (!queue) return Q.reject(buildBadParamEx('queue', "RPC call without defined queue")) var deferred = Q.defer() this.rpcCounter++ if (Object.keys(this.waitingReplies).length === 0) { emitEvent(this, C.EVENT_RPC_ACTIVITY_START) } this.pendingRPC++ var rpcCounter = this.rpcCounter this.waitingReplies[rpcCounter] = { deferred: deferred, queue: queue, headers: headers, data: data, numRetries: numRetries || 1 } // logger.log("[STOMP] queue: " + queue + " data: " + JSON.stringify(data) + " typeof(data): " + typeof data) if (!headers) headers = {} headers.id = rpcCounter if (data !== undefined && data !== null) this.stompClient.send(queue, typeof data === "object" ? JSON.stringify(data) : data, headers) else this.stompClient.send(queue, "{}", headers) var self = this return deferred.promise.timeout(this.rpcTimeout).catch(function (err) { logger.log("[RPC] Ex or Timeout -- res: ", err) var ex if (err.code && err.code === 'ETIMEDOUT') { ex = new MelisError('RpcTimeoutException', 'RPC call timeout after ' + self.rpcTimeout + 'ms') //ex = {ex: "rpcTimeout", msg: 'RPC call timeout after ' + self.rpcTimeout + 'ms'} delete self.waitingReplies[rpcCounter] } else ex = buildMelisErrorFromServerEx(err) return Q.reject(ex) }).finally(function () { if (Object.keys(self.waitingReplies).length === 0) { emitEvent(self, C.EVENT_RPC_ACTIVITY_END) } }) } CM.prototype.simpleRpcSlice = function (queue, data) { return this.rpc(queue, data).then(res => { return res.slice }) } // Fetch the STOMP endpoint from the melis discover server function fetchStompEndpoint(self) { var discoveryUrl = self.apiDiscoveryUrl logger.log("Discovering STOMP endpoint using: ", discoveryUrl) return fetch(discoveryUrl, { headers: { "user-agent": "melis-js-api/" + C.CLIENT_API_VERSION } }).then(function (res) { if (res.status !== 200) throw new MelisError('DiscoveryEx', 'Bad status code: ' + res.status) return res.json() }).then(function (discovered) { logger.log("Discovery result: ", discovered) self.apiUrls = discovered if (discovered.publicUrlPrefix || discovered.stompEndpoint) return discovered throw new MelisError('DiscoveryEx', 'Missing discovery data from ' + discoveryUrl) }).catch(function (res) { if (res.ex === "DiscoveryEx") throw res var stringMsg = "" + res if (stringMsg.includes("SyntaxError: Unexpected token")) throw new MelisError('DiscoveryEx', 'Unable to discover stompEndpoint from ' + discoveryUrl) else throw new MelisError('DiscoveryEx', stringMsg) }) } function enableKeepAliveFunc(self) { logger.log("[enableKeepAliveFunc] self.keepAliveFunc: " + self.keepAliveFunc) if (self.disableKeepAlive || self.keepAliveFunc) return self.keepAliveFunc = setInterval(function () { keepAliveFunction(self) }, (self.maxKeepAliveSeconds / 2 + 1) * 1000) } function disableKeepAliveFunc(self) { logger.log("[disableKeepAliveFunc] self.keepAliveFunc: " + self.keepAliveFunc) if (self.keepAliveFunc) { clearInterval(self.keepAliveFunc) self.keepAliveFunc = null } } function disableAutoReconnect(self) { if (self.autoReconnectFunc) { clearTimeout(self.autoReconnectFunc) self.autoReconnectFunc = null } } function stompDisconnected(self, frame, deferred) { var wasConnected = self.connected var wasPaused = self.paused logger.log("[CM] stompDisconnected wasConnected: " + wasConnected + " wasPaused: " + wasPaused)// + " err.code: " + frame.code + " err.wasClean: " + frame.wasClean) self.stompClient = null self.connected = false self.connecting = false self.paused = false self.cmConfiguration = null disableKeepAliveFunc(self) if (deferred) deferred.reject(frame) //logger.log("Open requests: ", Object.keys(self.waitingReplies)) Object.keys(self.waitingReplies).forEach(function (i) { var rpcData = self.waitingReplies[i] delete self.waitingReplies[i] logger.log('[CM] Cancelling open rpc request:', rpcData) rpcData.deferred.reject(buildConnectionFailureEx("Disconnected")) }) self.waitinReplies = {} emitEvent(self, C.EVENT_DISCONNECT) if (wasPaused || !wasConnected) return if (self.autoReconnectDelay > 0 && self.autoReconnectFunc === null) { var timeout = 10 + Math.random() * 10 + Math.random() * (self.autoReconnectDelay / 10) logger.log("[CM] NEXT AUTO RECONNECT in " + timeout + " seconds") self.autoReconnectFunc = setTimeout(function () { self.autoReconnectFunc = null self.connect(self.lastConfig) }, timeout * 1000) } } function retryConnect(self, config, errorMessage) { logger.log(errorMessage) if (self.autoReconnectDelay > 0) { var timeout = 10 + Math.random() * 10 + Math.random() * (self.autoReconnectDelay / 10) logger.log("[CM] retryConnect in " + timeout + " seconds") return Q.delay(timeout * 1000).then(function () { self.connecting = false return self.connect(config) }) } else throwConnectionEx(errorMessage) } CM.prototype.connect = function (config) { const self = this if (this.connecting) return Q() this.paused = false this.connecting = true if (this.autoReconnectFunc) { clearTimeout(this.autoReconnectFunc) this.autoReconnectFunc = null } if (this.stompClient !== null) { if (this.connected) return Q(self.cmConfiguration) this.stompClient.disconnect() this.stompClient = null } const discoverer = self.stompEndpoint ? Q(self.stompEndpoint) : Q(fetchStompEndpoint(self, config)).then(function (discovered) { return discovered.stompEndpoint }) return discoverer.then(stompEndpoint => { return self.connect_internal(stompEndpoint, config) }).catch(err => { logger.log("Discover err:", err) const errMsg = 'Unable to connect: ' + err.ex + " : " + err.msg const callback = config ? config.connectProgressCallback : null if (callback && typeof callback === 'function') callback({ errMsg: errMsg, err: err }) if (config && config.autoRetry) return retryConnect(self, config, errMsg) else { self.connecting = false return Q.reject(err) } }) } CM.prototype.connect_internal = function (stompEndpoint, config) { const self = this const deferred = Q.defer() const options = { debug: false, heartbeat: false, protocols: ['v12.stomp'] } if ((/^wss?:\/\//).test(stompEndpoint)) { if (isNode) { logger.log("[STOMP] Opening websocket (node):", stompEndpoint) const ws = new WebSocketClient(stompEndpoint) ws.on('error', function (error) { logger.log('[connect_internal] CONNECT ERROR:' + error.code) deferred.reject(error) }) this.stompClient = Stomp.over(ws, options) } else { logger.log("[STOMP] Opening websocket (browser) to " + stompEndpoint + " options:", options) this.stompClient = Stomp.client(stompEndpoint, options) } } else { logger.log("[STOMP] Opening sockjs:", stompEndpoint) this.stompClient = Stomp.over(new SockJS(stompEndpoint), options) } this.stompClient.debug = function (str) { //logger.log(str) } var headers = {} if (config && config.userAgent) headers.userAgent = JSON.stringify(config.userAgent) if (config && config.locale) headers.locale = config.locale if (config && config.currency) headers.currency = config.currency this.lastConfig = config this.stompClient.connect(headers, function (frame) { logger.log("[CM] Connected to websocket: " + frame) self.connected = true self.connecting = false self.paused = false self.stompClient.subscribe(C.QUEUE_RPC_REPLY, function (message) { rpcReplyHandler(self, message) }) self.stompClient.subscribe(C.QUEUE_RPC_ERROR, function (message) { rpcErrorHandler(self, message) }) self.stompClient.subscribe(C.QUEUE_SERVER_EVENTS, function (message) { //logger.log("[CM] Server event: " + message.body) var msg = JSON.parse(message.body) emitEvent(self, msg.type, msg.params) }) self.stompClient.subscribe(C.QUEUE_PUBLIC_MSGS, function (message) { var msg = JSON.parse(message.body) if (msg.type && msg.type === C.EVENT_PING) emitEvent(self, C.EVENT_PING, msg) else emitEvent(self, C.EVENT_PUBLIC_MESSAGE, msg) }) self.stompClient.subscribe(C.QUEUE_BLOCKS, function (message) { const msg = JSON.parse(message.body) self.lastBlocks[msg.coin] = msg emitEvent(self, C.EVENT_BLOCK, msg) }) self.stompClient.subscribe(C.QUEUE_CONFIG, function (message) { var initialEvents = JSON.parse(message.body) for (var i = 0; i < initialEvents.length; i++) { var event = initialEvents[i] emitEvent(self, event.type, event.params) } if (self.lastOpenParams) { if (self.lastOpenParams.accountExtendedKey) { self.accountOpen(self.lastOpenParams.accountExtendedKey, self.lastOpenParams).then(wallet => { emitEvent(self, C.EVENT_SESSION_RESTORED, wallet) }) } else if (self.lastOpenParams.seed) { self.walletOpen(self.lastOpenParams.seed, self.lastOpenParams).then(wallet => { emitEvent(self, C.EVENT_SESSION_RESTORED, wallet) }) } } if (self.cmConfiguration.maxKeepAliveSeconds && self.cmConfiguration.maxKeepAliveSeconds < self.maxKeepAliveSeconds) self.maxKeepAliveSeconds = self.cmConfiguration.maxKeepAliveSeconds enableKeepAliveFunc(self) emitEvent(self, C.EVENT_CONNECT) deferred.resolve(self.cmConfiguration) }) }, function (frame) { stompDisconnected(self, frame, deferred) }) return deferred.promise } CM.prototype.disconnect = function () { const self = this disableKeepAliveFunc(self) disableAutoReconnect(self) if (!this.connected) return Q() var deferred = Q.defer() this.stompClient.disconnect(res => { logger.log("[CM] STOMP Client disconnect: " + res) this.stompClient = null initializePrivateFields(self) deferred.resolve(res) }) return deferred.promise } CM.prototype.networkOnline = function () { if (this.autoReconnectDelay > 0) return this.connect() else return Q() } CM.prototype.networkOffline = function () { disableKeepAliveFunc(this) this.paused = true return handleConnectionLoss(this) } CM.prototype.hintDevicePaused = function () { disableKeepAliveFunc(this) this.paused = true if (this.connected) this.sessionSetParams({ paused: true }) return Q() } CM.prototype.verifyConnectionEstablished = function (timeout) { var self = this this.paused = false if (!timeout || timeout < 0) timeout = 5 if (timeout > this.maxKeepAliveSeconds) timeout = this.maxKeepAliveSeconds logger.log("[verifyConnectionEstablished] connected: " + this.connected + " timeout: " + timeout + " stompClient: " + (this.stompClient ? "yes" : "no")) if (!this.stompClient) return Q() if (!this.connected) return this.connect() return this.ping().timeout(timeout * 1000).catch(function (err) { logger.log("[verifyConnectionEstablished] ping timeout after " + timeout + " seconds") return handleConnectionLoss(self) }).then(function () { enableKeepAliveFunc(self) }) } CM.prototype.subscribe = function (queue, callback, headers) { if (!queue || !callback) throwBadParamEx('queue', "Call to subscribe without defined queue or callback") var self = this return this.stompClient.subscribe(queue, function (res) { // logger.log("[CM] message to queue " + queue + " : ", res) var msg = JSON.parse(res.body) callback(msg) }, headers) } CM.prototype.subscribeToTickers = function (currency, callback) { if (!currency || !callback) throwBadParamEx('currency', "Missing currency or callback while subscribing to tickers") return this.subscribe(C.QUEUE_TICKERS_PREFIX + currency, callback) } CM.prototype.subscribeToTickersHistory = function (period, currency, callback) { if (!period || !currency || !callback) throwBadParamEx('currency', "Missing period, currency or callback while subscribing to history: " + currency) var path = C.QUEUE_TICKERS_HISTORY_PREFIX + period + "/" + currency return this.subscribe(path, callback) } // // PUBLIC METHODS // CM.prototype.getPaymentAddressForAccount = function (accountIdOrAlias, param) { const opts = { name: accountIdOrAlias } if (param) { if (param.memo) opts.data = param.memo if (param.address) opts.address = param.address } return this.rpc(C.GET_PAYMENT_ADDRESS, opts).then(function (res) { //logger.log("[CM] getPaymentAddress: ", res) return res.address }) } CM.prototype.accountGetPublicInfo = function (params) { return this.rpc(C.GET_ACCOUNT_PUBLIC_INFO, { name: params.name, code: params.code }).then(function (res) { //logger.log("[CM] accountGetPublicInfo: " + JSON.stringify(res)) return res.account }) } CM.prototype.getLoginChallenge = function () { return this.rpc(C.GET_CHALLENGE) } // // UTILITIES // CM.prototype.decodeTxFromBuffer = function (buf) { return Bitcoin.Transaction.fromBuffer(buf) } CM.prototype.pushTx = function (coin, hex) { return this.rpc(C.UTILS_PUSH_TX, { coin, hex }) } CM.prototype.getFeeInfo = function (coin) { return this.rpc(C.UTILS_FEE_INFO + "/" + coin) } CM.prototype.ping = function () { return this.rpc(C.UTILS_PING) } CM.prototype.logException = function (account, data, deviceId, agent) { return this.rpc(C.UTILS_LOG_EX, { pubId: account ? account.pubId : null, data: data, deviceId: deviceId, ua: typeof agent === "object" ? agent : { application: agent } }) } CM.prototype.logData = function (account, data, deviceId, agent) { return this.rpc(C.UTILS_LOG_DATA, { pubId: account.pubId, data: data, deviceId: deviceId, ua: typeof agent === "object" ? agent : { application: agent } }) } CM.prototype.deviceSetPassword = function (deviceName, pin) { if (!deviceName || !pin) return failPromiseWithBadParam(deviceName ? "pin" : "deviceName", "missing deviceName or pin") var self = this return this.rpc(C.WALLET_DEVICE_SET_PASSWORD, { deviceName: deviceName, userPin: pin }).then(function (res) { // The result is base64 encoded logger.log("[CM] setDeviceName:", res) return { deviceId: res.info } }) } CM.prototype.deviceGetPassword = function (deviceId, pin) { if (!deviceId || !pin) return failPromiseWithBadParam(deviceId ? "pin" : "deviceId", "missing deviceId or pin") var self = this return this.rpc(C.WALLET_DEVICE_GET_PASSWORD, { deviceId: deviceId, userPin: pin }).then(function (res) { logger.log("[CM] getDevicePassword:", res) return res.info }) } CM.prototype.deviceUpdate = function (deviceId, newName) { if (!deviceId || !newName) return failPromiseWithBadParam("deviceId|newName", "missing deviceId or newName") var self = this return this.rpc(C.WALLET_DEVICE_UPDATE, { deviceId: deviceId, deviceName: newName }).then(function (res) { return res.info }) } CM.prototype.deviceChangePin = function (deviceId, oldPin, newPin) { if (!deviceId || !oldPin || !newPin) return failPromiseWithBadParam("deviceId|oldPin|newPin", "missing deviceId, newPin or oldPin") var self = this return this.rpc(C.WALLET_DEVICE_CHANGE_PIN, { deviceId: deviceId, userPin: oldPin, newPin: newPin }).then(function (res) { return res.info }) } CM.prototype.devicePromoteToPrimary = function (deviceId, tfa) { if (!deviceId) return failPromiseWithBadParam("deviceId", "missing deviceId") return this.rpc(C.WALLET_DEVICE_PROMOTE_TO_PRIMARY, { deviceId: deviceId, tfa: tfa }) } CM.prototype.deviceCancelPromotion = function (tfa) { return this.rpc(C.WALLET_DEVICE_CANCEL_PROMOTION, { tfa }) } CM.prototype.deviceGetRecoveryHours = function () { return this.rpc(C.WALLET_DEVICE_GET_RECOVERY_HOURS) } CM.prototype.deviceSetRecoveryHours = function (hours, tfa) { return this.rpc(C.WALLET_DEVICE_SET_RECOVERY_HOURS, { data: hours, tfa: tfa }) } CM.prototype.devicesGet = function () { return this.simpleRpcSlice(C.WALLET_DEVICES_GET) } CM.prototype.devicesDelete = function (param) { var data = {} if (param instanceof Array) data.deviceIds = param else data.deviceId = param return this.rpc(C.WALLET_DEVICES_DELETE, data) } CM.prototype.devicesDeleteAll = function (deviceId) { var data = {} if (deviceId) data.deviceId = deviceId return this.rpc(C.WALLET_DEVICES_DELETE_ALL, data) } // // WALLET functions // CM.prototype.walletLogin = function (seed, params) { throw ('todo') } CM.prototype.walletOpen = function (seed, params) { const self = this if (!params) params = {} return this.getLoginChallenge().then(res => { //logger.log("[CM] walletOpen challenge: " + challengeHex + " seed: " + seed + " isProduction:"+self.isProdNet()) const hd = self.hdNodeFromHexSeed(seed) // Keep the public key for ourselves const loginId = self.deriveKeyFromPath(hd, self.getLoginPath()) const chainCode = Buffer.alloc(32, "42", 'hex') const loginHD = new Bitcoin.HDNode(loginId.keyPair, chainCode) const loginPath = [simpleRandomInt(C.MAX_SUBPATH), simpleRandomInt(C.MAX_SUBPATH)] const loginKey = self.deriveKeyFromPath(loginHD, loginPath) const signature = loginKey.sign(Buffer.from(res.challenge, 'hex')) //logger.log("child: " + child.getPublicKeyBuffer().toString('hex')() + " sig: " + signature) //logger.log("pubKey: " + masterPubKey + " r: " + signature.r.toString() + " s: " + signature.s.toString()) return self.rpc(C.WALLET_OPEN, { id: loginId.getPublicKeyBuffer().toString('hex'), loginPath, signatureR: signature.r.toString(), signatureS: signature.s.toString(), sessionName: params.sessionName, deviceId: params.deviceId, usePinAsTfa: params.usePinAsTfa, supportedCoins: getSupportedCoins() }).then(res => { const wallet = res.wallet logger.log("[CM] walletOpen pubKey:" + wallet.pubKey + self.isProdNet() + " #accounts: " + Object.keys(wallet.accounts).length + " isProdNet: ") walletOpen(self, hd, wallet) self.lastOpenParams = { seed: seed, sessionName: params.sessionName, deviceId: params.deviceId } return wallet }) }) } CM.prototype.accountOpen = function (extendedKey, params) { const self = this if (!params) params = {} const accountHd = self.hdNodeFromBase58(extendedKey, params.coin) return this.getLoginChallenge().then(res => { const loginPath = [simpleRandomInt(C.MAX_SUBPATH), simpleRandomInt(C.MAX_SUBPATH)] const loginKey = self.deriveKeyFromPath(accountHd, loginPath) const challenge = Buffer.from(res.challenge, 'hex') const signature = loginKey.sign(challenge) //logger.log("child: " + child.getPublicKeyBuffer().toString('hex')() + " sig: " + signature) //logger.log("pubKey: " + masterPubKey + " r: " + signature.r.toString() + " s: " + signature.s.toString()) return self.rpc(C.ACCOUNT_OPEN, { coin: params.coin, xpub: self.hdNodeToBase58Xpub(accountHd), loginPath, signatureR: signature.r.toString(), signatureS: signature.s.toString(), sessionName: params.sessionName, deviceId: params.deviceId, usePinAsTfa: params.usePinAsTfa }).then(res => { const wallet = res.wallet const accountData = res.accountData logger.log("[CM] accountOpen isProdNet: " + self.isProdNet + " wallet: ", wallet) wallet.accounts = [accountData.account] wallet.balances = [accountData.balance] wallet.accountInfos = [accountData.accountInfo] walletOpen(self, accountHd, wallet, true) self.lastOpenParams = { accountExtendedKey: extendedKey, sessionName: params.sessionName, deviceId: params.deviceId } return res }) }) } CM.prototype.walletRegister = function (seed, params) { var self = this if (!params) params = {} var loginKey var self = this try { //var hd = self.hdNodeFromHexSeed(self.isProdNet() ? C.COIN_PROD_BTC : C.COIN_TEST_BTC, seed) var hd = self.hdNodeFromHexSeed(seed) loginKey = self.deriveKeyFromPath(hd, self.getLoginPath()) //logger.log('REGISTER hd: ', hd, ' loginKey: ', loginKey) } catch (error) { var ex = { ex: "clientAssertFailure", msg: error.message } logger.log(ex) return Q.reject(ex) } return self.rpc(C.WALLET_REGISTER, { xpub: self.hdNodeToBase58Xpub(loginKey), sessionName: params.sessionName, deviceId: params.deviceId, usePinAsTfa: params.usePinAsTfa, supportedCoins: getSupportedCoins() }).then(res => { logger.log("[CM] walletRegister: ", res) walletOpen(self, hd, res.wallet) self.lastOpenParams = { seed: seed, sessionName: params.sessionName, deviceId: params.deviceId } return res.wallet }) } CM.prototype.walletClose = function () { const self = this return self.rpc(C.WALLET_CLOSE).then(function (res) { walletClose(self) return res }) } CM.prototype.accountsGet = function (pagingInfo) { const pars = addPagingInfo({}, pagingInfo) return this.simpleRpcSlice(C.WALLET_ACCOUNTS_GET, pars) } CM.prototype.walletGetNumSessions = function () { return this.rpc(C.WALLET_GET_NUM_SESSIONS).then(function (res) { //console.log("[CM] number of sessions with wallet open: " + JSON.stringify(res)) return res.numWalletSessions }) } CM.prototype.walletGetNotifications = function (fromDate, pagingInfo) { const pars = addPagingInfo({ fromDate: fromDate }, pagingInfo) return this.simpleRpcSlice(C.WALLET_GET_NOTIFICATIONS, pars) } CM.prototype.walletGetAccountsStatus = async function (pubIds, fromDate, pagingInfo) { if (!pubIds || pubIds.length == 0) return failPromiseWithBadParam("pubIds", "missing pubIds") const pars = addPagingInfo({ pubIds, fromDate }, pagingInfo) const res = await this.rpc(C.WALLET_GET_ACCOUNTS_STATUS, pars) return res.accounts } CM.prototype.walletGetInfo = function () { var self = this return this.rpc(C.WALLET_GET_INFO).then(function (res) { logger.log("walletGetInfo:", res) updateWalletInfo(self, res.info) return res.info }) } // Creates random account numbers // in order to be unable to guess hidden account numbers CM.prototype.getFreeAccountNum = function () { return this.rpc(C.WALLET_GET_FREE_ACCOUNT_NUM).then(function (res) { return res.accountNum // + simpleRandomInt(2) }) } CM.prototype.addPushTokenGoogle = function (token) { return this.rpc(C.WALLET_PUSH_REGISTER_GOOGLE, { data: token }) } CM.prototype.aliasGetInfo = function (account) { return this.rpc(C.ACCOUNT_ALIAS_INFO, { pubId: account.pubId }) } CM.prototype.aliasIsAvailable = function (alias) { return this.rpc(C.ACCOUNT_ALIAS_AVAILABLE, { name: alias }) } CM.prototype.aliasDefine = function (account, alias) { return this.rpc(C.ACCOUNT_ALIAS_DEFINE, { pubId: account.pubId, name: alias }) } CM.prototype.walletMetaSet = function (name, value) { return this.rpc(C.WALLET_META_SET, { name: name, meta: value }) } CM.prototype.walletMetaGet = function (param) { if (Array.isArray(param)) return this.rpc(C.WALLET_META_GET, { names: param }) else return this.rpc(C.WALLET_META_GET, { name: param }).then(function (res) { return res.meta }) } CM.prototype.walletMetasGet = function (pagingInfo) { var pars = addPagingInfo({}, pagingInfo) return this.simpleRpcSlice(C.WALLET_METAS_GET, pars) } CM.prototype.walletMetaDelete = function (name) { return this.rpc(C.WALLET_META_DELETE, { name: name }) } // // Account functions // /* * Parameters: * type * accountNum * meta * hidden * cosigners * minSignatures * mandatorySignature */ CM.prototype.accountCreate = function (params) { if (!params || !params.type) throwBadParamEx('params', "Bad parameters") let numPromise if (params.accountNum === undefined) numPromise = this.getFreeAccountNum() else numPromise = Q(params.accountNum) const self = this return numPromise.then(accountNum => { logger.log("[CM] accountCreate coin: " + params.coin + " accountNum: " + params.accountNum) params.accountNum = accountNum const accountHd = self.deriveAccountHdKey(self.hdWallet, accountNum, params.coin) params.xpub = self.hdNodeToBase58Xpub(accountHd, params.coin) return self.rpc(C.ACCOUNT_REGISTER, params) }).then(res => { updateAccount(self, res, self.hdWallet) return res }) } CM.prototype.accountJoin = function (params) { const self = this logger.log("[CM] joinWallet params:", params) var numPromise = Q(params.accountNum) if (params.accountNum === undefined) numPromise = self.getFreeAccountNum().then(num => { params.accountNum = num }) var coinPromise = Q(params.coin) if (!params.coin) coinPromise = this.joinCodeGetInfo(params.code).then(res => { params.coin = res.info.coin }) return numPromise.then(coinPromise).then(() => { logger.log("[CM] joinWallet coin: " + params.coin + " accountNum: " + params.accountNum) const accountHd = self.deriveAccountHdKey(self.hdWallet, params.accountNum, params.coin) return self.rpc(C.ACCOUNT_JOIN, { code: params.code, accountNum: params.accountNum, xpub: self.hdNodeToBase58Xpub(accountHd, params.coin), meta: params.meta }) }).then(res => { updateAccount(self, res, self.hdWallet) return res }) } CM.prototype.accountRefresh = function (account) { var self = this return this.rpc(C.ACCOUNT_REFRESH, { pubId: account.pubId }).then(res => { if (res.account && res.balance && res.accountInfo) updateAccount(self, res) return res }) } CM.prototype.accountUpdate = function (account, options) { if (!options || typeof options !== 'object') return logger.log("[accountUpdate] " + account.pubId + " :", options) var self = this return this.rpc(C.ACCOUNT_UPDATE, { pubId: account.pubId, hidden: options.hidden, meta: options.meta, tfa: options.tfa, pubMeta: options.pubMeta }).then(res => { updateAccount(self, res) return res }) } CM.prototype.accountDelete = function (account, tfa) { const self = this return this.rpc(C.ACCOUNT_DELETE, { pubId: account.pubId , tfa}).then(res => { delete self.walletData.accounts[account.pubId] delete self.walletData.balances[account.pubId] delete self.walletData.infos[account.pubId] return res }) } CM.prototype.joinCodeGetInfo = function (code) { return this.rpc(C.ACCOUNT_GET_JOIN_CODE_INFO, { code: code }) } CM.prototype.getLocktimeDays = function (account) { return this.rpc(C.ACCOUNT_GET_LOCKTIME_DAYS, { pubId: account.pubId }) } CM.prototype.setLocktimeDays = function (account, days, tfa) { return this.rpc(C.ACCOUNT_SET_LOCKTIME_DAYS, { pubId: account.pubId, data: days, tfa: tfa }) } CM.prototype.getRecoveryInfo = function (account, fromDate) { return this.rpc(C.ACCOUNT_GET_RECOVERY_INFO, { pubId: account.pubId, fromDate: fromDate }) } CM.prototype.getUnusedAddress = function (account, address, labels, meta) { const self = this if (meta && Object.keys(meta).length === 0) meta = null if (labels && labels.length === 0) labels = null const promise = possiblyIncompleteAccountInfo(self.peekAccountInfo(account)) ? self.accountRefresh(account).then(res => res.account) : Q(account) return promise.then(account => { return this.rpc(C.ACCOUNT_GET_UNUSED_ADDRESS, { pubId: account.pubId, address: address, labels: labels, meta: meta }) }).then(res => { const aa = res.address if (!self.isAddressOfAccount(account, aa)) return failPromiseWithEx(buildInvalidAddressEx(aa.address, "Received address not matching account definition! addr:" + aa.address + " pubId: " + account.pubId)) return aa }) } CM.prototype.addressUpdate = function (account, address, labels, meta) { if (meta && Object.keys(meta).length === 0) meta = null if (labels && labels.length === 0) labels = null return this.rpc(C.ACCOUNT_ADDRESS_UPDATE, { pubId: account.pubId, address: address, labels: labels, meta: meta }).then(res => res.address) } CM.prototype.addressRelease = function (account, address) { return this.rpc(C.ACCOUNT_ADDRESS_RELEASE, { pubId: account.pubId, address: address }).then(res => res.address) } CM.prototype.addressGet = function (account, address, optionsAndPaging) { var pars = addPagingInfo({ pubId: account.pubId, address: address }, optionsAndPaging) if (optionsAndPaging && optionsAndPaging.includeTxInfos) pars.includeTxInfos = optionsAndPaging.includeTxInfos return this.rpc(C.ACCOUNT_ADDRESS_GET, pars) } CM.prototype.addressesGet = function (account, optionsAndPaging) { var pars = addPagingInfo({ pubId: account.pubId }, optionsAndPaging) if (optionsAndPaging && optionsAndPaging.onlyActives) pars.onlyActives = optionsAndPaging.onlyActives if (optionsAndPaging && optionsAndPaging.chain >= 0) pars.chain = optionsAndPaging.chain return this.simpleRpcSlice(C.ACCOUNT_ADDRESSES_GET, pars) } CM.prototype.addLegacyAddress = function (account, keyPair, params) { var data = this.prepareAddressSignature(account.coin, keyPair, C.MSG_PREFIX_LEGACY_ADDR) return this.rpc(C.WALLET_ADD_LEGACY_ADDRESS, { pubId: account.pubId, address: data.address, data: data.base64Sig, labels: params ? params.labels : null, meta: params ? params.meta : null }) } CM.prototype.accountGetNotifications = function (account, fromDate, pagingInfo) { var pars = addPagingInfo({ pubId: account.pubId, fromDate: fromDate }, pagingInfo) return this.simpleRpcSlice(C.ACCOUNT_GET_NOTIFICATIONS, pars) } CM.prototype.txInfosGet = function (account, filter, pagingInfo) { if (!filter) filter = {} var pars = addPagingInfo({ pubId: account.pubId, fromDate: filter.fromDate, txDate: filter.txDate, direction: filter.direction }, pagingInfo) return this.simpleRpcSlice(C.ACCOUNT_GET_TX_INFOS, pars) } CM.prototype.txInfoGet = function (id) { return this.rpc(C.ACCOUNT_GET_TX_INFO, { data: id }).then(function (res) { return res.txInfo }) } CM.prototype.txInfoSet = function (id, labels, meta) { return this.rpc(C.ACCOUNT_SET_TX_INFO, { data: id, labels: labels, meta: meta }).then(function (res) { //console.log("[CM SET_TX_INFO] res: " + JSON.stringify(res)) return res.txInfo }) } CM.prototype.getAllLabels = function (account) { return this.rpc(C.ACCOUNT_GET_ALL_LABELS, { pubId: account ? account.pubId : null }) } CM.prototype.ptxPrepare = function (account, recipients, options) { logger.log("[CM ptxPrepare] account: " + account.pubId + " to: " + JSON.stringify(recipients) + " opts: ", options) options = options || {} var params = { pubId: account.pubId, recipients: recipients, unspents: options.unspents, tfa: options.tfa, ptxOptions: {} } Object.keys(options) .filter(k => k !== 'autoSignIfValidated') .forEach(k => params.ptxOptions[k] = options[k]) logger.log("[CM ptxPrepare] params:", params) return this.rpc(C.ACCOUNT_PTX_PREPARE, params) } CM.prototype.ptxFeeBump = function (id, options) { const ptxOptions = { feeMultiplier: options.feeMultiplier, satoshisPerByte: options.satoshisPerByte} const params = { data: id, ptxOptions: ptxOptions} return this.rpc(C.ACCOUNT_PTX_FEE_BUMP, params) } CM.prototype.ptxGetById = function (id) { return this.rpc(C.ACCOUNT_PTX_GET, { data: id }) } CM.prototype.ptxGetByHash = function (account, hash) { return this.rpc(C.ACCOUNT_PTX_GET, { pubId: account.pubId, hash: hash }) } CM.prototype.ptxCancel = function (ptx) { return this.rpc(C.ACCOUNT_PTX_CANCEL, { data: ptx.id }) } CM.prototype.ptxSignFields = function (account, ptx) { const num1 = simpleRandomInt(C.MAX_SUBPATH), num2 = simpleRandomInt(C.MAX_SUBPATH) const key = this.deriveAddressKey(account.num, num1, num2) var sig = this.signMessageWithKP(account.coin, key.keyPair, ptx.rawTx) return this.rpc(C.ACCOUNT_PTX_SIGN_FIELDS, { data: ptx.id, num1: num1, num2: num2, signatures: [sig] }) } CM.prototype.ptxHasFieldsSignature = function (ptx) { if (!ptx.meta) return false var keyMessage = ptx.meta.ownerSig return !!(keyMessage && keyMessage.keyPath && keyMessage.ptxSig) } // TODO: add verification of cosigners public key CM.prototype.ptxVerifyFieldsSignature = function (account, ptx) { const self = this return this.ensureAccountInfo(account).then(function (account) { i