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:
co-authored by
Claude Opus 4.8
parent
360f264ed8
commit
6a8ecbc80a
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PolyTrader.Core.Streaming;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// Reine Logik zum Aufbau des Alchemy-Blockchain-Abos für die getrackten Master-Wallets.
|
||||
/// Aus <c>AlchemyWebsocketService</c> herausgezogen, damit das Filter-Batching und das
|
||||
/// Adress-Padding vollständig testbar sind (falsche Filter = verpasste Master-Trades).
|
||||
/// Verhalten 1:1 aus dem Listener übernommen.
|
||||
/// </summary>
|
||||
public static class BlockchainSubscription
|
||||
{
|
||||
/// <summary>Polymarket CTF (ConditionalTokens) Contract – Quelle der Transfer-Events.</summary>
|
||||
public const string CtfContractAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
|
||||
public const string TransferSingleTopic = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
|
||||
public const string TransferBatchTopic = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7ce";
|
||||
|
||||
/// <summary>Alchemy begrenzt Topic-Arrays auf ~3–4 Einträge → Batchgröße 3.</summary>
|
||||
public const int BatchSize = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Wandelt eine Wallet-Adresse in die 32-Byte-Topic-Form (lowercase, ohne 0x, links mit
|
||||
/// Nullen auf 64 Hex-Zeichen aufgefüllt, mit 0x-Präfix). Nötig, weil Log-Topics die
|
||||
/// Adresse in voller Wortbreite enthalten.
|
||||
/// </summary>
|
||||
public static string PadAddress(string address)
|
||||
{
|
||||
string stripped = address.Replace("0x", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant();
|
||||
return "0x" + stripped.PadLeft(64, '0');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut das Abo: pro 3er-Chunk der gepaddeten Master-Adressen je EIN Filter für Käufe
|
||||
/// (Master ist Empfänger → Topic3) und EIN Filter für Verkäufe (Master ist Sender → Topic2),
|
||||
/// beide auf dem CTF-Contract mit den Transfer-Topics.
|
||||
/// </summary>
|
||||
public static BlockchainWssSubscription Build(IEnumerable<TrackedTrader> activeTraders)
|
||||
{
|
||||
var subscription = new BlockchainWssSubscription();
|
||||
var paddedAddresses = activeTraders.Select(t => PadAddress(t.WalletAddress)).ToList();
|
||||
var topic0 = new List<string> { TransferSingleTopic, TransferBatchTopic };
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ namespace PolyTraderSharp.Models
|
||||
[DisplayName("Max. Kaufpreis (0–1)")]
|
||||
[Description("Absolute Preis-Obergrenze pro Share auf der 0–1-Skala (1.00 = 100 ¢). Kauf wird übersprungen, wenn der " +
|
||||
"Master über diesem Preis kauft; zusätzlich wird unser Limitpreis hierauf gedeckelt. " +
|
||||
"Beispiel: 0.98 = nichts über 98 ¢ kaufen (nahe 1.00 ist das Aufwärtspotenzial gering, das Risiko asymmetrisch).")]
|
||||
"Beispiel: 0.98 = nichts über 98 ¢ kaufen (nahe 1.00 ist das Aufwärtspotenzial gering, das Risiko asymmetrisch). " +
|
||||
"WICHTIG: Wert IMMER auf der 0–1-Skala angeben. Migrierte Alt-Werte auf Cent-Skala (z. B. 98.0) wurden auf " +
|
||||
"0.98 korrigiert – ein Wert > 1 hätte den Deckel nie greifen lassen (Vergleich erfolgt gegen den 0–1-Preis).")]
|
||||
public decimal MaxBuyPrice { get; set; } = 0.98m;
|
||||
|
||||
[Category("01. Risk Management")]
|
||||
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
[DbContext(typeof(CopyTradingDbContext))]
|
||||
[Migration("20260707084602_FixMaxBuyPriceScale")]
|
||||
partial class FixMaxBuyPriceScale
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.ClosedTrade", b =>
|
||||
{
|
||||
b.Property<int>("TradeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("EntryPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ExitPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("ExitReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<DateTime>("OpenedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<decimal>("PnlPercent")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<decimal>("TotalFees")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("TradeId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("SourceTraderId");
|
||||
|
||||
b.HasIndex("TokenId");
|
||||
|
||||
b.ToTable("mod_copytrading_closed_trades", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.CopyTradingAccountSettings", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MaxBuyPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxPriceDifference")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxSpreadPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MinSellRatioPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMarketLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMasterLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PreRedeemLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ProfitTarget")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("SellFloorPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime24h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime6h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime72h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTimeNone")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("mod_copytrading_account_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.MasterTraderHistoryRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClosedAt");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.ToTable("mod_copytrading_mt_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.TrackedTrader", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AssignedAccountIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("AutoPauseEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("MakerEntry")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Reasoning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<double>("TotalPnl")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<int>("TotalTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WalletAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("WinningTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double>("Winrate30t")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("mod_copytrading_traders", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class FixMaxBuyPriceScale : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Skalen-Korrektur analog PreRedeemLimit: MaxBuyPrice wird gegen den 0-1-Preis
|
||||
// verglichen/gedeckelt. Migrierte Cent-Skala-Werte (z. B. 98.0, 99.0) haetten den
|
||||
// Deckel nie greifen lassen -> Werte > 1 durch 100 teilen (98.0 -> 0.98).
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE mod_copytrading_account_settings SET MaxBuyPrice = MaxBuyPrice / 100 WHERE MaxBuyPrice > 1;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PolyTrader.Modules.CopyTrading.Logic;
|
||||
using PolyTraderSharp.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sichert den Aufbau des Alchemy-Blockchain-Abos ab: falsche Filter/Padding bedeuten
|
||||
/// verpasste (oder falsch zugeordnete) Master-Trades.
|
||||
/// </summary>
|
||||
public class BlockchainSubscriptionTests
|
||||
{
|
||||
private static List<TrackedTrader> Traders(int n) =>
|
||||
Enumerable.Range(1, n).Select(i => new TrackedTrader { Id = i, WalletAddress = $"0x{i:x40}" }).ToList();
|
||||
|
||||
// ---------------- PadAddress ----------------
|
||||
|
||||
[Fact]
|
||||
public void PadAddress_strips_prefix_lowercases_and_pads_to_32_bytes()
|
||||
{
|
||||
var r = BlockchainSubscription.PadAddress("0x628914CF1e96A9D1Ab8F0489A9f64be5633bac41");
|
||||
Assert.Equal("0x000000000000000000000000628914cf1e96a9d1ab8f0489a9f64be5633bac41", r);
|
||||
Assert.Equal(66, r.Length); // "0x" + 64 Hex-Zeichen
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PadAddress_pads_short_input_with_leading_zeros()
|
||||
{
|
||||
Assert.Equal("0x" + new string('0', 61) + "abc", BlockchainSubscription.PadAddress("0xABC"));
|
||||
}
|
||||
|
||||
// ---------------- Build: Chunk-Anzahl ----------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(1, 2)] // 1 Chunk -> Buy + Sell
|
||||
[InlineData(3, 2)] // 1 Chunk (voll)
|
||||
[InlineData(4, 4)] // 2 Chunks (3 + 1)
|
||||
[InlineData(6, 4)] // 2 Chunks (3 + 3)
|
||||
[InlineData(7, 6)] // 3 Chunks (3 + 3 + 1)
|
||||
public void Build_creates_two_filters_per_chunk(int traderCount, int expectedFilters)
|
||||
{
|
||||
Assert.Equal(expectedFilters, BlockchainSubscription.Build(Traders(traderCount)).Filters.Count);
|
||||
}
|
||||
|
||||
// ---------------- Build: Filter-Inhalt ----------------
|
||||
|
||||
[Fact]
|
||||
public void Build_pairs_buy_topic3_and_sell_topic2()
|
||||
{
|
||||
var sub = BlockchainSubscription.Build(Traders(1));
|
||||
var buy = sub.Filters[0];
|
||||
var sell = sub.Filters[1];
|
||||
|
||||
Assert.NotNull(buy.Topic3);
|
||||
Assert.Null(buy.Topic2);
|
||||
Assert.NotNull(sell.Topic2);
|
||||
Assert.Null(sell.Topic3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_sets_ctf_address_and_transfer_topics()
|
||||
{
|
||||
var buy = BlockchainSubscription.Build(Traders(1)).Filters[0];
|
||||
Assert.Equal(BlockchainSubscription.CtfContractAddress, buy.Address);
|
||||
Assert.Equal(
|
||||
new[] { BlockchainSubscription.TransferSingleTopic, BlockchainSubscription.TransferBatchTopic },
|
||||
buy.Topic0.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_chunks_addresses_by_three()
|
||||
{
|
||||
var sub = BlockchainSubscription.Build(Traders(4)); // 3 + 1
|
||||
Assert.Equal(3, sub.Filters[0].Topic3!.Count); // Chunk 1 (Buy)
|
||||
Assert.Equal(3, sub.Filters[1].Topic2!.Count); // Chunk 1 (Sell)
|
||||
Assert.Single(sub.Filters[2].Topic3!); // Chunk 2 (Buy)
|
||||
Assert.Single(sub.Filters[3].Topic2!); // Chunk 2 (Sell)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_uses_padded_addresses()
|
||||
{
|
||||
var addr = BlockchainSubscription.Build(Traders(1)).Filters[0].Topic3![0];
|
||||
Assert.Equal(66, addr.Length);
|
||||
Assert.StartsWith("0x", addr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_empty_returns_no_filters()
|
||||
{
|
||||
Assert.Empty(BlockchainSubscription.Build(Traders(0)).Filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user