docker files fixes from liaqat
This commit is contained in:
122
src/Managing.Infrastructure.Web3/Subgraphs/Chainlink.cs
Normal file
122
src/Managing.Infrastructure.Web3/Subgraphs/Chainlink.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using GraphQL.Client.Abstractions;
|
||||
using GraphQL;
|
||||
using Managing.Infrastructure.Evm.Abstractions;
|
||||
using Managing.Core;
|
||||
using Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Infrastructure.Evm.Extensions;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs;
|
||||
|
||||
public class Chainlink : ISubgraphPrices
|
||||
{
|
||||
SubgraphProvider ISubgraphPrices.GetProvider() => SubgraphProvider.ChainlinkPrice;
|
||||
|
||||
private readonly IGraphQLClient _graphQLClient;
|
||||
private readonly string _baseToken = "/USD";
|
||||
|
||||
public Chainlink(IGraphQLClient graphQLHttpClient)
|
||||
{
|
||||
_graphQLClient = graphQLHttpClient ?? throw new ArgumentNullException(nameof(graphQLHttpClient));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Candle>> GetPrices(Ticker ticker, DateTime startDate, Timeframe timeframe)
|
||||
{
|
||||
var path = ticker.ToString() + _baseToken;
|
||||
var batchSize = 1000;
|
||||
var batchMax = 6;
|
||||
var priceRounds = new List<ChainlinkPrice>();
|
||||
var feedCondition = $@"{{ assetPair: ""{path}"" }}";
|
||||
|
||||
// Fetching prices from graphql ticker
|
||||
for (int i = 0; i < batchMax; i++)
|
||||
{
|
||||
var query = $"{{ prices(first: {batchSize}, skip: {i * batchSize}, orderBy: timestamp, orderDirection: desc, where: {feedCondition} ) {{ timestamp,price}} }}";
|
||||
var graphQuery = new GraphQLRequest
|
||||
{
|
||||
Query = query
|
||||
};
|
||||
|
||||
var response = await _graphQLClient.SendQueryAsync<ChainlinkPrices>(graphQuery);
|
||||
priceRounds.AddRange(response.Data.Prices);
|
||||
}
|
||||
|
||||
var rounds = new List<Round>();
|
||||
|
||||
// Format response
|
||||
foreach (var round in priceRounds)
|
||||
{
|
||||
var timestamp = int.Parse(round.Timestamp);
|
||||
rounds.Add(new Round
|
||||
{
|
||||
UnixTimestamp = timestamp,
|
||||
Value = (double.Parse(round.Price) / 1e8).ToString(),
|
||||
Date = DateHelpers.GetFromUnixTimestamp(timestamp)
|
||||
});
|
||||
}
|
||||
|
||||
rounds.Sort((timeA, timeB) => timeA.UnixTimestamp - timeB.UnixTimestamp);
|
||||
|
||||
return rounds.ToArray().GetCandles(timeframe, ticker);
|
||||
}
|
||||
|
||||
public Task<decimal> GetVolume(Ticker ticker)
|
||||
{
|
||||
//var query = $"{{ assetPairs() {{ id }} }}";
|
||||
//var graphQuery = new GraphQLRequest
|
||||
//{
|
||||
// Query = query
|
||||
//};
|
||||
|
||||
//var response = await _graphQLClient.SendQueryAsync<AssetPairs>(graphQuery);
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Ticker>> GetTickers()
|
||||
{
|
||||
var batchSize = 100;
|
||||
var batchMax = 10;
|
||||
var tickers = new List<Ticker>();
|
||||
|
||||
for (int i = 0; i < batchMax; i++)
|
||||
{
|
||||
var query = $"{{ assetPairs(first: {batchSize}, skip: {i * batchSize}) {{ id }} }}";
|
||||
var graphQuery = new GraphQLRequest
|
||||
{
|
||||
Query = query
|
||||
};
|
||||
|
||||
var response = await _graphQLClient.SendQueryAsync<ChainlinkAssetPairs>(graphQuery);
|
||||
|
||||
if (response.Data?.AssetPairs != null)
|
||||
{
|
||||
tickers.AddRange(ParseTickers(response.Data.AssetPairs));
|
||||
}
|
||||
}
|
||||
|
||||
return tickers;
|
||||
}
|
||||
|
||||
private List<Ticker> ParseTickers(List<AssetPair> pairs)
|
||||
{
|
||||
var tickers = new List<Ticker>();
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var items = pair.Id.Split('/');
|
||||
|
||||
if (items.Length == 2 && items[1] == Constants.Stablecoins.Usd)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ticker = MiscExtensions.ParseEnum<Ticker>(items[0]);
|
||||
tickers.Add(ticker);
|
||||
}
|
||||
catch (Exception ex) { }
|
||||
}
|
||||
}
|
||||
|
||||
return tickers;
|
||||
}
|
||||
}
|
||||
92
src/Managing.Infrastructure.Web3/Subgraphs/ChainlinkGmx.cs
Normal file
92
src/Managing.Infrastructure.Web3/Subgraphs/ChainlinkGmx.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using GraphQL.Client.Abstractions;
|
||||
using GraphQL;
|
||||
using Managing.Core;
|
||||
using Managing.Infrastructure.Evm.Abstractions;
|
||||
using Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Infrastructure.Evm.Extensions;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs;
|
||||
|
||||
public class ChainlinkGmx : ISubgraphPrices
|
||||
{
|
||||
SubgraphProvider ISubgraphPrices.GetProvider() => SubgraphProvider.ChainlinkGmx;
|
||||
|
||||
private readonly IGraphQLClient _graphQLClient;
|
||||
private readonly string _baseToken = "_USD";
|
||||
private Dictionary<string, string> _feeds = new Dictionary<string, string>()
|
||||
{
|
||||
{"BTC_USD", "0xae74faa92cb67a95ebcab07358bc222e33a34da7" },
|
||||
{"ETH_USD", "0x37bc7498f4ff12c19678ee8fe19d713b87f6a9e6" },
|
||||
{"BNB_USD", "0xc45ebd0f901ba6b2b8c7e70b717778f055ef5e6d" },
|
||||
{"LINK_USD", "0xdfd03bfc3465107ce570a0397b247f546a42d0fa" },
|
||||
{"UNI_USD", "0x68577f915131087199fe48913d8b416b3984fd38" },
|
||||
{"SUSHI_USD", "0x7213536a36094cd8a768a5e45203ec286cba2d74" },
|
||||
{"AVAX_USD", "0x0fc3657899693648bba4dbd2d8b33b82e875105d" },
|
||||
{"AAVE_USD", "0xe3f0dede4b499c07e12475087ab1a084b5f93bc0" },
|
||||
{"YFI_USD", "0x8a4d74003870064d41d4f84940550911fbfccf04" },
|
||||
{"SPELL_USD", "0x8640b23468815902e011948f3ab173e1e83f9879" },
|
||||
};
|
||||
|
||||
public ChainlinkGmx(IGraphQLClient graphQLHttpClient)
|
||||
{
|
||||
_graphQLClient = graphQLHttpClient ?? throw new ArgumentNullException(nameof(graphQLHttpClient));
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Candle>> GetPrices(Ticker ticker, DateTime startDate, Timeframe timeframe)
|
||||
{
|
||||
var path = ticker.ToString() + _baseToken;
|
||||
var feed = _feeds.GetValueOrDefault(path);
|
||||
var perChunk = 1000;
|
||||
var totalChunk = 6;
|
||||
var priceRounds = new List<Round>();
|
||||
var feedCondition = $@"{{ feed: ""{feed}"" }}";
|
||||
|
||||
for (int i = 0; i < totalChunk; i++)
|
||||
{
|
||||
var query = $"{{ rounds(first: {perChunk}, skip: {i * perChunk}, orderBy: unixTimestamp, orderDirection: desc, where: {feedCondition} ) {{ unixTimestamp,value}} }}";
|
||||
var graphQuery = new GraphQLRequest
|
||||
{
|
||||
Query = query
|
||||
};
|
||||
|
||||
var response = await _graphQLClient.SendQueryAsync<PriceRoundsChainlinkGmx>(graphQuery);
|
||||
priceRounds.AddRange(response.Data.Rounds);
|
||||
}
|
||||
|
||||
var rounds = new List<Round>();
|
||||
var uniqTs = new HashSet<int>();
|
||||
|
||||
foreach (var round in priceRounds)
|
||||
{
|
||||
if (uniqTs.Contains(round.UnixTimestamp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqTs.Add(round.UnixTimestamp);
|
||||
rounds.Add(new Round
|
||||
{
|
||||
UnixTimestamp = round.UnixTimestamp,
|
||||
Value = (double.Parse(round.Value) / 1e8).ToString(),
|
||||
Date = DateHelpers.GetFromUnixTimestamp(round.UnixTimestamp)
|
||||
});
|
||||
}
|
||||
|
||||
rounds.Sort((timeA, timeB) => timeA.UnixTimestamp - timeB.UnixTimestamp);
|
||||
|
||||
return rounds.ToArray().GetCandles(timeframe, ticker);
|
||||
}
|
||||
|
||||
public Task<decimal> GetVolume(Ticker ticker)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Ticker>> GetTickers()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
104
src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs
Normal file
104
src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Abstractions;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Infrastructure.Evm.Abstractions;
|
||||
using Managing.Infrastructure.Evm.Services;
|
||||
using Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
using NBitcoin;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs;
|
||||
|
||||
public class Gbc : ISubgraphPrices
|
||||
{
|
||||
private readonly IGraphQLClient _graphQLClient;
|
||||
public SubgraphProvider GetProvider() => SubgraphProvider.Gbc;
|
||||
|
||||
public Gbc(IGraphQLClient graphQLHttpClient)
|
||||
{
|
||||
_graphQLClient = graphQLHttpClient ?? throw new ArgumentNullException(nameof(graphQLHttpClient));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Candle>> GetPrices(Ticker ticker, DateTime startDate, Timeframe timeframe)
|
||||
{
|
||||
var batchSize = 1000;
|
||||
var batchMax = 6;
|
||||
var priceRounds = new List<GbcPrice>();
|
||||
var tickerContract = TokenService.GetContractAddress(ticker);
|
||||
var unixTimeframe = timeframe.GetUnixInterval();
|
||||
var start = startDate.ToUnixTimestamp();
|
||||
var end = DateTime.UtcNow.ToUnixTimestamp();
|
||||
var feedCondition = $@"{{ tokenAddress: ""_{tickerContract}"", interval: ""_{unixTimeframe}"", timestamp_gte: {start}, timestamp_lte: {end} }}";
|
||||
|
||||
// Fetching prices from graphql ticker
|
||||
for (int i = 0; i < batchMax; i++)
|
||||
{
|
||||
var query = $"{{ pricefeeds(first: {batchSize}, skip: {i * batchSize}, orderBy: timestamp, orderDirection: desc, where: {feedCondition} ) {{ timestamp,o,h,l,c}} }}";
|
||||
var graphQuery = new GraphQLRequest
|
||||
{
|
||||
Query = query
|
||||
};
|
||||
|
||||
var response = await _graphQLClient.SendQueryAsync<GbcPrices>(graphQuery);
|
||||
priceRounds.AddRange(response.Data.PriceFeeds);
|
||||
}
|
||||
|
||||
priceRounds.Sort((timeA, timeB) => timeA.Timestamp - timeB.Timestamp);
|
||||
|
||||
var candles = new List<Candle>();
|
||||
|
||||
var firstRound = priceRounds.FirstOrDefault();
|
||||
if (firstRound == null)
|
||||
return candles;
|
||||
|
||||
var previousCandle = BuildCandle(firstRound, ticker, timeframe);
|
||||
|
||||
// Format response
|
||||
foreach (var price in priceRounds.Skip(1))
|
||||
{
|
||||
var candle = BuildCandle(price, ticker, timeframe);
|
||||
candle.OpenTime = previousCandle.Date;
|
||||
candles.Add(candle);
|
||||
}
|
||||
|
||||
return candles;
|
||||
}
|
||||
|
||||
private Candle BuildCandle(GbcPrice ohlc, Ticker ticker, Timeframe timeframe)
|
||||
{
|
||||
return new Candle()
|
||||
{
|
||||
Date = DateHelpers.GetFromUnixTimestamp(ohlc.Timestamp),
|
||||
Open = FormatPrice(ohlc.O),
|
||||
High = FormatPrice(ohlc.H),
|
||||
Low = FormatPrice(ohlc.L),
|
||||
Close = FormatPrice(ohlc.C),
|
||||
Exchange = TradingExchanges.Evm,
|
||||
Ticker = ticker.ToString(),
|
||||
Timeframe = timeframe
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal FormatPrice(string price)
|
||||
{
|
||||
return (decimal)(double.Parse(price) / 1e30);
|
||||
}
|
||||
|
||||
public Task<decimal> GetVolume(Ticker ticker)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Ticker>> GetTickers()
|
||||
{
|
||||
var tickers = new List<Ticker>() {
|
||||
Ticker.BTC,
|
||||
Ticker.LINK,
|
||||
Ticker.ETH,
|
||||
Ticker.UNI
|
||||
};
|
||||
|
||||
return Task.FromResult(tickers.AsEnumerable());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class GbcPrices
|
||||
{
|
||||
public List<GbcPrice> PriceFeeds { get; set; }
|
||||
}
|
||||
|
||||
public class GbcPrice
|
||||
{
|
||||
public int Timestamp { get; set; }
|
||||
public string O { get; set; }
|
||||
public string H { get; set; }
|
||||
public string L { get; set; }
|
||||
public string C { get; set; }
|
||||
}
|
||||
11
src/Managing.Infrastructure.Web3/Subgraphs/Models/Pair.cs
Normal file
11
src/Managing.Infrastructure.Web3/Subgraphs/Models/Pair.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Nethereum.Contracts.Standards.ERC20.TokenList;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class Pair
|
||||
{
|
||||
public string ReserveETH { get; set; }
|
||||
public string ReserveUSD { get; set; }
|
||||
public Token Token0 { get; set; }
|
||||
public Token Token1 { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class Pools
|
||||
{
|
||||
public List<Pair> Pairs { get; set; }
|
||||
}
|
||||
17
src/Managing.Infrastructure.Web3/Subgraphs/Models/Price.cs
Normal file
17
src/Managing.Infrastructure.Web3/Subgraphs/Models/Price.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class ChainlinkPrice
|
||||
{
|
||||
public string Price { get; set; }
|
||||
public string Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class ChainlinkAssetPairs
|
||||
{
|
||||
public List<AssetPair> AssetPairs { get; set; }
|
||||
}
|
||||
|
||||
public class AssetPair
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class ChainlinkPrices
|
||||
{
|
||||
public List<ChainlinkPrice> Prices { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class Round
|
||||
{
|
||||
public int UnixTimestamp { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class PriceRoundsChainlinkGmx
|
||||
{
|
||||
public List<Round> Rounds { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Nethereum.Contracts.Standards.ERC20.TokenList;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class TokenDetails : Token
|
||||
{
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string TradeVolume { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string TradeVolumeUSD { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string TotalSupply { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string TotalLiquidity { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
|
||||
public class TopTokens
|
||||
{
|
||||
public List<TokenDetails> Tokens { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Managing.Infrastructure.Evm.Abstractions;
|
||||
using Managing.Infrastructure.Evm.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs;
|
||||
|
||||
public static class SubgraphExtensions
|
||||
{
|
||||
public static void AddUniswapV2(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUniswap>(ctx =>
|
||||
{
|
||||
return new Uniswap(SubgraphService.GetSubgraphClient(SubgraphProvider.UniswapV2));
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddChainlink(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISubgraphPrices>(ctx =>
|
||||
{
|
||||
return new Chainlink(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkPrice));
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddGbcFeed(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISubgraphPrices>(ctx =>
|
||||
{
|
||||
return new Gbc(SubgraphService.GetSubgraphClient(SubgraphProvider.Gbc));
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddChainlinkGmx(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISubgraphPrices>(ctx =>
|
||||
{
|
||||
return new ChainlinkGmx(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkGmx));
|
||||
});
|
||||
}
|
||||
}
|
||||
85
src/Managing.Infrastructure.Web3/Subgraphs/Uniswap.cs
Normal file
85
src/Managing.Infrastructure.Web3/Subgraphs/Uniswap.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using GraphQL.Client.Abstractions;
|
||||
using GraphQL;
|
||||
using Managing.Infrastructure.Evm.Abstractions;
|
||||
using Managing.Infrastructure.Evm.Subgraphs.Models;
|
||||
using Managing.Domain.Candles;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Subgraphs;
|
||||
|
||||
public class Uniswap : IUniswap
|
||||
{
|
||||
SubgraphProvider ISubgraphPrices.GetProvider() => SubgraphProvider.UniswapV2;
|
||||
|
||||
private readonly IGraphQLClient _graphQLClient;
|
||||
|
||||
public Uniswap(IGraphQLClient graphQLHttpClient)
|
||||
{
|
||||
_graphQLClient = graphQLHttpClient ?? throw new ArgumentNullException(nameof(graphQLHttpClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the first 150 most liquid market pairs ordered by desc
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<Pools> GetMostLiquidMarketPairs()
|
||||
{
|
||||
var query = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
{
|
||||
pairs(first: 150, orderBy: reserveETH orderDirection: desc){
|
||||
token0 {
|
||||
symbol
|
||||
}
|
||||
token1 {
|
||||
symbol
|
||||
}
|
||||
reserveETH
|
||||
reserveUSD
|
||||
}
|
||||
}
|
||||
"
|
||||
};
|
||||
|
||||
GraphQLResponse<Pools> response = await _graphQLClient.SendQueryAsync<Pools>(query);
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
public async Task<TopTokens> GetTopTokens()
|
||||
{
|
||||
var query = new GraphQLRequest
|
||||
{
|
||||
Query = @"
|
||||
{
|
||||
tokens (first: 150, orderBy: tradeVolumeUSD orderDirection: desc){
|
||||
symbol
|
||||
name
|
||||
tradeVolume
|
||||
tradeVolumeUSD
|
||||
totalSupply
|
||||
totalLiquidity
|
||||
}
|
||||
}
|
||||
"
|
||||
};
|
||||
|
||||
GraphQLResponse<TopTokens> response = await _graphQLClient.SendQueryAsync<TopTokens>(query);
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Candle>> GetPrices(Ticker ticker, DateTime startDate, Timeframe timeframe)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<decimal> GetVolume(Ticker ticker)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Ticker>> GetTickers()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user