diff --git a/Framework/DiscordRewards/RewardRepo.cs b/Framework/DiscordRewards/RewardRepo.cs index 97b80a1..51ac3fc 100644 --- a/Framework/DiscordRewards/RewardRepo.cs +++ b/Framework/DiscordRewards/RewardRepo.cs @@ -21,11 +21,11 @@ namespace DiscordRewards }), // Finished a sizable slot - new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig + new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig { Type = CheckType.FinishedSlot, - MinSlotSize = 1.GB(), - MinDuration = TimeSpan.FromHours(24.0), + MinSlotSize = 10.MB(), + MinDuration = TimeSpan.FromMinutes(5.0), }), // Posted any contract @@ -41,12 +41,12 @@ namespace DiscordRewards }), // Started a sizable contract - new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig + new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig { - Type = CheckType.FinishedSlot, + Type = CheckType.StartedContract, MinNumberOfHosts = 4, - MinSlotSize = 1.GB(), - MinDuration = TimeSpan.FromHours(24.0), + MinSlotSize = 10.MB(), + MinDuration = TimeSpan.FromMinutes(5.0), }) }; } diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs index 1954963..c902eda 100644 --- a/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs +++ b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs @@ -35,5 +35,7 @@ if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null; return value; } + + public int Size { get { return entries.Count; } } } } diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs index 851a8bf..e4c73d5 100644 --- a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs @@ -24,7 +24,7 @@ namespace NethereumWorkflow.BlockUtils if (moment <= bounds.Genesis.Utc) return null; if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber; - return Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector); + return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector)); } public ulong? GetLowestBlockNumberAfter(DateTime moment) @@ -33,7 +33,16 @@ namespace NethereumWorkflow.BlockUtils if (moment >= bounds.Current.Utc) return null; if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber; - return Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector); + return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ; + } + + private ulong Log(Func operation) + { + var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder)); + var result = operation(); + sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})"); + + return result; } private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func isWhatIwant) @@ -70,7 +79,7 @@ namespace NethereumWorkflow.BlockUtils { var next = GetBlock(entry.BlockNumber + 1); return - entry.Utc < target && + entry.Utc <= target && next.Utc > target; } @@ -78,7 +87,7 @@ namespace NethereumWorkflow.BlockUtils { var previous = GetBlock(entry.BlockNumber - 1); return - entry.Utc > target && + entry.Utc >= target && previous.Utc < target; } diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 3240d9b..197cf2d 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -89,26 +89,9 @@ namespace NethereumWorkflow } } - public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() + public List> GetEvents(string address, BlockInterval blockRange) where TEvent : IEventDTO, new() { - var wrapper = new Web3Wrapper(web3, log); - var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); - - var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); - var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); - - if (!fromBlock.HasValue) - { - log.Error("Failed to find lowest block for time range: " + timeRange); - throw new Exception("Failed"); - } - if (!toBlock.HasValue) - { - log.Error("Failed to find highest block for time range: " + timeRange); - throw new Exception("Failed"); - } - - return GetEvents(address, fromBlock.Value, toBlock.Value); + return GetEvents(address, blockRange.From, blockRange.To); } public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() @@ -119,5 +102,24 @@ namespace NethereumWorkflow var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to)); return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter)); } + + public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange) + { + var wrapper = new Web3Wrapper(web3, log); + var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); + + var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); + var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); + + if (fromBlock == null || toBlock == null) + { + throw new Exception("Failed to convert time range to block range."); + } + + return new BlockInterval( + from: fromBlock.Value, + to: toBlock.Value + ); + } } } diff --git a/Framework/Utils/BlockInterval.cs b/Framework/Utils/BlockInterval.cs new file mode 100644 index 0000000..79229bf --- /dev/null +++ b/Framework/Utils/BlockInterval.cs @@ -0,0 +1,27 @@ +namespace Utils +{ + public class BlockInterval + { + public BlockInterval(ulong from, ulong to) + { + if (from < to) + { + From = from; + To = to; + } + else + { + From = to; + To = from; + } + } + + public ulong From { get; } + public ulong To { get; } + + public override string ToString() + { + return $"[{From} - {To}]"; + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index 97083fe..4279347 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -19,13 +19,13 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); - Request[] GetStorageRequests(TimeRange timeRange); + Request[] GetStorageRequests(BlockInterval blockRange); 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); + RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange); + RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange); + SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange); + SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange); } public enum RequestState @@ -77,9 +77,9 @@ namespace CodexContractsPlugin return balance.TestTokens(); } - public Request[] GetStorageRequests(TimeRange timeRange) + public Request[] GetStorageRequests(BlockInterval blockRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); var i = StartInteraction(); return events .Select(e => @@ -93,9 +93,9 @@ namespace CodexContractsPlugin .ToArray(); } - public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange) + public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); return events.Select(e => { var result = e.Event; @@ -104,9 +104,9 @@ namespace CodexContractsPlugin }).ToArray(); } - public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange) + public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); return events.Select(e => { var result = e.Event; @@ -115,9 +115,9 @@ namespace CodexContractsPlugin }).ToArray(); } - public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange) + public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); return events.Select(e => { var result = e.Event; @@ -127,9 +127,9 @@ namespace CodexContractsPlugin }).ToArray(); } - public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange) + public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, timeRange); + var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); return events.Select(e => { var result = e.Event; diff --git a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs index 91c38e4..cad50c1 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs @@ -35,6 +35,12 @@ namespace CodexDiscordBotPlugin return StartContainer(workflow, config); } + public RunningContainers DeployRewarder(RewarderBotStartupConfig config) + { + var workflow = tools.CreateWorkflow(); + return StartRewarderContainer(workflow, config); + } + private RunningContainers StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config) { var startupConfig = new StartupConfig(); @@ -42,5 +48,12 @@ namespace CodexDiscordBotPlugin startupConfig.Add(config); return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig); } + + private RunningContainers StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config) + { + var startupConfig = new StartupConfig(); + startupConfig.Add(config); + return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig); + } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs b/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs index 1c3d673..c17cec1 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs @@ -10,6 +10,11 @@ namespace CodexDiscordBotPlugin return Plugin(ci).Deploy(config); } + public static RunningContainers DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config) + { + return Plugin(ci).DeployRewarder(config); + } + private static CodexDiscordBotPlugin Plugin(CoreInterface ci) { return ci.GetPlugin(); diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs index ea69f3b..9a2e3fe 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs @@ -7,7 +7,9 @@ namespace CodexDiscordBotPlugin public class DiscordBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-bibliotech"; - public override string Image => "thatbenbierens/codex-discordbot:initial"; + public override string Image => "codexstorage/codex-discordbot:sha-8c64352"; + + public static string RewardsPort = "bot_rewards_port"; protected override void Initialize(StartupConfig startupConfig) { @@ -19,6 +21,7 @@ namespace CodexDiscordBotPlugin AddEnvVar("SERVERNAME", config.ServerName); AddEnvVar("ADMINROLE", config.AdminRoleName); AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName); + AddEnvVar("REWARDSCHANNELNAME", config.RewardChannelName); AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml"); AddEnvVar("KUBENAMESPACE", config.KubeNamespace); @@ -30,13 +33,13 @@ namespace CodexDiscordBotPlugin AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); + AddInternalPortAndVar("REWARDAPIPORT", RewardsPort); + if (!string.IsNullOrEmpty(config.DataPath)) { AddEnvVar("DATAPATH", config.DataPath); AddVolume(config.DataPath, 1.GB()); } - - AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig"); } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs index 85c103f..dfc6ad1 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs @@ -2,7 +2,7 @@ { public class DiscordBotStartupConfig { - public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo) + public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo, string rewardChannelName) { Name = name; Token = token; @@ -11,6 +11,7 @@ AdminChannelName = adminChannelName; KubeNamespace = kubeNamespace; GethInfo = gethInfo; + RewardChannelName = rewardChannelName; } public string Name { get; } @@ -18,11 +19,32 @@ public string ServerName { get; } public string AdminRoleName { get; } public string AdminChannelName { get; } + public string RewardChannelName { get; } public string KubeNamespace { get; } public DiscordBotGethInfo GethInfo { get; } public string? DataPath { get; set; } } + public class RewarderBotStartupConfig + { + public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string interval, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath) + { + DiscordBotHost = discordBotHost; + DiscordBotPort = discordBotPort; + Interval = interval; + HistoryStartUtc = historyStartUtc; + GethInfo = gethInfo; + DataPath = dataPath; + } + + public string DiscordBotHost { get; } + public int DiscordBotPort { get; } + public string Interval { get; } + public DateTime HistoryStartUtc { get; } + public DiscordBotGethInfo GethInfo { get; } + public string? DataPath { get; set; } + } + public class DiscordBotGethInfo { public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi) diff --git a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs new file mode 100644 index 0000000..3be2f7f --- /dev/null +++ b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs @@ -0,0 +1,39 @@ +using KubernetesWorkflow.Recipe; +using KubernetesWorkflow; +using Utils; + +namespace CodexDiscordBotPlugin +{ + public class RewarderBotContainerRecipe : ContainerRecipeFactory + { + public override string AppName => "discordbot-rewarder"; + public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2"; + + protected override void Initialize(StartupConfig startupConfig) + { + var config = startupConfig.Get(); + + SetSchedulingAffinity(notIn: "false"); + + AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost); + AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString()); + AddEnvVar("INTERVALMINUTES", config.Interval); + var offset = new DateTimeOffset(config.HistoryStartUtc); + AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString()); + + var gethInfo = config.GethInfo; + AddEnvVar("GETH_HOST", gethInfo.Host); + AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString()); + AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey); + AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress); + AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); + AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); + + if (!string.IsNullOrEmpty(config.DataPath)) + { + AddEnvVar("DATAPATH", config.DataPath); + AddVolume(config.DataPath, 1.GB()); + } + } + } +} diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 2ae834d..bfa3d0f 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -24,8 +24,9 @@ namespace GethPlugin decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); - List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new(); + List> GetEvents(string address, BlockInterval blockRange) where TEvent : IEventDTO, new(); List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); + BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -144,14 +145,19 @@ namespace GethPlugin return StartInteraction().IsContractAvailable(abi, contractAddress); } - public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + public List> GetEvents(string address, BlockInterval blockRange) where TEvent : IEventDTO, new() { - return StartInteraction().GetEvents(address, fromBlockNumber, toBlockNumber); + return StartInteraction().GetEvents(address, blockRange); } public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { - return StartInteraction().GetEvents(address, timeRange); + return StartInteraction().GetEvents(address, ConvertTimeRangeToBlockRange(timeRange)); + } + + public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange) + { + return StartInteraction().ConvertTimeRangeToBlockRange(timeRange); } protected abstract NethereumInteraction StartInteraction(); diff --git a/Tests/CodexTests/BasicTests/DiscordBotTests.cs b/Tests/CodexTests/BasicTests/DiscordBotTests.cs new file mode 100644 index 0000000..eed7f48 --- /dev/null +++ b/Tests/CodexTests/BasicTests/DiscordBotTests.cs @@ -0,0 +1,110 @@ +using CodexContractsPlugin; +using CodexDiscordBotPlugin; +using CodexPlugin; +using GethPlugin; +using NUnit.Framework; +using Utils; + +namespace CodexTests.BasicTests +{ + [TestFixture] + public class DiscordBotTests : AutoBootstrapDistTest + { + [Test] + public void BotRewardTest() + { + var myAccount = EthAccount.GenerateNew(); + + var sellerInitialBalance = 234.TestTokens(); + var buyerInitialBalance = 100000.TestTokens(); + var fileSize = 11.MB(); + + var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); + var contracts = Ci.StartCodexContracts(geth); + + // start bot and rewarder + var gethInfo = new DiscordBotGethInfo( + host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host, + port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port, + privKey: geth.StartResult.Account.PrivateKey, + marketplaceAddress: contracts.Deployment.MarketplaceAddress, + tokenAddress: contracts.Deployment.TokenAddress, + abi: contracts.Deployment.Abi + ); + var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( + name: "bot", + token: "aaa", + serverName: "ThatBen's server", + adminRoleName: "bottest-admins", + adminChannelName: "admin-channel", + rewardChannelName: "rewards-channel", + kubeNamespace: "notneeded", + gethInfo: gethInfo + )); + var botContainer = bot.Containers.Single(); + Ci.DeployRewarderBot(new RewarderBotStartupConfig( + //discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host, + //discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port, + discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host, + discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port, + interval: "60", + historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3), + gethInfo: gethInfo, + dataPath: null + )); + + var numberOfHosts = 3; + + for (var i = 0; i < numberOfHosts; i++) + { + var seller = AddCodex(s => s + .WithName("Seller") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(11.GB()) + .EnableMarketplace(geth, contracts, m => m + .WithAccount(myAccount) + .WithInitial(10.Eth(), sellerInitialBalance) + .AsStorageNode() + .AsValidator())); + + var availability = new StorageAvailability( + totalSpace: 10.GB(), + maxDuration: TimeSpan.FromMinutes(30), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens() + ); + seller.Marketplace.MakeStorageAvailable(availability); + } + + var testFile = GenerateTestFile(fileSize); + + var buyer = AddCodex(s => s + .WithName("Buyer") + .EnableMarketplace(geth, contracts, m => m + .WithAccount(myAccount) + .WithInitial(10.Eth(), buyerInitialBalance))); + + var contentId = buyer.UploadFile(testFile); + + var purchase = new StoragePurchaseRequest(contentId) + { + PricePerSlotPerSecond = 2.TestTokens(), + RequiredCollateral = 10.TestTokens(), + MinRequiredNumberOfNodes = 5, + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = TimeSpan.FromMinutes(6), + Expiry = TimeSpan.FromMinutes(5) + }; + + var purchaseContract = buyer.Marketplace.RequestStorage(purchase); + + purchaseContract.WaitForStorageContractStarted(); + + purchaseContract.WaitForStorageContractFinished(); + } + } +} diff --git a/Tests/CodexTests/BasicTests/MarketplaceTests.cs b/Tests/CodexTests/BasicTests/MarketplaceTests.cs index fcabb15..7474696 100644 --- a/Tests/CodexTests/BasicTests/MarketplaceTests.cs +++ b/Tests/CodexTests/BasicTests/MarketplaceTests.cs @@ -2,7 +2,6 @@ using CodexContractsPlugin.Marketplace; using CodexPlugin; using GethPlugin; -using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; @@ -71,29 +70,26 @@ namespace CodexTests.BasicTests var purchaseContract = client.Marketplace.RequestStorage(purchase); - WaitForAllSlotFilledEvents(contracts, purchase); + WaitForAllSlotFilledEvents(contracts, purchase, geth); purchaseContract.WaitForStorageContractStarted(); - //AssertBalance(contracts, host, Is.LessThan(hostInitialBalance), "Collateral was not placed."); - - var request = GetOnChainStorageRequest(contracts); + var request = GetOnChainStorageRequest(contracts, geth); AssertStorageRequest(request, purchase, contracts, client); - //AssertSlotFilledEvents(contracts, purchase, request, host); - //AssertContractSlot(contracts, request, 0, host); + AssertContractSlot(contracts, request, 0); purchaseContract.WaitForStorageContractFinished(); - //AssertBalance(contracts, host, Is.GreaterThan(hostInitialBalance), "Seller was not paid for storage."); AssertBalance(contracts, client, Is.LessThan(clientInitialBalance), "Buyer was not charged for storage."); Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); } - private void WaitForAllSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase) + private void WaitForAllSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase, IGethNode geth) { Time.Retry(() => { - var slotFilledEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); + var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()); + var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange); Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"); @@ -101,24 +97,6 @@ namespace CodexTests.BasicTests }, Convert.ToInt32(purchase.Duration.TotalSeconds / 5) + 10, TimeSpan.FromSeconds(5), "Checking SlotFilled events"); } - private void AssertSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase, Request request, ICodexNode seller) - { - // Expect 1 fulfilled event for the purchase. - var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange()); - Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1)); - CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId); - - // Expect 1 filled-slot event for each slot in the purchase. - var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); - Assert.That(filledSlotEvents.Length, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); - for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++) - { - var filledSlotEvent = filledSlotEvents.Single(e => e.SlotIndex == i); - Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex())); - Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); - } - } - private void AssertStorageRequest(Request request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer) { Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started)); @@ -126,17 +104,17 @@ namespace CodexTests.BasicTests Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); } - private Request GetOnChainStorageRequest(ICodexContracts contracts) + private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth) { - var requests = contracts.GetStorageRequests(GetTestRunTimeRange()); + var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange())); Assert.That(requests.Length, Is.EqualTo(1)); return requests.Single(); } - private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex, ICodexNode expectedSeller) + private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex) { var slotHost = contracts.GetSlotHost(request, contractSlotIndex); - Assert.That(slotHost, Is.EqualTo(expectedSeller.EthAddress)); + Assert.That(slotHost?.Address, Is.Not.Null); } } } diff --git a/Tests/CodexTests/CodexTests.csproj b/Tests/CodexTests/CodexTests.csproj index 2c04d03..495caf6 100644 --- a/Tests/CodexTests/CodexTests.csproj +++ b/Tests/CodexTests/CodexTests.csproj @@ -14,6 +14,7 @@ + diff --git a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs index 5653b45..d2be5da 100644 --- a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs +++ b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs @@ -117,6 +117,41 @@ namespace FrameworkTests.NethereumWorkflow Assert.That(notFound, Is.Null); } + + [Test] + public void FailsToFindBlockBeforeFrontOfChain_history() + { + var first = blocks.First().Value; + + var notFound = finder.GetHighestBlockNumberBefore(first.JustBefore); + + Assert.That(notFound, Is.Null); + } + + [Test] + public void FailsToFindBlockAfterTailOfChain_future() + { + var last = blocks.Last().Value; + + var notFound = finder.GetLowestBlockNumberAfter(last.JustAfter); + + Assert.That(notFound, Is.Null); + } + + [Test] + public void RunThrough() + { + foreach (var pair in blocks) + { + finder.GetHighestBlockNumberBefore(pair.Value.JustBefore); + finder.GetHighestBlockNumberBefore(pair.Value.Time); + finder.GetHighestBlockNumberBefore(pair.Value.JustAfter); + + finder.GetLowestBlockNumberAfter(pair.Value.JustBefore); + finder.GetLowestBlockNumberAfter(pair.Value.Time); + finder.GetLowestBlockNumberAfter(pair.Value.JustAfter); + } + } } public class Block @@ -131,5 +166,10 @@ namespace FrameworkTests.NethereumWorkflow public DateTime Time { get; } public DateTime JustBefore { get { return Time.AddSeconds(-1); } } public DateTime JustAfter { get { return Time.AddSeconds(1); } } + + public override string ToString() + { + return $"[{Number}]"; + } } } diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj index 427d3eb..85fa95a 100644 --- a/Tools/BiblioTech/BiblioTech.csproj +++ b/Tools/BiblioTech/BiblioTech.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index f21e486..7907177 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -26,12 +26,10 @@ namespace BiblioTech Program.AdminChecker.SetGuild(guild); Program.Log.Log($"Initializing for guild: '{guild.Name}'"); - var roleController = new RoleController(client); - var rewardsApi = new RewardsApi(roleController); - var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray(); if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel"); Program.AdminChecker.SetAdminChannel(adminChannels.First()); + Program.RoleDriver = new RoleDriver(client); var builders = commands.Select(c => { @@ -62,8 +60,6 @@ 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/Commands/MintCommand.cs b/Tools/BiblioTech/Commands/MintCommand.cs index 5b328b7..faf62c8 100644 --- a/Tools/BiblioTech/Commands/MintCommand.cs +++ b/Tools/BiblioTech/Commands/MintCommand.cs @@ -6,8 +6,6 @@ namespace BiblioTech.Commands { public class MintCommand : BaseGethCommand { - private readonly Ether defaultEthToSend = 10.Eth(); - private readonly TestToken defaultTestTokensToMint = 1024.TestTokens(); private readonly UserOption optionalUser = new UserOption( description: "If set, mint tokens for this user. (Optional, admin-only)", isRequired: false); @@ -47,9 +45,10 @@ namespace BiblioTech.Commands { if (ShouldMintTestTokens(contracts, addr)) { - var transaction = contracts.MintTestTokens(addr, defaultTestTokensToMint); - report.Add($"Minted {defaultTestTokensToMint} {FormatTransactionLink(transaction)}"); - return new Transaction(defaultTestTokensToMint, transaction); + var tokens = Program.Config.MintTT.TestTokens(); + var transaction = contracts.MintTestTokens(addr, tokens); + report.Add($"Minted {tokens} {FormatTransactionLink(transaction)}"); + return new Transaction(tokens, transaction); } report.Add("TestToken balance over threshold. (No TestTokens minted.)"); @@ -60,9 +59,10 @@ namespace BiblioTech.Commands { if (ShouldSendEth(gethNode, addr)) { - var transaction = gethNode.SendEth(addr, defaultEthToSend); - report.Add($"Sent {defaultEthToSend} {FormatTransactionLink(transaction)}"); - return new Transaction(defaultEthToSend, transaction); + var eth = Program.Config.SendEth.Eth(); + var transaction = gethNode.SendEth(addr, eth); + report.Add($"Sent {eth} {FormatTransactionLink(transaction)}"); + return new Transaction(eth, transaction); } report.Add("Eth balance is over threshold. (No Eth sent.)"); return null; @@ -71,13 +71,13 @@ namespace BiblioTech.Commands private bool ShouldMintTestTokens(ICodexContracts contracts, EthAddress addr) { var testTokens = contracts.GetTestTokenBalance(addr); - return testTokens.Amount < 64m; + return testTokens.Amount < Program.Config.MintTT; } private bool ShouldSendEth(IGethNode gethNode, EthAddress addr) { var eth = gethNode.GetEthBalance(addr); - return eth.Eth < 1.0m; + return eth.Eth < Program.Config.SendEth; } private string FormatTransactionLink(string transaction) diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index f324d23..2183a54 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -19,9 +19,17 @@ 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.")] + [Uniform("rewards-channel-name", "rc", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")] public string RewardsChannelName { get; set; } = ""; + [Uniform("reward-api-port", "rp", "REWARDAPIPORT", false, "TCP listen port for the reward API.")] + public int RewardApiPort { get; set; } = 31080; + + [Uniform("send-eth", "se", "SENDETH", false, "Amount of Eth send by the mint command. Default: 10.")] + public int SendEth { get; set; } = 10; + + [Uniform("mint-tt", "mt", "MINTTT", false, "Amount of TestTokens minted by the mint command. Default: 1073741824")] + public int MintTT { get; set; } = 1073741824; public string EndpointsPath { diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 7d6d690..8f7c98c 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; @@ -13,6 +14,7 @@ namespace BiblioTech public static Configuration Config { get; private set; } = null!; public static UserRepo UserRepo { get; } = new UserRepo(); public static AdminChecker AdminChecker { get; private set; } = null!; + public static IDiscordRoleDriver RoleDriver { get; set; } = null!; public static ILog Log { get; private set; } = null!; public static Task Main(string[] args) @@ -29,10 +31,10 @@ namespace BiblioTech EnsurePath(Config.UserDataPath); EnsurePath(Config.EndpointsPath); - return new Program().MainAsync(); + return new Program().MainAsync(args); } - public async Task MainAsync() + public async Task MainAsync(string[] args) { Log.Log("Starting Codex Discord Bot..."); client = new DiscordSocketClient(); @@ -52,10 +54,19 @@ namespace BiblioTech await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); await client.StartAsync(); - AdminChecker = new AdminChecker(); + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel((context, options) => + { + options.ListenAnyIP(Config.RewardApiPort); + }); + builder.Services.AddControllers(); + var app = builder.Build(); + app.MapControllers(); + Log.Log("Running..."); + await app.RunAsync(); await Task.Delay(-1); } diff --git a/Tools/BiblioTech/Properties/launchSettings.json b/Tools/BiblioTech/Properties/launchSettings.json new file mode 100644 index 0000000..74fbfe0 --- /dev/null +++ b/Tools/BiblioTech/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "BiblioTech": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:52960;http://localhost:52961" + } + } +} \ No newline at end of file diff --git a/Tools/BiblioTech/Rewards/RewardController.cs b/Tools/BiblioTech/Rewards/RewardController.cs new file mode 100644 index 0000000..c9a19de --- /dev/null +++ b/Tools/BiblioTech/Rewards/RewardController.cs @@ -0,0 +1,35 @@ +using DiscordRewards; +using Microsoft.AspNetCore.Mvc; + +namespace BiblioTech.Rewards +{ + public interface IDiscordRoleDriver + { + Task GiveRewards(GiveRewardsCommand rewards); + } + + [Route("api/[controller]")] + [ApiController] + public class RewardController : ControllerBase + { + [HttpGet] + public string Ping() + { + return "Pong"; + } + + [HttpPost] + public async Task Give(GiveRewardsCommand cmd) + { + try + { + await Program.RoleDriver.GiveRewards(cmd); + } + catch (Exception ex) + { + Program.Log.Error("Exception: " + ex); + } + return "OK"; + } + } +} diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs deleted file mode 100644 index b534490..0000000 --- a/Tools/BiblioTech/Rewards/RewardsApi.cs +++ /dev/null @@ -1,92 +0,0 @@ -using DiscordRewards; -using Newtonsoft.Json; -using System.Net; -using TaskFactory = Utils.TaskFactory; - -namespace BiblioTech.Rewards -{ - public interface IDiscordRoleController - { - Task GiveRewards(GiveRewardsCommand rewards); - } - - public class RewardsApi - { - private readonly HttpListener listener = new HttpListener(); - private readonly TaskFactory taskFactory = new TaskFactory(); - private readonly IDiscordRoleController roleController; - private CancellationTokenSource cts = new CancellationTokenSource(); - - public RewardsApi(IDiscordRoleController roleController) - { - this.roleController = roleController; - } - - public void Start() - { - cts = new CancellationTokenSource(); - listener.Prefixes.Add($"http://*:31080/"); - listener.Start(); - taskFactory.Run(ConnectionDispatcher, nameof(ConnectionDispatcher)); - } - - public void Stop() - { - listener.Stop(); - cts.Cancel(); - taskFactory.WaitAll(); - } - - private void ConnectionDispatcher() - { - while (!cts.Token.IsCancellationRequested) - { - var wait = listener.GetContextAsync(); - wait.Wait(cts.Token); - if (wait.IsCompletedSuccessfully) - { - taskFactory.Run(() => - { - var context = wait.Result; - try - { - HandleConnection(context).Wait(); - } - catch (Exception ex) - { - Program.Log.Error("Exception during HTTP handler: " + ex); - } - // Whatever happens, everything's always OK. - context.Response.StatusCode = 200; - context.Response.OutputStream.Close(); - }, nameof(HandleConnection)); - } - } - } - - private async Task HandleConnection(HttpListenerContext context) - { - using var reader = new StreamReader(context.Request.InputStream); - var content = reader.ReadToEnd(); - - if (content == "Ping") - { - using var writer = new StreamWriter(context.Response.OutputStream); - writer.Write("Pong"); - return; - } - - if (!content.StartsWith("{")) return; - var rewards = JsonConvert.DeserializeObject(content); - if (rewards != null) - { - await ProcessRewards(rewards); - } - } - - private async Task ProcessRewards(GiveRewardsCommand rewards) - { - await roleController.GiveRewards(rewards); - } - } -} diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleDriver.cs similarity index 93% rename from Tools/BiblioTech/Rewards/RoleController.cs rename to Tools/BiblioTech/Rewards/RoleDriver.cs index e59e804..2478f52 100644 --- a/Tools/BiblioTech/Rewards/RoleController.cs +++ b/Tools/BiblioTech/Rewards/RoleDriver.cs @@ -4,13 +4,13 @@ using DiscordRewards; namespace BiblioTech.Rewards { - public class RoleController : IDiscordRoleController + public class RoleDriver : IDiscordRoleDriver { private readonly DiscordSocketClient client; private readonly SocketTextChannel? rewardsChannel; private readonly RewardRepo repo = new RewardRepo(); - public RoleController(DiscordSocketClient client) + public RoleDriver(DiscordSocketClient client) { this.client = client; @@ -107,7 +107,13 @@ namespace BiblioTech.Rewards private SocketGuild GetGuild() { - return client.Guilds.Single(g => g.Name == Program.Config.ServerName); + var guild = client.Guilds.SingleOrDefault(g => g.Name == Program.Config.ServerName); + if (guild == null) + { + throw new Exception($"Unable to find guild by name: '{Program.Config.ServerName}'. " + + $"Known guilds: [{string.Join(",", client.Guilds.Select(g => g.Name))}]"); + } + return guild; } } diff --git a/Tools/CodexNetDeployer/Configuration.cs b/Tools/CodexNetDeployer/Configuration.cs index 675e51c..441e355 100644 --- a/Tools/CodexNetDeployer/Configuration.cs +++ b/Tools/CodexNetDeployer/Configuration.cs @@ -116,6 +116,9 @@ namespace CodexNetDeployer [Uniform("dbot-adminchannelname", "dbotacn", "DBOTADMINCHANNELNAME", false, "Required if discord-bot is true. Name of the Discord channel in which admin commands are allowed.")] public string DiscordBotAdminChannelName { get; set; } = string.Empty; + + [Uniform("dbot-rewardchannelname", "dbotrcn", "DBOTREWARDCHANNELNAME", false, "Required if discord-bot is true. Name of the Discord channel in which reward updates are posted.")] + public string DiscordBotRewardChannelName { get; set; } = string.Empty; [Uniform("dbot-datapath", "dbotdp", "DBOTDATAPATH", false, "Optional. Path in container where bot will save all data.")] public string DiscordBotDataPath { get; set; } = string.Empty; @@ -163,6 +166,7 @@ namespace CodexNetDeployer StringIsSet(nameof(DiscordBotServerName), DiscordBotServerName, errors); StringIsSet(nameof(DiscordBotAdminRoleName), DiscordBotAdminRoleName, errors); StringIsSet(nameof(DiscordBotAdminChannelName), DiscordBotAdminChannelName, errors); + StringIsSet(nameof(DiscordBotRewardChannelName), DiscordBotRewardChannelName, errors); } return errors; diff --git a/Tools/CodexNetDeployer/Deployer.cs b/Tools/CodexNetDeployer/Deployer.cs index f9d6a35..440d092 100644 --- a/Tools/CodexNetDeployer/Deployer.cs +++ b/Tools/CodexNetDeployer/Deployer.cs @@ -145,7 +145,8 @@ namespace CodexNetDeployer adminRoleName: config.DiscordBotAdminRoleName, adminChannelName: config.DiscordBotAdminChannelName, kubeNamespace: config.KubeNamespace, - gethInfo: info) + gethInfo: info, + rewardChannelName: config.DiscordBotRewardChannelName) { DataPath = config.DiscordBotDataPath }); diff --git a/Tools/TestNetRewarder/BotClient.cs b/Tools/TestNetRewarder/BotClient.cs index 91e6c83..c112af4 100644 --- a/Tools/TestNetRewarder/BotClient.cs +++ b/Tools/TestNetRewarder/BotClient.cs @@ -1,6 +1,8 @@ -using DiscordRewards; +using CodexContractsPlugin.Marketplace; +using DiscordRewards; using Logging; using Newtonsoft.Json; +using System.Net.Http.Json; namespace TestNetRewarder { @@ -17,21 +19,41 @@ namespace TestNetRewarder public async Task IsOnline() { - return await HttpPost("Ping") == "Ping"; + var result = await HttpGet(); + log.Log("Is DiscordBot online: " + result); + return result == "Pong"; } - public async Task SendRewards(GiveRewardsCommand command) + public async Task SendRewards(GiveRewardsCommand command) { - if (command == null || command.Rewards == null || !command.Rewards.Any()) return; - await HttpPost(JsonConvert.SerializeObject(command)); + if (command == null || command.Rewards == null || !command.Rewards.Any()) return false; + var result = await HttpPostJson(command); + log.Log("Reward response: " + result); + return result == "OK"; } - private async Task HttpPost(string content) + private async Task HttpGet() { try { var client = new HttpClient(); - var response = await client.PostAsync(GetUrl(), new StringContent(content)); + var response = await client.GetAsync(GetUrl()); + return await response.Content.ReadAsStringAsync(); + } + catch (Exception ex) + { + log.Error(ex.ToString()); + return string.Empty; + } + } + + private async Task HttpPostJson(T body) + { + try + { + using var client = new HttpClient(); + using var content = JsonContent.Create(body); + using var response = await client.PostAsync(GetUrl(), content); return await response.Content.ReadAsStringAsync(); } catch (Exception ex) @@ -43,7 +65,7 @@ namespace TestNetRewarder private string GetUrl() { - return $"{configuration.DiscordHost}:{configuration.DiscordPort}"; + return $"{configuration.DiscordHost}:{configuration.DiscordPort}/api/reward"; } } } diff --git a/Tools/TestNetRewarder/ChainState.cs b/Tools/TestNetRewarder/ChainState.cs index 13356fc..40e4bf6 100644 --- a/Tools/TestNetRewarder/ChainState.cs +++ b/Tools/TestNetRewarder/ChainState.cs @@ -8,18 +8,18 @@ namespace TestNetRewarder { private readonly HistoricState historicState; - public ChainState(HistoricState historicState, ICodexContracts contracts, TimeRange timeRange) + public ChainState(HistoricState historicState, ICodexContracts contracts, BlockInterval blockRange) { - NewRequests = contracts.GetStorageRequests(timeRange); + NewRequests = contracts.GetStorageRequests(blockRange); 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); + RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange); + RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange); + SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange); + SlotFreedEvents = contracts.GetSlotFreedEvents(blockRange); this.historicState = historicState; } diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index e56486b..032b0ef 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -11,30 +11,51 @@ namespace TestNetRewarder private static readonly HistoricState historicState = new HistoricState(); private static readonly RewardRepo rewardRepo = new RewardRepo(); private readonly ILog log; + private BlockInterval? lastBlockRange; public Processor(ILog log) { this.log = log; } - public async Task ProcessTimeSegment(TimeRange range) + public async Task ProcessTimeSegment(TimeRange timeRange) { + var connector = GethConnector.GethConnector.Initialize(log); + if (connector == null) throw new Exception("Invalid Geth information"); + try { - var connector = GethConnector.GethConnector.Initialize(log); - if (connector == null) return; - - var chainState = new ChainState(historicState, connector.CodexContracts, range); - await ProcessTimeSegment(chainState); + var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange); + if (!IsNewBlockRange(blockRange)) + { + log.Log($"Block range {blockRange} was previously processed. Skipping..."); + return; + } + var chainState = new ChainState(historicState, connector.CodexContracts, blockRange); + await ProcessChainState(chainState); } catch (Exception ex) { log.Error("Exception processing time segment: " + ex); + throw; } } - private async Task ProcessTimeSegment(ChainState chainState) + private bool IsNewBlockRange(BlockInterval blockRange) + { + if (lastBlockRange == null || + lastBlockRange.From != blockRange.From || + lastBlockRange.To != blockRange.To) + { + lastBlockRange = blockRange; + return true; + } + + return false; + } + + private async Task ProcessChainState(ChainState chainState) { var outgoingRewards = new List(); foreach (var reward in rewardRepo.Rewards) @@ -42,13 +63,17 @@ namespace TestNetRewarder ProcessReward(outgoingRewards, reward, chainState); } + log.Log($"Found {outgoingRewards.Count} rewards to send."); if (outgoingRewards.Any()) { - await SendRewardsCommand(outgoingRewards); + if (!await SendRewardsCommand(outgoingRewards)) + { + log.Error("Failed to send reward command."); + } } } - private async Task SendRewardsCommand(List outgoingRewards) + private async Task SendRewardsCommand(List outgoingRewards) { var cmd = new GiveRewardsCommand { @@ -56,12 +81,16 @@ namespace TestNetRewarder }; log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd)); - await Program.BotClient.SendRewards(cmd); + return await Program.BotClient.SendRewards(cmd); } private void ProcessReward(List outgoingRewards, RewardConfig reward, ChainState chainState) { var winningAddresses = PerformCheck(reward, chainState); + foreach (var win in winningAddresses) + { + log.Log($"Address '{win.Address}' wins '{reward.Message}'"); + } if (winningAddresses.Any()) { outgoingRewards.Add(new RewardUsersCommand diff --git a/Tools/TestNetRewarder/Program.cs b/Tools/TestNetRewarder/Program.cs index 3d10cff..0aaa41c 100644 --- a/Tools/TestNetRewarder/Program.cs +++ b/Tools/TestNetRewarder/Program.cs @@ -1,5 +1,4 @@ using ArgsUniform; -using GethConnector; using Logging; using Utils; @@ -12,6 +11,7 @@ namespace TestNetRewarder public static CancellationToken CancellationToken { get; private set; } public static BotClient BotClient { get; private set; } = null!; private static Processor processor = null!; + private static DateTime lastCheck = DateTime.MinValue; public static Task Main(string[] args) { @@ -47,7 +47,7 @@ namespace TestNetRewarder { await EnsureBotOnline(); await segmenter.WaitForNextSegment(processor.ProcessTimeSegment); - await Task.Delay(1000, CancellationToken); + await Task.Delay(100, CancellationToken); } } @@ -59,11 +59,15 @@ namespace TestNetRewarder var blockNumber = gc.GethNode.GetSyncedBlockNumber(); if (blockNumber == null || blockNumber < 1) throw new Exception("Geth connection failed."); + Log.Log("Geth OK. Block number: " + blockNumber); } private static async Task EnsureBotOnline() { var start = DateTime.UtcNow; + var timeSince = start - lastCheck; + if (timeSince.TotalSeconds < 30.0) return; + while (! await BotClient.IsOnline() && !CancellationToken.IsCancellationRequested) { await Task.Delay(5000); @@ -76,6 +80,8 @@ namespace TestNetRewarder throw new Exception(msg); } } + + lastCheck = start; } private static void PrintHelp() diff --git a/Tools/TestNetRewarder/TimeSegmenter.cs b/Tools/TestNetRewarder/TimeSegmenter.cs index a9ad71a..4bdced2 100644 --- a/Tools/TestNetRewarder/TimeSegmenter.cs +++ b/Tools/TestNetRewarder/TimeSegmenter.cs @@ -31,10 +31,12 @@ namespace TestNetRewarder if (end > now) { // Wait for the entire time segment to be in the past. - var delay = (end - now).Add(TimeSpan.FromSeconds(3)); + var delay = end - now; waited = true; + log.Log($"Waiting till time segment is in the past... {Time.FormatDuration(delay)}"); await Task.Delay(delay, Program.CancellationToken); } + await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken); if (Program.CancellationToken.IsCancellationRequested) return; diff --git a/Tools/TestNetRewarder/build-docker.bat b/Tools/TestNetRewarder/build-docker.bat new file mode 100644 index 0000000..32f207b --- /dev/null +++ b/Tools/TestNetRewarder/build-docker.bat @@ -0,0 +1,2 @@ +docker build -f docker/Dockerfile -t thatbenbierens/codex-rewardbot:initial ../.. +docker push thatbenbierens/codex-rewardbot:initial