Compare commits

..
Author SHA1 Message Date
benbierens 7eed6160eb working as intended 2024-06-28 09:16:12 +02:00
benbierens c57dc4daa1 concurrent purchases 2024-06-28 08:47:09 +02:00
Ben 4c75cebcd6 Implements autoclient with image generator 2024-06-27 15:38:13 +02:00
Ben a820788c7d Merge branch 'master' into feature/auto-client
# Conflicts:
#	cs-codex-dist-testing.sln
2024-06-27 13:42:54 +02:00
Ben 52a02abd3f Fixes incorrect block numbers in eventFormatter 2024-06-27 11:43:25 +02:00
Ben a92455b2a5 bump codex image to v0.1.2 + prover 2024-06-27 11:16:50 +02:00
Ben 933d5e7d4d Merge branch 'better-chain-events' 2024-06-27 11:16:34 +02:00
Ben ba43fd90c6 Adds admin option to set custom replacements 2024-06-27 11:16:17 +02:00
Ben 01ee514c73 Implements known username replacement 2024-06-27 10:44:37 +02:00
Ben 1eb30329c6 Pulls out chain events sender 2024-06-27 10:14:23 +02:00
Ben 0ef55abdf4 Adds events formatter 2024-06-27 10:07:10 +02:00
Ben 8341807d92 Disables role rewards (not configured in real server) 2024-06-26 11:27:19 +02:00
Ben c2712daccb latest images 2024-06-25 13:02:05 +02:00
benbierens 8033da1176 Updates to new Codex image. Fixes tests 2024-06-21 11:29:05 +02:00
benbierens 5afab577a7 Merge branch 'chainstate-update' 2024-06-21 10:19:32 +02:00
benbierens 4be9b9df9a Finished! 2024-06-21 10:19:09 +02:00
benbierens a905f0ce53 fixes crash in market buffer again 2024-06-21 09:59:24 +02:00
benbierens 0c4d3be912 Fixes crash in marketBuffer 2024-06-21 09:33:52 +02:00
benbierens 2b7ba61543 Cleanup 2024-06-21 09:18:00 +02:00
benbierens f9c7e18985 Merge branch 'master' into chainstate-update 2024-06-21 08:56:43 +02:00
benbierens 32e7028029 wip 2024-06-21 08:56:20 +02:00
Ben cc3eddf02d wip 2024-06-20 12:15:59 +02:00
Ben d73ad6db15 Merge branch 'feature/keymaker' 2024-06-19 14:06:20 +02:00
Ben f7c45d17d7 wip 2024-06-18 09:57:38 +02:00
Ben cb4cdfe69a better logging 2024-06-17 16:00:51 +02:00
Ben bed57dd35b Finish replace of old chainstate object in rewarder bot 2024-06-17 15:34:08 +02:00
benbierens c38a2242ba wip 2024-06-14 11:09:51 +02:00
benbierens d3488dc907 Adds block numbers to chainstate event logging 2024-06-14 10:22:30 +02:00
benbierens 1fa7787d3b Merge branch 'master' into chainstate-update
# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexNode.cs
#	ProjectPlugins/CodexPlugin/MarketplaceAccess.cs
2024-06-14 09:28:34 +02:00
Ben 1c856f7615 Fixes log 2024-06-10 14:17:19 +02:00
Ben cc2513bd2f Better chain state representation 2024-06-10 14:04:25 +02:00
benbierens ac7d323201 wip 2024-06-03 10:38:48 +02:00
benbierens a846d51c0c Implements ChainState in CodexContracts plugin 2024-05-31 11:19:50 +02:00
benbierens f5da80dc9c Sets up ChainEvents object 2024-05-31 10:16:57 +02:00
benbierens f94a67adb4 Extracts event methods from CodexContractsAccess 2024-05-31 10:06:34 +02:00
Ben f1d453251c cleanup 2024-05-30 11:33:16 +02:00
Ben 12dc7efd5b wip 2024-05-30 11:17:13 +02:00
Ben 117a30bb82 wip 2024-05-30 10:55:33 +02:00
Ben ccc6c815e4 Logging of entire chain state as seen by rewarder bot 2024-05-29 14:05:16 +02:00
benbierens ba9e4b098f checking that all rewards are sent. 2024-05-24 16:11:51 +02:00
benbierens 69aa3a998f Fixes bot test contract 2024-05-24 15:34:42 +02:00
benbierens 00fd2cebf9 Merge branch 'master' into feature/rewardbot-tests 2024-05-24 14:34:16 +02:00
Ben 5143361dcd wip 2024-05-23 11:37:57 +02:00
Ben b1818400ca Merge branch 'master' into feature/rewardbot-tests
# Conflicts:
#	Tests/CodexTests/UtilityTests/DiscordBotTests.cs
2024-05-22 11:11:47 +02:00
Ben a236544ee9 parameterizing the bot test 2024-05-21 16:10:14 +02:00
Ben fa1b560a91 Adds waiting for debug-mode start message 2024-05-21 12:58:17 +02:00
benbierens d6f7e225be successful downloading of bot log 2024-05-20 16:18:01 +02:00
Ben 22cf82b99b Adds no-discord debug option to discord bot. 2024-05-16 16:00:19 +02:00
benbierens 362040bd3c Sets up docker image and CI 2024-04-01 21:29:02 +02:00
benbierens 40393c3a5b Using raw version of purchase-status call. API doesn't line up 2024-04-01 21:09:24 +02:00
benbierens 7f972bac85 Mvoes autoclient 2024-04-01 20:47:56 +02:00
benbierens d532d9505a Sets up autoclient. 2024-04-01 20:40:03 +02:00
61 changed files with 2473 additions and 1042 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Docker - AutoClient
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/AutoClient/**'
- '!Tools/AutoClient/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-autoclient.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/AutoClient/docker/Dockerfile
docker_repo: codexstorage/codex-autoclient
secrets: inherit
+2 -1
View File
@@ -1,4 +1,5 @@
.vs
obj
bin
.vscode
.vscode
Tools/AutoClient/datapath
+4 -4
View File
@@ -13,9 +13,9 @@ namespace DiscordRewards
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}
@@ -5,6 +5,11 @@
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || Averages.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand
+41 -41
View File
@@ -1,53 +1,53 @@
using Utils;
namespace DiscordRewards
namespace DiscordRewards
{
public class RewardRepo
{
private static string Tag => RewardConfig.UsernameTag;
public RewardConfig[] Rewards { get; } = new RewardConfig[]
{
// Filled any slot
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
{
Type = CheckType.FilledSlot
}),
public RewardConfig[] Rewards { get; } = new RewardConfig[0];
// Finished any slot
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
{
Type = CheckType.FinishedSlot
}),
// Example configuration, from test server:
//{
// // Filled any slot
// new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
// {
// Type = CheckType.HostFilledSlot
// }),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
}),
// // Finished any slot
// new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
// {
// Type = CheckType.HostFinishedSlot
// }),
// Posted any contract
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
{
Type = CheckType.PostedContract
}),
// // Finished a sizable slot
// new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
// {
// Type = CheckType.HostFinishedSlot,
// MinSlotSize = 10.MB(),
// MinDuration = TimeSpan.FromMinutes(5.0),
// }),
// Started any contract
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.StartedContract
}),
// // Posted any contract
// new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
// {
// Type = CheckType.ClientPostedContract
// }),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.StartedContract,
MinNumberOfHosts = 4,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
})
};
// // Started any contract
// new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
// {
// Type = CheckType.ClientStartedContract
// }),
// // Started a sizable contract
// new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
// {
// Type = CheckType.ClientStartedContract,
// MinNumberOfHosts = 4,
// MinSlotSize = 10.MB(),
// MinDuration = TimeSpan.FromMinutes(5.0),
// })
//};
}
}
+16 -5
View File
@@ -57,16 +57,27 @@ namespace FileUtils
public void ScopedFiles(Action action)
{
PushFileSet();
action();
PopFileSet();
try
{
action();
}
finally
{
PopFileSet();
}
}
public T ScopedFiles<T>(Func<T> action)
{
PushFileSet();
var result = action();
PopFileSet();
return result;
try
{
return action();
}
finally
{
PopFileSet();
}
}
private void PushFileSet()
@@ -117,6 +117,7 @@ namespace NethereumWorkflow
}
return new BlockInterval(
timeRange: timeRange,
from: fromBlock.Value,
to: toBlock.Value
);
+4 -1
View File
@@ -2,7 +2,7 @@
{
public class BlockInterval
{
public BlockInterval(ulong from, ulong to)
public BlockInterval(TimeRange timeRange, ulong from, ulong to)
{
if (from < to)
{
@@ -14,10 +14,13 @@
From = to;
To = from;
}
TimeRange = timeRange;
}
public ulong From { get; }
public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public override string ToString()
{
@@ -0,0 +1,68 @@
using CodexContractsPlugin.Marketplace;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public class ChainEvents
{
private ChainEvents(
BlockInterval blockInterval,
Request[] requests,
RequestFulfilledEventDTO[] fulfilled,
RequestCancelledEventDTO[] cancelled,
SlotFilledEventDTO[] slotFilled,
SlotFreedEventDTO[] slotFreed
)
{
BlockInterval = blockInterval;
Requests = requests;
Fulfilled = fulfilled;
Cancelled = cancelled;
SlotFilled = slotFilled;
SlotFreed = slotFreed;
}
public BlockInterval BlockInterval { get; }
public Request[] Requests { get; }
public RequestFulfilledEventDTO[] Fulfilled { get; }
public RequestCancelledEventDTO[] Cancelled { get; }
public SlotFilledEventDTO[] SlotFilled { get; }
public SlotFreedEventDTO[] SlotFreed { get; }
public IHasBlock[] All
{
get
{
var all = new List<IHasBlock>();
all.AddRange(Requests);
all.AddRange(Fulfilled);
all.AddRange(Cancelled);
all.AddRange(SlotFilled);
all.AddRange(SlotFreed);
return all.ToArray();
}
}
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
{
return FromContractEvents(contracts.GetEvents(blockInterval));
}
public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange)
{
return FromContractEvents(contracts.GetEvents(timeRange));
}
public static ChainEvents FromContractEvents(ICodexContractsEvents events)
{
return new ChainEvents(
events.BlockInterval,
events.GetStorageRequests(),
events.GetRequestFulfilledEvents(),
events.GetRequestCancelledEvents(),
events.GetSlotFilledEvents(),
events.GetSlotFreedEvents()
);
}
}
}
@@ -0,0 +1,170 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using NethereumWorkflow.BlockUtils;
using System.Numerics;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateChangeHandler
{
void OnNewRequest(RequestEvent requestEvent);
void OnRequestFinished(RequestEvent requestEvent);
void OnRequestFulfilled(RequestEvent requestEvent);
void OnRequestCancelled(RequestEvent requestEvent);
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
}
public class RequestEvent
{
public RequestEvent(BlockTimeEntry block, IChainStateRequest request)
{
Block = block;
Request = request;
}
public BlockTimeEntry Block { get; }
public IChainStateRequest Request { get; }
}
public class ChainState
{
private readonly List<ChainStateRequest> requests = new List<ChainStateRequest>();
private readonly ILog log;
private readonly ICodexContracts contracts;
private readonly IChainStateChangeHandler handler;
public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc)
{
this.log = new LogPrefixer(log, "(ChainState) ");
this.contracts = contracts;
handler = changeHandler;
StartUtc = startUtc;
TotalSpan = new TimeRange(startUtc, startUtc);
}
public TimeRange TotalSpan { get; private set; }
public IChainStateRequest[] Requests => requests.ToArray();
public DateTime StartUtc { get; }
public void Update()
{
Update(DateTime.UtcNow);
}
public void Update(DateTime toUtc)
{
var span = new TimeRange(TotalSpan.To, toUtc);
var events = ChainEvents.FromTimeRange(contracts, span);
Apply(events);
TotalSpan = new TimeRange(TotalSpan.From, span.To);
}
private void Apply(ChainEvents events)
{
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
log.Log($"ChainState updating: {events.BlockInterval}");
// Run through each block and apply the events to the state in order.
var span = events.BlockInterval.TimeRange.Duration;
var numBlocks = events.BlockInterval.NumberOfBlocks;
var spanPerBlock = span / numBlocks;
var eventUtc = events.BlockInterval.TimeRange.From;
for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++)
{
var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray();
ApplyEvents(b, blockEvents, eventUtc);
eventUtc += spanPerBlock;
}
}
private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc)
{
foreach (var e in blockEvents)
{
dynamic d = e;
ApplyEvent(d);
}
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
}
private void ApplyEvent(Request request)
{
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
throw new Exception("Received NewRequest event for id that already exists.");
var newRequest = new ChainStateRequest(log, request, RequestState.New);
requests.Add(newRequest);
handler.OnNewRequest(new RequestEvent(request.Block, newRequest));
}
private void ApplyEvent(RequestFulfilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Started);
handler.OnRequestFulfilled(new RequestEvent(@event.Block, r));
}
private void ApplyEvent(RequestCancelledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Cancelled);
handler.OnRequestCancelled(new RequestEvent(@event.Block, r));
}
private void ApplyEvent(SlotFilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.Hosts.Add(@event.Host, (int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})");
handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex);
}
private void ApplyEvent(SlotFreedEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.Hosts.RemoveHost((int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})");
handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex);
}
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
{
foreach (var r in requests)
{
if (r.State == RequestState.Started
&& r.FinishedUtc < eventsUtc)
{
r.UpdateState(blockNumber, RequestState.Finished);
handler.OnRequestFinished(new RequestEvent(new BlockTimeEntry(blockNumber, eventsUtc), r));
}
}
}
private ChainStateRequest? FindRequest(byte[] requestId)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
if (r == null) log.Log("Unable to find request by ID!");
return r;
}
private bool Equal(byte[] a, byte[] b)
{
return a.SequenceEqual(b);
}
}
}
@@ -0,0 +1,80 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateRequest
{
Request Request { get; }
RequestState State { get; }
DateTime ExpiryUtc { get; }
DateTime FinishedUtc { get; }
EthAddress Client { get; }
RequestHosts Hosts { get; }
}
public class ChainStateRequest : IChainStateRequest
{
private readonly ILog log;
public ChainStateRequest(ILog log, Request request, RequestState state)
{
this.log = log;
Request = request;
State = state;
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
Log($"[{request.Block.BlockNumber}] Created as {State}.");
Client = new EthAddress(request.Client);
Hosts = new RequestHosts();
}
public Request Request { get; }
public RequestState State { get; private set; }
public DateTime ExpiryUtc { get; }
public DateTime FinishedUtc { get; }
public EthAddress Client { get; }
public RequestHosts Hosts { get; }
public void UpdateState(ulong blockNumber, RequestState newState)
{
Log($"[{blockNumber}] Transit: {State} -> {newState}");
State = newState;
}
public void Log(string msg)
{
log.Log($"Request '{Request.Id}': {msg}");
}
}
public class RequestHosts
{
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
public void Add(EthAddress host, int index)
{
hosts.Add(index, host);
}
public void RemoveHost(int index)
{
hosts.Remove(index);
}
public EthAddress? GetHost(int index)
{
if (!hosts.ContainsKey(index)) return null;
return hosts[index];
}
public EthAddress[] GetHosts()
{
return hosts.Values.ToArray();
}
}
}
@@ -0,0 +1,32 @@
using GethPlugin;
using System.Numerics;
namespace CodexContractsPlugin.ChainMonitor
{
public class DoNothingChainEventHandler : IChainStateChangeHandler
{
public void OnNewRequest(RequestEvent requestEvent)
{
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
}
public void OnRequestFinished(RequestEvent requestEvent)
{
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
}
}
@@ -2,10 +2,8 @@
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
@@ -22,13 +20,10 @@ namespace CodexContractsPlugin
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(BlockInterval blockRange);
ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
}
[JsonConverter(typeof(StringEnumConverter))]
@@ -81,65 +76,14 @@ namespace CodexContractsPlugin
return balance.TstWei();
}
public Request[] GetStorageRequests(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var i = StartInteraction();
return events
.Select(e =>
{
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
@@ -170,17 +114,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);
@@ -0,0 +1,108 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.Hex.HexTypes;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace CodexContractsPlugin
{
public interface ICodexContractsEvents
{
BlockInterval BlockInterval { get; }
Request[] GetStorageRequests();
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
RequestCancelledEventDTO[] GetRequestCancelledEvents();
SlotFilledEventDTO[] GetSlotFilledEvents();
SlotFreedEventDTO[] GetSlotFreedEvents();
}
public class CodexContractsEvents : ICodexContractsEvents
{
private readonly ILog log;
private readonly IGethNode gethNode;
private readonly CodexContractsDeployment deployment;
public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval)
{
this.log = log;
this.gethNode = gethNode;
this.deployment = deployment;
BlockInterval = blockInterval;
}
public BlockInterval BlockInterval { get; }
public Request[] GetStorageRequests()
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events
.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents()
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents()
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
}
}
@@ -5,35 +5,49 @@ using Newtonsoft.Json;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
public interface IHasBlock
{
BlockTimeEntry Block { get; set; }
}
public partial class Request : RequestBase, IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
[JsonIgnore]
public string Id
{
get
{
return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant();
}
}
}
public partial class RequestFulfilledEventDTO
public partial class RequestFulfilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO
public partial class RequestCancelledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO
public partial class SlotFilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
public partial class SlotFreedEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@@ -1,11 +1,13 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Utils;
namespace CodexDiscordBotPlugin
{
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
{
private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!";
private readonly IPluginTools tools;
public CodexDiscordBotPlugin(IPluginTools tools)
@@ -46,14 +48,59 @@ namespace CodexDiscordBotPlugin
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
WaitForStartupMessage(workflow, pod);
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod)
{
var finder = new LogLineFinder(ExpectedStartupMessage, workflow);
Time.WaitUntil(() =>
{
finder.FindLine(pod);
return finder.Found;
}, nameof(WaitForStartupMessage));
}
public class LogLineFinder : LogHandler
{
private readonly string message;
private readonly IStartupWorkflow workflow;
public LogLineFinder(string message, IStartupWorkflow workflow)
{
this.message = message;
this.workflow = workflow;
}
public void FindLine(RunningPod pod)
{
Found = false;
foreach (var c in pod.Containers)
{
workflow.DownloadContainerLog(c, this);
if (Found) return;
}
}
public bool Found { get; private set; }
protected override void ProcessLine(string line)
{
if (!Found && line.Contains(message)) Found = true;
}
}
}
}
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "codexstorage/codex-discordbot:sha-8c64352";
public override string Image => "codexstorage/codex-discordbot:sha-8033da1";
public static string RewardsPort = "bot_rewards_port";
@@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
AddEnvVar("NODISCORD", "1");
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
if (!string.IsNullOrEmpty(config.DataPath))
@@ -27,8 +27,9 @@
public class RewarderBotStartupConfig
{
public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
{
Name = name;
DiscordBotHost = discordBotHost;
DiscordBotPort = discordBotPort;
IntervalMinutes = intervalMinutes;
@@ -37,9 +38,10 @@
DataPath = dataPath;
}
public string Name { get; }
public string DiscordBotHost { get; }
public int DiscordBotPort { get; }
public string IntervalMinutes { get; }
public int IntervalMinutes { get; }
public DateTime HistoryStartUtc { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; }
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class RewarderBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-rewarder";
public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2";
public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1";
protected override void Initialize(StartupConfig startupConfig)
{
@@ -17,7 +17,7 @@ namespace CodexDiscordBotPlugin
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes);
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString());
var offset = new DateTimeOffset(config.HistoryStartUtc);
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString());
@@ -7,7 +7,7 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-b89493e-dist-tests";
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-471ebb2-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
@@ -109,8 +109,9 @@ namespace CodexPlugin
// Custom scripting in the Codex test image will write this variable to a private-key file,
// and pass the correct filename to Codex.
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey);
Additional(marketplaceSetup.EthAccount);
var account = marketplaceSetup.EthAccountSetup.GetNew();
AddEnvVar("PRIV_KEY", account.PrivateKey);
Additional(account);
SetCommandOverride(marketplaceSetup);
if (marketplaceSetup.IsValidator)
+20 -6
View File
@@ -26,13 +26,13 @@ namespace CodexPlugin
CrashWatcher CrashWatcher { get; }
PodInfo GetPodInfo();
ITransferSpeeds TransferSpeeds { get; }
EthAccount EthAccount { get; }
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteRepoFolder();
void Stop(bool waitTillStopped);
}
@@ -40,13 +40,13 @@ namespace CodexPlugin
{
private const string UploadFailedMessage = "Unable to store block";
private readonly IPluginTools tools;
private readonly EthAddress? ethAddress;
private readonly EthAccount? ethAccount;
private readonly TransferSpeeds transferSpeeds;
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress)
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAccount? ethAccount)
{
this.tools = tools;
this.ethAddress = ethAddress;
this.ethAccount = ethAccount;
CodexAccess = codexAccess;
Group = group;
Marketplace = marketplaceAccess;
@@ -76,8 +76,17 @@ namespace CodexPlugin
{
get
{
if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
return ethAddress;
EnsureMarketplace();
return ethAccount!.EthAddress;
}
}
public EthAccount EthAccount
{
get
{
EnsureMarketplace();
return ethAccount!;
}
}
@@ -231,6 +240,11 @@ namespace CodexPlugin
CodexAccess.LogDiskSpace("After download");
}
private void EnsureMarketplace()
{
if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
}
private void Log(string msg)
{
tools.GetLog().Log($"{GetName()}: {msg}");
@@ -22,22 +22,22 @@ namespace CodexPlugin
public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
{
var ethAddress = GetEthAddress(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAddress);
return new CodexNode(tools, access, group, marketplaceAccess, ethAddress);
var ethAccount = GetEthAccount(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAccount);
return new CodexNode(tools, access, group, marketplaceAccess, ethAccount);
}
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress)
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount)
{
if (ethAddress == null) return new MarketplaceUnavailable();
if (ethAccount == null) return new MarketplaceUnavailable();
return new MarketplaceAccess(tools.GetLog(), codexAccess);
}
private EthAddress? GetEthAddress(CodexAccess access)
private EthAccount? GetEthAccount(CodexAccess access)
{
var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
if (ethAccount == null) return null;
return ethAccount.EthAddress;
return ethAccount;
}
public CrashWatcher CreateCrashWatcher(RunningContainer c)
+34 -3
View File
@@ -169,7 +169,7 @@ namespace CodexPlugin
public bool IsValidator { get; private set; }
public Ether InitialEth { get; private set; } = 0.Eth();
public TestToken InitialTestTokens { get; private set; } = 0.Tst();
public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew();
public EthAccountSetup EthAccountSetup { get; private set; } = new EthAccountSetup();
public IMarketplaceSetup AsStorageNode()
{
@@ -185,7 +185,7 @@ namespace CodexPlugin
public IMarketplaceSetup WithAccount(EthAccount account)
{
EthAccount = account;
EthAccountSetup.Pin(account);
return this;
}
@@ -201,10 +201,41 @@ namespace CodexPlugin
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
result += IsStorageNode ? "(storageNode)" : "()";
result += IsValidator ? "(validator)" : "() ";
result += $"Address: '{EthAccount.EthAddress}' ";
result += $"Address: '{EthAccountSetup}' ";
result += $"{InitialEth.Eth} / {InitialTestTokens}";
result += "] ";
return result;
}
}
public class EthAccountSetup
{
private readonly List<EthAccount> accounts = new List<EthAccount>();
private bool pinned = false;
public void Pin(EthAccount account)
{
accounts.Add(account);
pinned = true;
}
public EthAccount GetNew()
{
if (pinned) return accounts.Last();
var a = EthAccount.GenerateNew();
accounts.Add(a);
return a;
}
public EthAccount[] GetAll()
{
return accounts.ToArray();
}
public override string ToString()
{
return string.Join(",", accounts.Select(a => a.ToString()).ToArray());
}
}
}
+3 -134
View File
@@ -1,5 +1,4 @@
using Logging;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
@@ -7,7 +6,7 @@ namespace CodexPlugin
public interface IMarketplaceAccess
{
string MakeStorageAvailable(StorageAvailability availability);
StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
}
public class MarketplaceAccess : IMarketplaceAccess
@@ -21,7 +20,7 @@ namespace CodexPlugin
this.codexAccess = codexAccess;
}
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{
purchase.Log(log);
@@ -68,7 +67,7 @@ namespace CodexPlugin
throw new NotImplementedException();
}
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{
Unavailable();
throw new NotImplementedException();
@@ -80,134 +79,4 @@ namespace CodexPlugin
throw new InvalidOperationException();
}
}
public class StoragePurchaseContract
{
private readonly ILog log;
private readonly CodexAccess codexAccess;
private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30);
private readonly DateTime contractPendingUtc = DateTime.UtcNow;
private DateTime? contractSubmittedUtc = DateTime.UtcNow;
private DateTime? contractStartedUtc;
private DateTime? contractFinishedUtc;
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase)
{
this.log = log;
this.codexAccess = codexAccess;
PurchaseId = purchaseId;
Purchase = purchase;
ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid);
}
public string PurchaseId { get; }
public StoragePurchaseRequest Purchase { get; }
public ContentId ContentId { get; }
public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc;
public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc;
public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc;
public void WaitForStorageContractSubmitted()
{
WaitForStorageContractState(gracePeriod, "submitted", sleep: 200);
contractSubmittedUtc = DateTime.UtcNow;
LogSubmittedDuration();
AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted));
}
public void WaitForStorageContractStarted()
{
var timeout = Purchase.Expiry + gracePeriod;
WaitForStorageContractState(timeout, "started");
contractStartedUtc = DateTime.UtcNow;
LogStartedDuration();
AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted));
}
public void WaitForStorageContractFinished()
{
if (!contractStartedUtc.HasValue)
{
WaitForStorageContractStarted();
}
var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
WaitForStorageContractState(timeout, "finished");
contractFinishedUtc = DateTime.UtcNow;
LogFinishedDuration();
AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished));
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000)
{
var lastState = "";
var waitStart = DateTime.UtcNow;
Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'.");
while (lastState != desiredState)
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.State != lastState)
{
lastState = purchaseStatus.State;
log.Debug("Purchase status: " + statusJson);
}
Thread.Sleep(sleep);
if (lastState == "errored")
{
FrameworkAssert.Fail("Contract errored: " + statusJson);
}
if (DateTime.UtcNow - waitStart > timeout)
{
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
}
}
}
private void LogSubmittedDuration()
{
Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " +
$"( < {Time.FormatDuration(gracePeriod)})");
}
private void LogStartedDuration()
{
Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " +
$"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
}
private void LogFinishedDuration()
{
Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " +
$"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})");
}
private void AssertDuration(TimeSpan? span, TimeSpan max, string message)
{
if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)");
if (span.Value.TotalDays >= max.TotalSeconds)
{
throw new Exception(nameof(MarketplaceAccess) +
$": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " +
message);
}
}
private void Log(string msg)
{
log.Log($"[{PurchaseId}] {msg}");
}
}
}
@@ -0,0 +1,146 @@
using Logging;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
{
public interface IStoragePurchaseContract
{
string PurchaseId { get; }
StoragePurchaseRequest Purchase { get; }
ContentId ContentId { get; }
void WaitForStorageContractSubmitted();
void WaitForStorageContractStarted();
void WaitForStorageContractFinished();
}
public class StoragePurchaseContract : IStoragePurchaseContract
{
private readonly ILog log;
private readonly CodexAccess codexAccess;
private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30);
private readonly DateTime contractPendingUtc = DateTime.UtcNow;
private DateTime? contractSubmittedUtc = DateTime.UtcNow;
private DateTime? contractStartedUtc;
private DateTime? contractFinishedUtc;
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase)
{
this.log = log;
this.codexAccess = codexAccess;
PurchaseId = purchaseId;
Purchase = purchase;
ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid);
}
public string PurchaseId { get; }
public StoragePurchaseRequest Purchase { get; }
public ContentId ContentId { get; }
public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc;
public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc;
public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc;
public void WaitForStorageContractSubmitted()
{
WaitForStorageContractState(gracePeriod, "submitted", sleep: 200);
contractSubmittedUtc = DateTime.UtcNow;
LogSubmittedDuration();
AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted));
}
public void WaitForStorageContractStarted()
{
var timeout = Purchase.Expiry + gracePeriod;
WaitForStorageContractState(timeout, "started");
contractStartedUtc = DateTime.UtcNow;
LogStartedDuration();
AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted));
}
public void WaitForStorageContractFinished()
{
if (!contractStartedUtc.HasValue)
{
WaitForStorageContractStarted();
}
var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
WaitForStorageContractState(timeout, "finished");
contractFinishedUtc = DateTime.UtcNow;
LogFinishedDuration();
AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished));
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000)
{
var lastState = "";
var waitStart = DateTime.UtcNow;
Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'.");
while (lastState != desiredState)
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.State != lastState)
{
lastState = purchaseStatus.State;
log.Debug("Purchase status: " + statusJson);
}
Thread.Sleep(sleep);
if (lastState == "errored")
{
FrameworkAssert.Fail("Contract errored: " + statusJson);
}
if (DateTime.UtcNow - waitStart > timeout)
{
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
}
}
}
private void LogSubmittedDuration()
{
Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " +
$"( < {Time.FormatDuration(gracePeriod)})");
}
private void LogStartedDuration()
{
Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " +
$"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
}
private void LogFinishedDuration()
{
Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " +
$"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})");
}
private void AssertDuration(TimeSpan? span, TimeSpan max, string message)
{
if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)");
if (span.Value.TotalDays >= max.TotalSeconds)
{
throw new Exception(nameof(MarketplaceAccess) +
$": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " +
message);
}
}
private void Log(string msg)
{
log.Log($"[{PurchaseId}] {msg}");
}
}
}
+5
View File
@@ -24,5 +24,10 @@ namespace GethPlugin
return new EthAccount(ethAddress, account.PrivateKey);
}
public override string ToString()
{
return EthAddress.ToString();
}
}
}
@@ -33,10 +33,9 @@ namespace CodexTests.BasicTests
.AsStorageNode()
.AsValidator()));
var expectedHostBalance = (numberOfHosts * hostInitialBalance.TstWei).TstWei();
foreach (var host in hosts)
{
AssertBalance(contracts, host, Is.EqualTo(expectedHostBalance));
AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance));
var availability = new StorageAvailability(
totalSpace: 10.GB(),
@@ -65,8 +64,8 @@ namespace CodexTests.BasicTests
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(5),
Expiry = TimeSpan.FromMinutes(4)
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var purchaseContract = client.Marketplace.RequestStorage(purchase);
@@ -129,8 +128,8 @@ namespace CodexTests.BasicTests
{
Time.Retry(() =>
{
var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange());
var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
var events = contracts.GetEvents(GetTestRunTimeRange());
var slotFilledEvents = events.GetSlotFilledEvents();
var msg = $"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}";
Debug(msg);
@@ -147,7 +146,8 @@ namespace CodexTests.BasicTests
private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth)
{
var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()));
var events = contracts.GetEvents(GetTestRunTimeRange());
var requests = events.GetStorageRequests();
Assert.That(requests.Length, Is.EqualTo(1));
return requests.Single();
}
+2
View File
@@ -13,11 +13,13 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexDiscordBotPlugin\CodexDiscordBotPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\MetricsPlugin\MetricsPlugin.csproj" />
<ProjectReference Include="..\..\Tools\TestNetRewarder\TestNetRewarder.csproj" />
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
</ItemGroup>
@@ -49,6 +49,8 @@ namespace CodexTests.PeerDiscoveryTests
private void AssertAllNodesConnected(IEnumerable<ICodexNode> nodes)
{
nodes = nodes.Concat(new[] { BootstrapNode }).ToArray()!;
CreatePeerConnectionTestHelpers().AssertFullyConnected(nodes);
CheckRoutingTable(nodes);
}
+326 -61
View File
@@ -1,7 +1,13 @@
using CodexContractsPlugin;
using CodexDiscordBotPlugin;
using CodexPlugin;
using Core;
using DiscordRewards;
using DistTestCore;
using GethPlugin;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using Utils;
@@ -10,21 +16,156 @@ namespace CodexTests.UtilityTests
[TestFixture]
public class DiscordBotTests : AutoBootstrapDistTest
{
private readonly RewardRepo repo = new RewardRepo();
private readonly TestToken hostInitialBalance = 3000000.TstWei();
private readonly TestToken clientInitialBalance = 1000000000.TstWei();
private readonly EthAccount clientAccount = EthAccount.GenerateNew();
private readonly List<EthAccount> hostAccounts = new List<EthAccount>();
private readonly List<ulong> rewardsSeen = new List<ulong>();
private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1);
private readonly List<string> receivedEvents = new List<string>();
private readonly List<MarketAverage> receivedAverages = new List<MarketAverage>();
[Test]
[Ignore("Used for debugging bots")]
[DontDownloadLogs]
[Ignore("Used to debug testnet bots.")]
public void BotRewardTest()
{
var myAccount = EthAccount.GenerateNew();
var sellerInitialBalance = 234.TstWei();
var buyerInitialBalance = 100000.TstWei();
var fileSize = 11.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var gethInfo = CreateGethInfo(geth, contracts);
// start bot and rewarder
var gethInfo = new DiscordBotGethInfo(
var botContainer = StartDiscordBot(gethInfo);
var rewarderContainer = StartRewarderBot(gethInfo, botContainer);
StartHosts(geth, contracts);
var client = StartClient(geth, contracts);
var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer);
apiCalls.Start(OnCommand);
var purchaseContract = ClientPurchasesStorage(client);
purchaseContract.WaitForStorageContractStarted();
purchaseContract.WaitForStorageContractFinished();
Thread.Sleep(rewarderInterval * 3);
apiCalls.Stop();
AssertEventOccurance("Created as New.", 1);
AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts()));
AssertEventOccurance("Transit: New -> Started", 1);
AssertEventOccurance("Transit: Started -> Finished", 1);
AssertMarketAverage();
foreach (var r in repo.Rewards)
{
var seen = rewardsSeen.Any(s => r.RoleId == s);
Log($"{Lookup(r.RoleId)} = {seen}");
}
Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId)));
}
private string Lookup(ulong rewardId)
{
var reward = repo.Rewards.Single(r => r.RoleId == rewardId);
return $"({rewardId})'{reward.Message}'";
}
private void AssertEventOccurance(string msg, int expectedCount)
{
Assert.That(receivedEvents.Count(e => e.Contains(msg)), Is.EqualTo(expectedCount),
$"Event '{msg}' did not occure correct number of times.");
}
private void AssertMarketAverage()
{
Assert.That(receivedAverages.Count, Is.EqualTo(1));
var a = receivedAverages.Single();
Assert.That(a.NumberOfFinished, Is.EqualTo(1));
Assert.That(a.TimeRangeSeconds, Is.EqualTo(5760));
Assert.That(a.Price, Is.EqualTo(2.0f).Within(0.1f));
Assert.That(a.Size, Is.EqualTo(GetMinFileSize().SizeInBytes).Within(1.0f));
Assert.That(a.Duration, Is.EqualTo(GetMinRequiredRequestDuration().TotalSeconds).Within(1.0f));
Assert.That(a.Collateral, Is.EqualTo(10.0f).Within(0.1f));
Assert.That(a.ProofProbability, Is.EqualTo(5.0f).Within(0.1f));
}
private void OnCommand(string timestamp, GiveRewardsCommand call)
{
Log($"<API call {timestamp}>");
receivedAverages.AddRange(call.Averages);
foreach (var a in call.Averages)
{
Log("\tAverage: " + JsonConvert.SerializeObject(a));
}
receivedEvents.AddRange(call.EventsOverview);
foreach (var e in call.EventsOverview)
{
Log("\tEvent: " + e);
}
foreach (var r in call.Rewards)
{
var reward = repo.Rewards.Single(a => a.RoleId == r.RewardId);
if (r.UserAddresses.Any()) rewardsSeen.Add(reward.RoleId);
foreach (var address in r.UserAddresses)
{
var user = IdentifyAccount(address);
Log("\tReward: " + user + ": " + reward.Message);
}
}
Log($"</API call>");
}
private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client)
{
var testFile = GenerateTestFile(GetMinFileSize());
var contentId = client.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{
PricePerSlotPerSecond = 2.TstWei(),
RequiredCollateral = 10.TstWei(),
MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(),
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = GetMinRequiredRequestDuration(),
Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1)
};
return client.Marketplace.RequestStorage(purchase);
}
private ICodexNode StartClient(IGethNode geth, ICodexContracts contracts)
{
var node = StartCodex(s => s
.WithName("Client")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(clientAccount)
.WithInitial(10.Eth(), clientInitialBalance)));
Log($"Client {node.EthAccount.EthAddress}");
return node;
}
private RunningPod StartRewarderBot(DiscordBotGethInfo gethInfo, RunningContainer botContainer)
{
return Ci.DeployRewarderBot(new RewarderBotStartupConfig(
name: "rewarder-bot",
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: Convert.ToInt32(Math.Round(rewarderInterval.TotalMinutes)),
historyStartUtc: DateTime.UtcNow,
gethInfo: gethInfo,
dataPath: null
));
}
private DiscordBotGethInfo CreateGethInfo(IGethNode geth, ICodexContracts contracts)
{
return new DiscordBotGethInfo(
host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host,
port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port,
privKey: geth.StartResult.Account.PrivateKey,
@@ -32,8 +173,12 @@ namespace CodexTests.UtilityTests
tokenAddress: contracts.Deployment.TokenAddress,
abi: contracts.Deployment.Abi
);
}
private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo)
{
var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "bot",
name: "discord-bot",
token: "aaa",
serverName: "ThatBen's server",
adminRoleName: "bottest-admins",
@@ -42,70 +187,190 @@ namespace CodexTests.UtilityTests
kubeNamespace: "notneeded",
gethInfo: gethInfo
));
var botContainer = bot.Containers.Single();
Ci.DeployRewarderBot(new RewarderBotStartupConfig(
//discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host,
//discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port,
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: "1",
historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3),
gethInfo: gethInfo,
dataPath: null
));
return bot.Containers.Single();
}
var numberOfHosts = 3;
private void StartHosts(IGethNode geth, ICodexContracts contracts)
{
var hosts = StartCodex(GetNumberOfLiveHosts(), s => s
.WithName("Host")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts()))
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), hostInitialBalance)
.AsStorageNode()
.AsValidator()));
for (var i = 0; i < numberOfHosts; i++)
var availability = new StorageAvailability(
totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TstWei(),
maxCollateral: hostInitialBalance
);
foreach (var host in hosts)
{
var seller = StartCodex(s => s
.WithName("Seller")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), sellerInitialBalance)
.AsStorageNode()
.AsValidator()));
hostAccounts.Add(host.EthAccount);
host.Marketplace.MakeStorageAvailable(availability);
}
}
var availability = new StorageAvailability(
totalSpace: 10.GB(),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TstWei(),
maxCollateral: 20.TstWei()
);
seller.Marketplace.MakeStorageAvailable(availability);
private int GetNumberOfLiveHosts()
{
return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3;
}
private ByteSize Mult(ByteSize size, int mult)
{
return new ByteSize(size.SizeInBytes * mult);
}
private ByteSize GetMinFileSizePlus(int plusMb)
{
return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes);
}
private ByteSize GetMinFileSize()
{
ulong minSlotSize = 0;
ulong minNumHosts = 0;
foreach (var r in repo.Rewards)
{
var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes);
var h = r.CheckConfig.MinNumberOfHosts;
if (s > minSlotSize) minSlotSize = s;
if (h > minNumHosts) minNumHosts = h;
}
var testFile = GenerateTestFile(fileSize);
var minFileSize = ((minSlotSize + 1024) * minNumHosts);
return new ByteSize(Convert.ToInt64(minFileSize));
}
var buyer = StartCodex(s => s
.WithName("Buyer")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), buyerInitialBalance)));
private uint GetNumberOfRequiredHosts()
{
return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts));
}
var contentId = buyer.UploadFile(testFile);
private TimeSpan GetMinRequiredRequestDuration()
{
return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10);
}
var purchase = new StoragePurchaseRequest(contentId)
private string IdentifyAccount(string address)
{
if (address == clientAccount.EthAddress.Address) return "Client";
try
{
PricePerSlotPerSecond = 2.TstWei(),
RequiredCollateral = 10.TstWei(),
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address);
return "Host" + index;
}
catch
{
return "UNKNOWN";
}
}
var purchaseContract = buyer.Marketplace.RequestStorage(purchase);
public class RewardApiCalls
{
private readonly ContainerFileMonitor monitor;
purchaseContract.WaitForStorageContractStarted();
public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log");
}
purchaseContract.WaitForStorageContractFinished();
public void Start(Action<string, GiveRewardsCommand> onCommand)
{
monitor.Start(line => ParseLine(line, onCommand));
}
public void Stop()
{
monitor.Stop();
}
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
{
try
{
var timestamp = line.Substring(0, 30);
var json = line.Substring(31);
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
if (cmd != null)
{
onCommand(timestamp, cmd);
}
}
catch
{
}
}
}
public class ContainerFileMonitor
{
private readonly ILog log;
private readonly CoreInterface ci;
private readonly RunningContainer botContainer;
private readonly string filePath;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly List<string> seenLines = new List<string>();
private Task worker = Task.CompletedTask;
private Action<string> onNewLine = c => { };
public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath)
{
this.log = log;
this.ci = ci;
this.botContainer = botContainer;
this.filePath = filePath;
}
public void Start(Action<string> onNewLine)
{
this.onNewLine = onNewLine;
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
}
// did any container crash? that's why it repeats?
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Update();
}
}
private void Update()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
if (cts.IsCancellationRequested) return;
var botLog = ci.ExecuteContainerCommand(botContainer, "cat", filePath);
var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// log.Log("line: " + line);
if (!seenLines.Contains(line))
{
seenLines.Add(line);
onNewLine(line);
}
}
}
}
}
}
+16
View File
@@ -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>
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>
+45
View File
@@ -0,0 +1,45 @@
using ArgsUniform;
namespace AutoClient
{
public class Configuration
{
[Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default 'http://localhost')")]
public string CodexHost { get; set; } = "http://localhost";
[Uniform("codex-port", "cp", "CODEXPORT", false, "port number of Codex API. (8080 by default)")]
public int CodexPort { get; set; } = 8080;
[Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
public string DataPath { get; set; } = "datapath";
[Uniform("purchases", "np", "PURCHASES", false, "Number of concurrent purchases.")]
public int NumConcurrentPurchases { get; set; } = 10;
[Uniform("contract-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 30)")]
public int ContractDurationMinutes { get; set; } = 30;
[Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15)")]
public int ContractExpiryMinutes { get; set; } = 15;
[Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")]
public int NumHosts { get; set; } = 5;
[Uniform("num-hosts-tolerance", "nt", "NUMTOL", false, "Number of host tolerance for contract. (default 2)")]
public int HostTolerance { get; set; } = 2;
[Uniform("price","p", "PRICE", false, "Price of contract. (default 10)")]
public int Price { get; set; } = 10;
[Uniform("collateral", "c", "COLLATERAL", false, "Required collateral. (default 1)")]
public int RequiredCollateral { get; set; } = 1;
public string LogPath
{
get
{
return Path.Combine(DataPath, "logs");
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace AutoClient
{
public class ImageGenerator
{
public async Task<string> GenerateImage()
{
var httpClient = new HttpClient();
var thing = await httpClient.GetStreamAsync("https://picsum.photos/3840/2160");
var filename = $"{Guid.NewGuid().ToString().ToLowerInvariant()}.jpg";
using var file = File.OpenWrite(filename);
await thing.CopyToAsync(file);
return filename;
}
}
}
+80
View File
@@ -0,0 +1,80 @@
using ArgsUniform;
using AutoClient;
using CodexOpenApi;
using Core;
using Logging;
public static class Program
{
public static async Task Main(string[] args)
{
var cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
Console.CancelKeyPress += (sender, args) => cts.Cancel();
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
var config = uniformArgs.Parse(true);
if (config.NumConcurrentPurchases < 1)
{
throw new Exception("Number of concurrent purchases must be > 0");
}
var log = new LogSplitter(
new FileLog(Path.Combine(config.LogPath, "autoclient")),
new ConsoleLog()
);
var address = new Utils.Address(
host: config.CodexHost,
port: config.CodexPort
);
log.Log($"Start. Address: {address}");
var imgGenerator = new ImageGenerator();
var client = new HttpClient();
var codex = new CodexApi(client);
codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
await CheckCodex(codex, log);
var purchasers = new List<Purchaser>();
for (var i = 0; i < config.NumConcurrentPurchases; i++)
{
purchasers.Add(
new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, cancellationToken, config, imgGenerator)
);
}
var delayPerPurchaser = TimeSpan.FromMinutes(config.ContractDurationMinutes) / config.NumConcurrentPurchases;
foreach (var purchaser in purchasers)
{
purchaser.Start();
await Task.Delay(delayPerPurchaser);
}
log.Log("Done.");
}
private static async Task CheckCodex(CodexApi codex, ILog log)
{
log.Log("Checking Codex...");
try
{
var info = await codex.GetDebugInfoAsync();
if (string.IsNullOrEmpty(info.Id)) throw new Exception("Failed to fetch Codex node id");
}
catch (Exception ex)
{
log.Log($"Codex not OK: {ex}");
throw;
}
}
private static void PrintHelp()
{
Console.WriteLine("Generates fake data and creates Codex storage contracts for it.");
}
}
+166
View File
@@ -0,0 +1,166 @@
using CodexOpenApi;
using CodexPlugin;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace AutoClient
{
public class Purchaser
{
private readonly ILog log;
private readonly HttpClient client;
private readonly Address address;
private readonly CodexApi codex;
private readonly CancellationToken ct;
private readonly Configuration config;
private readonly ImageGenerator generator;
public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, CancellationToken ct, Configuration config, ImageGenerator generator)
{
this.log = log;
this.client = client;
this.address = address;
this.codex = codex;
this.ct = ct;
this.config = config;
this.generator = generator;
}
public void Start()
{
Task.Run(Worker);
}
private async Task Worker()
{
while (!ct.IsCancellationRequested)
{
var pid = await StartNewPurchase();
await WaitTillFinished(pid);
}
}
private async Task<string> StartNewPurchase()
{
var file = await CreateFile();
var cid = await UploadFile(file);
return await RequestStorage(cid);
}
private async Task<string> CreateFile()
{
return await generator.GenerateImage();
}
private async Task<ContentId> UploadFile(string filename)
{
// Copied from CodexNode :/
using var fileStream = File.OpenRead(filename);
log.Log($"Uploading file {filename}...");
var response = await codex.UploadAsync(fileStream, ct);
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block.");
log.Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
private async Task<string> RequestStorage(ContentId cid)
{
log.Log("Requesting storage for " + cid.Id);
var result = await codex.CreateStorageRequestAsync(cid.Id, new StorageRequestCreation()
{
Collateral = config.RequiredCollateral.ToString(),
Duration = (config.ContractDurationMinutes * 60).ToString(),
Expiry = (config.ContractExpiryMinutes * 60).ToString(),
Nodes = config.NumHosts,
Reward = config.Price.ToString(),
ProofProbability = "15",
Tolerance = config.HostTolerance
}, ct);
log.Log("Purchase ID: " + result);
return result;
}
private async Task<string?> GetPurchaseState(string pid)
{
try
{
// openapi still don't match code.
var str = await client.GetStringAsync($"{address.Host}:{address.Port}/api/codex/v1/storage/purchases/{pid}");
if (string.IsNullOrEmpty(str)) return null;
var sp = JsonConvert.DeserializeObject<StoragePurchase>(str)!;
log.Log($"Purchase {pid} is {sp.State}");
if (!string.IsNullOrEmpty(sp.Error)) log.Log($"Purchase {pid} error is {sp.Error}");
return sp.State;
}
catch
{
return null;
}
}
private async Task WaitTillFinished(string pid)
{
log.Log("Waiting...");
try
{
var emptyResponseTolerance = 10;
while (true)
{
var status = (await GetPurchaseState(pid))?.ToLowerInvariant();
if (string.IsNullOrEmpty(status))
{
emptyResponseTolerance--;
if (emptyResponseTolerance == 0)
{
log.Log("Received 10 empty responses. Stop tracking this purchase.");
await ExpiryTimeDelay();
return;
}
}
else
{
if (status.Contains("cancel") ||
status.Contains("error") ||
status.Contains("finished"))
{
return;
}
if (status.Contains("started"))
{
await FixedDurationDelay();
}
}
await FixedShortDelay();
}
}
catch (Exception ex)
{
log.Log($"Wait failed with exception: {ex}. Assume contract will expire: Wait expiry time.");
await ExpiryTimeDelay();
}
}
private async Task FixedDurationDelay()
{
await Task.Delay(config.ContractDurationMinutes * 60 * 1000, ct);
}
private async Task ExpiryTimeDelay()
{
await Task.Delay(config.ContractExpiryMinutes * 60 * 1000, ct);
}
private async Task FixedShortDelay()
{
await Task.Delay(15 * 1000, ct);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
# Variables
ARG BUILDER=mcr.microsoft.com/dotnet/sdk:7.0
ARG IMAGE=${BUILDER}
ARG APP_HOME=/app
# Build
FROM ${IMAGE} AS builder
ARG APP_HOME
WORKDIR ${APP_HOME}
COPY ./Tools/AutoClient ./Tools/AutoClient
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
RUN dotnet restore Tools/AutoClient
RUN dotnet publish Tools/AutoClient -c Release -o out
# Create
FROM ${IMAGE}
ARG APP_HOME
ENV APP_HOME=${APP_HOME}
WORKDIR ${APP_HOME}
COPY --from=builder ${APP_HOME}/out .
CMD dotnet ${APP_HOME}/AutoClient.dll
+10 -6
View File
@@ -3,19 +3,23 @@ using Discord.WebSocket;
using Discord;
using Newtonsoft.Json;
using BiblioTech.Rewards;
using Logging;
namespace BiblioTech
{
public class CommandHandler
{
private readonly DiscordSocketClient client;
private readonly CustomReplacement replacement;
private readonly BaseCommand[] commands;
private readonly ILog log;
public CommandHandler(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;
client.SlashCommandExecuted += SlashCommandHandler;
}
@@ -24,12 +28,12 @@ namespace BiblioTech
{
var guild = client.Guilds.Single(g => g.Id == Program.Config.ServerId);
Program.AdminChecker.SetGuild(guild);
Program.Log.Log($"Initializing for guild: '{guild.Name}'");
log.Log($"Initializing for guild: '{guild.Name}'");
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);
Program.RoleDriver = new RoleDriver(client, log, replacement);
var builders = commands.Select(c =>
{
@@ -44,7 +48,7 @@ namespace BiblioTech
builder.AddOption(option.Build());
}
Program.Log.Log(msg);
log.Log(msg);
return builder;
});
@@ -58,7 +62,7 @@ namespace BiblioTech
catch (HttpException exception)
{
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
Program.Log.Error(json);
log.Error(json);
}
}
+59 -3
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}'.");
}
}
}
}
}
+6 -22
View File
@@ -35,28 +35,12 @@ namespace BiblioTech
[Uniform("mint-tt", "mt", "MINTTT", true, "Amount of TSTWEI minted by the mint command.")]
public BigInteger MintTT { get; set; } = 1073741824;
public string EndpointsPath
{
get
{
return Path.Combine(DataPath, "endpoints");
}
}
[Uniform("no-discord", "nd", "NODISCORD", false, "For debugging: Bypasses all Discord API calls.")]
public int NoDiscord { get; set; } = 0;
public string UserDataPath
{
get
{
return Path.Combine(DataPath, "users");
}
}
public string LogPath
{
get
{
return Path.Combine(DataPath, "logs");
}
}
public string EndpointsPath => Path.Combine(DataPath, "endpoints");
public string UserDataPath => Path.Combine(DataPath, "users");
public string LogPath => Path.Combine(DataPath, "logs");
public bool DebugNoDiscord => NoDiscord == 1;
}
}
+24
View File
@@ -0,0 +1,24 @@
using BiblioTech.Rewards;
using DiscordRewards;
using Logging;
using Newtonsoft.Json;
namespace BiblioTech
{
public class LoggingRoleDriver : IDiscordRoleDriver
{
private readonly ILog log;
public LoggingRoleDriver(ILog log)
{
this.log = log;
}
public async Task GiveRewards(GiveRewardsCommand rewards)
{
await Task.CompletedTask;
log.Log(JsonConvert.SerializeObject(rewards, Formatting.None));
}
}
}
+33 -19
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();
@@ -41,25 +42,15 @@ namespace BiblioTech
public async Task MainAsync(string[] args)
{
Log.Log("Starting Codex Discord Bot...");
client = new DiscordSocketClient();
client.Log += ClientLog;
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(client,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand),
new MarketCommand()
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
AdminChecker = new AdminChecker();
if (Config.DebugNoDiscord)
{
Log.Log("Debug option is set. Discord connection disabled!");
RoleDriver = new LoggingRoleDriver(Log);
}
else
{
await StartDiscordBot();
}
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel((context, options) =>
@@ -75,6 +66,29 @@ namespace BiblioTech
await Task.Delay(-1);
}
private async Task StartDiscordBot()
{
client = new DiscordSocketClient();
client.Log += ClientLog;
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(Log, client, replacement,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand, replacement),
new MarketCommand()
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
AdminChecker = new AdminChecker();
}
private static void PrintHelp()
{
Log.Log("BiblioTech - Codex Discord Bot");
@@ -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);
}
}
}
@@ -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;
}
}
}
+31 -38
View File
@@ -1,6 +1,7 @@
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using Logging;
using Newtonsoft.Json;
namespace BiblioTech.Rewards
@@ -8,41 +9,49 @@ namespace BiblioTech.Rewards
public class RoleDriver : IDiscordRoleDriver
{
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)
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)
{
Program.Log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'");
log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'");
if (rewards.Rewards.Any())
{
await ProcessRewards(rewards);
}
await ProcessChainEvents(rewards.EventsOverview);
await eventsSender.ProcessChainEvents(rewards.EventsOverview);
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
{
var guild = GetGuild();
// We load all role and user information first,
// so we don't ask the server for the same info multiple times.
var context = new RewardContext(
await LoadAllUsers(guild),
LookUpAllRoles(guild, rewards),
rewardsChannel);
try
{
var guild = GetGuild();
// We load all role and user information first,
// so we don't ask the server for the same info multiple times.
var context = new RewardContext(
await LoadAllUsers(guild),
LookUpAllRoles(guild, rewards),
rewardsChannel);
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
}
catch (Exception ex)
{
log.Error("Failed to process rewards: " + ex);
}
}
private SocketTextChannel? GetChannel(ulong id)
@@ -51,25 +60,9 @@ 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;
await Task.Run(async () =>
{
foreach (var e in eventsOverview)
{
if (!string.IsNullOrEmpty(e))
{
await eventsChannel.SendMessageAsync(e);
await Task.Delay(3000);
}
}
});
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
Program.Log.Log("Loading all users:");
log.Log("Loading all users..");
var result = new Dictionary<ulong, IGuildUser>();
var users = guild.GetUsersAsync();
await foreach (var ulist in users)
@@ -77,8 +70,8 @@ namespace BiblioTech.Rewards
foreach (var u in ulist)
{
result.Add(u.Id, u);
var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray());
Program.Log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]");
//var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray());
//log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]");
}
}
return result;
@@ -94,14 +87,14 @@ namespace BiblioTech.Rewards
var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
if (rewardConfig == null)
{
Program.Log.Log($"No Reward is configured for id '{r.RewardId}'.");
log.Log($"No Reward is configured for id '{r.RewardId}'.");
}
else
{
var socketRole = guild.GetRole(r.RewardId);
if (socketRole == null)
{
Program.Log.Log($"Guild Role by id '{r.RewardId}' not found.");
log.Log($"Guild Role by id '{r.RewardId}' not found.");
}
else
{
@@ -134,13 +127,13 @@ namespace BiblioTech.Rewards
try
{
var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
if (userData != null) Program.Log.Log($"User '{userData.Name}' was looked up.");
else Program.Log.Log($"Lookup for user was unsuccessful. EthAddress: '{address}'");
if (userData != null) log.Log($"User '{userData.Name}' was looked up.");
else log.Log($"Lookup for user was unsuccessful. EthAddress: '{address}'");
return userData;
}
catch (Exception ex)
{
Program.Log.Error("Error during UserData lookup: " + ex);
log.Error("Error during UserData lookup: " + ex);
return null;
}
}
+66
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; }
}
}
+44 -58
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; }
}
}
+46
View File
@@ -0,0 +1,46 @@
using CodexContractsPlugin.ChainMonitor;
using GethPlugin;
using System.Numerics;
namespace TestNetRewarder
{
public class ChainChangeMux : IChainStateChangeHandler
{
private readonly IChainStateChangeHandler[] handlers;
public ChainChangeMux(params IChainStateChangeHandler[] handlers)
{
this.handlers = handlers;
}
public void OnNewRequest(RequestEvent requestEvent)
{
foreach (var handler in handlers) handler.OnNewRequest(requestEvent);
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
foreach (var handler in handlers) handler.OnRequestCancelled(requestEvent);
}
public void OnRequestFinished(RequestEvent requestEvent)
{
foreach (var handler in handlers) handler.OnRequestFinished(requestEvent);
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
foreach (var handler in handlers) handler.OnRequestFulfilled(requestEvent);
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
foreach (var handler in handlers) handler.OnSlotFilled(requestEvent, host, slotIndex);
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
foreach (var handler in handlers) handler.OnSlotFreed(requestEvent, slotIndex);
}
}
}
-151
View File
@@ -1,151 +0,0 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class ChainState
{
private readonly HistoricState historicState;
private readonly string[] colorIcons = new[]
{
"🔴",
"🟠",
"🟡",
"🟢",
"🔵",
"🟣",
"🟤",
"⚫",
"⚪",
"🟥",
"🟧",
"🟨",
"🟩",
"🟦",
"🟪",
"🟫",
"⬛",
"⬜",
"🔶",
"🔷"
};
public ChainState(HistoricState historicState, ICodexContracts contracts, BlockInterval blockRange)
{
this.historicState = historicState;
NewRequests = contracts.GetStorageRequests(blockRange);
historicState.CleanUpOldRequests();
historicState.ProcessNewRequests(NewRequests);
historicState.UpdateStorageRequests(contracts);
StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray();
FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFinished).ToArray();
RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange);
RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange);
SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
SlotFreedEvents = contracts.GetSlotFreedEvents(blockRange);
}
public Request[] NewRequests { get; }
public StorageRequest[] AllRequests => historicState.StorageRequests;
public StorageRequest[] StartedRequests { get; private set; }
public StorageRequest[] FinishedRequests { get; private set; }
public RequestFulfilledEventDTO[] RequestFulfilledEvents { get; }
public RequestCancelledEventDTO[] RequestCancelledEvents { get; }
public SlotFilledEventDTO[] SlotFilledEvents { get; }
public SlotFreedEventDTO[] SlotFreedEvents { get; }
public string[] GenerateOverview()
{
var entries = new List<StringBlockNumberPair>();
entries.AddRange(NewRequests.Select(ToPair));
entries.AddRange(RequestFulfilledEvents.Select(ToPair));
entries.AddRange(RequestCancelledEvents.Select(ToPair));
entries.AddRange(SlotFilledEvents.Select(ToPair));
entries.AddRange(SlotFreedEvents.Select(ToPair));
entries.AddRange(FinishedRequests.Select(ToPair));
entries.Sort(new StringUtcComparer());
return entries.Select(ToLine).ToArray();
}
private StringBlockNumberPair ToPair(Request r)
{
return new StringBlockNumberPair("NewRequest", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
public StringBlockNumberPair ToPair(StorageRequest r)
{
return new StringBlockNumberPair("FinishedRequest", JsonConvert.SerializeObject(r), r.Request.Block, r.Request.RequestId);
}
private StringBlockNumberPair ToPair(RequestFulfilledEventDTO r)
{
return new StringBlockNumberPair("Fulfilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(RequestCancelledEventDTO r)
{
return new StringBlockNumberPair("Cancelled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFilledEventDTO r)
{
return new StringBlockNumberPair("SlotFilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFreedEventDTO r)
{
return new StringBlockNumberPair("SlotFreed", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private string ToLine(StringBlockNumberPair pair)
{
var nl = Environment.NewLine;
var colorIcon = GetColorIcon(pair.RequestId);
return $"{colorIcon} {pair.Block} ({pair.Name}){nl}" +
$"```json{nl}" +
$"{pair.Str}{nl}" +
$"```";
}
private string GetColorIcon(byte[] requestId)
{
var index = requestId[0] % colorIcons.Length;
return colorIcons[index];
}
public class StringBlockNumberPair
{
public StringBlockNumberPair(string name, string str, BlockTimeEntry block, byte[] requestId)
{
Name = name;
Str = str;
Block = block;
RequestId = requestId;
}
public string Name { get; }
public string Str { get; }
public BlockTimeEntry Block { get; }
public byte[] RequestId { get; }
}
public class StringUtcComparer : IComparer<StringBlockNumberPair>
{
public int Compare(StringBlockNumberPair? x, StringBlockNumberPair? y)
{
if (x == null && y == null) return 0;
if (x == null) return 1;
if (y == null) return -1;
return x.Block.BlockNumber.CompareTo(y.Block.BlockNumber);
}
}
}
}
-141
View File
@@ -1,141 +0,0 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using NethereumWorkflow;
using Utils;
namespace TestNetRewarder
{
public interface ICheck
{
EthAddress[] Check(ChainState state);
}
public class FilledAnySlotCheck : ICheck
{
public EthAddress[] Check(ChainState state)
{
return state.SlotFilledEvents.Select(e => e.Host).ToArray();
}
}
public class FinishedSlotCheck : ICheck
{
private readonly ByteSize minSize;
private readonly TimeSpan minDuration;
public FinishedSlotCheck(ByteSize minSize, TimeSpan minDuration)
{
this.minSize = minSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.FinishedRequests
.Where(r =>
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.SelectMany(r => r.Hosts)
.ToArray();
}
private bool MeetsSizeRequirement(StorageRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = minSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(StorageRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= minDuration;
}
}
public class PostedContractCheck : ICheck
{
private readonly ulong minNumberOfHosts;
private readonly ByteSize minSlotSize;
private readonly TimeSpan minDuration;
public PostedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
{
this.minNumberOfHosts = minNumberOfHosts;
this.minSlotSize = minSlotSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.NewRequests
.Where(r =>
MeetsNumSlotsRequirement(r) &&
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.Select(r => r.ClientAddress)
.ToArray();
}
private bool MeetsNumSlotsRequirement(Request r)
{
return r.Ask.Slots >= minNumberOfHosts;
}
private bool MeetsSizeRequirement(Request r)
{
var slotSize = r.Ask.SlotSize.ToDecimal();
decimal min = minSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(Request r)
{
var duration = TimeSpan.FromSeconds((double)r.Ask.Duration);
return duration >= minDuration;
}
}
public class StartedContractCheck : ICheck
{
private readonly ulong minNumberOfHosts;
private readonly ByteSize minSlotSize;
private readonly TimeSpan minDuration;
public StartedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
{
this.minNumberOfHosts = minNumberOfHosts;
this.minSlotSize = minSlotSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.StartedRequests
.Where(r =>
MeetsNumSlotsRequirement(r) &&
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.Select(r => r.Request.ClientAddress)
.ToArray();
}
private bool MeetsNumSlotsRequirement(StorageRequest r)
{
return r.Request.Ask.Slots >= minNumberOfHosts;
}
private bool MeetsSizeRequirement(StorageRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = minSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(StorageRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= minDuration;
}
}
}
+9
View File
@@ -40,5 +40,14 @@ namespace TestNetRewarder
return TimeSpan.FromMinutes(IntervalMinutes);
}
}
public DateTime HistoryStartUtc
{
get
{
if (CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
return DateTimeOffset.FromUnixTimeSeconds(CheckHistoryTimestamp).UtcDateTime;
}
}
}
}
+123
View File
@@ -0,0 +1,123 @@
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(RequestEvent requestEvent)
{
var request = requestEvent.Request;
AddRequestBlock(requestEvent, "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(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, "Cancelled");
}
public void OnRequestFinished(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, "Finished");
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, "Started");
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
AddRequestBlock(requestEvent, "Slot Filled",
$"Host: {host}",
$"Slot Index: {slotIndex}"
);
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
AddRequestBlock(requestEvent, "Slot Freed",
$"Slot Index: {slotIndex}"
);
}
private void AddRequestBlock(RequestEvent requestEvent, string eventName, params string[] content)
{
var blockNumber = $"[{requestEvent.Block.BlockNumber}]";
var title = $"{blockNumber} **{eventName}** `{requestEvent.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();
}
}
}
+14
View File
@@ -29,6 +29,20 @@ namespace TestNetRewarder
r.State == RequestState.Failed
);
}
public string EntireString()
{
return JsonConvert.SerializeObject(StorageRequests);
}
public HistoricState()
{
}
public HistoricState(StorageRequest[] requests)
{
storageRequests.AddRange(requests);
}
}
public class StorageRequest
+74
View File
@@ -0,0 +1,74 @@
using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using DiscordRewards;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketBuffer
{
private readonly List<RequestEvent> requestEvents = new List<RequestEvent>();
private readonly TimeSpan bufferSpan;
public MarketBuffer(TimeSpan bufferSpan)
{
this.bufferSpan = bufferSpan;
}
public void Add(RequestEvent requestEvent)
{
requestEvents.Add(requestEvent);
}
public void Update()
{
var now = DateTime.UtcNow;
requestEvents.RemoveAll(r => (now - r.Request.FinishedUtc) > bufferSpan);
}
public MarketAverage? GetAverage()
{
if (requestEvents.Count == 0) return null;
return new MarketAverage
{
NumberOfFinished = requestEvents.Count,
TimeRangeSeconds = (int)bufferSpan.TotalSeconds,
Price = Average(s => s.Request.Ask.Reward),
Duration = Average(s => s.Request.Ask.Duration),
Size = Average(s => GetTotalSize(s.Request.Ask)),
Collateral = Average(s => s.Request.Ask.Collateral),
ProofProbability = Average(s => s.Request.Ask.ProofProbability)
};
}
private float Average(Func<IChainStateRequest, BigInteger> getValue)
{
return Average(s =>
{
var value = getValue(s);
return (int)value;
});
}
private float Average(Func<IChainStateRequest, int> getValue)
{
var sum = 0.0f;
float count = requestEvents.Count;
foreach (var r in requestEvents)
{
sum += getValue(r.Request);
}
if (count < 1.0f) return 0.0f;
return sum / count;
}
private int GetTotalSize(Ask ask)
{
var nSlots = Convert.ToInt32(ask.Slots);
var slotSize = (int)ask.SlotSize;
return nSlots * slotSize;
}
}
}
+44 -94
View File
@@ -1,124 +1,74 @@
using CodexContractsPlugin.Marketplace;
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using GethPlugin;
using Logging;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketTracker
public class MarketTracker : IChainStateChangeHandler
{
private readonly List<ChainState> buffer = new List<ChainState>();
private readonly List<MarketBuffer> buffers = new List<MarketBuffer>();
private readonly ILog log;
public MarketAverage[] ProcessChainState(ChainState chainState)
public MarketTracker(Configuration config, ILog log)
{
var intervalCounts = GetInsightCounts();
if (!intervalCounts.Any()) return Array.Empty<MarketAverage>();
var intervals = GetInsightCounts(config);
UpdateBuffer(chainState, intervalCounts.Max());
var result = intervalCounts
.Select(GenerateMarketAverage)
.Where(a => a != null)
.Cast<MarketAverage>()
.ToArray();
if (!result.Any()) result = Array.Empty<MarketAverage>();
return result;
}
private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals)
{
buffer.Add(chainState);
while (buffer.Count > maxNumberOfIntervals)
foreach (var i in intervals)
{
buffer.RemoveAt(0);
buffers.Add(new MarketBuffer(
config.Interval * i
));
}
this.log = log;
}
private MarketAverage? GenerateMarketAverage(int numberOfIntervals)
public MarketAverage[] GetAverages()
{
var states = SelectStates(numberOfIntervals);
return CreateAverage(states);
foreach (var b in buffers) b.Update();
return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast<MarketAverage>().ToArray();
}
private ChainState[] SelectStates(int numberOfIntervals)
public void OnNewRequest(RequestEvent requestEvent)
{
if (numberOfIntervals < 1) return Array.Empty<ChainState>();
if (numberOfIntervals > buffer.Count) return Array.Empty<ChainState>();
return buffer.TakeLast(numberOfIntervals).ToArray();
}
private MarketAverage? CreateAverage(ChainState[] states)
public void OnRequestFinished(RequestEvent requestEvent)
{
foreach (var b in buffers) b.Add(requestEvent);
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
private int[] GetInsightCounts(Configuration config)
{
try
{
return new MarketAverage
{
NumberOfFinished = CountNumberOfFinishedRequests(states),
TimeRangeSeconds = GetTotalTimeRange(states),
Price = Average(states, s => s.Request.Ask.Reward),
Duration = Average(states, s => s.Request.Ask.Duration),
Size = Average(states, s => GetTotalSize(s.Request.Ask)),
Collateral = Average(states, s => s.Request.Ask.Collateral),
ProofProbability = Average(states, s => s.Request.Ask.ProofProbability)
};
}
catch (Exception ex)
{
Program.Log.Error($"Exception in CreateAverage: {ex}");
return null;
}
}
private int GetTotalSize(Ask ask)
{
var nSlots = Convert.ToInt32(ask.Slots);
var slotSize = Convert.ToInt32(ask.SlotSize);
return nSlots * slotSize;
}
private float Average(ChainState[] states, Func<StorageRequest, BigInteger> getValue)
{
return Average(states, s => Convert.ToInt32(getValue(s)));
}
private float Average(ChainState[] states, Func<StorageRequest, int> getValue)
{
var sum = 0.0f;
var count = 0.0f;
foreach (var state in states)
{
foreach (var finishedRequest in state.FinishedRequests)
{
sum += getValue(finishedRequest);
count++;
}
}
if (count < 1.0f) return 0.0f;
return sum / count;
}
private int GetTotalTimeRange(ChainState[] states)
{
return Convert.ToInt32((Program.Config.Interval * states.Length).TotalSeconds);
}
private int CountNumberOfFinishedRequests(ChainState[] states)
{
return states.Sum(s => s.FinishedRequests.Length);
}
private int[] GetInsightCounts()
{
try
{
var tokens = Program.Config.MarketInsights.Split(';').ToArray();
var tokens = config.MarketInsights.Split(';').ToArray();
return tokens.Select(t => Convert.ToInt32(t)).ToArray();
}
catch (Exception ex)
{
Program.Log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
}
return Array.Empty<int>();
return Array.Empty<int>();
}
}
}
+37 -122
View File
@@ -1,146 +1,61 @@
using DiscordRewards;
using GethPlugin;
using CodexContractsPlugin;
using CodexContractsPlugin.ChainMonitor;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class Processor
public class Processor : ITimeSegmentHandler
{
private static readonly HistoricState historicState = new HistoricState();
private static readonly RewardRepo rewardRepo = new RewardRepo();
private static readonly MarketTracker marketTracker = new MarketTracker();
private readonly RequestBuilder builder;
private readonly RewardChecker rewardChecker;
private readonly MarketTracker marketTracker;
private readonly EventsFormatter eventsFormatter;
private readonly ChainState chainState;
private readonly BotClient client;
private readonly ILog log;
private BlockInterval? lastBlockRange;
public Processor(ILog log)
public Processor(Configuration config, BotClient client, ICodexContracts contracts, ILog log)
{
this.client = client;
this.log = log;
builder = new RequestBuilder();
rewardChecker = new RewardChecker(builder);
marketTracker = new MarketTracker(config, log);
eventsFormatter = new EventsFormatter();
var handler = new ChainChangeMux(
rewardChecker.Handler,
marketTracker,
eventsFormatter
);
chainState = new ChainState(log, contracts, handler, config.HistoryStartUtc);
}
public async Task ProcessTimeSegment(TimeRange timeRange)
public async Task OnNewSegment(TimeRange timeRange)
{
var connector = GethConnector.GethConnector.Initialize(log);
if (connector == null) throw new Exception("Invalid Geth information");
try
{
var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange);
if (!IsNewBlockRange(blockRange))
{
log.Log($"Block range {blockRange} was previously processed. Skipping...");
return;
}
chainState.Update(timeRange.To);
var chainState = new ChainState(historicState, connector.CodexContracts, blockRange);
await ProcessChainState(chainState);
var averages = marketTracker.GetAverages();
var events = eventsFormatter.GetEvents();
var request = builder.Build(averages, events);
if (request.HasAny())
{
await client.SendRewards(request);
}
}
catch (Exception ex)
{
log.Error("Exception processing time segment: " + ex);
var msg = "Exception processing time segment: " + ex;
log.Error(msg);
eventsFormatter.AddError(msg);
throw;
}
}
private bool IsNewBlockRange(BlockInterval blockRange)
{
if (lastBlockRange == null ||
lastBlockRange.From != blockRange.From ||
lastBlockRange.To != blockRange.To)
{
lastBlockRange = blockRange;
return true;
}
return false;
}
private async Task ProcessChainState(ChainState chainState)
{
var outgoingRewards = new List<RewardUsersCommand>();
foreach (var reward in rewardRepo.Rewards)
{
ProcessReward(outgoingRewards, reward, chainState);
}
var marketAverages = GetMarketAverages(chainState);
var eventsOverview = GenerateEventsOverview(chainState);
log.Log($"Found {outgoingRewards.Count} rewards. " +
$"Found {marketAverages.Length} market averages. " +
$"Found {eventsOverview.Length} events.");
if (outgoingRewards.Any() || marketAverages.Any() || eventsOverview.Any())
{
if (!await SendRewardsCommand(outgoingRewards, marketAverages, eventsOverview))
{
log.Error("Failed to send reward command.");
}
}
}
private string[] GenerateEventsOverview(ChainState chainState)
{
return chainState.GenerateOverview();
}
private MarketAverage[] GetMarketAverages(ChainState chainState)
{
return marketTracker.ProcessChainState(chainState);
}
private async Task<bool> SendRewardsCommand(List<RewardUsersCommand> outgoingRewards, MarketAverage[] marketAverages, string[] eventsOverview)
{
var cmd = new GiveRewardsCommand
{
Rewards = outgoingRewards.ToArray(),
Averages = marketAverages.ToArray(),
EventsOverview = eventsOverview
};
log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd));
return await Program.BotClient.SendRewards(cmd);
}
private void ProcessReward(List<RewardUsersCommand> outgoingRewards, RewardConfig reward, ChainState chainState)
{
var winningAddresses = PerformCheck(reward, chainState);
foreach (var win in winningAddresses)
{
log.Log($"Address '{win.Address}' wins '{reward.Message}'");
}
if (winningAddresses.Any())
{
outgoingRewards.Add(new RewardUsersCommand
{
RewardId = reward.RoleId,
UserAddresses = winningAddresses.Select(a => a.Address).ToArray()
});
}
}
private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState)
{
var check = GetCheck(reward.CheckConfig);
return check.Check(chainState).Distinct().ToArray();
}
private ICheck GetCheck(CheckConfig config)
{
switch (config.Type)
{
case CheckType.FilledSlot:
return new FilledAnySlotCheck();
case CheckType.FinishedSlot:
return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration);
case CheckType.PostedContract:
return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
case CheckType.StartedContract:
return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
}
throw new Exception("Unknown check type: " + config.Type);
}
}
}
+10 -7
View File
@@ -6,10 +6,10 @@ namespace TestNetRewarder
{
public class Program
{
public static Configuration Config { get; private set; } = null!;
public static ILog Log { get; private set; } = null!;
public static CancellationToken CancellationToken { get; private set; }
public static BotClient BotClient { get; private set; } = null!;
public static CancellationToken CancellationToken;
private static Configuration Config = null!;
private static ILog Log = null!;
private static BotClient BotClient = null!;
private static Processor processor = null!;
private static DateTime lastCheck = DateTime.MinValue;
@@ -27,8 +27,11 @@ namespace TestNetRewarder
new ConsoleLog()
);
var connector = GethConnector.GethConnector.Initialize(Log);
if (connector == null) throw new Exception("Invalid Geth information");
BotClient = new BotClient(Config, Log);
processor = new Processor(Log);
processor = new Processor(Config, BotClient, connector.CodexContracts, Log);
EnsurePath(Config.DataPath);
EnsurePath(Config.LogPath);
@@ -41,12 +44,12 @@ namespace TestNetRewarder
EnsureGethOnline();
Log.Log("Starting TestNet Rewarder...");
var segmenter = new TimeSegmenter(Log, Config);
var segmenter = new TimeSegmenter(Log, Config, processor);
while (!CancellationToken.IsCancellationRequested)
{
await EnsureBotOnline();
await segmenter.WaitForNextSegment(processor.ProcessTimeSegment);
await segmenter.ProcessNextSegment();
await Task.Delay(100, CancellationToken);
}
}
+40
View File
@@ -0,0 +1,40 @@
using DiscordRewards;
using GethPlugin;
namespace TestNetRewarder
{
public class RequestBuilder : IRewardGiver
{
private readonly Dictionary<ulong, List<EthAddress>> rewards = new Dictionary<ulong, List<EthAddress>>();
public void Give(RewardConfig reward, EthAddress receiver)
{
if (rewards.ContainsKey(reward.RoleId))
{
rewards[reward.RoleId].Add(receiver);
}
else
{
rewards.Add(reward.RoleId, new List<EthAddress> { receiver });
}
}
public GiveRewardsCommand Build(MarketAverage[] marketAverages, string[] lines)
{
var result = new GiveRewardsCommand
{
Rewards = rewards.Select(p => new RewardUsersCommand
{
RewardId = p.Key,
UserAddresses = p.Value.Select(v => v.Address).ToArray()
}).ToArray(),
Averages = marketAverages,
EventsOverview = lines
};
rewards.Clear();
return result;
}
}
}
+97
View File
@@ -0,0 +1,97 @@
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using GethPlugin;
using NethereumWorkflow;
using System.Numerics;
namespace TestNetRewarder
{
public interface IRewardGiver
{
void Give(RewardConfig reward, EthAddress receiver);
}
public class RewardCheck : IChainStateChangeHandler
{
private readonly RewardConfig reward;
private readonly IRewardGiver giver;
public RewardCheck(RewardConfig reward, IRewardGiver giver)
{
this.reward = reward;
this.giver = giver;
}
public void OnNewRequest(RequestEvent requestEvent)
{
if (MeetsRequirements(CheckType.ClientPostedContract, requestEvent))
{
GiveReward(reward, requestEvent.Request.Client);
}
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
}
public void OnRequestFinished(RequestEvent requestEvent)
{
if (MeetsRequirements(CheckType.HostFinishedSlot, requestEvent))
{
foreach (var host in requestEvent.Request.Hosts.GetHosts())
{
GiveReward(reward, host);
}
}
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
if (MeetsRequirements(CheckType.ClientStartedContract, requestEvent))
{
GiveReward(reward, requestEvent.Request.Client);
}
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
if (MeetsRequirements(CheckType.HostFilledSlot, requestEvent))
{
if (host != null)
{
GiveReward(reward, host);
}
}
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
private void GiveReward(RewardConfig reward, EthAddress receiver)
{
giver.Give(reward, receiver);
}
private bool MeetsRequirements(CheckType type, RequestEvent requestEvent)
{
return
reward.CheckConfig.Type == type &&
MeetsDurationRequirement(requestEvent.Request) &&
MeetsSizeRequirement(requestEvent.Request);
}
private bool MeetsSizeRequirement(IChainStateRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = reward.CheckConfig.MinSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(IChainStateRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= reward.CheckConfig.MinDuration;
}
}
}
+17
View File
@@ -0,0 +1,17 @@
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
namespace TestNetRewarder
{
public class RewardChecker
{
public RewardChecker(IRewardGiver giver)
{
var repo = new RewardRepo();
var checks = repo.Rewards.Select(r => new RewardCheck(r, giver)).ToArray();
Handler = new ChainChangeMux(checks);
}
public IChainStateChangeHandler Handler { get; }
}
}
+32 -23
View File
@@ -3,51 +3,60 @@ using Utils;
namespace TestNetRewarder
{
public interface ITimeSegmentHandler
{
Task OnNewSegment(TimeRange timeRange);
}
public class TimeSegmenter
{
private readonly ILog log;
private readonly ITimeSegmentHandler handler;
private readonly TimeSpan segmentSize;
private DateTime start;
private DateTime latest;
public TimeSegmenter(ILog log, Configuration configuration)
public TimeSegmenter(ILog log, Configuration configuration, ITimeSegmentHandler handler)
{
this.log = log;
this.handler = handler;
if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1;
if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
segmentSize = configuration.Interval;
start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime;
latest = configuration.HistoryStartUtc;
log.Log("Starting time segments at " + start);
log.Log("Starting time segments at " + latest);
log.Log("Segment size: " + Time.FormatDuration(segmentSize));
}
public async Task WaitForNextSegment(Func<TimeRange, Task> onSegment)
public async Task ProcessNextSegment()
{
var now = DateTime.UtcNow;
var end = start + segmentSize;
var waited = false;
if (end > now)
{
// Wait for the entire time segment to be in the past.
var delay = end - now;
waited = true;
log.Log($"Waiting till time segment is in the past... {Time.FormatDuration(delay)}");
await Task.Delay(delay, Program.CancellationToken);
}
await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken);
var end = latest + segmentSize;
var waited = await WaitUntilTimeSegmentInPast(end);
if (Program.CancellationToken.IsCancellationRequested) return;
var postfix = "(Catching up...)";
if (waited) postfix = "(Real-time)";
log.Log($"Time segment [{latest} to {end}] {postfix}");
var range = new TimeRange(latest, end);
latest = end;
log.Log($"Time segment [{start} to {end}] {postfix}");
var range = new TimeRange(start, end);
start = end;
await handler.OnNewSegment(range);
}
await onSegment(range);
private async Task<bool> WaitUntilTimeSegmentInPast(DateTime end)
{
await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken);
var now = DateTime.UtcNow;
while (end > now)
{
var delay = (end - now) + TimeSpan.FromSeconds(3);
await Task.Delay(delay, Program.CancellationToken);
return true;
}
return false;
}
}
}
+7
View File
@@ -66,6 +66,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoClient", "Tools\AutoClient\AutoClient.csproj", "{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyMaker", "Tools\KeyMaker\KeyMaker.csproj", "{B57A4789-D8EF-42E0-8D20-581C4057FFD3}"
EndProject
Global
@@ -174,6 +176,10 @@ Global
{88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.Build.0 = Release|Any CPU
{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.Build.0 = Release|Any CPU
{B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -208,6 +214,7 @@ Global
{F730DA73-1C92-4107-BCFB-D33759DAB0C3} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
{B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
{88C212E9-308A-46A4-BAAD-468E8EBD8EDF} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{B57A4789-D8EF-42E0-8D20-581C4057FFD3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution