mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 09:19:24 +01:00
Remove custodians (#5863)
* Remove custodians * Hide Experimental checkbox in the server policies --------- Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
5b31d4de20
commit
cb25c225e9
79 changed files with 68 additions and 6248 deletions
|
@ -1,19 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
public class AssetQuoteResult
|
||||
{
|
||||
public string FromAsset { get; set; }
|
||||
public string ToAsset { get; set; }
|
||||
public decimal Bid { get; set; }
|
||||
public decimal Ask { get; set; }
|
||||
|
||||
public AssetQuoteResult() { }
|
||||
|
||||
public AssetQuoteResult(string fromAsset, string toAsset, decimal bid, decimal ask)
|
||||
{
|
||||
FromAsset = fromAsset;
|
||||
ToAsset = toAsset;
|
||||
Bid = bid;
|
||||
Ask = ask;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class AssetBalancesUnavailableException : CustodianApiException
|
||||
{
|
||||
public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e)
|
||||
{
|
||||
}
|
||||
|
||||
public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class AssetQuoteUnavailableException : CustodianApiException
|
||||
{
|
||||
public AssetPairData AssetPair { get; }
|
||||
|
||||
public AssetQuoteUnavailableException(AssetPairData assetPair) : base(400, "asset-price-unavailable", "Cannot find a quote for pair " + assetPair)
|
||||
{
|
||||
this.AssetPair = assetPair;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class BadConfigException : CustodianApiException
|
||||
{
|
||||
public string[] BadConfigKeys { get; set; }
|
||||
|
||||
public BadConfigException(string[] badConfigKeys) : base(500, "bad-custodian-account-config", "Wrong config values: " + String.Join(", ", badConfigKeys))
|
||||
{
|
||||
this.BadConfigKeys = badConfigKeys;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class CannotWithdrawException : CustodianApiException
|
||||
|
||||
{
|
||||
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string message) : base(403, "cannot-withdraw", message)
|
||||
{
|
||||
}
|
||||
|
||||
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "cannot-withdraw", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
using System;
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
public class CustodianApiException : Exception
|
||||
{
|
||||
public int HttpStatus { get; }
|
||||
public string Code { get; }
|
||||
|
||||
public CustodianApiException(int httpStatus, string code, string message, System.Exception ex) : base(message, ex)
|
||||
{
|
||||
HttpStatus = httpStatus;
|
||||
Code = code;
|
||||
}
|
||||
|
||||
public CustodianApiException(int httpStatus, string code, string message) : this(httpStatus, code, message, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class CustodianFeatureNotImplementedException : CustodianApiException
|
||||
{
|
||||
public CustodianFeatureNotImplementedException(string message) : base(400, "not-implemented", message)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class DepositsUnavailableException : CustodianApiException
|
||||
{
|
||||
public DepositsUnavailableException(string message) : base(404, "deposits-unavailable", message)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class InsufficientFundsException : CustodianApiException
|
||||
{
|
||||
public InsufficientFundsException(string message) : base(400, "insufficient-funds", message)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class InvalidWithdrawalTargetException : CustodianApiException
|
||||
|
||||
{
|
||||
public InvalidWithdrawalTargetException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "invalid-withdrawal-target", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class PermissionDeniedCustodianApiException : CustodianApiException
|
||||
|
||||
{
|
||||
public PermissionDeniedCustodianApiException(ICustodian custodian) : base(403, "custodian-api-permission-denied", $"{custodian.Name}'s API reported that you don't have permission.")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class TradeNotFoundException : CustodianApiException
|
||||
{
|
||||
private string tradeId { get; }
|
||||
|
||||
public TradeNotFoundException(string tradeId) : base(404, "trade-not-found", "Could not find trade ID " + tradeId)
|
||||
{
|
||||
this.tradeId = tradeId;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class WithdrawalNotFoundException : CustodianApiException
|
||||
{
|
||||
private string WithdrawalId { get; }
|
||||
|
||||
public WithdrawalNotFoundException(string withdrawalId) : base(404, "withdrawal-not-found", $"Could not find withdrawal ID {withdrawalId}.")
|
||||
{
|
||||
WithdrawalId = withdrawalId;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public class WrongTradingPairException : CustodianApiException
|
||||
{
|
||||
public const int HttpCode = 404;
|
||||
public WrongTradingPairException(string fromAsset, string toAsset) : base(HttpCode, "wrong-trading-pair", $"Cannot find a trading pair for converting {fromAsset} into {toAsset}.")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
/**
|
||||
* The result of a market trade. Used as a return type for custodians implementing ICanTrade
|
||||
*/
|
||||
public class MarketTradeResult
|
||||
{
|
||||
public string FromAsset { get; }
|
||||
public string ToAsset { get; }
|
||||
/**
|
||||
* The ledger entries that show the balances that were affected by the trade.
|
||||
*/
|
||||
public List<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;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
public class SimulateWithdrawalResult
|
||||
{
|
||||
public string PaymentMethod { get; }
|
||||
public string Asset { get; }
|
||||
public decimal MinQty { get; }
|
||||
public decimal MaxQty { get; }
|
||||
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
|
||||
// Fee can be NULL if unknown.
|
||||
public decimal? Fee { get; }
|
||||
|
||||
public SimulateWithdrawalResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries,
|
||||
decimal minQty, decimal maxQty)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
MinQty = minQty;
|
||||
MaxQty = maxQty;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
public class WithdrawResult
|
||||
{
|
||||
public string PaymentMethod { get; }
|
||||
public string Asset { get; set; }
|
||||
public List<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;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public interface ICanDeposit
|
||||
{
|
||||
/**
|
||||
* Get the address where we can deposit for the chosen payment method (crypto code + network).
|
||||
* The result can be a string in different formats like a bitcoin address or even a LN invoice.
|
||||
*/
|
||||
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public string[] GetDepositablePaymentMethods();
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public interface ICanTrade
|
||||
{
|
||||
/**
|
||||
* A list of tradable asset pairs, or NULL if the custodian cannot trade/convert assets. if thr asset pair contains fiat, fiat is always put last. If both assets are a cyrptocode or both are fiat, the pair is written alphabetically. Always in uppercase. Example: ["BTC/EUR","BTC/USD", "EUR/USD", "BTC/ETH",...]
|
||||
*/
|
||||
public List<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);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for custodians that can move funds to the store wallet.
|
||||
/// </summary>
|
||||
public interface ICanWithdraw
|
||||
{
|
||||
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public string[] GetWithdrawablePaymentMethods();
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
public interface ICustodian
|
||||
{
|
||||
/**
|
||||
* Get the unique code that identifies this custodian.
|
||||
*/
|
||||
string Code { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
/**
|
||||
* Get a list of assets and their qty in custody.
|
||||
*/
|
||||
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<Form.Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class CustodianExtensions
|
||||
{
|
||||
public static ICustodian? GetCustodianByCode(this IEnumerable<ICustodian> custodians, string code)
|
||||
{
|
||||
return custodians.FirstOrDefault(custodian => custodian.Code == code);
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<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> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token);
|
||||
return await HandleResponse<DepositAddressData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
|
||||
{
|
||||
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
|
||||
//return await HandleResponse<ApplicationUserData>(response);
|
||||
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
|
||||
request, HttpMethod.Post);
|
||||
var response = await _httpClient.SendAsync(internalRequest, token);
|
||||
return await HandleResponse<MarketTradeResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token);
|
||||
return await HandleResponse<MarketTradeResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<TradeQuoteResponseData> GetCustodianAccountTradeQuote(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> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token);
|
||||
return await HandleResponse<WithdrawalResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token);
|
||||
return await HandleResponse<WithdrawalSimulationResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<WithdrawalResponseData> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token);
|
||||
return await HandleResponse<WithdrawalResponseData>(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<CustodianData>> GetCustodians(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/custodians"), token);
|
||||
return await HandleResponse<IEnumerable<CustodianData>>(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
|
||||
{
|
||||
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Float:
|
||||
case JTokenType.Integer:
|
||||
case JTokenType.String:
|
||||
if (TradeQuantity.TryParse(token.ToString(), out var q))
|
||||
return q;
|
||||
break;
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
}
|
||||
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateCustodianAccountRequest
|
||||
{
|
||||
public string CustodianCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public JObject Config { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public abstract class CustodianAccountBaseData
|
||||
{
|
||||
public string CustodianCode { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public JObject Config { get; set; }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CustodianAccountData : CustodianAccountBaseData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class CustodianAccountResponse : CustodianAccountData
|
||||
{
|
||||
public IDictionary<string, decimal> AssetBalances { get; set; }
|
||||
|
||||
public CustodianAccountResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class CustodianData
|
||||
{
|
||||
public string Code { get; set; }
|
||||
public string Name { get; set; }
|
||||
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
|
||||
public string[] WithdrawablePaymentMethods { get; set; }
|
||||
public string[] DepositablePaymentMethods { get; set; }
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class DepositAddressData
|
||||
{
|
||||
// /**
|
||||
// * Example: P2PKH, P2SH, P2WPKH, P2TR, BOLT11, ...
|
||||
// */
|
||||
// public string Type { get; set; }
|
||||
|
||||
/**
|
||||
* Format depends hugely on the type.
|
||||
*/
|
||||
public string Address { get; set; }
|
||||
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class MarketTradeResponseData
|
||||
{
|
||||
public string FromAsset { get; }
|
||||
public string ToAsset { get; }
|
||||
/**
|
||||
* The ledger entries that show the balances that were affected by the trade.
|
||||
*/
|
||||
public List<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;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class TradeQuoteResponseData
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Bid { get; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Ask { get; }
|
||||
public string ToAsset { get; }
|
||||
public string FromAsset { get; }
|
||||
|
||||
public TradeQuoteResponseData(string fromAsset, string toAsset, decimal bid, decimal ask)
|
||||
{
|
||||
FromAsset = fromAsset;
|
||||
ToAsset = toAsset;
|
||||
Bid = bid;
|
||||
Ask = ask;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class TradeRequestData
|
||||
{
|
||||
public string FromAsset { set; get; }
|
||||
public string ToAsset { set; get; }
|
||||
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
|
||||
public TradeQuantity Qty { set; get; }
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawRequestData
|
||||
{
|
||||
public string PaymentMethod { set; get; }
|
||||
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
|
||||
public TradeQuantity Qty { set; get; }
|
||||
|
||||
public WithdrawRequestData()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Qty = qty;
|
||||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public record TradeQuantity
|
||||
{
|
||||
public TradeQuantity(decimal value, ValueType type)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public enum ValueType
|
||||
{
|
||||
Exact,
|
||||
Percent
|
||||
}
|
||||
|
||||
public ValueType Type { get; }
|
||||
public decimal Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Type == ValueType.Exact)
|
||||
return Value.ToString(CultureInfo.InvariantCulture);
|
||||
else
|
||||
return Value.ToString(CultureInfo.InvariantCulture) + "%";
|
||||
}
|
||||
public static TradeQuantity Parse(string str)
|
||||
{
|
||||
if (!TryParse(str, out var r))
|
||||
throw new FormatException("Invalid TradeQuantity");
|
||||
return r;
|
||||
}
|
||||
public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity)
|
||||
{
|
||||
if (str is null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
quantity = null;
|
||||
str = str.Trim();
|
||||
str = str.Replace(" ", "");
|
||||
if (str.Length == 0)
|
||||
return false;
|
||||
if (str[^1] == '%')
|
||||
{
|
||||
if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
|
||||
return false;
|
||||
if (r < 0.0m)
|
||||
return false;
|
||||
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
|
||||
return false;
|
||||
if (r < 0.0m)
|
||||
return false;
|
||||
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public abstract class WithdrawalBaseResponseData
|
||||
{
|
||||
public string Asset { get; }
|
||||
public string PaymentMethod { get; }
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
public string AccountId { get; }
|
||||
public string CustodianCode { get; }
|
||||
|
||||
public WithdrawalBaseResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string accountId,
|
||||
string custodianCode)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
AccountId = accountId;
|
||||
CustodianCode = custodianCode;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawalResponseData : WithdrawalBaseResponseData
|
||||
{
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public WithdrawalStatus Status { get; }
|
||||
|
||||
public string WithdrawalId { get; }
|
||||
public DateTimeOffset CreatedTime { get; }
|
||||
|
||||
public string TransactionId { get; }
|
||||
|
||||
public string TargetAddress { get; }
|
||||
|
||||
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
|
||||
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
|
||||
custodianCode)
|
||||
{
|
||||
WithdrawalId = withdrawalId;
|
||||
TargetAddress = targetAddress;
|
||||
TransactionId = transactionId;
|
||||
Status = status;
|
||||
CreatedTime = createdTime;
|
||||
}
|
||||
|
||||
|
||||
public enum WithdrawalStatus
|
||||
{
|
||||
Unknown = 0,
|
||||
Queued = 1,
|
||||
Complete = 2,
|
||||
Failed = 3
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? MinQty { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? MaxQty { get; set; }
|
||||
|
||||
public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId,
|
||||
string custodianCode, List<LedgerEntryData> ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod,
|
||||
asset, ledgerEntries, accountId, custodianCode)
|
||||
{
|
||||
MinQty = minQty;
|
||||
MaxQty = maxQty;
|
||||
}
|
||||
}
|
|
@ -40,11 +40,6 @@ namespace BTCPayServer.Client
|
|||
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
|
||||
public const string CanViewPullPayments = "btcpay.store.canviewpullpayments";
|
||||
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
|
||||
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
|
||||
public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts";
|
||||
public const string CanDepositToCustodianAccounts = "btcpay.store.candeposittocustodianaccount";
|
||||
public const string CanWithdrawFromCustodianAccounts = "btcpay.store.canwithdrawfromcustodianaccount";
|
||||
public const string CanTradeCustodianAccount = "btcpay.store.cantradecustodianaccount";
|
||||
public const string Unrestricted = "unrestricted";
|
||||
public static IEnumerable<string> AllPolicies
|
||||
{
|
||||
|
@ -79,11 +74,6 @@ namespace BTCPayServer.Client
|
|||
yield return CanCreatePullPayments;
|
||||
yield return CanViewPullPayments;
|
||||
yield return CanCreateNonApprovedPullPayments;
|
||||
yield return CanViewCustodianAccounts;
|
||||
yield return CanManageCustodianAccounts;
|
||||
yield return CanDepositToCustodianAccounts;
|
||||
yield return CanWithdrawFromCustodianAccounts;
|
||||
yield return CanTradeCustodianAccount;
|
||||
yield return CanManageUsers;
|
||||
yield return CanManagePayouts;
|
||||
yield return CanViewPayouts;
|
||||
|
@ -254,7 +244,6 @@ namespace BTCPayServer.Client
|
|||
{
|
||||
var policyMap = new Dictionary<string, HashSet<string>>();
|
||||
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
|
||||
Policies.CanManageCustodianAccounts,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewStoreSettings,
|
||||
|
@ -275,7 +264,6 @@ namespace BTCPayServer.Client
|
|||
Policies.CanUseInternalLightningNode,
|
||||
Policies.CanManageUsers);
|
||||
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts);
|
||||
PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts);
|
||||
|
|
|
@ -39,7 +39,6 @@ namespace BTCPayServer.Data
|
|||
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
|
||||
public DbSet<APIKeyData> ApiKeys { get; set; }
|
||||
public DbSet<AppData> Apps { get; set; }
|
||||
public DbSet<CustodianAccountData> CustodianAccount { get; set; }
|
||||
public DbSet<StoredFile> Files { get; set; }
|
||||
public DbSet<InvoiceEventData> InvoiceEvents { get; set; }
|
||||
public DbSet<InvoiceSearchData> InvoiceSearches { get; set; }
|
||||
|
@ -94,7 +93,6 @@ namespace BTCPayServer.Data
|
|||
AddressInvoiceData.OnModelCreating(builder);
|
||||
APIKeyData.OnModelCreating(builder, Database);
|
||||
AppData.OnModelCreating(builder, Database);
|
||||
CustodianAccountData.OnModelCreating(builder, Database);
|
||||
//StoredFile.OnModelCreating(builder);
|
||||
InvoiceEventData.OnModelCreating(builder);
|
||||
InvoiceSearchData.OnModelCreating(builder);
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
||||
public class CustodianAccountData : IHasBlob<JObject>
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string StoreId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string CustodianCode { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
[JsonIgnore]
|
||||
public string Blob2 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<CustodianAccountData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.CustodianAccounts)
|
||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<CustodianAccountData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<CustodianAccountData>()
|
||||
.Property(o => o.Blob2)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,7 +44,6 @@ namespace BTCPayServer.Data
|
|||
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
|
||||
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
|
||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
public IEnumerable<FormData> Forms { get; set; }
|
||||
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240325095923_RemoveCustodian")]
|
||||
public partial class RemoveCustodian : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustodianAccount");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustodianAccount",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
StoreId = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
Blob2 = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CustodianCode = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CustodianAccount", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustodianAccount_Stores_StoreId",
|
||||
column: x => x.StoreId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustodianAccount_StoreId",
|
||||
table: "CustodianAccount",
|
||||
column: "StoreId");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -189,40 +189,6 @@ namespace BTCPayServer.Migrations
|
|||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustodianCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("CustodianAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
@ -598,7 +564,7 @@ namespace BTCPayServer.Migrations
|
|||
.HasMaxLength(30)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
|
@ -612,7 +578,7 @@ namespace BTCPayServer.Migrations
|
|||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Proof")
|
||||
b.Property<string>("Proof")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PullPaymentDataId")
|
||||
|
@ -703,7 +669,7 @@ namespace BTCPayServer.Migrations
|
|||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
|
@ -1219,17 +1185,6 @@ namespace BTCPayServer.Migrations
|
|||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("CustodianAccounts")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
|
@ -1638,8 +1593,6 @@ namespace BTCPayServer.Migrations
|
|||
|
||||
b.Navigation("Apps");
|
||||
|
||||
b.Navigation("CustodianAccounts");
|
||||
|
||||
b.Navigation("Forms");
|
||||
|
||||
b.Navigation("Invoices");
|
||||
|
|
|
@ -7,13 +7,11 @@ using System.Security.Claims;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
@ -193,7 +191,6 @@ namespace BTCPayServer.Tests
|
|||
.ConfigureServices(services =>
|
||||
{
|
||||
services.TryAddSingleton<IFeeProviderFactory>(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1)));
|
||||
services.AddSingleton<ICustodian, MockCustodian>();
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
|
|
|
@ -152,13 +152,6 @@ namespace BTCPayServer.Tests
|
|||
CanParseDecimalsCore("{\"qty\": \"1.0\"}", 1.0m);
|
||||
CanParseDecimalsCore("{\"qty\": 6.1e-7}", 6.1e-7m);
|
||||
CanParseDecimalsCore("{\"qty\": \"6.1e-7\"}", 6.1e-7m);
|
||||
|
||||
var data = JsonConvert.DeserializeObject<TradeRequestData>("{\"qty\": \"6.1e-7\", \"fromAsset\":\"Test\"}");
|
||||
Assert.Equal(6.1e-7m, data.Qty.Value);
|
||||
Assert.Equal("Test", data.FromAsset);
|
||||
data = JsonConvert.DeserializeObject<TradeRequestData>("{\"fromAsset\":\"Test\", \"qty\": \"6.1e-7\"}");
|
||||
Assert.Equal(6.1e-7m, data.Qty.Value);
|
||||
Assert.Equal("Test", data.FromAsset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -202,8 +195,6 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
|
||||
Assert.Equal(expected, d.Qty);
|
||||
var d2 = JsonConvert.DeserializeObject<TradeRequestData>(str);
|
||||
Assert.Equal(new TradeQuantity(expected, TradeQuantity.ValueType.Exact), d2.Qty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -892,33 +883,6 @@ namespace BTCPayServer.Tests
|
|||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTradeQuantity()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.2345o"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("o"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse(""));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353%%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353 %%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353%"));
|
||||
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353"));
|
||||
|
||||
var qty = TradeQuantity.Parse("1.3%");
|
||||
Assert.Equal(1.3m, qty.Value);
|
||||
Assert.Equal(TradeQuantity.ValueType.Percent, qty.Type);
|
||||
var qty2 = TradeQuantity.Parse("1.3");
|
||||
Assert.Equal(1.3m, qty2.Value);
|
||||
Assert.Equal(TradeQuantity.ValueType.Exact, qty2.Type);
|
||||
Assert.NotEqual(qty, qty2);
|
||||
Assert.Equal(qty, TradeQuantity.Parse("1.3%"));
|
||||
Assert.Equal(qty2, TradeQuantity.Parse("1.3"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty.ToString()), TradeQuantity.Parse("1.3%"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse("1.3"));
|
||||
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
|
||||
}
|
||||
|
||||
|
||||
public static WalletFileParsers GetParsers()
|
||||
{
|
||||
var service = new ServiceCollection();
|
||||
|
|
|
@ -6,7 +6,6 @@ using System.Security.Cryptography;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
|
@ -18,7 +17,6 @@ using BTCPayServer.Payments;
|
|||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
@ -2317,7 +2315,7 @@ namespace BTCPayServer.Tests
|
|||
newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
|
||||
const string newOrderId = "UPDATED-ORDER-ID";
|
||||
JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}}");
|
||||
JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", \"newstuff\": [1,2,3,4,5]}}");
|
||||
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
|
||||
Assert.DoesNotContain(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
|
||||
await AssertHttpError(403, async () =>
|
||||
|
@ -4202,28 +4200,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Single(oneTestWithoutData);
|
||||
Assert.Null(oneTestWithoutData.First().Links.Select(n => n.ObjectData).FirstOrDefault());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodiansControllerTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
await tester.PayTester.EnableExperimental();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodians());
|
||||
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
var clientBasic = await user.CreateClient();
|
||||
var custodians = await clientBasic.GetCustodians();
|
||||
Assert.NotNull(custodians);
|
||||
Assert.NotEmpty(custodians);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreRateConfigTests()
|
||||
|
@ -4286,500 +4263,5 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
|||
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodianAccountControllerTests()
|
||||
{
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
await tester.PayTester.EnableExperimental();
|
||||
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
var authedButLackingPermissionsClient = await admin.CreateClient(Policies.CanViewStoreSettings);
|
||||
var viewerOnlyClient = await admin.CreateClient(Policies.CanViewCustodianAccounts);
|
||||
var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts);
|
||||
var store = await adminClient.GetStore(admin.StoreId);
|
||||
var storeId = store.Id;
|
||||
|
||||
// Load a custodian, we use the first one we find.
|
||||
var custodians = tester.PayTester.GetService<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);
|
||||
|
||||
// Wrong store ID
|
||||
await AssertApiError(403, "missing-permission", async () => await adminClient.GetCustodianAccounts("WRONG-STORE-ID"));
|
||||
|
||||
|
||||
|
||||
// Try to fetch 1 custodian account
|
||||
// Admin
|
||||
var singleAdminCustodianAccount = await adminClient.GetCustodianAccount(storeId, accountId);
|
||||
Assert.NotNull(singleAdminCustodianAccount);
|
||||
Assert.Equal(singleAdminCustodianAccount.CustodianCode, custodian.Code);
|
||||
|
||||
// Wrong store ID
|
||||
await AssertApiError(403, "missing-permission", async () => await adminClient.GetCustodianAccount("WRONG-STORE-ID", accountId));
|
||||
|
||||
// Wrong account ID
|
||||
await AssertApiError(404, "custodian-account-not-found", async () => await adminClient.GetCustodianAccount(storeId, "WRONG-ACCOUNT-ID"));
|
||||
|
||||
// Manager can see, including config
|
||||
var singleManagerCustodianAccount = await managerClient.GetCustodianAccount(storeId, accountId);
|
||||
Assert.NotNull(singleManagerCustodianAccount);
|
||||
Assert.Equal(singleManagerCustodianAccount.CustodianCode, custodian.Code);
|
||||
Assert.NotNull(singleManagerCustodianAccount.Config);
|
||||
Assert.True(JToken.DeepEquals(config, singleManagerCustodianAccount.Config));
|
||||
|
||||
// Viewer can see, but no config
|
||||
var singleViewerCustodianAccount = await viewerOnlyClient.GetCustodianAccount(storeId, accountId);
|
||||
Assert.NotNull(singleViewerCustodianAccount);
|
||||
Assert.Equal(singleViewerCustodianAccount.CustodianCode, custodian.Code);
|
||||
Assert.Null(singleViewerCustodianAccount.Config);
|
||||
|
||||
|
||||
|
||||
// Test updating the custodian account we created
|
||||
var updateCustodianAccountRequest = createCustodianAccountRequest;
|
||||
updateCustodianAccountRequest.Name = "My Custodian";
|
||||
updateCustodianAccountRequest.Config["ApiKey"] = "ZZZ";
|
||||
|
||||
// Unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.UpdateCustodianAccount(storeId, accountId, updateCustodianAccountRequest));
|
||||
|
||||
// Auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await viewerOnlyClient.UpdateCustodianAccount(storeId, accountId, updateCustodianAccountRequest));
|
||||
|
||||
// Correct auth: update permissions
|
||||
var updatedCustodianAccountData = await managerClient.UpdateCustodianAccount(storeId, accountId, createCustodianAccountRequest);
|
||||
Assert.NotNull(updatedCustodianAccountData);
|
||||
Assert.Equal(custodian.Code, updatedCustodianAccountData.CustodianCode);
|
||||
Assert.Equal(updateCustodianAccountRequest.Name, updatedCustodianAccountData.Name);
|
||||
Assert.Equal(storeId, custodianAccountData.StoreId);
|
||||
Assert.True(JToken.DeepEquals(updateCustodianAccountRequest.Config, createCustodianAccountRequest.Config));
|
||||
|
||||
// Admin
|
||||
updateCustodianAccountRequest.Name = "Admin Account";
|
||||
updateCustodianAccountRequest.Config["ApiKey"] = "AAA";
|
||||
updatedCustodianAccountData = await adminClient.UpdateCustodianAccount(storeId, accountId, createCustodianAccountRequest);
|
||||
Assert.NotNull(updatedCustodianAccountData);
|
||||
Assert.Equal(custodian.Code, updatedCustodianAccountData.CustodianCode);
|
||||
Assert.Equal(updateCustodianAccountRequest.Name, updatedCustodianAccountData.Name);
|
||||
Assert.Equal(storeId, custodianAccountData.StoreId);
|
||||
Assert.True(JToken.DeepEquals(updateCustodianAccountRequest.Config, createCustodianAccountRequest.Config));
|
||||
|
||||
// Admin tries to update a non-existing custodian account
|
||||
await AssertHttpError(404, async () => await adminClient.UpdateCustodianAccount(storeId, "WRONG-ACCOUNT-ID", updateCustodianAccountRequest));
|
||||
|
||||
|
||||
|
||||
// Get asset balances, but we cannot because of misconfiguration (we did enter dummy data)
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccounts(storeId, true));
|
||||
|
||||
// // Auth, viewer permission => Error 500 because of BadConfigException (dummy data)
|
||||
// await AssertHttpError(500, async () => await viewerOnlyClient.GetCustodianAccounts(storeId, true));
|
||||
//
|
||||
|
||||
// Delete custodian account
|
||||
// Unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.DeleteCustodianAccount(storeId, accountId));
|
||||
|
||||
// Auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await viewerOnlyClient.DeleteCustodianAccount(storeId, accountId));
|
||||
|
||||
// Auth, correct permission
|
||||
await managerClient.DeleteCustodianAccount(storeId, accountId);
|
||||
|
||||
// Check if the Custodian Account was actually deleted
|
||||
await AssertHttpError(404, async () => await managerClient.GetCustodianAccount(storeId, accountId));
|
||||
|
||||
|
||||
// TODO what if we try to create a custodian account for a custodian code that does not exist?
|
||||
// TODO what if we try so set config data that is not valid? In phase 2 we will validate the config and only allow you to save a config that makes sense!
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodianTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
await tester.PayTester.EnableExperimental();
|
||||
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await admin.CreateClient(Policies.CanViewInvoices);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts);
|
||||
var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts);
|
||||
var depositClient = await admin.CreateClient(Policies.CanDepositToCustodianAccounts);
|
||||
var tradeClient = await admin.CreateClient(Policies.CanTradeCustodianAccount);
|
||||
|
||||
var store = await adminClient.GetStore(admin.StoreId);
|
||||
var storeId = store.Id;
|
||||
|
||||
// Load a custodian, we use the first one we find.
|
||||
var custodians = tester.PayTester.GetService<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.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong payment method
|
||||
await AssertApiError(400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||
|
||||
// Test: GetDepositAddress, wrong store ID
|
||||
await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong account ID
|
||||
await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, correct payment method
|
||||
var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
|
||||
Assert.NotNull(depositAddress);
|
||||
Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address);
|
||||
|
||||
|
||||
// Test: Trade, unauth
|
||||
var tradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
|
||||
await AssertHttpError(401, async () => await unauthClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
|
||||
|
||||
// Test: Trade, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest));
|
||||
|
||||
// Test: Trade, correct permission, correct assets, correct amount
|
||||
var newTradeResult = await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, tradeRequest);
|
||||
Assert.NotNull(newTradeResult);
|
||||
Assert.Equal(accountId, newTradeResult.AccountId);
|
||||
Assert.Equal(mockCustodian.Code, newTradeResult.CustodianCode);
|
||||
Assert.Equal(MockCustodian.TradeId, newTradeResult.TradeId);
|
||||
Assert.Equal(tradeRequest.FromAsset, newTradeResult.FromAsset);
|
||||
Assert.Equal(tradeRequest.ToAsset, newTradeResult.ToAsset);
|
||||
Assert.NotNull(newTradeResult.LedgerEntries);
|
||||
Assert.Equal(3, newTradeResult.LedgerEntries.Count);
|
||||
Assert.Equal(MockCustodian.TradeQtyBought, newTradeResult.LedgerEntries[0].Qty);
|
||||
Assert.Equal(tradeRequest.ToAsset, newTradeResult.LedgerEntries[0].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, newTradeResult.LedgerEntries[0].Type);
|
||||
Assert.Equal(-1 * MockCustodian.TradeQtyBought * MockCustodian.BtcPriceInEuro, newTradeResult.LedgerEntries[1].Qty);
|
||||
Assert.Equal(tradeRequest.FromAsset, newTradeResult.LedgerEntries[1].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, newTradeResult.LedgerEntries[1].Type);
|
||||
Assert.Equal(-1 * MockCustodian.TradeFeeEuro, newTradeResult.LedgerEntries[2].Qty);
|
||||
Assert.Equal(tradeRequest.FromAsset, newTradeResult.LedgerEntries[2].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, newTradeResult.LedgerEntries[2].Type);
|
||||
|
||||
// Test: GetTradeQuote, SATS
|
||||
var satsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = "SATS", Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, satsTradeRequest));
|
||||
|
||||
// TODO Test: Trade with percentage qty
|
||||
|
||||
// Test: Trade, wrong assets method
|
||||
var wrongAssetsTradeRequest = new TradeRequestData { FromAsset = "WRONG", ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(MockCustodian.TradeQtyBought, TradeQuantity.ValueType.Exact) };
|
||||
await AssertHttpError(WrongTradingPairException.HttpCode, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, wrongAssetsTradeRequest));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, "WRONG-ACCOUNT-ID", tradeRequest));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.MarketTradeCustodianAccountAsset("WRONG-STORE-ID", accountId, tradeRequest));
|
||||
|
||||
// Test: Trade, correct assets, wrong amount
|
||||
var insufficientFundsTradeRequest = new TradeRequestData { FromAsset = MockCustodian.TradeFromAsset, ToAsset = MockCustodian.TradeToAsset, Qty = new TradeQuantity(0.01m, TradeQuantity.ValueType.Exact) };
|
||||
await AssertApiError(400, "insufficient-funds", async () => await tradeClient.MarketTradeCustodianAccountAsset(storeId, accountId, insufficientFundsTradeRequest));
|
||||
|
||||
|
||||
// Test: GetTradeQuote, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, correct permission
|
||||
var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
|
||||
Assert.NotNull(tradeQuote);
|
||||
Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset);
|
||||
Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset);
|
||||
Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Bid);
|
||||
Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Ask);
|
||||
|
||||
// Test: GetTradeQuote, SATS
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||
|
||||
// Test: GetTradeQuote, wrong asset
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Test: GetTradeInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, correct permission
|
||||
var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId);
|
||||
Assert.NotNull(tradeResult);
|
||||
Assert.Equal(accountId, tradeResult.AccountId);
|
||||
Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode);
|
||||
Assert.Equal(MockCustodian.TradeId, tradeResult.TradeId);
|
||||
Assert.Equal(tradeRequest.FromAsset, tradeResult.FromAsset);
|
||||
Assert.Equal(tradeRequest.ToAsset, tradeResult.ToAsset);
|
||||
Assert.NotNull(tradeResult.LedgerEntries);
|
||||
Assert.Equal(3, tradeResult.LedgerEntries.Count);
|
||||
Assert.Equal(MockCustodian.TradeQtyBought, tradeResult.LedgerEntries[0].Qty);
|
||||
Assert.Equal(tradeRequest.ToAsset, tradeResult.LedgerEntries[0].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, tradeResult.LedgerEntries[0].Type);
|
||||
Assert.Equal(-1 * MockCustodian.TradeQtyBought * MockCustodian.BtcPriceInEuro, tradeResult.LedgerEntries[1].Qty);
|
||||
Assert.Equal(tradeRequest.FromAsset, tradeResult.LedgerEntries[1].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Trade, tradeResult.LedgerEntries[1].Type);
|
||||
Assert.Equal(-1 * MockCustodian.TradeFeeEuro, tradeResult.LedgerEntries[2].Qty);
|
||||
Assert.Equal(tradeRequest.FromAsset, tradeResult.LedgerEntries[2].Asset);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, tradeResult.LedgerEntries[2].Type);
|
||||
|
||||
// Test: GetTradeInfo, wrong trade ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||
|
||||
var qty = new TradeQuantity(MockCustodian.WithdrawalAmount, TradeQuantity.ValueType.Exact);
|
||||
// Test: SimulateWithdrawal, unauth
|
||||
var simulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, correct amount
|
||||
var simulateWithdrawResponse = await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest);
|
||||
AssertMockWithdrawal(simulateWithdrawResponse, custodianAccountData);
|
||||
|
||||
// Test: SimulateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodSimulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong store ID
|
||||
// TODO it is weird that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongAmountSimulateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, unauth
|
||||
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount
|
||||
var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||
AssertMockWithdrawal(withdrawResponse, custodianAccountData);
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage
|
||||
var withdrawWithPercentageResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequestPercentage);
|
||||
AssertMockWithdrawal(withdrawWithPercentageResponse, custodianAccountData);
|
||||
|
||||
// Test: CreateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError(400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong store ID
|
||||
// TODO it is weird that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateCustodianAccountWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||
|
||||
// Test: GetWithdrawalInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, correct permission
|
||||
var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
|
||||
|
||||
// Test: GetWithdrawalInfo, wrong withdrawal ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: wrong store ID
|
||||
// TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered
|
||||
await AssertHttpError(403, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// TODO assert API error codes, not just status codes by using AssertCustodianApiError()
|
||||
// TODO also test withdrawals for the various "Status" (Queued, Complete, Failed)
|
||||
// TODO create a mock custodian with only ICustodian
|
||||
// TODO create a mock custodian with only ICustodian + ICanWithdraw
|
||||
// TODO create a mock custodian with only ICustodian + ICanTrade
|
||||
// TODO create a mock custodian with only ICustodian + ICanDeposit
|
||||
}
|
||||
|
||||
private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account)
|
||||
{
|
||||
Assert.NotNull(withdrawResponse);
|
||||
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset);
|
||||
Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod);
|
||||
Assert.Equal(account.Id, withdrawResponse.AccountId);
|
||||
Assert.Equal(account.CustodianCode, withdrawResponse.CustodianCode);
|
||||
|
||||
Assert.Equal(2, withdrawResponse.LedgerEntries.Count);
|
||||
|
||||
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.LedgerEntries[0].Asset);
|
||||
Assert.Equal(MockCustodian.WithdrawalAmount - MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[0].Qty);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Withdrawal, withdrawResponse.LedgerEntries[0].Type);
|
||||
|
||||
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.LedgerEntries[1].Asset);
|
||||
Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type);
|
||||
|
||||
if (withdrawResponse is WithdrawalResponseData withdrawalResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawalResponseData.Status);
|
||||
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawalResponseData.TargetAddress);
|
||||
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawalResponseData.TransactionId);
|
||||
Assert.Equal(MockCustodian.WithdrawalId, withdrawalResponseData.WithdrawalId);
|
||||
Assert.NotEqual(default, withdrawalResponseData.CreatedTime);
|
||||
}
|
||||
|
||||
if (withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalMinAmount, withdrawalSimulationResponseData.MinQty);
|
||||
Assert.Equal(MockCustodian.WithdrawalMaxAmount, withdrawalSimulationResponseData.MaxQty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,196 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
|
||||
public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
{
|
||||
public const string DepositPaymentMethod = "BTC-OnChain";
|
||||
public const string DepositAddress = "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||
public const string TradeId = "TRADE-ID-001";
|
||||
public const string TradeFromAsset = "EUR";
|
||||
public const string TradeToAsset = "BTC";
|
||||
public static readonly decimal TradeQtyBought = new decimal(1);
|
||||
public static readonly decimal TradeFeeEuro = new decimal(12.5);
|
||||
public static readonly decimal BtcPriceInEuro = new decimal(30000);
|
||||
public const string WithdrawalPaymentMethod = "BTC-OnChain";
|
||||
public const string WithdrawalAsset = "BTC";
|
||||
public const string WithdrawalId = "WITHDRAWAL-ID-001";
|
||||
public static readonly decimal WithdrawalAmount = new decimal(0.5);
|
||||
public static readonly string WithdrawalAmountPercentage = "12.5%";
|
||||
public static readonly decimal WithdrawalMinAmount = new decimal(0.001);
|
||||
public static readonly decimal WithdrawalMaxAmount = new decimal(0.6);
|
||||
public static readonly decimal WithdrawalFee = new decimal(0.0005);
|
||||
public const string WithdrawalTransactionId = "yyy";
|
||||
public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";
|
||||
public const WithdrawalResponseData.WithdrawalStatus WithdrawalStatus = WithdrawalResponseData.WithdrawalStatus.Queued;
|
||||
public static readonly decimal BalanceBTC = new decimal(1.23456);
|
||||
public static readonly decimal BalanceLTC = new decimal(50.123456);
|
||||
public static readonly decimal BalanceUSD = new decimal(1500.55);
|
||||
public static readonly decimal BalanceEUR = new decimal(1235.15);
|
||||
|
||||
public string Code
|
||||
{
|
||||
get => "mock";
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => "MOCK Exchange";
|
||||
}
|
||||
|
||||
public Task<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<Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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", (decimal)0.0001));
|
||||
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;
|
||||
}
|
||||
|
||||
private SimulateWithdrawalResult CreateWithdrawSimulationResult()
|
||||
{
|
||||
var ledgerEntries = new List<LedgerEntryData>();
|
||||
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
|
||||
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
|
||||
var r = new SimulateWithdrawalResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalMinAmount, WithdrawalMaxAmount);
|
||||
return r;
|
||||
}
|
||||
|
||||
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (paymentMethod == WithdrawalPaymentMethod)
|
||||
{
|
||||
if (amount.ToString(CultureInfo.InvariantCulture).Equals("" + WithdrawalAmount, StringComparison.InvariantCulture) || WithdrawalAmountPercentage.Equals(amount))
|
||||
{
|
||||
return Task.FromResult(CreateWithdrawResult());
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount} or {WithdrawalAmountPercentage}");
|
||||
}
|
||||
|
||||
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
|
||||
}
|
||||
|
||||
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (paymentMethod == WithdrawalPaymentMethod)
|
||||
{
|
||||
if (amount == WithdrawalAmount)
|
||||
{
|
||||
return Task.FromResult(CreateWithdrawSimulationResult());
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
|
||||
}
|
||||
|
||||
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
|
||||
}
|
||||
|
||||
public Task<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();
|
||||
}
|
||||
}
|
|
@ -216,5 +216,6 @@
|
|||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
</Project>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
@using BTCPayServer.Plugins
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
@ -95,26 +94,6 @@
|
|||
</li>
|
||||
}
|
||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||
@if (PoliciesSettings.Experimental)
|
||||
{
|
||||
@foreach (var custodianAccount in Model.CustodianAccounts)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
|
||||
@* TODO which icon should we use? *@
|
||||
<span>@custodianAccount.Name</span>
|
||||
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateCustodianAccount">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>Add Custodian</span>
|
||||
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,6 @@ using BTCPayServer.Payments;
|
|||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
@ -28,7 +27,6 @@ namespace BTCPayServer.Components.MainNav
|
|||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
|
||||
|
@ -39,7 +37,6 @@ namespace BTCPayServer.Components.MainNav
|
|||
BTCPayNetworkProvider networkProvider,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
SettingsRepository settingsRepository,
|
||||
PoliciesSettings policiesSettings)
|
||||
{
|
||||
|
@ -49,7 +46,6 @@ namespace BTCPayServer.Components.MainNav
|
|||
_networkProvider = networkProvider;
|
||||
_storesController = storesController;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
@ -88,13 +84,6 @@ namespace BTCPayServer.Components.MainNav
|
|||
}).ToList();
|
||||
|
||||
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
{
|
||||
// Custodian Accounts
|
||||
var custodianAccounts = await _custodianAccountRepository.FindByStoreId(store.Id);
|
||||
vm.CustodianAccounts = custodianAccounts;
|
||||
}
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
|
|
|
@ -10,7 +10,6 @@ namespace BTCPayServer.Components.MainNav
|
|||
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
|
||||
public List<StoreLightningNode> LightningNodes { get; set; }
|
||||
public List<StoreApp> Apps { get; set; }
|
||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
public string ContactUrl { get; set; }
|
||||
|
|
|
@ -1,479 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Custodian;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using CustodianAccountDataClient = BTCPayServer.Client.Models.CustodianAccountData;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
public class CustodianExceptionFilter : Attribute, IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is CustodianApiException ex)
|
||||
{
|
||||
context.Result = new ObjectResult(new GreenfieldAPIError(ex.Code, ex.Message)) { StatusCode = ex.HttpStatus };
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[CustodianExceptionFilter]
|
||||
[ExperimentalRouteAttribute] // if you remove this, also remove "x_experimental": true in swagger.template.custodians.json
|
||||
public class GreenfieldCustodianAccountController : ControllerBase
|
||||
{
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly IEnumerable<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 GetCustodianAccount(storeId, accountId);
|
||||
if (custodianAccountData == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "custodian-account-not-found", "The custodian account was not found.");
|
||||
}
|
||||
var custodianAccount = await ToModel(custodianAccountData, assetBalances, cancellationToken);
|
||||
return Ok(custodianAccount);
|
||||
}
|
||||
|
||||
// [HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/config")]
|
||||
// [Authorize(Policy = Policies.CanManageCustodianAccounts,
|
||||
// AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
// public async Task<IActionResult> FetchCustodianAccountConfigForm(string storeId, string accountId,
|
||||
// [FromQuery] string locale = "en-US", CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// // TODO this endpoint needs tests
|
||||
// var custodianAccountData = await GetCustodianAccount(storeId, accountId);
|
||||
// var custodianAccount = await ToModel(custodianAccountData, false, cancellationToken);
|
||||
//
|
||||
// var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
// var form = await custodian.GetConfigForm(custodianAccount.Config, locale, cancellationToken);
|
||||
//
|
||||
// return Ok(form);
|
||||
// }
|
||||
//
|
||||
// [HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/config")]
|
||||
// [Authorize(Policy = Policies.CanManageCustodianAccounts,
|
||||
// AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
// public async Task<IActionResult> PostCustodianAccountConfigForm(string storeId, string accountId, JObject values,
|
||||
// [FromQuery] string locale = "en-US", CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// // TODO this endpoint needs tests
|
||||
// var custodianAccountData = await GetCustodianAccount(storeId, accountId);
|
||||
// var custodianAccount = await ToModel(custodianAccountData, false, cancellationToken);
|
||||
//
|
||||
// var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
// var form = await custodian.GetConfigForm(values, locale, cancellationToken);
|
||||
//
|
||||
// if (form.IsValid())
|
||||
// {
|
||||
// // TODO save the data to the config so it is persisted
|
||||
// }
|
||||
//
|
||||
// return Ok(form);
|
||||
// }
|
||||
|
||||
private async Task<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 GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(request.CustodianCode);
|
||||
|
||||
// TODO If storeId is not valid, we get a foreign key SQL error. Is this okay or do we want to check the storeId first?
|
||||
custodianAccount.CustodianCode = custodian.Code;
|
||||
custodianAccount.StoreId = storeId;
|
||||
custodianAccount.Name = request.Name;
|
||||
|
||||
custodianAccount.SetBlob(request.Config);
|
||||
|
||||
await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
return Ok(await ToModel(custodianAccount, false, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}")]
|
||||
[Authorize(Policy = Policies.CanManageCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<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 GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(paymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var result = await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "deposit-payment-method-not-supported",
|
||||
$"Deposits to \"{custodian.Name}\" are not supported using \"{paymentMethod}\".");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market")]
|
||||
[Authorize(Policy = Policies.CanTradeCustodianAccount,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> MarketTradeCustodianAccountAsset(string storeId, string accountId,
|
||||
TradeRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO add SATS check everywhere. We cannot change to 'BTC' ourselves because the qty / price would be different too.
|
||||
if ("SATS".Equals(request.FromAsset) || "SATS".Equals(request.ToAsset))
|
||||
{
|
||||
return this.CreateAPIError(400, "use-asset-synonym",
|
||||
$"Please use 'BTC' instead of 'SATS'.");
|
||||
}
|
||||
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanTrade tradableCustodian)
|
||||
{
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, request.FromAsset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(request.FromAsset, ex.Message);
|
||||
}
|
||||
try
|
||||
{
|
||||
var result = await tradableCustodian.TradeMarketAsync(request.FromAsset, request.ToAsset, qty,
|
||||
custodianAccount.GetBlob(), cancellationToken);
|
||||
|
||||
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
|
||||
}
|
||||
catch (CustodianApiException e)
|
||||
{
|
||||
return this.CreateAPIError(e.HttpStatus, e.Code,
|
||||
e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "market-trade-not-supported",
|
||||
$"Placing market orders on \"{custodian.Name}\" is not supported.");
|
||||
}
|
||||
|
||||
private MarketTradeResponseData ToModel(MarketTradeResult marketTrade, string accountId, string custodianCode)
|
||||
{
|
||||
return new MarketTradeResponseData(marketTrade.FromAsset, marketTrade.ToAsset, marketTrade.LedgerEntries, marketTrade.TradeId, accountId, custodianCode);
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote")]
|
||||
[Authorize(Policy = Policies.CanTradeCustodianAccount, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<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 GetCustodianAccount(storeId, accountId);
|
||||
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanTrade tradableCustodian)
|
||||
{
|
||||
var priceQuote = await tradableCustodian.GetQuoteForAssetAsync(fromAsset, toAsset, custodianAccount.GetBlob(), cancellationToken);
|
||||
return Ok(new TradeQuoteResponseData(priceQuote.FromAsset, priceQuote.ToAsset, priceQuote.Bid, priceQuote.Ask));
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "getting-quote-not-supported",
|
||||
$"Getting a price quote on \"{custodian.Name}\" is not supported.");
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}")]
|
||||
[Authorize(Policy = Policies.CanTradeCustodianAccount,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanTrade tradableCustodian)
|
||||
{
|
||||
var result = await tradableCustodian.GetTradeInfoAsync(tradeId, custodianAccount.GetBlob(), cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "trade-not-found",
|
||||
$"Could not find the trade with ID {tradeId} on {custodianAccount.Name}");
|
||||
}
|
||||
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "fetching-trade-info-not-supported",
|
||||
$"Fetching past trade info on \"{custodian.Name}\" is not supported.");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> SimulateWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var simulateWithdrawResult =
|
||||
await withdrawableCustodian.SimulateWithdrawalAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalSimulationResponseData(simulateWithdrawResult.PaymentMethod, simulateWithdrawResult.Asset,
|
||||
accountId, custodian.Code, simulateWithdrawResult.LedgerEntries, simulateWithdrawResult.MinQty, simulateWithdrawResult.MaxQty);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "withdrawals-not-supported",
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var withdrawResult =
|
||||
await withdrawableCustodian.WithdrawToStoreWalletAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries,
|
||||
withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "withdrawals-not-supported",
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
private IActionResult UnsupportedAsset(string asset, string err)
|
||||
{
|
||||
return this.CreateAPIError(400, "invalid-qty", $"It is impossible to use % quantity with this asset ({err})");
|
||||
}
|
||||
|
||||
private async Task<decimal> ParseQty(TradeQuantity qty, string asset, CustodianAccountData custodianAccount, ICustodian custodian, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (qty.Type == TradeQuantity.ValueType.Exact)
|
||||
return qty.Value;
|
||||
// Percentage of current holdings => calculate the amount
|
||||
var config = custodianAccount.GetBlob();
|
||||
var balances = await custodian.GetAssetBalancesAsync(config, cancellationToken);
|
||||
if (!balances.TryGetValue(asset, out var assetBalance))
|
||||
return 0.0m;
|
||||
return (assetBalance * qty.Value) / 100m;
|
||||
}
|
||||
|
||||
async Task<CustodianAccountData> GetCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var cust = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (cust is null)
|
||||
throw CustodianAccountNotFound();
|
||||
return cust;
|
||||
}
|
||||
|
||||
JsonHttpException CustodianAccountNotFound()
|
||||
{
|
||||
return new JsonHttpException(this.CreateAPIError(404, "custodian-account-not-found", "Could not find the custodian account"));
|
||||
}
|
||||
|
||||
ICustodian GetCustodianByCode(string custodianCode)
|
||||
{
|
||||
var cust = _custodianRegistry.FirstOrDefault(custodian => custodian.Code.Equals(custodianCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (cust is null)
|
||||
throw new JsonHttpException(this.CreateAPIError(422, "custodian-code-not-found", "The custodian of this account isn't referenced in /api/v1/custodians"));
|
||||
return cust;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var withdrawResult = await withdrawableCustodian.GetWithdrawalInfoAsync(paymentMethod, withdrawalId, custodianAccount.GetBlob(), cancellationToken);
|
||||
if (withdrawResult == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "withdrawal-not-found", "The withdrawal was not found.");
|
||||
}
|
||||
var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries,
|
||||
withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "fetching-withdrawal-info-not-supported",
|
||||
$"Fetching withdrawal information is not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[ExperimentalRouteAttribute] // if you remove this, also remove "x_experimental": true in swagger.template.custodians.json
|
||||
public class GreenfieldCustodianController : ControllerBase
|
||||
{
|
||||
private readonly IEnumerable<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 tradableAssetPairsDict = new Dictionary<string, AssetPairData>(tradableAssetPairs.Count);
|
||||
foreach (var tradableAssetPair in tradableAssetPairs)
|
||||
{
|
||||
tradableAssetPairsDict.Add(tradableAssetPair.ToString(), tradableAssetPair);
|
||||
}
|
||||
result.TradableAssetPairs = tradableAssetPairsDict;
|
||||
}
|
||||
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
result.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
}
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
result.WithdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -217,27 +217,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
throw new NotSupportedException("This method is not supported by the LocalBTCPayServerClient.");
|
||||
}
|
||||
|
||||
public override async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId,
|
||||
TradeRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<MarketTradeResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().MarketTradeCustodianAccountAsset(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalSimulationResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().SimulateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().CreateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
GetWalletObjectsRequest query = null,
|
||||
CancellationToken token = default)
|
||||
|
|
|
@ -1,646 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Models.CustodianAccountViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog.Config;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[ExperimentalRouteAttribute]
|
||||
public class UICustodianAccountsController : Controller
|
||||
{
|
||||
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly BTCPayServerClient _btcPayServerClient;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly FormDataService _formDataService;
|
||||
|
||||
public UICustodianAccountsController(
|
||||
DisplayFormatter displayFormatter,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
IEnumerable<ICustodian> custodianRegistry,
|
||||
BTCPayServerClient btcPayServerClient,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
LinkGenerator linkGenerator,
|
||||
FormDataService formDataService
|
||||
)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_custodianRegistry = custodianRegistry;
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
_networkProvider = networkProvider;
|
||||
_linkGenerator = linkGenerator;
|
||||
_formDataService = formDataService;
|
||||
}
|
||||
|
||||
public string CreatedCustodianAccountId { get; set; }
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}")]
|
||||
public async Task<IActionResult> ViewCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = new ViewCustodianAccountViewModel();
|
||||
vm.Custodian = custodian;
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}.json")]
|
||||
public async Task<IActionResult> ViewCustodianAccountJson(string storeId, string accountId)
|
||||
{
|
||||
var vm = new ViewCustodianAccountBalancesViewModel();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var storeBlob = StoreDataExtensions.GetStoreBlob(store);
|
||||
var defaultCurrency = storeBlob.DefaultCurrency;
|
||||
vm.StoreId = store.Id;
|
||||
vm.DustThresholdInFiat = 1;
|
||||
vm.StoreDefaultFiat = defaultCurrency;
|
||||
try
|
||||
{
|
||||
var assetBalances = new Dictionary<string, AssetBalanceInfo>();
|
||||
var assetBalancesData =
|
||||
await custodian.GetAssetBalancesAsync(custodianAccount.GetBlob(), cancellationToken: default);
|
||||
|
||||
foreach (var pair in assetBalancesData)
|
||||
{
|
||||
var asset = pair.Key;
|
||||
|
||||
assetBalances.Add(asset,
|
||||
new AssetBalanceInfo
|
||||
{
|
||||
Asset = asset,
|
||||
Qty = pair.Value,
|
||||
FormattedQty = pair.Value.ToString(CultureInfo.InvariantCulture)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (custodian is ICanTrade tradingCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
var tradableAssetPairs = tradingCustodian.GetTradableAssetPairs();
|
||||
|
||||
foreach (var pair in assetBalances)
|
||||
{
|
||||
var asset = pair.Key;
|
||||
var assetBalance = assetBalances[asset];
|
||||
var tradableAssetPairsList =
|
||||
tradableAssetPairs.Where(o => o.AssetBought == asset || o.AssetSold == asset).ToList();
|
||||
var tradableAssetPairsDict =
|
||||
new Dictionary<string, AssetPairData>(tradableAssetPairsList.Count);
|
||||
foreach (var assetPair in tradableAssetPairsList)
|
||||
{
|
||||
tradableAssetPairsDict.Add(assetPair.ToString(), assetPair);
|
||||
}
|
||||
|
||||
assetBalance.TradableAssetPairs = tradableAssetPairsDict;
|
||||
|
||||
if (asset.Equals(defaultCurrency))
|
||||
{
|
||||
assetBalance.FormattedFiatValue =
|
||||
_displayFormatter.Currency(pair.Value.Qty, defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(defaultCurrency, asset,
|
||||
config, default);
|
||||
assetBalance.Bid = quote.Bid;
|
||||
assetBalance.Ask = quote.Ask;
|
||||
assetBalance.FormattedBid =
|
||||
_displayFormatter.Currency(quote.Bid, quote.FromAsset);
|
||||
assetBalance.FormattedAsk =
|
||||
_displayFormatter.Currency(quote.Ask, quote.FromAsset);
|
||||
assetBalance.FormattedFiatValue =
|
||||
_displayFormatter.Currency(pair.Value.Qty * quote.Bid,
|
||||
defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
|
||||
}
|
||||
catch (WrongTradingPairException)
|
||||
{
|
||||
// Cannot trade this asset, just ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var withdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
foreach (var withdrawablePaymentMethod in withdrawablePaymentMethods)
|
||||
{
|
||||
var withdrawableAsset = withdrawablePaymentMethod.Split("-")[0];
|
||||
if (assetBalances.ContainsKey(withdrawableAsset))
|
||||
{
|
||||
var assetBalance = assetBalances[withdrawableAsset];
|
||||
assetBalance.WithdrawablePaymentMethods.Add(withdrawablePaymentMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
vm.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
}
|
||||
|
||||
vm.AssetBalances = assetBalances;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.AssetBalanceExceptionMessage = e.Message;
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/edit")]
|
||||
public async Task<IActionResult> EditCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blob = custodianAccount.GetBlob();
|
||||
var configForm = await custodian.GetConfigForm(blob, HttpContext.RequestAborted);
|
||||
_formDataService.SetValues(configForm, blob);
|
||||
|
||||
var vm = new EditCustodianAccountViewModel();
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/edit")]
|
||||
public async Task<IActionResult> EditCustodianAccount(string storeId, string accountId,
|
||||
EditCustodianAccountViewModel vm)
|
||||
{
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
var configForm = await GetNextForm(custodian, vm.Config);
|
||||
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
var newData = _formDataService.GetValues(configForm);
|
||||
custodianAccount.SetBlob(newData);
|
||||
custodianAccount.Name = vm.CustodianAccount.Name;
|
||||
custodianAccount = await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
return RedirectToAction(nameof(ViewCustodianAccount),
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
// Form not valid: The user must fix the errors before we can save
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<Form> GetNextForm(ICustodian custodian, string config)
|
||||
{
|
||||
JObject b = null;
|
||||
try
|
||||
{
|
||||
if (config != null)
|
||||
b = JObject.Parse(config);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
b ??= new JObject();
|
||||
// First, we restore the previous form based on the previous blob that was
|
||||
// stored in config
|
||||
var form = await custodian.GetConfigForm(b, HttpContext.RequestAborted);
|
||||
_formDataService.SetValues(form, b);
|
||||
// Then we apply new values overriding the previous blob from the Form params
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
// We extract the new resulting blob, and request what is the next form based on it
|
||||
b = _formDataService.GetValues(form);
|
||||
form = await custodian.GetConfigForm(_formDataService.GetValues(form), HttpContext.RequestAborted);
|
||||
// We set all the values to this blob, and validate the form
|
||||
_formDataService.SetValues(form, b);
|
||||
_formDataService.Validate(form, ModelState);
|
||||
return form;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/create")]
|
||||
public IActionResult CreateCustodianAccount(string storeId)
|
||||
{
|
||||
var vm = new CreateCustodianAccountViewModel();
|
||||
vm.StoreId = storeId;
|
||||
vm.SetCustodianRegistry(_custodianRegistry);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/create")]
|
||||
public async Task<IActionResult> CreateCustodianAccount(string storeId, CreateCustodianAccountViewModel vm)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
vm.StoreId = store.Id;
|
||||
vm.SetCustodianRegistry(_custodianRegistry);
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(vm.SelectedCustodian);
|
||||
if (custodian == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedCustodian), "Invalid Custodian");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(vm.Name))
|
||||
{
|
||||
vm.Name = custodian.Name;
|
||||
}
|
||||
|
||||
var custodianAccountData = new CustodianAccountData
|
||||
{
|
||||
CustodianCode = vm.SelectedCustodian,
|
||||
StoreId = vm.StoreId,
|
||||
Name = custodian.Name
|
||||
};
|
||||
|
||||
|
||||
var configForm = await GetNextForm(custodian, vm.Config);
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
var configData = _formDataService.GetValues(configForm);
|
||||
custodianAccountData.SetBlob(configData);
|
||||
custodianAccountData.Name = vm.Name;
|
||||
custodianAccountData = await _custodianAccountRepository.CreateOrUpdate(custodianAccountData);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Custodian account successfully created";
|
||||
CreatedCustodianAccountId = custodianAccountData.Id;
|
||||
|
||||
return RedirectToAction(nameof(ViewCustodianAccount),
|
||||
new { storeId = custodianAccountData.StoreId, accountId = custodianAccountData.Id });
|
||||
}
|
||||
|
||||
// Ask for more data
|
||||
vm.ConfigForm = configForm;
|
||||
vm.Config = _formDataService.GetValues(configForm).ToString();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/delete")]
|
||||
public async Task<IActionResult> DeleteCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (custodianAccount == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var isDeleted = await _custodianAccountRepository.Remove(custodianAccount.Id, custodianAccount.StoreId);
|
||||
if (isDeleted)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Custodian account deleted";
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Could not delete custodian account";
|
||||
return RedirectToAction(nameof(ViewCustodianAccount),
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade/simulate")]
|
||||
public async Task<IActionResult> SimulateTradeJson(string storeId, string accountId,
|
||||
[FromBody] TradeRequestData request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.FromAsset) || string.IsNullOrEmpty(request.ToAsset))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
TradePrepareViewModel vm = new();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assetBalancesData =
|
||||
await custodian.GetAssetBalancesAsync(custodianAccount.GetBlob(), cancellationToken: default);
|
||||
|
||||
if (custodian is ICanTrade tradingCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
foreach (var pair in assetBalancesData)
|
||||
{
|
||||
var oneAsset = pair.Key;
|
||||
if (request.FromAsset.Equals(oneAsset))
|
||||
{
|
||||
vm.MaxQty = pair.Value;
|
||||
//vm.FormattedMaxQtyToTrade = pair.Value;
|
||||
|
||||
if (request.FromAsset.Equals(request.ToAsset))
|
||||
{
|
||||
// We cannot trade the asset for itself
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(request.FromAsset,
|
||||
request.ToAsset,
|
||||
config, default);
|
||||
|
||||
// TODO Ask is normally a higher number than Bid!! Let's check this!! Maybe a Unit Test?
|
||||
vm.Ask = quote.Ask;
|
||||
vm.Bid = quote.Bid;
|
||||
vm.FromAsset = quote.FromAsset;
|
||||
vm.ToAsset = quote.ToAsset;
|
||||
}
|
||||
catch (WrongTradingPairException)
|
||||
{
|
||||
// Cannot trade this asset
|
||||
return BadRequest(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade")]
|
||||
public async Task<IActionResult> Trade(string storeId, string accountId,
|
||||
[FromBody] TradeRequestData request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _btcPayServerClient.MarketTradeCustodianAccountAsset(storeId, accountId, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (GreenfieldAPIException e)
|
||||
{
|
||||
var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/deposit/prepare")]
|
||||
public async Task<IActionResult> GetDepositPrepareJson(string storeId, string accountId,
|
||||
[FromQuery] string paymentMethod)
|
||||
{
|
||||
if (string.IsNullOrEmpty(paymentMethod))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
DepositPrepareViewModel vm = new();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
vm.PaymentMethod = paymentMethod;
|
||||
var depositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
if (!depositablePaymentMethods.Contains(paymentMethod))
|
||||
{
|
||||
vm.ErrorMessage = $"Payment method \"{paymentMethod}\" is not supported by {custodian.Name}";
|
||||
return BadRequest(vm);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var depositAddressResult =
|
||||
await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, default);
|
||||
vm.Address = depositAddressResult.Address;
|
||||
|
||||
var paymentMethodObj = PaymentMethodId.Parse(paymentMethod);
|
||||
if (paymentMethodObj.IsBTCOnChain)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var bip21 = network.GenerateBIP21(depositAddressResult.Address, null);
|
||||
vm.Link = bip21.ToString();
|
||||
var paymentMethodId = PaymentMethodId.TryParse(paymentMethod);
|
||||
if (paymentMethodId != null)
|
||||
{
|
||||
var walletId = new WalletId(storeId, paymentMethodId.CryptoCode);
|
||||
var returnUrl = _linkGenerator.GetPathByAction(
|
||||
nameof(ViewCustodianAccount),
|
||||
"UICustodianAccounts",
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id },
|
||||
Request.PathBase);
|
||||
|
||||
vm.CryptoImageUrl = GetImage(paymentMethodId, network);
|
||||
vm.CreateTransactionUrl = _linkGenerator.GetPathByAction(
|
||||
nameof(UIWalletsController.WalletSend),
|
||||
"UIWallets",
|
||||
new { walletId, defaultDestination = vm.Address, returnUrl },
|
||||
Request.PathBase);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO support LN + shitcoins
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.ErrorMessage = e.Message;
|
||||
return new ObjectResult(vm) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
// TODO this method was copy-pasted from BTCPayServer.Controllers.UIWalletsController.GetImage(). Maybe refactor this?
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
? Url.Content(network.CryptoImagePath)
|
||||
: Url.Content(network.LightningImagePath);
|
||||
return Request.GetRelativePathOrAbsolute(res);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw/simulate")]
|
||||
public async Task<IActionResult> SimulateWithdrawJson(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData withdrawRequestData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(withdrawRequestData.PaymentMethod))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = new WithdrawalPrepareViewModel();
|
||||
|
||||
try
|
||||
{
|
||||
if (custodian is ICanWithdraw)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
try
|
||||
{
|
||||
var simulateWithdrawal =
|
||||
await _btcPayServerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, withdrawRequestData,
|
||||
default);
|
||||
vm = new WithdrawalPrepareViewModel(simulateWithdrawal);
|
||||
|
||||
// There are no bad config fields, so we need an empty array
|
||||
vm.BadConfigFields = Array.Empty<string>();
|
||||
}
|
||||
catch (BadConfigException e)
|
||||
{
|
||||
Form configForm = await custodian.GetConfigForm(config);
|
||||
_formDataService.SetValues(configForm, config);
|
||||
string[] badConfigFields = new string[e.BadConfigKeys.Length];
|
||||
int i = 0;
|
||||
foreach (var oneField in configForm.GetAllFields())
|
||||
{
|
||||
foreach (var badConfigKey in e.BadConfigKeys)
|
||||
{
|
||||
if (oneField.FullName.Equals(badConfigKey))
|
||||
{
|
||||
var field = configForm.GetFieldByFullName(oneField.FullName);
|
||||
badConfigFields[i] = field.Label;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.BadConfigFields = badConfigFields;
|
||||
return Ok(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.ErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw")]
|
||||
public async Task<IActionResult> Withdraw(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _btcPayServerClient.CreateCustodianAccountWithdrawal(storeId, accountId, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (GreenfieldAPIException e)
|
||||
{
|
||||
var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
|
@ -513,16 +513,6 @@ namespace BTCPayServer.Controllers
|
|||
{Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "Allows seeing exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "Allows seeing exchange accounts linked to the selected stores.")},
|
||||
{Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "Allows modifying exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "Allows modifying exchange accounts linked to selected stores.")},
|
||||
{Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "Allows depositing funds to your exchange accounts.")},
|
||||
{$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "Allows depositing funds to selected store's exchange accounts.")},
|
||||
{Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "Allows withdrawing funds from your exchange accounts to your store.")},
|
||||
{$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "Allows withdrawing funds from your selected store's exchange accounts.")},
|
||||
{Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "Allows trading funds on your store's exchange accounts.")},
|
||||
{$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "Allows trading funds on selected store's exchange accounts.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")},
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#nullable enable
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
||||
public static class CustodianAccountDataExtensions
|
||||
{
|
||||
public static JObject GetBlob(this CustodianAccountData custodianAccountData)
|
||||
{
|
||||
return ((IHasBlob<JObject>)custodianAccountData).GetBlob() ?? new JObject();
|
||||
}
|
||||
}
|
|
@ -38,7 +38,6 @@ using BTCPayServer.Security.Bitpay;
|
|||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Fees;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
|
@ -168,7 +167,6 @@ namespace BTCPayServer.Hosting
|
|||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<UserService>();
|
||||
services.AddSingleton<CustodianAccountRepository>();
|
||||
services.TryAddSingleton<WalletHistogramService>();
|
||||
services.AddSingleton<ApplicationDbContextFactory>();
|
||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels;
|
||||
|
||||
public class AssetBalanceInfo
|
||||
{
|
||||
|
||||
public string Asset { get; set; }
|
||||
public decimal? Bid { get; set; }
|
||||
public decimal? Ask { get; set; }
|
||||
public decimal Qty { get; set; }
|
||||
public string FormattedQty { get; set; }
|
||||
public string FormattedFiatValue { get; set; }
|
||||
public decimal? FiatValue { get; set; }
|
||||
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
|
||||
|
||||
public List<string> WithdrawablePaymentMethods { get; set; } = new();
|
||||
public string FormattedBid { get; set; }
|
||||
public string FormattedAsk { get; set; }
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
{
|
||||
public class CreateCustodianAccountViewModel
|
||||
{
|
||||
|
||||
public void SetCustodianRegistry(IEnumerable<ICustodian> custodianRegistry)
|
||||
{
|
||||
var choices = custodianRegistry.Select(o => new Format
|
||||
{
|
||||
Name = o.Name,
|
||||
Value = o.Code
|
||||
}).ToArray();
|
||||
var chosen = choices.FirstOrDefault();
|
||||
Custodians = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
}
|
||||
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
[MinLength(1)]
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Store")]
|
||||
public string StoreId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Custodian")]
|
||||
public string SelectedCustodian { get; set; }
|
||||
//
|
||||
public SelectList Custodians { get; set; }
|
||||
|
||||
public Form ConfigForm { get; set; }
|
||||
public string Config { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
namespace BTCPayServer.Models.CustodianAccountViewModels;
|
||||
|
||||
public class DepositPrepareViewModel
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string CryptoImageUrl { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string CreateTransactionUrl { get; set; }
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
{
|
||||
public class EditCustodianAccountViewModel
|
||||
{
|
||||
|
||||
public CustodianAccountData CustodianAccount { get; set; }
|
||||
public Form ConfigForm { get; set; }
|
||||
public string Config { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels;
|
||||
|
||||
public class TradePrepareViewModel : AssetQuoteResult
|
||||
{
|
||||
public decimal MaxQty { get; set; }
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
{
|
||||
public class ViewCustodianAccountBalancesViewModel
|
||||
{
|
||||
public Dictionary<string, AssetBalanceInfo> AssetBalances { get; set; }
|
||||
public string AssetBalanceExceptionMessage { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
public string StoreDefaultFiat { get; set; }
|
||||
public decimal DustThresholdInFiat { get; set; }
|
||||
public string[] DepositablePaymentMethods { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
{
|
||||
public class ViewCustodianAccountViewModel
|
||||
{
|
||||
public ICustodian Custodian { get; set; }
|
||||
public CustodianAccountData CustodianAccount { get; set; }
|
||||
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels;
|
||||
|
||||
public class WithdrawalPrepareViewModel : WithdrawalSimulationResponseData
|
||||
{
|
||||
public string ErrorMessage { get; set; }
|
||||
public string[] BadConfigFields { get; set; }
|
||||
|
||||
public WithdrawalPrepareViewModel(string paymentMethod, string asset, string accountId, string custodianCode,
|
||||
List<LedgerEntryData> ledgerEntries, decimal minQty, decimal maxQty) : base(paymentMethod, asset, accountId,
|
||||
custodianCode, ledgerEntries, minQty, maxQty)
|
||||
{
|
||||
}
|
||||
|
||||
public WithdrawalPrepareViewModel(WithdrawalSimulationResponseData simulateWithdrawal) : base(
|
||||
simulateWithdrawal.PaymentMethod, simulateWithdrawal.Asset, simulateWithdrawal.AccountId,
|
||||
simulateWithdrawal.CustodianCode, simulateWithdrawal.LedgerEntries, simulateWithdrawal.MinQty,
|
||||
simulateWithdrawal.MaxQty)
|
||||
{
|
||||
}
|
||||
|
||||
public WithdrawalPrepareViewModel() : base(null, null, null, null, null, null, null)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,357 +0,0 @@
|
|||
#if DEBUG
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Custodians.Client;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.FakeCustodian;
|
||||
|
||||
public class FakeCustodian : ICustodian, ICanDeposit, ICanWithdraw, ICanTrade
|
||||
{
|
||||
private const string TargetAddress = "3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
private const string ValidWithdrawalId = "FAKE_WITHDRAWAL_ID";
|
||||
private static readonly decimal _validWithdrawalAmount = new(0.05);
|
||||
private static readonly decimal _btcWithdrawalFee = new(0.001);
|
||||
private const string ValidWithdrawalPaymentMethod = "BTC-OnChain";
|
||||
private const string TransactionId = "FAKE_TRANSACTION_ID";
|
||||
private const string ValidAsset = "BTC";
|
||||
private const string ValidPaymentMethod = "BTC-OnChain";
|
||||
private const string ValidTradeId = "TRADE-ID-001";
|
||||
private const string TradeFromAsset = "EUR";
|
||||
private const string TradeToAsset = "BTC";
|
||||
private static readonly decimal _tradeQtyBought = new(1);
|
||||
private static readonly decimal _tradeFeeEuro = new(12.5);
|
||||
private static readonly decimal _btcPriceInEuro = new(30000);
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
|
||||
public FakeCustodian(CustodianAccountRepository custodianAccountRepository, Client.BTCPayServerClient client)
|
||||
{
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
}
|
||||
|
||||
public string Code
|
||||
{
|
||||
get => "fake";
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => "Fake Exchange";
|
||||
}
|
||||
|
||||
public Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
var fakeConfig = ParseConfig(config);
|
||||
var r = new Dictionary<string, decimal>() { { "BTC", fakeConfig.BTCBalance }, { "LTC", fakeConfig.LTCBalance }, { "USD", fakeConfig.USDBalance }, { "EUR", fakeConfig.EURBalance } };
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
public Task<Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var form = new Form();
|
||||
|
||||
var generalFieldset = Field.CreateFieldset();
|
||||
generalFieldset.Label = "General";
|
||||
// TODO we cannot validate the custodian account ID because we have no access to the correct value. This is fine given this is a development tool and won't be needed by actual custodians.
|
||||
var accountIdField = Field.Create("Custodian Account ID", "CustodianAccountId", null, false,
|
||||
"Enter the ID of this custodian account. This is needed as a workaround which only applies to the Fake Custodian. Fill out correctly to make trading and withdrawing work.");
|
||||
generalFieldset.Fields.Add(accountIdField);
|
||||
|
||||
// TODO we cannot validate the store ID because we have no access to the correct value. This is fine given this is a development tool and won't be needed by actual custodians.
|
||||
var storeIdField = Field.Create("Store ID", "StoreId", null, true,
|
||||
"Enter the ID of this store. This is needed as a workaround which only applies to the Fake Custodian. Fill out correctly to make trading and withdrawing work.");
|
||||
generalFieldset.Fields.Add(storeIdField);
|
||||
form.Fields.Add(generalFieldset);
|
||||
|
||||
var balancesFieldset = Field.CreateFieldset();
|
||||
|
||||
// Maybe a decimal type field would be better?
|
||||
var fakeBTCBalance = Field.Create("BTC Balance", "BTCBalance", null, true,
|
||||
"Enter the amount of BTC you want to have.");
|
||||
var fakeLTCBalance = Field.Create("LTC Balance", "LTCBalance", null, true,
|
||||
"Enter the amount of LTC you want to have.");
|
||||
var fakeEURBalance = Field.Create("EUR Balance", "EURBalance", null, true,
|
||||
"Enter the amount of EUR you want to have.");
|
||||
var fakeUSDBalance = Field.Create("USD Balance", "USDBalance", null, true,
|
||||
"Enter the amount of USD you want to have.");
|
||||
|
||||
balancesFieldset.Label = "Fake balances";
|
||||
balancesFieldset.Fields.Add(fakeBTCBalance);
|
||||
balancesFieldset.Fields.Add(fakeLTCBalance);
|
||||
balancesFieldset.Fields.Add(fakeEURBalance);
|
||||
balancesFieldset.Fields.Add(fakeUSDBalance);
|
||||
form.Fields.Add(balancesFieldset);
|
||||
|
||||
return Task.FromResult(form);
|
||||
}
|
||||
|
||||
private FakeCustodianConfig ParseConfig(JObject config)
|
||||
{
|
||||
return config?.ToObject<FakeCustodianConfig>() ?? throw new InvalidOperationException("Invalid config");
|
||||
}
|
||||
|
||||
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (paymentMethod.Equals(ValidPaymentMethod))
|
||||
{
|
||||
DepositAddressData r = new() { Address = "3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" };
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string[] GetDepositablePaymentMethods()
|
||||
{
|
||||
return new[] { ValidPaymentMethod };
|
||||
}
|
||||
|
||||
public async Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO Store fake withdrawals in the DB so we can have a history of withdrawals
|
||||
if (ValidWithdrawalPaymentMethod.Equals(paymentMethod))
|
||||
{
|
||||
LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, -amount, LedgerEntryData.LedgerEntryType.Withdrawal);
|
||||
LedgerEntryData ledgerEntryFee = new(ValidAsset, -_btcWithdrawalFee, LedgerEntryData.LedgerEntryType.Fee);
|
||||
List<LedgerEntryData> ledgerEntries = new();
|
||||
ledgerEntries.Add(ledgerEntryWithdrawal);
|
||||
ledgerEntries.Add(ledgerEntryFee);
|
||||
|
||||
var fakeConfig = ParseConfig(config);
|
||||
if (amount <= fakeConfig.BTCBalance)
|
||||
{
|
||||
fakeConfig.BTCBalance -= amount;
|
||||
|
||||
if (_btcWithdrawalFee <= fakeConfig.BTCBalance)
|
||||
{
|
||||
fakeConfig.BTCBalance -= _btcWithdrawalFee;
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(fakeConfig.StoreId, fakeConfig.CustodianAccountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
{
|
||||
// We could not load the custodian account using the config settings, so they are bad and should be reported to the user so he can fix them.
|
||||
throw new BadConfigException(new[] { "StoreId", "CustodianAccountId" });
|
||||
}
|
||||
|
||||
var newConfig = JObject.FromObject(fakeConfig);
|
||||
custodianAccount.SetBlob(newConfig);
|
||||
await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
|
||||
var r = new WithdrawResult(paymentMethod, ValidAsset, ledgerEntries, ValidWithdrawalId, WithdrawalResponseData.WithdrawalStatus.Queued, DateTimeOffset.Now, TargetAddress, TransactionId);
|
||||
return r;
|
||||
}
|
||||
CustodianApiException e3 = new(400, "insufficient-funds", "Cannot withdraw " + amount + " " + ValidAsset + " because you don't have enough to pay for fees");
|
||||
throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e3);
|
||||
}
|
||||
|
||||
CustodianApiException e1 = new(400, "insufficient-funds", "Cannot withdraw " + amount + " " + ValidAsset + " because you only hold " + fakeConfig.BTCBalance + " " + ValidAsset);
|
||||
throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e1);
|
||||
}
|
||||
|
||||
CustodianApiException e2 = new(400, "only-btc-supported", "The Fake Custodian can only withdraw using payment method " + ValidWithdrawalPaymentMethod);
|
||||
throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e2);
|
||||
}
|
||||
|
||||
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
if (ValidWithdrawalPaymentMethod.Equals(paymentMethod))
|
||||
{
|
||||
LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, -qty, LedgerEntryData.LedgerEntryType.Withdrawal);
|
||||
LedgerEntryData ledgerEntryFee = new(ValidAsset, new decimal(-0.001), LedgerEntryData.LedgerEntryType.Fee);
|
||||
List<LedgerEntryData> ledgerEntries = new();
|
||||
ledgerEntries.Add(ledgerEntryWithdrawal);
|
||||
ledgerEntries.Add(ledgerEntryFee);
|
||||
|
||||
var fakeConfig = ParseConfig(config);
|
||||
var r = new SimulateWithdrawalResult(paymentMethod, ValidAsset, ledgerEntries, new decimal(0.001), fakeConfig.BTCBalance);
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
CustodianApiException e = new(400, "only-btc-onchain-supported", "The Fake Custodian can only withdraw using payment method " + ValidWithdrawalPaymentMethod);
|
||||
throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e);
|
||||
}
|
||||
|
||||
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO make this Fake Custodian smarter and store previous fake withdrawals in the DB
|
||||
if (ValidWithdrawalPaymentMethod.Equals(paymentMethod) && withdrawalId.Equals(ValidWithdrawalId))
|
||||
{
|
||||
LedgerEntryData ledgerEntryWithdrawal = new(ValidAsset, _validWithdrawalAmount, LedgerEntryData.LedgerEntryType.Withdrawal);
|
||||
LedgerEntryData ledgerEntryFee = new(ValidAsset, new decimal(0.001), LedgerEntryData.LedgerEntryType.Fee);
|
||||
List<LedgerEntryData> ledgerEntries = new();
|
||||
ledgerEntries.Add(ledgerEntryWithdrawal);
|
||||
ledgerEntries.Add(ledgerEntryFee);
|
||||
|
||||
var r = new WithdrawResult(paymentMethod, ValidAsset, ledgerEntries, ValidWithdrawalId, WithdrawalResponseData.WithdrawalStatus.Queued, DateTimeOffset.Now, TargetAddress, TransactionId);
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
CustodianApiException e = new(400, "withdrawal-not-found", "The Fake Custodian can only fetch withdrawal ID " + ValidWithdrawalId);
|
||||
throw new CannotWithdrawException(this, paymentMethod, TargetAddress, e);
|
||||
}
|
||||
|
||||
public string[] GetWithdrawablePaymentMethods()
|
||||
{
|
||||
return new[] { ValidPaymentMethod };
|
||||
}
|
||||
|
||||
|
||||
public List<AssetPairData> GetTradableAssetPairs()
|
||||
{
|
||||
// We only support trading BTC -> EUR and EUR -> BTC
|
||||
var r = new List<AssetPairData>();
|
||||
r.Add(new AssetPairData(ValidAsset, "EUR", (decimal)0.0001));
|
||||
r.Add(new AssetPairData("EUR", ValidAsset, (decimal)5));
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
public async Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO store fake traded in the DB + Update the balances so this fake custodian behaves like the real thing.
|
||||
if ((fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset)) || (fromAsset.Equals(ValidAsset) && toAsset.Equals("EUR")))
|
||||
{
|
||||
// We only support trading BTC -> EUR and EUR -> BTC
|
||||
|
||||
var fakeConfig = ParseConfig(config);
|
||||
|
||||
if (fromAsset.Equals("BTC") && qty > fakeConfig.BTCBalance)
|
||||
{
|
||||
throw new InsufficientFundsException($"Insufficient funds. You only have {fakeConfig.BTCBalance} to trade.");
|
||||
}
|
||||
|
||||
if (fromAsset.Equals("EUR") && qty > fakeConfig.EURBalance)
|
||||
{
|
||||
throw new InsufficientFundsException($"Insufficient funds. You only have {fakeConfig.EURBalance} to trade.");
|
||||
}
|
||||
|
||||
decimal rate;
|
||||
|
||||
rate = getRate(fromAsset, toAsset);
|
||||
var qtyReceived = qty / rate;
|
||||
|
||||
var ledgerEntries = new List<LedgerEntryData>();
|
||||
ledgerEntries.Add(new LedgerEntryData(fromAsset, -qty, LedgerEntryData.LedgerEntryType.Trade));
|
||||
ledgerEntries.Add(new LedgerEntryData(toAsset, qtyReceived, LedgerEntryData.LedgerEntryType.Trade));
|
||||
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee));
|
||||
|
||||
|
||||
if (fromAsset.Equals("BTC"))
|
||||
{
|
||||
fakeConfig.BTCBalance -= qty;
|
||||
}
|
||||
|
||||
if (fromAsset.Equals("EUR"))
|
||||
{
|
||||
fakeConfig.EURBalance -= qty;
|
||||
}
|
||||
|
||||
if (toAsset.Equals("BTC"))
|
||||
{
|
||||
fakeConfig.BTCBalance += qtyReceived;
|
||||
}
|
||||
|
||||
if (toAsset.Equals("EUR"))
|
||||
{
|
||||
fakeConfig.EURBalance += qtyReceived;
|
||||
}
|
||||
|
||||
// Fees are always in EUR... for now...
|
||||
if (_tradeFeeEuro <= fakeConfig.EURBalance)
|
||||
{
|
||||
fakeConfig.EURBalance -= _tradeFeeEuro;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InsufficientFundsException($"Insufficient funds. You don't have enough EUR to pay for fees.");
|
||||
}
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(fakeConfig.StoreId, fakeConfig.CustodianAccountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
{
|
||||
// We could not load the custodian account using the config settings, so they are bad and should be reported to the user so he can fix them.
|
||||
throw new BadConfigException(new[] { "StoreId", "CustodianAccountId" });
|
||||
}
|
||||
|
||||
var newConfig = JObject.FromObject(fakeConfig);
|
||||
custodianAccount.SetBlob(newConfig);
|
||||
await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
|
||||
|
||||
return new MarketTradeResult(fromAsset, toAsset, ledgerEntries, ValidTradeId);
|
||||
}
|
||||
|
||||
throw new WrongTradingPairException(fromAsset, toAsset);
|
||||
}
|
||||
|
||||
private static decimal getRate(string fromAsset, string toAsset)
|
||||
{
|
||||
decimal rate;
|
||||
if (fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset))
|
||||
{
|
||||
rate = _btcPriceInEuro;
|
||||
}
|
||||
else
|
||||
{
|
||||
rate = 1 / _btcPriceInEuro;
|
||||
}
|
||||
|
||||
return rate;
|
||||
}
|
||||
|
||||
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO load the transaction from the DB which contains previous fake trades
|
||||
if (tradeId == ValidTradeId)
|
||||
{
|
||||
var ledgerEntries = new List<LedgerEntryData>();
|
||||
ledgerEntries.Add(new LedgerEntryData(ValidAsset, _tradeQtyBought, LedgerEntryData.LedgerEntryType.Trade));
|
||||
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeQtyBought * _btcPriceInEuro, LedgerEntryData.LedgerEntryType.Trade));
|
||||
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * _tradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee));
|
||||
var r = new MarketTradeResult(TradeFromAsset, TradeToAsset, ledgerEntries, ValidTradeId);
|
||||
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
return Task.FromResult<MarketTradeResult>(null);
|
||||
}
|
||||
|
||||
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO use the current market price for a realistic price
|
||||
|
||||
if ((fromAsset.Equals("EUR") && toAsset.Equals(ValidAsset)) || (fromAsset.Equals(ValidAsset) && toAsset.Equals("EUR")))
|
||||
{
|
||||
// We only support trading BTC -> EUR and EUR -> BTC
|
||||
decimal rate = getRate(fromAsset, toAsset);
|
||||
return Task.FromResult(new AssetQuoteResult(fromAsset, toAsset, rate, rate));
|
||||
}
|
||||
|
||||
throw new WrongTradingPairException(fromAsset, toAsset);
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeCustodianConfig
|
||||
{
|
||||
public string CustodianAccountId { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public decimal BTCBalance { get; set; }
|
||||
public decimal LTCBalance { get; set; }
|
||||
public decimal USDBalance { get; set; }
|
||||
public decimal EURBalance { get; set; }
|
||||
|
||||
public FakeCustodianConfig()
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -1,20 +0,0 @@
|
|||
#if DEBUG
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Plugins.FakeCustodian;
|
||||
|
||||
public class FakeCustodianPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override string Identifier { get; } = "BTCPayServer.Plugins.Custodians.Fake";
|
||||
public override string Name { get; } = "Custodian: Fake";
|
||||
public override string Description { get; } = "Adds a fake custodian for testing";
|
||||
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<FakeCustodian>();
|
||||
services.AddSingleton<ICustodian, FakeCustodian>(provider => provider.GetRequiredService<FakeCustodian>());
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -1,70 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Services.Custodian.Client
|
||||
{
|
||||
public class CustodianAccountRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _contextFactory;
|
||||
|
||||
public CustodianAccountRepository(ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task<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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Models.CustodianAccountViewModels.CreateCustodianAccountViewModel
|
||||
@{
|
||||
ViewData.SetActivePage( CustodianAccountsNavPages.Create, "Add a custodian account");
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<form asp-action="CreateCustodianAccount">
|
||||
<input asp-for="Config" type="hidden" />
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.SelectedCustodian))
|
||||
{
|
||||
<partial name="_FormTopMessages" model="Model.ConfigForm" />
|
||||
}
|
||||
else if(Model.Custodians.Count() > 0)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedCustodian" class="form-label" data-required></label>
|
||||
<select asp-for="SelectedCustodian" asp-items="Model.Custodians" class="form-select"></select>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>No custodians available. Install some plugins to add custodian / exchange support.</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.SelectedCustodian))
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedCustodian" class="form-label" data-required></label>
|
||||
<select disabled asp-for="SelectedCustodian" asp-items="Model.Custodians" class="form-select"></select>
|
||||
<input type="hidden" asp-for="SelectedCustodian" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label" data-required></label>
|
||||
<input asp-for="Name" class="form-control" required/>
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<partial name="_Form" model="Model.ConfigForm" />
|
||||
}
|
||||
|
||||
@if (Model.Custodians.Count() > 0)
|
||||
{
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" value="Continue" class="btn btn-primary" id="Continue"/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
namespace BTCPayServer.Views.CustodianAccounts
|
||||
{
|
||||
public enum CustodianAccountsNavPages
|
||||
{
|
||||
View, Create, Update
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Models
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Models.CustodianAccountViewModels.EditCustodianAccountViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(CustodianAccountsNavPages.Update, "Edit Custodian account: " + @Model?.CustodianAccount.Name, Model?.CustodianAccount.Id);
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<form asp-action="EditCustodianAccount" class="mb-5">
|
||||
<input asp-for="Config" type="hidden" />
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
}
|
||||
<partial name="_FormTopMessages" model="Model.ConfigForm"/>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="CustodianAccount.Name" class="form-label" data-required></label>
|
||||
<input asp-for="CustodianAccount.Name" class="form-control" required/>
|
||||
<span asp-validation-for="CustodianAccount.Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<partial name="_Form" model="Model.ConfigForm"/>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" value="Continue" class="btn btn-primary" id="Save"/>
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="DeleteCustodianAccount" asp-route-storeId="@Model.CustodianAccount.StoreId" asp-route-accountId="@Model.CustodianAccount.Id" class="btn btn-outline-danger" role="button" id="DeleteCustodianAccountConfig" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The custodian account <strong>@Html.Encode(Model.CustodianAccount.Name)</strong> will be permanently deleted." data-confirm-input="DELETE">
|
||||
<span class="fa fa-trash"></span> Delete this custodian account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_Confirm" model="@(new ConfirmModel("Delete custodian account", "The custodian account will be permanently deleted.", "Delete"))" />
|
|
@ -1,551 +0,0 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Custodians
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model BTCPayServer.Models.CustodianAccountViewModels.ViewCustodianAccountViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(CustodianAccountsNavPages.View, "Custodian account: " + @Model?.CustodianAccount.Name, Model.CustodianAccount.Id);
|
||||
Csp.UnsafeEval();
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
{
|
||||
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
}
|
||||
|
||||
<style>
|
||||
.trade-qty label{display: block; }
|
||||
</style>
|
||||
|
||||
<div id="custodianAccountView" v-cloak>
|
||||
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<h2 class="mb-0">
|
||||
@ViewData["Title"]
|
||||
</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-primary" role="button" v-if="account?.depositablePaymentMethods?.length > 0" v-on:click="openDepositModal()" href="#">
|
||||
<span class="fa fa-download"></span> Deposit
|
||||
</a>
|
||||
<a asp-action="EditCustodianAccount" asp-route-storeId="@Model.CustodianAccount.StoreId" asp-route-accountId="@Model.CustodianAccount.Id" class="btn btn-primary" role="button" id="EditCustodianAccountConfig">
|
||||
<span class="fa fa-gear"></span> Configure
|
||||
</a>
|
||||
<!--
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
<a class="btn btn-secondary" id="ViewApp" target="app_" href="/apps/MQ2sCVsmQ95JBZ4aZDtoSwMAnBY/pos">View</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div v-if="!account" class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="account">
|
||||
<p class="alert alert-danger" v-if="account.assetBalanceExceptionMessage">
|
||||
{{ account.assetBalanceExceptionMessage }}
|
||||
</p>
|
||||
|
||||
<h2>Balances</h2>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="hideDustAmounts" id="hideDustAmounts">
|
||||
<label class="form-check-label" for="hideDustAmounts">
|
||||
Hide holdings worth less than {{ account.dustThresholdInFiat }} {{ account.storeDefaultFiat }}.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th class="text-end">Balance</th>
|
||||
<th class="text-end">Unit Price (Bid)</th>
|
||||
<th class="text-end">Unit Price (Ask)</th>
|
||||
<th class="text-end">Fiat Value</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in sortedAssetRows" :key="row.asset">
|
||||
<td>{{ row.asset }}</td>
|
||||
<!-- TODO format as number? How? -->
|
||||
<th class="text-end">{{ row.formattedQty }}</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedBid }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedAsk }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedFiatValue }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
<a v-if="row.tradableAssetPairs" v-on:click="openTradeModal(row)" href="#">Trade</a>
|
||||
<a v-if="canDepositAsset(row.asset)" v-on:click="openDepositModal(row)" href="#">Deposit</a>
|
||||
<a v-if="row.withdrawablePaymentMethods.length" v-on:click="openWithdrawModal(row)" href="#">Withdraw</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalances.length === 0">
|
||||
<td colspan="999" class="text-center">No assets are stored with this custodian (yet).</td>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalances.length > 0 && sortedAssetRows.length === 0">
|
||||
<td colspan="999" class="text-center">No holdings are worth more than {{ account.dustThresholdInFiat }} {{ account.storeDefaultFiat }}.</td>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalanceExceptionMessage !== null">
|
||||
<td colspan="999" class="text-center">An error occured while loading assets and balances.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Features</h2>
|
||||
<p>The @Model?.Custodian.Name custodian supports:</p>
|
||||
<ul>
|
||||
<li>Viewing asset balances</li>
|
||||
@if (Model?.Custodian is ICanTrade)
|
||||
{
|
||||
<li>Converting assets using market orders</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanDeposit)
|
||||
{
|
||||
<li>Depositing</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanWithdraw)
|
||||
{
|
||||
<li>Withdrawing</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="withdrawModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" id="withdrawForm" v-on:submit="onWithdrawSubmit" method="post" asp-action="Withdraw" asp-route-accountId="@Model?.CustodianAccount?.Id" asp-route-storeId="@Model?.CustodianAccount?.StoreId">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Withdraw {{ withdraw.qty}} {{ withdraw.asset }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" v-if="!withdraw.isExecuting">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="withdraw.isExecuting">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!withdraw.isExecuting && withdraw.results === null">
|
||||
|
||||
<p v-if="withdraw.errorMsg" class="alert alert-danger">{{ withdraw.errorMsg }}</p>
|
||||
<div v-if="withdraw.badConfigFields?.length > 0" class="alert alert-danger">
|
||||
Please go to <a class="alert-link" asp-action="EditCustodianAccount" asp-route-storeId="@Model.CustodianAccount.StoreId" asp-route-accountId="@Model.CustodianAccount.Id">Configure</a> and fill out these fields to enable withdrawals for {{ withdraw.paymentMethod }}.
|
||||
<ul>
|
||||
<li v-for="badConfigField in withdraw.badConfigFields">
|
||||
{{ badConfigField }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawAsset">Asset to Withdraw</label>
|
||||
<select class="form-control" v-model="withdraw.asset" id="WithdrawAsset">
|
||||
<option v-for="option in availableAssetsToWithdraw" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawPaymentMethod">Payment Method</label>
|
||||
<select class="form-control" v-model="withdraw.paymentMethod" id="WithdrawPaymentMethod">
|
||||
<option v-for="option in availablePaymentMethodsToWithdraw" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawDestination">Destination</label>
|
||||
<select class="form-control" id="WithdrawDestination">
|
||||
<option selected="selected">
|
||||
Store wallet
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawQty">Quantity</label>
|
||||
<input type="number" :min="withdraw.minQty" :max="withdraw.maxQty" step="any" class="form-control" v-model="withdraw.qty" id="WithdrawQty"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="btn-group" role="group" aria-label="Set qty to a percentage of holdings">
|
||||
<button v-on:click="setWithdrawQtyPercent(10)" class="btn btn-secondary" type="button">10%</button>
|
||||
<button v-on:click="setWithdrawQtyPercent(25)" class="btn btn-secondary" type="button">25%</button>
|
||||
<button v-on:click="setWithdrawQtyPercent(50)" class="btn btn-secondary" type="button">50%</button>
|
||||
<button v-on:click="setWithdrawQtyPercent(75)" class="btn btn-secondary" type="button">75%</button>
|
||||
<button v-on:click="setWithdrawQtyPercent(90)" class="btn btn-secondary" type="button">90%</button>
|
||||
<button v-on:click="setWithdrawQtyPercent(100)" class="btn btn-secondary" type="button">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="withdraw.badConfigFields?.length === 0 && !withdraw.isUpdating">
|
||||
<div class="form-group">
|
||||
<span v-if="withdrawFees.length === 0">
|
||||
No fee for this withdrawal
|
||||
</span>
|
||||
<span v-if="withdrawFees.length === 1">
|
||||
Fee: {{ -1 * withdrawFees[0].qty}} {{ withdrawFees[0].asset}}
|
||||
<span v-if="trade.priceForPair[withdrawFees[0].asset + '/' + account.storeDefaultFiat ]">
|
||||
or {{ -1 * withdrawFees[0].qty * trade.priceForPair[withdrawFees[0].asset + '/' + account.storeDefaultFiat ] }} {{ account.storeDefaultFiat }}
|
||||
</span>
|
||||
</span>
|
||||
<ul v-if="withdrawFees.length > 1">
|
||||
<li v-for="entry in withdrawFees" v-if="entry.type === 'Fee'">
|
||||
{{ entry.qty}} {{ entry.asset}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p v-if="canExecuteWithdrawal">
|
||||
After withdrawing {{ account.assetBalances[withdraw.asset].qty - withdraw.qty }} {{ withdraw.asset }} will remain in your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div v-if="withdraw.results !== null">
|
||||
|
||||
<p class="alert alert-success" v-if="withdraw.results.status === 'Queued' || withdraw.results.status === 'Unknown'">
|
||||
Successfully requested withdrawal.
|
||||
</p>
|
||||
<p class="alert alert-success" v-if="withdraw.results.status === 'Complete'">
|
||||
Successfully completed withdrawal.
|
||||
</p>
|
||||
<p class="alert alert-danger" v-if="withdraw.results.status === 'Failed'">
|
||||
Withdrawal failed.
|
||||
</p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Asset</th>
|
||||
<th>Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in withdraw.results.ledgerEntries">
|
||||
<td class="text-end" v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }"><span v-if="entry.qty > 0">+</span>{{ entry.qty }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">{{ entry.asset }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">
|
||||
<span v-if="entry.type !== 'Trade'">{{ entry.type}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- TODO Add "Copy to Clipboard" buttons -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawalId">Withdrawal ID</label>
|
||||
<input type="text" class="form-control" v-model="withdraw.results.withdrawalId" id="WithdrawalId" readonly/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawalTransactionId">Transaction ID</label>
|
||||
<input type="text" class="form-control" v-model="withdraw.results.transactionId" id="WithdrawalTransactionId" readonly/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="WithdrawalTargetAddress">Target Address</label>
|
||||
<input type="text" class="form-control" v-model="withdraw.results.targetAddress" id="WithdrawalTargetAddress" readonly/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="!withdraw.isExecuting">
|
||||
<div class="modal-footer-left">
|
||||
<span v-if="withdraw.isUpdating">
|
||||
Updating...
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary" v-if="canExecuteWithdrawal">Withdraw</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="depositModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Deposit</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-if="deposit.errorMsg" class="alert alert-danger">{{ deposit.errorMsg }}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="DepositAsset">Asset to Deposit</label>
|
||||
<select class="form-select" v-model="deposit.asset" name="DepositAsset">
|
||||
<option v-for="option in availableAssetsToDeposit" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="DepositPaymentNetwork">Payment Method</label>
|
||||
<select class="form-select" v-model="deposit.paymentMethod" id="DepositPaymentNetwork">
|
||||
<option v-for="option in availablePaymentMethodsToDeposit" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="deposit.isLoading">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!deposit.isLoading && (deposit.link || deposit.address)">
|
||||
<div class="tab-content text-center">
|
||||
<div v-if="deposit.link" class="tab-pane" id="link-tab" role="tabpanel">
|
||||
<div class="qr-container mb-3">
|
||||
<img :src="deposit.cryptoImageUrl" class="qr-icon" :alt="deposit.asset"/>
|
||||
<qrcode v-bind:value="deposit.link" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"></qrcode>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group" :data-clipboard="deposit.link">
|
||||
<input type="text" class="form-control" readonly="readonly" :value="deposit.link" id="payment-link"/>
|
||||
<button type="button" class="btn btn-outline-secondary px-3">
|
||||
<vc:icon symbol="copy"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="deposit.address" class="tab-pane show active" id="address-tab" role="tabpanel">
|
||||
<div class="qr-container mb-3">
|
||||
<img v-bind:src="deposit.cryptoImageUrl" class="qr-icon" :alt="deposit.asset"/>
|
||||
<qrcode v-bind:value="deposit.address" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"></qrcode>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group" :data-clipboard="deposit.address">
|
||||
<input type="text" class="form-control" readonly="readonly" :value="deposit.address" id="address"/>
|
||||
<button type="button" class="btn btn-outline-secondary px-3">
|
||||
<vc:icon symbol="copy"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav justify-content-center">
|
||||
<a v-if="deposit.address" :class="{active: deposit.tab === 'address' }" class="btcpay-pill" data-bs-toggle="tab" href="#address-tab">Address</a>
|
||||
<a v-if="deposit.link" :class="{active: deposit.tab === 'link' }" class="btcpay-pill" data-bs-toggle="tab" href="#link-tab">Link</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<a v-if="deposit.createTransactionUrl" class="btn btn-primary" :href="deposit.createTransactionUrl">
|
||||
Create Transaction
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="tradeModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" id="tradeForm" v-on:submit="onTradeSubmit" method="post" asp-action="Trade" asp-route-accountId="@Model?.CustodianAccount?.Id" asp-route-storeId="@Model?.CustodianAccount?.StoreId">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Trade {{ trade.qty }} {{ trade.fromAsset }} into {{ trade.toAsset }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" v-if="!trade.isExecuting">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="trade.isExecuting">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!trade.isExecuting && trade.results === null">
|
||||
<p v-if="trade.errorMsg" class="alert alert-danger">{{ trade.errorMsg }}</p>
|
||||
|
||||
<div class="row mb-2 trade-qty">
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Convert
|
||||
<div class="input-group has-validation">
|
||||
<!--
|
||||
getMinQtyToTrade() = {{ getMinQtyToTrade(this.trade.toAsset, this.trade.fromAsset) }}
|
||||
<br/>
|
||||
Max Qty to Trade = {{ trade.maxQty }}
|
||||
-->
|
||||
<input name="Qty" type="number" min="0" step="any" :max="trade.maxQty" :min="getMinQtyToTrade()" class="form-control qty" v-bind:class="{ 'is-invalid': trade.qty < getMinQtyToTrade() || trade.qty > trade.maxQty }" v-model="trade.qty"/>
|
||||
<select name="FromAsset" v-model="trade.fromAsset" class="form-select">
|
||||
<option v-for="option in availableAssetsToTrade" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-center text-center">
|
||||
|
||||
<br/>
|
||||
<button v-if="canSwapTradeAssets()" type="button" class="btn btn-secondary btn-square" v-on:click="swapTradeAssets()" aria-label="Swap assets">
|
||||
<i class="fa fa-arrows-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Into
|
||||
<div class="input-group">
|
||||
<input disabled="disabled" type="number" class="form-control qty" v-model="tradeQtyToReceive"/>
|
||||
<select name="ToAsset" v-model="trade.toAsset" class="form-select">
|
||||
<option v-for="option in availableAssetsToTradeInto" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Set qty to a percentage of holdings">
|
||||
<button v-on:click="setTradeQtyPercent(10)" class="btn btn-secondary" type="button">10%</button>
|
||||
<button v-on:click="setTradeQtyPercent(25)" class="btn btn-secondary" type="button">25%</button>
|
||||
<button v-on:click="setTradeQtyPercent(50)" class="btn btn-secondary" type="button">50%</button>
|
||||
<button v-on:click="setTradeQtyPercent(75)" class="btn btn-secondary" type="button">75%</button>
|
||||
<button v-on:click="setTradeQtyPercent(90)" class="btn btn-secondary" type="button">90%</button>
|
||||
<button v-on:click="setTradeQtyPercent(100)" class="btn btn-secondary" type="button">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="trade.price">
|
||||
<br/>
|
||||
1 {{ trade.toAsset }} = {{ trade.price }} {{ trade.fromAsset }}
|
||||
<br/>
|
||||
1 {{ trade.fromAsset }} = {{ 1 / trade.price }} {{ trade.toAsset }}
|
||||
</p>
|
||||
<p v-if="canExecuteTrade">
|
||||
After the trade
|
||||
{{ trade.maxQty - trade.qty }} {{ trade.fromAsset }} will remain in your account.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
<p>
|
||||
trade.priceForPair = {{ trade.priceForPair }}
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Min Qty to Trade</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/BTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/BTC']}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
<div class="form-text">Final results may vary due to trading fees and slippage.</div>
|
||||
</div>
|
||||
<div v-if="trade.results !== null">
|
||||
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Asset</th>
|
||||
<th>Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in trade.results.ledgerEntries">
|
||||
<td class="text-end" v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }"><span v-if="entry.qty > 0">+</span>{{ entry.qty }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">{{ entry.asset }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">
|
||||
<span v-if="entry.type !== 'Trade'">{{ entry.type}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Trade ID: {{ trade.results.tradeId }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="!trade.isExecuting">
|
||||
<div class="modal-footer-left">
|
||||
<span v-if="trade.isUpdating">
|
||||
Updating quote...
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<span v-if="trade.results">Close</span>
|
||||
<span v-if="!trade.results">Cancel</span>
|
||||
</button>
|
||||
<button v-if="canExecuteTrade" type="submit" class="btn btn-primary">Execute</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script type="text/javascript">
|
||||
var ajaxBalanceUrl = "@Url.Action("ViewCustodianAccountJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxTradeSimulateUrl = "@Url.Action("SimulateTradeJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxDepositUrl = "@Url.Action("GetDepositPrepareJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxWithdrawSimulateUrl = "@Url.Action("SimulateWithdrawJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxWithdrawUrl = "@Url.Action("Withdraw", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
</script>
|
||||
<script src="~/js/custodian-account.js" asp-append-version="true"></script>
|
|
@ -149,6 +149,8 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@*
|
||||
Let's uncomment this when we have new experimental features.
|
||||
<div class="d-flex align-items-center my-3">
|
||||
<input asp-for="Experimental" type="checkbox" class="btcpay-toggle me-3" />
|
||||
<div>
|
||||
|
@ -160,7 +162,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> *@
|
||||
</div>
|
||||
|
||||
<h4 class="mb-3">Plugins</h4>
|
||||
|
|
|
@ -1,683 +0,0 @@
|
|||
new Vue({
|
||||
el: '#custodianAccountView',
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
},
|
||||
data: {
|
||||
account: null,
|
||||
hideDustAmounts: true,
|
||||
modals: {
|
||||
trade: null,
|
||||
withdraw: null,
|
||||
deposit: null
|
||||
},
|
||||
deposit: {
|
||||
asset: null,
|
||||
paymentMethod: null,
|
||||
address: null,
|
||||
link: null,
|
||||
errorMsg: null,
|
||||
cryptoImageUrl: null,
|
||||
tab: null,
|
||||
isLoading: false
|
||||
},
|
||||
trade: {
|
||||
row: null,
|
||||
results: null,
|
||||
errorMsg: null,
|
||||
isExecuting: false,
|
||||
isUpdating: false,
|
||||
simulationAbortController: null,
|
||||
priceRefresherInterval: null,
|
||||
fromAsset: null,
|
||||
toAsset: null,
|
||||
qty: null,
|
||||
maxQty: null,
|
||||
price: null,
|
||||
priceForPair: {}
|
||||
},
|
||||
withdraw: {
|
||||
asset: null,
|
||||
paymentMethod: null,
|
||||
errorMsg: null,
|
||||
qty: null,
|
||||
minQty: null,
|
||||
maxQty: null,
|
||||
badConfigFields: null,
|
||||
results: null,
|
||||
isUpdating: null,
|
||||
isExecuting: false,
|
||||
simulationAbortController: null,
|
||||
ledgerEntries: null
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tradeQtyToReceive: function () {
|
||||
return this.trade.qty / this.trade.price;
|
||||
},
|
||||
canExecuteTrade: function () {
|
||||
return this.trade.qty >= this.getMinQtyToTrade() && this.trade.price !== null && this.trade.fromAsset !== null && this.trade.toAsset !== null && !this.trade.isExecuting && this.trade.results === null;
|
||||
},
|
||||
availableAssetsToTrade: function () {
|
||||
let r = [];
|
||||
let balances = this?.account?.assetBalances;
|
||||
if (balances) {
|
||||
let t = this;
|
||||
let rows = Object.values(balances);
|
||||
rows = rows.filter(function (row) {
|
||||
return row.fiatValue > t.account.dustThresholdInFiat;
|
||||
});
|
||||
|
||||
for (let i in rows) {
|
||||
r.push(rows[i].asset);
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
availableAssetsToTradeInto: function () {
|
||||
let r = [];
|
||||
let pairs = this.account?.assetBalances?.[this.trade.fromAsset]?.tradableAssetPairs;
|
||||
if (pairs) {
|
||||
for (let i in pairs) {
|
||||
let pair = pairs[i];
|
||||
if (pair.assetBought === this.trade.fromAsset) {
|
||||
r.push(pair.assetSold);
|
||||
} else if (pair.assetSold === this.trade.fromAsset) {
|
||||
r.push(pair.assetBought);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
availableAssetsToDeposit: function () {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
let r = [];
|
||||
if (paymentMethods && paymentMethods.length > 0) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let asset = paymentMethods[i].split("-")[0];
|
||||
if (r.indexOf(asset) === -1) {
|
||||
r.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
availablePaymentMethodsToDeposit: function () {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
let r = [];
|
||||
if (Array.isArray(paymentMethods)) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let pm = paymentMethods[i];
|
||||
let asset = pm.split("-")[0];
|
||||
if (asset === this.deposit.asset) {
|
||||
r.push(pm);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
sortedAssetRows: function () {
|
||||
if (this.account?.assetBalances) {
|
||||
let rows = Object.values(this.account.assetBalances);
|
||||
let t = this;
|
||||
|
||||
if (this.hideDustAmounts) {
|
||||
rows = rows.filter(function (row) {
|
||||
return row.fiatValue === null || row.fiatValue > t.account.dustThresholdInFiat;
|
||||
});
|
||||
}
|
||||
|
||||
rows = rows.sort(function (a, b) {
|
||||
if(b.fiatValue !== null && a.fiatValue !== null){
|
||||
return b.fiatValue - a.fiatValue;
|
||||
}else if(b.fiatValue !== null && a.fiatValue === null){
|
||||
return 1;
|
||||
}else if(b.fiatValue === null && a.fiatValue !== null) {
|
||||
return -1;
|
||||
}else{
|
||||
return b.asset.localeCompare(a.asset);
|
||||
}
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
},
|
||||
canExecuteWithdrawal: function () {
|
||||
return (this.withdraw.minQty != null && this.withdraw.qty >= this.withdraw.minQty)
|
||||
&& (this.withdraw.maxQty != null && this.withdraw.qty <= this.withdraw.maxQty)
|
||||
&& this.withdraw.badConfigFields?.length === 0
|
||||
&& this.withdraw.paymentMethod
|
||||
&& !this.withdraw.isExecuting
|
||||
&& !this.withdraw.isUpdating
|
||||
&& this.withdraw.results === null;
|
||||
},
|
||||
availableAssetsToWithdraw: function () {
|
||||
let r = [];
|
||||
const balances = this?.account?.assetBalances;
|
||||
if (balances) {
|
||||
for (let asset in balances) {
|
||||
const balance = balances[asset];
|
||||
if (balance?.withdrawablePaymentMethods?.length) {
|
||||
r.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
return r.sort();
|
||||
},
|
||||
availablePaymentMethodsToWithdraw: function () {
|
||||
if (this.withdraw.asset) {
|
||||
let paymentMethods = this?.account?.assetBalances?.[this.withdraw.asset]?.withdrawablePaymentMethods;
|
||||
if (paymentMethods) {
|
||||
return paymentMethods.sort();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
withdrawFees: function(){
|
||||
let r = [];
|
||||
if(this.withdraw.ledgerEntries){
|
||||
for (let i = 0; i< this.withdraw.ledgerEntries.length; i++){
|
||||
let entry = this.withdraw.ledgerEntries[i];
|
||||
if(entry.type === 'Fee'){
|
||||
r.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getMaxQty: function (fromAsset) {
|
||||
let row = this.account?.assetBalances?.[fromAsset];
|
||||
if (row) {
|
||||
return row.qty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getMinQtyToTrade: function (fromAsset = this.trade.fromAsset, toAsset = this.trade.toAsset) {
|
||||
if (fromAsset && toAsset && this.account?.assetBalances) {
|
||||
for (let asset in this.account.assetBalances) {
|
||||
let row = this.account.assetBalances[asset];
|
||||
|
||||
let pairCode = fromAsset + "/" + toAsset;
|
||||
let pairCodeReverse = toAsset + "/" + fromAsset;
|
||||
|
||||
let pair = row.tradableAssetPairs?.[pairCode];
|
||||
let pairReverse = row.tradableAssetPairs?.[pairCodeReverse];
|
||||
|
||||
if (pair !== null || pairReverse !== null) {
|
||||
if (pair && !pairReverse) {
|
||||
return pair.minimumTradeQty;
|
||||
} else if (!pair && pairReverse) {
|
||||
let price = this.trade.priceForPair?.[pairCode];
|
||||
if (!price) {
|
||||
return null;
|
||||
}
|
||||
// if (reverse) {
|
||||
// return price / pairReverse.minimumTradeQty;
|
||||
// }else {
|
||||
return price * pairReverse.minimumTradeQty;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
setTradeQtyPercent: function (percent) {
|
||||
this.trade.qty = percent / 100 * this.trade.maxQty;
|
||||
},
|
||||
setWithdrawQtyPercent: function (percent) {
|
||||
this.withdraw.qty = percent / 100 * this.withdraw.maxQty;
|
||||
},
|
||||
openTradeModal: function (row) {
|
||||
let _this = this;
|
||||
this.trade.row = row;
|
||||
this.trade.results = null;
|
||||
this.trade.errorMsg = null;
|
||||
this.trade.fromAsset = row.asset;
|
||||
if (row.asset === this.account.storeDefaultFiat) {
|
||||
this.trade.toAsset = "BTC";
|
||||
} else {
|
||||
this.trade.toAsset = this.account.storeDefaultFiat;
|
||||
}
|
||||
|
||||
this.trade.qty = row.qty;
|
||||
this.trade.maxQty = row.qty;
|
||||
this.trade.price = row.bid;
|
||||
|
||||
if (this.modals.trade === null) {
|
||||
this.modals.trade = new window.bootstrap.Modal('#tradeModal');
|
||||
|
||||
// Disable price refreshing when modal closes...
|
||||
const tradeModelElement = document.getElementById('tradeModal')
|
||||
tradeModelElement.addEventListener('hide.bs.modal', event => {
|
||||
_this.setTradePriceRefresher(false);
|
||||
});
|
||||
}
|
||||
|
||||
this.setTradePriceRefresher(true);
|
||||
this.modals.trade.show();
|
||||
},
|
||||
openWithdrawModal: function (row) {
|
||||
this.withdraw.asset = row.asset;
|
||||
this.withdraw.qty = row.qty;
|
||||
this.withdraw.paymentMethod = null;
|
||||
this.withdraw.minQty = 0;
|
||||
this.withdraw.maxQty = row.qty;
|
||||
this.withdraw.results = null;
|
||||
this.withdraw.errorMsg = null;
|
||||
this.withdraw.isUpdating = null;
|
||||
this.withdraw.isExecuting = false;
|
||||
|
||||
if (this.modals.withdraw === null) {
|
||||
this.modals.withdraw = new window.bootstrap.Modal('#withdrawModal');
|
||||
}
|
||||
|
||||
this.modals.withdraw.show();
|
||||
},
|
||||
openDepositModal: function (row) {
|
||||
if (this.modals.deposit === null) {
|
||||
this.modals.deposit = new window.bootstrap.Modal('#depositModal');
|
||||
}
|
||||
if (row) {
|
||||
this.deposit.asset = row.asset;
|
||||
} else if (!this.deposit.asset && this.availableAssetsToDeposit.length > 0) {
|
||||
this.deposit.asset = this.availableAssetsToDeposit[0];
|
||||
}
|
||||
|
||||
this.modals.deposit.show();
|
||||
},
|
||||
onTradeSubmit: async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.currentTarget;
|
||||
const url = form.getAttribute('action');
|
||||
const method = form.getAttribute('method');
|
||||
|
||||
this.trade.isExecuting = true;
|
||||
|
||||
// Prevent the modal from closing by clicking outside or via the keyboard
|
||||
this.setModalCanBeClosed(this.modals.trade, false);
|
||||
|
||||
const _this = this;
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': this.getRequestVerificationToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fromAsset: _this.trade.fromAsset,
|
||||
toAsset: _this.trade.toAsset,
|
||||
qty: _this.trade.qty
|
||||
})
|
||||
});
|
||||
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
_this.trade.results = data;
|
||||
_this.trade.errorMsg = null;
|
||||
|
||||
_this.setTradePriceRefresher(false);
|
||||
_this.refreshAccountBalances();
|
||||
} else {
|
||||
_this.trade.errorMsg = data && data.message || "Error";
|
||||
}
|
||||
_this.setModalCanBeClosed(_this.modals.trade, true);
|
||||
_this.trade.isExecuting = false;
|
||||
},
|
||||
|
||||
onWithdrawSubmit: async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.currentTarget;
|
||||
const url = form.getAttribute('action');
|
||||
const method = form.getAttribute('method');
|
||||
|
||||
this.withdraw.isExecuting = true;
|
||||
this.setModalCanBeClosed(this.modals.withdraw, false);
|
||||
|
||||
let dataToSubmit = {
|
||||
paymentMethod: this.withdraw.paymentMethod,
|
||||
qty: this.withdraw.qty
|
||||
};
|
||||
|
||||
const _this = this;
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': this.getRequestVerificationToken()
|
||||
},
|
||||
body: JSON.stringify(dataToSubmit)
|
||||
});
|
||||
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
_this.withdraw.results = data;
|
||||
_this.withdraw.errorMsg = null;
|
||||
|
||||
_this.refreshAccountBalances();
|
||||
} else {
|
||||
_this.withdraw.errorMsg = data && data.message || "Error";
|
||||
}
|
||||
_this.setModalCanBeClosed(_this.modals.withdraw, true);
|
||||
_this.withdraw.isExecuting = false;
|
||||
},
|
||||
|
||||
setTradePriceRefresher: function (enabled) {
|
||||
if (enabled) {
|
||||
// Update immediately...
|
||||
this.updateTradePrice();
|
||||
|
||||
// And keep updating every few seconds...
|
||||
let _this = this;
|
||||
this.trade.priceRefresherInterval = setInterval(function () {
|
||||
_this.updateTradePrice();
|
||||
}, 5000);
|
||||
|
||||
} else {
|
||||
clearInterval(this.trade.priceRefresherInterval);
|
||||
}
|
||||
},
|
||||
|
||||
updateTradePrice: function () {
|
||||
if (!this.trade.fromAsset || !this.trade.toAsset) {
|
||||
// We need to know the 2 assets or we cannot do anything...
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.trade.fromAsset === this.trade.toAsset) {
|
||||
// The 2 assets must be different
|
||||
this.trade.price = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.trade.isUpdating) {
|
||||
// Previous request is still running. No need to hammer the server
|
||||
return;
|
||||
}
|
||||
|
||||
this.trade.isUpdating = true;
|
||||
|
||||
let dataToSubmit = {
|
||||
fromAsset: this.trade.fromAsset,
|
||||
toAsset: this.trade.toAsset,
|
||||
qty: this.trade.qty
|
||||
};
|
||||
let url = window.ajaxTradeSimulateUrl;
|
||||
|
||||
this.trade.simulationAbortController = new AbortController();
|
||||
|
||||
let _this = this;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(dataToSubmit),
|
||||
signal: this.trade.simulationAbortController.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': this.getRequestVerificationToken()
|
||||
}
|
||||
}
|
||||
).then(function (response) {
|
||||
_this.trade.isUpdating = false;
|
||||
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
// _this.trade.results = data;
|
||||
// _this.trade.errorMsg = null; }
|
||||
// Do nothing on error
|
||||
}
|
||||
).then(function (data) {
|
||||
_this.trade.maxQty = data.maxQty;
|
||||
|
||||
// By default trade everything
|
||||
if (_this.trade.qty === null) {
|
||||
_this.trade.qty = _this.trade.maxQty;
|
||||
}
|
||||
|
||||
// Cannot trade more than what we have
|
||||
if (data.maxQty < _this.trade.qty) {
|
||||
_this.trade.qty = _this.trade.maxQty;
|
||||
}
|
||||
let pair = data.toAsset + "/" + data.fromAsset;
|
||||
let pairReverse = data.fromAsset + "/" + data.toAsset;
|
||||
|
||||
// TODO Should we use "bid" in some cases? The spread can be huge with some shitcoins.
|
||||
_this.trade.price = data.ask;
|
||||
_this.trade.priceForPair[pair] = data.ask;
|
||||
_this.trade.priceForPair[pairReverse] = 1 / data.ask;
|
||||
|
||||
}).catch(function (e) {
|
||||
_this.trade.isUpdating = false;
|
||||
if (e instanceof DOMException && e.code === DOMException.ABORT_ERR) {
|
||||
// User aborted fetch request
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
},
|
||||
canDepositAsset: function (asset) {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
if (paymentMethods && paymentMethods.length > 0) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let pmParts = paymentMethods[i].split("-");
|
||||
if (asset === pmParts[0]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
canSwapTradeAssets: function () {
|
||||
let minQtyToTrade = this.getMinQtyToTrade(this.trade.toAsset, this.trade.fromAsset);
|
||||
let assetToTradeIntoHoldings = this.account?.assetBalances?.[this.trade.toAsset];
|
||||
if (assetToTradeIntoHoldings) {
|
||||
return assetToTradeIntoHoldings.qty >= minQtyToTrade;
|
||||
}
|
||||
},
|
||||
swapTradeAssets: function () {
|
||||
// Swap the 2 assets
|
||||
let tmp = this.trade.fromAsset;
|
||||
this.trade.fromAsset = this.trade.toAsset;
|
||||
this.trade.toAsset = tmp;
|
||||
this.trade.price = 1 / this.trade.price;
|
||||
|
||||
this.refreshTradeSimulation();
|
||||
},
|
||||
refreshTradeSimulation: function () {
|
||||
let maxQty = this.getMaxQty(this.trade.fromAsset);
|
||||
this.trade.qty = maxQty
|
||||
this.trade.maxQty = maxQty;
|
||||
|
||||
if(this.trade.simulationAbortController) {
|
||||
this.trade.simulationAbortController.abort();
|
||||
}
|
||||
|
||||
// Update the price asap, so we can continue
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.updateTradePrice();
|
||||
}, 100);
|
||||
},
|
||||
refreshAccountBalances: function () {
|
||||
let _this = this;
|
||||
fetch(window.ajaxBalanceUrl).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (result) {
|
||||
_this.account = result;
|
||||
|
||||
for(let asset in _this.account.assetBalances){
|
||||
let assetInfo = _this.account.assetBalances[asset];
|
||||
|
||||
if(asset !== _this.account.storeDefaultFiat) {
|
||||
let pair1 = asset + '/' + _this.account.storeDefaultFiat;
|
||||
_this.trade.priceForPair[pair1] = assetInfo.bid;
|
||||
|
||||
let pair2 = _this.account.storeDefaultFiat + '/' + asset;
|
||||
_this.trade.priceForPair[pair2] = 1 / assetInfo.bid;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
getRequestVerificationToken: function () {
|
||||
return document.querySelector("input[name='__RequestVerificationToken']").value;
|
||||
},
|
||||
setModalCanBeClosed: function (modal, flag) {
|
||||
modal._config.keyboard = flag;
|
||||
if (flag) {
|
||||
modal._config.backdrop = true;
|
||||
} else {
|
||||
modal._config.backdrop = 'static';
|
||||
}
|
||||
},
|
||||
refreshWithdrawalSimulation: function () {
|
||||
if(!this.withdraw.paymentMethod || !this.withdraw.qty){
|
||||
// We are missing required data, stop now.
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.withdraw.simulationAbortController) {
|
||||
this.withdraw.simulationAbortController.abort();
|
||||
}
|
||||
|
||||
let data = {
|
||||
paymentMethod: this.withdraw.paymentMethod,
|
||||
qty: this.withdraw.qty
|
||||
};
|
||||
const _this = this;
|
||||
const token = this.getRequestVerificationToken();
|
||||
|
||||
this.withdraw.isUpdating = true;
|
||||
this.withdraw.simulationAbortController = new AbortController();
|
||||
fetch(window.ajaxWithdrawSimulateUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
signal: this.withdraw.simulationAbortController.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': token
|
||||
}
|
||||
}).then(function (response) {
|
||||
_this.withdraw.isUpdating = false;
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
if (data.minQty === null) {
|
||||
_this.withdraw.minQty = 0;
|
||||
} else {
|
||||
_this.withdraw.minQty = data.minQty;
|
||||
}
|
||||
if (data.maxQty === null) {
|
||||
_this.withdraw.maxQty = _this.account.assetBalances?.[_this.withdraw.asset]?.qty;
|
||||
} else {
|
||||
_this.withdraw.maxQty = data.maxQty;
|
||||
}
|
||||
|
||||
if (_this.withdraw.qty === null || _this.withdraw.qty > _this.withdraw.maxQty) {
|
||||
_this.withdraw.qty = _this.withdraw.maxQty;
|
||||
}
|
||||
_this.withdraw.badConfigFields = data.badConfigFields;
|
||||
_this.withdraw.errorMsg = data.errorMessage;
|
||||
_this.withdraw.ledgerEntries = data.ledgerEntries;
|
||||
});
|
||||
},
|
||||
getStoreDefaultFiatValueForAsset: function(asset){
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'trade.fromAsset': function (newValue, oldValue) {
|
||||
if (newValue === this.trade.toAsset) {
|
||||
// This is the same as swapping the 2 assets
|
||||
this.trade.toAsset = oldValue;
|
||||
this.trade.price = 1 / this.trade.price;
|
||||
|
||||
this.refreshTradeSimulation();
|
||||
}
|
||||
if (newValue !== oldValue) {
|
||||
// The qty is going to be wrong, so set to 100%
|
||||
this.trade.qty = this.getMaxQty(this.trade.fromAsset);
|
||||
}
|
||||
},
|
||||
'deposit.asset': function (newValue, oldValue) {
|
||||
if (this.availablePaymentMethodsToDeposit.length > 0) {
|
||||
this.deposit.paymentMethod = this.availablePaymentMethodsToDeposit[0];
|
||||
} else {
|
||||
this.deposit.paymentMethod = null;
|
||||
}
|
||||
},
|
||||
'deposit.paymentMethod': function (newValue, oldValue) {
|
||||
let _this = this;
|
||||
const token = this.getRequestVerificationToken();
|
||||
this.deposit.isLoading = true;
|
||||
fetch(window.ajaxDepositUrl + "?paymentMethod=" + encodeURI(this.deposit.paymentMethod), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': token
|
||||
}
|
||||
}).then(function (response) {
|
||||
_this.deposit.isLoading = false;
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
_this.deposit.address = data.address;
|
||||
_this.deposit.link = data.link;
|
||||
_this.deposit.createTransactionUrl = data.createTransactionUrl;
|
||||
_this.deposit.cryptoImageUrl = data.cryptoImageUrl;
|
||||
|
||||
if (!_this.deposit.tab) {
|
||||
_this.deposit.tab = 'address';
|
||||
}
|
||||
if (_this.deposit.tab === 'address' && !_this.deposit.address && _this.deposit.link) {
|
||||
// Tab "address" is not available, but tab "link" is.
|
||||
_this.deposit.tab = 'link';
|
||||
}
|
||||
|
||||
_this.deposit.errorMsg = data.errorMessage;
|
||||
});
|
||||
},
|
||||
'withdraw.asset': function (newValue, oldValue) {
|
||||
if (this.availablePaymentMethodsToWithdraw.length > 0) {
|
||||
this.withdraw.paymentMethod = this.availablePaymentMethodsToWithdraw[0];
|
||||
} else {
|
||||
this.withdraw.paymentMethod = null;
|
||||
}
|
||||
},
|
||||
'withdraw.paymentMethod': function (newValue, oldValue) {
|
||||
if (this.withdraw.paymentMethod && this.withdraw.qty) {
|
||||
this.withdraw.minQty = 0;
|
||||
this.withdraw.maxQty = null;
|
||||
this.withdraw.errorMsg = null;
|
||||
this.withdraw.badConfigFields = null;
|
||||
|
||||
this.refreshWithdrawalSimulation();
|
||||
}
|
||||
},
|
||||
'withdraw.qty': function (newValue, oldValue) {
|
||||
if (newValue > this.withdraw.maxQty) {
|
||||
this.withdraw.qty = this.withdraw.maxQty;
|
||||
}
|
||||
this.refreshWithdrawalSimulation();
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.refreshAccountBalances();
|
||||
},
|
||||
mounted: function () {
|
||||
// Runs when the app is ready
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "BTCPay Greenfield API",
|
||||
"version": "v1",
|
||||
"description": "# Introduction\n\nThe BTCPay Server Greenfield API is a REST API. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n# Authentication\n\nYou can authenticate either via Basic Auth or an API key. It's recommended to use an API key for better security. You can create an API key in the BTCPay Server UI under `Account` -> `Manage Account` -> `API keys`. You can restrict the API key for one or multiple stores and for specific permissions. For testing purposes, you can give it the 'Unrestricted access' permission. On production you should limit the permissions to the actual endpoints you use, you can see the required permission on the API docs at the top of each endpoint under `AUTHORIZATIONS`.\n\nIf you want to simplify the process of creating API keys for your users, you can use the [Authorization endpoint](https:\/\/docs.btcpayserver.org\/API\/Greenfield\/v1\/#tag\/Authorization) to predefine permissions and redirect your users to the BTCPay Server Authorization UI. You can find more information about this on the [API Authorization Flow docs](https:\/\/docs.btcpayserver.org\/BTCPayServer\/greenfield-authorization\/) page.\n\n# Usage examples\n\nUse **Basic Auth** to read store information with cURL:\n```bash\nBTCPAY_INSTANCE=\"https:\/\/mainnet.demo.btcpayserver.org\"\nUSER=\"MyTestUser@gmail.com\"\nPASSWORD=\"notverysecurepassword\"\nPERMISSION=\"btcpay.store.canmodifystoresettings\"\nBODY=\"$(echo \"{}\" | jq --arg \"a\" \"$PERMISSION\" '. + {permissions:[$a]}')\"\n\nAPI_KEY=\"$(curl -s \\\n -H \"Content-Type: application\/json\" \\\n --user \"$USER:$PASSWORD\" \\\n -X POST \\\n -d \"$BODY\" \\\n \"$BTCPAY_INSTANCE\/api\/v1\/api-keys\" | jq -r .apiKey)\"\n```\n\n\nUse an **API key** to read store information with cURL:\n```bash\nSTORE_ID=\"yourStoreId\"\n\ncurl -s \\\n -H \"Content-Type: application\/json\" \\\n -H \"Authorization: token $API_KEY\" \\\n -X GET \\\n \"$BTCPAY_INSTANCE\/api\/v1\/stores\/$STORE_ID\"\n```\n\nYou can find more examples on our docs for different programming languages:\n- [cURL](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample\/)\n- [Javascript\/Node.Js](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-NodeJS\/)\n- [PHP](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-PHP\/)\n\n",
|
||||
"description": "# Introduction\n\nThe BTCPay Server Greenfield API is a REST API. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n# Authentication\n\nYou can authenticate either via Basic Auth or an API key. It's recommended to use an API key for better security. You can create an API key in the BTCPay Server UI under `Account` -> `Manage Account` -> `API keys`. You can restrict the API key for one or multiple stores and for specific permissions. For testing purposes, you can give it the 'Unrestricted access' permission. On production you should limit the permissions to the actual endpoints you use, you can see the required permission on the API docs at the top of each endpoint under `AUTHORIZATIONS`.\n\nIf you want to simplify the process of creating API keys for your users, you can use the [Authorization endpoint](https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Authorization) to predefine permissions and redirect your users to the BTCPay Server Authorization UI. You can find more information about this on the [API Authorization Flow docs](https://docs.btcpayserver.org/BTCPayServer/greenfield-authorization/) page.\n\n# Usage examples\n\nUse **Basic Auth** to read store information with cURL:\n```bash\nBTCPAY_INSTANCE=\"https://mainnet.demo.btcpayserver.org\"\nUSER=\"MyTestUser@gmail.com\"\nPASSWORD=\"notverysecurepassword\"\nPERMISSION=\"btcpay.store.canmodifystoresettings\"\nBODY=\"$(echo \"{}\" | jq --arg \"a\" \"$PERMISSION\" '. + {permissions:[$a]}')\"\n\nAPI_KEY=\"$(curl -s \\\n -H \"Content-Type: application/json\" \\\n --user \"$USER:$PASSWORD\" \\\n -X POST \\\n -d \"$BODY\" \\\n \"$BTCPAY_INSTANCE/api/v1/api-keys\" | jq -r .apiKey)\"\n```\n\n\nUse an **API key** to read store information with cURL:\n```bash\nSTORE_ID=\"yourStoreId\"\n\ncurl -s \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: token $API_KEY\" \\\n -X GET \\\n \"$BTCPAY_INSTANCE/api/v1/stores/$STORE_ID\"\n```\n\nYou can find more examples on our docs for different programming languages:\n- [cURL](https://docs.btcpayserver.org/Development/GreenFieldExample/)\n- [Javascript/Node.Js](https://docs.btcpayserver.org/Development/GreenFieldExample-NodeJS/)\n- [PHP](https://docs.btcpayserver.org/Development/GreenFieldExample-PHP/)\n\n",
|
||||
"contact": {
|
||||
"name": "BTCPay Server",
|
||||
"url": "https://btcpayserver.org"
|
||||
|
@ -128,7 +128,7 @@
|
|||
"securitySchemes": {
|
||||
"API_Key": {
|
||||
"type": "apiKey",
|
||||
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.canviewusers`: View users\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.canviewcustodianaccounts`: View exchange accounts linked to your stores\n* `btcpay.store.canmanagecustodianaccounts`: Manage exchange accounts linked to your stores\n* `btcpay.store.candeposittocustodianaccount`: Deposit funds to exchange accounts linked to your stores\n* `btcpay.store.canwithdrawfromcustodianaccount`: Withdraw funds from exchange accounts to your store\n* `btcpay.store.cantradecustodianaccount`: Trade funds on your store's exchange accounts\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n",
|
||||
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.canviewusers`: View users\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
},
|
||||
|
|
|
@ -327,10 +327,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"btcpay.store.canmodifystoresettings",
|
||||
"btcpay.store.cantradecustodianaccount",
|
||||
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
]
|
||||
},
|
||||
"isServerRole": {
|
||||
|
|
Loading…
Add table
Reference in a new issue