diff --git a/dotnet/.gitignore b/dotnet/.gitignore new file mode 100644 index 0000000..a847859 --- /dev/null +++ b/dotnet/.gitignore @@ -0,0 +1,9 @@ +*.userprefs +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +bin/ +obj/ +.vs \ No newline at end of file diff --git a/dotnet/.vscode/launch.json b/dotnet/.vscode/launch.json new file mode 100644 index 0000000..2db6534 --- /dev/null +++ b/dotnet/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": "Taker", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/EhterDelta.Bots.Dontnet.dll", + "args": [ + "taker", + "-v" + ], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "Maker", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/EhterDelta.Bots.Dontnet.dll", + "args": [ + "maker", + "-v" + ], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/dotnet/.vscode/settings.json b/dotnet/.vscode/settings.json new file mode 100644 index 0000000..e3f3c6c --- /dev/null +++ b/dotnet/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 4 +} \ No newline at end of file diff --git a/dotnet/.vscode/tasks.json b/dotnet/.vscode/tasks.json new file mode 100644 index 0000000..769daab --- /dev/null +++ b/dotnet/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EtherDeltaClient.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/dotnet/App.config b/dotnet/App.config new file mode 100644 index 0000000..d7f76e6 --- /dev/null +++ b/dotnet/App.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/BaseBot.cs b/dotnet/BaseBot.cs new file mode 100644 index 0000000..b6283cb --- /dev/null +++ b/dotnet/BaseBot.cs @@ -0,0 +1,194 @@ +using Nethereum.Util; +using System; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace EhterDelta.Bots.Dontnet +{ + public abstract class BaseBot + { + protected Service Service { get; set; } + + protected BigInteger EtherDeltaETH { get; set; } + protected BigInteger WalletETH { get; set; } + protected BigInteger EtherDeltaToken { get; set; } + protected BigInteger WalletToken { get; set; } + + public BaseBot(EtherDeltaConfiguration config, ILogger logger = null) + { + Console.Clear(); + Console.ResetColor(); + Service = new Service(config, logger); + + Task[] tasks = new[] { + GetMarket(), + GetBalanceAsync("ETH", config.User), + GetBalanceAsync(config.Token, config.User), + GetEtherDeltaBalance("ETH", config.User), + GetEtherDeltaBalance(config.Token, config.User) + }; + + Task.WaitAll(tasks); + + PrintOrders(); + PrintTrades(); + PrintWallet(); + + Console.WriteLine(); + } + + private async Task GetEtherDeltaBalance(string token, string user) + { + BigInteger balance = 0; + try + { + balance = await Service.GetEtherDeltaBalance(token, user); + } + catch (TimeoutException) + { + Console.WriteLine("Could not get balance"); + } + + if (token == "ETH") + { + EtherDeltaETH = balance; + } + else + { + EtherDeltaToken = balance; + } + return balance; + } + + private async Task GetBalanceAsync(string token, string user) + { + BigInteger balance = 0; + + try + { + balance = await Service.GetBalance(token, user); + } + catch (TimeoutException) + { + Console.WriteLine("Could not get balance"); + } + + if (token == "ETH") + { + WalletETH = balance; + } + else + { + WalletToken = balance; + } + return balance; + } + + private void PrintTrades() + { + Console.WriteLine(); + Console.WriteLine("Recent trades"); + Console.WriteLine("===================================="); + int numTrades = 10; + + if (Service.Trades != null) + { + var trades = Service.Trades.Take(numTrades); + foreach (var trade in trades) + { + Console.ForegroundColor = trade.Side == "sell" ? ConsoleColor.Red : ConsoleColor.Green; + Console.WriteLine($"{trade.Date.ToLocalTime()} {trade.Side} {trade.Amount.ToString("N3")} @ {trade.Price.ToString("N9")}"); + } + } + + Console.ResetColor(); + } + + private void PrintOrders() + { + Console.WriteLine(); + Console.WriteLine("Order book"); + Console.WriteLine("===================================="); + int ordersPerSide = 10; + + if (Service.Orders.Sells.Count() == 0 && Service.Orders.Buys.Count() == 0) + { + Console.WriteLine("No sell or buy orders"); + return; + } + + var sells = Service.Orders.Sells.Take(ordersPerSide).Reverse(); + var buys = Service.Orders.Buys.Take(ordersPerSide); + + Console.ForegroundColor = ConsoleColor.Red; + foreach (var order in sells) + { + Console.WriteLine(FormatOrder(order)); + } + Console.ResetColor(); + + if (buys.Count() > 0 && sells.Count() > 0) + { + var salesPrice = sells.Last().Price; + var buysPrice = buys.Last().Price; + Console.WriteLine($"---- Spread ({(salesPrice - buysPrice).ToString("N9")}) ----"); + } + else + { + Console.WriteLine("--------"); + } + + Console.ForegroundColor = ConsoleColor.Green; + + if (buys != null) + { + foreach (var order in buys) + { + Console.WriteLine(FormatOrder(order)); + } + } + + Console.ResetColor(); + } + + private void PrintWallet() + { + var uc = new UnitConversion(); + Console.WriteLine(); + Console.WriteLine("Account balances"); + Console.WriteLine("===================================="); + Console.WriteLine($"Wallet ETH balance: {uc.FromWei(WalletETH).ToString("N18")}"); + Console.WriteLine($"EtherDelta ETH balance: {uc.FromWei(EtherDeltaETH).ToString("N18")}"); + Console.WriteLine($"Wallet token balance: {uc.FromWei(WalletToken).ToString("N18")}"); + Console.WriteLine($"EtherDelta token balance: {uc.FromWei(EtherDeltaToken).ToString("N18")}"); + } + + private string FormatOrder(Order order) + { + return $"{order.Price.ToString("N9")} {order.EthAvailableVolume.ToString("N3"),20}"; + } + + private async Task GetMarket() + { + try + { + await Service.WaitForMarket(); + } + catch (TimeoutException) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Could not get Market!"); + Console.ResetColor(); + } + } + + ~BaseBot() + { + if (Service != null) + { + Service.Close(); + } + } + } +} \ No newline at end of file diff --git a/dotnet/EtherDeltaClient.csproj b/dotnet/EtherDeltaClient.csproj new file mode 100644 index 0000000..3d68bb5 --- /dev/null +++ b/dotnet/EtherDeltaClient.csproj @@ -0,0 +1,19 @@ + + + + EhterDelta.Bots.Dontnet + $(NethereumVersion) + Stojce Slavkovski + EhterDelta.Bots.Dontnet + Exe + netcoreapp2.0 + + + + + + + + + + diff --git a/dotnet/EtherDeltaClient.sln b/dotnet/EtherDeltaClient.sln new file mode 100644 index 0000000..57785e0 --- /dev/null +++ b/dotnet/EtherDeltaClient.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 14.00 +# Visual Studio 2017 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtherDeltaClient", "EtherDeltaClient.csproj", "{2E39944F-2564-4DF5-A46D-011F59CD704B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2E39944F-2564-4DF5-A46D-011F59CD704B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E39944F-2564-4DF5-A46D-011F59CD704B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E39944F-2564-4DF5-A46D-011F59CD704B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E39944F-2564-4DF5-A46D-011F59CD704B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/dotnet/EtherDeltaConfiguration.cs b/dotnet/EtherDeltaConfiguration.cs new file mode 100644 index 0000000..a5e9ec8 --- /dev/null +++ b/dotnet/EtherDeltaConfiguration.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace EhterDelta.Bots.Dontnet +{ + public class EtherDeltaConfiguration + { + public string AddressEtherDelta { get; set; } + public string Provider { get; set; } + public string SocketUrl { get; set; } + public string AbiFile { get; internal set; } + public string TokenFile { get; internal set; } + public string Token { get; internal set; } + public string User { get; internal set; } + public string PrivateKey { get; internal set; } + public int UnitDecimals { get; internal set; } + public BigInteger GasLimit { get; internal set; } + public BigInteger GasPrice { get; internal set; } + } +} \ No newline at end of file diff --git a/dotnet/ILogger.cs b/dotnet/ILogger.cs new file mode 100644 index 0000000..ec4be57 --- /dev/null +++ b/dotnet/ILogger.cs @@ -0,0 +1,7 @@ +namespace EhterDelta.Bots.Dontnet +{ + public interface ILogger + { + void Log(string message); + } +} \ No newline at end of file diff --git a/dotnet/Maker.cs b/dotnet/Maker.cs new file mode 100644 index 0000000..e510097 --- /dev/null +++ b/dotnet/Maker.cs @@ -0,0 +1,112 @@ +using Nethereum.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace EhterDelta.Bots.Dontnet +{ + public class Maker : BaseBot + { + public Maker(EtherDeltaConfiguration config, ILogger logger = null) : base(config, logger) + { + + PrintMyOrders(); + + var ordersPerSide = 1; + var expires = Service.GetBlockNumber().Result + 10; + var buyOrdersToPlace = ordersPerSide - Service.MyOrders.Buys.Count(); + var sellOrdersToPlace = ordersPerSide - Service.MyOrders.Sells.Count(); + var buyVolumeToPlace = EtherDeltaETH; + var sellVolumeToPlace = EtherDeltaToken; + var bestBuy = Service.GetBestAvailableBuy(); + var bestSell = Service.GetBestAvailableSell(); + + if (bestBuy == null || bestSell == null) + { + Console.WriteLine("Market is not two-sided, cannot calculate mid-market"); + return; + } + + // Make sure we have a reliable mid market + if (Math.Abs((bestBuy.Price - bestSell.Price) / (bestBuy.Price + bestSell.Price) / 2) > 0.05m) + { + Console.WriteLine("Market is too wide, will not place orders"); + return; + } + + var uc = new UnitConversion(); + + var midMarket = (bestBuy.Price + bestSell.Price) / 2; + var orders = new List(); + + for (var i = 0; i < sellOrdersToPlace; i += 1) + { + var price = midMarket + ((i + 1) * midMarket * 0.05m); + var amount = sellVolumeToPlace / sellOrdersToPlace; + Console.WriteLine($"Sell { amount.ToString("N3")} @ { price.ToString("N9")}"); + try + { + var order = Service.CreateOrder(OrderType.Sell, expires, uc.ToWei(price), amount); + orders.Add(order); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(ex.Message); + Console.ResetColor(); + } + } + + for (var i = 0; i < buyOrdersToPlace; i += 1) + { + var price = midMarket - ((i + 1) * midMarket * 0.05m); + var amount = uc.FromWei(buyVolumeToPlace) / price / buyOrdersToPlace; + Console.WriteLine($"Buy { amount.ToString("N3")} @ { price.ToString("N9")}"); + try + { + var order = Service.CreateOrder(OrderType.Buy, expires, uc.ToWei(price), uc.ToWei(amount)); + orders.Add(order); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + var orderTasks = new List(); + orders.ForEach(order => + { + var amount = order.TokenGive == Service.ZeroToken ? order.AmountGet : order.AmountGive; + orderTasks.Add(Service.TakeOrder(order, amount)); + }); + + try + { + Task.WaitAll(orderTasks.ToArray()); + } + catch (Exception ex) + { + + Console.ForegroundColor = ConsoleColor.Red; + if (ex.InnerException != null) + { + Console.WriteLine(ex.InnerException.Message); + } + else + { + Console.WriteLine(ex.Message); + } + Console.ResetColor(); + } + + Console.WriteLine("Done"); + } + + void PrintMyOrders() + { + Console.WriteLine($"My existing buy orders: {Service.MyOrders.Buys.Count()}"); + Console.WriteLine($"My existing sell orders: {Service.MyOrders.Sells.Count()}"); + } + } +} \ No newline at end of file diff --git a/dotnet/Message.cs b/dotnet/Message.cs new file mode 100644 index 0000000..829b1d7 --- /dev/null +++ b/dotnet/Message.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace EhterDelta.Bots.Dontnet +{ + internal class Message + { + internal Message() + { + Data = new { }; + } + public string Event { get; set; } + + public dynamic Data { get; set; } + + public override string ToString() + { + var ret = $"42[\"{this.Event}\", {JsonConvert.SerializeObject(Data)}]"; + return ret; + } + + internal static Message ParseMessage(string messageString) + { + var message = new Message(); + + // message is Text/Json + if (messageString.StartsWith("42")) + { + messageString = messageString.Remove(0, 2); + var tmpData = JsonConvert.DeserializeObject(messageString); + + if (tmpData != null) + { + if (tmpData.GetType() == typeof(JArray)) + { + var array = (JArray)tmpData; + if (array.Count > 0 && array[0].GetType() == typeof(JValue)) + { + message.Event = array[0].ToString(); + } + + if (array.Count > 1) + { + message.Data = (object)array[1]; + } + } + } + + } + + return message; + } + } +} \ No newline at end of file diff --git a/dotnet/OrderType.cs b/dotnet/OrderType.cs new file mode 100644 index 0000000..4964d20 --- /dev/null +++ b/dotnet/OrderType.cs @@ -0,0 +1,8 @@ +namespace EhterDelta.Bots.Dontnet +{ + internal enum OrderType + { + Buy, + Sell + } +} \ No newline at end of file diff --git a/dotnet/Orders.cs b/dotnet/Orders.cs new file mode 100644 index 0000000..db69940 --- /dev/null +++ b/dotnet/Orders.cs @@ -0,0 +1,69 @@ +using Nethereum.Hex.HexTypes; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace EhterDelta.Bots.Dontnet +{ + public class Orders : Object + { + public IEnumerable Sells { get; set; } + public IEnumerable Buys { get; set; } + } + + public class Order + { + public string Id { get; set; } + public string Amount { get; set; } + public decimal Price { get; set; } + public string TokenGet { get; set; } + public HexBigInteger AmountGet { get; set; } + public string TokenGive { get; set; } + public HexBigInteger AmountGive { get; set; } + public BigInteger Expires { get; set; } + public BigInteger Nonce { get; set; } + public string User { get; set; } + public string Updated { get; set; } + public string AvailableVolume { get; set; } + public decimal EthAvailableVolume { get; set; } + public string AvailableVolumeBase { get; set; } + public decimal EthAvailableVolumeBase { get; set; } + public string AmountFilled { get; set; } + public int V { get; internal set; } + public string R { get; internal set; } + public string S { get; internal set; } + public string ContractAddr { get; internal set; } + public string Raw { get; internal set; } + + internal static Order FromJson(JToken jtoken) + { + Order order = null; + try + { + order = jtoken.ToObject(); + order.V = jtoken.Value("v"); + order.R = jtoken.Value("r"); + order.S = jtoken.Value("s"); + order.Raw = jtoken.ToString(); + } + catch { } + + return order; + } + + public bool Equals(Order other) + { + if (other == null) + { + return false; + } + return other.Id == Id; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/dotnet/Program.cs b/dotnet/Program.cs new file mode 100644 index 0000000..6239fe5 --- /dev/null +++ b/dotnet/Program.cs @@ -0,0 +1,59 @@ + + +using System; +using System.Configuration; +using System.Numerics; + +namespace EhterDelta.Bots.Dontnet +{ + class Program + { + static void Main(string[] args) + { + + if (args.Length < 1 || args[0] != "taker" && args[0] != "maker") + { + Console.WriteLine("Please run with 'taker' or 'maker' argument!"); + return; + } + + var config = new EtherDeltaConfiguration + { + SocketUrl = ConfigurationManager.AppSettings["SocketUrl"], + Provider = ConfigurationManager.AppSettings["Provider"], + AddressEtherDelta = ConfigurationManager.AppSettings["AddressEtherDelta"], + AbiFile = ConfigurationManager.AppSettings["AbiFile"], + TokenFile = ConfigurationManager.AppSettings["TokenFile"], + Token = ConfigurationManager.AppSettings["Token"], + User = ConfigurationManager.AppSettings["User"], + PrivateKey = ConfigurationManager.AppSettings["PrivateKey"], + UnitDecimals = int.Parse(ConfigurationManager.AppSettings["UnitDecimals"]), + GasPrice = new BigInteger(UInt64.Parse(ConfigurationManager.AppSettings["GasPrice"])), + GasLimit = new BigInteger(UInt64.Parse(ConfigurationManager.AppSettings["GasLimit"])) + }; + + ILogger logger = null; + if (args.Length == 2 && args[1] == "-v") + { + logger = new ConsoleLogger(); + } + + if (args[0] == "taker") + { + new Taker(config, logger); + } + else + { + new Maker(config, logger); + } + } + + private class ConsoleLogger : ILogger + { + public void Log(string message) + { + Console.WriteLine($"{DateTimeOffset.Now.DateTime.ToUniversalTime()} : {message}"); + } + } + } +} \ No newline at end of file diff --git a/dotnet/README.md b/dotnet/README.md new file mode 100644 index 0000000..8b0c6a1 --- /dev/null +++ b/dotnet/README.md @@ -0,0 +1,22 @@ +# EtherDelta .Net bot # + +## Install NuGet dependencies ## + +`dotnet restore` + +## Configuration ## + +Add configuration settings into `App.config` file. + + +## Run the taker bot ## + +`dotnet run taker` + +## Run the maker bot ## + +`dotnet run maker` + +## Run bot in verbose mode ## + +`dotnet run maker -v` diff --git a/dotnet/Service.cs b/dotnet/Service.cs new file mode 100644 index 0000000..edff647 --- /dev/null +++ b/dotnet/Service.cs @@ -0,0 +1,402 @@ +using Nethereum.ABI.FunctionEncoding; +using Nethereum.ABI.Model; +using Nethereum.Contracts; +using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Signer; +using Nethereum.Web3; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using WebSocket4Net; + +namespace EhterDelta.Bots.Dontnet +{ + public class Service + { + public const string ZeroToken = "0x0000000000000000000000000000000000000000"; + private WebSocket socket; + const int socketTimeout = 20000; + private ILogger logger; + private bool gotMarket = false; + + public Service(EtherDeltaConfiguration config, ILogger configLogger) + { + logger = configLogger; + Log("Starting"); + + Orders = new Orders + { + Sells = new List(), + Buys = new List() + }; + + MyOrders = new Orders + { + Sells = new List(), + Buys = new List() + }; + + Trades = new List(); + MyTrades = new List(); + + Config = config; + Web3 = new Web3(config.Provider); + var addressEtherDelta = Web3.ToChecksumAddress(config.AddressEtherDelta); + + // TODO: check file exists + var abi = File.ReadAllText(config.AbiFile); + EtherDeltaContract = Web3.Eth.GetContract(abi, addressEtherDelta); + + var tokenAbi = File.ReadAllText(config.TokenFile); + EthContract = Web3.Eth.GetContract(tokenAbi, Config.Token); + + InitSocket(); + } + + public EtherDeltaConfiguration Config { get; } + public Web3 Web3 { get; } + public Contract EtherDeltaContract { get; } + public Contract EthContract { get; } + public Orders Orders { get; set; } + public Orders MyOrders { get; set; } + public IEnumerable Trades { get; set; } + public IEnumerable MyTrades { get; set; } + + internal async Task TakeOrder(Order order, BigInteger amount) + { + var funvtionInput = new object[] { + order.TokenGet, + order.AmountGet.Value, + order.TokenGive, + order.AmountGive.Value, + order.Expires, + order.Nonce, + order.User, + order.V, + order.R.HexToByteArray(), + order.S.HexToByteArray(), + amount + }; + + var fnTest = EtherDeltaContract.GetFunction("testTrade"); + var willPass = await fnTest.CallAsync(funvtionInput); + + if (!willPass) + { + Log("Order will fail"); + throw new Exception("Order will fail"); + } + + var fnTrade = EtherDeltaContract.GetFunction("trade"); + var data = fnTrade.GetData(funvtionInput); + + var txCount = await Web3.Eth.Transactions.GetTransactionCount.SendRequestAsync(Config.User); + var encoded = Web3.OfflineTransactionSigner.SignTransaction(Config.PrivateKey, Config.AddressEtherDelta, amount, + txCount, Config.GasPrice, Config.GasLimit, data); + + var txId = await Web3.Eth.Transactions.SendRawTransaction.SendRequestAsync(encoded.EnsureHexPrefix()); + var receipt = await Web3.Eth.Transactions.GetTransactionReceipt.SendRequestAsync(txId); + return receipt; + } + + internal Order GetBestAvailableSell() + { + return Orders.Sells.FirstOrDefault(); + } + + internal Order GetBestAvailableBuy() + { + return Orders.Buys.FirstOrDefault(); + } + + internal async Task GetBlockNumber() + { + return await Web3.Eth.Blocks.GetBlockNumber.SendRequestAsync(); + } + + internal Order CreateOrder(OrderType orderType, BigInteger expires, BigInteger price, BigInteger amount) + { + var amountBigNum = orderType == OrderType.Buy ? amount / price : amount; + var amountBaseBigNum = amount * price; + var contractAddr = Config.AddressEtherDelta; + var tokenGet = orderType == OrderType.Buy ? Config.Token : ZeroToken; + var tokenGive = orderType == OrderType.Sell ? Config.Token : ZeroToken; + var amountGet = orderType == OrderType.Buy ? amountBigNum : amountBaseBigNum; + var amountGive = orderType == OrderType.Sell ? amountBigNum : amountBaseBigNum; + var orderNonce = new Random().Next(); + + var plainData = new object[] { + Config.AddressEtherDelta, + tokenGive, + amountGet, + tokenGive, + amountGive, + expires, + orderNonce + }; + + var prms = new Parameter[] { + new Parameter("address",1), + new Parameter("address",1), + new Parameter("uint256",1), + new Parameter("address",1), + new Parameter("uint256",1), + new Parameter("uint256",1), + new Parameter("uint256",1), + }; + + var pe = new ParametersEncoder(); + var data = pe.EncodeParameters(prms, plainData); + + var ms = new MessageSigner(); + var signature = ms.HashAndSign(data, Config.PrivateKey); + + var ethEcdsa = MessageSigner.ExtractEcdsaSignature(signature); + + var order = new Order + { + AmountGet = new HexBigInteger(amountGet), + AmountGive = new HexBigInteger(amountGive), + TokenGet = tokenGet, + TokenGive = tokenGive, + ContractAddr = contractAddr, + Expires = expires, + Nonce = orderNonce, + User = Config.User, + V = ethEcdsa.V, + R = ethEcdsa.R.ToHex(true), + S = ethEcdsa.S.ToHex(true), + }; + + return order; + } + + internal void Close() + { + Log("Closing ..."); + if (socket != null && socket.State == WebSocketState.Open) + { + socket.Close(); + } + } + + private void SocketMessageReceived(object sender, MessageReceivedEventArgs e) + { + Message message = Message.ParseMessage(e.Message); + Log($"Got {message.Event} event"); + switch (message.Event) + { + case "market": + UpdateOrders(message.Data.orders); + UpdateTrades(message.Data.trades); + gotMarket = true; + break; + case "trades": + UpdateTrades(message.Data); + break; + case "orders": + UpdateOrders(message.Data); + break; + default: + Log(e.Message); + break; + } + } + + private void SocketClosed(object sender, EventArgs e) + { + Log("SOCKET CLOSED"); + Log(e.GetType().ToString()); + + var ea = e as ClosedEventArgs; + if (ea != null) + { + Log(ea.Code.ToString()); + if (ea.Code == 1005) // no reason given + { + Log("Reconnecting..."); + InitSocket(); + } + } + } + + internal async Task GetBalance(string token, string user) + { + BigInteger balance = 0; + user = Web3.ToChecksumAddress(user); + + try + { + if (token == "ETH") + { + balance = await Web3.Eth.GetBalance.SendRequestAsync(user); + Log("ETH - GET BALANCE"); + } + else + { + token = Web3.ToChecksumAddress(token); + var tokenFunction = EthContract.GetFunction("balanceOf"); + balance = await tokenFunction.CallAsync(user); + Log("TOKEN - GET BALANCE"); + } + } + catch (Exception ex) + { + Log(ex.Message); + } + + return balance; + } + + internal async Task GetEtherDeltaBalance(string token, string user) + { + BigInteger balance = 0; + + try + { + if (token == "ETH") + { + token = ZeroToken; + } + + var tokenFunction = EtherDeltaContract.GetFunction("balanceOf"); + balance = await tokenFunction.CallAsync(token, user); + Log("ETHER DELTA - GET BALANCE"); + } + catch (Exception ex) + { + Log(ex.Message); + } + + return balance; + } + + private void SocketError(object sender, SuperSocket.ClientEngine.ErrorEventArgs e) + { + Log("SOCKET ERROR: "); + Log(e.Exception.Message); + } + + private void SocketOpened(object sender, EventArgs e) + { + Log("SOCKET Connected"); + } + + public async Task WaitForMarket() + { + Log("Wait for Market"); + gotMarket = false; + socket.Send(new Message + { + Event = "getMarket", + Data = new + { + token = Config.Token, + user = Config.User + } + }.ToString()); + + var gotMarketTask = Task.Run(() => + { + while (!gotMarket) + { + Task.Delay(1000).Wait(); + } + }); + + var completed = await Task.WhenAny(gotMarketTask, Task.Delay(socketTimeout)); + Log("Market Completed ..."); + + if (!gotMarketTask.IsCompletedSuccessfully) + { + throw new TimeoutException("Get Market timeout"); + } + } + + private void UpdateOrders(dynamic ordersObj) + { + var minOrderSize = 0.001m; + var orders = ordersObj as JObject; + if (orders == null) + { + return; + } + + var sells = ((JArray)orders["sells"]) + .Where(jtoken => + jtoken["tokenGive"] != null && jtoken["tokenGive"].ToString() == Config.Token && + jtoken["ethAvailableVolumeBase"] != null && jtoken["ethAvailableVolumeBase"].ToObject() > minOrderSize && + (jtoken["deleted"] == null || jtoken["deleted"].ToObject() == false) + ) + .Select(jtoken => Order.FromJson(jtoken)); + + if (sells != null && sells.Count() > 0) + { + Log($"Got {sells.Count()} sells"); + Orders.Sells = Orders.Sells.Union(sells); + MyOrders.Sells = MyOrders.Sells.Union(sells.Where(s => s.User == Config.User)); + } + + var buys = ((JArray)orders["buys"]) + .Where(jtoken => + jtoken["tokenGet"] != null && jtoken["tokenGet"].ToString() == Config.Token && + jtoken["ethAvailableVolumeBase"] != null && jtoken["ethAvailableVolumeBase"].ToObject() > minOrderSize && + (jtoken["deleted"] == null || jtoken["deleted"].ToObject() == false) + ) + .Select(jtoken => Order.FromJson(jtoken)); + + if (buys != null && buys.Count() > 0) + { + Log($"Got {buys.Count()} buys"); + Orders.Buys = Orders.Buys.Union(buys); + MyOrders.Buys = MyOrders.Buys.Union(buys.Where(s => s.User == Config.User)); + } + } + + private void UpdateTrades(JArray trades) + { + if (trades == null) + { + return; + } + + Log($"Got {trades.Count} trades"); + var tradesArray = trades + .Where(jtoken => + jtoken["txHash"] != null && + jtoken["amount"] != null && jtoken["amount"].ToObject() > 0m + ) + .Select(jtoken => Trade.FromJson(jtoken)); + + Log($"Parsed {tradesArray.Count()} trades"); + Trades = Trades.Union(tradesArray); + Log($"total {Trades.Count()} trades"); + } + + + private void Log(string message) + { + if (logger != null) + { + logger.Log(message); + } + } + + private void InitSocket() + { + socket = new WebSocket(Config.SocketUrl); + socket.Opened += SocketOpened; + socket.Error += SocketError; + socket.Closed += SocketClosed; + socket.MessageReceived += SocketMessageReceived; + socket.OpenAsync().Wait(); + } + } + +} \ No newline at end of file diff --git a/dotnet/Taker.cs b/dotnet/Taker.cs new file mode 100644 index 0000000..2e297f1 --- /dev/null +++ b/dotnet/Taker.cs @@ -0,0 +1,46 @@ +using Nethereum.Util; +using System; + +namespace EhterDelta.Bots.Dontnet +{ + public class Taker : BaseBot + { + public Taker(EtherDeltaConfiguration config, ILogger logger = null) : base(config, logger) + { + var order = Service.GetBestAvailableSell(); + + if (order != null) + { + Console.WriteLine($"Best available: Sell {order.EthAvailableVolume.ToString("N3")} @ {order.Price.ToString("N9")}"); + var desiredAmountBase = 0.001m; + + var fraction = Math.Min(desiredAmountBase / order.EthAvailableVolumeBase, 1); + try + { + var uc = new UnitConversion(); + var amount = order.AmountGet.Value * uc.ToWei(fraction); + Service.TakeOrder(order, amount).Wait(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + if (ex.InnerException != null) + { + Console.WriteLine(ex.InnerException.Message); + } + else + { + Console.WriteLine(ex.Message); + } + Console.ResetColor(); + } + } + else + { + Console.WriteLine("No Available order"); + } + + Console.WriteLine(); + } + } +} \ No newline at end of file diff --git a/dotnet/Trade.cs b/dotnet/Trade.cs new file mode 100644 index 0000000..62ba879 --- /dev/null +++ b/dotnet/Trade.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace EhterDelta.Bots.Dontnet +{ + public class Trade + { + public Trade() + { + } + + string TxHash { get; set; } + public decimal Price { get; set; } + public DateTime Date { get; set; } + public decimal Amount { get; set; } + public decimal AmountBase { get; set; } + public string Side { get; set; } + public string Buyer { get; set; } + public string Seller { get; set; } + public string TokenAddr { get; set; } + + public static Trade FromJson(JToken jtoken) + { + var trade = jtoken.ToObject(); + + if (trade.TxHash == null && jtoken["txHash"] != null) + { + trade.TxHash = jtoken["txHash"].ToString(); + } + return trade; + } + + public bool Equals(Trade other) + { + if (other == null) + { + return false; + } + return other.TxHash == TxHash; + } + + public override int GetHashCode() + { + return TxHash.GetHashCode(); + } + } +} \ No newline at end of file