Add a way to customize lightning invoice description

This commit is contained in:
nicolas.dorier
2018-04-07 16:27:46 +09:00
parent 21215dc537
commit d7cb6f1cca
11 changed files with 53 additions and 16 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.76</Version> <Version>1.0.1.77</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn> <NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null) .Where(c => c.Network != null)
.Select(o => .Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod, (SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob))) PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.ToList(); .ToList();
List<string> paymentMethodErrors = new List<string>(); List<string> paymentMethodErrors = new List<string>();
@@ -183,15 +183,16 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob) private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{ {
var storeBlob = store.GetStoreBlob();
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency); var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod(); PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity; paymentMethod.ParentEntity = entity;
paymentMethod.Network = network; paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate; paymentMethod.Rate = rate;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network); var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled) if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee(); paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails); paymentMethod.SetPaymentMethodDetails(paymentDetails);

View File

@@ -282,6 +282,7 @@ namespace BTCPayServer.Controllers
vm.MonitoringExpiration = storeBlob.MonitoringExpiration; vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration; vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier(); vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange; vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
return View(vm); return View(vm);
} }
@@ -356,6 +357,7 @@ namespace BTCPayServer.Controllers
blob.NetworkFeeDisabled = !model.NetworkFee; blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration; blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration; blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
bool newExchange = blob.PreferredExchange != model.PreferredExchange; bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange; blob.PreferredExchange = model.PreferredExchange;

View File

@@ -270,6 +270,20 @@ namespace BTCPayServer.Data
[JsonConverter(typeof(UriJsonConverter))] [JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; } public Uri CustomCSS { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{
get
{
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
}
set
{
_LightningDescriptionTemplate = value;
}
}
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider) public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{ {
if (!PreferredExchange.IsCoinAverage()) if (!PreferredExchange.IsCoinAverage())

View File

@@ -91,6 +91,9 @@ namespace BTCPayServer.Models.StoreViewModels
get; set; get; set;
} }
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
public class LightningNode public class LightningNode
{ {
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
@@ -100,6 +103,5 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
get; set; get; set;
} = new List<LightningNode>(); } = new List<LightningNode>();
} }
} }

View File

@@ -27,7 +27,7 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider; _WalletProvider = walletProvider;
} }
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{ {
if (!_ExplorerProvider.IsAvailable(network)) if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available"); throw new PaymentMethodUnavailableException($"Full node not available");

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments namespace BTCPayServer.Payments
@@ -16,25 +17,26 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
/// <param name="supportedPaymentMethod"></param> /// <param name="supportedPaymentMethod"></param>
/// <param name="paymentMethod"></param> /// <param name="paymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param> /// <param name="network"></param>
/// <returns></returns> /// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network); Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
} }
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{ {
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network); Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
} }
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{ {
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network); public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{ {
if (supportedPaymentMethod is T method) if (supportedPaymentMethod is T method)
{ {
return CreatePaymentMethodDetails(method, paymentMethod, network); return CreatePaymentMethodDetails(method, paymentMethod, store, network);
} }
throw new NotSupportedException("Invalid supportedPaymentMethod"); throw new NotSupportedException("Invalid supportedPaymentMethod");
} }

View File

@@ -48,8 +48,10 @@ namespace BTCPayServer.Payments.Lightning.Charge
{ {
var message = CreateMessage(HttpMethod.Post, "invoice"); var message = CreateMessage(HttpMethod.Post, "invoice");
Dictionary<string, string> parameters = new Dictionary<string, string>(); Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture)); parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture)); parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
if(request.Description != null)
parameters.Add("description", request.Description);
message.Content = new FormUrlEncodedContent(parameters); message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation); var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode(); result.EnsureSuccessStatusCode();
@@ -154,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation) async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{ {
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry, Description = description ?? "" }); var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" }; return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
} }

View File

@@ -7,7 +7,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
{ {
public class CreateInvoiceRequest public class CreateInvoiceRequest
{ {
public LightMoney Amont { get; set; } public LightMoney Amount { get; set; }
public TimeSpan Expiry { get; set; } public TimeSpan Expiry { get; set; }
public string Description { get; set; } public string Description { get; set; }
} }

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge; using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning; using BTCPayServer.Payments.Lightning.CLightning;
@@ -23,8 +24,9 @@ namespace BTCPayServer.Payments.Lightning
_LightningClientFactory = lightningClientFactory; _LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard; _Dashboard = dashboard;
} }
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{ {
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network); var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity; var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8); var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
@@ -36,7 +38,11 @@ namespace BTCPayServer.Payments.Lightning
LightningInvoice lightningInvoice = null; LightningInvoice lightningInvoice = null;
try try
{ {
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), invoice.ProductInformation.ItemDesc, expiry); string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
} }
catch(Exception ex) catch(Exception ex)
{ {

View File

@@ -129,6 +129,14 @@
</table> </table>
</div> </div>
</div> </div>
<div class="form-group">
<label asp-for="LightningDescriptionTemplate"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control" />
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Placeholder available are: {StoreName}, {ItemDescription} and {OrderId}
</p>
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button> <button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
</form> </form>
</div> </div>