mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Exchange api no kraken (#3679)
* WIP New APIs for dealing with custodians/exchanges * Simplified things * More API refinements + index.html file for quick viewing * Finishing touches on spec * Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning * Moved draft API docs to "/docs-draft" * WIP baby steps * Added DB migration for CustodianAccountData * Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian * WIP + early Kraken API client * Moved service registration to proper location * Working create + list custodian accounts + permissions + WIP Kraken client * Kraken API Balances call is working * Added asset balances to response * List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed. * Call to get the details of 1 specific custodian account * Added permissions to swagger * Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours * Removed unused file * WIP + Moved files to better locations * Updated docs * Working API endpoint to get info on a trade (same response as creating a new trade) * Working API endpoints for Deposit + Trade + untested Withdraw * Delete custodian account * Trading works, better error handling, cleanup * Working withdrawals + New endpoint for getting bid/ask prices * Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings, * Better error handling when withdrawing to a wrong destination * WithdrawalAddressName in config is now a string per currency (dictionary) * Added TODOs * Only show the custodian account "config" to users who are allowed * Added the new permissions to the API Keys UI * Renamed KrakenClient to KrakenExchange * WIP Kraken Config Form * Removed files for UI again, will make separate PR later * Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere * Updated withdrawal info docs * First unit test * Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes * Mock custodian and more exceptions * Many more tests + cleanup, moved files to better locations * More tests * WIP more tests * Greenfield API tests complete * Added missing "Name" column * Cleanup, TODOs and beginning of Kraken Tests * Added Kraken tests using public endpoints + handling of "SATS" currency * Added 1st mocked Kraken API call: GetAssetBalancesAsync * Added assert for bad config * Mocked more Kraken API responses + added CreationDate to withdrawal response * pr review club changes * Make Kraken Custodian a plugin * Re-added User-Agent header as it is required * Fixed bug in market trade on Kraken using a percentage as qty * A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly. * Merged the draft swagger into the main swagger since it didn't work anymore * Fixed API permissions test * Removed 2 TODOs * Fixed unit test * Remove Kraken Api as it should be separate opt-in plugin * Flatten namespace hierarchy and use InnerExeption instead of OriginalException * Remove useless line * Make sure account is from a specific store * Proper error if custodian code not found * Remove various warnings * Remove various warnings * Handle CustodianApiException through an exception filter * Store custodian-account blob directly * Remove duplications, transform methods into property * Improve docs tags * Make sure the custodianCode saved is canonical * Fix test Co-authored-by: Wouter Samaey <wouter.samaey@storefront.be> Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public class AssetQuoteResult
|
||||||
|
{
|
||||||
|
public string FromAsset { get; }
|
||||||
|
|
||||||
|
public string ToAsset { get; }
|
||||||
|
public decimal Bid { get; }
|
||||||
|
public decimal Ask { get; }
|
||||||
|
|
||||||
|
public AssetQuoteResult(string fromAsset, string toAsset,decimal bid, decimal ask)
|
||||||
|
{
|
||||||
|
this.FromAsset = fromAsset;
|
||||||
|
this.ToAsset = toAsset;
|
||||||
|
this.Bid = bid;
|
||||||
|
this.Ask = ask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public class CustodianFeatureNotImplementedException: CustodianApiException
|
||||||
|
{
|
||||||
|
public CustodianFeatureNotImplementedException(string message) : base(400, "not-implemented", message)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public class DepositsUnavailableException : CustodianApiException
|
||||||
|
{
|
||||||
|
public DepositsUnavailableException(string message) : base(404, "deposits-unavailable", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public class InsufficientFundsException : CustodianApiException
|
||||||
|
{
|
||||||
|
public InsufficientFundsException(string message) : base(400, "insufficient-funds", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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}.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<LedgerEntryData> LedgerEntries { get; }
|
||||||
|
/**
|
||||||
|
* The unique ID of the trade that was executed.
|
||||||
|
*/
|
||||||
|
public string TradeId { get; }
|
||||||
|
|
||||||
|
public MarketTradeResult(string fromAsset, string toAsset, List<LedgerEntryData> ledgerEntries, string tradeId)
|
||||||
|
{
|
||||||
|
this.FromAsset = fromAsset;
|
||||||
|
this.ToAsset = toAsset;
|
||||||
|
this.LedgerEntries = ledgerEntries;
|
||||||
|
this.TradeId = tradeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public class WithdrawResult
|
||||||
|
{
|
||||||
|
public string PaymentMethod { get; }
|
||||||
|
public string Asset { get; set; }
|
||||||
|
public List<LedgerEntryData> 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<LedgerEntryData> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
BTCPayServer.Abstractions/Custodians/ICanDeposit.cs
Normal file
17
BTCPayServer.Abstractions/Custodians/ICanDeposit.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public string[] GetDepositablePaymentMethods();
|
||||||
|
}
|
||||||
32
BTCPayServer.Abstractions/Custodians/ICanTrade.cs
Normal file
32
BTCPayServer.Abstractions/Custodians/ICanTrade.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 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<AssetPairData> GetTradableAssetPairs();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a market order right now.
|
||||||
|
*/
|
||||||
|
public Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the details about a previous market trade.
|
||||||
|
*/
|
||||||
|
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
14
BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs
Normal file
14
BTCPayServer.Abstractions/Custodians/ICanWithdraw.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Abstractions.Custodians;
|
||||||
|
|
||||||
|
public interface ICanWithdraw
|
||||||
|
{
|
||||||
|
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public string[] GetWithdrawablePaymentMethods();
|
||||||
|
}
|
||||||
21
BTCPayServer.Abstractions/Custodians/ICustodian.cs
Normal file
21
BTCPayServer.Abstractions/Custodians/ICustodian.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
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<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
93
BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs
Normal file
93
BTCPayServer.Client/BTCPayServerClient.CustodianAccounts.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
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<IEnumerable<CustodianAccountData>> GetCustodianAccounts(string storeId, bool includeAssetBalances = false, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var queryPayload = new Dictionary<string, object>();
|
||||||
|
if (includeAssetBalances)
|
||||||
|
{
|
||||||
|
queryPayload.Add("assetBalances", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", queryPayload), token);
|
||||||
|
return await HandleResponse<IEnumerable<CustodianAccountData>>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<CustodianAccountResponse> GetCustodianAccount(string storeId, string accountId, bool includeAssetBalances = false, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var queryPayload = new Dictionary<string, object>();
|
||||||
|
if (includeAssetBalances)
|
||||||
|
{
|
||||||
|
queryPayload.Add("assetBalances", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", queryPayload), token);
|
||||||
|
return await HandleResponse<CustodianAccountResponse>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<CustodianAccountData> 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<CustodianAccountData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<CustodianAccountData> 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<CustodianAccountData>(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<DepositAddressData> GetDepositAddress(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<DepositAddressData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<MarketTradeResponseData> TradeMarket(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", bodyPayload: request, method: HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<MarketTradeResponseData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<MarketTradeResponseData> GetTradeInfo(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<MarketTradeResponseData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TradeQuoteResponseData> GetTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var queryPayload = new Dictionary<string, object>();
|
||||||
|
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<TradeQuoteResponseData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<WithdrawalResponseData> CreateWithdrawal(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<WithdrawalResponseData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<WithdrawalResponseData> GetWithdrawalInfo(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<WithdrawalResponseData>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
BTCPayServer.Client/BTCPayServerClient.Custodians.cs
Normal file
16
BTCPayServer.Client/BTCPayServerClient.Custodians.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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<IEnumerable<CustodianData>> GetCustodians(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/custodians"), token);
|
||||||
|
return await HandleResponse<IEnumerable<CustodianData>>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
BTCPayServer.Client/Models/AssetPairData.cs
Normal file
23
BTCPayServer.Client/Models/AssetPairData.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class AssetPairData
|
||||||
|
{
|
||||||
|
|
||||||
|
public AssetPairData()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetPairData(string AssetBought, string AssetSold)
|
||||||
|
{
|
||||||
|
this.AssetBought = AssetBought;
|
||||||
|
this.AssetSold = AssetSold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AssetBought { set; get; }
|
||||||
|
public string AssetSold { set; get; }
|
||||||
|
|
||||||
|
public string ToString()
|
||||||
|
{
|
||||||
|
return AssetBought + "/" + AssetSold;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs
Normal file
12
BTCPayServer.Client/Models/CreateCustodianAccountRequest.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
BTCPayServer.Client/Models/CustodianAccountBaseData.cs
Normal file
16
BTCPayServer.Client/Models/CustodianAccountBaseData.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
BTCPayServer.Client/Models/CustodianAccountData.cs
Normal file
7
BTCPayServer.Client/Models/CustodianAccountData.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class CustodianAccountData : CustodianAccountBaseData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
BTCPayServer.Client/Models/CustodianAccountResponse.cs
Normal file
14
BTCPayServer.Client/Models/CustodianAccountResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class CustodianAccountResponse: CustodianAccountData
|
||||||
|
{
|
||||||
|
public IDictionary<string, decimal> AssetBalances { get; set; }
|
||||||
|
|
||||||
|
public CustodianAccountResponse()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
BTCPayServer.Client/Models/CustodianData.cs
Normal file
11
BTCPayServer.Client/Models/CustodianData.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class CustodianData
|
||||||
|
{
|
||||||
|
public string Code { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string[] TradableAssetPairs { get; set; }
|
||||||
|
public string[] WithdrawablePaymentMethods { get; set; }
|
||||||
|
public string[] DepositablePaymentMethods { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
15
BTCPayServer.Client/Models/DepositAddressData.cs
Normal file
15
BTCPayServer.Client/Models/DepositAddressData.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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; }
|
||||||
|
|
||||||
|
}
|
||||||
27
BTCPayServer.Client/Models/LedgerEntryData.cs
Normal file
27
BTCPayServer.Client/Models/LedgerEntryData.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class LedgerEntryData
|
||||||
|
{
|
||||||
|
public string Asset { get; }
|
||||||
|
public decimal Qty { get; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public LedgerEntryType Type { get; }
|
||||||
|
|
||||||
|
public LedgerEntryData(string asset, decimal qty, LedgerEntryType type)
|
||||||
|
{
|
||||||
|
Asset = asset;
|
||||||
|
Qty = qty;
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LedgerEntryType
|
||||||
|
{
|
||||||
|
Trade = 0,
|
||||||
|
Fee = 1,
|
||||||
|
Withdrawal = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
31
BTCPayServer.Client/Models/MarketTradeResponseData.cs
Normal file
31
BTCPayServer.Client/Models/MarketTradeResponseData.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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<LedgerEntryData> 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<LedgerEntryData> ledgerEntries, string tradeId, string accountId, string custodianCode)
|
||||||
|
{
|
||||||
|
FromAsset = fromAsset;
|
||||||
|
ToAsset = toAsset;
|
||||||
|
LedgerEntries = ledgerEntries;
|
||||||
|
TradeId = tradeId;
|
||||||
|
AccountId = accountId;
|
||||||
|
CustodianCode = custodianCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
BTCPayServer.Client/Models/TradeQuoteResponseData.cs
Normal file
17
BTCPayServer.Client/Models/TradeQuoteResponseData.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class TradeQuoteResponseData
|
||||||
|
{
|
||||||
|
public decimal Bid { get; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
BTCPayServer.Client/Models/TradeRequestData.cs
Normal file
8
BTCPayServer.Client/Models/TradeRequestData.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class TradeRequestData
|
||||||
|
{
|
||||||
|
public string FromAsset { set; get; }
|
||||||
|
public string ToAsset { set; get; }
|
||||||
|
public string Qty { set; get; }
|
||||||
|
}
|
||||||
13
BTCPayServer.Client/Models/WithdrawRequestData.cs
Normal file
13
BTCPayServer.Client/Models/WithdrawRequestData.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class WithdrawRequestData
|
||||||
|
{
|
||||||
|
public string PaymentMethod { set; get; }
|
||||||
|
public decimal Qty { set; get; }
|
||||||
|
|
||||||
|
public WithdrawRequestData(string paymentMethod, decimal qty)
|
||||||
|
{
|
||||||
|
PaymentMethod = paymentMethod;
|
||||||
|
Qty = qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
BTCPayServer.Client/Models/WithdrawalResponseData.cs
Normal file
49
BTCPayServer.Client/Models/WithdrawalResponseData.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class WithdrawalResponseData
|
||||||
|
{
|
||||||
|
public string Asset { get; }
|
||||||
|
public string PaymentMethod { get; }
|
||||||
|
public List<LedgerEntryData> LedgerEntries { get; }
|
||||||
|
public string WithdrawalId { get; }
|
||||||
|
public string AccountId { get; }
|
||||||
|
public string CustodianCode { get; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public WithdrawalStatus Status { get; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedTime { get; }
|
||||||
|
|
||||||
|
public string TransactionId { get; }
|
||||||
|
|
||||||
|
public string TargetAddress { get; }
|
||||||
|
|
||||||
|
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
|
||||||
|
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
|
||||||
|
{
|
||||||
|
PaymentMethod = paymentMethod;
|
||||||
|
Asset = asset;
|
||||||
|
LedgerEntries = ledgerEntries;
|
||||||
|
WithdrawalId = withdrawalId;
|
||||||
|
AccountId = accountId;
|
||||||
|
CustodianCode = custodianCode;
|
||||||
|
TargetAddress = targetAddress;
|
||||||
|
TransactionId = transactionId;
|
||||||
|
Status = status;
|
||||||
|
CreatedTime = createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum WithdrawalStatus
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Queued = 1,
|
||||||
|
Complete = 2,
|
||||||
|
Failed = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,11 @@ namespace BTCPayServer.Client
|
|||||||
public const string CanCreateUser = "btcpay.server.cancreateuser";
|
public const string CanCreateUser = "btcpay.server.cancreateuser";
|
||||||
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
||||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||||
|
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 const string Unrestricted = "unrestricted";
|
||||||
public static IEnumerable<string> AllPolicies
|
public static IEnumerable<string> AllPolicies
|
||||||
{
|
{
|
||||||
@@ -55,6 +60,11 @@ namespace BTCPayServer.Client
|
|||||||
yield return CanUseLightningNodeInStore;
|
yield return CanUseLightningNodeInStore;
|
||||||
yield return CanCreateLightningInvoiceInStore;
|
yield return CanCreateLightningInvoiceInStore;
|
||||||
yield return CanManagePullPayments;
|
yield return CanManagePullPayments;
|
||||||
|
yield return CanViewCustodianAccounts;
|
||||||
|
yield return CanManageCustodianAccounts;
|
||||||
|
yield return CanDepositToCustodianAccounts;
|
||||||
|
yield return CanWithdrawFromCustodianAccounts;
|
||||||
|
yield return CanTradeCustodianAccount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static bool IsValidPolicy(string policy)
|
public static bool IsValidPolicy(string policy)
|
||||||
@@ -184,6 +194,9 @@ namespace BTCPayServer.Client
|
|||||||
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
|
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
|
||||||
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
|
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
|
||||||
case Policies.CanUseInternalLightningNode when this.Policy == Policies.CanModifyServerSettings:
|
case Policies.CanUseInternalLightningNode when this.Policy == Policies.CanModifyServerSettings:
|
||||||
|
case Policies.CanViewCustodianAccounts when this.Policy == Policies.CanManageCustodianAccounts:
|
||||||
|
case Policies.CanViewCustodianAccounts when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
|
case Policies.CanManageCustodianAccounts when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace BTCPayServer.Data
|
|||||||
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
|
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
|
||||||
public DbSet<APIKeyData> ApiKeys { get; set; }
|
public DbSet<APIKeyData> ApiKeys { get; set; }
|
||||||
public DbSet<AppData> Apps { get; set; }
|
public DbSet<AppData> Apps { get; set; }
|
||||||
|
public DbSet<CustodianAccountData> CustodianAccount { get; set; }
|
||||||
public DbSet<StoredFile> Files { get; set; }
|
public DbSet<StoredFile> Files { get; set; }
|
||||||
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices { get; set; }
|
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices { get; set; }
|
||||||
public DbSet<InvoiceEventData> InvoiceEvents { get; set; }
|
public DbSet<InvoiceEventData> InvoiceEvents { get; set; }
|
||||||
@@ -82,6 +83,7 @@ namespace BTCPayServer.Data
|
|||||||
AddressInvoiceData.OnModelCreating(builder);
|
AddressInvoiceData.OnModelCreating(builder);
|
||||||
APIKeyData.OnModelCreating(builder);
|
APIKeyData.OnModelCreating(builder);
|
||||||
AppData.OnModelCreating(builder);
|
AppData.OnModelCreating(builder);
|
||||||
|
CustodianAccountData.OnModelCreating(builder);
|
||||||
//StoredFile.OnModelCreating(builder);
|
//StoredFile.OnModelCreating(builder);
|
||||||
HistoricalAddressInvoiceData.OnModelCreating(builder);
|
HistoricalAddressInvoiceData.OnModelCreating(builder);
|
||||||
InvoiceEventData.OnModelCreating(builder);
|
InvoiceEventData.OnModelCreating(builder);
|
||||||
|
|||||||
41
BTCPayServer.Data/Data/CustodianAccountData.cs
Normal file
41
BTCPayServer.Data/Data/CustodianAccountData.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
|
public class CustodianAccountData
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string CustodianCode { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Blob { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public StoreData StoreData { get; set; }
|
||||||
|
|
||||||
|
internal static void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Entity<CustodianAccountData>()
|
||||||
|
.HasOne(o => o.StoreData)
|
||||||
|
.WithMany(i => i.CustodianAccounts)
|
||||||
|
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.Entity<APIKeyData>()
|
||||||
|
.HasIndex(o => o.StoreId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,5 +46,6 @@ namespace BTCPayServer.Data
|
|||||||
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
|
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
|
||||||
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
|
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
|
||||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||||
|
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
BTCPayServer.Data/Data/TradeResultData.cs
Normal file
6
BTCPayServer.Data/Data/TradeResultData.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
|
public class TradeResultData
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20220115184620_AddCustodianAccountData")]
|
||||||
|
public partial class AddCustodianAccountData : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CustodianAccount",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(maxLength: 50, nullable: false),
|
||||||
|
StoreId = table.Column<string>(maxLength: 50, nullable: false),
|
||||||
|
CustodianCode = table.Column<string>(maxLength: 50, nullable: false),
|
||||||
|
Name = table.Column<string>(maxLength: 50, nullable: true),
|
||||||
|
Blob = table.Column<byte[]>(nullable: true)
|
||||||
|
},
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CustodianAccount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,13 @@ using System.Security.Claims;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Custodians;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
@@ -179,6 +181,7 @@ namespace BTCPayServer.Tests
|
|||||||
.ConfigureServices(services =>
|
.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.TryAddSingleton<IFeeProviderFactory>(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1)));
|
services.TryAddSingleton<IFeeProviderFactory>(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1)));
|
||||||
|
services.AddSingleton<ICustodian, MockCustodian>();
|
||||||
})
|
})
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Custodians;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
@@ -15,6 +18,8 @@ using BTCPayServer.Lightning;
|
|||||||
using BTCPayServer.Models.InvoicingModels;
|
using BTCPayServer.Models.InvoicingModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Custodian.Client;
|
||||||
|
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
@@ -777,6 +782,13 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(code, ex.HttpCode);
|
Assert.Equal(code, ex.HttpCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AssertApiError(int httpStatus, string errorCode, Func<Task> act)
|
||||||
|
{
|
||||||
|
var ex = await Assert.ThrowsAsync<GreenfieldAPIException>(act);
|
||||||
|
Assert.Equal(httpStatus, ex.HttpCode);
|
||||||
|
Assert.Equal(errorCode, ex.APIError.Code);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task UsersControllerTests()
|
public async Task UsersControllerTests()
|
||||||
@@ -2500,5 +2512,474 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CustodiansControllerTests()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
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.Single(custodians);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CustodianAccountControllerTests()
|
||||||
|
{
|
||||||
|
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
|
||||||
|
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<IEnumerable<ICustodian>>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
// Try to fetch 1
|
||||||
|
// Admin
|
||||||
|
var singleAdminCustodianAccount = await adminClient.GetCustodianAccount(storeId, accountId);
|
||||||
|
Assert.NotNull(singleAdminCustodianAccount);
|
||||||
|
Assert.Equal(singleAdminCustodianAccount.CustodianCode, custodian.Code);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
var admin = tester.NewAccount();
|
||||||
|
await admin.GrantAccessAsync(true);
|
||||||
|
|
||||||
|
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||||
|
var authClientNoPermissions = 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<IEnumerable<ICustodian>>();
|
||||||
|
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.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||||
|
|
||||||
|
// Test: GetDepositAddress, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||||
|
|
||||||
|
// Test: GetDepositAddress, wrong payment method
|
||||||
|
await AssertHttpError(400, async () => await depositClient.GetDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||||
|
|
||||||
|
// Test: GetDepositAddress, wrong store ID
|
||||||
|
await AssertHttpError(403, async () => await depositClient.GetDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||||
|
|
||||||
|
// Test: GetDepositAddress, wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await depositClient.GetDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||||
|
|
||||||
|
// Test: GetDepositAddress, correct payment method
|
||||||
|
var depositAddress = await depositClient.GetDepositAddress(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 = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
|
||||||
|
await AssertHttpError(401, async () => await unauthClient.TradeMarket(storeId, accountId, tradeRequest));
|
||||||
|
|
||||||
|
// Test: Trade, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.TradeMarket(storeId, accountId, tradeRequest));
|
||||||
|
|
||||||
|
// Test: Trade, correct permission, correct assets, correct amount
|
||||||
|
var newTradeResult = await tradeClient.TradeMarket(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 = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
|
||||||
|
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.TradeMarket(storeId, accountId, satsTradeRequest));
|
||||||
|
|
||||||
|
// TODO Test: Trade with percentage qty
|
||||||
|
|
||||||
|
// Test: Trade, wrong assets method
|
||||||
|
var wrongAssetsTradeRequest = new TradeRequestData {FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = MockCustodian.TradeQtyBought.ToString(CultureInfo.InvariantCulture)};
|
||||||
|
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.TradeMarket(storeId, accountId, wrongAssetsTradeRequest));
|
||||||
|
|
||||||
|
// Test: wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await tradeClient.TradeMarket(storeId, "WRONG-ACCOUNT-ID", tradeRequest));
|
||||||
|
|
||||||
|
// Test: wrong store ID
|
||||||
|
await AssertHttpError(403, async () => await tradeClient.TradeMarket("WRONG-STORE-ID", accountId, tradeRequest));
|
||||||
|
|
||||||
|
// Test: Trade, correct assets, wrong amount
|
||||||
|
var wrongQtyTradeRequest = new TradeRequestData {FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = "0.01"};
|
||||||
|
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.TradeMarket(storeId, accountId, wrongQtyTradeRequest));
|
||||||
|
|
||||||
|
|
||||||
|
// Test: GetTradeQuote, unauth
|
||||||
|
await AssertHttpError(401, async () => await unauthClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||||
|
|
||||||
|
// Test: GetTradeQuote, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||||
|
|
||||||
|
// Test: GetTradeQuote, auth, correct permission
|
||||||
|
var tradeQuote = await tradeClient.GetTradeQuote(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.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||||
|
|
||||||
|
// Test: GetTradeQuote, wrong asset
|
||||||
|
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||||
|
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset , "WRONG-ASSET"));
|
||||||
|
|
||||||
|
// Test: wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||||
|
|
||||||
|
// Test: wrong store ID
|
||||||
|
await AssertHttpError(403, async () => await tradeClient.GetTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Test: GetTradeInfo, unauth
|
||||||
|
await AssertHttpError(401, async () => await unauthClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||||
|
|
||||||
|
// Test: GetTradeInfo, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||||
|
|
||||||
|
// Test: GetTradeInfo, auth, correct permission
|
||||||
|
var tradeResult = await tradeClient.GetTradeInfo(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.GetTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||||
|
|
||||||
|
// Test: wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||||
|
|
||||||
|
// Test: wrong store ID
|
||||||
|
await AssertHttpError(403, async () => await tradeClient.GetTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||||
|
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, unauth
|
||||||
|
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalAmount );
|
||||||
|
await AssertHttpError(401, async () => await unauthClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, correct payment method, correct amount
|
||||||
|
var withdrawResponse = await withdrawalClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||||
|
AssertMockWithdrawal(withdrawResponse, custodianAccountData);
|
||||||
|
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, wrong payment method
|
||||||
|
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", MockCustodian.WithdrawalAmount );
|
||||||
|
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await withdrawalClient.CreateWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, wrong store ID
|
||||||
|
// TODO it is wierd 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.CreateWithdrawal( "WRONG-STORE-ID",accountId, createWithdrawalRequest));
|
||||||
|
|
||||||
|
// Test: CreateWithdrawal, correct payment method, wrong amount
|
||||||
|
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, new decimal(0.666));
|
||||||
|
await AssertHttpError(400, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||||
|
|
||||||
|
|
||||||
|
// Test: GetWithdrawalInfo, unauth
|
||||||
|
await AssertHttpError(401, async () => await unauthClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||||
|
|
||||||
|
// Test: GetWithdrawalInfo, auth, but wrong permission
|
||||||
|
await AssertHttpError(403, async () => await managerClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||||
|
|
||||||
|
// Test: GetWithdrawalInfo, auth, correct permission
|
||||||
|
var withdrawalInfo = await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||||
|
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
|
||||||
|
|
||||||
|
// Test: GetWithdrawalInfo, wrong withdrawal ID
|
||||||
|
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||||
|
|
||||||
|
// Test: wrong account ID
|
||||||
|
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(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.GetWithdrawalInfo("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(WithdrawalResponseData withdrawResponse, CustodianAccountData account)
|
||||||
|
{
|
||||||
|
Assert.NotNull(withdrawResponse);
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset);
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod);
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawResponse.Status);
|
||||||
|
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);
|
||||||
|
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawResponse.TargetAddress);
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawResponse.TransactionId);
|
||||||
|
Assert.Equal(MockCustodian.WithdrawalId, withdrawResponse.WithdrawalId);
|
||||||
|
Assert.NotEqual(default, withdrawResponse.CreatedTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
161
BTCPayServer.Tests/MockCustodian/MockCustodian.cs
Normal file
161
BTCPayServer.Tests/MockCustodian/MockCustodian.cs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Custodians;
|
||||||
|
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 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<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var r = new Dictionary<string, decimal>()
|
||||||
|
{
|
||||||
|
{ "BTC", BalanceBTC }, { "LTC", BalanceLTC }, { "USD", BalanceUSD }, { "EUR", BalanceEUR },
|
||||||
|
};
|
||||||
|
return Task.FromResult(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<DepositAddressData> 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<AssetPairData> GetTradableAssetPairs()
|
||||||
|
{
|
||||||
|
var r = new List<AssetPairData>();
|
||||||
|
r.Add(new AssetPairData("BTC", "EUR"));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarketTradeResult GetMarketTradeResult()
|
||||||
|
{
|
||||||
|
var ledgerEntries = new List<LedgerEntryData>();
|
||||||
|
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<MarketTradeResult> 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<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (tradeId == TradeId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(GetMarketTradeResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<MarketTradeResult>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AssetQuoteResult> 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<LedgerEntryData>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (paymentMethod == WithdrawalPaymentMethod)
|
||||||
|
{
|
||||||
|
if (amount == WithdrawalAmount)
|
||||||
|
{
|
||||||
|
return Task.FromResult(CreateWithdrawResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (withdrawalId == WithdrawalId && WithdrawalPaymentMethod.Equals(paymentMethod))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CreateWithdrawResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<WithdrawResult>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetWithdrawablePaymentMethods()
|
||||||
|
{
|
||||||
|
return GetDepositablePaymentMethods();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
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.Extensions;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
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 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]
|
||||||
|
public class GreenfieldCustodianAccountController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||||
|
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
|
||||||
|
public GreenfieldCustodianAccountController(CustodianAccountRepository custodianAccountRepository,
|
||||||
|
IEnumerable<ICustodian> 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<IActionResult> 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<IActionResult> ViewCustodianAccount(string storeId, string accountId,
|
||||||
|
[FromQuery] bool assetBalances = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var custodianAccountData = await GetCustodian(storeId, accountId);
|
||||||
|
var custodianAccount = await ToModel(custodianAccountData, assetBalances, cancellationToken);
|
||||||
|
return Ok(custodianAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CanSeeCustodianAccountConfig()
|
||||||
|
{
|
||||||
|
return (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanManageCustodianAccounts))).Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CustodianAccountDataClient> 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<IActionResult> 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<IActionResult> UpdateCustodianAccount(string storeId, string accountId,
|
||||||
|
CreateCustodianAccountRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
request ??= new CreateCustodianAccountRequest();
|
||||||
|
|
||||||
|
var custodianAccount = await GetCustodian(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<IActionResult> 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<IActionResult> GetDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var custodianAccount = await GetCustodian(storeId, accountId);
|
||||||
|
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||||
|
var config = custodianAccount.GetBlob();
|
||||||
|
|
||||||
|
if (custodian is ICanDeposit depositableCustodian)
|
||||||
|
{
|
||||||
|
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<IActionResult> TradeMarket(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 GetCustodian(storeId, accountId);
|
||||||
|
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||||
|
|
||||||
|
if (custodian is ICanTrade tradableCustodian)
|
||||||
|
{
|
||||||
|
decimal Qty;
|
||||||
|
if (request.Qty.EndsWith("%", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
// Qty is a percentage of current holdings
|
||||||
|
var percentage = Decimal.Parse( request.Qty.Substring(0, request.Qty.Length - 1), CultureInfo.InvariantCulture);
|
||||||
|
var config = custodianAccount.GetBlob();
|
||||||
|
var balances = custodian.GetAssetBalancesAsync(config, cancellationToken).Result;
|
||||||
|
var fromAssetBalance = balances[request.FromAsset];
|
||||||
|
var priceQuote =
|
||||||
|
await tradableCustodian.GetQuoteForAssetAsync(request.FromAsset, request.ToAsset, config, cancellationToken);
|
||||||
|
Qty = fromAssetBalance / priceQuote.Ask * percentage / 100;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Qty is an exact amount
|
||||||
|
Qty = Decimal.Parse(request.Qty, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await tradableCustodian.TradeMarketAsync(request.FromAsset, request.ToAsset, Qty,
|
||||||
|
custodianAccount.GetBlob(), cancellationToken);
|
||||||
|
|
||||||
|
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IActionResult> 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 GetCustodian(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<IActionResult> GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var custodianAccount = await GetCustodian(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 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")]
|
||||||
|
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||||
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
public async Task<IActionResult> CreateWithdrawal(string storeId, string accountId,
|
||||||
|
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var custodianAccount = await GetCustodian(storeId, accountId);
|
||||||
|
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||||
|
|
||||||
|
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||||
|
{
|
||||||
|
var withdrawResult =
|
||||||
|
await withdrawableCustodian.WithdrawAsync(request.PaymentMethod, request.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}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async Task<CustodianAccountData> GetCustodian(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<IActionResult> GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var custodianAccount = await GetCustodian(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}\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Custodians;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers.Greenfield
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
|
||||||
|
[EnableCors(CorsPolicies.All)]
|
||||||
|
public class GreenfieldCustodianController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||||
|
|
||||||
|
public GreenfieldCustodianController(IEnumerable<ICustodian> 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 tradableAssetPairStrings = new string[tradableAssetPairs.Count];
|
||||||
|
for (int i = 0; i < tradableAssetPairs.Count; i++)
|
||||||
|
{
|
||||||
|
tradableAssetPairStrings[i] = tradableAssetPairs[i].ToString();
|
||||||
|
}
|
||||||
|
result.TradableAssetPairs = tradableAssetPairStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custodian is ICanDeposit depositableCustodian)
|
||||||
|
{
|
||||||
|
result.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||||
|
}
|
||||||
|
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||||
|
{
|
||||||
|
result.WithdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Fido2;
|
using BTCPayServer.Fido2;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Crypto;
|
using NBitcoin.Crypto;
|
||||||
|
|
||||||
@@ -27,10 +28,12 @@ namespace BTCPayServer
|
|||||||
public readonly ConcurrentDictionary<string, byte[]> FinalLoginStore =
|
public readonly ConcurrentDictionary<string, byte[]> FinalLoginStore =
|
||||||
new ConcurrentDictionary<string, byte[]>();
|
new ConcurrentDictionary<string, byte[]>();
|
||||||
private readonly ApplicationDbContextFactory _contextFactory;
|
private readonly ApplicationDbContextFactory _contextFactory;
|
||||||
|
private readonly ILogger<LnurlAuthService> _logger;
|
||||||
|
|
||||||
public LnurlAuthService(ApplicationDbContextFactory contextFactory)
|
public LnurlAuthService(ApplicationDbContextFactory contextFactory, ILogger<LnurlAuthService> logger)
|
||||||
{
|
{
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> RequestCreation(string userId)
|
public async Task<byte[]> RequestCreation(string userId)
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ namespace BTCPayServer.Controllers
|
|||||||
.ToArray());
|
.ToArray());
|
||||||
return Json(json);
|
return Json(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("docs")]
|
[Route("docs")]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public IActionResult SwaggerDocs()
|
public IActionResult SwaggerDocs()
|
||||||
|
|||||||
@@ -510,6 +510,16 @@ namespace BTCPayServer.Controllers
|
|||||||
{BTCPayServer.Client.Policies.CanDeleteUser, ("Delete user", "The app will be able to delete the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
{BTCPayServer.Client.Policies.CanDeleteUser, ("Delete user", "The app will be able to delete the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||||
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to manage invoices on all your stores and modify their settings.")},
|
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to manage invoices on all your stores and modify their settings.")},
|
||||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to manage invoices on the selected stores and modify their settings.")},
|
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to manage invoices on the selected stores and modify their settings.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "The app will be able to see exchange accounts linked to your stores.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "The app will be able to see exchange accounts linked to the selected stores.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "The app will be able to modify exchange accounts linked to your stores.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "The app will be able to modify exchange accounts linked to selected stores.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "The app will be able to deposit funds to your exchange accounts.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "The app will be able to deposit funds to selected store's exchange accounts.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "The app will be able to withdraw funds from your exchange accounts to your store.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "The app will be able to withdraw funds from your selected store's exchange accounts.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "The app will be able to trade funds on your store's exchange accounts.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "The app will be able to trade funds on selected store's exchange accounts.")},
|
||||||
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
|
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
|
||||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
|
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
|
||||||
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
||||||
|
|||||||
25
BTCPayServer/Data/CustodianAccountDataExtensions.cs
Normal file
25
BTCPayServer/Data/CustodianAccountDataExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
|
public static class CustodianAccountDataExtensions
|
||||||
|
{
|
||||||
|
public static JObject GetBlob(this CustodianAccountData custodianAccountData)
|
||||||
|
{
|
||||||
|
var result = custodianAccountData.Blob == null
|
||||||
|
? new JObject()
|
||||||
|
: JObject.Parse(ZipUtils.Unzip(custodianAccountData.Blob));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SetBlob(this CustodianAccountData custodianAccountData, JObject blob)
|
||||||
|
{
|
||||||
|
var original = custodianAccountData.GetBlob();
|
||||||
|
if (JToken.DeepEquals(original, blob))
|
||||||
|
return false;
|
||||||
|
custodianAccountData.Blob = blob is null ? null : ZipUtils.Zip(blob.ToString(Newtonsoft.Json.Formatting.None));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Custodians;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Abstractions.Services;
|
using BTCPayServer.Abstractions.Services;
|
||||||
@@ -28,6 +29,7 @@ using BTCPayServer.Security.Bitpay;
|
|||||||
using BTCPayServer.Security.Greenfield;
|
using BTCPayServer.Security.Greenfield;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Custodian.Client;
|
||||||
using BTCPayServer.Services.Fees;
|
using BTCPayServer.Services.Fees;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Labels;
|
using BTCPayServer.Services.Labels;
|
||||||
@@ -118,7 +120,11 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<EventAggregator>();
|
services.TryAddSingleton<EventAggregator>();
|
||||||
services.TryAddSingleton<PaymentRequestService>();
|
services.TryAddSingleton<PaymentRequestService>();
|
||||||
services.TryAddSingleton<UserService>();
|
services.TryAddSingleton<UserService>();
|
||||||
|
services.AddSingleton<CustodianAccountRepository>();
|
||||||
|
|
||||||
|
|
||||||
services.TryAddSingleton<WalletHistogramService>();
|
services.TryAddSingleton<WalletHistogramService>();
|
||||||
|
services.TryAddSingleton<CustodianAccountRepository>();
|
||||||
services.AddSingleton<ApplicationDbContextFactory>();
|
services.AddSingleton<ApplicationDbContextFactory>();
|
||||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||||
(options) =>
|
(options) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|||||||
@@ -280,7 +280,9 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
process.Start();
|
process.Start();
|
||||||
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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<CustodianAccountData> 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<bool> 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<CustodianAccountData[]> FindByStoreId(string storeId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (storeId is null)
|
||||||
|
throw new ArgumentNullException(nameof(storeId));
|
||||||
|
await using var context = _contextFactory.CreateContext();
|
||||||
|
IQueryable<CustodianAccountData> query = context.CustodianAccount
|
||||||
|
.Where(ca => ca.StoreId == storeId);
|
||||||
|
//.SelectMany(c => c.StoreData.Invoices);
|
||||||
|
|
||||||
|
var data = await query.ToArrayAsync( cancellationToken).ConfigureAwait(false);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CustodianAccountData> FindById(string storeId, string accountId)
|
||||||
|
{
|
||||||
|
await using var context = _contextFactory.CreateContext();
|
||||||
|
IQueryable<CustodianAccountData> query = context.CustodianAccount
|
||||||
|
.Where(ca => ca.StoreId == storeId && ca.Id == accountId);
|
||||||
|
|
||||||
|
var custodianAccountData = (await query.ToListAsync()).FirstOrDefault();
|
||||||
|
return custodianAccountData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
BTCPayServer/Views/UIHome/SwaggerDraftDocs.cshtml
Normal file
33
BTCPayServer/Views/UIHome/SwaggerDraftDocs.cshtml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@inject BTCPayServer.Security.ContentSecurityPolicies csp
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
csp.Add("script-src", "https://cdn.jsdelivr.net");
|
||||||
|
csp.Add("worker-src", "blob:");
|
||||||
|
}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>DRAFT: BTCPay Server Greenfield API</title>
|
||||||
|
<!-- needed for adaptive design -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="~/main/fonts/Roboto.css" rel="stylesheet" asp-append-version="true">
|
||||||
|
<link href="~/main/fonts/Montserrat.css" rel="stylesheet" asp-append-version="true">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ReDoc doesn't change outer page styles
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
@*Ignore this, this is for making the test ClickOnAllSideMenus happy*@
|
||||||
|
<div class="navbar-brand" style="visibility:collapse;"></div>
|
||||||
|
<redoc spec-url="@Url.ActionLink("SwaggerDraft")"></redoc>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.54/bundles/redoc.standalone.js" integrity="sha384-pxWFJkxrlfignEDb+sJ8XrdnJQ+V2bsiRqgPnfmOk1i3KKSubbydbolVZJeKisNY" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1040
BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json
Normal file
1040
BTCPayServer/wwwroot/swagger/v1/swagger.template.custodians.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@
|
|||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
"API_Key": {
|
"API_Key": {
|
||||||
"type": "apiKey",
|
"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.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the 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.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.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices 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.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the 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.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.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices 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",
|
"name": "Authorization",
|
||||||
"in": "header"
|
"in": "header"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user