Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4adce837ec | ||
|
|
e11a7d1600 | ||
|
|
ad70394333 | ||
|
|
50fbf0ad52 | ||
|
|
45fbd699a9 | ||
|
|
bf18fa03a2 | ||
|
|
116f62e73e | ||
|
|
8ef2e6023e | ||
|
|
e16b1ce079 | ||
|
|
4aa4731480 | ||
|
|
8ad2dee67c | ||
|
|
869aeb9253 | ||
|
|
8910c7ff27 | ||
|
|
2b10f2ec58 | ||
|
|
991927b95f | ||
|
|
b1bd1de027 | ||
|
|
3b258c9e2e | ||
|
|
766e2f5c20 | ||
|
|
888b19d8e5 | ||
|
|
f33866efc1 | ||
|
|
8c7229504e | ||
|
|
bcb05cd0c9 | ||
|
|
7179c70463 | ||
|
|
b3da42522f |
@@ -196,7 +196,7 @@ namespace Core
|
||||
|
||||
private T Retry<T>(Func<T> operation, string description)
|
||||
{
|
||||
return Time.Retry(operation, timeSet.HttpCallRetryTime(), timeSet.HttpCallRetryDelay(), description);
|
||||
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
|
||||
}
|
||||
|
||||
private HttpClient GetClient()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
public interface ITimeSet
|
||||
{
|
||||
TimeSpan HttpCallTimeout();
|
||||
TimeSpan HttpCallRetryTime();
|
||||
int HttpMaxNumberOfRetries();
|
||||
TimeSpan HttpCallRetryDelay();
|
||||
TimeSpan WaitForK8sServiceDelay();
|
||||
TimeSpan K8sOperationTimeout();
|
||||
@@ -13,12 +13,12 @@
|
||||
{
|
||||
public TimeSpan HttpCallTimeout()
|
||||
{
|
||||
return TimeSpan.FromMinutes(5);
|
||||
return TimeSpan.FromMinutes(3);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryTime()
|
||||
public int HttpMaxNumberOfRetries()
|
||||
{
|
||||
return TimeSpan.FromMinutes(1);
|
||||
return 3;
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
@@ -44,9 +44,9 @@
|
||||
return TimeSpan.FromHours(2);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryTime()
|
||||
public int HttpMaxNumberOfRetries()
|
||||
{
|
||||
return TimeSpan.FromHours(5);
|
||||
return 1;
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
|
||||
+18
-12
@@ -46,33 +46,35 @@
|
||||
|
||||
public static void Retry(Action action, string description)
|
||||
{
|
||||
Retry(action, TimeSpan.FromMinutes(1), description);
|
||||
Retry(action, 1, description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, string description)
|
||||
{
|
||||
return Retry(action, TimeSpan.FromMinutes(1), description);
|
||||
return Retry(action, 1, description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, TimeSpan timeout, string description)
|
||||
public static void Retry(Action action, int maxRetries, string description)
|
||||
{
|
||||
Retry(action, timeout, TimeSpan.FromSeconds(1), description);
|
||||
Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, TimeSpan timeout, string description)
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, string description)
|
||||
{
|
||||
return Retry(action, timeout, TimeSpan.FromSeconds(1), description);
|
||||
return Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, TimeSpan timeout, TimeSpan retryTime, string description)
|
||||
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -83,21 +85,24 @@
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
}
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, TimeSpan timeout, TimeSpan retryTime, string description)
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -107,6 +112,7 @@
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
public class CodexContractsContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-1854dfb-dist-tests";
|
||||
|
||||
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
|
||||
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public class CodexDeployment
|
||||
{
|
||||
public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata)
|
||||
public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata)
|
||||
{
|
||||
CodexContainers = codexContainers;
|
||||
GethDeployment = gethDeployment;
|
||||
CodexContractsDeployment = codexContractsDeployment;
|
||||
PrometheusContainer = prometheusContainer;
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public RunningContainer[] CodexContainers { get; }
|
||||
public GethDeployment GethDeployment { get; }
|
||||
public CodexContractsDeployment CodexContractsDeployment { get; }
|
||||
public RunningContainer? PrometheusContainer { get; }
|
||||
public DeploymentMetadata Metadata { get; }
|
||||
}
|
||||
|
||||
public class DeploymentMetadata
|
||||
{
|
||||
public DeploymentMetadata(DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
|
||||
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
|
||||
{
|
||||
Name = name;
|
||||
StartUtc = startUtc;
|
||||
FinishedUtc = finishedUtc;
|
||||
KubeNamespace = kubeNamespace;
|
||||
@@ -39,6 +43,7 @@ namespace CodexPlugin
|
||||
BlockMN = blockMN;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public DateTime StartUtc { get; }
|
||||
public DateTime FinishedUtc { get; }
|
||||
public string KubeNamespace { get; }
|
||||
|
||||
@@ -13,8 +13,9 @@ namespace CodexPlugin
|
||||
string GetName();
|
||||
CodexDebugResponse GetDebugInfo();
|
||||
CodexDebugPeerResponse GetDebugPeer(string peerId);
|
||||
CodexDebugBlockExchangeResponse GetDebugBlockExchange();
|
||||
CodexDebugRepoStoreResponse[] GetDebugRepoStore();
|
||||
// These debug methods are not available in master-line Codex. Use only for custom builds.
|
||||
//CodexDebugBlockExchangeResponse GetDebugBlockExchange();
|
||||
//CodexDebugRepoStoreResponse[] GetDebugRepoStore();
|
||||
ContentId UploadFile(TrackedFile file);
|
||||
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
|
||||
void ConnectToPeer(ICodexNode node);
|
||||
|
||||
@@ -13,5 +13,10 @@
|
||||
}
|
||||
|
||||
public string Address { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Wei} Wei";
|
||||
return $"{Eth} Eth";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace ContinuousTests
|
||||
{
|
||||
var testDuration = Time.FormatDuration(DateTime.UtcNow - startTime);
|
||||
var testData = FormatTestRuns(testLoops);
|
||||
overviewLog.Log("Total duration: " + testDuration);
|
||||
|
||||
if (config.TargetDurationSeconds > 0)
|
||||
{
|
||||
|
||||
@@ -57,8 +57,8 @@ namespace ContinuousTests.Tests
|
||||
|
||||
private void LogRepoStore(ICodexNode codexNode)
|
||||
{
|
||||
var response = codexNode.GetDebugRepoStore();
|
||||
Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
|
||||
//var response = codexNode.GetDebugRepoStore();
|
||||
//Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
|
||||
}
|
||||
|
||||
private void LogStoredBytes(ICodexNode node)
|
||||
@@ -90,8 +90,8 @@ namespace ContinuousTests.Tests
|
||||
|
||||
private void LogBlockExchangeStatus(ICodexNode codexNode, string msg)
|
||||
{
|
||||
var response = codexNode.GetDebugBlockExchange();
|
||||
Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
|
||||
//var response = codexNode.GetDebugBlockExchange();
|
||||
//Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ cd ../../Tools/CodexNetDeployer
|
||||
for i in $( seq 0 $replication)
|
||||
do
|
||||
dotnet run \
|
||||
--deploy-name=codex-continuous-$name-$i \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--kube-namespace=codex-continuous-$name-tests-$i \
|
||||
--deploy-file=codex-deployment-$name-$i.json \
|
||||
@@ -44,4 +45,6 @@ do
|
||||
--cleanup=1 \
|
||||
--full-container-logs=1 \
|
||||
--target-duration=172800 # 48 hours
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
@@ -42,21 +42,22 @@ namespace CodexTests.BasicTests
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
Time.Retry(() => AssertBlockExchangeIsEmpty(node), nameof(AssertExchangeIsEmpty));
|
||||
// API Call not available in master-line Codex image.
|
||||
//Time.Retry(() => AssertBlockExchangeIsEmpty(node), nameof(AssertExchangeIsEmpty));
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertBlockExchangeIsEmpty(ICodexNode node)
|
||||
{
|
||||
var msg = $"BlockExchange for {node.GetName()}: ";
|
||||
var response = node.GetDebugBlockExchange();
|
||||
foreach (var peer in response.peers)
|
||||
{
|
||||
var activeWants = peer.wants.Where(w => !w.cancel).ToArray();
|
||||
Assert.That(activeWants.Length, Is.EqualTo(0), msg + "thinks a peer has active wants.");
|
||||
}
|
||||
Assert.That(response.taskQueue, Is.EqualTo(0), msg + "has tasks in queue.");
|
||||
Assert.That(response.pendingBlocks, Is.EqualTo(0), msg + "has pending blocks.");
|
||||
}
|
||||
//private void AssertBlockExchangeIsEmpty(ICodexNode node)
|
||||
//{
|
||||
// var msg = $"BlockExchange for {node.GetName()}: ";
|
||||
// var response = node.GetDebugBlockExchange();
|
||||
// foreach (var peer in response.peers)
|
||||
// {
|
||||
// var activeWants = peer.wants.Where(w => !w.cancel).ToArray();
|
||||
// Assert.That(activeWants.Length, Is.EqualTo(0), msg + "thinks a peer has active wants.");
|
||||
// }
|
||||
// Assert.That(response.taskQueue, Is.EqualTo(0), msg + "has tasks in queue.");
|
||||
// Assert.That(response.pendingBlocks, Is.EqualTo(0), msg + "has pending blocks.");
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class AdminChecker
|
||||
{
|
||||
private SocketGuild guild = null!;
|
||||
private ulong[] adminIds = Array.Empty<ulong>();
|
||||
private DateTime lastUpdate = DateTime.MinValue;
|
||||
|
||||
public void SetGuild(SocketGuild guild)
|
||||
{
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
public bool IsUserAdmin(ulong userId)
|
||||
{
|
||||
if (ShouldUpdate()) UpdateAdminIds();
|
||||
|
||||
return adminIds.Contains(userId);
|
||||
}
|
||||
|
||||
private bool ShouldUpdate()
|
||||
{
|
||||
return !adminIds.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
|
||||
}
|
||||
|
||||
private void UpdateAdminIds()
|
||||
{
|
||||
lastUpdate = DateTime.UtcNow;
|
||||
var adminRole = guild.Roles.Single(r => r.Name == Program.Config.AdminRoleName);
|
||||
adminIds = adminRole.Members.Select(m => m.Id).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Discord.WebSocket;
|
||||
using Discord;
|
||||
using BiblioTech.Commands;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public abstract class BaseCommand
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract string StartingMessage { get; }
|
||||
public abstract string Description { get; }
|
||||
public virtual CommandOption[] Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return Array.Empty<CommandOption>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SlashCommandHandler(SocketSlashCommand command)
|
||||
{
|
||||
if (command.CommandName != Name) return;
|
||||
|
||||
try
|
||||
{
|
||||
await command.RespondAsync(StartingMessage);
|
||||
await Invoke(command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await command.FollowupAsync("Something failed while trying to do that...");
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task Invoke(SocketSlashCommand command);
|
||||
|
||||
protected bool IsSenderAdmin(SocketSlashCommand command)
|
||||
{
|
||||
return Program.AdminChecker.IsUserAdmin(command.User.Id);
|
||||
}
|
||||
|
||||
protected ulong GetUserId(UserOption userOption, SocketSlashCommand command)
|
||||
{
|
||||
var targetUser = userOption.GetOptionUserId(command);
|
||||
if (IsSenderAdmin(command) && targetUser != null) return targetUser.Value;
|
||||
return command.User.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandOption
|
||||
{
|
||||
public CommandOption(string name, string description, ApplicationCommandOptionType type, bool isRequired)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Type = type;
|
||||
IsRequired = isRequired;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public ApplicationCommandOptionType Type { get; }
|
||||
public bool IsRequired { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using CodexContractsPlugin;
|
||||
using Core;
|
||||
using Discord.WebSocket;
|
||||
using GethPlugin;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public abstract class BaseNetCommand : BaseCommand
|
||||
{
|
||||
private readonly DeploymentsFilesMonitor monitor;
|
||||
private readonly CoreInterface ci;
|
||||
|
||||
public BaseNetCommand(DeploymentsFilesMonitor monitor, CoreInterface ci)
|
||||
{
|
||||
this.monitor = monitor;
|
||||
this.ci = ci;
|
||||
}
|
||||
|
||||
protected override async Task Invoke(SocketSlashCommand command)
|
||||
{
|
||||
var deployments = monitor.GetDeployments();
|
||||
if (deployments.Length == 0)
|
||||
{
|
||||
await command.FollowupAsync("No deployments are currently available.");
|
||||
return;
|
||||
}
|
||||
if (deployments.Length > 1)
|
||||
{
|
||||
await command.FollowupAsync("Multiple deployments are online. I don't know which one to pick!");
|
||||
return;
|
||||
}
|
||||
|
||||
var codexDeployment = deployments.Single();
|
||||
var gethDeployment = codexDeployment.GethDeployment;
|
||||
var contractsDeployment = codexDeployment.CodexContractsDeployment;
|
||||
|
||||
var gethNode = ci.WrapGethDeployment(gethDeployment);
|
||||
var contracts = ci.WrapCodexContractsDeployment(contractsDeployment);
|
||||
|
||||
await Execute(command, gethNode, contracts);
|
||||
}
|
||||
|
||||
protected abstract Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.12.0" />
|
||||
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,63 @@
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
using Discord;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class CommandHandler
|
||||
{
|
||||
private readonly DiscordSocketClient client;
|
||||
private readonly BaseCommand[] commands;
|
||||
|
||||
public CommandHandler(DiscordSocketClient client, params BaseCommand[] commands)
|
||||
{
|
||||
this.client = client;
|
||||
this.commands = commands;
|
||||
|
||||
client.Ready += Client_Ready;
|
||||
client.SlashCommandExecuted += SlashCommandHandler;
|
||||
}
|
||||
|
||||
private async Task Client_Ready()
|
||||
{
|
||||
var guild = client.Guilds.Single(g => g.Name == Program.Config.ServerName);
|
||||
Program.AdminChecker.SetGuild(guild);
|
||||
|
||||
var builders = commands.Select(c =>
|
||||
{
|
||||
var builder = new SlashCommandBuilder()
|
||||
.WithName(c.Name)
|
||||
.WithDescription(c.Description);
|
||||
|
||||
foreach (var option in c.Options)
|
||||
{
|
||||
builder.AddOption(option.Name, option.Type, option.Description, isRequired: option.IsRequired);
|
||||
}
|
||||
|
||||
return builder;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var builder in builders)
|
||||
{
|
||||
await guild.CreateApplicationCommandAsync(builder.Build());
|
||||
}
|
||||
}
|
||||
catch (HttpException exception)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SlashCommandHandler(SocketSlashCommand command)
|
||||
{
|
||||
foreach (var cmd in commands)
|
||||
{
|
||||
await cmd.SlashCommandHandler(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class ClearUserAssociationCommand : BaseCommand
|
||||
{
|
||||
private readonly UserOption user = new UserOption(
|
||||
description: "User to clear Eth address for.",
|
||||
isRequired: true);
|
||||
|
||||
public override string Name => "clear";
|
||||
public override string StartingMessage => "Hold on...";
|
||||
public override string Description => "Admin only. Clears current Eth address for a user, allowing them to set a new one.";
|
||||
public override CommandOption[] Options => new[] { user };
|
||||
|
||||
protected override async Task Invoke(SocketSlashCommand command)
|
||||
{
|
||||
if (!IsSenderAdmin(command))
|
||||
{
|
||||
await command.FollowupAsync("You're not an admin.");
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = user.GetOptionUserId(command);
|
||||
if (userId == null)
|
||||
{
|
||||
await command.FollowupAsync("Failed to get user ID");
|
||||
return;
|
||||
}
|
||||
|
||||
Program.UserRepo.ClearUserAssociatedAddress(userId.Value);
|
||||
await command.FollowupAsync("Done."); ;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using CodexPlugin;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class DeploymentsCommand : BaseCommand
|
||||
{
|
||||
private readonly DeploymentsFilesMonitor monitor;
|
||||
|
||||
public DeploymentsCommand(DeploymentsFilesMonitor monitor)
|
||||
{
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
public override string Name => "deployments";
|
||||
public override string StartingMessage => "Fetching deployments information...";
|
||||
public override string Description => "Lists active TestNet deployments";
|
||||
|
||||
protected override async Task Invoke(SocketSlashCommand command)
|
||||
{
|
||||
var deployments = monitor.GetDeployments();
|
||||
|
||||
if (!deployments.Any())
|
||||
{
|
||||
await command.FollowupAsync("No deployments available.");
|
||||
return;
|
||||
}
|
||||
|
||||
await command.FollowupAsync($"Deployments: {string.Join(", ", deployments.Select(FormatDeployment))}");
|
||||
}
|
||||
|
||||
private string FormatDeployment(CodexDeployment deployment)
|
||||
{
|
||||
var m = deployment.Metadata;
|
||||
return $"{m.Name} ({m.StartUtc.ToString("o")})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Discord.WebSocket;
|
||||
using GethPlugin;
|
||||
using Nethereum.Util;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class EthAddressOption : CommandOption
|
||||
{
|
||||
public EthAddressOption()
|
||||
: base(name: "ethaddress",
|
||||
description: "Ethereum address starting with '0x'.",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
isRequired: true)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<EthAddress?> Parse(SocketSlashCommand command)
|
||||
{
|
||||
var ethOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
|
||||
if (ethOptionData == null)
|
||||
{
|
||||
await command.FollowupAsync("EthAddress option not received.");
|
||||
return null;
|
||||
}
|
||||
var ethAddressStr = ethOptionData.Value as string;
|
||||
if (string.IsNullOrEmpty(ethAddressStr))
|
||||
{
|
||||
await command.FollowupAsync("EthAddress is null or empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!AddressUtil.Current.IsValidAddressLength(ethAddressStr) ||
|
||||
!AddressUtil.Current.IsValidEthereumAddressHexFormat(ethAddressStr) ||
|
||||
!AddressUtil.Current.IsChecksumAddress(ethAddressStr))
|
||||
{
|
||||
await command.FollowupAsync("EthAddress is not valid.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EthAddress(ethAddressStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using CodexContractsPlugin;
|
||||
using Core;
|
||||
using Discord.WebSocket;
|
||||
using GethPlugin;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class GetBalanceCommand : BaseNetCommand
|
||||
{
|
||||
private readonly UserAssociateCommand userAssociateCommand;
|
||||
private readonly UserOption optionalUser = new UserOption(
|
||||
description: "If set, get balance for another user. (Optional, admin-only)",
|
||||
isRequired: false);
|
||||
|
||||
public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
|
||||
: base(monitor, ci)
|
||||
{
|
||||
this.userAssociateCommand = userAssociateCommand;
|
||||
}
|
||||
|
||||
public override string Name => "balance";
|
||||
public override string StartingMessage => "Fetching balance...";
|
||||
public override string Description => "Shows Eth and TestToken balance of an eth address.";
|
||||
public override CommandOption[] Options => new[] { optionalUser };
|
||||
|
||||
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
|
||||
{
|
||||
var userId = GetUserId(optionalUser, command);
|
||||
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
|
||||
if (addr == null)
|
||||
{
|
||||
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var eth = gethNode.GetEthBalance(addr);
|
||||
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
|
||||
|
||||
await command.FollowupAsync($"{command.User.Username} has {eth} and {testTokens}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using CodexContractsPlugin;
|
||||
using Core;
|
||||
using Discord.WebSocket;
|
||||
using GethPlugin;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class MintCommand : BaseNetCommand
|
||||
{
|
||||
private readonly Ether defaultEthToSend = 10.Eth();
|
||||
private readonly TestToken defaultTestTokensToMint = 1024.TestTokens();
|
||||
private readonly UserOption optionalUser = new UserOption(
|
||||
description: "If set, mint tokens for this user. (Optional, admin-only)",
|
||||
isRequired: false);
|
||||
private readonly UserAssociateCommand userAssociateCommand;
|
||||
|
||||
public MintCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
|
||||
: base(monitor, ci)
|
||||
{
|
||||
this.userAssociateCommand = userAssociateCommand;
|
||||
}
|
||||
|
||||
public override string Name => "mint";
|
||||
public override string StartingMessage => "Minting some tokens...";
|
||||
public override string Description => "Mint some TestTokens and send some Eth to the user if their balance is low.";
|
||||
public override CommandOption[] Options => new[] { optionalUser };
|
||||
|
||||
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
|
||||
{
|
||||
var userId = GetUserId(optionalUser, command);
|
||||
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
|
||||
if (addr == null)
|
||||
{
|
||||
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var report = new List<string>();
|
||||
|
||||
var sentEth = ProcessEth(gethNode, addr, report);
|
||||
var mintedTokens = ProcessTokens(gethNode, contracts, addr, report);
|
||||
|
||||
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
|
||||
|
||||
await command.FollowupAsync(string.Join(Environment.NewLine, report));
|
||||
}
|
||||
|
||||
private TestToken ProcessTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr, List<string> report)
|
||||
{
|
||||
if (ShouldMintTestTokens(gethNode, contracts, addr))
|
||||
{
|
||||
contracts.MintTestTokens(gethNode, addr, defaultTestTokensToMint);
|
||||
report.Add($"Minted {defaultTestTokensToMint}.");
|
||||
return defaultTestTokensToMint;
|
||||
}
|
||||
|
||||
report.Add("TestToken balance over threshold.");
|
||||
return 0.TestTokens();
|
||||
}
|
||||
|
||||
private Ether ProcessEth(IGethNode gethNode, EthAddress addr, List<string> report)
|
||||
{
|
||||
if (ShouldSendEth(gethNode, addr))
|
||||
{
|
||||
gethNode.SendEth(addr, defaultEthToSend);
|
||||
report.Add($"Sent {defaultEthToSend}.");
|
||||
return defaultEthToSend;
|
||||
}
|
||||
report.Add("Eth balance is over threshold.");
|
||||
return 0.Eth();
|
||||
}
|
||||
|
||||
private bool ShouldMintTestTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr)
|
||||
{
|
||||
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
|
||||
return testTokens.Amount < 64m;
|
||||
}
|
||||
|
||||
private bool ShouldSendEth(IGethNode gethNode, EthAddress addr)
|
||||
{
|
||||
var eth = gethNode.GetEthBalance(addr);
|
||||
return eth.Eth < 1.0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class ReportHistoryCommand : BaseCommand
|
||||
{
|
||||
private readonly UserOption user = new UserOption(
|
||||
description: "User to report history for.",
|
||||
isRequired: true);
|
||||
|
||||
public override string Name => "report";
|
||||
public override string StartingMessage => "Getting that data...";
|
||||
public override string Description => "Admin only. Reports bot-interaction history for a user.";
|
||||
public override CommandOption[] Options => new[] { user };
|
||||
|
||||
protected override async Task Invoke(SocketSlashCommand command)
|
||||
{
|
||||
if (!IsSenderAdmin(command))
|
||||
{
|
||||
await command.FollowupAsync("You're not an admin.");
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = user.GetOptionUserId(command);
|
||||
if (userId == null)
|
||||
{
|
||||
await command.FollowupAsync("Failed to get user ID");
|
||||
return;
|
||||
}
|
||||
|
||||
var report = Program.UserRepo.GetInteractionReport(userId.Value);
|
||||
await command.FollowupAsync(string.Join(Environment.NewLine, report));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class UserAssociateCommand : BaseCommand
|
||||
{
|
||||
private readonly EthAddressOption ethOption = new EthAddressOption();
|
||||
private readonly UserOption optionalUser = new UserOption(
|
||||
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
|
||||
isRequired: false);
|
||||
|
||||
public override string Name => "set";
|
||||
public override string StartingMessage => "hold on...";
|
||||
public override string Description => "Associates a Discord user with an Ethereum address.";
|
||||
public override CommandOption[] Options => new CommandOption[] { ethOption, optionalUser };
|
||||
|
||||
protected override async Task Invoke(SocketSlashCommand command)
|
||||
{
|
||||
var userId = GetUserId(optionalUser, command);
|
||||
var data = await ethOption.Parse(command);
|
||||
if (data == null) return;
|
||||
|
||||
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(userId);
|
||||
if (currentAddress != null && !IsSenderAdmin(command))
|
||||
{
|
||||
await command.FollowupAsync($"You've already set your Ethereum address to {currentAddress}.");
|
||||
return;
|
||||
}
|
||||
|
||||
Program.UserRepo.AssociateUserWithAddress(userId, data);
|
||||
await command.FollowupAsync("Done! Thank you for joining the test net!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class UserOption : CommandOption
|
||||
{
|
||||
public UserOption(string description, bool isRequired)
|
||||
: base("user", description, ApplicationCommandOptionType.User, isRequired)
|
||||
{
|
||||
}
|
||||
|
||||
public ulong? GetOptionUserId(SocketSlashCommand command)
|
||||
{
|
||||
var userOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
|
||||
if (userOptionData == null) return null;
|
||||
var user = userOptionData.Value as IUser;
|
||||
if (user == null) return null;
|
||||
return user.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ArgsUniform;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
[Uniform("token", "t", "TOKEN", true, "Discord Application Token")]
|
||||
public string ApplicationToken { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("server-name", "sn", "SERVERNAME", true, "Name of the Discord server")]
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("endpoints", "e", "ENDPOINTS", false, "Path where endpoint JSONs are located. Also accepts codex-deployment JSONs.")]
|
||||
public string EndpointsPath { get; set; } = "endpoints";
|
||||
|
||||
[Uniform("userdata", "u", "USERDATA", false, "Path where user data files will be saved.")]
|
||||
public string UserDataPath { get; set; } = "userdata";
|
||||
|
||||
[Uniform("admin-role", "a", "ADMINROLE", true, "Name of the Discord server admin role")]
|
||||
public string AdminRoleName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using CodexPlugin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class DeploymentsFilesMonitor
|
||||
{
|
||||
private DateTime lastUpdate = DateTime.MinValue;
|
||||
private CodexDeployment[] deployments = Array.Empty<CodexDeployment>();
|
||||
|
||||
public CodexDeployment[] GetDeployments()
|
||||
{
|
||||
if (ShouldUpdate()) UpdateDeployments();
|
||||
|
||||
return deployments;
|
||||
}
|
||||
|
||||
private void UpdateDeployments()
|
||||
{
|
||||
lastUpdate = DateTime.UtcNow;
|
||||
var path = Program.Config.EndpointsPath;
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
File.WriteAllText(Path.Combine(path, "readme.txt"), "Place codex-deployment.json here.");
|
||||
return;
|
||||
}
|
||||
|
||||
var files = Directory.GetFiles(path);
|
||||
deployments = files.Select(ProcessFile).Where(d => d != null).Cast<CodexDeployment>().ToArray();
|
||||
}
|
||||
|
||||
private CodexDeployment? ProcessFile(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = string.Join(" ", File.ReadAllLines(filename));
|
||||
return JsonConvert.DeserializeObject<CodexDeployment>(lines);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUpdate()
|
||||
{
|
||||
return !deployments.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using ArgsUniform;
|
||||
using BiblioTech.Commands;
|
||||
using Core;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Logging;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private DiscordSocketClient client = null!;
|
||||
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
public static DeploymentsFilesMonitor DeploymentFilesMonitor { get; } = new DeploymentsFilesMonitor();
|
||||
public static UserRepo UserRepo { get; } = new UserRepo();
|
||||
public static AdminChecker AdminChecker { get; } = new AdminChecker();
|
||||
|
||||
public static Task Main(string[] args)
|
||||
{
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
Config = uniformArgs.Parse();
|
||||
|
||||
if (!Directory.Exists(Config.UserDataPath))
|
||||
{
|
||||
Directory.CreateDirectory(Config.UserDataPath);
|
||||
}
|
||||
|
||||
return new Program().MainAsync();
|
||||
}
|
||||
|
||||
public async Task MainAsync()
|
||||
{
|
||||
Console.WriteLine("Starting Codex Discord Bot...");
|
||||
client = new DiscordSocketClient();
|
||||
client.Log += Log;
|
||||
|
||||
ProjectPlugin.Load<CodexPlugin.CodexPlugin>();
|
||||
ProjectPlugin.Load<GethPlugin.GethPlugin>();
|
||||
ProjectPlugin.Load<CodexContractsPlugin.CodexContractsPlugin>();
|
||||
|
||||
var entryPoint = new EntryPoint(new ConsoleLog(), new KubernetesWorkflow.Configuration(
|
||||
kubeConfigFile: null,
|
||||
operationTimeout: TimeSpan.FromMinutes(5),
|
||||
retryDelay: TimeSpan.FromSeconds(10),
|
||||
kubernetesNamespace: "not-applicable"), "datafiles");
|
||||
|
||||
var monitor = new DeploymentsFilesMonitor();
|
||||
|
||||
var ci = entryPoint.CreateInterface();
|
||||
|
||||
var associateCommand = new UserAssociateCommand();
|
||||
var handler = new CommandHandler(client,
|
||||
new ClearUserAssociationCommand(),
|
||||
new GetBalanceCommand(monitor, ci, associateCommand),
|
||||
new MintCommand(monitor, ci, associateCommand),
|
||||
new ReportHistoryCommand(),
|
||||
associateCommand,
|
||||
new DeploymentsCommand(monitor)
|
||||
);
|
||||
|
||||
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
|
||||
await client.StartAsync();
|
||||
Console.WriteLine("Running...");
|
||||
await Task.Delay(-1);
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("BiblioTech - Codex Discord Bot");
|
||||
}
|
||||
|
||||
private Task Log(LogMessage msg)
|
||||
{
|
||||
Console.WriteLine(msg.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class UserRepo
|
||||
{
|
||||
private readonly object repoLock = new object();
|
||||
|
||||
public void AssociateUserWithAddress(ulong discordId, EthAddress address)
|
||||
{
|
||||
lock (repoLock)
|
||||
{
|
||||
SetUserAddress(discordId, address);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearUserAssociatedAddress(ulong discordId)
|
||||
{
|
||||
lock (repoLock)
|
||||
{
|
||||
SetUserAddress(discordId, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMintEventForUser(ulong discordId, EthAddress usedAddress, Ether eth, TestToken tokens)
|
||||
{
|
||||
lock (repoLock)
|
||||
{
|
||||
var user = GetOrCreate(discordId);
|
||||
user.MintEvents.Add(new UserMintEvent(DateTime.UtcNow, usedAddress, eth, tokens));
|
||||
SaveUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public EthAddress? GetCurrentAddressForUser(ulong discordId)
|
||||
{
|
||||
lock (repoLock)
|
||||
{
|
||||
return GetOrCreate(discordId).CurrentAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetInteractionReport(ulong discordId)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
lock (repoLock)
|
||||
{
|
||||
var filename = GetFilename(discordId);
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
result.Add("User has not joined the test net.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = JsonConvert.DeserializeObject<User>(File.ReadAllText(filename));
|
||||
if (user == null)
|
||||
{
|
||||
result.Add("Failed to load user records.");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add("User joined on " + user.CreatedUtc.ToString("o"));
|
||||
result.Add("Current address: " + user.CurrentAddress);
|
||||
foreach (var ae in user.AssociateEvents)
|
||||
{
|
||||
result.Add($"{ae.Utc.ToString("o")} - Address set to: {ae.NewAddress}");
|
||||
}
|
||||
foreach (var me in user.MintEvents)
|
||||
{
|
||||
result.Add($"{me.Utc.ToString("o")} - Minted {me.EthReceived} and {me.TestTokensMinted} to {me.UsedAddress}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private void SetUserAddress(ulong discordId, EthAddress? address)
|
||||
{
|
||||
var user = GetOrCreate(discordId);
|
||||
user.CurrentAddress = address;
|
||||
user.AssociateEvents.Add(new UserAssociateAddressEvent(DateTime.UtcNow, address));
|
||||
SaveUser(user);
|
||||
}
|
||||
|
||||
private User GetOrCreate(ulong discordId)
|
||||
{
|
||||
var filename = GetFilename(discordId);
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
return CreateAndSaveNewUser(discordId);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<User>(File.ReadAllText(filename))!;
|
||||
}
|
||||
|
||||
private User CreateAndSaveNewUser(ulong discordId)
|
||||
{
|
||||
var newUser = new User(discordId, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
|
||||
SaveUser(newUser);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
private void SaveUser(User user)
|
||||
{
|
||||
var filename = GetFilename(user.DiscordId);
|
||||
if (File.Exists(filename)) File.Delete(filename);
|
||||
File.WriteAllText(filename, JsonConvert.SerializeObject(user));
|
||||
}
|
||||
|
||||
private static string GetFilename(ulong discordId)
|
||||
{
|
||||
return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
public User(ulong discordId, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
|
||||
{
|
||||
DiscordId = discordId;
|
||||
CreatedUtc = createdUtc;
|
||||
CurrentAddress = currentAddress;
|
||||
AssociateEvents = associateEvents;
|
||||
MintEvents = mintEvents;
|
||||
}
|
||||
|
||||
public ulong DiscordId { get; }
|
||||
public DateTime CreatedUtc { get; }
|
||||
public EthAddress? CurrentAddress { get; set; }
|
||||
public List<UserAssociateAddressEvent> AssociateEvents { get; }
|
||||
public List<UserMintEvent> MintEvents { get; }
|
||||
}
|
||||
|
||||
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, Ether ethReceived, TestToken testTokensMinted)
|
||||
{
|
||||
Utc = utc;
|
||||
UsedAddress = usedAddress;
|
||||
EthReceived = ethReceived;
|
||||
TestTokensMinted = testTokensMinted;
|
||||
}
|
||||
|
||||
public DateTime Utc { get; }
|
||||
public EthAddress UsedAddress { get; }
|
||||
public Ether EthReceived { get; }
|
||||
public TestToken TestTokensMinted { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
docker build -f docker/Dockerfile -t thatbenbierens/codex-discordbot:initial ../..
|
||||
docker push thatbenbierens/codex-discordbot:initial
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0
|
||||
|
||||
WORKDIR app
|
||||
COPY ./Tools/BiblioTech ./Tools/BiblioTech
|
||||
COPY ./Framework ./Framework
|
||||
COPY ./ProjectPlugins ./ProjectPlugins
|
||||
CMD ["dotnet", "run", "--project", "Tools/BiblioTech"]
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
bibliotech-discordbot:
|
||||
image: thatbenbierens/codex-discordbot:initial
|
||||
environment:
|
||||
- TOKEN=tokenplz
|
||||
- SERVERNAME=ThatBen's server
|
||||
- ADMINROLE=adminers
|
||||
@@ -8,6 +8,9 @@ namespace CodexNetDeployer
|
||||
public const int SecondsIn1Day = 24 * 60 * 60;
|
||||
public const int TenMinutes = 10 * 60;
|
||||
|
||||
[Uniform("deploy-name", "nm", "DEPLOYNAME", false, "Name of the deployment. (optional)")]
|
||||
public string DeploymentName { get; set; } = "unnamed";
|
||||
|
||||
[Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
|
||||
public string KubeConfigFile { get; set; } = "null";
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace CodexNetDeployer
|
||||
CheckContainerRestarts(startResults);
|
||||
|
||||
var codexContainers = startResults.Select(s => s.CodexNode.Container).ToArray();
|
||||
return new CodexDeployment(codexContainers, gethDeployment, metricsService, CreateMetadata(startUtc));
|
||||
return new CodexDeployment(codexContainers, gethDeployment, contractsDeployment, metricsService, CreateMetadata(startUtc));
|
||||
}
|
||||
|
||||
private EntryPoint CreateEntryPoint(ILog log)
|
||||
@@ -151,6 +151,7 @@ namespace CodexNetDeployer
|
||||
private DeploymentMetadata CreateMetadata(DateTime startUtc)
|
||||
{
|
||||
return new DeploymentMetadata(
|
||||
name: config.DeploymentName,
|
||||
startUtc: startUtc,
|
||||
finishedUtc: DateTime.UtcNow,
|
||||
kubeNamespace: config.KubeNamespace,
|
||||
@@ -180,9 +181,9 @@ namespace CodexNetDeployer
|
||||
return TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryTime()
|
||||
public int HttpMaxNumberOfRetries()
|
||||
{
|
||||
return TimeSpan.FromSeconds(2);
|
||||
return 2;
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallTimeout()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
dotnet run \
|
||||
--deploy-name=codex-continuous-test-deployment \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--kube-namespace=codex-continuous-tests \
|
||||
--deploy-file=codex-deployment.json \
|
||||
|
||||
@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistTestCore", "Tests\DistT
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexNetDeployer", "Tools\CodexNetDeployer\CodexNetDeployer.csproj", "{3417D508-E2F4-4974-8988-BB124046D9E2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BiblioTech", "Tools\BiblioTech\BiblioTech.csproj", "{078ABA6D-A04E-4F62-A44C-EA66F1B66548}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -113,6 +115,10 @@ Global
|
||||
{3417D508-E2F4-4974-8988-BB124046D9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -134,6 +140,7 @@ Global
|
||||
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{E849B7BA-FDCC-4CFF-998F-845ED2F1BF40} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{3417D508-E2F4-4974-8988-BB124046D9E2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
{078ABA6D-A04E-4F62-A44C-EA66F1B66548} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}
|
||||
|
||||
Reference in New Issue
Block a user