diff --git a/swappy-bot/Commands/AssetInfo.cs b/swappy-bot/Commands/AssetInfo.cs new file mode 100644 index 0000000..42c159f --- /dev/null +++ b/swappy-bot/Commands/AssetInfo.cs @@ -0,0 +1,46 @@ +namespace SwappyBot.Commands +{ + using System; + + public class AssetInfo + { + public string Id { get; } + public string Ticker { get; } + public string Name { get; } + public string Network { get; } + public int Decimals { get; } + public double MinimumAmount { get; } + public double MaximumAmount { get; } + public double[] SuggestedAmounts { get; } + public string FormatString { get; } + public Func AddressValidator { get; } + public Func AddressConverter { get; } + + public AssetInfo( + string id, + string ticker, + string name, + string network, + int decimals, + double minimumAmount, + double maximumAmount, + double[] suggestedAmounts, + Func addressValidator, + Func addressConverter) + { + Id = id; + Ticker = ticker; + Name = name; + Network = network; + Decimals = decimals; + MinimumAmount = minimumAmount; + MaximumAmount = maximumAmount; + SuggestedAmounts = suggestedAmounts; + AddressValidator = addressValidator; + AddressConverter = addressConverter; + + FormatString = $"0.00{new string('#', decimals - 2)}"; + + } + } +} \ No newline at end of file diff --git a/swappy-bot/Commands/Assets.cs b/swappy-bot/Commands/Assets.cs new file mode 100644 index 0000000..d7a9c2c --- /dev/null +++ b/swappy-bot/Commands/Assets.cs @@ -0,0 +1,97 @@ +namespace SwappyBot.Commands +{ + using System.Collections.Generic; + using Nethereum.Util; + using SwappyBot.Commands.Swap; + using SwappyBot.Infrastructure; + + public static class Assets + { + public static readonly Dictionary SupportedAssets = new() + { + { + "btc", + new AssetInfo( + "btc", + "BTC", + "Bitcoin", + "Bitcoin", + 8, + 0.0007, + 0.65, + [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5], + x => AddressValidator.IsValidAddress(x, "btc"), + x => x) + }, + + { + "dot", + new AssetInfo( + "dot", + "DOT", + "Polkadot", + "Polkadot", + 10, + 4, + 4_100, + [10, 20, 50, 150, 300, 700, 1000, 2000, 4000], + x => true, + hex => hex.ConvertToSs58()) + }, + + { + "eth", + new AssetInfo( + "eth", + "ETH", + "Ethereum", + "Ethereum", + 18, + 0.01, + 11, + [0.02, 0.04, 0.1, 0.2, 0.5, 1, 2, 5, 10], + x => AddressUtil.Current.IsNotAnEmptyAddress(x) && + AddressUtil.Current.IsValidAddressLength(x) && + AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && + (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper()), + x => x) + }, + + { + "flip", + new AssetInfo( + "flip", + "FLIP", + "Chainflip", + "Ethereum", + 18, + 4, + 5_700, + [10, 20, 50, 150, 300, 1000, 2000, 4000, 5500], + x => AddressUtil.Current.IsNotAnEmptyAddress(x) && + AddressUtil.Current.IsValidAddressLength(x) && + AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && + (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper()), + x => x) + }, + + { + "usdc", + new AssetInfo( + "usdc", + "USDC", + "ethUSDC", + "Ethereum", + 6, + 20, + 25_000, + [25, 50, 100, 500, 1000, 2500, 5000, 10000, 20000], + x => AddressUtil.Current.IsNotAnEmptyAddress(x) && + AddressUtil.Current.IsValidAddressLength(x) && + AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && + (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper()), + x => x) + }, + }; + } +} \ No newline at end of file diff --git a/swappy-bot/Commands/ConvertToSs58Extension.cs b/swappy-bot/Commands/ConvertToSs58Extension.cs new file mode 100644 index 0000000..efdb426 --- /dev/null +++ b/swappy-bot/Commands/ConvertToSs58Extension.cs @@ -0,0 +1,50 @@ +namespace SwappyBot.Commands +{ + using System; + using System.Linq; + using SimpleBase; + + public static class ConvertToSs58Extension + { + // First 00 = Polkadot + // Console.WriteLine("008dc56c5e01ddbbea748ab139d18e5cfc894955967a62599311d3ad7bb7571dca".ConvertToSs58()); + // 14CtQ8HLKxSwtwYGc7mvSMEW7wt6maGhhEqrWezg8Y3XpZuV + + // Console.WriteLine("00d513a8eb412b6bdffb39d9ebbe4edd7afad16b7bf9fa89fa225e084896757b35".ConvertToSs58()); + // 15pP3JL915qcrRpYu7H1dqn87MS3WE8nc6ivKYabD54HaBcu + + public static string ConvertToSs58(this string hex) + { + var publicKeyBytes = StringToByteArray(hex); + var ss58AddressBytes = Ss58Hash(publicKeyBytes); + return Base58.Bitcoin.Encode(ss58AddressBytes); + } + + private static byte[] StringToByteArray(string hex) => + Enumerable + .Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + + private static byte[] Ss58Hash(byte[] data) + { + var dataLength = data.Length; + const int prefixLength = 7; + var ssPrefix = "SS58PRE"u8.ToArray(); + + var ssPrefixed = new byte[dataLength + prefixLength]; + Buffer.BlockCopy(ssPrefix, 0, ssPrefixed, 0, prefixLength); + Buffer.BlockCopy(data, 0, ssPrefixed, prefixLength, dataLength); + + var hash = Blake2Core.Blake2B.ComputeHash(ssPrefixed); + + var ss58 = new byte[data.Length + 2]; + Buffer.BlockCopy(data, 0, ss58, 0, dataLength); + ss58[dataLength] = hash[0]; + ss58[dataLength + 1] = hash[1]; + + return ss58; + } + } +} \ No newline at end of file diff --git a/swappy-bot/Commands/Quote.cs b/swappy-bot/Commands/Quote.cs new file mode 100644 index 0000000..0e394e1 --- /dev/null +++ b/swappy-bot/Commands/Quote.cs @@ -0,0 +1,80 @@ +namespace SwappyBot.Commands +{ + using System; + using System.Globalization; + using System.Net.Http; + using System.Net.Http.Json; + using System.Text.Json.Serialization; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using SwappyBot.Configuration; + + public static class Quote + { + public static async Task GetQuoteAsync( + ILogger logger, + BotConfiguration configuration, + IHttpClientFactory httpClientFactory, + double amount, + AssetInfo assetFrom, + AssetInfo assetTo) + { + // https://chainflip-swap.chainflip.io/quote?amount=1500000000000000000&srcAsset=ETH&destAsset=BTC + using var client = httpClientFactory.CreateClient("Quote"); + + var commissionPercent = (double)configuration.CommissionBps / 100; + var commission = amount * (commissionPercent / 100); + var ingressAmount = amount - commission; + var convertedAmount = ingressAmount * Math.Pow(10, assetFrom.Decimals); + + var quoteRequest = + $"quote?amount={convertedAmount:0}&srcAsset={assetFrom.Ticker}&destAsset={assetTo.Ticker}"; + var quoteResponse = await client.GetAsync(quoteRequest); + + if (quoteResponse.IsSuccessStatusCode) + { + var quote = await quoteResponse.Content.ReadFromJsonAsync(); + quote.IngressAmount = convertedAmount.ToString(CultureInfo.InvariantCulture); + return quote; + } + + logger.LogError( + "Quote API returned {StatusCode}: {Error}\nRequest: {QuoteRequest}", + quoteResponse.StatusCode, + await quoteResponse.Content.ReadAsStringAsync(), + quoteRequest); + + return null; + } + } + + public class QuoteResponse + { + [JsonIgnore] + public string IngressAmount { get; set; } + + [JsonPropertyName("egressAmount")] + public string EgressAmount { get; set; } + + [JsonPropertyName("intermediateAmount")] + public string IntermediateAmount { get; set; } + + [JsonPropertyName("includedFees")] + public QuoteFees[] Fees { get; set; } + } + + public class QuoteFees + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("chain")] + public string Chain { get; set; } + + [JsonPropertyName("asset")] + public string Asset { get; set; } + + [JsonPropertyName("amount")] + public string Amount { get; set; } + } +} \ No newline at end of file diff --git a/swappy-bot/Commands/Swap/AssetInfo.cs b/swappy-bot/Commands/Swap/AssetInfo.cs deleted file mode 100644 index a7b1be7..0000000 --- a/swappy-bot/Commands/Swap/AssetInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SwappyBot.Commands.Swap -{ - using System; - - public record AssetInfo( - string Id, - string Ticker, - string Name, - string Network, - int Decimals, - double MinimumAmount, - double MaximumAmount, - double[] SuggestedAmounts, - string FormatString, - Func AddressValidator); -} \ No newline at end of file diff --git a/swappy-bot/Commands/Swap/ChainId.cs b/swappy-bot/Commands/Swap/ChainId.cs deleted file mode 100644 index 3b2bf79..0000000 --- a/swappy-bot/Commands/Swap/ChainId.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace SwappyBot.Commands.Swap -{ - using System.Text.Json.Serialization; - - public class ChainId - { - [JsonPropertyName("chain")] - public string Network { get; } - - [JsonPropertyName("asset")] - public string Asset { get; } - - public ChainId( - string network, - string asset) - { - Network = network; - Asset = asset; - } - } -} \ No newline at end of file diff --git a/swappy-bot/Commands/Swap/DepositAddress.cs b/swappy-bot/Commands/Swap/DepositAddress.cs index 4cf729a..10879fb 100644 --- a/swappy-bot/Commands/Swap/DepositAddress.cs +++ b/swappy-bot/Commands/Swap/DepositAddress.cs @@ -49,4 +49,21 @@ public DepositAddressRequest( ]; } } + + public class ChainId + { + [JsonPropertyName("chain")] + public string Network { get; } + + [JsonPropertyName("asset")] + public string Asset { get; } + + public ChainId( + string network, + string asset) + { + Network = network; + Asset = asset; + } + } } \ No newline at end of file diff --git a/swappy-bot/Commands/Swap/Quote.cs b/swappy-bot/Commands/Swap/Quote.cs deleted file mode 100644 index 3aa2257..0000000 --- a/swappy-bot/Commands/Swap/Quote.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace SwappyBot.Commands.Swap -{ - using System.Text.Json.Serialization; - - public class QuoteResponse - { - [JsonIgnore] - public string IngressAmount { get; set; } - - [JsonPropertyName("egressAmount")] - public string EgressAmount { get; set; } - - [JsonPropertyName("intermediateAmount")] - public string IntermediateAmount { get; set; } - - [JsonPropertyName("includedFees")] - public QuoteFees[] Fees { get; set; } - } - - public class QuoteFees - { - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("chain")] - public string Chain { get; set; } - - [JsonPropertyName("asset")] - public string Asset { get; set; } - - [JsonPropertyName("amount")] - public string Amount { get; set; } - } -} \ No newline at end of file diff --git a/swappy-bot/Commands/Swap/Swap.cs b/swappy-bot/Commands/Swap/Swap.cs index 6fb14b4..675cc8c 100644 --- a/swappy-bot/Commands/Swap/Swap.cs +++ b/swappy-bot/Commands/Swap/Swap.cs @@ -1,8 +1,6 @@ namespace SwappyBot.Commands.Swap { using System; - using System.Collections.Generic; - using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Json; @@ -12,101 +10,12 @@ namespace SwappyBot.Commands.Swap using Discord.WebSocket; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; - using Nethereum.Util; using SwappyBot.Configuration; using SwappyBot.EntityFramework; - using SwappyBot.Infrastructure; using Emoji = Discord.Emoji; public class Swap : InteractionModuleBase { - private static readonly Dictionary SupportedAssets = new() - { - { - "btc", - new AssetInfo( - "btc", - "BTC", - "Bitcoin", - "Bitcoin", - 8, - 0.0007, - 0.65, - [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5], - $"0.00{new string('#', 6)}", - x => AddressValidator.IsValidAddress(x, "btc")) - }, - - { - "dot", - new AssetInfo( - "dot", - "DOT", - "Polkadot", - "Polkadot", - 10, - 4, - 4_100, - [10, 20, 50, 150, 300, 700, 1000, 2000, 4000], - $"0.00{new string('#', 8)}", - x => true) - }, - - { - "eth", - new AssetInfo( - "eth", - "ETH", - "Ethereum", - "Ethereum", - 18, - 0.01, - 11, - [0.02, 0.04, 0.1, 0.2, 0.5, 1, 2, 5, 10], - $"0.00{new string('#', 16)}", - x => AddressUtil.Current.IsNotAnEmptyAddress(x) && - AddressUtil.Current.IsValidAddressLength(x) && - AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && - (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper())) - }, - - { - "flip", - new AssetInfo( - "flip", - "FLIP", - "Chainflip", - "Ethereum", - 18, - 4, - 5_700, - [10, 20, 50, 150, 300, 1000, 2000, 4000, 5500], - $"0.00{new string('#', 16)}", - x => AddressUtil.Current.IsNotAnEmptyAddress(x) && - AddressUtil.Current.IsValidAddressLength(x) && - AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && - (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper())) - }, - - { - "usdc", - new AssetInfo( - "usdc", - "USDC", - "ethUSDC", - "Ethereum", - 6, - 20, - 25_000, - [25, 50, 100, 500, 1000, 2500, 5000, 10000, 20000], - $"0.00{new string('#', 4)}", - x => AddressUtil.Current.IsNotAnEmptyAddress(x) && - AddressUtil.Current.IsValidAddressLength(x) && - AddressUtil.Current.IsValidEthereumAddressHexFormat(x) && - (AddressUtil.Current.IsChecksumAddress(x) || x == x.ToLower() || x[2..] == x[2..].ToUpper())) - }, - }; - private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly BotConfiguration _configuration; @@ -226,7 +135,7 @@ public async Task SwapStep2( await DeferAsync(ephemeral: true); var data = ((SocketMessageComponent)Context.Interaction).Data.Values.First(); - var assetFrom = SupportedAssets[data]; + var assetFrom = Assets.SupportedAssets[data]; _logger.LogInformation( "[{StateId}] Chose {SourceAsset} as source asset", @@ -267,8 +176,8 @@ public async Task SwapStep3( var swapState = await _dbContext.SwapState.FindAsync(stateId); var data = ((SocketMessageComponent)Context.Interaction).Data.Values.First(); - var assetFrom = SupportedAssets[swapState.AssetFrom]; - var assetTo = SupportedAssets[data]; + var assetFrom = Assets.SupportedAssets[swapState.AssetFrom]; + var assetTo = Assets.SupportedAssets[data]; _logger.LogInformation( "[{StateId}] Chose {DestinationAsset} as destination asset", @@ -335,8 +244,8 @@ public async Task SwapStep4( var swapState = await _dbContext.SwapState.FindAsync(stateId); - var assetFrom = SupportedAssets[swapState.AssetFrom]; - var assetTo = SupportedAssets[swapState.AssetTo]; + var assetFrom = Assets.SupportedAssets[swapState.AssetFrom]; + var assetTo = Assets.SupportedAssets[swapState.AssetTo]; _logger.LogInformation( "[{StateId}] Provided {Amount} as amount", @@ -398,7 +307,10 @@ await Context.Channel.SendMessageAsync( assetFrom.Ticker, assetTo.Ticker); - var quote = await GetQuoteAsync( + var quote = await Quote.GetQuoteAsync( + _logger, + _configuration, + _httpClientFactory, amount, assetFrom, assetTo); @@ -509,8 +421,8 @@ public async Task SwapStep5b( var swapState = await _dbContext.SwapState.FindAsync(stateId); - var assetFrom = SupportedAssets[swapState.AssetFrom]; - var assetTo = SupportedAssets[swapState.AssetTo]; + var assetFrom = Assets.SupportedAssets[swapState.AssetFrom]; + var assetTo = Assets.SupportedAssets[swapState.AssetTo]; await ModifyOriginalResponseAsync(x => x.Components = BuildAddressButton( @@ -562,7 +474,10 @@ await Context.Channel.SendMessageAsync( assetFrom.Ticker, assetTo.Ticker); - var quote = await GetQuoteAsync( + var quote = await Quote.GetQuoteAsync( + _logger, + _configuration, + _httpClientFactory, swapState.Amount.Value, assetFrom, assetTo); @@ -654,8 +569,8 @@ await Context.Channel.SendMessageAsync( var swapState = await _dbContext.SwapState.FindAsync(stateId); - var assetFrom = SupportedAssets[swapState.AssetFrom]; - var assetTo = SupportedAssets[swapState.AssetTo]; + var assetFrom = Assets.SupportedAssets[swapState.AssetFrom]; + var assetTo = Assets.SupportedAssets[swapState.AssetTo]; swapState.SwapAccepted = DateTimeOffset.UtcNow; @@ -908,12 +823,12 @@ private static MessageComponent BuildAssetSelect( .WithMaxValues(1) .WithDisabled(!enabled); - foreach (var asset in SupportedAssets.Keys) + foreach (var asset in Assets.SupportedAssets.Keys) { if (excludeAsset != null && asset == excludeAsset.Id) continue; - var assetInfo = SupportedAssets[asset]; + var assetInfo = Assets.SupportedAssets[asset]; assetsSelect = assetsSelect .AddOption( @@ -942,38 +857,6 @@ private static MessageComponent BuildSelectedAssetSelect( .Build(); } - private async Task GetQuoteAsync( - double amount, - AssetInfo assetFrom, - AssetInfo assetTo) - { - // https://chainflip-swap.chainflip.io/quote?amount=1500000000000000000&srcAsset=ETH&destAsset=BTC - using var client = _httpClientFactory.CreateClient("Quote"); - - var commissionPercent = (double)_configuration.CommissionBps / 100; - var commission = amount * (commissionPercent / 100); - var ingressAmount = amount - commission; - var convertedAmount = ingressAmount * Math.Pow(10, assetFrom.Decimals); - - var quoteRequest = $"quote?amount={convertedAmount:0}&srcAsset={assetFrom.Ticker}&destAsset={assetTo.Ticker}"; - var quoteResponse = await client.GetAsync(quoteRequest); - - if (quoteResponse.IsSuccessStatusCode) - { - var quote = await quoteResponse.Content.ReadFromJsonAsync(); - quote.IngressAmount = convertedAmount.ToString(CultureInfo.InvariantCulture); - return quote; - } - - _logger.LogError( - "Quote API returned {StatusCode}: {Error}\nRequest: {QuoteRequest}", - quoteResponse.StatusCode, - await quoteResponse.Content.ReadAsStringAsync(), - quoteRequest); - - return null; - } - private async Task GetDepositChannelAsync( AssetInfo assetFrom, AssetInfo assetTo, diff --git a/swappy-bot/Program.cs b/swappy-bot/Program.cs index 0a230eb..904d4b3 100644 --- a/swappy-bot/Program.cs +++ b/swappy-bot/Program.cs @@ -7,7 +7,6 @@ using Autofac; using Autofac.Extensions.DependencyInjection; using Discord; - using Discord.Commands; using Discord.Interactions; using Discord.WebSocket; using SwappyBot.Configuration; diff --git a/swappy-bot/swappy-bot.csproj b/swappy-bot/swappy-bot.csproj index 96f4a40..3693ea4 100644 --- a/swappy-bot/swappy-bot.csproj +++ b/swappy-bot/swappy-bot.csproj @@ -49,6 +49,7 @@ + @@ -74,5 +75,6 @@ +