From cb25c225e916afaeae655264aa6d68338a4c134a Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 28 Mar 2024 16:28:27 +0100 Subject: [PATCH] Remove custodians (#5863) * Remove custodians * Hide Experimental checkbox in the server policies --------- Co-authored-by: Nicolas Dorier --- .../Custodians/Client/AssetQuoteResult.cs | 19 - .../AssetBalancesUnavailableException.cs | 12 - .../AssetQuoteUnavailableException.cs | 13 - .../Client/Exception/BadConfigException.cs | 13 - .../Exception/CannotWithdrawException.cs | 13 - .../Client/Exception/CustodianApiException.cs | 18 - ...CustodianFeatureNotImplementedException.cs | 8 - .../Exception/DepositsUnavailableException.cs | 8 - .../Exception/InsufficientFundsException.cs | 8 - .../InvalidWithdrawalTargetException.cs | 9 - .../PermissionDeniedCustodianApiException.cs | 9 - .../Exception/TradeNotFoundException.cs | 11 - .../Exception/WithdrawalNotFoundException.cs | 11 - .../Exception/WrongTradingPairException.cs | 9 - .../Custodians/Client/MarketTradeResult.cs | 29 - .../Client/SimulateWithdrawalResult.cs | 28 - .../Custodians/Client/WithdrawResult.cs | 29 - .../Custodians/ICanDeposit.cs | 17 - .../Custodians/ICanTrade.cs | 31 - .../Custodians/ICanWithdraw.cs | 20 - .../Custodians/ICustodian.cs | 26 - .../Extensions/CustodianExtensions.cs | 14 - .../BTCPayServerClient.CustodianAccounts.cs | 102 -- .../BTCPayServerClient.Custodians.cs | 16 - .../TradeQuantityJsonConverter.cs | 36 - .../Models/CreateCustodianAccountRequest.cs | 12 - .../Models/CustodianAccountBaseData.cs | 16 - .../Models/CustodianAccountData.cs | 7 - .../Models/CustodianAccountResponse.cs | 14 - BTCPayServer.Client/Models/CustodianData.cs | 13 - .../Models/DepositAddressData.cs | 15 - .../Models/MarketTradeResponseData.cs | 31 - .../Models/TradeQuoteResponseData.cs | 22 - .../Models/TradeRequestData.cs | 11 - .../Models/WithdrawRequestData.cs | 85 -- .../Models/WithdrawalBaseResponseData.cs | 22 - .../Models/WithdrawalResponseData.cs | 40 - .../WithdrawalSimulationResponseData.cs | 21 - BTCPayServer.Client/Permissions.cs | 12 - BTCPayServer.Data/ApplicationDbContext.cs | 2 - .../Data/CustodianAccountData.cs | 54 - BTCPayServer.Data/Data/StoreData.cs | 1 - .../20240325095923_RemoveCustodian.cs | 54 + .../ApplicationDbContextModelSnapshot.cs | 55 +- BTCPayServer.Tests/BTCPayServerTester.cs | 3 - BTCPayServer.Tests/FastTests.cs | 36 - BTCPayServer.Tests/GreenfieldAPITests.cs | 520 +------ .../MockCustodian/MockCustodian.cs | 196 --- BTCPayServer/BTCPayServer.csproj | 1 + .../Components/MainNav/Default.cshtml | 21 - BTCPayServer/Components/MainNav/MainNav.cs | 11 - .../Components/MainNav/MainNavViewModel.cs | 1 - .../GreenfieldCustodianAccountController.cs | 479 ------ .../GreenfieldCustodianController.cs | 63 - .../GreenField/LocalBTCPayServerClient.cs | 21 - .../UICustodianAccountsController.cs | 646 -------- .../Controllers/UIManageController.APIKeys.cs | 10 - .../Data/CustodianAccountDataExtensions.cs | 13 - BTCPayServer/Hosting/BTCPayServerServices.cs | 2 - .../AssetBalanceInfo.cs | 21 - .../CreateCustodianAccountViewModel.cs | 48 - .../DepositPrepareViewModel.cs | 11 - .../EditCustodianAccountViewModel.cs | 13 - .../TradePrepareViewModel.cs | 9 - .../ViewCustodianAccountBalancesViewModel.cs | 15 - .../ViewCustodianAccountViewModel.cs | 12 - .../WithdrawalPrepareViewModel.cs | 28 - .../Plugins/FakeCustodian/FakeCustodian.cs | 357 ----- .../FakeCustodian/FakeCustodianPlugin.cs | 20 - .../Custodian/CustodianAccountRepository.cs | 70 - .../CreateCustodianAccount.cshtml | 66 - .../CustodianAccountsNavPages.cs | 7 - .../EditCustodianAccount.cshtml | 46 - .../ViewCustodianAccount.cshtml | 551 ------- BTCPayServer/Views/UIServer/Policies.cshtml | 6 +- BTCPayServer/wwwroot/js/custodian-account.js | 683 --------- .../v1/swagger.template.custodians.json | 1323 ----------------- .../wwwroot/swagger/v1/swagger.template.json | 6 +- .../swagger/v1/swagger.template.stores.json | 5 +- 79 files changed, 68 insertions(+), 6248 deletions(-) delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/AssetQuoteResult.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/AssetBalancesUnavailableException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/AssetQuoteUnavailableException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/BadConfigException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/CannotWithdrawException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianApiException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianFeatureNotImplementedException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/DepositsUnavailableException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/InsufficientFundsException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/InvalidWithdrawalTargetException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/PermissionDeniedCustodianApiException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/TradeNotFoundException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/WithdrawalNotFoundException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/Exception/WrongTradingPairException.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/MarketTradeResult.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/SimulateWithdrawalResult.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/Client/WithdrawResult.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/ICanDeposit.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/ICanTrade.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs delete mode 100644 BTCPayServer.Abstractions/Custodians/ICustodian.cs delete mode 100644 BTCPayServer.Abstractions/Extensions/CustodianExtensions.cs delete mode 100644 BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs delete mode 100644 BTCPayServer.Client/BTCPayServerClient.Custodians.cs delete mode 100644 BTCPayServer.Client/JsonConverters/TradeQuantityJsonConverter.cs delete mode 100644 BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs delete mode 100644 BTCPayServer.Client/Models/CustodianAccountBaseData.cs delete mode 100644 BTCPayServer.Client/Models/CustodianAccountData.cs delete mode 100644 BTCPayServer.Client/Models/CustodianAccountResponse.cs delete mode 100644 BTCPayServer.Client/Models/CustodianData.cs delete mode 100644 BTCPayServer.Client/Models/DepositAddressData.cs delete mode 100644 BTCPayServer.Client/Models/MarketTradeResponseData.cs delete mode 100644 BTCPayServer.Client/Models/TradeQuoteResponseData.cs delete mode 100644 BTCPayServer.Client/Models/TradeRequestData.cs delete mode 100644 BTCPayServer.Client/Models/WithdrawRequestData.cs delete mode 100644 BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs delete mode 100644 BTCPayServer.Client/Models/WithdrawalResponseData.cs delete mode 100644 BTCPayServer.Client/Models/WithdrawalSimulationResponseData.cs delete mode 100644 BTCPayServer.Data/Data/CustodianAccountData.cs create mode 100644 BTCPayServer.Data/Migrations/20240325095923_RemoveCustodian.cs delete mode 100644 BTCPayServer.Tests/MockCustodian/MockCustodian.cs delete mode 100644 BTCPayServer/Controllers/GreenField/GreenfieldCustodianAccountController.cs delete mode 100644 BTCPayServer/Controllers/GreenField/GreenfieldCustodianController.cs delete mode 100644 BTCPayServer/Controllers/UICustodianAccountsController.cs delete mode 100644 BTCPayServer/Data/CustodianAccountDataExtensions.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/AssetBalanceInfo.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/CreateCustodianAccountViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/DepositPrepareViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/EditCustodianAccountViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/TradePrepareViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountBalancesViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountViewModel.cs delete mode 100644 BTCPayServer/Models/CustodianAccountViewModels/WithdrawalPrepareViewModel.cs delete mode 100644 BTCPayServer/Plugins/FakeCustodian/FakeCustodian.cs delete mode 100644 BTCPayServer/Plugins/FakeCustodian/FakeCustodianPlugin.cs delete mode 100644 BTCPayServer/Services/Custodian/CustodianAccountRepository.cs delete mode 100644 BTCPayServer/Views/UICustodianAccounts/CreateCustodianAccount.cshtml delete mode 100644 BTCPayServer/Views/UICustodianAccounts/CustodianAccountsNavPages.cs delete mode 100644 BTCPayServer/Views/UICustodianAccounts/EditCustodianAccount.cshtml delete mode 100644 BTCPayServer/Views/UICustodianAccounts/ViewCustodianAccount.cshtml delete mode 100644 BTCPayServer/wwwroot/js/custodian-account.js delete mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json diff --git a/BTCPayServer.Abstractions/Custodians/Client/AssetQuoteResult.cs b/BTCPayServer.Abstractions/Custodians/Client/AssetQuoteResult.cs deleted file mode 100644 index e41ff2bf3..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/AssetQuoteResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians.Client; - -public class AssetQuoteResult -{ - public string FromAsset { get; set; } - public string ToAsset { get; set; } - public decimal Bid { get; set; } - public decimal Ask { get; set; } - - public AssetQuoteResult() { } - - public AssetQuoteResult(string fromAsset, string toAsset, decimal bid, decimal ask) - { - FromAsset = fromAsset; - ToAsset = toAsset; - Bid = bid; - Ask = ask; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetBalancesUnavailableException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetBalancesUnavailableException.cs deleted file mode 100644 index dac727213..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetBalancesUnavailableException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class AssetBalancesUnavailableException : CustodianApiException -{ - public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e) - { - } - - public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}") - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetQuoteUnavailableException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetQuoteUnavailableException.cs deleted file mode 100644 index 4ad4f127f..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/AssetQuoteUnavailableException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Abstractions.Custodians; - -public class AssetQuoteUnavailableException : CustodianApiException -{ - public AssetPairData AssetPair { get; } - - public AssetQuoteUnavailableException(AssetPairData assetPair) : base(400, "asset-price-unavailable", "Cannot find a quote for pair " + assetPair) - { - this.AssetPair = assetPair; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/BadConfigException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/BadConfigException.cs deleted file mode 100644 index a38bf0f20..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/BadConfigException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace BTCPayServer.Abstractions.Custodians; - -public class BadConfigException : CustodianApiException -{ - public string[] BadConfigKeys { get; set; } - - public BadConfigException(string[] badConfigKeys) : base(500, "bad-custodian-account-config", "Wrong config values: " + String.Join(", ", badConfigKeys)) - { - this.BadConfigKeys = badConfigKeys; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/CannotWithdrawException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/CannotWithdrawException.cs deleted file mode 100644 index cfbcb6140..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/CannotWithdrawException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class CannotWithdrawException : CustodianApiException - -{ - public CannotWithdrawException(ICustodian custodian, string paymentMethod, string message) : base(403, "cannot-withdraw", message) - { - } - - public CannotWithdrawException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "cannot-withdraw", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}") - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianApiException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianApiException.cs deleted file mode 100644 index be6792bcb..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianApiException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -namespace BTCPayServer.Abstractions.Custodians; -public class CustodianApiException : Exception -{ - public int HttpStatus { get; } - public string Code { get; } - - public CustodianApiException(int httpStatus, string code, string message, System.Exception ex) : base(message, ex) - { - HttpStatus = httpStatus; - Code = code; - } - - public CustodianApiException(int httpStatus, string code, string message) : this(httpStatus, code, message, null) - { - } -} - diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianFeatureNotImplementedException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianFeatureNotImplementedException.cs deleted file mode 100644 index 4c8c96e27..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/CustodianFeatureNotImplementedException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class CustodianFeatureNotImplementedException : CustodianApiException -{ - public CustodianFeatureNotImplementedException(string message) : base(400, "not-implemented", message) - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/DepositsUnavailableException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/DepositsUnavailableException.cs deleted file mode 100644 index 7684278eb..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/DepositsUnavailableException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class DepositsUnavailableException : CustodianApiException -{ - public DepositsUnavailableException(string message) : base(404, "deposits-unavailable", message) - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/InsufficientFundsException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/InsufficientFundsException.cs deleted file mode 100644 index b6c759f72..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/InsufficientFundsException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class InsufficientFundsException : CustodianApiException -{ - public InsufficientFundsException(string message) : base(400, "insufficient-funds", message) - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/InvalidWithdrawalTargetException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/InvalidWithdrawalTargetException.cs deleted file mode 100644 index 49e1f85fd..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/InvalidWithdrawalTargetException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class InvalidWithdrawalTargetException : CustodianApiException - -{ - public InvalidWithdrawalTargetException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "invalid-withdrawal-target", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}") - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/PermissionDeniedCustodianApiException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/PermissionDeniedCustodianApiException.cs deleted file mode 100644 index de26c0af1..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/PermissionDeniedCustodianApiException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class PermissionDeniedCustodianApiException : CustodianApiException - -{ - public PermissionDeniedCustodianApiException(ICustodian custodian) : base(403, "custodian-api-permission-denied", $"{custodian.Name}'s API reported that you don't have permission.") - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/TradeNotFoundException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/TradeNotFoundException.cs deleted file mode 100644 index 7c2d4bb35..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/TradeNotFoundException.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class TradeNotFoundException : CustodianApiException -{ - private string tradeId { get; } - - public TradeNotFoundException(string tradeId) : base(404, "trade-not-found", "Could not find trade ID " + tradeId) - { - this.tradeId = tradeId; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/WithdrawalNotFoundException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/WithdrawalNotFoundException.cs deleted file mode 100644 index 38d149187..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/WithdrawalNotFoundException.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class WithdrawalNotFoundException : CustodianApiException -{ - private string WithdrawalId { get; } - - public WithdrawalNotFoundException(string withdrawalId) : base(404, "withdrawal-not-found", $"Could not find withdrawal ID {withdrawalId}.") - { - WithdrawalId = withdrawalId; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/Exception/WrongTradingPairException.cs b/BTCPayServer.Abstractions/Custodians/Client/Exception/WrongTradingPairException.cs deleted file mode 100644 index 7fa308715..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/Exception/WrongTradingPairException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BTCPayServer.Abstractions.Custodians; - -public class WrongTradingPairException : CustodianApiException -{ - public const int HttpCode = 404; - public WrongTradingPairException(string fromAsset, string toAsset) : base(HttpCode, "wrong-trading-pair", $"Cannot find a trading pair for converting {fromAsset} into {toAsset}.") - { - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/MarketTradeResult.cs b/BTCPayServer.Abstractions/Custodians/Client/MarketTradeResult.cs deleted file mode 100644 index d393e1c08..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/MarketTradeResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Abstractions.Custodians.Client; - -/** - * The result of a market trade. Used as a return type for custodians implementing ICanTrade - */ -public class MarketTradeResult -{ - public string FromAsset { get; } - public string ToAsset { get; } - /** - * The ledger entries that show the balances that were affected by the trade. - */ - public List LedgerEntries { get; } - /** - * The unique ID of the trade that was executed. - */ - public string TradeId { get; } - - public MarketTradeResult(string fromAsset, string toAsset, List ledgerEntries, string tradeId) - { - this.FromAsset = fromAsset; - this.ToAsset = toAsset; - this.LedgerEntries = ledgerEntries; - this.TradeId = tradeId; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/SimulateWithdrawalResult.cs b/BTCPayServer.Abstractions/Custodians/Client/SimulateWithdrawalResult.cs deleted file mode 100644 index f90cbf6ed..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/SimulateWithdrawalResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.Client.Models; -using BTCPayServer.JsonConverters; - -namespace BTCPayServer.Abstractions.Custodians.Client; - -public class SimulateWithdrawalResult -{ - public string PaymentMethod { get; } - public string Asset { get; } - public decimal MinQty { get; } - public decimal MaxQty { get; } - - public List LedgerEntries { get; } - - // Fee can be NULL if unknown. - public decimal? Fee { get; } - - public SimulateWithdrawalResult(string paymentMethod, string asset, List ledgerEntries, - decimal minQty, decimal maxQty) - { - PaymentMethod = paymentMethod; - Asset = asset; - LedgerEntries = ledgerEntries; - MinQty = minQty; - MaxQty = maxQty; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/Client/WithdrawResult.cs b/BTCPayServer.Abstractions/Custodians/Client/WithdrawResult.cs deleted file mode 100644 index 0cfb255c4..000000000 --- a/BTCPayServer.Abstractions/Custodians/Client/WithdrawResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Abstractions.Custodians.Client; - -public class WithdrawResult -{ - public string PaymentMethod { get; } - public string Asset { get; set; } - public List LedgerEntries { get; } - public string WithdrawalId { get; } - public WithdrawalResponseData.WithdrawalStatus Status { get; } - public DateTimeOffset CreatedTime { get; } - public string TargetAddress { get; } - public string TransactionId { get; } - - public WithdrawResult(string paymentMethod, string asset, List ledgerEntries, string withdrawalId, WithdrawalResponseData.WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) - { - PaymentMethod = paymentMethod; - Asset = asset; - LedgerEntries = ledgerEntries; - WithdrawalId = withdrawalId; - CreatedTime = createdTime; - Status = status; - TargetAddress = targetAddress; - TransactionId = transactionId; - } -} diff --git a/BTCPayServer.Abstractions/Custodians/ICanDeposit.cs b/BTCPayServer.Abstractions/Custodians/ICanDeposit.cs deleted file mode 100644 index 25fccd743..000000000 --- a/BTCPayServer.Abstractions/Custodians/ICanDeposit.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Abstractions.Custodians; - -public interface ICanDeposit -{ - /** - * Get the address where we can deposit for the chosen payment method (crypto code + network). - * The result can be a string in different formats like a bitcoin address or even a LN invoice. - */ - public Task GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken); - - public string[] GetDepositablePaymentMethods(); -} diff --git a/BTCPayServer.Abstractions/Custodians/ICanTrade.cs b/BTCPayServer.Abstractions/Custodians/ICanTrade.cs deleted file mode 100644 index 9b3b429bf..000000000 --- a/BTCPayServer.Abstractions/Custodians/ICanTrade.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Custodians.Client; -using BTCPayServer.Client.Models; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Abstractions.Custodians; - -public interface ICanTrade -{ - /** - * A list of tradable asset pairs, or NULL if the custodian cannot trade/convert assets. if thr asset pair contains fiat, fiat is always put last. If both assets are a cyrptocode or both are fiat, the pair is written alphabetically. Always in uppercase. Example: ["BTC/EUR","BTC/USD", "EUR/USD", "BTC/ETH",...] - */ - public List GetTradableAssetPairs(); - - /** - * Execute a market order right now. - */ - public Task TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken); - - /** - * Get the details about a previous market trade. - */ - public Task GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken); - - public Task GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken); -} - - - diff --git a/BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs b/BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs deleted file mode 100644 index 244b87761..000000000 --- a/BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Custodians.Client; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Abstractions.Custodians; - -/// -/// Interface for custodians that can move funds to the store wallet. -/// -public interface ICanWithdraw -{ - public Task WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken); - - public Task SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken); - - public Task GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken); - - public string[] GetWithdrawablePaymentMethods(); -} diff --git a/BTCPayServer.Abstractions/Custodians/ICustodian.cs b/BTCPayServer.Abstractions/Custodians/ICustodian.cs deleted file mode 100644 index 0bc49c141..000000000 --- a/BTCPayServer.Abstractions/Custodians/ICustodian.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Abstractions.Custodians; - -public interface ICustodian -{ - /** - * Get the unique code that identifies this custodian. - */ - string Code { get; } - - string Name { get; } - - /** - * Get a list of assets and their qty in custody. - */ - Task> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken); - - public Task GetConfigForm(JObject config, CancellationToken cancellationToken = default); - -} diff --git a/BTCPayServer.Abstractions/Extensions/CustodianExtensions.cs b/BTCPayServer.Abstractions/Extensions/CustodianExtensions.cs deleted file mode 100644 index ca72b36db..000000000 --- a/BTCPayServer.Abstractions/Extensions/CustodianExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Abstractions.Custodians; - -namespace BTCPayServer.Abstractions.Extensions; - -public static class CustodianExtensions -{ - public static ICustodian? GetCustodianByCode(this IEnumerable custodians, string code) - { - return custodians.FirstOrDefault(custodian => custodian.Code == code); - } -} diff --git a/BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs b/BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs deleted file mode 100644 index 95b3b572e..000000000 --- a/BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Client -{ - public partial class BTCPayServerClient - { - public virtual async Task> GetCustodianAccounts(string storeId, bool includeAssetBalances = false, CancellationToken token = default) - { - var queryPayload = new Dictionary(); - if (includeAssetBalances) - { - queryPayload.Add("assetBalances", "true"); - } - - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", queryPayload), token); - return await HandleResponse>(response); - } - - public virtual async Task GetCustodianAccount(string storeId, string accountId, bool includeAssetBalances = false, CancellationToken token = default) - { - var queryPayload = new Dictionary(); - if (includeAssetBalances) - { - queryPayload.Add("assetBalances", "true"); - } - - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", queryPayload), token); - return await HandleResponse(response); - } - - public virtual async Task CreateCustodianAccount(string storeId, CreateCustodianAccountRequest request, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", bodyPayload: request, method: HttpMethod.Post), token); - return await HandleResponse(response); - } - - public virtual async Task UpdateCustodianAccount(string storeId, string accountId, CreateCustodianAccountRequest request, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", bodyPayload: request, method: HttpMethod.Put), token); - return await HandleResponse(response); - } - - public virtual async Task DeleteCustodianAccount(string storeId, string accountId, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", method: HttpMethod.Delete), token); - await HandleResponse(response); - } - - public virtual async Task GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token); - return await HandleResponse(response); - } - - public virtual async Task MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default) - { - //var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token); - //return await HandleResponse(response); - var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null, - request, HttpMethod.Post); - var response = await _httpClient.SendAsync(internalRequest, token); - return await HandleResponse(response); - } - - public virtual async Task GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token); - return await HandleResponse(response); - } - - public virtual async Task GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default) - { - var queryPayload = new Dictionary(); - queryPayload.Add("fromAsset", fromAsset); - queryPayload.Add("toAsset", toAsset); - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote", queryPayload), token); - return await HandleResponse(response); - } - - public virtual async Task CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token); - return await HandleResponse(response); - } - - public virtual async Task SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token); - return await HandleResponse(response); - } - - public virtual async Task GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token); - return await HandleResponse(response); - } - } -} diff --git a/BTCPayServer.Client/BTCPayServerClient.Custodians.cs b/BTCPayServer.Client/BTCPayServerClient.Custodians.cs deleted file mode 100644 index 05a932da5..000000000 --- a/BTCPayServer.Client/BTCPayServerClient.Custodians.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Client -{ - public partial class BTCPayServerClient - { - public virtual async Task> GetCustodians(CancellationToken token = default) - { - var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/custodians"), token); - return await HandleResponse>(response); - } - } -} diff --git a/BTCPayServer.Client/JsonConverters/TradeQuantityJsonConverter.cs b/BTCPayServer.Client/JsonConverters/TradeQuantityJsonConverter.cs deleted file mode 100644 index e9e420bc6..000000000 --- a/BTCPayServer.Client/JsonConverters/TradeQuantityJsonConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Globalization; -using BTCPayServer.Client.Models; -using BTCPayServer.Lightning; -using NBitcoin.JsonConverters; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Client.JsonConverters -{ - public class TradeQuantityJsonConverter : JsonConverter - { - public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer) - { - JToken token = JToken.Load(reader); - switch (token.Type) - { - case JTokenType.Float: - case JTokenType.Integer: - case JTokenType.String: - if (TradeQuantity.TryParse(token.ToString(), out var q)) - return q; - break; - case JTokenType.Null: - return null; - } - throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader); - } - - public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer) - { - if (value is not null) - writer.WriteValue(value.ToString()); - } - } -} diff --git a/BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs b/BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs deleted file mode 100644 index a8181e099..000000000 --- a/BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Client.Models -{ - public class CreateCustodianAccountRequest - { - public string CustodianCode { get; set; } - public string Name { get; set; } - - public JObject Config { get; set; } - } -} diff --git a/BTCPayServer.Client/Models/CustodianAccountBaseData.cs b/BTCPayServer.Client/Models/CustodianAccountBaseData.cs deleted file mode 100644 index 9a157a165..000000000 --- a/BTCPayServer.Client/Models/CustodianAccountBaseData.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Client.Models -{ - public abstract class CustodianAccountBaseData - { - public string CustodianCode { get; set; } - - public string Name { get; set; } - - public string StoreId { get; set; } - - public JObject Config { get; set; } - } - -} diff --git a/BTCPayServer.Client/Models/CustodianAccountData.cs b/BTCPayServer.Client/Models/CustodianAccountData.cs deleted file mode 100644 index 72619ea31..000000000 --- a/BTCPayServer.Client/Models/CustodianAccountData.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BTCPayServer.Client.Models -{ - public class CustodianAccountData : CustodianAccountBaseData - { - public string Id { get; set; } - } -} diff --git a/BTCPayServer.Client/Models/CustodianAccountResponse.cs b/BTCPayServer.Client/Models/CustodianAccountResponse.cs deleted file mode 100644 index 519aae602..000000000 --- a/BTCPayServer.Client/Models/CustodianAccountResponse.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Client.Models; - -public class CustodianAccountResponse : CustodianAccountData -{ - public IDictionary AssetBalances { get; set; } - - public CustodianAccountResponse() - { - - } - -} diff --git a/BTCPayServer.Client/Models/CustodianData.cs b/BTCPayServer.Client/Models/CustodianData.cs deleted file mode 100644 index b4c57b981..000000000 --- a/BTCPayServer.Client/Models/CustodianData.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Client.Models; - -public class CustodianData -{ - public string Code { get; set; } - public string Name { get; set; } - public Dictionary TradableAssetPairs { get; set; } - public string[] WithdrawablePaymentMethods { get; set; } - public string[] DepositablePaymentMethods { get; set; } - -} diff --git a/BTCPayServer.Client/Models/DepositAddressData.cs b/BTCPayServer.Client/Models/DepositAddressData.cs deleted file mode 100644 index 818b9df5f..000000000 --- a/BTCPayServer.Client/Models/DepositAddressData.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace BTCPayServer.Client.Models; - -public class DepositAddressData -{ - // /** - // * Example: P2PKH, P2SH, P2WPKH, P2TR, BOLT11, ... - // */ - // public string Type { get; set; } - - /** - * Format depends hugely on the type. - */ - public string Address { get; set; } - -} diff --git a/BTCPayServer.Client/Models/MarketTradeResponseData.cs b/BTCPayServer.Client/Models/MarketTradeResponseData.cs deleted file mode 100644 index 330c0f142..000000000 --- a/BTCPayServer.Client/Models/MarketTradeResponseData.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Client.Models; - -public class MarketTradeResponseData -{ - public string FromAsset { get; } - public string ToAsset { get; } - /** - * The ledger entries that show the balances that were affected by the trade. - */ - public List LedgerEntries { get; } - /** - * The unique ID of the trade that was executed. - */ - public string TradeId { get; } - - public string AccountId { get; } - - public string CustodianCode { get; } - - public MarketTradeResponseData(string fromAsset, string toAsset, List ledgerEntries, string tradeId, string accountId, string custodianCode) - { - FromAsset = fromAsset; - ToAsset = toAsset; - LedgerEntries = ledgerEntries; - TradeId = tradeId; - AccountId = accountId; - CustodianCode = custodianCode; - } -} diff --git a/BTCPayServer.Client/Models/TradeQuoteResponseData.cs b/BTCPayServer.Client/Models/TradeQuoteResponseData.cs deleted file mode 100644 index 1ad4d7adb..000000000 --- a/BTCPayServer.Client/Models/TradeQuoteResponseData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using BTCPayServer.JsonConverters; -using Newtonsoft.Json; - -namespace BTCPayServer.Client.Models; - -public class TradeQuoteResponseData -{ - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal Bid { get; } - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal Ask { get; } - public string ToAsset { get; } - public string FromAsset { get; } - - public TradeQuoteResponseData(string fromAsset, string toAsset, decimal bid, decimal ask) - { - FromAsset = fromAsset; - ToAsset = toAsset; - Bid = bid; - Ask = ask; - } -} diff --git a/BTCPayServer.Client/Models/TradeRequestData.cs b/BTCPayServer.Client/Models/TradeRequestData.cs deleted file mode 100644 index eee04817e..000000000 --- a/BTCPayServer.Client/Models/TradeRequestData.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Client.Models; - -public class TradeRequestData -{ - public string FromAsset { set; get; } - public string ToAsset { set; get; } - [JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))] - public TradeQuantity Qty { set; get; } -} diff --git a/BTCPayServer.Client/Models/WithdrawRequestData.cs b/BTCPayServer.Client/Models/WithdrawRequestData.cs deleted file mode 100644 index a0529f7f1..000000000 --- a/BTCPayServer.Client/Models/WithdrawRequestData.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net.Http.Headers; -using Newtonsoft.Json; - -namespace BTCPayServer.Client.Models; - -public class WithdrawRequestData -{ - public string PaymentMethod { set; get; } - [JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))] - public TradeQuantity Qty { set; get; } - - public WithdrawRequestData() - { - - } - - public WithdrawRequestData(string paymentMethod, TradeQuantity qty) - { - PaymentMethod = paymentMethod; - Qty = qty; - } -} - -#nullable enable -public record TradeQuantity -{ - public TradeQuantity(decimal value, ValueType type) - { - Type = type; - Value = value; - } - - public enum ValueType - { - Exact, - Percent - } - - public ValueType Type { get; } - public decimal Value { get; set; } - - public override string ToString() - { - if (Type == ValueType.Exact) - return Value.ToString(CultureInfo.InvariantCulture); - else - return Value.ToString(CultureInfo.InvariantCulture) + "%"; - } - public static TradeQuantity Parse(string str) - { - if (!TryParse(str, out var r)) - throw new FormatException("Invalid TradeQuantity"); - return r; - } - public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity) - { - if (str is null) - throw new ArgumentNullException(nameof(str)); - quantity = null; - str = str.Trim(); - str = str.Replace(" ", ""); - if (str.Length == 0) - return false; - if (str[^1] == '%') - { - if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r)) - return false; - if (r < 0.0m) - return false; - quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent); - } - else - { - if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r)) - return false; - if (r < 0.0m) - return false; - quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact); - } - return true; - } -} diff --git a/BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs b/BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs deleted file mode 100644 index a5fcbf269..000000000 --- a/BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Client.Models; - -public abstract class WithdrawalBaseResponseData -{ - public string Asset { get; } - public string PaymentMethod { get; } - public List LedgerEntries { get; } - public string AccountId { get; } - public string CustodianCode { get; } - - public WithdrawalBaseResponseData(string paymentMethod, string asset, List ledgerEntries, string accountId, - string custodianCode) - { - PaymentMethod = paymentMethod; - Asset = asset; - LedgerEntries = ledgerEntries; - AccountId = accountId; - CustodianCode = custodianCode; - } -} diff --git a/BTCPayServer.Client/Models/WithdrawalResponseData.cs b/BTCPayServer.Client/Models/WithdrawalResponseData.cs deleted file mode 100644 index 14bc8aa4c..000000000 --- a/BTCPayServer.Client/Models/WithdrawalResponseData.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace BTCPayServer.Client.Models; - -public class WithdrawalResponseData : WithdrawalBaseResponseData -{ - - [JsonConverter(typeof(StringEnumConverter))] - public WithdrawalStatus Status { get; } - - public string WithdrawalId { get; } - public DateTimeOffset CreatedTime { get; } - - public string TransactionId { get; } - - public string TargetAddress { get; } - - public WithdrawalResponseData(string paymentMethod, string asset, List ledgerEntries, string withdrawalId, string accountId, - string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId, - custodianCode) - { - WithdrawalId = withdrawalId; - TargetAddress = targetAddress; - TransactionId = transactionId; - Status = status; - CreatedTime = createdTime; - } - - - public enum WithdrawalStatus - { - Unknown = 0, - Queued = 1, - Complete = 2, - Failed = 3 - } -} diff --git a/BTCPayServer.Client/Models/WithdrawalSimulationResponseData.cs b/BTCPayServer.Client/Models/WithdrawalSimulationResponseData.cs deleted file mode 100644 index 2fc494481..000000000 --- a/BTCPayServer.Client/Models/WithdrawalSimulationResponseData.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.JsonConverters; -using Newtonsoft.Json; - -namespace BTCPayServer.Client.Models; - -public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData -{ - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal? MinQty { get; set; } - [JsonConverter(typeof(NumericStringJsonConverter))] - public decimal? MaxQty { get; set; } - - public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId, - string custodianCode, List ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod, - asset, ledgerEntries, accountId, custodianCode) - { - MinQty = minQty; - MaxQty = maxQty; - } -} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 3e5072b5a..8087a620f 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -40,11 +40,6 @@ namespace BTCPayServer.Client public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments"; public const string CanViewPullPayments = "btcpay.store.canviewpullpayments"; public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments"; - public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts"; - public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts"; - public const string CanDepositToCustodianAccounts = "btcpay.store.candeposittocustodianaccount"; - public const string CanWithdrawFromCustodianAccounts = "btcpay.store.canwithdrawfromcustodianaccount"; - public const string CanTradeCustodianAccount = "btcpay.store.cantradecustodianaccount"; public const string Unrestricted = "unrestricted"; public static IEnumerable AllPolicies { @@ -79,11 +74,6 @@ namespace BTCPayServer.Client yield return CanCreatePullPayments; yield return CanViewPullPayments; yield return CanCreateNonApprovedPullPayments; - yield return CanViewCustodianAccounts; - yield return CanManageCustodianAccounts; - yield return CanDepositToCustodianAccounts; - yield return CanWithdrawFromCustodianAccounts; - yield return CanTradeCustodianAccount; yield return CanManageUsers; yield return CanManagePayouts; yield return CanViewPayouts; @@ -254,7 +244,6 @@ namespace BTCPayServer.Client { var policyMap = new Dictionary>(); PolicyHasChild(policyMap, Policies.CanModifyStoreSettings, - Policies.CanManageCustodianAccounts, Policies.CanManagePullPayments, Policies.CanModifyInvoices, Policies.CanViewStoreSettings, @@ -275,7 +264,6 @@ namespace BTCPayServer.Client Policies.CanUseInternalLightningNode, Policies.CanManageUsers); PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode); - PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts); PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore); PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts); PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts); diff --git a/BTCPayServer.Data/ApplicationDbContext.cs b/BTCPayServer.Data/ApplicationDbContext.cs index 826b21a83..bc60fa9dd 100644 --- a/BTCPayServer.Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/ApplicationDbContext.cs @@ -39,7 +39,6 @@ namespace BTCPayServer.Data public DbSet AddressInvoices { get; set; } public DbSet ApiKeys { get; set; } public DbSet Apps { get; set; } - public DbSet CustodianAccount { get; set; } public DbSet Files { get; set; } public DbSet InvoiceEvents { get; set; } public DbSet InvoiceSearches { get; set; } @@ -94,7 +93,6 @@ namespace BTCPayServer.Data AddressInvoiceData.OnModelCreating(builder); APIKeyData.OnModelCreating(builder, Database); AppData.OnModelCreating(builder, Database); - CustodianAccountData.OnModelCreating(builder, Database); //StoredFile.OnModelCreating(builder); InvoiceEventData.OnModelCreating(builder); InvoiceSearchData.OnModelCreating(builder); diff --git a/BTCPayServer.Data/Data/CustodianAccountData.cs b/BTCPayServer.Data/Data/CustodianAccountData.cs deleted file mode 100644 index 4c5235028..000000000 --- a/BTCPayServer.Data/Data/CustodianAccountData.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Data; - -public class CustodianAccountData : IHasBlob -{ - [Required] - [MaxLength(50)] - public string Id { get; set; } - - [Required] - [MaxLength(50)] - public string StoreId { get; set; } - - [Required] - [MaxLength(50)] - public string CustodianCode { get; set; } - - [Required] - [MaxLength(50)] - public string Name { get; set; } - - [JsonIgnore] - [Obsolete("Use Blob2 instead")] - public byte[] Blob { get; set; } - [JsonIgnore] - public string Blob2 { get; set; } - - [JsonIgnore] - public StoreData StoreData { get; set; } - - internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) - { - builder.Entity() - .HasOne(o => o.StoreData) - .WithMany(i => i.CustodianAccounts) - .HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .HasIndex(o => o.StoreId); - - if (databaseFacade.IsNpgsql()) - { - builder.Entity() - .Property(o => o.Blob2) - .HasColumnType("JSONB"); - } - } -} diff --git a/BTCPayServer.Data/Data/StoreData.cs b/BTCPayServer.Data/Data/StoreData.cs index 789fd54cb..09bc09a12 100644 --- a/BTCPayServer.Data/Data/StoreData.cs +++ b/BTCPayServer.Data/Data/StoreData.cs @@ -44,7 +44,6 @@ namespace BTCPayServer.Data public IEnumerable LightningAddresses { get; set; } public IEnumerable PayoutProcessors { get; set; } public IEnumerable Payouts { get; set; } - public IEnumerable CustodianAccounts { get; set; } public IEnumerable Settings { get; set; } public IEnumerable Forms { get; set; } public IEnumerable StoreRoles { get; set; } diff --git a/BTCPayServer.Data/Migrations/20240325095923_RemoveCustodian.cs b/BTCPayServer.Data/Migrations/20240325095923_RemoveCustodian.cs new file mode 100644 index 000000000..bde89afd0 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20240325095923_RemoveCustodian.cs @@ -0,0 +1,54 @@ +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +#nullable disable + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240325095923_RemoveCustodian")] + public partial class RemoveCustodian : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustodianAccount"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CustodianAccount", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 50, nullable: false), + StoreId = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Blob = table.Column(type: "BLOB", nullable: true), + Blob2 = table.Column(type: "TEXT", nullable: true), + CustodianCode = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CustodianAccount", x => x.Id); + table.ForeignKey( + name: "FK_CustodianAccount_Stores_StoreId", + column: x => x.StoreId, + principalTable: "Stores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CustodianAccount_StoreId", + table: "CustodianAccount", + column: "StoreId"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 116c57e87..f6c3d81fb 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; @@ -189,40 +189,6 @@ namespace BTCPayServer.Migrations b.ToTable("AspNetUsers", (string)null); }); - modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b => - { - b.Property("Id") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Blob2") - .HasColumnType("TEXT"); - - b.Property("CustodianCode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("StoreId") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("StoreId"); - - b.ToTable("CustodianAccount"); - }); - modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b => { b.Property("Id") @@ -598,7 +564,7 @@ namespace BTCPayServer.Migrations .HasMaxLength(30) .HasColumnType("TEXT"); - b.Property("Blob") + b.Property("Blob") .HasColumnType("TEXT"); b.Property("Date") @@ -612,7 +578,7 @@ namespace BTCPayServer.Migrations .HasMaxLength(20) .HasColumnType("TEXT"); - b.Property("Proof") + b.Property("Proof") .HasColumnType("TEXT"); b.Property("PullPaymentDataId") @@ -703,7 +669,7 @@ namespace BTCPayServer.Migrations b.Property("Archived") .HasColumnType("INTEGER"); - b.Property("Blob") + b.Property("Blob") .HasColumnType("TEXT"); b.Property("EndDate") @@ -1219,17 +1185,6 @@ namespace BTCPayServer.Migrations b.Navigation("StoreData"); }); - modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b => - { - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("CustodianAccounts") - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("StoreData"); - }); - modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b => { b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") @@ -1638,8 +1593,6 @@ namespace BTCPayServer.Migrations b.Navigation("Apps"); - b.Navigation("CustodianAccounts"); - b.Navigation("Forms"); b.Navigation("Invoices"); diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index f22e6801d..3f0997b10 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -7,13 +7,11 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; -using BTCPayServer.Abstractions.Custodians; using BTCPayServer.Configuration; using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.Rating; using BTCPayServer.Services; -using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; @@ -193,7 +191,6 @@ namespace BTCPayServer.Tests .ConfigureServices(services => { services.TryAddSingleton(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1))); - services.AddSingleton(); }) .UseKestrel() .UseStartup() diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 13fceaa42..683793ab5 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -152,13 +152,6 @@ namespace BTCPayServer.Tests CanParseDecimalsCore("{\"qty\": \"1.0\"}", 1.0m); CanParseDecimalsCore("{\"qty\": 6.1e-7}", 6.1e-7m); CanParseDecimalsCore("{\"qty\": \"6.1e-7\"}", 6.1e-7m); - - var data = JsonConvert.DeserializeObject("{\"qty\": \"6.1e-7\", \"fromAsset\":\"Test\"}"); - Assert.Equal(6.1e-7m, data.Qty.Value); - Assert.Equal("Test", data.FromAsset); - data = JsonConvert.DeserializeObject("{\"fromAsset\":\"Test\", \"qty\": \"6.1e-7\"}"); - Assert.Equal(6.1e-7m, data.Qty.Value); - Assert.Equal("Test", data.FromAsset); } [Fact] @@ -202,8 +195,6 @@ namespace BTCPayServer.Tests { var d = JsonConvert.DeserializeObject(str); Assert.Equal(expected, d.Qty); - var d2 = JsonConvert.DeserializeObject(str); - Assert.Equal(new TradeQuantity(expected, TradeQuantity.ValueType.Exact), d2.Qty); } [Fact] @@ -892,33 +883,6 @@ namespace BTCPayServer.Tests Assert.Throws(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general Assert.Throws(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum } - - [Fact] - public void ParseTradeQuantity() - { - Assert.Throws(() => TradeQuantity.Parse("1.2345o")); - Assert.Throws(() => TradeQuantity.Parse("o")); - Assert.Throws(() => TradeQuantity.Parse("")); - Assert.Throws(() => TradeQuantity.Parse("1.353%%")); - Assert.Throws(() => TradeQuantity.Parse("1.353 %%")); - Assert.Throws(() => TradeQuantity.Parse("-1.353%")); - Assert.Throws(() => TradeQuantity.Parse("-1.353")); - - var qty = TradeQuantity.Parse("1.3%"); - Assert.Equal(1.3m, qty.Value); - Assert.Equal(TradeQuantity.ValueType.Percent, qty.Type); - var qty2 = TradeQuantity.Parse("1.3"); - Assert.Equal(1.3m, qty2.Value); - Assert.Equal(TradeQuantity.ValueType.Exact, qty2.Type); - Assert.NotEqual(qty, qty2); - Assert.Equal(qty, TradeQuantity.Parse("1.3%")); - Assert.Equal(qty2, TradeQuantity.Parse("1.3")); - Assert.Equal(TradeQuantity.Parse(qty.ToString()), TradeQuantity.Parse("1.3%")); - Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse("1.3")); - Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 ")); - } - - public static WalletFileParsers GetParsers() { var service = new ServiceCollection(); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 869bdbe8a..957255b6c 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -6,7 +6,6 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Abstractions.Custodians; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; @@ -18,7 +17,6 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.PayoutProcessors; using BTCPayServer.Services; -using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Services.Stores; @@ -2317,7 +2315,7 @@ namespace BTCPayServer.Tests newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id); const string newOrderId = "UPDATED-ORDER-ID"; - JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}}"); + JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", \"newstuff\": [1,2,3,4,5]}}"); Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking); Assert.DoesNotContain(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking); await AssertHttpError(403, async () => @@ -4202,28 +4200,7 @@ namespace BTCPayServer.Tests Assert.Single(oneTestWithoutData); Assert.Null(oneTestWithoutData.First().Links.Select(n => n.ObjectData).FirstOrDefault()); } - - } - - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CustodiansControllerTests() - { - using var tester = CreateServerTester(); - await tester.StartAsync(); - await tester.PayTester.EnableExperimental(); - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await AssertHttpError(401, async () => await unauthClient.GetCustodians()); - - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - var clientBasic = await user.CreateClient(); - var custodians = await clientBasic.GetCustodians(); - Assert.NotNull(custodians); - Assert.NotEmpty(custodians); - } - [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task StoreRateConfigTests() @@ -4286,500 +4263,5 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () => clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" })); } - - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CustodianAccountControllerTests() - { - - using var tester = CreateServerTester(); - await tester.StartAsync(); - await tester.PayTester.EnableExperimental(); - - var admin = tester.NewAccount(); - await admin.GrantAccessAsync(true); - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - var adminClient = await admin.CreateClient(Policies.Unrestricted); - var authedButLackingPermissionsClient = await admin.CreateClient(Policies.CanViewStoreSettings); - var viewerOnlyClient = await admin.CreateClient(Policies.CanViewCustodianAccounts); - var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts); - var store = await adminClient.GetStore(admin.StoreId); - var storeId = store.Id; - - // Load a custodian, we use the first one we find. - var custodians = tester.PayTester.GetService>(); - var custodian = custodians.First(); - - // List custodian accounts - // Unauth - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccounts(storeId)); - - // Auth, but wrong permission - await AssertHttpError(403, async () => await authedButLackingPermissionsClient.GetCustodianAccounts(storeId)); - - // Auth, correct permission, empty result - var emptyCustodianAccounts = await viewerOnlyClient.GetCustodianAccounts(storeId); - Assert.Empty(emptyCustodianAccounts); - - - // Create custodian account - - JObject config = JObject.Parse(@"{ -'WithdrawToAddressNamePerPaymentMethod': { - 'BTC-OnChain': 'My Ledger Nano' -}, -'ApiKey': 'APIKEY', -'PrivateKey': 'UFJJVkFURUtFWQ==' -}"); - - var createCustodianAccountRequest = new CreateCustodianAccountRequest(); - createCustodianAccountRequest.Config = config; - createCustodianAccountRequest.CustodianCode = custodian.Code; - - // Unauthorized - await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccount(storeId, createCustodianAccountRequest)); - - // Auth, but wrong permission - await AssertHttpError(403, async () => await viewerOnlyClient.CreateCustodianAccount(storeId, createCustodianAccountRequest)); - - // Auth, correct permission - var custodianAccountData = await managerClient.CreateCustodianAccount(storeId, createCustodianAccountRequest); - Assert.NotNull(custodianAccountData); - Assert.NotNull(custodianAccountData.Id); - var accountId = custodianAccountData.Id; - Assert.Equal(custodian.Code, custodianAccountData.CustodianCode); - - // We did not provide a name, so the custodian's name should've been picked as a fallback - Assert.Equal(custodian.Name, custodianAccountData.Name); - - Assert.Equal(storeId, custodianAccountData.StoreId); - Assert.True(JToken.DeepEquals(config, custodianAccountData.Config)); - - - - // List all Custodian Accounts, now that we have 1 result - - // Admin can see all - var adminCustodianAccounts = await adminClient.GetCustodianAccounts(storeId); - Assert.Single(adminCustodianAccounts); - var adminCustodianAccount = adminCustodianAccounts.First(); - Assert.Equal(adminCustodianAccount.CustodianCode, custodian.Code); - - // Manager can see all, including config - var managerCustodianAccounts = await managerClient.GetCustodianAccounts(storeId); - Assert.Single(managerCustodianAccounts); - Assert.Equal(managerCustodianAccounts.First().CustodianCode, custodian.Code); - Assert.NotNull(managerCustodianAccounts.First().Config); - Assert.True(JToken.DeepEquals(config, managerCustodianAccounts.First().Config)); - - // Viewer can see all, but no config - var viewerCustodianAccounts = await viewerOnlyClient.GetCustodianAccounts(storeId); - Assert.Single(viewerCustodianAccounts); - Assert.Equal(viewerCustodianAccounts.First().CustodianCode, custodian.Code); - Assert.Null(viewerCustodianAccounts.First().Config); - - // Wrong store ID - await AssertApiError(403, "missing-permission", async () => await adminClient.GetCustodianAccounts("WRONG-STORE-ID")); - - - - // Try to fetch 1 custodian account - // Admin - var singleAdminCustodianAccount = await adminClient.GetCustodianAccount(storeId, accountId); - Assert.NotNull(singleAdminCustodianAccount); - Assert.Equal(singleAdminCustodianAccount.CustodianCode, custodian.Code); - - // Wrong store ID - await AssertApiError(403, "missing-permission", async () => await adminClient.GetCustodianAccount("WRONG-STORE-ID", accountId)); - - // Wrong account ID - await AssertApiError(404, "custodian-account-not-found", async () => await adminClient.GetCustodianAccount(storeId, "WRONG-ACCOUNT-ID")); - - // Manager can see, including config - var singleManagerCustodianAccount = await managerClient.GetCustodianAccount(storeId, accountId); - Assert.NotNull(singleManagerCustodianAccount); - Assert.Equal(singleManagerCustodianAccount.CustodianCode, custodian.Code); - Assert.NotNull(singleManagerCustodianAccount.Config); - Assert.True(JToken.DeepEquals(config, singleManagerCustodianAccount.Config)); - - // Viewer can see, but no config - var singleViewerCustodianAccount = await viewerOnlyClient.GetCustodianAccount(storeId, accountId); - Assert.NotNull(singleViewerCustodianAccount); - Assert.Equal(singleViewerCustodianAccount.CustodianCode, custodian.Code); - Assert.Null(singleViewerCustodianAccount.Config); - - - - // Test updating the custodian account we created - var updateCustodianAccountRequest = createCustodianAccountRequest; - updateCustodianAccountRequest.Name = "My Custodian"; - updateCustodianAccountRequest.Config["ApiKey"] = "ZZZ"; - - // Unauth - await AssertHttpError(401, async () => await unauthClient.UpdateCustodianAccount(storeId, accountId, updateCustodianAccountRequest)); - - // Auth, but wrong permission - await AssertHttpError(403, async () => await viewerOnlyClient.UpdateCustodianAccount(storeId, accountId, updateCustodianAccountRequest)); - - // Correct auth: update permissions - var updatedCustodianAccountData = await managerClient.UpdateCustodianAccount(storeId, accountId, createCustodianAccountRequest); - Assert.NotNull(updatedCustodianAccountData); - Assert.Equal(custodian.Code, updatedCustodianAccountData.CustodianCode); - Assert.Equal(updateCustodianAccountRequest.Name, updatedCustodianAccountData.Name); - Assert.Equal(storeId, custodianAccountData.StoreId); - Assert.True(JToken.DeepEquals(updateCustodianAccountRequest.Config, createCustodianAccountRequest.Config)); - - // Admin - updateCustodianAccountRequest.Name = "Admin Account"; - updateCustodianAccountRequest.Config["ApiKey"] = "AAA"; - updatedCustodianAccountData = await adminClient.UpdateCustodianAccount(storeId, accountId, createCustodianAccountRequest); - Assert.NotNull(updatedCustodianAccountData); - Assert.Equal(custodian.Code, updatedCustodianAccountData.CustodianCode); - Assert.Equal(updateCustodianAccountRequest.Name, updatedCustodianAccountData.Name); - Assert.Equal(storeId, custodianAccountData.StoreId); - Assert.True(JToken.DeepEquals(updateCustodianAccountRequest.Config, createCustodianAccountRequest.Config)); - - // Admin tries to update a non-existing custodian account - await AssertHttpError(404, async () => await adminClient.UpdateCustodianAccount(storeId, "WRONG-ACCOUNT-ID", updateCustodianAccountRequest)); - - - - // Get asset balances, but we cannot because of misconfiguration (we did enter dummy data) - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccounts(storeId, true)); - - // // Auth, viewer permission => Error 500 because of BadConfigException (dummy data) - // await AssertHttpError(500, async () => await viewerOnlyClient.GetCustodianAccounts(storeId, true)); - // - - // Delete custodian account - // Unauth - await AssertHttpError(401, async () => await unauthClient.DeleteCustodianAccount(storeId, accountId)); - - // Auth, but wrong permission - await AssertHttpError(403, async () => await viewerOnlyClient.DeleteCustodianAccount(storeId, accountId)); - - // Auth, correct permission - await managerClient.DeleteCustodianAccount(storeId, accountId); - - // Check if the Custodian Account was actually deleted - await AssertHttpError(404, async () => await managerClient.GetCustodianAccount(storeId, accountId)); - - - // TODO what if we try to create a custodian account for a custodian code that does not exist? - // TODO what if we try so set config data that is not valid? In phase 2 we will validate the config and only allow you to save a config that makes sense! - } - - - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CustodianTests() - { - using var tester = CreateServerTester(); - await tester.StartAsync(); - await tester.PayTester.EnableExperimental(); - - var admin = tester.NewAccount(); - await admin.GrantAccessAsync(true); - - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await admin.CreateClient(Policies.CanViewInvoices); - var adminClient = await admin.CreateClient(Policies.Unrestricted); - var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts); - var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts); - var depositClient = await admin.CreateClient(Policies.CanDepositToCustodianAccounts); - var tradeClient = await admin.CreateClient(Policies.CanTradeCustodianAccount); - - var store = await adminClient.GetStore(admin.StoreId); - var storeId = store.Id; - - // Load a custodian, we use the first one we find. - var custodians = tester.PayTester.GetService>(); - var mockCustodian = custodians.First(c => c.Code == "mock"); - - // Create custodian account - var createCustodianAccountRequest = new CreateCustodianAccountRequest(); - createCustodianAccountRequest.CustodianCode = mockCustodian.Code; - - var custodianAccountData = await managerClient.CreateCustodianAccount(storeId, createCustodianAccountRequest); - Assert.NotNull(custodianAccountData); - Assert.Equal(mockCustodian.Code, custodianAccountData.CustodianCode); - Assert.NotNull(custodianAccountData.Id); - var accountId = custodianAccountData.Id; - - - // Test: Get Asset Balances - var custodianAccountWithBalances = await adminClient.GetCustodianAccount(storeId, accountId, true); - Assert.NotNull(custodianAccountWithBalances); - Assert.NotNull(custodianAccountWithBalances.AssetBalances); - Assert.Equal(4, custodianAccountWithBalances.AssetBalances.Count); - Assert.True(custodianAccountWithBalances.AssetBalances.Keys.Contains("BTC")); - Assert.True(custodianAccountWithBalances.AssetBalances.Keys.Contains("LTC")); - Assert.True(custodianAccountWithBalances.AssetBalances.Keys.Contains("EUR")); - Assert.True(custodianAccountWithBalances.AssetBalances.Keys.Contains("USD")); - Assert.Equal(MockCustodian.BalanceBTC, custodianAccountWithBalances.AssetBalances["BTC"]); - Assert.Equal(MockCustodian.BalanceLTC, custodianAccountWithBalances.AssetBalances["LTC"]); - Assert.Equal(MockCustodian.BalanceEUR, custodianAccountWithBalances.AssetBalances["EUR"]); - Assert.Equal(MockCustodian.BalanceUSD, custodianAccountWithBalances.AssetBalances["USD"]); - - // Test: Get Asset Balances omitted if we choose so - var custodianAccountWithoutBalances = await adminClient.GetCustodianAccount(storeId, accountId, false); - Assert.NotNull(custodianAccountWithoutBalances); - Assert.Null(custodianAccountWithoutBalances.AssetBalances); - - - // Test: GetDepositAddress, unauth - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod)); - - // Test: GetDepositAddress, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod)); - - // Test: GetDepositAddress, wrong payment method - await AssertApiError(400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod")); - - // Test: GetDepositAddress, wrong store ID - await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod)); - - // Test: GetDepositAddress, wrong account ID - await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod)); - - // Test: GetDepositAddress, correct payment method - var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod); - Assert.NotNull(depositAddress); - Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address); - - - // Test: Trade, unauth - var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) }; - await AssertHttpError(401, async () => await unauthClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest)); - - // Test: Trade, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest)); - - // Test: Trade, correct permission, correct assets, correct amount - var newTradeResult = await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest); - Assert.NotNull(newTradeResult); - Assert.Equal(accountId, newTradeResult.AccountId); - Assert.Equal(mockCustodian.Code, newTradeResult.CustodianCode); - Assert.Equal(MockCustodian.TradeId, newTradeResult.TradeId); - Assert.Equal(tradeRequest.FromAsset, newTradeResult.FromAsset); - Assert.Equal(tradeRequest.ToAsset, newTradeResult.ToAsset); - Assert.NotNull(newTradeResult.LedgerEntries); - Assert.Equal(3, newTradeResult.LedgerEntries.Count); - Assert.Equal(MockCustodian.TradeQtyBought, newTradeResult.LedgerEntries[0].Qty); - Assert.Equal(tradeRequest.ToAsset, newTradeResult.LedgerEntries[0].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, newTradeResult.LedgerEntries[0].Type); - Assert.Equal(-1 * MockCustodian.TradeQtyBought * MockCustodian.BtcPriceInEuro, newTradeResult.LedgerEntries[1].Qty); - Assert.Equal(tradeRequest.FromAsset, newTradeResult.LedgerEntries[1].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, newTradeResult.LedgerEntries[1].Type); - Assert.Equal(-1 * MockCustodian.TradeFeeEuro, newTradeResult.LedgerEntries[2].Qty); - Assert.Equal(tradeRequest.FromAsset, newTradeResult.LedgerEntries[2].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, newTradeResult.LedgerEntries[2].Type); - - // Test: GetTradeQuote, SATS - var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) }; - await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, satsTradeRequest)); - - // TODO Test: Trade with percentage qty - - // Test: Trade, wrong assets method - var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) }; - await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongAssetsTradeRequest)); - - // Test: wrong account ID - await AssertHttpError(404, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, "WRONG-ACCOUNT-ID", tradeRequest)); - - // Test: wrong store ID - await AssertHttpError(403, async () => await tradeClient.MarketTradeCustodianAccountAsset("WRONG-STORE-ID", accountId, tradeRequest)); - - // Test: Trade, correct assets, wrong amount - var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(0.01m, TradeQuantity.ValueType.Exact) }; - await AssertApiError(400, "insufficient-funds", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, insufficientFundsTradeRequest)); - - - // Test: GetTradeQuote, unauth - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset)); - - // Test: GetTradeQuote, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset)); - - // Test: GetTradeQuote, auth, correct permission - var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset); - Assert.NotNull(tradeQuote); - Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset); - Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset); - Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Bid); - Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Ask); - - // Test: GetTradeQuote, SATS - await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS")); - - // Test: GetTradeQuote, wrong asset - await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset)); - await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET")); - - // Test: wrong account ID - await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset)); - - // Test: wrong store ID - await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset)); - - - - - - // Test: GetTradeInfo, unauth - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId)); - - // Test: GetTradeInfo, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId)); - - // Test: GetTradeInfo, auth, correct permission - var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId); - Assert.NotNull(tradeResult); - Assert.Equal(accountId, tradeResult.AccountId); - Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode); - Assert.Equal(MockCustodian.TradeId, tradeResult.TradeId); - Assert.Equal(tradeRequest.FromAsset, tradeResult.FromAsset); - Assert.Equal(tradeRequest.ToAsset, tradeResult.ToAsset); - Assert.NotNull(tradeResult.LedgerEntries); - Assert.Equal(3, tradeResult.LedgerEntries.Count); - Assert.Equal(MockCustodian.TradeQtyBought, tradeResult.LedgerEntries[0].Qty); - Assert.Equal(tradeRequest.ToAsset, tradeResult.LedgerEntries[0].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, tradeResult.LedgerEntries[0].Type); - Assert.Equal(-1 * MockCustodian.TradeQtyBought * MockCustodian.BtcPriceInEuro, tradeResult.LedgerEntries[1].Qty); - Assert.Equal(tradeRequest.FromAsset, tradeResult.LedgerEntries[1].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, tradeResult.LedgerEntries[1].Type); - Assert.Equal(-1 * MockCustodian.TradeFeeEuro, tradeResult.LedgerEntries[2].Qty); - Assert.Equal(tradeRequest.FromAsset, tradeResult.LedgerEntries[2].Asset); - Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, tradeResult.LedgerEntries[2].Type); - - // Test: GetTradeInfo, wrong trade ID - await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID")); - - // Test: wrong account ID - await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId)); - - // Test: wrong store ID - await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId)); - - var qty = new TradeQuantity(MockCustodian.WithdrawalAmount, TradeQuantity.ValueType.Exact); - // Test: SimulateWithdrawal, unauth - var simulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty); - await AssertHttpError(401, async () => await unauthClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest)); - - // Test: SimulateWithdrawal, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest)); - - // Test: SimulateWithdrawal, correct payment method, correct amount - var simulateWithdrawResponse = await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest); - AssertMockWithdrawal(simulateWithdrawResponse, custodianAccountData); - - // Test: SimulateWithdrawal, wrong payment method - var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty); - await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodSimulateWithdrawalRequest)); - - // Test: SimulateWithdrawal, wrong account ID - await AssertHttpError(404, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", simulateWithdrawalRequest)); - - // Test: SimulateWithdrawal, wrong store ID - // TODO it is weird that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found. - await AssertHttpError(403, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, simulateWithdrawalRequest)); - - // Test: SimulateWithdrawal, correct payment method, wrong amount - var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666")); - await AssertHttpError(400, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongAmountSimulateWithdrawalRequest)); - - // Test: CreateWithdrawal, unauth - var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty); - var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty); - await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest)); - - // Test: CreateWithdrawal, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest)); - - // Test: CreateWithdrawal, correct payment method, correct amount - var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest); - AssertMockWithdrawal(withdrawResponse, custodianAccountData); - - // Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage - var withdrawWithPercentageResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequestPercentage); - AssertMockWithdrawal(withdrawWithPercentageResponse, custodianAccountData); - - // Test: CreateWithdrawal, wrong payment method - var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty); - await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest)); - - // Test: CreateWithdrawal, wrong account ID - await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest)); - - // Test: CreateWithdrawal, wrong store ID - // TODO it is weird that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found. - await AssertHttpError(403, async () => await withdrawalClient.CreateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest)); - - // Test: CreateWithdrawal, correct payment method, wrong amount - var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666")); - await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest)); - - // Test: GetWithdrawalInfo, unauth - await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId)); - - // Test: GetWithdrawalInfo, auth, but wrong permission - await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId)); - - // Test: GetWithdrawalInfo, auth, correct permission - var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId); - AssertMockWithdrawal(withdrawalInfo, custodianAccountData); - - // Test: GetWithdrawalInfo, wrong withdrawal ID - await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID")); - - // Test: wrong account ID - await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId)); - - // Test: wrong store ID - // TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered - await AssertHttpError(403, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId)); - - // TODO assert API error codes, not just status codes by using AssertCustodianApiError() - // TODO also test withdrawals for the various "Status" (Queued, Complete, Failed) - // TODO create a mock custodian with only ICustodian - // TODO create a mock custodian with only ICustodian + ICanWithdraw - // TODO create a mock custodian with only ICustodian + ICanTrade - // TODO create a mock custodian with only ICustodian + ICanDeposit - } - - private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account) - { - Assert.NotNull(withdrawResponse); - Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset); - Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod); - Assert.Equal(account.Id, withdrawResponse.AccountId); - Assert.Equal(account.CustodianCode, withdrawResponse.CustodianCode); - - Assert.Equal(2, withdrawResponse.LedgerEntries.Count); - - Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.LedgerEntries[0].Asset); - Assert.Equal(MockCustodian.WithdrawalAmount - MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[0].Qty); - Assert.Equal(LedgerEntryData.LedgerEntryType.Withdrawal, withdrawResponse.LedgerEntries[0].Type); - - Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.LedgerEntries[1].Asset); - Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty); - Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type); - - if (withdrawResponse is WithdrawalResponseData withdrawalResponseData) - { - Assert.Equal(MockCustodian.WithdrawalStatus, withdrawalResponseData.Status); - Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawalResponseData.TargetAddress); - Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawalResponseData.TransactionId); - Assert.Equal(MockCustodian.WithdrawalId, withdrawalResponseData.WithdrawalId); - Assert.NotEqual(default, withdrawalResponseData.CreatedTime); - } - - if (withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData) - { - Assert.Equal(MockCustodian.WithdrawalMinAmount, withdrawalSimulationResponseData.MinQty); - Assert.Equal(MockCustodian.WithdrawalMaxAmount, withdrawalSimulationResponseData.MaxQty); - } - } } } diff --git a/BTCPayServer.Tests/MockCustodian/MockCustodian.cs b/BTCPayServer.Tests/MockCustodian/MockCustodian.cs deleted file mode 100644 index c009744ed..000000000 --- a/BTCPayServer.Tests/MockCustodian/MockCustodian.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Abstractions.Custodians.Client; -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Client.Models; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Services.Custodian.Client.MockCustodian; - -public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw -{ - public const string DepositPaymentMethod = "BTC-OnChain"; - public const string DepositAddress = "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; - public const string TradeId = "TRADE-ID-001"; - public const string TradeFromAsset = "EUR"; - public const string TradeToAsset = "BTC"; - public static readonly decimal TradeQtyBought = new decimal(1); - public static readonly decimal TradeFeeEuro = new decimal(12.5); - public static readonly decimal BtcPriceInEuro = new decimal(30000); - public const string WithdrawalPaymentMethod = "BTC-OnChain"; - public const string WithdrawalAsset = "BTC"; - public const string WithdrawalId = "WITHDRAWAL-ID-001"; - public static readonly decimal WithdrawalAmount = new decimal(0.5); - public static readonly string WithdrawalAmountPercentage = "12.5%"; - public static readonly decimal WithdrawalMinAmount = new decimal(0.001); - public static readonly decimal WithdrawalMaxAmount = new decimal(0.6); - public static readonly decimal WithdrawalFee = new decimal(0.0005); - public const string WithdrawalTransactionId = "yyy"; - public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"; - public const WithdrawalResponseData.WithdrawalStatus WithdrawalStatus = WithdrawalResponseData.WithdrawalStatus.Queued; - public static readonly decimal BalanceBTC = new decimal(1.23456); - public static readonly decimal BalanceLTC = new decimal(50.123456); - public static readonly decimal BalanceUSD = new decimal(1500.55); - public static readonly decimal BalanceEUR = new decimal(1235.15); - - public string Code - { - get => "mock"; - } - - public string Name - { - get => "MOCK Exchange"; - } - - public Task> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken) - { - var r = new Dictionary() - { - { "BTC", BalanceBTC }, { "LTC", BalanceLTC }, { "USD", BalanceUSD }, { "EUR", BalanceEUR }, - }; - return Task.FromResult(r); - } - - public Task
GetConfigForm(JObject config, CancellationToken cancellationToken = default) - { - return null; - } - - public Task GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken) - { - if (paymentMethod.Equals(DepositPaymentMethod)) - { - var r = new DepositAddressData(); - r.Address = DepositAddress; - return Task.FromResult(r); - } - - throw new CustodianFeatureNotImplementedException($"Only BTC-OnChain is implemented for {this.Name}"); - } - - public string[] GetDepositablePaymentMethods() - { - return new[] { "BTC-OnChain" }; - } - - public List GetTradableAssetPairs() - { - var r = new List(); - r.Add(new AssetPairData("BTC", "EUR", (decimal)0.0001)); - return r; - } - - private MarketTradeResult GetMarketTradeResult() - { - var ledgerEntries = new List(); - ledgerEntries.Add(new LedgerEntryData("BTC", TradeQtyBought, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeQtyBought * BtcPriceInEuro, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee)); - return new MarketTradeResult(TradeFromAsset, TradeToAsset, ledgerEntries, TradeId); - } - - public Task TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken) - { - if (!fromAsset.Equals("EUR") || !toAsset.Equals("BTC")) - { - throw new WrongTradingPairException(fromAsset, toAsset); - } - - if (qty != TradeQtyBought) - { - throw new InsufficientFundsException($"With {Name}, you can only buy {TradeQtyBought} {TradeToAsset} with {TradeFromAsset} and nothing else."); - } - - return Task.FromResult(GetMarketTradeResult()); - } - - public Task GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken) - { - if (tradeId == TradeId) - { - return Task.FromResult(GetMarketTradeResult()); - } - - return Task.FromResult(null); - } - - public Task GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken) - { - if (fromAsset.Equals(TradeFromAsset) && toAsset.Equals(TradeToAsset)) - { - return Task.FromResult(new AssetQuoteResult(TradeFromAsset, TradeToAsset, BtcPriceInEuro, BtcPriceInEuro)); - } - - throw new WrongTradingPairException(fromAsset, toAsset); - //throw new AssetQuoteUnavailableException(pair); - } - - private WithdrawResult CreateWithdrawResult() - { - var ledgerEntries = new List(); - ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal)); - ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee)); - DateTimeOffset createdTime = new DateTimeOffset(2021, 9, 1, 6, 45, 0, new TimeSpan(-7, 0, 0)); - var r = new WithdrawResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalId, WithdrawalStatus, createdTime, WithdrawalTargetAddress, WithdrawalTransactionId); - return r; - } - - private SimulateWithdrawalResult CreateWithdrawSimulationResult() - { - var ledgerEntries = new List(); - ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal)); - ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee)); - var r = new SimulateWithdrawalResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalMinAmount, WithdrawalMaxAmount); - return r; - } - - public Task WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken) - { - if (paymentMethod == WithdrawalPaymentMethod) - { - if (amount.ToString(CultureInfo.InvariantCulture).Equals("" + WithdrawalAmount, StringComparison.InvariantCulture) || WithdrawalAmountPercentage.Equals(amount)) - { - return Task.FromResult(CreateWithdrawResult()); - } - - throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount} or {WithdrawalAmountPercentage}"); - } - - throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}"); - } - - public Task SimulateWithdrawalAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken) - { - if (paymentMethod == WithdrawalPaymentMethod) - { - if (amount == WithdrawalAmount) - { - return Task.FromResult(CreateWithdrawSimulationResult()); - } - - throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}"); - } - - throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}"); - } - - public Task GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken) - { - if (withdrawalId == WithdrawalId && WithdrawalPaymentMethod.Equals(paymentMethod)) - { - return Task.FromResult(CreateWithdrawResult()); - } - - return Task.FromResult(null); - } - - public string[] GetWithdrawablePaymentMethods() - { - return GetDepositablePaymentMethods(); - } -} diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 6219946e4..d342bf02a 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -216,5 +216,6 @@ + diff --git a/BTCPayServer/Components/MainNav/Default.cshtml b/BTCPayServer/Components/MainNav/Default.cshtml index fff2a6420..90dbd4cea 100644 --- a/BTCPayServer/Components/MainNav/Default.cshtml +++ b/BTCPayServer/Components/MainNav/Default.cshtml @@ -10,7 +10,6 @@ @using BTCPayServer.Plugins @using BTCPayServer.Services @using BTCPayServer.Views.Apps -@using BTCPayServer.Views.CustodianAccounts @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext; @inject BTCPayServerEnvironment Env @inject SignInManager SignInManager @@ -95,26 +94,6 @@ } - @if (PoliciesSettings.Experimental) - { - @foreach (var custodianAccount in Model.CustodianAccounts) - { - - } - - } diff --git a/BTCPayServer/Components/MainNav/MainNav.cs b/BTCPayServer/Components/MainNav/MainNav.cs index b648e77af..edca42264 100644 --- a/BTCPayServer/Components/MainNav/MainNav.cs +++ b/BTCPayServer/Components/MainNav/MainNav.cs @@ -9,7 +9,6 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services; using BTCPayServer.Services.Apps; -using BTCPayServer.Services.Custodian.Client; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Identity; @@ -28,7 +27,6 @@ namespace BTCPayServer.Components.MainNav private readonly BTCPayNetworkProvider _networkProvider; private readonly UserManager _userManager; private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; - private readonly CustodianAccountRepository _custodianAccountRepository; private readonly SettingsRepository _settingsRepository; public PoliciesSettings PoliciesSettings { get; } @@ -39,7 +37,6 @@ namespace BTCPayServer.Components.MainNav BTCPayNetworkProvider networkProvider, UserManager userManager, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, - CustodianAccountRepository custodianAccountRepository, SettingsRepository settingsRepository, PoliciesSettings policiesSettings) { @@ -49,7 +46,6 @@ namespace BTCPayServer.Components.MainNav _networkProvider = networkProvider; _storesController = storesController; _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; - _custodianAccountRepository = custodianAccountRepository; _settingsRepository = settingsRepository; PoliciesSettings = policiesSettings; } @@ -88,13 +84,6 @@ namespace BTCPayServer.Components.MainNav }).ToList(); vm.ArchivedAppsCount = apps.Count(a => a.Archived); - - if (PoliciesSettings.Experimental) - { - // Custodian Accounts - var custodianAccounts = await _custodianAccountRepository.FindByStoreId(store.Id); - vm.CustodianAccounts = custodianAccounts; - } } return View(vm); diff --git a/BTCPayServer/Components/MainNav/MainNavViewModel.cs b/BTCPayServer/Components/MainNav/MainNavViewModel.cs index 169448a05..0c5a73cb9 100644 --- a/BTCPayServer/Components/MainNav/MainNavViewModel.cs +++ b/BTCPayServer/Components/MainNav/MainNavViewModel.cs @@ -10,7 +10,6 @@ namespace BTCPayServer.Components.MainNav public List DerivationSchemes { get; set; } public List LightningNodes { get; set; } public List Apps { get; set; } - public CustodianAccountData[] CustodianAccounts { get; set; } public bool AltcoinsBuild { get; set; } public int ArchivedAppsCount { get; set; } public string ContactUrl { get; set; } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldCustodianAccountController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldCustodianAccountController.cs deleted file mode 100644 index 74939379e..000000000 --- a/BTCPayServer/Controllers/GreenField/GreenfieldCustodianAccountController.cs +++ /dev/null @@ -1,479 +0,0 @@ -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.Abstractions.Custodians; -using BTCPayServer.Abstractions.Custodians.Client; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Client; -using BTCPayServer.Client.Models; -using BTCPayServer.Data; -using BTCPayServer.Filters; -using BTCPayServer.Payments; -using BTCPayServer.Security; -using BTCPayServer.Services.Custodian; -using BTCPayServer.Services.Custodian.Client; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Cors; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; -using CustodianAccountData = BTCPayServer.Data.CustodianAccountData; -using CustodianAccountDataClient = BTCPayServer.Client.Models.CustodianAccountData; - -namespace BTCPayServer.Controllers.Greenfield -{ - public class CustodianExceptionFilter : Attribute, IExceptionFilter - { - public void OnException(ExceptionContext context) - { - if (context.Exception is CustodianApiException ex) - { - context.Result = new ObjectResult(new GreenfieldAPIError(ex.Code, ex.Message)) { StatusCode = ex.HttpStatus }; - context.ExceptionHandled = true; - } - } - } - - [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)] - [EnableCors(CorsPolicies.All)] - [CustodianExceptionFilter] - [ExperimentalRouteAttribute] // if you remove this, also remove "x_experimental": true in swagger.template.custodians.json - public class GreenfieldCustodianAccountController : ControllerBase - { - private readonly CustodianAccountRepository _custodianAccountRepository; - private readonly IEnumerable _custodianRegistry; - private readonly IAuthorizationService _authorizationService; - - public GreenfieldCustodianAccountController(CustodianAccountRepository custodianAccountRepository, - IEnumerable custodianRegistry, - IAuthorizationService authorizationService) - { - _custodianAccountRepository = custodianAccountRepository; - _custodianRegistry = custodianRegistry; - _authorizationService = authorizationService; - } - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts")] - [Authorize(Policy = Policies.CanViewCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task ListCustodianAccount(string storeId, [FromQuery] bool assetBalances = false, CancellationToken cancellationToken = default) - { - var custodianAccounts = await _custodianAccountRepository.FindByStoreId(storeId); - - CustodianAccountDataClient[] responses = new CustodianAccountDataClient[custodianAccounts.Length]; - - for (int i = 0; i < custodianAccounts.Length; i++) - { - var custodianAccountData = custodianAccounts[i]; - responses[i] = await ToModel(custodianAccountData, assetBalances, cancellationToken); - } - - return Ok(responses); - } - - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}")] - [Authorize(Policy = Policies.CanViewCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task ViewCustodianAccount(string storeId, string accountId, - [FromQuery] bool assetBalances = false, CancellationToken cancellationToken = default) - { - var custodianAccountData = await GetCustodianAccount(storeId, accountId); - if (custodianAccountData == null) - { - return this.CreateAPIError(404, "custodian-account-not-found", "The custodian account was not found."); - } - var custodianAccount = await ToModel(custodianAccountData, assetBalances, cancellationToken); - return Ok(custodianAccount); - } - - // [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/config")] - // [Authorize(Policy = Policies.CanManageCustodianAccounts, - // AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - // public async Task FetchCustodianAccountConfigForm(string storeId, string accountId, - // [FromQuery] string locale = "en-US", CancellationToken cancellationToken = default) - // { - // // TODO this endpoint needs tests - // var custodianAccountData = await GetCustodianAccount(storeId, accountId); - // var custodianAccount = await ToModel(custodianAccountData, false, cancellationToken); - // - // var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - // var form = await custodian.GetConfigForm(custodianAccount.Config, locale, cancellationToken); - // - // return Ok(form); - // } - // - // [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/config")] - // [Authorize(Policy = Policies.CanManageCustodianAccounts, - // AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - // public async Task PostCustodianAccountConfigForm(string storeId, string accountId, JObject values, - // [FromQuery] string locale = "en-US", CancellationToken cancellationToken = default) - // { - // // TODO this endpoint needs tests - // var custodianAccountData = await GetCustodianAccount(storeId, accountId); - // var custodianAccount = await ToModel(custodianAccountData, false, cancellationToken); - // - // var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - // var form = await custodian.GetConfigForm(values, locale, cancellationToken); - // - // if (form.IsValid()) - // { - // // TODO save the data to the config so it is persisted - // } - // - // return Ok(form); - // } - - private async Task CanSeeCustodianAccountConfig() - { - return (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanManageCustodianAccounts))).Succeeded; - } - - private async Task ToModel(CustodianAccountData custodianAccount, bool includeAsset, CancellationToken cancellationToken) - { - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - var r = includeAsset ? new CustodianAccountResponse() : new CustodianAccountDataClient(); - r.Id = custodianAccount.Id; - r.CustodianCode = custodian.Code; - r.Name = custodianAccount.Name; - r.StoreId = custodianAccount.StoreId; - if (await CanSeeCustodianAccountConfig()) - { - // Only show the "config" field if the user can create or manage the Custodian Account, because config contains sensitive information (API key, etc). - r.Config = custodianAccount.GetBlob(); - } - if (includeAsset) - { - var balances = await GetCustodianByCode(r.CustodianCode).GetAssetBalancesAsync(r.Config, cancellationToken); - ((CustodianAccountResponse)r).AssetBalances = balances; - } - return r; - } - - [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts")] - [Authorize(Policy = Policies.CanManageCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task CreateCustodianAccount(string storeId, CreateCustodianAccountRequest request, CancellationToken cancellationToken) - { - request ??= new CreateCustodianAccountRequest(); - var custodian = GetCustodianByCode(request.CustodianCode); - - // Use the name provided or if none provided use the name of the custodian. - string name = string.IsNullOrEmpty(request.Name) ? custodian.Name : request.Name; - - var custodianAccount = new CustodianAccountData() { CustodianCode = custodian.Code, Name = name, StoreId = storeId, }; - custodianAccount.SetBlob(request.Config); - - await _custodianAccountRepository.CreateOrUpdate(custodianAccount); - return Ok(await ToModel(custodianAccount, false, cancellationToken)); - } - - - [HttpPut("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}")] - [Authorize(Policy = Policies.CanManageCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task UpdateCustodianAccount(string storeId, string accountId, - CreateCustodianAccountRequest request, CancellationToken cancellationToken = default) - { - request ??= new CreateCustodianAccountRequest(); - - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(request.CustodianCode); - - // TODO If storeId is not valid, we get a foreign key SQL error. Is this okay or do we want to check the storeId first? - custodianAccount.CustodianCode = custodian.Code; - custodianAccount.StoreId = storeId; - custodianAccount.Name = request.Name; - - custodianAccount.SetBlob(request.Config); - - await _custodianAccountRepository.CreateOrUpdate(custodianAccount); - return Ok(await ToModel(custodianAccount, false, cancellationToken)); - } - - [HttpDelete("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}")] - [Authorize(Policy = Policies.CanManageCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task DeleteCustodianAccount(string storeId, string accountId) - { - var isDeleted = await _custodianAccountRepository.Remove(accountId, storeId); - if (isDeleted) - { - return Ok(); - } - - throw CustodianAccountNotFound(); - } - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}")] - [Authorize(Policy = Policies.CanDepositToCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task GetDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken cancellationToken = default) - { - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - var config = custodianAccount.GetBlob(); - - if (custodian is ICanDeposit depositableCustodian) - { - var pm = PaymentMethodId.TryParse(paymentMethod); - if (pm == null) - { - return this.CreateAPIError(400, "unsupported-payment-method", - $"Unsupported payment method."); - } - var result = await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, cancellationToken); - return Ok(result); - } - - return this.CreateAPIError(400, "deposit-payment-method-not-supported", - $"Deposits to \"{custodian.Name}\" are not supported using \"{paymentMethod}\"."); - } - - [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market")] - [Authorize(Policy = Policies.CanTradeCustodianAccount, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task MarketTradeCustodianAccountAsset(string storeId, string accountId, - TradeRequestData request, CancellationToken cancellationToken = default) - { - // TODO add SATS check everywhere. We cannot change to 'BTC' ourselves because the qty / price would be different too. - if ("SATS".Equals(request.FromAsset) || "SATS".Equals(request.ToAsset)) - { - return this.CreateAPIError(400, "use-asset-synonym", - $"Please use 'BTC' instead of 'SATS'."); - } - - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanTrade tradableCustodian) - { - decimal qty; - try - { - qty = await ParseQty(request.Qty, request.FromAsset, custodianAccount, custodian, cancellationToken); - } - catch (Exception ex) - { - return UnsupportedAsset(request.FromAsset, ex.Message); - } - try - { - var result = await tradableCustodian.TradeMarketAsync(request.FromAsset, request.ToAsset, qty, - custodianAccount.GetBlob(), cancellationToken); - - return Ok(ToModel(result, accountId, custodianAccount.CustodianCode)); - } - catch (CustodianApiException e) - { - return this.CreateAPIError(e.HttpStatus, e.Code, - e.Message); - } - } - - return this.CreateAPIError(400, "market-trade-not-supported", - $"Placing market orders on \"{custodian.Name}\" is not supported."); - } - - private MarketTradeResponseData ToModel(MarketTradeResult marketTrade, string accountId, string custodianCode) - { - return new MarketTradeResponseData(marketTrade.FromAsset, marketTrade.ToAsset, marketTrade.LedgerEntries, marketTrade.TradeId, accountId, custodianCode); - } - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote")] - [Authorize(Policy = Policies.CanTradeCustodianAccount, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task GetTradeQuote(string storeId, string accountId, [FromQuery] string fromAsset, [FromQuery] string toAsset, CancellationToken cancellationToken = default) - { - // TODO add SATS check everywhere. We cannot change to 'BTC' ourselves because the qty / price would be different too. - if ("SATS".Equals(fromAsset) || "SATS".Equals(toAsset)) - { - return this.CreateAPIError(400, "use-asset-synonym", - $"Please use 'BTC' instead of 'SATS'."); - } - - var custodianAccount = await GetCustodianAccount(storeId, accountId); - - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanTrade tradableCustodian) - { - var priceQuote = await tradableCustodian.GetQuoteForAssetAsync(fromAsset, toAsset, custodianAccount.GetBlob(), cancellationToken); - return Ok(new TradeQuoteResponseData(priceQuote.FromAsset, priceQuote.ToAsset, priceQuote.Bid, priceQuote.Ask)); - } - - return this.CreateAPIError(400, "getting-quote-not-supported", - $"Getting a price quote on \"{custodian.Name}\" is not supported."); - } - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}")] - [Authorize(Policy = Policies.CanTradeCustodianAccount, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken cancellationToken = default) - { - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanTrade tradableCustodian) - { - var result = await tradableCustodian.GetTradeInfoAsync(tradeId, custodianAccount.GetBlob(), cancellationToken); - if (result == null) - { - return this.CreateAPIError(404, "trade-not-found", - $"Could not find the trade with ID {tradeId} on {custodianAccount.Name}"); - } - return Ok(ToModel(result, accountId, custodianAccount.CustodianCode)); - } - - return this.CreateAPIError(400, "fetching-trade-info-not-supported", - $"Fetching past trade info on \"{custodian.Name}\" is not supported."); - } - - [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation")] - [Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task SimulateWithdrawal(string storeId, string accountId, - WithdrawRequestData request, CancellationToken cancellationToken = default) - { - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanWithdraw withdrawableCustodian) - { - var pm = PaymentMethodId.TryParse(request.PaymentMethod); - if (pm == null) - { - return this.CreateAPIError(400, "unsupported-payment-method", - $"Unsupported payment method."); - } - var asset = pm.CryptoCode; - decimal qty; - try - { - qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken); - } - catch (Exception ex) - { - return UnsupportedAsset(asset, ex.Message); - } - - var simulateWithdrawResult = - await withdrawableCustodian.SimulateWithdrawalAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken); - var result = new WithdrawalSimulationResponseData(simulateWithdrawResult.PaymentMethod, simulateWithdrawResult.Asset, - accountId, custodian.Code, simulateWithdrawResult.LedgerEntries, simulateWithdrawResult.MinQty, simulateWithdrawResult.MaxQty); - return Ok(result); - } - - return this.CreateAPIError(400, "withdrawals-not-supported", - $"Withdrawals are not supported for \"{custodian.Name}\"."); - } - - [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals")] - [Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task CreateWithdrawal(string storeId, string accountId, - WithdrawRequestData request, CancellationToken cancellationToken = default) - { - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanWithdraw withdrawableCustodian) - { - var pm = PaymentMethodId.TryParse(request.PaymentMethod); - if (pm == null) - { - return this.CreateAPIError(400, "unsupported-payment-method", - $"Unsupported payment method."); - } - var asset = pm.CryptoCode; - decimal qty; - try - { - qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken); - } - catch (Exception ex) - { - return UnsupportedAsset(asset, ex.Message); - } - - var withdrawResult = - await withdrawableCustodian.WithdrawToStoreWalletAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken); - var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries, - withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId); - return Ok(result); - } - - return this.CreateAPIError(400, "withdrawals-not-supported", - $"Withdrawals are not supported for \"{custodian.Name}\"."); - } - - private IActionResult UnsupportedAsset(string asset, string err) - { - return this.CreateAPIError(400, "invalid-qty", $"It is impossible to use % quantity with this asset ({err})"); - } - - private async Task ParseQty(TradeQuantity qty, string asset, CustodianAccountData custodianAccount, ICustodian custodian, CancellationToken cancellationToken = default) - { - if (qty.Type == TradeQuantity.ValueType.Exact) - return qty.Value; - // Percentage of current holdings => calculate the amount - var config = custodianAccount.GetBlob(); - var balances = await custodian.GetAssetBalancesAsync(config, cancellationToken); - if (!balances.TryGetValue(asset, out var assetBalance)) - return 0.0m; - return (assetBalance * qty.Value) / 100m; - } - - async Task GetCustodianAccount(string storeId, string accountId) - { - var cust = await _custodianAccountRepository.FindById(storeId, accountId); - if (cust is null) - throw CustodianAccountNotFound(); - return cust; - } - - JsonHttpException CustodianAccountNotFound() - { - return new JsonHttpException(this.CreateAPIError(404, "custodian-account-not-found", "Could not find the custodian account")); - } - - ICustodian GetCustodianByCode(string custodianCode) - { - var cust = _custodianRegistry.FirstOrDefault(custodian => custodian.Code.Equals(custodianCode, StringComparison.OrdinalIgnoreCase)); - if (cust is null) - throw new JsonHttpException(this.CreateAPIError(422, "custodian-code-not-found", "The custodian of this account isn't referenced in /api/v1/custodians")); - return cust; - } - - [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}")] - [Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken cancellationToken = default) - { - var custodianAccount = await GetCustodianAccount(storeId, accountId); - var custodian = GetCustodianByCode(custodianAccount.CustodianCode); - - if (custodian is ICanWithdraw withdrawableCustodian) - { - var withdrawResult = await withdrawableCustodian.GetWithdrawalInfoAsync(paymentMethod, withdrawalId, custodianAccount.GetBlob(), cancellationToken); - if (withdrawResult == null) - { - return this.CreateAPIError(404, "withdrawal-not-found", "The withdrawal was not found."); - } - var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries, - withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId); - return Ok(result); - } - - return this.CreateAPIError(400, "fetching-withdrawal-info-not-supported", - $"Fetching withdrawal information is not supported for \"{custodian.Name}\"."); - } - } - -} diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldCustodianController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldCustodianController.cs deleted file mode 100644 index 05422305c..000000000 --- a/BTCPayServer/Controllers/GreenField/GreenfieldCustodianController.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Client.Models; -using BTCPayServer.Filters; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Cors; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Controllers.Greenfield -{ - [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)] - [EnableCors(CorsPolicies.All)] - [ExperimentalRouteAttribute] // if you remove this, also remove "x_experimental": true in swagger.template.custodians.json - public class GreenfieldCustodianController : ControllerBase - { - private readonly IEnumerable _custodianRegistry; - - public GreenfieldCustodianController(IEnumerable custodianRegistry) - { - _custodianRegistry = custodianRegistry; - } - - [HttpGet("~/api/v1/custodians")] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public IActionResult ListCustodians() - { - var all = _custodianRegistry.ToList().Select(ToModel); - return Ok(all); - } - - private CustodianData ToModel(ICustodian custodian) - { - var result = new CustodianData(); - result.Code = custodian.Code; - result.Name = custodian.Name; - - if (custodian is ICanTrade tradableCustodian) - { - var tradableAssetPairs = tradableCustodian.GetTradableAssetPairs(); - var tradableAssetPairsDict = new Dictionary(tradableAssetPairs.Count); - foreach (var tradableAssetPair in tradableAssetPairs) - { - tradableAssetPairsDict.Add(tradableAssetPair.ToString(), tradableAssetPair); - } - result.TradableAssetPairs = tradableAssetPairsDict; - } - - if (custodian is ICanDeposit depositableCustodian) - { - result.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods(); - } - if (custodian is ICanWithdraw withdrawableCustodian) - { - result.WithdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods(); - } - return result; - } - - } -} diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 8d1d83e56..d15fe0396 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -217,27 +217,6 @@ namespace BTCPayServer.Controllers.Greenfield throw new NotSupportedException("This method is not supported by the LocalBTCPayServerClient."); } - public override async Task MarketTradeCustodianAccountAsset(string storeId, string accountId, - TradeRequestData request, CancellationToken cancellationToken = default) - { - return GetFromActionResult( - await GetController().MarketTradeCustodianAccountAsset(storeId, accountId, request, cancellationToken)); - } - - public override async Task SimulateCustodianAccountWithdrawal(string storeId, string accountId, - WithdrawRequestData request, CancellationToken cancellationToken = default) - { - return GetFromActionResult( - await GetController().SimulateWithdrawal(storeId, accountId, request, cancellationToken)); - } - - public override async Task CreateCustodianAccountWithdrawal(string storeId, string accountId, - WithdrawRequestData request, CancellationToken cancellationToken = default) - { - return GetFromActionResult( - await GetController().CreateWithdrawal(storeId, accountId, request, cancellationToken)); - } - public override async Task GetOnChainWalletObjects(string storeId, string cryptoCode, GetWalletObjectsRequest query = null, CancellationToken token = default) diff --git a/BTCPayServer/Controllers/UICustodianAccountsController.cs b/BTCPayServer/Controllers/UICustodianAccountsController.cs deleted file mode 100644 index b0141fae0..000000000 --- a/BTCPayServer/Controllers/UICustodianAccountsController.cs +++ /dev/null @@ -1,646 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Client; -using BTCPayServer.Client.Models; -using BTCPayServer.Data; -using BTCPayServer.Filters; -using BTCPayServer.Forms; -using BTCPayServer.Models.CustodianAccountViewModels; -using BTCPayServer.Payments; -using BTCPayServer.Services; -using BTCPayServer.Services.Custodian.Client; -using BTCPayServer.Services.Rates; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Newtonsoft.Json.Linq; -using NLog.Config; -using CustodianAccountData = BTCPayServer.Data.CustodianAccountData; -using StoreData = BTCPayServer.Data.StoreData; - -namespace BTCPayServer.Controllers -{ - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [AutoValidateAntiforgeryToken] - [ExperimentalRouteAttribute] - public class UICustodianAccountsController : Controller - { - private readonly IEnumerable _custodianRegistry; - private readonly CustodianAccountRepository _custodianAccountRepository; - private readonly DisplayFormatter _displayFormatter; - private readonly BTCPayServerClient _btcPayServerClient; - private readonly BTCPayNetworkProvider _networkProvider; - private readonly LinkGenerator _linkGenerator; - private readonly FormDataService _formDataService; - - public UICustodianAccountsController( - DisplayFormatter displayFormatter, - UserManager userManager, - CustodianAccountRepository custodianAccountRepository, - IEnumerable custodianRegistry, - BTCPayServerClient btcPayServerClient, - BTCPayNetworkProvider networkProvider, - LinkGenerator linkGenerator, - FormDataService formDataService - ) - { - _displayFormatter = displayFormatter; - _custodianAccountRepository = custodianAccountRepository; - _custodianRegistry = custodianRegistry; - _btcPayServerClient = btcPayServerClient; - _networkProvider = networkProvider; - _linkGenerator = linkGenerator; - _formDataService = formDataService; - } - - public string CreatedCustodianAccountId { get; set; } - - [HttpGet("/stores/{storeId}/custodian-accounts/{accountId}")] - public async Task ViewCustodianAccount(string storeId, string accountId) - { - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - var vm = new ViewCustodianAccountViewModel(); - vm.Custodian = custodian; - vm.CustodianAccount = custodianAccount; - - return View(vm); - } - - [HttpGet("/stores/{storeId}/custodian-accounts/{accountId}.json")] - public async Task ViewCustodianAccountJson(string storeId, string accountId) - { - var vm = new ViewCustodianAccountBalancesViewModel(); - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - var store = GetCurrentStore(); - var storeBlob = StoreDataExtensions.GetStoreBlob(store); - var defaultCurrency = storeBlob.DefaultCurrency; - vm.StoreId = store.Id; - vm.DustThresholdInFiat = 1; - vm.StoreDefaultFiat = defaultCurrency; - try - { - var assetBalances = new Dictionary(); - var assetBalancesData = - await custodian.GetAssetBalancesAsync(custodianAccount.GetBlob(), cancellationToken: default); - - foreach (var pair in assetBalancesData) - { - var asset = pair.Key; - - assetBalances.Add(asset, - new AssetBalanceInfo - { - Asset = asset, - Qty = pair.Value, - FormattedQty = pair.Value.ToString(CultureInfo.InvariantCulture) - } - ); - } - - if (custodian is ICanTrade tradingCustodian) - { - var config = custodianAccount.GetBlob(); - var tradableAssetPairs = tradingCustodian.GetTradableAssetPairs(); - - foreach (var pair in assetBalances) - { - var asset = pair.Key; - var assetBalance = assetBalances[asset]; - var tradableAssetPairsList = - tradableAssetPairs.Where(o => o.AssetBought == asset || o.AssetSold == asset).ToList(); - var tradableAssetPairsDict = - new Dictionary(tradableAssetPairsList.Count); - foreach (var assetPair in tradableAssetPairsList) - { - tradableAssetPairsDict.Add(assetPair.ToString(), assetPair); - } - - assetBalance.TradableAssetPairs = tradableAssetPairsDict; - - if (asset.Equals(defaultCurrency)) - { - assetBalance.FormattedFiatValue = - _displayFormatter.Currency(pair.Value.Qty, defaultCurrency); - assetBalance.FiatValue = pair.Value.Qty; - } - else - { - try - { - var quote = await tradingCustodian.GetQuoteForAssetAsync(defaultCurrency, asset, - config, default); - assetBalance.Bid = quote.Bid; - assetBalance.Ask = quote.Ask; - assetBalance.FormattedBid = - _displayFormatter.Currency(quote.Bid, quote.FromAsset); - assetBalance.FormattedAsk = - _displayFormatter.Currency(quote.Ask, quote.FromAsset); - assetBalance.FormattedFiatValue = - _displayFormatter.Currency(pair.Value.Qty * quote.Bid, - defaultCurrency); - assetBalance.FiatValue = pair.Value.Qty * quote.Bid; - } - catch (WrongTradingPairException) - { - // Cannot trade this asset, just ignore - } - } - } - } - - if (custodian is ICanWithdraw withdrawableCustodian) - { - var withdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods(); - foreach (var withdrawablePaymentMethod in withdrawablePaymentMethods) - { - var withdrawableAsset = withdrawablePaymentMethod.Split("-")[0]; - if (assetBalances.ContainsKey(withdrawableAsset)) - { - var assetBalance = assetBalances[withdrawableAsset]; - assetBalance.WithdrawablePaymentMethods.Add(withdrawablePaymentMethod); - } - } - } - - if (custodian is ICanDeposit depositableCustodian) - { - vm.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods(); - } - - vm.AssetBalances = assetBalances; - } - catch (Exception e) - { - vm.AssetBalanceExceptionMessage = e.Message; - } - - return Ok(vm); - } - - [HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/edit")] - public async Task EditCustodianAccount(string storeId, string accountId) - { - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - var blob = custodianAccount.GetBlob(); - var configForm = await custodian.GetConfigForm(blob, HttpContext.RequestAborted); - _formDataService.SetValues(configForm, blob); - - var vm = new EditCustodianAccountViewModel(); - vm.CustodianAccount = custodianAccount; - vm.ConfigForm = configForm; - vm.Config = _formDataService.GetValues(configForm).ToString(); - return View(vm); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/edit")] - public async Task EditCustodianAccount(string storeId, string accountId, - EditCustodianAccountViewModel vm) - { - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - var configForm = await GetNextForm(custodian, vm.Config); - - if (configForm.IsValid()) - { - var newData = _formDataService.GetValues(configForm); - custodianAccount.SetBlob(newData); - custodianAccount.Name = vm.CustodianAccount.Name; - custodianAccount = await _custodianAccountRepository.CreateOrUpdate(custodianAccount); - return RedirectToAction(nameof(ViewCustodianAccount), - new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id }); - } - - // Form not valid: The user must fix the errors before we can save - vm.CustodianAccount = custodianAccount; - vm.ConfigForm = configForm; - vm.Config = _formDataService.GetValues(configForm).ToString(); - return View(vm); - } - - private async Task GetNextForm(ICustodian custodian, string config) - { - JObject b = null; - try - { - if (config != null) - b = JObject.Parse(config); - } - catch - { - } - b ??= new JObject(); - // First, we restore the previous form based on the previous blob that was - // stored in config - var form = await custodian.GetConfigForm(b, HttpContext.RequestAborted); - _formDataService.SetValues(form, b); - // Then we apply new values overriding the previous blob from the Form params - form.ApplyValuesFromForm(Request.Form); - // We extract the new resulting blob, and request what is the next form based on it - b = _formDataService.GetValues(form); - form = await custodian.GetConfigForm(_formDataService.GetValues(form), HttpContext.RequestAborted); - // We set all the values to this blob, and validate the form - _formDataService.SetValues(form, b); - _formDataService.Validate(form, ModelState); - return form; - } - - [HttpGet("/stores/{storeId}/custodian-accounts/create")] - public IActionResult CreateCustodianAccount(string storeId) - { - var vm = new CreateCustodianAccountViewModel(); - vm.StoreId = storeId; - vm.SetCustodianRegistry(_custodianRegistry); - return View(vm); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/create")] - public async Task CreateCustodianAccount(string storeId, CreateCustodianAccountViewModel vm) - { - var store = GetCurrentStore(); - vm.StoreId = store.Id; - vm.SetCustodianRegistry(_custodianRegistry); - - var custodian = _custodianRegistry.GetCustodianByCode(vm.SelectedCustodian); - if (custodian == null) - { - ModelState.AddModelError(nameof(vm.SelectedCustodian), "Invalid Custodian"); - return View(vm); - } - - if (string.IsNullOrEmpty(vm.Name)) - { - vm.Name = custodian.Name; - } - - var custodianAccountData = new CustodianAccountData - { - CustodianCode = vm.SelectedCustodian, - StoreId = vm.StoreId, - Name = custodian.Name - }; - - - var configForm = await GetNextForm(custodian, vm.Config); - if (configForm.IsValid()) - { - var configData = _formDataService.GetValues(configForm); - custodianAccountData.SetBlob(configData); - custodianAccountData.Name = vm.Name; - custodianAccountData = await _custodianAccountRepository.CreateOrUpdate(custodianAccountData); - TempData[WellKnownTempData.SuccessMessage] = "Custodian account successfully created"; - CreatedCustodianAccountId = custodianAccountData.Id; - - return RedirectToAction(nameof(ViewCustodianAccount), - new { storeId = custodianAccountData.StoreId, accountId = custodianAccountData.Id }); - } - - // Ask for more data - vm.ConfigForm = configForm; - vm.Config = _formDataService.GetValues(configForm).ToString(); - return View(vm); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/delete")] - public async Task DeleteCustodianAccount(string storeId, string accountId) - { - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - if (custodianAccount == null) - { - return NotFound(); - } - - var isDeleted = await _custodianAccountRepository.Remove(custodianAccount.Id, custodianAccount.StoreId); - if (isDeleted) - { - TempData[WellKnownTempData.SuccessMessage] = "Custodian account deleted"; - return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId }); - } - - TempData[WellKnownTempData.ErrorMessage] = "Could not delete custodian account"; - return RedirectToAction(nameof(ViewCustodianAccount), - new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id }); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade/simulate")] - public async Task SimulateTradeJson(string storeId, string accountId, - [FromBody] TradeRequestData request) - { - if (string.IsNullOrEmpty(request.FromAsset) || string.IsNullOrEmpty(request.ToAsset)) - { - return BadRequest(); - } - - TradePrepareViewModel vm = new(); - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - try - { - var assetBalancesData = - await custodian.GetAssetBalancesAsync(custodianAccount.GetBlob(), cancellationToken: default); - - if (custodian is ICanTrade tradingCustodian) - { - var config = custodianAccount.GetBlob(); - - foreach (var pair in assetBalancesData) - { - var oneAsset = pair.Key; - if (request.FromAsset.Equals(oneAsset)) - { - vm.MaxQty = pair.Value; - //vm.FormattedMaxQtyToTrade = pair.Value; - - if (request.FromAsset.Equals(request.ToAsset)) - { - // We cannot trade the asset for itself - return BadRequest(); - } - - try - { - var quote = await tradingCustodian.GetQuoteForAssetAsync(request.FromAsset, - request.ToAsset, - config, default); - - // TODO Ask is normally a higher number than Bid!! Let's check this!! Maybe a Unit Test? - vm.Ask = quote.Ask; - vm.Bid = quote.Bid; - vm.FromAsset = quote.FromAsset; - vm.ToAsset = quote.ToAsset; - } - catch (WrongTradingPairException) - { - // Cannot trade this asset - return BadRequest(vm); - } - } - } - } - } - catch (Exception) - { - return BadRequest(); - } - - return Ok(vm); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade")] - public async Task Trade(string storeId, string accountId, - [FromBody] TradeRequestData request) - { - try - { - var result = await _btcPayServerClient.MarketTradeCustodianAccountAsset(storeId, accountId, request); - return Ok(result); - } - catch (GreenfieldAPIException e) - { - var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode }; - return result; - } - } - - [HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/deposit/prepare")] - public async Task GetDepositPrepareJson(string storeId, string accountId, - [FromQuery] string paymentMethod) - { - if (string.IsNullOrEmpty(paymentMethod)) - { - return BadRequest(); - } - - DepositPrepareViewModel vm = new(); - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - try - { - if (custodian is ICanDeposit depositableCustodian) - { - var config = custodianAccount.GetBlob(); - - vm.PaymentMethod = paymentMethod; - var depositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods(); - if (!depositablePaymentMethods.Contains(paymentMethod)) - { - vm.ErrorMessage = $"Payment method \"{paymentMethod}\" is not supported by {custodian.Name}"; - return BadRequest(vm); - } - - try - { - var depositAddressResult = - await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, default); - vm.Address = depositAddressResult.Address; - - var paymentMethodObj = PaymentMethodId.Parse(paymentMethod); - if (paymentMethodObj.IsBTCOnChain) - { - var network = _networkProvider.GetNetwork("BTC"); - var bip21 = network.GenerateBIP21(depositAddressResult.Address, null); - vm.Link = bip21.ToString(); - var paymentMethodId = PaymentMethodId.TryParse(paymentMethod); - if (paymentMethodId != null) - { - var walletId = new WalletId(storeId, paymentMethodId.CryptoCode); - var returnUrl = _linkGenerator.GetPathByAction( - nameof(ViewCustodianAccount), - "UICustodianAccounts", - new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id }, - Request.PathBase); - - vm.CryptoImageUrl = GetImage(paymentMethodId, network); - vm.CreateTransactionUrl = _linkGenerator.GetPathByAction( - nameof(UIWalletsController.WalletSend), - "UIWallets", - new { walletId, defaultDestination = vm.Address, returnUrl }, - Request.PathBase); - } - } - else - { - // TODO support LN + shitcoins - } - } - catch (Exception e) - { - vm.ErrorMessage = e.Message; - return new ObjectResult(vm) { StatusCode = 500 }; - } - } - } - catch (Exception) - { - return BadRequest(); - } - - return Ok(vm); - } - - private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network) - { - // TODO this method was copy-pasted from BTCPayServer.Controllers.UIWalletsController.GetImage(). Maybe refactor this? - var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike - ? Url.Content(network.CryptoImagePath) - : Url.Content(network.LightningImagePath); - return Request.GetRelativePathOrAbsolute(res); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw/simulate")] - public async Task SimulateWithdrawJson(string storeId, string accountId, - [FromBody] WithdrawRequestData withdrawRequestData) - { - if (string.IsNullOrEmpty(withdrawRequestData.PaymentMethod)) - { - return BadRequest(); - } - - var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId); - - if (custodianAccount == null) - return NotFound(); - - var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode); - if (custodian == null) - { - // TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account? - return NotFound(); - } - - var vm = new WithdrawalPrepareViewModel(); - - try - { - if (custodian is ICanWithdraw) - { - var config = custodianAccount.GetBlob(); - - try - { - var simulateWithdrawal = - await _btcPayServerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, withdrawRequestData, - default); - vm = new WithdrawalPrepareViewModel(simulateWithdrawal); - - // There are no bad config fields, so we need an empty array - vm.BadConfigFields = Array.Empty(); - } - catch (BadConfigException e) - { - Form configForm = await custodian.GetConfigForm(config); - _formDataService.SetValues(configForm, config); - string[] badConfigFields = new string[e.BadConfigKeys.Length]; - int i = 0; - foreach (var oneField in configForm.GetAllFields()) - { - foreach (var badConfigKey in e.BadConfigKeys) - { - if (oneField.FullName.Equals(badConfigKey)) - { - var field = configForm.GetFieldByFullName(oneField.FullName); - badConfigFields[i] = field.Label; - i++; - } - } - } - - vm.BadConfigFields = badConfigFields; - return Ok(vm); - } - } - } - catch (Exception e) - { - vm.ErrorMessage = e.Message; - } - - return Ok(vm); - } - - [HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw")] - public async Task Withdraw(string storeId, string accountId, - [FromBody] WithdrawRequestData request) - { - try - { - var result = await _btcPayServerClient.CreateCustodianAccountWithdrawal(storeId, accountId, request); - return Ok(result); - } - catch (GreenfieldAPIException e) - { - var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode }; - return result; - } - } - - private StoreData GetCurrentStore() => HttpContext.GetStoreData(); - } -} diff --git a/BTCPayServer/Controllers/UIManageController.APIKeys.cs b/BTCPayServer/Controllers/UIManageController.APIKeys.cs index 7c88329af..b4d257635 100644 --- a/BTCPayServer/Controllers/UIManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/UIManageController.APIKeys.cs @@ -513,16 +513,6 @@ namespace BTCPayServer.Controllers {Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")}, {Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")}, {$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")}, - {Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "Allows seeing exchange accounts linked to your stores.")}, - {$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "Allows seeing exchange accounts linked to the selected stores.")}, - {Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "Allows modifying exchange accounts linked to your stores.")}, - {$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "Allows modifying exchange accounts linked to selected stores.")}, - {Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "Allows depositing funds to your exchange accounts.")}, - {$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "Allows depositing funds to selected store's exchange accounts.")}, - {Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "Allows withdrawing funds from your exchange accounts to your store.")}, - {$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "Allows withdrawing funds from your selected store's exchange accounts.")}, - {Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "Allows trading funds on your store's exchange accounts.")}, - {$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "Allows trading funds on selected store's exchange accounts.")}, {Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")}, {$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")}, {Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")}, diff --git a/BTCPayServer/Data/CustodianAccountDataExtensions.cs b/BTCPayServer/Data/CustodianAccountDataExtensions.cs deleted file mode 100644 index 6d22c4d6a..000000000 --- a/BTCPayServer/Data/CustodianAccountDataExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable enable -using BTCPayServer.Services.Invoices; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Data; - -public static class CustodianAccountDataExtensions -{ - public static JObject GetBlob(this CustodianAccountData custodianAccountData) - { - return ((IHasBlob)custodianAccountData).GetBlob() ?? new JObject(); - } -} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 9922ebfad..3ad9fb1af 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -38,7 +38,6 @@ using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services; using BTCPayServer.Services.Apps; -using BTCPayServer.Services.Custodian.Client; using BTCPayServer.Services.Fees; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Labels; @@ -168,7 +167,6 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.AddSingleton(); services.TryAddSingleton(); services.AddSingleton(); services.AddOptions().Configure( diff --git a/BTCPayServer/Models/CustodianAccountViewModels/AssetBalanceInfo.cs b/BTCPayServer/Models/CustodianAccountViewModels/AssetBalanceInfo.cs deleted file mode 100644 index c6eb99c08..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/AssetBalanceInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Models.CustodianAccountViewModels; - -public class AssetBalanceInfo -{ - - public string Asset { get; set; } - public decimal? Bid { get; set; } - public decimal? Ask { get; set; } - public decimal Qty { get; set; } - public string FormattedQty { get; set; } - public string FormattedFiatValue { get; set; } - public decimal? FiatValue { get; set; } - public Dictionary TradableAssetPairs { get; set; } - - public List WithdrawablePaymentMethods { get; set; } = new(); - public string FormattedBid { get; set; } - public string FormattedAsk { get; set; } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/CreateCustodianAccountViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/CreateCustodianAccountViewModel.cs deleted file mode 100644 index 5db9e462c..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/CreateCustodianAccountViewModel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Abstractions.Form; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace BTCPayServer.Models.CustodianAccountViewModels -{ - public class CreateCustodianAccountViewModel - { - - public void SetCustodianRegistry(IEnumerable custodianRegistry) - { - var choices = custodianRegistry.Select(o => new Format - { - Name = o.Name, - Value = o.Code - }).ToArray(); - var chosen = choices.FirstOrDefault(); - Custodians = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); - } - - class Format - { - public string Name { get; set; } - public string Value { get; set; } - } - - [Required] - [MaxLength(50)] - [MinLength(1)] - [Display(Name = "Name")] - public string Name { get; set; } - - [Display(Name = "Store")] - public string StoreId { get; set; } - - [Required] - [Display(Name = "Custodian")] - public string SelectedCustodian { get; set; } - // - public SelectList Custodians { get; set; } - - public Form ConfigForm { get; set; } - public string Config { get; set; } - } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/DepositPrepareViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/DepositPrepareViewModel.cs deleted file mode 100644 index c340245da..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/DepositPrepareViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace BTCPayServer.Models.CustodianAccountViewModels; - -public class DepositPrepareViewModel -{ - public string PaymentMethod { get; set; } - public string Address { get; set; } - public string Link { get; set; } - public string CryptoImageUrl { get; set; } - public string ErrorMessage { get; set; } - public string CreateTransactionUrl { get; set; } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/EditCustodianAccountViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/EditCustodianAccountViewModel.cs deleted file mode 100644 index af359bb63..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/EditCustodianAccountViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Data; - -namespace BTCPayServer.Models.CustodianAccountViewModels -{ - public class EditCustodianAccountViewModel - { - - public CustodianAccountData CustodianAccount { get; set; } - public Form ConfigForm { get; set; } - public string Config { get; set; } - } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/TradePrepareViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/TradePrepareViewModel.cs deleted file mode 100644 index db28562e1..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/TradePrepareViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using BTCPayServer.Abstractions.Custodians.Client; - -namespace BTCPayServer.Models.CustodianAccountViewModels; - -public class TradePrepareViewModel : AssetQuoteResult -{ - public decimal MaxQty { get; set; } - -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountBalancesViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountBalancesViewModel.cs deleted file mode 100644 index 98ec07bf1..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountBalancesViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace BTCPayServer.Models.CustodianAccountViewModels -{ - public class ViewCustodianAccountBalancesViewModel - { - public Dictionary AssetBalances { get; set; } - public string AssetBalanceExceptionMessage { get; set; } - - public string StoreId { get; set; } - public string StoreDefaultFiat { get; set; } - public decimal DustThresholdInFiat { get; set; } - public string[] DepositablePaymentMethods { get; set; } - } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountViewModel.cs deleted file mode 100644 index bf7c58c65..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/ViewCustodianAccountViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Data; - -namespace BTCPayServer.Models.CustodianAccountViewModels -{ - public class ViewCustodianAccountViewModel - { - public ICustodian Custodian { get; set; } - public CustodianAccountData CustodianAccount { get; set; } - - } -} diff --git a/BTCPayServer/Models/CustodianAccountViewModels/WithdrawalPrepareViewModel.cs b/BTCPayServer/Models/CustodianAccountViewModels/WithdrawalPrepareViewModel.cs deleted file mode 100644 index 7394f3946..000000000 --- a/BTCPayServer/Models/CustodianAccountViewModels/WithdrawalPrepareViewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Client.Models; - -namespace BTCPayServer.Models.CustodianAccountViewModels; - -public class WithdrawalPrepareViewModel : WithdrawalSimulationResponseData -{ - public string ErrorMessage { get; set; } - public string[] BadConfigFields { get; set; } - - public WithdrawalPrepareViewModel(string paymentMethod, string asset, string accountId, string custodianCode, - List ledgerEntries, decimal minQty, decimal maxQty) : base(paymentMethod, asset, accountId, - custodianCode, ledgerEntries, minQty, maxQty) - { - } - - public WithdrawalPrepareViewModel(WithdrawalSimulationResponseData simulateWithdrawal) : base( - simulateWithdrawal.PaymentMethod, simulateWithdrawal.Asset, simulateWithdrawal.AccountId, - simulateWithdrawal.CustodianCode, simulateWithdrawal.LedgerEntries, simulateWithdrawal.MinQty, - simulateWithdrawal.MaxQty) - { - } - - public WithdrawalPrepareViewModel() : base(null, null, null, null, null, null, null) - { - } -} diff --git a/BTCPayServer/Plugins/FakeCustodian/FakeCustodian.cs b/BTCPayServer/Plugins/FakeCustodian/FakeCustodian.cs deleted file mode 100644 index 227d43d50..000000000 --- a/BTCPayServer/Plugins/FakeCustodian/FakeCustodian.cs +++ /dev/null @@ -1,357 +0,0 @@ -#if DEBUG -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Abstractions.Custodians.Client; -using BTCPayServer.Abstractions.Form; -using BTCPayServer.Client; -using BTCPayServer.Client.Models; -using BTCPayServer.Data; -using BTCPayServer.Services.Custodian.Client; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Plugins.FakeCustodian; - -public class FakeCustodian : ICustodian, ICanDeposit, ICanWithdraw, ICanTrade -{ - private const string TargetAddress = "3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - private const string ValidWithdrawalId = "FAKE_WITHDRAWAL_ID"; - private static readonly decimal _validWithdrawalAmount = new(0.05); - private static readonly decimal _btcWithdrawalFee = new(0.001); - private const string ValidWithdrawalPaymentMethod = "BTC-OnChain"; - private const string TransactionId = "FAKE_TRANSACTION_ID"; - private const string ValidAsset = "BTC"; - private const string ValidPaymentMethod = "BTC-OnChain"; - private const string ValidTradeId = "TRADE-ID-001"; - private const string TradeFromAsset = "EUR"; - private const string TradeToAsset = "BTC"; - private static readonly decimal _tradeQtyBought = new(1); - private static readonly decimal _tradeFeeEuro = new(12.5); - private static readonly decimal _btcPriceInEuro = new(30000); - private readonly CustodianAccountRepository _custodianAccountRepository; - - public FakeCustodian(CustodianAccountRepository custodianAccountRepository, Client.BTCPayServerClient client) - { - _custodianAccountRepository = custodianAccountRepository; - } - - public string Code - { - get => "fake"; - } - - public string Name - { - get => "Fake Exchange"; - } - - public Task> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken) - { - var fakeConfig = ParseConfig(config); - var r = new Dictionary() { { "BTC", fakeConfig.BTCBalance }, { "LTC", fakeConfig.LTCBalance }, { "USD", fakeConfig.USDBalance }, { "EUR", fakeConfig.EURBalance } }; - return Task.FromResult(r); - } - - public Task GetConfigForm(JObject config, CancellationToken cancellationToken = default) - { - var form = new Form(); - - var generalFieldset = Field.CreateFieldset(); - generalFieldset.Label = "General"; - // TODO we cannot validate the custodian account ID because we have no access to the correct value. This is fine given this is a development tool and won't be needed by actual custodians. - var accountIdField = Field.Create("Custodian Account ID", "CustodianAccountId", null, false, - "Enter the ID of this custodian account. This is needed as a workaround which only applies to the Fake Custodian. Fill out correctly to make trading and withdrawing work."); - generalFieldset.Fields.Add(accountIdField); - - // TODO we cannot validate the store ID because we have no access to the correct value. This is fine given this is a development tool and won't be needed by actual custodians. - var storeIdField = Field.Create("Store ID", "StoreId", null, true, - "Enter the ID of this store. This is needed as a workaround which only applies to the Fake Custodian. Fill out correctly to make trading and withdrawing work."); - generalFieldset.Fields.Add(storeIdField); - form.Fields.Add(generalFieldset); - - var balancesFieldset = Field.CreateFieldset(); - - // Maybe a decimal type field would be better? - var fakeBTCBalance = Field.Create("BTC Balance", "BTCBalance", null, true, - "Enter the amount of BTC you want to have."); - var fakeLTCBalance = Field.Create("LTC Balance", "LTCBalance", null, true, - "Enter the amount of LTC you want to have."); - var fakeEURBalance = Field.Create("EUR Balance", "EURBalance", null, true, - "Enter the amount of EUR you want to have."); - var fakeUSDBalance = Field.Create("USD Balance", "USDBalance", null, true, - "Enter the amount of USD you want to have."); - - balancesFieldset.Label = "Fake balances"; - balancesFieldset.Fields.Add(fakeBTCBalance); - balancesFieldset.Fields.Add(fakeLTCBalance); - balancesFieldset.Fields.Add(fakeEURBalance); - balancesFieldset.Fields.Add(fakeUSDBalance); - form.Fields.Add(balancesFieldset); - - return Task.FromResult(form); - } - - private FakeCustodianConfig ParseConfig(JObject config) - { - return config?.ToObject() ?? throw new InvalidOperationException("Invalid config"); - } - - public Task GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken) - { - if (paymentMethod.Equals(ValidPaymentMethod)) - { - DepositAddressData r = new() { Address = "3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }; - return Task.FromResult(r); - } - - return null; - } - - public string[] GetDepositablePaymentMethods() - { - return new[] { ValidPaymentMethod }; - } - - public async Task WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken) - { - // TODO Store fake withdrawals in the DB so we can have a history of withdrawals - if (ValidWithdrawalPaymentMethod.Equals(paymentMethod)) - { - LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, -amount, LedgerEntryData.LedgerEntryType.Withdrawal); - LedgerEntryData ledgerEntryFee = new(ValidAsset, -_btcWithdrawalFee, LedgerEntryData.LedgerEntryType.Fee); - List ledgerEntries = new(); - ledgerEntries.Add(ledgerEntryWithdrawal); - ledgerEntries.Add(ledgerEntryFee); - - var fakeConfig = ParseConfig(config); - if (amount <= fakeConfig.BTCBalance) - { - fakeConfig.BTCBalance -= amount; - - if (_btcWithdrawalFee <= fakeConfig.BTCBalance) - { - fakeConfig.BTCBalance -= _btcWithdrawalFee; - - var custodianAccount = await _custodianAccountRepository.FindById(fakeConfig.StoreId, fakeConfig.CustodianAccountId); - - if (custodianAccount == null) - { - // We could not load the custodian account using the config settings, so they are bad and should be reported to the user so he can fix them. - throw new BadConfigException(new[] { "StoreId", "CustodianAccountId" }); - } - - var newConfig = JObject.FromObject(fakeConfig); - custodianAccount.SetBlob(newConfig); - await _custodianAccountRepository.CreateOrUpdate(custodianAccount); - - var r = new WithdrawResult(paymentMethod, ValidAsset, ledgerEntries, ValidWithdrawalId, WithdrawalResponseData.WithdrawalStatus.Queued, DateTimeOffset.Now, TargetAddress, TransactionId); - return r; - } - CustodianApiException e3 = new(400, "insufficient-funds", "Cannot withdraw " + amount + " " + ValidAsset + " because you don't have enough to pay for fees"); - throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e3); - } - - CustodianApiException e1 = new(400, "insufficient-funds", "Cannot withdraw " + amount + " " + ValidAsset + " because you only hold " + fakeConfig.BTCBalance + " " + ValidAsset); - throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e1); - } - - CustodianApiException e2 = new(400, "only-btc-supported", "The Fake Custodian can only withdraw using payment method " + ValidWithdrawalPaymentMethod); - throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e2); - } - - public Task SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken) - { - if (ValidWithdrawalPaymentMethod.Equals(paymentMethod)) - { - LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, -qty, LedgerEntryData.LedgerEntryType.Withdrawal); - LedgerEntryData ledgerEntryFee = new(ValidAsset, new decimal(-0.001), LedgerEntryData.LedgerEntryType.Fee); - List ledgerEntries = new(); - ledgerEntries.Add(ledgerEntryWithdrawal); - ledgerEntries.Add(ledgerEntryFee); - - var fakeConfig = ParseConfig(config); - var r = new SimulateWithdrawalResult(paymentMethod, ValidAsset, ledgerEntries, new decimal(0.001), fakeConfig.BTCBalance); - return Task.FromResult(r); - } - - CustodianApiException e = new(400, "only-btc-onchain-supported", "The Fake Custodian can only withdraw using payment method " + ValidWithdrawalPaymentMethod); - throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e); - } - - public Task GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken) - { - // TODO make this Fake Custodian smarter and store previous fake withdrawals in the DB - if (ValidWithdrawalPaymentMethod.Equals(paymentMethod) && withdrawalId.Equals(ValidWithdrawalId)) - { - LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, _validWithdrawalAmount, LedgerEntryData.LedgerEntryType.Withdrawal); - LedgerEntryData ledgerEntryFee = new(ValidAsset, new decimal(0.001), LedgerEntryData.LedgerEntryType.Fee); - List ledgerEntries = new(); - ledgerEntries.Add(ledgerEntryWithdrawal); - ledgerEntries.Add(ledgerEntryFee); - - var r = new WithdrawResult(paymentMethod, ValidAsset, ledgerEntries, ValidWithdrawalId, WithdrawalResponseData.WithdrawalStatus.Queued, DateTimeOffset.Now, TargetAddress, TransactionId); - return Task.FromResult(r); - } - - CustodianApiException e = new(400, "withdrawal-not-found", "The Fake Custodian can only fetch withdrawal ID " + ValidWithdrawalId); - throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e); - } - - public string[] GetWithdrawablePaymentMethods() - { - return new[] { ValidPaymentMethod }; - } - - - public List GetTradableAssetPairs() - { - // We only support trading BTC -> EUR and EUR -> BTC - var r = new List(); - r.Add(new AssetPairData(ValidAsset, "EUR", (decimal)0.0001)); - r.Add(new AssetPairData("EUR", ValidAsset, (decimal)5)); - return r; - } - - - public async Task TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken) - { - // TODO store fake traded in the DB + Update the balances so this fake custodian behaves like the real thing. - if ((fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset)) || (fromAsset.Equals(ValidAsset) && toAsset.Equals("EUR"))) - { - // We only support trading BTC -> EUR and EUR -> BTC - - var fakeConfig = ParseConfig(config); - - if (fromAsset.Equals("BTC") && qty > fakeConfig.BTCBalance) - { - throw new InsufficientFundsException($"Insufficient funds. You only have {fakeConfig.BTCBalance} to trade."); - } - - if (fromAsset.Equals("EUR") && qty > fakeConfig.EURBalance) - { - throw new InsufficientFundsException($"Insufficient funds. You only have {fakeConfig.EURBalance} to trade."); - } - - decimal rate; - - rate = getRate(fromAsset, toAsset); - var qtyReceived = qty / rate; - - var ledgerEntries = new List(); - ledgerEntries.Add(new LedgerEntryData(fromAsset, -qty, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData(toAsset, qtyReceived, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee)); - - - if (fromAsset.Equals("BTC")) - { - fakeConfig.BTCBalance -= qty; - } - - if (fromAsset.Equals("EUR")) - { - fakeConfig.EURBalance -= qty; - } - - if (toAsset.Equals("BTC")) - { - fakeConfig.BTCBalance += qtyReceived; - } - - if (toAsset.Equals("EUR")) - { - fakeConfig.EURBalance += qtyReceived; - } - - // Fees are always in EUR... for now... - if (_tradeFeeEuro <= fakeConfig.EURBalance) - { - fakeConfig.EURBalance -= _tradeFeeEuro; - } - else - { - throw new InsufficientFundsException($"Insufficient funds. You don't have enough EUR to pay for fees."); - } - - var custodianAccount = await _custodianAccountRepository.FindById(fakeConfig.StoreId, fakeConfig.CustodianAccountId); - - if (custodianAccount == null) - { - // We could not load the custodian account using the config settings, so they are bad and should be reported to the user so he can fix them. - throw new BadConfigException(new[] { "StoreId", "CustodianAccountId" }); - } - - var newConfig = JObject.FromObject(fakeConfig); - custodianAccount.SetBlob(newConfig); - await _custodianAccountRepository.CreateOrUpdate(custodianAccount); - - - return new MarketTradeResult(fromAsset, toAsset, ledgerEntries, ValidTradeId); - } - - throw new WrongTradingPairException(fromAsset, toAsset); - } - - private static decimal getRate(string fromAsset, string toAsset) - { - decimal rate; - if (fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset)) - { - rate = _btcPriceInEuro; - } - else - { - rate = 1 / _btcPriceInEuro; - } - - return rate; - } - - public Task GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken) - { - // TODO load the transaction from the DB which contains previous fake trades - if (tradeId == ValidTradeId) - { - var ledgerEntries = new List(); - ledgerEntries.Add(new LedgerEntryData(ValidAsset, _tradeQtyBought, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeQtyBought * _btcPriceInEuro, LedgerEntryData.LedgerEntryType.Trade)); - ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee)); - var r = new MarketTradeResult(TradeFromAsset, TradeToAsset, ledgerEntries, ValidTradeId); - - return Task.FromResult(r); - } - - return Task.FromResult(null); - } - - public Task GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken) - { - // TODO use the current market price for a realistic price - - if ((fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset)) || (fromAsset.Equals(ValidAsset) && toAsset.Equals("EUR"))) - { - // We only support trading BTC -> EUR and EUR -> BTC - decimal rate = getRate(fromAsset, toAsset); - return Task.FromResult(new AssetQuoteResult(fromAsset, toAsset, rate, rate)); - } - - throw new WrongTradingPairException(fromAsset, toAsset); - } -} - -public class FakeCustodianConfig -{ - public string CustodianAccountId { get; set; } - public string StoreId { get; set; } - public decimal BTCBalance { get; set; } - public decimal LTCBalance { get; set; } - public decimal USDBalance { get; set; } - public decimal EURBalance { get; set; } - - public FakeCustodianConfig() - { - } -} -#endif diff --git a/BTCPayServer/Plugins/FakeCustodian/FakeCustodianPlugin.cs b/BTCPayServer/Plugins/FakeCustodian/FakeCustodianPlugin.cs deleted file mode 100644 index b4534346c..000000000 --- a/BTCPayServer/Plugins/FakeCustodian/FakeCustodianPlugin.cs +++ /dev/null @@ -1,20 +0,0 @@ -#if DEBUG -using BTCPayServer.Abstractions.Custodians; -using BTCPayServer.Abstractions.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace BTCPayServer.Plugins.FakeCustodian; - -public class FakeCustodianPlugin : BaseBTCPayServerPlugin -{ - public override string Identifier { get; } = "BTCPayServer.Plugins.Custodians.Fake"; - public override string Name { get; } = "Custodian: Fake"; - public override string Description { get; } = "Adds a fake custodian for testing"; - - public override void Execute(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(provider => provider.GetRequiredService()); - } -} -#endif diff --git a/BTCPayServer/Services/Custodian/CustodianAccountRepository.cs b/BTCPayServer/Services/Custodian/CustodianAccountRepository.cs deleted file mode 100644 index 9df0b7e15..000000000 --- a/BTCPayServer/Services/Custodian/CustodianAccountRepository.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Data; -using Microsoft.EntityFrameworkCore; - -namespace BTCPayServer.Services.Custodian.Client -{ - public class CustodianAccountRepository - { - private readonly ApplicationDbContextFactory _contextFactory; - - public CustodianAccountRepository(ApplicationDbContextFactory contextFactory) - { - _contextFactory = contextFactory; - } - - public async Task CreateOrUpdate(CustodianAccountData entity) - { - await using var context = _contextFactory.CreateContext(); - if (string.IsNullOrEmpty(entity.Id)) - { - entity.Id = Guid.NewGuid().ToString(); - await context.CustodianAccount.AddAsync(entity); - } - else - { - context.CustodianAccount.Update(entity); - } - - await context.SaveChangesAsync(); - return entity; - } - - public async Task Remove(string id, string storeId) - { - await using var context = _contextFactory.CreateContext(); - var key = await context.CustodianAccount.SingleOrDefaultAsync(data => data.Id == id && data.StoreId == storeId); - if (key == null) - return false; - context.CustodianAccount.Remove(key); - await context.SaveChangesAsync(); - return true; - } - - public async Task FindByStoreId(string storeId, - CancellationToken cancellationToken = default) - { - if (storeId is null) - throw new ArgumentNullException(nameof(storeId)); - await using var context = _contextFactory.CreateContext(); - IQueryable query = context.CustodianAccount - .Where(ca => ca.StoreId == storeId); - - var data = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); - return data; - } - - public async Task FindById(string storeId, string accountId) - { - await using var context = _contextFactory.CreateContext(); - IQueryable query = context.CustodianAccount - .Where(ca => ca.StoreId == storeId && ca.Id == accountId); - - var custodianAccountData = (await query.ToListAsync()).FirstOrDefault(); - return custodianAccountData; - } - } -} diff --git a/BTCPayServer/Views/UICustodianAccounts/CreateCustodianAccount.cshtml b/BTCPayServer/Views/UICustodianAccounts/CreateCustodianAccount.cshtml deleted file mode 100644 index 7774a4d2a..000000000 --- a/BTCPayServer/Views/UICustodianAccounts/CreateCustodianAccount.cshtml +++ /dev/null @@ -1,66 +0,0 @@ -@using BTCPayServer.Abstractions.Extensions -@using BTCPayServer.Views.CustodianAccounts -@using Microsoft.AspNetCore.Mvc.TagHelpers -@model BTCPayServer.Models.CustodianAccountViewModels.CreateCustodianAccountViewModel -@{ - ViewData.SetActivePage( CustodianAccountsNavPages.Create, "Add a custodian account"); -} - -@section PageFootContent { - -} - - - -

@ViewData["Title"]

- -
-
- - - @if (!ViewContext.ModelState.IsValid) - { -
- } - @if (!string.IsNullOrEmpty(Model.SelectedCustodian)) - { - - } - else if(Model.Custodians.Count() > 0) - { -
- - -
- } - else - { -

No custodians available. Install some plugins to add custodian / exchange support.

- } - - @if (!string.IsNullOrEmpty(Model.SelectedCustodian)) - { -
- - - -
- -
- - - -
- - - } - - @if (Model.Custodians.Count() > 0) - { -
- -
- } - -
-
diff --git a/BTCPayServer/Views/UICustodianAccounts/CustodianAccountsNavPages.cs b/BTCPayServer/Views/UICustodianAccounts/CustodianAccountsNavPages.cs deleted file mode 100644 index 5744bbc9d..000000000 --- a/BTCPayServer/Views/UICustodianAccounts/CustodianAccountsNavPages.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BTCPayServer.Views.CustodianAccounts -{ - public enum CustodianAccountsNavPages - { - View, Create, Update - } -} diff --git a/BTCPayServer/Views/UICustodianAccounts/EditCustodianAccount.cshtml b/BTCPayServer/Views/UICustodianAccounts/EditCustodianAccount.cshtml deleted file mode 100644 index 5bc8cd7ee..000000000 --- a/BTCPayServer/Views/UICustodianAccounts/EditCustodianAccount.cshtml +++ /dev/null @@ -1,46 +0,0 @@ -@using BTCPayServer.Abstractions.Extensions -@using BTCPayServer.Abstractions.Models -@using BTCPayServer.Views.CustodianAccounts -@using Microsoft.AspNetCore.Mvc.TagHelpers -@model BTCPayServer.Models.CustodianAccountViewModels.EditCustodianAccountViewModel -@{ - ViewData.SetActivePage(CustodianAccountsNavPages.Update, "Edit Custodian account: " + @Model?.CustodianAccount.Name, Model?.CustodianAccount.Id); -} - -@section PageFootContent { - -} - - - -

@ViewData["Title"]

- -
-
-
- - @if (!ViewContext.ModelState.IsValid) - { -
- } - - -
- - - -
- - - -
- -
- - - Delete this custodian account - -
-
- - diff --git a/BTCPayServer/Views/UICustodianAccounts/ViewCustodianAccount.cshtml b/BTCPayServer/Views/UICustodianAccounts/ViewCustodianAccount.cshtml deleted file mode 100644 index 1714be782..000000000 --- a/BTCPayServer/Views/UICustodianAccounts/ViewCustodianAccount.cshtml +++ /dev/null @@ -1,551 +0,0 @@ -@using BTCPayServer.Abstractions.Extensions -@using BTCPayServer.Abstractions.Custodians -@using BTCPayServer.Abstractions.TagHelpers -@using BTCPayServer.Views.CustodianAccounts -@using Microsoft.AspNetCore.Mvc.TagHelpers -@inject BTCPayServer.Security.ContentSecurityPolicies Csp -@model BTCPayServer.Models.CustodianAccountViewModels.ViewCustodianAccountViewModel -@{ - ViewData.SetActivePage(CustodianAccountsNavPages.View, "Custodian account: " + @Model?.CustodianAccount.Name, Model.CustodianAccount.Id); - Csp.UnsafeEval(); -} - -@section PageHeadContent -{ - -} - -@section PageFootContent { - -} - - - -
- - - - -
-
-
-
- Loading... -
-
- -
-

- {{ account.assetBalanceExceptionMessage }} -

- -

Balances

- -
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AssetBalanceUnit Price (Bid)Unit Price (Ask)Fiat ValueActions
{{ row.asset }}{{ row.formattedQty }} - {{ row.formattedBid }} - - {{ row.formattedAsk }} - - {{ row.formattedFiatValue }} - - Trade - Deposit - Withdraw -
No assets are stored with this custodian (yet).
No holdings are worth more than {{ account.dustThresholdInFiat }} {{ account.storeDefaultFiat }}.
An error occured while loading assets and balances.
-
- -

Features

-

The @Model?.Custodian.Name custodian supports:

-
    -
  • Viewing asset balances
  • - @if (Model?.Custodian is ICanTrade) - { -
  • Converting assets using market orders
  • - } - @if (Model?.Custodian is ICanDeposit) - { -
  • Depositing
  • - } - @if (Model?.Custodian is ICanWithdraw) - { -
  • Withdrawing
  • - } -
-
-
-
- - - - - - - -
- - - - - diff --git a/BTCPayServer/Views/UIServer/Policies.cshtml b/BTCPayServer/Views/UIServer/Policies.cshtml index acc4be42e..e929d015c 100644 --- a/BTCPayServer/Views/UIServer/Policies.cshtml +++ b/BTCPayServer/Views/UIServer/Policies.cshtml @@ -149,7 +149,9 @@ } -
+ @* + Let's uncomment this when we have new experimental features. +
@@ -160,7 +162,7 @@
-
+ *@

