2
0
mirror of synced 2025-01-11 17:14:25 +00:00

Merge branch 'better-chain-events'

This commit is contained in:
Ben 2024-06-27 11:16:34 +02:00
commit 933d5e7d4d
No known key found for this signature in database
GPG Key ID: 541B9D8C9F1426A1
16 changed files with 428 additions and 156 deletions

View File

@ -1,4 +1,5 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using System.Numerics;
using Utils;
@ -11,7 +12,7 @@ namespace CodexContractsPlugin.ChainMonitor
void OnRequestFinished(IChainStateRequest request);
void OnRequestFulfilled(IChainStateRequest request);
void OnRequestCancelled(IChainStateRequest request);
void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex);
void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex);
void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex);
}
@ -116,7 +117,7 @@ namespace CodexContractsPlugin.ChainMonitor
if (r == null) return;
r.Hosts.Add(request.Host, (int)request.SlotIndex);
r.Log($"[{request.Block.BlockNumber}] SlotFilled (host:'{request.Host}', slotIndex:{request.SlotIndex})");
handler.OnSlotFilled(r, request.SlotIndex);
handler.OnSlotFilled(r, request.Host, request.SlotIndex);
}
private void ApplyEvent(SlotFreedEventDTO request)

View File

@ -1,4 +1,5 @@
using System.Numerics;
using GethPlugin;
using System.Numerics;
namespace CodexContractsPlugin.ChainMonitor
{
@ -20,7 +21,7 @@ namespace CodexContractsPlugin.ChainMonitor
{
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
public void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex)
{
}

View File

@ -10,12 +10,14 @@ namespace BiblioTech
public class CommandHandler
{
private readonly DiscordSocketClient client;
private readonly CustomReplacement replacement;
private readonly BaseCommand[] commands;
private readonly ILog log;
public CommandHandler(ILog log, DiscordSocketClient client, params BaseCommand[] commands)
public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, params BaseCommand[] commands)
{
this.client = client;
this.replacement = replacement;
this.commands = commands;
this.log = log;
client.Ready += Client_Ready;
@ -31,7 +33,7 @@ namespace BiblioTech
var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray();
if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel");
Program.AdminChecker.SetAdminChannel(adminChannels.First());
Program.RoleDriver = new RoleDriver(client, log);
Program.RoleDriver = new RoleDriver(client, log, replacement);
var builders = commands.Select(c =>
{

View File

@ -1,4 +1,5 @@
using BiblioTech.Options;
using BiblioTech.Rewards;
namespace BiblioTech.Commands
{
@ -10,12 +11,14 @@ namespace BiblioTech.Commands
private readonly AddSprCommand addSprCommand;
private readonly ClearSprsCommand clearSprsCommand;
private readonly GetSprCommand getSprCommand;
private readonly LogReplaceCommand logReplaceCommand;
public AdminCommand(SprCommand sprCommand)
public AdminCommand(SprCommand sprCommand, CustomReplacement replacement)
{
addSprCommand = new AddSprCommand(sprCommand);
clearSprsCommand = new ClearSprsCommand(sprCommand);
getSprCommand = new GetSprCommand(sprCommand);
logReplaceCommand = new LogReplaceCommand(replacement);
}
public override string Name => "admin";
@ -29,7 +32,8 @@ namespace BiblioTech.Commands
whoIsCommand,
addSprCommand,
clearSprsCommand,
getSprCommand
getSprCommand,
logReplaceCommand
};
protected override async Task Invoke(CommandContext context)
@ -52,6 +56,7 @@ namespace BiblioTech.Commands
await addSprCommand.CommandHandler(context);
await clearSprsCommand.CommandHandler(context);
await getSprCommand.CommandHandler(context);
await logReplaceCommand.CommandHandler(context);
}
public class ClearUserAssociationCommand : SubCommandOption
@ -194,7 +199,7 @@ namespace BiblioTech.Commands
}
}
public class GetSprCommand: SubCommandOption
public class GetSprCommand : SubCommandOption
{
private readonly SprCommand sprCommand;
@ -210,5 +215,56 @@ namespace BiblioTech.Commands
await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'")));
}
}
public class LogReplaceCommand : SubCommandOption
{
private readonly CustomReplacement replacement;
private readonly StringOption fromOption = new StringOption("from", "string to replace", true);
private readonly StringOption toOption = new StringOption("to", "string to replace with", false);
public LogReplaceCommand(CustomReplacement replacement)
: base(name: "logreplace",
description: "Replaces all occurances of 'from' with 'to' in ChainEvent messages. Leave 'to' empty to remove a replacement.")
{
this.replacement = replacement;
}
public override CommandOption[] Options => new[] { fromOption, toOption };
protected override async Task onSubCommand(CommandContext context)
{
var from = await fromOption.Parse(context);
var to = await toOption.Parse(context);
if (string.IsNullOrEmpty(from))
{
await context.Followup("'from' not received");
return;
}
if (from.Length < 5)
{
await context.Followup("'from' must be length 5 or greater.");
return;
}
if (string.IsNullOrEmpty(to))
{
replacement.Remove(from);
await context.Followup($"Replace for '{from}' removed.");
}
else
{
if (to.Length < 5)
{
await context.Followup("'to' must be length 5 or greater.");
return;
}
replacement.Add(from, to);
await context.Followup($"Replace added '{from}' -->> '{to}'.");
}
}
}
}
}

View File

@ -11,6 +11,7 @@ namespace BiblioTech
public class Program
{
private DiscordSocketClient client = null!;
private readonly CustomReplacement replacement = new CustomReplacement();
public static Configuration Config { get; private set; } = null!;
public static UserRepo UserRepo { get; } = new UserRepo();
@ -73,13 +74,13 @@ namespace BiblioTech
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(Log, client,
var handler = new CommandHandler(Log, client, replacement,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand),
new AdminCommand(sprCommand, replacement),
new MarketCommand()
);

View File

@ -0,0 +1,72 @@
using Discord.WebSocket;
using Logging;
namespace BiblioTech.Rewards
{
public class ChainEventsSender
{
private readonly ILog log;
private readonly CustomReplacement replacement;
private readonly SocketTextChannel? eventsChannel;
public ChainEventsSender(ILog log, CustomReplacement replacement, SocketTextChannel? eventsChannel)
{
this.log = log;
this.replacement = replacement;
this.eventsChannel = eventsChannel;
}
public async Task ProcessChainEvents(string[] eventsOverview)
{
if (eventsChannel == null || eventsOverview == null || !eventsOverview.Any()) return;
try
{
await Task.Run(async () =>
{
var users = Program.UserRepo.GetAllUserData();
foreach (var e in eventsOverview)
{
if (!string.IsNullOrEmpty(e))
{
var @event = ApplyReplacements(users, e);
await eventsChannel.SendMessageAsync(@event);
await Task.Delay(3000);
}
}
});
}
catch (Exception ex)
{
log.Error("Failed to process chain events: " + ex);
}
}
private string ApplyReplacements(UserData[] users, string msg)
{
var result = ApplyUserAddressReplacements(users, msg);
result = ApplyCustomReplacements(result);
return result;
}
private string ApplyUserAddressReplacements(UserData[] users, string msg)
{
foreach (var user in users)
{
if (user.CurrentAddress != null &&
!string.IsNullOrEmpty(user.CurrentAddress.Address) &&
!string.IsNullOrEmpty(user.Name))
{
msg = msg.Replace(user.CurrentAddress.Address, user.Name);
}
}
return msg;
}
private string ApplyCustomReplacements(string result)
{
return replacement.Apply(result);
}
}
}

View File

@ -0,0 +1,34 @@
namespace BiblioTech.Rewards
{
public class CustomReplacement
{
private readonly Dictionary<string, string> replacements = new Dictionary<string, string>();
public void Add(string from, string to)
{
if (replacements.ContainsKey(from))
{
replacements[from] = to;
}
else
{
replacements.Add(from, to);
}
}
public void Remove(string from)
{
replacements.Remove(from);
}
public string Apply(string msg)
{
var result = msg;
foreach (var pair in replacements)
{
result.Replace(pair.Key, pair.Value);
}
return result;
}
}
}

View File

@ -11,15 +11,15 @@ namespace BiblioTech.Rewards
private readonly DiscordSocketClient client;
private readonly ILog log;
private readonly SocketTextChannel? rewardsChannel;
private readonly SocketTextChannel? eventsChannel;
private readonly ChainEventsSender eventsSender;
private readonly RewardRepo repo = new RewardRepo();
public RoleDriver(DiscordSocketClient client, ILog log)
public RoleDriver(DiscordSocketClient client, ILog log, CustomReplacement replacement)
{
this.client = client;
this.log = log;
rewardsChannel = GetChannel(Program.Config.RewardsChannelId);
eventsChannel = GetChannel(Program.Config.ChainEventsChannelId);
eventsSender = new ChainEventsSender(log, replacement, GetChannel(Program.Config.ChainEventsChannelId));
}
public async Task GiveRewards(GiveRewardsCommand rewards)
@ -31,7 +31,7 @@ namespace BiblioTech.Rewards
await ProcessRewards(rewards);
}
await ProcessChainEvents(rewards.EventsOverview);
await eventsSender.ProcessChainEvents(rewards.EventsOverview);
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
@ -60,29 +60,6 @@ namespace BiblioTech.Rewards
return GetGuild().TextChannels.SingleOrDefault(c => c.Id == id);
}
private async Task ProcessChainEvents(string[] eventsOverview)
{
if (eventsChannel == null || eventsOverview == null || !eventsOverview.Any()) return;
try
{
await Task.Run(async () =>
{
foreach (var e in eventsOverview)
{
if (!string.IsNullOrEmpty(e))
{
await eventsChannel.SendMessageAsync(e);
await Task.Delay(3000);
}
}
});
}
catch (Exception ex)
{
log.Error("Failed to process chain events: " + ex);
}
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
log.Log("Loading all users..");

View File

@ -0,0 +1,66 @@
using CodexContractsPlugin;
using GethPlugin;
namespace BiblioTech
{
public class UserData
{
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents, bool notificationsEnabled)
{
DiscordId = discordId;
Name = name;
CreatedUtc = createdUtc;
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
NotificationsEnabled = notificationsEnabled;
}
public ulong DiscordId { get; }
public string Name { get; }
public DateTime CreatedUtc { get; }
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
public bool NotificationsEnabled { get; set; }
public string[] CreateOverview()
{
return new[]
{
$"name: '{Name}' - id:{DiscordId}",
$"joined: {CreatedUtc.ToString("o")}",
$"current address: {CurrentAddress}",
$"{AssociateEvents.Count + MintEvents.Count} total bot events."
};
}
}
public class UserAssociateAddressEvent
{
public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress)
{
Utc = utc;
NewAddress = newAddress;
}
public DateTime Utc { get; }
public EthAddress? NewAddress { get; }
}
public class UserMintEvent
{
public UserMintEvent(DateTime utc, EthAddress usedAddress, Transaction<Ether>? ethReceived, Transaction<TestToken>? testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Transaction<Ether>? EthReceived { get; }
public Transaction<TestToken>? TestTokensMinted { get; }
}
}

View File

@ -8,6 +8,7 @@ namespace BiblioTech
public class UserRepo
{
private readonly object repoLock = new object();
private readonly Dictionary<ulong, UserData> cache = new Dictionary<ulong, UserData>();
public bool AssociateUserWithAddress(IUser user, EthAddress address)
{
@ -33,6 +34,12 @@ namespace BiblioTech
}
}
public UserData[] GetAllUserData()
{
if (cache.Count == 0) LoadAllUserData();
return cache.Values.ToArray();
}
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction<Ether>? eth, Transaction<TestToken>? tokens)
{
lock (repoLock)
@ -151,12 +158,19 @@ namespace BiblioTech
private UserData? GetUserData(IUser user)
{
if (cache.ContainsKey(user.Id))
{
return cache[user.Id];
}
var filename = GetFilename(user);
if (!File.Exists(filename))
{
return null;
}
return JsonConvert.DeserializeObject<UserData>(File.ReadAllText(filename))!;
var userData = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(filename))!;
cache.Add(userData.DiscordId, userData);
return userData;
}
private UserData GetOrCreate(IUser user)
@ -181,6 +195,15 @@ namespace BiblioTech
var filename = GetFilename(userData);
if (File.Exists(filename)) File.Delete(filename);
File.WriteAllText(filename, JsonConvert.SerializeObject(userData));
if (cache.ContainsKey(userData.DiscordId))
{
cache[userData.DiscordId] = userData;
}
else
{
cache.Add(userData.DiscordId, userData);
}
}
private static string GetFilename(IUser user)
@ -197,66 +220,29 @@ namespace BiblioTech
{
return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json");
}
}
public class UserData
{
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents, bool notificationsEnabled)
private void LoadAllUserData()
{
DiscordId = discordId;
Name = name;
CreatedUtc = createdUtc;
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
NotificationsEnabled = notificationsEnabled;
}
public ulong DiscordId { get; }
public string Name { get; }
public DateTime CreatedUtc { get; }
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
public bool NotificationsEnabled { get; set; }
public string[] CreateOverview()
{
return new[]
try
{
$"name: '{Name}' - id:{DiscordId}",
$"joined: {CreatedUtc.ToString("o")}",
$"current address: {CurrentAddress}",
$"{AssociateEvents.Count + MintEvents.Count} total bot events."
};
var files = Directory.GetFiles(Program.Config.UserDataPath);
foreach (var file in files)
{
try
{
var userData = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
if (userData != null && userData.DiscordId > 0)
{
cache.Add(userData.DiscordId, userData);
}
}
catch { }
}
}
catch (Exception ex)
{
Program.Log.Error("Exception while trying to load all user data: " + ex);
}
}
}
public class UserAssociateAddressEvent
{
public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress)
{
Utc = utc;
NewAddress = newAddress;
}
public DateTime Utc { get; }
public EthAddress? NewAddress { get; }
}
public class UserMintEvent
{
public UserMintEvent(DateTime utc, EthAddress usedAddress, Transaction<Ether>? ethReceived, Transaction<TestToken>? testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Transaction<Ether>? EthReceived { get; }
public Transaction<TestToken>? TestTokensMinted { get; }
}
}

View File

@ -1,41 +0,0 @@
using Logging;
namespace TestNetRewarder
{
public class BufferLogger : ILog
{
private readonly List<string> lines = new List<string>();
public void AddStringReplace(string from, string to)
{
throw new NotImplementedException();
}
public LogFile CreateSubfile(string ext = "log")
{
throw new NotImplementedException();
}
public void Debug(string message = "", int skipFrames = 0)
{
lines.Add(message);
}
public void Error(string message)
{
lines.Add($"Error: {message}");
}
public void Log(string message)
{
lines.Add(message);
}
public string[] Get()
{
var result = lines.ToArray();
lines.Clear();
return result;
}
}
}

View File

@ -1,4 +1,5 @@
using CodexContractsPlugin.ChainMonitor;
using GethPlugin;
using System.Numerics;
namespace TestNetRewarder
@ -32,9 +33,9 @@ namespace TestNetRewarder
foreach (var handler in handlers) handler.OnRequestFulfilled(request);
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
public void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex)
{
foreach (var handler in handlers) handler.OnSlotFilled(request, slotIndex);
foreach (var handler in handlers) handler.OnSlotFilled(request, host, slotIndex);
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)

View File

@ -0,0 +1,122 @@
using CodexContractsPlugin;
using CodexContractsPlugin.ChainMonitor;
using GethPlugin;
using System.Numerics;
using Utils;
namespace TestNetRewarder
{
public class EventsFormatter : IChainStateChangeHandler
{
private static readonly string nl = Environment.NewLine;
private readonly List<string> events = new List<string>();
public string[] GetEvents()
{
var result = events.ToArray();
events.Clear();
return result;
}
public void AddError(string error)
{
AddBlock("📢 **Error**", error);
}
public void OnNewRequest(IChainStateRequest request)
{
AddRequestBlock(request, "New Request",
$"Client: {request.Client}",
$"Content: {request.Request.Content.Cid}",
$"Duration: {BigIntToDuration(request.Request.Ask.Duration)}",
$"Expiry: {BigIntToDuration(request.Request.Expiry)}",
$"Collateral: {BitIntToTestTokens(request.Request.Ask.Collateral)}",
$"Reward: {BitIntToTestTokens(request.Request.Ask.Reward)}",
$"Number of Slots: {request.Request.Ask.Slots}",
$"Slot Tolerance: {request.Request.Ask.MaxSlotLoss}",
$"Slot Size: {BigIntToByteSize(request.Request.Ask.SlotSize)}"
);
}
public void OnRequestCancelled(IChainStateRequest request)
{
AddRequestBlock(request, "Cancelled");
}
public void OnRequestFinished(IChainStateRequest request)
{
AddRequestBlock(request, "Finished");
}
public void OnRequestFulfilled(IChainStateRequest request)
{
AddRequestBlock(request, "Started");
}
public void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex)
{
AddRequestBlock(request, "Slot Filled",
$"Host: {host}",
$"Slot Index: {slotIndex}"
);
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
AddRequestBlock(request, "Slot Freed",
$"Slot Index: {slotIndex}"
);
}
private void AddRequestBlock(IChainStateRequest request, string eventName, params string[] content)
{
var blockNumber = $"[{request.Request.Block.BlockNumber}]";
var title = $"{blockNumber} **{eventName}** `{request.Request.Id}`";
AddBlock(title, content);
}
private void AddBlock(string title, params string[] content)
{
events.Add(FormatBlock(title, content));
}
private string FormatBlock(string title, params string[] content)
{
if (content == null || !content.Any())
{
return $"{title}{nl}{nl}";
}
return string.Join(nl,
new string[]
{
title,
"```"
}
.Concat(content)
.Concat(new string[]
{
"```"
})
) + nl + nl;
}
private string BigIntToDuration(BigInteger big)
{
var span = TimeSpan.FromSeconds((int)big);
return Time.FormatDuration(span);
}
private string BigIntToByteSize(BigInteger big)
{
var size = new ByteSize((long)big);
return size.ToString();
}
private string BitIntToTestTokens(BigInteger big)
{
var tt = new TestToken(big);
return tt.ToString();
}
}
}

