diff --git a/src/Agent/API.xml b/src/Agent/API.xml index 65fbf36..e8e0c69 100644 --- a/src/Agent/API.xml +++ b/src/Agent/API.xml @@ -1228,20 +1228,24 @@ Model for a reponse to a read state request - + - The status of the call ('replied', 'rejected', 'done) + The request has been processed and it has reply data - + - The certificate data of the current canister state + The status of the call ('replied', 'non_replicated_rejection') - - The status of the call ('replied', 'rejected', 'done) - The certificate data of the current canister state - + + The status of the call ('replied', 'non_replicated_rejection') + The certificate data of the current canister state + + + + Returns the 'replied' certificate IF the status is 'replied', otherwise throws exception + diff --git a/src/Agent/Agents/HttpAgent.cs b/src/Agent/Agents/HttpAgent.cs index cf79ec5..c3272c4 100644 --- a/src/Agent/Agents/HttpAgent.cs +++ b/src/Agent/Agents/HttpAgent.cs @@ -103,13 +103,25 @@ public async Task CallAsync( byte[] cborBytes = await httpResponse.GetContentAsync(); var reader = new CborReader(cborBytes); V3CallResponse v3CallResponse = V3CallResponse.ReadCbor(reader); + Certificate certificate; + switch (v3CallResponse.Status) + { + case V3CallResponse.StatusType.Replied: + certificate = v3CallResponse.AsReplied(); + break; + case V3CallResponse.StatusType.NonReplicatedRejection: + CallRejectedResponse callRejectedResponse = v3CallResponse.AsNonReplicatedRejection(); + throw new CallRejectedException(callRejectedResponse.Code, callRejectedResponse.Message, callRejectedResponse.ErrorCode); + default: + throw new NotImplementedException($"Invalid v3 call response status '{v3CallResponse.Status}'"); + } SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken); - if (!v3CallResponse.Certificate.IsValid(this.bls, rootPublicKey)) + if (!certificate.IsValid(this.bls, rootPublicKey)) { throw new InvalidCertificateException("Certificate signature does not match the IC public key"); } - HashTree? requestStatusData = v3CallResponse.Certificate.Tree.GetValueOrDefault(StatePath.FromSegments("request_status", requestId.RawValue)); + HashTree? requestStatusData = 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 index f148c63..1990d5d 100644 --- a/src/Agent/Responses/V3CallResponse.cs +++ b/src/Agent/Responses/V3CallResponse.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Formats.Cbor; using EdjCase.ICP.Agent.Models; @@ -9,26 +10,55 @@ namespace EdjCase.ICP.Agent.Responses /// /// Model for a reponse to a read state request /// - public class V3CallResponse + internal class V3CallResponse { + public enum StatusType + { + /// + /// The request has been processed and it has reply data + /// + Replied, + NonReplicatedRejection + } + + /// - /// The status of the call ('replied', 'rejected', 'done) + /// The status of the call ('replied', 'non_replicated_rejection') /// - public string Status { get; } + public StatusType Status { get; } + + private object? value { get; } + + /// The status of the call ('replied', 'non_replicated_rejection') + /// The certificate data of the current canister state + private V3CallResponse(StatusType status, object? value) + { + this.Status = status; + this.value = value; + } + /// - /// The certificate data of the current canister state + /// Returns the 'replied' certificate IF the status is 'replied', otherwise throws exception /// - public Certificate Certificate { get; } + public Certificate AsReplied() + { + this.ValidateType(StatusType.Replied); + return (Certificate)this.value!; + } - /// The status of the call ('replied', 'rejected', 'done) - /// The certificate data of the current canister state - /// - public V3CallResponse(string status, Certificate certificate) + public CallRejectedResponse AsNonReplicatedRejection() { - this.Status = status ?? throw new ArgumentNullException(nameof(status)); - this.Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + this.ValidateType(StatusType.NonReplicatedRejection); + return (CallRejectedResponse)this.value!; } + private void ValidateType(StatusType type) + { + if (this.Status != type) + { + throw new InvalidOperationException($"Expected status '{type}' but was '{this.Status}'"); + } + } internal static V3CallResponse ReadCbor(CborReader reader) { @@ -36,8 +66,7 @@ internal static V3CallResponse ReadCbor(CborReader reader) { throw new CborContentException("Expected self describe tag"); } - Certificate? certificate = null; - string? status = null; + Dictionary map = new (); reader.ReadStartMap(); while (reader.PeekState() != CborReaderState.EndMap) { @@ -46,10 +75,19 @@ internal static V3CallResponse ReadCbor(CborReader reader) { case "certificate": var certReader = new CborReader(reader.ReadByteString()); - certificate = Certificate.FromCbor(certReader); + map["certificate"] = Certificate.FromCbor(certReader); break; case "status": - status = reader.ReadTextString(); + map["status"] = reader.ReadTextString(); + break; + case "reject_code": + map["reject_code"] = (RejectCode)reader.ReadUInt64(); + break; + case "reject_message": + map["reject_message"] = reader.ReadTextString(); + break; + case "error_code": + map["error_code"] = reader.ReadTextString(); break; default: Debug.WriteLine($"Unknown field '{field}' in v3 call response"); @@ -59,17 +97,30 @@ internal static V3CallResponse ReadCbor(CborReader reader) } reader.ReadEndMap(); - if (status == null) + if (map["status"] == null) { throw new CborContentException("Missing field: status"); } + StatusType status = Enum.Parse((string)map["status"], true); - if (certificate == null) + switch (status) { - throw new CborContentException("Missing field: certificate"); + case StatusType.Replied: + Certificate? certificate = map["certificate"] as Certificate; + if (certificate == null) + { + throw new CborContentException("Missing field: certificate"); + } + return new V3CallResponse(status, certificate); + case StatusType.NonReplicatedRejection: + RejectCode rejectCode = (RejectCode)(map["reject_code"] ?? throw new CborContentException("Missing field: reject_code")); + string message = (string)(map["reject_message"] ?? throw new CborContentException("Missing field: reject_message")); + string? errorCode = (string?)map["error_code"]; + CallRejectedResponse? rejectedResponse = new CallRejectedResponse(rejectCode, message, errorCode); + return new V3CallResponse(status, rejectedResponse); + default: + throw new NotImplementedException($"Unknown status '{status}' in v3 call response"); } - - return new V3CallResponse(status, certificate); } } -} \ No newline at end of file +}