mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-19 19:55:51 +00:00
438 lines
13 KiB
JavaScript
438 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const assert = require('assert');
|
|
const UInt160 = require('./uint160').UInt160;
|
|
const Amount = require('./amount').Amount;
|
|
const 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._currencyPays = currencyPays;
|
|
this._issuerGets = issuerGets;
|
|
this._issuerPays = issuerPays;
|
|
this.legOneOffers = _.cloneDeep(legOneOffers);
|
|
this.legTwoOffers = _.cloneDeep(legTwoOffers);
|
|
|
|
this._ownerFundsLeftover = {};
|
|
}
|
|
|
|
/**
|
|
* Calculates an ordered array of autobridged offers by quality
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
|
|
AutobridgeCalculator.prototype.calculate = function() {
|
|
let legOnePointer = 0;
|
|
let legTwoPointer = 0;
|
|
|
|
let offersAutobridged = [];
|
|
|
|
this.clearOwnerFundsLeftover();
|
|
|
|
while (this.legOneOffers[legOnePointer] && this.legTwoOffers[legTwoPointer]) {
|
|
const legOneOffer = this.legOneOffers[legOnePointer];
|
|
const legTwoOffer = this.legTwoOffers[legTwoPointer];
|
|
const leftoverFunds = this.getLeftoverOwnerFunds(legOneOffer.Account);
|
|
let autobridgedOffer;
|
|
|
|
if (legOneOffer.Account === legTwoOffer.Account) {
|
|
this.unclampLegOneOwnerFunds(legOneOffer);
|
|
} else if (!legOneOffer.is_fully_funded && !leftoverFunds.is_zero()) {
|
|
this.adjustLegOneFundedAmount(legOneOffer);
|
|
}
|
|
|
|
const legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer);
|
|
const legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer);
|
|
|
|
if (legOneTakerGetsFunded.is_zero()) {
|
|
legOnePointer++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (legTwoTakerPaysFunded.is_zero()) {
|
|
legTwoPointer++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (legOneTakerGetsFunded.compareTo(legTwoTakerPaysFunded) > 0) {
|
|
autobridgedOffer = this.getAutobridgedOfferWithClampedLegOne(
|
|
legOneOffer,
|
|
legTwoOffer
|
|
);
|
|
|
|
legTwoPointer++;
|
|
} else if (legTwoTakerPaysFunded.compareTo(legOneTakerGetsFunded) > 0) {
|
|
autobridgedOffer = this.getAutobridgedOfferWithClampedLegTwo(
|
|
legOneOffer,
|
|
legTwoOffer
|
|
);
|
|
|
|
legOnePointer++;
|
|
} else {
|
|
autobridgedOffer = this.getAutobridgedOfferWithoutClamps(
|
|
legOneOffer,
|
|
legTwoOffer
|
|
);
|
|
|
|
legOnePointer++;
|
|
legTwoPointer++;
|
|
}
|
|
|
|
offersAutobridged.push(autobridgedOffer);
|
|
}
|
|
|
|
return 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) {
|
|
const legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer);
|
|
const legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer);
|
|
const legOneQuality = Utils.getOfferQuality(legOneOffer, this._currencyGets);
|
|
|
|
const autobridgedTakerGets = Utils.getOfferTakerGetsFunded(legTwoOffer);
|
|
const autobridgedTakerPays = legTwoTakerPaysFunded.multiply(legOneQuality);
|
|
|
|
if (legOneOffer.Account === legTwoOffer.Account) {
|
|
const legOneTakerGets = Utils.getOfferTakerGets(legOneOffer);
|
|
const updatedTakerGets = legOneTakerGets.subtract(legTwoTakerPaysFunded);
|
|
|
|
this.setLegOneTakerGets(legOneOffer, updatedTakerGets);
|
|
|
|
this.clampLegOneOwnerFunds(legOneOffer);
|
|
} else {
|
|
// Update funded amount since leg one offer was not completely consumed
|
|
const 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) {
|
|
const legOneTakerGetsFunded = Utils.getOfferTakerGetsFunded(legOneOffer);
|
|
const legTwoTakerPaysFunded = Utils.getOfferTakerPaysFunded(legTwoOffer);
|
|
const legTwoQuality = Utils.getOfferQuality(legTwoOffer, this._currencyGets);
|
|
|
|
const autobridgedTakerGets = legOneTakerGetsFunded.divide(legTwoQuality);
|
|
const 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) {
|
|
const autobridgedTakerGets = Utils.getOfferTakerGetsFunded(legTwoOffer);
|
|
const 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) {
|
|
assert(UInt160.is_valid(account), 'Account is invalid');
|
|
|
|
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) {
|
|
assert(UInt160.is_valid(account), 'Account is invalid');
|
|
|
|
let amount = this._ownerFundsLeftover[account];
|
|
|
|
if (!amount) {
|
|
amount = Utils.normalizeAmount('0');
|
|
}
|
|
|
|
return amount;
|
|
};
|
|
|
|
/**
|
|
* Add funds to account's leftover funds
|
|
*
|
|
* @param {String} account
|
|
* @param {Amount} amount
|
|
*
|
|
* @return {Amount}
|
|
*/
|
|
|
|
AutobridgeCalculator.prototype.addLeftoverOwnerFunds =
|
|
function(account, amount) {
|
|
assert(UInt160.is_valid(account), 'Account is invalid');
|
|
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(UInt160.is_valid(account), 'Account is invalid');
|
|
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');
|
|
|
|
const autobridgedOffer = {};
|
|
const quality = takerPays.divide(takerGets);
|
|
|
|
autobridgedOffer.TakerGets = {
|
|
value: takerGets.to_text(),
|
|
currency: this._currencyGets.to_hex(),
|
|
issuer: this._issuerGets
|
|
};
|
|
|
|
autobridgedOffer.TakerPays = {
|
|
value: takerPays.to_text(),
|
|
currency: this._currencyPays.to_hex(),
|
|
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.convertOfferQualityToHex(quality);
|
|
|
|
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');
|
|
|
|
const 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 {
|
|
const 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');
|
|
|
|
const 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
|
|
const legOneTakerGets = Utils.getOfferTakerGets(legOneOffer);
|
|
const 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');
|
|
|
|
const legOneQuality = Utils.getOfferQuality(legOneOffer, this._currencyGets);
|
|
|
|
legOneOffer.TakerGets = takerGets.to_text();
|
|
legOneOffer.TakerPays = takerGets.multiply(legOneQuality);
|
|
};
|
|
|
|
module.exports = AutobridgeCalculator;
|