public ExchangeOrder(Timestamp timestamp, Address creator, BigInteger quantity, BigInteger rate, ExchangeOrderSide side) { Timestamp = timestamp; Creator = creator; Quantity = quantity; Rate = rate; Side = side; }
public ExchangeOrder(ExchangeOrder order, BigInteger newPrice, BigInteger newOrderSize) { Uid = order.Uid; Timestamp = order.Timestamp; Creator = order.Creator; Amount = newOrderSize; BaseSymbol = order.BaseSymbol; Price = newOrderSize; QuoteSymbol = order.QuoteSymbol; Side = order.Side; Type = order.Type; }
public ExchangeOrder(BigInteger uid, Timestamp timestamp, Address creator, BigInteger amount, string baseSymbol, BigInteger price, string quoteSymbol, ExchangeOrderSide side, ExchangeOrderType type) { Uid = uid; Timestamp = timestamp; Creator = creator; Amount = amount; BaseSymbol = baseSymbol; Price = price; QuoteSymbol = quoteSymbol; Side = side; Type = type; }
public decimal OpenMarketOrder(decimal orderSize, ExchangeOrderSide side) { var nexus = simulator.Nexus; var baseSymbol = baseToken.Symbol; var baseDecimals = baseToken.Decimals; var quoteSymbol = quoteToken.Symbol; var quoteDecimals = quoteToken.Decimals; var orderToken = side == Buy ? quoteToken : baseToken; var orderSizeBigint = UnitConversion.ToBigInteger(orderSize, orderToken.Decimals); var OpenerBaseTokensInitial = simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, baseSymbol, user.Address); var OpenerQuoteTokensInitial = simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, quoteSymbol, user.Address); BigInteger OpenerBaseTokensDelta = 0; BigInteger OpenerQuoteTokensDelta = 0; //get the starting balance for every address on the opposite side of the orderbook, so we can compare it to the final balance of each of those addresses var otherSide = side == Buy ? Sell : Buy; var startingOppositeOrderbook = (ExchangeOrder[])simulator.Nexus.RootChain.InvokeContract(simulator.Nexus.RootStorage, "exchange", "GetOrderBook", baseSymbol, quoteSymbol, otherSide).ToObject(); var OtherAddressesTokensInitial = new Dictionary <Address, BigInteger>(); //******************************************************************************************************************************************************************************* //*** the following method to check token balance state only works for the scenario of a single new exchange order per block that triggers other pre-existing exchange orders *** //******************************************************************************************************************************************************************************* foreach (var oppositeOrder in startingOppositeOrderbook) { if (OtherAddressesTokensInitial.ContainsKey(oppositeOrder.Creator) == false) { var targetSymbol = otherSide == Buy ? baseSymbol : quoteSymbol; OtherAddressesTokensInitial.Add(oppositeOrder.Creator, simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, targetSymbol, oppositeOrder.Creator)); } } //-------------------------- simulator.BeginBlock(); var tx = simulator.GenerateCustomTransaction(user, ProofOfWork.None, () => ScriptUtils.BeginScript().AllowGas(user.Address, Address.Null, 1, 9999) .CallContract("exchange", "OpenMarketOrder", user.Address, baseSymbol, quoteSymbol, orderSizeBigint, side). SpendGas(user.Address).EndScript()); simulator.EndBlock(); var txCost = simulator.Nexus.RootChain.GetTransactionFee(tx); BigInteger escrowedAmount = orderSizeBigint; //take into account the transfer of the owner's wallet to the chain address if (side == Buy) { OpenerQuoteTokensDelta -= escrowedAmount; } else if (side == Sell) { OpenerBaseTokensDelta -= escrowedAmount; } //take into account tx cost in case one of the symbols is the FuelToken if (baseSymbol == DomainSettings.FuelTokenSymbol) { OpenerBaseTokensDelta -= txCost; } else if (quoteSymbol == DomainSettings.FuelTokenSymbol) { OpenerQuoteTokensDelta -= txCost; } var events = nexus.FindBlockByTransaction(tx).GetEventsForTransaction(tx.Hash); var ordersCreated = events.Count(x => x.Kind == EventKind.OrderCreated && x.Address == user.Address); var wasNewOrderCreated = ordersCreated >= 1; Assert.IsTrue(wasNewOrderCreated, "No orders were created"); var ordersClosed = events.Count(x => x.Kind == EventKind.OrderClosed && x.Address == user.Address); var wasNewOrderClosed = ordersClosed == 1; var wasNewOrderCancelled = events.Count(x => x.Kind == EventKind.OrderCancelled && x.Address == user.Address) == 1; var createdOrderEvent = events.First(x => x.Kind == EventKind.OrderCreated); var createdOrderUid = Serialization.Unserialize <BigInteger>(createdOrderEvent.Data); ExchangeOrder createdOrderPostFill = new ExchangeOrder(); //---------------- //verify the order does not exist in the orderbook //in case the new order was IoC and it wasnt closed, order should have been cancelled if (wasNewOrderClosed == false) { Assert.IsTrue(wasNewOrderCancelled, "Non closed order did not get cancelled"); } else //if the new order was closed if (wasNewOrderClosed) { Assert.IsTrue(wasNewOrderCancelled == false, "Closed order also got cancelled"); } //check that the order no longer exists on the orderbook try { simulator.Nexus.RootChain.InvokeContract(simulator.Nexus.RootStorage, "exchange", "GetExchangeOrder", createdOrderUid); Assert.IsTrue(false, "Market order exists on the orderbooks"); } catch (Exception e) { //purposefully empty, this is the expected code-path } //------------------ //validate that everyone received their tokens appropriately BigInteger escrowedUsage = 0; //this will hold the amount of the escrowed amount that was actually used in the filling of the order //for IoC orders, we need to make sure that what wasn't used gets returned properly //for non IoC orders, we need to make sure that what wasn't used stays on the orderbook BigInteger baseTokensReceived = 0, quoteTokensReceived = 0; var OtherAddressesTokensDelta = new Dictionary <Address, BigInteger>(); //******************************************************************************************************************************************************************************* //*** the following method to check token balance state only works for the scenario of a single new exchange order per block that triggers other pre-existing exchange orders *** //******************************************************************************************************************************************************************************* //calculate the expected delta of the balances of all addresses involved var tokenExchangeEvents = events.Where(x => x.Kind == EventKind.TokenReceive); foreach (var tokenExchangeEvent in tokenExchangeEvents) { var eventData = Serialization.Unserialize <TokenEventData>(tokenExchangeEvent.Data); if (tokenExchangeEvent.Address == user.Address) { if (eventData.symbol == baseSymbol) { baseTokensReceived += eventData.value; } else if (eventData.symbol == quoteSymbol) { quoteTokensReceived += eventData.value; } } else { Assert.IsTrue(OtherAddressesTokensInitial.ContainsKey(tokenExchangeEvent.Address), "Address that was not on this orderbook received tokens"); if (OtherAddressesTokensDelta.ContainsKey(tokenExchangeEvent.Address)) { OtherAddressesTokensDelta[tokenExchangeEvent.Address] += eventData.value; } else { OtherAddressesTokensDelta.Add(tokenExchangeEvent.Address, eventData.value); } escrowedUsage += eventData.value; //the tokens other addresses receive come from the escrowed amount of the order opener } } OpenerBaseTokensDelta += baseTokensReceived; OpenerQuoteTokensDelta += quoteTokensReceived; var expectedRemainingEscrow = escrowedAmount - escrowedUsage; switch (side) { case Buy: Assert.IsTrue(Abs(OpenerQuoteTokensDelta) == escrowedUsage - (quoteSymbol == DomainSettings.FuelTokenSymbol ? txCost : 0)); break; case Sell: Assert.IsTrue(Abs(OpenerBaseTokensDelta) == escrowedUsage - (baseSymbol == DomainSettings.FuelTokenSymbol ? txCost : 0)); break; } //get the actual final balance of all addresses involved and make sure it matches the expected deltas var OpenerBaseTokensFinal = simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, baseSymbol, user.Address); var OpenerQuoteTokensFinal = simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, quoteSymbol, user.Address); Assert.IsTrue(OpenerBaseTokensFinal == OpenerBaseTokensDelta + OpenerBaseTokensInitial); Assert.IsTrue(OpenerQuoteTokensFinal == OpenerQuoteTokensDelta + OpenerQuoteTokensInitial); foreach (var entry in OtherAddressesTokensInitial) { var otherAddressInitialTokens = entry.Value; BigInteger delta = 0; if (OtherAddressesTokensDelta.ContainsKey(entry.Key)) { delta = OtherAddressesTokensDelta[entry.Key]; } var targetSymbol = otherSide == Buy ? baseSymbol : quoteSymbol; var otherAddressFinalTokens = simulator.Nexus.RootChain.GetTokenBalance(simulator.Nexus.RootStorage, targetSymbol, entry.Key); Assert.IsTrue(otherAddressFinalTokens == delta + otherAddressInitialTokens); } return(side == Buy?UnitConversion.ToDecimal(baseTokensReceived, baseToken.Decimals) : UnitConversion.ToDecimal(quoteTokensReceived, quoteToken.Decimals)); }
public decimal OpenLimitOrder(BigInteger orderSize, BigInteger orderPrice, ExchangeOrderSide side, bool IoC = false) { return(OpenLimitOrder(UnitConversion.ToDecimal(orderSize, baseToken.Decimals), UnitConversion.ToDecimal(orderPrice, quoteToken.Decimals), side, IoC)); }
public void OpenOrder(Address from, string baseSymbol, string quoteSymbol, BigInteger quantity, BigInteger rate, ExchangeOrderSide side) { Runtime.Expect(IsWitness(from), "invalid witness"); Runtime.Expect(Runtime.Nexus.TokenExists(baseSymbol), "invalid base token"); var baseToken = Runtime.Nexus.GetTokenInfo(baseSymbol); Runtime.Expect(baseToken.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(Runtime.Nexus.TokenExists(quoteSymbol), "invalid quote token"); var quoteToken = Runtime.Nexus.GetTokenInfo(quoteSymbol); Runtime.Expect(quoteToken.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); //var tokenABI = Chain.FindABI(NativeABI.Token); //Runtime.Expect(baseTokenContract.ABI.Implements(tokenABI)); var pair = baseSymbol + "_" + quoteSymbol; switch (side) { case ExchangeOrderSide.Sell: { var balances = new BalanceSheet(baseSymbol); var balance = balances.Get(this.Storage, from); Runtime.Expect(balance >= quantity, "not enought balance"); Runtime.Expect(Runtime.Nexus.TransferTokens(baseSymbol, this.Storage, Runtime.Chain, from, Runtime.Chain.Address, quantity), "transfer failed"); break; } case ExchangeOrderSide.Buy: { var balances = new BalanceSheet(quoteSymbol); var balance = balances.Get(this.Storage, from); var expectedAmount = quantity / rate; Runtime.Expect(balance >= expectedAmount, "not enought balance"); // TODO check this Runtime.Expect(Runtime.Nexus.TransferTokens(quoteSymbol, this.Storage, Runtime.Chain, from, Runtime.Chain.Address, expectedAmount), "transfer failed"); break; } default: throw new ContractException("invalid order side"); } var order = new ExchangeOrder(Runtime.Time, from, quantity, rate, side); var list = _orders.Get <string, StorageList>(pair); list.Add(order); }
private ExchangeOrder[] GetOrderBook(string baseSymbol, string quoteSymbol, bool oneSideFlag, ExchangeOrderSide side = Buy) { var buyKey = BuildOrderKey(Buy, quoteSymbol, baseSymbol); var sellKey = BuildOrderKey(Sell, quoteSymbol, baseSymbol); var buyOrders = ((oneSideFlag && side == Buy) || !oneSideFlag) ? _orders.Get <string, StorageList>(buyKey) : new StorageList(); var sellOrders = ((oneSideFlag && side == Sell) || !oneSideFlag) ? _orders.Get <string, StorageList>(sellKey) : new StorageList(); var buyCount = buyOrders.Context == null ? 0 : buyOrders.Count(); var sellCount = sellOrders.Context == null ? 0 : sellOrders.Count(); ExchangeOrder[] orderbook = new ExchangeOrder[(long)(buyCount + sellCount)]; for (long i = 0; i < buyCount; i++) { orderbook[i] = buyOrders.Get <ExchangeOrder>(i); } for (long i = (long)buyCount; i < orderbook.Length; i++) { orderbook[i] = sellOrders.Get <ExchangeOrder>(i); } return(orderbook); }
public ExchangeOrder[] GetOrderBook(string baseSymbol, string quoteSymbol, ExchangeOrderSide side) { return(GetOrderBook(baseSymbol, quoteSymbol, true, side)); }
public string CalculateEscrowSymbol(IToken baseToken, IToken quoteToken, ExchangeOrderSide side) => side == Sell ? baseToken.Symbol : quoteToken.Symbol;
/* * TODO: implement methods that allow cleaning up the order history book.. make sure only the exchange that placed the orders can clear them */ /* * TODO: implement code for trail stops and a method to allow a 3rd party to update the trail stop, without revealing user or order info */ public BigInteger CalculateEscrowAmount(BigInteger orderSize, BigInteger orderPrice, IToken baseToken, IToken quoteToken, ExchangeOrderSide side) { switch (side) { case Sell: return(orderSize); case Buy: return(Runtime.ConvertBaseToQuote(orderSize, orderPrice, baseToken, quoteToken)); default: throw new ContractException("invalid order side"); } }
/// <summary> /// Creates a limit order on the exchange /// </summary> /// <param name="from"></param> /// <param name="baseSymbol">For SOUL/KCAL pair, SOUL would be the base symbol</param> /// <param name="quoteSymbol">For SOUL/KCAL pair, KCAL would be the quote symbol</param> /// <param name="orderSize">Amount of base symbol tokens the user wants to buy/sell</param> /// <param name="price">Amount of quote symbol tokens the user wants to pay/receive per unit of base symbol tokens</param> /// <param name="side">If the order is a buy or sell order</param> /// <param name="IoC">"Immediate or Cancel" flag: if true, requires any unfulfilled parts of the order to be cancelled immediately after a single attempt at fulfilling it.</param> public void OpenLimitOrder(Address from, Address provider, string baseSymbol, string quoteSymbol, BigInteger orderSize, BigInteger price, ExchangeOrderSide side, bool IoC) { OpenOrder(from, provider, baseSymbol, quoteSymbol, side, IoC ? ImmediateOrCancel : Limit, orderSize, price); }
public void OpenMarketOrder(Address from, Address provider, string baseSymbol, string quoteSymbol, BigInteger orderSize, ExchangeOrderSide side) { OpenOrder(from, provider, baseSymbol, quoteSymbol, side, Market, orderSize, 0); }
private void OpenOrder(Address from, Address provider, string baseSymbol, string quoteSymbol, ExchangeOrderSide side, ExchangeOrderType orderType, BigInteger orderSize, BigInteger price) { Runtime.Expect(Runtime.IsWitness(from), "invalid witness"); Runtime.Expect(Runtime.GasTarget == provider, "invalid gas target"); Runtime.Expect(baseSymbol != quoteSymbol, "invalid base/quote pair"); Runtime.Expect(Runtime.TokenExists(baseSymbol), "invalid base token"); var baseToken = Runtime.GetToken(baseSymbol); Runtime.Expect(baseToken.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(Runtime.TokenExists(quoteSymbol), "invalid quote token"); var quoteToken = Runtime.GetToken(quoteSymbol); Runtime.Expect(quoteToken.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); if (orderType != Market) { Runtime.Expect(orderSize >= GetMinimumTokenQuantity(baseToken), "order size is not sufficient"); Runtime.Expect(price >= GetMinimumTokenQuantity(quoteToken), "order price is not sufficient"); } var uid = Runtime.GenerateUID(); //-------------- //perform escrow for non-market orders string orderEscrowSymbol = CalculateEscrowSymbol(baseToken, quoteToken, side); IToken orderEscrowToken = orderEscrowSymbol == baseSymbol ? baseToken : quoteToken; BigInteger orderEscrowAmount; BigInteger orderEscrowUsage = 0; if (orderType == Market) { orderEscrowAmount = orderSize; Runtime.Expect(orderEscrowAmount >= GetMinimumTokenQuantity(orderEscrowToken), "market order size is not sufficient"); } else { orderEscrowAmount = CalculateEscrowAmount(orderSize, price, baseToken, quoteToken, side); } //BigInteger baseTokensUnfilled = orderSize; var balance = Runtime.GetBalance(orderEscrowSymbol, from); Runtime.Expect(balance >= orderEscrowAmount, "not enough balance"); Runtime.TransferTokens(orderEscrowSymbol, from, this.Address, orderEscrowAmount); //------------ var thisOrder = new ExchangeOrder(); StorageList orderList; BigInteger orderIndex = 0; thisOrder = new ExchangeOrder(uid, Runtime.Time, from, provider, orderSize, baseSymbol, price, quoteSymbol, side, orderType); Runtime.Notify(EventKind.OrderCreated, from, uid); var key = BuildOrderKey(side, quoteSymbol, baseSymbol); orderList = _orders.Get <string, StorageList>(key); orderIndex = orderList.Add <ExchangeOrder>(thisOrder); _orderMap.Set <BigInteger, string>(uid, key); var makerSide = side == Buy ? Sell : Buy; var makerKey = BuildOrderKey(makerSide, quoteSymbol, baseSymbol); var makerOrders = _orders.Get <string, StorageList>(makerKey); do { int bestIndex = -1; BigInteger bestPrice = 0; Timestamp bestPriceTimestamp = 0; ExchangeOrder takerOrder = thisOrder; var makerOrdersCount = makerOrders.Count(); for (int i = 0; i < makerOrdersCount; i++) { var makerOrder = makerOrders.Get <ExchangeOrder>(i); if (side == Buy) { if (makerOrder.Price > takerOrder.Price && orderType != Market) // too expensive, we wont buy at this price { continue; } if (bestIndex == -1 || makerOrder.Price < bestPrice || (makerOrder.Price == bestPrice && makerOrder.Timestamp < bestPriceTimestamp)) { bestIndex = i; bestPrice = makerOrder.Price; bestPriceTimestamp = makerOrder.Timestamp; } } else { if (makerOrder.Price < takerOrder.Price && orderType != Market) // too cheap, we wont sell at this price { continue; } if (bestIndex == -1 || makerOrder.Price > bestPrice || (makerOrder.Price == bestPrice && makerOrder.Timestamp < bestPriceTimestamp)) { bestIndex = i; bestPrice = makerOrder.Price; bestPriceTimestamp = makerOrder.Timestamp; } } } if (bestIndex >= 0) { //since order "uid" has found a match, the creator of this order will be a taker as he will remove liquidity from the market //and the creator of the "bestIndex" order is the maker as he is providing liquidity to the taker var takerAvailableEscrow = orderEscrowAmount - orderEscrowUsage; var takerEscrowUsage = BigInteger.Zero; var takerEscrowSymbol = orderEscrowSymbol; var makerOrder = makerOrders.Get <ExchangeOrder>(bestIndex); var makerEscrow = _escrows.Get <BigInteger, BigInteger>(makerOrder.Uid); var makerEscrowUsage = BigInteger.Zero;; var makerEscrowSymbol = orderEscrowSymbol == baseSymbol ? quoteSymbol : baseSymbol; //Get fulfilled order size in base tokens //and then calculate the corresponding fulfilled order size in quote tokens if (takerEscrowSymbol == baseSymbol) { var makerEscrowBaseEquivalent = Runtime.ConvertQuoteToBase(makerEscrow, makerOrder.Price, baseToken, quoteToken); takerEscrowUsage = takerAvailableEscrow < makerEscrowBaseEquivalent ? takerAvailableEscrow : makerEscrowBaseEquivalent; makerEscrowUsage = CalculateEscrowAmount(takerEscrowUsage, makerOrder.Price, baseToken, quoteToken, Buy); } else { var takerEscrowBaseEquivalent = Runtime.ConvertQuoteToBase(takerAvailableEscrow, makerOrder.Price, baseToken, quoteToken); makerEscrowUsage = makerEscrow < takerEscrowBaseEquivalent ? makerEscrow : takerEscrowBaseEquivalent; takerEscrowUsage = CalculateEscrowAmount(makerEscrowUsage, makerOrder.Price, baseToken, quoteToken, Buy); } Runtime.Expect(takerEscrowUsage <= takerAvailableEscrow, "Taker tried to use more escrow than available"); Runtime.Expect(makerEscrowUsage <= makerEscrow, "Maker tried to use more escrow than available"); if (takerEscrowUsage < GetMinimumSymbolQuantity(takerEscrowSymbol) || makerEscrowUsage < GetMinimumSymbolQuantity(makerEscrowSymbol)) { break; } Runtime.TransferTokens(takerEscrowSymbol, this.Address, makerOrder.Creator, takerEscrowUsage); Runtime.TransferTokens(makerEscrowSymbol, this.Address, takerOrder.Creator, makerEscrowUsage); orderEscrowUsage += takerEscrowUsage; Runtime.Notify(EventKind.OrderFilled, takerOrder.Creator, takerOrder.Uid); Runtime.Notify(EventKind.OrderFilled, makerOrder.Creator, makerOrder.Uid); if (makerEscrowUsage == makerEscrow) { makerOrders.RemoveAt <ExchangeOrder>(bestIndex); _orderMap.Remove(makerOrder.Uid); Runtime.Expect(_escrows.ContainsKey(makerOrder.Uid), "An orderbook entry must have registered escrow"); _escrows.Remove(makerOrder.Uid); Runtime.Notify(EventKind.OrderClosed, makerOrder.Creator, makerOrder.Uid); } else { _escrows.Set(makerOrder.Uid, makerEscrow - makerEscrowUsage); } } else { break; } } while (orderEscrowUsage < orderEscrowAmount); var leftoverEscrow = orderEscrowAmount - orderEscrowUsage; if (leftoverEscrow == 0 || orderType != Limit) { orderList.RemoveAt <ExchangeOrder>(orderIndex); _orderMap.Remove(thisOrder.Uid); _escrows.Remove(thisOrder.Uid); if (leftoverEscrow > 0) { Runtime.TransferTokens(orderEscrowSymbol, this.Address, thisOrder.Creator, leftoverEscrow); Runtime.Notify(EventKind.OrderCancelled, thisOrder.Creator, thisOrder.Uid); } else { Runtime.Notify(EventKind.OrderClosed, thisOrder.Creator, thisOrder.Uid); } } else { _escrows.Set(uid, leftoverEscrow); } //TODO: ADD FEES, SEND THEM TO this.Address FOR NOW }
private string BuildOrderKey(ExchangeOrderSide side, string baseSymbol, string quoteSymbol) => $"{side}_{baseSymbol}_{quoteSymbol}";
/* * TODO: implement methods that allow cleaning up the order history book.. make sure only the exchange that placed the orders can clear them */ /* * TODO: implement code for trail stops and a method to allow a 3rd party to update the trail stop, without revealing user or order info */ public BigInteger CalculateEscrowAmount(BigInteger orderSize, BigInteger orderPrice, TokenInfo baseToken, TokenInfo quoteToken, ExchangeOrderSide side) { switch (side) { case Sell: return(orderSize); case Buy: return(UnitConversion.ToBigInteger(UnitConversion.ToDecimal(orderSize, baseToken.Decimals) * UnitConversion.ToDecimal(orderPrice, quoteToken.Decimals), quoteToken.Decimals)); default: throw new ContractException("invalid order side"); } }