bringin poc

This commit is contained in:
Kukks
2024-01-25 15:25:19 +01:00
parent 37be856d82
commit 1efe97d4e1
8 changed files with 324 additions and 41 deletions

View File

@@ -7,8 +7,8 @@
<!-- -->
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>Dynamic Reports</Product>
<Description>Allows you to create custom reports using SQL.</Description>
<Product>Bringin</Product>
<Description>Euro Offramp</Description>
<Version>1.0.0</Version>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
@@ -33,4 +33,9 @@
<EmbeddedResource Include="Resources\**" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Bringin\" />
</ItemGroup>
</Project>

View File

@@ -43,40 +43,57 @@ public class BringinClient
public async Task<CreateOrderResponse> 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<CreateOrderResponse>();
var error = JObject.Parse(responseContent).ToObject<BringinErrorResponse>();
throw new BringinException(error);
}
public async Task<RateResponse> GetOrderInfo(GetOrderRequest request)
public async Task<decimal> 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<RateResponse>();
if (response.IsSuccessStatusCode)
{
var balance = JObject.Parse(responseContent).ToObject<BalanceResponse>();
return balance.Balance / 100m; //response is in cents
}
var error = JObject.Parse(responseContent).ToObject<BringinErrorResponse>();
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; }
@@ -137,13 +153,3 @@ public class BringinClient
public JObject ErrorDetails { get; set; }
}
}
public class BringinException : Exception
{
private readonly BringinClient.BringinErrorResponse _error;
public BringinException(BringinClient.BringinErrorResponse error)
{
_error = error;
}
}

View File

@@ -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<RequiredAttribute>(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<string, IFormComponentProvider> 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<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config,
CancellationToken cancellationToken)
{
var bringinClient = FromConfig(config);
if (bringinClient is null)
throw new BadConfigException(Array.Empty<string>());
var balance = await bringinClient.GetFiatBalance();
return new Dictionary<string, decimal>()
{
{"EUR", balance}
};
}
public async Task<Form> 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<string>(),
Required = true,
HelpText = "Enter your Bringin API Key which can be obtained from <a href=\"https://dev-app.bringin.xyz\" target=\"_blank\">here</a>"
});
// fieldset.Fields.Add(new Field()
// {
// Name = "server",
// Label = "Bringin Server (optional)",
// Type = "password",
// Value = config["server"]?.Value<string>(),
// 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<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config,
CancellationToken cancellationToken)
{
var amount = config["depositAddressConfig"]?.Value<decimal>();
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<string>() is { } server &&
// !string.IsNullOrEmpty(server))
// {
// backend = new Uri(server);
// }
var httpClient = _httpClientFactory.CreateClient("bringin");
httpClient.BaseAddress = backend;
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("api-key", apiKey.Value<string>());
return new BringinClient(apiKey.Value<string>(), httpClient);
}
return null;
}
}

View File

@@ -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;
}
}

View File

@@ -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<CustodianEnablerTask>();
applicationBuilder.AddSingleton<ICustodian, BringinCustodian>();
applicationBuilder.AddSingleton<IFormComponentProvider, BringinApiKeyFormComponentProvider>();
}
}

View File

@@ -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<PoliciesSettings>() ?? new PoliciesSettings();
if(policySettings.Experimental)
return;
policySettings.Experimental = true;
await _settingsRepository.UpdateSetting(policySettings);
}
}

View File

@@ -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;
}
<div class="form-group">
<label class="form-label" for="@Model.Name"@(Model.Required ? " data-required" : "")>
@Safe.Raw(Model.Label)
</label>
<input id="@Model.Name" type="password" class="form-control @(errors is null ? "" : "is-invalid")"
name="@Model.Name" value="@Model.Value" data-val="true" readonly="@Model.Constant"
@if (!string.IsNullOrEmpty(Model.HelpText))
{
@Safe.Raw($" aria-describedby=\"HelpText-{Model.Name}\"")
}
@if (Model.Required)
{
@Safe.Raw($" data-val-required=\"{Model.Label} is required.\" required")
}
/>
<span class="text-danger" data-valmsg-for="@Model.Name" data-valmsg-replace="true">@(isInvalid && errors.Any() ? errors.First().ErrorMessage : string.Empty)</span>
@if (!string.IsNullOrEmpty(Model.HelpText))
{
<div id="@($"HelpText-{Model.Name}")" class="form-text">
@Safe.Raw(Model.HelpText)
</div>
}
</div>

View File

@@ -0,0 +1,5 @@
@using BTCPayServer.Abstractions.Services
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer
@addTagHelper *, BTCPayServer.Abstractions