UNPKG

newpay-wallet-js

Version:

734 lines (622 loc) 23.1 kB
import {Fraction} from "fractional"; const GRAPHENE_100_PERCENT = 10000; function limitByPrecision(value, p = 8) { if (typeof p !== "number") throw new Error("Input must be a number"); let valueString = value.toString(); let splitString = valueString.split("."); if (splitString.length === 1 || splitString.length === 2 && splitString[1].length <= p) { return parseFloat(valueString); } else { return parseFloat(splitString[0] + "." + splitString[1].substr(0, p)); } } function precisionToRatio(p) { if (typeof p !== "number") throw new Error("Input must be a number"); return Math.pow(10, p); } function didOrdersChange(newOrders, oldOrders) { let changed = oldOrders && (oldOrders.size !== newOrders.size); if (changed) return changed; newOrders.forEach((a, key) => { let oldOrder = oldOrders.get(key); if (!oldOrder) { changed = true; } else { if (a.market_base === oldOrder.market_base) { changed = changed || a.ne(oldOrder); } } }); return changed; } class Asset { constructor({asset_id = "1.3.0", amount = 0, precision = 5, real = null} = {}) { this.satoshi = precisionToRatio(precision); this.asset_id = asset_id; this.setAmount({sats: amount, real}); this.precision = precision; } hasAmount() { return this.amount > 0; } toSats(amount = 1) { // Return the full integer amount in 'satoshis' return Math.floor(amount * this.satoshi); } setAmount({sats, real}) { if (typeof sats === "string") sats = parseInt(sats, 10); if (typeof real === "string") real = parseFloat(real); if (typeof sats !== "number" && typeof real !== "number") { throw new Error("Invalid arguments for setAmount"); } if (real && typeof real !== "undefined") { if (typeof real !== "number" || isNaN(real)) throw new Error("Invalid argument 'real' for setAmount"); this.amount = this.toSats(real); this._clearCache(); } else if(typeof sats === "number") { this.amount = Math.floor(sats); this._clearCache(); } else { throw new Error("Invalid setAmount input"); } } _clearCache() { this._real_amount = null; } getAmount({real = false} = {}) { if (real) { if (this._real_amount) return this._real_amount; return this._real_amount = limitByPrecision(this.amount / this.toSats(), this.precision); } else { return Math.floor(this.amount); } } plus(asset) { if (asset.asset_id !== this.asset_id) throw new Error("Assets are not the same type"); this.amount += asset.amount; this._clearCache(); } minus(asset) { if (asset.asset_id !== this.asset_id) throw new Error("Assets are not the same type"); this.amount -= asset.amount; this.amount = Math.max(0, this.amount); this._clearCache(); } equals(asset) { return (this.asset_id === asset.asset_id && this.getAmount() === asset.getAmount()); } ne(asset) { return !this.equals(asset); } gt(asset) { return this.getAmount() > asset.getAmount(); } lt(asset) { return this.getAmount() < asset.getAmount(); } times(p, isBid = false) { // asset amount times a price p let temp, amount; if (this.asset_id === p.base.asset_id) { temp = (this.amount * p.quote.amount) / p.base.amount; amount = Math.floor(temp); /* * Sometimes prices are inexact for the relevant amounts, in the case * of bids this means we need to round up in order to pay 1 sat more * than the floored price, if we don't do this the orders don't match */ if (isBid && temp !== amount) { amount += 1; } if (amount === 0) amount = 1; return new Asset({asset_id: p.quote.asset_id, amount, precision: p.quote.precision}); } else if (this.asset_id === p.quote.asset_id) { temp = (this.amount * p.base.amount) / p.quote.amount; amount = Math.floor(temp); /* * Sometimes prices are inexact for the relevant amounts, in the case * of bids this means we need to round up in order to pay 1 sat more * than the floored price, if we don't do this the orders don't match */ if (isBid && temp !== amount) { amount += 1; } if (amount === 0) amount = 1; return new Asset({asset_id: p.base.asset_id, amount, precision: p.base.precision}); } throw new Error("Invalid asset types for price multiplication"); } divide(quote, base = this) { return new Price({base, quote}); } toObject() { return { asset_id: this.asset_id, amount: this.amount }; } clone(amount = this.amount) { return new Asset({ amount, asset_id: this.asset_id, precision: this.precision }); } } /** * @brief The price struct stores asset prices in the Graphene system. * * A price is defined as a ratio between two assets, and represents a possible exchange rate between those two * assets. prices are generally not stored in any simplified form, i.e. a price of (1000 CORE)/(20 USD) is perfectly * normal. * * The assets within a price are labeled base and quote. Throughout the Graphene code base, the convention used is * that the base asset is the asset being sold, and the quote asset is the asset being purchased, where the price is * represented as base/quote, so in the example price above the seller is looking to sell CORE asset and get USD in * return. */ class Price { constructor({base, quote, real = false} = {}) { if (!base || !quote) { throw new Error("Base and Quote assets must be defined"); } if (base.asset_id === quote.asset_id) { throw new Error("Base and Quote assets must be different"); } base = base.clone(); quote = quote.clone(); if (real && typeof real === "number") { /* * In order to make large numbers work properly, we assume numbers * larger than 100k do not need more than 5 decimals. Without this we * quickly encounter JavaScript floating point errors for large numbers. */ if (real > 100000) { real = limitByPrecision(real, 5); } let frac = new Fraction(real); let baseSats = base.toSats(), quoteSats = quote.toSats(); let numRatio = (baseSats / quoteSats), denRatio = quoteSats / baseSats; if (baseSats >= quoteSats) { denRatio = 1; } else { numRatio = 1; } base.amount = frac.numerator * numRatio; quote.amount = frac.denominator * denRatio; } else if (real === 0) { base.amount = 0; quote.amount = 0; } if (!base.asset_id || !("amount" in base) || !quote.asset_id || !("amount" in quote)) throw new Error("Invalid Price inputs"); this.base = base; this.quote = quote; } getUnits() { return this.base.asset_id + "_" + this.quote.asset_id; } isValid() { return ( this.base.amount !== 0 && this.quote.amount !== 0) && !isNaN(this.toReal()) && isFinite(this.toReal()); } toReal(sameBase = false) { const key = sameBase ? "_samebase_real" : "_not_samebase_real"; if (this[key]) { return this[key]; } let real = sameBase ? (this.quote.amount * this.base.toSats()) / (this.base.amount * this.quote.toSats()) : (this.base.amount * this.quote.toSats()) / (this.quote.amount * this.base.toSats()); return this[key] = parseFloat(real.toFixed(8)); // toFixed and parseFloat helps avoid floating point errors for really big or small numbers } invert() { return new Price({ base: this.quote, quote: this.base }); } clone(real = null) { return new Price({ base: this.base, quote: this.quote, real }); } equals(b) { if (this.base.asset_id !== b.base.asset_id || this.quote.asset_id !== b.quote.asset_id) { console.error("Cannot compare prices for different assets"); return false; } const amult = b.quote.amount * this.base.amount; const bmult = this.quote.amount * b.base.amount; return amult === bmult; } lt(b) { if (this.base.asset_id !== b.base.asset_id || this.quote.asset_id !== b.quote.asset_id) { throw new Error("Cannot compare prices for different assets"); } const amult = b.quote.amount * this.base.amount; const bmult = this.quote.amount * b.base.amount; return amult < bmult; } lte(b) { return (this.equals(b)) || (this.lt(b)); } ne(b) { return !(this.equals(b)); } gt(b) { return !(this.lte(b)); } gte(b) { return !(this.lt(b)); } toObject() { return { base: this.base.toObject(), quote: this.quote.toObject() }; } } class FeedPrice extends Price { constructor({priceObject, assets, market_base, sqr, real = false}) { if (!priceObject || typeof priceObject !== "object" || !market_base || !assets || !sqr) { throw new Error("Invalid FeedPrice inputs"); } if (priceObject.toJS) { priceObject = priceObject.toJS(); } const inverted = market_base === priceObject.base.asset_id; const base = new Asset({ asset_id: priceObject.base.asset_id, amount: priceObject.base.amount, precision: assets[priceObject.base.asset_id].precision }); const quote = new Asset({ asset_id: priceObject.quote.asset_id, amount: priceObject.quote.amount, precision: assets[priceObject.quote.asset_id].precision }); super({ base: inverted ? quote : base, quote: inverted ? base : quote, real }); this.sqr = parseInt(sqr, 10) / 1000; this.inverted = inverted; } getSqueezePrice({real = false} = {}) { if (!this._squeeze_price) { this._squeeze_price = this.clone(); if (this.inverted) this._squeeze_price.base.amount = Math.floor(this._squeeze_price.base.amount * this.sqr); if (!this.inverted) this._squeeze_price.quote.amount = Math.floor(this._squeeze_price.quote.amount * this.sqr); } if (real) { return this._squeeze_price.toReal(); } return this._squeeze_price; } } class LimitOrderCreate { constructor({for_sale, to_receive, seller = "", expiration = new Date(), fill_or_kill = false, fee = {amount: 0, asset_id: "1.3.0"}} = {}) { if (!for_sale || !to_receive) { throw new Error("Missing order amounts"); } if (for_sale.asset_id === to_receive.asset_id) { throw new Error("Order assets cannot be the same"); } this.amount_for_sale = for_sale; this.min_to_receive = to_receive; this.setExpiration(expiration); this.fill_or_kill = fill_or_kill; this.seller = seller; this.fee = fee; } setExpiration(expiration = null) { if (!expiration) { expiration = new Date(); expiration.setYear(expiration.getFullYear() + 5); } this.expiration = expiration; } getExpiration() { return this.expiration; } toObject() { return { seller: this.seller, min_to_receive: this.min_to_receive.toObject(), amount_to_sell: this.amount_for_sale.toObject(), expiration: this.expiration, fill_or_kill: this.fill_or_kill, fee: this.fee }; } } class LimitOrder { constructor(order, assets, market_base) { if (!market_base) { throw new Error("LimitOrder requires a market_base id"); } this.order = order; this.assets = assets; this.market_base = market_base; this.id = order.id; this.expiration = order.expiration && new Date(order.expiration); this.seller = order.seller; this.for_sale = parseInt(order.for_sale, 10); // asset id is sell_price.base.asset_id let base = new Asset({ asset_id: order.sell_price.base.asset_id, amount: parseInt(order.sell_price.base.amount, 10), precision: assets[order.sell_price.base.asset_id].precision }); let quote = new Asset({ asset_id: order.sell_price.quote.asset_id, amount: parseInt(order.sell_price.quote.amount, 10), precision: assets[order.sell_price.quote.asset_id].precision }); this.sell_price = new Price({ base, quote }); this.fee = order.deferred_fee; } getPrice(p = this.sell_price) { if (this._real_price) { return this._real_price; } return this._real_price = p.toReal(p.base.asset_id === this.market_base); } isBid() { return !(this.sell_price.base.asset_id === this.market_base); } isCall() { return false; } sellPrice() { return this.sell_price; } amountForSale() { if (this._for_sale) return this._for_sale; return this._for_sale = new Asset({ asset_id: this.sell_price.base.asset_id, amount: this.for_sale, precision: this.assets[this.sell_price.base.asset_id].precision }); } amountToReceive(isBid = this.isBid()) { if (this._to_receive) return this._to_receive; this._to_receive = this.amountForSale().times(this.sell_price, isBid); return this._to_receive; } sum(order) { let newOrder = this.clone(); newOrder.for_sale += order.for_sale; return newOrder; } clone() { return new LimitOrder(this.order, this.assets, this.market_base); } ne(order) { return ( this.sell_price.ne(order.sell_price) || this.for_sale !== order.for_sale ); } equals(order) { return !this.ne(order); } setTotalToReceive(total) { this.total_to_receive = total; } setTotalForSale(total) { this.total_for_sale = total; this._total_to_receive = null; } totalToReceive({noCache = false} = {}) { if (!noCache && this._total_to_receive) return this._total_to_receive; this._total_to_receive = (this.total_to_receive || this.amountToReceive()).clone(); return this._total_to_receive; } totalForSale({noCache = false} = {}) { if (!noCache && this._total_for_sale) return this._total_for_sale; return this._total_for_sale = (this.total_for_sale || this.amountForSale()).clone(); } } class CallOrder { constructor(order, assets, market_base, feed, is_prediction_market = false) { if (!order || !assets ||!market_base || !feed) { throw new Error("CallOrder missing inputs"); } this.order = order; this.assets = assets; this.market_base = market_base; this.is_prediction_market = is_prediction_market; this.inverted = market_base === order.call_price.base.asset_id; this.id = order.id; this.borrower = order.borrower; /* Collateral asset type is call_price.base.asset_id */ this.for_sale = parseInt(order.collateral, 10); this.for_sale_id = order.call_price.base.asset_id; /* Debt asset type is call_price.quote.asset_id */ this.to_receive = parseInt(order.debt, 10); this.to_receive_id = order.call_price.quote.asset_id; let base = new Asset({ asset_id: order.call_price.base.asset_id, amount: parseInt(order.call_price.base.amount, 10), precision: assets[order.call_price.base.asset_id].precision }); let quote = new Asset({ asset_id: order.call_price.quote.asset_id, amount: parseInt(order.call_price.quote.amount, 10), precision: assets[order.call_price.quote.asset_id].precision }); /* * The call price is DEBT * MCR / COLLATERAL. This calculation is already * done by the witness_node before returning the orders so it is not necessary * to deal with the MCR (maintenance collateral ratio) here. */ this.call_price = new Price({ base: this.inverted ? quote : base, quote: this.inverted ? base : quote }); if (feed.base.asset_id !== this.call_price.base.asset_id) { throw new Error("Feed price assets and call price assets must be the same"); } this.feed_price = feed; } clone(f = this.feed_price) { return new CallOrder(this.order, this.assets, this.market_base, f); } setFeed(f) { this.feed_price = f; this._clearCache(); } getPrice(squeeze = true, p = this.call_price) { if (squeeze) { return this.getSqueezePrice(); } if (this._real_price) { return this._real_price; } return this._real_price = p.toReal(p.base.asset_id === this.market_base); } getFeedPrice(f = this.feed_price) { if (this._feed_price) { return this._feed_price; } return this._feed_price = f.toReal(f.base.asset_id === this.market_base); } getSqueezePrice(f = this.feed_price) { if (this._squeeze_price) { return this._squeeze_price; } return this._squeeze_price = f.getSqueezePrice().toReal(); } isMarginCalled() { if (this.is_prediction_market) return false; return this.isBid() ? this.call_price.lt(this.feed_price) : this.call_price.gt(this.feed_price); } isBid() { return !this.inverted; } isCall() { return true; } sellPrice(squeeze = true) { if (squeeze) { return this.isBid() ? this.feed_price.getSqueezePrice() : this.feed_price.getSqueezePrice().invert(); } return this.call_price; } /* * Assume a USD:BTS market * The call order will always be selling BTS in order to buy USD * The asset being sold is always the collateral, which is call_price.base.asset_id. * The amount being sold depends on how big the debt is, only enough * collateral will be sold to cover the debt */ amountForSale(isBid = this.isBid()) { if (this._for_sale) return this._for_sale; // return this._for_sale = new Asset({ // asset_id: this.for_sale_id, // amount: this.for_sale, // precision: this.assets[this.for_sale_id].precision // }); return this._for_sale = this.amountToReceive().times(this.feed_price.getSqueezePrice(), isBid); } amountToReceive() { if (this._to_receive) return this._to_receive; // return this._to_receive = this.amountForSale().times(this.feed_price.getSqueezePrice(), isBid); return this._to_receive = new Asset({ asset_id: this.to_receive_id, amount: this.to_receive, precision: this.assets[this.to_receive_id].precision }); } sum(order) { let newOrder = this.clone(); newOrder.to_receive += order.to_receive; newOrder.for_sale += order.for_sale; newOrder._clearCache(); return newOrder; } _clearCache() { this._for_sale = null; this._to_receive = null; this._feed_price = null; this._squeeze_price = null; this._total_to_receive = null; this._total_for_sale = null; } ne(order) { return ( this.call_price.ne(order.call_price) || this.feed_price.ne(order.feed_price) || this.to_receive !== order.to_receive || this.for_sale !== order.for_sale ); } equals(order) { return !this.ne(order); } setTotalToReceive(total) { this.total_to_receive = total; } setTotalForSale(total) { this.total_for_sale = total; } totalToReceive({noCache = false} = {}) { if (!noCache && this._total_to_receive) return this._total_to_receive; this._total_to_receive = (this.total_to_receive || this.amountToReceive()).clone(); return this._total_to_receive; } totalForSale({noCache = false} = {}) { if (!noCache && this._total_for_sale) return this._total_for_sale; return this._total_for_sale = (this.total_for_sale || this.amountForSale()).clone(); } } class SettleOrder extends LimitOrder { constructor(order, assets, market_base, feed_price, bitasset_options) { if (!feed_price || !bitasset_options) { throw new Error("SettleOrder needs feed_price and bitasset_options inputs"); } order.sell_price = feed_price.toObject(); order.seller = order.owner; super(order, assets, market_base); this.offset_percent = bitasset_options.force_settlement_offset_percent; this.settlement_date = new Date(order.settlement_date); this.for_sale = new Asset({ amount: order.balance.amount, asset_id: order.balance.asset_id, precision: assets[order.balance.asset_id].precision }); this.inverted = this.for_sale.asset_id === market_base; this.feed_price = feed_price[this.inverted ? "invert" : "clone"](); } isBefore(order) { return this.settlement_date < order.settlement_date; } amountForSale() { return this.for_sale; } amountToReceive() { let to_receive = this.for_sale.times(this.feed_price, this.isBid()); to_receive.setAmount({sats: to_receive.getAmount() * ((GRAPHENE_100_PERCENT - this.offset_percent) / GRAPHENE_100_PERCENT) }); return this._to_receive = to_receive; } isBid() { return !this.inverted; } } export { Asset, Price, FeedPrice, LimitOrderCreate, limitByPrecision, precisionToRatio, LimitOrder, CallOrder, SettleOrder, didOrdersChange };