MaxBuyPrice-Skala korrigiert + Blockchain-Abo-Logik extrahiert & getestet

- Fix (Richard): MaxBuyPrice-Skalen-Korrektur analog PreRedeemLimit
  (Migration FixMaxBuyPriceScale: Werte > 1 wie 98.0/99.0 -> /100 = 0.98/0.99;
  auf MySQL angewendet). Cap greift bei den betroffenen Accounts jetzt wieder.
  Beschreibung des Feldes um den Skalen-Hinweis ergaenzt.

- Testnetz erweitert: reine Klasse BlockchainSubscription (Logic/) fuer das
  Alchemy-Abo (PadAddress + Filter-Batching in 3er-Chunks, Buy=Topic3/Sell=Topic2
  je Chunk). AlchemyWebsocketService ruft sie jetzt (verhaltensneutral;
  Konstanten + lokale Methoden entfernt). BlockchainSubscriptionTests: 13 Faelle
  (Padding, Chunk-Anzahl 0/1/3/4/6/7, Topic-Zuordnung, CTF-Adresse/Topics).

Gesamt 137 Tests gruen. Build/Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 10:54:46 +02:00
co-authored by Claude Opus 4.8
parent 360f264ed8
commit 6a8ecbc80a
6 changed files with 457 additions and 31 deletions
@@ -6,16 +6,13 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PolyTrader.Core.Streaming;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services
{
public class AlchemyWebsocketService : BackgroundService
{
private const string CtfContractAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
private const string TransferSingleTopic = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
private const string TransferBatchTopic = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7ce";
private readonly TradingState _state;
private readonly CopyTradingState _copyState;
private readonly ServerSettings _settings;
@@ -98,7 +95,7 @@ namespace PolyTraderSharp.Services
var activeTraders = _copyState.Traders.Values.Where(t => t.IsActive).ToList();
var activeStateHash = string.Join(",", activeTraders.OrderBy(t => t.Id).Select(t => t.WalletAddress.ToLowerInvariant()));
var subscription = BuildSubscription(activeTraders);
var subscription = BlockchainSubscription.Build(activeTraders);
if (subscription.Filters.Count == 0)
{
_logger.Info("Keine aktiven Master-Trader. Socket läuft im Standby...");
@@ -148,24 +145,6 @@ namespace PolyTraderSharp.Services
}
}
private BlockchainWssSubscription BuildSubscription(List<TrackedTrader> activeTraders)
{
var subscription = new BlockchainWssSubscription();
var paddedAddresses = activeTraders.Select(t => PadAddress(t.WalletAddress)).ToList();
var topic0 = new List<string> { TransferSingleTopic, TransferBatchTopic };
const int batchSize = 3; // Alchemy begrenzt Topic-Arrays auf max. 3-4 Einträge
for (int i = 0; i < paddedAddresses.Count; i += batchSize)
{
var chunk = paddedAddresses.Skip(i).Take(batchSize).ToList();
// Buys: Master-Trader ist Empfänger (Topic 3)
subscription.Filters.Add(new LogSubscriptionFilter { Address = CtfContractAddress, Topic0 = topic0, Topic3 = chunk });
// Sells: Master-Trader ist Sender (Topic 2)
subscription.Filters.Add(new LogSubscriptionFilter { Address = CtfContractAddress, Topic0 = topic0, Topic2 = chunk });
}
return subscription;
}
private void OnLog(BlockchainLogEvent evt)
{
if (evt.Topics.Count < 4) return;
@@ -179,7 +158,7 @@ namespace PolyTraderSharp.Services
foreach (var trader in activeTraders)
{
var padded = PadAddress(trader.WalletAddress);
var padded = BlockchainSubscription.PadAddress(trader.WalletAddress);
if (fromTopic == padded || toTopic == padded)
{
triggeredAddress = trader.WalletAddress;
@@ -192,11 +171,5 @@ namespace PolyTraderSharp.Services
_traderMonitor.TriggerFastBlockchainPoll(evt.TransactionHash, _settings.PolygonRpcUrl, triggeredAddress);
}
}
private string PadAddress(string address)
{
string stripped = address.Replace("0x", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant();
return "0x" + stripped.PadLeft(64, '0');
}
}
}