mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
GreenField API: Wallet API (#2246)
* GreenField: Wallet API * more work * wip * rough fiunish of transaction sending api * Allow to create tx without broadcasting and small fixes * Refactor Wallet Receive feature ad add greenfield api for address reserve for wallet * add wallet api client * add docs * fix json converter tags * fixes and add wallet tests * fix tests * fix rebase * fixes * just pass the tests already * ugggh * small cleanup * revert int support in numeric string converter and make block id as native number in json * fix LN endpoint * try fix flaky test * Revert "try fix flaky test" This reverts commit 2e0d256325b892f7741325dcbab01196f74d182a. * try fix other flaky test * return proepr error if fee rate could not be fetched * try fix test again * reduce fee related logic for wallet api * try reduce code changes for pr scope * change auth logic for initial release of wallet api
This commit is contained in:
109
BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs
Normal file
109
BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token);
|
||||||
|
return await HandleResponse<OnChainWalletOverviewData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{"forceGenerate", forceGenerate}
|
||||||
|
}), token);
|
||||||
|
return await HandleResponse<OnChainWalletAddressData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address",method:HttpMethod.Delete), token);
|
||||||
|
await HandleResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||||
|
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var query = new Dictionary<string, object>();
|
||||||
|
if (statusFilter?.Any() is true)
|
||||||
|
{
|
||||||
|
query.Add(nameof(statusFilter), statusFilter);
|
||||||
|
}
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", query), token);
|
||||||
|
return await HandleResponse<IEnumerable<OnChainWalletTransactionData>>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
|
||||||
|
string storeId, string cryptoCode, string transactionId,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
|
||||||
|
return await HandleResponse<OnChainWalletTransactionData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<IEnumerable<OnChainWalletUTXOData>> GetOnChainWalletUTXOs(string storeId,
|
||||||
|
string cryptoCode,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/utxos"), token);
|
||||||
|
return await HandleResponse<IEnumerable<OnChainWalletUTXOData>>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
|
||||||
|
string cryptoCode, CreateOnChainTransactionRequest request,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!request.ProceedWithBroadcast)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||||
|
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
|
||||||
|
}
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<OnChainWalletTransactionData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
|
||||||
|
string cryptoCode, CreateOnChainTransactionRequest request, Network network,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (request.ProceedWithBroadcast)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||||
|
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
|
||||||
|
}
|
||||||
|
var response =
|
||||||
|
await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||||
|
return Transaction.Parse(await HandleResponse<string>(response), network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
public class CreateInvoiceRequest
|
public class CreateInvoiceRequest
|
||||||
{
|
{
|
||||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
public JObject Metadata { get; set; }
|
public JObject Metadata { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class CreateOnChainTransactionRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public class CreateOnChainTransactionRequestDestination
|
||||||
|
{
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal? Amount { get; set; }
|
||||||
|
public bool SubtractFromAmount { get; set; }
|
||||||
|
}
|
||||||
|
[JsonConverter(typeof(FeeRateJsonConverter))]
|
||||||
|
public FeeRate FeeRate { get; set; }
|
||||||
|
public bool ProceedWithPayjoin { get; set; }= true;
|
||||||
|
public bool ProceedWithBroadcast { get; set; } = true;
|
||||||
|
public bool NoChange { get; set; } = false;
|
||||||
|
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
|
||||||
|
public List<OutPoint> SelectedInputs { get; set; } = null;
|
||||||
|
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
|
||||||
|
[JsonProperty("rbf")]
|
||||||
|
public bool? RBF { get; set; } = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
BTCPayServer.Client/Models/LabelData.cs
Normal file
14
BTCPayServer.Client/Models/LabelData.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class LabelData
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
|
||||||
|
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
BTCPayServer.Client/Models/OnChainWalletAddressData.cs
Normal file
13
BTCPayServer.Client/Models/OnChainWalletAddressData.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class OnChainWalletAddressData
|
||||||
|
{
|
||||||
|
public string Address { get; set; }
|
||||||
|
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||||
|
public KeyPath KeyPath { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
BTCPayServer.Client/Models/OnChainWalletOverviewData.cs
Normal file
12
BTCPayServer.Client/Models/OnChainWalletOverviewData.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class OnChainWalletOverviewData
|
||||||
|
{
|
||||||
|
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal Balance { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
34
BTCPayServer.Client/Models/OnChainWalletTransactionData.cs
Normal file
34
BTCPayServer.Client/Models/OnChainWalletTransactionData.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class OnChainWalletTransactionData
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||||
|
public uint256 TransactionHash { get; set; }
|
||||||
|
|
||||||
|
public string Comment { get; set; }
|
||||||
|
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||||
|
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||||
|
public uint256 BlockHash { get; set; }
|
||||||
|
|
||||||
|
public int? BlockHeight { get; set; }
|
||||||
|
|
||||||
|
public int Confirmations { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
|
public TransactionStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
20
BTCPayServer.Client/Models/OnChainWalletUTXOData.cs
Normal file
20
BTCPayServer.Client/Models/OnChainWalletUTXOData.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class OnChainWalletUTXOData
|
||||||
|
{
|
||||||
|
public string Comment { get; set; }
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
[JsonConverter(typeof(OutpointJsonConverter))]
|
||||||
|
public OutPoint Outpoint { get; set; }
|
||||||
|
public string Link { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, LabelData> Labels { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
BTCPayServer.Client/Models/TransactionStatus.cs
Normal file
9
BTCPayServer.Client/Models/TransactionStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public enum TransactionStatus
|
||||||
|
{
|
||||||
|
Unconfirmed,
|
||||||
|
Confirmed,
|
||||||
|
Replaced
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,31 +24,15 @@ namespace BTCPayServer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
|
public override List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
|
||||||
{
|
{
|
||||||
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
|
return transactionInformationSet.FindAll(information =>
|
||||||
{
|
information.Outputs.Any(output =>
|
||||||
return new TransactionInformationSet()
|
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
|
||||||
{
|
information.Inputs.Any(output =>
|
||||||
Transactions =
|
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
||||||
transactionInformationSet.Transactions.FindAll(information =>
|
|
||||||
information.Outputs.Any(output =>
|
|
||||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
|
|
||||||
information.Inputs.Any(output =>
|
|
||||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GetTransactionsResponse()
|
|
||||||
{
|
|
||||||
Height = response.Height,
|
|
||||||
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
|
|
||||||
ReplacedTransactions = Filter(response.ReplacedTransactions),
|
|
||||||
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||||
{
|
{
|
||||||
//precision 0: 10 = 0.00000010
|
//precision 0: 10 = 0.00000010
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ namespace BTCPayServer
|
|||||||
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
|
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
|
public virtual List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
|
||||||
{
|
{
|
||||||
return response;
|
return transactionInformationSet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -19,6 +20,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.OpenAsset;
|
using NBitcoin.OpenAsset;
|
||||||
|
using NBitcoin.Payment;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -1183,7 +1185,6 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task NotificationAPITests()
|
public async Task NotificationAPITests()
|
||||||
@@ -1280,8 +1281,6 @@ namespace BTCPayServer.Tests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
[Trait("Lightning", "Lightning")]
|
[Trait("Lightning", "Lightning")]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
@@ -1396,6 +1395,222 @@ namespace BTCPayServer.Tests
|
|||||||
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
|
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task WalletAPITests()
|
||||||
|
{
|
||||||
|
using var tester = ServerTester.Create();
|
||||||
|
await tester.StartAsync();
|
||||||
|
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
await user.GrantAccessAsync(true);
|
||||||
|
|
||||||
|
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||||
|
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
|
||||||
|
var walletId = await user.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||||
|
|
||||||
|
//view only clients can't do jack shit with this API
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
});
|
||||||
|
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
Assert.Equal(0m, overview.Balance);
|
||||||
|
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
});
|
||||||
|
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true );
|
||||||
|
Assert.Equal(address.Address, address2.Address);
|
||||||
|
Assert.NotEqual(address.Address, address3.Address);
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode);
|
||||||
|
});
|
||||||
|
Assert.Empty(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
|
||||||
|
uint256 txhash = null;
|
||||||
|
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||||
|
{
|
||||||
|
txhash = await tester.ExplorerNode.SendToAddressAsync(
|
||||||
|
BitcoinAddress.Create(address3.Address, tester.ExplorerClient.Network.NBitcoinNetwork),
|
||||||
|
new Money(0.01m, MoneyUnit.BTC));
|
||||||
|
});
|
||||||
|
await tester.ExplorerNode.GenerateAsync(1);
|
||||||
|
|
||||||
|
var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false );
|
||||||
|
Assert.NotEqual(address3.Address, address4.Address);
|
||||||
|
await client.UnReserveOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||||
|
var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true );
|
||||||
|
Assert.Equal(address5.Address, address4.Address);
|
||||||
|
|
||||||
|
|
||||||
|
var utxo = Assert.Single(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
|
||||||
|
Assert.Equal(0.01m, utxo.Amount);
|
||||||
|
Assert.Equal(txhash, utxo.Outpoint.Hash);
|
||||||
|
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode );
|
||||||
|
Assert.Equal(0.01m, overview.Balance);
|
||||||
|
|
||||||
|
//the simplest request:
|
||||||
|
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
|
||||||
|
var createTxRequest = new CreateOnChainTransactionRequest()
|
||||||
|
{
|
||||||
|
Destinations =
|
||||||
|
new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
|
||||||
|
{
|
||||||
|
new CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination()
|
||||||
|
{
|
||||||
|
Destination = nodeAddress.ToString(), Amount = 0.001m
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FeeRate = new FeeRate(5m) //only because regtest may fail but not required
|
||||||
|
};
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest );
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
|
||||||
|
{
|
||||||
|
await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
|
||||||
|
{
|
||||||
|
createTxRequest.ProceedWithBroadcast = false;
|
||||||
|
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest);
|
||||||
|
});
|
||||||
|
Transaction tx;
|
||||||
|
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.NotNull(tx);
|
||||||
|
Assert.Contains(tx.Outputs, txout => txout.IsTo(nodeAddress) && txout.Value.ToDecimal(MoneyUnit.BTC) == 0.001m);
|
||||||
|
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
|
||||||
|
|
||||||
|
// no change test
|
||||||
|
createTxRequest.NoChange = true;
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
Assert.NotNull(tx);
|
||||||
|
Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress) );
|
||||||
|
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
|
||||||
|
|
||||||
|
createTxRequest.NoChange = false;
|
||||||
|
//coin selection
|
||||||
|
await AssertValidationError(new []{nameof(createTxRequest.SelectedInputs)}, async () =>
|
||||||
|
{
|
||||||
|
createTxRequest.SelectedInputs = new List<OutPoint>();
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
createTxRequest.SelectedInputs = new List<OutPoint>()
|
||||||
|
{
|
||||||
|
utxo.Outpoint
|
||||||
|
};
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
createTxRequest.SelectedInputs = null;
|
||||||
|
|
||||||
|
//destination testing
|
||||||
|
await AssertValidationError(new []{ "Destinations"}, async () =>
|
||||||
|
{
|
||||||
|
createTxRequest.Destinations[0].Amount = utxo.Amount;
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
|
||||||
|
createTxRequest.Destinations[0].SubtractFromAmount = true;
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
|
|
||||||
|
await AssertValidationError(new []{ "Destinations[0]"}, async () =>
|
||||||
|
{
|
||||||
|
createTxRequest.Destinations[0].Amount = 0m;
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
|
||||||
|
//dest can be a bip21
|
||||||
|
|
||||||
|
//cant use bip with subtractfromamount
|
||||||
|
createTxRequest.Destinations[0].Amount = null;
|
||||||
|
createTxRequest.Destinations[0].Destination = $"bitcoin:{nodeAddress}?amount=0.001";
|
||||||
|
await AssertValidationError(new []{ "Destinations[0]"}, async () =>
|
||||||
|
{
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
//if amt specified, it overrides bip21 amount
|
||||||
|
createTxRequest.Destinations[0].Amount = 0.0001m;
|
||||||
|
createTxRequest.Destinations[0].SubtractFromAmount = false;
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")) ==0.0001m );
|
||||||
|
|
||||||
|
//fee rate test
|
||||||
|
createTxRequest.FeeRate = FeeRate.Zero;
|
||||||
|
await AssertValidationError(new []{ "FeeRate"}, async () =>
|
||||||
|
{
|
||||||
|
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
createTxRequest.FeeRate = null;
|
||||||
|
|
||||||
|
createTxRequest.Destinations[0].Amount = 0.001m;
|
||||||
|
createTxRequest.Destinations[0].Destination = nodeAddress.ToString();
|
||||||
|
createTxRequest.Destinations[0].SubtractFromAmount = false;
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
});
|
||||||
|
createTxRequest.ProceedWithBroadcast = true;
|
||||||
|
var txdata=
|
||||||
|
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
createTxRequest);
|
||||||
|
Assert.Equal(TransactionStatus.Unconfirmed, txdata.Status);
|
||||||
|
Assert.Null(txdata.BlockHeight);
|
||||||
|
Assert.Null(txdata.BlockHash);
|
||||||
|
Assert.NotNull(await tester.ExplorerClient.GetTransactionAsync(txdata.TransactionHash));
|
||||||
|
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||||
|
});
|
||||||
|
await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||||
|
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
|
||||||
|
});
|
||||||
|
Assert.True(Assert.Single(
|
||||||
|
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
new[] {TransactionStatus.Confirmed})).TransactionHash == utxo.Outpoint.Hash);
|
||||||
|
Assert.Contains(
|
||||||
|
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
new[] {TransactionStatus.Unconfirmed}), data => data.TransactionHash == txdata.TransactionHash);
|
||||||
|
Assert.Contains(
|
||||||
|
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash);
|
||||||
|
await tester.WaitForEvent<NewBlockEvent>(async () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
await tester.ExplorerNode.GenerateAsync(1);
|
||||||
|
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
|
||||||
|
new[] {TransactionStatus.Confirmed}), data => data.TransactionHash == txdata.TransactionHash);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Fast", "Fast")]
|
[Trait("Fast", "Fast")]
|
||||||
public void NumericJsonConverterTests()
|
public void NumericJsonConverterTests()
|
||||||
@@ -1411,7 +1626,6 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.True(jsonConverter.CanConvert(typeof(double)));
|
Assert.True(jsonConverter.CanConvert(typeof(double)));
|
||||||
Assert.True(jsonConverter.CanConvert(typeof(double?)));
|
Assert.True(jsonConverter.CanConvert(typeof(double?)));
|
||||||
Assert.False(jsonConverter.CanConvert(typeof(float)));
|
Assert.False(jsonConverter.CanConvert(typeof(float)));
|
||||||
Assert.False(jsonConverter.CanConvert(typeof(int)));
|
|
||||||
Assert.False(jsonConverter.CanConvert(typeof(string)));
|
Assert.False(jsonConverter.CanConvert(typeof(string)));
|
||||||
|
|
||||||
var numberJson = "1";
|
var numberJson = "1";
|
||||||
|
|||||||
@@ -840,17 +840,24 @@ retry:
|
|||||||
settings.PaymentId == paymentMethodId);
|
settings.PaymentId == paymentMethodId);
|
||||||
|
|
||||||
ReceivedCoin[] senderCoins = null;
|
ReceivedCoin[] senderCoins = null;
|
||||||
|
ReceivedCoin coin = null;
|
||||||
|
ReceivedCoin coin2 = null;
|
||||||
|
ReceivedCoin coin3 = null;
|
||||||
|
ReceivedCoin coin4 = null;
|
||||||
|
ReceivedCoin coin5 = null;
|
||||||
|
ReceivedCoin coin6 = null;
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
|
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
|
||||||
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
||||||
|
coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
|
||||||
|
coin2 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
|
||||||
|
coin3 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
|
||||||
|
coin4 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
|
||||||
|
coin5 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
|
||||||
|
coin6 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
||||||
});
|
});
|
||||||
var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
|
|
||||||
var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
|
|
||||||
var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
|
|
||||||
var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
|
|
||||||
var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
|
|
||||||
var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
|
||||||
|
|
||||||
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
|
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||||
signingKeySettings.RootFingerprint =
|
signingKeySettings.RootFingerprint =
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
ScriptPubKeyType = segwit,
|
ScriptPubKeyType = segwit,
|
||||||
SavePrivateKeys = importKeysToNBX,
|
SavePrivateKeys = importKeysToNBX,
|
||||||
|
ImportKeysToRPC = importKeysToNBX
|
||||||
});
|
});
|
||||||
await store.UpdateWallet(
|
await store.UpdateWallet(
|
||||||
new WalletSetupViewModel
|
new WalletSetupViewModel
|
||||||
|
|||||||
@@ -0,0 +1,539 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.BIP78.Sender;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
|
using BTCPayServer.Payments.PayJoin.Sender;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.Payment;
|
||||||
|
using NBXplorer;
|
||||||
|
using NBXplorer.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers.GreenField
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[EnableCors(CorsPolicies.All)]
|
||||||
|
public class StoreOnChainWalletsController : Controller
|
||||||
|
{
|
||||||
|
private StoreData Store => HttpContext.GetStoreData();
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
private readonly BTCPayWalletProvider _btcPayWalletProvider;
|
||||||
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||||
|
private readonly WalletRepository _walletRepository;
|
||||||
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||||
|
private readonly CssThemeManager _cssThemeManager;
|
||||||
|
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
||||||
|
private readonly WalletsController _walletsController;
|
||||||
|
private readonly PayjoinClient _payjoinClient;
|
||||||
|
private readonly DelayedTransactionBroadcaster _delayedTransactionBroadcaster;
|
||||||
|
private readonly EventAggregator _eventAggregator;
|
||||||
|
private readonly WalletReceiveService _walletReceiveService;
|
||||||
|
|
||||||
|
public StoreOnChainWalletsController(
|
||||||
|
IAuthorizationService authorizationService,
|
||||||
|
BTCPayWalletProvider btcPayWalletProvider,
|
||||||
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
WalletRepository walletRepository,
|
||||||
|
ExplorerClientProvider explorerClientProvider,
|
||||||
|
CssThemeManager cssThemeManager,
|
||||||
|
NBXplorerDashboard nbXplorerDashboard,
|
||||||
|
WalletsController walletsController,
|
||||||
|
PayjoinClient payjoinClient,
|
||||||
|
DelayedTransactionBroadcaster delayedTransactionBroadcaster,
|
||||||
|
EventAggregator eventAggregator,
|
||||||
|
WalletReceiveService walletReceiveService)
|
||||||
|
{
|
||||||
|
_authorizationService = authorizationService;
|
||||||
|
_btcPayWalletProvider = btcPayWalletProvider;
|
||||||
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_walletRepository = walletRepository;
|
||||||
|
_explorerClientProvider = explorerClientProvider;
|
||||||
|
_cssThemeManager = cssThemeManager;
|
||||||
|
_nbXplorerDashboard = nbXplorerDashboard;
|
||||||
|
_walletsController = walletsController;
|
||||||
|
_payjoinClient = payjoinClient;
|
||||||
|
_delayedTransactionBroadcaster = delayedTransactionBroadcaster;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_walletReceiveService = walletReceiveService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet")]
|
||||||
|
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string cryptoCode)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||||
|
return Ok(new OnChainWalletOverviewData()
|
||||||
|
{
|
||||||
|
Balance = await wallet.GetBalance(derivationScheme.AccountDerivation)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, cryptoCode), forceGenerate);
|
||||||
|
if (kpi is null)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
return Ok(new OnChainWalletAddressData()
|
||||||
|
{
|
||||||
|
Address = kpi.Address.ToString(),
|
||||||
|
KeyPath = kpi.KeyPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||||
|
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
|
||||||
|
if (addr is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||||
|
public async Task<IActionResult> ShowOnChainWalletTransactions(string storeId, string cryptoCode,
|
||||||
|
[FromQuery]TransactionStatus[] statusFilter = null)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||||
|
var walletId = new WalletId(storeId, cryptoCode);
|
||||||
|
var walletBlobAsync = await _walletRepository.GetWalletInfo(walletId);
|
||||||
|
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||||
|
|
||||||
|
var txs = await wallet.FetchTransactions(derivationScheme.AccountDerivation);
|
||||||
|
var filteredFlatList = new List<TransactionInformation>();
|
||||||
|
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Confirmed))
|
||||||
|
{
|
||||||
|
filteredFlatList.AddRange(txs.ConfirmedTransactions.Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Unconfirmed))
|
||||||
|
{
|
||||||
|
filteredFlatList.AddRange(txs.UnconfirmedTransactions.Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter is null || !statusFilter.Any() ||statusFilter.Contains(TransactionStatus.Replaced))
|
||||||
|
{
|
||||||
|
filteredFlatList.AddRange(txs.ReplacedTransactions.Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = filteredFlatList.Select(information =>
|
||||||
|
{
|
||||||
|
walletTransactionsInfoAsync.TryGetValue(information.TransactionId.ToString(), out var transactionInfo);
|
||||||
|
return ToModel(transactionInfo, information, wallet);
|
||||||
|
}).ToList();
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||||
|
public async Task<IActionResult> GetOnChainWalletTransaction(string storeId, string cryptoCode,
|
||||||
|
string transactionId)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||||
|
var tx = await wallet.FetchTransaction(derivationScheme.AccountDerivation, uint256.Parse(transactionId));
|
||||||
|
if (tx is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var walletId = new WalletId(storeId, cryptoCode);
|
||||||
|
var walletTransactionsInfoAsync =
|
||||||
|
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] {transactionId})).Values
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return Ok(ToModel(walletTransactionsInfoAsync, tx, wallet));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos")]
|
||||||
|
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string cryptoCode)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||||
|
|
||||||
|
var walletId = new WalletId(storeId, cryptoCode);
|
||||||
|
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||||
|
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||||
|
return Ok(utxos.Select(coin =>
|
||||||
|
{
|
||||||
|
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||||
|
return new OnChainWalletUTXOData()
|
||||||
|
{
|
||||||
|
Outpoint = coin.OutPoint,
|
||||||
|
Amount = coin.Value.GetValue(network),
|
||||||
|
Comment = info?.Comment,
|
||||||
|
Labels = info?.Labels,
|
||||||
|
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||||
|
coin.OutPoint.Hash.ToString())
|
||||||
|
};
|
||||||
|
}).ToList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||||
|
public async Task<IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode,
|
||||||
|
[FromBody] CreateOnChainTransactionRequest request)
|
||||||
|
{
|
||||||
|
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||||
|
if (network.ReadonlyWallet)
|
||||||
|
{
|
||||||
|
return this.CreateAPIError("not-available",
|
||||||
|
$"{cryptoCode} sending services are not currently available");
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode);
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||||
|
|
||||||
|
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||||
|
if (request.SelectedInputs != null || !utxos.Any())
|
||||||
|
{
|
||||||
|
utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true)
|
||||||
|
.ToArray();
|
||||||
|
if (utxos.Any() is false)
|
||||||
|
{
|
||||||
|
//no valid utxos selected
|
||||||
|
request.AddModelError(transactionRequest => transactionRequest.SelectedInputs,
|
||||||
|
"There are no available utxos based on your request", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var balanceAvailable = utxos.Sum(coin => coin.Value.GetValue(network));
|
||||||
|
|
||||||
|
var subtractFeesOutputsCount = new List<int>();
|
||||||
|
var subtractFees = request.Destinations.Any(o => o.SubtractFromAmount);
|
||||||
|
int? payjoinOutputIndex = null;
|
||||||
|
var sum = 0m;
|
||||||
|
var outputs = new List<WalletSendModel.TransactionOutput>();
|
||||||
|
for (var index = 0; index < request.Destinations.Count; index++)
|
||||||
|
{
|
||||||
|
var destination = request.Destinations[index];
|
||||||
|
|
||||||
|
if (destination.SubtractFromAmount)
|
||||||
|
{
|
||||||
|
subtractFeesOutputsCount.Add(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
BitcoinUrlBuilder bip21 = null;
|
||||||
|
var amount = destination.Amount;
|
||||||
|
if (amount.GetValueOrDefault(0) <= 0)
|
||||||
|
{
|
||||||
|
amount = null;
|
||||||
|
}
|
||||||
|
var address = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destination.Destination = destination.Destination.Replace(network.UriScheme+":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
|
||||||
|
amount ??= bip21.Amount.GetValue(network);
|
||||||
|
address = bip21.Address.ToString();
|
||||||
|
if (destination.SubtractFromAmount)
|
||||||
|
{
|
||||||
|
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||||
|
"You cannot use a BIP21 destination along with SubtractFromAmount", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
address = BitcoinAddress.Create(destination.Destination, network.NBitcoinNetwork).ToString();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||||
|
"Destination must be a BIP21 payment link or an address", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (request.ProceedWithPayjoin && bip21?.UnknowParameters?.ContainsKey("pj") is true)
|
||||||
|
{
|
||||||
|
payjoinOutputIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.Add(new WalletSendModel.TransactionOutput()
|
||||||
|
{
|
||||||
|
DestinationAddress = address,
|
||||||
|
Amount = amount,
|
||||||
|
SubtractFeesFromOutput = destination.SubtractFromAmount
|
||||||
|
});
|
||||||
|
sum += destination.Amount ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtractFeesOutputsCount.Count > 1)
|
||||||
|
{
|
||||||
|
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
|
||||||
|
{
|
||||||
|
request.AddModelError(model => model.Destinations[subtractFeesOutput].SubtractFromAmount,
|
||||||
|
"You can only subtract fees from one destination", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balanceAvailable < sum)
|
||||||
|
{
|
||||||
|
request.AddModelError(transactionRequest => transactionRequest.Destinations,
|
||||||
|
"You are attempting to send more than is available", this);
|
||||||
|
}
|
||||||
|
else if (balanceAvailable == sum && !subtractFees)
|
||||||
|
{
|
||||||
|
request.AddModelError(transactionRequest => transactionRequest.Destinations,
|
||||||
|
"You are sending your entire balance, you should subtract the fees from a destination", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
var minRelayFee = this._nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
|
||||||
|
new FeeRate(1.0m);
|
||||||
|
if (request.FeeRate != null && request.FeeRate < minRelayFee)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.FeeRate),
|
||||||
|
"The fee rate specified is lower than the current minimum relay fee");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePSBTResponse psbt;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
psbt = await _walletsController.CreatePSBT(network, derivationScheme,
|
||||||
|
new WalletSendModel()
|
||||||
|
{
|
||||||
|
SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()),
|
||||||
|
Outputs = outputs,
|
||||||
|
AlwaysIncludeNonWitnessUTXO = true,
|
||||||
|
InputSelection = request.SelectedInputs?.Any() is true,
|
||||||
|
AllowFeeBump =
|
||||||
|
!request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe :
|
||||||
|
request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes :
|
||||||
|
WalletSendModel.ThreeStateBool.No,
|
||||||
|
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
|
||||||
|
NoChange = request.NoChange
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (NBXplorerException ex)
|
||||||
|
{
|
||||||
|
return this.CreateAPIError(ex.Error.Code, ex.Error.Message);
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
return this.CreateAPIError("not-available", "You need to update your version of NBXplorer");
|
||||||
|
}
|
||||||
|
|
||||||
|
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||||
|
|
||||||
|
var signingContext = new SigningContextModel()
|
||||||
|
{
|
||||||
|
PayJoinBIP21 =
|
||||||
|
payjoinOutputIndex is null
|
||||||
|
? null
|
||||||
|
: request.Destinations.ElementAt(payjoinOutputIndex.Value).Destination,
|
||||||
|
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR,
|
||||||
|
ChangeAddress = psbt.ChangeAddress?.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
var signingKeyStr = await explorerClient
|
||||||
|
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||||
|
WellknownMetadataKeys.MasterHDKey);
|
||||||
|
if (signingKeyStr is null)
|
||||||
|
{
|
||||||
|
return this.CreateAPIError("not-available",
|
||||||
|
$"{cryptoCode} sending services are not currently available");
|
||||||
|
}
|
||||||
|
|
||||||
|
var signingKey = ExtKey.Parse(signingKeyStr, network.NBitcoinNetwork);
|
||||||
|
|
||||||
|
var signingKeySettings = derivationScheme.GetSigningAccountKeySettings();
|
||||||
|
signingKeySettings.RootFingerprint ??= signingKey.GetPublicKey().GetHDFingerPrint();
|
||||||
|
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||||
|
psbt.PSBT.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
|
||||||
|
var accountKey = signingKey.Derive(rootedKeyPath.KeyPath);
|
||||||
|
|
||||||
|
var changed = psbt.PSBT.PSBTChanged(() => psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey,
|
||||||
|
rootedKeyPath, new SigningOptions() {EnforceLowR = !(signingContext?.EnforceLowR is false)}));
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
return this.CreateAPIError("psbt-signing-error",
|
||||||
|
"Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
psbt.PSBT.Finalize();
|
||||||
|
var transaction = psbt.PSBT.ExtractTransaction();
|
||||||
|
var transactionHash = transaction.GetHash();
|
||||||
|
BroadcastResult broadcastResult;
|
||||||
|
if (!string.IsNullOrEmpty(signingContext.PayJoinBIP21))
|
||||||
|
{
|
||||||
|
signingContext.OriginalPSBT = psbt.PSBT.ToBase64();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
psbt.PSBT, CancellationToken.None);
|
||||||
|
payjoinPSBT = psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey, rootedKeyPath,
|
||||||
|
new SigningOptions() {EnforceLowR = !(signingContext?.EnforceLowR is false)});
|
||||||
|
payjoinPSBT.Finalize();
|
||||||
|
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||||
|
var hash = payjoinTransaction.GetHash();
|
||||||
|
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
|
||||||
|
UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||||
|
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||||
|
if (broadcastResult.Success)
|
||||||
|
{
|
||||||
|
return await GetOnChainWalletTransaction(storeId, cryptoCode, hash.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (PayjoinException)
|
||||||
|
{
|
||||||
|
//not a critical thing, payjoin is great if possible, fine if not
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.ProceedWithBroadcast)
|
||||||
|
{
|
||||||
|
return Ok(new JValue(transaction.ToHex()));
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastResult = await explorerClient.BroadcastAsync(transaction);
|
||||||
|
if (broadcastResult.Success)
|
||||||
|
{
|
||||||
|
return await GetOnChainWalletTransaction(storeId, cryptoCode, transactionHash.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this.CreateAPIError("broadcast-error", broadcastResult.RPCMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||||
|
{
|
||||||
|
return await _authorizationService.CanUseHotWallet(_cssThemeManager.Policies, User);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ExtKey> GetWallet(DerivationSchemeSettings derivationScheme)
|
||||||
|
{
|
||||||
|
if (!derivationScheme.IsHotWallet)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var result = await _explorerClientProvider.GetExplorerClient(derivationScheme.Network.CryptoCode)
|
||||||
|
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||||
|
WellknownMetadataKeys.MasterHDKey);
|
||||||
|
return string.IsNullOrEmpty(result) ? null : ExtKey.Parse(result, derivationScheme.Network.NBitcoinNetwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsInvalidWalletRequest(string cryptoCode, out BTCPayNetwork network,
|
||||||
|
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)
|
||||||
|
{
|
||||||
|
derivationScheme = null;
|
||||||
|
actionResult = null;
|
||||||
|
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
if (network is null)
|
||||||
|
{
|
||||||
|
actionResult = NotFound();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!network.WalletSupported || !_btcPayWalletProvider.IsAvailable(network))
|
||||||
|
{
|
||||||
|
actionResult = this.CreateAPIError("not-available",
|
||||||
|
$"{cryptoCode} services are not currently available");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
derivationScheme = GetDerivationSchemeSettings(cryptoCode);
|
||||||
|
if (derivationScheme?.AccountDerivation is null)
|
||||||
|
{
|
||||||
|
actionResult = NotFound();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DerivationSchemeSettings GetDerivationSchemeSettings(string cryptoCode)
|
||||||
|
{
|
||||||
|
var paymentMethod = Store
|
||||||
|
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||||
|
.OfType<DerivationSchemeSettings>()
|
||||||
|
.FirstOrDefault(p =>
|
||||||
|
p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike &&
|
||||||
|
p.PaymentId.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
return paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OnChainWalletTransactionData ToModel(WalletTransactionInfo walletTransactionsInfoAsync,
|
||||||
|
TransactionInformation tx,
|
||||||
|
BTCPayWallet wallet)
|
||||||
|
{
|
||||||
|
return new OnChainWalletTransactionData()
|
||||||
|
{
|
||||||
|
TransactionHash = tx.TransactionId,
|
||||||
|
Comment = walletTransactionsInfoAsync?.Comment?? string.Empty,
|
||||||
|
Labels = walletTransactionsInfoAsync?.Labels?? new Dictionary<string, LabelData>(),
|
||||||
|
Amount = tx.BalanceChange.GetValue(wallet.Network),
|
||||||
|
BlockHash = tx.BlockHash,
|
||||||
|
BlockHeight = tx.Height,
|
||||||
|
Confirmations = tx.Confirmations,
|
||||||
|
Timestamp = tx.Timestamp,
|
||||||
|
Status = tx.Confirmations > 0 ? TransactionStatus.Confirmed :
|
||||||
|
tx.ReplacedBy != null ? TransactionStatus.Replaced : TransactionStatus.Unconfirmed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -579,13 +579,8 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||||
{
|
{
|
||||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
|
|
||||||
.Succeeded;
|
|
||||||
if (isAdmin)
|
|
||||||
return (true, true);
|
|
||||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||||
var hotWallet = policies?.AllowHotWalletForAll is true;
|
return await _authorizationService.CanUseHotWallet(policies, User);
|
||||||
return (hotWallet, hotWallet && policies.AllowHotWalletRPCImportForAll is true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> ReadAllText(IFormFile file)
|
private async Task<string> ReadAllText(IFormFile file)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Constants;
|
|||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.ModelBinders;
|
using BTCPayServer.ModelBinders;
|
||||||
@@ -31,6 +32,7 @@ using NBXplorer;
|
|||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
@@ -50,7 +52,7 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly IFeeProviderFactory _feeRateProvider;
|
private readonly IFeeProviderFactory _feeRateProvider;
|
||||||
private readonly BTCPayWalletProvider _walletProvider;
|
private readonly BTCPayWalletProvider _walletProvider;
|
||||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
private readonly WalletReceiveService _walletReceiveService;
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
private readonly SettingsRepository _settingsRepository;
|
private readonly SettingsRepository _settingsRepository;
|
||||||
private readonly DelayedTransactionBroadcaster _broadcaster;
|
private readonly DelayedTransactionBroadcaster _broadcaster;
|
||||||
@@ -75,7 +77,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
IFeeProviderFactory feeRateProvider,
|
IFeeProviderFactory feeRateProvider,
|
||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
WalletReceiveStateService walletReceiveStateService,
|
WalletReceiveService walletReceiveService,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
DelayedTransactionBroadcaster broadcaster,
|
DelayedTransactionBroadcaster broadcaster,
|
||||||
@@ -97,7 +99,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ExplorerClientProvider = explorerProvider;
|
ExplorerClientProvider = explorerProvider;
|
||||||
_feeRateProvider = feeRateProvider;
|
_feeRateProvider = feeRateProvider;
|
||||||
_walletProvider = walletProvider;
|
_walletProvider = walletProvider;
|
||||||
_WalletReceiveStateService = walletReceiveStateService;
|
_walletReceiveService = walletReceiveService;
|
||||||
_EventAggregator = eventAggregator;
|
_EventAggregator = eventAggregator;
|
||||||
_settingsRepository = settingsRepository;
|
_settingsRepository = settingsRepository;
|
||||||
_broadcaster = broadcaster;
|
_broadcaster = broadcaster;
|
||||||
@@ -361,7 +363,7 @@ namespace BTCPayServer.Controllers
|
|||||||
if (network == null)
|
if (network == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var address = _WalletReceiveStateService.Get(walletId)?.Address;
|
var address = _walletReceiveService.Get(walletId)?.Address;
|
||||||
return View(new WalletReceiveViewModel()
|
return View(new WalletReceiveViewModel()
|
||||||
{
|
{
|
||||||
CryptoCode = walletId.CryptoCode,
|
CryptoCode = walletId.CryptoCode,
|
||||||
@@ -383,29 +385,22 @@ namespace BTCPayServer.Controllers
|
|||||||
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||||
if (network == null)
|
if (network == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var wallet = _walletProvider.GetWallet(network);
|
|
||||||
switch (command)
|
switch (command)
|
||||||
{
|
{
|
||||||
case "unreserve-current-address":
|
case "unreserve-current-address":
|
||||||
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
|
var address = await _walletReceiveService.UnReserveAddress(walletId);
|
||||||
if (cachedAddress == null)
|
if (!string.IsNullOrEmpty(address))
|
||||||
{
|
{
|
||||||
break;
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
AllowDismiss = true,
|
||||||
|
Message = $"Address {address} was unreserved.",
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
|
|
||||||
ExplorerClientProvider.GetExplorerClient(network)
|
|
||||||
.CancelReservation(cachedAddress.DerivationStrategy, new[] { cachedAddress.KeyPath });
|
|
||||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
AllowDismiss = true,
|
|
||||||
Message = $"Address {address} was unreserved.",
|
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
|
||||||
});
|
|
||||||
_WalletReceiveStateService.Remove(walletId);
|
|
||||||
break;
|
break;
|
||||||
case "generate-new-address":
|
case "generate-new-address":
|
||||||
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
|
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||||
_WalletReceiveStateService.Set(walletId, reserve);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
||||||
@@ -413,11 +408,8 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
private async Task<bool> CanUseHotWallet()
|
private async Task<bool> CanUseHotWallet()
|
||||||
{
|
{
|
||||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
|
|
||||||
if (isAdmin)
|
|
||||||
return true;
|
|
||||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||||
return policies?.AllowHotWalletForAll is true;
|
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -907,7 +899,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(nameof(SignWithSeed), viewModel);
|
return View(nameof(SignWithSeed), viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
|
var changed = psbt.PSBTChanged( () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
|
||||||
{
|
{
|
||||||
EnforceLowR = !(viewModel.SigningContext?.EnforceLowR is false)
|
EnforceLowR = !(viewModel.SigningContext?.EnforceLowR is false)
|
||||||
}));
|
}));
|
||||||
@@ -926,15 +918,6 @@ namespace BTCPayServer.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private bool PSBTChanged(PSBT psbt, Action act)
|
|
||||||
{
|
|
||||||
var before = psbt.ToBase64();
|
|
||||||
act();
|
|
||||||
var after = psbt.ToBase64();
|
|
||||||
return before != after;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ValueToString(Money v, BTCPayNetworkBase network)
|
private string ValueToString(Money v, BTCPayNetworkBase network)
|
||||||
{
|
{
|
||||||
return v.ToString() + " " + network.CryptoCode;
|
return v.ToString() + " " + network.CryptoCode;
|
||||||
@@ -1041,11 +1024,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
||||||
{
|
{
|
||||||
var paymentMethod = CurrentStore
|
return CurrentStore.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||||
.GetSupportedPaymentMethods(NetworkProvider)
|
|
||||||
.OfType<DerivationSchemeSettings>()
|
|
||||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
|
||||||
return paymentMethod;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Services.Labels;
|
using BTCPayServer.Services.Labels;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -12,7 +13,7 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
public string Comment { get; set; } = string.Empty;
|
public string Comment { get; set; } = string.Empty;
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Dictionary<string, Label> Labels { get; set; } = new Dictionary<string, Label>();
|
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||||
}
|
}
|
||||||
public static class WalletTransactionDataExtensions
|
public static class WalletTransactionDataExtensions
|
||||||
{
|
{
|
||||||
|
|||||||
22
BTCPayServer/Extensions/AuthorizationExtensions.cs
Normal file
22
BTCPayServer/Extensions/AuthorizationExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Security.Bitpay;
|
||||||
|
using BTCPayServer.Security.GreenField;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public static class AuthorizationExtensions
|
||||||
|
{
|
||||||
|
public static async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet(
|
||||||
|
this IAuthorizationService authorizationService,
|
||||||
|
PoliciesSettings policiesSettings,
|
||||||
|
ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
return (await authorizationService.AuthorizeAsync(user, Policies.CanModifyServerSettings))
|
||||||
|
.Succeeded ? (true, true) : (policiesSettings?.AllowHotWalletForAll is true, policiesSettings?.AllowHotWalletRPCImportForAll is true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
BTCPayServer/Extensions/PSBTExtensions.cs
Normal file
16
BTCPayServer/Extensions/PSBTExtensions.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public static class PSBTExtensions
|
||||||
|
{
|
||||||
|
public static bool PSBTChanged(this PSBT psbt, Action act)
|
||||||
|
{
|
||||||
|
var before = psbt.ToBase64();
|
||||||
|
act();
|
||||||
|
var after = psbt.ToBase64();
|
||||||
|
return before != after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
BTCPayServer/Extensions/StoreExtensions.cs
Normal file
18
BTCPayServer/Extensions/StoreExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public static class StoreExtensions
|
||||||
|
{
|
||||||
|
public static DerivationSchemeSettings GetDerivationSchemeSettings(this StoreData store, BTCPayNetworkProvider networkProvider, string cryptoCode)
|
||||||
|
{
|
||||||
|
var paymentMethod = store
|
||||||
|
.GetSupportedPaymentMethods(networkProvider)
|
||||||
|
.OfType<DerivationSchemeSettings>()
|
||||||
|
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == cryptoCode);
|
||||||
|
return paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Events;
|
|
||||||
using BTCPayServer.Services.Wallets;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using NBXplorer;
|
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices
|
|
||||||
{
|
|
||||||
public class WalletReceiveCacheUpdater : IHostedService
|
|
||||||
{
|
|
||||||
private readonly EventAggregator _EventAggregator;
|
|
||||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
|
||||||
|
|
||||||
private readonly CompositeDisposable _Leases = new CompositeDisposable();
|
|
||||||
|
|
||||||
public WalletReceiveCacheUpdater(EventAggregator eventAggregator,
|
|
||||||
WalletReceiveStateService walletReceiveStateService)
|
|
||||||
{
|
|
||||||
_EventAggregator = eventAggregator;
|
|
||||||
_WalletReceiveStateService = walletReceiveStateService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_Leases.Add(_EventAggregator.Subscribe<WalletChangedEvent>(evt =>
|
|
||||||
_WalletReceiveStateService.Remove(evt.WalletId)));
|
|
||||||
|
|
||||||
_Leases.Add(_EventAggregator.Subscribe<NewOnChainTransactionEvent>(evt =>
|
|
||||||
{
|
|
||||||
var matching = _WalletReceiveStateService
|
|
||||||
.GetByDerivation(evt.CryptoCode, evt.NewTransactionEvent.DerivationStrategy).Where(pair =>
|
|
||||||
evt.NewTransactionEvent.Outputs.Any(output => output.ScriptPubKey == pair.Value.ScriptPubKey));
|
|
||||||
|
|
||||||
foreach (var keyValuePair in matching)
|
|
||||||
{
|
|
||||||
_WalletReceiveStateService.Remove(keyValuePair.Key);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_Leases.Dispose();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -293,7 +293,8 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<StoreRepository>();
|
services.TryAddSingleton<StoreRepository>();
|
||||||
services.TryAddSingleton<PaymentRequestRepository>();
|
services.TryAddSingleton<PaymentRequestRepository>();
|
||||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||||
services.TryAddSingleton<WalletReceiveStateService>();
|
services.TryAddSingleton<WalletReceiveService>();
|
||||||
|
services.AddSingleton<IHostedService>( provider => provider.GetService<WalletReceiveService>());
|
||||||
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
||||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||||
{
|
{
|
||||||
@@ -346,7 +347,6 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
||||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||||
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
|
|
||||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using BTCPayServer.Client.Models;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Labels
|
namespace BTCPayServer.Services.Labels
|
||||||
{
|
{
|
||||||
public abstract class Label
|
|
||||||
|
public abstract class Label: LabelData
|
||||||
{
|
{
|
||||||
public string Type { get; set; }
|
|
||||||
public string Text { get; set; }
|
|
||||||
static void FixLegacy(JObject jObj, ReferenceLabel refLabel)
|
static void FixLegacy(JObject jObj, ReferenceLabel refLabel)
|
||||||
{
|
{
|
||||||
if (refLabel.Reference is null)
|
if (refLabel.Reference is null)
|
||||||
@@ -80,8 +77,6 @@ namespace BTCPayServer.Services.Labels
|
|||||||
return new RawLabel(str);
|
return new RawLabel(str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[JsonExtensionData]
|
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RawLabel : Label
|
public class RawLabel : Label
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Amazon.Util.Internal.PlatformServices;
|
using Amazon.Util.Internal.PlatformServices;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -37,7 +38,7 @@ namespace BTCPayServer.Services.Labels
|
|||||||
}
|
}
|
||||||
|
|
||||||
const string DefaultColor = "#000";
|
const string DefaultColor = "#000";
|
||||||
private ColoredLabel CreateLabel(Label uncoloredLabel, string color, HttpRequest request)
|
private ColoredLabel CreateLabel(LabelData uncoloredLabel, string color, HttpRequest request)
|
||||||
{
|
{
|
||||||
if (uncoloredLabel == null)
|
if (uncoloredLabel == null)
|
||||||
throw new ArgumentNullException(nameof(uncoloredLabel));
|
throw new ArgumentNullException(nameof(uncoloredLabel));
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId)
|
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
|
||||||
{
|
{
|
||||||
if (walletId == null)
|
if (walletId == null)
|
||||||
throw new ArgumentNullException(nameof(walletId));
|
throw new ArgumentNullException(nameof(walletId));
|
||||||
@@ -46,6 +46,7 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
return (await ctx.WalletTransactions
|
return (await ctx.WalletTransactions
|
||||||
.Where(w => w.WalletDataId == walletId.ToString())
|
.Where(w => w.WalletDataId == walletId.ToString())
|
||||||
|
.Where(data => transactionIds == null || transactionIds.Contains(data.TransactionId))
|
||||||
.Select(w => w)
|
.Select(w => w)
|
||||||
.ToArrayAsync())
|
.ToArrayAsync())
|
||||||
.ToDictionary(w => w.TransactionId, w => w.GetBlobInfo());
|
.ToDictionary(w => w.TransactionId, w => w.GetBlobInfo());
|
||||||
|
|||||||
@@ -202,7 +202,40 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
|
|
||||||
public async Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
|
public async Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
|
||||||
{
|
{
|
||||||
return _Network.FilterValidTransactions(await _Client.GetTransactionsAsync(derivationStrategyBase));
|
return FilterValidTransactions(await _Client.GetTransactionsAsync(derivationStrategyBase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
|
||||||
|
{
|
||||||
|
return new GetTransactionsResponse()
|
||||||
|
{
|
||||||
|
Height = response.Height,
|
||||||
|
UnconfirmedTransactions =
|
||||||
|
new TransactionInformationSet()
|
||||||
|
{
|
||||||
|
Transactions = _Network.FilterValidTransactions(response.UnconfirmedTransactions.Transactions)
|
||||||
|
},
|
||||||
|
ConfirmedTransactions =
|
||||||
|
new TransactionInformationSet()
|
||||||
|
{
|
||||||
|
Transactions = _Network.FilterValidTransactions(response.ConfirmedTransactions.Transactions)
|
||||||
|
},
|
||||||
|
ReplacedTransactions = new TransactionInformationSet()
|
||||||
|
{
|
||||||
|
Transactions = _Network.FilterValidTransactions(response.ReplacedTransactions.Transactions)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TransactionInformation> FetchTransaction(DerivationStrategyBase derivationStrategyBase, uint256 transactionId)
|
||||||
|
{
|
||||||
|
var tx = await _Client.GetTransactionAsync(derivationStrategyBase, transactionId);
|
||||||
|
if (tx is null || !_Network.FilterValidTransactions(new List<TransactionInformation>() {tx}).Any())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
|
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
|
||||||
|
|||||||
132
BTCPayServer/Services/Wallets/WalletReceiveService.cs
Normal file
132
BTCPayServer/Services/Wallets/WalletReceiveService.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using NBXplorer.Models;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.Wallets
|
||||||
|
{
|
||||||
|
public class WalletReceiveService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly CompositeDisposable _leases = new CompositeDisposable();
|
||||||
|
private readonly EventAggregator _eventAggregator;
|
||||||
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||||
|
private readonly BTCPayWalletProvider _btcPayWalletProvider;
|
||||||
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||||
|
private readonly StoreRepository _storeRepository;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<WalletId, KeyPathInformation> _walletReceiveState =
|
||||||
|
new ConcurrentDictionary<WalletId, KeyPathInformation>();
|
||||||
|
|
||||||
|
public WalletReceiveService(EventAggregator eventAggregator, ExplorerClientProvider explorerClientProvider,
|
||||||
|
BTCPayWalletProvider btcPayWalletProvider, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_explorerClientProvider = explorerClientProvider;
|
||||||
|
_btcPayWalletProvider = btcPayWalletProvider;
|
||||||
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_storeRepository = storeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> UnReserveAddress(WalletId walletId)
|
||||||
|
{
|
||||||
|
var kpi = Get(walletId);
|
||||||
|
if (kpi is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var explorerClient = _explorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||||
|
if (explorerClient is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await explorerClient.CancelReservationAsync(kpi.DerivationStrategy, new[] {kpi.KeyPath});
|
||||||
|
return kpi.Address.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyPathInformation> GetOrGenerate(WalletId walletId, bool forceGenerate = false)
|
||||||
|
{
|
||||||
|
var existing = Get(walletId);
|
||||||
|
if (existing != null && !forceGenerate)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallet = _btcPayWalletProvider.GetWallet(walletId.CryptoCode);
|
||||||
|
var store = await _storeRepository.FindStore(walletId.StoreId);
|
||||||
|
var derivationScheme = store?.GetDerivationSchemeSettings(_btcPayNetworkProvider, walletId.CryptoCode);
|
||||||
|
if (wallet is null || derivationScheme is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reserve = (await wallet.ReserveAddressAsync(derivationScheme.AccountDerivation));
|
||||||
|
Set(walletId, reserve);
|
||||||
|
return reserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(WalletId walletId)
|
||||||
|
{
|
||||||
|
_walletReceiveState.TryRemove(walletId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPathInformation Get(WalletId walletId)
|
||||||
|
{
|
||||||
|
if (_walletReceiveState.ContainsKey(walletId))
|
||||||
|
{
|
||||||
|
return _walletReceiveState[walletId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Set(WalletId walletId, KeyPathInformation information)
|
||||||
|
{
|
||||||
|
_walletReceiveState.AddOrReplace(walletId, information);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<WalletId, KeyPathInformation>> GetByDerivation(string cryptoCode,
|
||||||
|
DerivationStrategyBase derivationStrategyBase)
|
||||||
|
{
|
||||||
|
return _walletReceiveState.Where(pair =>
|
||||||
|
pair.Key.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCulture) &&
|
||||||
|
pair.Value.DerivationStrategy == derivationStrategyBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_leases.Add(_eventAggregator.Subscribe<WalletChangedEvent>(evt =>
|
||||||
|
Remove(evt.WalletId)));
|
||||||
|
|
||||||
|
_leases.Add(_eventAggregator.Subscribe<NewOnChainTransactionEvent>(evt =>
|
||||||
|
{
|
||||||
|
var matching = GetByDerivation(evt.CryptoCode, evt.NewTransactionEvent.DerivationStrategy).Where(pair =>
|
||||||
|
evt.NewTransactionEvent.Outputs.Any(output => output.ScriptPubKey == pair.Value.ScriptPubKey));
|
||||||
|
|
||||||
|
foreach (var keyValuePair in matching)
|
||||||
|
{
|
||||||
|
Remove(keyValuePair.Key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_leases.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NBitcoin;
|
|
||||||
using NBXplorer.DerivationStrategy;
|
|
||||||
using NBXplorer.Models;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Wallets
|
|
||||||
{
|
|
||||||
public class WalletReceiveStateService
|
|
||||||
{
|
|
||||||
private readonly ConcurrentDictionary<WalletId, KeyPathInformation> _walletReceiveState =
|
|
||||||
new ConcurrentDictionary<WalletId, KeyPathInformation>();
|
|
||||||
|
|
||||||
public void Remove(WalletId walletId)
|
|
||||||
{
|
|
||||||
_walletReceiveState.TryRemove(walletId, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyPathInformation Get(WalletId walletId)
|
|
||||||
{
|
|
||||||
if (_walletReceiveState.ContainsKey(walletId))
|
|
||||||
{
|
|
||||||
return _walletReceiveState[walletId];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(WalletId walletId, KeyPathInformation information)
|
|
||||||
{
|
|
||||||
_walletReceiveState.AddOrReplace(walletId, information);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<KeyValuePair<WalletId, KeyPathInformation>> GetByDerivation(string cryptoCode,
|
|
||||||
DerivationStrategyBase derivationStrategyBase)
|
|
||||||
{
|
|
||||||
return _walletReceiveState.Where(pair =>
|
|
||||||
pair.Key.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCulture) &&
|
|
||||||
pair.Value.DerivationStrategy == derivationStrategyBase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
{
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/stores/{storeId}/payment-methods/OnChain/{cryptoCode}/wallet": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Store Wallet (On Chain)"
|
||||||
|
],
|
||||||
|
"summary": "Get store on-chain wallet overview",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "View information about the specified wallet",
|
||||||
|
"operationId": "StoreOnChainWallets_ShowOnChainWalletOverview",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "specified wallet",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletOverviewData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/address": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Store Wallet (On Chain)"
|
||||||
|
],
|
||||||
|
"summary": "Get store on-chain wallet address",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forceGenerate",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Whether to generate a new address for this request even if the previous one was not used",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get or generate address for wallet",
|
||||||
|
"operationId": "StoreOnChainWallets_GetOnChainWalletReceiveAddress",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "reserved address",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletAddressData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": "UnReserve last store on-chain wallet address",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "UnReserve address",
|
||||||
|
"operationId": "StoreOnChainWallets_UnReserveOnChainWalletReceiveAddress",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "address unreserved"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "If you are authenticated but forbidden to view the specified store"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The key is not found for this store/wallet or there was no address reserved"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/payment-methods/OnChain/{cryptoCode}/wallet/transactions": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Store Wallet (On Chain)"
|
||||||
|
],
|
||||||
|
"summary": "Get store on-chain wallet transactions",
|
||||||
|
"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 wallet to fetch",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "statusFilter",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "statuses to filter the transactions with",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TransactionStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get store on-chain wallet transactions",
|
||||||
|
"operationId": "StoreOnChainWallets_ShowOnChainWalletTransactions",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "transactions list",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletTransactionData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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)"
|
||||||
|
],
|
||||||
|
"summary": "Create store on-chain wallet transaction",
|
||||||
|
"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 wallet",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateOnChainTransactionRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Create store on-chain wallet transaction",
|
||||||
|
"operationId": "StoreOnChainWallets_CreateOnChainTransaction",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "the tx",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "The unbroadcasted transaction in hex format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletTransactionData"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/transactions/{transactionId}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Store Wallet (On Chain)"
|
||||||
|
],
|
||||||
|
"summary": "Get store on-chain wallet transactions",
|
||||||
|
"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 wallet to fetch",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transactionId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The transaction id to fetch",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get store on-chain wallet transaction",
|
||||||
|
"operationId": "StoreOnChainWallets_GetOnChainWalletTransaction",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "transaction",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletTransactionData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/utxos": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Store Wallet (On Chain)"
|
||||||
|
],
|
||||||
|
"summary": "Get store on-chain wallet UTXOS",
|
||||||
|
"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 wallet to fetch",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get store on-chain wallet utxos",
|
||||||
|
"operationId": "StoreOnChainWallets_GetOnChainWalletUTXOs",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "utxo list",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/OnChainWalletUTXOData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"OnChainWalletOverviewData": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"balance": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"description": "The current balance of the wallet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OnChainWalletAddressData": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"address": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The bitcoin address"
|
||||||
|
},
|
||||||
|
"keyPath": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "keypath",
|
||||||
|
"description": "the derivation path in relation to the HD account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OnChainPaymentMethodPreviewResultAddressItem": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"keyPath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The key path relative to the account key path."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The address generated at the key path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TransactionStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "",
|
||||||
|
"x-enumNames": [
|
||||||
|
"Unconfirmed",
|
||||||
|
"Confirmed",
|
||||||
|
"Replaced"
|
||||||
|
],
|
||||||
|
"enum": [
|
||||||
|
"Unconfirmed",
|
||||||
|
"Confirmed",
|
||||||
|
"Replaced"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"LabelData": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The type of label"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Information about this label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OnChainWalletTransactionData": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"transactionHash": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The transaction id"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A comment linked to the transaction"
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"description": "The amount the wallet balance changed with this transaction"
|
||||||
|
},
|
||||||
|
"blockHash": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The hash of the block that confirmed this transaction. Null if still unconfirmed."
|
||||||
|
},
|
||||||
|
"blockHeight": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The height of the block that confirmed this transaction. Null if still unconfirmed."
|
||||||
|
},
|
||||||
|
"confirmations": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The number of confirmations for this transaction"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "The time of the transaction"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/components/schemas/TransactionStatus",
|
||||||
|
"description": "The status for this transaction"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"description": "Labels linked to this transaction",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/components/schemas/LabelData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OnChainWalletUTXOData": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"comment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A comment linked to this utxo"
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the value of this utxo"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url",
|
||||||
|
"description": "a link to the configured blockchain explorer to view the utxo"
|
||||||
|
},
|
||||||
|
"outpoint": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "{txid}:{outputIndex}",
|
||||||
|
"description": "outpoint of this utxo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CreateOnChainTransactionRequestDestination": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"destination": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A wallet address or a BIP21 payment link"
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The amount to send. If `destination` is a BIP21 link, the amount must be the same or null."
|
||||||
|
},
|
||||||
|
"subtractFromAmount": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to subtract from the provided amount. Must be false if `destination` is a BIP21 link"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CreateOnChainTransactionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"destinations": {
|
||||||
|
"nullable": false,
|
||||||
|
"description": "What and where to send money",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CreateOnChainTransactionRequestDestination"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feeRate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal or long (sats/byte)",
|
||||||
|
"description": "A wallet address or a BIP21 payment link"
|
||||||
|
},
|
||||||
|
"proceedWithPayjoin": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Whether to attempt to do a BIP78 payjoin if one of the destinations is a BIP21 with payjoin enabled"
|
||||||
|
},
|
||||||
|
"proceedWithBroadcast": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Whether to broadcast the transaction after creating it or to simply return the transaction in hex format."
|
||||||
|
},
|
||||||
|
"noChange": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Whether to send all the spent coins to the destinations (THIS CAN COST YOU SIGNIFICANT AMOUNTS OF MONEY, LEAVE FALSE UNLESS YOU KNOW WHAT YOU ARE DOING)."
|
||||||
|
},
|
||||||
|
"rbf": {
|
||||||
|
"type": "boolean",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Whether to enable RBF for the transaction. Leave blank to have it random (beneficial to privacy)"
|
||||||
|
},
|
||||||
|
"selectedInputs": {
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Restrict the creation of the transactions from the outpoints provided ONLY (coin selection)",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Store Wallet (On Chain)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user