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
|
||||
{
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { 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 new TransactionInformationSet()
|
||||
{
|
||||
Transactions =
|
||||
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)
|
||||
};
|
||||
return transactionInformationSet.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));
|
||||
}
|
||||
|
||||
|
||||
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
{
|
||||
//precision 0: 10 = 0.00000010
|
||||
|
||||
@@ -126,9 +126,9 @@ namespace BTCPayServer
|
||||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@@ -19,6 +20,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.OpenAsset;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -1183,7 +1185,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task NotificationAPITests()
|
||||
@@ -1280,8 +1281,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
[Trait("Integration", "Integration")]
|
||||
@@ -1396,6 +1395,222 @@ namespace BTCPayServer.Tests
|
||||
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)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void NumericJsonConverterTests()
|
||||
@@ -1411,7 +1626,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(jsonConverter.CanConvert(typeof(double)));
|
||||
Assert.True(jsonConverter.CanConvert(typeof(double?)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(float)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(int)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(string)));
|
||||
|
||||
var numberJson = "1";
|
||||
|
||||
@@ -840,17 +840,24 @@ retry:
|
||||
settings.PaymentId == paymentMethodId);
|
||||
|
||||
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 () =>
|
||||
{
|
||||
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
|
||||
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();
|
||||
signingKeySettings.RootFingerprint =
|
||||
|
||||
@@ -183,6 +183,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
ScriptPubKeyType = segwit,
|
||||
SavePrivateKeys = importKeysToNBX,
|
||||
ImportKeysToRPC = importKeysToNBX
|
||||
});
|
||||
await store.UpdateWallet(
|
||||
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()
|
||||
{
|
||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
|
||||
.Succeeded;
|
||||
if (isAdmin)
|
||||
return (true, true);
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
var hotWallet = policies?.AllowHotWalletForAll is true;
|
||||
return (hotWallet, hotWallet && policies.AllowHotWalletRPCImportForAll is true);
|
||||
return await _authorizationService.CanUseHotWallet(policies, User);
|
||||
}
|
||||
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
|
||||
@@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
@@ -31,6 +32,7 @@ using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@@ -50,7 +52,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly DelayedTransactionBroadcaster _broadcaster;
|
||||
@@ -75,7 +77,7 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletReceiveStateService walletReceiveStateService,
|
||||
WalletReceiveService walletReceiveService,
|
||||
EventAggregator eventAggregator,
|
||||
SettingsRepository settingsRepository,
|
||||
DelayedTransactionBroadcaster broadcaster,
|
||||
@@ -97,7 +99,7 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_WalletReceiveStateService = walletReceiveStateService;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_settingsRepository = settingsRepository;
|
||||
_broadcaster = broadcaster;
|
||||
@@ -361,7 +363,7 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
var address = _WalletReceiveStateService.Get(walletId)?.Address;
|
||||
var address = _walletReceiveService.Get(walletId)?.Address;
|
||||
return View(new WalletReceiveViewModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
@@ -383,29 +385,22 @@ namespace BTCPayServer.Controllers
|
||||
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
switch (command)
|
||||
{
|
||||
case "unreserve-current-address":
|
||||
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
|
||||
if (cachedAddress == null)
|
||||
var address = await _walletReceiveService.UnReserveAddress(walletId);
|
||||
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;
|
||||
case "generate-new-address":
|
||||
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
|
||||
_WalletReceiveStateService.Set(walletId, reserve);
|
||||
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||
break;
|
||||
}
|
||||
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
||||
@@ -413,11 +408,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private async Task<bool> CanUseHotWallet()
|
||||
{
|
||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
|
||||
if (isAdmin)
|
||||
return true;
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
return policies?.AllowHotWalletForAll is true;
|
||||
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -907,7 +899,7 @@ namespace BTCPayServer.Controllers
|
||||
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)
|
||||
}));
|
||||
@@ -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)
|
||||
{
|
||||
return v.ToString() + " " + network.CryptoCode;
|
||||
@@ -1041,11 +1024,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
||||
{
|
||||
var paymentMethod = CurrentStore
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
return CurrentStore.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -12,7 +13,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
[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
|
||||
{
|
||||
|
||||
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<PaymentRequestRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<WalletReceiveStateService>();
|
||||
services.TryAddSingleton<WalletReceiveService>();
|
||||
services.AddSingleton<IHostedService>( provider => provider.GetService<WalletReceiveService>());
|
||||
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
@@ -346,7 +347,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
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)
|
||||
{
|
||||
if (refLabel.Reference is null)
|
||||
@@ -80,8 +77,6 @@ namespace BTCPayServer.Services.Labels
|
||||
return new RawLabel(str);
|
||||
}
|
||||
}
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
|
||||
public class RawLabel : Label
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Amazon.Util.Internal.PlatformServices;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -37,7 +38,7 @@ namespace BTCPayServer.Services.Labels
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
throw new ArgumentNullException(nameof(walletId));
|
||||
@@ -46,6 +46,7 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
return (await ctx.WalletTransactions
|
||||
.Where(w => w.WalletDataId == walletId.ToString())
|
||||
.Where(data => transactionIds == null || transactionIds.Contains(data.TransactionId))
|
||||
.Select(w => w)
|
||||
.ToArrayAsync())
|
||||
.ToDictionary(w => w.TransactionId, w => w.GetBlobInfo());
|
||||
|
||||
@@ -202,7 +202,40 @@ namespace BTCPayServer.Services.Wallets
|
||||
|
||||
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)
|
||||
|
||||
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