diff --git a/.vscode/launch.json b/.vscode/launch.json index 7cc21291..08ffe06e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,20 +2,10 @@ "version": "0.2.0", "configurations": [ { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", - "type": "coreclr", + "name": "Sample.RestAPI", + "type": "dotnet", "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/Sample/bin/Debug/net8.0/Sample.dll", - "args": [], - "cwd": "${workspaceFolder}/src/Sample", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false + "projectPath": "${workspaceFolder}/samples/Sample.RestAPI/Sample.RestAPI.csproj" }, { "name": ".NET Core Attach", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 24547a3e..ddbabe2e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,7 +12,11 @@ "${workspaceFolder}", "/property:GenerateFullPaths=true" ], - "problemMatcher": "$msCompile" + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } } ] } \ No newline at end of file diff --git a/samples/Sample.PocketIC/PocketIc.Tests.cs b/samples/Sample.PocketIC/PocketIc.Tests.cs index 4a1e10fa..ee2f89fd 100644 --- a/samples/Sample.PocketIC/PocketIc.Tests.cs +++ b/samples/Sample.PocketIC/PocketIc.Tests.cs @@ -120,7 +120,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid() CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); - CandidArg incResponseArg = await agent.CallAndWaitAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); + CandidArg incResponseArg = await agent.CallAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token); Assert.Equal(CandidArg.Empty(), incResponseArg); // This alternative also doesnt work @@ -137,7 +137,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid() Assert.Equal((UnboundedUInt)1, getResponseValue); CandidArg setRequestArg = CandidArg.FromObjects((UnboundedUInt)10); - CandidArg setResponseArg = await agent.CallAndWaitAsync(canisterId, "set", setRequestArg); + CandidArg setResponseArg = await agent.CallAsync(canisterId, "set", setRequestArg); Assert.Equal(CandidArg.Empty(), setResponseArg); getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty()); diff --git a/samples/Sample.RestAPI/Controllers/GovernanceController.cs b/samples/Sample.RestAPI/Controllers/GovernanceController.cs index 4c983f8c..5c604eb2 100644 --- a/samples/Sample.RestAPI/Controllers/GovernanceController.cs +++ b/samples/Sample.RestAPI/Controllers/GovernanceController.cs @@ -22,5 +22,13 @@ public async Task GetProposalInfo(ulong id) OptionalValue info = await this.Client.GetProposalInfo(id); return this.Ok(info); } + + [Route("rewards")] + [HttpGet] + public async Task GetRewards() + { + Result4 rewards = await this.Client.GetMonthlyNodeProviderRewards(); + return this.Ok(rewards); + } } } \ No newline at end of file diff --git a/src/Agent/API.xml b/src/Agent/API.xml index 806bcfce..d38d926e 100644 --- a/src/Agent/API.xml +++ b/src/Agent/API.xml @@ -96,7 +96,7 @@ - + @@ -156,7 +156,7 @@ Optional. Token to cancel request The id of the request that can be used to look up its status with `GetRequestStatusAsync` - + Sends a call request to a specified canister method and gets back an id of the request that is being processed using /v2/../call. This call does NOT wait for the request to be complete. @@ -211,10 +211,10 @@ Optional. Token to cancel request The id of the request that can be used to look up its status with `GetRequestStatusAsync` - + Sends a call request to a specified canister method, waits for the request to be processed, - the returns the candid response to the call. This is helper method built on top of `CallV2Async` + the returns the candid response to the call. This is helper method built on top of `CallAsynchronousAsync` to wait for the response so it doesn't need to be implemented manually The agent to use for the call @@ -1223,6 +1223,26 @@ Internet Computer, agents must have an independent trustworthy source for this data, and must not be tempted to fetch it from this insecure location + + + Model for a reponse to a read state request + + + + + The status of the call ('replied', 'rejected', 'done) + + + + + The certificate data of the current canister state + + + + The status of the call ('replied', 'rejected', 'done) + The certificate data of the current canister state + + Represents a client for interacting with the Asset Canister API. diff --git a/src/Agent/Agents/HttpAgent.cs b/src/Agent/Agents/HttpAgent.cs index ab67c052..2d0dc555 100644 --- a/src/Agent/Agents/HttpAgent.cs +++ b/src/Agent/Agents/HttpAgent.cs @@ -100,14 +100,14 @@ public async Task CallAsync( byte[] cborBytes = await httpResponse.GetContentAsync(); var reader = new CborReader(cborBytes); - Certificate certificate = Certificate.FromCbor(reader); + V3CallResponse v3CallResponse = V3CallResponse.ReadCbor(reader); SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken); - if (!certificate.IsValid(this.bls, rootPublicKey)) + if (!v3CallResponse.Certificate.IsValid(this.bls, rootPublicKey)) { throw new InvalidCertificateException("Certificate signature does not match the IC public key"); } - HashTree? requestStatusData = certificate.Tree.GetValueOrDefault(StatePath.FromSegments("request_status", requestId.RawValue)); + HashTree? requestStatusData = v3CallResponse.Certificate.Tree.GetValueOrDefault(StatePath.FromSegments("request_status", requestId.RawValue)); RequestStatus? requestStatus = ParseRequestStatus(requestStatusData); switch (requestStatus?.Type) { diff --git a/src/Agent/Responses/V3CallResponse.cs b/src/Agent/Responses/V3CallResponse.cs new file mode 100644 index 00000000..f148c633 --- /dev/null +++ b/src/Agent/Responses/V3CallResponse.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics; +using System.Formats.Cbor; +using EdjCase.ICP.Agent.Models; +using EdjCase.ICP.Candid.Models; + +namespace EdjCase.ICP.Agent.Responses +{ + /// + /// Model for a reponse to a read state request + /// + public class V3CallResponse + { + /// + /// The status of the call ('replied', 'rejected', 'done) + /// + public string Status { get; } + /// + /// The certificate data of the current canister state + /// + public Certificate Certificate { get; } + + /// The status of the call ('replied', 'rejected', 'done) + /// The certificate data of the current canister state + /// + public V3CallResponse(string status, Certificate certificate) + { + this.Status = status ?? throw new ArgumentNullException(nameof(status)); + this.Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + } + + + internal static V3CallResponse ReadCbor(CborReader reader) + { + if (reader.ReadTag() != CborTag.SelfDescribeCbor) + { + throw new CborContentException("Expected self describe tag"); + } + Certificate? certificate = null; + string? status = null; + reader.ReadStartMap(); + while (reader.PeekState() != CborReaderState.EndMap) + { + string field = reader.ReadTextString(); + switch (field) + { + case "certificate": + var certReader = new CborReader(reader.ReadByteString()); + certificate = Certificate.FromCbor(certReader); + break; + case "status": + status = reader.ReadTextString(); + break; + default: + Debug.WriteLine($"Unknown field '{field}' in v3 call response"); + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + if (status == null) + { + throw new CborContentException("Missing field: status"); + } + + if (certificate == null) + { + throw new CborContentException("Missing field: certificate"); + } + + return new V3CallResponse(status, certificate); + } + } +} \ No newline at end of file