diff --git a/samples/Sample.PocketIC/PocketIc.Tests.cs b/samples/Sample.PocketIC/PocketIc.Tests.cs index 63e1967..8e4f64c 100644 --- a/samples/Sample.PocketIC/PocketIc.Tests.cs +++ b/samples/Sample.PocketIC/PocketIc.Tests.cs @@ -16,10 +16,12 @@ public class PocketIcServerFixture : IDisposable { public PocketIcServer Server { get; private set; } + public PocketIcServerFixture() { // Start the server for all tests this.Server = PocketIcServer.StartAsync(runtimeLogLevel: LogLevel.Debug, showErrorLogs: true).GetAwaiter().GetResult(); + } public void Dispose() @@ -110,7 +112,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid() await pocketIc.SetTimeAsync(ICTimestamp.Now()); // Let time progress so that update calls get processed - await using (await pocketIc.AutoProgressTimeAsync()) + await using (await pocketIc.AutoProgressAsync()) { await using (HttpGateway httpGateway = await pocketIc.RunHttpGatewayAsync()) { @@ -121,72 +123,24 @@ public async Task HttpGateway_CounterWasm__Basic__Valid() Assert.Equal((UnboundedUInt)0, getResponseValue); - var processInfo = new ProcessStartInfo - { - FileName = "dfx", - Arguments = $"canister call {canisterId} inc () --network {httpGateway.Url} --identity anonymous --verbose", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = "/home/gekctek/git/ICP.NET", - }; - Debug.WriteLine("dfx " + processInfo.Arguments); - // RequestId requestId; - using (var process = Process.Start(processInfo)) - { - string output = process!.StandardOutput.ReadToEnd(); - string error = process.StandardError.ReadToEnd(); - process.WaitForExit(); - - // Optionally log or handle the output/error - Debug.WriteLine($"Output: {output}"); - Debug.WriteLine($"Error: {error}"); - // requestId = new RequestId(Convert.FromHexString(output.Trim().Substring(2))); - } - Debug.WriteLine("---Start-----\n\n\n\n\n\n\n\n\n\n\n"); - - // CandidArg incResponseArg = await agent.CallAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); - // Assert.Equal(CandidArg.Empty(), incResponseArg); - - // This alternative also doesnt work - RequestId requestId = await agent.CallAsynchronousAsync(canisterId, "inc", CandidArg.Empty()); - // ICTimestamp currentTime = await pocketIc.GetTimeAsync(); - // await pocketIc.SetTimeAsync(currentTime + TimeSpan.FromSeconds(5)); - await pocketIc.TickAsync(1); - // await Task.Delay(5000); - IngressStatus ingressStatus = await pocketIc.GetIngressStatusAsync(requestId, EffectivePrincipal.Canister(canisterId)); - - switch (ingressStatus.Type) - { - case IngressStatusType.Ok: - break; - case IngressStatusType.NotFound: - throw new Exception("Ingress message not found"); - default: - throw new Exception("Ingress message ????"); - } - CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); - CandidArg incResponseArg = await agent.WaitForRequestAsync(canisterId, requestId, cts.Token); // Waits indefinitely here + CandidArg incResponseArg = await agent.CallAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); Assert.Equal(CandidArg.Empty(), incResponseArg); getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); getResponseArg = getResponse.ThrowOrGetReply(); getResponseValue = getResponseArg.ToObjects(); - Assert.Equal((UnboundedUInt)2, getResponseValue); - - // CandidArg setRequestArg = CandidArg.FromObjects((UnboundedUInt)10); - // cts = new(TimeSpan.FromSeconds(5)); - // CandidArg setResponseArg = await agent.CallAsync(canisterId, "set", setRequestArg, cancellationToken: cts.Token); - // Assert.Equal(CandidArg.Empty(), setResponseArg); + Assert.Equal((UnboundedUInt)1, getResponseValue); - // await pocketIc.TickAsync(); + CandidArg setRequestArg = CandidArg.FromObjects((UnboundedUInt)10); + cts = new(TimeSpan.FromSeconds(5)); + CandidArg setResponseArg = await agent.CallAsync(canisterId, "set", setRequestArg, cancellationToken: cts.Token); + Assert.Equal(CandidArg.Empty(), setResponseArg); - // getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); - // getResponseArg = getResponse.ThrowOrGetReply(); - // getResponseValue = getResponseArg.ToObjects(); - // Assert.Equal((UnboundedUInt)10, getResponseValue); + getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); + getResponseArg = getResponse.ThrowOrGetReply(); + getResponseValue = getResponseArg.ToObjects(); + Assert.Equal((UnboundedUInt)10, getResponseValue); } } diff --git a/src/Agent/Agents/HttpAgent.cs b/src/Agent/Agents/HttpAgent.cs index 2d0dc55..cf79ec5 100644 --- a/src/Agent/Agents/HttpAgent.cs +++ b/src/Agent/Agents/HttpAgent.cs @@ -18,6 +18,8 @@ using System.Linq; using EdjCase.ICP.BLS; using System.Threading; +using System.Security.Cryptography; +using System.Diagnostics; namespace EdjCase.ICP.Agent.Agents { @@ -128,7 +130,9 @@ public async Task CallAsync( CallRequest BuildRequest(Principal sender, ICTimestamp now) { - return new CallRequest(canisterId, method, arg, sender, now); + byte[] nonce = new byte[16]; + RandomNumberGenerator.Fill(nonce); + return new CallRequest(canisterId, method, arg, sender, now, nonce); } } @@ -172,7 +176,9 @@ public async Task CallAsynchronousAsync( CallRequest BuildRequest(Principal sender, ICTimestamp now) { - return new CallRequest(canisterId, method, arg, sender, now); + byte[] nonce = new byte[16]; + RandomNumberGenerator.Fill(nonce); + return new CallRequest(canisterId, method, arg, sender, now, nonce); } } @@ -313,7 +319,7 @@ public async Task GetReplicaStatusAsync( SubjectPublicKeyInfo publicKey = this.Identity.GetPublicKey(); principal = publicKey.ToPrincipal(); } - TRequest request = getRequest(principal, ICTimestamp.Future(TimeSpan.FromSeconds(10))); + TRequest request = getRequest(principal, ICTimestamp.Future(TimeSpan.FromMinutes(3))); Dictionary content = request.BuildHashableItem(); SignedContent signedContent; diff --git a/src/Candid/Models/ICTimestamp.cs b/src/Candid/Models/ICTimestamp.cs index 653c720..e698f15 100644 --- a/src/Candid/Models/ICTimestamp.cs +++ b/src/Candid/Models/ICTimestamp.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using EdjCase.ICP.Candid.Crypto; using EdjCase.ICP.Candid.Encodings; using EdjCase.ICP.Candid.Models.Values; @@ -136,13 +137,13 @@ public override string ToString() private static UnboundedUInt EpochNowInNanoseconds() { - ulong nanoseconds = (ulong)(((DateTime.UtcNow - epoch).TotalMilliseconds + REPLICA_PERMITTED_DRIFT_MILLISECONDS) * 1_000_000); + ulong nanoseconds = (ulong)((DateTime.UtcNow - epoch).TotalMilliseconds * 1_000_000); return UnboundedUInt.FromUInt64(nanoseconds); } private static UnboundedUInt GetNanosecondsFromTimeSpan(TimeSpan timespanFromNow) { - return (ulong)(timespanFromNow.Ticks / 100L); + return (ulong)(timespanFromNow.TotalMilliseconds * 1_000_000); } } } \ No newline at end of file diff --git a/src/PocketIC/API.xml b/src/PocketIC/API.xml index c6d146c..7c0b851 100644 --- a/src/PocketIC/API.xml +++ b/src/PocketIC/API.xml @@ -130,6 +130,15 @@ The subnet id The public key principal of the subnet + + + Gets the status of an ingress message + + The id of the PocketIC instance + The id of the request to check the status for + Optional effective principal for the call + + Submits an ingress message to a canister without waiting for execution @@ -261,6 +270,49 @@ The instance has been deleted + + + A variant model representing the status of an ingress message + + + + + The type of ingress status + + + + + Gets the Ok variant data for the status + + + + + Creates a new Ok status with the variant data + + The request status data + An Ok ingress variant model + + + + Creates a new NotFound status + + A NotFound ingress variant model + + + + The variant types of ingress status + + + + + Not Found + + + + + Ok + + Configuration for HTTPS support @@ -376,6 +428,26 @@ The principal id + + + Creates an empty effective principal + + An empty effective principal + + + + Creates an effective principal for a subnet + + The subnet id + A subnet effective principal + + + + Creates an effective principal for a canister + + The canister id + A canister effective principal + Types of effective principals @@ -644,6 +716,9 @@ + + + @@ -907,9 +982,9 @@ The timestamp to set - + - Enables automatic time progression for the IC instance until disposed + Enables automatic time/tick progression for the IC instance until disposed Optional delay between time updates A disposable object that will stop auto progression when disposed @@ -935,6 +1010,14 @@ Whether to use cached topology info. Defaults to true List of subnet topology information + + + Gets the ingress status of the specified request + + The request ID to check + The effective principal for the request + The ingress status of the request + Gets the cycles balance of a canister diff --git a/src/PocketIC/IPocketIcHttpClient.cs b/src/PocketIC/IPocketIcHttpClient.cs index a0de04f..410ca37 100644 --- a/src/PocketIC/IPocketIcHttpClient.cs +++ b/src/PocketIC/IPocketIcHttpClient.cs @@ -150,7 +150,17 @@ Task QueryCallAsync( /// The public key principal of the subnet Task GetPublicKeyForSubnetAsync(int instanceId, Principal subnetId); - Task GetIngressStatusAsync(int instanceId, RequestId messageId, EffectivePrincipal effectivePrincipal); + /// + /// Gets the status of an ingress message + /// + /// The id of the PocketIC instance + /// The id of the request to check the status for + /// Optional effective principal for the call + /// + Task GetIngressStatusAsync( + int instanceId, + RequestId requestId, + EffectivePrincipal effectivePrincipal); /// /// Submits an ingress message to a canister without waiting for execution @@ -302,9 +312,14 @@ public enum InstanceStatus /// Deleted } - +/// +/// A variant model representing the status of an ingress message +/// public class IngressStatus { + /// + /// The type of ingress status + /// public IngressStatusType Type { get; } private object? value; @@ -315,25 +330,46 @@ private IngressStatus(IngressStatusType type, object? value) this.value = value; } + /// + /// Gets the Ok variant data for the status + /// public RequestStatus AsOk() { return (RequestStatus)this.value!; } + /// + /// Creates a new Ok status with the variant data + /// + /// The request status data + /// An Ok ingress variant model public static IngressStatus Ok(RequestStatus status) { return new IngressStatus(IngressStatusType.Ok, status); } + /// + /// Creates a new NotFound status + /// + /// A NotFound ingress variant model public static IngressStatus NotFound() { return new IngressStatus(IngressStatusType.NotFound, null); } } +/// +/// The variant types of ingress status +/// public enum IngressStatusType { + /// + /// Not Found + /// NotFound, + /// + /// Ok + /// Ok } @@ -456,16 +492,30 @@ private EffectivePrincipal(EffectivePrincipalType type, Principal id) this.Id = id; } + /// + /// Creates an empty effective principal + /// + /// An empty effective principal public static EffectivePrincipal None() { return new EffectivePrincipal(EffectivePrincipalType.None, Principal.Anonymous()); } + /// + /// Creates an effective principal for a subnet + /// + /// The subnet id + /// A subnet effective principal public static EffectivePrincipal Subnet(Principal id) { return new EffectivePrincipal(EffectivePrincipalType.Subnet, id); } + /// + /// Creates an effective principal for a canister + /// + /// The canister id + /// A canister effective principal public static EffectivePrincipal Canister(Principal id) { return new EffectivePrincipal(EffectivePrincipalType.Canister, id); diff --git a/src/PocketIC/PocketIc.cs b/src/PocketIC/PocketIc.cs index 4e98d21..c95c358 100644 --- a/src/PocketIC/PocketIc.cs +++ b/src/PocketIC/PocketIc.cs @@ -211,11 +211,11 @@ public Task SetTimeAsync(ICTimestamp time) } /// - /// Enables automatic time progression for the IC instance until disposed + /// Enables automatic time/tick progression for the IC instance until disposed /// /// Optional delay between time updates /// A disposable object that will stop auto progression when disposed - public async Task AutoProgressTimeAsync(TimeSpan? artificalDelay = null) + public async Task AutoProgressAsync(TimeSpan? artificalDelay = null) { await this.HttpClient.AutoProgressTimeAsync(this.InstanceId, artificalDelay); @@ -262,9 +262,15 @@ public async ValueTask> GetTopologyAsync(bool useCache = tr return topologies; } - public async Task GetIngressStatusAsync(RequestId messageId, EffectivePrincipal effectivePrincipal) + /// + /// Gets the ingress status of the specified request + /// + /// The request ID to check + /// The effective principal for the request + /// The ingress status of the request + public async Task GetIngressStatusAsync(RequestId requestId, EffectivePrincipal effectivePrincipal) { - return await this.HttpClient.GetIngressStatusAsync(this.InstanceId, messageId, effectivePrincipal); + return await this.HttpClient.GetIngressStatusAsync(this.InstanceId, requestId, effectivePrincipal); } /// diff --git a/src/PocketIC/PocketIcHttpClient.cs b/src/PocketIC/PocketIcHttpClient.cs index 6c69cac..001664e 100644 --- a/src/PocketIC/PocketIcHttpClient.cs +++ b/src/PocketIC/PocketIcHttpClient.cs @@ -205,6 +205,7 @@ public async Task DeleteInstanceAsync(int id) { await this.DeleteAsync($"/instances/{id}"); } + /// public async Task QueryCallAsync( int instanceId, @@ -212,7 +213,7 @@ public async Task QueryCallAsync( Principal canisterId, string method, CandidArg request, - EffectivePrincipal effectivePrincipal) + EffectivePrincipal? effectivePrincipal = null) { return await this.ProcessIngressMessageInternalAsync( $"/instances/{instanceId}/read/query", @@ -326,7 +327,11 @@ public async Task GetPublicKeyForSubnetAsync(int instanceId, Principa return Principal.FromBytes(publicKey); } - public async Task GetIngressStatusAsync(int instanceId, RequestId messageId, EffectivePrincipal effectivePrincipal) + /// + public async Task GetIngressStatusAsync( + int instanceId, + RequestId messageId, + EffectivePrincipal effectivePrincipal) { var data = new JsonObject { @@ -381,7 +386,7 @@ public async Task SubmitIngressMessageAsync( Principal canisterId, string method, CandidArg request, - EffectivePrincipal effectivePrincipal) + EffectivePrincipal? effectivePrincipal = null) { return await this.ProcessIngressMessageInternalAsync( $"/instances/{instanceId}/update/submit_ingress_message", @@ -399,7 +404,7 @@ public async Task ExecuteIngressMessageAsync( Principal canisterId, string method, CandidArg request, - EffectivePrincipal effectivePrincipal) + EffectivePrincipal? effectivePrincipal = null) { return await this.ProcessIngressMessageInternalAsync( $"/instances/{instanceId}/update/execute_ingress_message", @@ -438,11 +443,11 @@ private async Task ProcessIngressMessageInternalAsync( Principal canisterId, string method, CandidArg arg, - EffectivePrincipal effectivePrincipal) + EffectivePrincipal? effectivePrincipal = null) { byte[] payload = arg.Encode(); - JsonNode effectivePrincipalJson = EffectivePrincipalToJson(effectivePrincipal); + JsonNode effectivePrincipalJson = EffectivePrincipalToJson(effectivePrincipal ?? EffectivePrincipal.Canister(canisterId)); var options = new JsonObject { diff --git a/src/PocketIC/PocketIcServer.cs b/src/PocketIC/PocketIcServer.cs index eea5543..2f2108d 100644 --- a/src/PocketIC/PocketIcServer.cs +++ b/src/PocketIC/PocketIcServer.cs @@ -62,7 +62,7 @@ public static async Task StartAsync( string binPath = GetBinPath(); EnsureExecutablePermission(binPath); - int pid = Process.GetCurrentProcess().Id; + string pid = Guid.NewGuid().ToString(); string picFilePrefix = $"pocket_ic_{pid}"; string portFilePath = Path.Combine(Path.GetTempPath(), $"{picFilePrefix}.port"); diff --git a/test/PocketIC.Tests/PocketIc.Tests.cs b/test/PocketIC.Tests/PocketIc.Tests.cs index 3607d6f..0f0f75c 100644 --- a/test/PocketIC.Tests/PocketIc.Tests.cs +++ b/test/PocketIC.Tests/PocketIc.Tests.cs @@ -122,26 +122,13 @@ await pocketIc.UpdateCallNoResponseAsync( // Test time ICTimestamp initialTime = await pocketIc.GetTimeAsync(); - ICTimestamp newTime = new(initialTime.NanoSeconds + (UnboundedUInt)1_000); + ICTimestamp newTime = ICTimestamp.Now(); await pocketIc.SetTimeAsync(newTime); ICTimestamp resetTime = await pocketIc.GetTimeAsync(); Assert.Equal(newTime.NanoSeconds, resetTime.NanoSeconds); - // Test auto progress time - await using (await pocketIc.AutoProgressTimeAsync()) - { - await Task.Delay(100); - ICTimestamp autoProgressedTime = await pocketIc.GetTimeAsync(); - Assert.True(autoProgressedTime.NanoSeconds > resetTime.NanoSeconds); - } - - // Verify time is stopped - ICTimestamp stopProgressTime = await pocketIc.GetTimeAsync(); - await Task.Delay(100); - ICTimestamp stopProgressTime2 = await pocketIc.GetTimeAsync(); - Assert.Equal(stopProgressTime.NanoSeconds, stopProgressTime2.NanoSeconds); // Test subnet id Principal subnetId = await pocketIc.GetSubnetIdForCanisterAsync(canisterId); @@ -159,26 +146,38 @@ await pocketIc.UpdateCallNoResponseAsync( Assert.Equal(newStableMemory, stableMemory[..8]); - // Setup http gateway and test api call to canister - await using (HttpGateway httpGateway = await pocketIc.RunHttpGatewayAsync()) + // Test auto progress time + await using (await pocketIc.AutoProgressAsync()) { - HttpAgent agent = httpGateway.BuildHttpAgent(); - QueryResponse getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); - CandidArg getResponseArg = getResponse.ThrowOrGetReply(); - UnboundedUInt getResponseValue = getResponseArg.ToObjects(); - Assert.Equal((UnboundedUInt)1, getResponseValue); - - CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); - CandidArg incResponseArg = await agent.CallAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); - Assert.Equal(CandidArg.Empty(), incResponseArg); - - getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); - getResponseArg = getResponse.ThrowOrGetReply(); - getResponseValue = getResponseArg.ToObjects(); - Assert.Equal((UnboundedUInt)2, getResponseValue); - + await Task.Delay(100); + ICTimestamp autoProgressedTime = await pocketIc.GetTimeAsync(); + Assert.True(autoProgressedTime.NanoSeconds > resetTime.NanoSeconds); + // Setup http gateway and test api call to canister (needs auto progress) + await using (HttpGateway httpGateway = await pocketIc.RunHttpGatewayAsync()) + { + HttpAgent agent = httpGateway.BuildHttpAgent(); + QueryResponse getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); + CandidArg getResponseArg = getResponse.ThrowOrGetReply(); + UnboundedUInt getResponseValue = getResponseArg.ToObjects(); + Assert.Equal((UnboundedUInt)1, getResponseValue); + + CancellationTokenSource cts = new(TimeSpan.FromSeconds(50)); + CandidArg incResponseArg = await agent.CallAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); + Assert.Equal(CandidArg.Empty(), incResponseArg); + + getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); + getResponseArg = getResponse.ThrowOrGetReply(); + getResponseValue = getResponseArg.ToObjects(); + Assert.Equal((UnboundedUInt)2, getResponseValue); + + } } + // Verify time is stopped + ICTimestamp stopProgressTime = await pocketIc.GetTimeAsync(); + await Task.Delay(100); + ICTimestamp stopProgressTime2 = await pocketIc.GetTimeAsync(); + Assert.Equal(stopProgressTime.NanoSeconds, stopProgressTime2.NanoSeconds); // Stop canister await pocketIc.StopCanisterAsync(canisterId); diff --git a/test/PocketIC.Tests/PocketIcServerFixture.cs b/test/PocketIC.Tests/PocketIcServerFixture.cs index 6442599..0da4e4c 100644 --- a/test/PocketIC.Tests/PocketIcServerFixture.cs +++ b/test/PocketIC.Tests/PocketIcServerFixture.cs @@ -11,7 +11,7 @@ public class PocketIcServerFixture : IDisposable public PocketIcServerFixture() { // Start the server for all tests - this.Server = PocketIcServer.StartAsync(runtimeLogLevel: LogLevel.Debug).GetAwaiter().GetResult(); + this.Server = PocketIcServer.StartAsync(runtimeLogLevel: LogLevel.Debug, showErrorLogs: true).GetAwaiter().GetResult(); } public void Dispose() @@ -19,6 +19,7 @@ public void Dispose() // Stop the server after all tests if (this.Server != null) { + this.Server.StopAsync().GetAwaiter().GetResult(); this.Server.DisposeAsync().GetAwaiter().GetResult(); } }