mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 14:04:12 +01:00
Greenfield: Wallet Objects (#4274)
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
324112b73b
commit
2740dfea87
8 changed files with 1072 additions and 63 deletions
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", method:HttpMethod.Get, bodyPayload: query), token);
|
||||
return await HandleResponse<OnChainWalletObjectData[]>(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
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);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task AddOrUpdateOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectData[] 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);
|
||||
}
|
||||
public virtual async Task AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode, AddOnChainWalletObjectLinkRequest[] request,
|
||||
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);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
RemoveOnChainWalletObjectLinkRequest[] request,
|
||||
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);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,9 +4,52 @@ using BTCPayServer.JsonConverters;
|
|||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
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 string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class RemoveOnChainWalletObjectLinkRequest
|
||||
{
|
||||
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 JObject? LinkData { get; set; }
|
||||
}
|
||||
public JObject? Data { get; set; }
|
||||
public OnChainWalletObjectLink[]? Parents { get; set; }
|
||||
public OnChainWalletObjectLink[]? Children { get; set; }
|
||||
}
|
||||
|
||||
public class OnChainWalletTransactionData
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
|
|
|
@ -2878,7 +2878,121 @@ namespace BTCPayServer.Tests
|
|||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseWalletObjectsAPI()
|
||||
{
|
||||
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",
|
||||
|
||||
},}
|
||||
}});
|
||||
|
||||
var objs = await client.GetOnChainWalletObjects(admin.StoreId, "BTC", new() {Types = new[] {"test"}, IncludeLinks = true});
|
||||
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);
|
||||
|
||||
await client.RemoveOnChainWalletLinks(admin.StoreId, "BTC",
|
||||
new[]
|
||||
{
|
||||
new RemoveOnChainWalletObjectLinkRequest()
|
||||
{
|
||||
Parent = new OnChainWalletObjectId() {Id = "test-parent", Type = "test"},
|
||||
Child = new OnChainWalletObjectId() {Id = "test", Type = "test"}
|
||||
}
|
||||
});
|
||||
|
||||
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)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
|
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
@ -112,7 +112,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var feeRateTarget = blockTarget ?? Store.GetStoreBlob().RecommendedFeeBlockTarget;
|
||||
|
@ -125,10 +125,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false)
|
||||
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode,
|
||||
bool forceGenerate = false)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, cryptoCode), forceGenerate);
|
||||
|
@ -141,13 +142,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
var allowedPayjoin = derivationScheme.IsHotWallet && Store.GetStoreBlob().PayJoinEnabled;
|
||||
if (allowedPayjoin)
|
||||
{
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new { cryptoCode })));
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
|
||||
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new {cryptoCode})));
|
||||
}
|
||||
|
||||
return Ok(new OnChainWalletAddressData()
|
||||
{
|
||||
Address = kpi.Address?.ToString(),
|
||||
PaymentLink = bip21.ToString(),
|
||||
KeyPath = kpi.KeyPath
|
||||
Address = kpi.Address?.ToString(), PaymentLink = bip21.ToString(), KeyPath = kpi.KeyPath
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -156,7 +158,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
|
||||
|
@ -165,6 +167,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return this.CreateAPIError("no-reserved-address",
|
||||
$"There was no reserved address for {cryptoCode} on this store.");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -180,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
@ -190,7 +193,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
var preFiltering = true;
|
||||
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))
|
||||
preFiltering = false;
|
||||
var txs = await wallet.FetchTransactionHistory(derivationScheme.AccountDerivation, preFiltering ? skip : 0, preFiltering ? limit : int.MaxValue);
|
||||
var txs = await wallet.FetchTransactionHistory(derivationScheme.AccountDerivation, preFiltering ? skip : 0,
|
||||
preFiltering ? limit : int.MaxValue);
|
||||
if (!preFiltering)
|
||||
{
|
||||
var filteredList = new List<TransactionHistoryLine>(txs.Count);
|
||||
|
@ -202,6 +206,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
if (transactionInfo?.LabelColors.ContainsKey(labelFilter) is true)
|
||||
filteredList.Add(t);
|
||||
}
|
||||
|
||||
if (statusFilter?.Any() is true)
|
||||
{
|
||||
if (statusFilter.Contains(TransactionStatus.Confirmed) && t.Confirmations != 0)
|
||||
|
@ -210,6 +215,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
filteredList.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
txs = filteredList;
|
||||
}
|
||||
|
||||
|
@ -227,7 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
string transactionId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
@ -239,16 +245,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId })).Values
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] {transactionId})).Values
|
||||
.FirstOrDefault();
|
||||
|
||||
return Ok(ToModel(walletTransactionsInfoAsync, tx, wallet));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPatch("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||
[HttpPatch(
|
||||
"~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||
public async Task<IActionResult> PatchOnChainWalletTransaction(
|
||||
string storeId,
|
||||
string storeId,
|
||||
string cryptoCode,
|
||||
string transactionId,
|
||||
[FromBody] PatchOnChainTransactionRequest request,
|
||||
|
@ -256,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
@ -280,7 +287,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
}
|
||||
|
||||
var walletTransactionsInfo =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId }))
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] {transactionId}))
|
||||
.Values
|
||||
.FirstOrDefault();
|
||||
|
||||
|
@ -292,14 +299,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
|
@ -317,7 +325,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
Timestamp = coin.Timestamp,
|
||||
KeyPath = coin.KeyPath,
|
||||
Confirmations = coin.Confirmations,
|
||||
Address = network.NBXplorerNetwork.CreateAddress(derivationScheme.AccountDerivation, coin.KeyPath, coin.ScriptPubKey).ToString()
|
||||
Address = network.NBXplorerNetwork
|
||||
.CreateAddress(derivationScheme.AccountDerivation, coin.KeyPath, coin.ScriptPubKey)
|
||||
.ToString()
|
||||
};
|
||||
}).ToList()
|
||||
);
|
||||
|
@ -329,7 +339,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
[FromBody] CreateOnChainTransactionRequest request)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
out var derivationScheme, out var actionResult))
|
||||
out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
if (network.ReadonlyWallet)
|
||||
{
|
||||
|
@ -340,7 +350,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
|
||||
if (!(await CanUseHotWallet()).HotWallet)
|
||||
{
|
||||
return this.CreateAPIError(503, "not-available", $"You need to allow non-admins to use hotwallets for their stores (in /server/policies)");
|
||||
return this.CreateAPIError(503, "not-available",
|
||||
$"You need to allow non-admins to use hotwallets for their stores (in /server/policies)");
|
||||
}
|
||||
|
||||
if (request.Destinations == null || !request.Destinations.Any())
|
||||
|
@ -401,6 +412,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
{
|
||||
amount = null;
|
||||
}
|
||||
|
||||
var address = string.Empty;
|
||||
try
|
||||
{
|
||||
|
@ -433,9 +445,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
if (amount is null || amount <= 0)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||
"Amount must be specified or destination must be a BIP21 payment link, and greater than 0", this);
|
||||
"Amount must be specified or destination must be a BIP21 payment link, and greater than 0",
|
||||
this);
|
||||
}
|
||||
if (request.ProceedWithPayjoin && bip21?.UnknownParameters?.ContainsKey(PayjoinClient.BIP21EndpointKey) is true)
|
||||
|
||||
if (request.ProceedWithPayjoin &&
|
||||
bip21?.UnknownParameters?.ContainsKey(PayjoinClient.BIP21EndpointKey) is true)
|
||||
{
|
||||
payjoinOutputIndex = index;
|
||||
}
|
||||
|
@ -565,14 +580,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
await _delayedTransactionBroadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0),
|
||||
transaction, network);
|
||||
var payjoinPSBT = await _payjoinClient.RequestPayjoin(
|
||||
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork), new PayjoinWallet(derivationScheme),
|
||||
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork),
|
||||
new PayjoinWallet(derivationScheme),
|
||||
psbt.PSBT, CancellationToken.None);
|
||||
psbt.PSBT.Settings.SigningOptions = new SigningOptions() { EnforceLowR = !(signingContext?.EnforceLowR is false) };
|
||||
psbt.PSBT.Settings.SigningOptions =
|
||||
new SigningOptions() {EnforceLowR = !(signingContext?.EnforceLowR is false)};
|
||||
payjoinPSBT = psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey, rootedKeyPath);
|
||||
payjoinPSBT.Finalize();
|
||||
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||
var hash = payjoinTransaction.GetHash();
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode), hash, Attachment.Payjoin());
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode),
|
||||
hash, Attachment.Payjoin());
|
||||
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
|
@ -601,19 +619,157 @@ 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)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
return Ok((await _walletRepository.GetWalletObjects(walletId, query)).Select(ToModel).ToArray());
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
[FromBody] OnChainWalletObjectQuery query)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
await _walletRepository.RemoveWalletObjects(walletId, query);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
foreach (OnChainWalletObjectData onChainWalletObjectData in request)
|
||||
{
|
||||
await _walletRepository.SetWalletObject(
|
||||
new WalletObjectId(walletId, onChainWalletObjectData.Type, onChainWalletObjectData.Id),
|
||||
onChainWalletObjectData.Data);
|
||||
}
|
||||
|
||||
foreach (OnChainWalletObjectData onChainWalletObjectData in request)
|
||||
{
|
||||
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 Ok();
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
[FromBody] AddOnChainWalletObjectLinkRequest[] request)
|
||||
{
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
foreach (AddOnChainWalletObjectLinkRequest addOnChainWalletObjectLinkRequest in request)
|
||||
{
|
||||
await _walletRepository.SetWalletObjectLink(
|
||||
new WalletObjectId(walletId, addOnChainWalletObjectLinkRequest.Parent.Type,
|
||||
addOnChainWalletObjectLinkRequest.Parent.Id),
|
||||
new WalletObjectId(walletId, addOnChainWalletObjectLinkRequest.Child.Type,
|
||||
addOnChainWalletObjectLinkRequest.Child.Id), addOnChainWalletObjectLinkRequest.Data);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/object-links")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
[FromBody] RemoveOnChainWalletObjectLinkRequest[] request)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private OnChainWalletObjectData ToModel(WalletObjectData data)
|
||||
{
|
||||
return new OnChainWalletObjectData()
|
||||
{
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
private OnChainWalletObjectData.OnChainWalletObjectLink ToModel(WalletObjectLinkData data, bool isParent)
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||
{
|
||||
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
|
||||
}
|
||||
|
||||
private bool IsInvalidWalletRequest(string cryptoCode, [MaybeNullWhen(true)] out BTCPayNetwork network,
|
||||
[MaybeNullWhen(true)] out DerivationSchemeSettings derivationScheme, [MaybeNullWhen(false)] out IActionResult actionResult)
|
||||
[MaybeNullWhen(true)] out DerivationSchemeSettings derivationScheme,
|
||||
[MaybeNullWhen(false)] out IActionResult actionResult)
|
||||
{
|
||||
derivationScheme = null;
|
||||
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network is null)
|
||||
{
|
||||
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance"));
|
||||
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode",
|
||||
"This crypto code isn't set up in this BTCPay Server instance"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -215,7 +215,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
throw new NotSupportedException("This method is not supported by the LocalBTCPayServerClient.");
|
||||
}
|
||||
|
||||
|
||||
public override async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId,
|
||||
TradeRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
@ -223,6 +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,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<OnChainWalletObjectData[]>(
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletObjects(storeId, cryptoCode, query));
|
||||
|
||||
}
|
||||
|
||||
public override async Task RemoveOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletObjects(storeId, cryptoCode, query));
|
||||
}
|
||||
|
||||
public override async Task AddOrUpdateOnChainWalletObjects(string storeId, string cryptoCode, OnChainWalletObjectData[] request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletObjects(storeId, cryptoCode, request));
|
||||
}
|
||||
|
||||
public override async Task AddOrUpdateOnChainWalletLinks(string storeId, string cryptoCode, AddOnChainWalletObjectLinkRequest[] request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().AddOrUpdateOnChainWalletLinks(storeId, cryptoCode, request));
|
||||
}
|
||||
|
||||
public override async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode, RemoveOnChainWalletObjectLinkRequest[] request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreOnChainWalletsController>().RemoveOnChainWalletLinks(storeId, cryptoCode, request));
|
||||
}
|
||||
|
||||
public override async Task<StoreWebhookData> CreateWebhook(string storeId, CreateStoreWebhookRequest create,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -1167,7 +1198,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
{
|
||||
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
|
||||
}
|
||||
|
||||
|
||||
public override async Task MarkPayoutPaid(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldPullPaymentController>().MarkPayoutPaid(storeId, payoutId, cancellationToken));
|
||||
|
|
68
BTCPayServer/QueryableExtensions.cs
Normal file
68
BTCPayServer/QueryableExtensions.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
|
@ -107,26 +105,76 @@ namespace BTCPayServer.Services
|
|||
|
||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletId walletId)
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
return (await ctx.WalletObjects
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Label)
|
||||
.Select(o => new { o.Id, o.Data })
|
||||
.ToArrayAsync())
|
||||
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();
|
||||
}
|
||||
|
||||
public async Task EnsureWalletObjectLink(WalletObjectId parent, WalletObjectId child)
|
||||
public async Task<WalletObjectData[]> GetWalletObjects(WalletId walletId, OnChainWalletObjectQuery query)
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
ParentId = parent.Id,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
ctx.WalletObjectLinks.Add(l);
|
||||
try
|
||||
|
@ -138,6 +186,30 @@ namespace BTCPayServer.Services
|
|||
}
|
||||
}
|
||||
|
||||
public async Task SetWalletObjectLink(WalletObjectId parent, WalletObjectId child, JObject? data = null)
|
||||
{
|
||||
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,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
var e = ctx.WalletObjectLinks.Add(l);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // already exists
|
||||
{
|
||||
e.State = EntityState.Modified;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public static int MaxCommentSize = 200;
|
||||
public async Task SetWalletObjectComment(WalletObjectId id, string comment)
|
||||
{
|
||||
|
@ -194,6 +266,21 @@ namespace BTCPayServer.Services
|
|||
await EnsureWalletObjectLink(labelObjId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddWalletObjects(WalletObjectId id, params string[] labels)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
await EnsureWalletObject(id);
|
||||
foreach (var l in labels.Select(l => l.Trim().Truncate(MaxLabelSize)))
|
||||
{
|
||||
var labelObjId = new WalletObjectId(id.WalletId, WalletObjectData.Types.Label, l);
|
||||
await EnsureWalletObject(labelObjId, new JObject()
|
||||
{
|
||||
["color"] = ColorPalette.Default.DeterministicColor(l)
|
||||
});
|
||||
await EnsureWalletObjectLink(labelObjId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment)
|
||||
{
|
||||
|
@ -221,35 +308,40 @@ namespace BTCPayServer.Services
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveWalletObjectLink(WalletObjectId parent, WalletObjectId child)
|
||||
{
|
||||
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
|
||||
});
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Already deleted, do nothing
|
||||
{
|
||||
}
|
||||
}
|
||||
public async Task RemoveWalletObjectLabels(WalletObjectId id, params string[] labels)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
foreach (var l in labels.Select(l => l.Trim()))
|
||||
{
|
||||
var labelObjId = new WalletObjectId(id.WalletId, WalletObjectData.Types.Label, l);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = id.WalletId.ToString(),
|
||||
ChildId = id.Id,
|
||||
ChildType = id.Type,
|
||||
ParentId = labelObjId.Id,
|
||||
ParentType = labelObjId.Type
|
||||
});
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Already deleted, do nothing
|
||||
{
|
||||
}
|
||||
await RemoveWalletObjectLink(labelObjId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetWalletObject(WalletObjectId id, JObject? data)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var o = NewWalletObjectData(id, data);
|
||||
ctx.WalletObjects.Add(o);
|
||||
try
|
||||
|
@ -266,7 +358,7 @@ namespace BTCPayServer.Services
|
|||
public async Task EnsureWalletObject(WalletObjectId id, JObject? data = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjects.Add(NewWalletObjectData(id, data));
|
||||
try
|
||||
{
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
{
|
||||
"paths": {
|
||||
"/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
],
|
||||
"summary": "Get 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": "View wallet objects",
|
||||
"operationId": "StoreOnChainWallets_GetOnChainWalletObjects",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified wallet",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$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) 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"
|
||||
],
|
||||
"summary": "Add/Update 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": "Add/Update wallet objects",
|
||||
"operationId": "StoreOnChainWallets_AddOrUpdateOnChainWalletObjects",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "action completed"
|
||||
},
|
||||
"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/object-links": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain) Objects"
|
||||
],
|
||||
"summary": "Remove store on-chain wallet object links",
|
||||
"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 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",
|
||||
"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": "Add/Update wallet object links",
|
||||
"operationId": "StoreOnChainWallets_AddOrUpdateOnChainWalletLinks",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AddOnChainWalletObjectLinkRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "action completed"
|
||||
},
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"OnChainWalletObjectId": {
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"linkData": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"OnChainWalletObjectData": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/OnChainWalletObjectId"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"parents": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Store Wallet (On Chain) Objects",
|
||||
"description": "Store Wallet (On Chain) operations"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue