import {autorun, runInAction, makeAutoObservable} from 'mobx' import {Asset, Memo} from 'stellar-sdk' import {debounce} from 'throttle-debounce' import BigNumber from 'bignumber.js' import {AssetDescriptor} from '@stellar-expert/asset-descriptor' import {streamLedgers} from '../../../util/ledger-stream' import {createHorizon} from '../../../util/horizon-connector' import {encodeMemo} from '../../../util/memo' import accountLedgerData from '../../../state/ledger-data/account-ledger-data' import {prepareTransferTx} from './transfer-tx-builder' export default class TransferSettings { constructor(network, mode = 'direct') { this.network = network this.mode = mode this.asset = ['XLM', 'XLM'] this.amount = ['0', '0'] this.conversionSlippage = 0.5 makeAutoObservable(this) autorun(() => { const { destination, mode, asset, amount, conversionDirection, conversionSlippage, currentLedgerSequence } = this this.recalculateSwap() }) this.findConversionPath = debounce(400, false, this.findConversionPath.bind(this)) } network /** * @type {TransferMode} */ mode = 'direct' /** * @type {String} */ destination /** * @type {String} */ destinationFederationAddress /** * @type {Boolean} */ createDestination = false /** * @type {Memo} */ memo = null /** * @type {Boolean} */ invalidMemo = false /** * @type {[String]} */ asset /** * @type {[String]} */ amount /** * @type {Boolean} */ createTrustline /** * @type {'source'|'dest'} */ conversionDirection /** * @type {Number} */ conversionSlippage /** * @type {Array<Asset>} */ conversionPath /** * @type {String} */ conversionPrice /** * @type {Boolean} */ conversionFeasible /** * @type {Boolean} */ conversionPathLoaded = false /** * @type {Number} */ currentLedgerSequence /** * @type {String} */ get source() { return accountLedgerData.address } get isSelfPayment() { return this.source === this.destination } get hasSufficientBalance() { return new BigNumber(accountLedgerData.getAvailableBalance(this.asset[0])) .greaterThanOrEqualTo(this.amount[0] || 0) } /** * Set transfer mode * @param {TransferMode} mode */ setMode(mode) { this.mode = mode if (mode === 'claimable') { this.createDestination = false this.createTrustline = false } this.asset[1] = this.asset[0] this.conversionDirection = 'source' } /** * Set transfer destination * @param {String} address * @param {Object} federationInfo */ setDestination(address, federationInfo = null) { this.destination = address || null this.createDestination = false this.createTrustline = false this.destinationFederationAddress = null this.memo = null this.invalidMemo = false if (federationInfo) { this.destinationFederationAddress = federationInfo.link this.memo = encodeMemo(federationInfo) this.invalidMemo = false } } /** * Set transfer tokens amount * @param {String} amount * @param {Number} index */ setAmount(amount, index) { this.conversionDirection = index === 0 ? 'source' : 'dest' this.conversionPathLoaded = false if (this.asset[0] === this.asset[1]) { this.amount = [amount, amount] } else { this.amount[index] = amount } } /** * Maximum allowed price slippage * @param {Number} slippage */ setSlippage(slippage) { this.conversionPathLoaded = false this.conversionSlippage = slippage } /** * Set asset transfer * @param {String|AssetDescriptor} asset * @param {Number} index */ setAsset(asset, index) { this.conversionPathLoaded = false this.createTrustline = false if (this.mode !== 'convert') { this.asset = [asset, asset] this.createDestination = false this.amount[1] = this.amount[0] } else { this.asset[index] = asset if (this.asset[0] === this.asset[1]) { this.amount[1] = this.amount[0] } } } /** * Reverse swap direction */ reverse() { if (this.conversionDirection === 'source') { this.amount = ['0', this.amount[0]] this.conversionDirection = 'dest' } else { this.amount = [this.amount[1], '0'] this.conversionDirection = 'source' } this.asset = this.asset.slice().reverse() this.createTrustline = false this.conversionPathLoaded = false this.createDestination = false } /** * Estimate swap price and amount */ recalculateSwap() { if (this.conversionPathLoaded || this.mode !== 'convert') return this.conversionPath = undefined this.conversionPrice = undefined if (this.asset[0] === this.asset[1]) { this.conversionFeasible = true this.conversionPathLoaded = true return } this.conversionFeasible = false const target = this.conversionDirection === 'source' ? 1 : 0 this.amount[target] = '' if (!this.amount[0] && !this.amount[1] || !this.conversionDirection) return this.findConversionPath() } /** * Find path payment path */ findConversionPath() { const horizon = createHorizon(this.network) let endpoint if (this.conversionDirection === 'source') { if (!parseFloat(this.amount[0])) return endpoint = horizon.strictSendPaths(AssetDescriptor.parse(this.asset[0]).toAsset(), this.amount[0], [AssetDescriptor.parse(this.asset[1]).toAsset()]) } else { if (!parseFloat(this.amount[1])) return endpoint = horizon.strictReceivePaths([AssetDescriptor.parse(this.asset[0]).toAsset()], AssetDescriptor.parse(this.asset[1]).toAsset(), this.amount[1]) } return endpoint.call() .then(({records}) => { if (records.length) { const [result] = records runInAction(() => { if (this.conversionDirection === 'source') { this.amount[1] = adjustWithSlippage(result.destination_amount, -1, this.conversionSlippage) } else { this.amount[0] = adjustWithSlippage(result.source_amount, 1, this.conversionSlippage) } this.conversionPrice = result.destination_amount / result.source_amount this.conversionPath = (result.path || []).map(a => a.asset_type === 'native' ? Asset.native() : new Asset(a.asset_code, a.asset_issuer)) this.conversionPathLoaded = true this.conversionFeasible = true }) } else { runInAction(() => { this.conversionPathLoaded = true }) } }) .catch(e => { this.conversionPathLoaded = true console.error(e) }) } /** * Trigger price and amount estimates when new ledger arrives */ startLedgerStreaming() { this.stopLedgerStreaming = streamLedgers({ network: this.network, onNewLedger: ({sequence}) => { this.currentLedgerSequence = sequence } }) } /** * Set amounts to zero */ resetOperationAmount() { this.createDestination = false this.amount = ['0', '0'] this.setAmount('0', 0) } /** * Build transfer transaction * @return {Promise<Transaction>} */ prepareTransaction() { return prepareTransferTx(this) } } /** * Adjust converted amount with regard to maximum slippage amount * @param {String} value * @param {Number} direction * @param {Number} slippage * @return {String} */ function adjustWithSlippage(value, direction, slippage) { return new BigNumber(value) .times((1 + direction * slippage / 100).toPrecision(15)) .round(7, direction < 0 ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP) .toString() } /** * @typedef {'direct'|'convert'|'claimable'} TransferMode */