907 lines
32 KiB
C#
907 lines
32 KiB
C#
using System.Net.Http.Json;
|
|
using System.Numerics;
|
|
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Common;
|
|
using Managing.Core;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Candles;
|
|
using Managing.Domain.Evm;
|
|
using Managing.Domain.Statistics;
|
|
using Managing.Domain.Trades;
|
|
using Managing.Infrastructure.Evm.Abstractions;
|
|
using Managing.Infrastructure.Evm.Models;
|
|
using Managing.Infrastructure.Evm.Models.Gmx.v2;
|
|
using Managing.Infrastructure.Evm.Models.Privy;
|
|
using Managing.Infrastructure.Evm.Models.Proxy;
|
|
using Managing.Infrastructure.Evm.Referentials;
|
|
using Managing.Infrastructure.Evm.Services;
|
|
using Managing.Infrastructure.Evm.Services.Gmx;
|
|
using NBitcoin;
|
|
using Nethereum.Contracts;
|
|
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
|
|
using Nethereum.HdWallet;
|
|
using Nethereum.Hex.HexTypes;
|
|
using Nethereum.Signer;
|
|
using Nethereum.Web3;
|
|
using static Managing.Common.Enums;
|
|
using BalanceOfFunction = Nethereum.Contracts.Standards.ERC20.ContractDefinition.BalanceOfFunction;
|
|
using BalanceOfOutputDTO = Nethereum.Contracts.Standards.ERC20.ContractDefinition.BalanceOfOutputDTO;
|
|
using Chain = Managing.Domain.Evm.Chain;
|
|
using TransferEventDTO = Nethereum.Contracts.Standards.ERC721.ContractDefinition.TransferEventDTO;
|
|
|
|
namespace Managing.Infrastructure.Evm;
|
|
|
|
public class EvmManager : IEvmManager
|
|
{
|
|
private readonly Web3 _web3;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly string _password = "!StrongPassword94";
|
|
private readonly IEnumerable<ISubgraphPrices> _subgraphs;
|
|
private readonly GmxV2Service _gmxV2Service;
|
|
private readonly IWeb3ProxyService _web3ProxyService;
|
|
private readonly ICacheService _cacheService;
|
|
|
|
public EvmManager(IEnumerable<ISubgraphPrices> subgraphs,
|
|
IWeb3ProxyService web3ProxyService, ICacheService cacheService)
|
|
{
|
|
var defaultChain = ChainService.GetEthereum();
|
|
_web3 = new Web3(defaultChain.RpcUrl);
|
|
_httpClient = new HttpClient();
|
|
_subgraphs = subgraphs;
|
|
_web3ProxyService = web3ProxyService;
|
|
_gmxV2Service = new GmxV2Service();
|
|
_cacheService = cacheService;
|
|
}
|
|
|
|
public async Task<decimal> GetAddressBalance(string address)
|
|
{
|
|
var balance = await _web3.Eth.GetBalance.SendRequestAsync(address);
|
|
var etherAmount = Web3.Convert.FromWei(balance.Value);
|
|
return etherAmount;
|
|
}
|
|
|
|
public async Task<List<Holder>> GetContractHolders(string contractAddress, DateTime since)
|
|
{
|
|
var holders = new List<Holder>();
|
|
var contract = _web3.Eth.ERC721.GetContractService(contractAddress);
|
|
|
|
// Retrieve total supply to iterate over all token id generated within the contract
|
|
var totalSupply = await contract.TotalSupplyQueryAsync();
|
|
|
|
for (int tokenId = 1; tokenId < 10; tokenId++)
|
|
{
|
|
// Retrieve the owner of the nft
|
|
var tokenOwner = await contract.OwnerOfQueryAsync(tokenId);
|
|
|
|
// If holder already have an nft we get the holder
|
|
// Otherwise we create a new holder
|
|
var holder = holders.FirstOrDefault(h => h.HolderAddress == tokenOwner) ?? new Holder(tokenOwner);
|
|
|
|
// Retrieve all events related to the owner on the contract address
|
|
var nfts = await GetNftEvent(contractAddress, tokenOwner);
|
|
|
|
// Get tokenId related event
|
|
var nftEvent = nfts.FirstOrDefault(n => n.Event.TokenId == new BigInteger(tokenId));
|
|
|
|
if (nftEvent != null)
|
|
{
|
|
// Retrieve the date of the nft event that occur in the blocknumber
|
|
var blockDate = await GetBlockDate(nftEvent.Log.BlockNumber);
|
|
var nft = new Nft(contractAddress, tokenId, blockDate);
|
|
|
|
// Verify if the date of the holding is before the date passed from the parameters
|
|
if (blockDate <= since)
|
|
{
|
|
holder.AddNft(nft);
|
|
|
|
// If holder do not exist in the list we add it
|
|
if (!holders.Exists(h => h.HolderAddress == tokenOwner))
|
|
{
|
|
holders.Add(holder);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"TokenId #{tokenId} for owner {tokenOwner} date not in range ({blockDate:f})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Error when getting tokenId #{tokenId} for owner {tokenOwner}");
|
|
}
|
|
|
|
UpdateLine(tokenId, (int)totalSupply, holders.Count);
|
|
}
|
|
|
|
return holders;
|
|
}
|
|
|
|
public async Task<List<EventLog<TransferEventDTO>>>
|
|
GetNftEvent(string contractAddress, string tokenOwner)
|
|
{
|
|
return await NftService.GetNftEvent(_web3, tokenOwner, contractAddress);
|
|
}
|
|
|
|
public async Task<DateTime> GetBlockDate(int blockNumber)
|
|
{
|
|
return await GetBlockDate(new HexBigInteger(blockNumber));
|
|
}
|
|
|
|
public async Task<DateTime> GetBlockDate(HexBigInteger blockNumber)
|
|
{
|
|
var block = await _web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(blockNumber);
|
|
var date = DateHelpers.GetFromUnixTimestamp((int)block.Timestamp.Value);
|
|
return date;
|
|
}
|
|
|
|
private void UpdateLine(int count, int total, int holders)
|
|
{
|
|
Console.WriteLine($"{count}/{total} - {(((decimal)count * 100m) / (decimal)total)} % - Holders : {holders}");
|
|
}
|
|
|
|
public string VerifySignature(string signature, string message)
|
|
{
|
|
var signer = new EthereumMessageSigner();
|
|
var addressRecovered = signer.EncodeUTF8AndEcRecover(message, signature);
|
|
return addressRecovered;
|
|
}
|
|
|
|
public string SignMessage(string message, string privateKey)
|
|
{
|
|
var signer = new EthereumMessageSigner();
|
|
var signature = signer.EncodeUTF8AndSign(message, new EthECKey(privateKey));
|
|
return signature;
|
|
}
|
|
|
|
public (string Key, string Secret) GenerateAddress()
|
|
{
|
|
var mnemo = new Mnemonic(Wordlist.English, WordCount.Twelve);
|
|
var wallet = new Wallet(mnemo.ToString(), _password);
|
|
var account = wallet.GetAccount(0);
|
|
|
|
return (account.Address, mnemo.ToString());
|
|
}
|
|
|
|
public string GetAddressFromMnemo(string mnemo)
|
|
{
|
|
var wallet = new Wallet(mnemo, _password);
|
|
return wallet.GetAccount(0).Address;
|
|
}
|
|
|
|
public async Task<EvmBalance> GetEtherBalance(Chain chain, string account)
|
|
{
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
var etherBalance = Web3.Convert.FromWei(await web3.Eth.GetBalance.SendRequestAsync(account));
|
|
var lastCandle = await GetCandle(Ticker.ETH);
|
|
|
|
return new EvmBalance()
|
|
{
|
|
Balance = etherBalance, Price = lastCandle.Close, TokenName = "ETH", Value = etherBalance * lastCandle.Close
|
|
};
|
|
}
|
|
|
|
public async Task<List<EvmBalance>> GetAllBalances(Chain chain, string publicAddress)
|
|
{
|
|
var balances = new List<EvmBalance>();
|
|
foreach (var ticker in Enum.GetValues<Ticker>())
|
|
{
|
|
try
|
|
{
|
|
var balance = await GetTokenBalance(chain.Name, ticker, publicAddress);
|
|
if (balance != null && balance.Balance > 0)
|
|
{
|
|
balances.Add(balance);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// TODO : handle exception
|
|
}
|
|
}
|
|
|
|
var etherBalance = await GetEtherBalance(chain, publicAddress);
|
|
etherBalance.Chain = chain;
|
|
balances.Add(etherBalance);
|
|
|
|
return balances;
|
|
}
|
|
|
|
public async Task<EvmBalance> GetTokenBalance(string chainName, Ticker ticker, string publicAddress)
|
|
{
|
|
var chain = ChainService.GetChain(chainName);
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
var balanceOfMessage = new BalanceOfFunction() { Owner = publicAddress };
|
|
|
|
//Creating a new query handler
|
|
var queryHandler = web3.Eth.GetContractQueryHandler<BalanceOfFunction>();
|
|
|
|
var contractAddress = TokenService.GetContractAddress(ticker);
|
|
|
|
if (contractAddress == Arbitrum.Address.Zero)
|
|
return null;
|
|
|
|
var balance = await queryHandler
|
|
.QueryAsync<BigInteger>(contractAddress, balanceOfMessage)
|
|
.ConfigureAwait(false);
|
|
|
|
var lastCandle = await GetCandle(ticker);
|
|
var tokenUsdPrice = lastCandle.Close;
|
|
var tokenDecimal = TokenService.GetDecimal(ticker);
|
|
var balanceFromWei = Web3.Convert.FromWei(balance, tokenDecimal);
|
|
|
|
var evmBalance = new EvmBalance
|
|
{
|
|
TokenName = ticker.ToString(),
|
|
Balance = balanceFromWei,
|
|
TokenAddress = contractAddress,
|
|
Value = tokenUsdPrice * balanceFromWei,
|
|
Price = tokenUsdPrice,
|
|
Chain = chain
|
|
};
|
|
|
|
return evmBalance;
|
|
}
|
|
|
|
public async Task<List<EvmBalance>> GetBalances(Chain chain, int page, int pageSize,
|
|
string publicAddress)
|
|
{
|
|
var callList = new List<IMulticallInputOutput>();
|
|
var startItem = (page * pageSize);
|
|
var tokens = TokenService.GetTokens();
|
|
var totaItemsToFetch = startItem + pageSize <= tokens.Count ? startItem + pageSize : tokens.Count + startItem;
|
|
|
|
for (int i = startItem; i < totaItemsToFetch; i++)
|
|
{
|
|
var balanceOfMessage = new BalanceOfFunction() { Owner = publicAddress };
|
|
var call = new MulticallInputOutput<BalanceOfFunction, BalanceOfOutputDTO>(balanceOfMessage,
|
|
tokens[i].Address);
|
|
callList.Add(call);
|
|
}
|
|
|
|
var evmTokens = new List<(EvmBalance Balance, GeckoToken GeckoToken)>();
|
|
|
|
try
|
|
{
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
var geckoTokens = TokenService.GetGeckoTokens();
|
|
|
|
await web3.Eth.GetMultiQueryHandler().MultiCallAsync(callList.ToArray());
|
|
|
|
for (int i = startItem; i < totaItemsToFetch; i++)
|
|
{
|
|
var balance = ((MulticallInputOutput<BalanceOfFunction, BalanceOfOutputDTO>)callList[i - startItem])
|
|
.Output.Balance;
|
|
if (balance > 0)
|
|
{
|
|
var tokenBalance = new EvmBalance()
|
|
{
|
|
Balance = Web3.Convert.FromWei(balance, tokens[i].Decimals),
|
|
TokenName = tokens[i].Symbol,
|
|
TokenAddress = tokens[i].Address,
|
|
Chain = chain
|
|
};
|
|
|
|
var geckoToken = geckoTokens.FirstOrDefault(x =>
|
|
string.Equals(x.Symbol, tokens[i].Symbol, StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
evmTokens.Add((tokenBalance, geckoToken)!);
|
|
}
|
|
}
|
|
|
|
if (evmTokens.Count > 0)
|
|
{
|
|
var ids = evmTokens.Select(x => x.GeckoToken?.Id).Distinct().ToList();
|
|
var prices = await GetPrices(ids).ConfigureAwait(false);
|
|
|
|
foreach (var balance in evmTokens)
|
|
{
|
|
if (balance.GeckoToken != null)
|
|
{
|
|
var price = prices[balance.GeckoToken.Id.ToLower()];
|
|
balance.Balance.Price = price["usd"];
|
|
balance.Balance.Value = balance.Balance.Price * balance.Balance.Balance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// TODO : Handle error
|
|
// No enable to reach rpc
|
|
|
|
var test = ex.Message;
|
|
}
|
|
|
|
return evmTokens.Select(e => e.Balance).ToList();
|
|
}
|
|
|
|
public async Task<Dictionary<string, Dictionary<string, decimal>>> GetPrices(List<string> geckoIds)
|
|
{
|
|
var idsCombined = string.Join(",", geckoIds);
|
|
return await _httpClient.GetFromJsonAsync<Dictionary<string, Dictionary<string, decimal>>>(
|
|
"https://api.coingecko.com/api/v3/simple/price?ids=" + idsCombined + "&vs_currencies=usd");
|
|
}
|
|
|
|
public async Task<List<EvmBalance>> GetAllBalancesOnAllChain(string publicAddress)
|
|
{
|
|
var chainBalances = new List<EvmBalance>();
|
|
var chains = ChainService.GetChains();
|
|
|
|
foreach (var chain in chains)
|
|
{
|
|
chainBalances.AddRange(await GetAllBalances(chain, publicAddress));
|
|
}
|
|
|
|
return chainBalances;
|
|
}
|
|
|
|
public async Task<List<Candle>> GetCandles(Ticker ticker, DateTime startDate, Timeframe timeframe,
|
|
bool isFirstCall = false)
|
|
{
|
|
string gmxTimeframe = GmxHelpers.GeTimeframe(timeframe);
|
|
int limit = isFirstCall ? 10000 : CalculateCandleLimit(startDate, timeframe);
|
|
|
|
GmxV2Prices? gmxPrices = null;
|
|
int maxRetries = 3;
|
|
int delayMs = 1000;
|
|
|
|
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
gmxPrices = await _httpClient.GetFromJsonAsync<GmxV2Prices>(
|
|
$"https://arbitrum-api.gmxinfra.io/prices/candles?tokenSymbol={ticker}&period={gmxTimeframe}&limit={limit}");
|
|
break;
|
|
}
|
|
catch (HttpRequestException ex) when (ex.InnerException is IOException)
|
|
{
|
|
Console.Error.WriteLine($"Attempt {attempt}: Network error while fetching candles: {ex.Message}");
|
|
if (attempt == maxRetries)
|
|
throw;
|
|
await Task.Delay(delayMs * attempt);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"Unexpected error: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
if (gmxPrices == null)
|
|
return null;
|
|
|
|
var filteredCandles = gmxPrices.Candles.Where(p => p[0] >= startDate.ToUnixTimestamp()).ToList();
|
|
var candles = new List<Candle>();
|
|
var timeBetweenCandles =
|
|
gmxPrices.Candles.Count > 2 ? gmxPrices.Candles[0][0] - gmxPrices.Candles[1][0] : 900; // Default 15 minutes
|
|
|
|
for (int i = 0; i < filteredCandles.Count; i++)
|
|
{
|
|
var c = GmxV2Mappers.Map(filteredCandles[i], ticker, timeframe, (int)timeBetweenCandles);
|
|
candles.Add(c);
|
|
}
|
|
|
|
return candles.OrderBy(c => c.Date).ToList();
|
|
}
|
|
|
|
private int CalculateCandleLimit(DateTime startDate, Timeframe timeframe)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var minutesPerCandle = timeframe switch
|
|
{
|
|
Timeframe.OneMinute => 1,
|
|
Timeframe.FiveMinutes => 5,
|
|
Timeframe.FifteenMinutes => 15,
|
|
Timeframe.ThirtyMinutes => 30,
|
|
Timeframe.OneHour => 60,
|
|
Timeframe.FourHour => 240,
|
|
Timeframe.OneDay => 1440,
|
|
_ => 15
|
|
};
|
|
var totalMinutes = (now - startDate).TotalMinutes;
|
|
var candlesNeeded = (int)Math.Ceiling(totalMinutes / minutesPerCandle);
|
|
return Math.Min(candlesNeeded + 5, 10000);
|
|
}
|
|
|
|
public decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker)
|
|
{
|
|
var subgraph = GetSubgraph(subgraphProvider);
|
|
var volume = subgraph.GetVolume(ticker).Result;
|
|
return volume;
|
|
}
|
|
|
|
private ISubgraphPrices GetSubgraph(SubgraphProvider subgraphProvider)
|
|
{
|
|
return _subgraphs.First(s => s.GetProvider() == subgraphProvider);
|
|
}
|
|
|
|
public async Task<List<Ticker>> GetAvailableTicker()
|
|
{
|
|
var tokenList = await _httpClient.GetFromJsonAsync<GmxV2TokenList>(
|
|
"https://arbitrum-api.gmxinfra.io/tokens");
|
|
|
|
if (tokenList == null)
|
|
return null;
|
|
|
|
return GmxV2Mappers.Map(tokenList);
|
|
}
|
|
|
|
public async Task<Candle> GetCandle(Ticker ticker)
|
|
{
|
|
var key = $"lastcandle-{ticker}";
|
|
var cachedCandle = _cacheService.GetValue<Candle>(key);
|
|
|
|
if (cachedCandle == null)
|
|
{
|
|
var lastCandles = await GetCandles(ticker, DateTime.UtcNow.AddMinutes(-5),
|
|
Timeframe.OneMinute);
|
|
cachedCandle = lastCandles.Last();
|
|
|
|
_cacheService.SaveValue(key, cachedCandle, TimeSpan.FromMinutes(5));
|
|
}
|
|
|
|
return cachedCandle;
|
|
}
|
|
|
|
public async Task<PrivyInitAddressResponse> InitAddress(string publicAddress)
|
|
{
|
|
try
|
|
{
|
|
var response = await _web3ProxyService.CallPrivyServiceAsync<PrivyInitAddressResponse>(
|
|
"/init-address",
|
|
new { address = publicAddress });
|
|
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log the error
|
|
Console.Error.WriteLine($"Error initializing address: {ex.Message}");
|
|
return new PrivyInitAddressResponse
|
|
{
|
|
Success = false,
|
|
Error = ex.Message
|
|
};
|
|
}
|
|
}
|
|
|
|
public async Task<BigInteger> GetAllowance(string publicAddress, Ticker ticker)
|
|
{
|
|
var contractAddress = TokenService.GetContractAddress(ticker);
|
|
var allowance = await EvmBase.GetAllowance(_web3, publicAddress, contractAddress);
|
|
return allowance;
|
|
}
|
|
|
|
public async Task<bool> Send(
|
|
Chain chain,
|
|
Ticker ticker,
|
|
decimal amount,
|
|
string publicAddress,
|
|
string privateKey,
|
|
string receiverAddress)
|
|
{
|
|
var account = new Wallet(privateKey, _password).GetAccount(publicAddress);
|
|
var web3 = new Web3(account, chain.RpcUrl);
|
|
|
|
try
|
|
{
|
|
if (ticker == Ticker.ETH)
|
|
{
|
|
return await SendEth(amount, receiverAddress, web3);
|
|
}
|
|
|
|
return await SendToken(ticker, amount, publicAddress, receiverAddress, web3);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static async Task<bool> SendEth(decimal amount, string receiverAddress, Web3 web3)
|
|
{
|
|
web3.TransactionManager.UseLegacyAsDefault = true;
|
|
var ethService = web3.Eth.GetEtherTransferService();
|
|
var gas = await ethService.EstimateGasAsync(receiverAddress, amount);
|
|
var transaction = await ethService.TransferEtherAndWaitForReceiptAsync(receiverAddress, amount, gas: gas);
|
|
|
|
return transaction.Status.Value == 1;
|
|
}
|
|
|
|
private static async Task<bool> SendToken(
|
|
Ticker ticker,
|
|
decimal amount,
|
|
string senderAddress,
|
|
string receiverAddress,
|
|
Web3 web3)
|
|
{
|
|
var contractAddress = TokenService.GetContractAddress(ticker);
|
|
var transactionMessage = new TransferFunction
|
|
{
|
|
FromAddress = senderAddress,
|
|
To = receiverAddress,
|
|
Value = Web3.Convert.ToWei(amount)
|
|
};
|
|
|
|
var transferHandler = web3.Eth.GetContractTransactionHandler<TransferFunction>();
|
|
var transferReceipt =
|
|
await transferHandler.SendRequestAndWaitForReceiptAsync(contractAddress, transactionMessage);
|
|
|
|
var transaction =
|
|
await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transferReceipt.TransactionHash);
|
|
return transaction != null;
|
|
}
|
|
|
|
public async Task<bool> CancelOrders(Account account, Ticker ticker)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
try
|
|
{
|
|
var response = await _web3ProxyService.CallGmxServiceAsync<Web3ProxyResponse>("/cancel-orders",
|
|
new
|
|
{
|
|
account = account.Key,
|
|
walletId = account.Secret,
|
|
ticker = ticker.ToString()
|
|
});
|
|
|
|
return response.Success;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"Error canceling orders via Fastify API: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
var web3 = new Web3(wallet, chain.RpcUrl);
|
|
var service = new GmxV2Service();
|
|
return await service.CancelOrders(web3, account.Key, ticker);
|
|
}
|
|
}
|
|
|
|
public async Task<Trade> IncreasePosition(
|
|
Account account,
|
|
Ticker ticker,
|
|
TradeDirection direction,
|
|
decimal price,
|
|
decimal quantity,
|
|
decimal? leverage,
|
|
decimal? stopLossPrice = null,
|
|
decimal? takeProfitPrice = null)
|
|
{
|
|
Trade trade = null;
|
|
|
|
try
|
|
{
|
|
// If this is a Privy wallet, call the GMX service through Fastify API
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
try
|
|
{
|
|
var response = await _web3ProxyService.CallGmxServiceAsync<object>("/open-position",
|
|
new
|
|
{
|
|
account = account.Key,
|
|
walletId = account.Secret,
|
|
tradeType = price > 0 ? "limit" : "market",
|
|
ticker = ticker.ToString(),
|
|
direction = direction.ToString(),
|
|
price = price,
|
|
quantity,
|
|
leverage = leverage ?? 1.0m,
|
|
stopLossPrice = stopLossPrice,
|
|
takeProfitPrice = takeProfitPrice
|
|
});
|
|
|
|
// Create a trade object using the returned hash
|
|
var tradeType = price > 0 ? TradeType.Limit : TradeType.Market;
|
|
var tradeStatus = TradeStatus.Requested; // Use a valid enum value that exists in TradeStatus
|
|
|
|
trade = new Trade(
|
|
DateTime.UtcNow,
|
|
direction,
|
|
tradeStatus,
|
|
tradeType,
|
|
ticker,
|
|
quantity,
|
|
price,
|
|
leverage ?? 1.0m,
|
|
account.Key,
|
|
""
|
|
);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
throw;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Continue with the existing direct service call for non-Privy wallets
|
|
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
var web3 = new Web3(wallet, chain.RpcUrl);
|
|
|
|
trade = await _gmxV2Service.IncreasePosition(web3, account.Key, ticker, direction, price, quantity,
|
|
leverage);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
return trade;
|
|
}
|
|
|
|
public async Task<Trade> DecreasePosition(
|
|
Account account,
|
|
Ticker ticker,
|
|
TradeDirection direction,
|
|
decimal price,
|
|
decimal quantity,
|
|
decimal? leverage)
|
|
{
|
|
Trade trade = null;
|
|
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
try
|
|
{
|
|
var response = await _web3ProxyService.CallGmxServiceAsync<ClosePositionResponse>("/close-position",
|
|
new
|
|
{
|
|
account = account.Key,
|
|
ticker = ticker.ToString(),
|
|
direction = direction == TradeDirection.Long
|
|
? TradeDirection.Short.ToString()
|
|
: TradeDirection.Long.ToString(),
|
|
});
|
|
trade = new Trade(
|
|
DateTime.UtcNow,
|
|
direction,
|
|
TradeStatus.Requested,
|
|
TradeType.Market,
|
|
ticker,
|
|
quantity,
|
|
price,
|
|
leverage ?? 1,
|
|
response.Hash,
|
|
""
|
|
);
|
|
|
|
return trade;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
var web3 = new Web3(wallet, chain.RpcUrl);
|
|
|
|
try
|
|
{
|
|
trade = await _gmxV2Service.DecreasePosition(web3, account.Key, ticker, direction, price, quantity,
|
|
leverage ?? 1);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
return trade;
|
|
}
|
|
|
|
public async Task<Trade> DecreaseOrder(Account account, TradeType tradeType, Ticker ticker,
|
|
TradeDirection direction, decimal price, decimal quantity, decimal? leverage,
|
|
decimal? stopLossPrice = null,
|
|
decimal? takeProfitPrice = null)
|
|
{
|
|
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
var web3 = new Web3(wallet, chain.RpcUrl);
|
|
|
|
Trade trade;
|
|
try
|
|
{
|
|
// TODO: This method in GmxV2Service might not exist or needs different handling for Privy wallets.
|
|
// Commenting out for now as IncreasePosition is the priority.
|
|
// trade = await _gmxV2Service.DecreaseOrder(web3, account.Key, tradeType, ticker, direction, price,
|
|
// quantity, leverage);
|
|
trade = null; // Placeholder return
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
return trade;
|
|
}
|
|
|
|
public async Task<Trade> GetTrade(Account account, string chainName, Ticker ticker)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
var result = await _web3ProxyService.GetGmxServiceAsync<GetGmxPositionsResponse>(
|
|
"/positions",
|
|
new { account = account.Key, ticker = ticker.ToString() });
|
|
|
|
var position = result.Positions.FirstOrDefault(p => p.Ticker == ticker.ToString());
|
|
|
|
if (position == null)
|
|
return null;
|
|
|
|
// TODO: Map the position object to a Trade object
|
|
var trade = new Trade(
|
|
position.Date,
|
|
MiscExtensions.ParseEnum<TradeDirection>(position.Direction),
|
|
MiscExtensions.ParseEnum<TradeStatus>(position.Status),
|
|
MiscExtensions.ParseEnum<TradeType>(position.TradeType),
|
|
MiscExtensions.ParseEnum<Ticker>(position.Ticker),
|
|
(decimal)position.Quantity,
|
|
(decimal)position.Price,
|
|
(decimal?)position.Leverage,
|
|
account.Key,
|
|
position.ExchangeOrderId
|
|
);
|
|
|
|
return trade;
|
|
}
|
|
|
|
return await GetTrade(account.Key, chainName, ticker);
|
|
}
|
|
|
|
public async Task<List<Position>> GetPositions(Account account)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
var result = await _web3ProxyService.GetGmxServiceAsync<GetGmxPositionsResponse>(
|
|
"/positions",
|
|
new { account = account.Key });
|
|
|
|
|
|
return GmxV2Mappers.Map(result.Positions);
|
|
}
|
|
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
|
|
public async Task<Trade> GetTrade(string reference, string chainName, Ticker ticker)
|
|
{
|
|
var chain = ChainService.GetChain(chainName);
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
return await _gmxV2Service.GetTrade(web3, reference, ticker);
|
|
}
|
|
|
|
public async Task<List<FundingRate>> GetFundingRates()
|
|
{
|
|
return await _web3ProxyService.CallGmxServiceAsync<List<FundingRate>>("/gmx/funding-rates", new { });
|
|
}
|
|
|
|
public async Task<decimal> QuantityInPosition(string chainName, Account account, Ticker ticker)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
var positions = await GetPositions(account);
|
|
var positionForTicker = positions.FirstOrDefault(p => p.Ticker == ticker);
|
|
if (positionForTicker != null)
|
|
{
|
|
return positionForTicker.Open.Quantity;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
var chain = ChainService.GetChain(chainName);
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
var quantity = await _gmxV2Service.QuantityInPosition(web3, account.Key, ticker);
|
|
return quantity;
|
|
}
|
|
|
|
public async Task<decimal> GetFee(string chainName)
|
|
{
|
|
var chain = ChainService.GetChain(chainName);
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
var etherPrice = (await GetPrices(new List<string> { "ethereum" }))["ethereum"]["usd"];
|
|
var fee = await GmxService.GetFee(web3, etherPrice);
|
|
return fee;
|
|
}
|
|
|
|
public async Task<List<Trade>> GetOrders(Account account, Ticker ticker)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
var result = await _web3ProxyService.GetGmxServiceAsync<GetGmxTradesResponse>("/trades",
|
|
new { account = account.Key, ticker = ticker.ToString() });
|
|
|
|
return GmxV2Mappers.Map(result.Trades);
|
|
}
|
|
else
|
|
{
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
var web3 = new Web3(chain.RpcUrl);
|
|
// var orders = await GmxService.GetOrders(web3, account.Key, ticker);
|
|
var orders = await _gmxV2Service.GetOrders(web3, account.Key, ticker);
|
|
|
|
return GmxV2Mappers.Map(orders);
|
|
}
|
|
|
|
return new List<Trade>();
|
|
}
|
|
|
|
public async Task<bool> SetAllowance(Account account, Ticker ticker, BigInteger amount)
|
|
{
|
|
if (account.IsPrivyWallet)
|
|
{
|
|
var allowance = await _web3ProxyService.CallPrivyServiceAsync<PrivyApproveTokenResponse>("/approve-token",
|
|
new
|
|
{
|
|
address = account.Key, walletId = account.Secret, ticker = ticker.ToString(),
|
|
amount = amount.Equals(0) ? null : amount.ToString()
|
|
});
|
|
return false;
|
|
}
|
|
|
|
var web3 = BuildWeb3ForAccount(account);
|
|
var contractAddress = TokenService.GetContractAddress(ticker);
|
|
var approval = await EvmBase.ApproveToken(web3, account.Key, contractAddress,
|
|
Arbitrum.AddressV2.SyntheticsRouter, amount);
|
|
return approval;
|
|
}
|
|
|
|
private Web3 BuildWeb3ForAccount(Account account)
|
|
{
|
|
var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key);
|
|
var chain = ChainService.GetChain(Constants.Chains.Arbitrum);
|
|
return new Web3(wallet, chain.RpcUrl);
|
|
}
|
|
|
|
|
|
public async Task<(string Id, string Address)> CreatePrivyWallet()
|
|
{
|
|
var privyWallet = await _web3ProxyService.CallPrivyServiceAsync<PrivyWallet>("/privy/create-wallet", new { });
|
|
return (privyWallet.Id, privyWallet.Address);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Signs a message using the embedded wallet
|
|
/// </summary>
|
|
/// <param name="embeddedWalletId">The wallet id of the embedded wallet</param>
|
|
/// <param name="address">The address of the embedded wallet</param>
|
|
/// <param name="message">The message to sign</param>
|
|
/// <returns>The signature response</returns>
|
|
public async Task<string> SignMessageAsync(string embeddedWalletId, string address, string message)
|
|
{
|
|
// Construct the request body using the exact format from Privy documentati
|
|
var requestBody = new
|
|
{
|
|
address = address,
|
|
walletId = embeddedWalletId,
|
|
message = message,
|
|
};
|
|
|
|
var response =
|
|
await _web3ProxyService.CallPrivyServiceAsync<PrivySigningResponse>("sign-message", requestBody);
|
|
return response.Signature;
|
|
}
|
|
|
|
// Overload to match IEvmManager interface
|
|
public async Task<List<Candle>> GetCandles(Ticker ticker, DateTime startDate, Timeframe timeframe)
|
|
{
|
|
return await GetCandles(ticker, startDate, timeframe, false);
|
|
}
|
|
} |