update bringin

This commit is contained in:
Kukks
2024-03-21 14:55:20 +01:00
parent 66b735da6c
commit 1c64038245
4 changed files with 178 additions and 49 deletions

View File

@@ -117,6 +117,71 @@ public class BringinClient
[JsonConverter(typeof(NumericStringJsonConverter))] [JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; } public decimal Balance { get; set; }
} }
public async Task<GetTransactionListResponse> GetTransactions()
{
var content = new StringContent(JsonConvert.SerializeObject(new
{
startDate = DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeMilliseconds(),
endDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
}), Encoding.UTF8, "application/json");
var response = await HttpClient.PostAsync($"/api/v0/account/transactions", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JObject.Parse(responseContent).ToObject<GetTransactionListResponse>();
}
var error = JObject.Parse(responseContent).ToObject<BringinErrorResponse>();
throw new BringinException(error);
}
public class GetTransactionListResponse
{
[JsonProperty("transactions")]
public BringinTransaction[] Transactions { get; set; }
}
public class BringinTransaction
{
// {
// "orderId": "3521154c-30b4-480c-834d-38f80d507963",
// "type": "OFFRAMP_WITHOUT_FIAT_WITHDRAWAL",
// "subType": "LIGHTNING",
// "sourceAmount": "100000",
// "sourceCurrency": "BTC",
// "destinationAmount": "3816",
// "destinationAddress": "b0a4c862-c941-4d3c-8727-18e5097a3b5a",
// "destinationCurrency": "EUR",
// "status": "SUCCESSFUL",
// "createdAt": "2024-01-18T14:02:59.709Z",
// }
[JsonProperty("orderId")]
public string OrderId { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("subType")]
public string SubType { get; set; }
[JsonProperty("sourceAmount")]
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal SourceAmount { get; set; }
[JsonProperty("sourceCurrency")]
public string SourceCurrency { get; set; }
[JsonProperty("destinationAmount")]
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal DestinationAmount { get; set; }
[JsonProperty("destinationCurrency")]
public string DestinationCurrency { get; set; }
[JsonProperty("destinationAddress")]
public string DestinationAddress { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
}
// //
// public class GetOrderResponse // public class GetOrderResponse
// { // {
@@ -154,6 +219,7 @@ public class BringinClient
public decimal Amount { get; set; } public decimal Amount { get; set; }
[JsonProperty("invoice")] public string Invoice { get; set; } [JsonProperty("invoice")] public string Invoice { get; set; }
[JsonProperty("depositAddress")] public string DepositAddress { get; set; }
[JsonProperty("expiresAt")] public long Expiry { get; set; } [JsonProperty("expiresAt")] public long Expiry { get; set; }
} }
@@ -188,6 +254,7 @@ public class BringinClient
public string Message { get; set; } public string Message { get; set; }
public string StatusCode { get; set; } public string StatusCode { get; set; }
public string ErrorCode { get; set; } public string ErrorCode { get; set; }
public JObject ErrorDetails { get; set; } public string ErrorMessage { get; set; }
public JToken ErrorDetails { get; set; }
} }
} }

View File

