+}
+
+@code {
+ public ulong? ProposalId { get; set; }
+ public bool MadeCall { get; set; }
+ public OptionalValue? Info { get; set; }
+
+ public async Task ReloadProposalInfo()
+ {
+ if (this.ProposalId != null)
+ {
+ this.Info = await this.Client.GetProposalInfo(this.ProposalId.Value);
+ this.MadeCall = true;
+ }
+ }
+
+ public async Task Login()
+ {
+ var dotNetReference = DotNetObjectReference.Create(this);
+ await JSRuntime.InvokeVoidAsync("window.authClientLogin", dotNetReference);
+ }
+
+ public async Task Logout()
+ {
+ await JSRuntime.InvokeVoidAsync("window.authClientLogout");
+ this.StateHasChanged();
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await this.SetIdentityAsync();
+ }
+
+ [JSInvokable("OnLoginSuccess")] // This is required in order to JS be able to execute it
+ public async Task OnLoginSuccessAsync()
+ {
+ await this.SetIdentityAsync();
+
+ }
+
+ public async Task SetIdentityAsync()
+ {
+ List? identityValues = await this.GetLocalStorageValueAsync>("ic-identity");
+ DelegationChainModel? chainModel = await this.GetLocalStorageValueAsync("ic-delegation");
+ if (identityValues != null && chainModel != null)
+ {
byte[] privateKey = FromHexString(identityValues[1]);
var identity = Ed25519Identity.FromPrivateKey(privateKey);
- DelegationChain chain = chainModel.ToCommon();
- this.Client.Agent.Identity = new DelegationIdentity(identity, chain);
- }
- this.StateHasChanged();
- }
-
- public static byte[] FromHexString(string hexString)
- {
- var bytes = new byte[hexString.Length / 2];
- for (var i = 0; i < bytes.Length; i++)
- {
- bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
- }
- return bytes;
- }
-
- public async Task GetLocalStorageValueAsync(string key)
- {
- JsonElement storageJsonElement = await JSRuntime.InvokeAsync("localStorage.getItem", key);
- string? json = storageJsonElement.GetString();
- if (json == null)
- {
- return default;
- }
- return JsonSerializer.Deserialize(json)!;
- }
-
- public class DelegationChainModel
- {
- [JsonPropertyName("delegations")]
- public List Delegations { get; set; } = new List();
- [JsonPropertyName("publicKey")]
- public string PublicKey { get; set; }
-
- public DelegationChain ToCommon()
- {
- List delegations = this.Delegations
- .Select(d => d.ToCommon())
- .ToList();
- byte[] publicKeyBytes = Convert.FromHexString(this.PublicKey);
- SubjectPublicKeyInfo publicKey = SubjectPublicKeyInfo.FromDerEncoding(publicKeyBytes);
- return new DelegationChain(publicKey, delegations);
- }
- }
-
- public class SignedDelegationModel
- {
- [JsonPropertyName("delegation")]
- public DelegationModel Delegation { get; set; }
- [JsonPropertyName("signature")]
- public string Signature { get; set; }
-
- public SignedDelegation ToCommon()
- {
- Delegation delegation = this.Delegation.ToCommon();
- byte[] signature = Convert.FromHexString(this.Signature);
- return new SignedDelegation(delegation, signature);
- }
- }
-
- public class DelegationModel
- {
- [JsonPropertyName("expiration")]
- public string Expiration { get; set; }
- [JsonPropertyName("pubkey")]
- public string PubKey { get; set; }
-
- public Delegation ToCommon()
- {
- byte[] publicKeyBytes = Convert.FromHexString(this.PubKey);
- SubjectPublicKeyInfo publicKey = SubjectPublicKeyInfo.FromDerEncoding(publicKeyBytes);
- ulong nanosecondsFromNow = (ulong)ToBigInteger(FromHexString(this.Expiration), isUnsigned: true, isBigEndian: true);
- ICTimestamp expiration = ICTimestamp.FromNanoSeconds(nanosecondsFromNow);
- return new Delegation(publicKey, expiration, targets: null);
- }
-
- public static System.Numerics.BigInteger ToBigInteger(byte[] bytes, bool isUnsigned, bool isBigEndian)
- {
- // BigInteger takes a twos compliment little endian value
- if (isUnsigned || isBigEndian)
- {
- BinarySequence bits = BinarySequence.FromBytes(bytes, isBigEndian);
- if (isUnsigned && bits.MostSignificantBit)
- {
- // Convert unsigned to signed
- bits = bits.ToTwosCompliment();
- }
- bytes = bits.ToByteArray(bigEndian: false);
- }
- return new System.Numerics.BigInteger(bytes);
- }
-
- internal class BinarySequence
- {
- // Least signifcant bit (index 0) => Most signifcant bit (index n - 1)
- private readonly bool[] bits;
-
- public bool MostSignificantBit => this.bits[this.bits.Length - 1];
-
- /// Least signifcant to most ordered bits
- public BinarySequence(bool[] bits)
- {
- this.bits = bits;
- }
-
- public BinarySequence ToTwosCompliment()
- {
- // If value most significant bit is `1`, the 2's compliment needs to be 1 bit larger to hold sign bit
- if (this.MostSignificantBit)
- {
- bool[] newBits = new bool[this.bits.Length + 1];
- this.bits.CopyTo(newBits, 0);
- return new BinarySequence(newBits);
- }
- bool[] bits = this.ToTwosComplimentInternal().ToArray();
- return new BinarySequence(bits);
- }
-
- public BinarySequence ToReverseTwosCompliment()
- {
- if (this.bits.Last())
- {
- throw new InvalidOperationException("Cannot reverse two's compliment on a negative number");
- }
- bool[] bits = this.ToTwosComplimentInternal().ToArray();
- return new BinarySequence(bits);
- }
-
- public byte[] ToByteArray(bool bigEndian = false)
- {
- IEnumerable bytes = this.bits
- .Chunk(8)
- .Select(BitsToByte);
- // Reverse if need big endian
- if (bigEndian)
- {
- bytes = bytes.Reverse();
- }
-
- return bytes.ToArray();
-
- byte BitsToByte(bool[] bits)
- {
- if (bits.Length > 8)
- {
- throw new ArgumentException("Bit length must be less than or equal to 8");
- }
- // Bits are in least significant first order
- int value = 0;
- for (int i = 0; i < bits.Length; i++)
- {
- bool b = bits[i];
- if (b)
- {
- value |= 1 << i;
- }
- }
- return (byte)value;
- }
- }
-
- public override string ToString()
- {
- var stringBuilder = new System.Text.StringBuilder();
- foreach (bool bit in this.bits.Reverse()) // Reverse to show LSB on right (normal display)
- {
- stringBuilder.Append(bit ? "1" : "0");
- }
- return stringBuilder.ToString();
- }
-
- public static BinarySequence FromBytes(byte[] bytes, bool isBigEndian)
- {
- IEnumerable byteSeq = bytes;
- if (isBigEndian)
- {
- byteSeq = byteSeq.Reverse();
- }
- bool[] bits = byteSeq
- .SelectMany(ByteToBits)
- .ToArray();
- return new BinarySequence(bits);
-
- IEnumerable ByteToBits(byte b)
- {
- // Least significant first
- yield return (b & 0b00000001) == 0b00000001;
- yield return (b & 0b00000010) == 0b00000010;
- yield return (b & 0b00000100) == 0b00000100;
- yield return (b & 0b00001000) == 0b00001000;
- yield return (b & 0b00010000) == 0b00010000;
- yield return (b & 0b00100000) == 0b00100000;
- yield return (b & 0b01000000) == 0b01000000;
- yield return (b & 0b10000000) == 0b10000000;
- }
- }
-
- private IEnumerable ToTwosComplimentInternal()
- {
- // Invert all numbers left of the right-most `1` to get 2's compliment
-
- bool flipBits = false;
- foreach (bool bit in this.bits.Reverse())
- {
- yield return flipBits ? !bit : bit;
- // If bit is `1`, all bits to left are flipped
- if (bit)
- {
- flipBits = true;
- }
- }
- }
- }
- }
-}
\ No newline at end of file
+ DelegationChain chain = chainModel.ToCommon();
+ this.Client.Identity = new DelegationIdentity(identity, chain);
+ }
+ this.StateHasChanged();
+ }
+
+ public static byte[] FromHexString(string hexString)
+ {
+ var bytes = new byte[hexString.Length / 2];
+ for (var i = 0; i < bytes.Length; i++)
+ {
+ bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
+ }
+ return bytes;
+ }
+
+ public async Task GetLocalStorageValueAsync(string key)
+ {
+ JsonElement storageJsonElement = await JSRuntime.InvokeAsync("localStorage.getItem", key);
+ string? json = storageJsonElement.GetString();
+ if (json == null)
+ {
+ return default;
+ }
+ return JsonSerializer.Deserialize(json)!;
+ }
+
+ public class DelegationChainModel
+ {
+ [JsonPropertyName("delegations")]
+ public List Delegations { get; set; } = new List();
+ [JsonPropertyName("publicKey")]
+ public string PublicKey { get; set; }
+
+ public DelegationChain ToCommon()
+ {
+ List delegations = this.Delegations
+ .Select(d => d.ToCommon())
+ .ToList();
+ byte[] publicKeyBytes = Convert.FromHexString(this.PublicKey);
+ SubjectPublicKeyInfo publicKey = SubjectPublicKeyInfo.FromDerEncoding(publicKeyBytes);
+ return new DelegationChain(publicKey, delegations);
+ }
+ }
+
+ public class SignedDelegationModel
+ {
+ [JsonPropertyName("delegation")]
+ public DelegationModel Delegation { get; set; }
+ [JsonPropertyName("signature")]
+ public string Signature { get; set; }
+
+ public SignedDelegation ToCommon()
+ {
+ Delegation delegation = this.Delegation.ToCommon();
+ byte[] signature = Convert.FromHexString(this.Signature);
+ return new SignedDelegation(delegation, signature);
+ }
+ }
+
+ public class DelegationModel
+ {
+ [JsonPropertyName("expiration")]
+ public string Expiration { get; set; }
+ [JsonPropertyName("pubkey")]
+ public string PubKey { get; set; }
+
+ public Delegation ToCommon()
+ {
+ byte[] publicKeyBytes = Convert.FromHexString(this.PubKey);
+ SubjectPublicKeyInfo publicKey = SubjectPublicKeyInfo.FromDerEncoding(publicKeyBytes);
+ ulong nanosecondsFromNow = (ulong)ToBigInteger(FromHexString(this.Expiration), isUnsigned: true, isBigEndian: true);
+ ICTimestamp expiration = ICTimestamp.FromNanoSeconds(nanosecondsFromNow);
+ return new Delegation(publicKey, expiration, targets: null);
+ }
+
+ public static System.Numerics.BigInteger ToBigInteger(byte[] bytes, bool isUnsigned, bool isBigEndian)
+ {
+ // BigInteger takes a twos compliment little endian value
+ if (isUnsigned || isBigEndian)
+ {
+ BinarySequence bits = BinarySequence.FromBytes(bytes, isBigEndian);
+ if (isUnsigned && bits.MostSignificantBit)
+ {
+ // Convert unsigned to signed
+ bits = bits.ToTwosCompliment();
+ }
+ bytes = bits.ToByteArray(bigEndian: false);
+ }
+ return new System.Numerics.BigInteger(bytes);
+ }
+
+ internal class BinarySequence
+ {
+ // Least signifcant bit (index 0) => Most signifcant bit (index n - 1)
+ private readonly bool[] bits;
+
+ public bool MostSignificantBit => this.bits[this.bits.Length - 1];
+
+ /// Least signifcant to most ordered bits
+ public BinarySequence(bool[] bits)
+ {
+ this.bits = bits;
+ }
+
+ public BinarySequence ToTwosCompliment()
+ {
+ // If value most significant bit is `1`, the 2's compliment needs to be 1 bit larger to hold sign bit
+ if (this.MostSignificantBit)
+ {
+ bool[] newBits = new bool[this.bits.Length + 1];
+ this.bits.CopyTo(newBits, 0);
+ return new BinarySequence(newBits);
+ }
+ bool[] bits = this.ToTwosComplimentInternal().ToArray();
+ return new BinarySequence(bits);
+ }
+
+ public BinarySequence ToReverseTwosCompliment()
+ {
+ if (this.bits.Last())
+ {
+ throw new InvalidOperationException("Cannot reverse two's compliment on a negative number");
+ }
+ bool[] bits = this.ToTwosComplimentInternal().ToArray();
+ return new BinarySequence(bits);
+ }
+
+ public byte[] ToByteArray(bool bigEndian = false)
+ {
+ IEnumerable bytes = this.bits
+ .Chunk(8)
+ .Select(BitsToByte);
+ // Reverse if need big endian
+ if (bigEndian)
+ {
+ bytes = bytes.Reverse();
+ }
+
+ return bytes.ToArray();
+
+ byte BitsToByte(bool[] bits)
+ {
+ if (bits.Length > 8)
+ {
+ throw new ArgumentException("Bit length must be less than or equal to 8");
+ }
+ // Bits are in least significant first order
+ int value = 0;
+ for (int i = 0; i < bits.Length; i++)
+ {
+ bool b = bits[i];
+ if (b)
+ {
+ value |= 1 << i;
+ }
+ }
+ return (byte)value;
+ }
+ }
+
+ public override string ToString()
+ {
+ var stringBuilder = new System.Text.StringBuilder();
+ foreach (bool bit in this.bits.Reverse()) // Reverse to show LSB on right (normal display)
+ {
+ stringBuilder.Append(bit ? "1" : "0");
+ }
+ return stringBuilder.ToString();
+ }
+
+ public static BinarySequence FromBytes(byte[] bytes, bool isBigEndian)
+ {
+ IEnumerable byteSeq = bytes;
+ if (isBigEndian)
+ {
+ byteSeq = byteSeq.Reverse();
+ }
+ bool[] bits = byteSeq
+ .SelectMany(ByteToBits)
+ .ToArray();
+ return new BinarySequence(bits);
+
+ IEnumerable ByteToBits(byte b)
+ {
+ // Least significant first
+ yield return (b & 0b00000001) == 0b00000001;
+ yield return (b & 0b00000010) == 0b00000010;
+ yield return (b & 0b00000100) == 0b00000100;
+ yield return (b & 0b00001000) == 0b00001000;
+ yield return (b & 0b00010000) == 0b00010000;
+ yield return (b & 0b00100000) == 0b00100000;
+ yield return (b & 0b01000000) == 0b01000000;
+ yield return (b & 0b10000000) == 0b10000000;
+ }
+ }
+
+ private IEnumerable ToTwosComplimentInternal()
+ {
+ // Invert all numbers left of the right-most `1` to get 2's compliment
+
+ bool flipBits = false;
+ foreach (bool bit in this.bits.Reverse())
+ {
+ yield return flipBits ? !bit : bit;
+ // If bit is `1`, all bits to left are flipped
+ if (bit)
+ {
+ flipBits = true;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/samples/Sample.CLI/Program.cs b/samples/Sample.CLI/Program.cs
index 0eb38642..54f990b7 100644
--- a/samples/Sample.CLI/Program.cs
+++ b/samples/Sample.CLI/Program.cs
@@ -27,7 +27,7 @@ public class Program
[Verb("upload-asset", HelpText = "Upload a file to an asset canister.")]
class UploadOptions
{
- [Option('u', "url", Required = false, HelpText = "The url to the boundy node", Default = "https://ic0.app")]
+ [Option('u', "url", Required = false, HelpText = "The url to the boundy node", Default = "https://icp-api.io")]
public string? Url { get; set; }
[Option('c', "canister-id", Required = true, HelpText = "The asset canister id to upload to")]
@@ -55,7 +55,7 @@ class UploadOptions
[Verb("download-asset", HelpText = "Download a file from an asset canister.")]
class DownloadOptions
{
- [Option('u', "url", Required = false, HelpText = "The url to the boundy node", Default = "https://ic0.app")]
+ [Option('u', "url", Required = false, HelpText = "The url to the boundy node", Default = "https://icp-api.io")]
public string? Url { get; set; }
[Option('c', "canister-id", Required = true, HelpText = "The asset canister id to download from")]
@@ -89,7 +89,7 @@ await result
identity = IdentityUtil.FromPemFile(options.IdentityPEMFilePath, options.IdentityPassword);
}
Uri httpBoundryNodeUrl = new Uri(options.Url!);
- var agent = new HttpAgent(identity, httpBoundryNodeUrl);
+ var agent = new HttpAgent(httpBoundryNodeUrl);
Samples s = new(agent);
Principal canisterId = Principal.FromText(options.CanisterId!);
@@ -111,7 +111,7 @@ await result
identity = IdentityUtil.FromPemFile(options.IdentityPEMFilePath, options.IdentityPassword);
}
Uri httpBoundryNodeUrl = new Uri(options.Url!);
- var agent = new HttpAgent(identity, httpBoundryNodeUrl);
+ var agent = new HttpAgent(httpBoundryNodeUrl);
Samples s = new(agent);
Principal canisterId = Principal.FromText(options.CanisterId!);
@@ -160,7 +160,7 @@ public async Task DownloadFileAsync(
string outputFilePath
)
{
- AssetCanisterApiClient client = new(this.agent, canisterId);
+ AssetCanisterApiClient client = new(this.agent, canisterId, identity: null);
Console.WriteLine($"Downloading asset '{key}'...");
(byte[] assetBytes, string contentEncoding) = await client.DownloadAssetAsync(key);
@@ -210,7 +210,7 @@ public async Task UploadChunkedFileAsync(
string filePath
)
{
- var client = new AssetCanisterApiClient(this.agent, canisterId);
+ var client = new AssetCanisterApiClient(this.agent, canisterId, identity: null);
Console.WriteLine($"Uploading chunks for asset '{key}'...");
Stream? contentStream = null;
diff --git a/samples/Sample.CLI/README.md b/samples/Sample.CLI/README.md
index 796178bf..5e3ebd29 100644
--- a/samples/Sample.CLI/README.md
+++ b/samples/Sample.CLI/README.md
@@ -18,7 +18,7 @@ Required:
- file_path - The path of the file to upload
Optional:
-- ic_url - Defaults to ic0.app. Use if wanting to use local (e.g http://127.0.0.1:4943)
+- ic_url - Defaults to icp-api.io. Use if wanting to use local (e.g http://127.0.0.1:4943)
- pem_file_path - Use to set identity of upload request. Defaults to anonymous identity
- pem_password - Use if the pem_file_path is set and it is encrypted
```
@@ -36,7 +36,7 @@ Required:
- output_file_path - The path of the file to download to
Optional:
-- ic_url - Defaults to ic0.app. Use if wanting to use local (e.g http://127.0.0.1:4943)
+- ic_url - Defaults to icp-api.io. Use if wanting to use local (e.g http://127.0.0.1:4943)
- pem_file_path - Use to set identity of upload request. Defaults to anonymous identity
- pem_password - Use if the pem_file_path is set and it is encrypted
```
diff --git a/samples/Sample.PocketIC/PocketIc.Tests.cs b/samples/Sample.PocketIC/PocketIc.Tests.cs
index 8e4f64cc..d371c2f8 100644
--- a/samples/Sample.PocketIC/PocketIc.Tests.cs
+++ b/samples/Sample.PocketIC/PocketIc.Tests.cs
@@ -117,8 +117,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid()
await using (HttpGateway httpGateway = await pocketIc.RunHttpGatewayAsync())
{
HttpAgent agent = httpGateway.BuildHttpAgent();
- QueryResponse getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
- CandidArg getResponseArg = getResponse.ThrowOrGetReply();
+ CandidArg getResponseArg = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
UnboundedUInt getResponseValue = getResponseArg.ToObjects();
Assert.Equal((UnboundedUInt)0, getResponseValue);
@@ -127,8 +126,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid()
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();
+ getResponseArg = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
getResponseValue = getResponseArg.ToObjects();
Assert.Equal((UnboundedUInt)1, getResponseValue);
@@ -137,8 +135,7 @@ public async Task HttpGateway_CounterWasm__Basic__Valid()
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();
+ getResponseArg = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
getResponseValue = getResponseArg.ToObjects();
Assert.Equal((UnboundedUInt)10, getResponseValue);
diff --git a/samples/Sample.PocketIC/Sample.PocketIC.csproj b/samples/Sample.PocketIC/Sample.PocketIC.csproj
index 048314ff..0cac97ab 100644
--- a/samples/Sample.PocketIC/Sample.PocketIC.csproj
+++ b/samples/Sample.PocketIC/Sample.PocketIC.csproj
@@ -10,17 +10,17 @@
-
-
-
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
-
+
diff --git a/samples/Sample.RestAPI/Controllers/ReplicaController.cs b/samples/Sample.RestAPI/Controllers/ReplicaController.cs
new file mode 100644
index 00000000..b7fcffdf
--- /dev/null
+++ b/samples/Sample.RestAPI/Controllers/ReplicaController.cs
@@ -0,0 +1,47 @@
+using EdjCase.ICP.Agent.Agents;
+using EdjCase.ICP.Agent.Responses;
+using EdjCase.ICP.Candid.Models;
+using Microsoft.AspNetCore.Mvc;
+using Sample.Shared.Governance.Models;
+
+namespace Sample.RestAPI.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class ReplicaController : ControllerBase
+ {
+ public IAgent Agent { get; }
+ public HttpClient HttpClient { get; }
+ public ReplicaController(IAgent agent)
+ {
+ this.Agent = agent;
+ this.HttpClient = new HttpClient
+ {
+ BaseAddress = new Uri("https://icp-api.io/")
+ };
+ }
+
+ [Route("status")]
+ [HttpGet]
+ public async Task GetRewards()
+ {
+ StatusResponse status = await this.Agent.GetReplicaStatusAsync();
+ return this.Ok(status);
+ }
+
+ [Route("canisterState/{canisterIdText}")]
+ [HttpGet]
+ public async Task GetA(string canisterIdText)
+ {
+ Principal canisterId = Principal.FromText(canisterIdText);
+ var candidServicePath = StatePath.FromSegments("canister", canisterId.Raw, "metadata", "candid:service");
+ var paths = new List
+ {
+ candidServicePath
+ };
+ ReadStateResponse response = await this.Agent.ReadStateAsync(canisterId, paths);
+ return this.Ok(response);
+ }
+ }
+
+}
diff --git a/samples/Sample.Shared/Address/AddressBookApiClient.cs b/samples/Sample.Shared/Address/AddressBookApiClient.cs
index 9d9c797c..3b4ab98b 100644
--- a/samples/Sample.Shared/Address/AddressBookApiClient.cs
+++ b/samples/Sample.Shared/Address/AddressBookApiClient.cs
@@ -32,25 +32,22 @@ public async Task _set_address(string name, Address addr)
public async Task> _get_address(string name)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(name, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_address", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_address", arg);
return reply.ToObjects>(this.Converter);
}
public async Task _get_self()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_self", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_self", arg);
return reply.ToObjects(this.Converter);
}
public async Task _get_dict()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_dict", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_dict", arg);
return reply.ToObjects(this.Converter);
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/Sample.Shared/Dex/DexApiClient.cs b/samples/Sample.Shared/Dex/DexApiClient.cs
index 345071cf..43ed1a1b 100644
--- a/samples/Sample.Shared/Dex/DexApiClient.cs
+++ b/samples/Sample.Shared/Dex/DexApiClient.cs
@@ -54,24 +54,21 @@ public async Task Credit(Principal arg0, Token arg1, UnboundedUInt arg2)
public async Task> GetAllBalances()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "getAllBalances", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "getAllBalances", arg);
return reply.ToObjects>(this.Converter);
}
public async Task GetBalance(Token arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "getBalance", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "getBalance", arg);
return reply.ToObjects(this.Converter);
}
public async Task> GetBalances()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "getBalances", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "getBalances", arg);
return reply.ToObjects>(this.Converter);
}
@@ -113,8 +110,7 @@ public async Task GetSymbol(Token arg0)
public async Task Whoami()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "whoami", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "whoami", arg);
return reply.ToObjects(this.Converter);
}
diff --git a/samples/Sample.Shared/Governance/GovernanceApiClient.cs b/samples/Sample.Shared/Governance/GovernanceApiClient.cs
index ebdfc223..c8bc9b8d 100644
--- a/samples/Sample.Shared/Governance/GovernanceApiClient.cs
+++ b/samples/Sample.Shared/Governance/GovernanceApiClient.cs
@@ -2,23 +2,29 @@
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid;
using System.Threading.Tasks;
-using Sample.Shared.Governance;
using System.Collections.Generic;
-using EdjCase.ICP.Agent.Responses;
+using EdjCase.ICP.Agent.Identities;
namespace Sample.Shared.Governance
{
public class GovernanceApiClient
{
+ public IIdentity? Identity { get; set; }
public IAgent Agent { get; }
public Principal CanisterId { get; }
public CandidConverter? Converter { get; }
- public GovernanceApiClient(IAgent agent, Principal canisterId, CandidConverter? converter = default)
+ public GovernanceApiClient(
+ IAgent agent,
+ Principal canisterId,
+ IIdentity? identity = null,
+ CandidConverter? converter = default
+ )
{
this.Agent = agent;
+ this.Identity = identity;
this.CanisterId = canisterId;
this.Converter = converter;
}
@@ -26,193 +32,176 @@ public GovernanceApiClient(IAgent agent, Principal canisterId, CandidConverter?
public async Task ClaimGtcNeurons(Principal arg0, List arg1)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter), CandidTypedValue.FromObject(arg1, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "claim_gtc_neurons", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "claim_gtc_neurons", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task ClaimOrRefreshNeuronFromAccount(Models.ClaimOrRefreshNeuronFromAccount arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "claim_or_refresh_neuron_from_account", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "claim_or_refresh_neuron_from_account", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetBuildMetadata()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_build_metadata", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_build_metadata", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetFullNeuron(ulong arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_full_neuron", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_full_neuron", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetFullNeuronByIdOrSubaccount(Models.NeuronIdOrSubaccount arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_full_neuron_by_id_or_subaccount", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_full_neuron_by_id_or_subaccount", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetLatestRewardEvent()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_latest_reward_event", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_latest_reward_event", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetMetrics()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_metrics", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_metrics", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetMonthlyNodeProviderRewards()
{
CandidArg arg = CandidArg.FromCandid();
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "get_monthly_node_provider_rewards", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "get_monthly_node_provider_rewards", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task> GetMostRecentMonthlyNodeProviderRewards()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_most_recent_monthly_node_provider_rewards", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_most_recent_monthly_node_provider_rewards", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
public async Task GetNetworkEconomicsParameters()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_network_economics_parameters", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_network_economics_parameters", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task> GetNeuronIds()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_ids", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_ids", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
public async Task GetNeuronInfo(ulong arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_info", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_info", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetNeuronInfoByIdOrSubaccount(Models.NeuronIdOrSubaccount arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_info_by_id_or_subaccount", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_neuron_info_by_id_or_subaccount", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task GetNodeProviderByCaller(NullValue arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_node_provider_by_caller", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_node_provider_by_caller", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task> GetPendingProposals()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_pending_proposals", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_pending_proposals", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
public async Task> GetProposalInfo(ulong arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_proposal_info", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_proposal_info", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
public async Task ListKnownNeurons()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "list_known_neurons", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "list_known_neurons", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task ListNeurons(Models.ListNeurons arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "list_neurons", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "list_neurons", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task ListNodeProviders()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "list_node_providers", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "list_node_providers", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task ListProposals(Models.ListProposalInfo arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "list_proposals", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "list_proposals", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task ManageNeuron(Models.ManageNeuron arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "manage_neuron", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "manage_neuron", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task SettleCommunityFundParticipation(Models.SettleCommunityFundParticipation arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "settle_community_fund_participation", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "settle_community_fund_participation", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task SimulateManageNeuron(Models.ManageNeuron arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "simulate_manage_neuron", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "simulate_manage_neuron", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task TransferGtcNeuron(Models.NeuronId arg0, Models.NeuronId arg1)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter), CandidTypedValue.FromObject(arg1, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "transfer_gtc_neuron", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "transfer_gtc_neuron", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
public async Task UpdateNodeProvider(Models.UpdateNodeProvider arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "update_node_provider", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "update_node_provider", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/Sample.Shared/ICRC1Ledger/ICRC1LedgerApiClient.cs b/samples/Sample.Shared/ICRC1Ledger/ICRC1LedgerApiClient.cs
index b142f85d..de8b0993 100644
--- a/samples/Sample.Shared/ICRC1Ledger/ICRC1LedgerApiClient.cs
+++ b/samples/Sample.Shared/ICRC1Ledger/ICRC1LedgerApiClient.cs
@@ -27,88 +27,77 @@ public ICRC1LedgerApiClient(IAgent agent, Principal canisterId, CandidConverter?
public async Task GetTransactions(Models.GetTransactionsRequest arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_transactions", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_transactions", arg);
return reply.ToObjects(this.Converter);
}
public async Task GetBlocks(Models.GetBlocksArgs arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_blocks", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_blocks", arg);
return reply.ToObjects(this.Converter);
}
public async Task GetDataCertificate()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_data_certificate", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_data_certificate", arg);
return reply.ToObjects(this.Converter);
}
public async Task Icrc1Name()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_name", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_name", arg);
return reply.ToObjects(this.Converter);
}
public async Task Icrc1Symbol()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_symbol", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_symbol", arg);
return reply.ToObjects(this.Converter);
}
public async Task Icrc1Decimals()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_decimals", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_decimals", arg);
return reply.ToObjects(this.Converter);
}
public async Task> Icrc1Metadata()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_metadata", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_metadata", arg);
return reply.ToObjects>(this.Converter);
}
public async Task Icrc1TotalSupply()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_total_supply", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_total_supply", arg);
return reply.ToObjects(this.Converter);
}
public async Task Icrc1Fee()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_fee", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_fee", arg);
return reply.ToObjects(this.Converter);
}
public async Task> Icrc1MintingAccount()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_minting_account", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_minting_account", arg);
return reply.ToObjects>(this.Converter);
}
public async Task Icrc1BalanceOf(Models.Account arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_balance_of", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_balance_of", arg);
return reply.ToObjects(this.Converter);
}
@@ -122,8 +111,7 @@ public async Task Icrc1BalanceOf(Models.Account arg0)
public async Task> Icrc1SupportedStandards()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_supported_standards", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_supported_standards", arg);
return reply.ToObjects>(this.Converter);
}
@@ -137,8 +125,7 @@ public async Task Icrc1BalanceOf(Models.Account arg0)
public async Task Icrc2Allowance(Models.AllowanceArgs arg0)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(arg0, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc2_allowance", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc2_allowance", arg);
return reply.ToObjects(this.Converter);
}
@@ -149,4 +136,4 @@ public async Task Icrc1BalanceOf(Models.Account arg0)
return reply.ToObjects(this.Converter);
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/Sample.WebSockets/Sample.WebSockets.csproj b/samples/Sample.WebSockets/Sample.WebSockets.csproj
index 42fa78ab..5e18990c 100644
--- a/samples/Sample.WebSockets/Sample.WebSockets.csproj
+++ b/samples/Sample.WebSockets/Sample.WebSockets.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/Agent/API.xml b/src/Agent/API.xml
index b32fd7c6..f766c2a6 100644
--- a/src/Agent/API.xml
+++ b/src/Agent/API.xml
@@ -4,6 +4,37 @@
EdjCase.ICP.Agent
+
+
+ An `IAgent` implementation using HTTP to make requests to the IC
+
+
+
+ Optional. Sets the http client to use, otherwise will use the default http client
+ If true, will skip response certificate validation. Defaults to false
+
+
+ Url to the boundry node to connect to. Defaults to `https://icp-api.io/`
+ If true, will skip response certificate validation. Defaults to false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The default http client to use with the built in `HttpClient`
@@ -31,7 +62,7 @@
Sends a GET http request and awaits the response
The url to send the GET request to
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
The http response
@@ -40,7 +71,7 @@
The url to send the POST request to
The CBOR encoded body to send
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
The http response
@@ -72,146 +103,129 @@
-
+
- An `IAgent` implementation using HTTP to make requests to the IC
+ An agent is used to communicate with the Internet Computer with certain protocols that
+ are specific to an `IAgent` implementation
-
+
- The identity that will be used on each request unless overriden
- This identity can be anonymous
+ Sends a call request to a canister with already signed content
+ The signed content containing the call request
+ Optional. Specifies the relevant canister id if calling the root canister
+ Optional. Token to cancel waiting on the response
+ The candid arg response to the call
-
- Optional. Identity to use for each request. If unspecified, will use anonymous identity
- Optional. Bls crypto implementation to validate signatures. If unspecified, will use default implementation
- Optional. Sets the http client to use, otherwise will use the default http client
-
-
- Optional. Identity to use for each request. If unspecified, will use anonymous identity
- Optional. Bls crypto implementation to validate signatures. If unspecified, will use default implementation
- Url to the boundry node to connect to. Defaults to `https://ic0.app/`
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Sends an asynchronous call request to a canister with already signed content
+
+ The signed content containing the call request
+ Optional. Specifies the relevant canister id if calling the root canister
+ Optional. Token to cancel waiting on the response
+ The id of the request that can be used to look up its status with `GetRequestStatusAsync`
-
-
+
+
+ Sends a query request to a canister with already signed content
+
+ The signed content containing the query request
+ Optional. Specifies the relevant canister id if calling the root canister
+ Optional. Token to cancel waiting on the response
+ The response data of the query call
-
-
+
+
+ Reads the state of a specified canister with already signed content
+
+ Canister to read state for
+ The signed content containing the read state request
+ Optional. Token to cancel waiting on the response
+ A response that contains the certificate of the current canister state
-
-
+
+
+ Gets the status of the IC replica. This includes versioning information
+ about the replica
+
+ A response containing all replica status information
-
+
- An agent is used to communicate with the Internet Computer with certain protocols that
- are specific to an `IAgent` implementation
+ Gets the root public key of the current Internet Computer network
+ Optional. Token to cancel waiting on the response
+ The root public key bytes
-
+
- The identity to use for requests. If null, then it will use the anonymous identity
+ Extension methods for the `IAgent` interface
-
+
Gets the state of a specified canister with the subset of state information
specified by the paths parameter
+ The agent to use for the call
Canister to read state for
The state paths to get information for. Other state data will be pruned if not specified
- Optional. Token to cancel request
+ Optional. Identity to sign the request with
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
A response that contains the certificate of the current canister state
-
-
- Gets the status of a request that is being processed by the specified canister
-
- Canister where the request was sent to
- Id of the request to get a status for
- Optional. Token to cancel request
- A status variant of the request. If request is not found, will return null
-
-
+
Sends a call request to a specified canister method and gets the response candid arg back using /v3/../call
and falls back to /v2/../call if the v3 is not available
+ The agent to use for the call
Canister to read state for
The name of the method to call on the canister
The candid arg to send with the request
+ Optional. Identity to sign the request with
+ Optional. If specified will make the request unique even with the same arguments
Optional. Specifies the relevant canister id if calling the root canister
- 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.
- Either check the status with `GetRequestStatusAsync` or use the `CallV2AndWaitAsync` method
-
- Canister to read state for
- The name of the method to call on the canister
- The candid arg to send with the request
- Optional. Specifies the relevant canister id if calling the root canister
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
The id of the request that can be used to look up its status with `GetRequestStatusAsync`
-
-
- Gets the status of the IC replica. This includes versioning information
- about the replica
-
- A response containing all replica status information
-
-
+
Sends a query request to a specified canister method
+ The agent to use for the call
Canister to read state for
The name of the method to call on the canister
The candid arg to send with the request
- Optional. Token to cancel request
+ Optional. If specified will make the request unique even with the same arguments
+ Optional. Identity to sign the request with
+ Optional. Specifies the relevant canister id if calling the root canister
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
The response data of the query call
-
-
- Gets the root public key of the current Internet Computer network
-
- Optional. Token to cancel request
- The root public key bytes
-
-
+
- Extension methods for the `IAgent` interface
-
-
-
-
- Wrapper to call `CallAsync` (v3/.../call) to avoid breaking auto generated clients
- If v2/.../call is wanted, use `CallV2AndWaitAsync`
+ 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.
+ Either check the status with `GetRequestStatusAsync` or use the `CallV2AndWaitAsync` method
The agent to use for the call
Canister to read state for
The name of the method to call on the canister
The candid arg to send with the request
+ Optional. Identity to sign the request with
+ Optional. If specified will make the request unique even with the same arguments
Optional. Specifies the relevant canister id if calling the root canister
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
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 `CallAsynchronousAsync`
@@ -221,10 +235,37 @@
Canister to read state for
The name of the method to call on the canister
The candid arg to send with the request
+ Optional. Identity to sign the request with
+ Optional. If specified will make the request unique even with the same arguments
+ Optional. Specifies the relevant canister id if calling the root canister
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
+ The raw candid arg response
+
+
+
+ Sends a call request with already signed content to a specified canister method, waits for the request to be processed,
+ then returns the candid response to the call. This is a 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
+ The signed content containing the call request
Optional. Specifies the relevant canister id if calling the root canister
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
The raw candid arg response
+
+
+ Gets the status of a request that is being processed by the specified canister
+
+ The agent to use for the call
+ Canister where the request was sent to
+ Id of the request to get a status for
+ Optional. Identity to sign the request with
+ Optional. Token to cancel waiting on the response
+ Optional. The expiration of the request if not processed in time
+ A status variant of the request. If request is not found, will return null
+
Waits for a request to be processed and returns the candid response to the call. This is a helper
@@ -234,7 +275,7 @@
The agent to use for the call
Canister to read state for
The unique identifier for the request
- Optional. Token to cancel request
+ Optional. Token to cancel waiting on the response
The raw candid arg response
@@ -562,17 +603,11 @@
The byte data to sign
The signature bytes of the specified data bytes
-
-
- Extension methods for the IIdentity interface
-
-
-
+
Signs the hashable content
- The identity to sign the content with
- The data that needs to be signed
+ The data that needs to be signed
The content with signature(s) from the identity
@@ -628,12 +663,11 @@
Optional. A signed delegation that links a public key to the root public key
Throws if either `tree` or `signature` are null
-
+
- Checks the validity of the certificate based off the
+ Checks the validity of the certificate based off the
specified root public key
- BLS crytography implementation to verify signature
The root public key (DER encoded) of the internet computer network
True if the certificate is valid, otherwise false
@@ -742,42 +776,6 @@
Optional. List of pricipals where requests can originate from
A delegation chain signed by the delegating identity
-
-
- A model containing content and the signature information of it
-
-
-
-
- The content that is signed in the form of key value pairs
-
-
-
-
- Public key used to authenticate this request, unless anonymous, then null
-
-
-
-
- Optional. A chain of delegations, starting with the one signed by sender_pubkey
- and ending with the one delegating to the key relating to sender_sig.
-
-
-
-
- Signature to authenticate this request, unless anonymous, then null
-
-
-
- The content that is signed in the form of key value pairs
- Public key used to authenticate this request, unless anonymous, then null
- Optional. A chain of delegations, starting with the one signed by sender_pubkey
- and ending with the one delegating to the key relating to sender_sig.
- Signature to authenticate this request, unless anonymous, then null
-
-
-
-
A delegation that has been signed by an identity
@@ -822,6 +820,56 @@
+
+
+ A model containing content and the signature information of it
+
+
+
+
+ The request that is signed
+
+
+
+
+ Public key used to authenticate this request, unless anonymous, then null
+
+
+
+
+ Optional. A chain of delegations, starting with the one signed by sender_pubkey
+ and ending with the one delegating to the key relating to sender_sig.
+
+
+
+
+ Signature to authenticate this request, unless anonymous, then null
+
+
+
+ The content that is signed in the form of key value pairs
+ Public key used to authenticate this request, unless anonymous, then null
+ Optional. A chain of delegations, starting with the one signed by sender_pubkey
+ and ending with the one delegating to the key relating to sender_sig.
+ Optional. Precomputed request id to use, otherwise will build it
+ Signature to authenticate this request, unless anonymous, then null
+
+
+
+ Get the unique id for the request, which is hash of the content
+ Builds the id on the first call and caches it for future calls
+
+ The id for the request
+
+
+
+
+
+
+ Serializes this signed content object to CBOR (Concise Binary Object Representation) format.
+
+ A byte array containing the CBOR-encoded representation of this object.
+
A model for making call requests to a canister
@@ -854,7 +902,7 @@
- Optional. Arbitrary user-provided data, typically randomly generated. This can be used to create distinct requests with otherwise identical fields.
+ Optional. If specified will make the request unique even with the same arguments.
@@ -863,7 +911,7 @@
Argument to pass to the canister method
The user who issued the request
An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01
- Optional. Arbitrary user-provided data, typically randomly generated. This can be used to create distinct requests with otherwise identical fields.
+ Optional. If specified will make the request unique even with the same arguments
@@ -905,16 +953,16 @@
- Optional. Arbitrary user-provided data, typically randomly generated.
- This can be used to create distinct requests with otherwise identical fields.
+ Optional. If specified will make the request unique even with the same arguments
-
+
The principal of the canister to call
Name of the canister method to call
Arguments to pass to the canister method
The user who issued the request
The expiration of the request to avoid replay attacks
+ Optional. If specified will make the request unique even with the same arguments
@@ -1230,7 +1278,7 @@
- The status of the call ('replied', 'rejected', 'done)
+ The status of the call ('replied', 'rejected', etc..)
@@ -1238,8 +1286,8 @@
The certificate data of the current canister state
-
- The status of the call ('replied', 'rejected', 'done)
+
+ The status of the call ('replied', 'rejected', etc..)
The certificate data of the current canister state
@@ -1259,6 +1307,11 @@
It is set to just under 2MB.
+
+
+ The identity to use for requests.
+
+
The IC agent
@@ -1274,12 +1327,13 @@
The optional custom candid converter for request and response objects
-
+
Initializes a new instance of the class.
The agent used for communication.
The ID of the asset canister.
+ The identity to use for requests (optional).
The Candid converter to use for encoding and decoding values (optional).
@@ -2910,6 +2964,11 @@
A pre-generated client for the ICRC1 standard
+
+
+ Identity to use for requests
+
+
Agent to use to make requests to the IC
@@ -2920,12 +2979,13 @@
The id of the canister to make requests to
-
+
Primary constructor
Agent to use to make requests to the IC
The id of the canister to make requests to
+ Optional. The identity to use for requests
diff --git a/src/Agent/Agents/Http/IHttpClient.cs b/src/Agent/Agents/Http/IHttpClient.cs
index 95837a81..dd2c54f4 100644
--- a/src/Agent/Agents/Http/IHttpClient.cs
+++ b/src/Agent/Agents/Http/IHttpClient.cs
@@ -17,7 +17,7 @@ public interface IHttpClient
/// Sends a GET http request and awaits the response
///
/// The url to send the GET request to
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
/// The http response
Task GetAsync(string url, CancellationToken? cancellationToken = null);
@@ -26,7 +26,7 @@ public interface IHttpClient
///
/// The url to send the POST request to
/// The CBOR encoded body to send
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
/// The http response
Task PostAsync(string url, byte[] cborBody, CancellationToken? cancellationToken = null);
}
@@ -69,7 +69,7 @@ public async Task GetContentAsync()
///
public async ValueTask ThrowIfErrorAsync()
{
- bool non2XXStatusCode = this.StatusCode >= HttpStatusCode.Ambiguous && this.StatusCode < HttpStatusCode.OK;
+ bool non2XXStatusCode = this.StatusCode >= HttpStatusCode.Ambiguous || this.StatusCode < HttpStatusCode.OK;
if (non2XXStatusCode)
{
byte[] bytes = await this.GetContentAsync();
diff --git a/src/Agent/Agents/HttpAgent.cs b/src/Agent/Agents/HttpAgent.cs
index cf79ec54..a2561491 100644
--- a/src/Agent/Agents/HttpAgent.cs
+++ b/src/Agent/Agents/HttpAgent.cs
@@ -1,25 +1,14 @@
using EdjCase.ICP.Agent.Models;
-using EdjCase.ICP.Agent.Identities;
using EdjCase.ICP.Agent.Requests;
using EdjCase.ICP.Agent.Responses;
-using EdjCase.ICP.Candid.Crypto;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid.Utilities;
using System;
-using System.Collections.Generic;
-using System.IO;
using System.Net.Http;
-using System.Text;
using System.Threading.Tasks;
using EdjCase.ICP.Agent.Agents.Http;
-using System.Net.Http.Headers;
using System.Formats.Cbor;
-using EdjCase.ICP.Candid.Encodings;
-using System.Linq;
-using EdjCase.ICP.BLS;
using System.Threading;
-using System.Security.Cryptography;
-using System.Diagnostics;
namespace EdjCase.ICP.Agent.Agents
{
@@ -30,73 +19,57 @@ public class HttpAgent : IAgent
{
private byte[]? rootKeyCache = null;
- ///
- /// The identity that will be used on each request unless overriden
- /// This identity can be anonymous
- ///
- public IIdentity? Identity { get; set; }
-
private readonly IHttpClient httpClient;
- private readonly IBlsCryptography bls;
+ private bool skipCertificateValidation = false;
private bool v3CallSupported = true;
- /// Optional. Identity to use for each request. If unspecified, will use anonymous identity
- /// Optional. Bls crypto implementation to validate signatures. If unspecified, will use default implementation
/// Optional. Sets the http client to use, otherwise will use the default http client
- public HttpAgent(
- IHttpClient httpClient,
- IIdentity? identity = null,
- IBlsCryptography? bls = null
- )
+ /// If true, will skip response certificate validation. Defaults to false
+ public HttpAgent(IHttpClient httpClient, bool skipCertificateValidation = false)
{
- this.Identity = identity;
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
- this.bls = bls ?? new DefaultBlsCryptograhy();
+ this.skipCertificateValidation = skipCertificateValidation;
}
- /// Optional. Identity to use for each request. If unspecified, will use anonymous identity
- /// Optional. Bls crypto implementation to validate signatures. If unspecified, will use default implementation
- /// Url to the boundry node to connect to. Defaults to `https://ic0.app/`
- public HttpAgent(
- IIdentity? identity = null,
- Uri? httpBoundryNodeUrl = null,
- IBlsCryptography? bls = null
- )
+ /// Url to the boundry node to connect to. Defaults to `https://icp-api.io/`
+ /// If true, will skip response certificate validation. Defaults to false
+ public HttpAgent(Uri? httpBoundryNodeUrl = null, bool skipCertificateValidation = false)
{
- this.Identity = identity;
- this.httpClient = new DefaultHttpClient(new HttpClient()
+ this.httpClient = new DefaultHttpClient(new HttpClient
{
- BaseAddress = httpBoundryNodeUrl ?? new Uri("https://ic0.app/")
+ BaseAddress = httpBoundryNodeUrl ?? new Uri("https://icp-api.io/")
});
- this.bls = bls ?? new DefaultBlsCryptograhy();
+ this.skipCertificateValidation = skipCertificateValidation;
}
+
+
+
+
///
public async Task CallAsync(
- Principal canisterId,
- string method,
- CandidArg arg,
+ SignedRequest request,
Principal? effectiveCanisterId = null,
CancellationToken? cancellationToken = null
)
{
- if (!this.v3CallSupported)
- {
- return await this.CallAsynchronousAndWaitAsync(canisterId, method, arg, effectiveCanisterId, cancellationToken);
- }
- effectiveCanisterId ??= canisterId;
- (HttpResponse httpResponse, RequestId requestId) = await this.SendAsync($"/api/v3/canister/{effectiveCanisterId.ToText()}/call", BuildRequest, cancellationToken);
+ effectiveCanisterId ??= request.Content.CanisterId;
+ string url = this.GetCallUrl(effectiveCanisterId, this.v3CallSupported);
+
+ HttpResponse httpResponse = await this.SendAsync(url, request, cancellationToken);
+
if (httpResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// If v3 is not available, fall back to v2
this.v3CallSupported = false;
- return await this.CallAsynchronousAndWaitAsync(canisterId, method, arg, effectiveCanisterId, cancellationToken);
+ return await this.CallAsynchronousAndWaitAsync(request, effectiveCanisterId, cancellationToken);
}
+ RequestId requestId = request.GetOrBuildRequestId();
if (httpResponse.StatusCode == System.Net.HttpStatusCode.Accepted)
{
// If request takes too long, then it will return 202 Accepted and polling is required
- return await this.WaitForRequestAsync(canisterId, requestId, cancellationToken);
+ return await this.WaitForRequestAsync(request.Content.CanisterId, requestId, cancellationToken);
}
await httpResponse.ThrowIfErrorAsync();
@@ -104,13 +77,16 @@ public async Task CallAsync(
var reader = new CborReader(cborBytes);
V3CallResponse v3CallResponse = V3CallResponse.ReadCbor(reader);
- SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken);
- if (!v3CallResponse.Certificate.IsValid(this.bls, rootPublicKey))
+ if (!this.skipCertificateValidation)
{
- throw new InvalidCertificateException("Certificate signature does not match the IC public key");
+ SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken);
+ if (!v3CallResponse.Certificate.IsValid(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));
- RequestStatus? requestStatus = ParseRequestStatus(requestStatusData);
+ RequestStatus? requestStatus = IAgentExtensions.ParseRequestStatus(requestStatusData);
switch (requestStatus?.Type)
{
case RequestStatus.StatusType.Replied:
@@ -127,30 +103,19 @@ public async Task CallAsync(
default:
throw new NotImplementedException($"Invalid request status '{requestStatus.Type}'");
}
-
- CallRequest BuildRequest(Principal sender, ICTimestamp now)
- {
- byte[] nonce = new byte[16];
- RandomNumberGenerator.Fill(nonce);
- return new CallRequest(canisterId, method, arg, sender, now, nonce);
- }
}
-
///
public async Task CallAsynchronousAsync(
- Principal canisterId,
- string method,
- CandidArg arg,
+ SignedRequest request,
Principal? effectiveCanisterId = null,
CancellationToken? cancellationToken = null
)
{
- if (effectiveCanisterId == null)
- {
- effectiveCanisterId = canisterId;
- }
- (HttpResponse httpResponse, RequestId requestId) = await this.SendAsync($"/api/v2/canister/{effectiveCanisterId.ToText()}/call", BuildRequest, cancellationToken);
+ effectiveCanisterId ??= request.Content.CanisterId;
+ string url = this.GetCallUrl(effectiveCanisterId, false);
+
+ HttpResponse httpResponse = await this.SendAsync(url, request, cancellationToken);
await httpResponse.ThrowIfErrorAsync();
if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK)
@@ -172,106 +137,86 @@ public async Task CallAsynchronousAsync(
}
throw new CallRejectedException(response.Code, response.Message, response.ErrorCode);
}
- return requestId;
-
- CallRequest BuildRequest(Principal sender, ICTimestamp now)
- {
- byte[] nonce = new byte[16];
- RandomNumberGenerator.Fill(nonce);
- return new CallRequest(canisterId, method, arg, sender, now, nonce);
- }
+ return request.GetOrBuildRequestId();
}
///
- public async Task QueryAsync(
- Principal canisterId,
- string method,
- CandidArg arg,
+ public async Task QueryAsync(
+ SignedRequest request,
+ Principal? effectiveCanisterId = null,
CancellationToken? cancellationToken = null
)
{
- (HttpResponse httpResponse, RequestId requestId) = await this.SendAsync($"/api/v2/canister/{canisterId.ToText()}/query", BuildRequest, cancellationToken);
+ effectiveCanisterId ??= request.Content.CanisterId;
+ HttpResponse httpResponse = await this.SendAsync($"/api/v2/canister/{effectiveCanisterId.ToText()}/query", request, cancellationToken);
await httpResponse.ThrowIfErrorAsync();
byte[] cborBytes = await httpResponse.GetContentAsync();
- return QueryResponse.ReadCbor(new CborReader(cborBytes));
-
- QueryRequest BuildRequest(Principal sender, ICTimestamp now)
- {
- return new QueryRequest(canisterId, method, arg, sender, now);
- }
+#if DEBUG
+ string hex = ByteUtil.ToHexString(cborBytes);
+#endif
+ return QueryResponse.ReadCbor(new CborReader(cborBytes)).ThrowOrGetReply();
}
///
public async Task ReadStateAsync(
Principal canisterId,
- List paths,
+ SignedRequest content,
CancellationToken? cancellationToken = null
)
{
string url = $"/api/v2/canister/{canisterId.ToText()}/read_state";
- (HttpResponse httpResponse, RequestId requestId) = await this.SendAsync(url, BuildRequest, cancellationToken);
+ HttpResponse httpResponse = await this.SendAsync(url, content, cancellationToken);
await httpResponse.ThrowIfErrorAsync();
byte[] cborBytes = await httpResponse.GetContentAsync();
+#if DEBUG
+ string cborHex = ByteUtil.ToHexString(cborBytes);
+#endif
var reader = new CborReader(cborBytes);
ReadStateResponse response = ReadStateResponse.ReadCbor(reader);
- SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken);
- if (!response.Certificate.IsValid(this.bls, rootPublicKey))
+ if (!this.skipCertificateValidation)
{
- throw new InvalidCertificateException("Certificate signature does not match the IC public key");
+ SubjectPublicKeyInfo rootPublicKey = await this.GetRootKeyAsync(cancellationToken);
+ if (!response.Certificate.IsValid(rootPublicKey))
+ {
+ throw new InvalidCertificateException("Certificate signature does not match the IC public key");
+ }
}
return response;
-
- ReadStateRequest BuildRequest(Principal sender, ICTimestamp now)
- {
- return new ReadStateRequest(paths, sender, now);
- }
}
- ///
- public async Task GetRequestStatusAsync(
- Principal canisterId,
- RequestId id,
- CancellationToken? cancellationToken = null
- )
- {
- var pathRequestStatus = StatePath.FromSegments("request_status", id.RawValue);
- var paths = new List { pathRequestStatus };
- ReadStateResponse response = await this.ReadStateAsync(canisterId, paths, cancellationToken);
- HashTree? requestStatus = response.Certificate.Tree.GetValueOrDefault(pathRequestStatus);
- return ParseRequestStatus(requestStatus);
- }
- private static RequestStatus? ParseRequestStatus(HashTree? requestStatus)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private string GetCallUrl(Principal canisterId, bool v3)
{
- string? status = requestStatus?.GetValueOrDefault("status")?.AsLeaf().AsUtf8();
- //received, processing, replied, rejected or done
- switch (status)
+ if (v3)
{
- case null:
- return null;
- case "received":
- return RequestStatus.Received();
- case "processing":
- return RequestStatus.Processing();
- case "replied":
- HashTree.EncodedValue r = requestStatus!.GetValueOrDefault("reply")!.AsLeaf();
- return RequestStatus.Replied(CandidArg.FromBytes(r));
- case "rejected":
- RejectCode code = (RejectCode)(ulong)requestStatus!.GetValueOrDefault("reject_code")!.AsLeaf().AsNat();
- string message = requestStatus.GetValueOrDefault("reject_message")!.AsLeaf().AsUtf8();
- string? errorCode = requestStatus.GetValueOrDefault("error_code")?.AsLeaf().AsUtf8();
- return RequestStatus.Rejected(code, message, errorCode);
- case "done":
- return RequestStatus.Done();
- default:
- throw new NotImplementedException($"Invalid request status '{status}'");
+ return $"/api/v3/canister/{canisterId.ToText()}/call";
}
+ return $"/api/v2/canister/{canisterId.ToText()}/call";
}
+
+
+
///
public async Task GetRootKeyAsync(
CancellationToken? cancellationToken = null
@@ -302,56 +247,22 @@ public async Task GetReplicaStatusAsync(
return StatusResponse.ReadCbor(new CborReader(bytes));
}
- private async Task<(HttpResponse Response, RequestId RequestId)> SendAsync(
+
+ private async Task SendAsync(
string url,
- Func getRequest,
+ SignedRequest request,
CancellationToken? cancellationToken = null
)
where TRequest : IRepresentationIndependentHashItem
{
- Principal principal;
- if (this.Identity == null)
- {
- principal = Principal.Anonymous();
- }
- else
- {
- SubjectPublicKeyInfo publicKey = this.Identity.GetPublicKey();
- principal = publicKey.ToPrincipal();
- }
- TRequest request = getRequest(principal, ICTimestamp.Future(TimeSpan.FromMinutes(3)));
- Dictionary content = request.BuildHashableItem();
-
- SignedContent signedContent;
- if (this.Identity == null)
- {
- signedContent = new SignedContent(content, null, null, null);
- }
- else
- {
- signedContent = this.Identity.SignContent(content);
- }
-
-
- byte[] cborBody = this.SerializeSignedContent(signedContent);
+ byte[] cborBody = request.ToCborBytes();
#if DEBUG
string hex = ByteUtil.ToHexString(cborBody);
#endif
- HttpResponse httpResponse = await this.httpClient.PostAsync(url, cborBody, cancellationToken);
- var sha256 = SHA256HashFunction.Create();
- RequestId requestId = RequestId.FromObject(content, sha256); // TODO this is redundant, `CreateSignedContent` hashes it too
- return (httpResponse, requestId);
+ return await this.httpClient.PostAsync(url, cborBody, cancellationToken);
- }
- private byte[] SerializeSignedContent(SignedContent signedContent)
- {
- var writer = new CborWriter();
- writer.WriteTag(CborTag.SelfDescribeCbor);
- signedContent.WriteCbor(writer);
- return writer.Encode();
}
-
}
}
diff --git a/src/Agent/Agents/IAgent.cs b/src/Agent/Agents/IAgent.cs
index cc4aa4ec..a6c41960 100644
--- a/src/Agent/Agents/IAgent.cs
+++ b/src/Agent/Agents/IAgent.cs
@@ -1,9 +1,14 @@
using EdjCase.ICP.Agent.Identities;
+using EdjCase.ICP.Agent.Models;
+using EdjCase.ICP.Agent.Requests;
using EdjCase.ICP.Agent.Responses;
+using EdjCase.ICP.Candid.Crypto;
using EdjCase.ICP.Candid.Models;
using System;
using System.Collections.Generic;
+using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
@@ -16,52 +21,56 @@ namespace EdjCase.ICP.Agent.Agents
public interface IAgent
{
///
- /// The identity to use for requests. If null, then it will use the anonymous identity
+ /// Sends a call request to a canister with already signed content
///
- public IIdentity? Identity { get; set; }
- ///
- /// Gets the state of a specified canister with the subset of state information
- /// specified by the paths parameter
- ///
- /// Canister to read state for
- /// The state paths to get information for. Other state data will be pruned if not specified
- /// Optional. Token to cancel request
- /// A response that contains the certificate of the current canister state
- Task ReadStateAsync(Principal canisterId, List paths, CancellationToken? cancellationToken = null);
+ /// The signed content containing the call request
+ /// Optional. Specifies the relevant canister id if calling the root canister
+ /// Optional. Token to cancel waiting on the response
+ /// The candid arg response to the call
+ Task CallAsync(
+ SignedRequest content,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null
+ );
///
- /// Gets the status of a request that is being processed by the specified canister
+ /// Sends an asynchronous call request to a canister with already signed content
///
- /// Canister where the request was sent to
- /// Id of the request to get a status for
- /// Optional. Token to cancel request
- /// A status variant of the request. If request is not found, will return null
- Task GetRequestStatusAsync(Principal canisterId, RequestId id, CancellationToken? cancellationToken = null);
+ /// The signed content containing the call request
+ /// Optional. Specifies the relevant canister id if calling the root canister
+ /// Optional. Token to cancel waiting on the response
+ /// The id of the request that can be used to look up its status with `GetRequestStatusAsync`
+ Task CallAsynchronousAsync(
+ SignedRequest content,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null
+ );
///
- /// Sends a call request to a specified canister method and gets the response candid arg back using /v3/../call
- /// and falls back to /v2/../call if the v3 is not available
+ /// Sends a query request to a canister with already signed content
///
- /// Canister to read state for
- /// The name of the method to call on the canister
- /// The candid arg to send with the request
+ /// The signed content containing the query request
/// Optional. Specifies the relevant canister id if calling the root canister
- /// Optional. Token to cancel request
- /// The id of the request that can be used to look up its status with `GetRequestStatusAsync`
- Task CallAsync(Principal canisterId, string method, CandidArg arg, Principal? effectiveCanisterId = null, CancellationToken? cancellationToken = null);
+ /// Optional. Token to cancel waiting on the response
+ /// The response data of the query call
+ Task QueryAsync(
+ SignedRequest content,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null
+ );
///
- /// 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.
- /// Either check the status with `GetRequestStatusAsync` or use the `CallV2AndWaitAsync` method
+ /// Reads the state of a specified canister with already signed content
///
/// Canister to read state for
- /// The name of the method to call on the canister
- /// The candid arg to send with the request
- /// Optional. Specifies the relevant canister id if calling the root canister
- /// Optional. Token to cancel request
- /// The id of the request that can be used to look up its status with `GetRequestStatusAsync`
- Task CallAsynchronousAsync(Principal canisterId, string method, CandidArg arg, Principal? effectiveCanisterId = null, CancellationToken? cancellationToken = null);
+ /// The signed content containing the read state request
+ /// Optional. Token to cancel waiting on the response
+ /// A response that contains the certificate of the current canister state
+ Task ReadStateAsync(
+ Principal canisterId,
+ SignedRequest content,
+ CancellationToken? cancellationToken = null
+ );
///
/// Gets the status of the IC replica. This includes versioning information
@@ -70,20 +79,10 @@ public interface IAgent
/// A response containing all replica status information
Task GetReplicaStatusAsync(CancellationToken? cancellationToken = null);
- ///
- /// Sends a query request to a specified canister method
- ///
- /// Canister to read state for
- /// The name of the method to call on the canister
- /// The candid arg to send with the request
- /// Optional. Token to cancel request
- /// The response data of the query call
- Task QueryAsync(Principal canisterId, string method, CandidArg arg, CancellationToken? cancellationToken = null);
-
///
/// Gets the root public key of the current Internet Computer network
///
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
/// The root public key bytes
Task GetRootKeyAsync(CancellationToken? cancellationToken = null);
}
@@ -94,27 +93,145 @@ public interface IAgent
public static class IAgentExtensions
{
///
- /// Wrapper to call `CallAsync` (v3/.../call) to avoid breaking auto generated clients
- /// If v2/.../call is wanted, use `CallV2AndWaitAsync`
+ /// Gets the state of a specified canister with the subset of state information
+ /// specified by the paths parameter
+ ///
+ /// The agent to use for the call
+ /// Canister to read state for
+ /// The state paths to get information for. Other state data will be pruned if not specified
+ /// Optional. Identity to sign the request with
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
+ /// A response that contains the certificate of the current canister state
+ public static async Task ReadStateAsync(
+ this IAgent agent,
+ Principal canisterId,
+ List paths,
+ IIdentity? identity = null,
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
+ {
+ SignedRequest content = SignRequest(
+ identity,
+ (sender, ingressExpiry) => new ReadStateRequest(paths, sender, ingressExpiry),
+ ingressExpiry
+ );
+ return await agent.ReadStateAsync(canisterId, content, cancellationToken);
+ }
+
+
+ ///
+ /// Sends a call request to a specified canister method and gets the response candid arg back using /v3/../call
+ /// and falls back to /v2/../call if the v3 is not available
+ ///
+ /// The agent to use for the call
+ /// Canister to read state for
+ /// The name of the method to call on the canister
+ /// The candid arg to send with the request
+ /// Optional. Identity to sign the request with
+ /// Optional. If specified will make the request unique even with the same arguments
+ /// Optional. Specifies the relevant canister id if calling the root canister
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
+ /// The id of the request that can be used to look up its status with `GetRequestStatusAsync`
+ public static async Task CallAsync(
+ this IAgent agent,
+ Principal canisterId,
+ string method,
+ CandidArg arg,
+ IIdentity? identity = null,
+ byte[]? nonce = null,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
+ {
+ SignedRequest content = SignRequest(
+ identity,
+ (sender, ingressExpiry) =>
+ {
+ return new CallRequest(canisterId, method, arg, sender, ingressExpiry, nonce);
+ },
+ ingressExpiry
+ );
+ return await agent.CallAsync(content, effectiveCanisterId, cancellationToken);
+ }
+
+
+ ///
+ /// Sends a query request to a specified canister method
///
/// The agent to use for the call
/// Canister to read state for
/// The name of the method to call on the canister
/// The candid arg to send with the request
+ /// Optional. If specified will make the request unique even with the same arguments
+ /// Optional. Identity to sign the request with
/// Optional. Specifies the relevant canister id if calling the root canister
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
+ /// The response data of the query call
+ public static async Task QueryAsync(
+ this IAgent agent,
+ Principal canisterId,
+ string method,
+ CandidArg arg,
+ byte[]? nonce = null,
+ IIdentity? identity = null,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
+ {
+ SignedRequest content = SignRequest(
+ identity,
+ (sender, ingressExpiry) => new QueryRequest(canisterId, method, arg, sender, ingressExpiry, nonce),
+ ingressExpiry
+ );
+ return await agent.QueryAsync(content, effectiveCanisterId, cancellationToken);
+ }
+
+
+ ///
+ /// 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.
+ /// Either check the status with `GetRequestStatusAsync` or use the `CallV2AndWaitAsync` method
+ ///
+ /// The agent to use for the call
+ /// Canister to read state for
+ /// The name of the method to call on the canister
+ /// The candid arg to send with the request
+ /// Optional. Identity to sign the request with
+ /// Optional. If specified will make the request unique even with the same arguments
+ /// Optional. Specifies the relevant canister id if calling the root canister
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
/// The id of the request that can be used to look up its status with `GetRequestStatusAsync`
- [Obsolete("Use CallAsync or CallAsynchronousAndWaitAsync instead")]
- public static async Task CallAndWaitAsync(
+ public static async Task CallAsynchronousAsync(
this IAgent agent,
Principal canisterId,
string method,
CandidArg arg,
+ IIdentity? identity = null,
+ byte[]? nonce = null,
Principal? effectiveCanisterId = null,
- CancellationToken? cancellationToken = null)
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
{
- return await agent.CallAsync(canisterId, method, arg, effectiveCanisterId, cancellationToken);
+ SignedRequest content = SignRequest(
+ identity,
+ (sender, ingressExpiry) =>
+ {
+ return new CallRequest(canisterId, method, arg, sender, ingressExpiry, nonce);
+ },
+ ingressExpiry
+ );
+ return await agent.CallAsynchronousAsync(content, effectiveCanisterId, cancellationToken);
}
+
+
///
/// 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 `CallAsynchronousAsync`
@@ -124,21 +241,90 @@ public static async Task CallAndWaitAsync(
/// Canister to read state for
/// The name of the method to call on the canister
/// The candid arg to send with the request
+ /// Optional. Identity to sign the request with
+ /// Optional. If specified will make the request unique even with the same arguments
/// Optional. Specifies the relevant canister id if calling the root canister
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
/// The raw candid arg response
public static async Task CallAsynchronousAndWaitAsync(
this IAgent agent,
Principal canisterId,
string method,
CandidArg arg,
+ IIdentity? identity = null,
+ byte[]? nonce = null,
Principal? effectiveCanisterId = null,
- CancellationToken? cancellationToken = null)
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
{
- RequestId id = await agent.CallAsynchronousAsync(canisterId, method, arg, effectiveCanisterId);
+ RequestId id = await agent.CallAsynchronousAsync(
+ canisterId,
+ method,
+ arg,
+ identity,
+ nonce,
+ effectiveCanisterId,
+ cancellationToken,
+ ingressExpiry
+ );
return await agent.WaitForRequestAsync(canisterId, id, cancellationToken);
}
+ ///
+ /// Sends a call request with already signed content to a specified canister method, waits for the request to be processed,
+ /// then returns the candid response to the call. This is a 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
+ /// The signed content containing the call request
+ /// Optional. Specifies the relevant canister id if calling the root canister
+ /// Optional. Token to cancel waiting on the response
+ /// The raw candid arg response
+ public static async Task CallAsynchronousAndWaitAsync(
+ this IAgent agent,
+ SignedRequest content,
+ Principal? effectiveCanisterId = null,
+ CancellationToken? cancellationToken = null
+ )
+ {
+ RequestId id = await agent.CallAsynchronousAsync(content, effectiveCanisterId, cancellationToken);
+ return await agent.WaitForRequestAsync(content.Content.CanisterId, id, cancellationToken);
+ }
+
+
+ ///
+ /// Gets the status of a request that is being processed by the specified canister
+ ///
+ /// The agent to use for the call
+ /// Canister where the request was sent to
+ /// Id of the request to get a status for
+ /// Optional. Identity to sign the request with
+ /// Optional. Token to cancel waiting on the response
+ /// Optional. The expiration of the request if not processed in time
+ /// A status variant of the request. If request is not found, will return null
+ public static async Task GetRequestStatusAsync(
+ this IAgent agent,
+ Principal canisterId,
+ RequestId id,
+ IIdentity? identity = null,
+ CancellationToken? cancellationToken = null,
+ ICTimestamp? ingressExpiry = null
+ )
+ {
+ var pathRequestStatus = StatePath.FromSegments("request_status", id.RawValue);
+ var paths = new List { pathRequestStatus };
+ SignedRequest signedContent = SignRequest(
+ identity,
+ (sender, ingressExpiry) => new ReadStateRequest(paths, sender, ingressExpiry),
+ ingressExpiry
+ );
+ ReadStateResponse response = await agent.ReadStateAsync(canisterId, signedContent, cancellationToken);
+ HashTree? requestStatus = response.Certificate.Tree.GetValueOrDefault(pathRequestStatus);
+ return ParseRequestStatus(requestStatus);
+ }
+
///
/// Waits for a request to be processed and returns the candid response to the call. This is a helper
/// method built on top of `GetRequestStatusAsync` to wait for the response so it doesn't need to be
@@ -147,7 +333,7 @@ public static async Task CallAsynchronousAndWaitAsync(
/// The agent to use for the call
/// Canister to read state for
/// The unique identifier for the request
- /// Optional. Token to cancel request
+ /// Optional. Token to cancel waiting on the response
/// The raw candid arg response
public static async Task WaitForRequestAsync(
this IAgent agent,
@@ -180,5 +366,62 @@ public static async Task WaitForRequestAsync(
}
}
}
+
+
+ internal static RequestStatus? ParseRequestStatus(HashTree? requestStatus)
+ {
+ string? status = requestStatus?.GetValueOrDefault("status")?.AsLeaf().AsUtf8();
+ //received, processing, replied, rejected or done
+ switch (status)
+ {
+ case null:
+ return null;
+ case "received":
+ return RequestStatus.Received();
+ case "processing":
+ return RequestStatus.Processing();
+ case "replied":
+ HashTree.EncodedValue r = requestStatus!.GetValueOrDefault("reply")!.AsLeaf();
+ return RequestStatus.Replied(CandidArg.FromBytes(r));
+ case "rejected":
+ RejectCode code = (RejectCode)(ulong)requestStatus!.GetValueOrDefault("reject_code")!.AsLeaf().AsNat();
+ string message = requestStatus.GetValueOrDefault("reject_message")!.AsLeaf().AsUtf8();
+ string? errorCode = requestStatus.GetValueOrDefault("error_code")?.AsLeaf().AsUtf8();
+ return RequestStatus.Rejected(code, message, errorCode);
+ case "done":
+ return RequestStatus.Done();
+ default:
+ throw new NotImplementedException($"Invalid request status '{status}'");
+ }
+ }
+
+
+ private static SignedRequest SignRequest(
+ IIdentity? identity,
+ Func getRequest,
+ ICTimestamp? ingressExpiry = null
+ )
+ where TRequest : IRepresentationIndependentHashItem
+ {
+
+ Principal principal;
+ if (identity == null)
+ {
+ principal = Principal.Anonymous();
+ }
+ else
+ {
+ SubjectPublicKeyInfo publicKey = identity.GetPublicKey();
+ principal = publicKey.ToPrincipal();
+ }
+ TRequest request = getRequest(principal, ingressExpiry ?? ICTimestamp.Future(TimeSpan.FromMinutes(3)));
+
+ if (identity == null)
+ {
+ return new SignedRequest(request, null, null, null);
+ }
+ return identity.Sign(request);
+
+ }
}
}
diff --git a/src/Agent/EdjCase.ICP.Agent.csproj b/src/Agent/EdjCase.ICP.Agent.csproj
index 3726df0c..ad83d0db 100644
--- a/src/Agent/EdjCase.ICP.Agent.csproj
+++ b/src/Agent/EdjCase.ICP.Agent.csproj
@@ -27,8 +27,8 @@
-
-
+
+
diff --git a/src/Agent/Identities/IIdentity.cs b/src/Agent/Identities/IIdentity.cs
index 783d3d97..91947c8d 100644
--- a/src/Agent/Identities/IIdentity.cs
+++ b/src/Agent/Identities/IIdentity.cs
@@ -38,28 +38,23 @@ public interface IIdentity
/// The byte data to sign
/// The signature bytes of the specified data bytes
public byte[] Sign(byte[] data);
- }
- ///
- /// Extension methods for the IIdentity interface
- ///
- public static class IIdentityExtensions
- {
+
///
/// Signs the hashable content
///
- /// The identity to sign the content with
- /// The data that needs to be signed
+ /// The data that needs to be signed
/// The content with signature(s) from the identity
- public static SignedContent SignContent(this IIdentity identity, Dictionary content)
+ public SignedRequest Sign(TRequest request)
+ where TRequest : IRepresentationIndependentHashItem
{
- SubjectPublicKeyInfo senderPublicKey = identity.GetPublicKey();
+ SubjectPublicKeyInfo senderPublicKey = this.GetPublicKey();
var sha256 = SHA256HashFunction.Create();
- byte[] contentHash = content.ToHashable().ComputeHash(sha256);
+ RequestId requestId = RequestId.FromObject(request.BuildHashableItem(), sha256);
byte[] domainSeparator = Encoding.UTF8.GetBytes("\x0Aic-request");
- byte[] senderSignature = identity.Sign(domainSeparator.Concat(contentHash).ToArray());
- List? senderDelegations = identity.GetSenderDelegations();
- return new SignedContent(content, senderPublicKey, senderDelegations, senderSignature);
+ byte[] senderSignature = this.Sign([.. domainSeparator, .. requestId.RawValue]);
+ List? senderDelegations = this.GetSenderDelegations();
+ return new SignedRequest(request, senderPublicKey, senderDelegations, senderSignature, precomputedRequestId: requestId);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Agent/Models/Certificate.cs b/src/Agent/Models/Certificate.cs
index 256a5f1e..d534442c 100644
--- a/src/Agent/Models/Certificate.cs
+++ b/src/Agent/Models/Certificate.cs
@@ -41,13 +41,12 @@ public Certificate(HashTree tree, byte[] signature, CertificateDelegation? deleg
}
///
- /// Checks the validity of the certificate based off the
+ /// Checks the validity of the certificate based off the
/// specified root public key
///
- /// BLS crytography implementation to verify signature
/// The root public key (DER encoded) of the internet computer network
/// True if the certificate is valid, otherwise false
- public bool IsValid(IBlsCryptography bls, SubjectPublicKeyInfo rootPublicKey)
+ public bool IsValid(SubjectPublicKeyInfo rootPublicKey)
{
/*
verify_cert(cert) =
@@ -61,14 +60,14 @@ public bool IsValid(IBlsCryptography bls, SubjectPublicKeyInfo rootPublicKey)
if (this.Delegation != null)
{
// override the root key to the delegated one
- if (!this.Delegation.Certificate.IsValid(bls, rootPublicKey))
+ if (!this.Delegation.Certificate.IsValid(rootPublicKey))
{
// If delegation is not valid, then the cert is also not valid
return false;
}
rootPublicKey = this.Delegation.GetPublicKey();
}
- return bls.VerifySignature(rootPublicKey.PublicKey, rootHash, this.Signature);
+ return DefaultBlsCryptograhy.VerifySignature(rootPublicKey.PublicKey, rootHash, this.Signature);
}
internal static Certificate FromCbor(CborReader reader)
diff --git a/src/Agent/Models/SignedContent.cs b/src/Agent/Models/SignedRequest.cs
similarity index 65%
rename from src/Agent/Models/SignedContent.cs
rename to src/Agent/Models/SignedRequest.cs
index ec9dbbb3..8aedd97c 100644
--- a/src/Agent/Models/SignedContent.cs
+++ b/src/Agent/Models/SignedRequest.cs
@@ -1,3 +1,5 @@
+using EdjCase.ICP.Agent.Identities;
+using EdjCase.ICP.Candid.Crypto;
using EdjCase.ICP.Candid.Models;
using System;
using System.Collections.Generic;
@@ -9,12 +11,15 @@ namespace EdjCase.ICP.Agent.Models
///
/// A model containing content and the signature information of it
///
- public class SignedContent : IRepresentationIndependentHashItem
+ public class SignedRequest : IRepresentationIndependentHashItem
+ where TRequest : IRepresentationIndependentHashItem
{
+ private RequestId? requestIdCache;
+
///
- /// The content that is signed in the form of key value pairs
+ /// The request that is signed
///
- public Dictionary Content { get; }
+ public TRequest Content { get; }
///
/// Public key used to authenticate this request, unless anonymous, then null
@@ -34,20 +39,32 @@ public class SignedContent : IRepresentationIndependentHashItem
/// The content that is signed in the form of key value pairs
/// Public key used to authenticate this request, unless anonymous, then null
- /// Optional. A chain of delegations, starting with the one signed by sender_pubkey
+ /// Optional. A chain of delegations, starting with the one signed by sender_pubkey
/// and ending with the one delegating to the key relating to sender_sig.
+ /// Optional. Precomputed request id to use, otherwise will build it
/// Signature to authenticate this request, unless anonymous, then null
- public SignedContent(
- Dictionary content,
+ public SignedRequest(
+ TRequest content,
SubjectPublicKeyInfo? senderPublicKey,
List? delegations,
- byte[]? senderSignature
+ byte[]? senderSignature,
+ RequestId? precomputedRequestId = null
)
{
this.Content = content ?? throw new ArgumentNullException(nameof(content));
this.SenderPublicKey = senderPublicKey;
this.SenderDelegations = delegations;
this.SenderSignature = senderSignature;
+ this.requestIdCache = precomputedRequestId;
+ }
+ ///
+ /// Get the unique id for the request, which is hash of the content
+ /// Builds the id on the first call and caches it for future calls
+ ///
+ /// The id for the request
+ public RequestId GetOrBuildRequestId()
+ {
+ return this.requestIdCache ?? RequestId.FromObject(this.Content.BuildHashableItem(), SHA256HashFunction.Create());
}
///
@@ -55,7 +72,7 @@ public Dictionary BuildHashableItem()
{
var properties = new Dictionary
{
- {Properties.CONTENT, this.Content.ToHashable()}
+ {Properties.CONTENT, this.Content.BuildHashableItem().ToHashable()}
};
if (this.SenderPublicKey != null)
{
@@ -72,12 +89,26 @@ public Dictionary BuildHashableItem()
return properties;
}
+ ///
+ /// Serializes this signed content object to CBOR (Concise Binary Object Representation) format.
+ ///
+ /// A byte array containing the CBOR-encoded representation of this object.
+ public byte[] ToCborBytes()
+ {
+ CborWriter writer = new();
+ writer.WriteTag(CborTag.SelfDescribeCbor);
+ this.WriteCbor(writer);
+ return writer.Encode();
+ }
+
+
internal void WriteCbor(CborWriter writer)
{
writer.WriteStartMap(null);
writer.WriteTextString(Properties.CONTENT);
- writer.WriteStartMap(this.Content.Count);
- foreach ((string key, IHashable value) in this.Content)
+ Dictionary hashableContent = this.Content.BuildHashableItem();
+ writer.WriteStartMap(hashableContent.Count);
+ foreach ((string key, IHashable value) in hashableContent)
{
writer.WriteTextString(key);
writer.WriteHashableValue(value);
@@ -92,7 +123,7 @@ internal void WriteCbor(CborWriter writer)
{
writer.WriteTextString(Properties.SENDER_DELEGATION);
writer.WriteStartArray(this.SenderDelegations.Count);
- foreach(IHashable value in this.SenderDelegations)
+ foreach (IHashable value in this.SenderDelegations)
{
writer.WriteHashableValue(value);
}
diff --git a/src/Agent/Properties/AssemblyInfo.cs b/src/Agent/Properties/AssemblyInfo.cs
index 14aa3549..f89437bf 100644
--- a/src/Agent/Properties/AssemblyInfo.cs
+++ b/src/Agent/Properties/AssemblyInfo.cs
@@ -3,5 +3,6 @@
[assembly: InternalsVisibleTo("Candid.Tests")]
[assembly: InternalsVisibleTo("WebSockets.Tests")]
[assembly: InternalsVisibleTo("Performance.Tests")]
+[assembly: InternalsVisibleTo("Agent.Tests")]
[assembly: InternalsVisibleTo("EdjCase.ICP.WebSockets")]
[assembly: InternalsVisibleTo("EdjCase.ICP.PocketIC")]
diff --git a/src/Agent/Requests/CallRequest.cs b/src/Agent/Requests/CallRequest.cs
index 4186a90d..73768632 100644
--- a/src/Agent/Requests/CallRequest.cs
+++ b/src/Agent/Requests/CallRequest.cs
@@ -31,7 +31,7 @@ public class CallRequest : IRepresentationIndependentHashItem
///
public ICTimestamp IngressExpiry { get; }
///
- /// Optional. Arbitrary user-provided data, typically randomly generated. This can be used to create distinct requests with otherwise identical fields.
+ /// Optional. If specified will make the request unique even with the same arguments.
///
public byte[]? Nonce { get; }
@@ -40,7 +40,7 @@ public class CallRequest : IRepresentationIndependentHashItem
/// Argument to pass to the canister method
/// The user who issued the request
/// An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01
- /// Optional. Arbitrary user-provided data, typically randomly generated. This can be used to create distinct requests with otherwise identical fields.
+ /// Optional. If specified will make the request unique even with the same arguments
public CallRequest(
Principal canisterId,
string method,
@@ -116,4 +116,4 @@ private static class Properties
public const string NONCE = "nonce";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Agent/Requests/QueryRequest.cs b/src/Agent/Requests/QueryRequest.cs
index b7418c8f..da665f05 100644
--- a/src/Agent/Requests/QueryRequest.cs
+++ b/src/Agent/Requests/QueryRequest.cs
@@ -41,8 +41,7 @@ public class QueryRequest : IRepresentationIndependentHashItem
public ICTimestamp IngressExpiry { get; }
///
- /// Optional. Arbitrary user-provided data, typically randomly generated.
- /// This can be used to create distinct requests with otherwise identical fields.
+ /// Optional. If specified will make the request unique even with the same arguments
///
public byte[]? Nonce { get; }
@@ -51,12 +50,14 @@ public class QueryRequest : IRepresentationIndependentHashItem
/// Arguments to pass to the canister method
/// The user who issued the request
/// The expiration of the request to avoid replay attacks
+ /// Optional. If specified will make the request unique even with the same arguments
public QueryRequest(
Principal canisterId,
string method,
CandidArg arg,
Principal sender,
- ICTimestamp ingressExpiry
+ ICTimestamp ingressExpiry,
+ byte[]? nonce = null
)
{
this.CanisterId = canisterId ?? throw new ArgumentNullException(nameof(canisterId));
@@ -64,6 +65,7 @@ ICTimestamp ingressExpiry
this.Arg = arg ?? throw new ArgumentNullException(nameof(arg));
this.Sender = sender ?? throw new ArgumentNullException(nameof(sender));
this.IngressExpiry = ingressExpiry ?? throw new ArgumentNullException(nameof(ingressExpiry));
+ this.Nonce = nonce;
}
///
@@ -128,4 +130,4 @@ private static class Properties
public const string NONCE = "nonce";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Agent/Responses/V3CallResponse.cs b/src/Agent/Responses/V3CallResponse.cs
index f148c633..ef8aa31a 100644
--- a/src/Agent/Responses/V3CallResponse.cs
+++ b/src/Agent/Responses/V3CallResponse.cs
@@ -12,20 +12,20 @@ namespace EdjCase.ICP.Agent.Responses
public class V3CallResponse
{
///
- /// The status of the call ('replied', 'rejected', 'done)
+ /// The status of the call ('replied', 'rejected', etc..)
///
- public string Status { get; }
+ public RequestStatus.StatusType Status { get; }
///
/// The certificate data of the current canister state
///
public Certificate Certificate { get; }
- /// The status of the call ('replied', 'rejected', 'done)
+ /// The status of the call ('replied', 'rejected', etc..)
/// The certificate data of the current canister state
///
- public V3CallResponse(string status, Certificate certificate)
+ public V3CallResponse(RequestStatus.StatusType status, Certificate certificate)
{
- this.Status = status ?? throw new ArgumentNullException(nameof(status));
+ this.Status = status;
this.Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
}
@@ -37,7 +37,7 @@ internal static V3CallResponse ReadCbor(CborReader reader)
throw new CborContentException("Expected self describe tag");
}
Certificate? certificate = null;
- string? status = null;
+ string? statusString = null;
reader.ReadStartMap();
while (reader.PeekState() != CborReaderState.EndMap)
{
@@ -49,7 +49,7 @@ internal static V3CallResponse ReadCbor(CborReader reader)
certificate = Certificate.FromCbor(certReader);
break;
case "status":
- status = reader.ReadTextString();
+ statusString = reader.ReadTextString();
break;
default:
Debug.WriteLine($"Unknown field '{field}' in v3 call response");
@@ -59,7 +59,7 @@ internal static V3CallResponse ReadCbor(CborReader reader)
}
reader.ReadEndMap();
- if (status == null)
+ if (statusString == null)
{
throw new CborContentException("Missing field: status");
}
@@ -69,7 +69,12 @@ internal static V3CallResponse ReadCbor(CborReader reader)
throw new CborContentException("Missing field: certificate");
}
+ if (!Enum.TryParse(statusString, true, out var status))
+ {
+ throw new CborContentException($"Invalid status: {statusString}");
+ }
+
return new V3CallResponse(status, certificate);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Agent/Standards/AssetCanister/AssetCanisterApiClient.cs b/src/Agent/Standards/AssetCanister/AssetCanisterApiClient.cs
index 003a27ad..01bfd1ff 100644
--- a/src/Agent/Standards/AssetCanister/AssetCanisterApiClient.cs
+++ b/src/Agent/Standards/AssetCanister/AssetCanisterApiClient.cs
@@ -9,7 +9,7 @@
using EdjCase.ICP.Agent.Standards.AssetCanister.Models;
using System.Linq;
using System.Threading;
-using System.IO.Compression;
+using EdjCase.ICP.Agent.Identities;
namespace EdjCase.ICP.Agent.Standards.AssetCanister
{
@@ -28,6 +28,11 @@ public class AssetCanisterApiClient
///
public const int MAX_CHUNK_SIZE = MAX_INGRESS_MESSAGE_SIZE - 500; // Just under 2MB
+ ///
+ /// The identity to use for requests.
+ ///
+ public IIdentity? Identity { get; set; }
+
///
/// The IC agent
///
@@ -48,15 +53,18 @@ public class AssetCanisterApiClient
///
/// The agent used for communication.
/// The ID of the asset canister.
+ /// The identity to use for requests (optional).
/// The Candid converter to use for encoding and decoding values (optional).
public AssetCanisterApiClient(
IAgent agent,
Principal canisterId,
+ IIdentity? identity,
CandidConverter? converter = default
)
{
this.Agent = agent;
this.CanisterId = canisterId;
+ this.Identity = identity;
this.Converter = converter;
}
@@ -251,8 +259,7 @@ public async Task UploadAssetChunkedAsync(
public async Task GetApiVersionAsync()
{
CandidArg arg = CandidArg.FromCandid();
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "api_version", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "api_version", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -266,8 +273,7 @@ public async Task GetAsync(string key, List acceptEncodings)
{
GetRequest request = new(key, acceptEncodings);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -292,8 +298,7 @@ public async Task GetChunkAsync(
GetChunkRequest request = new(key, contentEncoding, index, sha256Opt);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_chunk", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_chunk", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -304,8 +309,7 @@ public async Task GetChunkAsync(
public async Task> ListAsync()
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.EmptyRecord());
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "list", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "list", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
@@ -316,8 +320,7 @@ public async Task> ListAsync()
public async Task GetCertifiedTreeAsync()
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.EmptyRecord());
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "certified_tree", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "certified_tree", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -328,7 +331,7 @@ public async Task GetCertifiedTreeAsync()
public async Task CreateBatchAsync()
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.EmptyRecord());
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "create_batch", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "create_batch", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -342,7 +345,7 @@ public async Task CreateChunkAsync(UnboundedUInt batchId, byt
{
CreateChunkRequest request = new(batchId, content);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "create_chunk", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "create_chunk", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -355,7 +358,7 @@ public async Task CommitBatchAsync(UnboundedUInt batchId, List
@@ -367,7 +370,7 @@ public async Task ProposeCommitBatchAsync(UnboundedUInt batchId, List
@@ -379,7 +382,7 @@ public async Task CommitProposedBatchAsync(UnboundedUInt batchId, byte[] evidenc
{
CommitProposedBatchRequest request = new(batchId, evidence);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "commit_proposed_batch", arg);
+ await this.Agent.CallAsync(this.CanisterId, "commit_proposed_batch", arg, identity: this.Identity);
}
///
@@ -395,7 +398,7 @@ public async Task> ComputeEvidenceAsync(UnboundedUInt batc
: OptionalValue.WithValue(maxIterations.Value);
ComputeEvidenceArguments request = new ComputeEvidenceArguments(batchId, maxIterationsOpt);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "compute_evidence", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "compute_evidence", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
@@ -407,7 +410,7 @@ public async Task DeleteBatchAsync(UnboundedUInt batchId)
{
DeleteBatchArguments request = new(batchId);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "delete_batch", arg);
+ await this.Agent.CallAsync(this.CanisterId, "delete_batch", arg, identity: this.Identity);
}
///
@@ -453,7 +456,7 @@ public async Task CreateAssetAsync(
allowRawAccessOpt
);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "create_asset", arg);
+ await this.Agent.CallAsync(this.CanisterId, "create_asset", arg, identity: this.Identity);
}
///
@@ -475,7 +478,7 @@ public async Task SetAssetContentAsync(
: OptionalValue.WithValue(sha256);
SetAssetContentArguments request = new(key, contentEncoding, chunkIds, sha256Opt);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "set_asset_content", arg);
+ await this.Agent.CallAsync(this.CanisterId, "set_asset_content", arg, identity: this.Identity);
}
///
@@ -488,7 +491,7 @@ public async Task UnsetAssetContentAsync(string key, string contentEncoding)
{
UnsetAssetContentArguments request = new(key, contentEncoding);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "unset_asset_content", arg);
+ await this.Agent.CallAsync(this.CanisterId, "unset_asset_content", arg, identity: this.Identity);
}
///
@@ -499,7 +502,7 @@ public async Task DeleteAssetAsync(string key)
{
DeleteAssetArguments request = new(key);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "delete_asset", arg);
+ await this.Agent.CallAsync(this.CanisterId, "delete_asset", arg, identity: this.Identity);
}
///
@@ -508,7 +511,7 @@ public async Task DeleteAssetAsync(string key)
public async Task ClearAsync()
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.EmptyRecord());
- await this.Agent.CallAsync(this.CanisterId, "clear", arg);
+ await this.Agent.CallAsync(this.CanisterId, "clear", arg, identity: this.Identity);
}
///
@@ -532,7 +535,7 @@ public async Task StoreAsync(
: OptionalValue.WithValue(sha256);
StoreRequest request = new(key, contentType, contextEncoding, content, sha256Opt);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "store", arg);
+ await this.Agent.CallAsync(this.CanisterId, "store", arg, identity: this.Identity);
}
///
@@ -542,7 +545,7 @@ public async Task StoreAsync(
public async Task AuthorizeAsync(Principal principal)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(principal, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "authorize", arg);
+ await this.Agent.CallAsync(this.CanisterId, "authorize", arg, identity: this.Identity);
}
///
@@ -552,7 +555,7 @@ public async Task AuthorizeAsync(Principal principal)
public async Task DeauthorizeAsync(Principal principal)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(principal, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "deauthorize", arg);
+ await this.Agent.CallAsync(this.CanisterId, "deauthorize", arg, identity: this.Identity);
}
///
@@ -562,7 +565,7 @@ public async Task DeauthorizeAsync(Principal principal)
public async Task> ListAuthorizedAsync()
{
CandidArg arg = CandidArg.FromCandid();
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "list_authorized", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "list_authorized", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
@@ -575,7 +578,7 @@ public async Task GrantPermissionAsync(Principal principal, Permission permissio
{
GrantPermission request = new(principal, permission);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "grant_permission", arg);
+ await this.Agent.CallAsync(this.CanisterId, "grant_permission", arg, identity: this.Identity);
}
///
@@ -587,7 +590,7 @@ public async Task RevokePermissionAsync(Principal principal, Permission permissi
{
RevokePermission request = new RevokePermission(principal, permission);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "revoke_permission", arg);
+ await this.Agent.CallAsync(this.CanisterId, "revoke_permission", arg, identity: this.Identity);
}
///
@@ -599,7 +602,7 @@ public async Task> ListPermittedAsync(Permission withPermission)
{
ListPermitted request = new ListPermitted(withPermission);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "list_permitted", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "list_permitted", arg, identity: this.Identity);
return reply.ToObjects>(this.Converter);
}
@@ -609,7 +612,7 @@ public async Task> ListPermittedAsync(Permission withPermission)
public async Task TakeOwnershipAsync()
{
CandidArg arg = CandidArg.FromCandid();
- await this.Agent.CallAsync(this.CanisterId, "take_ownership", arg);
+ await this.Agent.CallAsync(this.CanisterId, "take_ownership", arg, identity: this.Identity);
}
///
@@ -620,8 +623,7 @@ public async Task TakeOwnershipAsync()
public async Task GetAssetPropertiesAsync(string key)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(key, this.Converter));
- QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "get_asset_properties", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "get_asset_properties", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -632,7 +634,7 @@ public async Task GetAssetPropertiesAsync(string key)
public async Task SetAssetPropertiesAsync(SetAssetPropertiesRequest request)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "set_asset_properties", arg);
+ await this.Agent.CallAsync(this.CanisterId, "set_asset_properties", arg, identity: this.Identity);
}
///
@@ -642,7 +644,7 @@ public async Task SetAssetPropertiesAsync(SetAssetPropertiesRequest request)
public async Task GetConfigurationAsync()
{
CandidArg arg = CandidArg.FromCandid();
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "get_configuration", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "get_configuration", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -653,7 +655,7 @@ public async Task GetConfigurationAsync()
public async Task ConfigureAsync(ConfigureRequest request)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- await this.Agent.CallAsync(this.CanisterId, "configure", arg);
+ await this.Agent.CallAsync(this.CanisterId, "configure", arg, identity: this.Identity);
}
///
@@ -666,7 +668,7 @@ public async Task ValidateGrantPermissionAsync(Principal princ
{
GrantPermission request = new(principal, permission);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_grant_permission", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_grant_permission", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -680,7 +682,7 @@ public async Task ValidateRevokePermissionAsync(Principal prin
{
RevokePermission request = new(principal, permission);
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_revoke_permission", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_revoke_permission", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -691,7 +693,7 @@ public async Task ValidateRevokePermissionAsync(Principal prin
public async Task ValidateTakeOwnershipAsync()
{
CandidArg arg = CandidArg.FromCandid();
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_take_ownership", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_take_ownership", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -703,7 +705,7 @@ public async Task ValidateTakeOwnershipAsync()
public async Task ValidateCommitProposedBatchAsync(CommitProposedBatchRequest request)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_commit_proposed_batch", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_commit_proposed_batch", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
@@ -715,7 +717,7 @@ public async Task ValidateCommitProposedBatchAsync(CommitPropo
public async Task ValidateConfigureAsync(ConfigureRequest request)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(request, this.Converter));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_configure", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "validate_configure", arg, identity: this.Identity);
return reply.ToObjects(this.Converter);
}
}
diff --git a/src/Agent/Standards/ICRC1/ICRC1Client.cs b/src/Agent/Standards/ICRC1/ICRC1Client.cs
index 8434d597..26cf6cc8 100644
--- a/src/Agent/Standards/ICRC1/ICRC1Client.cs
+++ b/src/Agent/Standards/ICRC1/ICRC1Client.cs
@@ -1,4 +1,5 @@
using EdjCase.ICP.Agent.Agents;
+using EdjCase.ICP.Agent.Identities;
using EdjCase.ICP.Agent.Standards.ICRC1.Models;
using EdjCase.ICP.Candid.Models;
using System.Collections.Generic;
@@ -11,6 +12,10 @@ namespace EdjCase.ICP.Agent.Standards.ICRC1
///
public class ICRC1Client
{
+ ///
+ /// Identity to use for requests
+ ///
+ public IIdentity? Identity { get; set; }
///
/// Agent to use to make requests to the IC
///
@@ -26,10 +31,12 @@ public class ICRC1Client
///
/// Agent to use to make requests to the IC
/// The id of the canister to make requests to
- public ICRC1Client(IAgent agent, Principal canisterId)
+ /// Optional. The identity to use for requests
+ public ICRC1Client(IAgent agent, Principal canisterId, IIdentity? identity)
{
this.Agent = agent;
this.CanisterId = canisterId;
+ this.Identity = identity;
}
///
@@ -39,8 +46,7 @@ public ICRC1Client(IAgent agent, Principal canisterId)
public async Task> MetaData()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_meta_data", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_meta_data", arg, identity: this.Identity);
return reply.ToObjects>();
}
@@ -51,8 +57,7 @@ public async Task> MetaData()
public async Task Name()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_name", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_name", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -63,8 +68,7 @@ public async Task Name()
public async Task Symbol()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_symbol", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_symbol", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -75,8 +79,7 @@ public async Task Symbol()
public async Task Decimals()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_decimals", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_decimals", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -87,8 +90,7 @@ public async Task Decimals()
public async Task Fee()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_fee", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_fee", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -99,8 +101,7 @@ public async Task Fee()
public async Task TotalSupply()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_total_supply", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_total_supply", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -111,8 +112,7 @@ public async Task TotalSupply()
public async Task> MintingAccount()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_minting_account", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_minting_account", arg, identity: this.Identity);
return reply.ToObjects>();
}
@@ -124,8 +124,7 @@ public async Task> MintingAccount()
public async Task BalanceOf(Account account)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(account));
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_balance_of", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_balance_of", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -137,7 +136,7 @@ public async Task BalanceOf(Account account)
public async Task Transfer(TransferArgs args)
{
CandidArg arg = CandidArg.FromCandid(CandidTypedValue.FromObject(args));
- CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "icrc1_transfer", arg);
+ CandidArg reply = await this.Agent.CallAsync(this.CanisterId, "icrc1_transfer", arg, identity: this.Identity);
return reply.ToObjects();
}
@@ -148,9 +147,8 @@ public async Task Transfer(TransferArgs args)
public async Task> SupportedStandards()
{
CandidArg arg = CandidArg.FromCandid();
- Responses.QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "icrc1_supported_standards", arg);
- CandidArg reply = response.ThrowOrGetReply();
+ CandidArg reply = await this.Agent.QueryAsync(this.CanisterId, "icrc1_supported_standards", arg, identity: this.Identity);
return reply.ToObjects>();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/BLS/API.xml b/src/BLS/API.xml
index dabc0304..1b632b52 100644
--- a/src/BLS/API.xml
+++ b/src/BLS/API.xml
@@ -4,15 +4,6 @@
EdjCase.ICP.BLS
-
-
- Bls cryptography class that AWLAYS returns TRUE. This is intended only for
- development scenarios and is never recommended
-
-
-
-
-
Represents the default implementation of the IBlsCryptography interface for BLS cryptography.
@@ -21,19 +12,5 @@
-
-
- An interface for all BLS crytography operations
-
-
-
-
- Verifies a BLS signature (ICP flavor only)
-
- The signer public key
- The SHA256 hash of the message
- The signature of the message
- True if the signature is valid, otherwise false
-
diff --git a/src/BLS/BypassedBlsCryptography.cs b/src/BLS/BypassedBlsCryptography.cs
deleted file mode 100644
index f8832ead..00000000
--- a/src/BLS/BypassedBlsCryptography.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace EdjCase.ICP.BLS
-{
- ///
- /// Bls cryptography class that AWLAYS returns TRUE. This is intended only for
- /// development scenarios and is never recommended
- ///
- public class BypassedBlsCryptography : IBlsCryptography
- {
- ///
- public bool VerifySignature(byte[] publicKey, byte[] messageHash, byte[] signature)
- {
- return true;
- }
- }
-}
diff --git a/src/BLS/DefaultBlsCryptograhy.cs b/src/BLS/DefaultBlsCryptograhy.cs
index c182e8aa..6270f432 100644
--- a/src/BLS/DefaultBlsCryptograhy.cs
+++ b/src/BLS/DefaultBlsCryptograhy.cs
@@ -12,8 +12,9 @@ namespace EdjCase.ICP.BLS
///
/// Represents the default implementation of the IBlsCryptography interface for BLS cryptography.
///
- public class DefaultBlsCryptograhy : IBlsCryptography
+ internal static class DefaultBlsCryptograhy
{
+
internal static readonly byte[] DomainSeperator;
static DefaultBlsCryptograhy()
@@ -22,7 +23,7 @@ static DefaultBlsCryptograhy()
}
///
- public bool VerifySignature(byte[] publicKey, byte[] messageHash, byte[] signature)
+ public static bool VerifySignature(byte[] publicKey, byte[] messageHash, byte[] signature)
{
G1Affine sig = G1Affine.FromCompressed(signature);
G2Prepared g2Gen = G2Affine.Generator().Neg().ToProjective().ToPrepared();
@@ -34,10 +35,10 @@ public bool VerifySignature(byte[] publicKey, byte[] messageHash, byte[] signatu
(sig, g2Gen),
(msg, pk)
};
- return this.VerifyInternal(pairs);
+ return VerifyInternal(pairs);
}
- private bool VerifyInternal(
+ private static bool VerifyInternal(
(G1Affine G1, G2Prepared G2)[] pairs
)
{
diff --git a/src/BLS/IBlsCryptography.cs b/src/BLS/IBlsCryptography.cs
deleted file mode 100644
index 9ecb96e9..00000000
--- a/src/BLS/IBlsCryptography.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace EdjCase.ICP.BLS
-{
- ///
- /// An interface for all BLS crytography operations
- ///
- public interface IBlsCryptography
- {
-
- ///
- /// Verifies a BLS signature (ICP flavor only)
- ///
- /// The signer public key
- /// The SHA256 hash of the message
- /// The signature of the message
- /// True if the signature is valid, otherwise false
- bool VerifySignature(
- byte[] publicKey,
- byte[] messageHash,
- byte[] signature
- );
- }
-}
diff --git a/src/BLS/Properties/AssemblyInfo.cs b/src/BLS/Properties/AssemblyInfo.cs
index 265facf3..5772320e 100644
--- a/src/BLS/Properties/AssemblyInfo.cs
+++ b/src/BLS/Properties/AssemblyInfo.cs
@@ -2,4 +2,7 @@
[assembly: InternalsVisibleTo("BLS.Tests")]
[assembly: InternalsVisibleTo("Performance.Tests")]
+[assembly: InternalsVisibleTo("PocketIC.Tests")]
+[assembly: InternalsVisibleTo("WebSockets.Tests")]
[assembly: InternalsVisibleTo("Sample.CLI")]
+[assembly: InternalsVisibleTo("EdjCase.ICP.Agent")]
diff --git a/src/Candid/API.xml b/src/Candid/API.xml
index f0be53ef..83548de0 100644
--- a/src/Candid/API.xml
+++ b/src/Candid/API.xml
@@ -1413,7 +1413,7 @@
- A helper class that wraps around a byte array, giving functions to convert
+ A helper class that wraps around a byte array, giving functions to convert
to common types like text and numbers
@@ -1482,6 +1482,18 @@
The UTF8 string value to use with the encoded value
+
+
+ A helper method to implicitly convert an encoded value to an unbounded uint
+
+ The encoded value to get the raw value from
+
+
+
+ A helper method to implicitly convert an unbounded uint to an encoded value
+
+ The unbounded uint value to use with the encoded value
+
Creates an encoded value from a utf8 string value
@@ -1489,6 +1501,13 @@
UTF8 encoded string
UTF8 encoded value
+
+
+ Creates an encoded value from an unbounded uint value
+
+ Unbounded uint value
+ Unbounded uint encoded value
+
@@ -1901,11 +1920,11 @@
Raw request id bytes
A request id object
-
+
Converts a hashable object into a request id
- The properties of the object to hash
+ The object to hash
The hash function to use to generate the hash
A request id object
diff --git a/src/Candid/EdjCase.ICP.Candid.csproj b/src/Candid/EdjCase.ICP.Candid.csproj
index b6baba3a..18a76852 100644
--- a/src/Candid/EdjCase.ICP.Candid.csproj
+++ b/src/Candid/EdjCase.ICP.Candid.csproj
@@ -26,9 +26,9 @@
-
-
-
+
+
+
diff --git a/src/Candid/Models/HashTree.cs b/src/Candid/Models/HashTree.cs
index 43989a5e..d65edc3d 100644
--- a/src/Candid/Models/HashTree.cs
+++ b/src/Candid/Models/HashTree.cs
@@ -257,7 +257,7 @@ public override string ToString()
}
///
- /// A helper class that wraps around a byte array, giving functions to convert
+ /// A helper class that wraps around a byte array, giving functions to convert
/// to common types like text and numbers
///
public class EncodedValue : IEquatable
@@ -337,7 +337,8 @@ public bool Equals(byte[]? other)
if (ReferenceEquals(other, null))
{
return false;
- };
+ }
+ ;
return this.Value.AsSpan().SequenceEqual(other);
}
@@ -403,6 +404,24 @@ public static implicit operator EncodedValue(string utf8Value)
return EncodedValue.Utf8Value(utf8Value);
}
+ ///
+ /// A helper method to implicitly convert an encoded value to an unbounded uint
+ ///
+ /// The encoded value to get the raw value from
+ public static implicit operator UnboundedUInt(EncodedValue value)
+ {
+ return value.AsNat();
+ }
+
+ ///
+ /// A helper method to implicitly convert an unbounded uint to an encoded value
+ ///
+ /// The unbounded uint value to use with the encoded value
+ public static implicit operator EncodedValue(UnboundedUInt value)
+ {
+ return EncodedValue.NatValue(value);
+ }
+
///
/// Creates an encoded value from a utf8 string value
///
@@ -413,6 +432,16 @@ public static EncodedValue Utf8Value(string value)
return Encoding.UTF8.GetBytes(value);
}
+ ///
+ /// Creates an encoded value from an unbounded uint value
+ ///
+ /// Unbounded uint value
+ /// Unbounded uint encoded value
+ public static EncodedValue NatValue(UnboundedUInt value)
+ {
+ return LEB128.EncodeUnsigned(value);
+ }
+
internal static byte[] WithDomainSeperator(string value, params byte[][] encodedValues)
{
@@ -480,7 +509,8 @@ public bool Equals(HashTree? other)
if (ReferenceEquals(other, null))
{
return false;
- };
+ }
+ ;
if (this.Type != other.Type)
{
return false;
diff --git a/src/Candid/Models/IHashable.cs b/src/Candid/Models/IHashable.cs
index b82007fc..98c148fb 100644
--- a/src/Candid/Models/IHashable.cs
+++ b/src/Candid/Models/IHashable.cs
@@ -190,4 +190,4 @@ public static HashableObject ToHashable(this Dictionary value)
return new HashableObject(value.ToDictionary(v => v.Key, v => (IHashable)v.Value));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Candid/Models/RequestId.cs b/src/Candid/Models/RequestId.cs
index 83a80f2b..afe02fb6 100644
--- a/src/Candid/Models/RequestId.cs
+++ b/src/Candid/Models/RequestId.cs
@@ -34,12 +34,12 @@ public static RequestId FromBytes(byte[] bytes)
///
/// Converts a hashable object into a request id
///
- /// The properties of the object to hash
+ /// The object to hash
/// The hash function to use to generate the hash
/// A request id object
- public static RequestId FromObject(IDictionary properties, IHashFunction hashFunction)
+ public static RequestId FromObject(Dictionary item, IHashFunction hashFunction)
{
- var orderedProperties = properties
+ byte[] bytes = item
.Where(o => o.Value != null) // Remove empty/null ones
.Select(o =>
{
@@ -48,8 +48,7 @@ public static RequestId FromObject(IDictionary properties, IH
return (KeyHash: keyDigest, ValueHash: valueDigest);
}) // Hash key and value bytes
- .OrderBy(o => o.KeyHash, new HashComparer()); // Keys in order
- byte[] bytes = orderedProperties
+ .OrderBy(o => o.KeyHash, new HashComparer()) // Keys in order
.SelectMany(o => o.KeyHash.Concat(o.ValueHash))
.ToArray(); // Create single byte[] by concatinating them all together
return new RequestId(hashFunction.ComputeHash(bytes));
diff --git a/src/Candid/Properties/AssemblyInfo.cs b/src/Candid/Properties/AssemblyInfo.cs
index f4f42ad6..cde6e1e0 100644
--- a/src/Candid/Properties/AssemblyInfo.cs
+++ b/src/Candid/Properties/AssemblyInfo.cs
@@ -4,6 +4,7 @@
[assembly: InternalsVisibleTo("BLS.Tests")]
[assembly: InternalsVisibleTo("WebSockets.Tests")]
[assembly: InternalsVisibleTo("Performance.Tests")]
+[assembly: InternalsVisibleTo("Agent.Tests")]
[assembly: InternalsVisibleTo("EdjCase.ICP.ClientGenerator")]
[assembly: InternalsVisibleTo("EdjCase.ICP.Agent")]
[assembly: InternalsVisibleTo("EdjCase.ICP.WebSockets")]
diff --git a/src/ClientGenerator/API.xml b/src/ClientGenerator/API.xml
index 387f9955..d10a5806 100644
--- a/src/ClientGenerator/API.xml
+++ b/src/ClientGenerator/API.xml
@@ -97,7 +97,7 @@
- Optional. The url of the boundry node for the internet computer. Defaults to ic0.app
+ Optional. The url of the boundry node for the internet computer. Defaults to icp-api.io
@@ -118,7 +118,7 @@
If true, variant classes will be generated with properties instead of methods
If true, the names of properties and methods will keep the raw candid name. Otherwise they will be converted to something prettier
If false, OptionalValue will be used for opt values, Otherwise will use just the nullable class values or nullable struct where possible. Defaults to false
- Optional. The url of the boundry node for the internet computer. Defaults to ic0.app
+ Optional. The url of the boundry node for the internet computer. Defaults to icp-api.io
Optional. Specifies options for each candid type in the definition
diff --git a/src/ClientGenerator/ClientCodeGenerator.cs b/src/ClientGenerator/ClientCodeGenerator.cs
index 6ee0c06a..cbd98fac 100644
--- a/src/ClientGenerator/ClientCodeGenerator.cs
+++ b/src/ClientGenerator/ClientCodeGenerator.cs
@@ -30,7 +30,7 @@ public static async Task GenerateClientFromCanisterAsync(
ClientGenerationOptions options
)
{
- var agent = new HttpAgent(identity: null, httpBoundryNodeUrl: options.BoundryNodeUrl);
+ IAgent agent = new HttpAgent(httpBoundryNodeUrl: options.BoundryNodeUrl);
var candidServicePath = StatePath.FromSegments("canister", canisterId.Raw, "metadata", "candid:service");
var paths = new List
{
diff --git a/src/ClientGenerator/ClientGenerationOptions.cs b/src/ClientGenerator/ClientGenerationOptions.cs
index 6666f5e5..c50821dd 100644
--- a/src/ClientGenerator/ClientGenerationOptions.cs
+++ b/src/ClientGenerator/ClientGenerationOptions.cs
@@ -75,7 +75,7 @@ public class ClientGenerationOptions
public bool OverrideOptionalValue { get; }
///
- /// Optional. The url of the boundry node for the internet computer. Defaults to ic0.app
+ /// Optional. The url of the boundry node for the internet computer. Defaults to icp-api.io
///
public Uri? BoundryNodeUrl { get; }
@@ -97,7 +97,7 @@ public class ClientGenerationOptions
/// If true, variant classes will be generated with properties instead of methods
/// If true, the names of properties and methods will keep the raw candid name. Otherwise they will be converted to something prettier
/// If false, OptionalValue will be used for opt values, Otherwise will use just the nullable class values or nullable struct where possible. Defaults to false
- /// Optional. The url of the boundry node for the internet computer. Defaults to ic0.app
+ /// Optional. The url of the boundry node for the internet computer. Defaults to icp-api.io
/// Optional. Specifies options for each candid type in the definition
public ClientGenerationOptions(
string name,
diff --git a/src/ClientGenerator/EdjCase.ICP.ClientGenerator.csproj b/src/ClientGenerator/EdjCase.ICP.ClientGenerator.csproj
index 593919f1..a9ae3073 100644
--- a/src/ClientGenerator/EdjCase.ICP.ClientGenerator.csproj
+++ b/src/ClientGenerator/EdjCase.ICP.ClientGenerator.csproj
@@ -32,9 +32,9 @@
-
-
-
+
+
+
diff --git a/src/ClientGenerator/README.md b/src/ClientGenerator/README.md
index 317c1047..93e5dfba 100644
--- a/src/ClientGenerator/README.md
+++ b/src/ClientGenerator/README.md
@@ -59,7 +59,7 @@ candid-client-generator gen ./
Files generated in a sub-folder will have a more specific namespace to match. This namespace can be overidden per client.
- `output-directory` - (Text) OPTIONAL. Directory to put all generated files. Each client will have a sub-folder within the output directory that will match the client name. If not specified, the working directory will be used
- `no-folders` - (Bool) OPTIONAL. If true, no sub-folders will be generated for the clients or the models within the clients. All generated files will be in a flat structure. Defaults to false
-- `url` - (Text) OPTIONAL. Sets the boundry node url to use for making calls to canisters on the IC. Can be set to a local developer instance/localhost. Defaults to 'https://ic0.app/'. This setting is only useful for clients of generation type `canister`
+- `url` - (Text) OPTIONAL. Sets the boundry node url to use for making calls to canisters on the IC. Can be set to a local developer instance/localhost. Defaults to 'https://icp-api.io/'. This setting is only useful for clients of generation type `canister`
- `feature-nullable` - (Bool) Optional. Sets whether to use the C# nullable feature when generating the client (like `object?`). Defaults to true
- `variants-use-properties` - (Bool) Optional. If true, the generated variant classes will use properties instead of methods for data access. Defaults to false
- `keep-candid-case` - (Bool) Optional. If true, the names of properties and methods will keep the raw candid name. Otherwise they will be converted to something prettier. Defaults to false
diff --git a/src/ClientGenerator/RoslynTypeResolver.cs b/src/ClientGenerator/RoslynTypeResolver.cs
index e0881a4d..30438de4 100644
--- a/src/ClientGenerator/RoslynTypeResolver.cs
+++ b/src/ClientGenerator/RoslynTypeResolver.cs
@@ -1,1932 +1,1946 @@
-using EdjCase.ICP.ClientGenerator;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using EdjCase.ICP.Agent.Agents;
-using EdjCase.ICP.Candid.Models;
-using EdjCase.ICP.Candid.Mapping;
-using EdjCase.ICP.Candid.Models.Values;
-using EdjCase.ICP.Agent.Responses;
-using System.Threading.Tasks;
-using EdjCase.ICP.Candid;
-using Org.BouncyCastle.Asn1.Cms;
-using EdjCase.ICP.Candid.Models.Types;
-
-namespace EdjCase.ICP.ClientGenerator
-{
- internal class RoslynTypeResolver
- {
- private readonly Dictionary _resolvedTypes = new();
-
- public string ModelNamespace { get; }
- public HashSet Aliases { get; }
- public bool FeatureNullable { get; }
- public bool VariantsUseProperties { get; }
- public NameHelper NameHelper { get; }
- public Dictionary DeclaredTypes { get; }
-
- public RoslynTypeResolver(
- string modelNamespace,
- HashSet aliases,
- bool featureNullable,
- bool variantsUseProperties,
- NameHelper nameHelper,
- Dictionary declaredTypes
- )
- {
- this.ModelNamespace = modelNamespace;
- this.Aliases = aliases;
- this.FeatureNullable = featureNullable;
- this.VariantsUseProperties = variantsUseProperties;
- this.NameHelper = nameHelper;
- this.DeclaredTypes = declaredTypes;
- }
-
- public ResolvedType ResolveTypeDeclaration(
- string typeId,
- string typeName,
- SourceCodeType type
- )
- {
- // note that this only works for only one level of type nesting, so type aliases to generics whose argument is a user-defined type
- // will fail, for example:
- // type A = record { left : A, right : B };
- // type X = blob;
- // type F = A;
-
-
- if (this._resolvedTypes.TryGetValue(typeId, out ResolvedType? existing))
- {
- return existing;
- }
- Stack parentTypeIds = new Stack();
- parentTypeIds.Push(typeId);
- ResolvedType res = this.ResolveType(type, typeName, parentType: null, parentTypeIds);
- this._resolvedTypes[typeId] = res;
- return res;
- }
-
- private ResolvedType ResolveType(
- SourceCodeType type,
- string nameContext,
- TypeName? parentType,
- Stack parentTypeIds
- )
- {
- switch (type)
- {
- case RawCandidType t:
- {
- CandidTypeTypeName typeName = new(t.CandidType);
- return new ResolvedType(typeName);
- }
- case NonGenericSourceCodeType c:
- {
- var cType = new SimpleTypeName(
- c.Type.Name,
- c.Type.Namespace,
- isDefaultNullable: c.Type.IsClass
- );
- return new ResolvedType(cType);
- }
- case ListSourceCodeType l:
- {
- if (l.IsPredefinedType)
- {
- ResolvedType resolvedGenericType = this.ResolveType(l.ElementType, nameContext + "Item", parentType, parentTypeIds);
-
- var name = new ListTypeName(resolvedGenericType.Name);
- return new ResolvedType(name, resolvedGenericType.GeneratedSyntax);
- }
- else
- {
- TypeName listName = this.BuildType(nameContext, parentType, isDefaultNullable: true);
- ClassDeclarationSyntax syntax = this.GenerateList(listName, l, parentTypeIds);
- return new ResolvedType(listName, new MemberDeclarationSyntax[] { syntax });
- }
- }
- case DictionarySourceCodeType d:
- {
- if (d.IsPredefinedType)
- {
- ResolvedType resolvedKeyType = this.ResolveType(d.KeyType, nameContext + "Key", parentType, parentTypeIds);
- ResolvedType resolvedValueType = this.ResolveType(d.ValueType, nameContext + "Value", parentType, parentTypeIds);
- MemberDeclarationSyntax[] generatedSyntax = (resolvedKeyType.GeneratedSyntax ?? Array.Empty())
- .Concat(resolvedValueType.GeneratedSyntax ?? Array.Empty())
- .ToArray(); ;
- var name = new DictionaryTypeName(resolvedKeyType.Name, resolvedValueType.Name);
- return new ResolvedType(name, generatedSyntax);
- }
- else
- {
- TypeName dictName = this.BuildType(nameContext, parentType, isDefaultNullable: true);
- ClassDeclarationSyntax syntax = this.GenerateDictionary(dictName, d, parentTypeIds);
- return new ResolvedType(dictName, new MemberDeclarationSyntax[] { syntax });
- }
- }
- case TupleSourceCodeType t:
- {
- List resolvedGenericTypes = t.Fields
- .Select((f, i) => this.ResolveType(f, nameContext + "Value_" + i, parentType, parentTypeIds))
- .ToList();
- List elementTypeNames = resolvedGenericTypes
- .Select(f => f.Name)
- .ToList();
- MemberDeclarationSyntax[] generatedSyntax = resolvedGenericTypes
- .SelectMany(t => t.GeneratedSyntax ?? Array.Empty())
- .ToArray();
- var name = new TupleTypeName(elementTypeNames);
- return new ResolvedType(name, generatedSyntax);
- }
- case OptionalValueSourceCodeType v:
- {
- TypeName name;
- if (v.IsPredefinedType)
- {
- ResolvedType resolvedGenericType = this.ResolveType(v.GenericType, nameContext + "Value", parentType, parentTypeIds);
- name = new OptionalValueTypeName(resolvedGenericType.Name);
- return new ResolvedType(name, resolvedGenericType.GeneratedSyntax);
- }
- else
- {
- TypeName oValueName = this.BuildType(nameContext, parentType, isDefaultNullable: true);
- ClassDeclarationSyntax syntax = this.GenerateOptionalValue(oValueName, v, parentTypeIds);
- return new ResolvedType(oValueName, new MemberDeclarationSyntax[] { syntax });
- }
- }
- case ArraySourceCodeType a:
- {
- if (a.ElementType == null)
- {
- return new ResolvedType(new ArrayTypeName(null));
- }
- ResolvedType resolvedGenericType = this.ResolveType(a.ElementType, nameContext + "Item", parentType, parentTypeIds);
-
- var name = new ArrayTypeName(resolvedGenericType.Name);
- return new ResolvedType(name, resolvedGenericType.GeneratedSyntax);
- }
- case ReferenceSourceCodeType re:
- {
- bool isAlias = this.Aliases.Contains(re.Id.Value);
- (string name, SourceCodeType sourceCodeType) = this.DeclaredTypes[re.Id.Value];
-
- string? @namespace = isAlias ? null : this.ModelNamespace;
- TypeName typeName = new SimpleTypeName(
- name,
- @namespace,
- true // TODO
- );
- return new ResolvedType(typeName);
-
- }
- case VariantSourceCodeType v:
- {
- TypeName variantName = this.BuildType(nameContext, parentType, isDefaultNullable: true);
- (ClassDeclarationSyntax? ClassSyntax, EnumDeclarationSyntax EnumSyntax) result = this.GenerateVariant(variantName, v, parentType, parentTypeIds);
- if (result.ClassSyntax != null)
- {
- return new ResolvedType(variantName, new MemberDeclarationSyntax[] { result.ClassSyntax, result.EnumSyntax });
- }
- return new ResolvedType(variantName, new MemberDeclarationSyntax[] { result.EnumSyntax });
- }
- case RecordSourceCodeType r:
- {
- TypeName recordName = this.BuildType(nameContext, parentType, isDefaultNullable: true);
- ClassDeclarationSyntax classSyntax = this.GenerateRecord(recordName, r, parentTypeIds);
- return new ResolvedType(recordName, new MemberDeclarationSyntax[] { classSyntax });
- }
- default:
- throw new NotImplementedException();
- }
- }
-
-
- public ClassDeclarationSyntax GenerateClient(
- TypeName clientName,
- ServiceSourceCodeType service)
- {
- string candidConverterProperty = "Converter";
- List properties = new()
- {
- // public IAgent Agent { get; }
- new ClassProperty(
- name: "Agent",
- type: SimpleTypeName.FromType(),
- access: AccessType.Public,
- hasSetter: false
- ),
-
- // public Principal CanisterId { get; }
- new ClassProperty(
- name: "CanisterId",
- type: SimpleTypeName.FromType(),
- access: AccessType.Public,
- hasSetter: false
- ),
- };
-
- List optionalProperties = new()
- {
- // public CandidConverter? Converter { get; }
- new ClassProperty(
- name: candidConverterProperty,
- type: SimpleTypeName.FromType(isNullable: this.FeatureNullable),
- access: AccessType.Public,
- hasSetter: false
- )
- };
-
- Stack parentTypeIds = new();
- List<(MethodDeclarationSyntax Method, List SubTypes)> methods = service.Methods
- .Select(method => this.GenerateFuncMethod(method.CsharpName, method.CandidName, method.FuncInfo, clientName, this, candidConverterProperty, parentTypeIds))
- .ToList();
-
- return this.GenerateClass(
- clientName,
- properties,
- optionalProperties: optionalProperties,
- methods: methods.Select(m => m.Method).ToList(),
- subTypes: methods.SelectMany(m => m.SubTypes).ToList()
- );
- }
-
- internal ClassDeclarationSyntax GenerateOptionalValue(
- TypeName oValueName,
- OptionalValueSourceCodeType v,
- Stack parentTypeIds
- )
- {
- string parentName = oValueName.BuildName(this.FeatureNullable, false, true);
- ResolvedType resolvedGenericType = this.ResolveType(v.GenericType, parentName + "Value", oValueName, parentTypeIds);
- List properties = new();
- List methods = new();
-
- var constructorProps = new List<(string Name, TypeName Type, bool SetValue)>
- {
- ("value", resolvedGenericType.Name, false)
- };
- var constructor = this.GenerateConstructor(oValueName, AccessType.Public, constructorProps)
- .WithInitializer(SyntaxFactory.ConstructorInitializer(
- SyntaxKind.BaseConstructorInitializer,
- SyntaxFactory.ArgumentList(
- SyntaxFactory.SingletonSeparatedList(
- SyntaxFactory.Argument(
- SyntaxFactory.IdentifierName("value")
- )
- )
- )
- ));
- List subTypes = new()
- {
- constructor
- };
- if (resolvedGenericType.GeneratedSyntax?.Any() == true)
- {
- subTypes.AddRange(resolvedGenericType.GeneratedSyntax);
- }
- return this.GenerateClass(
- name: oValueName,
- properties: properties,
- optionalProperties: null,
- customProperties: null,
- methods: methods,
- attributes: null,
- emptyConstructorAccess: AccessType.Public,
- subTypes: subTypes,
- implementTypes: new List
- {
- new OptionalValueTypeName(resolvedGenericType.Name)
- }
- );
- }
-
- internal ClassDeclarationSyntax GenerateList(
- TypeName listName,
- ListSourceCodeType type,
- Stack parentTypeIds
- )
- {
- string parentName = listName.BuildName(this.FeatureNullable, false, true);
- ResolvedType elementType = this.ResolveType(type.ElementType, parentName + "Element", listName, parentTypeIds);
-
- List properties = new();
- List methods = new();
- return this.GenerateClass(
- name: listName,
- properties: properties,
- optionalProperties: null,
- customProperties: null,
- methods: methods,
- attributes: null,
- emptyConstructorAccess: AccessType.Public,
- subTypes: elementType.GeneratedSyntax?.ToList(),
- implementTypes: new List
- {
- new ListTypeName(elementType.Name)
- }
- );
- }
-
- internal ClassDeclarationSyntax GenerateDictionary(
- TypeName dictName,
- DictionarySourceCodeType type,
- Stack parentTypeIds
- )
- {
- string parentName = dictName.BuildName(this.FeatureNullable, false, true);
- ResolvedType keyType = this.ResolveType(type.KeyType, parentName + "Key", dictName, parentTypeIds);
- ResolvedType valueType = this.ResolveType(type.ValueType, parentName + "Value", dictName, parentTypeIds);
-
- List properties = new();
- List methods = new();
- List subTypes = new();
- if (keyType.GeneratedSyntax != null)
- {
- subTypes.AddRange(keyType.GeneratedSyntax);
- }
- if (valueType.GeneratedSyntax != null)
- {
- subTypes.AddRange(valueType.GeneratedSyntax);
- }
- return this.GenerateClass(
- name: dictName,
- properties: properties,
- optionalProperties: null,
- customProperties: null,
- methods: methods,
- attributes: null,
- emptyConstructorAccess: AccessType.Public,
- subTypes: subTypes,
- implementTypes: new List
- {
- new DictionaryTypeName(keyType.Name, valueType.Name)
- }
- );
- }
-
-
- internal (ClassDeclarationSyntax? Class, EnumDeclarationSyntax Type) GenerateVariant(
- TypeName variantTypeName,
- VariantSourceCodeType variant,
- TypeName? parentType,
- Stack parentTypeIds
- )
- {
- (ResolvedName Name, ResolvedType? Type, bool OptionalOverridden) ResolveOption(VariantSourceCodeType.VariantOption option, int i)
- {
- ResolvedType? resolvedType;
- if (option.Type == null)
- {
- resolvedType = null;
- }
- else
- {
- string nameContext = option.Type.IsPredefinedType
- ? option.Tag.Name
- : option.Tag.Name + "Info"; // If need to generate sub type, add suffix to avoid name collision
- resolvedType = this.ResolveType(option.Type, nameContext, variantTypeName, parentTypeIds);
- }
- return (option.Tag, resolvedType, option.OptionalOverridden);
- }
-
-
- List<(ResolvedName Name, ResolvedType? Type, bool OptionalOverridden)> resolvedOptions = variant.Options
- .Select(ResolveOption)
- .ToList();
-
- List<(ResolvedName Name, TypeName? Type)> enumOptions = resolvedOptions
- .Select(o => (o.Name, o.Type?.Name))
- .ToList();
-
- if (enumOptions.All(o => o.Type == null))
- {
- // If there are no types, just create an enum value
-
- TypeName enumTypeName = this.BuildType(variantTypeName.BuildName(this.FeatureNullable, false), parentType, isDefaultNullable: false);
- EnumDeclarationSyntax enumSyntax = this.GenerateEnum(enumTypeName, enumOptions);
- return (null, enumSyntax);
- }
- else
- {
- TypeName enumTypeName = this.BuildType(variantTypeName.BuildName(this.FeatureNullable, false) + "Tag", parentType, isDefaultNullable: false);
-
- // TODO auto change the property values of all class types if it matches the name
- bool containsClashingTag = variantTypeName.BuildName(this.FeatureNullable, false) == "Tag"
- || variant.Options.Any(o => o.Tag.Name == "Tag");
- string tagName = containsClashingTag ? "Tag_" : "Tag";
-
- bool containsClashingValue = variantTypeName.BuildName(this.FeatureNullable, false) == "Value"
- || variant.Options.Any(o => o.Tag.Name == "Value");
- string valueName = containsClashingValue ? "Value_" : "Value";
-
- List properties = new()
- {
- new ClassProperty(
- tagName,
- enumTypeName,
- access: AccessType.Public,
- hasSetter: true,
- AttributeInfo.FromType()
- ),
- new ClassProperty(
- valueName,
- SimpleTypeName.FromType