UNPKG

ripplelib

Version:

A JavaScript API for interacting with Ripple in Node.js and the browser

416 lines (323 loc) 14.1 kB
'use strict'; var _ = require('lodash'); var assert = require('assert'); var Amount = require('./amount').Amount; var Utils = require('./orderbookutils'); function assertValidNumber(number, message) { assert(!_.isNull(number) && !isNaN(number), message); } function assertValidLegOneOffer(legOneOffer, message) { assert(legOneOffer); assert.strictEqual(typeof legOneOffer, 'object', message); assert.strictEqual(typeof legOneOffer.TakerPays, 'object', message); assertValidNumber(legOneOffer.TakerGets, message); } function AutobridgeCalculator(currencyGets, currencyPays, legOneOffers, legTwoOffers, issuerGets, issuerPays) { this._currencyGets = currencyGets; this._currencyGetsHex = currencyGets.to_hex(); this._currencyPaysHex = currencyPays.to_hex(); this._issuerGets = issuerGets; this._issuerPays = issuerPays; this.legOneOffers = _.cloneDeep(legOneOffers); this.legTwoOffers = _.cloneDeep(legTwoOffers); this._ownerFundsLeftover = {}; } AutobridgeCalculator.NULL_AMOUNT = Utils.normalizeAmount('0'); /** * Calculates an ordered array of autobridged offers by quality * * @return {Array} */ AutobridgeCalculator.prototype.calculate = function (callback) { var legOnePointer = 0; var legTwoPointer = 0; var offersAutobridged = []; this.clearOwnerFundsLeftover(); this._calculateInternal(legOnePointer, legTwoPointer, offersAutobridged, callback); }; AutobridgeCalculator.prototype._calculateInternal = function (legOnePointer_, legTwoPointer_, offersAutobridged, callback) { var _this = this; var legOnePointer = legOnePointer_; var legTwoPointer = legTwoPointer_; var startTime = Date.now(); while (this.legOneOffers[legOnePointer] && this.legTwoOffers[legTwoPointer]) { // manually implement cooperative multitasking that yields after 30ms // of execution so user's browser stays responsive var lasted = Date.now() - startTime; if (lasted > 30) { setTimeout(function () { _this._calculateInternal(legOnePointer, legTwoPointer, offersAutobridged, callback); }, 0); return; } var legOneOffer = this.legOneOffers[legOnePointer]; var legTwoOffer = this.legTwoOffers[legTwoPointer]; var leftoverFunds = this.getLeftoverOwnerFunds(legOneOffer.Account); var autobridgedOffer = undefined; if (legOneOffer.Account === legTwoOffer.Account) { this.unclampLegOneOwnerFunds(legOneOffer); } else if (!legOneOffer.is_fully_funded && !leftoverFunds.is_zero()) { this.adjustLegOneFundedAmount(legOneOffer); } var legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer); var legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer); if (legOneTakerGetsFunded.is_zero()) { legOnePointer++; continue; } if (legTwoTakerPaysFunded.is_zero()) { legTwoPointer++; continue; } // using private fields for speed if (legOneTakerGetsFunded._value.comparedTo(legTwoTakerPaysFunded._value) > 0) { autobridgedOffer = this.getAutobridgedOfferWithClampedLegOne(legOneOffer, legTwoOffer); legTwoPointer++; } else if (legTwoTakerPaysFunded._value.comparedTo(legOneTakerGetsFunded._value) > 0) { autobridgedOffer = this.getAutobridgedOfferWithClampedLegTwo(legOneOffer, legTwoOffer); legOnePointer++; } else { autobridgedOffer = this.getAutobridgedOfferWithoutClamps(legOneOffer, legTwoOffer); legOnePointer++; legTwoPointer++; } offersAutobridged.push(autobridgedOffer); } callback(offersAutobridged); }; /** * In this case, the output from leg one is greater than the input to leg two. * Therefore, we must effectively clamp leg one output to leg two input. * * @param {Object} legOneOffer * @param {Object} legTwoOffer * * @return {Object} */ AutobridgeCalculator.prototype.getAutobridgedOfferWithClampedLegOne = function (legOneOffer, legTwoOffer) { var legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer); var legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer); var legOneQuality = Utils.getOfferQuality(legOneOffer, this._currencyGets); var autobridgedTakerGets = Utils.getOfferTakerGetsFunded(legTwoOffer); var autobridgedTakerPays = legTwoTakerPaysFunded.multiply(legOneQuality); if (legOneOffer.Account === legTwoOffer.Account) { var legOneTakerGets = Utils.getOfferTakerGets(legOneOffer); var updatedTakerGets = legOneTakerGets.subtract(legTwoTakerPaysFunded); this.setLegOneTakerGets(legOneOffer, updatedTakerGets); this.clampLegOneOwnerFunds(legOneOffer); } else { // Update funded amount since leg one offer was not completely consumed var updatedTakerGetsFunded = legOneTakerGetsFunded.subtract(legTwoTakerPaysFunded); this.setLegOneTakerGetsFunded(legOneOffer, updatedTakerGetsFunded); } return this.formatAutobridgedOffer(autobridgedTakerGets, autobridgedTakerPays); }; /** * In this case, the input from leg two is greater than the output to leg one. * Therefore, we must effectively clamp leg two input to leg one output. * * @param {Object} legOneOffer * @param {Object} legTwoOffer * * @return {Object} */ AutobridgeCalculator.prototype.getAutobridgedOfferWithClampedLegTwo = function (legOneOffer, legTwoOffer) { var legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer); var legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer); var legTwoQuality = Utils.getOfferQuality(legTwoOffer, this._currencyGets); var autobridgedTakerGets = legOneTakerGetsFunded.divide(legTwoQuality); var autobridgedTakerPays = Utils.getOfferTakerPaysFunded(legOneOffer); // Update funded amount since leg two offer was not completely consumed legTwoOffer.taker_gets_funded = Utils.getOfferTakerGetsFunded(legTwoOffer).subtract(autobridgedTakerGets).to_text(); legTwoOffer.taker_pays_funded = legTwoTakerPaysFunded.subtract(legOneTakerGetsFunded).to_text(); return this.formatAutobridgedOffer(autobridgedTakerGets, autobridgedTakerPays); }; /** * In this case, the output from leg one and the input to leg two are the same. * We do not need to clamp either. * @param {Object} legOneOffer * @param {Object} legTwoOffer * * @return {Object} */ AutobridgeCalculator.prototype.getAutobridgedOfferWithoutClamps = function (legOneOffer, legTwoOffer) { var autobridgedTakerGets = Utils.getOfferTakerGetsFunded(legTwoOffer); var autobridgedTakerPays = Utils.getOfferTakerPaysFunded(legOneOffer); return this.formatAutobridgedOffer(autobridgedTakerGets, autobridgedTakerPays); }; /** * Clear owner funds leftovers */ AutobridgeCalculator.prototype.clearOwnerFundsLeftover = function () { this._ownerFundsLeftover = {}; }; /** * Reset owner funds leftovers for an account to 0 * * @param {String} account * * @return {Amount} */ AutobridgeCalculator.prototype.resetOwnerFundsLeftover = function (account) { this._ownerFundsLeftover[account] = Utils.normalizeAmount('0'); return this._ownerFundsLeftover[account]; }; /** * Retrieve leftover funds found after clamping leg one by account * * @param {String} account * * @return {Amount} */ AutobridgeCalculator.prototype.getLeftoverOwnerFunds = function (account) { var amount = this._ownerFundsLeftover[account]; if (!amount) { amount = AutobridgeCalculator.NULL_AMOUNT.clone(); } return amount; }; /** * Add funds to account's leftover funds * * @param {String} account * @param {Amount} amount * * @return {Amount} */ AutobridgeCalculator.prototype.addLeftoverOwnerFunds = function (account, amount) { assert(amount instanceof Amount, 'Amount is invalid'); this._ownerFundsLeftover[account] = this.getLeftoverOwnerFunds(account).add(amount); return this._ownerFundsLeftover[account]; }; /** * Set account's leftover funds * * @param {String} account * @param {Amount} amount */ AutobridgeCalculator.prototype.setLeftoverOwnerFunds = function (account, amount) { assert(amount instanceof Amount, 'Amount is invalid'); this._ownerFundsLeftover[account] = amount; }; /** * Format an autobridged offer and compute synthetic values (e.g. quality) * * @param {Amount} takerGets * @param {Amount} takerPays * * @return {Object} */ AutobridgeCalculator.prototype.formatAutobridgedOffer = function (takerGets, takerPays) { assert(takerGets instanceof Amount, 'Autobridged taker gets is invalid'); assert(takerPays instanceof Amount, 'Autobridged taker pays is invalid'); var autobridgedOffer = {}; var quality = takerPays.divide(takerGets); autobridgedOffer.TakerGets = { value: takerGets.to_text(), currency: this._currencyGetsHex, issuer: this._issuerGets }; autobridgedOffer.TakerPays = { value: takerPays.to_text(), currency: this._currencyPaysHex, issuer: this._issuerPays }; autobridgedOffer.quality = quality.to_text(); autobridgedOffer.taker_gets_funded = autobridgedOffer.TakerGets.value; autobridgedOffer.taker_pays_funded = autobridgedOffer.TakerPays.value; autobridgedOffer.autobridged = true; autobridgedOffer.BookDirectory = Utils.convertOfferQualityToHexFromText(autobridgedOffer.quality); autobridgedOffer.qualityHex = autobridgedOffer.BookDirectory; return autobridgedOffer; }; /** * Remove funds clamp on leg one offer. This is necessary when the two offers * are owned by the same account. In this case, it doesn't matter if offer one * is not fully funded. Leg one out goes to leg two in and since its the same * account, an infinite amount can flow. * * @param {Object} legOneOffer - IOU:XRP offer */ AutobridgeCalculator.prototype.unclampLegOneOwnerFunds = function (legOneOffer) { assertValidLegOneOffer(legOneOffer, 'Leg one offer is invalid'); legOneOffer.initTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer); this.setLegOneTakerGetsFunded(legOneOffer, Utils.getOfferTakerGets(legOneOffer)); }; /** * Apply clamp back on leg one offer after a round of autobridge calculation * completes. We must reapply clamps that have been removed because we cannot * guarantee that the next offer from leg two will also be from the same * account. * * When we reapply, it could happen that the amount of TakerGets left after * the autobridge calculation is less than the original funded amount. In this * case, we have extra funds we can use towards unfunded offers with worse * quality by the same owner. * * @param {Object} legOneOffer - IOU:XRP offer */ AutobridgeCalculator.prototype.clampLegOneOwnerFunds = function (legOneOffer) { assertValidLegOneOffer(legOneOffer, 'Leg one offer is invalid'); var takerGets = Utils.getOfferTakerGets(legOneOffer); if (takerGets.compareTo(legOneOffer.initTakerGetsFunded) > 0) { // After clamping, TakerGets is still greater than initial funded amount this.setLegOneTakerGetsFunded(legOneOffer, legOneOffer.initTakerGetsFunded); } else { var updatedLeftover = legOneOffer.initTakerGetsFunded.subtract(takerGets); this.setLegOneTakerGetsFunded(legOneOffer, takerGets); this.addLeftoverOwnerFunds(legOneOffer.Account, updatedLeftover); } }; /** * Increase leg one offer funded amount with extra funds found after applying * clamp. * * @param {Object} legOneOffer - IOU:XRP offer */ AutobridgeCalculator.prototype.adjustLegOneFundedAmount = function (legOneOffer) { assertValidLegOneOffer(legOneOffer, 'Leg one offer is invalid'); assert(!legOneOffer.is_fully_funded, 'Leg one offer cannot be fully funded'); var fundedSum = Utils.getOfferTakerGetsFunded(legOneOffer).add(this.getLeftoverOwnerFunds(legOneOffer.Account)); if (fundedSum.compareTo(Utils.getOfferTakerGets(legOneOffer)) >= 0) { // There are enough extra funds to fully fund the offer var legOneTakerGets = Utils.getOfferTakerGets(legOneOffer); var updatedLeftover = fundedSum.subtract(legOneTakerGets); this.setLegOneTakerGetsFunded(legOneOffer, legOneTakerGets); this.setLeftoverOwnerFunds(legOneOffer.Account, updatedLeftover); } else { // There are not enough extra funds to fully fund the offer this.setLegOneTakerGetsFunded(legOneOffer, fundedSum); this.resetOwnerFundsLeftover(legOneOffer.Account); } }; /** * Set taker gets funded amount for a IOU:XRP offer. Also calculates taker * pays funded using offer quality and updates is_fully_funded flag * * @param {Object} legOneOffer - IOU:XRP offer * @param {Amount} takerGetsFunded */ AutobridgeCalculator.prototype.setLegOneTakerGetsFunded = function setLegOneTakerGetsFunded(legOneOffer, takerGetsFunded) { assertValidLegOneOffer(legOneOffer, 'Leg one offer is invalid'); assert(takerGetsFunded instanceof Amount, 'Taker gets funded is invalid'); legOneOffer.taker_gets_funded = takerGetsFunded.to_text(); legOneOffer.taker_pays_funded = takerGetsFunded.multiply(Utils.getOfferQuality(legOneOffer, this._currencyGets)).to_text(); if (legOneOffer.taker_gets_funded === legOneOffer.TakerGets.value) { legOneOffer.is_fully_funded = true; } }; /** * Set taker gets amount for a IOU:XRP offer. Also calculates taker pays * using offer quality * * @param {Object} legOneOffer - IOU:XRP offer * @param {Amount} takerGets */ AutobridgeCalculator.prototype.setLegOneTakerGets = function (legOneOffer, takerGets) { assertValidLegOneOffer(legOneOffer, 'Leg one offer is invalid'); assert(takerGets instanceof Amount, 'Taker gets funded is invalid'); var legOneQuality = Utils.getOfferQuality(legOneOffer, this._currencyGets); legOneOffer.TakerGets = takerGets.to_text(); legOneOffer.TakerPays = takerGets.multiply(legOneQuality).to_json(); }; module.exports = AutobridgeCalculator;