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