Price reminder and init approval
* Start price reminder grain * Add config and init grain at startup * Save init wallet when already init
This commit is contained in:
@@ -23,7 +23,7 @@ namespace Managing.Infrastructure.Exchanges
|
||||
{
|
||||
return new Candle()
|
||||
{
|
||||
Ticker = ticker.ToString(),
|
||||
Ticker = ticker,
|
||||
Timeframe = timeframe,
|
||||
Volume = candle.Volume,
|
||||
Close = candle.ClosePrice,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using Binance.Net.Interfaces.Clients;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Shared;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Infrastructure.Exchanges.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Exchanges;
|
||||
|
||||
public class ExchangeStream : IExchangeStream
|
||||
{
|
||||
private readonly ILogger<StreamService> logger;
|
||||
private readonly IBinanceSocketClient _binanceSocketClient;
|
||||
|
||||
public ExchangeStream(IBinanceSocketClient binanceSocketClient, ILogger<StreamService> logger)
|
||||
{
|
||||
_binanceSocketClient = binanceSocketClient;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartBinanceWorker(Ticker ticker, Func<Candle, Task> action)
|
||||
{
|
||||
logger.LogInformation($"Starting binance worker for {ticker}");
|
||||
|
||||
await _binanceSocketClient.SpotApi.ExchangeData.SubscribeToKlineUpdatesAsync(BinanceHelpers.ToBinanceTicker(ticker), Binance.Net.Enums.KlineInterval.OneSecond, candle =>
|
||||
{
|
||||
if (candle.Data.Data?.Final == true)
|
||||
{
|
||||
//action((candle) => { CandleHelpers.Map(candle.Data.Data, ticker, Timeframe.FiveMinutes)});
|
||||
action(CandleHelpers.Map(candle.Data.Data, ticker, Timeframe.FiveMinutes));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task StopBinanceWorker()
|
||||
{
|
||||
logger.LogInformation($"Stoping all Binance worker subscription");
|
||||
await _binanceSocketClient.UnsubscribeAllAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
using Binance.Net.Enums;
|
||||
using Binance.Net.Interfaces;
|
||||
using Binance.Net.Objects.Models.Futures;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Exchanges.Helpers
|
||||
{
|
||||
public static class BinanceHelpers
|
||||
{
|
||||
public static Trade Map(BinanceFuturesOrder data)
|
||||
{
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
return new Trade(data.CreateTime,
|
||||
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
|
||||
(TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
|
||||
data.Quantity, data.AveragePrice, 1,
|
||||
data.ClientOrderId, "");
|
||||
}
|
||||
|
||||
public static FuturesOrderType BinanceOrderTypeMap(TradeType tradeType)
|
||||
{
|
||||
switch (tradeType)
|
||||
{
|
||||
case TradeType.Limit:
|
||||
return FuturesOrderType.Limit;
|
||||
case TradeType.Market:
|
||||
return FuturesOrderType.Market;
|
||||
case TradeType.StopMarket:
|
||||
return FuturesOrderType.StopMarket;
|
||||
case TradeType.StopLossLimit:
|
||||
return FuturesOrderType.Stop;
|
||||
default:
|
||||
return FuturesOrderType.Limit;
|
||||
}
|
||||
}
|
||||
|
||||
public static TradeType BinanceOrderTradeType(FuturesOrderType orderType)
|
||||
{
|
||||
switch (orderType)
|
||||
{
|
||||
case FuturesOrderType.Stop:
|
||||
case FuturesOrderType.Limit:
|
||||
return TradeType.Limit;
|
||||
case FuturesOrderType.Market:
|
||||
return TradeType.Market;
|
||||
case FuturesOrderType.StopMarket:
|
||||
return TradeType.StopMarket;
|
||||
default:
|
||||
return TradeType.Limit;
|
||||
}
|
||||
}
|
||||
|
||||
public static Trade Map(WebCallResult<BinanceFuturesPlacedOrder> result, decimal? leverage = null)
|
||||
{
|
||||
var data = result.Data;
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return new Trade(DateTime.Now, TradeDirection.None,
|
||||
TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0,
|
||||
"", result.Error?.Message);
|
||||
}
|
||||
|
||||
return new Trade(DateTime.Now, TradeDirection.None,
|
||||
TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0,
|
||||
"", result.Error?.Message);
|
||||
}
|
||||
|
||||
public static Candle Map(IBinanceKline binanceKline, Ticker ticker, TradingExchanges exchange)
|
||||
{
|
||||
return new Candle
|
||||
{
|
||||
Date = binanceKline.CloseTime,
|
||||
Volume = binanceKline.Volume,
|
||||
Close = binanceKline.ClosePrice,
|
||||
High = binanceKline.HighPrice,
|
||||
Low = binanceKline.LowPrice,
|
||||
Open = binanceKline.OpenPrice,
|
||||
Ticker = ticker.ToString(),
|
||||
OpenTime = binanceKline.OpenTime,
|
||||
Exchange = exchange
|
||||
};
|
||||
}
|
||||
|
||||
internal static KlineInterval Map(Timeframe interval) => interval switch
|
||||
{
|
||||
Timeframe.FiveMinutes => KlineInterval.FiveMinutes,
|
||||
Timeframe.FifteenMinutes => KlineInterval.FifteenMinutes,
|
||||
Timeframe.ThirtyMinutes => KlineInterval.ThirtyMinutes,
|
||||
Timeframe.OneHour => KlineInterval.OneHour,
|
||||
Timeframe.FourHour => KlineInterval.FourHour,
|
||||
Timeframe.OneDay => KlineInterval.OneDay,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
public static string ToBinanceTicker(Ticker ticker)
|
||||
{
|
||||
switch (ticker)
|
||||
{
|
||||
case Ticker.ADA:
|
||||
return "ADAUSDT";
|
||||
case Ticker.ALGO:
|
||||
return "ALGOUSDT";
|
||||
case Ticker.ATOM:
|
||||
return "ATOMUSDT";
|
||||
case Ticker.AVAX:
|
||||
return "AVAXUSDT";
|
||||
case Ticker.BNB:
|
||||
return "BNBUSDT";
|
||||
case Ticker.BTC:
|
||||
return "BTCUSDT";
|
||||
case Ticker.CRV:
|
||||
return "CRVUSDT";
|
||||
case Ticker.DOGE:
|
||||
return "DOGEUSDT";
|
||||
case Ticker.DOT:
|
||||
return "DOTUSDT";
|
||||
case Ticker.DYDX:
|
||||
return "DYDXUSDT";
|
||||
case Ticker.ETC:
|
||||
return "ETCUSDT";
|
||||
case Ticker.ETH:
|
||||
return "ETHUSDT";
|
||||
case Ticker.FTM:
|
||||
return "FTMUSDT";
|
||||
case Ticker.GALA:
|
||||
return "GALAUSDT";
|
||||
case Ticker.GRT:
|
||||
return "GRTUSDT";
|
||||
case Ticker.IMX:
|
||||
return "IMXUSDT";
|
||||
case Ticker.KSM:
|
||||
return "KSMUSDT";
|
||||
case Ticker.LINK:
|
||||
return "LINKUSDT";
|
||||
case Ticker.LRC:
|
||||
return "LRCUSDT";
|
||||
case Ticker.LTC:
|
||||
return "LTCUSDT";
|
||||
case Ticker.MATIC:
|
||||
return "MATICUSDT";
|
||||
case Ticker.MKR:
|
||||
return "MKRUSDT";
|
||||
case Ticker.NEAR:
|
||||
return "NEARUSDT";
|
||||
case Ticker.SAND:
|
||||
return "SANDUSDT";
|
||||
case Ticker.SOL:
|
||||
return "SOLUSDT";
|
||||
case Ticker.SRM:
|
||||
return "SRMUSDT";
|
||||
case Ticker.SUSHI:
|
||||
return "SUSHIUSDT";
|
||||
case Ticker.THETA:
|
||||
return "THETAUSDT";
|
||||
case Ticker.UNI:
|
||||
return "UNIUSDT";
|
||||
case Ticker.XMR:
|
||||
return "XMRUSDT";
|
||||
case Ticker.XRP:
|
||||
return "XRPUSDT";
|
||||
case Ticker.XTZ:
|
||||
return "XTZUSDT";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal static object Map(WebCallResult<BinanceUsdFuturesOrder> binanceResult, decimal? leverage)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BinanceFuturesPlacedOrder
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using FTX.Net.Enums;
|
||||
using FTX.Net.Objects.Models;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Exchanges.Helpers
|
||||
{
|
||||
public static class FtxHelpers
|
||||
{
|
||||
public static string ToFtxTicker(Ticker ticker)
|
||||
{
|
||||
switch (ticker)
|
||||
{
|
||||
case Ticker.ADA:
|
||||
return "ADA-PERP";
|
||||
case Ticker.APE:
|
||||
return "APE-PERP";
|
||||
case Ticker.ALGO:
|
||||
return "ALGO-PERP";
|
||||
case Ticker.ATOM:
|
||||
return "ATOM-PERP";
|
||||
case Ticker.AVAX:
|
||||
return "AVAX-PERP";
|
||||
case Ticker.BNB:
|
||||
return "BNB-PERP";
|
||||
case Ticker.BTC:
|
||||
return "BTC-PERP";
|
||||
case Ticker.BAL:
|
||||
return "BAL-PERP";
|
||||
case Ticker.CHZ:
|
||||
return "CHZ-PERP";
|
||||
case Ticker.COMP:
|
||||
return "COMP-PERP";
|
||||
case Ticker.CRO:
|
||||
return "CRO-PERP";
|
||||
case Ticker.CRV:
|
||||
return "CRV-PERP";
|
||||
case Ticker.DOGE:
|
||||
return "DOGE-PERP";
|
||||
case Ticker.DOT:
|
||||
return "DOT-PERP";
|
||||
case Ticker.DYDX:
|
||||
return "DYDX-PERP";
|
||||
case Ticker.ENS:
|
||||
return "ENS-PERP";
|
||||
case Ticker.ETC:
|
||||
return "ETC-PERP";
|
||||
case Ticker.ETH:
|
||||
return "ETH-PERP";
|
||||
case Ticker.FIL:
|
||||
return "FIL-PERP";
|
||||
case Ticker.FLM:
|
||||
return "FLM-PERP";
|
||||
case Ticker.FTM:
|
||||
return "FTM-PERP";
|
||||
case Ticker.GALA:
|
||||
return "GALA-PERP";
|
||||
case Ticker.GRT:
|
||||
return "GRT-PERP";
|
||||
case Ticker.KSM:
|
||||
return "KSM-PERP";
|
||||
case Ticker.LDO:
|
||||
return "LDO-PERP";
|
||||
case Ticker.LINK:
|
||||
return "LINK-PERP";
|
||||
case Ticker.LRC:
|
||||
return "LRC-PERP";
|
||||
case Ticker.LTC:
|
||||
return "LTC-PERP";
|
||||
case Ticker.MANA:
|
||||
return "MANA-PERP";
|
||||
case Ticker.MATIC:
|
||||
return "MATIC-PERP";
|
||||
case Ticker.MKR:
|
||||
return "MKR-PERP";
|
||||
case Ticker.NEAR:
|
||||
return "NEAR-PERP";
|
||||
case Ticker.QTUM:
|
||||
return "QTUM-PERP";
|
||||
case Ticker.REN:
|
||||
return "REN-PERP";
|
||||
case Ticker.ROSE:
|
||||
return "ROSE-PERP";
|
||||
case Ticker.RSR:
|
||||
return "RSR-PERP";
|
||||
case Ticker.RUNE:
|
||||
return "RUNE-PERP";
|
||||
case Ticker.SAND:
|
||||
return "SAND-PERP";
|
||||
case Ticker.SOL:
|
||||
return "SOL-PERP";
|
||||
case Ticker.SRM:
|
||||
return "SRM-PERP";
|
||||
case Ticker.SUSHI:
|
||||
return "SUSHI-PERP";
|
||||
case Ticker.THETA:
|
||||
return "THETA-PERP";
|
||||
case Ticker.UNI:
|
||||
return "UNI-PERP";
|
||||
case Ticker.XMR:
|
||||
return "XMR-PERP";
|
||||
case Ticker.XRP:
|
||||
return "XRP-PERP";
|
||||
case Ticker.XTZ:
|
||||
return "XTZ-PERP";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public static Trade Map(WebCallResult<FTXOrder> result, decimal? leverage = null)
|
||||
{
|
||||
var data = result.Data;
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return new Trade(DateTime.Now, TradeDirection.None,
|
||||
TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0,
|
||||
"", result.Error?.Message);
|
||||
}
|
||||
|
||||
return new Trade(data.CreateTime,
|
||||
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
|
||||
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
|
||||
data.Quantity, data.AverageFillPrice ?? 0, leverage,
|
||||
data.ClientOrderId, "");
|
||||
}
|
||||
|
||||
internal static Trade Map(WebCallResult<FTXTriggerOrder> ftxResult, decimal? leverage)
|
||||
{
|
||||
var data = ftxResult.Data;
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return new Trade(DateTime.Now, TradeDirection.None,
|
||||
TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0,
|
||||
"", ftxResult.Error?.Message);
|
||||
}
|
||||
|
||||
return new Trade(data.CreateTime,
|
||||
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
|
||||
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
|
||||
data.Quantity, data.TriggerPrice ?? 0, leverage,
|
||||
Guid.NewGuid().ToString(), "");
|
||||
}
|
||||
|
||||
public static OrderType FtxOrderTypeMap(TradeType tradeType)
|
||||
{
|
||||
switch (tradeType)
|
||||
{
|
||||
case TradeType.Limit:
|
||||
return OrderType.Limit;
|
||||
case TradeType.Market:
|
||||
return OrderType.Market;
|
||||
default:
|
||||
return OrderType.Limit;
|
||||
}
|
||||
}
|
||||
|
||||
public static Trade Map(FTXOrder data)
|
||||
{
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
return new Trade(data.CreateTime, TradeDirection.None,
|
||||
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
|
||||
data.Quantity, data.AverageFillPrice ?? 0, 0,
|
||||
data.ClientOrderId, "");
|
||||
}
|
||||
|
||||
public static Candle Map(
|
||||
FTXKline ftxKline,
|
||||
Ticker ticker,
|
||||
TradingExchanges exchange,
|
||||
Timeframe timeframe)
|
||||
{
|
||||
return new Candle
|
||||
{
|
||||
Date = ftxKline.OpenTime,
|
||||
Volume = ftxKline.Volume ?? 0,
|
||||
Close = ftxKline.ClosePrice,
|
||||
High = ftxKline.HighPrice,
|
||||
Low = ftxKline.LowPrice,
|
||||
Open = ftxKline.OpenPrice,
|
||||
Ticker = ticker.ToString(),
|
||||
OpenTime = ftxKline.OpenTime,
|
||||
Exchange = exchange,
|
||||
Timeframe = timeframe
|
||||
};
|
||||
}
|
||||
|
||||
internal static KlineInterval Map(Timeframe interval) => interval switch
|
||||
{
|
||||
Timeframe.FiveMinutes => KlineInterval.FiveMinutes,
|
||||
Timeframe.FifteenMinutes => KlineInterval.FifteenMinutes,
|
||||
Timeframe.OneHour => KlineInterval.OneHour,
|
||||
Timeframe.FourHour => KlineInterval.FourHours,
|
||||
Timeframe.OneDay => KlineInterval.OneDay,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
internal static TriggerOrderType FtxTriggerOrderTypeMap(TradeType tradeType) => tradeType switch
|
||||
{
|
||||
TradeType.StopMarket => TriggerOrderType.Stop,
|
||||
TradeType.StopLimit => TriggerOrderType.Stop,
|
||||
TradeType.StopLoss => TriggerOrderType.Stop,
|
||||
TradeType.TakeProfit => TriggerOrderType.TakeProfit,
|
||||
TradeType.StopLossProfit => TriggerOrderType.Stop,
|
||||
TradeType.StopLossProfitLimit => TriggerOrderType.Stop,
|
||||
TradeType.StopLossLimit => TriggerOrderType.Stop,
|
||||
TradeType.TakeProfitLimit => TriggerOrderType.TakeProfit,
|
||||
TradeType.TrailingStop => TriggerOrderType.TrailingStop,
|
||||
TradeType.TrailingStopLimit => TriggerOrderType.TrailingStop,
|
||||
TradeType.StopLossAndLimit => TriggerOrderType.Stop,
|
||||
TradeType.SettlePosition => TriggerOrderType.Stop,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
internal static Orderbook Map(WebCallResult<FTXOrderbook> ftxOrderBook)
|
||||
{
|
||||
return new Orderbook()
|
||||
{
|
||||
Asks = Map(ftxOrderBook.Data.Asks),
|
||||
Bids = Map(ftxOrderBook.Data.Bids)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<OrderBookEntry> Map(IEnumerable<FTXOrderBookEntry> entry)
|
||||
{
|
||||
return entry.Select(ask => new OrderBookEntry() { Price = ask.Price, Quantity = ask.Quantity }).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using Kraken.Net.Objects.Models;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Exchanges.Helpers;
|
||||
|
||||
public static class KrakenHelpers
|
||||
{
|
||||
public static Trade Map(KrakenOrder order)
|
||||
{
|
||||
var leverageParse = order.OrderDetails.Leverage.Split((char)':')[0];
|
||||
long.TryParse(leverageParse, out long leverage);
|
||||
|
||||
return new Trade(order.CreateTime,
|
||||
TradeDirection.None,
|
||||
(TradeStatus)order.Status,
|
||||
(TradeType)order.OrderDetails.Type,
|
||||
MiscExtensions.ParseEnum<Ticker>(order.OrderDetails.Symbol),
|
||||
order.Quantity,
|
||||
order.AveragePrice,
|
||||
leverage,
|
||||
order.ClientOrderId,
|
||||
"");
|
||||
}
|
||||
|
||||
public static Kraken.Net.Enums.OrderType KrakenOrderTypeMap(TradeType tradeType)
|
||||
{
|
||||
switch (tradeType)
|
||||
{
|
||||
case TradeType.Limit:
|
||||
return Kraken.Net.Enums.OrderType.Limit;
|
||||
case TradeType.Market:
|
||||
return Kraken.Net.Enums.OrderType.Market;
|
||||
case TradeType.StopMarket:
|
||||
return Kraken.Net.Enums.OrderType.StopMarket;
|
||||
default:
|
||||
return Kraken.Net.Enums.OrderType.Limit;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Trade Map(KeyValuePair<string, KrakenOrder> o)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.9.7" />
|
||||
<PackageReference Include="CryptoExchange.Net" Version="7.5.2" />
|
||||
<PackageReference Include="FTX.Net" Version="1.0.16" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.9.7"/>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="7.5.2"/>
|
||||
<PackageReference Include="FTX.Net" Version="1.0.16"/>
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.6.5"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
|
||||
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user