mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-04 01:53:52 +01:00
Refactor walletobj API, make wallet object graph directionless (#4297)
This commit is contained in:
parent
bf91efc756
commit
9b5c6ece90
14 changed files with 916 additions and 662 deletions
|
@ -11,45 +11,71 @@ namespace BTCPayServer.Client
|
|||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
CancellationToken token = default)
|
||||
public virtual async Task<OnChainWalletObjectData> GetOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId, bool? includeNeighbourData = null, CancellationToken token = default)
|
||||
{
|
||||
Dictionary<string, object> parameters = new Dictionary<string, object>();
|
||||
if (includeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, method: HttpMethod.Get), token);
|
||||
try
|
||||
{
|
||||
return await HandleResponse<OnChainWalletObjectData>(response);
|
||||
}
|
||||
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public virtual async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, GetWalletObjectsRequest query = null, CancellationToken token = default)
|
||||
{
|
||||
Dictionary<string, object> parameters = new Dictionary<string, object>();
|
||||
if (query?.Type is string s)
|
||||
parameters.Add("type", s);
|
||||
if (query?.Ids is string[] ids)
|
||||
parameters.Add("ids", ids);
|
||||
if (query?.IncludeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Get, bodyPayload: query), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, method:HttpMethod.Get), token);
|
||||
return await HandleResponse<OnChainWalletObjectData[]>(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Delete, bodyPayload: query), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", method:HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task AddOrUpdateOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectData[] request,
|
||||
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Post, bodyPayload: request), token);
|
||||
await HandleResponse(response);
|
||||
return await HandleResponse<OnChainWalletObjectData>(response);
|
||||
}
|
||||
public virtual async Task AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode, AddOnChainWalletObjectLinkRequest[] request,
|
||||
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
|
||||
OnChainWalletObjectId objectId,
|
||||
AddOnChainWalletObjectLinkRequest request = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links", method:HttpMethod.Post, bodyPayload: request), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", method:HttpMethod.Post, bodyPayload: request), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
RemoveOnChainWalletObjectLinkRequest[] request,
|
||||
OnChainWalletObjectId objectId,
|
||||
OnChainWalletObjectId link,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links", method:HttpMethod.Delete, bodyPayload: request), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", method:HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
#nullable enable
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
|
|
@ -8,48 +8,81 @@ using Newtonsoft.Json.Linq;
|
|||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
|
||||
public class OnChainWalletObjectQuery
|
||||
{
|
||||
public string[]? Types { get; set; }
|
||||
public OnChainWalletObjectId[]? Parents { get; set; }
|
||||
public OnChainWalletObjectId[]? Children { get; set; }
|
||||
|
||||
public bool IncludeLinks { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectId()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectId(string type, string id)
|
||||
{
|
||||
Type = type;
|
||||
Id = id;
|
||||
}
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class RemoveOnChainWalletObjectLinkRequest
|
||||
public class AddOnChainWalletObjectLinkRequest : OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectId Parent { get; set; }
|
||||
public OnChainWalletObjectId Child { get; set; }
|
||||
}
|
||||
public class AddOnChainWalletObjectLinkRequest
|
||||
{
|
||||
public OnChainWalletObjectId Parent { get; set; }
|
||||
public OnChainWalletObjectId Child { get; set; }
|
||||
public JObject? Data { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class OnChainWalletObjectData:OnChainWalletObjectId
|
||||
{
|
||||
public class OnChainWalletObjectLink:OnChainWalletObjectId
|
||||
public AddOnChainWalletObjectLinkRequest()
|
||||
{
|
||||
public JObject? LinkData { get; set; }
|
||||
|
||||
}
|
||||
public JObject? Data { get; set; }
|
||||
public OnChainWalletObjectLink[]? Parents { get; set; }
|
||||
public OnChainWalletObjectLink[]? Children { get; set; }
|
||||
public AddOnChainWalletObjectLinkRequest(string objectType, string objectId) : base(objectType, objectId)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class GetWalletObjectsRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string[] Ids { get; set; }
|
||||
public bool? IncludeNeighbourData { get; set; }
|
||||
}
|
||||
|
||||
public class AddOnChainWalletObjectRequest : OnChainWalletObjectId
|
||||
{
|
||||
public AddOnChainWalletObjectRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public AddOnChainWalletObjectRequest(string objectType, string objectId) : base(objectType, objectId)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
}
|
||||
|
||||
public class OnChainWalletObjectData : OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectData()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectData(string type, string id) : base(type, id)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class OnChainWalletObjectLink : OnChainWalletObjectId
|
||||
{
|
||||
public OnChainWalletObjectLink()
|
||||
{
|
||||
|
||||
}
|
||||
public OnChainWalletObjectLink(string type, string id) : base(type, id)
|
||||
{
|
||||
|
||||
}
|
||||
public JObject LinkData { get; set; }
|
||||
public JObject ObjectData { get; set; }
|
||||
}
|
||||
public JObject Data { get; set; }
|
||||
public OnChainWalletObjectLink[] Links { get; set; }
|
||||
}
|
||||
|
||||
public class OnChainWalletTransactionData
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
|
|
|
@ -30,6 +30,36 @@ namespace BTCPayServer.Data
|
|||
public List<WalletObjectLinkData> ChildLinks { get; set; }
|
||||
public List<WalletObjectLinkData> ParentLinks { get; set; }
|
||||
|
||||
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
|
||||
{
|
||||
if (ChildLinks is not null)
|
||||
foreach (var c in ChildLinks)
|
||||
{
|
||||
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
|
||||
}
|
||||
if (ParentLinks is not null)
|
||||
foreach (var c in ParentLinks)
|
||||
{
|
||||
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<WalletObjectData> GetNeighbours()
|
||||
{
|
||||
if (ChildLinks != null)
|
||||
foreach (var c in ChildLinks)
|
||||
{
|
||||
if (c.Child != null)
|
||||
yield return c.Child;
|
||||
}
|
||||
if (ParentLinks != null)
|
||||
foreach (var c in ParentLinks)
|
||||
{
|
||||
if (c.Parent != null)
|
||||
yield return c.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectData>().HasKey(o =>
|
||||
|
|
|
@ -2996,113 +2996,119 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
|
||||
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
|
||||
|
||||
var client = await admin.CreateClient(Policies.Unrestricted);
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()));
|
||||
await client.AddOrUpdateOnChainWalletObjects(admin.StoreId, "BTC",
|
||||
new OnChainWalletObjectData[] {new OnChainWalletObjectData() {Id = "test", Type = "test"}});
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()));
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()
|
||||
{
|
||||
Types = new []{ "test"},
|
||||
}));
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()
|
||||
{
|
||||
Types = new []{ "test-wrong"},
|
||||
}));
|
||||
await client.RemoveOnChainWalletObjects(admin.StoreId, "BTC", new()
|
||||
{
|
||||
Types = new []{ "test"},
|
||||
});
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()));
|
||||
|
||||
await client.AddOrUpdateOnChainWalletObjects(admin.StoreId, "BTC",
|
||||
new OnChainWalletObjectData[] {new OnChainWalletObjectData() {Id = "test", Type = "test", Children = new []
|
||||
{
|
||||
new OnChainWalletObjectData.OnChainWalletObjectLink()
|
||||
{
|
||||
Id = "test-child",
|
||||
Type = "test",
|
||||
|
||||
},
|
||||
},
|
||||
Parents = new []{ new OnChainWalletObjectData.OnChainWalletObjectLink()
|
||||
{
|
||||
Id = "test-parent",
|
||||
Type = "test",
|
||||
|
||||
},}
|
||||
}});
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new()
|
||||
{
|
||||
Types = new []{ "test"},
|
||||
}));
|
||||
|
||||
|
||||
await client.AddOrUpdateOnChainWalletObjects(admin.StoreId, "BTC",
|
||||
new OnChainWalletObjectData[] {
|
||||
new OnChainWalletObjectData() {Id = "test-child", Type = "test",},
|
||||
new OnChainWalletObjectData() {Id = "test-parent", Type = "test",},
|
||||
|
||||
|
||||
new OnChainWalletObjectData() {Id = "test", Type = "test", Children = new []
|
||||
{
|
||||
new OnChainWalletObjectData.OnChainWalletObjectLink()
|
||||
{
|
||||
Id = "test-child",
|
||||
Type = "test",
|
||||
|
||||
},
|
||||
},
|
||||
Parents = new []{ new OnChainWalletObjectData.OnChainWalletObjectLink()
|
||||
{
|
||||
Id = "test-parent",
|
||||
Type = "test",
|
||||
|
||||
},}
|
||||
}});
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
var test = new OnChainWalletObjectId("test", "test");
|
||||
Assert.NotNull(await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id)));
|
||||
|
||||
var objs = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new() {Types = new[] {"test"}, IncludeLinks = true});
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
Assert.NotNull(await client.GetOnChainWalletObject(admin.StoreId, "BTC", test));
|
||||
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test-wrong", "test")));
|
||||
Assert.Null(await client.GetOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test-wrong")));
|
||||
|
||||
await client.RemoveOnChainWalletObject(admin.StoreId, "BTC", new OnChainWalletObjectId("test", "test"));
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
|
||||
var test1 = new OnChainWalletObjectId("test", "test1");
|
||||
var test2 = new OnChainWalletObjectId("test", "test2");
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test.Type, test.Id));
|
||||
// Those links don't exists
|
||||
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id)));
|
||||
await AssertAPIError("wallet-object-not-found", () => client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id)));
|
||||
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC"));
|
||||
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test1.Type, test1.Id));
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest(test2.Type, test2.Id));
|
||||
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test1.Type, test1.Id));
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC", test, new AddOnChainWalletObjectLinkRequest(test2.Type, test2.Id));
|
||||
|
||||
var objs = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
|
||||
Assert.Equal(3, objs.Length);
|
||||
var middleObj = objs.Single(data => data.Id == "test" && data.Type == "test");
|
||||
Assert.Equal("test-child", Assert.Single(middleObj.Children).Id);
|
||||
Assert.Equal("test-parent", Assert.Single(middleObj.Parents).Id);
|
||||
|
||||
Assert.Equal("test", Assert.Single(objs.Single(data => data.Id == "test-parent" && data.Type == "test").Children).Id);
|
||||
Assert.Equal("test", Assert.Single(objs.Single(data => data.Id == "test-child" && data.Type == "test").Parents).Id);
|
||||
Assert.Equal(2, middleObj.Links.Length);
|
||||
Assert.Contains("test1", middleObj.Links.Select(l => l.Id));
|
||||
Assert.Contains("test2", middleObj.Links.Select(l => l.Id));
|
||||
|
||||
var test1Obj = objs.Single(data => data.Id == "test1" && data.Type == "test");
|
||||
var test2Obj = objs.Single(data => data.Id == "test2" && data.Type == "test");
|
||||
Assert.Single(test1Obj.Links.Select(l => l.Id), l => l == "test");
|
||||
Assert.Single(test2Obj.Links.Select(l => l.Id), l => l == "test");
|
||||
|
||||
await client.RemoveOnChainWalletLinks(admin.StoreId, "BTC",
|
||||
new[]
|
||||
{
|
||||
new RemoveOnChainWalletObjectLinkRequest()
|
||||
{
|
||||
Parent = new OnChainWalletObjectId() {Id = "test-parent", Type = "test"},
|
||||
Child = new OnChainWalletObjectId() {Id = "test", Type = "test"}
|
||||
}
|
||||
});
|
||||
test1,
|
||||
test);
|
||||
|
||||
var testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
|
||||
Assert.Single(testObj.Links.Select(l => l.Id), l => l == "test2");
|
||||
Assert.Single(testObj.Links);
|
||||
test1Obj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test1);
|
||||
Assert.Empty(test1Obj.Links);
|
||||
|
||||
await client.AddOrUpdateOnChainWalletLink(admin.StoreId, "BTC",
|
||||
test1,
|
||||
new AddOnChainWalletObjectLinkRequest(test.Type, test.Id) { Data = new JObject() { ["testData"] = "lol" } });
|
||||
|
||||
// Add some data to test1
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = test1.Type, Id = test1.Id, Data = new JObject() { ["testData"] = "test1" } });
|
||||
|
||||
// Create a new type
|
||||
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id });
|
||||
|
||||
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1"));
|
||||
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false);
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
|
||||
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null));
|
||||
|
||||
async Task TestWalletRepository(bool useInefficient)
|
||||
{
|
||||
// We should have 4 nodes, two `test` type and one `newtype`
|
||||
// Only the node `test` `test` is connected to `test1`
|
||||
var wid = new WalletId(admin.StoreId, "BTC");
|
||||
var repo = tester.PayTester.GetService<WalletRepository>();
|
||||
var allObjects = await repo.GetWalletObjects((GetWalletObjectsQuery)(new(wid, null) { UseInefficientPath = useInefficient }));
|
||||
var allTests = await repo.GetWalletObjects((GetWalletObjectsQuery)(new(wid, "test") { UseInefficientPath = useInefficient }));
|
||||
var twoTests2 = await repo.GetWalletObjects((GetWalletObjectsQuery)(new(wid, "test", new[] { "test1", "test2", "test-unk" }) { UseInefficientPath = useInefficient }));
|
||||
var oneTest = await repo.GetWalletObjects((GetWalletObjectsQuery)(new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient }));
|
||||
var oneTestWithoutData = await repo.GetWalletObjects((GetWalletObjectsQuery)(new(wid, "test", new[] { "test" }) { UseInefficientPath = useInefficient, IncludeNeighbours = false }));
|
||||
|
||||
Assert.Equal(4, allObjects.Count);
|
||||
Assert.Equal(3, allTests.Count);
|
||||
Assert.Equal(2, twoTests2.Count);
|
||||
Assert.Single(oneTest);
|
||||
Assert.NotNull(oneTest.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
|
||||
Assert.Single(oneTestWithoutData);
|
||||
Assert.Null(oneTestWithoutData.First().Value.GetNeighbours().Select(n => n.Data).FirstOrDefault());
|
||||
}
|
||||
await TestWalletRepository(false);
|
||||
await TestWalletRepository(true);
|
||||
|
||||
{
|
||||
var allObjects = await client.GetOnChainWalletObjects(admin.StoreId, "BTC");
|
||||
var allTests = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test" });
|
||||
var twoTests2 = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test1", "test2", "test-unk" } });
|
||||
var oneTest = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids=new[] { "test" } });
|
||||
var oneTestWithoutData = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new GetWalletObjectsRequest() { Type = "test", Ids = new[] { "test" }, IncludeNeighbourData = false });
|
||||
|
||||
Assert.Equal(4, allObjects.Length);
|
||||
Assert.Equal(3, allTests.Length);
|
||||
Assert.Equal(2, twoTests2.Length);
|
||||
Assert.Single(oneTest);
|
||||
Assert.NotNull(oneTest.First().Links.Select(n => n.ObjectData).FirstOrDefault());
|
||||
Assert.Single(oneTestWithoutData);
|
||||
Assert.Null(oneTestWithoutData.First().Links.Select(n => n.ObjectData).FirstOrDefault());
|
||||
}
|
||||
|
||||
Assert.Empty(await client.GetOnChainWalletObjects(admin.StoreId, "BTC",
|
||||
new() {Parents = new OnChainWalletObjectId[] {new() {Id = "test-parent", Type = "test"}}})
|
||||
);
|
||||
|
||||
await client.AddOrUpdateOnChainWalletLinks(admin.StoreId, "BTC",
|
||||
new AddOnChainWalletObjectLinkRequest[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Parent = new OnChainWalletObjectId() {Id = "test-parent", Type = "test"},
|
||||
Child = new OnChainWalletObjectId() {Id = "test", Type = "test"}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Single(await client.GetOnChainWalletObjects(admin.StoreId, "BTC",
|
||||
new() {Parents = new OnChainWalletObjectId[] {new() {Id = "test-parent", Type = "test"}}})
|
||||
);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
<OutputType>Exe</OutputType>
|
||||
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
|
||||
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
|
||||
|
||||
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\**" />
|
||||
|
|
|
@ -28,6 +28,7 @@ using NBXplorer;
|
|||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
|
@ -621,115 +622,114 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
[FromBody] OnChainWalletObjectQuery query)
|
||||
public async Task<IActionResult> GetOnChainWalletObjects(string storeId, string cryptoCode, string? type = null, [FromQuery(Name = "ids")] string[]? ids = null, bool? includeNeighbourData = null)
|
||||
{
|
||||
if (ids?.Length is 0 && !Request.Query.ContainsKey("ids"))
|
||||
ids = null;
|
||||
if (type is null && ids is not null)
|
||||
ModelState.AddModelError(nameof(ids), "If ids is specified, type should be specified");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
return Ok((await _walletRepository.GetWalletObjects(new(walletId, type, ids) { IncludeNeighbours = includeNeighbourData ?? true })).Select(kv => kv.Value).Select(ToModel).ToArray());
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetOnChainWalletObject(string storeId, string cryptoCode,
|
||||
string objectType, string objectId,
|
||||
bool? includeNeighbourData = null)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
return Ok((await _walletRepository.GetWalletObjects(walletId, query)).Select(ToModel).ToArray());
|
||||
var wo = await _walletRepository.GetWalletObject(new(walletId, objectType, objectId), includeNeighbourData ?? true);
|
||||
if (wo is null)
|
||||
return WalletObjectNotFound();
|
||||
return Ok(ToModel(wo));
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
[FromBody] OnChainWalletObjectQuery query)
|
||||
public async Task<IActionResult> RemoveOnChainWalletObject(string storeId, string cryptoCode,
|
||||
string objectType, string objectId)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
await _walletRepository.RemoveWalletObjects(walletId, query);
|
||||
return Ok();
|
||||
if (await _walletRepository.RemoveWalletObjects(new WalletObjectId(walletId, objectType, objectId)))
|
||||
return Ok();
|
||||
else
|
||||
return WalletObjectNotFound();
|
||||
}
|
||||
|
||||
private IActionResult WalletObjectNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "wallet-object-not-found", "This wallet object's can't be found");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
[FromBody] OnChainWalletObjectData[] request)
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode,
|
||||
[FromBody] AddOnChainWalletObjectRequest request)
|
||||
{
|
||||
if (request?.Type is null)
|
||||
ModelState.AddModelError(nameof(request.Type), "Type is required");
|
||||
if (request?.Id is null)
|
||||
ModelState.AddModelError(nameof(request.Id), "Id is required");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
foreach (OnChainWalletObjectData onChainWalletObjectData in request)
|
||||
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObject(
|
||||
new WalletObjectId(walletId, onChainWalletObjectData.Type, onChainWalletObjectData.Id),
|
||||
onChainWalletObjectData.Data);
|
||||
new WalletObjectId(walletId, request!.Type, request.Id), request.Data);
|
||||
return await GetOnChainWalletObject(storeId, cryptoCode, request!.Type, request.Id);
|
||||
}
|
||||
|
||||
foreach (OnChainWalletObjectData onChainWalletObjectData in request)
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
if (onChainWalletObjectData.Children?.Any() is true)
|
||||
{
|
||||
var parent = new WalletObjectId(walletId, onChainWalletObjectData.Type,
|
||||
onChainWalletObjectData.Id);
|
||||
|
||||
foreach (var onChainWalletObjectLink in onChainWalletObjectData.Children)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObjectLink(parent,
|
||||
new WalletObjectId(walletId, onChainWalletObjectLink.Type, onChainWalletObjectLink.Id),
|
||||
onChainWalletObjectData.Data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onChainWalletObjectData.Parents?.Any() is true)
|
||||
{
|
||||
var child = new WalletObjectId(walletId, onChainWalletObjectData.Type,
|
||||
onChainWalletObjectData.Id);
|
||||
|
||||
foreach (var onChainWalletObjectLink in onChainWalletObjectData.Parents)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObjectLink(
|
||||
new WalletObjectId(walletId, onChainWalletObjectLink.Type, onChainWalletObjectLink.Id),
|
||||
child, onChainWalletObjectData.Data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
return WalletObjectNotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
[FromBody] AddOnChainWalletObjectLinkRequest[] request)
|
||||
string objectType, string objectId,
|
||||
[FromBody] AddOnChainWalletObjectLinkRequest request)
|
||||
{
|
||||
if (request?.Type is null)
|
||||
ModelState.AddModelError(nameof(request.Type), "Type is required");
|
||||
if (request?.Id is null)
|
||||
ModelState.AddModelError(nameof(request.Id), "Id is required");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
foreach (AddOnChainWalletObjectLinkRequest addOnChainWalletObjectLinkRequest in request)
|
||||
try
|
||||
{
|
||||
await _walletRepository.SetWalletObjectLink(
|
||||
new WalletObjectId(walletId, addOnChainWalletObjectLinkRequest.Parent.Type,
|
||||
addOnChainWalletObjectLinkRequest.Parent.Id),
|
||||
new WalletObjectId(walletId, addOnChainWalletObjectLinkRequest.Child.Type,
|
||||
addOnChainWalletObjectLinkRequest.Child.Id), addOnChainWalletObjectLinkRequest.Data);
|
||||
new WalletObjectId(walletId, objectType, objectId),
|
||||
new WalletObjectId(walletId, request!.Type, request.Id),
|
||||
request?.Data);
|
||||
return Ok();
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
return WalletObjectNotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links/{linkType}/{linkId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
[FromBody] RemoveOnChainWalletObjectLinkRequest[] request)
|
||||
public async Task<IActionResult> RemoveOnChainWalletLink(string storeId, string cryptoCode,
|
||||
string objectType, string objectId,
|
||||
string linkType, string linkId)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
foreach (RemoveOnChainWalletObjectLinkRequest removeOnChainWalletObjectLinkRequest in request)
|
||||
{
|
||||
await _walletRepository.RemoveWalletObjectLink(
|
||||
new WalletObjectId(walletId, removeOnChainWalletObjectLinkRequest.Parent.Type,
|
||||
removeOnChainWalletObjectLinkRequest.Parent.Id),
|
||||
new WalletObjectId(walletId, removeOnChainWalletObjectLinkRequest.Child.Type,
|
||||
removeOnChainWalletObjectLinkRequest.Child.Id));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
if (await _walletRepository.RemoveWalletObjectLink(
|
||||
new WalletObjectId(walletId, objectType, objectId),
|
||||
new WalletObjectId(walletId, linkType, linkId)))
|
||||
return Ok();
|
||||
else
|
||||
return WalletObjectNotFound();
|
||||
}
|
||||
|
||||
private OnChainWalletObjectData ToModel(WalletObjectData data)
|
||||
|
@ -739,18 +739,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
Data = string.IsNullOrEmpty(data.Data) ? null : JObject.Parse(data.Data),
|
||||
Type = data.Type,
|
||||
Id = data.Id,
|
||||
Children = data.ChildLinks?.Select(linkData => ToModel(linkData, false)).ToArray(),
|
||||
Parents = data.ParentLinks?.Select(linkData => ToModel(linkData, true)).ToArray()
|
||||
Links = data.GetLinks().Select(linkData => ToModel(linkData)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private OnChainWalletObjectData.OnChainWalletObjectLink ToModel(WalletObjectLinkData data, bool isParent)
|
||||
private OnChainWalletObjectData.OnChainWalletObjectLink ToModel((string type, string id, string linkdata, string objectdata) data)
|
||||
{
|
||||
return new OnChainWalletObjectData.OnChainWalletObjectLink()
|
||||
{
|
||||
LinkData = string.IsNullOrEmpty(data.Data) ? null : JObject.Parse(data.Data),
|
||||
Type = isParent ? data.ParentType : data.ChildType,
|
||||
Id = isParent ? data.ParentId : data.ChildId,
|
||||
LinkData = string.IsNullOrEmpty(data.linkdata) ? null : JObject.Parse(data.linkdata),
|
||||
ObjectData = string.IsNullOrEmpty(data.objectdata) ? null : JObject.Parse(data.objectdata),
|
||||
Type = data.type,
|
||||
Id = data.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -222,36 +222,38 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
await GetController<GreenfieldCustodianAccountController>().MarketTradeCustodianAccountAsset(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
public override async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
GetWalletObjectsRequest query = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<OnChainWalletObjectData[]>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletObjects(storeId, cryptoCode, query));
|
||||
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletObjects(storeId, cryptoCode, query?.Type, query?.Ids, query?.IncludeNeighbourData));
|
||||
}
|
||||
|
||||
public override async Task RemoveOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
CancellationToken token = default)
|
||||
public override async Task<OnChainWalletObjectData> GetOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId, bool? includeNeighbourData = null, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletObjects(storeId, cryptoCode, query));
|
||||
return GetFromActionResult<OnChainWalletObjectData>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletObject(storeId, cryptoCode, objectId.Type, objectId.Id, includeNeighbourData));
|
||||
}
|
||||
public override async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<OnChainWalletObjectData>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletObject(storeId, cryptoCode, request));
|
||||
}
|
||||
|
||||
public override async Task AddOrUpdateOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectData[] request,
|
||||
CancellationToken token = default)
|
||||
public override async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode, OnChainWalletObjectId objectId, OnChainWalletObjectId link, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletObjects(storeId, cryptoCode, request));
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletLink(storeId, cryptoCode, objectId.Type, objectId.Id, link.Type, link.Id));
|
||||
}
|
||||
|
||||
public override async Task AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode, AddOnChainWalletObjectLinkRequest[] request,
|
||||
CancellationToken token = default)
|
||||
public override async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletLinks(storeId, cryptoCode, request));
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletObject(storeId, cryptoCode, objectId.Type, objectId.Id));
|
||||
}
|
||||
|
||||
public override async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode, RemoveOnChainWalletObjectLinkRequest[] request,
|
||||
CancellationToken token = default)
|
||||
public override async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode, OnChainWalletObjectId objectId, AddOnChainWalletObjectLinkRequest request = null, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletLinks(storeId, cryptoCode, request));
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletLinks(storeId, cryptoCode, objectId.Type, objectId.Id, request));
|
||||
}
|
||||
|
||||
public override async Task<StoreWebhookData> CreateWebhook(string storeId, CreateStoreWebhookRequest create,
|
||||
|
|
|
@ -429,8 +429,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
var storeWalletMatched = (await _explorerClientProvider.GetExplorerClient(newTransaction.CryptoCode)
|
||||
.GetTransactionAsync(derivationSchemeSettings,
|
||||
newTransaction.NewTransactionEvent.TransactionData.TransactionHash));
|
||||
//if the wallet related to the store related to the payout does not have the tx: it is external
|
||||
var isInternal = storeWalletMatched is { };
|
||||
//if the wallet related to the store of the payout does not have the tx: it has been paid externally
|
||||
var isInternal = storeWalletMatched is not null;
|
||||
|
||||
var proof = ParseProof(payout) as PayoutTransactionOnChainBlob ??
|
||||
new PayoutTransactionOnChainBlob() { Accounted = isInternal };
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace BTCPayServer.Data
|
|||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
|
||||
}
|
||||
|
||||
public static JObject? GetProofBlobJson(this PayoutData data)
|
||||
public static JObject GetProofBlobJson(this PayoutData data)
|
||||
{
|
||||
return data?.Proof is null ? null : JObject.Parse(Encoding.UTF8.GetString(data.Proof));
|
||||
}
|
||||
|
|
|
@ -763,7 +763,7 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
|
||||
public string PayoutId { get; set; }
|
||||
public JObject? Proof { get; set; }
|
||||
public JObject Proof { get; set; }
|
||||
public PayoutState State { get; set; }
|
||||
|
||||
public static string GetErrorMessage(PayoutPaidResult result)
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace BTCPayServer;
|
||||
//from https://stackoverflow.com/a/67666993/275504
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IQueryable<T> FilterByItems<T, TItem>(this IQueryable<T> query, IEnumerable<TItem> items,
|
||||
Expression<Func<T, TItem, bool>> filterPattern, bool isOr)
|
||||
{
|
||||
Expression predicate = null;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemExpr = Expression.Constant(item);
|
||||
var itemCondition = ExpressionReplacer.Replace(filterPattern.Body, filterPattern.Parameters[1], itemExpr);
|
||||
if (predicate == null)
|
||||
predicate = itemCondition;
|
||||
else
|
||||
{
|
||||
predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
|
||||
itemCondition);
|
||||
}
|
||||
}
|
||||
|
||||
predicate ??= Expression.Constant(false);
|
||||
var filterLambda = Expression.Lambda<Func<T, bool>>(predicate, filterPattern.Parameters[0]);
|
||||
|
||||
return query.Where(filterLambda);
|
||||
}
|
||||
|
||||
class ExpressionReplacer : ExpressionVisitor
|
||||
{
|
||||
readonly IDictionary<Expression, Expression> _replaceMap;
|
||||
|
||||
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
|
||||
{
|
||||
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
|
||||
}
|
||||
|
||||
public override Expression Visit(Expression exp)
|
||||
{
|
||||
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
|
||||
return replacement;
|
||||
return base.Visit(exp);
|
||||
}
|
||||
|
||||
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
|
||||
{
|
||||
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
|
||||
}
|
||||
|
||||
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
|
||||
{
|
||||
return new ExpressionReplacer(replaceMap).Visit(expr);
|
||||
}
|
||||
|
||||
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
|
||||
{
|
||||
if (lambda.Parameters.Count != toReplace.Length)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
|
||||
.ToDictionary(i => (Expression) lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,32 @@ namespace BTCPayServer.Services
|
|||
{
|
||||
#nullable enable
|
||||
public record WalletObjectId(WalletId WalletId, string Type, string Id);
|
||||
public class GetWalletObjectsQuery
|
||||
{
|
||||
public GetWalletObjectsQuery(WalletId walletId) : this(walletId, null, null)
|
||||
{
|
||||
}
|
||||
public GetWalletObjectsQuery(WalletObjectId walletObjectId) : this(walletObjectId.WalletId, walletObjectId.Type, new[] { walletObjectId.Id })
|
||||
{
|
||||
|
||||
}
|
||||
public GetWalletObjectsQuery(WalletId walletId, string type) : this (walletId, type, null)
|
||||
{
|
||||
}
|
||||
public GetWalletObjectsQuery(WalletId walletId, string? type, string[]? ids)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
WalletId = walletId;
|
||||
Type = type;
|
||||
Ids = ids;
|
||||
}
|
||||
|
||||
public WalletId WalletId { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string[]? Ids { get; set; }
|
||||
public bool IncludeNeighbours { get; set; } = true;
|
||||
public bool UseInefficientPath { get; set; }
|
||||
}
|
||||
#nullable restore
|
||||
public class WalletRepository
|
||||
{
|
||||
|
@ -23,157 +49,212 @@ namespace BTCPayServer.Services
|
|||
{
|
||||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
|
||||
#nullable enable
|
||||
public async Task<WalletObjectData?> GetWalletObject(WalletObjectId walletObjectId, bool includeNeighbours = true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
var r = await GetWalletObjects(new(walletObjectId) { IncludeNeighbours = includeNeighbours });
|
||||
return r.Select(o => o.Value).FirstOrDefault();
|
||||
}
|
||||
public async Task<Dictionary<WalletObjectId, WalletObjectData>> GetWalletObjects(GetWalletObjectsQuery queryObject)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryObject);
|
||||
if (queryObject.Ids != null && queryObject.Type is null)
|
||||
throw new ArgumentException("If \"Ids\" is not null, \"Type\" is mandatory");
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
|
||||
|
||||
IQueryable<WalletObjectLinkData> wols;
|
||||
IQueryable<WalletObjectData> wos;
|
||||
|
||||
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
|
||||
// Such request isn't well optimized by postgres, and create different requests clogging up
|
||||
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
|
||||
if (ctx.Database.IsNpgsql() && transactionIds is not null)
|
||||
// On top of this, the entity version is doing 2 left join to satisfy the Include queries, resulting in n*m row returned for each transaction.
|
||||
// n being the number of children, m the number of parents.
|
||||
if (ctx.Database.IsNpgsql() && !queryObject.UseInefficientPath)
|
||||
{
|
||||
wos = ctx.WalletObjects
|
||||
.FromSqlInterpolated($"SELECT wos.* FROM unnest({transactionIds}) t JOIN \"WalletObjects\" wos ON wos.\"WalletId\"={walletId.ToString()} AND wos.\"Type\"={WalletObjectData.Types.Tx} AND wos.\"Id\"=t")
|
||||
.AsNoTracking();
|
||||
wols = ctx.WalletObjectLinks
|
||||
.FromSqlInterpolated($"SELECT wol.* FROM unnest({transactionIds}) t JOIN \"WalletObjectLinks\" wol ON wol.\"WalletId\"={walletId.ToString()} AND wol.\"ChildType\"={WalletObjectData.Types.Tx} AND wol.\"ChildId\"=t")
|
||||
.AsNoTracking();
|
||||
var connection = ctx.Database.GetDbConnection();
|
||||
if (connection.State != System.Data.ConnectionState.Open)
|
||||
await connection.OpenAsync();
|
||||
|
||||
string typeFilter = queryObject.Type is not null ? "AND wos.\"Type\"=@type " : "";
|
||||
var cmd = connection.CreateCommand();
|
||||
var selectWalletObjects =
|
||||
queryObject.Ids is null ?
|
||||
$"SELECT wos.* FROM \"WalletObjects\" wos WHERE wos.\"WalletId\"=@walletId {typeFilter}" :
|
||||
queryObject.Ids.Length == 1 ?
|
||||
"SELECT wos.* FROM \"WalletObjects\" wos WHERE wos.\"WalletId\"=@walletId AND wos.\"Type\"=@type AND wos.\"Id\"=@id" :
|
||||
"SELECT wos.* FROM unnest(@ids) t JOIN \"WalletObjects\" wos ON wos.\"WalletId\"=@walletId AND wos.\"Type\"=@type AND wos.\"Id\"=t";
|
||||
|
||||
var includeNeighbourSelect = queryObject.IncludeNeighbours ? ", wos2.\"Data\" AS \"Data2\"" : "";
|
||||
var includeNeighbourJoin = queryObject.IncludeNeighbours ? "LEFT JOIN \"WalletObjects\" wos2 ON wos.\"WalletId\"=wos2.\"WalletId\" AND wol.\"Type2\"=wos2.\"Type\" AND wol.\"Id2\"=wos2.\"Id\"" : "";
|
||||
var query =
|
||||
$"SELECT wos.\"Id\", wos.\"Type\", wos.\"Data\", wol.\"LinkData\", wol.\"Type2\", wol.\"Id2\"{includeNeighbourSelect} FROM ({selectWalletObjects}) wos " +
|
||||
$"LEFT JOIN LATERAL ( " +
|
||||
"SELECT \"ParentType\" AS \"Type2\", \"ParentId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ChildType\"=wos.\"Type\" AND \"ChildId\"=wos.\"Id\" " +
|
||||
"UNION " +
|
||||
"SELECT \"ChildType\" AS \"Type2\", \"ChildId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ParentType\"=wos.\"Type\" AND \"ParentId\"=wos.\"Id\"" +
|
||||
$" ) wol ON true " + includeNeighbourJoin;
|
||||
cmd.CommandText = query;
|
||||
var walletIdParam = cmd.CreateParameter();
|
||||
walletIdParam.ParameterName = "walletId";
|
||||
walletIdParam.Value = queryObject.WalletId.ToString();
|
||||
walletIdParam.DbType = System.Data.DbType.String;
|
||||
cmd.Parameters.Add(walletIdParam);
|
||||
|
||||
if (queryObject.Type != null)
|
||||
{
|
||||
var typeParam = cmd.CreateParameter();
|
||||
typeParam.ParameterName = "type";
|
||||
typeParam.Value = queryObject.Type;
|
||||
typeParam.DbType = System.Data.DbType.String;
|
||||
cmd.Parameters.Add(typeParam);
|
||||
}
|
||||
|
||||
if (queryObject.Ids != null)
|
||||
{
|
||||
if (queryObject.Ids.Length == 1)
|
||||
{
|
||||
var txIdParam = cmd.CreateParameter();
|
||||
txIdParam.ParameterName = "id";
|
||||
txIdParam.Value = queryObject.Ids[0];
|
||||
txIdParam.DbType = System.Data.DbType.String;
|
||||
cmd.Parameters.Add(txIdParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
var txIdsParam = cmd.CreateParameter();
|
||||
txIdsParam.ParameterName = "ids";
|
||||
txIdsParam.Value = queryObject.Ids.ToHashSet().ToList();
|
||||
txIdsParam.DbType = System.Data.DbType.Object;
|
||||
cmd.Parameters.Add(txIdsParam);
|
||||
}
|
||||
}
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
var wosById = new Dictionary<WalletObjectId, WalletObjectData>();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
WalletObjectData wo = new WalletObjectData();
|
||||
wo.Type = (string)reader["Type"];
|
||||
wo.Id = (string)reader["Id"];
|
||||
var id = new WalletObjectId(queryObject.WalletId, wo.Type, wo.Id);
|
||||
wo.Data = reader["Data"] is DBNull ? null : (string)reader["Data"];
|
||||
if (wosById.TryGetValue(id, out var wo2))
|
||||
wo = wo2;
|
||||
else
|
||||
{
|
||||
wosById.Add(id, wo);
|
||||
wo.ChildLinks = new List<WalletObjectLinkData>();
|
||||
}
|
||||
if (reader["Type2"] is not DBNull)
|
||||
{
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
ChildType = (string)reader["Type2"],
|
||||
ChildId = (string)reader["Id2"],
|
||||
Data = reader["LinkData"] is DBNull ? null : (string)reader["LinkData"]
|
||||
};
|
||||
wo.ChildLinks.Add(l);
|
||||
l.Child = new WalletObjectData()
|
||||
{
|
||||
Type = l.ChildType,
|
||||
Id = l.ChildId,
|
||||
Data = (!queryObject.IncludeNeighbours || reader["Data2"] is DBNull) ? null : (string)reader["Data2"]
|
||||
};
|
||||
}
|
||||
}
|
||||
return wosById;
|
||||
}
|
||||
else // Unefficient path
|
||||
{
|
||||
wos = ctx.WalletObjects
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Tx && (transactionIds == null || transactionIds.Contains(w.Id)));
|
||||
wols = ctx.WalletObjectLinks
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.ChildType == WalletObjectData.Types.Tx && (transactionIds == null || transactionIds.Contains(w.ChildId)));
|
||||
}
|
||||
var links = await wols
|
||||
.Select(tx =>
|
||||
new
|
||||
var q = ctx.WalletObjects
|
||||
.Where(w => w.WalletId == queryObject.WalletId.ToString() && (queryObject.Type == null || w.Type == queryObject.Type) && (queryObject.Ids == null || queryObject.Ids.Contains(w.Id)));
|
||||
if (queryObject.IncludeNeighbours)
|
||||
{
|
||||
TxId = tx.ChildId,
|
||||
AssociatedDataId = tx.ParentId,
|
||||
AssociatedDataType = tx.ParentType,
|
||||
AssociatedData = tx.Parent.Data
|
||||
})
|
||||
.ToArrayAsync();
|
||||
var objs = await wos
|
||||
.Select(tx =>
|
||||
new
|
||||
{
|
||||
TxId = tx.Id,
|
||||
Data = tx.Data
|
||||
})
|
||||
.ToArrayAsync();
|
||||
q = q.Include(o => o.ChildLinks).ThenInclude(o => o.Child)
|
||||
.Include(o => o.ParentLinks).ThenInclude(o => o.Parent);
|
||||
}
|
||||
q = q.AsNoTracking();
|
||||
|
||||
var result = new Dictionary<string, WalletTransactionInfo>(objs.Length);
|
||||
foreach (var obj in objs)
|
||||
var wosById = new Dictionary<WalletObjectId, WalletObjectData>();
|
||||
foreach (var row in await q.ToListAsync())
|
||||
{
|
||||
var id = new WalletObjectId(queryObject.WalletId, row.Type, row.Id);
|
||||
wosById.TryAdd(id, row);
|
||||
}
|
||||
return wosById;
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
|
||||
{
|
||||
var wos = await GetWalletObjects((GetWalletObjectsQuery)(new(walletId, WalletObjectData.Types.Tx, transactionIds)));
|
||||
var result = new Dictionary<string, WalletTransactionInfo>(wos.Count);
|
||||
foreach (var obj in wos.Values)
|
||||
{
|
||||
var data = obj.Data is null ? null : JObject.Parse(obj.Data);
|
||||
result.Add(obj.TxId, new WalletTransactionInfo(walletId)
|
||||
var info = new WalletTransactionInfo(walletId)
|
||||
{
|
||||
Comment = data?["comment"]?.Value<string>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
foreach (var row in links)
|
||||
{
|
||||
JObject data = row.AssociatedData is null ? null : JObject.Parse(row.AssociatedData);
|
||||
var info = result[row.TxId];
|
||||
|
||||
if (row.AssociatedDataType == WalletObjectData.Types.Label)
|
||||
};
|
||||
result.Add(obj.Id, info);
|
||||
foreach (var neighbour in obj.GetNeighbours())
|
||||
{
|
||||
info.LabelColors.TryAdd(row.AssociatedDataId, data["color"]?.Value<string>() ?? "#000");
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Attachments.Add(new Attachment(row.AssociatedDataType, row.AssociatedDataId, row.AssociatedData is null ? null : JObject.Parse(row.AssociatedData)));
|
||||
var neighbourData = neighbour.Data is null ? null : JObject.Parse(neighbour.Data);
|
||||
if (neighbour.Type == WalletObjectData.Types.Label)
|
||||
{
|
||||
info.LabelColors.TryAdd(neighbour.Id, neighbourData?["color"]?.Value<string>() ?? "#000");
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Attachments.Add(new Attachment(neighbour.Type, neighbour.Id, neighbourData));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletId walletId)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
return (await (await GetWalletObjects(ctx, ctx.WalletObjects
|
||||
.AsNoTracking(), walletId, new OnChainWalletObjectQuery() {Types = new[] {WalletObjectData.Types.Label}})).ToArrayAsync())
|
||||
.Select(o => (o.Id, JObject.Parse(o.Data)["color"]!.Value<string>()!))
|
||||
.ToArray();
|
||||
return (await
|
||||
ctx.WalletObjects.AsNoTracking().Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Label)
|
||||
.ToArrayAsync())
|
||||
.Select(o => (o.Id, JObject.Parse(o.Data)["color"]!.Value<string>()!)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<WalletObjectData[]> GetWalletObjects(WalletId walletId, OnChainWalletObjectQuery query)
|
||||
public async Task<bool> RemoveWalletObjects(WalletObjectId walletObjectId)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
return await (await GetWalletObjects(ctx,ctx.WalletObjects.AsNoTracking(), walletId, query)).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveWalletObjects( WalletId walletId, OnChainWalletObjectQuery query)
|
||||
{
|
||||
query.IncludeLinks = false;
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjects.RemoveRange(await GetWalletObjects(ctx,ctx.WalletObjects, walletId, query));
|
||||
await ctx.SaveChangesAsync();
|
||||
var entity = new WalletObjectData()
|
||||
{
|
||||
WalletId = walletObjectId.WalletId.ToString(),
|
||||
Type = walletObjectId.Type,
|
||||
Id = walletObjectId.Id
|
||||
};
|
||||
ctx.WalletObjects.Add(entity);
|
||||
ctx.Entry(entity).State = EntityState.Deleted;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException) // doesn't exists
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IQueryable<WalletObjectData>> GetWalletObjects(ApplicationDbContext applicationDbContext,
|
||||
IQueryable<WalletObjectData> queryable, WalletId walletId, OnChainWalletObjectQuery query)
|
||||
{
|
||||
var result = queryable.AsQueryable();
|
||||
result = result.Where(w => w.WalletId == walletId.ToString());
|
||||
if (query.IncludeLinks)
|
||||
{
|
||||
result = result
|
||||
.Include(data => data.ChildLinks)
|
||||
.Include(data => data.ParentLinks);
|
||||
}
|
||||
if (query.Types is not null)
|
||||
{
|
||||
result = result.Where(w => query.Types.Contains(w.Type));
|
||||
}
|
||||
|
||||
if (query.Parents is not null)
|
||||
{
|
||||
var allowedChildren = await applicationDbContext.WalletObjectLinks
|
||||
.Where(data => data.WalletId == walletId.ToString())
|
||||
.FilterByItems(query.Parents, (data, id) => data.ParentId == id.Id && data.ParentType == id.Type,
|
||||
true).Select(data => new WalletObjectId(walletId, data.ChildType, data.ChildId)).ToArrayAsync();
|
||||
|
||||
result = result.FilterByItems(allowedChildren,(data, id) => data.Id == id.Id && data.Type == id.Type, true);
|
||||
}
|
||||
if (query.Children is not null)
|
||||
{
|
||||
var allowedParents = await applicationDbContext.WalletObjectLinks
|
||||
.Where(data => data.WalletId == walletId.ToString())
|
||||
.FilterByItems(query.Children, (data, id) => data.ChildId == id.Id && data.ChildType == id.Type,
|
||||
true).Select(data => new WalletObjectId(walletId, data.ParentType, data.ParentId)).ToArrayAsync();
|
||||
|
||||
result = result.FilterByItems(allowedParents,(data, id) => data.Id == id.Id && data.Type == id.Type, true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task EnsureWalletObjectLink(WalletObjectId parent, WalletObjectId child, JObject? data = null)
|
||||
public async Task EnsureWalletObjectLink(WalletObjectId a, WalletObjectId b, JObject? data = null)
|
||||
{
|
||||
SortWalletObjectLinks(ref a, ref b);
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = parent.WalletId.ToString(),
|
||||
ChildType = child.Type,
|
||||
ChildId = child.Id,
|
||||
ParentType = parent.Type,
|
||||
ParentId = parent.Id,
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentType = a.Type,
|
||||
ParentId = a.Id,
|
||||
ChildType = b.Type,
|
||||
ChildId = b.Id,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
ctx.WalletObjectLinks.Add(l);
|
||||
|
@ -186,16 +267,40 @@ namespace BTCPayServer.Services
|
|||
}
|
||||
}
|
||||
|
||||
public async Task SetWalletObjectLink(WalletObjectId parent, WalletObjectId child, JObject? data = null)
|
||||
class WalletObjectIdComparer : IComparer<WalletObjectId>
|
||||
{
|
||||
public static readonly WalletObjectIdComparer Instance = new WalletObjectIdComparer();
|
||||
public int Compare(WalletObjectId? x, WalletObjectId? y)
|
||||
{
|
||||
var c = StringComparer.InvariantCulture.Compare(x?.Type, y?.Type);
|
||||
if (c == 0)
|
||||
c = StringComparer.InvariantCulture.Compare(x?.Id, y?.Id);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
private void SortWalletObjectLinks(ref WalletObjectId a, ref WalletObjectId b)
|
||||
{
|
||||
if (a.WalletId != b.WalletId)
|
||||
throw new ArgumentException("It shouldn't be possible to set a link between different wallets");
|
||||
var ab = new[] { a, b };
|
||||
Array.Sort(ab, WalletObjectIdComparer.Instance);
|
||||
a = ab[0];
|
||||
b = ab[1];
|
||||
}
|
||||
public async Task SetWalletObjectLink(WalletObjectId a, WalletObjectId b, JObject? data = null)
|
||||
{
|
||||
SortWalletObjectLinks(ref a, ref b);
|
||||
|
||||
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = parent.WalletId.ToString(),
|
||||
ChildType = child.Type,
|
||||
ChildId = child.Id,
|
||||
ParentType = parent.Type,
|
||||
ParentId = parent.Id,
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentType = a.Type,
|
||||
ParentId = a.Id,
|
||||
ChildType = b.Type,
|
||||
ChildId = b.Id,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
var e = ctx.WalletObjectLinks.Add(l);
|
||||
|
@ -293,23 +398,26 @@ namespace BTCPayServer.Services
|
|||
}
|
||||
}
|
||||
|
||||
public async Task RemoveWalletObjectLink(WalletObjectId parent, WalletObjectId child)
|
||||
public async Task<bool> RemoveWalletObjectLink(WalletObjectId a, WalletObjectId b)
|
||||
{
|
||||
SortWalletObjectLinks(ref a, ref b);
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = parent.WalletId.ToString(),
|
||||
ChildId = child.Id,
|
||||
ChildType = child.Type,
|
||||
ParentId = parent.Id,
|
||||
ParentType = parent.Type
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentId = a.Id,
|
||||
ParentType = a.Type,
|
||||
ChildId = b.Id,
|
||||
ChildType = b.Type
|
||||
});
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException) // Already deleted, do nothing
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task RemoveWalletObjectLabels(WalletObjectId id, params string[] labels)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Get store on-chain wallet objects",
|
||||
"parameters": [
|
||||
|
@ -25,23 +25,39 @@
|
|||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "The type of object to fetch",
|
||||
"schema": { "type": "string" },
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "ids",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "The ids of objects to fetch, if used, type should be specified",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"example": "03abcde..."
|
||||
},
|
||||
{
|
||||
"name": "includeNeighbourData",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Whether or not you should include neighbour's node data in the result (ie, `links.objectData`)",
|
||||
"schema": { "type": "boolean", "default": true }
|
||||
}
|
||||
],
|
||||
"description": "View wallet objects",
|
||||
"operationId": "StoreOnChainWallets_GetOnChainWalletObjects",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified wallet",
|
||||
"description": "Selected objects and their links",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
|
@ -69,67 +85,9 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
],
|
||||
"summary": "Remove store on-chain wallet objects",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The crypto code of the payment method to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
}
|
||||
],
|
||||
"description": "Remove wallet objects",
|
||||
"operationId": "StoreOnChainWallets_RemoveOnChainWalletObjects",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful removal of filtered objects"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Add/Update store on-chain wallet objects",
|
||||
"parameters": [
|
||||
|
@ -160,12 +118,251 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Wallet object's data and its links",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Get store on-chain wallet object",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The crypto code of the payment method to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
},
|
||||
{
|
||||
"name": "objectType",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object type to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "objectId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object id to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "abc392..."
|
||||
},
|
||||
{
|
||||
"name": "includeNeighbourData",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Whether or not you should include neighbour's node data in the result (ie, `links.objectData`)",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View wallet object",
|
||||
"operationId": "StoreOnChainWallets_GetOnChainWalletObject",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Wallet object's data and its links",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Remove store on-chain wallet objects",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The crypto code of the payment method to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
},
|
||||
{
|
||||
"name": "objectType",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object type to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "objectId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object id to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "abc392..."
|
||||
}
|
||||
],
|
||||
"description": "Remove wallet object",
|
||||
"operationId": "StoreOnChainWallets_RemoveOnChainWalletObject",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful removal of filtered object"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Add/Update store on-chain wallet object link",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The crypto code of the payment method to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
},
|
||||
{
|
||||
"name": "objectType",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object type to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "objectId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object id to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "abc392..."
|
||||
}
|
||||
],
|
||||
"description": "Add/Update wallet object link",
|
||||
"operationId": "StoreOnChainWallets_AddOrUpdateOnChainWalletLink",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AddOnChainWalletObjectLinkRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
|
@ -189,10 +386,10 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links": {
|
||||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectType}/{objectId}/links/{linkType}/{linkId}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Remove store on-chain wallet object links",
|
||||
"parameters": [
|
||||
|
@ -214,87 +411,53 @@
|
|||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
}
|
||||
],
|
||||
"description": "Remove wallet object links",
|
||||
"operationId": "StoreOnChainWallets_RemoveOnChainWalletLinks",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RemoveOnChainWalletObjectLinkRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful removal of filtered object links"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
],
|
||||
"summary": "Add/Update store on-chain wallet object links",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"name": "objectType",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The crypto code of the payment method to fetch",
|
||||
"description": "The object type to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "objectId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object id to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "abc392..."
|
||||
},
|
||||
{
|
||||
"name": "linkType",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object type of the linked neighbour",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "tx"
|
||||
},
|
||||
{
|
||||
"name": "linkId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The object id of the linked neighbour",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "abc392..."
|
||||
}
|
||||
],
|
||||
"description": "Add/Update wallet object links",
|
||||
"operationId": "StoreOnChainWallets_AddOrUpdateOnChainWalletLinks",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AddOnChainWalletObjectLinkRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Remove wallet object link",
|
||||
"operationId": "StoreOnChainWallets_RemoveOnChainWalletLink",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "action completed"
|
||||
"description": "successful removal of filtered object link"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
|
@ -330,69 +493,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"OnChainWalletObjectQuery": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"types": {
|
||||
"nullable": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The types of wallet objects you want to query"
|
||||
},
|
||||
"parents": {
|
||||
"nullable": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
},
|
||||
"description": "Filter out objects which have these parents"
|
||||
},
|
||||
"children": {
|
||||
"nullable": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
},
|
||||
"description": "Filter out objects which have these children"
|
||||
},
|
||||
"includeLinks": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to include the links in the object results. Note that if `Parents` or `Children` are used, this setting is implicitly true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RemoveOnChainWalletObjectLinkRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"parent": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
},
|
||||
"child": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AddOnChainWalletObjectLinkRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"parent": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
},
|
||||
"child": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"OnChainWalletObjectLink": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
|
@ -401,9 +502,34 @@
|
|||
}
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "The data of the link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OnChainWalletObjectLink": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of wallet object"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the wallet object (unique per type, per wallet)"
|
||||
},
|
||||
"linkData": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
"additionalProperties": true,
|
||||
"description": "The data of the link"
|
||||
},
|
||||
"objectData": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "The data of the neighbour's node (`null` if there isn't any data or `includeNeighbourData` is `false`)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -420,30 +546,16 @@
|
|||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"parents": {
|
||||
"links": {
|
||||
"nullable": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectLink"
|
||||
},
|
||||
"description": "objects which are parent to this object"
|
||||
},
|
||||
"children": {
|
||||
"nullable": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectLink"
|
||||
},
|
||||
"description": "objects which are children to this object"
|
||||
"description": "Links of this object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Store Wallet (On Chain) Objects",
|
||||
"description": "Store Wallet (On Chain) operations"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue