From 7df1f3da7bc23bccee029d2b634a10f3c3b66d83 Mon Sep 17 00:00:00 2001 From: benbierens Date: Tue, 19 Dec 2023 15:30:46 +0100 Subject: [PATCH 01/22] block time finder --- .../NethereumWorkflow/BlockTimeFinder.cs | 205 ++++++++++++++++++ .../NethereumWorkflow/NethereumInteraction.cs | 22 ++ ProjectPlugins/GethPlugin/GethNode.cs | 17 ++ 3 files changed, 244 insertions(+) create mode 100644 Framework/NethereumWorkflow/BlockTimeFinder.cs diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs new file mode 100644 index 0000000..acc8b1c --- /dev/null +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -0,0 +1,205 @@ +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; +using Utils; + +namespace NethereumWorkflow +{ + public class BlockTimeFinder + { + private class BlockTimeEntry + { + public BlockTimeEntry(ulong blockNumber, DateTime utc) + { + BlockNumber = blockNumber; + Utc = utc; + } + + public ulong BlockNumber { get; } + public DateTime Utc { get; } + } + + private const ulong FetchRange = 6; + private const int MaxEntries = 1024; + private readonly Web3 web3; + private static readonly Dictionary entries = new Dictionary(); + + public BlockTimeFinder(Web3 web3) + { + this.web3 = web3; + } + + public ulong GetHighestBlockNumberBefore(DateTime moment) + { + AssertMomentIsInPast(moment); + Initialize(); + + var closestBefore = FindClosestBeforeEntry(moment); + var closestAfter = FindClosestAfterEntry(moment); + + if (closestBefore.Utc < moment && + closestAfter.Utc > moment && + closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) + { + return closestBefore.BlockNumber; + } + + FetchBlocksAround(moment); + return GetHighestBlockNumberBefore(moment); + } + + public ulong GetLowestBlockNumberAfter(DateTime moment) + { + AssertMomentIsInPast(moment); + Initialize(); + + var closestBefore = FindClosestBeforeEntry(moment); + var closestAfter = FindClosestAfterEntry(moment); + + if (closestBefore.Utc < moment && + closestAfter.Utc > moment && + closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) + { + return closestAfter.BlockNumber; + } + + FetchBlocksAround(moment); + return GetLowestBlockNumberAfter(moment); + } + + private void FetchBlocksAround(DateTime moment) + { + var timePerBlock = EstimateTimePerBlock(); + EnsureRecentBlockIfNecessary(moment, timePerBlock); + + var max = entries.Keys.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); + + var fetchStart = (max - blockDifference) - (FetchRange / 2); + for (ulong i = 0; i < FetchRange; i++) + { + AddBlockNumber(fetchStart + i); + } + } + + 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.BlockNumber == latest.BlockNumber) + { + maxRetry--; + if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries."); + Thread.Sleep(timePerBlock); + } + } + } + + 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) + { + entries.Clear(); + Initialize(); + } + + var time = GetTimestampFromBlock(blockNumber); + var entry = new BlockTimeEntry(blockNumber, time); + entries.Add(blockNumber, entry); + return entry; + } + + private TimeSpan EstimateTimePerBlock() + { + var min = entries.Keys.Min(); + var max = entries.Keys.Max(); + var minTime = entries[min].Utc; + var maxTime = entries[max].Utc; + var elapsedTime = maxTime - minTime; + + double elapsedSeconds = elapsedTime.TotalSeconds; + double numberOfBlocks = max - min; + double secondsPerBlock = elapsedSeconds / numberOfBlocks; + + return TimeSpan.FromSeconds(secondsPerBlock); + } + + 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) + { + var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber))); + return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime; + } + + private BlockTimeEntry FindClosestBeforeEntry(DateTime moment) + { + var result = entries.Values.First(); + var highestTime = result.Utc; + + foreach (var entry in entries.Values) + { + if (entry.Utc > highestTime && entry.Utc < moment) + { + highestTime = entry.Utc; + result = entry; + } + } + return result; + } + + private BlockTimeEntry FindClosestAfterEntry(DateTime moment) + { + var result = entries.Values.First(); + var lowestTime = result.Utc; + + foreach (var entry in entries.Values) + { + if (entry.Utc < lowestTime && entry.Utc > moment) + { + lowestTime = entry.Utc; + result = entry; + } + } + return result; + } + } +} diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 1efef81..1139175 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -1,4 +1,5 @@ using Logging; +using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; @@ -77,5 +78,26 @@ namespace NethereumWorkflow return false; } } + + public List> GetEvent(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + { + if (from >= to) throw new Exception("Time range is invalid."); + + var blockTimeFinder = new BlockTimeFinder(web3); + + var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(from); + var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(to); + + return GetEvent(address, fromBlock, toBlock); + } + + public List> GetEvent(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + { + var eventHandler = web3.Eth.GetEvent(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)); + } } } diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index d2e2122..8c83d34 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -1,6 +1,7 @@ using Core; using KubernetesWorkflow.Types; using Logging; +using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using NethereumWorkflow; @@ -20,6 +21,9 @@ namespace GethPlugin decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); + List> GetEvents(string address) where TEvent : IEventDTO, new(); + List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new(); + List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -133,6 +137,19 @@ namespace GethPlugin return StartInteraction().IsContractAvailable(abi, contractAddress); } + public List> GetEvents(string address) where TEvent : IEventDTO, new() + { + StartInteraction().GetEvent(); + } + + public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + { + } + + public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + { + } + protected abstract NethereumInteraction StartInteraction(); } } From 4f2539c59f6635109bf5dfb8191b2b0279228576 Mon Sep 17 00:00:00 2001 From: benbierens Date: Tue, 19 Dec 2023 15:43:26 +0100 Subject: [PATCH 02/22] Adds marketplace generated code --- .../NethereumWorkflow/NethereumInteraction.cs | 6 +- .../Marketplace/Marketplace.cs | 517 ++++++++++++++++++ .../Marketplace/README.md | 1 + ProjectPlugins/GethPlugin/GethNode.cs | 8 +- 4 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs create mode 100644 ProjectPlugins/CodexContractsPlugin/Marketplace/README.md diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 1139175..48a62f2 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -79,7 +79,7 @@ namespace NethereumWorkflow } } - public List> GetEvent(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() { if (from >= to) throw new Exception("Time range is invalid."); @@ -88,10 +88,10 @@ namespace NethereumWorkflow var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(from); var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(to); - return GetEvent(address, fromBlock, toBlock); + return GetEvents(address, fromBlock, toBlock); } - public List> GetEvent(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() { var eventHandler = web3.Eth.GetEvent(address); var from = new BlockParameter(fromBlockNumber); diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs new file mode 100644 index 0000000..507334a --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs @@ -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 ReturnValue1 { get; set; } + } + + public partial class MySlotsOutputDTO : MySlotsOutputDTOBase { } + + [FunctionOutput] + public class MySlotsOutputDTOBase : IFunctionOutputDTO + { + [Parameter("bytes32[]", "", 1)] + public virtual List 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. diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md b/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md new file mode 100644 index 0000000..bfb23ce --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md @@ -0,0 +1 @@ +This code was generated using the Nethereum code generator, here: http://playground.nethereum.com diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 8c83d34..126792e 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -21,7 +21,6 @@ namespace GethPlugin decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); - List> GetEvents(string address) where TEvent : IEventDTO, new(); List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new(); List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new(); } @@ -137,17 +136,14 @@ namespace GethPlugin return StartInteraction().IsContractAvailable(abi, contractAddress); } - public List> GetEvents(string address) where TEvent : IEventDTO, new() - { - StartInteraction().GetEvent(); - } - public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() { + return StartInteraction().GetEvents(address, fromBlockNumber, toBlockNumber); } public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() { + return StartInteraction().GetEvents(address, from, to); } protected abstract NethereumInteraction StartInteraction(); From 4b74a9d5fe36aa5fa933ebd4e1d7428e29f69f6d Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 09:48:22 +0100 Subject: [PATCH 03/22] working block time finder --- .../NethereumWorkflow/BlockTimeFinder.cs | 114 ++++++++++++++---- .../NethereumWorkflow/NethereumInteraction.cs | 10 +- Framework/Utils/TimeRange.cs | 24 ++++ .../CodexContractsAccess.cs | 23 +++- ProjectPlugins/GethPlugin/GethNode.cs | 7 +- Tests/CodexTests/BasicTests/ExampleTests.cs | 4 +- Tests/DistTestCore/TestLifecycle.cs | 6 +- 7 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 Framework/Utils/TimeRange.cs diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index acc8b1c..0d429f5 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -1,4 +1,5 @@ -using Nethereum.RPC.Eth.DTOs; +using Logging; +using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; using Utils; @@ -16,30 +17,48 @@ namespace NethereumWorkflow public ulong BlockNumber { get; } public DateTime Utc { get; } + + public override string ToString() + { + return $"[{BlockNumber}] @ {Utc.ToString("o")}"; + } } private const ulong FetchRange = 6; private const int MaxEntries = 1024; private readonly Web3 web3; + private readonly ILog log; private static readonly Dictionary entries = new Dictionary(); - public BlockTimeFinder(Web3 web3) + 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(); var closestBefore = FindClosestBeforeEntry(moment); var closestAfter = FindClosestAfterEntry(moment); - + + if (closestBefore == null || closestAfter == null) + { + FetchBlocksAround(moment); + return GetHighestBlockNumberBefore(moment); + } + + log.Log("Closest before: " + closestBefore); + log.Log("Closest after: " + closestAfter); + if (closestBefore.Utc < moment && closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { + log.Log("Found highest-Before: " + closestBefore); return closestBefore.BlockNumber; } @@ -49,16 +68,27 @@ namespace NethereumWorkflow public ulong GetLowestBlockNumberAfter(DateTime moment) { + log.Log("Looking for lowest block after " + moment.ToString("o")); AssertMomentIsInPast(moment); Initialize(); var closestBefore = FindClosestBeforeEntry(moment); var closestAfter = FindClosestAfterEntry(moment); + if (closestBefore == null || closestAfter == null) + { + FetchBlocksAround(moment); + return GetLowestBlockNumberAfter(moment); + } + + log.Log("Closest before: " + closestBefore); + log.Log("Closest after: " + closestAfter); + if (closestBefore.Utc < moment && closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { + log.Log("Found lowest-after: " + closestAfter); return closestAfter.BlockNumber; } @@ -68,6 +98,8 @@ namespace NethereumWorkflow private void FetchBlocksAround(DateTime moment) { + log.Log("Fetching..."); + var timePerBlock = EstimateTimePerBlock(); EnsureRecentBlockIfNecessary(moment, timePerBlock); @@ -79,11 +111,36 @@ namespace NethereumWorkflow double numberOfBlocksDifference = secondsDifference / secondsPerBlock; var blockDifference = Convert.ToUInt64(numberOfBlocksDifference); + if (blockDifference < 1) blockDifference = 1; - var fetchStart = (max - blockDifference) - (FetchRange / 2); - for (ulong i = 0; i < FetchRange; i++) + var fetchUp = FetchRange; + var fetchDown = FetchRange; + var target = max - blockDifference; + log.Log("up - target: " + target); + while (fetchUp > 0) { - AddBlockNumber(fetchStart + i); + if (!entries.ContainsKey(target)) + { + var newBlock = AddBlockNumber(target); + if (newBlock != null) fetchUp--; + else fetchUp = 0; + } + target++; + //if (target >= max) fetchUp = 0; + } + + target = max - blockDifference - 1; + log.Log("down - target: " + target); + while (fetchDown > 0) + { + if (!entries.ContainsKey(target)) + { + var newBlock = AddBlockNumber(target); + if (newBlock != null) fetchDown--; + else fetchDown = 0; + } + target--; + //if (target <= 0) fetchDown = 0; } } @@ -95,7 +152,7 @@ namespace NethereumWorkflow while (moment > latest.Utc) { var newBlock = AddCurrentBlock(); - if (newBlock.BlockNumber == latest.BlockNumber) + if (newBlock == null || newBlock.BlockNumber == latest.BlockNumber) { maxRetry--; if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries."); @@ -104,12 +161,12 @@ namespace NethereumWorkflow } } - private BlockTimeEntry AddBlockNumber(decimal blockNumber) + private BlockTimeEntry? AddBlockNumber(decimal blockNumber) { return AddBlockNumber(Convert.ToUInt64(blockNumber)); } - private BlockTimeEntry AddBlockNumber(ulong blockNumber) + private BlockTimeEntry? AddBlockNumber(ulong blockNumber) { if (entries.ContainsKey(blockNumber)) { @@ -123,7 +180,9 @@ namespace NethereumWorkflow } var time = GetTimestampFromBlock(blockNumber); - var entry = new BlockTimeEntry(blockNumber, time); + if (time == null) return null; + var entry = new BlockTimeEntry(blockNumber, time.Value); + log.Log("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o")); entries.Add(blockNumber, entry); return entry; } @@ -157,46 +216,49 @@ namespace NethereumWorkflow if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past."); } - private BlockTimeEntry AddCurrentBlock() + private BlockTimeEntry? AddCurrentBlock() { var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); var blockNumber = number.ToDecimal(); return AddBlockNumber(blockNumber); } - private DateTime GetTimestampFromBlock(ulong blockNumber) + private DateTime? GetTimestampFromBlock(ulong blockNumber) { 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; } - private BlockTimeEntry FindClosestBeforeEntry(DateTime moment) + private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment) { - var result = entries.Values.First(); - var highestTime = result.Utc; - + BlockTimeEntry? result = null; foreach (var entry in entries.Values) { - if (entry.Utc > highestTime && entry.Utc < moment) + if (result == null) { - highestTime = entry.Utc; - result = entry; + if (entry.Utc < moment) result = entry; + } + else + { + if (entry.Utc > result.Utc && entry.Utc < moment) result = entry; } } return result; } - private BlockTimeEntry FindClosestAfterEntry(DateTime moment) + private BlockTimeEntry? FindClosestAfterEntry(DateTime moment) { - var result = entries.Values.First(); - var lowestTime = result.Utc; - + BlockTimeEntry? result = null; foreach (var entry in entries.Values) { - if (entry.Utc < lowestTime && entry.Utc > moment) + if (result == null) { - lowestTime = entry.Utc; - result = entry; + if (entry.Utc > moment) result = entry; + } + else + { + if (entry.Utc < result.Utc && entry.Utc > moment) result = entry; } } return result; diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 48a62f2..172e9ce 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -79,14 +79,12 @@ namespace NethereumWorkflow } } - public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { - if (from >= to) throw new Exception("Time range is invalid."); + var blockTimeFinder = new BlockTimeFinder(web3, log); - var blockTimeFinder = new BlockTimeFinder(web3); - - var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(from); - var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(to); + var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); + var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); return GetEvents(address, fromBlock, toBlock); } diff --git a/Framework/Utils/TimeRange.cs b/Framework/Utils/TimeRange.cs new file mode 100644 index 0000000..35578d1 --- /dev/null +++ b/Framework/Utils/TimeRange.cs @@ -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; } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index be22f3e..b111492 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -1,5 +1,6 @@ using GethPlugin; using Logging; +using Utils; namespace CodexContractsPlugin { @@ -12,6 +13,8 @@ namespace CodexContractsPlugin string MintTestTokens(EthAddress ethAddress, TestToken testTokens); TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); + + void GetStorageRequests(TimeRange range); } public class CodexContractsAccess : ICodexContracts @@ -30,8 +33,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 +43,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 +53,19 @@ 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 void GetStorageRequests(TimeRange timeRange) + { + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var iii = 0; + } + + private ContractInteractions StartInteraction() + { + return new ContractInteractions(log, gethNode); + } } } diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 126792e..9ec5e76 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -4,6 +4,7 @@ using Logging; using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using NethereumWorkflow; +using Utils; namespace GethPlugin { @@ -22,7 +23,7 @@ namespace GethPlugin bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new(); - List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new(); + List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -141,9 +142,9 @@ namespace GethPlugin return StartInteraction().GetEvents(address, fromBlockNumber, toBlockNumber); } - public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { - return StartInteraction().GetEvents(address, from, to); + return StartInteraction().GetEvents(address, timeRange); } protected abstract NethereumInteraction StartInteraction(); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 3976d46..59993dd 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -95,7 +95,9 @@ namespace CodexTests.BasicTests AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage."); AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage."); - CheckLogForErrors(seller, buyer); + //CheckLogForErrors(seller, buyer); + + contracts.GetStorageRequests(new TimeRange(Get().TestStart, DateTime.UtcNow)); } [Test] diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 45cfff4..3ed08bb 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -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 metadata; private readonly List runningContainers = new List(); @@ -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) From 2be31a4d3bbd9c2193fa1c707b22a6c6e4f0594a Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 10:33:44 +0100 Subject: [PATCH 04/22] Cleanup --- Framework/NethereumWorkflow/BlockTimeEntry.cs | 22 +++ .../NethereumWorkflow/BlockTimeFinder.cs | 129 ++++++++---------- 2 files changed, 80 insertions(+), 71 deletions(-) create mode 100644 Framework/NethereumWorkflow/BlockTimeEntry.cs diff --git a/Framework/NethereumWorkflow/BlockTimeEntry.cs b/Framework/NethereumWorkflow/BlockTimeEntry.cs new file mode 100644 index 0000000..03817ae --- /dev/null +++ b/Framework/NethereumWorkflow/BlockTimeEntry.cs @@ -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")}"; + } + } + } +} diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index 0d429f5..3f39c26 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -1,34 +1,18 @@ using Logging; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; +using Org.BouncyCastle.Asn1.X509; using Utils; namespace NethereumWorkflow { - public class BlockTimeFinder + public partial class BlockTimeFinder { - private 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")}"; - } - } - private const ulong FetchRange = 6; private const int MaxEntries = 1024; + private static readonly Dictionary entries = new Dictionary(); private readonly Web3 web3; private readonly ILog log; - private static readonly Dictionary entries = new Dictionary(); public BlockTimeFinder(Web3 web3, ILog log) { @@ -45,16 +29,9 @@ namespace NethereumWorkflow var closestBefore = FindClosestBeforeEntry(moment); var closestAfter = FindClosestAfterEntry(moment); - if (closestBefore == null || closestAfter == null) - { - FetchBlocksAround(moment); - return GetHighestBlockNumberBefore(moment); - } - - log.Log("Closest before: " + closestBefore); - log.Log("Closest after: " + closestAfter); - - if (closestBefore.Utc < moment && + if (closestBefore != null && + closestAfter != null && + closestBefore.Utc < moment && closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { @@ -75,16 +52,9 @@ namespace NethereumWorkflow var closestBefore = FindClosestBeforeEntry(moment); var closestAfter = FindClosestAfterEntry(moment); - if (closestBefore == null || closestAfter == null) - { - FetchBlocksAround(moment); - return GetLowestBlockNumberAfter(moment); - } - - log.Log("Closest before: " + closestBefore); - log.Log("Closest after: " + closestAfter); - - if (closestBefore.Utc < moment && + if (closestBefore != null && + closestAfter != null && + closestBefore.Utc < moment && closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { @@ -98,12 +68,52 @@ namespace NethereumWorkflow private void FetchBlocksAround(DateTime moment) { - log.Log("Fetching..."); - var timePerBlock = EstimateTimePerBlock(); 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); @@ -112,36 +122,7 @@ namespace NethereumWorkflow double numberOfBlocksDifference = secondsDifference / secondsPerBlock; var blockDifference = Convert.ToUInt64(numberOfBlocksDifference); if (blockDifference < 1) blockDifference = 1; - - var fetchUp = FetchRange; - var fetchDown = FetchRange; - var target = max - blockDifference; - log.Log("up - target: " + target); - while (fetchUp > 0) - { - if (!entries.ContainsKey(target)) - { - var newBlock = AddBlockNumber(target); - if (newBlock != null) fetchUp--; - else fetchUp = 0; - } - target++; - //if (target >= max) fetchUp = 0; - } - - target = max - blockDifference - 1; - log.Log("down - target: " + target); - while (fetchDown > 0) - { - if (!entries.ContainsKey(target)) - { - var newBlock = AddBlockNumber(target); - if (newBlock != null) fetchDown--; - else fetchDown = 0; - } - target--; - //if (target <= 0) fetchDown = 0; - } + return blockDifference; } private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock) @@ -158,6 +139,8 @@ namespace NethereumWorkflow if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries."); Thread.Sleep(timePerBlock); } + max = entries.Keys.Max(); + latest = entries[max]; } } @@ -191,7 +174,11 @@ namespace NethereumWorkflow { 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; From 75757e37fbd267db2395cdd89d2527c3b5c2372c Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 10:55:29 +0100 Subject: [PATCH 05/22] Can fetch storage requests from chain --- Framework/NethereumWorkflow/BlockTimeFinder.cs | 1 - .../CodexContractsPlugin/CodexContractsAccess.cs | 15 ++++++++++----- .../CodexContractsPlugin/ContractInteractions.cs | 15 ++++++++++++++- Tests/CodexTests/BasicTests/ExampleTests.cs | 8 ++++++-- Tests/DistTestCore/DistTest.cs | 5 +++++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index 3f39c26..9f6481e 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -1,7 +1,6 @@ using Logging; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; -using Org.BouncyCastle.Asn1.X509; using Utils; namespace NethereumWorkflow diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index b111492..d6813b4 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -1,4 +1,5 @@ -using GethPlugin; +using CodexContractsPlugin.Marketplace; +using GethPlugin; using Logging; using Utils; @@ -14,7 +15,7 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); - void GetStorageRequests(TimeRange range); + Request[] GetStorageRequests(TimeRange range); } public class CodexContractsAccess : ICodexContracts @@ -57,10 +58,14 @@ namespace CodexContractsPlugin return balance.TestTokens(); } - public void GetStorageRequests(TimeRange timeRange) + public Request[] GetStorageRequests(TimeRange timeRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); - var iii = 0; + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var i = StartInteraction(); + return events + .Select(e => i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId)) + .Select(r => r.ReturnValue1) + .ToArray(); } private ContractInteractions StartInteraction() diff --git a/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs b/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs index 4af2ac6..93045db 100644 --- a/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs +++ b/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs @@ -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(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(marketplaceAddress, func); + } + public bool IsSynced(string marketplaceAddress, string marketplaceAbi) { log.Debug(); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 59993dd..9b39af3 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -88,6 +88,12 @@ namespace CodexTests.BasicTests purchaseContract.WaitForStorageContractStarted(fileSize); + var requests = contracts.GetStorageRequests(GetTestRunTimeRange()); + Assert.That(requests.Length, Is.EqualTo(1)); + var request = requests.Single(); + Assert.That(request.Client, Is.EqualTo(buyer.EthAddress.Address)); + Assert.That(request.Ask.Slots, Is.EqualTo(1)); + AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); purchaseContract.WaitForStorageContractFinished(); @@ -96,8 +102,6 @@ namespace CodexTests.BasicTests AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage."); //CheckLogForErrors(seller, buyer); - - contracts.GetStorageRequests(new TimeRange(Get().TestStart, DateTime.UtcNow)); } [Test] diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 06014e0..5237bbc 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -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) { } From 55be07d711aaff8aa369d9d1089891a9a2f5ccaa Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 11:34:23 +0100 Subject: [PATCH 06/22] Implements getting slot host address --- .../CodexContractsAccess.cs | 31 +++++++++++++++++-- .../Marketplace/Customizations.cs | 13 ++++++++ ProjectPlugins/GethPlugin/EthAddress.cs | 11 +++++++ Tests/CodexTests/BasicTests/ExampleTests.cs | 5 ++- 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index d6813b4..b5c920f 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -1,6 +1,9 @@ using CodexContractsPlugin.Marketplace; using GethPlugin; using Logging; +using Nethereum.ABI; +using Nethereum.Util; +using NethereumWorkflow; using Utils; namespace CodexContractsPlugin @@ -16,6 +19,7 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(EthAddress ethAddress); Request[] GetStorageRequests(TimeRange range); + EthAddress GetSlotHost(Request storageRequest, decimal slotIndex); } public class CodexContractsAccess : ICodexContracts @@ -62,12 +66,35 @@ namespace CodexContractsPlugin { var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); var i = StartInteraction(); + return events - .Select(e => i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId)) - .Select(r => r.ReturnValue1) + .Select(e => + { + var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId); + var request = requestEvent.ReturnValue1; + request.RequestId = e.Event.RequestId; + return request; + }) .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 + }; + return new EthAddress(gethNode.Call(Deployment.MarketplaceAddress, func)); + } + private ContractInteractions StartInteraction() { return new ContractInteractions(log, gethNode); diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs new file mode 100644 index 0000000..d75d712 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -0,0 +1,13 @@ +#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 byte[] RequestId { get; set; } + + public EthAddress ClientAddress { get { return new EthAddress(Client); } } + } +} +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/ProjectPlugins/GethPlugin/EthAddress.cs b/ProjectPlugins/GethPlugin/EthAddress.cs index 2954c48..bc660b6 100644 --- a/ProjectPlugins/GethPlugin/EthAddress.cs +++ b/ProjectPlugins/GethPlugin/EthAddress.cs @@ -14,6 +14,17 @@ 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; diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 9b39af3..5fe34ef 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -91,11 +91,14 @@ namespace CodexTests.BasicTests var requests = contracts.GetStorageRequests(GetTestRunTimeRange()); Assert.That(requests.Length, Is.EqualTo(1)); var request = requests.Single(); - Assert.That(request.Client, Is.EqualTo(buyer.EthAddress.Address)); + 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 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."); From 391a2653d976021fb95dad371cb77e2142a9975e Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 13:21:53 +0100 Subject: [PATCH 07/22] Sets up getting of slot filled and freed events --- .../NethereumWorkflow/NethereumInteraction.cs | 7 ++++ .../CodexContractsAccess.cs | 40 +++++++++++++++++-- .../Marketplace/Customizations.cs | 12 ++++++ ProjectPlugins/GethPlugin/GethNode.cs | 7 ++++ Tests/CodexTests/BasicTests/ExampleTests.cs | 8 ++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 172e9ce..0a8c2e4 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -3,6 +3,7 @@ using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; +using System.Runtime.CompilerServices; using Utils; namespace NethereumWorkflow @@ -55,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(); diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index b5c920f..d6ba837 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -2,6 +2,7 @@ using GethPlugin; using Logging; using Nethereum.ABI; +using Nethereum.Hex.HexTypes; using Nethereum.Util; using NethereumWorkflow; using Utils; @@ -20,6 +21,8 @@ namespace CodexContractsPlugin Request[] GetStorageRequests(TimeRange range); EthAddress GetSlotHost(Request storageRequest, decimal slotIndex); + SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange); + SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange); } public class CodexContractsAccess : ICodexContracts @@ -66,18 +69,41 @@ namespace CodexContractsPlugin { var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); var i = StartInteraction(); - return events .Select(e => { var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId); - var request = requestEvent.ReturnValue1; - request.RequestId = e.Event.RequestId; - return request; + var result = requestEvent.ReturnValue1; + result.BlockNumber = e.Log.BlockNumber.ToUlong(); + result.RequestId = e.Event.RequestId; + return result; }) .ToArray(); } + public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange) + { + var events = gethNode.GetEvents(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(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(); @@ -95,6 +121,12 @@ namespace CodexContractsPlugin return new EthAddress(gethNode.Call(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); diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index d75d712..088909d 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -5,9 +5,21 @@ 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 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. diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 9ec5e76..2ae834d 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -3,6 +3,7 @@ using KubernetesWorkflow.Types; using Logging; using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; +using Nethereum.RPC.Eth.DTOs; using NethereumWorkflow; using Utils; @@ -19,6 +20,7 @@ namespace GethPlugin string SendEth(EthAddress account, Ether eth); TResult Call(string contractAddress, TFunction function) where TFunction : FunctionMessage, new(); string SendTransaction(string contractAddress, TFunction function) where TFunction : FunctionMessage, new(); + Transaction GetTransaction(string transactionHash); decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); @@ -127,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(); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 5fe34ef..ef3f7ac 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -3,6 +3,7 @@ using CodexPlugin; using DistTestCore; using GethPlugin; using MetricsPlugin; +using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; @@ -96,6 +97,13 @@ namespace CodexTests.BasicTests AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); + 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)); From 2f10b302835fa8dc4e5d0e62c9dcacf2eec360cc Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 15:56:03 +0100 Subject: [PATCH 08/22] Sets up rewards api and handling. --- .../Utils}/TaskFactory.cs | 2 +- Framework/Utils/Time.cs | 5 + ProjectPlugins/GethPlugin/EthAddress.cs | 2 +- .../ContinuousTestRunner.cs | 1 + Tests/CodexContinuousTests/SingleTestRun.cs | 1 + Tests/CodexContinuousTests/TestLoop.cs | 1 + Tools/BiblioTech/CommandHandler.cs | 6 ++ Tools/BiblioTech/Configuration.cs | 4 + Tools/BiblioTech/Program.cs | 1 + Tools/BiblioTech/Rewards/GiveRewards.cs | 13 +++ Tools/BiblioTech/Rewards/RewardsApi.cs | 93 +++++++++++++++++++ Tools/BiblioTech/Rewards/RoleController.cs | 82 ++++++++++++++++ Tools/BiblioTech/UserRepo.cs | 52 ++++++----- 13 files changed, 236 insertions(+), 27 deletions(-) rename {Tests/CodexContinuousTests => Framework/Utils}/TaskFactory.cs (97%) create mode 100644 Tools/BiblioTech/Rewards/GiveRewards.cs create mode 100644 Tools/BiblioTech/Rewards/RewardsApi.cs create mode 100644 Tools/BiblioTech/Rewards/RoleController.cs diff --git a/Tests/CodexContinuousTests/TaskFactory.cs b/Framework/Utils/TaskFactory.cs similarity index 97% rename from Tests/CodexContinuousTests/TaskFactory.cs rename to Framework/Utils/TaskFactory.cs index e0be06e..44ff489 100644 --- a/Tests/CodexContinuousTests/TaskFactory.cs +++ b/Framework/Utils/TaskFactory.cs @@ -1,4 +1,4 @@ -namespace ContinuousTests +namespace Utils { public class TaskFactory { diff --git a/Framework/Utils/Time.cs b/Framework/Utils/Time.cs index ca4e115..82a836e 100644 --- a/Framework/Utils/Time.cs +++ b/Framework/Utils/Time.cs @@ -13,6 +13,11 @@ return task.Result; } + public static void Wait(Task task) + { + task.Wait(); + } + public static string FormatDuration(TimeSpan d) { var result = ""; diff --git a/ProjectPlugins/GethPlugin/EthAddress.cs b/ProjectPlugins/GethPlugin/EthAddress.cs index bc660b6..803a1f7 100644 --- a/ProjectPlugins/GethPlugin/EthAddress.cs +++ b/ProjectPlugins/GethPlugin/EthAddress.cs @@ -9,7 +9,7 @@ { public EthAddress(string address) { - Address = address; + Address = address.ToLowerInvariant(); } public string Address { get; } diff --git a/Tests/CodexContinuousTests/ContinuousTestRunner.cs b/Tests/CodexContinuousTests/ContinuousTestRunner.cs index b591d30..c79fc71 100644 --- a/Tests/CodexContinuousTests/ContinuousTestRunner.cs +++ b/Tests/CodexContinuousTests/ContinuousTestRunner.cs @@ -3,6 +3,7 @@ using DistTestCore.Logs; using Logging; using Newtonsoft.Json; using Utils; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tests/CodexContinuousTests/SingleTestRun.cs b/Tests/CodexContinuousTests/SingleTestRun.cs index 39b45e5..d7ccd80 100644 --- a/Tests/CodexContinuousTests/SingleTestRun.cs +++ b/Tests/CodexContinuousTests/SingleTestRun.cs @@ -6,6 +6,7 @@ using CodexPlugin; using DistTestCore.Logs; using Core; using KubernetesWorkflow.Types; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tests/CodexContinuousTests/TestLoop.cs b/Tests/CodexContinuousTests/TestLoop.cs index 7e44f73..46b4e29 100644 --- a/Tests/CodexContinuousTests/TestLoop.cs +++ b/Tests/CodexContinuousTests/TestLoop.cs @@ -1,5 +1,6 @@ using DistTestCore.Logs; using Logging; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index 800ac4b..5669636 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -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(guild); + 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) diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index bce62ea..f324d23 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -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 diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 22e68f8..f5c897c 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -1,5 +1,6 @@ using ArgsUniform; using BiblioTech.Commands; +using BiblioTech.Rewards; using Discord; using Discord.WebSocket; using Logging; diff --git a/Tools/BiblioTech/Rewards/GiveRewards.cs b/Tools/BiblioTech/Rewards/GiveRewards.cs new file mode 100644 index 0000000..18c300a --- /dev/null +++ b/Tools/BiblioTech/Rewards/GiveRewards.cs @@ -0,0 +1,13 @@ +namespace BiblioTech.Rewards +{ + public class GiveRewards + { + public Reward[] Rewards { get; set; } = Array.Empty(); + } + + public class Reward + { + public ulong RewardId { get; set; } + public string[] UserAddresses { get; set; } = Array.Empty(); + } +} diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs new file mode 100644 index 0000000..30eca5b --- /dev/null +++ b/Tools/BiblioTech/Rewards/RewardsApi.cs @@ -0,0 +1,93 @@ +using Newtonsoft.Json; +using System.Net; +using TaskFactory = Utils.TaskFactory; + +namespace BiblioTech.Rewards +{ + public interface IDiscordRoleController + { + void GiveRole(ulong roleId, UserData userData); + } + + 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); + } + 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 void HandleConnection(HttpListenerContext context) + { + var reader = new StreamReader(context.Request.InputStream); + var content = reader.ReadToEnd(); + + var rewards = JsonConvert.DeserializeObject(content); + if (rewards != null) ProcessRewards(rewards); + } + + private void ProcessRewards(GiveRewards rewards) + { + foreach (var reward in rewards.Rewards) ProcessReward(reward); + } + + private void ProcessReward(Reward reward) + { + foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress); + } + + private void GiveRoleToUser(ulong rewardId, string userAddress) + { + var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress)); + if (userData == null) return; + + roleController.GiveRole(rewardId, userData); + } + } +} diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs new file mode 100644 index 0000000..e437e2e --- /dev/null +++ b/Tools/BiblioTech/Rewards/RoleController.cs @@ -0,0 +1,82 @@ +using Discord.WebSocket; +using Utils; + +namespace BiblioTech.Rewards +{ + public class RoleController : IDiscordRoleController + { + private const string UsernameTag = ""; + private readonly SocketGuild guild; + private readonly SocketTextChannel? rewardsChannel; + + private readonly RoleReward[] roleRewards = new[] + { + new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!") + }; + + public RoleController(SocketGuild guild) + { + this.guild = guild; + + if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName)) + { + rewardsChannel = guild.TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName); + } + } + + public void GiveRole(ulong roleId, UserData userData) + { + var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId); + if (reward == null) return; + + var user = guild.Users.SingleOrDefault(u => u.Id == userData.DiscordId); + if (user == null) return; + + var role = guild.Roles.SingleOrDefault(r => r.Id == roleId); + if (role == null) return; + + + GiveRole(user, role); + SendNotification(reward, userData, user, role); + } + + private void GiveRole(SocketGuildUser user, SocketRole role) + { + try + { + Time.Wait(user.AddRoleAsync(role)); + } + catch (Exception ex) + { + Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}"); + } + } + + private void SendNotification(RoleReward reward, UserData userData, SocketGuildUser user, SocketRole role) + { + try + { + if (userData.NotificationsEnabled && rewardsChannel != null) + { + Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName))); + } + } + catch (Exception ex) + { + Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}"); + } + } + } + + public class RoleReward + { + public RoleReward(ulong roleId, string message) + { + RoleId = roleId; + Message = message; + } + + public ulong RoleId { get; } + public string Message { get; } + } +} diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index 23b25d5..0ce7a34 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -96,6 +96,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(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) @@ -132,34 +155,11 @@ namespace BiblioTech private UserData CreateAndSaveNewUserData(IUser user) { - var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List(), new List()); + var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List(), new List(), 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(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 +185,7 @@ namespace BiblioTech public class UserData { - public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents) + public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents, bool notificationsEnabled) { DiscordId = discordId; Name = name; @@ -193,6 +193,7 @@ namespace BiblioTech CurrentAddress = currentAddress; AssociateEvents = associateEvents; MintEvents = mintEvents; + NotificationsEnabled = notificationsEnabled; } public ulong DiscordId { get; } @@ -201,6 +202,7 @@ namespace BiblioTech public EthAddress? CurrentAddress { get; set; } public List AssociateEvents { get; } public List MintEvents { get; } + public bool NotificationsEnabled { get; } public string[] CreateOverview() { From 29fa554146160849911f20924acfd1aa26f84de7 Mon Sep 17 00:00:00 2001 From: benbierens Date: Thu, 18 Jan 2024 09:55:07 +0100 Subject: [PATCH 09/22] Adds log topics for block exchange --- ProjectPlugins/CodexPlugin/CodexSetup.cs | 8 ++++++++ ProjectPlugins/CodexPlugin/CodexStartupConfig.cs | 13 +++++++++++++ Tests/CodexTests/BasicTests/ExampleTests.cs | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index a491fb3..ccac252 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -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 diff --git a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs index ed840d0..bd508c1 100644 --- a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs +++ b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs @@ -54,10 +54,23 @@ namespace CodexPlugin "websock", "ws-session" }; + var blockExchangeTopics = new[] + { + "codex", + "pendingblocks", + "peerctxstore", + "discoveryengine", + "repostore" + }; 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; } diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index ef3f7ac..7916d38 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -112,6 +112,11 @@ namespace CodexTests.BasicTests AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage."); AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage."); + 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); } From f7fcef56c738831c2188985d08a5ea300e550fa3 Mon Sep 17 00:00:00 2001 From: benbierens Date: Thu, 18 Jan 2024 10:24:59 +0100 Subject: [PATCH 10/22] upgrades log filtering --- ProjectPlugins/CodexPlugin/CodexStartupConfig.cs | 10 ++++++++-- Tests/CodexTests/BasicTests/ExampleTests.cs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs index bd508c1..1210968 100644 --- a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs +++ b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs @@ -52,7 +52,11 @@ namespace CodexPlugin "connection", "connmanager", "websock", - "ws-session" + "ws-session", + "dialer", + "muxedupgrade", + "upgrade", + "identify" }; var blockExchangeTopics = new[] { @@ -60,7 +64,9 @@ namespace CodexPlugin "pendingblocks", "peerctxstore", "discoveryengine", - "repostore" + "blockexcengine", + "blockexcnetwork", + "blockexcnetworkpeer" }; level = $"{level};" + diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 7916d38..2f01ae2 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -60,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)); From c6a7489f11900349a6678a718b920970fdc11f03 Mon Sep 17 00:00:00 2001 From: benbierens Date: Sat, 20 Jan 2024 13:07:56 +0100 Subject: [PATCH 11/22] wip --- Tools/BiblioTech/CommandHandler.cs | 2 +- Tools/BiblioTech/Program.cs | 1 - Tools/BiblioTech/Rewards/RewardsApi.cs | 3 ++- Tools/BiblioTech/Rewards/RoleController.cs | 28 +++++++++++++++------- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index 5669636..f21e486 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -26,7 +26,7 @@ namespace BiblioTech Program.AdminChecker.SetGuild(guild); Program.Log.Log($"Initializing for guild: '{guild.Name}'"); - var roleController = new RoleController(guild); + var roleController = new RoleController(client); var rewardsApi = new RewardsApi(roleController); var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray(); diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index f5c897c..22e68f8 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -1,6 +1,5 @@ using ArgsUniform; using BiblioTech.Commands; -using BiblioTech.Rewards; using Discord; using Discord.WebSocket; using Logging; diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs index 30eca5b..657112a 100644 --- a/Tools/BiblioTech/Rewards/RewardsApi.cs +++ b/Tools/BiblioTech/Rewards/RewardsApi.cs @@ -74,6 +74,7 @@ namespace BiblioTech.Rewards private void ProcessRewards(GiveRewards rewards) { + Program.Log.Log("Processing: " + JsonConvert.SerializeObject(rewards)); foreach (var reward in rewards.Rewards) ProcessReward(reward); } @@ -85,7 +86,7 @@ namespace BiblioTech.Rewards private void GiveRoleToUser(ulong rewardId, string userAddress) { var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress)); - if (userData == null) return; + if (userData == null) { Program.Log.Log("no userdata"); return; } roleController.GiveRole(rewardId, userData); } diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs index e437e2e..0aedfb6 100644 --- a/Tools/BiblioTech/Rewards/RoleController.cs +++ b/Tools/BiblioTech/Rewards/RoleController.cs @@ -6,7 +6,7 @@ namespace BiblioTech.Rewards public class RoleController : IDiscordRoleController { private const string UsernameTag = ""; - private readonly SocketGuild guild; + private readonly DiscordSocketClient client; private readonly SocketTextChannel? rewardsChannel; private readonly RoleReward[] roleRewards = new[] @@ -14,27 +14,31 @@ namespace BiblioTech.Rewards new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!") }; - public RoleController(SocketGuild guild) + public RoleController(DiscordSocketClient client) { - this.guild = guild; + this.client = client; if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName)) { - rewardsChannel = guild.TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName); + rewardsChannel = GetGuild().TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName); } } public void GiveRole(ulong roleId, UserData userData) { var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId); - if (reward == null) return; + if (reward == null) { Program.Log.Log("no reward"); return; }; - var user = guild.Users.SingleOrDefault(u => u.Id == userData.DiscordId); - if (user == null) return; + var guild = GetGuild(); - var role = guild.Roles.SingleOrDefault(r => r.Id == roleId); - if (role == null) return; + var user = guild.GetUser(userData.DiscordId); + if (user == null) { Program.Log.Log("no user"); return; }; + var role = guild.GetRole(roleId); + if (role == null) { Program.Log.Log("no role"); return; }; + + Program.Log.Log($"User has roles: {string.Join(",", user.Roles.Select(r => r.Name + "=" + r.Id))}"); + if (user.Roles.Any(r => r.Id == role.Id)) { Program.Log.Log("already has"); return; }; GiveRole(user, role); SendNotification(reward, userData, user, role); @@ -44,6 +48,7 @@ namespace BiblioTech.Rewards { try { + Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}"); Time.Wait(user.AddRoleAsync(role)); } catch (Exception ex) @@ -66,6 +71,11 @@ namespace BiblioTech.Rewards Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}"); } } + + private SocketGuild GetGuild() + { + return client.Guilds.Single(g => g.Name == Program.Config.ServerName); + } } public class RoleReward From 1b7c11b849bc4ce2cd9b1469832e77bf5f454503 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 22 Jan 2024 10:27:07 +0100 Subject: [PATCH 12/22] Setting up rewards --- GethConnector/GethConnector.cs | 39 ++++ GethConnector/GethConnector.csproj | 14 ++ GethConnector/GethInput.cs | 52 ++++++ TestNetRewarder/BotClient.cs | 49 +++++ TestNetRewarder/Configuration.cs | 30 +++ TestNetRewarder/Program.cs | 83 +++++++++ TestNetRewarder/TestNetRewarder.csproj | 16 ++ TestNetRewarder/TimeSegmenter.cs | 51 +++++ Tools/BiblioTech/BaseGethCommand.cs | 77 +------- Tools/BiblioTech/BiblioTech.csproj | 1 + Tools/BiblioTech/Commands/NotifyCommand.cs | 24 +++ .../Commands/UserAssociateCommand.cs | 13 +- Tools/BiblioTech/Options/BoolOption.cs | 23 +++ Tools/BiblioTech/Program.cs | 6 +- Tools/BiblioTech/Rewards/GiveRewards.cs | 13 -- .../BiblioTech/Rewards/GiveRewardsCommand.cs | 18 ++ Tools/BiblioTech/Rewards/RewardsApi.cs | 50 +++-- Tools/BiblioTech/Rewards/RewardsRepo.cs | 48 +++++ Tools/BiblioTech/Rewards/RoleController.cs | 176 +++++++++++++----- Tools/BiblioTech/UserRepo.cs | 18 +- cs-codex-dist-testing.sln | 14 ++ 21 files changed, 665 insertions(+), 150 deletions(-) create mode 100644 GethConnector/GethConnector.cs create mode 100644 GethConnector/GethConnector.csproj create mode 100644 GethConnector/GethInput.cs create mode 100644 TestNetRewarder/BotClient.cs create mode 100644 TestNetRewarder/Configuration.cs create mode 100644 TestNetRewarder/Program.cs create mode 100644 TestNetRewarder/TestNetRewarder.csproj create mode 100644 TestNetRewarder/TimeSegmenter.cs create mode 100644 Tools/BiblioTech/Commands/NotifyCommand.cs create mode 100644 Tools/BiblioTech/Options/BoolOption.cs delete mode 100644 Tools/BiblioTech/Rewards/GiveRewards.cs create mode 100644 Tools/BiblioTech/Rewards/GiveRewardsCommand.cs create mode 100644 Tools/BiblioTech/Rewards/RewardsRepo.cs diff --git a/GethConnector/GethConnector.cs b/GethConnector/GethConnector.cs new file mode 100644 index 0000000..c7c7687 --- /dev/null +++ b/GethConnector/GethConnector.cs @@ -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; + } + } +} diff --git a/GethConnector/GethConnector.csproj b/GethConnector/GethConnector.csproj new file mode 100644 index 0000000..b1bdda3 --- /dev/null +++ b/GethConnector/GethConnector.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/GethConnector/GethInput.cs b/GethConnector/GethInput.cs new file mode 100644 index 0000000..e38af8a --- /dev/null +++ b/GethConnector/GethInput.cs @@ -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(); + 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 error, string name) + { + var result = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set."); + return result; + } + } +} diff --git a/TestNetRewarder/BotClient.cs b/TestNetRewarder/BotClient.cs new file mode 100644 index 0000000..6907fae --- /dev/null +++ b/TestNetRewarder/BotClient.cs @@ -0,0 +1,49 @@ +using BiblioTech.Rewards; +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 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 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}"; + } + } +} diff --git a/TestNetRewarder/Configuration.cs b/TestNetRewarder/Configuration.cs new file mode 100644 index 0000000..cb9f95d --- /dev/null +++ b/TestNetRewarder/Configuration.cs @@ -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", false, "if not 0, Unix epoc timestamp of a moment in history on which processing should begin. (default 0)")] + public int CheckHistoryTimestamp { get; set; } = 0; + + public string LogPath + { + get + { + return Path.Combine(DataPath, "logs"); + } + } + } +} diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs new file mode 100644 index 0000000..a5dce56 --- /dev/null +++ b/TestNetRewarder/Program.cs @@ -0,0 +1,83 @@ +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 Task Main(string[] args) + { + var cts = new CancellationTokenSource(); + CancellationToken = cts.Token; + Console.CancelKeyPress += (sender, args) => cts.Cancel(); + + var uniformArgs = new ArgsUniform(PrintHelp, args); + Config = uniformArgs.Parse(true); + + Log = new LogSplitter( + new FileLog(Path.Combine(Config.LogPath, "testnetrewarder")), + new ConsoleLog() + ); + + EnsurePath(Config.DataPath); + EnsurePath(Config.LogPath); + + return new Program().MainAsync(); + } + + public async Task MainAsync() + { + Log.Log("Starting TestNet Rewarder..."); + var segmenter = new TimeSegmenter(Log, Config); + + while (!CancellationToken.IsCancellationRequested) + { + await segmenter.WaitForNextSegment(ProcessTimeSegment); + await Task.Delay(1000, CancellationToken); + } + } + + private async Task ProcessTimeSegment(TimeRange range) + { + try + { + var connector = GethConnector.GethConnector.Initialize(Log); + if (connector == null) return; + + var newRequests = connector.CodexContracts.GetStorageRequests(range); + foreach (var request in newRequests) + { + for (ulong i = 0; i < request.Ask.Slots; i++) + { + var host = connector.CodexContracts.GetSlotHost(request, i); + } + } + var newSlotsFilled = connector.CodexContracts.GetSlotFilledEvents(range); + var newSlotsFreed = connector.CodexContracts.GetSlotFreedEvents(range); + + // can we get them all? + } + catch (Exception ex) + { + Log.Error("Exception processing time segment: " + ex); + } + } + + private static void PrintHelp() + { + Log.Log("TestNet Rewarder"); + } + + private static void EnsurePath(string path) + { + if (Directory.Exists(path)) return; + Directory.CreateDirectory(path); + } + } +} diff --git a/TestNetRewarder/TestNetRewarder.csproj b/TestNetRewarder/TestNetRewarder.csproj new file mode 100644 index 0000000..75b2ee1 --- /dev/null +++ b/TestNetRewarder/TestNetRewarder.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + diff --git a/TestNetRewarder/TimeSegmenter.cs b/TestNetRewarder/TimeSegmenter.cs new file mode 100644 index 0000000..3608c79 --- /dev/null +++ b/TestNetRewarder/TimeSegmenter.cs @@ -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; + segmentSize = TimeSpan.FromSeconds(configuration.Interval); + if (configuration.CheckHistoryTimestamp != 0) + { + start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; + } + else + { + start = DateTime.UtcNow - segmentSize; + } + + log.Log("Starting time segments at " + start); + log.Log("Segment size: " + Time.FormatDuration(segmentSize)); + } + + public async Task WaitForNextSegment(Func onSegment) + { + var now = DateTime.UtcNow; + var end = start + segmentSize; + if (end > now) + { + // Wait for the entire time segment to be in the past. + var delay = (end - now).Add(TimeSpan.FromSeconds(3)); + await Task.Delay(delay, Program.CancellationToken); + } + + if (Program.CancellationToken.IsCancellationRequested) return; + + log.Log($"Time segment {start} to {end}"); + var range = new TimeRange(start, end); + start = end; + + await onSegment(range); + } + } +} diff --git a/Tools/BiblioTech/BaseGethCommand.cs b/Tools/BiblioTech/BaseGethCommand.cs index ecbdfec..4906995 100644 --- a/Tools/BiblioTech/BaseGethCommand.cs +++ b/Tools/BiblioTech/BaseGethCommand.cs @@ -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(); - 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 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()) { diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj index 9c32ad4..c941a71 100644 --- a/Tools/BiblioTech/BiblioTech.csproj +++ b/Tools/BiblioTech/BiblioTech.csproj @@ -10,6 +10,7 @@ + diff --git a/Tools/BiblioTech/Commands/NotifyCommand.cs b/Tools/BiblioTech/Commands/NotifyCommand.cs new file mode 100644 index 0000000..3e0bd15 --- /dev/null +++ b/Tools/BiblioTech/Commands/NotifyCommand.cs @@ -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!"); + } + } +} diff --git a/Tools/BiblioTech/Commands/UserAssociateCommand.cs b/Tools/BiblioTech/Commands/UserAssociateCommand.cs index c23718c..81e4646 100644 --- a/Tools/BiblioTech/Commands/UserAssociateCommand.cs +++ b/Tools/BiblioTech/Commands/UserAssociateCommand.cs @@ -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 { diff --git a/Tools/BiblioTech/Options/BoolOption.cs b/Tools/BiblioTech/Options/BoolOption.cs new file mode 100644 index 0000000..6318ee1 --- /dev/null +++ b/Tools/BiblioTech/Options/BoolOption.cs @@ -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 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; + } + } +} diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 22e68f8..7d6d690 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -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) ); diff --git a/Tools/BiblioTech/Rewards/GiveRewards.cs b/Tools/BiblioTech/Rewards/GiveRewards.cs deleted file mode 100644 index 18c300a..0000000 --- a/Tools/BiblioTech/Rewards/GiveRewards.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace BiblioTech.Rewards -{ - public class GiveRewards - { - public Reward[] Rewards { get; set; } = Array.Empty(); - } - - public class Reward - { - public ulong RewardId { get; set; } - public string[] UserAddresses { get; set; } = Array.Empty(); - } -} diff --git a/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs b/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs new file mode 100644 index 0000000..494285b --- /dev/null +++ b/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace BiblioTech.Rewards +{ + public class GiveRewardsCommand + { + public RewardUsersCommand[] Rewards { get; set; } = Array.Empty(); + } + + public class RewardUsersCommand + { + public ulong RewardId { get; set; } + public string[] UserAddresses { get; set; } = Array.Empty(); + + [JsonIgnore] + public UserData[] Users { get; set; } = Array.Empty(); + } +} diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs index 657112a..17d4362 100644 --- a/Tools/BiblioTech/Rewards/RewardsApi.cs +++ b/Tools/BiblioTech/Rewards/RewardsApi.cs @@ -6,7 +6,7 @@ namespace BiblioTech.Rewards { public interface IDiscordRoleController { - void GiveRole(ulong roleId, UserData userData); + Task GiveRewards(GiveRewardsCommand rewards); } public class RewardsApi @@ -49,7 +49,7 @@ namespace BiblioTech.Rewards var context = wait.Result; try { - HandleConnection(context); + HandleConnection(context).Wait(); } catch (Exception ex) { @@ -63,32 +63,50 @@ namespace BiblioTech.Rewards } } - private void HandleConnection(HttpListenerContext context) + private async Task HandleConnection(HttpListenerContext context) { - var reader = new StreamReader(context.Request.InputStream); + using var reader = new StreamReader(context.Request.InputStream); var content = reader.ReadToEnd(); - var rewards = JsonConvert.DeserializeObject(content); - if (rewards != null) ProcessRewards(rewards); + if (content == "Ping") + { + using var writer = new StreamWriter(context.Response.OutputStream); + writer.Write("Pong"); + return; + } + + if (!content.StartsWith("{")) return; + var rewards = JsonConvert.DeserializeObject(content); + if (rewards != null) + { + AttachUsers(rewards); + await ProcessRewards(rewards); + } } - private void ProcessRewards(GiveRewards rewards) + private void AttachUsers(GiveRewardsCommand rewards) { - Program.Log.Log("Processing: " + JsonConvert.SerializeObject(rewards)); - foreach (var reward in rewards.Rewards) ProcessReward(reward); + foreach (var reward in rewards.Rewards) + { + reward.Users = reward.UserAddresses.Select(GetUserFromAddress).Where(u => u != null).Cast().ToArray(); + } } - private void ProcessReward(Reward reward) + private UserData? GetUserFromAddress(string address) { - foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress); + try + { + return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address)); + } + catch + { + return null; + } } - private void GiveRoleToUser(ulong rewardId, string userAddress) + private async Task ProcessRewards(GiveRewardsCommand rewards) { - var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress)); - if (userData == null) { Program.Log.Log("no userdata"); return; } - - roleController.GiveRole(rewardId, userData); + await roleController.GiveRewards(rewards); } } } diff --git a/Tools/BiblioTech/Rewards/RewardsRepo.cs b/Tools/BiblioTech/Rewards/RewardsRepo.cs new file mode 100644 index 0000000..bbff34a --- /dev/null +++ b/Tools/BiblioTech/Rewards/RewardsRepo.cs @@ -0,0 +1,48 @@ +namespace BiblioTech.Rewards +{ + public class RewardsRepo + { + private static string Tag => RoleController.UsernameTag; + + public RoleRewardConfig[] Rewards { get; } + + public RewardsRepo() + { + Rewards = new[] + { + // Join reward + new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Hosting: + //// Filled any slot: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Finished any slot: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Finished a min-256MB min-8h slot: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Finished a min-64GB min-24h slot: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Oops: + //// Missed a storage proof: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Clienting: + //// Posted any contract: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Posted any contract that reached started state: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Started a contract with min-4 hosts, min-256MB per host, min-8h duration: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + + //// Started a contract with min-4 hosts, min-64GB per host, min-24h duration: + //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), + }; + } + } +} diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs index 0aedfb6..63274fd 100644 --- a/Tools/BiblioTech/Rewards/RoleController.cs +++ b/Tools/BiblioTech/Rewards/RoleController.cs @@ -1,18 +1,14 @@ -using Discord.WebSocket; -using Utils; +using Discord; +using Discord.WebSocket; namespace BiblioTech.Rewards { public class RoleController : IDiscordRoleController { - private const string UsernameTag = ""; + public const string UsernameTag = ""; private readonly DiscordSocketClient client; private readonly SocketTextChannel? rewardsChannel; - - private readonly RoleReward[] roleRewards = new[] - { - new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!") - }; + private readonly RewardsRepo repo = new RewardsRepo(); public RoleController(DiscordSocketClient client) { @@ -24,52 +20,62 @@ namespace BiblioTech.Rewards } } - public void GiveRole(ulong roleId, UserData userData) + public async Task GiveRewards(GiveRewardsCommand rewards) { - var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId); - if (reward == null) { Program.Log.Log("no reward"); return; }; - 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); - var user = guild.GetUser(userData.DiscordId); - if (user == null) { Program.Log.Log("no user"); return; }; - - var role = guild.GetRole(roleId); - if (role == null) { Program.Log.Log("no role"); return; }; - - Program.Log.Log($"User has roles: {string.Join(",", user.Roles.Select(r => r.Name + "=" + r.Id))}"); - if (user.Roles.Any(r => r.Id == role.Id)) { Program.Log.Log("already has"); return; }; - - GiveRole(user, role); - SendNotification(reward, userData, user, role); + await context.ProcessGiveRewardsCommand(rewards); } - private void GiveRole(SocketGuildUser user, SocketRole role) + private async Task> LoadAllUsers(SocketGuild guild) { - try + var result = new Dictionary(); + var users = guild.GetUsersAsync(); + await foreach (var ulist in users) { - Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}"); - Time.Wait(user.AddRoleAsync(role)); - } - catch (Exception ex) - { - Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}"); - } - } - - private void SendNotification(RoleReward reward, UserData userData, SocketGuildUser user, SocketRole role) - { - try - { - if (userData.NotificationsEnabled && rewardsChannel != null) + foreach (var u in ulist) { - Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName))); + result.Add(u.Id, u); } } - catch (Exception ex) + return result; + } + + private Dictionary LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards) + { + var result = new Dictionary(); + foreach (var r in rewards.Rewards) { - Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}"); + var role = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId); + if (role == null) + { + Program.Log.Log($"No RoleReward is configured for reward with id '{r.RewardId}'."); + } + else + { + if (role.SocketRole == null) + { + var socketRole = guild.GetRole(r.RewardId); + if (socketRole == null) + { + Program.Log.Log($"Guild Role by id '{r.RewardId}' not found."); + } + else + { + role.SocketRole = socketRole; + } + } + result.Add(role.RoleId, role); + } } + + return result; } private SocketGuild GetGuild() @@ -78,9 +84,90 @@ namespace BiblioTech.Rewards } } - public class RoleReward + public class RewardContext { - public RoleReward(ulong roleId, string message) + private readonly Dictionary users; + private readonly Dictionary roles; + private readonly SocketTextChannel? rewardsChannel; + + public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel) + { + this.users = users; + this.roles = roles; + this.rewardsChannel = rewardsChannel; + } + + public async Task ProcessGiveRewardsCommand(GiveRewardsCommand rewards) + { + foreach (var rewardCommand in rewards.Rewards) + { + if (roles.ContainsKey(rewardCommand.RewardId)) + { + var role = roles[rewardCommand.RewardId]; + await ProcessRewardCommand(role, rewardCommand); + } + } + } + + private async Task ProcessRewardCommand(RoleRewardConfig role, RewardUsersCommand reward) + { + foreach (var user in reward.Users) + { + await GiveReward(role, user); + } + } + + private async Task GiveReward(RoleRewardConfig 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.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(RoleRewardConfig reward, UserData userData, IGuildUser user) + { + try + { + if (userData.NotificationsEnabled && rewardsChannel != null) + { + var msg = reward.Message.Replace(RoleController.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}"); + } + } + } + + public class RoleRewardConfig + { + public RoleRewardConfig(ulong roleId, string message) { RoleId = roleId; Message = message; @@ -88,5 +175,6 @@ namespace BiblioTech.Rewards public ulong RoleId { get; } public string Message { get; } + public SocketRole? SocketRole { get; set; } } } diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index 0ce7a34..01afbe9 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -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? eth, Transaction? tokens) { lock (repoLock) @@ -133,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); @@ -202,7 +218,7 @@ namespace BiblioTech public EthAddress? CurrentAddress { get; set; } public List AssociateEvents { get; } public List MintEvents { get; } - public bool NotificationsEnabled { get; } + public bool NotificationsEnabled { get; set; } public string[] CreateOverview() { diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index 6b879f1..f5074ed 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -53,6 +53,10 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestNetRewarder", "TestNetRewarder\TestNetRewarder.csproj", "{27B56A82-E8CE-4B50-9746-D574BAD646A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GethConnector", "GethConnector\GethConnector.csproj", "{04F2D26E-0768-4F93-9A1A-834089646B56}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +147,14 @@ 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 + {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.Build.0 = Release|Any CPU + {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -169,6 +181,8 @@ 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} + {27B56A82-E8CE-4B50-9746-D574BAD646A2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {04F2D26E-0768-4F93-9A1A-834089646B56} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 678b719cef03ea89ebed5ec70fc59473bb6c734f Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 22 Jan 2024 11:47:28 +0100 Subject: [PATCH 13/22] wip --- .../CodexContractsAccess.cs | 1 + TestNetRewarder/Configuration.cs | 2 +- TestNetRewarder/TimeSegmenter.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index d6ba837..e4222d4 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -21,6 +21,7 @@ namespace CodexContractsPlugin Request[] GetStorageRequests(TimeRange range); EthAddress GetSlotHost(Request storageRequest, decimal slotIndex); + // add 'RequestFulfilled' to see request is started. SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange); SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange); } diff --git a/TestNetRewarder/Configuration.cs b/TestNetRewarder/Configuration.cs index cb9f95d..02401d2 100644 --- a/TestNetRewarder/Configuration.cs +++ b/TestNetRewarder/Configuration.cs @@ -16,7 +16,7 @@ namespace TestNetRewarder [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", false, "if not 0, Unix epoc timestamp of a moment in history on which processing should begin. (default 0)")] + [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 diff --git a/TestNetRewarder/TimeSegmenter.cs b/TestNetRewarder/TimeSegmenter.cs index 3608c79..a9ad71a 100644 --- a/TestNetRewarder/TimeSegmenter.cs +++ b/TestNetRewarder/TimeSegmenter.cs @@ -14,15 +14,10 @@ namespace TestNetRewarder 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); - if (configuration.CheckHistoryTimestamp != 0) - { - start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; - } - else - { - start = DateTime.UtcNow - segmentSize; - } + start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; log.Log("Starting time segments at " + start); log.Log("Segment size: " + Time.FormatDuration(segmentSize)); @@ -32,16 +27,21 @@ namespace TestNetRewarder { 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; - log.Log($"Time segment {start} to {end}"); + 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; From 890cff93d57e4a2da733b4daaa738ee33512e8a9 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 22 Jan 2024 16:05:04 +0100 Subject: [PATCH 14/22] Adds method for getting request-fulfilled events --- .../CodexContractsPlugin/CodexContractsAccess.cs | 15 +++++++++++++-- .../Marketplace/Customizations.cs | 5 +++++ Tests/CodexTests/BasicTests/ExampleTests.cs | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index e4222d4..5d03e0e 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -19,9 +19,9 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); - Request[] GetStorageRequests(TimeRange range); + Request[] GetStorageRequests(TimeRange timeRange); EthAddress GetSlotHost(Request storageRequest, decimal slotIndex); - // add 'RequestFulfilled' to see request is started. + RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange); SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange); SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange); } @@ -82,6 +82,17 @@ namespace CodexContractsPlugin .ToArray(); } + public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange) + { + var events = gethNode.GetEvents(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(Deployment.MarketplaceAddress, timeRange); diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index 088909d..dafd88a 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -11,6 +11,11 @@ namespace CodexContractsPlugin.Marketplace public EthAddress ClientAddress { get { return new EthAddress(Client); } } } + public partial class RequestFulfilledEventDTO + { + public ulong BlockNumber { get; set; } + } + public partial class SlotFilledEventDTO { public ulong BlockNumber { get; set; } diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 2f01ae2..a901343 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -98,6 +98,9 @@ namespace CodexTests.BasicTests 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(); From 00c720137a425000f89688871867b87cd50f7265 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 22 Jan 2024 16:27:32 +0100 Subject: [PATCH 15/22] Adds method for getting request state and receiving cancelled events --- .../CodexContractsAccess.cs | 31 +++++++++++++++++++ .../Marketplace/Customizations.cs | 5 +++ TestNetRewarder/Program.cs | 3 +- Tests/CodexTests/BasicTests/ExampleTests.cs | 2 ++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index 5d03e0e..a4140e3 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -21,11 +21,22 @@ namespace CodexContractsPlugin 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 { private readonly ILog log; @@ -93,6 +104,17 @@ namespace CodexContractsPlugin }).ToArray(); } + public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange) + { + var events = gethNode.GetEvents(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(Deployment.MarketplaceAddress, timeRange); @@ -133,6 +155,15 @@ namespace CodexContractsPlugin return new EthAddress(gethNode.Call(Deployment.MarketplaceAddress, func)); } + public RequestState GetRequestState(Request request) + { + var func = new RequestStateFunction + { + RequestId = request.RequestId + }; + return gethNode.Call(Deployment.MarketplaceAddress, func); + } + private EthAddress GetEthAddressFromTransaction(string transactionHash) { var transaction = gethNode.GetTransaction(transactionHash); diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index dafd88a..96490b7 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -16,6 +16,11 @@ namespace CodexContractsPlugin.Marketplace public ulong BlockNumber { get; set; } } + public partial class RequestCancelledEventDTO + { + public ulong BlockNumber { get; set; } + } + public partial class SlotFilledEventDTO { public ulong BlockNumber { get; set; } diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs index a5dce56..e813005 100644 --- a/TestNetRewarder/Program.cs +++ b/TestNetRewarder/Program.cs @@ -1,5 +1,4 @@ using ArgsUniform; -using GethConnector; using Logging; using Utils; @@ -67,6 +66,8 @@ namespace TestNetRewarder { Log.Error("Exception processing time segment: " + ex); } + + await Task.Delay(1); } private static void PrintHelp() diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index a901343..c73a913 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -93,6 +93,7 @@ namespace CodexTests.BasicTests 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)); @@ -115,6 +116,7 @@ namespace CodexTests.BasicTests 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)); var log = Ci.DownloadLog(seller); log.AssertLogContains("Received a request to store a slot!"); From f25bca727d146879c93006a784eb786ada977168 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 26 Jan 2024 11:27:37 -0500 Subject: [PATCH 16/22] Fixes crash in blocktime finder. Increases smart contract deploy timeout. --- .../NethereumWorkflow/BlockTimeFinder.cs | 53 ++++++++++++++----- .../CodexContractsStarter.cs | 2 +- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index 9f6481e..e7b8c4a 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -25,6 +25,20 @@ namespace NethereumWorkflow 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); @@ -39,15 +53,11 @@ namespace NethereumWorkflow } FetchBlocksAround(moment); - return GetHighestBlockNumberBefore(moment); + return GetHighestBlockBefore(moment); } - public ulong GetLowestBlockNumberAfter(DateTime moment) + private ulong GetLowestBlockAfter(DateTime moment) { - log.Log("Looking for lowest block after " + moment.ToString("o")); - AssertMomentIsInPast(moment); - Initialize(); - var closestBefore = FindClosestBeforeEntry(moment); var closestAfter = FindClosestAfterEntry(moment); @@ -62,12 +72,14 @@ namespace NethereumWorkflow } FetchBlocksAround(moment); - return GetLowestBlockNumberAfter(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(); @@ -157,14 +169,19 @@ namespace NethereumWorkflow if (entries.Count > MaxEntries) { + log.Debug("Entries cleared!"); entries.Clear(); Initialize(); } var time = GetTimestampFromBlock(blockNumber); - if (time == null) return null; + if (time == null) + { + log.Log("Failed to get block for number: " + blockNumber); + return null; + } var entry = new BlockTimeEntry(blockNumber, time.Value); - log.Log("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o")); + log.Debug("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o")); entries.Add(blockNumber, entry); return entry; } @@ -185,7 +202,9 @@ namespace NethereumWorkflow double numberOfBlocks = max - min; double secondsPerBlock = elapsedSeconds / numberOfBlocks; - return TimeSpan.FromSeconds(secondsPerBlock); + var result = TimeSpan.FromSeconds(secondsPerBlock); + if (result.TotalSeconds < 1.0) result = TimeSpan.FromSeconds(1.0); + return result; } private void Initialize() @@ -211,9 +230,17 @@ namespace NethereumWorkflow private DateTime? GetTimestampFromBlock(ulong blockNumber) { - 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; + 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) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs index acfc0ac..f7864de 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs @@ -85,7 +85,7 @@ namespace CodexContractsPlugin private void WaitUntil(Func predicate) { - Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2)); + Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2)); } private StartupConfig CreateStartupConfig(IGethNode gethNode) From 255e08a3017aa97852083f013d5c34ad4637b594 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 26 Jan 2024 17:29:57 -0500 Subject: [PATCH 17/22] Implements check classes --- .../CodexContractsAccess.cs | 8 +- TestNetRewarder/ChainState.cs | 35 ++++++ TestNetRewarder/Checks.cs | 105 ++++++++++++++++++ TestNetRewarder/HistoricState.cs | 68 ++++++++++++ TestNetRewarder/Program.cs | 25 +++-- 5 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 TestNetRewarder/ChainState.cs create mode 100644 TestNetRewarder/Checks.cs create mode 100644 TestNetRewarder/HistoricState.cs diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index a4140e3..97083fe 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -20,7 +20,7 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(EthAddress ethAddress); Request[] GetStorageRequests(TimeRange timeRange); - EthAddress GetSlotHost(Request storageRequest, decimal slotIndex); + EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex); RequestState GetRequestState(Request request); RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange); RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange); @@ -138,7 +138,7 @@ namespace CodexContractsPlugin }).ToArray(); } - public EthAddress GetSlotHost(Request storageRequest, decimal slotIndex) + public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex) { var encoder = new ABIEncode(); var encoded = encoder.GetABIEncoded( @@ -152,7 +152,9 @@ namespace CodexContractsPlugin { SlotId = hashed }; - return new EthAddress(gethNode.Call(Deployment.MarketplaceAddress, func)); + var address = gethNode.Call(Deployment.MarketplaceAddress, func); + if (string.IsNullOrEmpty(address)) return null; + return new EthAddress(address); } public RequestState GetRequestState(Request request) diff --git a/TestNetRewarder/ChainState.cs b/TestNetRewarder/ChainState.cs new file mode 100644 index 0000000..13356fc --- /dev/null +++ b/TestNetRewarder/ChainState.cs @@ -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; } + } +} diff --git a/TestNetRewarder/Checks.cs b/TestNetRewarder/Checks.cs new file mode 100644 index 0000000..e82c225 --- /dev/null +++ b/TestNetRewarder/Checks.cs @@ -0,0 +1,105 @@ +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 FinishedSlot(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 + { + public EthAddress[] Check(ChainState state) + { + return state.NewRequests.Select(r => r.ClientAddress).ToArray(); + } + } + + public class StartedContractCheck : ICheck + { + private readonly ulong minNumberOfHosts; + private readonly ByteSize minSlotSize; + private readonly TimeSpan minDuration; + + public StartedContract(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; + } + } +} diff --git a/TestNetRewarder/HistoricState.cs b/TestNetRewarder/HistoricState.cs new file mode 100644 index 0000000..b1ac699 --- /dev/null +++ b/TestNetRewarder/HistoricState.cs @@ -0,0 +1,68 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.Marketplace; +using GethPlugin; + +namespace TestNetRewarder +{ + public class HistoricState + { + private readonly List storageRequests = new List(); + + 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(); + } + + 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(); + + for (decimal i = 0; i < Request.Ask.Slots; i++) + { + var host = contracts.GetSlotHost(Request, i); + if (host != null) result.Add(host); + } + + return result.ToArray(); + } + } +} diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs index e813005..01945c7 100644 --- a/TestNetRewarder/Program.cs +++ b/TestNetRewarder/Program.cs @@ -1,4 +1,7 @@ using ArgsUniform; +using CodexContractsPlugin.Marketplace; +using CodexContractsPlugin; +using GethPlugin; using Logging; using Utils; @@ -49,18 +52,16 @@ namespace TestNetRewarder var connector = GethConnector.GethConnector.Initialize(Log); if (connector == null) return; - var newRequests = connector.CodexContracts.GetStorageRequests(range); - foreach (var request in newRequests) - { - for (ulong i = 0; i < request.Ask.Slots; i++) - { - var host = connector.CodexContracts.GetSlotHost(request, i); - } - } - var newSlotsFilled = connector.CodexContracts.GetSlotFilledEvents(range); - var newSlotsFreed = connector.CodexContracts.GetSlotFreedEvents(range); - - // can we get them all? + //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); + + + } catch (Exception ex) { From 4de2626a338f716d1a24d921a2e497957dde5b53 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 26 Jan 2024 18:17:56 -0500 Subject: [PATCH 18/22] wip --- TestNetRewarder/Checks.cs | 4 +- TestNetRewarder/Processor.cs | 74 +++++++++++++++++++++++++++++++++ TestNetRewarder/Program.cs | 38 +++++++---------- TestNetRewarder/RewardConfig.cs | 50 ++++++++++++++++++++++ 4 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 TestNetRewarder/Processor.cs create mode 100644 TestNetRewarder/RewardConfig.cs diff --git a/TestNetRewarder/Checks.cs b/TestNetRewarder/Checks.cs index e82c225..8563cf7 100644 --- a/TestNetRewarder/Checks.cs +++ b/TestNetRewarder/Checks.cs @@ -22,7 +22,7 @@ namespace TestNetRewarder private readonly ByteSize minSize; private readonly TimeSpan minDuration; - public FinishedSlot(ByteSize minSize, TimeSpan minDuration) + public FinishedSlotCheck(ByteSize minSize, TimeSpan minDuration) { this.minSize = minSize; this.minDuration = minDuration; @@ -66,7 +66,7 @@ namespace TestNetRewarder private readonly ByteSize minSlotSize; private readonly TimeSpan minDuration; - public StartedContract(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration) + public StartedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration) { this.minNumberOfHosts = minNumberOfHosts; this.minSlotSize = minSlotSize; diff --git a/TestNetRewarder/Processor.cs b/TestNetRewarder/Processor.cs new file mode 100644 index 0000000..a1418e3 --- /dev/null +++ b/TestNetRewarder/Processor.cs @@ -0,0 +1,74 @@ +using BiblioTech.Rewards; +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(); + foreach (var reward in rewardRepo.Rewards) + { + ProcessReward(outgoingRewards, reward, chainState); + } + + if (outgoingRewards.Any()) + { + await SendRewardsCommand(outgoingRewards); + } + } + + private async Task SendRewardsCommand(List outgoingRewards) + { + var cmd = new GiveRewardsCommand + { + Rewards = outgoingRewards.ToArray() + }; + + log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd)); + await Program.BotClient.SendRewards(cmd); + } + + private void ProcessReward(List outgoingRewards, RewardConfig reward, ChainState chainState) + { + var winningAddresses = reward.Check.Check(chainState); + if (winningAddresses.Any()) + { + outgoingRewards.Add(new RewardUsersCommand + { + RewardId = reward.RewardId, + UserAddresses = winningAddresses.Select(a => a.Address).ToArray() + }); + } + } + } +} diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs index 01945c7..ac248c1 100644 --- a/TestNetRewarder/Program.cs +++ b/TestNetRewarder/Program.cs @@ -1,7 +1,4 @@ using ArgsUniform; -using CodexContractsPlugin.Marketplace; -using CodexContractsPlugin; -using GethPlugin; using Logging; using Utils; @@ -12,6 +9,7 @@ namespace TestNetRewarder public static Configuration Config { get; private set; } = null!; public static ILog Log { get; private set; } = null!; public static CancellationToken CancellationToken { get; private set; } + public static BotClient BotClient { get; private set; } = null!; public static Task Main(string[] args) { @@ -27,6 +25,8 @@ namespace TestNetRewarder new ConsoleLog() ); + BotClient = new BotClient(Config, Log); + EnsurePath(Config.DataPath); EnsurePath(Config.LogPath); @@ -40,35 +40,27 @@ namespace TestNetRewarder while (!CancellationToken.IsCancellationRequested) { + await EnsureBotOnline(); await segmenter.WaitForNextSegment(ProcessTimeSegment); await Task.Delay(1000, CancellationToken); } } - private async Task ProcessTimeSegment(TimeRange range) + private async Task EnsureBotOnline() { - try + var start = DateTime.UtcNow; + while (! await BotClient.IsOnline() && !CancellationToken.IsCancellationRequested) { - var connector = GethConnector.GethConnector.Initialize(Log); - if (connector == null) return; - - //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); - - + 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); + } } - catch (Exception ex) - { - Log.Error("Exception processing time segment: " + ex); - } - - await Task.Delay(1); } private static void PrintHelp() diff --git a/TestNetRewarder/RewardConfig.cs b/TestNetRewarder/RewardConfig.cs new file mode 100644 index 0000000..9906359 --- /dev/null +++ b/TestNetRewarder/RewardConfig.cs @@ -0,0 +1,50 @@ +using Utils; + +namespace TestNetRewarder +{ + public class RewardConfig + { + public RewardConfig(ulong rewardId, ICheck check) + { + RewardId = rewardId; + Check = check; + } + + public ulong RewardId { get; } + public ICheck Check { get; } + } + + public class RewardRepo + { + public RewardConfig[] Rewards { get; } = new RewardConfig[] + { + // Filled any slot + new RewardConfig(123, new FilledAnySlotCheck()), + + // Finished any slot + new RewardConfig(124, new FinishedSlotCheck( + minSize: 0.Bytes(), + minDuration: TimeSpan.Zero)), + + // Finished a sizable slot + new RewardConfig(125, new FinishedSlotCheck( + minSize: 1.GB(), + minDuration: TimeSpan.FromHours(24.0))), + + // Posted any contract + new RewardConfig(126, new PostedContractCheck()), + + // Started any contract + new RewardConfig(127, new StartedContractCheck( + minNumberOfHosts: 1, + minSlotSize: 0.Bytes(), + minDuration: TimeSpan.Zero)), + + // Started a sizable contract + new RewardConfig(127, new StartedContractCheck( + minNumberOfHosts: 4, + minSlotSize: 1.GB(), + minDuration: TimeSpan.FromHours(24.0))) + }; + } +} From 43a160a9cc15ece34a274c162b423e01e84b026f Mon Sep 17 00:00:00 2001 From: benbierens Date: Sat, 27 Jan 2024 09:12:20 -0500 Subject: [PATCH 19/22] Wires everything up --- TestNetRewarder/Program.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs index ac248c1..6090f79 100644 --- a/TestNetRewarder/Program.cs +++ b/TestNetRewarder/Program.cs @@ -10,6 +10,7 @@ namespace TestNetRewarder 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) { @@ -26,6 +27,7 @@ namespace TestNetRewarder ); BotClient = new BotClient(Config, Log); + processor = new Processor(Log); EnsurePath(Config.DataPath); EnsurePath(Config.LogPath); @@ -35,18 +37,30 @@ namespace TestNetRewarder public async Task MainAsync() { + EnsureGethOnline(); + Log.Log("Starting TestNet Rewarder..."); var segmenter = new TimeSegmenter(Log, Config); while (!CancellationToken.IsCancellationRequested) { await EnsureBotOnline(); - await segmenter.WaitForNextSegment(ProcessTimeSegment); + await segmenter.WaitForNextSegment(processor.ProcessTimeSegment); await Task.Delay(1000, CancellationToken); } } - private async Task EnsureBotOnline() + 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) From acc9526cd5d8ce0102d480235677677fc1f3ae1d Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 29 Jan 2024 11:02:47 -0500 Subject: [PATCH 20/22] Extracts reward config from bot and rewarder --- TestNetRewarder/RewardConfig.cs | 50 -------- TestNetRewarder/TestNetRewarder.csproj | 16 --- Tools/BiblioTech/BiblioTech.csproj | 3 +- Tools/BiblioTech/Rewards/RewardsApi.cs | 24 +--- Tools/BiblioTech/Rewards/RewardsRepo.cs | 48 ------- Tools/BiblioTech/Rewards/RoleController.cs | 118 ++++++++++++------ Tools/DiscordRewards/CheckConfig.cs | 21 ++++ .../DiscordRewards/DiscordRewards.csproj | 3 +- .../GiveRewardsCommand.cs | 7 +- Tools/DiscordRewards/RewardConfig.cs | 18 +++ Tools/DiscordRewards/RewardRepo.cs | 53 ++++++++ .../GethConnector}/GethConnector.cs | 0 Tools/GethConnector/GethConnector.csproj | 15 +++ .../GethConnector}/GethInput.cs | 0 .../TestNetRewarder}/BotClient.cs | 2 +- .../TestNetRewarder}/ChainState.cs | 0 .../TestNetRewarder}/Checks.cs | 40 +++++- .../TestNetRewarder}/Configuration.cs | 0 .../TestNetRewarder}/HistoricState.cs | 0 .../TestNetRewarder}/Processor.cs | 30 ++++- .../TestNetRewarder}/Program.cs | 1 + Tools/TestNetRewarder/TestNetRewarder.csproj | 17 +++ .../TestNetRewarder}/TimeSegmenter.cs | 0 cs-codex-dist-testing.sln | 31 +++-- 24 files changed, 294 insertions(+), 203 deletions(-) delete mode 100644 TestNetRewarder/RewardConfig.cs delete mode 100644 TestNetRewarder/TestNetRewarder.csproj delete mode 100644 Tools/BiblioTech/Rewards/RewardsRepo.cs create mode 100644 Tools/DiscordRewards/CheckConfig.cs rename GethConnector/GethConnector.csproj => Tools/DiscordRewards/DiscordRewards.csproj (57%) rename Tools/{BiblioTech/Rewards => DiscordRewards}/GiveRewardsCommand.cs (67%) create mode 100644 Tools/DiscordRewards/RewardConfig.cs create mode 100644 Tools/DiscordRewards/RewardRepo.cs rename {GethConnector => Tools/GethConnector}/GethConnector.cs (100%) create mode 100644 Tools/GethConnector/GethConnector.csproj rename {GethConnector => Tools/GethConnector}/GethInput.cs (100%) rename {TestNetRewarder => Tools/TestNetRewarder}/BotClient.cs (97%) rename {TestNetRewarder => Tools/TestNetRewarder}/ChainState.cs (100%) rename {TestNetRewarder => Tools/TestNetRewarder}/Checks.cs (69%) rename {TestNetRewarder => Tools/TestNetRewarder}/Configuration.cs (100%) rename {TestNetRewarder => Tools/TestNetRewarder}/HistoricState.cs (100%) rename {TestNetRewarder => Tools/TestNetRewarder}/Processor.cs (65%) rename {TestNetRewarder => Tools/TestNetRewarder}/Program.cs (99%) create mode 100644 Tools/TestNetRewarder/TestNetRewarder.csproj rename {TestNetRewarder => Tools/TestNetRewarder}/TimeSegmenter.cs (100%) diff --git a/TestNetRewarder/RewardConfig.cs b/TestNetRewarder/RewardConfig.cs deleted file mode 100644 index 9906359..0000000 --- a/TestNetRewarder/RewardConfig.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Utils; - -namespace TestNetRewarder -{ - public class RewardConfig - { - public RewardConfig(ulong rewardId, ICheck check) - { - RewardId = rewardId; - Check = check; - } - - public ulong RewardId { get; } - public ICheck Check { get; } - } - - public class RewardRepo - { - public RewardConfig[] Rewards { get; } = new RewardConfig[] - { - // Filled any slot - new RewardConfig(123, new FilledAnySlotCheck()), - - // Finished any slot - new RewardConfig(124, new FinishedSlotCheck( - minSize: 0.Bytes(), - minDuration: TimeSpan.Zero)), - - // Finished a sizable slot - new RewardConfig(125, new FinishedSlotCheck( - minSize: 1.GB(), - minDuration: TimeSpan.FromHours(24.0))), - - // Posted any contract - new RewardConfig(126, new PostedContractCheck()), - - // Started any contract - new RewardConfig(127, new StartedContractCheck( - minNumberOfHosts: 1, - minSlotSize: 0.Bytes(), - minDuration: TimeSpan.Zero)), - - // Started a sizable contract - new RewardConfig(127, new StartedContractCheck( - minNumberOfHosts: 4, - minSlotSize: 1.GB(), - minDuration: TimeSpan.FromHours(24.0))) - }; - } -} diff --git a/TestNetRewarder/TestNetRewarder.csproj b/TestNetRewarder/TestNetRewarder.csproj deleted file mode 100644 index 75b2ee1..0000000 --- a/TestNetRewarder/TestNetRewarder.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net7.0 - enable - enable - - - - - - - - - diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj index c941a71..9eb33a4 100644 --- a/Tools/BiblioTech/BiblioTech.csproj +++ b/Tools/BiblioTech/BiblioTech.csproj @@ -10,8 +10,9 @@ - + + diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs index 17d4362..b534490 100644 --- a/Tools/BiblioTech/Rewards/RewardsApi.cs +++ b/Tools/BiblioTech/Rewards/RewardsApi.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using DiscordRewards; +using Newtonsoft.Json; using System.Net; using TaskFactory = Utils.TaskFactory; @@ -79,31 +80,10 @@ namespace BiblioTech.Rewards var rewards = JsonConvert.DeserializeObject(content); if (rewards != null) { - AttachUsers(rewards); await ProcessRewards(rewards); } } - private void AttachUsers(GiveRewardsCommand rewards) - { - foreach (var reward in rewards.Rewards) - { - reward.Users = reward.UserAddresses.Select(GetUserFromAddress).Where(u => u != null).Cast().ToArray(); - } - } - - private UserData? GetUserFromAddress(string address) - { - try - { - return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address)); - } - catch - { - return null; - } - } - private async Task ProcessRewards(GiveRewardsCommand rewards) { await roleController.GiveRewards(rewards); diff --git a/Tools/BiblioTech/Rewards/RewardsRepo.cs b/Tools/BiblioTech/Rewards/RewardsRepo.cs deleted file mode 100644 index bbff34a..0000000 --- a/Tools/BiblioTech/Rewards/RewardsRepo.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace BiblioTech.Rewards -{ - public class RewardsRepo - { - private static string Tag => RoleController.UsernameTag; - - public RoleRewardConfig[] Rewards { get; } - - public RewardsRepo() - { - Rewards = new[] - { - // Join reward - new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Hosting: - //// Filled any slot: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Finished any slot: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Finished a min-256MB min-8h slot: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Finished a min-64GB min-24h slot: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Oops: - //// Missed a storage proof: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Clienting: - //// Posted any contract: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Posted any contract that reached started state: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Started a contract with min-4 hosts, min-256MB per host, min-8h duration: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - - //// Started a contract with min-4 hosts, min-64GB per host, min-24h duration: - //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"), - }; - } - } -} diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs index 63274fd..e59e804 100644 --- a/Tools/BiblioTech/Rewards/RoleController.cs +++ b/Tools/BiblioTech/Rewards/RoleController.cs @@ -1,14 +1,14 @@ using Discord; using Discord.WebSocket; +using DiscordRewards; namespace BiblioTech.Rewards { public class RoleController : IDiscordRoleController { - public const string UsernameTag = ""; private readonly DiscordSocketClient client; private readonly SocketTextChannel? rewardsChannel; - private readonly RewardsRepo repo = new RewardsRepo(); + private readonly RewardRepo repo = new RewardRepo(); public RoleController(DiscordSocketClient client) { @@ -30,7 +30,7 @@ namespace BiblioTech.Rewards LookUpAllRoles(guild, rewards), rewardsChannel); - await context.ProcessGiveRewardsCommand(rewards); + await context.ProcessGiveRewardsCommand(LookUpUsers(rewards)); } private async Task> LoadAllUsers(SocketGuild guild) @@ -47,19 +47,19 @@ namespace BiblioTech.Rewards return result; } - private Dictionary LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards) + private Dictionary LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards) { - var result = new Dictionary(); + var result = new Dictionary(); foreach (var r in rewards.Rewards) { - var role = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId); - if (role == null) + if (!result.ContainsKey(r.RewardId)) { - Program.Log.Log($"No RoleReward is configured for reward with id '{r.RewardId}'."); - } - else - { - if (role.SocketRole == null) + 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) @@ -68,48 +68,99 @@ namespace BiblioTech.Rewards } else { - role.SocketRole = socketRole; + result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig)); } } - result.Add(role.RoleId, role); } } 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() + .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 users; - private readonly Dictionary roles; + private readonly Dictionary roles; private readonly SocketTextChannel? rewardsChannel; - public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel) + public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel) { this.users = users; this.roles = roles; this.rewardsChannel = rewardsChannel; } - public async Task ProcessGiveRewardsCommand(GiveRewardsCommand rewards) + public async Task ProcessGiveRewardsCommand(UserReward[] rewards) { - foreach (var rewardCommand in rewards.Rewards) + foreach (var rewardCommand in rewards) { - if (roles.ContainsKey(rewardCommand.RewardId)) + if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId)) { - var role = roles[rewardCommand.RewardId]; + var role = roles[rewardCommand.RewardCommand.RewardId]; await ProcessRewardCommand(role, rewardCommand); } } } - private async Task ProcessRewardCommand(RoleRewardConfig role, RewardUsersCommand reward) + private async Task ProcessRewardCommand(RoleReward role, UserReward reward) { foreach (var user in reward.Users) { @@ -117,7 +168,7 @@ namespace BiblioTech.Rewards } } - private async Task GiveReward(RoleRewardConfig role, UserData user) + private async Task GiveReward(RoleReward role, UserData user) { if (!users.ContainsKey(user.DiscordId)) { @@ -128,9 +179,9 @@ namespace BiblioTech.Rewards var guildUser = users[user.DiscordId]; var alreadyHas = guildUser.RoleIds.ToArray(); - if (alreadyHas.Any(r => r == role.RoleId)) return; + if (alreadyHas.Any(r => r == role.Reward.RoleId)) return; - await GiveRole(guildUser, role.SocketRole!); + await GiveRole(guildUser, role.SocketRole); await SendNotification(role, user, guildUser); await Task.Delay(1000); } @@ -148,33 +199,20 @@ namespace BiblioTech.Rewards } } - private async Task SendNotification(RoleRewardConfig reward, UserData userData, IGuildUser user) + private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user) { try { if (userData.NotificationsEnabled && rewardsChannel != null) { - var msg = reward.Message.Replace(RoleController.UsernameTag, $"<@{user.Id}>"); + 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}"); + Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}"); } } } - - public class RoleRewardConfig - { - public RoleRewardConfig(ulong roleId, string message) - { - RoleId = roleId; - Message = message; - } - - public ulong RoleId { get; } - public string Message { get; } - public SocketRole? SocketRole { get; set; } - } } diff --git a/Tools/DiscordRewards/CheckConfig.cs b/Tools/DiscordRewards/CheckConfig.cs new file mode 100644 index 0000000..9c4fccb --- /dev/null +++ b/Tools/DiscordRewards/CheckConfig.cs @@ -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, + } +} diff --git a/GethConnector/GethConnector.csproj b/Tools/DiscordRewards/DiscordRewards.csproj similarity index 57% rename from GethConnector/GethConnector.csproj rename to Tools/DiscordRewards/DiscordRewards.csproj index b1bdda3..71c56d4 100644 --- a/GethConnector/GethConnector.csproj +++ b/Tools/DiscordRewards/DiscordRewards.csproj @@ -7,8 +7,7 @@ - - + diff --git a/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs b/Tools/DiscordRewards/GiveRewardsCommand.cs similarity index 67% rename from Tools/BiblioTech/Rewards/GiveRewardsCommand.cs rename to Tools/DiscordRewards/GiveRewardsCommand.cs index 494285b..03bc835 100644 --- a/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs +++ b/Tools/DiscordRewards/GiveRewardsCommand.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; - -namespace BiblioTech.Rewards +namespace DiscordRewards { public class GiveRewardsCommand { @@ -11,8 +9,5 @@ namespace BiblioTech.Rewards { public ulong RewardId { get; set; } public string[] UserAddresses { get; set; } = Array.Empty(); - - [JsonIgnore] - public UserData[] Users { get; set; } = Array.Empty(); } } diff --git a/Tools/DiscordRewards/RewardConfig.cs b/Tools/DiscordRewards/RewardConfig.cs new file mode 100644 index 0000000..dda0dfe --- /dev/null +++ b/Tools/DiscordRewards/RewardConfig.cs @@ -0,0 +1,18 @@ +namespace DiscordRewards +{ + public class RewardConfig + { + public const string UsernameTag = ""; + + 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; } + } +} diff --git a/Tools/DiscordRewards/RewardRepo.cs b/Tools/DiscordRewards/RewardRepo.cs new file mode 100644 index 0000000..61656eb --- /dev/null +++ b/Tools/DiscordRewards/RewardRepo.cs @@ -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(123, "Filled a slot", new CheckConfig + { + Type = CheckType.FilledSlot + }), + + // Finished any slot + new RewardConfig(124, "Finished any slot", new CheckConfig + { + Type = CheckType.FinishedSlot + }), + + // Finished a sizable slot + new RewardConfig(125, "Finished sizable slot", new CheckConfig + { + Type = CheckType.FinishedSlot, + MinSlotSize = 1.GB(), + MinDuration = TimeSpan.FromHours(24.0), + }), + + // Posted any contract + new RewardConfig(126, "Posted any contract", new CheckConfig + { + Type = CheckType.PostedContract + }), + + // Started any contract + new RewardConfig(127, "Started any contract", new CheckConfig + { + Type = CheckType.StartedContract + }), + + // Started a sizable contract + new RewardConfig(125, "Started sizable contract", new CheckConfig + { + Type = CheckType.FinishedSlot, + MinNumberOfHosts = 4, + MinSlotSize = 1.GB(), + MinDuration = TimeSpan.FromHours(24.0), + }) + }; + } +} diff --git a/GethConnector/GethConnector.cs b/Tools/GethConnector/GethConnector.cs similarity index 100% rename from GethConnector/GethConnector.cs rename to Tools/GethConnector/GethConnector.cs diff --git a/Tools/GethConnector/GethConnector.csproj b/Tools/GethConnector/GethConnector.csproj new file mode 100644 index 0000000..57715aa --- /dev/null +++ b/Tools/GethConnector/GethConnector.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/GethConnector/GethInput.cs b/Tools/GethConnector/GethInput.cs similarity index 100% rename from GethConnector/GethInput.cs rename to Tools/GethConnector/GethInput.cs diff --git a/TestNetRewarder/BotClient.cs b/Tools/TestNetRewarder/BotClient.cs similarity index 97% rename from TestNetRewarder/BotClient.cs rename to Tools/TestNetRewarder/BotClient.cs index 6907fae..91e6c83 100644 --- a/TestNetRewarder/BotClient.cs +++ b/Tools/TestNetRewarder/BotClient.cs @@ -1,4 +1,4 @@ -using BiblioTech.Rewards; +using DiscordRewards; using Logging; using Newtonsoft.Json; diff --git a/TestNetRewarder/ChainState.cs b/Tools/TestNetRewarder/ChainState.cs similarity index 100% rename from TestNetRewarder/ChainState.cs rename to Tools/TestNetRewarder/ChainState.cs diff --git a/TestNetRewarder/Checks.cs b/Tools/TestNetRewarder/Checks.cs similarity index 69% rename from TestNetRewarder/Checks.cs rename to Tools/TestNetRewarder/Checks.cs index 8563cf7..43102e0 100644 --- a/TestNetRewarder/Checks.cs +++ b/Tools/TestNetRewarder/Checks.cs @@ -1,4 +1,5 @@ -using GethPlugin; +using CodexContractsPlugin.Marketplace; +using GethPlugin; using NethereumWorkflow; using Utils; @@ -54,9 +55,44 @@ namespace TestNetRewarder 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.Select(r => r.ClientAddress).ToArray(); + 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; } } diff --git a/TestNetRewarder/Configuration.cs b/Tools/TestNetRewarder/Configuration.cs similarity index 100% rename from TestNetRewarder/Configuration.cs rename to Tools/TestNetRewarder/Configuration.cs diff --git a/TestNetRewarder/HistoricState.cs b/Tools/TestNetRewarder/HistoricState.cs similarity index 100% rename from TestNetRewarder/HistoricState.cs rename to Tools/TestNetRewarder/HistoricState.cs diff --git a/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs similarity index 65% rename from TestNetRewarder/Processor.cs rename to Tools/TestNetRewarder/Processor.cs index a1418e3..e56486b 100644 --- a/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -1,4 +1,5 @@ -using BiblioTech.Rewards; +using DiscordRewards; +using GethPlugin; using Logging; using Newtonsoft.Json; using Utils; @@ -60,15 +61,38 @@ namespace TestNetRewarder private void ProcessReward(List outgoingRewards, RewardConfig reward, ChainState chainState) { - var winningAddresses = reward.Check.Check(chainState); + var winningAddresses = PerformCheck(reward, chainState); if (winningAddresses.Any()) { outgoingRewards.Add(new RewardUsersCommand { - RewardId = reward.RewardId, + 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); + } } } diff --git a/TestNetRewarder/Program.cs b/Tools/TestNetRewarder/Program.cs similarity index 99% rename from TestNetRewarder/Program.cs rename to Tools/TestNetRewarder/Program.cs index 6090f79..3d10cff 100644 --- a/TestNetRewarder/Program.cs +++ b/Tools/TestNetRewarder/Program.cs @@ -1,4 +1,5 @@ using ArgsUniform; +using GethConnector; using Logging; using Utils; diff --git a/Tools/TestNetRewarder/TestNetRewarder.csproj b/Tools/TestNetRewarder/TestNetRewarder.csproj new file mode 100644 index 0000000..2b3bd49 --- /dev/null +++ b/Tools/TestNetRewarder/TestNetRewarder.csproj @@ -0,0 +1,17 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + diff --git a/TestNetRewarder/TimeSegmenter.cs b/Tools/TestNetRewarder/TimeSegmenter.cs similarity index 100% rename from TestNetRewarder/TimeSegmenter.cs rename to Tools/TestNetRewarder/TimeSegmenter.cs diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index f5074ed..bf199b1 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -53,9 +53,11 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestNetRewarder", "TestNetRewarder\TestNetRewarder.csproj", "{27B56A82-E8CE-4B50-9746-D574BAD646A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRewards", "Tools\DiscordRewards\DiscordRewards.csproj", "{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GethConnector", "GethConnector\GethConnector.csproj", "{04F2D26E-0768-4F93-9A1A-834089646B56}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GethConnector", "Tools\GethConnector\GethConnector.csproj", "{41811923-AF3A-4CC4-B508-D90EC2AFEC55}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestNetRewarder", "Tools\TestNetRewarder\TestNetRewarder.csproj", "{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -147,14 +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 - {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.Build.0 = Release|Any CPU - {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.Build.0 = Release|Any CPU + {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.Build.0 = Release|Any CPU + {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -181,8 +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} - {27B56A82-E8CE-4B50-9746-D574BAD646A2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} - {04F2D26E-0768-4F93-9A1A-834089646B56} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {497352BE-0A1D-4D83-8E21-A07B4A80F2F3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {41811923-AF3A-4CC4-B508-D90EC2AFEC55} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {570C0DBE-0EF1-47B5-9A3B-E1F7895722A5} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 9d8ec6f794753a4e56cead918b88b61c0c161ade Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 31 Jan 2024 11:18:22 -0500 Subject: [PATCH 21/22] loads up reward-repo with test server roles --- Tools/DiscordRewards/RewardRepo.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tools/DiscordRewards/RewardRepo.cs b/Tools/DiscordRewards/RewardRepo.cs index 61656eb..97b80a1 100644 --- a/Tools/DiscordRewards/RewardRepo.cs +++ b/Tools/DiscordRewards/RewardRepo.cs @@ -9,19 +9,19 @@ namespace DiscordRewards public RewardConfig[] Rewards { get; } = new RewardConfig[] { // Filled any slot - new RewardConfig(123, "Filled a slot", new CheckConfig + new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig { Type = CheckType.FilledSlot }), // Finished any slot - new RewardConfig(124, "Finished any slot", new CheckConfig + new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig { Type = CheckType.FinishedSlot }), // Finished a sizable slot - new RewardConfig(125, "Finished sizable slot", new CheckConfig + new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig { Type = CheckType.FinishedSlot, MinSlotSize = 1.GB(), @@ -29,19 +29,19 @@ namespace DiscordRewards }), // Posted any contract - new RewardConfig(126, "Posted any contract", new CheckConfig + new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig { Type = CheckType.PostedContract }), // Started any contract - new RewardConfig(127, "Started any contract", new CheckConfig + 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(125, "Started sizable contract", new CheckConfig + new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig { Type = CheckType.FinishedSlot, MinNumberOfHosts = 4, From cb259a9086b262138aff83baaa96d4fbb231329e Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 31 Jan 2024 11:30:29 -0500 Subject: [PATCH 22/22] Setting up docker for rewarder bot --- .github/workflows/docker-rewarder.yml | 26 +++++++++++++++++ .../DiscordRewards/CheckConfig.cs | 0 .../DiscordRewards/DiscordRewards.csproj | 0 .../DiscordRewards/GiveRewardsCommand.cs | 0 .../DiscordRewards/RewardConfig.cs | 0 .../DiscordRewards/RewardRepo.cs | 0 .../GethConnector/GethConnector.cs | 0 .../GethConnector/GethConnector.csproj | 0 .../GethConnector/GethInput.cs | 0 Tools/BiblioTech/BiblioTech.csproj | 4 +-- Tools/TestNetRewarder/TestNetRewarder.csproj | 4 +-- Tools/TestNetRewarder/docker/Dockerfile | 13 +++++++++ cs-codex-dist-testing.sln | 28 +++++++++---------- 13 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/docker-rewarder.yml rename {Tools => Framework}/DiscordRewards/CheckConfig.cs (100%) rename {Tools => Framework}/DiscordRewards/DiscordRewards.csproj (100%) rename {Tools => Framework}/DiscordRewards/GiveRewardsCommand.cs (100%) rename {Tools => Framework}/DiscordRewards/RewardConfig.cs (100%) rename {Tools => Framework}/DiscordRewards/RewardRepo.cs (100%) rename {Tools => Framework}/GethConnector/GethConnector.cs (100%) rename {Tools => Framework}/GethConnector/GethConnector.csproj (100%) rename {Tools => Framework}/GethConnector/GethInput.cs (100%) create mode 100644 Tools/TestNetRewarder/docker/Dockerfile diff --git a/.github/workflows/docker-rewarder.yml b/.github/workflows/docker-rewarder.yml new file mode 100644 index 0000000..a2bcfaa --- /dev/null +++ b/.github/workflows/docker-rewarder.yml @@ -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 + diff --git a/Tools/DiscordRewards/CheckConfig.cs b/Framework/DiscordRewards/CheckConfig.cs similarity index 100% rename from Tools/DiscordRewards/CheckConfig.cs rename to Framework/DiscordRewards/CheckConfig.cs diff --git a/Tools/DiscordRewards/DiscordRewards.csproj b/Framework/DiscordRewards/DiscordRewards.csproj similarity index 100% rename from Tools/DiscordRewards/DiscordRewards.csproj rename to Framework/DiscordRewards/DiscordRewards.csproj diff --git a/Tools/DiscordRewards/GiveRewardsCommand.cs b/Framework/DiscordRewards/GiveRewardsCommand.cs similarity index 100% rename from Tools/DiscordRewards/GiveRewardsCommand.cs rename to Framework/DiscordRewards/GiveRewardsCommand.cs diff --git a/Tools/DiscordRewards/RewardConfig.cs b/Framework/DiscordRewards/RewardConfig.cs similarity index 100% rename from Tools/DiscordRewards/RewardConfig.cs rename to Framework/DiscordRewards/RewardConfig.cs diff --git a/Tools/DiscordRewards/RewardRepo.cs b/Framework/DiscordRewards/RewardRepo.cs similarity index 100% rename from Tools/DiscordRewards/RewardRepo.cs rename to Framework/DiscordRewards/RewardRepo.cs diff --git a/Tools/GethConnector/GethConnector.cs b/Framework/GethConnector/GethConnector.cs similarity index 100% rename from Tools/GethConnector/GethConnector.cs rename to Framework/GethConnector/GethConnector.cs diff --git a/Tools/GethConnector/GethConnector.csproj b/Framework/GethConnector/GethConnector.csproj similarity index 100% rename from Tools/GethConnector/GethConnector.csproj rename to Framework/GethConnector/GethConnector.csproj diff --git a/Tools/GethConnector/GethInput.cs b/Framework/GethConnector/GethInput.cs similarity index 100% rename from Tools/GethConnector/GethInput.cs rename to Framework/GethConnector/GethInput.cs diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj index 9eb33a4..19422e4 100644 --- a/Tools/BiblioTech/BiblioTech.csproj +++ b/Tools/BiblioTech/BiblioTech.csproj @@ -10,9 +10,9 @@ + + - - diff --git a/Tools/TestNetRewarder/TestNetRewarder.csproj b/Tools/TestNetRewarder/TestNetRewarder.csproj index 2b3bd49..e4e7463 100644 --- a/Tools/TestNetRewarder/TestNetRewarder.csproj +++ b/Tools/TestNetRewarder/TestNetRewarder.csproj @@ -9,9 +9,9 @@ + + - - diff --git a/Tools/TestNetRewarder/docker/Dockerfile b/Tools/TestNetRewarder/docker/Dockerfile new file mode 100644 index 0000000..1a96c3f --- /dev/null +++ b/Tools/TestNetRewarder/docker/Dockerfile @@ -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"] diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index bf199b1..3a403fe 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -53,12 +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}") = "DiscordRewards", "Tools\DiscordRewards\DiscordRewards.csproj", "{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GethConnector", "Tools\GethConnector\GethConnector.csproj", "{41811923-AF3A-4CC4-B508-D90EC2AFEC55}" -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 @@ -149,18 +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 - {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.Build.0 = Release|Any CPU - {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41811923-AF3A-4CC4-B508-D90EC2AFEC55}.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 @@ -187,9 +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} - {497352BE-0A1D-4D83-8E21-A07B4A80F2F3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} - {41811923-AF3A-4CC4-B508-D90EC2AFEC55} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} {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}