2
0
mirror of synced 2025-02-23 13:38:07 +00:00

Merge branch 'feature/bot-upgrade'

This commit is contained in:
benbierens 2024-01-31 11:31:52 -05:00
commit 8a4d9fecea
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
52 changed files with 2451 additions and 116 deletions

26
.github/workflows/docker-rewarder.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Docker - Rewarder Bot
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/TestNetRewarder/**'
- '!Tools/TestNetRewarder/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-rewarder.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/TestNetRewarder/docker/Dockerfile
docker_repo: codexstorage/codex-rewarderbot
secrets: inherit

View File

@ -0,0 +1,21 @@
using Utils;
namespace DiscordRewards
{
public class CheckConfig
{
public CheckType Type { get; set; }
public ulong MinNumberOfHosts { get; set; }
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
}
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
namespace DiscordRewards
{
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
}
public class RewardUsersCommand
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
}

View File

@ -0,0 +1,18 @@
namespace DiscordRewards
{
public class RewardConfig
{
public const string UsernameTag = "<USER>";
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
{
RoleId = roleId;
Message = message;
CheckConfig = checkConfig;
}
public ulong RoleId { get; }
public string Message { get; }
public CheckConfig CheckConfig { get; }
}
}

View File

@ -0,0 +1,53 @@
using Utils;
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
}),
// Finished any slot
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
{
Type = CheckType.FinishedSlot
}),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
}),
// Posted any contract
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
{
Type = CheckType.PostedContract
}),
// Started any contract
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.StartedContract
}),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinNumberOfHosts = 4,
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
})
};
}
}

View File

@ -0,0 +1,39 @@
using CodexContractsPlugin;
using GethPlugin;
using Logging;
namespace GethConnector
{
public class GethConnector
{
public IGethNode GethNode { get; }
public ICodexContracts CodexContracts { get; }
public static GethConnector? Initialize(ILog log)
{
if (!string.IsNullOrEmpty(GethInput.LoadError))
{
var msg = "Geth input incorrect: " + GethInput.LoadError;
log.Error(msg);
return null;
}
var contractsDeployment = new CodexContractsDeployment(
marketplaceAddress: GethInput.MarketplaceAddress,
abi: GethInput.ABI,
tokenAddress: GethInput.TokenAddress
);
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
return new GethConnector(gethNode, contracts);
}
private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
{
GethNode = gethNode;
CodexContracts = codexContracts;
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
namespace GethConnector
{
public static class GethInput
{
private const string GethHostVar = "GETH_HOST";
private const string GethPortVar = "GETH_HTTP_PORT";
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
private const string AbiVar = "CODEXCONTRACTS_ABI";
static GethInput()
{
var error = new List<string>();
var gethHost = GetEnvVar(error, GethHostVar);
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
var privateKey = GetEnvVar(error, GethPrivKeyVar);
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
var tokenAddress = GetEnvVar(error, TokenAddressVar);
var abi = GetEnvVar(error, AbiVar);
if (error.Any())
{
LoadError = string.Join(", ", error);
}
else
{
GethHost = gethHost!;
GethPort = gethPort;
PrivateKey = privateKey!;
MarketplaceAddress = marketplaceAddress!;
TokenAddress = tokenAddress!;
ABI = abi!;
}
}
public static string GethHost { get; } = string.Empty;
public static int GethPort { get; }
public static string PrivateKey { get; } = string.Empty;
public static string MarketplaceAddress { get; } = string.Empty;
public static string TokenAddress { get; } = string.Empty;
public static string ABI { get; } = string.Empty;
public static string LoadError { get; } = string.Empty;
private static string? GetEnvVar(List<string> error, string name)
{
var result = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
return result;
}
}
}

View File

@ -0,0 +1,22 @@
namespace NethereumWorkflow
{
public partial class BlockTimeFinder
{
public class BlockTimeEntry
{
public BlockTimeEntry(ulong blockNumber, DateTime utc)
{
BlockNumber = blockNumber;
Utc = utc;
}
public ulong BlockNumber { get; }
public DateTime Utc { get; }
public override string ToString()
{
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
}
}
}
}

View File

@ -0,0 +1,280 @@
using Logging;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using Utils;
namespace NethereumWorkflow
{
public partial class BlockTimeFinder
{
private const ulong FetchRange = 6;
private const int MaxEntries = 1024;
private static readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
private readonly Web3 web3;
private readonly ILog log;
public BlockTimeFinder(Web3 web3, ILog log)
{
this.web3 = web3;
this.log = log;
}
public ulong GetHighestBlockNumberBefore(DateTime moment)
{
log.Log("Looking for highest block before " + moment.ToString("o"));
AssertMomentIsInPast(moment);
Initialize();
return GetHighestBlockBefore(moment);
}
public ulong GetLowestBlockNumberAfter(DateTime moment)
{
log.Log("Looking for lowest block after " + moment.ToString("o"));
AssertMomentIsInPast(moment);
Initialize();
return GetLowestBlockAfter(moment);
}
private ulong GetHighestBlockBefore(DateTime moment)
{
var closestBefore = FindClosestBeforeEntry(moment);
var closestAfter = FindClosestAfterEntry(moment);
if (closestBefore != null &&
closestAfter != null &&
closestBefore.Utc < moment &&
closestAfter.Utc > moment &&
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
{
log.Log("Found highest-Before: " + closestBefore);
return closestBefore.BlockNumber;
}
FetchBlocksAround(moment);
return GetHighestBlockBefore(moment);
}
private ulong GetLowestBlockAfter(DateTime moment)
{
var closestBefore = FindClosestBeforeEntry(moment);
var closestAfter = FindClosestAfterEntry(moment);
if (closestBefore != null &&
closestAfter != null &&
closestBefore.Utc < moment &&
closestAfter.Utc > moment &&
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
{
log.Log("Found lowest-after: " + closestAfter);
return closestAfter.BlockNumber;
}
FetchBlocksAround(moment);
return GetLowestBlockAfter(moment);
}
private void FetchBlocksAround(DateTime moment)
{
var timePerBlock = EstimateTimePerBlock();
log.Debug("Fetching blocks around " + moment.ToString("o") + " timePerBlock: " + timePerBlock.TotalSeconds);
EnsureRecentBlockIfNecessary(moment, timePerBlock);
var max = entries.Keys.Max();
var blockDifference = CalculateBlockDifference(moment, timePerBlock, max);
FetchUp(max, blockDifference);
FetchDown(max, blockDifference);
}
private void FetchDown(ulong max, ulong blockDifference)
{
var target = max - blockDifference - 1;
var fetchDown = FetchRange;
while (fetchDown > 0)
{
if (!entries.ContainsKey(target))
{
var newBlock = AddBlockNumber(target);
if (newBlock == null) return;
fetchDown--;
}
target--;
if (target <= 0) return;
}
}
private void FetchUp(ulong max, ulong blockDifference)
{
var target = max - blockDifference;
var fetchUp = FetchRange;
while (fetchUp > 0)
{
if (!entries.ContainsKey(target))
{
var newBlock = AddBlockNumber(target);
if (newBlock == null) return;
fetchUp--;
}
target++;
if (target >= max) return;
}
}
private ulong CalculateBlockDifference(DateTime moment, TimeSpan timePerBlock, ulong max)
{
var latest = entries[max];
var timeDifference = latest.Utc - moment;
double secondsDifference = Math.Abs(timeDifference.TotalSeconds);
double secondsPerBlock = timePerBlock.TotalSeconds;
double numberOfBlocksDifference = secondsDifference / secondsPerBlock;
var blockDifference = Convert.ToUInt64(numberOfBlocksDifference);
if (blockDifference < 1) blockDifference = 1;
return blockDifference;
}
private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock)
{
var max = entries.Keys.Max();
var latest = entries[max];
var maxRetry = 10;
while (moment > latest.Utc)
{
var newBlock = AddCurrentBlock();
if (newBlock == null || newBlock.BlockNumber == latest.BlockNumber)
{
maxRetry--;
if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries.");
Thread.Sleep(timePerBlock);
}
max = entries.Keys.Max();
latest = entries[max];
}
}
private BlockTimeEntry? AddBlockNumber(decimal blockNumber)
{
return AddBlockNumber(Convert.ToUInt64(blockNumber));
}
private BlockTimeEntry? AddBlockNumber(ulong blockNumber)
{
if (entries.ContainsKey(blockNumber))
{
return entries[blockNumber];
}
if (entries.Count > MaxEntries)
{
log.Debug("Entries cleared!");
entries.Clear();
Initialize();
}
var time = GetTimestampFromBlock(blockNumber);
if (time == null)
{
log.Log("Failed to get block for number: " + blockNumber);
return null;
}
var entry = new BlockTimeEntry(blockNumber, time.Value);
log.Debug("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o"));
entries.Add(blockNumber, entry);
return entry;
}
private TimeSpan EstimateTimePerBlock()
{
var min = entries.Keys.Min();
var max = entries.Keys.Max();
var clippedMin = Math.Max(max - 100, min);
var minTime = entries[min].Utc;
var clippedMinBlock = AddBlockNumber(clippedMin);
if (clippedMinBlock != null) minTime = clippedMinBlock.Utc;
var maxTime = entries[max].Utc;
var elapsedTime = maxTime - minTime;
double elapsedSeconds = elapsedTime.TotalSeconds;
double numberOfBlocks = max - min;
double secondsPerBlock = elapsedSeconds / numberOfBlocks;
var result = TimeSpan.FromSeconds(secondsPerBlock);
if (result.TotalSeconds < 1.0) result = TimeSpan.FromSeconds(1.0);
return result;
}
private void Initialize()
{
if (!entries.Any())
{
AddCurrentBlock();
AddBlockNumber(entries.Single().Key - 1);
}
}
private static void AssertMomentIsInPast(DateTime moment)
{
if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past.");
}
private BlockTimeEntry? AddCurrentBlock()
{
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
var blockNumber = number.ToDecimal();
return AddBlockNumber(blockNumber);
}
private DateTime? GetTimestampFromBlock(ulong blockNumber)
{
try
{
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
if (block == null) return null;
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
}
catch (Exception ex)
{
int i = 0;
throw;
}
}
private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment)
{
BlockTimeEntry? result = null;
foreach (var entry in entries.Values)
{
if (result == null)
{
if (entry.Utc < moment) result = entry;
}
else
{
if (entry.Utc > result.Utc && entry.Utc < moment) result = entry;
}
}
return result;
}
private BlockTimeEntry? FindClosestAfterEntry(DateTime moment)
{
BlockTimeEntry? result = null;
foreach (var entry in entries.Values)
{
if (result == null)
{
if (entry.Utc > moment) result = entry;
}
else
{
if (entry.Utc < result.Utc && entry.Utc > moment) result = entry;
}
}
return result;
}
}
}

View File

@ -1,7 +1,9 @@
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using System.Runtime.CompilerServices;
using Utils;
namespace NethereumWorkflow
@ -54,6 +56,12 @@ namespace NethereumWorkflow
return receipt.TransactionHash;
}
public Transaction GetTransaction(string transactionHash)
{
log.Debug();
return Time.Wait(web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash));
}
public decimal? GetSyncedBlockNumber()
{
log.Debug();
@ -77,5 +85,24 @@ namespace NethereumWorkflow
return false;
}
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
{
var blockTimeFinder = new BlockTimeFinder(web3, log);
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
return GetEvents<TEvent>(address, fromBlock, toBlock);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
{
var eventHandler = web3.Eth.GetEvent<TEvent>(address);
var from = new BlockParameter(fromBlockNumber);
var to = new BlockParameter(toBlockNumber);
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
}
}
}

View File

@ -1,4 +1,4 @@
namespace ContinuousTests
namespace Utils
{
public class TaskFactory
{

View File

@ -13,6 +13,11 @@
return task.Result;
}
public static void Wait(Task task)
{
task.Wait();
}
public static string FormatDuration(TimeSpan d)
{
var result = "";

View File

@ -0,0 +1,24 @@
namespace Utils
{
public class TimeRange
{
public TimeRange(DateTime from, DateTime to)
{
if (from < to)
{
From = from;
To = to;
}
else
{
From = to;
To = from;
}
Duration = To - From;
}
public DateTime From { get; }
public DateTime To { get; }
public TimeSpan Duration { get; }
}
}

View File

@ -1,5 +1,11 @@
using GethPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using Utils;
namespace CodexContractsPlugin
{
@ -12,6 +18,23 @@ namespace CodexContractsPlugin
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(TimeRange timeRange);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange);
SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange);
SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange);
}
public enum RequestState
{
New,
Started,
Cancelled,
Finished,
Failed
}
public class CodexContractsAccess : ICodexContracts
@ -30,8 +53,7 @@ namespace CodexContractsPlugin
public bool IsDeployed()
{
var interaction = new ContractInteractions(log, gethNode);
return !string.IsNullOrEmpty(interaction.GetTokenName(Deployment.TokenAddress));
return !string.IsNullOrEmpty(StartInteraction().GetTokenName(Deployment.TokenAddress));
}
public string MintTestTokens(IHasEthAddress owner, TestToken testTokens)
@ -41,8 +63,7 @@ namespace CodexContractsPlugin
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
{
var interaction = new ContractInteractions(log, gethNode);
return interaction.MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
}
public TestToken GetTestTokenBalance(IHasEthAddress owner)
@ -52,9 +73,108 @@ namespace CodexContractsPlugin
public TestToken GetTestTokenBalance(EthAddress ethAddress)
{
var interaction = new ContractInteractions(log, gethNode);
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
return balance.TestTokens();
}
public Request[] GetStorageRequests(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, timeRange);
var i = StartInteraction();
return events
.Select(e =>
{
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.BlockNumber = e.Log.BlockNumber.ToUlong();
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.BlockNumber = e.Log.BlockNumber.ToUlong();
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
{
var encoder = new ABIEncode();
var encoded = encoder.GetABIEncoded(
new ABIValue("bytes32", storageRequest.RequestId),
new ABIValue("uint256", slotIndex.ToBig())
);
var hashed = Sha3Keccack.Current.CalculateHash(encoded);
var func = new GetHostFunction
{
SlotId = hashed
};
var address = gethNode.Call<GetHostFunction, string>(Deployment.MarketplaceAddress, func);
if (string.IsNullOrEmpty(address)) return null;
return new EthAddress(address);
}
public RequestState GetRequestState(Request request)
{
var func = new RequestStateFunction
{
RequestId = request.RequestId
};
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);
}
}
}

View File

@ -1,7 +1,9 @@
using GethPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.Hex.HexConvertors.Extensions;
using NethereumWorkflow;
using System.Numerics;
@ -59,6 +61,17 @@ namespace CodexContractsPlugin
return gethNode.Call<GetTokenBalanceFunction, BigInteger>(tokenAddress, function).ToDecimal();
}
public GetRequestOutputDTO GetRequest(string marketplaceAddress, byte[] requestId)
{
log.Debug($"({marketplaceAddress}) {requestId.ToHex(true)}");
var func = new GetRequestFunction
{
RequestId = requestId
};
return gethNode.Call<GetRequestFunction, GetRequestOutputDTO>(marketplaceAddress, func);
}
public bool IsSynced(string marketplaceAddress, string marketplaceAbi)
{
log.Debug();

View File

@ -0,0 +1,35 @@
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
using GethPlugin;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
{
public ulong BlockNumber { get; set; }
public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
}
public partial class RequestFulfilledEventDTO
{
public ulong BlockNumber { get; set; }
}
public partial class RequestCancelledEventDTO
{
public ulong BlockNumber { get; set; }
}
public partial class SlotFilledEventDTO
{
public ulong BlockNumber { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
{
public ulong BlockNumber { get; set; }
}
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

View File

@ -0,0 +1,517 @@
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using System.Numerics;
// Generated code, do not modify.
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
namespace CodexContractsPlugin.Marketplace
{
public partial class ConfigFunction : ConfigFunctionBase { }
[Function("config", typeof(ConfigOutputDTO))]
public class ConfigFunctionBase : FunctionMessage
{
}
public partial class FillSlotFunction : FillSlotFunctionBase { }
[Function("fillSlot")]
public class FillSlotFunctionBase : FunctionMessage
{
[Parameter("bytes32", "requestId", 1)]
public virtual byte[] RequestId { get; set; }
[Parameter("uint256", "slotIndex", 2)]
public virtual BigInteger SlotIndex { get; set; }
[Parameter("bytes", "proof", 3)]
public virtual byte[] Proof { get; set; }
}
public partial class FreeSlotFunction : FreeSlotFunctionBase { }
[Function("freeSlot")]
public class FreeSlotFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
}
public partial class GetActiveSlotFunction : GetActiveSlotFunctionBase { }
[Function("getActiveSlot", typeof(GetActiveSlotOutputDTO))]
public class GetActiveSlotFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
}
public partial class GetChallengeFunction : GetChallengeFunctionBase { }
[Function("getChallenge", "bytes32")]
public class GetChallengeFunctionBase : FunctionMessage
{
[Parameter("bytes32", "id", 1)]
public virtual byte[] Id { get; set; }
}
public partial class GetHostFunction : GetHostFunctionBase { }
[Function("getHost", "address")]
public class GetHostFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
}
public partial class GetPointerFunction : GetPointerFunctionBase { }
[Function("getPointer", "uint8")]
public class GetPointerFunctionBase : FunctionMessage
{
[Parameter("bytes32", "id", 1)]
public virtual byte[] Id { get; set; }
}
public partial class GetRequestFunction : GetRequestFunctionBase { }
[Function("getRequest", typeof(GetRequestOutputDTO))]
public class GetRequestFunctionBase : FunctionMessage
{
[Parameter("bytes32", "requestId", 1)]
public virtual byte[] RequestId { get; set; }
}
public partial class IsProofRequiredFunction : IsProofRequiredFunctionBase { }
[Function("isProofRequired", "bool")]
public class IsProofRequiredFunctionBase : FunctionMessage
{
[Parameter("bytes32", "id", 1)]
public virtual byte[] Id { get; set; }
}
public partial class MarkProofAsMissingFunction : MarkProofAsMissingFunctionBase { }
[Function("markProofAsMissing")]
public class MarkProofAsMissingFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
[Parameter("uint256", "period", 2)]
public virtual BigInteger Period { get; set; }
}
public partial class MissingProofsFunction : MissingProofsFunctionBase { }
[Function("missingProofs", "uint256")]
public class MissingProofsFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
}
public partial class MyRequestsFunction : MyRequestsFunctionBase { }
[Function("myRequests", "bytes32[]")]
public class MyRequestsFunctionBase : FunctionMessage
{
}
public partial class MySlotsFunction : MySlotsFunctionBase { }
[Function("mySlots", "bytes32[]")]
public class MySlotsFunctionBase : FunctionMessage
{
}
public partial class RequestEndFunction : RequestEndFunctionBase { }
[Function("requestEnd", "uint256")]
public class RequestEndFunctionBase : FunctionMessage
{
[Parameter("bytes32", "requestId", 1)]
public virtual byte[] RequestId { get; set; }
}
public partial class RequestStateFunction : RequestStateFunctionBase { }
[Function("requestState", "uint8")]
public class RequestStateFunctionBase : FunctionMessage
{
[Parameter("bytes32", "requestId", 1)]
public virtual byte[] RequestId { get; set; }
}
public partial class RequestStorageFunction : RequestStorageFunctionBase { }
[Function("requestStorage")]
public class RequestStorageFunctionBase : FunctionMessage
{
[Parameter("tuple", "request", 1)]
public virtual Request Request { get; set; }
}
public partial class SlotStateFunction : SlotStateFunctionBase { }
[Function("slotState", "uint8")]
public class SlotStateFunctionBase : FunctionMessage
{
[Parameter("bytes32", "slotId", 1)]
public virtual byte[] SlotId { get; set; }
}
public partial class SubmitProofFunction : SubmitProofFunctionBase { }
[Function("submitProof")]
public class SubmitProofFunctionBase : FunctionMessage
{
[Parameter("bytes32", "id", 1)]
public virtual byte[] Id { get; set; }
[Parameter("bytes", "proof", 2)]
public virtual byte[] Proof { get; set; }
}
public partial class TokenFunction : TokenFunctionBase { }
[Function("token", "address")]
public class TokenFunctionBase : FunctionMessage
{
}
public partial class WillProofBeRequiredFunction : WillProofBeRequiredFunctionBase { }
[Function("willProofBeRequired", "bool")]
public class WillProofBeRequiredFunctionBase : FunctionMessage
{
[Parameter("bytes32", "id", 1)]
public virtual byte[] Id { get; set; }
}
public partial class WithdrawFundsFunction : WithdrawFundsFunctionBase { }
[Function("withdrawFunds")]
public class WithdrawFundsFunctionBase : FunctionMessage
{
[Parameter("bytes32", "requestId", 1)]
public virtual byte[] RequestId { get; set; }
}
public partial class ProofSubmittedEventDTO : ProofSubmittedEventDTOBase { }
[Event("ProofSubmitted")]
public class ProofSubmittedEventDTOBase : IEventDTO
{
[Parameter("bytes32", "id", 1, false)]
public virtual byte[] Id { get; set; }
[Parameter("bytes", "proof", 2, false)]
public virtual byte[] Proof { get; set; }
}
public partial class RequestCancelledEventDTO : RequestCancelledEventDTOBase { }
[Event("RequestCancelled")]
public class RequestCancelledEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, true)]
public virtual byte[] RequestId { get; set; }
}
public partial class RequestFailedEventDTO : RequestFailedEventDTOBase { }
[Event("RequestFailed")]
public class RequestFailedEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, true)]
public virtual byte[] RequestId { get; set; }
}
public partial class RequestFulfilledEventDTO : RequestFulfilledEventDTOBase { }
[Event("RequestFulfilled")]
public class RequestFulfilledEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, true)]
public virtual byte[] RequestId { get; set; }
}
public partial class SlotFilledEventDTO : SlotFilledEventDTOBase { }
[Event("SlotFilled")]
public class SlotFilledEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, true)]
public virtual byte[] RequestId { get; set; }
[Parameter("uint256", "slotIndex", 2, false)]
public virtual BigInteger SlotIndex { get; set; }
}
public partial class SlotFreedEventDTO : SlotFreedEventDTOBase { }
[Event("SlotFreed")]
public class SlotFreedEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, true)]
public virtual byte[] RequestId { get; set; }
[Parameter("uint256", "slotIndex", 2, false)]
public virtual BigInteger SlotIndex { get; set; }
}
public partial class StorageRequestedEventDTO : StorageRequestedEventDTOBase { }
[Event("StorageRequested")]
public class StorageRequestedEventDTOBase : IEventDTO
{
[Parameter("bytes32", "requestId", 1, false)]
public virtual byte[] RequestId { get; set; }
[Parameter("tuple", "ask", 2, false)]
public virtual Ask Ask { get; set; }
[Parameter("uint256", "expiry", 3, false)]
public virtual BigInteger Expiry { get; set; }
}
public partial class ConfigOutputDTO : ConfigOutputDTOBase { }
[FunctionOutput]
public class ConfigOutputDTOBase : IFunctionOutputDTO
{
[Parameter("tuple", "collateral", 1)]
public virtual CollateralConfig Collateral { get; set; }
[Parameter("tuple", "proofs", 2)]
public virtual ProofConfig Proofs { get; set; }
}
public partial class GetActiveSlotOutputDTO : GetActiveSlotOutputDTOBase { }
[FunctionOutput]
public class GetActiveSlotOutputDTOBase : IFunctionOutputDTO
{
[Parameter("tuple", "", 1)]
public virtual ActiveSlot ReturnValue1 { get; set; }
}
public partial class GetChallengeOutputDTO : GetChallengeOutputDTOBase { }
[FunctionOutput]
public class GetChallengeOutputDTOBase : IFunctionOutputDTO
{
[Parameter("bytes32", "", 1)]
public virtual byte[] ReturnValue1 { get; set; }
}
public partial class GetHostOutputDTO : GetHostOutputDTOBase { }
[FunctionOutput]
public class GetHostOutputDTOBase : IFunctionOutputDTO
{
[Parameter("address", "", 1)]
public virtual string ReturnValue1 { get; set; }
}
public partial class GetPointerOutputDTO : GetPointerOutputDTOBase { }
[FunctionOutput]
public class GetPointerOutputDTOBase : IFunctionOutputDTO
{
[Parameter("uint8", "", 1)]
public virtual byte ReturnValue1 { get; set; }
}
public partial class GetRequestOutputDTO : GetRequestOutputDTOBase { }
[FunctionOutput]
public class GetRequestOutputDTOBase : IFunctionOutputDTO
{
[Parameter("tuple", "", 1)]
public virtual Request ReturnValue1 { get; set; }
}
public partial class IsProofRequiredOutputDTO : IsProofRequiredOutputDTOBase { }
[FunctionOutput]
public class IsProofRequiredOutputDTOBase : IFunctionOutputDTO
{
[Parameter("bool", "", 1)]
public virtual bool ReturnValue1 { get; set; }
}
public partial class MissingProofsOutputDTO : MissingProofsOutputDTOBase { }
[FunctionOutput]
public class MissingProofsOutputDTOBase : IFunctionOutputDTO
{
[Parameter("uint256", "", 1)]
public virtual BigInteger ReturnValue1 { get; set; }
}
public partial class MyRequestsOutputDTO : MyRequestsOutputDTOBase { }
[FunctionOutput]
public class MyRequestsOutputDTOBase : IFunctionOutputDTO
{
[Parameter("bytes32[]", "", 1)]
public virtual List<byte[]> ReturnValue1 { get; set; }
}
public partial class MySlotsOutputDTO : MySlotsOutputDTOBase { }
[FunctionOutput]
public class MySlotsOutputDTOBase : IFunctionOutputDTO
{
[Parameter("bytes32[]", "", 1)]
public virtual List<byte[]> ReturnValue1 { get; set; }
}
public partial class RequestEndOutputDTO : RequestEndOutputDTOBase { }
[FunctionOutput]
public class RequestEndOutputDTOBase : IFunctionOutputDTO
{
[Parameter("uint256", "", 1)]
public virtual BigInteger ReturnValue1 { get; set; }
}
public partial class RequestStateOutputDTO : RequestStateOutputDTOBase { }
[FunctionOutput]
public class RequestStateOutputDTOBase : IFunctionOutputDTO
{
[Parameter("uint8", "", 1)]
public virtual byte ReturnValue1 { get; set; }
}
public partial class SlotStateOutputDTO : SlotStateOutputDTOBase { }
[FunctionOutput]
public class SlotStateOutputDTOBase : IFunctionOutputDTO
{
[Parameter("uint8", "", 1)]
public virtual byte ReturnValue1 { get; set; }
}
public partial class TokenOutputDTO : TokenOutputDTOBase { }
[FunctionOutput]
public class TokenOutputDTOBase : IFunctionOutputDTO
{
[Parameter("address", "", 1)]
public virtual string ReturnValue1 { get; set; }
}
public partial class WillProofBeRequiredOutputDTO : WillProofBeRequiredOutputDTOBase { }
[FunctionOutput]
public class WillProofBeRequiredOutputDTOBase : IFunctionOutputDTO
{
[Parameter("bool", "", 1)]
public virtual bool ReturnValue1 { get; set; }
}
public partial class CollateralConfig : CollateralConfigBase { }
public class CollateralConfigBase
{
[Parameter("uint8", "repairRewardPercentage", 1)]
public virtual byte RepairRewardPercentage { get; set; }
[Parameter("uint8", "maxNumberOfSlashes", 2)]
public virtual byte MaxNumberOfSlashes { get; set; }
[Parameter("uint16", "slashCriterion", 3)]
public virtual ushort SlashCriterion { get; set; }
[Parameter("uint8", "slashPercentage", 4)]
public virtual byte SlashPercentage { get; set; }
}
public partial class ProofConfig : ProofConfigBase { }
public class ProofConfigBase
{
[Parameter("uint256", "period", 1)]
public virtual BigInteger Period { get; set; }
[Parameter("uint256", "timeout", 2)]
public virtual BigInteger Timeout { get; set; }
[Parameter("uint8", "downtime", 3)]
public virtual byte Downtime { get; set; }
}
public partial class MarketplaceConfig : MarketplaceConfigBase { }
public class MarketplaceConfigBase
{
[Parameter("tuple", "collateral", 1)]
public virtual CollateralConfig Collateral { get; set; }
[Parameter("tuple", "proofs", 2)]
public virtual ProofConfig Proofs { get; set; }
}
public partial class Ask : AskBase { }
public class AskBase
{
[Parameter("uint64", "slots", 1)]
public virtual ulong Slots { get; set; }
[Parameter("uint256", "slotSize", 2)]
public virtual BigInteger SlotSize { get; set; }
[Parameter("uint256", "duration", 3)]
public virtual BigInteger Duration { get; set; }
[Parameter("uint256", "proofProbability", 4)]
public virtual BigInteger ProofProbability { get; set; }
[Parameter("uint256", "reward", 5)]
public virtual BigInteger Reward { get; set; }
[Parameter("uint256", "collateral", 6)]
public virtual BigInteger Collateral { get; set; }
[Parameter("uint64", "maxSlotLoss", 7)]
public virtual ulong MaxSlotLoss { get; set; }
}
public partial class Content : ContentBase { }
public class ContentBase
{
[Parameter("string", "cid", 1)]
public virtual string Cid { get; set; }
[Parameter("bytes32", "merkleRoot", 2)]
public virtual byte[] MerkleRoot { get; set; }
}
public partial class Request : RequestBase { }
public class RequestBase
{
[Parameter("address", "client", 1)]
public virtual string Client { get; set; }
[Parameter("tuple", "ask", 2)]
public virtual Ask Ask { get; set; }
[Parameter("tuple", "content", 3)]
public virtual Content Content { get; set; }
[Parameter("uint256", "expiry", 4)]
public virtual BigInteger Expiry { get; set; }
[Parameter("bytes32", "nonce", 5)]
public virtual byte[] Nonce { get; set; }
}
public partial class ActiveSlot : ActiveSlotBase { }
public class ActiveSlotBase
{
[Parameter("tuple", "request", 1)]
public virtual Request Request { get; set; }
[Parameter("uint256", "slotIndex", 2)]
public virtual BigInteger SlotIndex { get; set; }
}
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

View File

@ -0,0 +1 @@
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com

View File

@ -27,6 +27,13 @@ namespace CodexPlugin
public class CodexLogCustomTopics
{
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange)
{
DiscV5 = discV5;
Libp2p = libp2p;
BlockExchange = blockExchange;
}
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p)
{
DiscV5 = discV5;
@ -35,6 +42,7 @@ namespace CodexPlugin
public CodexLogLevel DiscV5 { get; set; }
public CodexLogLevel Libp2p { get; set; }
public CodexLogLevel? BlockExchange { get; }
}
public class CodexSetup : CodexStartupConfig, ICodexSetup

View File

@ -52,12 +52,31 @@ namespace CodexPlugin
"connection",
"connmanager",
"websock",
"ws-session"
"ws-session",
"dialer",
"muxedupgrade",
"upgrade",
"identify"
};
var blockExchangeTopics = new[]
{
"codex",
"pendingblocks",
"peerctxstore",
"discoveryengine",
"blockexcengine",
"blockexcnetwork",
"blockexcnetworkpeer"
};
level = $"{level};" +
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)}";
if (CustomTopics.BlockExchange != null)
{
level += $";{CustomTopics.BlockExchange.ToString()!.ToLowerInvariant()}:{string.Join(",", blockExchangeTopics)}";
}
}
return level;
}

View File

@ -9,11 +9,22 @@
{
public EthAddress(string address)
{
Address = address;
Address = address.ToLowerInvariant();
}
public string Address { get; }
public override bool Equals(object? obj)
{
return obj is EthAddress address &&
Address == address.Address;
}
public override int GetHashCode()
{
return HashCode.Combine(Address);
}
public override string ToString()
{
return Address;

View File

@ -1,8 +1,11 @@
using Core;
using KubernetesWorkflow.Types;
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.RPC.Eth.DTOs;
using NethereumWorkflow;
using Utils;
namespace GethPlugin
{
@ -17,9 +20,12 @@ namespace GethPlugin
string SendEth(EthAddress account, Ether eth);
TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
Transaction GetTransaction(string transactionHash);
decimal? GetSyncedBlockNumber();
bool IsContractAvailable(string abi, string contractAddress);
GethBootstrapNode GetBootstrapRecord();
List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new();
List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new();
}
public class DeploymentGethNode : BaseGethNode, IGethNode
@ -123,6 +129,11 @@ namespace GethPlugin
return StartInteraction().SendTransaction(contractAddress, function);
}
public Transaction GetTransaction(string transactionHash)
{
return StartInteraction().GetTransaction(transactionHash);
}
public decimal? GetSyncedBlockNumber()
{
return StartInteraction().GetSyncedBlockNumber();
@ -133,6 +144,16 @@ namespace GethPlugin
return StartInteraction().IsContractAvailable(abi, contractAddress);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
{
return StartInteraction().GetEvents<TEvent>(address, fromBlockNumber, toBlockNumber);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
{
return StartInteraction().GetEvents<TEvent>(address, timeRange);
}
protected abstract NethereumInteraction StartInteraction();
}
}

View File

@ -3,6 +3,7 @@ using DistTestCore.Logs;
using Logging;
using Newtonsoft.Json;
using Utils;
using TaskFactory = Utils.TaskFactory;
namespace ContinuousTests
{

View File

@ -6,6 +6,7 @@ using CodexPlugin;
using DistTestCore.Logs;
using Core;
using KubernetesWorkflow.Types;
using TaskFactory = Utils.TaskFactory;
namespace ContinuousTests
{

View File

@ -1,5 +1,6 @@
using DistTestCore.Logs;
using Logging;
using TaskFactory = Utils.TaskFactory;
namespace ContinuousTests
{

View File

@ -3,6 +3,7 @@ using CodexPlugin;
using DistTestCore;
using GethPlugin;
using MetricsPlugin;
using Nethereum.Hex.HexConvertors.Extensions;
using NUnit.Framework;
using Utils;
@ -59,6 +60,7 @@ namespace CodexTests.BasicTests
var contracts = Ci.StartCodexContracts(geth);
var seller = AddCodex(s => s
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn))
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
.WithSimulateProofFailures(failEveryNProofs: 3));
@ -88,14 +90,40 @@ namespace CodexTests.BasicTests
purchaseContract.WaitForStorageContractStarted(fileSize);
var requests = contracts.GetStorageRequests(GetTestRunTimeRange());
Assert.That(requests.Length, Is.EqualTo(1));
var request = requests.Single();
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started));
Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress));
Assert.That(request.Ask.Slots, Is.EqualTo(1));
AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange());
Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1));
CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId);
var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange());
Assert.That(filledSlotEvents.Length, Is.EqualTo(1));
var filledSlotEvent = filledSlotEvents.Single();
Assert.That(filledSlotEvent.SlotIndex.IsZero);
Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex()));
Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress));
var slotHost = contracts.GetSlotHost(request, 0);
Assert.That(slotHost, Is.EqualTo(seller.EthAddress));
purchaseContract.WaitForStorageContractFinished();
AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
CheckLogForErrors(seller, buyer);
var log = Ci.DownloadLog(seller);
log.AssertLogContains("Received a request to store a slot!");
log.AssertLogContains("Received proof challenge");
log.AssertLogContains("Collecting input for proof");
//CheckLogForErrors(seller, buyer);
}
[Test]

View File

@ -143,6 +143,11 @@ namespace DistTestCore
Stopwatch.Measure(Get().Log, name, action);
}
protected TimeRange GetTestRunTimeRange()
{
return new TimeRange(Get().TestStart, DateTime.UtcNow);
}
protected virtual void Initialize(FixtureLog fixtureLog)
{
}

View File

@ -11,7 +11,6 @@ namespace DistTestCore
public class TestLifecycle : IK8sHooks
{
private const string TestsType = "dist-tests";
private readonly DateTime testStart;
private readonly EntryPoint entryPoint;
private readonly Dictionary<string, string> metadata;
private readonly List<RunningContainers> runningContainers = new List<RunningContainers>();
@ -21,7 +20,7 @@ namespace DistTestCore
Log = log;
Configuration = configuration;
TimeSet = timeSet;
testStart = DateTime.UtcNow;
TestStart = DateTime.UtcNow;
entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(timeSet, this, testNamespace), configuration.GetFileManagerFolder(), timeSet);
metadata = entryPoint.GetPluginMetadata();
@ -30,6 +29,7 @@ namespace DistTestCore
log.WriteLogTag();
}
public DateTime TestStart { get; }
public TestLog Log { get; }
public Configuration Configuration { get; }
public ITimeSet TimeSet { get; }
@ -60,7 +60,7 @@ namespace DistTestCore
public TimeSpan GetTestDuration()
{
return DateTime.UtcNow - testStart;
return DateTime.UtcNow - TestStart;
}
public void OnContainersStarted(RunningContainers rc)

View File

@ -1,87 +1,18 @@
using BiblioTech.Options;
using CodexContractsPlugin;
using GethPlugin;
using Logging;
namespace BiblioTech
{
public static class GethInput
{
private const string GethHostVar = "GETH_HOST";
private const string GethPortVar = "GETH_HTTP_PORT";
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
private const string AbiVar = "CODEXCONTRACTS_ABI";
static GethInput()
{
var error = new List<string>();
var gethHost = GetEnvVar(error, GethHostVar);
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
var privateKey = GetEnvVar(error, GethPrivKeyVar);
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
var tokenAddress = GetEnvVar(error, TokenAddressVar);
var abi = GetEnvVar(error, AbiVar);
if (error.Any())
{
LoadError = string.Join(", ", error);
}
else
{
GethHost = gethHost!;
GethPort = gethPort;
PrivateKey = privateKey!;
MarketplaceAddress = marketplaceAddress!;
TokenAddress = tokenAddress!;
ABI = abi!;
}
}
public static string GethHost { get; } = string.Empty;
public static int GethPort { get; }
public static string PrivateKey { get; } = string.Empty;
public static string MarketplaceAddress { get; } = string.Empty;
public static string TokenAddress { get; } = string.Empty;
public static string ABI { get; } = string.Empty;
public static string LoadError { get; } = string.Empty;
private static string? GetEnvVar(List<string> error, string name)
{
var result = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
return result;
}
}
public abstract class BaseGethCommand : BaseCommand
{
protected override async Task Invoke(CommandContext context)
{
if (!string.IsNullOrEmpty(GethInput.LoadError))
{
var msg = "Geth input incorrect: " + GethInput.LoadError;
Program.Log.Error(msg);
if (IsInAdminChannel(context.Command))
{
await context.Followup(msg);
}
else
{
await context.Followup("I'm sorry, there seems to be a configuration error.");
}
return;
}
var gethConnector = GethConnector.GethConnector.Initialize(Program.Log);
var contractsDeployment = new CodexContractsDeployment(
marketplaceAddress: GethInput.MarketplaceAddress,
abi: GethInput.ABI,
tokenAddress: GethInput.TokenAddress
);
var gethNode = new CustomGethNode(Program.Log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
var contracts = new CodexContractsAccess(Program.Log, gethNode, contractsDeployment);
if (gethConnector == null) return;
var gethNode = gethConnector.GethNode;
var contracts = gethConnector.CodexContracts;
if (!contracts.IsDeployed())
{

View File

@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\..\Framework\GethConnector\GethConnector.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>

View File

@ -2,6 +2,7 @@
using Discord.WebSocket;
using Discord;
using Newtonsoft.Json;
using BiblioTech.Rewards;
namespace BiblioTech
{
@ -25,6 +26,9 @@ namespace BiblioTech
Program.AdminChecker.SetGuild(guild);
Program.Log.Log($"Initializing for guild: '{guild.Name}'");
var roleController = new RoleController(client);
var rewardsApi = new RewardsApi(roleController);
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());
@ -58,6 +62,8 @@ namespace BiblioTech
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
Program.Log.Error(json);
}
rewardsApi.Start();
}
private async Task SlashCommandHandler(SocketSlashCommand command)

View File

@ -0,0 +1,24 @@
using BiblioTech.Options;
namespace BiblioTech.Commands
{
public class NotifyCommand : BaseCommand
{
private readonly BoolOption boolOption = new BoolOption(name: "enabled", description: "Controls whether the bot will @-mention you.", isRequired: false);
public override string Name => "notify";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Enable or disable notifications from the bot.";
public override CommandOption[] Options => new CommandOption[] { boolOption };
protected override async Task Invoke(CommandContext context)
{
var user = context.Command.User;
var enabled = await boolOption.Parse(context);
if (enabled == null) return;
Program.UserRepo.SetUserNotificationPreference(user, enabled.Value);
await context.Followup("Done!");
}
}
}

View File

@ -4,6 +4,12 @@ namespace BiblioTech.Commands
{
public class UserAssociateCommand : BaseCommand
{
public UserAssociateCommand(NotifyCommand notifyCommand)
{
this.notifyCommand = notifyCommand;
}
private readonly NotifyCommand notifyCommand;
private readonly EthAddressOption ethOption = new EthAddressOption(isRequired: false);
private readonly UserOption optionalUser = new UserOption(
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
@ -30,7 +36,12 @@ namespace BiblioTech.Commands
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
if (result)
{
await context.Followup("Done! Thank you for joining the test net!");
await context.Followup(new string[]
{
"Done! Thank you for joining the test net!",
"By default, the bot will @-mention you with test-net reward related notifications.",
$"You can enable/disable this behavior with the '/{notifyCommand.Name}' command."
});
}
else
{

View File

@ -19,6 +19,10 @@ namespace BiblioTech
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")]
public string AdminChannelName { get; set; } = "admin-channel";
[Uniform("rewards-channel-name", "ac", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")]
public string RewardsChannelName { get; set; } = "";
public string EndpointsPath
{
get

View File

@ -0,0 +1,23 @@
using Discord;
namespace BiblioTech.Options
{
public class BoolOption : CommandOption
{
public BoolOption(string name, string description, bool isRequired)
: base(name, description, type: ApplicationCommandOptionType.Boolean, isRequired)
{
}
public async Task<bool?> Parse(CommandContext context)
{
var bData = context.Options.SingleOrDefault(o => o.Name == Name);
if (bData == null || !(bData.Value is bool))
{
await context.Followup("Bool option not received.");
return null;
}
return (bool) bData.Value;
}
}
}

View File

@ -21,7 +21,7 @@ namespace BiblioTech
Config = uniformArgs.Parse();
Log = new LogSplitter(
new FileLog(Path.Combine(Config.LogPath, "discordbot.log")),
new FileLog(Path.Combine(Config.LogPath, "discordbot")),
new ConsoleLog()
);
@ -38,13 +38,15 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
var associateCommand = new UserAssociateCommand();
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)
);

View File

@ -0,0 +1,92 @@
using DiscordRewards;
using Newtonsoft.Json;
using System.Net;
using TaskFactory = Utils.TaskFactory;
namespace BiblioTech.Rewards
{
public interface IDiscordRoleController
{
Task GiveRewards(GiveRewardsCommand rewards);
}
public class RewardsApi
{
private readonly HttpListener listener = new HttpListener();
private readonly TaskFactory taskFactory = new TaskFactory();
private readonly IDiscordRoleController roleController;
private CancellationTokenSource cts = new CancellationTokenSource();
public RewardsApi(IDiscordRoleController roleController)
{
this.roleController = roleController;
}
public void Start()
{
cts = new CancellationTokenSource();
listener.Prefixes.Add($"http://*:31080/");
listener.Start();
taskFactory.Run(ConnectionDispatcher, nameof(ConnectionDispatcher));
}
public void Stop()
{
listener.Stop();
cts.Cancel();
taskFactory.WaitAll();
}
private void ConnectionDispatcher()
{
while (!cts.Token.IsCancellationRequested)
{
var wait = listener.GetContextAsync();
wait.Wait(cts.Token);
if (wait.IsCompletedSuccessfully)
{
taskFactory.Run(() =>
{
var context = wait.Result;
try
{
HandleConnection(context).Wait();
}
catch (Exception ex)
{
Program.Log.Error("Exception during HTTP handler: " + ex);
}
// Whatever happens, everything's always OK.
context.Response.StatusCode = 200;
context.Response.OutputStream.Close();
}, nameof(HandleConnection));
}
}
}
private async Task HandleConnection(HttpListenerContext context)
{
using var reader = new StreamReader(context.Request.InputStream);
var content = reader.ReadToEnd();
if (content == "Ping")
{
using var writer = new StreamWriter(context.Response.OutputStream);
writer.Write("Pong");
return;
}
if (!content.StartsWith("{")) return;
var rewards = JsonConvert.DeserializeObject<GiveRewardsCommand>(content);
if (rewards != null)
{
await ProcessRewards(rewards);
}
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
{
await roleController.GiveRewards(rewards);
}
}
}

View File

@ -0,0 +1,218 @@
using Discord;
using Discord.WebSocket;
using DiscordRewards;
namespace BiblioTech.Rewards
{
public class RoleController : IDiscordRoleController
{
private readonly DiscordSocketClient client;
private readonly SocketTextChannel? rewardsChannel;
private readonly RewardRepo repo = new RewardRepo();
public RoleController(DiscordSocketClient client)
{
this.client = client;
if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName))
{
rewardsChannel = GetGuild().TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName);
}
}
public async Task GiveRewards(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);
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
var result = new Dictionary<ulong, IGuildUser>();
var users = guild.GetUsersAsync();
await foreach (var ulist in users)
{
foreach (var u in ulist)
{
result.Add(u.Id, u);
}
}
return result;
}
private Dictionary<ulong, RoleReward> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
{
var result = new Dictionary<ulong, RoleReward>();
foreach (var r in rewards.Rewards)
{
if (!result.ContainsKey(r.RewardId))
{
var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
if (rewardConfig == null)
{
Program.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.");
}
else
{
result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig));
}
}
}
}
return result;
}
private UserReward[] LookUpUsers(GiveRewardsCommand rewards)
{
return rewards.Rewards.Select(LookUpUserData).ToArray();
}
private UserReward LookUpUserData(RewardUsersCommand command)
{
return new UserReward(command,
command.UserAddresses
.Select(LookUpUserDataForAddress)
.Where(d => d != null)
.Cast<UserData>()
.ToArray());
}
private UserData? LookUpUserDataForAddress(string address)
{
try
{
return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
}
catch (Exception ex)
{
Program.Log.Error("Error during UserData lookup: " + ex);
return null;
}
}
private SocketGuild GetGuild()
{
return client.Guilds.Single(g => g.Name == Program.Config.ServerName);
}
}
public class RoleReward
{
public RoleReward(SocketRole socketRole, RewardConfig reward)
{
SocketRole = socketRole;
Reward = reward;
}
public SocketRole SocketRole { get; }
public RewardConfig Reward { get; }
}
public class UserReward
{
public UserReward(RewardUsersCommand rewardCommand, UserData[] users)
{
RewardCommand = rewardCommand;
Users = users;
}
public RewardUsersCommand RewardCommand { get; }
public UserData[] Users { get; }
}
public class RewardContext
{
private readonly Dictionary<ulong, IGuildUser> users;
private readonly Dictionary<ulong, RoleReward> roles;
private readonly SocketTextChannel? rewardsChannel;
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleReward> roles, SocketTextChannel? rewardsChannel)
{
this.users = users;
this.roles = roles;
this.rewardsChannel = rewardsChannel;
}
public async Task ProcessGiveRewardsCommand(UserReward[] rewards)
{
foreach (var rewardCommand in rewards)
{
if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId))
{
var role = roles[rewardCommand.RewardCommand.RewardId];
await ProcessRewardCommand(role, rewardCommand);
}
}
}
private async Task ProcessRewardCommand(RoleReward role, UserReward reward)
{
foreach (var user in reward.Users)
{
await GiveReward(role, user);
}
}
private async Task GiveReward(RoleReward role, UserData user)
{
if (!users.ContainsKey(user.DiscordId))
{
Program.Log.Log($"User by id '{user.DiscordId}' not found.");
return;
}
var guildUser = users[user.DiscordId];
var alreadyHas = guildUser.RoleIds.ToArray();
if (alreadyHas.Any(r => r == role.Reward.RoleId)) return;
await GiveRole(guildUser, role.SocketRole);
await SendNotification(role, user, guildUser);
await Task.Delay(1000);
}
private async Task GiveRole(IGuildUser user, SocketRole role)
{
try
{
Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}");
await user.AddRoleAsync(role);
}
catch (Exception ex)
{
Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
}
}
private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user)
{
try
{
if (userData.NotificationsEnabled && rewardsChannel != null)
{
var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>");
await rewardsChannel.SendMessageAsync(msg);
}
}
catch (Exception ex)
{
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}");
}
}
}
}

View File

@ -25,6 +25,14 @@ namespace BiblioTech
}
}
public void SetUserNotificationPreference(IUser user, bool enableNotifications)
{
lock (repoLock)
{
SetUserNotification(user, enableNotifications);
}
}
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction<Ether>? eth, Transaction<TestToken>? tokens)
{
lock (repoLock)
@ -96,6 +104,29 @@ namespace BiblioTech
return userData.CreateOverview();
}
public UserData? GetUserDataForAddress(EthAddress? address)
{
if (address == null) return null;
// If this becomes a performance problem, switch to in-memory cached list.
var files = Directory.GetFiles(Program.Config.UserDataPath);
foreach (var file in files)
{
try
{
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
if (user.CurrentAddress != null &&
user.CurrentAddress.Address == address.Address)
{
return user;
}
}
catch { }
}
return null;
}
private bool SetUserAddress(IUser user, EthAddress? address)
{
if (GetUserDataForAddress(address) != null)
@ -110,6 +141,14 @@ namespace BiblioTech
return true;
}
private void SetUserNotification(IUser user, bool notifyEnabled)
{
var userData = GetUserData(user);
if (userData == null) return;
userData.NotificationsEnabled = notifyEnabled;
SaveUserData(userData);
}
private UserData? GetUserData(IUser user)
{
var filename = GetFilename(user);
@ -132,34 +171,11 @@ namespace BiblioTech
private UserData CreateAndSaveNewUserData(IUser user)
{
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>(), true);
SaveUserData(newUser);
return newUser;
}
private UserData? GetUserDataForAddress(EthAddress? address)
{
if (address == null) return null;
// If this becomes a performance problem, switch to in-memory cached list.
var files = Directory.GetFiles(Program.Config.UserDataPath);
foreach (var file in files)
{
try
{
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
if (user.CurrentAddress != null &&
user.CurrentAddress.Address == address.Address)
{
return user;
}
}
catch { }
}
return null;
}
private void SaveUserData(UserData userData)
{
var filename = GetFilename(userData);
@ -185,7 +201,7 @@ namespace BiblioTech
public class UserData
{
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents, bool notificationsEnabled)
{
DiscordId = discordId;
Name = name;
@ -193,6 +209,7 @@ namespace BiblioTech
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
NotificationsEnabled = notificationsEnabled;
}
public ulong DiscordId { get; }
@ -201,6 +218,7 @@ namespace BiblioTech
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
public bool NotificationsEnabled { get; set; }
public string[] CreateOverview()
{

View File

@ -0,0 +1,49 @@
using DiscordRewards;
using Logging;
using Newtonsoft.Json;
namespace TestNetRewarder
{
public class BotClient
{
private readonly Configuration configuration;
private readonly ILog log;
public BotClient(Configuration configuration, ILog log)
{
this.configuration = configuration;
this.log = log;
}
public async Task<bool> IsOnline()
{
return await HttpPost("Ping") == "Ping";
}
public async Task SendRewards(GiveRewardsCommand command)
{
if (command == null || command.Rewards == null || !command.Rewards.Any()) return;
await HttpPost(JsonConvert.SerializeObject(command));
}
private async Task<string> HttpPost(string content)
{
try
{
var client = new HttpClient();
var response = await client.PostAsync(GetUrl(), new StringContent(content));
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
log.Error(ex.ToString());
return string.Empty;
}
}
private string GetUrl()
{
return $"{configuration.DiscordHost}:{configuration.DiscordPort}";
}
}
}

View File

@ -0,0 +1,35 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using Utils;
namespace TestNetRewarder
{
public class ChainState
{
private readonly HistoricState historicState;
public ChainState(HistoricState historicState, ICodexContracts contracts, TimeRange timeRange)
{
NewRequests = contracts.GetStorageRequests(timeRange);
historicState.ProcessNewRequests(NewRequests);
historicState.UpdateStorageRequests(contracts);
StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray();
FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFininshed).ToArray();
RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(timeRange);
RequestCancelledEvents = contracts.GetRequestCancelledEvents(timeRange);
SlotFilledEvents = contracts.GetSlotFilledEvents(timeRange);
SlotFreedEvents = contracts.GetSlotFreedEvents(timeRange);
this.historicState = historicState;
}
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; }
}
}

View File

@ -0,0 +1,141 @@
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;
}
}
}

View File

@ -0,0 +1,30 @@
using ArgsUniform;
namespace TestNetRewarder
{
public class Configuration
{
[Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
public string DataPath { get; set; } = "datapath";
[Uniform("discordbot-host", "dh", "DISCORDBOTHOST", true, "http address of the discord bot.")]
public string DiscordHost { get; set; } = "host";
[Uniform("discordbot-port", "dp", "DISCORDBOTPORT", true, "port number of the discord bot reward API. (31080 by default)")]
public int DiscordPort { get; set; } = 31080;
[Uniform("interval-minutes", "im", "INTERVALMINUTES", false, "time in minutes between reward updates. (default 15)")]
public int Interval { get; set; } = 15;
[Uniform("check-history", "ch", "CHECKHISTORY", true, "Unix epoc timestamp of a moment in history on which processing begins. Required for hosting rewards. Should be 'launch of the testnet'.")]
public int CheckHistoryTimestamp { get; set; } = 0;
public string LogPath
{
get
{
return Path.Combine(DataPath, "logs");
}
}
}
}

View File

@ -0,0 +1,68 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
namespace TestNetRewarder
{
public class HistoricState
{
private readonly List<StorageRequest> storageRequests = new List<StorageRequest>();
public StorageRequest[] StorageRequests { get { return storageRequests.ToArray(); } }
public void ProcessNewRequests(Request[] requests)
{
storageRequests.AddRange(requests.Select(r => new StorageRequest(r)));
}
public void UpdateStorageRequests(ICodexContracts contracts)
{
foreach (var r in storageRequests) r.Update(contracts);
}
}
public class StorageRequest
{
public StorageRequest(Request request)
{
Request = request;
Hosts = Array.Empty<EthAddress>();
}
public Request Request { get; }
public EthAddress[] Hosts { get; private set; }
public RequestState State { get; private set; }
public bool RecentlyStarted { get; private set; }
public bool RecentlyFininshed { get; private set; }
public void Update(ICodexContracts contracts)
{
Hosts = GetHosts(contracts);
var newState = contracts.GetRequestState(Request);
RecentlyStarted =
State == RequestState.New &&
newState == RequestState.Started;
RecentlyFininshed =
State == RequestState.Started &&
newState == RequestState.Finished;
State = newState;
}
private EthAddress[] GetHosts(ICodexContracts contracts)
{
var result = new List<EthAddress>();
for (decimal i = 0; i < Request.Ask.Slots; i++)
{
var host = contracts.GetSlotHost(Request, i);
if (host != null) result.Add(host);
}
return result.ToArray();
}
}
}

View File

@ -0,0 +1,98 @@
using DiscordRewards;
using GethPlugin;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class Processor
{
private static readonly HistoricState historicState = new HistoricState();
private static readonly RewardRepo rewardRepo = new RewardRepo();
private readonly ILog log;
public Processor(ILog log)
{
this.log = log;
}
public async Task ProcessTimeSegment(TimeRange range)
{
try
{
var connector = GethConnector.GethConnector.Initialize(log);
if (connector == null) return;
var chainState = new ChainState(historicState, connector.CodexContracts, range);
await ProcessTimeSegment(chainState);
}
catch (Exception ex)
{
log.Error("Exception processing time segment: " + ex);
}
}
private async Task ProcessTimeSegment(ChainState chainState)
{
var outgoingRewards = new List<RewardUsersCommand>();
foreach (var reward in rewardRepo.Rewards)
{
ProcessReward(outgoingRewards, reward, chainState);
}
if (outgoingRewards.Any())
{
await SendRewardsCommand(outgoingRewards);
}
}
private async Task SendRewardsCommand(List<RewardUsersCommand> outgoingRewards)
{
var cmd = new GiveRewardsCommand
{
Rewards = outgoingRewards.ToArray()
};
log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd));
await Program.BotClient.SendRewards(cmd);
}
private void ProcessReward(List<RewardUsersCommand> outgoingRewards, RewardConfig reward, ChainState chainState)
{
var winningAddresses = PerformCheck(reward, chainState);
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);
}
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);
}
}
}

View File

@ -0,0 +1,92 @@
using ArgsUniform;
using GethConnector;
using Logging;
using Utils;
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!;
private static Processor processor = null!;
public static Task Main(string[] args)
{
var cts = new CancellationTokenSource();
CancellationToken = cts.Token;
Console.CancelKeyPress += (sender, args) => cts.Cancel();
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
Config = uniformArgs.Parse(true);
Log = new LogSplitter(
new FileLog(Path.Combine(Config.LogPath, "testnetrewarder")),
new ConsoleLog()
);
BotClient = new BotClient(Config, Log);
processor = new Processor(Log);
EnsurePath(Config.DataPath);
EnsurePath(Config.LogPath);
return new Program().MainAsync();
}
public async Task MainAsync()
{
EnsureGethOnline();
Log.Log("Starting TestNet Rewarder...");
var segmenter = new TimeSegmenter(Log, Config);
while (!CancellationToken.IsCancellationRequested)
{
await EnsureBotOnline();
await segmenter.WaitForNextSegment(processor.ProcessTimeSegment);
await Task.Delay(1000, CancellationToken);
}
}
private static void EnsureGethOnline()
{
Log.Log("Checking Geth...");
var gc = GethConnector.GethConnector.Initialize(Log);
if (gc == null) throw new Exception("Geth input incorrect");
var blockNumber = gc.GethNode.GetSyncedBlockNumber();
if (blockNumber == null || blockNumber < 1) throw new Exception("Geth connection failed.");
}
private static async Task EnsureBotOnline()
{
var start = DateTime.UtcNow;
while (! await BotClient.IsOnline() && !CancellationToken.IsCancellationRequested)
{
await Task.Delay(5000);
var elapsed = DateTime.UtcNow - start;
if (elapsed.TotalMinutes > 10)
{
var msg = "Unable to connect to bot for " + Time.FormatDuration(elapsed);
Log.Error(msg);
throw new Exception(msg);
}
}
}
private static void PrintHelp()
{
Log.Log("TestNet Rewarder");
}
private static void EnsurePath(string path)
{
if (Directory.Exists(path)) return;
Directory.CreateDirectory(path);
}
}
}

View File

@ -0,0 +1,17 @@
<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\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\..\Framework\GethConnector\GethConnector.csproj" />
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,51 @@
using Logging;
using Utils;
namespace TestNetRewarder
{
public class TimeSegmenter
{
private readonly ILog log;
private readonly TimeSpan segmentSize;
private DateTime start;
public TimeSegmenter(ILog log, Configuration configuration)
{
this.log = log;
if (configuration.Interval < 0) configuration.Interval = 15;
if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
segmentSize = TimeSpan.FromSeconds(configuration.Interval);
start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime;
log.Log("Starting time segments at " + start);
log.Log("Segment size: " + Time.FormatDuration(segmentSize));
}
public async Task WaitForNextSegment(Func<TimeRange, Task> onSegment)
{
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).Add(TimeSpan.FromSeconds(3));
waited = true;
await Task.Delay(delay, Program.CancellationToken);
}
if (Program.CancellationToken.IsCancellationRequested) return;
var postfix = "(Catching up...)";
if (waited) postfix = "(Real-time)";
log.Log($"Time segment [{start} to {end}] {postfix}");
var range = new TimeRange(start, end);
start = end;
await onSegment(range);
}
}
}

View File

@ -0,0 +1,13 @@
# Variables
ARG IMAGE=mcr.microsoft.com/dotnet/sdk:7.0
ARG APP_HOME=/app
# Create
FROM ${IMAGE}
ARG APP_HOME
WORKDIR ${APP_HOME}
COPY ./Tools/TestNetRewarder ./Tools/TestNetRewarder
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
CMD ["dotnet", "run", "--project", "Tools/TestNetRewarder"]

View File

@ -53,6 +53,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAndRunPlugin", "Proje
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Tests\FrameworkTests\FrameworkTests.csproj", "{25E72004-4D71-4D1E-A193-FC125D12FF96}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestNetRewarder", "Tools\TestNetRewarder\TestNetRewarder.csproj", "{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GethConnector", "Framework\GethConnector\GethConnector.csproj", "{F730DA73-1C92-4107-BCFB-D33759DAB0C3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRewards", "Framework\DiscordRewards\DiscordRewards.csproj", "{B07820C4-309F-4454-BCC1-1D4902C9C67B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -143,6 +149,18 @@ Global
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.Build.0 = Release|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.Build.0 = Release|Any CPU
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Release|Any CPU.Build.0 = Release|Any CPU
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -169,6 +187,9 @@ Global
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
{25E72004-4D71-4D1E-A193-FC125D12FF96} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{F730DA73-1C92-4107-BCFB-D33759DAB0C3} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
{B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}