diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs new file mode 100644 index 0000000..e0f8941 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs @@ -0,0 +1,30 @@ +using System.Numerics; +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; + +namespace FlamingoSwapOrderBook +{ + public static class Extensions + { + /// + /// uint160 转为正整数,用于合约地址排序,其它场景勿用 + /// + /// 合约地址 + /// + [OpCode(OpCode.PUSHDATA1, "0100")] + [OpCode(OpCode.CAT)] + [OpCode(OpCode.CONVERT, "21")] + public static extern BigInteger ToUInteger(this UInt160 val); + + /// + /// Is Valid and not Zero address + /// + /// + /// + public static bool IsAddress(this UInt160 address) + { + return address.IsValid && !address.IsZero; + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj new file mode 100644 index 0000000..ec0abe1 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj @@ -0,0 +1,12 @@ + + + net6.0 + + + + + + + + + \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs new file mode 100644 index 0000000..bb39c66 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs @@ -0,0 +1,110 @@ +using Neo; +using Neo.SmartContract; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + #region Admin + +#warning Update the admin address if necessary + [InitialValue("NdrUjmLFCmr6RjM52njho5sFUeeTdKPxG9", ContractParameterType.Hash160)] + static readonly UInt160 superAdmin = default; + + [InitialValue("0xc0695bdb8a87a40aff33c73ff6349ccc05fa9f01", ContractParameterType.Hash160)] + static readonly UInt160 Factory = default; + + [InitialValue("0x48c40d4666f93408be1bef038b6722404d9a4c2a", ContractParameterType.Hash160)] + static readonly UInt160 bNEO = default; + + private const string AdminKey = nameof(superAdmin); + private const string GASAdminKey = nameof(GASAdminKey); + private const string FundAddresskey = nameof(FundAddresskey); + + private static readonly byte[] OrderIDKey = new byte[] { 0x00 }; + private static readonly byte[] BookMapPrefix = new byte[] { 0x01 }; + private static readonly byte[] OrderMapPrefix = new byte[] { 0x02 }; + private static readonly byte[] ReceiptMapPrefix = new byte[] { 0x03 }; + private static readonly byte[] PauseMapPrefix = new byte[] { 0x04 }; + private static readonly byte[] FeeMapPrefix = new byte[] { 0x05 }; + + // When this contract address is included in the transaction signature, + // this method will be triggered as a VerificationTrigger to verify that the signature is correct. + // For example, this method needs to be called when withdrawing token from the contract. + [Safe] + public static bool Verify() => Runtime.CheckWitness(GetAdmin()); + + [Safe] + public static UInt160 GetAdmin() + { + var admin = Storage.Get(Storage.CurrentReadOnlyContext, AdminKey); + return admin?.Length == 20 ? (UInt160)admin : superAdmin; + } + + public static bool SetAdmin(UInt160 admin) + { + Assert(Verify(), "No Authorization"); + Assert(admin.IsAddress(), "Invalid Address"); + Storage.Put(Storage.CurrentContext, AdminKey, admin); + return true; + } + + public static void ClaimGASFrombNEO(UInt160 receiveAddress) + { + Assert(Runtime.CheckWitness(GetGASAdmin()), "Forbidden"); + var me = Runtime.ExecutingScriptHash; + var beforeBalance = GAS.BalanceOf(me); + Assert((bool)Contract.Call(bNEO, "transfer", CallFlags.All, Runtime.ExecutingScriptHash, bNEO, 0, null), "claim fail"); + var afterBalance = GAS.BalanceOf(me); + + GAS.Transfer(me, receiveAddress, afterBalance - beforeBalance); + } + + [Safe] + public static UInt160 GetGASAdmin() + { + var address = Storage.Get(Storage.CurrentReadOnlyContext, GASAdminKey); + return address?.Length == 20 ? (UInt160)address : null; + } + + public static bool SetGASAdmin(UInt160 GASAdmin) + { + Assert(GASAdmin.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + Storage.Put(Storage.CurrentContext, GASAdminKey, GASAdmin); + return true; + } + #endregion + + #region FundFee + + [Safe] + public static UInt160 GetFundAddress() + { + var address = Storage.Get(Storage.CurrentReadOnlyContext, FundAddresskey); + return address?.Length == 20 ? (UInt160)address : null; + } + + public static bool SetFundAddress(UInt160 address) + { + Assert(address.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + Storage.Put(Storage.CurrentContext, FundAddresskey, address); + return true; + } + #endregion + + #region Upgrade + + public static void Update(ByteString nefFile, string manifest) + { + Assert(Verify(), "No Authorization"); + ContractManagement.Update(nefFile, manifest, null); + } + #endregion + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs new file mode 100644 index 0000000..dd3a4da --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using System.Numerics; +using Neo; +using Neo.SmartContract.Framework; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + /// + /// When orderbook status changed + /// + [DisplayName("BookStatusChanged")] + public static event BookStatusChangedEvent onBookStatusChanged; + public delegate void BookStatusChangedEvent(UInt160 baseToken, UInt160 quoteToken, BigInteger quoteScale, BigInteger minOrderAmount, BigInteger maxOrderAmount, bool isPaused); + + /// + /// When order status changed + /// + [DisplayName("OrderStatusChanged")] + public static event OrderStatusChangedEvent onOrderStatusChanged; + public delegate void OrderStatusChangedEvent(UInt160 baseToken, UInt160 quoteToken, ByteString id, bool isBuy, UInt160 maker, BigInteger price, BigInteger leftAmount); + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs new file mode 100644 index 0000000..30c6aec --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs @@ -0,0 +1,535 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + /// + /// Insert a not-fully-deal limit order into orderbook + /// + /// + /// + /// + /// + /// + /// + private static bool InsertOrder(byte[] pairKey, OrderBook book, ByteString id, LimitOrder order, bool isBuy) + { + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + + // Check if there is no order + var canBeFirst = firstID is null; + if (!canBeFirst) + { + // Check if this order is the worthiest + var firstOrder = GetOrder(firstID); + canBeFirst = (isBuy && (firstOrder.price < order.price)) || (!isBuy && (firstOrder.price > order.price)); + } + if (canBeFirst) + { + // Insert to the first + if (isBuy) book.firstBuyID = id; + else book.firstSellID = id; + SetOrderBook(pairKey, book); + order.nextID = firstID; + SetOrder(id, order); + return true; + } + else + { + // Find the position + return InsertNotFirst(firstID, id, order, isBuy); + } + } + + private static bool InsertNotFirst(ByteString firstID, ByteString id, LimitOrder order, bool isBuy) + { + var currentID = firstID; + while (currentID is not null) + { + // Check before + var currentOrder = GetOrder(currentID); + var canFollow = (isBuy && (order.price <= currentOrder.price)) || (!isBuy && (order.price >= currentOrder.price)); + if (!canFollow) break; + + if (currentOrder.nextID is not null) + { + // Check after + var nextOrder = GetOrder(currentOrder.nextID); + var canPrecede = (isBuy && (nextOrder.price < order.price)) || (!isBuy && (nextOrder.price > order.price)); + canFollow = canFollow && canPrecede; + } + if (canFollow) + { + // Do insert + order.nextID = currentOrder.nextID; + SetOrder(id, order); + currentOrder.nextID = id; + SetOrder(currentID, currentOrder); + return true; + } + currentID = currentOrder.nextID; + } + return false; + } + + private static bool InsertOrderAt(ByteString parentID, ByteString id, LimitOrder order, bool isBuy) + { + var parentOrder = GetOrder(parentID); + var canFollow = (isBuy && (order.price <= parentOrder.price)) || (!isBuy && (order.price >= parentOrder.price)); + if (!canFollow) return false; + + if (parentOrder.nextID is not null) + { + // Check after + var nextOrder = GetOrder(parentOrder.nextID); + var canPrecede = (isBuy && (nextOrder.price < order.price)) || (!isBuy && (nextOrder.price > order.price)); + canFollow = canFollow && canPrecede; + } + if (canFollow) + { + // Do insert + order.nextID = parentOrder.nextID; + SetOrder(id, order); + parentOrder.nextID = id; + SetOrder(parentID, parentOrder); + return true; + } + return false; + } + + /// + /// Get the parent order id + /// + /// + /// + /// + /// + private static ByteString GetParentID(byte[] pairKey, bool isBuy, ByteString childID) + { + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID == childID) return null; + + var currentID = firstID; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if (currentOrder.nextID == childID) return currentID; + currentID = currentOrder.nextID; + } + return null; + } + + /// + /// Remove a canceled limit order from orderbook + /// + /// + /// + /// + /// + /// + private static bool RemoveOrder(byte[] pairKey, OrderBook book, ByteString id, bool isBuy) + { + // Remove from BookMap + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID is null) return false; + if (firstID == id) + { + // Delete the first + if (isBuy) book.firstBuyID = GetOrder(firstID).nextID; + else book.firstSellID = GetOrder(firstID).nextID; + SetOrderBook(pairKey, book); + DeleteOrder(firstID); + return true; + } + else + { + // Find the position + return RemoveNotFirst(firstID, id); + } + } + + private static bool RemoveNotFirst(ByteString firstID, ByteString id) + { + var currentID = firstID; + var currentOrder = GetOrder(currentID); + while (currentOrder.nextID is not null) + { + // Check next + if (currentOrder.nextID == id) + { + // Do remove + var newNextID = GetOrder(currentOrder.nextID).nextID; + DeleteOrder(currentOrder.nextID); + currentOrder.nextID = newNextID; + SetOrder(currentID, currentOrder); + return true; + } + currentID = currentOrder.nextID; + currentOrder = GetOrder(currentID); + } + return false; + } + + private static bool RemoveOrderAt(ByteString parentID, ByteString id) + { + var parentOrder = GetOrder(parentID); + if (parentOrder.nextID != id) return false; + + // Do remove + var newNextID = GetOrder(id).nextID; + DeleteOrder(id); + parentOrder.nextID = newNextID; + SetOrder(parentID, parentOrder); + return true; + } + + /// + /// Check if a limit order exists + /// + /// + /// + private static bool OrderExists(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentReadOnlyContext, OrderMapPrefix); + return orderMap.Get(id) is not null; + } + + /// + /// Check if an order book exists + /// + /// + /// + private static bool BookExists(byte[] pairKey) + { + StorageMap bookMap = new(Storage.CurrentReadOnlyContext, BookMapPrefix); + return bookMap.Get(pairKey) is not null; + } + + /// + /// Set an order book as paused + /// + /// + /// + private static void SetPaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentContext, PauseMapPrefix); + pauseMap.Put(pairKey, 1); + } + + /// + /// Remove an order book from paused + /// + /// + /// + private static void RemovePaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentContext, PauseMapPrefix); + pauseMap.Delete(pairKey); + } + + /// + /// Check if an order book is paused + /// + /// + /// + private static bool BookPaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentReadOnlyContext, PauseMapPrefix); + return pauseMap.Get(pairKey) is not null; + } + + /// + /// Get the detail of a limit order + /// + /// + /// + private static LimitOrder GetOrder(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentReadOnlyContext, OrderMapPrefix); + var order = orderMap.Get(id); + return order is null ? new LimitOrder() : (LimitOrder)StdLib.Deserialize(order); + } + + /// + /// Update a limit order + /// + /// + /// + private static void SetOrder(ByteString id, LimitOrder order) + { + StorageMap orderMap = new(Storage.CurrentContext, OrderMapPrefix); + orderMap.Put(id, StdLib.Serialize(order)); + } + + /// + /// Delete a limit order + /// + /// + private static void DeleteOrder(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentContext, OrderMapPrefix); + orderMap.Delete(id); + } + + /// + /// Get the detail of an order receipt + /// + /// + /// + /// + private static OrderReceipt GetReceipt(UInt160 maker, ByteString id) + { + StorageMap receiptMap = new(Storage.CurrentReadOnlyContext, ReceiptMapPrefix); + var receipt = receiptMap.Get(maker + id); + return receipt is null ? new OrderReceipt() : (OrderReceipt)StdLib.Deserialize(receipt); + } + + /// + /// Get all receipts of the maker + /// + /// + /// + private static Iterator ReceiptsOf(UInt160 maker) + { + StorageMap receiptMap = new(Storage.CurrentReadOnlyContext, ReceiptMapPrefix); + return receiptMap.Find(maker, FindOptions.ValuesOnly | FindOptions.DeserializeValues); + } + + [OpCode(OpCode.APPEND)] + private static extern void Append(T[] array, T newItem); + + /// + /// Update an order receipt + /// + /// + /// + /// + private static void SetReceipt(UInt160 maker, ByteString id, OrderReceipt receipt) + { + StorageMap receiptMap = new(Storage.CurrentContext, ReceiptMapPrefix); + receiptMap.Put(maker + id, StdLib.Serialize(receipt)); + } + + /// + /// Delete an order receipt + /// + /// + /// + private static void DeleteReceipt(UInt160 maker, ByteString id) + { + StorageMap receiptMap = new(Storage.CurrentContext, ReceiptMapPrefix); + receiptMap.Delete(maker + id); + } + + /// + /// Get the detail of a book + /// + /// + /// + private static OrderBook GetOrderBook(byte[] pairKey) + { + StorageMap bookMap = new(Storage.CurrentReadOnlyContext, BookMapPrefix); + var book = bookMap.Get(pairKey); + return book is null ? new OrderBook() : (OrderBook)StdLib.Deserialize(book); + } + + /// + /// Update a book + /// + /// + /// + private static void SetOrderBook(byte[] pairKey, OrderBook book) + { + StorageMap bookMap = new(Storage.CurrentContext, BookMapPrefix); + bookMap.Put(pairKey, StdLib.Serialize(book)); + } + + /// + /// Stage the fundfee payment for later claim + /// + /// + /// + private static void StageFundFee(UInt160 token, BigInteger amount) + { + Assert(amount >= 0, "Invalid Fee Amount"); + StorageMap feeMap = new(Storage.CurrentContext, FeeMapPrefix); + feeMap.Put(token, (BigInteger)feeMap.Get(token) + amount); + } + + /// + /// Get staged fundfee amount + /// + /// + /// + private static BigInteger GetStagedFundFee(UInt160 token) + { + StorageMap feeMap = new(Storage.CurrentReadOnlyContext, FeeMapPrefix); + return (BigInteger)feeMap.Get(token); + } + + /// + /// Reset the staged fundfee amount to 0 + /// + /// + private static void CleanStagedFundFee(UInt160 token) + { + StorageMap feeMap = new(Storage.CurrentContext, FeeMapPrefix); + feeMap.Delete(token); + } + + /// + /// Find a random number as order ID + /// + /// + private static ByteString GetUnusedID() + { + StorageContext context = Storage.CurrentContext; + ByteString counter = Storage.Get(context, OrderIDKey); + Storage.Put(context, OrderIDKey, (BigInteger)counter + 1); + ByteString data = Runtime.ExecutingScriptHash; + if (counter is not null) data += counter; + return CryptoLib.Sha256(data); + } + + /// + /// Get the pair contract + /// + /// + /// + /// + public static UInt160 GetExchangePairWithAssert(UInt160 tokenA, UInt160 tokenB) + { + Assert(tokenA.IsValid && tokenB.IsValid, "Invalid A or B Address"); + var pairContract = (byte[])Contract.Call(Factory, "getExchangePair", CallFlags.ReadOnly, new object[] { tokenA, tokenB }); + Assert(pairContract != null && pairContract.Length == 20, "PairContract Not Found", tokenA, tokenB); + return (UInt160)pairContract; + } + + /// + /// Get TokenA and TokenB reserves from paicontract + /// + /// + /// + /// + /// + public static BigInteger[] GetReserves(UInt160 pairContract, UInt160 tokenA, UInt160 tokenB) + { + var reserveData = (ReservesData)Contract.Call(pairContract, "getReserves", CallFlags.ReadOnly, new object[] { }); + return tokenA.ToUInteger() < tokenB.ToUInteger() ? new BigInteger[] { reserveData.Reserve0, reserveData.Reserve1 } : new BigInteger[] { reserveData.Reserve1, reserveData.Reserve0 }; + } + + /// + /// Check if pair contract charge a fundfee + /// + /// + /// + public static bool HasFundAddress(UInt160 pairContract) + { + return (byte[])Contract.Call(pairContract, "getFundAddress", CallFlags.ReadOnly, new object[] { }) != null; + } + + /// + /// Get amountOut from pair + /// + /// + /// + /// + /// + private static void SwapOut(UInt160 pairContract, BigInteger amount0Out, BigInteger amount1Out, UInt160 toAddress) + { + Contract.Call(pairContract, "swap", CallFlags.All, new object[] { amount0Out, amount1Out, toAddress, null }); + } + + /// + /// Transfer NEP-5 tokens + /// + /// + /// + /// + /// + /// + /// + private static void SafeTransfer(UInt160 token, UInt160 from, UInt160 to, BigInteger amount, byte[] data = null) + { + try + { + var result = (bool)Contract.Call(token, "transfer", CallFlags.All, new object[] { from, to, amount, data }); + Assert(result, "Transfer Fail in OrderBook", token); + } + catch (Exception) + { + Assert(false, "Transfer Error in OrderBook", token); + } + } + + private static void RequestTransfer(UInt160 token, UInt160 from, UInt160 to, BigInteger amount, byte[] data = null) + { + try + { + var balanceBefore = GetBalanceOf(token, to); + var result = (bool)Contract.Call(from, "approvedTransfer", CallFlags.All, new object[] { token, to, amount, data }); + var balanceAfter = GetBalanceOf(token, to); + Assert(result, "Transfer Not Approved in OrderBook", token); + Assert(balanceAfter == balanceBefore + amount, "Unexpected Transfer in OrderBook", token); + } + catch (Exception) + { + Assert(false, "Transfer Error in OrderBook", token); + } + } + + private static BigInteger GetBalanceOf(UInt160 token, UInt160 address) + { + return (BigInteger)Contract.Call(token, "balanceOf", CallFlags.ReadOnly, new object[] { address }); + } + + /// + /// Get unique pair key + /// + /// + /// + /// + private static byte[] GetPairKey(UInt160 tokenA, UInt160 tokenB) + { + return (BigInteger)tokenA < (BigInteger)tokenB + ? BookMapPrefix.Concat(tokenA).Concat(tokenB) + : BookMapPrefix.Concat(tokenB).Concat(tokenA); + } + + /// + /// Check if + /// + /// + /// + /// + private static void Assert(bool condition, string message, object data = null) + { + if (!condition) + { + throw new Exception(message); + } + } + + /// + /// Check if + /// + /// + /// + /// + private static void Assert(bool condition, string message, params object[] data) + { + if (!condition) + { + throw new Exception(message); + } + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs new file mode 100644 index 0000000..5c8185b --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -0,0 +1,1150 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System.Numerics; +using System.ComponentModel; + +namespace FlamingoSwapOrderBook +{ + [DisplayName("FlamingoSwapOrderBook")] + [ManifestExtra("Author", "Flamingo Finance")] + [ManifestExtra("Email", "developer@flamingo.finance")] + [ManifestExtra("Description", "This is a Flamingo Contract")] + [ContractPermission("*")] + public partial class FlamingoSwapOrderBookContract : SmartContract + { + /// + /// Deal and add limit order base on input strategy, settled by base amount + /// + /// + /// + /// + /// + /// Total buy(real get)/sell base token amount of the limit order + /// Price limit of the order + /// Expected base token amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part + /// + /// + public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) + { + Assert(amount > 0 && price > 0 && bookAmount >= 0 && bookPrice > 0 && amount >= bookAmount, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book Is Paused"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); + if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded The Deadline"); + + // Market order + var leftAmount = amount; + if (isBuy) leftAmount -= bookAmount > 0 ? DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, true, false)[1] : 0; + else leftAmount -= bookAmount > 0 ? DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, true, false)[0] : 0; + if (leftAmount == 0) return null; + + // Swap AMM + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + var pairContract = GetExchangePairWithAssert(tokenA, tokenB); + var hasFundFee = HasFundAddress(pairContract); + + var reverse = isBuy + ? GetReserves(pairContract, book.quoteToken, book.baseToken) + : GetReserves(pairContract, book.baseToken, book.quoteToken); + var amountIn = hasFundFee + ? GetAmountInTillPriceWithFundFee(isBuy, price, book.quoteScale, reverse[0], reverse[1]) + : GetAmountInTillPrice(isBuy, price, book.quoteScale, reverse[0], reverse[1]); + if (amountIn < 0) amountIn = 0; + var amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); + + if (isBuy && leftAmount < amountOut) + { + amountOut = leftAmount; + amountIn = GetAmountIn(amountOut, reverse[0], reverse[1]); + } + if (!isBuy && leftAmount < amountIn) + { + amountIn = leftAmount; + amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); + } + + if (amountOut > 0) + { + SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); + leftAmount -= isBuy ? amountOut : amountIn; + } + + // Add limit order + if (leftAmount < book.minOrderAmount || leftAmount > book.maxOrderAmount) return null; + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, sender, me, leftAmount * price / book.quoteScale); + else SafeTransfer(book.baseToken, sender, me, leftAmount); + + var id = GetUnusedID(); + Assert(InsertOrder(pairKey, book, id, new LimitOrder(){ + maker = sender, + price = price, + amount = leftAmount + }, isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(sender, id, new OrderReceipt(){ + baseToken = book.baseToken, + quoteToken = book.quoteToken, + id = id, + time = Runtime.Time, + isBuy = isBuy, + totalAmount = amount + }); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, sender, price, leftAmount); + return id; + } + + /// + /// Deal market order based on input strategy, settled by base amount or quote amount + /// + /// + /// + /// + /// + /// + /// Expected amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part + /// + /// + public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, BigInteger amountToBook, BigInteger bookPrice, BigInteger deadLine) + { + Assert(amountIn > 0 && amountOutMin > 0 && amountToBook >= 0 && amountIn >= amountToBook && bookPrice > 0, "Invalid Parameters"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); + + // Deal in order book + if (amountToBook > 0) + { + // Check pair status + var pairKey = GetPairKey(tokenFrom, tokenTo); + Assert(!BookPaused(pairKey), "Book is Paused"); + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + + // Check price limit + var isBuy = tokenTo == book.baseToken; + var price = isBuy ? amountIn * book.quoteScale / amountOutMin : amountOutMin * book.quoteScale / amountIn; + Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + + // Deal and record consumed amountIn and satisfied amountOut + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, amountToBook, !isBuy, false); + amountOutMin -= result[1]; + amountIn -= result[0]; + + Assert(amountIn >= 0, "Exceeded AmountIn"); // Should not spend more than amountIn + if (amountIn == 0) // No longer need further swap + { + Assert(amountOutMin <= 0, "Insufficient AmountOut"); // Should get more than amountOutMin + return true; + } + } + + // Swap AMM + var pairContract = GetExchangePairWithAssert(tokenFrom, tokenTo); + var reverse = GetReserves(pairContract, tokenFrom, tokenTo); + var amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); + Assert(amountOut >= amountOutMin, "Insufficient AmountOut"); + + if (amountOut > 0) SwapAMM(pairContract, sender, tokenFrom, tokenTo, amountIn, amountOut); + return true; + } + + /// + /// Deal market order based on input strategy, half swap like (amountIn & amountOut) but half book like (bookAmount) + /// + /// + /// + /// + /// + /// + /// Expected amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part + /// + /// + public static bool RouteMarketOrderOutForIn(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountOut, BigInteger amountInMax, BigInteger amountFromBook, BigInteger bookPrice, BigInteger deadLine) + { + Assert(amountOut > 0 && amountInMax > 0 && amountFromBook >= 0 && amountOut >= amountFromBook && bookPrice > 0, "Invalid Parameters"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); + + // Market order + if (amountFromBook > 0) + { + // Check pair status + var pairKey = GetPairKey(tokenFrom, tokenTo); + Assert(!BookPaused(pairKey), "Book is Paused"); + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + + // Check price limit + var isBuy = tokenTo == book.baseToken; + var price = isBuy ? amountInMax * book.quoteScale / amountOut : amountOut * book.quoteScale / amountInMax; + Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + + // Deal and record consumed amountIn and satisfied amountOut + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (amountFromBook * 1000 + 996) / 997, isBuy, false); + amountOut -= result[1]; + amountInMax -= result[0]; + + Assert(amountOut >= 0, "Exceeded AmountIn"); // Should not get more than amountOut + Assert(amountInMax >= 0, "Insufficient AmountIn"); // Should spend no more than amountInMax + if (amountOut <= 0) return true; // No longer need further swap + } + + // Swap AMM + var pairContract = GetExchangePairWithAssert(tokenFrom, tokenTo); + var reverse = GetReserves(pairContract, tokenFrom, tokenTo); + var amountIn = GetAmountIn(amountOut, reverse[0], reverse[1]); + Assert(amountIn <= amountInMax, "Insufficient AmountIn"); + + if (amountOut > 0) SwapAMM(pairContract, sender, tokenFrom, tokenTo, amountIn, amountOut); + return true; + } + + #region DEX like API + /// + /// Register a new book + /// + /// + /// + /// + /// + /// + /// + public static bool RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint quoteDecimals, BigInteger minOrderAmount, BigInteger maxOrderAmount) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(baseToken != quoteToken, "Invalid Trade Pair"); + Assert(minOrderAmount > 0 && maxOrderAmount > 0 && minOrderAmount <= maxOrderAmount, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var quoteScale = BigInteger.Pow(10, (int)quoteDecimals); + Assert(!BookExists(pairKey), "Book Already Registered"); + SetOrderBook(pairKey, new OrderBook(){ + baseToken = baseToken, + quoteToken = quoteToken, + quoteScale = quoteScale, + minOrderAmount = minOrderAmount, + maxOrderAmount = maxOrderAmount + }); + onBookStatusChanged(baseToken, quoteToken, quoteScale, minOrderAmount, maxOrderAmount, BookPaused(pairKey)); + return true; + } + + /// + /// Set the minimum order amount for addLimitOrder + /// + /// + /// + /// + /// + public static bool SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger minOrderAmount) + { + Assert(minOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(minOrderAmount <= book.maxOrderAmount, "Invalid Amount Limit"); + + book.minOrderAmount = minOrderAmount; + SetOrderBook(pairKey, book); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; + } + + /// + /// Set the maximum trade amount for addLimitOrder + /// + /// + /// + /// + /// + public static bool SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger maxOrderAmount) + { + Assert(maxOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(maxOrderAmount >= book.minOrderAmount, "Invalid Amount Limit"); + + book.maxOrderAmount = maxOrderAmount; + SetOrderBook(pairKey, book); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; + } + + /// + /// Pause an existing order book + /// + /// + /// + /// + public static bool PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) + { + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(!BookPaused(pairKey), "Book Already Paused"); + + SetPaused(pairKey); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; + } + + /// + /// Resume a paused order book + /// + /// + /// + /// + public static bool ResumeOrderBook(UInt160 baseToken, UInt160 quoteToken) + { + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(BookPaused(pairKey), "Book Not Paused"); + + RemovePaused(pairKey); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; + } + + /// + /// Add a new order into orderbook but try deal it first + /// + /// + /// + /// + /// + /// + /// + /// Null or a new order id + public static ByteString AddLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 maker, bool isBuy, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book Is Paused"); + Assert(Runtime.CheckWitness(maker), "No Authorization"); + Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); + + // Deposit token + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + if (amount < book.minOrderAmount || amount > book.maxOrderAmount) return null; + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, maker, me, amount * price / book.quoteScale); + else SafeTransfer(book.baseToken, maker, me, amount); + + // Do add + var id = GetUnusedID(); + Assert(InsertOrder(pairKey, book, id, new LimitOrder(){ + maker = maker, + price = price, + amount = amount + }, isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(maker, id, new OrderReceipt(){ + baseToken = book.baseToken, + quoteToken = book.quoteToken, + id = id, + time = Runtime.Time, + isBuy = isBuy, + totalAmount = amount + }); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, maker, price, amount); + return id; + } + + /// + /// Add a new order into orderbook with an expected parent order id + /// + /// + /// + /// + /// + /// Null or a new order id + public static ByteString AddLimitOrderAt(ByteString parentID, UInt160 maker, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var parent = GetOrder(parentID); + Assert(parent.maker.IsAddress(), "Parent Not Exists"); + var receipt = GetReceipt(parent.maker, parentID); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + Assert(!BookPaused(pairKey), "Book Is Paused"); + Assert(Runtime.CheckWitness(maker), "No Authorization"); + Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); + + // Check amount + var book = GetOrderBook(pairKey); + Assert(amount >= book.minOrderAmount && amount <= book.maxOrderAmount, "Invalid Limit Order Amount"); + + // Deposit token + var me = Runtime.ExecutingScriptHash; + if (receipt.isBuy) SafeTransfer(receipt.quoteToken, maker, me, amount * price / book.quoteScale); + else SafeTransfer(receipt.baseToken, maker, me, amount); + + // Insert new order + var id = GetUnusedID(); + Assert(InsertOrderAt(parentID, id, new LimitOrder(){ + maker = maker, + price = price, + amount = amount + }, receipt.isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(maker, id, new OrderReceipt(){ + baseToken = receipt.baseToken, + quoteToken = receipt.quoteToken, + id = id, + time = Runtime.Time, + isBuy = receipt.isBuy, + totalAmount = amount + }); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, maker, price, amount); + return id; + } + + /// + /// Cancel a limit order with its id + /// + /// + /// + /// + /// + /// + public static bool CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) + { + // Check if exist + var pairKey = GetPairKey(tokenA, tokenB); + var order = GetOrder(id); + Assert(order.maker.IsAddress(), "Order Not Exists"); + Assert(Runtime.CheckWitness(order.maker), "No Authorization"); + + // Do remove + var book = GetOrderBook(pairKey); + Assert(RemoveOrder(pairKey, book, id, isBuy), "Remove Order Fail"); + + // Remove receipt + DeleteReceipt(order.maker, id); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, order.maker, order.price, 0); + + // Withdraw token + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, me, order.maker, order.amount * order.price / book.quoteScale); + else SafeTransfer(book.baseToken, me, order.maker, order.amount); + return true; + } + + /// + /// Cancel a limit order with its id and parent order id + /// + /// + /// + /// + public static bool CancelOrderAt(ByteString parentID, ByteString id) + { + var order = GetOrder(id); + Assert(order.maker.IsAddress(), "Order Not Exists"); + Assert(Runtime.CheckWitness(order.maker), "No Authorization"); + + // Do remove + var receipt = GetReceipt(order.maker, id); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + var book = GetOrderBook(pairKey); + + Assert(RemoveOrderAt(parentID, id), "Remove Order Fail"); + + // Remove receipt + DeleteReceipt(order.maker, id); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, order.maker, order.price, 0); + + // Withdraw token + var me = Runtime.ExecutingScriptHash; + if (receipt.isBuy) SafeTransfer(receipt.quoteToken, me, order.maker, order.amount * order.price / book.quoteScale); + else SafeTransfer(receipt.baseToken, me, order.maker, order.amount); + return true; + } + + /// + /// Try get the parent order id of an existing order + /// + /// + /// + /// + /// + /// + [Safe] + public static ByteString GetParentOrderID(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) + { + var pairKey = GetPairKey(tokenA, tokenB); + return GetParentID(pairKey, isBuy, id); + } + + /// + /// Get first N limit orders and their details, start from pos + /// + /// + /// + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetFirstNOrders(UInt160 tokenA, UInt160 tokenB, bool isBuy, uint pos, uint n) + { + var results = new OrderReceipt[n]; + + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID is null) return results; + + var currentOrderID = firstID; + var currentOrder = GetOrder(currentOrderID); + for (int i = 0; i < pos + n; i++) + { + if (i >= pos) + { + var receipt = GetReceipt(currentOrder.maker, currentOrderID); + receipt.maker = currentOrder.maker; + receipt.price = currentOrder.price; + receipt.leftAmount = currentOrder.amount; + results[i - pos] = receipt; + + if (currentOrder.nextID is null) break; + } + currentOrderID = currentOrder.nextID; + currentOrder = GetOrder(currentOrder.nextID); + } + + return results; + } + + /// + /// Get first N limit orders and their details, start from orderID + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetFirstNOrders(ByteString orderID, uint n) + { + var results = new OrderReceipt[n]; + + var currentOrderID = orderID; + var currentOrder = GetOrder(currentOrderID); + if (!currentOrder.maker.IsAddress()) return results; + for (int i = 0; i < n; i++) + { + var receipt = GetReceipt(currentOrder.maker, currentOrderID); + receipt.maker = currentOrder.maker; + receipt.price = currentOrder.price; + receipt.leftAmount = currentOrder.amount; + results[i] = receipt; + + if (currentOrder.nextID is null) break; + + currentOrderID = currentOrder.nextID; + currentOrder = GetOrder(currentOrder.nextID); + } + return results; + } + + /// + /// Get N orders of maker and their details, start from pos + /// + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetOrdersOf(UInt160 maker, uint pos, uint n) + { + var results = new OrderReceipt[n]; + var iterator = ReceiptsOf(maker); + // Make up details + for (int i = 0; i < pos + n; i++) + { + if (iterator.Next() && i >= pos) + { + results[i - pos] = (OrderReceipt)iterator.Value; + var order = GetOrder(results[i - pos].id); + results[i - pos].maker = order.maker; + results[i - pos].price = order.price; + results[i - pos].leftAmount = order.amount; + } + } + return results; + } + + /// + /// Get N orders of maker and their details, start from orderID + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetOrdersOf(ByteString orderID, uint n) + { + var results = new OrderReceipt[n]; + var order = GetOrder(orderID); + if (!order.maker.IsAddress()) return results; + var iterator = ReceiptsOf(order.maker); + // Make up details + while (iterator.Next()) + { + var receipt = (OrderReceipt)iterator.Value; + if (receipt.id.Equals(orderID)) + { + for (int i = 0; i < n; i++) + { + results[i] = receipt; + order = GetOrder(results[i].id); + results[i].maker = order.maker; + results[i].price = order.price; + results[i].leftAmount = order.amount; + + if (!iterator.Next()) break; + receipt = (OrderReceipt)iterator.Value; + } + return results; + } + } + return results; + } + + /// + /// Get the total reverse of tradable orders with an expected price + /// + /// + /// + /// + /// + /// + [Safe] + public static BigInteger GetTotalTradable(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price) + { + var pairKey = GetPairKey(tokenA, tokenB); + if (BookPaused(pairKey)) return 0; + return GetTotalTradableInternal(pairKey, isBuy, price); + } + + private static BigInteger GetTotalTradableInternal(byte[] pairKey, bool isBuy, BigInteger price) + { + var totalTradable = BigInteger.Zero; + var book = GetOrderBook(pairKey); + var currentID = isBuy ? book.firstSellID : book.firstBuyID; + if (currentID is null) return totalTradable; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + totalTradable += currentOrder.amount; + currentID = currentOrder.nextID; + } + return totalTradable; + } + + /// + /// Try to match without real payment + /// + /// + /// + /// + /// + /// + /// + /// Unsatisfied input amount and the amount of another token + [Safe] + public static BigInteger[] MatchOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) + { + // Check if exist + var pairKey = GetPairKey(tokenA, tokenB); + if (BookPaused(pairKey)) return new BigInteger[] { amount, 0 }; + return MatchOrderInternal(pairKey, isBuy, price, amount, isAmountInBase); + } + + private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) + { + var result = BigInteger.Zero; + var book = GetOrderBook(pairKey); + var currentID = isBuy ? book.firstSellID : book.firstBuyID; + if (currentID is null) return new BigInteger[] { amount, 0 }; + var currentOrder = GetOrder(currentID); + + while (amount > 0) + { + // Check price + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + + var quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; + var baseAmount = currentOrder.amount; + + if ((isAmountInBase && amount >= baseAmount) || (!isAmountInBase && amount >= quoteAmount)) + { + result += isAmountInBase ? quoteAmount : baseAmount; + amount -= isAmountInBase ? baseAmount : quoteAmount; + } + else + { + result += isAmountInBase ? amount * currentOrder.price / book.quoteScale : + isBuy ? amount * book.quoteScale / currentOrder.price : (amount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; + amount = 0; + } + + if (currentOrder.nextID is null) break; + currentOrder = GetOrder(currentOrder.nextID); + } + + return new BigInteger[] { amount, result }; + } + + /// + /// Try to make a market deal with orderbook, taker is not a contract + /// + /// + /// + /// + /// + /// + /// + /// + /// Payment and receive + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book Is Paused"); + Assert(Runtime.CheckWitness(taker), "No Authorization"); + Assert(ContractManagement.GetContract(taker) == null, "Forbidden"); + + return DealMarketOrderInternal(pairKey, taker, isBuy, price, amount, isAmountInBase, false); + } + + /// + /// Try to make a market deal with orderbook, taker is a contract + /// + /// + /// + /// + /// + /// + /// + /// Payment and receive + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book Is Paused"); + var caller = Runtime.CallingScriptHash; + Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); + + return DealMarketOrderInternal(pairKey, caller, isBuy, price, amount, isAmountInBase, true); + } + + /// + /// Order execution + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase, bool shouldRequest) + { + // Check if can deal + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + var firstID = isBuy ? book.firstSellID : book.firstBuyID; + if (firstID is null) return new BigInteger[] { 0, 0 }; + var firstOrder = GetOrder(firstID); + var canDeal = (isBuy && firstOrder.price <= price) || (!isBuy && firstOrder.price >= price); + if (!canDeal) return new BigInteger[] { 0, 0 }; + + var me = Runtime.ExecutingScriptHash; + var fundAddress = GetFundAddress(); + + var quoteFee = BigInteger.Zero; + var baseFee = BigInteger.Zero; + + var takerReceive = BigInteger.Zero; + var takerPayment = BigInteger.Zero; + var makerReceive = new Map(); + + var currentID = firstID; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + + var quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; + var baseAmount = currentOrder.amount; + + if ((isAmountInBase && amount >= baseAmount) || (!isAmountInBase && amount >= quoteAmount)) + { + // Full-fill + // Remove full-fill order + DeleteOrder(currentID); + DeleteReceipt(currentOrder.maker, currentID); + firstID = currentOrder.nextID; + + onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, 0); + amount -= isAmountInBase ? baseAmount : quoteAmount; + } + else + { + // Part-fill + quoteAmount = isAmountInBase ? amount * currentOrder.price / book.quoteScale : amount; + baseAmount = isAmountInBase ? amount : isBuy ? amount * book.quoteScale / currentOrder.price : (amount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; + + // Update order + currentOrder.amount -= baseAmount; + SetOrder(currentID, currentOrder); + + onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, currentOrder.amount); + amount = 0; + } + + // Record payment + var quotePayment = quoteAmount * 997 / 1000; + var basePayment = baseAmount * 997 / 1000; + quoteFee += quoteAmount - quotePayment; + baseFee += baseAmount - basePayment; + + if (isBuy) + { + takerPayment += quoteAmount; + takerReceive += basePayment; + if (makerReceive.HasKey(currentOrder.maker)) makerReceive[currentOrder.maker] += quotePayment; + else makerReceive[currentOrder.maker] = quotePayment; + } + else + { + takerPayment += baseAmount; + takerReceive += quotePayment; + if (makerReceive.HasKey(taker)) makerReceive[taker] += basePayment; + else makerReceive[taker] = basePayment; + } + + // Check if still tradable + if (amount == 0) break; + currentID = currentOrder.nextID; + } + + // Update book if necessary + if (isBuy && book.firstSellID != firstID) + { + book.firstSellID = firstID; + SetOrderBook(pairKey, book); + } + if (!isBuy && book.firstBuyID != firstID) + { + book.firstBuyID = firstID; + SetOrderBook(pairKey, book); + } + + // Do transfer + if (takerPayment == 0) takerPayment += 1; + if (isBuy) + { + if (shouldRequest) RequestTransfer(book.quoteToken, taker, me, takerPayment); + else SafeTransfer(book.quoteToken, taker, me, takerPayment); + SafeTransfer(book.baseToken, me, taker, takerReceive); + foreach (var toAddress in makerReceive.Keys) SafeTransfer(book.quoteToken, me, toAddress, makerReceive[toAddress]); + } + else + { + if (shouldRequest) RequestTransfer(book.baseToken, taker, me, takerPayment); + else SafeTransfer(book.baseToken, taker, me, takerPayment); + SafeTransfer(book.quoteToken, me, taker, takerReceive); + foreach (var toAddress in makerReceive.Keys) SafeTransfer(book.baseToken, me, toAddress, makerReceive[toAddress]); + } + + StageFundFee(book.baseToken, baseFee); + StageFundFee(book.quoteToken, quoteFee); + + return new BigInteger[] { takerPayment, takerReceive }; + } + + /// + /// Deal a whole limit order with it id and parent id + /// + /// + /// + /// + /// + public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteString id) + { + // Check parameters + Assert(Runtime.CheckWitness(taker), "No Authorization"); + var order = GetOrder(id); + Assert(order.maker.IsAddress(), "Order Not Exists"); + var receipt = GetReceipt(order.maker, id); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + Assert(!BookPaused(pairKey), "Book Is Paused"); + + // Do deal + var me = Runtime.ExecutingScriptHash; + var quoteScale = GetOrderBook(pairKey).quoteScale; + var fundAddress = GetFundAddress(); + + var baseAmount = order.amount; + var quoteAmount = order.amount * order.price / quoteScale; + var basePayment = baseAmount * 997 / 1000; + var quotePayment = quoteAmount * 997 / 1000; + var baseFee = baseAmount - basePayment; + var quoteFee = quoteAmount - quotePayment; + + if (receipt.isBuy) SafeTransfer(receipt.baseToken, taker, me, baseAmount); + else SafeTransfer(receipt.quoteToken, taker, me, quoteAmount); + + // Remove order and receipt + Assert(RemoveOrderAt(parentID, id), "Remove Order Fail"); + DeleteReceipt(order.maker, id); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, order.maker, order.price, 0); + + // Transfer + if (receipt.isBuy) + { + SafeTransfer(receipt.baseToken, me, order.maker, basePayment); + SafeTransfer(receipt.quoteToken, me, taker, quotePayment); + } + else + { + SafeTransfer(receipt.baseToken, me, taker, basePayment); + SafeTransfer(receipt.quoteToken, me, order.maker, quotePayment); + } + + StageFundFee(receipt.baseToken, baseFee); + StageFundFee(receipt.quoteToken, quoteFee); + return true; + } + + /// + /// Claim the staged fundfee payment to fund address + /// + /// + /// + public static bool ClaimFundFee(UInt160[] tokens) + { + var fundAddress = GetFundAddress(); + if (fundAddress is null) return false; + var me = Runtime.ExecutingScriptHash; + foreach (var token in tokens) + { + var amount = GetStagedFundFee(token); + if (amount > 0) + { + CleanStagedFundFee(token); + SafeTransfer(token, me, fundAddress, amount); + } + } + return true; + } + + /// + /// Price reporter + /// + /// + /// + /// + /// + [Safe] + public static BigInteger GetMarketPrice(UInt160 tokenA, UInt160 tokenB, bool isBuy) + { + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstSellID : book.firstBuyID; + if (firstID is null) return 0; + return GetOrder(firstID).price; + } + + /// + /// Get trade pair details + /// + /// + /// + /// + [Safe] + public static OrderBook GetBookInfo(UInt160 tokenA, UInt160 tokenB) + { + var pairKey = GetPairKey(tokenA, tokenB); + return GetOrderBook(pairKey); + } + + /// + /// Check if a pair of token is tradable + /// + /// + /// + /// + [Safe] + public static bool BookTradable(UInt160 tokenA, UInt160 tokenB) + { + var pairKey = GetPairKey(tokenA, tokenB); + return BookExists(pairKey) && !BookPaused(pairKey); + } + #endregion + + #region AMM like API + /// + /// Calculate amountOut with amountIn + /// + /// + /// + /// + /// + /// Unsatisfied amountIn and amountOut + [Safe] + public static BigInteger[] GetAmountOut(UInt160 tokenFrom, UInt160 tokenTo, BigInteger price, BigInteger amountIn) + { + // Check if exist + var pairKey = GetPairKey(tokenFrom, tokenTo); + if (BookPaused(pairKey)) return new BigInteger[] { amountIn, 0 }; + var book = GetOrderBook(pairKey); + var isBuy = tokenFrom == book.quoteToken; + + var result = MatchOrderInternal(pairKey, isBuy, price, amountIn, !isBuy); + return new BigInteger[] { result[0], result[1] * 997 / 1000 }; // 0.3% fee + } + + /// + /// Calculate amountOut with amountIn + /// + /// + /// + /// + /// + /// Unsatisfied amountOut and amountIn + [Safe] + public static BigInteger[] GetAmountIn(UInt160 tokenFrom, UInt160 tokenTo, BigInteger price, BigInteger amountOut) + { + // Check if exist + var pairKey = GetPairKey(tokenFrom, tokenTo); + if (BookPaused(pairKey)) return new BigInteger[] { amountOut, 0 }; + var book = GetOrderBook(pairKey); + var isBuy = tokenFrom == book.quoteToken; + + var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997, isBuy); // 0.3% fee + return isBuy ? new BigInteger[] { result[0] * 997 / 1000, result[1] + 1 } : new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] }; + } + #endregion + + /// + /// Accept NEP-17 token + /// SwapTokenInForTokenOut + /// + /// + /// + /// + public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data) + { + + } + + /// + /// Calculate amountIn to reach AMM price + /// + /// + /// + /// + /// + /// + private static BigInteger GetAmountInTillPrice(bool isBuy, BigInteger price, BigInteger quoteScale, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(price > 0 && quoteScale > 0 && reserveIn > 0 && reserveOut > 0, "Parameter Invalid"); + var amountIn = BigInteger.Pow(reserveIn, 2) * 9000000; + if (isBuy) amountIn += reserveIn * reserveOut * price * 3988000000000 / quoteScale; + else amountIn += reserveIn * reserveOut * quoteScale * 3988000000000 / price; + return (amountIn.Sqrt() - reserveIn * 1997000) / 1994000; + } + + private static BigInteger GetAmountInTillPriceWithFundFee(bool isBuy, BigInteger price, BigInteger quoteScale, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(price > 0 && quoteScale > 0 && reserveIn > 0 && reserveOut > 0, "Parameter Invalid"); + var amountIn = BigInteger.Pow(reserveIn, 2) * 6250000; + if (isBuy) amountIn += reserveIn * reserveOut * price * 3986006000000 / quoteScale; + else amountIn += reserveIn * reserveOut * quoteScale * 3986006000000 / price; + return (amountIn.Sqrt() - reserveIn * 1996500) / 1993003; + } + + /// + /// AMM GetAmountOut + /// + /// + /// + /// + /// + private static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(amountIn >= 0 && reserveIn > 0 && reserveOut > 0, "AmountIn Must >= 0"); + var amountInWithFee = amountIn * 997; + var numerator = amountInWithFee * reserveOut; + var denominator = reserveIn * 1000 + amountInWithFee; + var amountOut = numerator / denominator; + return amountOut; + } + + /// + /// AMM GetAmountIn + /// + /// + /// + /// + /// + private static BigInteger GetAmountIn(BigInteger amountOut, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(amountOut >= 0 && reserveIn > 0 && reserveOut > 0, "AmountOut Must >= 0"); + var numerator = reserveIn * amountOut * 1000; + var denominator = (reserveOut - amountOut) * 997; + var amountIn = (numerator / denominator) + 1; + return amountIn; + } + + /// + /// Swap as a router + /// + /// + /// + /// + /// + /// + /// + private static void SwapAMM(UInt160 pairContract, UInt160 sender, UInt160 tokenIn, UInt160 tokenOut, BigInteger amountIn, BigInteger amountOut) + { + SafeTransfer(tokenIn, sender, pairContract, amountIn); + + BigInteger amount0Out = 0; + BigInteger amount1Out = 0; + if (tokenIn.ToUInteger() < tokenOut.ToUInteger()) amount1Out = amountOut; + else amount0Out = amountOut; + + SwapOut(pairContract, amount0Out, amount1Out, sender); + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs new file mode 100644 index 0000000..3c2aa46 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs @@ -0,0 +1,14 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct LimitOrder + { + public UInt160 maker; + public BigInteger price; + public BigInteger amount; + public ByteString nextID; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs new file mode 100644 index 0000000..5c8f02c --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs @@ -0,0 +1,18 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct OrderBook + { + public UInt160 baseToken; + public UInt160 quoteToken; + public BigInteger quoteScale; + public BigInteger minOrderAmount; + public BigInteger maxOrderAmount; + + public ByteString firstBuyID; + public ByteString firstSellID; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs new file mode 100644 index 0000000..90770a5 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs @@ -0,0 +1,19 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct OrderReceipt + { + public UInt160 baseToken; + public UInt160 quoteToken; + public ByteString id; + public ulong time; + public bool isBuy; + public UInt160 maker; + public BigInteger price; + public BigInteger totalAmount; + public BigInteger leftAmount; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs new file mode 100644 index 0000000..4a89fbf --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct ReservesData + { + public BigInteger Reserve0; + public BigInteger Reserve1; + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs index 5bd733d..37b108f 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs @@ -31,7 +31,7 @@ public partial class FlamingoSwapRouterContract : SmartContract public static BigInteger[] AddLiquidity(UInt160 sender, UInt160 tokenA, UInt160 tokenB, BigInteger amountADesired, BigInteger amountBDesired, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && tokenA.IsValid && tokenB.IsValid && amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -80,7 +80,7 @@ public static BigInteger[] AddLiquidity(UInt160 sender, UInt160 tokenA, UInt160 public static BigInteger[] AddLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteger amountADesired, BigInteger amountBDesired, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(tokenA.IsValid && tokenB.IsValid && amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -142,7 +142,7 @@ public static BigInteger[] AddLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteg public static BigInteger[] RemoveLiquidity(UInt160 sender, UInt160 tokenA, UInt160 tokenB, BigInteger liquidity, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && tokenA.IsValid && tokenB.IsValid && liquidity >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(liquidity > 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -166,7 +166,7 @@ public static BigInteger[] RemoveLiquidity(UInt160 sender, UInt160 tokenA, UInt1 public static BigInteger[] RemoveLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteger liquidity, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(tokenA.IsValid && tokenB.IsValid && liquidity > 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(liquidity > 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -214,8 +214,6 @@ public static BigInteger Quote(BigInteger amountA, BigInteger reserveA, BigInteg /// public static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, BigInteger reserveOut) { - // Assert(amountIn > 0, "amountIn should be positive number"); - // Assert(reserveIn > 0 && reserveOut > 0, "reserve should be positive number"); Assert(amountIn > 0 && reserveIn > 0 && reserveOut > 0, "AmountIn Must > 0"); var amountInWithFee = amountIn * 997; @@ -234,8 +232,6 @@ public static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, /// public static BigInteger GetAmountIn(BigInteger amountOut, BigInteger reserveIn, BigInteger reserveOut) { - //Assert(amountOut > 0, "amountOut should be positive number"); - //Assert(reserveIn > 0 && reserveOut > 0, "reserve should be positive number"); Assert(amountOut > 0 && reserveIn > 0 && reserveOut > 0, "AmountOut Must > 0"); var numerator = reserveIn * amountOut * 1000; var denominator = (reserveOut - amountOut) * 997; @@ -320,7 +316,7 @@ public static void OnNEP17Payment(UInt160 sender, BigInteger amountIn, object da public static bool SwapTokenInForTokenOut(UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && amountIn > 0 && amountOutMin >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountOutMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -339,7 +335,7 @@ public static bool SwapTokenInForTokenOut(UInt160 sender, BigInteger amountIn, B public static bool SwapTokenInForTokenOut(BigInteger amountIn, BigInteger amountOutMin, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(amountIn > 0 && amountOutMin >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountOutMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -369,7 +365,7 @@ public static bool SwapTokenInForTokenOut(BigInteger amountIn, BigInteger amount public static bool SwapTokenOutForTokenIn(UInt160 sender, BigInteger amountOut, BigInteger amountInMax, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && amountOut > 0 && amountInMax >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountInMax >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -388,7 +384,7 @@ public static bool SwapTokenOutForTokenIn(UInt160 sender, BigInteger amountOut, public static bool SwapTokenOutForTokenIn(BigInteger amountOut, BigInteger amountInMax, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(amountOut > 0 && amountInMax >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountInMax >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); diff --git a/Swap/flamingo-contract-swap/flamingo-contract-swap.sln b/Swap/flamingo-contract-swap/flamingo-contract-swap.sln index 16c7070..cc927ca 100644 --- a/Swap/flamingo-contract-swap/flamingo-contract-swap.sln +++ b/Swap/flamingo-contract-swap/flamingo-contract-swap.sln @@ -15,7 +15,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashLoanTemple", "FlashLoa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlamingoSwapFactory", "FlamingoSwapFactory\FlamingoSwapFactory.csproj", "{7F4FDB74-D11E-45A7-B6A0-C85D4528748A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProxyTemplate", "ProxyTemplate\ProxyTemplate.csproj", "{71582763-E444-4646-BCCD-828BE7859047}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxyTemplate", "ProxyTemplate\ProxyTemplate.csproj", "{71582763-E444-4646-BCCD-828BE7859047}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlamingoSwapOrderBook", "FlamingoSwapOrderBook\FlamingoSwapOrderBook.csproj", "{99811C5F-FF6B-465F-A341-E60B13BBCD1B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -51,6 +53,10 @@ Global {71582763-E444-4646-BCCD-828BE7859047}.Debug|Any CPU.Build.0 = Debug|Any CPU {71582763-E444-4646-BCCD-828BE7859047}.Release|Any CPU.ActiveCfg = Release|Any CPU {71582763-E444-4646-BCCD-828BE7859047}.Release|Any CPU.Build.0 = Release|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE