Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fbef6ff51 | ||
|
|
38ee2e4eb5 | ||
|
|
e4f7249f1e | ||
|
|
e3ca516b97 | ||
|
|
88daab379f | ||
|
|
00ed3caafe | ||
|
|
daad6468c6 | ||
|
|
3d347a936d | ||
|
|
87bda475df | ||
|
|
44dfa39737 | ||
|
|
d38f6da26f |
@@ -0,0 +1,30 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Docker - MarketInsights API
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/MarketInsights/**'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-marketinsights.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/MarketInsights/Dockerfile
|
||||
docker_repo: codexstorage/codex-marketinsights
|
||||
secrets: inherit
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
public class GiveRewardsCommand
|
||||
{
|
||||
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();
|
||||
return Rewards.Any() || EventsOverview.Any();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +16,4 @@
|
||||
public ulong RewardId { get; set; }
|
||||
public string[] UserAddresses { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class MarketAverage
|
||||
{
|
||||
public int NumberOfFinished { get; set; }
|
||||
public int TimeRangeSeconds { get; set; }
|
||||
public float Price { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public float Collateral { get; set; }
|
||||
public float ProofProbability { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Utils
|
||||
{
|
||||
public static class RollingAverage
|
||||
{
|
||||
/// <param name="currentAverage">Value of average before new value is added.</param>
|
||||
/// <param name="newNumberOfValues">Number of values in average after new value is added.</param>
|
||||
/// <param name="newValue">New value to be added.</param>
|
||||
/// <returns>New average value.</returns>
|
||||
/// <exception cref="Exception">newNumberOfValues must be 1 or greater.</exception>
|
||||
public static float GetNewAverage(float currentAverage, int newNumberOfValues, float newValue)
|
||||
{
|
||||
if (newNumberOfValues < 1) throw new Exception("Should be at least 1 value.");
|
||||
|
||||
float n = newNumberOfValues;
|
||||
var originalValue = currentAverage;
|
||||
var originalValueWeight = ((n - 1.0f) / n);
|
||||
var newValueWeight = (1.0f / n);
|
||||
return GetWeightedAverage(originalValue, originalValueWeight, newValue, newValueWeight);
|
||||
}
|
||||
|
||||
public static float GetWeightedAverage(float value1, float weight1, float value2, float weight2)
|
||||
{
|
||||
float totalWeight = weight1 + weight2;
|
||||
if (totalWeight == 0.0f) return 0.0f;
|
||||
return ((value1 * weight1) + (value2 * weight2)) / totalWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
Request[] requests,
|
||||
RequestFulfilledEventDTO[] fulfilled,
|
||||
RequestCancelledEventDTO[] cancelled,
|
||||
RequestFailedEventDTO[] failed,
|
||||
SlotFilledEventDTO[] slotFilled,
|
||||
SlotFreedEventDTO[] slotFreed
|
||||
)
|
||||
@@ -18,6 +19,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
Requests = requests;
|
||||
Fulfilled = fulfilled;
|
||||
Cancelled = cancelled;
|
||||
Failed = failed;
|
||||
SlotFilled = slotFilled;
|
||||
SlotFreed = slotFreed;
|
||||
}
|
||||
@@ -26,6 +28,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
public Request[] Requests { get; }
|
||||
public RequestFulfilledEventDTO[] Fulfilled { get; }
|
||||
public RequestCancelledEventDTO[] Cancelled { get; }
|
||||
public RequestFailedEventDTO[] Failed { get; }
|
||||
public SlotFilledEventDTO[] SlotFilled { get; }
|
||||
public SlotFreedEventDTO[] SlotFreed { get; }
|
||||
|
||||
@@ -37,6 +40,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
all.AddRange(Requests);
|
||||
all.AddRange(Fulfilled);
|
||||
all.AddRange(Cancelled);
|
||||
all.AddRange(Failed);
|
||||
all.AddRange(SlotFilled);
|
||||
all.AddRange(SlotFreed);
|
||||
return all.ToArray();
|
||||
@@ -60,6 +64,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
events.GetStorageRequests(),
|
||||
events.GetRequestFulfilledEvents(),
|
||||
events.GetRequestCancelledEvents(),
|
||||
events.GetRequestFailedEvents(),
|
||||
events.GetSlotFilledEvents(),
|
||||
events.GetSlotFreedEvents()
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
void OnRequestFinished(RequestEvent requestEvent);
|
||||
void OnRequestFulfilled(RequestEvent requestEvent);
|
||||
void OnRequestCancelled(RequestEvent requestEvent);
|
||||
void OnRequestFailed(RequestEvent requestEvent);
|
||||
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
|
||||
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
|
||||
}
|
||||
@@ -41,15 +42,12 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
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);
|
||||
@@ -124,6 +122,14 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
handler.OnRequestCancelled(new RequestEvent(@event.Block, r));
|
||||
}
|
||||
|
||||
private void ApplyEvent(RequestFailedEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.UpdateState(@event.Block.BlockNumber, RequestState.Failed);
|
||||
handler.OnRequestFailed(new RequestEvent(@event.Block, r));
|
||||
}
|
||||
|
||||
private void ApplyEvent(SlotFilledEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using GethPlugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public class ChainStateChangeHandlerMux : IChainStateChangeHandler
|
||||
{
|
||||
public ChainStateChangeHandlerMux(params IChainStateChangeHandler[] handlers)
|
||||
{
|
||||
Handlers = handlers.ToList();
|
||||
}
|
||||
|
||||
public List<IChainStateChangeHandler> Handlers { get; } = new List<IChainStateChangeHandler>();
|
||||
|
||||
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 OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnRequestFailed(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace CodexContractsPlugin
|
||||
Request[] GetStorageRequests();
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents();
|
||||
RequestFailedEventDTO[] GetRequestFailedEvents();
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents();
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents();
|
||||
}
|
||||
@@ -71,6 +72,17 @@ namespace CodexContractsPlugin
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestFailedEventDTO[] GetRequestFailedEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFailedEventDTO>(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);
|
||||
|
||||
@@ -40,6 +40,12 @@ namespace CodexContractsPlugin.Marketplace
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestFailedEventDTO : IHasBlock
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO : IHasBlock
|
||||
{
|
||||
[JsonIgnore]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,6 @@ namespace CodexTests.UtilityTests
|
||||
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]
|
||||
[DontDownloadLogs]
|
||||
@@ -56,8 +55,6 @@ namespace CodexTests.UtilityTests
|
||||
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);
|
||||
@@ -80,28 +77,9 @@ namespace CodexTests.UtilityTests
|
||||
$"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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Interfaces;
|
||||
using static FrameworkTests.Utils.RunLengthEncodingTests;
|
||||
|
||||
namespace FrameworkTests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class RunLengthEncodingRunTests
|
||||
{
|
||||
[Test]
|
||||
[Combinatorial]
|
||||
public void RunIncludes(
|
||||
[Values(0, 1, 2, 3)] int start,
|
||||
[Values(1, 2, 3, 4)] int length)
|
||||
{
|
||||
var run = new Run(start, length);
|
||||
|
||||
var shouldInclude = Enumerable.Range(start, length).ToArray();
|
||||
var shouldExclude = new int[]
|
||||
{
|
||||
shouldInclude.Min() - 1,
|
||||
shouldInclude.Max() + 1
|
||||
};
|
||||
|
||||
foreach (var incl in shouldInclude)
|
||||
{
|
||||
Assert.That(run.Includes(incl));
|
||||
}
|
||||
foreach (var excl in shouldExclude)
|
||||
{
|
||||
Assert.That(!run.Includes(excl));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunExpandToInclude()
|
||||
{
|
||||
var run = new Run(2, 3);
|
||||
Assert.That(run.Includes(2));
|
||||
Assert.That(run.Includes(4));
|
||||
Assert.That(!run.Includes(5));
|
||||
|
||||
Assert.That(run.ExpandToInclude(1), Is.False);
|
||||
Assert.That(run.ExpandToInclude(2), Is.False);
|
||||
Assert.That(run.ExpandToInclude(4), Is.False);
|
||||
Assert.That(run.ExpandToInclude(6), Is.False);
|
||||
|
||||
Assert.That(run.ExpandToInclude(5), Is.True);
|
||||
Assert.That(run.Includes(5));
|
||||
Assert.That(!run.Includes(6));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunCanUnsetLastIndex()
|
||||
{
|
||||
var run = new Run(0, 3);
|
||||
Assert.That(run.Includes(2));
|
||||
var update = run.Unset(2);
|
||||
Assert.That(!run.Includes(2));
|
||||
|
||||
Assert.That(update.NewRuns.Length, Is.EqualTo(0));
|
||||
Assert.That(update.RemoveRuns.Length, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunCanSplit()
|
||||
{
|
||||
var run = new Run(0, 6); // 0, 1, 2, 3, 4, 5
|
||||
var update = run.Unset(2);
|
||||
|
||||
Assert.That(run.Start, Is.EqualTo(0));
|
||||
Assert.That(run.Length, Is.EqualTo(2)); // 0, 1
|
||||
Assert.That(!run.Includes(2));
|
||||
|
||||
Assert.That(update.NewRuns.Length, Is.EqualTo(1));
|
||||
Assert.That(update.RemoveRuns.Length, Is.EqualTo(0));
|
||||
|
||||
Assert.That(!update.NewRuns[0].Includes(2));
|
||||
Assert.That(update.NewRuns[0].Start, Is.EqualTo(3));
|
||||
Assert.That(update.NewRuns[0].Length, Is.EqualTo(3)); // 3, 4, 5
|
||||
Assert.That(!update.NewRuns[0].Includes(6));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunReplacesSelfWhenUnsetFirstIndex()
|
||||
{
|
||||
var run = new Run(0, 5);
|
||||
var update = run.Unset(0);
|
||||
|
||||
Assert.That(update.NewRuns.Length, Is.EqualTo(1));
|
||||
Assert.That(update.RemoveRuns.Length, Is.EqualTo(1));
|
||||
|
||||
Assert.That(update.RemoveRuns[0], Is.SameAs(run));
|
||||
Assert.That(update.NewRuns[0].Start, Is.EqualTo(1));
|
||||
Assert.That(update.NewRuns[0].Length, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanIterateIndices()
|
||||
{
|
||||
var run = new Run(2, 4);
|
||||
var seen = new List<int>();
|
||||
run.Iterate(i => seen.Add(i));
|
||||
|
||||
CollectionAssert.AreEqual(new[] { 2, 3, 4, 5 }, seen);
|
||||
}
|
||||
}
|
||||
|
||||
public class Run
|
||||
{
|
||||
public Run(int start, int length)
|
||||
{
|
||||
Start = start;
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public int Start { get; }
|
||||
public int Length { get; private set; }
|
||||
|
||||
public bool Includes(int index)
|
||||
{
|
||||
return index >= Start && index < (Start + Length);
|
||||
}
|
||||
|
||||
public bool ExpandToInclude(int index)
|
||||
{
|
||||
if (index == (Start + Length))
|
||||
{
|
||||
Length++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public RunUpdate Unset(int index)
|
||||
{
|
||||
if (!Includes(index))
|
||||
{
|
||||
return new RunUpdate();
|
||||
}
|
||||
|
||||
if (index == Start)
|
||||
{
|
||||
// First index: Replace self with new run at next index, unless empty.
|
||||
if (Length == 1)
|
||||
{
|
||||
return new RunUpdate(Array.Empty<Run>(), new[] { this });
|
||||
}
|
||||
return new RunUpdate(
|
||||
newRuns: new[] { new Run(Start + 1, Length - 1) },
|
||||
removeRuns: new[] { this }
|
||||
);
|
||||
}
|
||||
|
||||
if (index == (Start + Length - 1))
|
||||
{
|
||||
// Last index: Become one smaller.
|
||||
Length--;
|
||||
return new RunUpdate();
|
||||
}
|
||||
|
||||
// Split:
|
||||
var newRunLength = (Start + Length - 1) - index;
|
||||
Length = index - Start;
|
||||
return new RunUpdate(new[] { new Run(index + 1, newRunLength) }, Array.Empty<Run>());
|
||||
}
|
||||
|
||||
public void Iterate(Action<int> action)
|
||||
{
|
||||
for (var i = 0; i < Length; i++)
|
||||
{
|
||||
action(Start + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RunUpdate
|
||||
{
|
||||
public RunUpdate()
|
||||
: this(Array.Empty<Run>(), Array.Empty<Run>())
|
||||
{
|
||||
}
|
||||
|
||||
public RunUpdate(Run[] newRuns, Run[] removeRuns)
|
||||
{
|
||||
NewRuns = newRuns;
|
||||
RemoveRuns = removeRuns;
|
||||
}
|
||||
|
||||
public Run[] NewRuns { get; }
|
||||
public Run[] RemoveRuns { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
using Logging;
|
||||
using Microsoft.VisualStudio.TestPlatform.Common;
|
||||
using NuGet.Frameworks;
|
||||
using NUnit.Framework;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using Utils;
|
||||
|
||||
namespace FrameworkTests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class RunLengthEncodingTests
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
|
||||
[Test]
|
||||
public void EmptySet()
|
||||
{
|
||||
var set = new IndexSet();
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
Assert.That(set.IsSet(i), Is.False);
|
||||
}
|
||||
|
||||
var calls = 0;
|
||||
set.Iterate(i => calls++);
|
||||
Assert.That(calls, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetsIndex()
|
||||
{
|
||||
var set = new IndexSet();
|
||||
var index = 1234;
|
||||
set.Set(index);
|
||||
|
||||
Assert.That(set.IsSet(index), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnsetsIndex()
|
||||
{
|
||||
var set = new IndexSet();
|
||||
var index = 1234;
|
||||
set.Set(index);
|
||||
set.Unset(index);
|
||||
|
||||
Assert.That(set.IsSet(index), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RandomIndices()
|
||||
{
|
||||
var indices = GenerateRandomIndices();
|
||||
var set = new IndexSet(indices);
|
||||
|
||||
AssertEqual(set, indices);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RandomRunLengthEncoding()
|
||||
{
|
||||
var indices = GenerateRandomIndices();
|
||||
var set = new IndexSet(indices);
|
||||
|
||||
var encoded = set.RunLengthEncoded();
|
||||
var decoded = IndexSet.FromRunLengthEncoded(encoded);
|
||||
|
||||
AssertEqual(decoded, indices);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunLengthEncoding()
|
||||
{
|
||||
var indices = new[] { 0, 1, 2, 4, 6, 7 };
|
||||
var set = new IndexSet(indices);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
0, 3,
|
||||
4, 1,
|
||||
6, 2
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunLengthDecoding()
|
||||
{
|
||||
var encoded = new[]
|
||||
{
|
||||
2, 4, // 2, 3, 4, 5
|
||||
7, 1, // 7
|
||||
9, 2 // 9, 10
|
||||
};
|
||||
|
||||
var set = IndexSet.FromRunLengthEncoded(encoded);
|
||||
var seen = new List<int>();
|
||||
set.Iterate(i => seen.Add(i));
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
2, 3, 4, 5,
|
||||
7,
|
||||
9, 10
|
||||
}, seen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetIndexBeforeRun()
|
||||
{
|
||||
var set = new IndexSet(new[] { 12, 13, 14 });
|
||||
set.Set(11);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
11, 4
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetIndexAfterRun()
|
||||
{
|
||||
var set = new IndexSet(new[] { 12, 13, 14 });
|
||||
set.Set(15);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
12, 4
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnsetIndexAtStartOfRun()
|
||||
{
|
||||
var set = new IndexSet(new[] { 11, 12, 13, 14 });
|
||||
set.Unset(11);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
12, 3
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnsetIndexAtEndOfRun()
|
||||
{
|
||||
var set = new IndexSet(new[] { 11, 12, 13, 14 });
|
||||
set.Unset(14);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
11, 3
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnsetIndexInRun()
|
||||
{
|
||||
var set = new IndexSet(new[] { 11, 12, 13, 14 });
|
||||
set.Unset(12);
|
||||
var encoded = set.RunLengthEncoded();
|
||||
|
||||
CollectionAssert.AreEqual(new[]
|
||||
{
|
||||
11, 1,
|
||||
13, 2
|
||||
}, encoded);
|
||||
}
|
||||
|
||||
private void AssertEqual(IndexSet set, int[] indices)
|
||||
{
|
||||
var max = indices.Max() + 1;
|
||||
for (var i = 0; i < max; i++)
|
||||
{
|
||||
Assert.That(set.IsSet(i), Is.EqualTo(indices.Contains(i)));
|
||||
}
|
||||
|
||||
var seen = new List<int>();
|
||||
set.Iterate(i => seen.Add(i));
|
||||
|
||||
CollectionAssert.AreEqual(indices, seen);
|
||||
}
|
||||
|
||||
private int[] GenerateRandomIndices()
|
||||
{
|
||||
var number = 1000;
|
||||
var max = 2000;
|
||||
var all = Enumerable.Range(0, max).ToList();
|
||||
var result = new List<int>();
|
||||
|
||||
while (all.Any() && result.Count < number)
|
||||
{
|
||||
result.Add(all.PickOneRandom());
|
||||
}
|
||||
|
||||
all.Sort();
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
public class IndexSet
|
||||
{
|
||||
private readonly SortedList<int, Run> runs = new SortedList<int, Run>();
|
||||
|
||||
public IndexSet()
|
||||
{
|
||||
}
|
||||
|
||||
public IndexSet(int[] indices)
|
||||
{
|
||||
foreach (var i in indices) Set(i);
|
||||
}
|
||||
|
||||
public static IndexSet FromRunLengthEncoded(int[] rle)
|
||||
{
|
||||
var set = new IndexSet();
|
||||
for (var i = 0; i < rle.Length; i += 2)
|
||||
{
|
||||
var start = rle[i];
|
||||
var length = rle[i + 1];
|
||||
set.runs.Add(start, new Run(start, length));
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
public bool IsSet(int index)
|
||||
{
|
||||
if (runs.ContainsKey(index)) return true;
|
||||
|
||||
var run = GetRunBefore(index);
|
||||
if (run == null) return false;
|
||||
|
||||
return run.Includes(index);
|
||||
}
|
||||
|
||||
public void Set(int index)
|
||||
{
|
||||
if (runs.ContainsKey(index)) return;
|
||||
|
||||
var run = GetRunBefore(index);
|
||||
if (run == null || !run.ExpandToInclude(index))
|
||||
{
|
||||
CreateNewRun(index);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unset(int index)
|
||||
{
|
||||
if (runs.ContainsKey(index))
|
||||
{
|
||||
HandleUpdate(runs[index].Unset(index));
|
||||
}
|
||||
else
|
||||
{
|
||||
var run = GetRunBefore(index);
|
||||
if (run == null) return;
|
||||
HandleUpdate(run.Unset(index));
|
||||
}
|
||||
}
|
||||
|
||||
public void Iterate(Action<int> onIndex)
|
||||
{
|
||||
foreach (var run in runs.Values)
|
||||
{
|
||||
run.Iterate(onIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public int[] RunLengthEncoded()
|
||||
{
|
||||
return Encode().ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<int> Encode()
|
||||
{
|
||||
foreach (var pair in runs)
|
||||
{
|
||||
yield return pair.Value.Start;
|
||||
yield return pair.Value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private Run? GetRunBefore(int index)
|
||||
{
|
||||
Run? result = null;
|
||||
foreach (var pair in runs)
|
||||
{
|
||||
if (pair.Key < index) result = pair.Value;
|
||||
else return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void HandleUpdate(RunUpdate runUpdate)
|
||||
{
|
||||
foreach (var newRun in runUpdate.NewRuns) runs.Add(newRun.Start, newRun);
|
||||
foreach (var removeRun in runUpdate.RemoveRuns) runs.Remove(removeRun.Start);
|
||||
}
|
||||
|
||||
private void CreateNewRun(int index)
|
||||
{
|
||||
if (runs.ContainsKey(index + 1))
|
||||
{
|
||||
var length = runs[index + 1].Length + 1;
|
||||
runs.Add(index, new Run(index, length));
|
||||
runs.Remove(index + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
runs.Add(index, new Run(index, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,16 @@ namespace FrameworkTests.Utils
|
||||
TimeSpan.FromSeconds(28)
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Averaging()
|
||||
{
|
||||
var first = RollingAverage.GetNewAverage(0.0f, 1, 1.0f);
|
||||
Assert.That(first, Is.EqualTo(1.0f));
|
||||
|
||||
var fifth = RollingAverage.GetNewAverage(5.0f, 5, 0.0f);
|
||||
var expected = new[] { 5.0f, 5.0f, 5.0f, 5.0f, 0.0f }.Average();
|
||||
Assert.That(fifth, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ namespace AutoClient
|
||||
[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-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 6 hours)")]
|
||||
public int ContractDurationMinutes { get; set; } = 60 * 6;
|
||||
|
||||
[Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15)")]
|
||||
[Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15 minutes)")]
|
||||
public int ContractExpiryMinutes { get; set; } = 15;
|
||||
|
||||
[Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")]
|
||||
@@ -34,6 +34,9 @@ namespace AutoClient
|
||||
[Uniform("collateral", "c", "COLLATERAL", false, "Required collateral. (default 1)")]
|
||||
public int RequiredCollateral { get; set; } = 1;
|
||||
|
||||
[Uniform("filesizemb", "smb", "FILESIZEMB", false, "When greater than zero, size of file generated and uploaded. When zero, random images are used instead.")]
|
||||
public int FileSizeMb { get; set; } = 0;
|
||||
|
||||
public string LogPath
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
namespace AutoClient
|
||||
using FileUtils;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
namespace AutoClient
|
||||
{
|
||||
public class ImageGenerator
|
||||
public interface IFileGenerator
|
||||
{
|
||||
public async Task<string> GenerateImage()
|
||||
Task<string> Generate();
|
||||
}
|
||||
|
||||
public class ImageGenerator : IFileGenerator
|
||||
{
|
||||
private LogSplitter log;
|
||||
|
||||
public ImageGenerator(LogSplitter log)
|
||||
{
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public async Task<string> Generate()
|
||||
{
|
||||
log.Log("Fetching random image from picsum.photos...");
|
||||
var httpClient = new HttpClient();
|
||||
var thing = await httpClient.GetStreamAsync("https://picsum.photos/3840/2160");
|
||||
|
||||
@@ -14,4 +31,25 @@
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
public class RandomFileGenerator : IFileGenerator
|
||||
{
|
||||
private readonly ByteSize size;
|
||||
private readonly FileManager fileManager;
|
||||
|
||||
public RandomFileGenerator(Configuration config, ILog log)
|
||||
{
|
||||
size = config.FileSizeMb.MB();
|
||||
fileManager = new FileManager(log, config.DataPath);
|
||||
}
|
||||
|
||||
public Task<string> Generate()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var file = fileManager.GenerateFile(size);
|
||||
return file.Filename;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using AutoClient;
|
||||
using CodexOpenApi;
|
||||
using Core;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
@@ -25,14 +26,14 @@ public static class Program
|
||||
new ConsoleLog()
|
||||
);
|
||||
|
||||
var address = new Utils.Address(
|
||||
var address = new Address(
|
||||
host: config.CodexHost,
|
||||
port: config.CodexPort
|
||||
);
|
||||
|
||||
log.Log($"Start. Address: {address}");
|
||||
|
||||
var imgGenerator = new ImageGenerator();
|
||||
var generator = CreateGenerator(config, log);
|
||||
|
||||
var client = new HttpClient();
|
||||
var codex = new CodexApi(client);
|
||||
@@ -44,7 +45,7 @@ public static class Program
|
||||
for (var i = 0; i < config.NumConcurrentPurchases; i++)
|
||||
{
|
||||
purchasers.Add(
|
||||
new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, config, imgGenerator, cancellationToken)
|
||||
new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, config, generator, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,6 +61,15 @@ public static class Program
|
||||
log.Log("Done.");
|
||||
}
|
||||
|
||||
private static IFileGenerator CreateGenerator(Configuration config, LogSplitter log)
|
||||
{
|
||||
if (config.FileSizeMb > 0)
|
||||
{
|
||||
return new RandomFileGenerator(config, log);
|
||||
}
|
||||
return new ImageGenerator(log);
|
||||
}
|
||||
|
||||
private static async Task CheckCodex(CodexApi codex, ILog log)
|
||||
{
|
||||
log.Log("Checking Codex...");
|
||||
|
||||
@@ -13,10 +13,10 @@ namespace AutoClient
|
||||
private readonly Address address;
|
||||
private readonly CodexApi codex;
|
||||
private readonly Configuration config;
|
||||
private readonly ImageGenerator generator;
|
||||
private readonly IFileGenerator generator;
|
||||
private readonly CancellationToken ct;
|
||||
|
||||
public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, Configuration config, ImageGenerator generator, CancellationToken ct)
|
||||
public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, Configuration config, IFileGenerator generator, CancellationToken ct)
|
||||
{
|
||||
this.log = log;
|
||||
this.client = client;
|
||||
@@ -50,7 +50,7 @@ namespace AutoClient
|
||||
|
||||
private async Task<string> CreateFile()
|
||||
{
|
||||
return await generator.GenerateImage();
|
||||
return await generator.Generate();
|
||||
}
|
||||
|
||||
private async Task<ContentId> UploadFile(string filename)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Codex auto-client
|
||||
|
||||
This thing will generate files, upload them, and purchase storage for them in an endless loop.
|
||||
|
||||
Can generate random images or random data of a specified size.
|
||||
|
||||
## How to run
|
||||
|
||||
- dotnet 7.0 and CLI arguments: `dotnet run -- --codex-host=... --codex-port=...`
|
||||
- docker and env-vars: `codexstorage/codex-autoclient:sha-88daab3`
|
||||
|
||||
## Configuration options
|
||||
Options can be configured via CLI option or environment variable.
|
||||
|
||||
| CLI option | Environment variable | Description |
|
||||
|-------------------------|----------------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| "--codex-host" | "CODEXHOST" | Codex Host address. (default 'http://localhost') |
|
||||
| "--codex-port" | "CODEXPORT" | port number of Codex API. (8080 by default) |
|
||||
| "--datapath" | "DATAPATH" | Root path where all data files will be saved. |
|
||||
| "--purchases" | "PURCHASES" | Number of concurrent purchases. |
|
||||
| "--contract-duration" | "CONTRACTDURATION" | contract duration in minutes. (default 6 hours) |
|
||||
| "--contract-expiry" | "CONTRACTEXPIRY" | contract expiry in minutes. (default 15 minutes) |
|
||||
| "--num-hosts" | "NUMHOSTS" | Number of hosts for contract. (default 5) |
|
||||
| "--num-hosts-tolerance" | "NUMTOL" | Number of host tolerance for contract. (default 2) |
|
||||
| "--price" | "PRICE" | Price of contract. (default 10) |
|
||||
| "--collateral" | "COLLATERAL" | Required collateral. (default 1) |
|
||||
| "--filesizemb" | "FILESIZEMB" | When greater than zero, size of file generated and uploaded. When zero, random images are used instead. (default 0) |
|
||||
|
||||
## Timing
|
||||
|
||||
Configuration: `purchases` controls the number of concurrently running storage requests.
|
||||
Configuration: `contract-duration` controls the duration in minutes of each storage request.
|
||||
Auto-client will create a new storage request every X minutes, where X is the contract duration divided by the number of purchases.
|
||||
(Timing may start to vary when contracts fail or time out.)
|
||||
@@ -1,58 +0,0 @@
|
||||
using BiblioTech.Options;
|
||||
using DiscordRewards;
|
||||
using System.Globalization;
|
||||
using Utils;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class MarketCommand : BaseCommand
|
||||
{
|
||||
public override string Name => "market";
|
||||
public override string StartingMessage => RandomBusyMessage.Get();
|
||||
public override string Description => "Fetch some insights about current market conditions.";
|
||||
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
await context.Followup(GetInsights());
|
||||
}
|
||||
|
||||
private string[] GetInsights()
|
||||
{
|
||||
var result = Program.Averages.SelectMany(GetInsight).ToArray();
|
||||
if (result.Length > 0)
|
||||
{
|
||||
result = new[]
|
||||
{
|
||||
"No market insights available."
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string[] GetInsight(MarketAverage avg)
|
||||
{
|
||||
var timeRange = TimeSpan.FromSeconds(avg.TimeRangeSeconds);
|
||||
var headerLine = $"[Last {Time.FormatDuration(timeRange)}] ({avg.NumberOfFinished} Contracts finished)";
|
||||
|
||||
if (avg.NumberOfFinished == 0)
|
||||
{
|
||||
return new[] { headerLine };
|
||||
}
|
||||
|
||||
return new[]
|
||||
{
|
||||
headerLine,
|
||||
$"Price: {Format(avg.Price)}",
|
||||
$"Size: {Format(avg.Size)}",
|
||||
$"Duration: {Format(avg.Duration)}",
|
||||
$"Collateral: {Format(avg.Collateral)}",
|
||||
$"ProofProbability: {Format(avg.ProofProbability)}"
|
||||
};
|
||||
}
|
||||
|
||||
private string Format(float f)
|
||||
{
|
||||
return f.ToString("F3", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ namespace BiblioTech
|
||||
public static AdminChecker AdminChecker { get; private set; } = null!;
|
||||
public static IDiscordRoleDriver RoleDriver { get; set; } = null!;
|
||||
public static ILog Log { get; private set; } = null!;
|
||||
public static MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
|
||||
|
||||
public static Task Main(string[] args)
|
||||
{
|
||||
@@ -91,8 +90,7 @@ namespace BiblioTech
|
||||
sprCommand,
|
||||
associateCommand,
|
||||
notifyCommand,
|
||||
new AdminCommand(sprCommand, replacement),
|
||||
new MarketCommand()
|
||||
new AdminCommand(sprCommand, replacement)
|
||||
);
|
||||
|
||||
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
|
||||
|
||||
@@ -23,10 +23,6 @@ namespace BiblioTech.Rewards
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmd.Averages != null && cmd.Averages.Any())
|
||||
{
|
||||
Program.Averages = cmd.Averages;
|
||||
}
|
||||
await Program.RoleDriver.GiveRewards(cmd);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Logging;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class AppState
|
||||
{
|
||||
public AppState(Configuration config)
|
||||
{
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public bool Realtime { get; set; }
|
||||
public MarketOverview MarketOverview { get; set; } = new();
|
||||
public Configuration Config { get; }
|
||||
public ILog Log { get; } = new ConsoleLog();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using CodexContractsPlugin;
|
||||
using CodexContractsPlugin.ChainMonitor;
|
||||
using Nethereum.Model;
|
||||
using TestNetRewarder;
|
||||
using Utils;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class AverageHistory : ITimeSegmentHandler
|
||||
{
|
||||
private readonly List<MarketTimeSegment> contributions = new List<MarketTimeSegment>();
|
||||
private readonly ChainStateChangeHandlerMux mux = new ChainStateChangeHandlerMux();
|
||||
private readonly AppState appState;
|
||||
private readonly int maxContributions;
|
||||
private readonly ChainState chainState;
|
||||
|
||||
public AverageHistory(AppState appState, ICodexContracts contracts, int maxContributions)
|
||||
{
|
||||
this.appState = appState;
|
||||
this.maxContributions = maxContributions;
|
||||
chainState = new ChainState(appState.Log, contracts, mux, appState.Config.HistoryStartUtc);
|
||||
}
|
||||
|
||||
public MarketTimeSegment[] Segments { get; private set; } = Array.Empty<MarketTimeSegment>();
|
||||
|
||||
public Task OnNewSegment(TimeRange timeRange)
|
||||
{
|
||||
var contribution = BuildContribution(timeRange);
|
||||
contributions.Add(contribution);
|
||||
|
||||
while (contributions.Count > maxContributions)
|
||||
{
|
||||
contributions.RemoveAt(0);
|
||||
}
|
||||
|
||||
Segments = contributions.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private MarketTimeSegment BuildContribution(TimeRange timeRange)
|
||||
{
|
||||
var builder = new ContributionBuilder(timeRange);
|
||||
mux.Handlers.Add(builder);
|
||||
chainState.Update(timeRange.To);
|
||||
mux.Handlers.Remove(builder);
|
||||
return builder.GetSegment();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using ArgsUniform;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
[Uniform("interval-minutes", "im", "INTERVALMINUTES", true, "time in minutes between updates.")]
|
||||
public int UpdateIntervalMinutes { get; set; } = 10;
|
||||
|
||||
[Uniform("max-random-seconds", "mrs", "MAXRANDOMSECONDS", false, "maximum random number of seconds added to update delay.")]
|
||||
public int MaxRandomIntervalSeconds { get; set; } = 120;
|
||||
|
||||
[Uniform("check-history", "ch", "CHECKHISTORY", true, "Unix epoc timestamp of a moment in history on which processing begins. Should be 'launch of the testnet'.")]
|
||||
public int CheckHistoryTimestamp { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 6 = 1h
|
||||
/// 144 = 24h
|
||||
/// 2520 = 1 week
|
||||
/// 10080 = 4 weeks
|
||||
/// </summary>
|
||||
[Uniform("timesegments", "ts", "TIMESEGMENTS", false, "Semi-colon separated integers. Each represents a multiple of intervals, for which a market timesegment will be generated.")]
|
||||
public string TimeSegments { get; set; } = "6;144;2520;10080";
|
||||
|
||||
[Uniform("fullhistory", "fh", "FULLHISTORY", false, "When not zero, market timesegment for 'entire history' will be included.")]
|
||||
public int FullHistory { get; set; } = 1;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan UpdateInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return TimeSpan.FromMinutes(UpdateIntervalMinutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using CodexContractsPlugin.ChainMonitor;
|
||||
using GethPlugin;
|
||||
using System.Numerics;
|
||||
using Utils;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class ContributionBuilder : IChainStateChangeHandler
|
||||
{
|
||||
private readonly MarketTimeSegment segment = new MarketTimeSegment();
|
||||
|
||||
public ContributionBuilder(TimeRange timeRange)
|
||||
{
|
||||
segment = new MarketTimeSegment
|
||||
{
|
||||
FromUtc = timeRange.From,
|
||||
ToUtc = timeRange.To
|
||||
};
|
||||
}
|
||||
|
||||
public void OnNewRequest(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestToAverage(segment.Submitted, requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestCancelled(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestToAverage(segment.Expired, requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestToAverage(segment.Failed, requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestToAverage(segment.Finished, requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFulfilled(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestToAverage(segment.Started, requestEvent);
|
||||
}
|
||||
|
||||
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
|
||||
{
|
||||
}
|
||||
|
||||
public MarketTimeSegment GetSegment()
|
||||
{
|
||||
return segment;
|
||||
}
|
||||
|
||||
private void AddRequestToAverage(ContractAverages average, RequestEvent requestEvent)
|
||||
{
|
||||
average.Number++;
|
||||
average.Price = GetNewAverage(average.Price, average.Number, requestEvent.Request.Request.Ask.Reward);
|
||||
average.Size = GetNewAverage(average.Size, average.Number, requestEvent.Request.Request.Ask.SlotSize);
|
||||
average.Duration = GetNewAverage(average.Duration, average.Number, requestEvent.Request.Request.Ask.Duration);
|
||||
average.Collateral = GetNewAverage(average.Collateral, average.Number, requestEvent.Request.Request.Ask.Collateral);
|
||||
average.ProofProbability = GetNewAverage(average.ProofProbability, average.Number, requestEvent.Request.Request.Ask.ProofProbability);
|
||||
}
|
||||
|
||||
private float GetNewAverage(float currentAverage, int newNumberOfValues, BigInteger newValue)
|
||||
{
|
||||
return GetNewAverage(currentAverage, newNumberOfValues, (float)newValue);
|
||||
}
|
||||
|
||||
private float GetNewAverage(float currentAverage, int newNumberOfValues, float newValue)
|
||||
{
|
||||
return RollingAverage.GetNewAverage(currentAverage, newNumberOfValues, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MarketInsights.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class MarketController : ControllerBase
|
||||
{
|
||||
private readonly AppState appState;
|
||||
|
||||
public MarketController(AppState appState)
|
||||
{
|
||||
this.appState = appState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent market overview.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public MarketOverview Get()
|
||||
{
|
||||
return appState.MarketOverview;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER app
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Tools/MarketInsights/MarketInsights.csproj", "Tools/MarketInsights/"]
|
||||
RUN dotnet restore "./Tools/MarketInsights/MarketInsights.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Tools/MarketInsights"
|
||||
RUN dotnet build "./MarketInsights.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./MarketInsights.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MarketInsights.dll"]
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>ae71e621-bb16-41b2-b6f3-c597d2d21157</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
|
||||
<ProjectReference Include="..\TestNetRewarder\TestNetRewarder.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ActiveDebugProfile>Container (Dockerfile)</ActiveDebugProfile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@MarketInsights_HostAddress = http://localhost:5169
|
||||
|
||||
GET {{MarketInsights_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class MarketOverview
|
||||
{
|
||||
/// <summary>
|
||||
/// Moment when overview was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdatedUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When false, service is busy processing history in order to catch up to the present.
|
||||
/// </summary>
|
||||
public bool IsUpToDate { get; set; }
|
||||
|
||||
public MarketTimeSegment[] TimeSegments { get; set; } = Array.Empty<MarketTimeSegment>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Segment of time over which market statistics are available.
|
||||
/// </summary>
|
||||
public class MarketTimeSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// Start of time segment.
|
||||
/// </summary>
|
||||
public DateTime FromUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// End of time segment.
|
||||
/// </summary>
|
||||
public DateTime ToUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Averages over contracts that were submitted during this time segment.
|
||||
/// </summary>
|
||||
public ContractAverages Submitted { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Averages over contracts that expired during this time segment.
|
||||
/// </summary>
|
||||
public ContractAverages Expired { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Averages over contracts that started during this time segment.
|
||||
/// </summary>
|
||||
public ContractAverages Started { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Averages over contracts that finished (succesfully) during this time segment.
|
||||
/// </summary>
|
||||
public ContractAverages Finished { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Averages over contracts that failed during this time segment.
|
||||
/// </summary>
|
||||
public ContractAverages Failed { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ContractAverages
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of contracts.
|
||||
/// </summary>
|
||||
public int Number { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average price of contracts. (TSTWEI)
|
||||
/// </summary>
|
||||
public float Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average size of slots in contracts. (bytes)
|
||||
/// </summary>
|
||||
public float Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average duration of contracts. (seconds)
|
||||
/// </summary>
|
||||
public float Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average collateral of contracts. (TSTWEI)
|
||||
/// </summary>
|
||||
public float Collateral { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average proof probability of contracts.
|
||||
/// </summary>
|
||||
public float ProofProbability { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using ArgsUniform;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Nethereum.Model;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
var config = uniformArgs.Parse(true);
|
||||
var cts = new CancellationTokenSource();
|
||||
var appState = new AppState(config);
|
||||
|
||||
Console.CancelKeyPress += (s, e) =>
|
||||
{
|
||||
appState.Log.Log("Stopping...");
|
||||
cts.Cancel();
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
var connector = GethConnector.GethConnector.Initialize(appState.Log);
|
||||
if (connector == null) throw new Exception("Invalid Geth information");
|
||||
|
||||
var updater = new Updater(appState, connector.CodexContracts, cts.Token);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddSingleton(appState);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(s =>
|
||||
{
|
||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
s.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
updater.Run();
|
||||
app.Run();
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("WebAPI for generating market overview for Codex network. Comes with OpenAPI swagger endpoint.");
|
||||
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine($"Required environment variables: {nl}" +
|
||||
$"'GETH_HOST'{nl}",
|
||||
$"'GETH_HTTP_PORT'{nl}",
|
||||
$"'CODEXCONTRACTS_MARKETPLACEADDRESS'{nl}",
|
||||
$"'CODEXCONTRACTS_TOKENADDRESS'{nl}",
|
||||
$"'CODEXCONTRACTS_ABI'{nl}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5169"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7011;http://localhost:5169"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:36575",
|
||||
"sslPort": 44390
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using CodexContractsPlugin.ChainMonitor;
|
||||
using Utils;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class Tracker
|
||||
{
|
||||
private readonly AverageHistory history;
|
||||
|
||||
public Tracker(int numberOfSegments, AverageHistory history)
|
||||
{
|
||||
NumberOfSegments = numberOfSegments;
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
public int NumberOfSegments { get; }
|
||||
|
||||
public MarketTimeSegment? CreateMarketTimeSegment()
|
||||
{
|
||||
if (history.Segments.Length < NumberOfSegments) return null;
|
||||
|
||||
var mySegments = history.Segments.TakeLast(NumberOfSegments);
|
||||
return AverageSegments(mySegments);
|
||||
}
|
||||
|
||||
private MarketTimeSegment AverageSegments(IEnumerable<MarketTimeSegment> mySegments)
|
||||
{
|
||||
var result = new MarketTimeSegment();
|
||||
|
||||
foreach (var segment in mySegments)
|
||||
{
|
||||
result.FromUtc = Min(result.FromUtc, segment.FromUtc);
|
||||
result.ToUtc = Max(result.ToUtc, segment.ToUtc);
|
||||
|
||||
Combine(result.Submitted, segment.Submitted);
|
||||
Combine(result.Expired, segment.Expired);
|
||||
Combine(result.Started, segment.Started);
|
||||
Combine(result.Finished, segment.Finished);
|
||||
Combine(result.Failed, segment.Failed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Combine(ContractAverages result, ContractAverages toAdd)
|
||||
{
|
||||
float weight1 = result.Number;
|
||||
float weight2 = toAdd.Number;
|
||||
|
||||
result.Price = RollingAverage.GetWeightedAverage(result.Price, weight1, toAdd.Price, weight2);
|
||||
result.Size = RollingAverage.GetWeightedAverage(result.Size, weight1, toAdd.Size, weight2);
|
||||
result.Duration = RollingAverage.GetWeightedAverage(result.Duration, weight1, toAdd.Duration, weight2);
|
||||
result.Collateral = RollingAverage.GetWeightedAverage(result.Collateral, weight1, toAdd.Collateral, weight2);
|
||||
result.ProofProbability = RollingAverage.GetWeightedAverage(result.ProofProbability, weight1, toAdd.ProofProbability, weight2);
|
||||
}
|
||||
|
||||
private DateTime Max(DateTime a, DateTime b)
|
||||
{
|
||||
if (a > b) return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
private DateTime Min(DateTime a, DateTime b)
|
||||
{
|
||||
if (a > b) return b;
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using CodexContractsPlugin;
|
||||
using TestNetRewarder;
|
||||
|
||||
namespace MarketInsights
|
||||
{
|
||||
public class Updater
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
private readonly AppState appState;
|
||||
private readonly CancellationToken ct;
|
||||
private readonly Tracker[] trackers;
|
||||
private readonly AverageHistory averageHistory;
|
||||
|
||||
public Updater(AppState appState, ICodexContracts contracts, CancellationToken ct)
|
||||
{
|
||||
this.appState = appState;
|
||||
this.ct = ct;
|
||||
|
||||
trackers = CreateTrackers();
|
||||
averageHistory = new AverageHistory(appState, contracts, trackers.Max(t => t.NumberOfSegments));
|
||||
}
|
||||
|
||||
private Tracker[] CreateTrackers()
|
||||
{
|
||||
var tokens = appState.Config.TimeSegments.Split(";", StringSplitOptions.RemoveEmptyEntries);
|
||||
var nums = tokens.Select(t => Convert.ToInt32(t)).ToArray();
|
||||
return nums.Select(n => new Tracker(n, averageHistory)).ToArray();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
Task.Run(Runner);
|
||||
}
|
||||
|
||||
private async Task Runner()
|
||||
{
|
||||
var segmenter = new TimeSegmenter(
|
||||
appState.Log,
|
||||
segmentSize: appState.Config.UpdateInterval,
|
||||
historyStartUtc: appState.Config.HistoryStartUtc,
|
||||
handler: averageHistory
|
||||
);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await segmenter.ProcessNextSegment();
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), ct);
|
||||
|
||||
var marketTimeSegments = trackers
|
||||
.Select(t => t.CreateMarketTimeSegment())
|
||||
.Where(t => t != null)
|
||||
.Cast<MarketTimeSegment>()
|
||||
.ToArray();
|
||||
|
||||
appState.MarketOverview = new MarketOverview
|
||||
{
|
||||
TimeSegments = marketTimeSegments,
|
||||
IsUpToDate = segmenter.IsRealtime,
|
||||
LastUpdatedUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var r = random.Next(appState.Config.MaxRandomIntervalSeconds);
|
||||
await Task.Delay(TimeSpan.FromSeconds(r), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ namespace TestNetRewarder
|
||||
AddRequestBlock(requestEvent, "Cancelled");
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestBlock(requestEvent, "Failed");
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
AddRequestBlock(requestEvent, "Finished");
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using CodexContractsPlugin.ChainMonitor;
|
||||
using DiscordRewards;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using System.Numerics;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class MarketTracker : IChainStateChangeHandler
|
||||
{
|
||||
private readonly List<MarketBuffer> buffers = new List<MarketBuffer>();
|
||||
private readonly ILog log;
|
||||
|
||||
public MarketTracker(Configuration config, ILog log)
|
||||
{
|
||||
var intervals = GetInsightCounts(config);
|
||||
|
||||
foreach (var i in intervals)
|
||||
{
|
||||
buffers.Add(new MarketBuffer(
|
||||
config.Interval * i
|
||||
));
|
||||
}
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public MarketAverage[] GetAverages()
|
||||
{
|
||||
foreach (var b in buffers) b.Update();
|
||||
|
||||
return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast<MarketAverage>().ToArray();
|
||||
}
|
||||
|
||||
public void OnNewRequest(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
var tokens = config.MarketInsights.Split(';').ToArray();
|
||||
return tokens.Select(t => Convert.ToInt32(t)).ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
|
||||
}
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ namespace TestNetRewarder
|
||||
{
|
||||
private readonly RequestBuilder builder;
|
||||
private readonly RewardChecker rewardChecker;
|
||||
private readonly MarketTracker marketTracker;
|
||||
private readonly EventsFormatter eventsFormatter;
|
||||
private readonly ChainState chainState;
|
||||
private readonly BotClient client;
|
||||
@@ -22,12 +21,10 @@ namespace TestNetRewarder
|
||||
|
||||
builder = new RequestBuilder();
|
||||
rewardChecker = new RewardChecker(builder);
|
||||
marketTracker = new MarketTracker(config, log);
|
||||
eventsFormatter = new EventsFormatter();
|
||||
|
||||
var handler = new ChainChangeMux(
|
||||
var handler = new ChainStateChangeHandlerMux(
|
||||
rewardChecker.Handler,
|
||||
marketTracker,
|
||||
eventsFormatter
|
||||
);
|
||||
|
||||
@@ -40,10 +37,9 @@ namespace TestNetRewarder
|
||||
{
|
||||
chainState.Update(timeRange.To);
|
||||
|
||||
var averages = marketTracker.GetAverages();
|
||||
var events = eventsFormatter.GetEvents();
|
||||
|
||||
var request = builder.Build(averages, events);
|
||||
var request = builder.Build(events);
|
||||
if (request.HasAny())
|
||||
{
|
||||
await client.SendRewards(request);
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace TestNetRewarder
|
||||
EnsureGethOnline();
|
||||
|
||||
Log.Log("Starting TestNet Rewarder...");
|
||||
var segmenter = new TimeSegmenter(Log, Config, processor);
|
||||
var segmenter = new TimeSegmenter(Log, Config.Interval, Config.HistoryStartUtc, processor);
|
||||
|
||||
while (!CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace TestNetRewarder
|
||||
}
|
||||
}
|
||||
|
||||
public GiveRewardsCommand Build(MarketAverage[] marketAverages, string[] lines)
|
||||
public GiveRewardsCommand Build(string[] lines)
|
||||
{
|
||||
var result = new GiveRewardsCommand
|
||||
{
|
||||
@@ -28,7 +28,6 @@ namespace TestNetRewarder
|
||||
RewardId = p.Key,
|
||||
UserAddresses = p.Value.Select(v => v.Address).ToArray()
|
||||
}).ToArray(),
|
||||
Averages = marketAverages,
|
||||
EventsOverview = lines
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace TestNetRewarder
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
if (MeetsRequirements(CheckType.HostFinishedSlot, requestEvent))
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace TestNetRewarder
|
||||
{
|
||||
var repo = new RewardRepo();
|
||||
var checks = repo.Rewards.Select(r => new RewardCheck(r, giver)).ToArray();
|
||||
Handler = new ChainChangeMux(checks);
|
||||
Handler = new ChainStateChangeHandlerMux(checks);
|
||||
}
|
||||
|
||||
public IChainStateChangeHandler Handler { get; }
|
||||
|
||||
@@ -15,28 +15,28 @@ namespace TestNetRewarder
|
||||
private readonly TimeSpan segmentSize;
|
||||
private DateTime latest;
|
||||
|
||||
public TimeSegmenter(ILog log, Configuration configuration, ITimeSegmentHandler handler)
|
||||
public TimeSegmenter(ILog log, TimeSpan segmentSize, DateTime historyStartUtc, ITimeSegmentHandler handler)
|
||||
{
|
||||
this.log = log;
|
||||
this.handler = handler;
|
||||
if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1;
|
||||
|
||||
segmentSize = configuration.Interval;
|
||||
latest = configuration.HistoryStartUtc;
|
||||
this.segmentSize = segmentSize;
|
||||
latest = historyStartUtc;
|
||||
|
||||
log.Log("Starting time segments at " + latest);
|
||||
log.Log("Segment size: " + Time.FormatDuration(segmentSize));
|
||||
}
|
||||
|
||||
public bool IsRealtime { get; private set; } = false;
|
||||
|
||||
public async Task ProcessNextSegment()
|
||||
{
|
||||
var end = latest + segmentSize;
|
||||
var waited = await WaitUntilTimeSegmentInPast(end);
|
||||
IsRealtime = await WaitUntilTimeSegmentInPast(end);
|
||||
|
||||
if (Program.CancellationToken.IsCancellationRequested) return;
|
||||
|
||||
var postfix = "(Catching up...)";
|
||||
if (waited) postfix = "(Real-time)";
|
||||
if (IsRealtime) postfix = "(Real-time)";
|
||||
log.Log($"Time segment [{latest} to {end}] {postfix}");
|
||||
|
||||
var range = new TimeRange(latest, end);
|
||||
|
||||
@@ -74,6 +74,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OverwatchTranscript", "Fram
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranscriptAnalysis", "Tools\TranscriptAnalysis\TranscriptAnalysis.csproj", "{C0EEBD32-23CB-45EC-A863-79FB948508C8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarketInsights", "Tools\MarketInsights\MarketInsights.csproj", "{004614DF-1C65-45E3-882D-59AE44282573}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -196,6 +198,10 @@ Global
|
||||
{C0EEBD32-23CB-45EC-A863-79FB948508C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0EEBD32-23CB-45EC-A863-79FB948508C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0EEBD32-23CB-45EC-A863-79FB948508C8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{004614DF-1C65-45E3-882D-59AE44282573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{004614DF-1C65-45E3-882D-59AE44282573}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{004614DF-1C65-45E3-882D-59AE44282573}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{004614DF-1C65-45E3-882D-59AE44282573}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -230,6 +236,7 @@ Global
|
||||
{B57A4789-D8EF-42E0-8D20-581C4057FFD3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
{870DDFBE-D7ED-4196-9681-13CA947BDEA6} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
|
||||
{C0EEBD32-23CB-45EC-A863-79FB948508C8} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
{004614DF-1C65-45E3-882D-59AE44282573} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}
|
||||
|
||||
Reference in New Issue
Block a user