using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Events; using BTCPayServer.HostedServices; using BTCPayServer.Services.Invoices; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BTCPayServer.Plugins.SideShift { public class SideShiftService:EventHostedServiceBase { private readonly InvoiceRepository _invoiceRepository; private readonly ISettingsRepository _settingsRepository; private readonly IMemoryCache _memoryCache; private readonly IStoreRepository _storeRepository; private readonly IHttpClientFactory _httpClientFactory; private readonly JsonSerializerSettings _serializerSettings; public SideShiftService( InvoiceRepository invoiceRepository, ISettingsRepository settingsRepository, IMemoryCache memoryCache, IStoreRepository storeRepository, IHttpClientFactory httpClientFactory, ILogger logger, EventAggregator eventAggregator, JsonSerializerSettings serializerSettings) : base(eventAggregator, logger) { _invoiceRepository = invoiceRepository; _settingsRepository = settingsRepository; _memoryCache = memoryCache; _storeRepository = storeRepository; _httpClientFactory = httpClientFactory; _serializerSettings = serializerSettings; } protected override void SubscribeToEvents() { Subscribe(); base.SubscribeToEvents(); } protected override Task ProcessEvent(object evt, CancellationToken cancellationToken) { if (evt is InvoiceEvent invoiceEvent && invoiceEvent.EventCode == InvoiceEventCode.Created) { var invoiceSettings = GetSideShiftSettingsFromInvoice(invoiceEvent.Invoice); if (invoiceSettings is not null) { var cacheKey = CreateCacheKeyForInvoice(invoiceEvent.InvoiceId); var entry = _memoryCache.CreateEntry(cacheKey); entry.AbsoluteExpiration = invoiceEvent.Invoice.ExpirationTime; entry.Value = invoiceSettings; } } return base.ProcessEvent(evt, cancellationToken); } public async Task GetSideShiftForInvoice(string invoiceId, string storeId) { var cacheKey = CreateCacheKeyForInvoice(invoiceId); var invoiceSettings = await _memoryCache.GetOrCreateAsync(cacheKey, async entry => { var invoice = await _invoiceRepository.GetInvoice(invoiceId); entry.AbsoluteExpiration = invoice?.ExpirationTime; return GetSideShiftSettingsFromInvoice(invoice); }); var storeSettings = await GetSideShiftForStore(storeId); if (invoiceSettings is null) { return storeSettings; } if (storeSettings is null) { return invoiceSettings.ToObject();; } var storeSettingsJObject = JObject.FromObject(storeSettings, JsonSerializer.Create(_serializerSettings)); storeSettingsJObject.Merge(invoiceSettings); return storeSettingsJObject.ToObject(); } private string CreateCacheKeyForInvoice(string invoiceId) => $"{nameof(SideShiftSettings)}_{invoiceId}"; private JObject? GetSideShiftSettingsFromInvoice(InvoiceEntity invoice) { return invoice?.Metadata.GetAdditionalData("sideshift"); } public async Task GetSideShiftForStore(string storeId) { if (storeId is null) { return null; } var k = $"{nameof(SideShiftSettings)}_{storeId}"; return await _memoryCache.GetOrCreateAsync(k, async _ => { var res = await _storeRepository.GetSettingAsync(storeId, nameof(SideShiftSettings)); if (res is not null) return res; res = await _settingsRepository.GetSettingAsync(k); if (res is not null) { await SetSideShiftForStore(storeId, res); } await _settingsRepository.UpdateSetting(null, k); return res; }); } public async Task SetSideShiftForStore(string storeId, SideShiftSettings SideShiftSettings) { var k = $"{nameof(SideShiftSettings)}_{storeId}"; await _storeRepository.UpdateSetting(storeId, nameof(SideShiftSettings), SideShiftSettings); _memoryCache.Set(k, SideShiftSettings); } public async Task> GetSettleCoins() { return await _memoryCache.GetOrCreateAsync>("sideshift-coins", async entry => { var client = _httpClientFactory.CreateClient("sideshift"); var request = new HttpRequestMessage(HttpMethod.Get, "https://sideshift.ai/api/v2/coins"); var response = await client.SendAsync(request); var result = new List(); if (!response.IsSuccessStatusCode) { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); return result; } var coins = await response.Content.ReadAsStringAsync().ContinueWith(t => JsonConvert.DeserializeObject>(t.Result)); coins.ForEach(coin => { Array.ForEach (coin.networks,network => { if(coin.settleOffline.Type == JTokenType.Boolean && coin.settleOffline.Value()) return; if (coin.settleOffline is JArray settleOfflineArray && settleOfflineArray.Any(v => v.Value() == network)) { return; } var coinType = CoinType.Both; if (coin.fixedOnly.Type == JTokenType.Boolean && coin.fixedOnly.Value()) { coinType = CoinType.FixedOnly; } else if (coin.fixedOnly is JArray fixedOnlyArray && fixedOnlyArray.Any(v => v.Value() == network)) { coinType = CoinType.FixedOnly; } else if (coin.variableOnly.Type == JTokenType.Boolean && coin.variableOnly.Value()) { coinType = CoinType.VariableOnly; } else if (coin.variableOnly is JArray variableOnlyArray && variableOnlyArray.Any(v => v.Value() == network)) { coinType = CoinType.VariableOnly; } result.Add(new SideshiftSettleCoin() { Id = $"{coin.coin}_{network}", CryptoCode = coin.coin, Network = network, DisplayName = coin.name, HasMemo = coin.hasMemo, Type = coinType, }); }); }); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); entry.Value = result; return entry.Value as List; }); } public enum CoinType { FixedOnly, VariableOnly, Both } public class SideshiftSettleCoin:SideshiftDepositCoin { public bool HasMemo { get; set; } } public class SideshiftDepositCoin { public string DisplayName { get; set; } public string Id { get; set; } public CoinType Type { get; set; } public string CryptoCode { get; set; } public string Network { get; set; } public override string ToString() { return $"{DisplayName} {(DisplayName.Equals(Network, StringComparison.InvariantCultureIgnoreCase)? string.Empty: $"({Network})")}"; } } public async Task> GetDepositOptions() { return (List) await _memoryCache.GetOrCreateAsync("sideshift-deposit", async entry => { var client = _httpClientFactory.CreateClient("sideshift"); var request = new HttpRequestMessage(HttpMethod.Get, "https://sideshift.ai/api/v1/facts"); var response = await client.SendAsync(request); var result = new List(); if (!response.IsSuccessStatusCode) { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); return result; } var coins = await response.Content.ReadAsStringAsync().ContinueWith(t => JsonConvert.DeserializeObject(t.Result)); foreach (var asset in coins["depositMethods"].Children()) { if (asset.Value["enabled"].Value() is not true) { continue; } var id = asset.Name; var displayName = asset.Value["displayName"].Value(); var coinType = asset.Value["fixedOnly"].Value() ? CoinType.FixedOnly : asset.Value["variableOnly"].Value()? CoinType.VariableOnly : CoinType.Both; var network = asset.Value["network"].Value(); var cryptoCode = asset.Value["asset"].Value(); result.Add(new SideshiftDepositCoin() { Id = id, DisplayName = displayName, Type = coinType, Network = network, CryptoCode = cryptoCode, }); } entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); entry.Value = result; return entry.Value; }); } } }