From 1efe97d4e17c08d81000664af86acada34f4b60a Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 25 Jan 2024 15:25:19 +0100 Subject: [PATCH] bringin poc --- .../BTCPayServer.Plugins.Bringin.csproj | 9 +- .../{Class1.cs => BringinClient.cs} | 84 +++++---- .../BringinCustodian.cs | 176 ++++++++++++++++++ .../BringinException.cs | 13 ++ .../BringinPlugin.cs | 26 +++ .../CustodianEnablerTask.cs | 24 +++ .../Views/Shared/Bringin/ApiKeyElement.cshtml | 28 +++ .../_ViewImports.cshtml | 5 + 8 files changed, 324 insertions(+), 41 deletions(-) rename Plugins/BTCPayServer.Plugins.Bringin/{Class1.cs => BringinClient.cs} (70%) create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/BringinCustodian.cs create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/BringinException.cs create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/BringinPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/CustodianEnablerTask.cs create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/Views/Shared/Bringin/ApiKeyElement.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Bringin/_ViewImports.cshtml diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj b/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj index 97dfcba..0b916b0 100644 --- a/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj +++ b/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj @@ -7,8 +7,8 @@ - Dynamic Reports - Allows you to create custom reports using SQL. + Bringin + Euro Offramp 1.0.0 true @@ -33,4 +33,9 @@ + + + + + diff --git a/Plugins/BTCPayServer.Plugins.Bringin/Class1.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinClient.cs similarity index 70% rename from Plugins/BTCPayServer.Plugins.Bringin/Class1.cs rename to Plugins/BTCPayServer.Plugins.Bringin/BringinClient.cs index 99295df..42012a4 100644 --- a/Plugins/BTCPayServer.Plugins.Bringin/Class1.cs +++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinClient.cs @@ -43,40 +43,57 @@ public class BringinClient public async Task PlaceOrder(CreateOrderRequest request) { var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - var response = await HttpClient.PostAsync($"/api/v0/offramp/order/lightning", content); + var response = await HttpClient.PostAsync($"/api/v0/offramp/order", content); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) return JObject.Parse(responseContent).ToObject(); var error = JObject.Parse(responseContent).ToObject(); throw new BringinException(error); } - public async Task GetOrderInfo(GetOrderRequest request) + public async Task GetFiatBalance(string currency = "EUR") { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - var response = await HttpClient.PostAsync($"/api/v0/offramp/order/lightning", content); + var content = new StringContent(JsonConvert.SerializeObject(new + { + currency + }), Encoding.UTF8, "application/json"); + var response = await HttpClient.PostAsync($"/api/v0/user/get-balance/fiat", content); var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) return JObject.Parse(responseContent).ToObject(); + if (response.IsSuccessStatusCode) + { + var balance = JObject.Parse(responseContent).ToObject(); + return balance.Balance / 100m; //response is in cents + } var error = JObject.Parse(responseContent).ToObject(); throw new BringinException(error); } - public class GetOrderResponse + public class BalanceResponse { - public string OrderId { get; set; } - public string Status { get; set; } - public string SubType { get; set; } - + [JsonProperty("balance")] [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal SourceAmount { get; set; } - - public string SourceCurrency { get; set; } - - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal DestinationAmount { get; set; } - - public string DestinationCurrency { get; set; } - public BringinPrice BringinPrice { get; set; } + public decimal Balance { + get; + set; + } } + // + // public class GetOrderResponse + // { + // public string OrderId { get; set; } + // public string Status { get; set; } + // public string SubType { get; set; } + // + // [JsonConverter(typeof(NumericStringJsonConverter))] + // public decimal SourceAmount { get; set; } + // + // public string SourceCurrency { get; set; } + // + // [JsonConverter(typeof(NumericStringJsonConverter))] + // public decimal DestinationAmount { get; set; } + // + // public string DestinationCurrency { get; set; } + // public BringinPrice BringinPrice { get; set; } + // } public class BringinPrice { @@ -86,32 +103,31 @@ public class BringinClient public string Currency { get; set; } } - public class GetOrderRequest - { - public string UserId { get; set; } - public string OrderId { get; set; } - } public class CreateOrderResponse { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("amount")] [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } + [JsonProperty("invoice")] public string Invoice { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] [JsonProperty("expiresAt")] - public string Expiry { get; set; } + public long Expiry { get; set; } } public class CreateOrderRequest - { + { [JsonProperty("sourceAmount")] [JsonConverter(typeof(NumericStringJsonConverter))] public decimal SourceAmount { get; set; } [JsonProperty("ipAddress")] public string IP { get; set; } + [JsonProperty("paymentMethod")] + public string PaymentMethod { get; set; } } public class RateResponse @@ -119,8 +135,8 @@ public class BringinClient public string Ticker { get; set; } public string Currency { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public string Timestamp { get; set; } + + public long Timestamp { get; set; } [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Price { get; set; } @@ -136,14 +152,4 @@ public class BringinClient public string ErrorCode { get; set; } public JObject ErrorDetails { get; set; } } -} - -public class BringinException : Exception -{ - private readonly BringinClient.BringinErrorResponse _error; - - public BringinException(BringinClient.BringinErrorResponse error) - { - _error = error; - } } \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BringinCustodian.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinCustodian.cs new file mode 100644 index 0000000..9133fef --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinCustodian.cs @@ -0,0 +1,176 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Custodians; +using BTCPayServer.Abstractions.Form; +using BTCPayServer.Client.Models; +using BTCPayServer.Forms; +using BTCPayServer.Payments; +using NBitcoin; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.Bringin; + +public class BringinApiKeyFormComponentProvider:FormComponentProviderBase +{ + private readonly IHttpClientFactory _httpClientFactory; + public override string View => "Bringin/ApiKeyElement"; + + public BringinApiKeyFormComponentProvider(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public override void Validate(Form form, Field field) + { + if (field.Required) + { + ValidateField(field); + } + if(field.ValidationErrors.Any()) + return; + var httpClient = _httpClientFactory.CreateClient("bringin"); + httpClient.BaseAddress = new Uri("https://dev.bringin.xyz"); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("api-key", GetValue(form, field)); + try + { + var userId = new BringinClient(GetValue(form, field), httpClient).GetUserId().GetAwaiter().GetResult(); + if(userId is null) + field.ValidationErrors.Add("Invalid API Key"); + } + catch (Exception e) + { + field.ValidationErrors.Add("Invalid API Key"); + } + } + + public override void Register(Dictionary typeToComponentProvider) + { + typeToComponentProvider.Add("bringin-apikey", this); + } +} + + +public class BringinCustodian : ICustodian, ICanDeposit +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly FormDataService _formDataService; + public string Code => "bringin"; + + public string Name => "Bringin"; + + public BringinCustodian(IHttpClientFactory httpClientFactory, FormDataService formDataService) + { + _httpClientFactory = httpClientFactory; + _formDataService = formDataService; + } + + public async Task> GetAssetBalancesAsync(JObject config, + CancellationToken cancellationToken) + { + var bringinClient = FromConfig(config); + if (bringinClient is null) + throw new BadConfigException(Array.Empty()); + + var balance = await bringinClient.GetFiatBalance(); + return new Dictionary() + { + {"EUR", balance} + }; + } + + public async Task
GetConfigForm(JObject config, CancellationToken cancellationToken = default) + { + + var f = new Form(); + + var fieldset = Field.CreateFieldset(); + fieldset.Label = "Connection details"; + fieldset.Fields.Add(new Field() + { + Name = "apiKey", + Label = "API Key", + Type = "bringin-apikey", + Value = config["apiKey"]?.Value(), + Required = true, + HelpText = "Enter your Bringin API Key which can be obtained from here" + }); + // fieldset.Fields.Add(new Field() + // { + // Name = "server", + // Label = "Bringin Server (optional)", + // Type = "password", + // Value = config["server"]?.Value(), + // Required = false, + // OriginalValue = "https://dev.bringin.xyz", + // HelpText = "Enter the Bringin server URL. This is optional and defaults to https://dev.bringin.xyz" + // }); + // + f.Fields.Add(fieldset); + return f; + } + + public async Task GetDepositAddressAsync(string paymentMethod, JObject config, + CancellationToken cancellationToken) + { + var amount = config["depositAddressConfig"]?.Value(); + + var bringinClient = FromConfig(config); + if (bringinClient is null) + throw new Exception("Invalid API Key"); + if (amount is null or <= 0) + { + var rate = await bringinClient.GetRate(); + //rate.bringinRate is the price for 1 BTC in EUR + //100eur = 1/rate.bringinRate BTC + amount = 100m / rate.BringinPrice; + } + // var rate = await bringinClient.GetRate(); + var host = await Dns.GetHostEntryAsync(Dns.GetHostName(), cancellationToken); + var ipToUse = host.AddressList.FirstOrDefault(address => address.AddressFamily == AddressFamily.InterNetwork)?.ToString(); + var request = new BringinClient.CreateOrderRequest() + { + SourceAmount = Money.Coins(amount.Value).Satoshi, + IP = ipToUse, + PaymentMethod = "LIGHTNING" + }; + var order = await bringinClient.PlaceOrder(request); + return new DepositAddressData() + { + Address = order.Invoice + }; + } + + public string[] GetDepositablePaymentMethods() + { + return new[] {new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString()}; + } + + private BringinClient? FromConfig(JObject config) + { + Uri backend = new Uri("https://dev.bringin.xyz"); + if (config.TryGetValue("apiKey", out var apiKey)) + { + // if (config.TryGetValue("server", out var serverToken) && serverToken.Value() is { } server && + // !string.IsNullOrEmpty(server)) + // { + // backend = new Uri(server); + // } + + var httpClient = _httpClientFactory.CreateClient("bringin"); + httpClient.BaseAddress = backend; + + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("api-key", apiKey.Value()); + return new BringinClient(apiKey.Value(), httpClient); + } + + return null; + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BringinException.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinException.cs new file mode 100644 index 0000000..106a1d2 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinException.cs @@ -0,0 +1,13 @@ +using System; + +namespace BTCPayServer.Plugins.Bringin; + +public class BringinException : Exception +{ + private readonly BringinClient.BringinErrorResponse _error; + + public BringinException(BringinClient.BringinErrorResponse error) + { + _error = error; + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BringinPlugin.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinPlugin.cs new file mode 100644 index 0000000..1c22955 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinPlugin.cs @@ -0,0 +1,26 @@ +using System; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Custodians; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Forms; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.Bringin; + +public class BringinPlugin : BaseBTCPayServerPlugin +{ + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() {Identifier = nameof(BTCPayServer), Condition = ">=1.12.0"} + }; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddStartupTask(); + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(); + + } + +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Bringin/CustodianEnablerTask.cs b/Plugins/BTCPayServer.Plugins.Bringin/CustodianEnablerTask.cs new file mode 100644 index 0000000..d09bb98 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/CustodianEnablerTask.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Services; + +namespace BTCPayServer.Plugins.Bringin; + +public class CustodianEnablerTask: IStartupTask +{ + private readonly SettingsRepository _settingsRepository; + + public CustodianEnablerTask(SettingsRepository settingsRepository) + { + _settingsRepository = settingsRepository; + } + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var policySettings = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); + if(policySettings.Experimental) + return; + policySettings.Experimental = true; + await _settingsRepository.UpdateSetting(policySettings); + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Bringin/Views/Shared/Bringin/ApiKeyElement.cshtml b/Plugins/BTCPayServer.Plugins.Bringin/Views/Shared/Bringin/ApiKeyElement.cshtml new file mode 100644 index 0000000..ea11d45 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/Views/Shared/Bringin/ApiKeyElement.cshtml @@ -0,0 +1,28 @@ +@model BTCPayServer.Abstractions.Form.Field +@{ + var isInvalid = ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid; + var errors = isInvalid ? ViewContext.ModelState[Model.Name].Errors : null; +} +
+ + + @(isInvalid && errors.Any() ? errors.First().ErrorMessage : string.Empty) + @if (!string.IsNullOrEmpty(Model.HelpText)) + { +
+ @Safe.Raw(Model.HelpText) +
+ } +
diff --git a/Plugins/BTCPayServer.Plugins.Bringin/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.Bringin/_ViewImports.cshtml new file mode 100644 index 0000000..d897d63 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Bringin/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using BTCPayServer.Abstractions.Services +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer +@addTagHelper *, BTCPayServer.Abstractions \ No newline at end of file