diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 94bf051..0000000 --- a/Program.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Nethereum.Hex.HexConvertors.Extensions; -using System.Numerics; -using Nethereum.Signer; -using Newtonsoft.Json; - -namespace NethereumSample -{ - class Program - { - static async Task Main(string[] args) - { - var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY"); - if (privateKey == null) - { - throw new Exception("PRIVATE_KEY is not defined"); - } - - var order = new Order - { - marketHash = "0x78d2b90f2b585ead37b34005997f551e3f7de26b194b09322f0df6e5246b6f86", - baseToken = "0x6A383cf1F8897585718DCA629a8f1471339abFe4", - totalBetSize = BigInteger.Parse("1000000000000000000000"), - percentageOdds = BigInteger.Parse("51302654005356720000"), - maker = new EthECKey(privateKey).GetPublicAddress(), - executor = "0x3259E7Ccc0993368fCB82689F5C534669A0C06ca", - isMakerBettingOutcomeOne = true, - apiExpiry = DateTimeOffset.Now.AddSeconds(1000).ToUnixTimeSeconds(), - salt = Order.Random32Bytes() - }; - - Console.WriteLine("Order hash: " + order.GetHash().ToHex(true)); - Console.WriteLine("Order signature: " + order.GetSignature(privateKey)); - - var api = new SportXAPI { url = "https://mumbai.api.sportx.bet" }; - var newOrderResult = await api.PostNewOrder(order.GetSignedOrder(privateKey)); - Console.WriteLine("New order result: " + newOrderResult); - - var deserialized = JsonConvert.DeserializeAnonymousType( - newOrderResult, - new { status = default(string), data = new { orders = default(string[]) } } - ); - Console.WriteLine("Deserialized new order result: " + deserialized); - var cancelSignature = await Order.GetCancelSignature( - deserialized.data.orders, - privateKey, - 80001 // use 137 if mainnet. 80001 = mumbai testnet, 137 = polygon mainnet - ); - Console.WriteLine("Cancel signature: " + cancelSignature); - - var cancelOrderResult = await api.CancelOrder( - deserialized.data.orders, - "Are you sure you want to cancel these orders", - cancelSignature - ); - Console.WriteLine("Cancel API call result: " + cancelOrderResult); - } - } -} diff --git a/README.md b/README.md index e4eefaf..b35e0fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SportX.bet C# examples +# SX.bet C# examples This repo contains some examples for signing various order operations on sportx.bet using C#. Currently has examples for the following: diff --git a/SigningExampleNethereum.csproj b/SigningExampleNethereum.csproj index cc5d898..99b6ee1 100644 --- a/SigningExampleNethereum.csproj +++ b/SigningExampleNethereum.csproj @@ -5,12 +5,27 @@ net6.0 enable enable + false - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/node_signatures/src/cancelAllSignature.ts b/node_signatures/src/cancelAllSignature.ts new file mode 100644 index 0000000..eab2016 --- /dev/null +++ b/node_signatures/src/cancelAllSignature.ts @@ -0,0 +1,43 @@ +import ethSigUtil from "eth-sig-util"; + +function getCancelAllOrdersEIP712Payload( + salt: string, + timestamp: number, + chainId: number +) { + const payload = { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "salt", type: "bytes32" }, + ], + Details: [{ name: "timestamp", type: "uint256" }], + }, + primaryType: "Details", + domain: { + name: "CancelAllOrdersSportX", + version: "1.0", + chainId, + salt, + }, + message: { timestamp }, + }; + return payload; +} + +export = function getCancelAllOrdersSignature( + callback: any, + salt: string, + timestamp: number, + privateKey: string, + chainId: number +) { + const payload = getCancelAllOrdersEIP712Payload(salt, timestamp, chainId); + const bufferPrivateKey = Buffer.from(privateKey.substring(2), "hex"); + const signature = (ethSigUtil as any).signTypedData_v4(bufferPrivateKey, { + data: payload, + }); + callback(null, signature); +}; diff --git a/Order.cs b/src/Order.cs similarity index 81% rename from Order.cs rename to src/Order.cs index 1774996..9bae35e 100644 --- a/Order.cs +++ b/src/Order.cs @@ -6,11 +6,12 @@ using Jering.Javascript.NodeJS; using Newtonsoft.Json; -namespace NethereumSample +namespace SigningExampleNethereum { public static class GlobalVar { - public const string JS_FILE_LOCATION = "./node_signatures/dist/main.js"; + public const string CANCEL_ORDERS_V1_JS_FILE_LOCATION = "./node_signatures/dist/main.js"; + public const string CANCEL_ALL_ORDERS_FILE_LOCATION = "./node_signatures/dist/cancelAllSignature.js"; } class BigIntegerConverter : JsonConverter @@ -100,8 +101,22 @@ int chainId { // Invoke javascript string? result = await StaticNodeJSService.InvokeFromFileAsync( - GlobalVar.JS_FILE_LOCATION, - args: new object[] { orderHashes, privateKey, 80001 } + GlobalVar.CANCEL_ORDERS_V1_JS_FILE_LOCATION, + args: new object[] { orderHashes, privateKey, chainId } + ); + return result; + } + + public static async Task GetCancelAllSignature( + string salt, + string timestamp, + string privateKey, + int chainId + ) { + // Invoke javascript + string? result = await StaticNodeJSService.InvokeFromFileAsync( + GlobalVar.CANCEL_ALL_ORDERS_FILE_LOCATION, + args: new object[] { salt, timestamp, privateKey, chainId } ); return result; } diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..c863009 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,79 @@ +using Nethereum.Hex.HexConvertors.Extensions; +using System.Numerics; +using Nethereum.Signer; +using Newtonsoft.Json; + +namespace SigningExampleNethereum +{ + class Program + { + static async Task Main(string[] args) + { + var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY_NO_PREFIX"); + if (privateKey == null) + { + throw new Exception("PRIVATE_KEY_NO_PREFIX is not defined"); + } + var chainIdRaw = Environment.GetEnvironmentVariable("CHAIN_ID"); + if (chainIdRaw == null) { + throw new Exception("CHAIN_ID is not defined"); + } + var chainId = Int32.Parse(chainIdRaw); + + EthECKey wallet = new EthECKey(privateKey); + Console.WriteLine("Wallet address: " + wallet.GetPublicAddress()); + + + // var order = new Order + // { + // marketHash = "0x88c43af903e373ec2b2717813f6c42188aeb2d818a56eda46b6d63a08ad1e406", + // baseToken = "0x5147891461a7C81075950f8eE6384e019e39ab98", + // totalBetSize = BigInteger.Parse("100000000"), + // percentageOdds = BigInteger.Parse("50000000000000000000"), + // maker = wallet.GetPublicAddress(), + // executor = "0x3259E7Ccc0993368fCB82689F5C534669A0C06ca", + // isMakerBettingOutcomeOne = true, + // apiExpiry = DateTimeOffset.Now.AddSeconds(1000).ToUnixTimeSeconds(), + // salt = Order.Random32Bytes() + // }; + + // Console.WriteLine("Order hash: " + order.GetHash().ToHex(true)); + // Console.WriteLine("Order signature: " + order.GetSignature(privateKey)); + + // var api = new SXBetApi { url = "http://localhost:8080" }; + // var newOrderResult = await api.PostNewOrder(order.GetSignedOrder(privateKey)); + // Console.WriteLine("New order result: " + newOrderResult); + + // var deserialized = JsonConvert.DeserializeAnonymousType( + // newOrderResult, + // new { status = default(string), data = new { orders = default(string[]) } } + // ); + // Console.WriteLine("Deserialized new order result: " + deserialized); + + // var ordersList = new List(deserialized.data.orders); + + // var cancelSignature = CancelOrdersV1.GetCancelOrdersEIP712Payload(ordersList, chainId, wallet); + + // Console.WriteLine("Cancel signature: " + cancelSignature); + + // var cancelOrderResult = await api.CancelOrder( + // deserialized.data.orders, + // CancelOrdersV1.PROMPT, + // cancelSignature + // ); + // Console.WriteLine("Cancel API call result: " + cancelOrderResult); + + var salt = Utilities.Random32Bytes(); + + var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + + var cancelAllOrdersSignature = CancelAllOrders.GetCancelAllOrdersEIP712Payload(chainId, timestamp, wallet, salt); + var jsCancelAllOrdersSignature = await Order.GetCancelAllSignature(salt.ToHex(), timestamp.ToString(), wallet.GetPrivateKey(), chainId); + Console.WriteLine("Cancel all orders signature (c#): " + cancelAllOrdersSignature); + Console.WriteLine("Cancel all orders signature (js): " + jsCancelAllOrdersSignature); + + // var cancelAllOrdersResult = await api.CancelAllOrders(cancelAllOrdersSignature, salt.ToString(), wallet.GetPublicAddress(), timestamp); + // Console.WriteLine("Cancel all API call result: " + cancelAllOrdersResult); + } + } +} diff --git a/SportXAPI.cs b/src/SXBetApi.cs similarity index 65% rename from SportXAPI.cs rename to src/SXBetApi.cs index 6e78eb8..8c2f4d0 100644 --- a/SportXAPI.cs +++ b/src/SXBetApi.cs @@ -1,9 +1,10 @@ using Newtonsoft.Json; using System.Text; +using System.Numerics; -namespace NethereumSample +namespace SigningExampleNethereum { - class SportXAPI + class SXBetApi { static readonly HttpClient client = new HttpClient(); @@ -29,6 +30,30 @@ public async Task PostNewOrder(SignedOrder order) } } + public async Task CancelAllOrders( + string signature, + string salt, + string maker, + BigInteger timestamp + ) + { + var obj = new { signature, salt, maker, timestamp }; + var serialized = JsonConvert.SerializeObject(obj); + var httpContent = new StringContent(serialized, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await client.PostAsync( + url + "/orders/cancel/all", + httpContent + ); + if (response.Content != null) + { + return await response.Content.ReadAsStringAsync(); + } + else + { + throw new Exception("Content is null"); + } + } + public async Task CancelOrder( string[] orderHashes, string message, diff --git a/src/SignatureGeneration/CancelAllOrders.cs b/src/SignatureGeneration/CancelAllOrders.cs new file mode 100644 index 0000000..a9470ce --- /dev/null +++ b/src/SignatureGeneration/CancelAllOrders.cs @@ -0,0 +1,50 @@ +using Nethereum.Signer.EIP712; +using Nethereum.ABI.FunctionEncoding.Attributes; +using Nethereum.Signer; +using Nethereum.ABI.EIP712; +using System.Numerics; + +namespace SigningExampleNethereum +{ + public static class CancelAllOrders + { + + private static readonly Eip712TypedDataSigner _signer = new Eip712TypedDataSigner(); + + //Message types for easier input + private class Details + { + [Parameter("uint256", "timestamp", 1)] + public BigInteger Timestamp { get; set; } + } + + //The generic Typed schema defintion for this message + private static TypedData getTypedDefinition(byte[] salt, BigInteger chainId) + { + return new TypedData + { + Domain = new SaltDomain + { + Name = "CancelAllOrdersSportX", + Version = "1.0", + ChainId = chainId, + Salt = salt + }, + Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(SaltDomain), typeof(Details)), + PrimaryType = nameof(Details), + }; + } + + public static string GetCancelAllOrdersEIP712Payload(BigInteger chainId, BigInteger timestamp, EthECKey key, byte[] salt) + { + var typedData = getTypedDefinition(salt, chainId); + + var details = new Details + { + Timestamp = timestamp + }; + + return _signer.SignTypedDataV4(details, typedData, key); + } + } +} diff --git a/src/SignatureGeneration/CancelOrdersV1.cs b/src/SignatureGeneration/CancelOrdersV1.cs new file mode 100644 index 0000000..2bd3904 --- /dev/null +++ b/src/SignatureGeneration/CancelOrdersV1.cs @@ -0,0 +1,55 @@ +using Nethereum.Signer.EIP712; +using Nethereum.ABI.FunctionEncoding.Attributes; +using Nethereum.Signer; +using Nethereum.ABI.EIP712; +using System.Numerics; + +namespace SigningExampleNethereum +{ + public static class CancelOrdersV1 + { + private static readonly Eip712TypedDataSigner _signer = new Eip712TypedDataSigner(); + + public static readonly string PROMPT = "Are you sure you want to cancel these orders"; + + //Message types for easier input + private class Details + { + [Parameter("string", "message", 1)] + public string Message { get; set; } + + [Parameter("string[]", "orders", 2)] + public List Orders { get; set; } + } + + //The generic Typed schema defintion for this message + private static TypedData GetTypedDefinition(BigInteger chainId) + { + return new TypedData + { + Domain = new DomainWithNameVersionAndChainId + { + Name = "CancelOrderSportX", + Version = "1.0", + ChainId = chainId, + }, + Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(DomainWithNameVersionAndChainId), typeof(Details)), + PrimaryType = nameof(Details), + }; + } + + public static string GetCancelOrdersEIP712Payload(List orders, BigInteger chainId, EthECKey key) + { + var typedData = GetTypedDefinition(chainId); + + var mail = new Details + { + Message = PROMPT, + Orders = orders + }; + + return _signer.SignTypedDataV4(mail, typedData, key); + } + + } +} diff --git a/src/SignatureGeneration/SaltDomain.cs b/src/SignatureGeneration/SaltDomain.cs new file mode 100644 index 0000000..0de962a --- /dev/null +++ b/src/SignatureGeneration/SaltDomain.cs @@ -0,0 +1,22 @@ +using Nethereum.ABI.FunctionEncoding.Attributes; +using System.Numerics; + +namespace Nethereum.ABI.EIP712 +{ + [Struct("EIP712Domain")] + public class SaltDomain : IDomain + { + + [Parameter("string", "name", 1)] + public virtual string Name { get; set; } + + [Parameter("string", "version", 2)] + public virtual string Version { get; set; } + + [Parameter("uint256", "chainId", 3)] + public virtual BigInteger? ChainId { get; set; } + + [Parameter("bytes32", "salt", 4)] + public virtual byte[] Salt { get; set; } + } +} \ No newline at end of file diff --git a/src/Utils.cs b/src/Utils.cs new file mode 100644 index 0000000..286fce8 --- /dev/null +++ b/src/Utils.cs @@ -0,0 +1,17 @@ +using System.Numerics; +using System.Security.Cryptography; + +namespace SigningExampleNethereum { + + public static class Utilities { + + public static byte[] Random32Bytes() + { + byte[] number = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(number); + number[^1] &= (byte)0x7F; //force sign bit to positive + return number; + } + } +} \ No newline at end of file