From 36ea17a6b7eb9712d5ce397b5f94bcc099badd60 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 24 Jul 2023 11:37:18 +0200 Subject: [PATCH] Introduce Payout metadata for api and plugins (#5182) * Introduce Payout metadata for api and plugins * fix controller * fix metadata requirement * save an object * pr changes --- .../Models/CreatePayoutThroughStoreRequest.cs | 3 ++ BTCPayServer.Client/Models/PayoutData.cs | 1 + BTCPayServer.Tests/GreenfieldAPITests.cs | 34 ++++++++++++++++++- .../GreenfieldPullPaymentController.cs | 9 +++-- ...torePullPaymentsController.PullPayments.cs | 25 +++++++++++++- BTCPayServer/Data/Payouts/PayoutBlob.cs | 7 ++++ BTCPayServer/Data/Payouts/PayoutExtensions.cs | 4 ++- .../PullPaymentHostedService.cs | 4 ++- .../Models/WalletViewModels/PayoutsModel.cs | 3 +- .../Views/UIStorePullPayments/Payouts.cshtml | 9 ++++- .../v1/swagger.template.pull-payments.json | 30 ++++++++++++++++ 11 files changed, 120 insertions(+), 9 deletions(-) diff --git a/BTCPayServer.Client/Models/CreatePayoutThroughStoreRequest.cs b/BTCPayServer.Client/Models/CreatePayoutThroughStoreRequest.cs index cff40560f..7d1478f1d 100644 --- a/BTCPayServer.Client/Models/CreatePayoutThroughStoreRequest.cs +++ b/BTCPayServer.Client/Models/CreatePayoutThroughStoreRequest.cs @@ -1,8 +1,11 @@ #nullable enable +using Newtonsoft.Json.Linq; + namespace BTCPayServer.Client.Models; public class CreatePayoutThroughStoreRequest : CreatePayoutRequest { public string? PullPaymentId { get; set; } public bool? Approved { get; set; } + public JObject? Metadata { get; set; } } diff --git a/BTCPayServer.Client/Models/PayoutData.cs b/BTCPayServer.Client/Models/PayoutData.cs index b310bfc1f..2f1f336e1 100644 --- a/BTCPayServer.Client/Models/PayoutData.cs +++ b/BTCPayServer.Client/Models/PayoutData.cs @@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models public PayoutState State { get; set; } public int Revision { get; set; } public JObject PaymentProof { get; set; } + public JObject Metadata { get; set; } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 5c04affc4..7266bac59 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1152,7 +1152,8 @@ namespace BTCPayServer.Tests Approved = false, PaymentMethod = "BTC", Amount = 0.0001m, - Destination = address.ToString() + Destination = address.ToString(), + }); await AssertAPIError("invalid-state", async () => { @@ -3545,6 +3546,7 @@ namespace BTCPayServer.Tests PaymentMethod = "BTC_LightningNetwork", Destination = customerInvoice.BOLT11 }); + Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")); await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork", new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) }); @@ -3555,6 +3557,36 @@ namespace BTCPayServer.Tests (await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id); Assert.Equal(PayoutState.Completed, payoutC.State); }); + + payout = await adminClient.CreatePayout(admin.StoreId, + new CreatePayoutThroughStoreRequest() + { + Approved = true, + PaymentMethod = "BTC", + Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(), + Amount = 0.0001m, + Metadata = JObject.FromObject(new + { + source ="apitest", + sourceLink = "https://chocolate.com" + }) + }); + Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new + { + source = "apitest", + sourceLink = "https://chocolate.com" + }).ToString()); + + payout = + (await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id); + + Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new + { + source = "apitest", + sourceLink = "https://chocolate.com" + }).ToString()); + + } [Fact(Timeout = 60 * 2 * 1000)] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs index d2d31593d..ad739ca68 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; namespace BTCPayServer.Controllers.Greenfield @@ -284,7 +285,8 @@ namespace BTCPayServer.Controllers.Greenfield Amount = blob.Amount, PaymentMethodAmount = blob.CryptoAmount, Revision = blob.Revision, - State = p.State + State = p.State, + Metadata = blob.Metadata?? new JObject(), }; model.Destination = blob.Destination; model.PaymentMethod = p.PaymentMethodId; @@ -341,7 +343,7 @@ namespace BTCPayServer.Controllers.Greenfield Destination = destination.destination, PullPaymentId = pullPaymentId, Value = request.Amount, - PaymentMethodId = paymentMethodId, + PaymentMethodId = paymentMethodId }); return HandleClaimResult(result); @@ -415,7 +417,8 @@ namespace BTCPayServer.Controllers.Greenfield PreApprove = request.Approved, Value = request.Amount, PaymentMethodId = paymentMethodId, - StoreId = storeId + StoreId = storeId, + Metadata = request.Metadata }); return HandleClaimResult(result); } diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs index 261fd0ab0..a5f8af4f6 100644 --- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs +++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs @@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; using PayoutData = BTCPayServer.Data.PayoutData; using PullPaymentData = BTCPayServer.Data.PullPaymentData; @@ -529,10 +530,32 @@ namespace BTCPayServer.Controllers { var ppBlob = item.PullPayment?.GetBlob(); var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings); + string payoutSource; + if (payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase, + out var source) is true) + { + payoutSource = source.Value(); + } + else + { + payoutSource = ppBlob?.Name ?? item.PullPayment?.Id; + } + + string payoutSourceLink = null; + if (payoutBlob.Metadata?.TryGetValue("sourceLink", StringComparison.InvariantCultureIgnoreCase, + out var sourceLink) is true) + { + payoutSourceLink = sourceLink.Value(); + } + else if(item.PullPayment?.Id is not null) + { + payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id }); + } var m = new PayoutsModel.PayoutModel { PullPaymentId = item.PullPayment?.Id, - PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id, + Source = payoutSource, + SourceLink = payoutSourceLink, Date = item.Payout.Date, PayoutId = item.Payout.Id, Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode), diff --git a/BTCPayServer/Data/Payouts/PayoutBlob.cs b/BTCPayServer/Data/Payouts/PayoutBlob.cs index ac16a4933..82ecc560f 100644 --- a/BTCPayServer/Data/Payouts/PayoutBlob.cs +++ b/BTCPayServer/Data/Payouts/PayoutBlob.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using BTCPayServer.JsonConverters; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Data { @@ -12,5 +14,10 @@ namespace BTCPayServer.Data public int MinimumConfirmation { get; set; } = 1; public string Destination { get; set; } public int Revision { get; set; } + + [JsonExtensionData] + public Dictionary AdditionalData { get; set; } = new(); + + public JObject Metadata { get; set; } } } diff --git a/BTCPayServer/Data/Payouts/PayoutExtensions.cs b/BTCPayServer/Data/Payouts/PayoutExtensions.cs index 8e5f04a9a..f7b0409fe 100644 --- a/BTCPayServer/Data/Payouts/PayoutExtensions.cs +++ b/BTCPayServer/Data/Payouts/PayoutExtensions.cs @@ -35,7 +35,9 @@ namespace BTCPayServer.Data public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers) { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); + var result = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); + result.Metadata ??= new JObject(); + return result; } public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers) { diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index ff2757c28..699012338 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -597,7 +597,8 @@ namespace BTCPayServer.HostedServices var payoutBlob = new PayoutBlob() { Amount = claimed, - Destination = req.ClaimRequest.Destination.ToString() + Destination = req.ClaimRequest.Destination.ToString(), + Metadata = req.ClaimRequest.Metadata?? new JObject(), }; payout.SetBlob(payoutBlob, _jsonSerializerSettings); await ctx.Payouts.AddAsync(payout); @@ -890,6 +891,7 @@ namespace BTCPayServer.HostedServices public IClaimDestination Destination { get; set; } public string StoreId { get; set; } public bool? PreApprove { get; set; } + public JObject Metadata { get; set; } } public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout) diff --git a/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs b/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs index 3f57346e0..caffa7ebc 100644 --- a/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs +++ b/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs @@ -26,7 +26,8 @@ namespace BTCPayServer.Models.WalletViewModels public bool Selected { get; set; } public DateTimeOffset Date { get; set; } public string PullPaymentId { get; set; } - public string PullPaymentName { get; set; } + public string Source { get; set; } + public string SourceLink { get; set; } public string Destination { get; set; } public string Amount { get; set; } public string ProofLink { get; set; } diff --git a/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml b/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml index d33f4d5ab..255e8fea5 100644 --- a/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml +++ b/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml @@ -196,7 +196,14 @@ @pp.Date.ToBrowserDate() - @pp.PullPaymentName + @if (pp.SourceLink is not null && pp.Source is not null) + { + @pp.Source + } + else if (pp.Source is not null) + { + @pp.Source + } @pp.Destination diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json index 2eb0dba73..5c2ae81a9 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json @@ -904,6 +904,10 @@ "approved": { "type": "boolean", "description": "Whether to approve this payout automatically upon creation" + }, + "metadata": { + "type": "object", + "description": "Additional metadata to store with the payout" } } } @@ -1012,6 +1016,32 @@ }, "paymentProof": { "$ref": "#/components/schemas/PayoutPaymentProof" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional information around the payout that can be supplied. The mentioned properties are all optional and you can introduce any json format you wish.", + "example": { + "source": "Payout created through the API" + }, + "anyOf": [ + { + "title": "General information", + "properties": { + "source": { + "type": "string", + "nullable": true, + "description": "The source of the payout creation. Shown on the payout list page." + }, + "sourceLink": { + "type": "string", + "format": "url", + "nullable": true, + "description": "A link to the source of the payout creation. Shown on the payout list page." + } + } + } + ] } } },