View File

@ -1,5 +1,6 @@
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using GethPlugin;
using Logging;
using System.Numerics;
@ -48,7 +49,7 @@ namespace TestNetRewarder
{
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
public void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex)
{
}

View File

@ -10,7 +10,7 @@ namespace TestNetRewarder
private readonly RequestBuilder builder;
private readonly RewardChecker rewardChecker;
private readonly MarketTracker marketTracker;
private readonly BufferLogger bufferLogger;
private readonly EventsFormatter eventsFormatter;
private readonly ChainState chainState;
private readonly BotClient client;
private readonly ILog log;
@ -23,14 +23,15 @@ namespace TestNetRewarder
builder = new RequestBuilder();
rewardChecker = new RewardChecker(builder);
marketTracker = new MarketTracker(config, log);
bufferLogger = new BufferLogger();
eventsFormatter = new EventsFormatter();
var handler = new ChainChangeMux(
rewardChecker.Handler,
marketTracker
marketTracker,
eventsFormatter
);
chainState = new ChainState(new LogSplitter(log, bufferLogger), contracts, handler, config.HistoryStartUtc);
chainState = new ChainState(log, contracts, handler, config.HistoryStartUtc);
}
public async Task OnNewSegment(TimeRange timeRange)
@ -40,9 +41,9 @@ namespace TestNetRewarder
chainState.Update(timeRange.To);
var averages = marketTracker.GetAverages();
var lines = RemoveFirstLine(bufferLogger.Get());
var events = eventsFormatter.GetEvents();
var request = builder.Build(averages, lines);
var request = builder.Build(averages, events);
if (request.HasAny())
{
await client.SendRewards(request);
@ -52,16 +53,9 @@ namespace TestNetRewarder
{
var msg = "Exception processing time segment: " + ex;
log.Error(msg);
bufferLogger.Error(msg);
eventsFormatter.AddError(msg);
throw;
}
}
private string[] RemoveFirstLine(string[] lines)
{
//if (!lines.Any()) return Array.Empty<string>();
//return lines.Skip(1).ToArray();
return lines;
}
}
}

View File

@ -53,11 +53,10 @@ namespace TestNetRewarder
}
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
public void OnSlotFilled(IChainStateRequest request, EthAddress host, BigInteger slotIndex)
{
if (MeetsRequirements(CheckType.HostFilledSlot, request))
{
var host = request.Hosts.GetHost((int)slotIndex);
if (host != null)
{
GiveReward(reward, host);