diff --git a/DistTestCore/Codex/CodexAccess.cs b/DistTestCore/Codex/CodexAccess.cs index c07a457..ae84a91 100644 --- a/DistTestCore/Codex/CodexAccess.cs +++ b/DistTestCore/Codex/CodexAccess.cs @@ -26,6 +26,16 @@ namespace DistTestCore.Codex return Http().HttpGetStream("download/" + contentId); } + public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request) + { + return Http().HttpPostJson("sales/availability", request); + } + + public CodexSalesRequestStorageResponse RequestStorage(CodexSalesRequestStorageRequest request, string contentId) + { + return Http().HttpPostJson($"storage/request/{contentId}", request); + } + private Http Http() { var ip = Container.Pod.Cluster.IP; @@ -53,4 +63,37 @@ namespace DistTestCore.Codex public string version { get; set; } = string.Empty; public string revision { get; set; } = string.Empty; } + + public class CodexSalesAvailabilityRequest + { + public string size { get; set; } = string.Empty; + public string duration { get; set; } = string.Empty; + public string minPrice { get; set; } = string.Empty; + public string maxCollateral { get; set; } = string.Empty; + } + + public class CodexSalesAvailabilityResponse + { + public string id { get; set; } = string.Empty; + public string size { get; set; } = string.Empty; + public string duration { get; set; } = string.Empty; + public string minPrice { get; set; } = string.Empty; + public string maxCollateral { get; set; } = string.Empty; + } + + public class CodexSalesRequestStorageRequest + { + public string duration { get; set; } = string.Empty; + public string proofProbability { get; set; } = string.Empty; + public string reward { get; set; } = string.Empty; + public string collateral { get; set; } = string.Empty; + public string? expiry { get; set; } + public uint? nodes { get; set; } + public uint? tolerance { get; set;} + } + + public class CodexSalesRequestStorageResponse + { + public string purchaseId { get; set; } = string.Empty; + } } diff --git a/DistTestCore/GethStarter.cs b/DistTestCore/GethStarter.cs index 50a2d86..2b2cbb3 100644 --- a/DistTestCore/GethStarter.cs +++ b/DistTestCore/GethStarter.cs @@ -32,7 +32,7 @@ namespace DistTestCore private void TransferInitialBalance(MarketplaceNetwork marketplaceNetwork, MarketplaceInitialConfig marketplaceConfig, GethCompanionNodeInfo[] companionNodes) { var interaction = marketplaceNetwork.StartInteraction(lifecycle.Log); - var tokenAddress = interaction.GetTokenAddress(marketplaceNetwork.Marketplace.Address); + var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress; foreach (var node in companionNodes) { @@ -79,7 +79,7 @@ namespace DistTestCore if (network == null) { var bootstrapInfo = bootstrapNodeStarter.StartGethBootstrapNode(); - var marketplaceInfo = codexContractsStarter.Start(bootstrapInfo.RunningContainers.Containers[0]); + var marketplaceInfo = codexContractsStarter.Start(bootstrapInfo); network = new MarketplaceNetwork(bootstrapInfo, marketplaceInfo ); } return network; diff --git a/DistTestCore/Http.cs b/DistTestCore/Http.cs index f75768b..0d242cf 100644 --- a/DistTestCore/Http.cs +++ b/DistTestCore/Http.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using NUnit.Framework; using System.Net.Http.Headers; +using System.Net.Http.Json; using Utils; namespace DistTestCore @@ -37,6 +38,19 @@ namespace DistTestCore return JsonConvert.DeserializeObject(HttpGetString(route))!; } + public TResponse HttpPostJson(string route, TRequest body) + { + return Retry(() => + { + using var client = GetClient(); + var url = GetUrl() + route; + using var content = JsonContent.Create(body); + var result = Time.Wait(client.PostAsync(url, content)); + var json = Time.Wait(result.Content.ReadAsStringAsync()); + return JsonConvert.DeserializeObject(json)!; + }); + } + public string HttpPostStream(string route, Stream stream) { return Retry(() => diff --git a/DistTestCore/Marketplace/CodexContractsStarter.cs b/DistTestCore/Marketplace/CodexContractsStarter.cs index 1be0df3..841e830 100644 --- a/DistTestCore/Marketplace/CodexContractsStarter.cs +++ b/DistTestCore/Marketplace/CodexContractsStarter.cs @@ -12,12 +12,12 @@ namespace DistTestCore.Marketplace { } - public MarketplaceInfo Start(RunningContainer bootstrapContainer) + public MarketplaceInfo Start(GethBootstrapNodeInfo bootstrapNode) { LogStart("Deploying Codex contracts..."); var workflow = workflowCreator.CreateWorkflow(); - var startupConfig = CreateStartupConfig(bootstrapContainer); + var startupConfig = CreateStartupConfig(bootstrapNode.RunningContainers.Containers[0]); var containers = workflow.Start(1, Location.Unspecified, new CodexContractsContainerRecipe(), startupConfig); if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure."); @@ -33,9 +33,12 @@ namespace DistTestCore.Marketplace var extractor = new ContainerInfoExtractor(workflow, container); var marketplaceAddress = extractor.ExtractMarketplaceAddress(); + var interaction = bootstrapNode.StartInteraction(lifecycle.Log); + var tokenAddress = interaction.GetTokenAddress(marketplaceAddress); + LogEnd("Contracts deployed."); - return new MarketplaceInfo(marketplaceAddress); + return new MarketplaceInfo(marketplaceAddress, tokenAddress); } private void WaitUntil(Func predicate) @@ -54,12 +57,14 @@ namespace DistTestCore.Marketplace public class MarketplaceInfo { - public MarketplaceInfo(string address) + public MarketplaceInfo(string address, string tokenAddress) { Address = address; + TokenAddress = tokenAddress; } public string Address { get; } + public string TokenAddress { get; } } public class ContractsReadyLogHandler : LogHandler diff --git a/DistTestCore/Marketplace/GethBootstrapNodeInfo.cs b/DistTestCore/Marketplace/GethBootstrapNodeInfo.cs index ab4196f..68c7485 100644 --- a/DistTestCore/Marketplace/GethBootstrapNodeInfo.cs +++ b/DistTestCore/Marketplace/GethBootstrapNodeInfo.cs @@ -1,4 +1,6 @@ using KubernetesWorkflow; +using Logging; +using NethereumWorkflow; namespace DistTestCore.Marketplace { @@ -20,5 +22,16 @@ namespace DistTestCore.Marketplace public string PubKey { get; } public string PrivateKey { get; } public Port DiscoveryPort { get; } + + public NethereumInteraction StartInteraction(TestLog log) + { + var ip = RunningContainers.RunningPod.Cluster.IP; + var port = RunningContainers.Containers[0].ServicePorts[0].Number; + var account = Account; + var privateKey = PrivateKey; + + var creator = new NethereumInteractionCreator(log, ip, port, account, privateKey); + return creator.CreateWorkflow(); + } } } diff --git a/DistTestCore/Marketplace/MarketplaceAccess.cs b/DistTestCore/Marketplace/MarketplaceAccess.cs index 40e8506..dd784c9 100644 --- a/DistTestCore/Marketplace/MarketplaceAccess.cs +++ b/DistTestCore/Marketplace/MarketplaceAccess.cs @@ -1,13 +1,15 @@ -using Logging; +using DistTestCore.Codex; +using Logging; using NUnit.Framework; using NUnit.Framework.Constraints; +using System.Numerics; namespace DistTestCore.Marketplace { public interface IMarketplaceAccess { - void MakeStorageAvailable(ByteSize size, int minPricePerBytePerSecond, float maxCollateral); - void RequestStorage(ContentId contentId, int pricePerBytePerSecond, float requiredCollateral, float minRequiredNumberOfNodes); + string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan maxDuration); + string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration); void AssertThatBalance(IResolveConstraint constraint, string message = ""); decimal GetBalance(); } @@ -17,22 +19,58 @@ namespace DistTestCore.Marketplace private readonly TestLog log; private readonly MarketplaceNetwork marketplaceNetwork; private readonly GethCompanionNodeInfo companionNode; + private readonly CodexAccess codexAccess; - public MarketplaceAccess(TestLog log, MarketplaceNetwork marketplaceNetwork, GethCompanionNodeInfo companionNode) + public MarketplaceAccess(TestLog log, MarketplaceNetwork marketplaceNetwork, GethCompanionNodeInfo companionNode, CodexAccess codexAccess) { this.log = log; this.marketplaceNetwork = marketplaceNetwork; this.companionNode = companionNode; + this.codexAccess = codexAccess; } - public void RequestStorage(ContentId contentId, int pricePerBytePerSecond, float requiredCollateral, float minRequiredNumberOfNodes) + public string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration) { - throw new NotImplementedException(); + var request = new CodexSalesRequestStorageRequest + { + duration = ToHexBigInt(duration.TotalSeconds), + proofProbability = ToHexBigInt(proofProbability), + reward = ToHexBigInt(pricePerBytePerSecond), + collateral = ToHexBigInt(requiredCollateral), + expiry = null, + nodes = minRequiredNumberOfNodes, + tolerance = null, + }; + + var response = codexAccess.RequestStorage(request, contentId.Id); + + return response.purchaseId; } - public void MakeStorageAvailable(ByteSize size, int minPricePerBytePerSecond, float maxCollateral) + public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration) { - throw new NotImplementedException(); + var request = new CodexSalesAvailabilityRequest + { + size = ToHexBigInt(size.SizeInBytes), + duration = ToHexBigInt(duration.TotalSeconds), + maxCollateral = ToHexBigInt(maxCollateral), + minPrice = ToHexBigInt(minPricePerBytePerSecond) + }; + + var response = codexAccess.SalesAvailability(request); + + return response.id; + } + + private string ToHexBigInt(double d) + { + return "0x" + string.Format("{0:X}", Convert.ToInt64(d)); + } + + public string ToHexBigInt(TestToken t) + { + var bigInt = new BigInteger(t.Amount); + return "0x" + bigInt.ToString("X"); } public void AssertThatBalance(IResolveConstraint constraint, string message = "") @@ -43,20 +81,22 @@ namespace DistTestCore.Marketplace public decimal GetBalance() { var interaction = marketplaceNetwork.StartInteraction(log); - return interaction.GetBalance(companionNode.Account); + return interaction.GetBalance(marketplaceNetwork.Marketplace.TokenAddress, companionNode.Account); } } public class MarketplaceUnavailable : IMarketplaceAccess { - public void RequestStorage(ContentId contentId, int pricePerBytePerSecond, float requiredCollateral, float minRequiredNumberOfNodes) + public string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration) { Unavailable(); + return string.Empty; } - public void MakeStorageAvailable(ByteSize size, int minPricePerBytePerSecond, float maxCollateral) + public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration) { Unavailable(); + return string.Empty; } public void AssertThatBalance(IResolveConstraint constraint, string message = "") diff --git a/DistTestCore/Marketplace/MarketplaceAccessFactory.cs b/DistTestCore/Marketplace/MarketplaceAccessFactory.cs index cf268ae..b0389e1 100644 --- a/DistTestCore/Marketplace/MarketplaceAccessFactory.cs +++ b/DistTestCore/Marketplace/MarketplaceAccessFactory.cs @@ -30,7 +30,7 @@ namespace DistTestCore.Marketplace public IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access) { var companionNode = GetGethCompanionNode(access); - return new MarketplaceAccess(log, marketplaceNetwork, companionNode); + return new MarketplaceAccess(log, marketplaceNetwork, companionNode, access); } private GethCompanionNodeInfo GetGethCompanionNode(CodexAccess access) diff --git a/DistTestCore/Marketplace/MarketplaceNetwork.cs b/DistTestCore/Marketplace/MarketplaceNetwork.cs index 7d27372..5f43fa9 100644 --- a/DistTestCore/Marketplace/MarketplaceNetwork.cs +++ b/DistTestCore/Marketplace/MarketplaceNetwork.cs @@ -16,13 +16,7 @@ namespace DistTestCore.Marketplace public NethereumInteraction StartInteraction(TestLog log) { - var ip = Bootstrap.RunningContainers.RunningPod.Cluster.IP; - var port = Bootstrap.RunningContainers.Containers[0].ServicePorts[0].Number; - var account = Bootstrap.Account; - var privateKey = Bootstrap.PrivateKey; - - var creator = new NethereumInteractionCreator(log, ip, port, account, privateKey); - return creator.CreateWorkflow(); + return Bootstrap.StartInteraction(log); } } } diff --git a/Nethereum/NethereumInteraction.cs b/Nethereum/NethereumInteraction.cs index 9092524..0388bd0 100644 --- a/Nethereum/NethereumInteraction.cs +++ b/Nethereum/NethereumInteraction.cs @@ -56,10 +56,16 @@ namespace NethereumWorkflow Time.Wait(handler.SendRequestAndWaitForReceiptAsync(tokenAddress, function)); } - public decimal GetBalance(string account) + public decimal GetBalance(string tokenAddress, string account) { - var bigInt = Time.Wait(web3.Eth.GetBalance.SendRequestAsync(account)); - var result = ToDecimal(bigInt); + var function = new GetTokenBalanceFunction + { + Owner = account + }; + + var handler = web3.Eth.GetContractQueryHandler(); + var result = ToDecimal(Time.Wait(handler.QueryAsync(tokenAddress, function))); + Log($"Balance of {account} is {result}"); return result; } @@ -81,6 +87,11 @@ namespace NethereumWorkflow return (decimal)hexBigInteger.Value; } + private decimal ToDecimal(BigInteger bigInteger) + { + return (decimal)bigInteger; + } + private void Log(string msg) { log.Log(msg); @@ -101,4 +112,11 @@ namespace NethereumWorkflow [Parameter("uint256", "amount", 2)] public BigInteger Amount { get; set; } } + + [Function("balanceOf", "uint256")] + public class GetTokenBalanceFunction :FunctionMessage + { + [Parameter("address", "owner", 1)] + public string Owner { get; set; } + } } diff --git a/Tests/BasicTests/ExampleTests.cs b/Tests/BasicTests/ExampleTests.cs index 01eee02..3269225 100644 --- a/Tests/BasicTests/ExampleTests.cs +++ b/Tests/BasicTests/ExampleTests.cs @@ -1,6 +1,7 @@ using DistTestCore; using DistTestCore.Codex; using NUnit.Framework; +using Utils; namespace Tests.BasicTests { @@ -49,38 +50,44 @@ namespace Tests.BasicTests [Test] public void MarketplaceExample() { - var group = SetupCodexNodes(2) + var primary = SetupCodexNodes(1) .WithStorageQuota(10.GB()) - .EnableMarketplace(20.TestTokens()) - .BringOnline(); + .EnableMarketplace(initialBalance: 234.TestTokens()) + .BringOnline()[0]; - foreach (var node in group) - { - Assert.That(node.Marketplace.GetBalance(), Is.EqualTo(20)); - } + Assert.That(primary.Marketplace.GetBalance(), Is.EqualTo(234)); - // WIP: Balance is now only ETH. - // todo: All nodes should have plenty of ETH to pay for transactions. - // todo: Upload our own token, use this exclusively. ETH should be invisibile to the tests. + var secondary = SetupCodexNodes(1) + .EnableMarketplace(initialBalance: 1000.TestTokens()) + .BringOnline()[0]; + primary.ConnectToPeer(secondary); - //var secondary = SetupCodexNodes(1) - // .EnableMarketplace(initialBalance: 1000) - // .BringOnline()[0]; + // Gives 503 - Persistance not enabled in current codex image. + primary.Marketplace.MakeStorageAvailable( + size: 10.GB(), + minPricePerBytePerSecond: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); - //primary.ConnectToPeer(secondary); - //primary.Marketplace.MakeStorageAvailable(10.GB(), minPricePerBytePerSecond: 1, maxCollateral: 20); + var testFile = GenerateTestFile(10.MB()); + var contentId = secondary.UploadFile(testFile); - //var testFile = GenerateTestFile(10.MB()); - //var contentId = secondary.UploadFile(testFile); - //secondary.Marketplace.RequestStorage(contentId, pricePerBytePerSecond: 2, - // requiredCollateral: 10, minRequiredNumberOfNodes: 1); + // Gives 500 - Persistance not enabled in current codex image. + secondary.Marketplace.RequestStorage(contentId, + pricePerBytePerSecond: 2.TestTokens(), + requiredCollateral: 10.TestTokens(), + minRequiredNumberOfNodes: 1, + proofProbability: 5, + duration: TimeSpan.FromMinutes(2)); - //primary.Marketplace.AssertThatBalance(Is.LessThan(20), "Collateral was not placed."); - //var primaryBalance = primary.Marketplace.GetBalance(); + Time.Sleep(TimeSpan.FromMinutes(3)); - //secondary.Marketplace.AssertThatBalance(Is.LessThan(1000), "Contractor was not charged for storage."); - //primary.Marketplace.AssertThatBalance(Is.GreaterThan(primaryBalance), "Storer was not paid for storage."); + primary.Marketplace.AssertThatBalance(Is.LessThan(20), "Collateral was not placed."); + var primaryBalance = primary.Marketplace.GetBalance(); + + secondary.Marketplace.AssertThatBalance(Is.LessThan(1000), "Contractor was not charged for storage."); + primary.Marketplace.AssertThatBalance(Is.GreaterThan(primaryBalance), "Storer was not paid for storage."); } } }