@@ -6,7 +6,7 @@ public class BringinException : Exception
{ {
private readonly BringinClient.BringinErrorResponse _error; private readonly BringinClient.BringinErrorResponse _error;
public BringinException(BringinClient.BringinErrorResponse error) public BringinException(BringinClient.BringinErrorResponse error):base(error.Message?? error.ErrorMessage)
{ {
_error = error; _error = error;
} }

View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -27,6 +28,15 @@ using PayoutData = BTCPayServer.Data.PayoutData;
namespace BTCPayServer.Plugins.Bringin; namespace BTCPayServer.Plugins.Bringin;
public static class StringExtensions
{
public static string ToHumanReadable(this string str)
{
return string.Join(' ', str.Split('_', '-').Select(part =>
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(part.ToLower(CultureInfo.CurrentCulture))));
}
}
public class BringinService : EventHostedServiceBase public class BringinService : EventHostedServiceBase
{ {
private readonly ILogger<BringinService> _logger; private readonly ILogger<BringinService> _logger;
@@ -265,7 +275,7 @@ public class BringinService : EventHostedServiceBase
var rate = await bringinClient.GetRate(); var rate = await bringinClient.GetRate();
var thresholdAmount = supportedMethod.FiatMinimumAmount / rate.BringinPrice; var thresholdAmount = supportedMethod.FiatMinimumAmount / rate.BringinPrice;
if (amountBtc.ToDecimal(MoneyUnit.BTC) < thresholdAmount) if (amountBtc.ToDecimal(MoneyUnit.BTC) <= thresholdAmount)
{ {
throw new Exception($"Amount is too low. Minimum amount is {Money.Coins(thresholdAmount)} BTC"); throw new Exception($"Amount is too low. Minimum amount is {Money.Coins(thresholdAmount)} BTC");
} }
@@ -283,15 +293,17 @@ public class BringinService : EventHostedServiceBase
if (!payout) if (!payout)
{ {
return order.Invoice; return order.Invoice?? order.DepositAddress;
} }
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode); var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var destination = !string.IsNullOrEmpty(order.Invoice)? (IClaimDestination) new BoltInvoiceClaimDestination(order.Invoice, BOLT11PaymentRequest.Parse(order.Invoice, network.NBitcoinNetwork)):
new AddressClaimDestination(BitcoinAddress.Create(order.DepositAddress, network.NBitcoinNetwork));
var claim = await _pullPaymentHostedService.Claim(new ClaimRequest() var claim = await _pullPaymentHostedService.Claim(new ClaimRequest()
{ {
PaymentMethodId = paymentMethodId, PaymentMethodId = paymentMethodId,
StoreId = storeId, StoreId = storeId,
Destination = new BoltInvoiceClaimDestination(order.Invoice, BOLT11PaymentRequest.Parse(order.Invoice, network.NBitcoinNetwork)), Destination = destination,
Value = orderMoney.ToUnit(MoneyUnit.BTC), Value = orderMoney.ToUnit(MoneyUnit.BTC),
PreApprove = true, PreApprove = true,
Metadata = JObject.FromObject(new Metadata = JObject.FromObject(new
@@ -379,7 +391,8 @@ public class BringinService : EventHostedServiceBase
public static readonly SupportedMethodOptions[] SupportedMethods = new[] public static readonly SupportedMethodOptions[] SupportedMethods = new[]
{ {
new SupportedMethodOptions(new PaymentMethodId("BTC", LightningPaymentType.Instance), true, 15, "LIGHTNING") new SupportedMethodOptions(new PaymentMethodId("BTC", LightningPaymentType.Instance), true, 15, "LIGHTNING"),
new SupportedMethodOptions(new PaymentMethodId("BTC", BitcoinPaymentType.Instance), true, 20, "ON_CHAIN"),
}; };
private ConcurrentDictionary<string, (IDisposable, BringinStoreSettings, DateTimeOffset Expiry)> _editModes = new(); private ConcurrentDictionary<string, (IDisposable, BringinStoreSettings, DateTimeOffset Expiry)> _editModes = new();

View File

@@ -11,7 +11,6 @@
@using NBitcoin @using NBitcoin
@implements IAsyncDisposable; @implements IAsyncDisposable;
@code { @code {
private BringinService.BringinStoreSettings? _settings; private BringinService.BringinStoreSettings? _settings;
private bool _isLoaded = false; private bool _isLoaded = false;
@@ -25,7 +24,7 @@
[Inject] private BTCPayNetworkProvider BTCPayNetworkProvider { get; set; } [Inject] private BTCPayNetworkProvider BTCPayNetworkProvider { get; set; }
[Inject] private IHttpClientFactory HttpClientFactory { get; set; } [Inject] private IHttpClientFactory HttpClientFactory { get; set; }
[Inject] private PayoutProcessorService PayoutProcessorService { get; set; } [Inject] private PayoutProcessorService PayoutProcessorService { get; set; }
[Inject] private IAuthorizationService AuthorizationService { get; set; } [Inject] private IAuthorizationService AuthorizationService { get; set; }
[Parameter] public string StoreId { get; set; } [Parameter] public string StoreId { get; set; }
private decimal? LastFiatBalance { get; set; } private decimal? LastFiatBalance { get; set; }
private DateTimeOffset? LastDataFetch { get; set; } private DateTimeOffset? LastDataFetch { get; set; }
@@ -75,7 +74,7 @@
{ {
if (firstRender) if (firstRender)
{ {
_readOnly = !(await AuthorizationService.AuthorizeAsync(HttpContextAccessor.HttpContext.User, StoreId, Policies.CanModifyStoreSettings )).Succeeded; _readOnly = !(await AuthorizationService.AuthorizeAsync(HttpContextAccessor.HttpContext.User, StoreId, Policies.CanModifyStoreSettings)).Succeeded;
OnboardLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "Onboard", "Bringin", new {StoreId}); OnboardLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "Onboard", "Bringin", new {StoreId});
PmiLink = $"A payout processor has not been configured for this payment method. Payouts generated by Bringin will not be automatically handled. <a href=\"{LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "ConfigureStorePayoutProcessors", "UIPayoutProcessors", new {StoreId})}\">Configure now</a>"; PmiLink = $"A payout processor has not been configured for this payment method. Payouts generated by Bringin will not be automatically handled. <a href=\"{LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "ConfigureStorePayoutProcessors", "UIPayoutProcessors", new {StoreId})}\">Configure now</a>";
_callbackLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "Callback", "Bringin", new {StoreId}); _callbackLink = LinkGenerator.GetUriByAction(HttpContextAccessor.HttpContext, "Callback", "Bringin", new {StoreId});
@@ -188,12 +187,13 @@
{ {
if (_settings?.ApiKey is null || EditMode) if (_settings?.ApiKey is null || EditMode)
return; return;
var network = BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"); var network = BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var client = _settings.CreateClient(HttpClientFactory, network.NBitcoinNetwork); var client = _settings.CreateClient(HttpClientFactory, network.NBitcoinNetwork);
LastFiatBalance = await client.GetFiatBalance(); LastFiatBalance = await client.GetFiatBalance();
LastFiatRate = (await client.GetRate()).BringinPrice; LastFiatRate = (await client.GetRate()).BringinPrice;
LastDataFetch = DateTimeOffset.UtcNow; LastDataFetch = DateTimeOffset.UtcNow;
LastTxs = await client.GetTransactions();
_ = InvokeAsync(StateHasChanged); _ = InvokeAsync(StateHasChanged);
} }
finally finally
@@ -210,6 +210,8 @@
} }
} }
public BringinClient.GetTransactionListResponse LastTxs { get; set; }
private void UpdateDestinationValue(BringinService.BringinStoreSettings.PaymentMethodSettings settings, object eValue) private void UpdateDestinationValue(BringinService.BringinStoreSettings.PaymentMethodSettings settings, object eValue)
{ {
@@ -379,7 +381,7 @@
<p class="text-secondary my-3"> <p class="text-secondary my-3">
Bringin is a service that allows you to automatically convert your BTC to EUR and send it to your bank account. Start configuring it by clicking on the button below. Bringin is a service that allows you to automatically convert your BTC to EUR and send it to your bank account. Start configuring it by clicking on the button below.
</p> </p>
@if (!_readOnly) @if (!_readOnly)
{ {
<div class="d-flex"> <div class="d-flex">
@@ -417,9 +419,9 @@
<div class="form-group"> <div class="form-group">
@* <label class="form-label">Payment method</label> *@ @* <label class="form-label">Payment method</label> *@
<select @bind="ManualOrderPaymentMethod" class="form-select"> <select @bind="ManualOrderPaymentMethod" class="form-select">
<option value="">Select a payment method</option>
@foreach (var opt in items) @foreach (var opt in items)
{ {
<option value="">Select a payment method</option>
<option value="@opt.ToString()">@opt.ToPrettyString()</option> <option value="@opt.ToString()">@opt.ToPrettyString()</option>
} }
</select> </select>
@@ -569,49 +571,96 @@
</div> </div>
</div> </div>
<div class="row"> <div class="container">
@foreach (var method in _settings.MethodSettings) <div class="row gx-5">
{ @for (int i = 0; i < _settings.MethodSettings.Count; i++)
var pmId = PaymentMethodId.TryParse(method.Key); {
if (pmId is null) var method = _settings.MethodSettings.ElementAt(i);
continue; var pmId = PaymentMethodId.TryParse(method.Key);
var supportedMethod = BringinService.SupportedMethods.FirstOrDefault(s => s.PaymentMethod.ToString() == method.Key); if (pmId is null)
<div class="card col-xxl-constrain col-12 @(_settings.MethodSettings.Count > 1 ? "col-xl-6" : "")"> continue;
<h5 class="card-header border-bottom-0 text-muted">@pmId.ToPrettyString()</h5> var supportedMethod = BringinService.SupportedMethods.FirstOrDefault(s => s.PaymentMethod.ToString() == method.Key);
<div class="card-body"> <div class="col-xxl-constrain col-12 @(_settings.MethodSettings.Count > 1 ? $"col-xl-6 {(i == 0 ? "border-end" : "")}" : "")">
<div class="form-group"> <h5 class=" border-bottom-0 text-muted mb-4">@pmId.ToPrettyString()</h5>
<label class="form-label">Percentage</label> <div class="card-body">
<input type="range" value="@method.Value.PercentageToForward" @oninput="@((e) => UpdateDestinationValue(method.Value, e.Value))" min="0" step='0.01' class="form-range" max="100"/>
<div class="input-group input-group-sm">
<input type="number" step='0.01' value="@method.Value.PercentageToForward" @onchange="@((e) => UpdateDestinationValue(method.Value, e.Value))" class="form-control form-control-sm"/>
<span class="input-group-text">%</span>
</div>
<p class="text-muted my-2">Every time an invoice becomes Settled, we take the sum of all settled payments of this payment method, get the specified percentage of it and add it to the current balance.</p>
</div>
<div class="form-group">
<label class="form-label">Threshold</label>
<div class="input-group input-group-sm">
<input type="number" @bind="method.Value.Threshold" min="@supportedMethod?.FiatMinimumAmount" class="form-control form-control-sm"/>
<span class="input-group-text">@(method.Value.FiatThreshold ? "EUR" : "BTC")</span>
</div>
<p class="text-muted my-2">Once the threshold is reached, we create a payout sending the balance to Bringin to be converted.</p>
</div>
@if (supportedMethod?.FiatMinimum is not true)
{
<div class="form-group"> <div class="form-group">
<div class="d-flex align-items-center"> <label class="form-label">Percentage</label>
<input type="checkbox" class="btcpay-toggle me-2" @bind="method.Value.FiatThreshold"/> <input type="range" value="@method.Value.PercentageToForward" @oninput="@((e) => UpdateDestinationValue(method.Value, e.Value))" min="0" step='0.01' class="form-range" max="100"/>
<label class="form-label mb-0 me-1">Threshold in EUR</label> <div class="input-group input-group-sm">
<input type="number" step='0.01' value="@method.Value.PercentageToForward" @onchange="@((e) => UpdateDestinationValue(method.Value, e.Value))" class="form-control form-control-sm"/>
<span class="input-group-text">%</span>
</div> </div>
<p class="text-muted my-2">Every time an invoice becomes Settled, we take the sum of all settled payments of this payment method, get the specified percentage of it and add it to the current balance.</p>
</div> </div>
} <div class="form-group">
<label class="form-label">Threshold</label>
<div class="input-group input-group-sm">
<input type="number" @bind="method.Value.Threshold" min="@supportedMethod?.FiatMinimumAmount" class="form-control form-control-sm"/>
<span class="input-group-text">@(method.Value.FiatThreshold ? "EUR" : "BTC")</span>
</div>
<p class="text-muted my-2">Once the threshold is reached, we create a payout sending the balance to Bringin to be converted.</p>
</div>
@if (supportedMethod?.FiatMinimum is not true)
{
<div class="form-group">
<div class="d-flex align-items-center">
<input type="checkbox" class="btcpay-toggle me-2" @bind="method.Value.FiatThreshold"/>
<label class="form-label mb-0 me-1">Threshold in EUR</label>
</div>
</div>
}
</div>
</div> </div>
</div> }
} </div>
</div> </div>
} }
} }
} }
</div> </div>
@if (LastTxs is not null)
{
<div class="widget store-numbers">
<header>
<h4 class="text-muted">Bringin Transactions</h4>
</header>
@if (LastTxs.Transactions.Any())
{
<div class="table-responsive my-0 " style=" max-height: 400px;">
<table class="table table-hover mt-3 mb-0">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Status</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var tx in LastTxs.Transactions.OrderByDescending(transaction => transaction.CreatedAt))
{
<tr>
<td>@tx.CreatedAt.ToTimeAgo()</td>
<td>@tx.SubType.ToHumanReadable()</td>
<td>
@tx.Status.ToHumanReadable()
</td>
<td class="amount-col">
<span data-sensitive>@(tx.SourceCurrency == "BTC" ? Money.Satoshis(tx.SourceAmount).ToDecimal(MoneyUnit.BTC): tx.SourceAmount)@tx.SourceCurrency -> @tx.DestinationAmount @tx.DestinationCurrency </span>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<p class="text-secondary mt-3 mb-0">
There are no recent transactions.
</p>
}
</div>
}