public async Task Process(Swap swap, CancellationToken cancellationToken) { // Ignore transactions that are beyond their deadline, or outside of the configured range. if (swap.Deadline < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { return; } await _backoffService.Throttle(async() => { using var waiter = _transactionWaitService.GetWaiter(swap.TransactionHash); var prepWeb3 = new Web3(new Account(_batBotOptions.PrepPrivateKey), _batBotOptions.BlockchainEndpointHttpUrl); var detectedMessage = $"🔎 Detected transaction: {_messagingService.GetEtherscanUrl($"tx/{swap.TransactionHash}")}"; await SendPreFrontRunMessage(detectedMessage, MessageTier.Double); var pair = await _pairInfoService.GetPair(prepWeb3, (swap.Path.First(), swap.Path.Last()), cancellationToken); if (pair is null) { await SendPreFrontRunMessage("⛔ Unable to obtain pair contract for this swap", MessageTier.End); return; } var tokenIn = pair.GetToken(swap.Path.First()); var tokenOut = pair.GetToken(swap.Path.Last()); var swapMessage = $"💱 Swap {Web3.Convert.FromWei(swap.AmountIn)} {tokenIn.Symbol} for {Web3.Convert.FromWei(swap.AmountOutMin, tokenOut.Decimals)} {tokenOut.Symbol} with gas price {Web3.Convert.FromWei(swap.GasPrice.GetValueOrDefault(), UnitConversion.EthUnit.Gwei)} Gwei"; await SendPreFrontRunMessage(swapMessage, MessageTier.End | MessageTier.Single); try { #region Validation if (!_settingsOptions.YoloMode) { if (_pairWhitelist.TryGetValue(pair.Id, out var allowed) && !allowed) { await SendPreFrontRunMessage("⛔ This token is blacklisted from a previous validation", MessageTier.End); return; } var swapChecks = new List <Func <Task <(bool, string)> > > { // Ignore transactions that are outside of the configured ETH range. async() => (await Task.Run(() => swap.AmountIn.Between(Web3.Convert.ToWei(_settingsOptions.MinimumTargetSwapEth), Web3.Convert.ToWei(_settingsOptions.MaximumTargetSwapEth)), cancellationToken), "Amount to send outside of the configured range"), // Ignore transactions with too high gas price. async() => (await Task.Run(() => swap.GasPrice <= Web3.Convert.ToWei(_settingsOptions.MaximumGasPrice, UnitConversion.EthUnit.Gwei), cancellationToken), "Gas price higher than the configured limit") }; var pairChecks = new List <Func <Task <(bool, string)> > > { // Check that the liquidity of the token pair is within the configured range. async() => (await Task.Run(() => pair.ReserveUsd.Between((Rational)_settingsOptions.MinimumLiquidity, (Rational)_settingsOptions.MaximumLiquidity), cancellationToken), $"Liquidity (${pair.ReserveUsd.WholePart:N0}) outside of the configured range"), // Check that the pair contract wasn't created too recently to protect against possible honeypots. async() => (await Task.Run(() => pair.Created.AddDays(_settingsOptions.MinimumPairContractAge) < DateTime.UtcNow, cancellationToken), $"Pair contract created too recently ({pair.Created:g})"), async() => { // Check that there have been historical swaps selling the output token, to ensure the contract is legitimate. var idName = JsonHelper.GetJsonPropertyName <PairType>(nameof(PairType.Id)); var recentSwaps = new List <Models.Graph.Swap>(); int previousCount; do { // Retrieve the swaps in batches until there are no more fetched, or the configured threshold is reached. previousCount = recentSwaps.Count; recentSwaps.AddRange(_mapper.Map <IEnumerable <Models.Graph.Swap> >((await _graphService.SendQuery <SwapsResponse>( new Dictionary <string, string> { { idName, Graph.Types.Id } }, new Dictionary <string, object> { { Graph.OrderBy, JsonHelper.GetJsonPropertyName <SwapType>(nameof(SwapType.Timestamp)) }, { Graph.OrderDirection, Graph.Descending }, { Graph.Where, new Dictionary <string, object> { { JsonHelper.GetJsonPropertyName <PairResponse>(nameof(PairResponse.Pair)), $"${idName}" }, { $"{JsonHelper.GetJsonPropertyName<SwapType>(pair.GetToken(tokenIn.Id) == pair.Token0 ? nameof(SwapType.Amount0Out) : nameof(SwapType.Amount1Out))}{Graph.Gte}", _settingsOptions.HistoricalAnalysisMinEthOut }, { $"{JsonHelper.GetJsonPropertyName<SwapType>(nameof(SwapType.Timestamp))}{Graph.Lt}", recentSwaps.Any() ? ((DateTimeOffset)recentSwaps.Last().Timestamp).ToUnixTimeSeconds() : DateTimeOffset.UtcNow.ToUnixTimeSeconds() } } } }, new Dictionary <string, HashSet <string> > { { nameof(SwapType), new HashSet <string> { nameof(SwapType.Pair) } } }, new { id = pair.Id }, cancellationToken: cancellationToken)).Swaps)); } while (recentSwaps.Count.Between(previousCount, _settingsOptions.HistoricalAnalysisMinFetchThreshold, false)); var relevantSwaps = (await Task.WhenAll(recentSwaps.Select(async s => await _ethereumService.GetSwap(s.Transaction.Id)))).Where(s => s != null).ToArray(); if (relevantSwaps.Length < _settingsOptions.HistoricalAnalysisMinRelevantSwaps) { return(false, $"Number of relevant swaps ({relevantSwaps.Length} out of {recentSwaps.Count}) is too low."); } var uniqueRecipients = relevantSwaps.GroupBy(s => s.To).Count(); return(uniqueRecipients / (double)relevantSwaps.Length < _settingsOptions.MinimumUniqueSellRecipientsRatio ? (false, $"Number of unique recipients in the relevant swaps ({uniqueRecipients} out of {relevantSwaps.Length}) is too low.") : (true, default));
public async Task Subscribe(CancellationToken cancellationToken) { // http://docs.nethereum.com/en/latest/nethereum-subscriptions-streaming/ using var streamingClient = new StreamingWebSocketClient(_batBotOptions.BlockchainEndpointWssUrl); var subscription = new EthNewPendingTransactionObservableSubscription(streamingClient); await _ethereumService.HandleSubscription(streamingClient, subscription, async() => await subscription.SubscribeAsync(), async th => { await _messagingService.SendLogMessage($"Pending Transaction Hash: {th}"); // Don't process transactions (discard) when one is being processed for front running. if (_backoffService.BackOff()) { return; } await _backoffService.Protect(async() => await _transactionProcessorService.Process(await _ethereumService.GetSwap(th), cancellationToken)); }, null, cancellationToken); }