btcpayserver/BTCPayServer.Tests/MockCustodian/MockCustodian.cs
Andrew Camilleri 76a6d94bbe
Exchange api no kraken (#3679)
* WIP New APIs for dealing with custodians/exchanges

* Simplified things

* More API refinements + index.html file for quick viewing

* Finishing touches on spec

* Switched cryptoCode to paymentMethod as this allows us to differentiate between onchain and lightning

* Moved draft API docs to "/docs-draft"

* WIP baby steps

* Added DB migration for CustodianAccountData

* Rough but working POST /v1/api/custodian-account + GET /v1/api/custodian

* WIP + early Kraken API client

* Moved service registration to proper location

* Working create + list custodian accounts + permissions + WIP Kraken client

* Kraken API Balances call is working

* Added asset balances to response

* List Custodian Accounts call does not load assetBalances by default, because it can fail. Can be requested when needed.

* Call to get the details of 1 specific custodian account

* Added permissions to swagger

* Added "tradableAssetPairs" to Kraken custodian response + cache the tradable pairs in memory for 24 hours

* Removed unused file

* WIP + Moved files to better locations

* Updated docs

* Working API endpoint to get info on a trade (same response as creating a new trade)

* Working API endpoints for Deposit + Trade + untested Withdraw

* Delete custodian account

* Trading works, better error handling, cleanup

* Working withdrawals + New endpoint for getting bid/ask prices

* Completed withdrawals + new endpoint for getting info on a past withdrawal to simplify testing, Enums are output as strings,

* Better error handling when withdrawing to a wrong destination

* WithdrawalAddressName in config is now a string per currency (dictionary)

* Added TODOs

* Only show the custodian account "config" to users who are allowed

* Added the new permissions to the API Keys UI

* Renamed KrakenClient to KrakenExchange

* WIP Kraken Config Form

* Removed files for UI again, will make separate PR later

* Fixed docs + Refactored to use PaymentMethod more + Added "name" to custodian account + Using cancelationToken everywhere

* Updated withdrawal info docs

* First unit test

* Complete tests for /api/v1/custodians and /api/v1/custodian-accounts endpoints + Various improvements and fixes

* Mock custodian and more exceptions

* Many more tests + cleanup, moved files to better locations

* More tests

* WIP more tests

* Greenfield API tests complete

* Added missing "Name" column

* Cleanup, TODOs and beginning of Kraken Tests

* Added Kraken tests using public endpoints + handling of "SATS" currency

* Added 1st mocked Kraken API call: GetAssetBalancesAsync

* Added assert for bad config

* Mocked more Kraken API responses + added CreationDate to withdrawal response

* pr review club changes

* Make Kraken Custodian a plugin

* Re-added User-Agent header as it is required

* Fixed bug in market trade on Kraken using a percentage as qty

* A short delay so Kraken has the time to execute the market order and we don't fetch the details too quickly.

* Merged the draft swagger into the main swagger since it didn't work anymore

* Fixed API permissions test

* Removed 2 TODOs

* Fixed unit test

* Remove Kraken Api as it should be separate opt-in plugin

* Flatten namespace hierarchy and use InnerExeption instead of OriginalException

* Remove useless line

* Make sure account is from a specific store

* Proper error if custodian code not found

* Remove various warnings

* Remove various warnings

* Handle CustodianApiException through an exception filter

* Store custodian-account blob directly

* Remove duplications, transform methods into property

* Improve docs tags

* Make sure the custodianCode saved is canonical

* Fix test

Co-authored-by: Wouter Samaey <wouter.samaey@storefront.be>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2022-05-18 14:59:56 +09:00

161 lines
6.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Custodian.Client.MockCustodian;
public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
{
public const string DepositPaymentMethod = "BTC-OnChain";
public const string DepositAddress = "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
public const string TradeId = "TRADE-ID-001";
public const string TradeFromAsset = "EUR";
public const string TradeToAsset = "BTC";
public static readonly decimal TradeQtyBought = new decimal(1);
public static readonly decimal TradeFeeEuro = new decimal(12.5);
public static readonly decimal BtcPriceInEuro = new decimal(30000);
public const string WithdrawalPaymentMethod = "BTC-OnChain";
public const string WithdrawalAsset = "BTC";
public const string WithdrawalId = "WITHDRAWAL-ID-001";
public static readonly decimal WithdrawalAmount = new decimal(0.5);
public static readonly decimal WithdrawalFee = new decimal(0.0005);
public const string WithdrawalTransactionId = "yyy";
public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";
public const WithdrawalResponseData.WithdrawalStatus WithdrawalStatus = WithdrawalResponseData.WithdrawalStatus.Queued;
public static readonly decimal BalanceBTC = new decimal(1.23456);
public static readonly decimal BalanceLTC = new decimal(50.123456);
public static readonly decimal BalanceUSD = new decimal(1500.55);
public static readonly decimal BalanceEUR = new decimal(1235.15);
public string Code
{
get => "mock";
}
public string Name
{
get => "MOCK Exchange";
}
public Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken)
{
var r = new Dictionary<string, decimal>()
{
{ "BTC", BalanceBTC }, { "LTC", BalanceLTC }, { "USD", BalanceUSD }, { "EUR", BalanceEUR },
};
return Task.FromResult(r);
}
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod.Equals(DepositPaymentMethod))
{
var r = new DepositAddressData();
r.Address = DepositAddress;
return Task.FromResult(r);
}
throw new CustodianFeatureNotImplementedException($"Only BTC-OnChain is implemented for {this.Name}");
}
public string[] GetDepositablePaymentMethods()
{
return new[] { "BTC-OnChain" };
}
public List<AssetPairData> GetTradableAssetPairs()
{
var r = new List<AssetPairData>();
r.Add(new AssetPairData("BTC", "EUR"));
return r;
}
private MarketTradeResult GetMarketTradeResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData("BTC", TradeQtyBought, LedgerEntryData.LedgerEntryType.Trade));
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeQtyBought * BtcPriceInEuro, LedgerEntryData.LedgerEntryType.Trade));
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee));
return new MarketTradeResult(TradeFromAsset, TradeToAsset, ledgerEntries, TradeId);
}
public Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken)
{
if (!fromAsset.Equals("EUR") || !toAsset.Equals("BTC"))
{
throw new WrongTradingPairException(fromAsset, toAsset);
}
if (qty != TradeQtyBought)
{
throw new InsufficientFundsException($"With {Name}, you can only buy {TradeQtyBought} {TradeToAsset} with {TradeFromAsset} and nothing else.");
}
return Task.FromResult(GetMarketTradeResult());
}
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken)
{
if (tradeId == TradeId)
{
return Task.FromResult(GetMarketTradeResult());
}
return Task.FromResult<MarketTradeResult>(null);
}
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken)
{
if (fromAsset.Equals(TradeFromAsset) && toAsset.Equals(TradeToAsset))
{
return Task.FromResult(new AssetQuoteResult(TradeFromAsset, TradeToAsset, BtcPriceInEuro, BtcPriceInEuro));
}
throw new WrongTradingPairException(fromAsset, toAsset);
//throw new AssetQuoteUnavailableException(pair);
}
private WithdrawResult CreateWithdrawResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
DateTimeOffset createdTime = new DateTimeOffset(2021, 9, 1, 6, 45, 0, new TimeSpan(-7, 0, 0));
var r = new WithdrawResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalId, WithdrawalStatus, createdTime, WithdrawalTargetAddress, WithdrawalTransactionId);
return r;
}
public Task<WithdrawResult> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount == WithdrawalAmount)
{
return Task.FromResult(CreateWithdrawResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
}
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
}
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken)
{
if (withdrawalId == WithdrawalId && WithdrawalPaymentMethod.Equals(paymentMethod))
{
return Task.FromResult(CreateWithdrawResult());
}
return Task.FromResult<WithdrawResult>(null);
}
public string[] GetWithdrawablePaymentMethods()
{
return GetDepositablePaymentMethods();
}
}