Plugins

diff --git a/BTCPayServer/wwwroot/js/custodian-account.js b/BTCPayServer/wwwroot/js/custodian-account.js deleted file mode 100644 index 1a93415d5..000000000 --- a/BTCPayServer/wwwroot/js/custodian-account.js +++ /dev/null @@ -1,683 +0,0 @@ -new Vue({ - el: '#custodianAccountView', - components: { - qrcode: VueQrcode - }, - data: { - account: null, - hideDustAmounts: true, - modals: { - trade: null, - withdraw: null, - deposit: null - }, - deposit: { - asset: null, - paymentMethod: null, - address: null, - link: null, - errorMsg: null, - cryptoImageUrl: null, - tab: null, - isLoading: false - }, - trade: { - row: null, - results: null, - errorMsg: null, - isExecuting: false, - isUpdating: false, - simulationAbortController: null, - priceRefresherInterval: null, - fromAsset: null, - toAsset: null, - qty: null, - maxQty: null, - price: null, - priceForPair: {} - }, - withdraw: { - asset: null, - paymentMethod: null, - errorMsg: null, - qty: null, - minQty: null, - maxQty: null, - badConfigFields: null, - results: null, - isUpdating: null, - isExecuting: false, - simulationAbortController: null, - ledgerEntries: null - }, - }, - computed: { - tradeQtyToReceive: function () { - return this.trade.qty / this.trade.price; - }, - canExecuteTrade: function () { - return this.trade.qty >= this.getMinQtyToTrade() && this.trade.price !== null && this.trade.fromAsset !== null && this.trade.toAsset !== null && !this.trade.isExecuting && this.trade.results === null; - }, - availableAssetsToTrade: function () { - let r = []; - let balances = this?.account?.assetBalances; - if (balances) { - let t = this; - let rows = Object.values(balances); - rows = rows.filter(function (row) { - return row.fiatValue > t.account.dustThresholdInFiat; - }); - - for (let i in rows) { - r.push(rows[i].asset); - } - } - return r.sort(); - }, - availableAssetsToTradeInto: function () { - let r = []; - let pairs = this.account?.assetBalances?.[this.trade.fromAsset]?.tradableAssetPairs; - if (pairs) { - for (let i in pairs) { - let pair = pairs[i]; - if (pair.assetBought === this.trade.fromAsset) { - r.push(pair.assetSold); - } else if (pair.assetSold === this.trade.fromAsset) { - r.push(pair.assetBought); - } - } - } - return r.sort(); - }, - availableAssetsToDeposit: function () { - let paymentMethods = this?.account?.depositablePaymentMethods; - let r = []; - if (paymentMethods && paymentMethods.length > 0) { - for (let i = 0; i < paymentMethods.length; i++) { - let asset = paymentMethods[i].split("-")[0]; - if (r.indexOf(asset) === -1) { - r.push(asset); - } - } - } - return r.sort(); - }, - availablePaymentMethodsToDeposit: function () { - let paymentMethods = this?.account?.depositablePaymentMethods; - let r = []; - if (Array.isArray(paymentMethods)) { - for (let i = 0; i < paymentMethods.length; i++) { - let pm = paymentMethods[i]; - let asset = pm.split("-")[0]; - if (asset === this.deposit.asset) { - r.push(pm); - } - } - } - return r.sort(); - }, - sortedAssetRows: function () { - if (this.account?.assetBalances) { - let rows = Object.values(this.account.assetBalances); - let t = this; - - if (this.hideDustAmounts) { - rows = rows.filter(function (row) { - return row.fiatValue === null || row.fiatValue > t.account.dustThresholdInFiat; - }); - } - - rows = rows.sort(function (a, b) { - if(b.fiatValue !== null && a.fiatValue !== null){ - return b.fiatValue - a.fiatValue; - }else if(b.fiatValue !== null && a.fiatValue === null){ - return 1; - }else if(b.fiatValue === null && a.fiatValue !== null) { - return -1; - }else{ - return b.asset.localeCompare(a.asset); - } - }); - - return rows; - } - }, - canExecuteWithdrawal: function () { - return (this.withdraw.minQty != null && this.withdraw.qty >= this.withdraw.minQty) - && (this.withdraw.maxQty != null && this.withdraw.qty <= this.withdraw.maxQty) - && this.withdraw.badConfigFields?.length === 0 - && this.withdraw.paymentMethod - && !this.withdraw.isExecuting - && !this.withdraw.isUpdating - && this.withdraw.results === null; - }, - availableAssetsToWithdraw: function () { - let r = []; - const balances = this?.account?.assetBalances; - if (balances) { - for (let asset in balances) { - const balance = balances[asset]; - if (balance?.withdrawablePaymentMethods?.length) { - r.push(asset); - } - } - } - ; - return r.sort(); - }, - availablePaymentMethodsToWithdraw: function () { - if (this.withdraw.asset) { - let paymentMethods = this?.account?.assetBalances?.[this.withdraw.asset]?.withdrawablePaymentMethods; - if (paymentMethods) { - return paymentMethods.sort(); - } - } - return []; - }, - withdrawFees: function(){ - let r = []; - if(this.withdraw.ledgerEntries){ - for (let i = 0; i< this.withdraw.ledgerEntries.length; i++){ - let entry = this.withdraw.ledgerEntries[i]; - if(entry.type === 'Fee'){ - r.push(entry); - } - } - } - return r; - } - }, - methods: { - getMaxQty: function (fromAsset) { - let row = this.account?.assetBalances?.[fromAsset]; - if (row) { - return row.qty; - } - return null; - }, - getMinQtyToTrade: function (fromAsset = this.trade.fromAsset, toAsset = this.trade.toAsset) { - if (fromAsset && toAsset && this.account?.assetBalances) { - for (let asset in this.account.assetBalances) { - let row = this.account.assetBalances[asset]; - - let pairCode = fromAsset + "/" + toAsset; - let pairCodeReverse = toAsset + "/" + fromAsset; - - let pair = row.tradableAssetPairs?.[pairCode]; - let pairReverse = row.tradableAssetPairs?.[pairCodeReverse]; - - if (pair !== null || pairReverse !== null) { - if (pair && !pairReverse) { - return pair.minimumTradeQty; - } else if (!pair && pairReverse) { - let price = this.trade.priceForPair?.[pairCode]; - if (!price) { - return null; - } - // if (reverse) { - // return price / pairReverse.minimumTradeQty; - // }else { - return price * pairReverse.minimumTradeQty; - // } - } - } - } - } - return 0; - }, - setTradeQtyPercent: function (percent) { - this.trade.qty = percent / 100 * this.trade.maxQty; - }, - setWithdrawQtyPercent: function (percent) { - this.withdraw.qty = percent / 100 * this.withdraw.maxQty; - }, - openTradeModal: function (row) { - let _this = this; - this.trade.row = row; - this.trade.results = null; - this.trade.errorMsg = null; - this.trade.fromAsset = row.asset; - if (row.asset === this.account.storeDefaultFiat) { - this.trade.toAsset = "BTC"; - } else { - this.trade.toAsset = this.account.storeDefaultFiat; - } - - this.trade.qty = row.qty; - this.trade.maxQty = row.qty; - this.trade.price = row.bid; - - if (this.modals.trade === null) { - this.modals.trade = new window.bootstrap.Modal('#tradeModal'); - - // Disable price refreshing when modal closes... - const tradeModelElement = document.getElementById('tradeModal') - tradeModelElement.addEventListener('hide.bs.modal', event => { - _this.setTradePriceRefresher(false); - }); - } - - this.setTradePriceRefresher(true); - this.modals.trade.show(); - }, - openWithdrawModal: function (row) { - this.withdraw.asset = row.asset; - this.withdraw.qty = row.qty; - this.withdraw.paymentMethod = null; - this.withdraw.minQty = 0; - this.withdraw.maxQty = row.qty; - this.withdraw.results = null; - this.withdraw.errorMsg = null; - this.withdraw.isUpdating = null; - this.withdraw.isExecuting = false; - - if (this.modals.withdraw === null) { - this.modals.withdraw = new window.bootstrap.Modal('#withdrawModal'); - } - - this.modals.withdraw.show(); - }, - openDepositModal: function (row) { - if (this.modals.deposit === null) { - this.modals.deposit = new window.bootstrap.Modal('#depositModal'); - } - if (row) { - this.deposit.asset = row.asset; - } else if (!this.deposit.asset && this.availableAssetsToDeposit.length > 0) { - this.deposit.asset = this.availableAssetsToDeposit[0]; - } - - this.modals.deposit.show(); - }, - onTradeSubmit: async function (e) { - e.preventDefault(); - - const form = e.currentTarget; - const url = form.getAttribute('action'); - const method = form.getAttribute('method'); - - this.trade.isExecuting = true; - - // Prevent the modal from closing by clicking outside or via the keyboard - this.setModalCanBeClosed(this.modals.trade, false); - - const _this = this; - const response = await fetch(url, { - method: method, - headers: { - 'Content-Type': 'application/json', - 'RequestVerificationToken': this.getRequestVerificationToken() - }, - body: JSON.stringify({ - fromAsset: _this.trade.fromAsset, - toAsset: _this.trade.toAsset, - qty: _this.trade.qty - }) - }); - - let data = null; - try { - data = await response.json(); - } catch (e) { - } - - if (response.ok) { - _this.trade.results = data; - _this.trade.errorMsg = null; - - _this.setTradePriceRefresher(false); - _this.refreshAccountBalances(); - } else { - _this.trade.errorMsg = data && data.message || "Error"; - } - _this.setModalCanBeClosed(_this.modals.trade, true); - _this.trade.isExecuting = false; - }, - - onWithdrawSubmit: async function (e) { - e.preventDefault(); - - const form = e.currentTarget; - const url = form.getAttribute('action'); - const method = form.getAttribute('method'); - - this.withdraw.isExecuting = true; - this.setModalCanBeClosed(this.modals.withdraw, false); - - let dataToSubmit = { - paymentMethod: this.withdraw.paymentMethod, - qty: this.withdraw.qty - }; - - const _this = this; - const response = await fetch(url, { - method: method, - headers: { - 'Content-Type': 'application/json', - 'RequestVerificationToken': this.getRequestVerificationToken() - }, - body: JSON.stringify(dataToSubmit) - }); - - let data = null; - try { - data = await response.json(); - } catch (e) { - } - - if (response.ok) { - _this.withdraw.results = data; - _this.withdraw.errorMsg = null; - - _this.refreshAccountBalances(); - } else { - _this.withdraw.errorMsg = data && data.message || "Error"; - } - _this.setModalCanBeClosed(_this.modals.withdraw, true); - _this.withdraw.isExecuting = false; - }, - - setTradePriceRefresher: function (enabled) { - if (enabled) { - // Update immediately... - this.updateTradePrice(); - - // And keep updating every few seconds... - let _this = this; - this.trade.priceRefresherInterval = setInterval(function () { - _this.updateTradePrice(); - }, 5000); - - } else { - clearInterval(this.trade.priceRefresherInterval); - } - }, - - updateTradePrice: function () { - if (!this.trade.fromAsset || !this.trade.toAsset) { - // We need to know the 2 assets or we cannot do anything... - return; - } - - if (this.trade.fromAsset === this.trade.toAsset) { - // The 2 assets must be different - this.trade.price = null; - return; - } - - if (this.trade.isUpdating) { - // Previous request is still running. No need to hammer the server - return; - } - - this.trade.isUpdating = true; - - let dataToSubmit = { - fromAsset: this.trade.fromAsset, - toAsset: this.trade.toAsset, - qty: this.trade.qty - }; - let url = window.ajaxTradeSimulateUrl; - - this.trade.simulationAbortController = new AbortController(); - - let _this = this; - fetch(url, { - method: "POST", - body: JSON.stringify(dataToSubmit), - signal: this.trade.simulationAbortController.signal, - headers: { - 'Content-Type': 'application/json', - 'RequestVerificationToken': this.getRequestVerificationToken() - } - } - ).then(function (response) { - _this.trade.isUpdating = false; - - if (response.ok) { - return response.json(); - } - // _this.trade.results = data; - // _this.trade.errorMsg = null; } - // Do nothing on error - } - ).then(function (data) { - _this.trade.maxQty = data.maxQty; - - // By default trade everything - if (_this.trade.qty === null) { - _this.trade.qty = _this.trade.maxQty; - } - - // Cannot trade more than what we have - if (data.maxQty < _this.trade.qty) { - _this.trade.qty = _this.trade.maxQty; - } - let pair = data.toAsset + "/" + data.fromAsset; - let pairReverse = data.fromAsset + "/" + data.toAsset; - - // TODO Should we use "bid" in some cases? The spread can be huge with some shitcoins. - _this.trade.price = data.ask; - _this.trade.priceForPair[pair] = data.ask; - _this.trade.priceForPair[pairReverse] = 1 / data.ask; - - }).catch(function (e) { - _this.trade.isUpdating = false; - if (e instanceof DOMException && e.code === DOMException.ABORT_ERR) { - // User aborted fetch request - } else { - throw e; - } - }); - }, - canDepositAsset: function (asset) { - let paymentMethods = this?.account?.depositablePaymentMethods; - if (paymentMethods && paymentMethods.length > 0) { - for (let i = 0; i < paymentMethods.length; i++) { - let pmParts = paymentMethods[i].split("-"); - if (asset === pmParts[0]) { - return true; - } - } - } - return false; - }, - canSwapTradeAssets: function () { - let minQtyToTrade = this.getMinQtyToTrade(this.trade.toAsset, this.trade.fromAsset); - let assetToTradeIntoHoldings = this.account?.assetBalances?.[this.trade.toAsset]; - if (assetToTradeIntoHoldings) { - return assetToTradeIntoHoldings.qty >= minQtyToTrade; - } - }, - swapTradeAssets: function () { - // Swap the 2 assets - let tmp = this.trade.fromAsset; - this.trade.fromAsset = this.trade.toAsset; - this.trade.toAsset = tmp; - this.trade.price = 1 / this.trade.price; - - this.refreshTradeSimulation(); - }, - refreshTradeSimulation: function () { - let maxQty = this.getMaxQty(this.trade.fromAsset); - this.trade.qty = maxQty - this.trade.maxQty = maxQty; - - if(this.trade.simulationAbortController) { - this.trade.simulationAbortController.abort(); - } - - // Update the price asap, so we can continue - let _this = this; - setTimeout(function () { - _this.updateTradePrice(); - }, 100); - }, - refreshAccountBalances: function () { - let _this = this; - fetch(window.ajaxBalanceUrl).then(function (response) { - return response.json(); - }).then(function (result) { - _this.account = result; - - for(let asset in _this.account.assetBalances){ - let assetInfo = _this.account.assetBalances[asset]; - - if(asset !== _this.account.storeDefaultFiat) { - let pair1 = asset + '/' + _this.account.storeDefaultFiat; - _this.trade.priceForPair[pair1] = assetInfo.bid; - - let pair2 = _this.account.storeDefaultFiat + '/' + asset; - _this.trade.priceForPair[pair2] = 1 / assetInfo.bid; - } - } - - }); - }, - getRequestVerificationToken: function () { - return document.querySelector("input[name='__RequestVerificationToken']").value; - }, - setModalCanBeClosed: function (modal, flag) { - modal._config.keyboard = flag; - if (flag) { - modal._config.backdrop = true; - } else { - modal._config.backdrop = 'static'; - } - }, - refreshWithdrawalSimulation: function () { - if(!this.withdraw.paymentMethod || !this.withdraw.qty){ - // We are missing required data, stop now. - return; - } - - if(this.withdraw.simulationAbortController) { - this.withdraw.simulationAbortController.abort(); - } - - let data = { - paymentMethod: this.withdraw.paymentMethod, - qty: this.withdraw.qty - }; - const _this = this; - const token = this.getRequestVerificationToken(); - - this.withdraw.isUpdating = true; - this.withdraw.simulationAbortController = new AbortController(); - fetch(window.ajaxWithdrawSimulateUrl, { - method: "POST", - body: JSON.stringify(data), - signal: this.withdraw.simulationAbortController.signal, - headers: { - 'Content-Type': 'application/json', - 'RequestVerificationToken': token - } - }).then(function (response) { - _this.withdraw.isUpdating = false; - return response.json(); - }).then(function (data) { - if (data.minQty === null) { - _this.withdraw.minQty = 0; - } else { - _this.withdraw.minQty = data.minQty; - } - if (data.maxQty === null) { - _this.withdraw.maxQty = _this.account.assetBalances?.[_this.withdraw.asset]?.qty; - } else { - _this.withdraw.maxQty = data.maxQty; - } - - if (_this.withdraw.qty === null || _this.withdraw.qty > _this.withdraw.maxQty) { - _this.withdraw.qty = _this.withdraw.maxQty; - } - _this.withdraw.badConfigFields = data.badConfigFields; - _this.withdraw.errorMsg = data.errorMessage; - _this.withdraw.ledgerEntries = data.ledgerEntries; - }); - }, - getStoreDefaultFiatValueForAsset: function(asset){ - // TODO - } - }, - watch: { - 'trade.fromAsset': function (newValue, oldValue) { - if (newValue === this.trade.toAsset) { - // This is the same as swapping the 2 assets - this.trade.toAsset = oldValue; - this.trade.price = 1 / this.trade.price; - - this.refreshTradeSimulation(); - } - if (newValue !== oldValue) { - // The qty is going to be wrong, so set to 100% - this.trade.qty = this.getMaxQty(this.trade.fromAsset); - } - }, - 'deposit.asset': function (newValue, oldValue) { - if (this.availablePaymentMethodsToDeposit.length > 0) { - this.deposit.paymentMethod = this.availablePaymentMethodsToDeposit[0]; - } else { - this.deposit.paymentMethod = null; - } - }, - 'deposit.paymentMethod': function (newValue, oldValue) { - let _this = this; - const token = this.getRequestVerificationToken(); - this.deposit.isLoading = true; - fetch(window.ajaxDepositUrl + "?paymentMethod=" + encodeURI(this.deposit.paymentMethod), { - method: "GET", - headers: { - 'Content-Type': 'application/json', - 'RequestVerificationToken': token - } - }).then(function (response) { - _this.deposit.isLoading = false; - return response.json(); - }).then(function (data) { - _this.deposit.address = data.address; - _this.deposit.link = data.link; - _this.deposit.createTransactionUrl = data.createTransactionUrl; - _this.deposit.cryptoImageUrl = data.cryptoImageUrl; - - if (!_this.deposit.tab) { - _this.deposit.tab = 'address'; - } - if (_this.deposit.tab === 'address' && !_this.deposit.address && _this.deposit.link) { - // Tab "address" is not available, but tab "link" is. - _this.deposit.tab = 'link'; - } - - _this.deposit.errorMsg = data.errorMessage; - }); - }, - 'withdraw.asset': function (newValue, oldValue) { - if (this.availablePaymentMethodsToWithdraw.length > 0) { - this.withdraw.paymentMethod = this.availablePaymentMethodsToWithdraw[0]; - } else { - this.withdraw.paymentMethod = null; - } - }, - 'withdraw.paymentMethod': function (newValue, oldValue) { - if (this.withdraw.paymentMethod && this.withdraw.qty) { - this.withdraw.minQty = 0; - this.withdraw.maxQty = null; - this.withdraw.errorMsg = null; - this.withdraw.badConfigFields = null; - - this.refreshWithdrawalSimulation(); - } - }, - 'withdraw.qty': function (newValue, oldValue) { - if (newValue > this.withdraw.maxQty) { - this.withdraw.qty = this.withdraw.maxQty; - } - this.refreshWithdrawalSimulation(); - } - }, - created: function () { - this.refreshAccountBalances(); - }, - mounted: function () { - // Runs when the app is ready - } -}); diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json deleted file mode 100644 index ad8384357..000000000 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json +++ /dev/null @@ -1,1323 +0,0 @@ -{ - "paths": { - "/api/v1/custodians": { - "get": { - "tags": [ - "Custodians" - ], - "summary": "List supported custodians", - "description": "List all supported custodians for the BTCPay instance. You can install plugins to add more custodians.", - "operationId": "Custodians_GetSupportedCustodians", - "responses": { - "200": { - "description": "List of supported custodians", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustodianData" - } - } - } - } - } - }, - "security": [ - { - "API_Key": [], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts": { - "get": { - "operationId": "Custodians_GetStoreCustodianAccounts", - "tags": [ - "Custodians" - ], - "summary": "List store custodian accounts", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "assetBalances", - "in": "query", - "required": false, - "description": "Enable if you want the result to include the 'assetBalances' field. This will make the call slower or could cause the call to fail if the asset balances cannot be loaded (i.e. due to a bad API key).", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "200": { - "description": "List of custodian accounts for the store.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustodianAccountData" - } - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden to view the store's custodian accounts" - } - } - }, - "post": { - "operationId": "Custodians_AddStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Add a custodial account to a store.", - "description": "Add a custodial account to a store.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { "type": "string" } - } - ], - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCustodianAccountRequest" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "Information about the new custodian account", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustodianAccountData" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden to add new custodian account" - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canmanagecustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}": { - "get": { - "operationId": "Custodians_GetStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Get store custodian account", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID", - "schema": { - "type": "string" - } - }, - { - "name": "assetBalances", - "in": "query", - "required": false, - "description": "Enable if you want the result to include the 'assetBalances' field. This will make the call slower or could cause the call to fail if the asset balances cannot be loaded (i.e. due to a bad API key).", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "200": { - "description": "List of custodian accounts for the store.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustodianAccountData" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden to view the custodian account" - } - } - }, - "put": { - "operationId": "Custodians_UpdateStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Update custodial account", - "description": "Update custodial account", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCustodianAccountRequest" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "The updated custodian account", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustodianAccountData" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden to modify new custodian account" - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canmanagecustodianaccounts" - ], - "Basic": [] - } - ] - }, - "delete": { - "operationId": "Custodians_DeleteStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Delete store custodian account", - "description": "Deletes a custodial account", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Custodian account deleted" - }, - "403": { - "description": "If you are authenticated but forbidden to delete the custodian account" - } - } - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote": { - "get": { - "operationId": "Custodians_GetStoreCustodianAccountTradeQuote", - "tags": [ - "Custodians" - ], - "summary": "Get quote for trading one asset for another", - "description": "Get the current bid and ask price for converting one asset into another.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - }, - { - "name": "fromAsset", - "in": "query", - "required": true, - "description": "The asset to convert.", - "schema": { - "type": "string" - } - }, - { - "name": "toAsset", - "in": "query", - "required": true, - "description": "The asset you want.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The quote for converting one asset to another.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuoteResultData" - } - } - } - }, - "404": { - "description": "No tradable asset pair found for this trade." - }, - "403": { - "description": "If you are authenticated but forbidden to create trades" - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canviewcustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market": { - "post": { - "operationId": "Custodians_StoreCustodianAccountTradeMarket", - "tags": [ - "Custodians" - ], - "summary": "Trade one asset for another", - "description": "Trade one asset for another using a market order (=instant purchase with instant result or failure). A suitable asset pair will automatically be selected. If no asset pair is available, the call will fail.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TradeRequestData" - } - } - } - }, - "responses": { - "200": { - "description": "Information about the trade that was executed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TradeResultData" - } - } - } - }, - "404": { - "description": "No tradable asset pair found for this trade." - }, - "403": { - "description": "If you are authenticated but forbidden to create trades" - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.cantradecustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}": { - "get": { - "operationId": "Custodians_GetStoreCustodianAccountDepositAddress", - "tags": [ - "Custodians" - ], - "summary": "Get a deposit address for custodian", - "description": "Get a new deposit address for the custodian using the specified payment method (network + crypto code).", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - }, - { - "name": "paymentMethod", - "in": "path", - "required": true, - "description": "The payment method to use for the deposit. Example: \"BTC-OnChain\" or \"BTC-Lightning\"", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "deposit address", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "depositAddress": { - "type": "string", - "description": "The address to deposit your funds." - } - }, - "description": "A bitcoin address belonging to the custodian" - }, - "example": { - "depositAddress": "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } - } - }, - "404": { - "description": "The custodian does not support deposits using this payment method." - }, - "403": { - "description": "If you are authenticated but forbidden to get the deposit address" - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.candeposittocustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation": { - "post": { - "operationId": "Custodians_SimulateWithdrawFromStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Simulate a withdrawal", - "description": "Get more information about a potential withdrawal including fees, minimum and maximum quantities for the given asset and quantity.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - }, - { - "name": "paymentMethod", - "in": "query", - "required": true, - "description": "The payment method to be used for the withdrawal.", - "schema": { - "type": "string" - } - }, - { - "name": "qty", - "in": "query", - "required": true, - "description": "The quantity to simulate a withdrawal for.", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WithdrawalRequestData" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Information about a potential withdrawal including fees, minimum and maximum quantities.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WithdrawalSimulationResultData" - } - } - } - }, - "400": { - "description": "Withdrawal is not possible because you don't have this much in your account." - }, - "404": { - "description": "Withdrawal is not possible for this payment method." - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "string", - "description": "If you are authenticated but forbidden to create withdrawals" - }, - { - "type": "string", - "description": "Withdrawing to the address provided is not allowed" - } - ] - } - } - } - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canwithdrawfromcustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals": { - "post": { - "operationId": "Custodians_WithdrawFromStoreCustodianAccount", - "tags": [ - "Custodians" - ], - "summary": "Withdraw to store wallet", - "description": "Withdraw an asset to your store wallet.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WithdrawalRequestData" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "Information about the withdrawal that was executed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WithdrawalResultData" - } - } - } - }, - "400": { - "description": "Withdrawal is not possible because you don't have this much in your account." - }, - "404": { - "description": "Withdrawal is not possible for this asset." - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "string", - "description": "If you are authenticated but forbidden to withdraw" - }, - { - "type": "string", - "description": "Withdrawing to the address provided is not allowed" - } - ] - } - } - } - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canwithdrawfromcustodianaccounts" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{withdrawalId}": { - "get": { - "operationId": "Custodians_GetStoreCustodianAccountWithdrawalInfo", - "tags": [ - "Custodians" - ], - "summary": "Get withdrawal info", - "description": "Get the details about a past withdrawal.", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The Store ID", - "schema": { - "type": "string" - } - }, - { - "name": "accountId", - "in": "path", - "required": true, - "description": "The Custodian Account ID.", - "schema": { - "type": "string" - } - }, - { - "name": "withdrawalId", - "in": "path", - "required": true, - "description": "The Withdrawal ID.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Information about the withdrawal", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WithdrawalResultData" - } - } - } - }, - "404": { - "description": "Withdrawal not found." - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "string", - "description": "If you are authenticated but forbidden to create trades" - }, - { - "type": "string", - "description": "Withdrawing to the address provided is not allowed" - } - ] - } - } - } - } - }, - "security": [ - { - "API_Key": [ - "btcpay.store.canwithdrawfromcustodianaccounts" - ], - "Basic": [] - } - ] - } - } - }, - "components": { - "schemas": { - "CustodianData": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The unique code of the custodian.", - "nullable": false - }, - "label": { - "type": "string", - "description": "The name of the custodian.", - "nullable": false - }, - "depositablePaymentMethods": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of payment methods (crypto code + network) you can deposit to the custodian.", - "nullable": false - }, - "withdrawablePaymentMethods": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of payment methods (crypto code + network) you can withdraw from the custodian.", - "nullable": false - }, - "tradableAssetPairs": { - "type": "array", - "description": "A list of tradable asset pair objects, or NULL if the custodian cannot trades/convert assets.", - "nullable": true, - "items": { - "$ref": "#/components/schemas/AssetPairData" - } - } - }, - "example": { - "code": "kraken", - "name": "Kraken", - "tradableAssetPairs": { - "BTC/USD": { - "assetBought": "BTC", - "assetSold": "USD", - "minimumTradeQty": 0.001 - }, - "BTC/EUR": { - "assetBought": "BTC", - "assetSold": "EUR", - "minimumTradeQty": 0.001 - }, - "LTC/USD": { - "assetBought": "LTC", - "assetSold": "USD", - "minimumTradeQty": 0.05 - }, - "LTC/EUR": { - "assetBought": "LTC", - "assetSold": "EUR", - "minimumTradeQty": 0.05 - } - }, - "withdrawablePaymentMethods": [ - "BTC-OnChain", - "LTC-OnChain" - ], - "depositablePaymentMethods": [ - "BTC-OnChain", - "LTC-OnChain" - ] - } - }, - "CustodianAccountData": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "The unique code of the customer's account with this custodian. The format depends on the custodian." - }, - "storeId": { - "type": "string", - "description": "The store ID." - }, - "custodianCode": { - "type": "string", - "description": "The code for the custodian." - }, - "name": { - "type": "string", - "description": "The name of the custodian account." - }, - "assetBalances": { - "type": "array", - "nullable": true, - "description": "A real-time loaded list of all assets (fiat and crypto) on this custodian and the quantity held in the account. Assets with qty 0 can be omitted.", - "items": { - "$ref": "#/components/schemas/AssetBalanceData" - } - }, - "config": { - "type": "object", - "nullable": true, - "description": "The configuration of this custodian account. Specific contents depend on the custodian and your access permissions." - } - }, - "example": { - "accountId": "xxxxxxxxxxxxxxx", - "storeId": "xxxxxxxxxxxxxx", - "custodianCode": "kraken", - "name": "My Kraken Account", - "assetBalances": [ - { - "asset": "BTC", - "qty": "1.23456" - }, - { - "asset": "USD", - "qty": "123456.78" - } - ], - "config": { - "WithdrawToAddressNamePerPaymentMethod": { - "BTC-OnChain": "My Ledger Nano" - }, - "ApiKey": "xxx", - "PrivateKey": "xxx" - } - } - }, - "CreateCustodianAccountRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "The unique code of the customer's account with this custodian. The format depends on the custodian." - }, - "storeId": { - "type": "string", - "description": "The store ID." - }, - "custodianCode": { - "type": "string", - "description": "The code for the custodian." - }, - "name": { - "type": "string", - "description": "The name of the custodian account." - }, - "config": { - "type": "object", - "nullable": true, - "description": "The configuration of this custodian account. Specific contents depend on the custodian and your access permissions." - } - }, - "example": { - "accountId": "xxxxxxxxxxxxxxx", - "storeId": "xxxxxxxxxxxxxx", - "custodianCode": "kraken", - "name": "My Kraken Account", - "config": { - "WithdrawToAddressNamePerPaymentMethod": { - "BTC-OnChain": "My Ledger Nano" - }, - "ApiKey": "xxx", - "PrivateKey": "xxx" - } - } - }, - "QuoteResultData": { - "type": "object", - "properties": { - "fromAsset": { - "type": "string", - "description": "The asset to trade.", - "nullable": false - }, - "toAsset": { - "type": "string", - "description": "The asset you want.", - "nullable": false - }, - "bid": { - "type": "string", - "format": "decimal", - "description": "The bid price.", - "nullable": false - }, - "ask": { - "type": "string", - "format": "decimal", - "description": "The ask price", - "nullable": false - } - }, - "example": { - "fromAsset": "USD", - "toAsset": "BTC", - "bid": "30000.12", - "ask": "30002.24" - } - }, - "TradeRequestData": { - "type": "object", - "properties": { - "fromAsset": { - "type": "string", - "description": "The asset to trade.", - "nullable": false - }, - "toAsset": { - "type": "string", - "description": "The asset you want.", - "nullable": false - }, - "qty": { - "oneOf": [ - { - "type": "string", - "format": "decimal", - "description": "The qty of fromAsset to convert into toAsset.", - "example": "1.50" - }, - { - "type": "string", - "description": "The percent of fromAsset to convert into toAsset. The value must end with \"%\" to be considered a percentage.", - "example": "50%" - } - ], - "nullable": false - } - }, - "example": { - "fromAsset": "USD", - "toAsset": "BTC", - "qty": "50%" - } - }, - "TradeResultData": { - "type": "object", - "properties": { - "fromAsset": { - "type": "string", - "description": "The asset to trade." - }, - "toAsset": { - "type": "string", - "description": "The asset you want." - }, - "ledgerEntries": { - "type": "array", - "description": "The asset entries that were changed during the trade. This is an array of at least 2 items with the asset sold and the asset gained. It may also include ledger entries for the costs of the trade and possibly exchange tokens used.", - "items": { - "$ref": "#/components/schemas/LedgerEntryData" - } - }, - "tradeId": { - "type": "string", - "description": "The unique ID of the trade used by the exchange. This ID can be used to get the details of this trade at a later time.", - "nullable": true - }, - "accountId": { - "type": "string", - "description": "The unique ID of the custodian account used.", - "nullable": false - }, - "custodianCode": { - "type": "string", - "description": "The code of the custodian used.", - "nullable": false - } - }, - "example": { - "fromAsset": "USD", - "toAsset": "BTC", - "ledgerEntries": [ - { - "asset": "BTC", - "qty": "1.23456", - "type": "Trade" - }, - { - "asset": "USD", - "qty": "-61728", - "type": "Trade" - }, - { - "asset": "BTC", - "qty": "-0.00123456", - "type": "Fee" - }, - { - "asset": "KFEE", - "qty": "-123.456", - "type": "Fee" - } - ], - "tradeId": "XXXX-XXXX-XXXX-XXXX", - "accountId": "xxxxxxxxxxxxxx", - "custodianCode": "kraken" - } - }, - "WithdrawalRequestData": { - "type": "object", - "properties": { - "paymentMethod": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodId" - } - ], - "nullable": false - }, - "qty": { - "oneOf": [ - { - "type": "string", - "format": "decimal", - "description": "The quantity to withdraw." - }, - { - "type": "string", - "description": "The percent of your holdings to withdraw. The value must end with \"%\" to be considered a percentage." - } - ] - } - }, - "example": { - "paymentMethod": "BTC-OnChain", - "qty": "0.123456" - } - }, - "WithdrawalResultData": { - "type": "object", - "properties": { - "asset": { - "type": "string", - "description": "The asset that is being withdrawn." - }, - "paymentMethod": { - "type": "string", - "description": "The payment method that is used (crypto code + network)." - }, - "ledgerEntries": { - "type": "array", - "description": "The asset entries that were changed during the withdrawal. The first item is always the withdrawal itself. It could also includes ledger entries for the costs and may include credits or exchange tokens to give a discount.", - "items": { - "$ref": "#/components/schemas/LedgerEntryData" - } - }, - "withdrawalId": { - "type": "string", - "description": "The unique ID of the withdrawal used by the exchange.", - "nullable": true - }, - "accountId": { - "type": "string", - "description": "The unique ID of the custodian account used.", - "nullable": false - }, - "custodianCode": { - "type": "string", - "description": "The code of the custodian used.", - "nullable": false - }, - "status": { - "type": "string", - "description": "The status of the withdrawal: 'Queued', 'Complete', 'Failed' or 'Unknown'.", - "nullable": false - }, - "transactionId": { - "type": "string", - "description": "The transaction ID on the blockchain once the withdrawal has been executed.", - "nullable": true - }, - "targetAddress": { - "type": "string", - "description": "The address where the funds were sent to once the withdrawal has been executed.", - "nullable": true - } - }, - "example": { - "asset": "BTC", - "paymentMethod": "BTC-OnChain", - "ledgerEntries": [ - { - "asset": "BTC", - "qty": "-0.123456", - "type": "Withdrawal" - }, - { - "asset": "BTC", - "qty": "-0.005", - "type": "Fee" - } - ], - "withdrawalId": "XXXX-XXXX-XXXX-XXXX", - "accountId": "xxxxxxxxxxxxxxx", - "custodianCode": "kraken", - "status": "Complete", - "transactionId": "xxxxxxxxxxxxxxx", - "targetAddress": "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - }, - "WithdrawalSimulationResultData": { - "type": "object", - "properties": { - "asset": { - "type": "string", - "description": "The asset that is being withdrawn." - }, - "paymentMethod": { - "type": "string", - "description": "The payment method that is used (crypto code + network)." - }, - "ledgerEntries": { - "type": "array", - "description": "The asset entries that would be changed if this were a real withdrawal. The first item is always the withdrawal itself. It could also includes ledger entries for the costs and may include credits or exchange tokens to give a discount.", - "items": { - "$ref": "#/components/schemas/LedgerEntryData" - } - }, - "accountId": { - "type": "string", - "description": "The unique ID of the custodian account used.", - "nullable": false - }, - "custodianCode": { - "type": "string", - "description": "The code of the custodian used.", - "nullable": false - }, - "minQty": { - "type": "string", - "format": "decimal", - "description": "The minimum amount to withdraw", - "nullable": true - }, - "maxQty": { - "type": "string", - "format": "decimal", - "description": "The maximum amount to withdraw", - "nullable": true - } - }, - "example": { - "asset": "BTC", - "paymentMethod": "BTC-OnChain", - "ledgerEntries": [ - { - "asset": "BTC", - "qty": "-0.123456", - "type": "Withdrawal" - }, - { - "asset": "BTC", - "qty": "-0.005", - "type": "Fee" - } - ], - "withdrawalId": "XXXX-XXXX-XXXX-XXXX", - "accountId": "xxxxxxxxxxxxxxx", - "custodianCode": "kraken", - "status": "Complete", - "transactionId": "xxxxxxxxxxxxxxx", - "targetAddress": "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - }, - "LedgerEntryData": { - "type": "object", - "description": "A single ledger entry meaning an asset and qty has changed (increased or decreased).", - "properties": { - "asset": { - "type": "string", - "description": "An asset.", - "nullable": false - }, - "qty": { - "type": "string", - "format": "decimal", - "description": "The quantity changed of the asset. Can be positive or negative.", - "nullable": false - }, - "type": { - "type": "string", - "description": "Trade, Fee or Withdrawal", - "nullable": false - } - }, - "example": { - "asset": "BTC", - "qty": "1.23456", - "type": "Trade" - } - }, - "AssetBalanceData": { - "type": "object", - "description": "An asset and it's qty.", - "properties": { - "asset": { - "type": "string", - "description": "An asset.", - "nullable": false - }, - "qty": { - "type": "string", - "format": "decimal", - "description": "The quantity changed of the asset. Can be positive or negative.", - "nullable": false - } - }, - "example": { - "asset": "BTC", - "qty": "1.23456" - } - }, - "AssetPairData": { - "type": "object", - "description": "An asset pair we can trade.", - "properties": { - "pair": { - "type": "string", - "description": "The name of the asset pair.", - "nullable": false - }, - "minimumTradeQty": { - "type": "number", - "description": "The smallest amount we can buy or sell.", - "nullable": false - } - }, - "example": { - "assetBought": "BTC", - "assetSold": "USD", - "minimumTradeQty": 0.0001 - } - } - } - }, - "tags": [ - { - "name": "Custodians", - "description": "Custodian operations" - } - ] -} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index fdeae7c40..643ccc14d 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -3,7 +3,7 @@ "info": { "title": "BTCPay Greenfield API", "version": "v1", - "description": "# Introduction\n\nThe BTCPay Server Greenfield API is a REST API. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n# Authentication\n\nYou can authenticate either via Basic Auth or an API key. It's recommended to use an API key for better security. You can create an API key in the BTCPay Server UI under `Account` -> `Manage Account` -> `API keys`. You can restrict the API key for one or multiple stores and for specific permissions. For testing purposes, you can give it the 'Unrestricted access' permission. On production you should limit the permissions to the actual endpoints you use, you can see the required permission on the API docs at the top of each endpoint under `AUTHORIZATIONS`.\n\nIf you want to simplify the process of creating API keys for your users, you can use the [Authorization endpoint](https:\/\/docs.btcpayserver.org\/API\/Greenfield\/v1\/#tag\/Authorization) to predefine permissions and redirect your users to the BTCPay Server Authorization UI. You can find more information about this on the [API Authorization Flow docs](https:\/\/docs.btcpayserver.org\/BTCPayServer\/greenfield-authorization\/) page.\n\n# Usage examples\n\nUse **Basic Auth** to read store information with cURL:\n```bash\nBTCPAY_INSTANCE=\"https:\/\/mainnet.demo.btcpayserver.org\"\nUSER=\"MyTestUser@gmail.com\"\nPASSWORD=\"notverysecurepassword\"\nPERMISSION=\"btcpay.store.canmodifystoresettings\"\nBODY=\"$(echo \"{}\" | jq --arg \"a\" \"$PERMISSION\" '. + {permissions:[$a]}')\"\n\nAPI_KEY=\"$(curl -s \\\n -H \"Content-Type: application\/json\" \\\n --user \"$USER:$PASSWORD\" \\\n -X POST \\\n -d \"$BODY\" \\\n \"$BTCPAY_INSTANCE\/api\/v1\/api-keys\" | jq -r .apiKey)\"\n```\n\n\nUse an **API key** to read store information with cURL:\n```bash\nSTORE_ID=\"yourStoreId\"\n\ncurl -s \\\n -H \"Content-Type: application\/json\" \\\n -H \"Authorization: token $API_KEY\" \\\n -X GET \\\n \"$BTCPAY_INSTANCE\/api\/v1\/stores\/$STORE_ID\"\n```\n\nYou can find more examples on our docs for different programming languages:\n- [cURL](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample\/)\n- [Javascript\/Node.Js](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-NodeJS\/)\n- [PHP](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-PHP\/)\n\n", + "description": "# Introduction\n\nThe BTCPay Server Greenfield API is a REST API. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n# Authentication\n\nYou can authenticate either via Basic Auth or an API key. It's recommended to use an API key for better security. You can create an API key in the BTCPay Server UI under `Account` -> `Manage Account` -> `API keys`. You can restrict the API key for one or multiple stores and for specific permissions. For testing purposes, you can give it the 'Unrestricted access' permission. On production you should limit the permissions to the actual endpoints you use, you can see the required permission on the API docs at the top of each endpoint under `AUTHORIZATIONS`.\n\nIf you want to simplify the process of creating API keys for your users, you can use the [Authorization endpoint](https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Authorization) to predefine permissions and redirect your users to the BTCPay Server Authorization UI. You can find more information about this on the [API Authorization Flow docs](https://docs.btcpayserver.org/BTCPayServer/greenfield-authorization/) page.\n\n# Usage examples\n\nUse **Basic Auth** to read store information with cURL:\n```bash\nBTCPAY_INSTANCE=\"https://mainnet.demo.btcpayserver.org\"\nUSER=\"MyTestUser@gmail.com\"\nPASSWORD=\"notverysecurepassword\"\nPERMISSION=\"btcpay.store.canmodifystoresettings\"\nBODY=\"$(echo \"{}\" | jq --arg \"a\" \"$PERMISSION\" '. + {permissions:[$a]}')\"\n\nAPI_KEY=\"$(curl -s \\\n -H \"Content-Type: application/json\" \\\n --user \"$USER:$PASSWORD\" \\\n -X POST \\\n -d \"$BODY\" \\\n \"$BTCPAY_INSTANCE/api/v1/api-keys\" | jq -r .apiKey)\"\n```\n\n\nUse an **API key** to read store information with cURL:\n```bash\nSTORE_ID=\"yourStoreId\"\n\ncurl -s \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: token $API_KEY\" \\\n -X GET \\\n \"$BTCPAY_INSTANCE/api/v1/stores/$STORE_ID\"\n```\n\nYou can find more examples on our docs for different programming languages:\n- [cURL](https://docs.btcpayserver.org/Development/GreenFieldExample/)\n- [Javascript/Node.Js](https://docs.btcpayserver.org/Development/GreenFieldExample-NodeJS/)\n- [PHP](https://docs.btcpayserver.org/Development/GreenFieldExample-PHP/)\n\n", "contact": { "name": "BTCPay Server", "url": "https://btcpayserver.org" @@ -128,7 +128,7 @@ "securitySchemes": { "API_Key": { "type": "apiKey", - "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.canviewusers`: View users\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.canviewcustodianaccounts`: View exchange accounts linked to your stores\n* `btcpay.store.canmanagecustodianaccounts`: Manage exchange accounts linked to your stores\n* `btcpay.store.candeposittocustodianaccount`: Deposit funds to exchange accounts linked to your stores\n* `btcpay.store.canwithdrawfromcustodianaccount`: Withdraw funds from exchange accounts to your store\n* `btcpay.store.cantradecustodianaccount`: Trade funds on your store's exchange accounts\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.canviewusers`: View users\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n", "name": "Authorization", "in": "header" }, @@ -145,4 +145,4 @@ "Basic": [] } ] -} +} \ No newline at end of file diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json index 186083b0b..ca90d8c0b 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json @@ -327,10 +327,7 @@ "type": "string" }, "example": [ - "btcpay.store.canmodifystoresettings", - "btcpay.store.cantradecustodianaccount", - "btcpay.store.canwithdrawfromcustodianaccount", - "btcpay.store.candeposittocustodianaccount" + "btcpay.store.canmodifystoresettings" ] }, "isServerRole": {