UNPKG

scpx-wallet

Version:

Scoop Core Wallet: dual-signature timelock crypto wallet - multi-asset, cross-platform and open-source

569 lines (501 loc) 28.5 kB
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris. const _ = require('lodash') const BigNumber = require('bignumber.js') const BigDecimal = require('js-big-decimal') const utilsWallet = require('../utils') const { XS_SET_EXCHANGE_ASSET, XS_SET_RECEIVE_ASSET, XS_SET_MINMAX_AMOUNT, XS_SET_EST_RECEIVE_AMOUNT, XS_SET_FIXED_RECEIVE_AMOUNT, XS_UPDATE_EXCHANGE_TX, XS_SET_CURRENCIES } = require('.') const { getCurrenciesFullApi, getMinAmountApi, getEstReceiveAmountApi, getFixRateApi, getFixRateForAmountApi, getPairsParamsApi, createTransactionApi, createTransactionFixedApi, getStatusApi, getTransactionsApi } = require('../api/exchange') const { toXsSymbol } = require('../api/xs-changelly') const { walletExternal_config } = require('../config/wallet-external') const configWallet = require('../config/wallet') const { createTxHex, createAndPushTx } = require('./wallet-external') const { ExchangeStatusEnum } = require('../exchange/constants') const USE_CHANGELLY_FIXEDRATE_V2 = false var receiveAssetValue_intId var exchangeStatusTimer_intId = [] module.exports = { // currency statuses XS_getCurrencies: (store) => { return getCurrenciesFullApi() .then(res => { if (res && res.result) { //console.log('XS - getCurrencies, res=', res) // disable erc20 usdt manually -- not supported by changelly (have asked them if they can add it) // TODO: changelly -- "If it's about ERC20 USDT, we have already listed it. The ticker is usdt20." // const usdt = res.result.find(p => p.name === 'usdt') // if (usdt) { // usdt.enabled = false // } store.dispatch({ type: XS_SET_CURRENCIES, payload: res.result }) } }) }, // currently selected from and to assets XS_setExchangeAsset: (store, assetSymbol) => { if (!assetSymbol) { console.error('XS - XS_setExchangeAsset - invalid params') } console.log(`XS - XS_setExchangeAsset - ${assetSymbol}`) store.dispatch({ type: XS_SET_EXCHANGE_ASSET, payload: assetSymbol }) const cur_toSymbol = store.getState().userData.exchange.cur_toSymbol if (cur_toSymbol !== undefined) { return getMinAmount(store, assetSymbol, cur_toSymbol) } return null }, XS_setReceiveAsset: (store, assetSymbol) => { console.log(`XS - XS_setReceiveAsset - ${assetSymbol}`) store.dispatch({ type: XS_SET_RECEIVE_ASSET, payload: assetSymbol }) store.dispatch({ type: XS_SET_FIXED_RECEIVE_AMOUNT, payload: { derivedExpected: undefined, rateId: undefined } }) store.dispatch({ type: XS_SET_EST_RECEIVE_AMOUNT, payload: { derivedExpected: undefined, rateId: undefined } }) const cur_fromSymbol = store.getState().userData.exchange.cur_fromSymbol if (cur_fromSymbol !== undefined && assetSymbol !== undefined) { return getMinAmount(store, cur_fromSymbol, assetSymbol) } return null }, // min/max allowable for pair XS_getMinAmount: async (store, fromSymbol, toSymbol) => getMinAmount(store, fromSymbol, toSymbol), XS_clearMinAmount: () => {return { type: XS_SET_MINMAX_AMOUNT, payload: { min: 0, max: 0 } } }, // estimate receive amount XS_getEstReceiveAmount: async (store, fromSymbol, toSymbol, amount) => getEstReceiveAmount(store, fromSymbol, toSymbol, amount), XS_getEstReceiveAmount_SetTimer: async (store, fromSymbol, toSymbol, amount) => { console.log(`XS_getEstReceiveAmount_SetTimer fromSymbol=${fromSymbol} toSymbol=${toSymbol}...`) getEstReceiveAmount_ClearTimer() getEstReceiveAmount(store, fromSymbol, toSymbol, amount) receiveAssetValue_intId = setInterval(() => { getEstReceiveAmount(store, fromSymbol, toSymbol, amount) }, configWallet.IS_DEV ? 20000 : 20000) // refresh frequently -- v2 fixed rate API rateId's expire after 30s! }, XS_getEstReceiveAmount_ClearTimer: () => getEstReceiveAmount_ClearTimer(), // execute exchange XS_initiateExchange: (store, p) => { const { wallet, exchangeAsset, receiveAsset, amount, cur_estReceiveAmount, feeParams, addrNdx, rateId, apk, h_mpk, owner } = p getEstReceiveAmount_ClearTimer() return new Promise(async (resolve, reject) => { console.log(`XS - XS_initiateExchange (${rateId ? 'FIXED' : 'VARIABLE'}) - ${amount} ${exchangeAsset.symbol}==>${receiveAsset.symbol}, rateId=${rateId}...`) console.log('amount', amount) console.log('amount.toString()', amount.toString()) // eth send-max rounding // get actual value amount that we will send - changelly requires an exact match for its fixed-rate api const payToDummy = [{ receiver: walletExternal_config[exchangeAsset.symbol].donate, value: amount }] const txHexAndValue = await createTxHex({ payTo: payToDummy, asset: exchangeAsset, encryptedAssetsRaw: wallet.assetsRaw, feeParams: feeParams, sendMode: false, sendFromAddrNdx: addrNdx, apk, h_mpk, }) //console.log('DBG1 - txHexAndValue.cu_sendValue', txHexAndValue.cu_sendValue) const du_sendAmountActual = utilsWallet.toDisplayUnit(new BigNumber(txHexAndValue.cu_sendValue), exchangeAsset) //console.log('DBG1 - du_sendAmountActual=', du_sendAmountActual) // init changelly var xsCreateTx if (rateId === undefined) { xsCreateTx = await createTransactionApi({ // variable-rate api fromSymbol: exchangeAsset.symbol, toSymbol: receiveAsset.symbol, receiveAddress: receiveAsset.addresses[0].addr, refundAddress: exchangeAsset.addresses[addrNdx == -1 ? 0 : addrNdx].addr, amount: du_sendAmountActual }) } else { xsCreateTx = await createTransactionFixedApi({ // fixed-rate api fromSymbol: exchangeAsset.symbol, toSymbol: receiveAsset.symbol, receiveAddress: receiveAsset.addresses[0].addr, refundAddress: exchangeAsset.addresses[addrNdx == -1 ? 0 : addrNdx].addr, rateId: rateId, amount: du_sendAmountActual }) } if (xsCreateTx) { if (xsCreateTx.error) { console.error(`## XS - xsCreateTx - error=`, xsCreateTx.error) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 6', info: `xsCreateTx - ${xsCreateTx.error.message}` }}) reject() return } if (!xsCreateTx.result) { console.error(`## XS - xsCreateTx - error=`, xsCreateTx.error) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 10', info: `xsCreateTx - no result` }}) if (Sentry) { Sentry.captureMessage(`!xsCreateTx.result, xsCreateTx=${JSON.stringify(xsCreateTx)}`) } reject() return } // push tx: send to XS const payToActual = [{ receiver: xsCreateTx.result.payinAddress, value: amount }] createAndPushTx( { store: store, payTo: payToActual, wallet: wallet, asset: exchangeAsset, feeParams: feeParams, sendFromAddrNdx: addrNdx, apk, h_mpk, }, (res, err) => { if (err) { store.dispatch(setCurrentStatus({ from: exchangeAsset, status: ExchangeStatusEnum.done, owner })) // clear exchange status if (exchangeStatusTimer_intId !== undefined) { clearTimeout(exchangeStatusTimer_intId) } console.error(`## XS - XS_initiateExchange (${rateId ? 'FIXED' : 'VARIABLE'}) - createAndPushTx - err=`, err) utilsWallet.error(err) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 7', info: `createAndPushTx - ${err.message || err.toString()}` }}) reject() } else { if (!res) { store.dispatch(setCurrentStatus({ from: exchangeAsset, status: ExchangeStatusEnum.done, owner })) // clear exchange status if (exchangeStatusTimer_intId !== undefined) { clearTimeout(exchangeStatusTimer_intId) } utilsWallet.error(err) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 8', info: 'createAndPushTx - no data' }}) console.error(`## XS - XS_initiateExchange (${rateId ? 'FIXED' : 'VARIABLE'}) - createAndPushTx - no data`) reject() } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'info', headline: `${exchangeAsset.displaySymbol}: Broadcast TX`, info: `Node accepted`, desc1: `For exchange into ${receiveAsset.displayName}`, txid: res.tx.txid }}) // create exchange tx record var xsTx = {} xsTx[exchangeAsset.symbol] = { txid: res.tx.txid, sentAt: new Date().getTime(), xs: xsCreateTx.result, fromSymbol: exchangeAsset.symbol, toSymbol: receiveAsset.symbol, amountSent: amount, cur_estReceiveAmount: cur_estReceiveAmount, fixedRateId: rateId, cur_xsTxStatus: ExchangeStatusEnum.waiting, } store.dispatch({ type: XS_UPDATE_EXCHANGE_TX, payload: { data: xsTx, owner, //: utils.getBrowserStorage().owner } }) // poll for exchange status store.dispatch(pollExchangeStatus(store, exchangeAsset, xsTx[exchangeAsset.symbol], owner)) //console.log(`Exchange - XS_initiateExchange - createAndPushTx OK`, res) resolve() } } }) } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 9', info: 'xsCreateTx - no data' }}) console.error(`## XS - XS_initiateExchange (${rateId ? 'FIXED' : 'VARIABLE'}) - xsCreateTx - no data`) reject() } }) }, // get transaction(s) getTransaction: async (xsTx) => getTransaction(xsTx), // exchange status XS_pollExchangeStatus: (store, from, xsTx, owner) => pollExchangeStatus(store, from, xsTx, owner), //getExchangeStatus_SetTimer: (store, from, xsTx, owner) => getExchangeStatus_SetTimer(store, from, xsTx, owner), XS_setCurrentStatus: (p) => setCurrentStatus(p) } function setCurrentStatus(p) { const { from, status, owner } = p return (dispatch) => { if (status === ExchangeStatusEnum.done) { // exchange completed and acknowledged by user (could be successful or otherwise) // stop polling //getExchangeStatus_ClearTimer() // TODO (history) -- should push the xsTx to a history queue, or similar ... // remove exchange tx // var xsTx = {} // xsTx[from.symbol] = undefined // dispatch({ type: XS_UPDATE_EXCHANGE_TX, payload: { data: xsTx, owner: utils.getBrowserStorage().owner } }) } // update current exchange status var xsTx = {} xsTx[from.symbol] = { cur_xsTxStatus: status, } dispatch({ type: XS_UPDATE_EXCHANGE_TX, payload: { data: xsTx, owner, //: utils.getBrowserStorage().owner } }) } } async function getTransaction(xsTx) { return getTransactionsApi({ currency: xsTx.xs.currencyFrom, address: xsTx.xs.payinAddress }) .then(res => { if (res && res.result) { //console.log(`getTransactions - ${xsTx.xs.currencyFrom}, res=`, res) const txs = res.result.filter(p => p.id === xsTx.xs.id) if (txs.length != 1) { // it is possible that we may not get back the finalized xs tx data - getTransactionsApi() caps at the last 99 tx's // this can happen if we have executed many other XS tx's from the same fromCurrency since we last polled utilsWallet.reportErr(`getTransactions - ${xsTx.xs.currencyFrom} - failed to get id ${xsTx.xs.id}`) return null } //console.log(`getTransactions - ${xsTx.xs.currencyFrom} - txid ${xsTx.xs.id} [0]=`, txs[0]) return txs[0] } }) } function pollExchangeStatus(store, from, xsTx, owner) { return (dispatch) => { //getExchangeStatus_ClearTimer() //console.log(`XS - pollExchangeStatus - [XS ID ${xsTx.xs.id}]<==${from.symbol}...`)//, xsTx) getStatusApi(xsTx.xs.id) .then(res => { var continuePolling = true if (res) { if (res.error) { console.error('XS - getStatusApi, error=', res.error) return } var status = res.result //console.log(`XS - pollExchangeStatus - ${from.symbol}==>[XS ID ${xsTx.xs.id}], status=`, status) if (status === 'sending') { status = ExchangeStatusEnum.receiving } // get current store state for this xs tx var storeState = store.getState() if (storeState.userData.exchange && storeState.userData.exchange.cur_xsTx) { const store_cur_xsTx = storeState.userData.exchange.cur_xsTx[from.symbol] if (store_cur_xsTx) { // has tx status changed? update store if so if (store_cur_xsTx.cur_xsTxStatus !== status) { dispatch(setCurrentStatus({ from, status, owner })) } // is tx concluded? mark it finalized if so if (status === 'finished' || status === 'failed' || status === 'refunded' || status === 'overdue' || status === 'hold') { continuePolling = false storeState = store.getState() // refresh store state // update store & notify -- only on actual state change if (storeState.userData.exchange && storeState.userData.exchange.cur_xsTx) { const xsTx = storeState.userData.exchange.cur_xsTx[from.symbol] if (xsTx && xsTx.cur_xsTxStatus !== status) { // state changed? // update the XS tx data with finalized data getTransaction(xsTx) .then((tx) => { if (storeState.userData.exchange && storeState.userData.exchange.cur_xsTx) { const updated = _.cloneDeep(storeState.userData.exchange.cur_xsTx) if (updated[from.symbol]) { // edge-case: it is possible that we might fail to get the finalized tx data from changelly // in this case, don't overwrite what we do have if (tx) { updated[from.symbol].xs = tx } updated[from.symbol].cur_xsTxStatus = status dispatch({ type: XS_UPDATE_EXCHANGE_TX, payload: { data: updated, owner }}) } } }) // notify user - XS concluded utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: status === 'finished' ? 'success' : 'error', headline: `${from.displaySymbol}: Exchange Completed`, info: `Status: ${status.toUpperCase()} (Changelly)`, desc1: `xsTx.xs.id: ${xsTx.xs.id}`, //`For exchange into ${receiveAsset.displayName}`, txid: undefined }}) } } } } } else { continuePolling = true } } else { console.error('XS - getStatusApi no data') } if (continuePolling) { dispatch(getExchangeStatus_SetTimer(store, from, xsTx, owner)) } }) } } function getExchangeStatus_SetTimer(store, from, xsTx, owner) { return (dispatch) => { //console.log(`XS - getExchangeStatus_SetTimer[${xsTx.xs.id}]`)//, xsTx) exchangeStatusTimer_intId[xsTx.xs.id] = setTimeout(() => { dispatch(pollExchangeStatus(store, from, xsTx, owner)) }, configWallet.IS_DEV ? 10000 : 10000) } } function roundUp(decimals, value) { if (!decimals) { return value } const rounded = new BigDecimal(value).round(decimals, BigDecimal.RoundingModes.UP).getValue() return rounded } function getEstReceiveAmount_ClearTimer() { if (receiveAssetValue_intId !== undefined) { clearInterval(receiveAssetValue_intId) } } async function getEstReceiveAmount(store, fromSymbol, toSymbol, amount) { // clear first - prevents stale values showing store.dispatch({ type: XS_SET_FIXED_RECEIVE_AMOUNT, payload: { derivedExpected: undefined, rateId: undefined } }) if (amount == 0 || !fromSymbol || !toSymbol) return const storeState = store.getState() if (!storeState || !storeState.userData || !storeState.userData.exchange || !storeState.userData.exchange.currencies) { console.error('XS - getMinAmount - invalid store state') return null } const fromSymbolLookup = toXsSymbol(fromSymbol) const toSymbolLookup = toXsSymbol(toSymbol) const xsCcyFrom = storeState.userData.exchange.currencies.find((p) => { return p.name === fromSymbolLookup.toLowerCase() }) const xsCcyTo = storeState.userData.exchange.currencies.find((p) => { return p.name === toSymbolLookup.toLowerCase() }) //console.log(`getEstReceiveAmount - xsCcyFrom=`, xsCcyFrom) //console.log(`getEstReceiveAmount - xsCcyTo=`, xsCcyTo) if (!xsCcyFrom.fixRateEnabled || !xsCcyTo.fixRateEnabled) { // variable-rate api const res = await getEstReceiveAmountApi({ fromSymbol, toSymbol, amount }) if (res) { //console.log(`XS - getEstReceiveAmount (VARIABLE) - ${amount} ${fromSymbol}==>${toSymbol}, res=`, res) store.dispatch({ type: XS_SET_EST_RECEIVE_AMOUNT, payload: { result: res.result * configWallet.XS_CHANGELLY_VARRATE_MARKDOWN } }) } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 4', info: `getEstReceiveAmountApi - no data` }}) console.error('XS - getEstReceiveAmount - getEstReceiveAmountApi - no data') return null } } else { // fixed-rate api -- calc. expected amount, keep track of the rateId // (v1 - deprecated Dec 1st 2019) if (USE_CHANGELLY_FIXEDRATE_V2 == false) { const res = await getFixRateApi({ fromSymbol, toSymbol }) if (res) { //console.log('getEstReceiveAmount - v1(dep) - fixed getFixRate', res); const rateId = res.id const fixedRate = res.result const derivedExpected = fixedRate * amount //console.log(`XS - getEstReceiveAmount (FIXED) - ${amount} ${fromSymbol}==>${toSymbol}, rateId=${rateId} derivedExpected=${derivedExpected}, fixedRes=`, res) store.dispatch({ type: XS_SET_FIXED_RECEIVE_AMOUNT, payload: { derivedExpected, rateId } }) } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 5', info: `getFixRateApi - no data` }}) console.error('XS - getEstReceiveAmount - getFixRateApi - no data') return null } } else { // v2 - getFixRateForAmount // ### -- createTransactionFixedApi() returning "invalid currency pair" when using this rateId... const res = await getFixRateForAmountApi({ fromSymbol, toSymbol, amountFrom: amount }) if (res) { const rateId = res.id const fixedRate = res.result const derivedExpected = fixedRate * amount //console.log(`XS - getEstReceiveAmount (FIXED) - ${amount} ${fromSymbol}==>${toSymbol}, rateId=${rateId} derivedExpected=${derivedExpected}, fixedRes=`, res) store.dispatch({ type: XS_SET_FIXED_RECEIVE_AMOUNT, payload: { derivedExpected, rateId } }) } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 5', info: `getFixRateApi - no data` }}) console.error('XS - getEstReceiveAmount - getFixRateApi - no data') return null } } } } async function getMinAmount(store, fromSymbol, toSymbol) { if (!fromSymbol || !toSymbol) { console.error('XS - getMinAmount - invalid params') return null } if (fromSymbol === toSymbol) { return null } const storeState = store.getState() if (!storeState || !storeState.userData || !storeState.userData.exchange || !storeState.userData.exchange.currencies) { console.error('XS - getMinAmount - invalid store state') return null } const fromSymbolXs = toXsSymbol(fromSymbol) const toSymbolXs = toXsSymbol(toSymbol) const xsCcyFrom = storeState.userData.exchange.currencies.find((p) => p.name === fromSymbolXs.toLowerCase()) const xsCcyTo = storeState.userData.exchange.currencies.find((p) => p.name === toSymbolXs.toLowerCase()) if (xsCcyFrom === undefined || xsCcyTo === undefined) { console.warn('XS - getMinAmount - xsCcyFrom||xsCcyFrom undefined in XS currency list - nop') return null } const metaFrom = configWallet.getMetaBySymbol(fromSymbol) if (!xsCcyFrom.fixRateEnabled || !xsCcyTo.fixRateEnabled) { // variable-rate api const res = await getMinAmountApi({ fromSymbol, toSymbol }) if (res && res.result) { const rounded = roundUp( metaFrom.decimals, (Number(res.result) * 1.1).toFixed(4) // hack: observed - mimatch on minimum reported vs. accepted, from changelly ) store.dispatch({ type: XS_SET_MINMAX_AMOUNT, payload: { min: rounded, max: undefined // TODO: see below -- should be calling getPairsParams() and reading the float fields? }}) //console.log('rounded', rounded.toString()) return rounded } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 2', info: `getMinAmountApi - no data` }}) console.error('XS - getMinAmount - getMinAmountApi - no data') return null } } else { // fixed-rate api (v1 - deprecated Dec 1st 2019) if (USE_CHANGELLY_FIXEDRATE_V2 == false) { const res = await getFixRateApi({ fromSymbol, toSymbol }) if (res) { //console.log('getMinAmount - v1(dep) - fixed getFixRate', res); const rounded = roundUp(metaFrom.decimals, (Number(res.min) * 1.1).toFixed(4) ) store.dispatch({ type: XS_SET_MINMAX_AMOUNT, payload: { min: rounded, max: res.max }}) //console.log('rounded', rounded.toString()) return rounded } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 3', info: `getFixRateApi - no data` }}) console.error('XS - getMinAmount - getFixRateApi - no data') return null } } else { // new v2 - getPairsParams const res = await getPairsParamsApi({ fromSymbol, toSymbol }) // TODO: this API's float (variable) fields also replace getMinAmountApi()? if (res) { const roundedMin = roundUp(metaFrom.decimals, Number(res.minAmountFixed) * 1.1) store.dispatch({ type: XS_SET_MINMAX_AMOUNT, payload: { min: roundedMin, max: res.maxAmountFixed // TODO: is max amount used anywhere?! some are certainly capped by Changelly on fixed, e.g. TUSD max is ~ $8k... }}) return roundedMin } else { utilsWallet.getAppWorker().postMessageWrapped({ msg: 'NOTIFY_USER', data: { type: 'error', headline: 'Exchange Error 3', info: `getPairsParamsApi - no data` }}) console.error('XS - getMinAmount - v2 getPairsParamsApi - no data') return null } } } }