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

View file

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

View file

@ -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)]

View file

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

View file

@ -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<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
{
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),

View file

@ -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<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)
{
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)
{

View file

@ -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)

View file

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

View file

@ -196,7 +196,14 @@
<span>@pp.Date.ToBrowserDate()</span>
</td>
<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 title="@pp.Destination">
<span class="text-break">@pp.Destination</span>

View file

@ -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."
}
}
}
]
}
}
},