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
This commit is contained in:
Andrew Camilleri 2023-07-24 11:37:18 +02:00 committed by GitHub
parent dc986959fd
commit 36ea17a6b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 120 additions and 9 deletions

View file

@ -1,8 +1,11 @@
#nullable enable #nullable enable
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models; namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{ {
public string? PullPaymentId { get; set; } public string? PullPaymentId { get; set; }
public bool? Approved { get; set; } public bool? Approved { get; set; }
public JObject? Metadata { get; set; }
} }

View file

@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
public PayoutState State { get; set; } public PayoutState State { get; set; }
public int Revision { get; set; } public int Revision { get; set; }
public JObject PaymentProof { get; set; } public JObject PaymentProof { get; set; }
public JObject Metadata { get; set; }
} }
} }

View file

@ -1152,7 +1152,8 @@ namespace BTCPayServer.Tests
Approved = false, Approved = false,
PaymentMethod = "BTC", PaymentMethod = "BTC",
Amount = 0.0001m, Amount = 0.0001m,
Destination = address.ToString() Destination = address.ToString(),
}); });
await AssertAPIError("invalid-state", async () => await AssertAPIError("invalid-state", async () =>
{ {
@ -3545,6 +3546,7 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC_LightningNetwork", PaymentMethod = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11 Destination = customerInvoice.BOLT11
}); });
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")); Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork", await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) }); 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); (await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC.State); 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)] [Fact(Timeout = 60 * 2 * 1000)]

View file

@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
namespace BTCPayServer.Controllers.Greenfield namespace BTCPayServer.Controllers.Greenfield
@ -284,7 +285,8 @@ namespace BTCPayServer.Controllers.Greenfield
Amount = blob.Amount, Amount = blob.Amount,
PaymentMethodAmount = blob.CryptoAmount, PaymentMethodAmount = blob.CryptoAmount,
Revision = blob.Revision, Revision = blob.Revision,
State = p.State State = p.State,
Metadata = blob.Metadata?? new JObject(),
}; };
model.Destination = blob.Destination; model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId; model.PaymentMethod = p.PaymentMethodId;
@ -341,7 +343,7 @@ namespace BTCPayServer.Controllers.Greenfield
Destination = destination.destination, Destination = destination.destination,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
Value = request.Amount, Value = request.Amount,
PaymentMethodId = paymentMethodId, PaymentMethodId = paymentMethodId
}); });
return HandleClaimResult(result); return HandleClaimResult(result);
@ -415,7 +417,8 @@ namespace BTCPayServer.Controllers.Greenfield
PreApprove = request.Approved, PreApprove = request.Approved,
Value = request.Amount, Value = request.Amount,
PaymentMethodId = paymentMethodId, PaymentMethodId = paymentMethodId,
StoreId = storeId StoreId = storeId,
Metadata = request.Metadata
}); });
return HandleClaimResult(result); return HandleClaimResult(result);
} }

View file

@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
using PayoutData = BTCPayServer.Data.PayoutData; using PayoutData = BTCPayServer.Data.PayoutData;
using PullPaymentData = BTCPayServer.Data.PullPaymentData; using PullPaymentData = BTCPayServer.Data.PullPaymentData;
@ -529,10 +530,32 @@ namespace BTCPayServer.Controllers
{ {
var ppBlob = item.PullPayment?.GetBlob(); var ppBlob = item.PullPayment?.GetBlob();
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings); var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
string payoutSource;
if (payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase,
out var source) is true)
{
payoutSource = source.Value<string>();
}
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<string>();
}
else if(item.PullPayment?.Id is not null)
{
payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id });
}
var m = new PayoutsModel.PayoutModel var m = new PayoutsModel.PayoutModel
{ {
PullPaymentId = item.PullPayment?.Id, PullPaymentId = item.PullPayment?.Id,
PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id, Source = payoutSource,
SourceLink = payoutSourceLink,
Date = item.Payout.Date, Date = item.Payout.Date,
PayoutId = item.Payout.Id, PayoutId = item.Payout.Id,
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode), Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),

View file

@ -1,5 +1,7 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@ -12,5 +14,10 @@ namespace BTCPayServer.Data
public int MinimumConfirmation { get; set; } = 1; public int MinimumConfirmation { get; set; } = 1;
public string Destination { get; set; } public string Destination { get; set; }
public int Revision { get; set; } public int Revision { get; set; }
[JsonExtensionData]
public Dictionary<string, JToken> AdditionalData { get; set; } = new();
public JObject Metadata { get; set; }
} }
} }

View file

@ -35,7 +35,9 @@ namespace BTCPayServer.Data
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers) public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{ {
return JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); var result = JsonConvert.DeserializeObject<PayoutBlob>(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) public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
{ {

View file

@ -597,7 +597,8 @@ namespace BTCPayServer.HostedServices
var payoutBlob = new PayoutBlob() var payoutBlob = new PayoutBlob()
{ {
Amount = claimed, Amount = claimed,
Destination = req.ClaimRequest.Destination.ToString() Destination = req.ClaimRequest.Destination.ToString(),
Metadata = req.ClaimRequest.Metadata?? new JObject(),
}; };
payout.SetBlob(payoutBlob, _jsonSerializerSettings); payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout); await ctx.Payouts.AddAsync(payout);
@ -890,6 +891,7 @@ namespace BTCPayServer.HostedServices
public IClaimDestination Destination { get; set; } public IClaimDestination Destination { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
public bool? PreApprove { get; set; } public bool? PreApprove { get; set; }
public JObject Metadata { get; set; }
} }
public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout) public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout)

View file

@ -26,7 +26,8 @@ namespace BTCPayServer.Models.WalletViewModels
public bool Selected { get; set; } public bool Selected { get; set; }
public DateTimeOffset Date { get; set; } public DateTimeOffset Date { get; set; }
public string PullPaymentId { 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 Destination { get; set; }
public string Amount { get; set; } public string Amount { get; set; }
public string ProofLink { get; set; } public string ProofLink { get; set; }

View file

@ -196,7 +196,14 @@
<span>@pp.Date.ToBrowserDate()</span> <span>@pp.Date.ToBrowserDate()</span>
</td> </td>
<td class="mw-100"> <td class="mw-100">
<span>@pp.PullPaymentName</span> @if (pp.SourceLink is not null && pp.Source is not null)
{
<a href="@pp.SourceLink" rel="noreferrer noopener">@pp.Source</a>
}
else if (pp.Source is not null)
{
<span>@pp.Source</span>
}
</td> </td>
<td title="@pp.Destination"> <td title="@pp.Destination">
<span class="text-break">@pp.Destination</span> <span class="text-break">@pp.Destination</span>

View file

@ -904,6 +904,10 @@
"approved": { "approved": {
"type": "boolean", "type": "boolean",
"description": "Whether to approve this payout automatically upon creation" "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": { "paymentProof": {
"$ref": "#/components/schemas/PayoutPaymentProof" "$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."
}
}
}
]
} }
} }
}, },