diff --git a/src/Reown.Core.Crypto/Runtime/Crypto.cs b/src/Reown.Core.Crypto/Runtime/Crypto.cs index c84eee4..24a8875 100644 --- a/src/Reown.Core.Crypto/Runtime/Crypto.cs +++ b/src/Reown.Core.Crypto/Runtime/Crypto.cs @@ -119,10 +119,12 @@ public string Context /// The hash of the given input as a hex string public string HashKey(string key) { - using (var sha256 = SHA256.Create()) - { - return sha256.ComputeHash(key.HexToByteArray()).ToHex(); - } +#if NET7_0_OR_GREATER + return SHA256.HashData(key.HexToByteArray()).ToHex(); +#else + using var sha256 = SHA256.Create(); + return sha256.ComputeHash(key.HexToByteArray()).ToHex(); +#endif } public void Dispose() diff --git a/src/Reown.Sign/Runtime/Engine.cs b/src/Reown.Sign/Runtime/Engine.cs index 235d660..201ee06 100644 --- a/src/Reown.Sign/Runtime/Engine.cs +++ b/src/Reown.Sign/Runtime/Engine.cs @@ -929,9 +929,7 @@ public async Task Authenticate(AuthParams authParams) { "wc_sessionAuthenticate" }); - - ReownLogger.Log($"Generated new pairing: {pairingData.Uri}"); - + var publicKey = await Client.CoreClient.Crypto.GenerateKeyPair(); var responseTopic = Client.CoreClient.Crypto.HashKey(publicKey); @@ -946,22 +944,16 @@ await Task.WhenAll( ); await Client.CoreClient.Relayer.Subscribe(responseTopic); - - ReownLogger.Log($"Subscribed to relayer: {responseTopic}. Pairing topic: {pairingData.Topic}"); - + if (authParams.Methods is { Length: > 0 }) { - ReownLogger.Log("Methods provided, creating recap"); - var chainId = authParams.Chains[0]; var @namespace = Core.Utils.ExtractChainNamespace(chainId); - var recapStr = ReCapUtils.CreateEncodedRecap(@namespace, "request", authParams.Methods); - - ReownLogger.Log($"Created recap: {recapStr}"); - + var recapStr = ReCap.CreateEncodedRecap(@namespace, "request", authParams.Methods); + authParams.Resources ??= new List(); - if (!ReCapUtils.TryGetRecapFromResources(authParams.Resources, out var existingRecap)) + if (!ReCap.TryGetRecapFromResources(authParams.Resources, out var existingRecap)) { authParams.Resources.Add(recapStr); } @@ -971,7 +963,7 @@ await Task.WhenAll( // using .RemoveAt to remove the last element given we already checked it's a recap and will replace it authParams.Resources.RemoveAt(authParams.Resources.Count - 1); - var mergedRecap = ReCapUtils.MergeEncodedRecaps(recapStr, existingRecap); + var mergedRecap = ReCap.MergeEncodedRecaps(recapStr, existingRecap); authParams.Resources.Add(mergedRecap); } } @@ -1146,7 +1138,6 @@ public async Task RejectSessionAuthenticate(RejectParams rejectParams) if (pendingRequest == null) throw new InvalidOperationException($"No pending request found for the id {rejectParams.Id}"); - // var receiverPublicKey = pendingRequest.Requester.PublicKey; var senderPublicKey = await Client.CoreClient.Crypto.GenerateKeyPair(); var responseTopic = Client.CoreClient.Crypto.HashKey(senderPublicKey); @@ -1197,10 +1188,10 @@ public async Task ApproveSessionAuthenticate(long requestId, Caca var address = CacaoUtils.ExtractDidAddress(cacao.Payload.Iss); - if (ReCapUtils.TryGetRecapFromResources(cacao.Payload.Resources, out var recap)) + if (ReCap.TryGetRecapFromResources(cacao.Payload.Resources, out var encodedRecap)) { - var methodsFromRecap = ReCapUtils.GetActionsFromRecap(recap); - var chainsFromRecap = ReCapUtils.GetChainsFromRecap(recap); + var methodsFromRecap = ReCap.GetActionsFromEncodedRecap(encodedRecap); + var chainsFromRecap = ReCap.GetChainsFromEncodedRecap(encodedRecap); approvedMethods.UnionWith(methodsFromRecap); approvedChains.UnionWith(chainsFromRecap); @@ -1212,9 +1203,6 @@ public async Task ApproveSessionAuthenticate(long requestId, Caca } } - ReownLogger.Log($"Approved methods: {string.Join(", ", approvedMethods)}"); - ReownLogger.Log($"Approved accounts: {string.Join(", ", approvedAccounts)}"); - var sessionTopic = await Client.CoreClient.Crypto.GenerateSharedKey(senderPublicKey, receiverPublicKey); SessionStruct session = default; @@ -1244,15 +1232,12 @@ public async Task ApproveSessionAuthenticate(long requestId, Caca PairingTopic = pendingRequest.PairingTopic }; - ReownLogger.Log($"Approving session with topic {sessionTopic}"); await Client.CoreClient.Relayer.Subscribe(sessionTopic); await Client.Session.Set(sessionTopic, session); - ReownLogger.Log($"Updating session metadata for pairing topic {pendingRequest.PairingTopic}"); await Client.CoreClient.Pairing.UpdateMetadata(pendingRequest.PairingTopic, session.Peer.Metadata); } - ReownLogger.Log("Sending response to session authenticate request"); await MessageHandler.SendResult(requestId, responseTopic, new AuthenticateResponse { Cacaos = auths, diff --git a/src/Reown.Sign/Runtime/Internals/EngineHandler.cs b/src/Reown.Sign/Runtime/Internals/EngineHandler.cs index 13ca7ba..5ee6258 100644 --- a/src/Reown.Sign/Runtime/Internals/EngineHandler.cs +++ b/src/Reown.Sign/Runtime/Internals/EngineHandler.cs @@ -391,10 +391,10 @@ async Task IEnginePrivate.OnAuthenticateResponse(string topic, JsonRpcResponse(); var parsedAddress = CacaoUtils.ExtractDidAddress(cacao.Payload.Iss); - if (ReCapUtils.TryGetRecapFromResources(cacao.Payload.Resources, out var recapStr)) + if (ReCap.TryGetRecapFromResources(cacao.Payload.Resources, out var recapStr)) { - var methodsFromRecap = ReCapUtils.GetActionsFromRecap(recapStr); - var chainsFromRecap = ReCapUtils.GetChainsFromRecap(recapStr); + var methodsFromRecap = ReCap.GetActionsFromEncodedRecap(recapStr); + var chainsFromRecap = ReCap.GetChainsFromEncodedRecap(recapStr); approvedMethods.UnionWith(methodsFromRecap); approvedChains.UnionWith(chainsFromRecap); } @@ -431,12 +431,6 @@ async Task IEnginePrivate.OnAuthenticateResponse(string topic, JsonRpcResponse? Resources; + public List? Resources { get; set; } [JsonProperty("pairingTopic", NullValueHandling = NullValueHandling.Ignore)] - public string? PairingTopic; + public string? PairingTopic { get; set; } [JsonProperty("methods", NullValueHandling = NullValueHandling.Ignore)] - public string[]? Methods; + public string[]? Methods { get; set; } [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] - public string? Version; + public string? Version { get; set; } public AuthPayloadParams() @@ -85,17 +85,17 @@ public void Populate(ICollection supportedCains, ICollection sup var statement = Statement ?? string.Empty; - if (!ReCapUtils.TryGetDecodedRecapFromResources(Resources, out var recap)) + if (!ReCap.TryGetDecodedRecapFromResources(Resources, out var recap)) throw new InvalidOperationException("Recap not found in resources"); - var actionsFromRecap = ReCapUtils.GetActionsFromRecap(recap); + var actionsFromRecap = recap.GetActions(); var approvedActions = actionsFromRecap.Intersect(supportedMethods).ToArray(); if (approvedActions.Length == 0) throw new InvalidOperationException($"Supported methods don't satisfy the requested: {string.Join(", ", actionsFromRecap)}. " + $"Supported methods: {string.Join(", ", supportedMethods)}"); var updatedResources = Resources ?? new List(); - var formattedActions = ReCapUtils.AssignAbilityToActions("request", approvedActions, new Dictionary + var formattedActions = ReCap.AssignAbilityToActions("request", approvedActions, new Dictionary { { "chains", approvedChains } }); @@ -103,10 +103,10 @@ public void Populate(ICollection supportedCains, ICollection sup recap.AddResources("eip115", formattedActions); updatedResources.RemoveAt(updatedResources.Count - 1); - updatedResources.Add(ReCapUtils.EncodeRecap(recap)); + updatedResources.Add(recap.Encode()); Chains = approvedChains; Methods = approvedActions; - Statement = ReCapUtils.FormatStatementFromRecap(recap, statement); + Statement = recap.FormatStatement(statement); Resources = updatedResources; } } diff --git a/src/Reown.Sign/Runtime/Models/Cacao/CacaoPayload.cs b/src/Reown.Sign/Runtime/Models/Cacao/CacaoPayload.cs index 1f5718d..bd70240 100644 --- a/src/Reown.Sign/Runtime/Models/Cacao/CacaoPayload.cs +++ b/src/Reown.Sign/Runtime/Models/Cacao/CacaoPayload.cs @@ -10,37 +10,37 @@ namespace Reown.Sign.Models.Cacao public class CacaoPayload { [JsonProperty("domain")] - public readonly string Domain; + public string Domain { get; } [JsonProperty("iss")] - public readonly string Iss; // did:pkh + public string Iss { get; } // did:pkh [JsonProperty("aud")] - public readonly string Aud; + public string Aud { get; } [JsonProperty("version")] - public readonly string Version; + public string Version { get; } [JsonProperty("nonce")] - public readonly string Nonce; + public string Nonce { get; } [JsonProperty("iat")] - public readonly string IssuedAt; + public string IssuedAt { get; } [JsonProperty("nbf")] - public readonly string? NotBefore; + public string? NotBefore { get; } [JsonProperty("exp")] - public readonly string? Expiration; + public string? Expiration { get; } [JsonProperty("statement", NullValueHandling = NullValueHandling.Ignore)] - public readonly string? Statement; + public string? Statement { get; } [JsonProperty("requestId", NullValueHandling = NullValueHandling.Ignore)] - public readonly string? RequestId; + public string? RequestId { get; } [JsonProperty("resources", NullValueHandling = NullValueHandling.Ignore)] - public readonly string[]? Resources; + public string[]? Resources { get; } public CacaoPayload( string domain, @@ -106,10 +106,10 @@ public string FormatMessage() ? $"Resources:\n{string.Join('\n', Resources.Select(resource => $"- {resource}"))}" : null; - if (ReCapUtils.TryGetRecapFromResources(Resources, out var recapStr)) + if (ReCap.TryGetRecapFromResources(Resources, out var recapStr)) { - var decoded = ReCapUtils.DecodeRecap(recapStr); - statement ??= ReCapUtils.FormatStatementFromRecap(decoded, statement); + var decoded = ReCap.Decode(recapStr); + statement ??= decoded.FormatStatement(statement); } var message = string.Join('\n', new[] diff --git a/src/Reown.Sign/Runtime/Utils/ReCapUtils.cs b/src/Reown.Sign/Runtime/Models/Cacao/ReCap.cs similarity index 82% rename from src/Reown.Sign/Runtime/Utils/ReCapUtils.cs rename to src/Reown.Sign/Runtime/Models/Cacao/ReCap.cs index 5947560..2a15bfd 100644 --- a/src/Reown.Sign/Runtime/Utils/ReCapUtils.cs +++ b/src/Reown.Sign/Runtime/Models/Cacao/ReCap.cs @@ -6,10 +6,111 @@ namespace Reown.Sign.Utils { + public class AttValue + { + [JsonExtensionData] + public Dictionary Properties { get; set; } + } + + [Serializable] public class ReCap { [JsonProperty("att")] - public Dictionary Att; + public Dictionary Att { get; private set; } + + [JsonConstructor] + public ReCap(Dictionary att) + { + Att = att; + } + + public ReCap(string resource, string ability, string[] actions, Dictionary limits = null) + { + Att = new Dictionary + { + { + resource, new AttValue + { + Properties = AssignAbilityToActions(ability, actions, limits) + .ToDictionary( + kvp => kvp.Key, + kvp => JToken.FromObject(kvp.Value) + ) + } + } + }; + } + + public string Encode() + { + Validate(this); + + var recapStr = JsonConvert.SerializeObject(this); + var recapBytes = System.Text.Encoding.UTF8.GetBytes(recapStr); + var recapBase64 = Convert.ToBase64String(recapBytes); + var recapPadded = recapBase64.Replace("=", string.Empty); + + return $"urn:recap:{recapPadded}"; + } + + public string FormatStatement(string customStatement = "") + { + Validate(this); + + const string statementBase = "I further authorize the stated URI to perform the following actions on my behalf: "; + + if (!string.IsNullOrWhiteSpace(customStatement) && customStatement.Contains(statementBase)) + return customStatement; + + var statementForRecap = new List(); + var currentCounter = 0; + foreach (var resource in Att.Keys) + { + var actions = Att[resource].Properties.Keys.Select(ability => new + { + Ability = ability.Split("/")[0], + Action = ability.Split("/")[1] + }); + + actions = actions.OrderBy(action => action.Action); + var uniqueAbilities = new Dictionary>(); + foreach (var action in actions) + { + if (!uniqueAbilities.ContainsKey(action.Ability)) + uniqueAbilities[action.Ability] = new List(); + + uniqueAbilities[action.Ability].Add(action.Action); + } + + var abilities = uniqueAbilities.Keys.Select(ability => + { + currentCounter++; + return $"({currentCounter}) '{ability}': '{string.Join("', '", uniqueAbilities[ability])}' for '{resource}'."; + }); + + statementForRecap.Add(string.Join(", ", abilities).Replace(".,", ".")); + } + + var recapStatement = string.Join(" ", statementForRecap); + recapStatement = $"{statementBase}{recapStatement}"; + + return !string.IsNullOrWhiteSpace(customStatement) ? $"{customStatement} {recapStatement}" : recapStatement; + } + + public string[] GetActions() + { + try + { + // Methods are only available for eip155 as per the current implementation + return Att["eip155"] is { } resources + ? resources.Properties.Keys.Select(ability => ability.Split("/")[1]).ToArray() + : Array.Empty(); + } + catch (Exception) + { + return Array.Empty(); + } + } public void AddResources(string resource, Dictionary[]> actions) { @@ -51,80 +152,44 @@ public void AddResources(string resource, Dictionary Properties { get; set; } - } - - public class ReCapUtils - { public static string CreateEncodedRecap(string resource, string ability, string[] actions, Dictionary limits = null) { - var recap = CreateRecap(resource, ability, actions, limits); - return EncodeRecap(recap); + var recap = new ReCap(resource, ability, actions, limits); + return recap.Encode(); } - public static ReCap CreateRecap(string resource, string ability, string[] actions, Dictionary limits = null) + public static ReCap Decode(string recapStr) { - return new ReCap - { - Att = new Dictionary - { - { - resource, new AttValue - { - Properties = AssignAbilityToActions(ability, actions, limits) - .ToDictionary( - kvp => kvp.Key, - kvp => JToken.FromObject(kvp.Value) - ) - } - } - } - }; - } - - - public static Dictionary[]> AssignAbilityToActions( - string ability, - IEnumerable actions, - Dictionary limits = null) - { - if (string.IsNullOrWhiteSpace(ability)) - throw new ArgumentException("Ability cannot be null or whitespace.", nameof(ability)); + var paddedRecap = recapStr.Replace("urn:recap:", string.Empty); - if (actions == null) - throw new ArgumentNullException(nameof(actions), "Actions list cannot be null."); + paddedRecap = paddedRecap.TrimEnd('='); + var paddingNeeded = (4 - paddedRecap.Length % 4) % 4; + paddedRecap = paddedRecap.PadRight(paddedRecap.Length + paddingNeeded, '='); - // Sort actions alphabetically - var sortedActions = actions.OrderBy(action => action, StringComparer.Ordinal); + var decodedRecap = Convert.FromBase64String(paddedRecap); + var decodedRecapStr = System.Text.Encoding.UTF8.GetString(decodedRecap); + var recap = JsonConvert.DeserializeObject(decodedRecapStr); - limits ??= new Dictionary(); + Validate(recap); - var abilitiesDictionary = new Dictionary[]>(); - foreach (var action in sortedActions) - { - var key = $"{ability}/{action}"; - abilitiesDictionary[key] = new[] - { - limits - }; - } + return recap; + } - return abilitiesDictionary; + public static string[] GetActionsFromEncodedRecap(string recapStr) + { + var decodedRecap = Decode(recapStr); + return decodedRecap.GetActions(); } public static bool TryGetDecodedRecapFromResources(IEnumerable resources, out ReCap recap) { var success = TryGetRecapFromResources(resources, out var recapStr); - recap = success ? DecodeRecap(recapStr) : null; + recap = success ? Decode(recapStr) : null; return success; } - + public static bool TryGetRecapFromResources(IEnumerable resources, out string recap) { if (resources == null) @@ -132,7 +197,7 @@ public static bool TryGetRecapFromResources(IEnumerable resources, out s recap = null; return false; } - + // Per ERC-5573, ReCap is always the last resource in the list recap = resources.LastOrDefault(); return IsReCap(recap); @@ -143,30 +208,9 @@ public static bool IsReCap(string resource) return !string.IsNullOrWhiteSpace(resource) && resource.StartsWith("urn:recap"); } - public static string[] GetActionsFromRecap(string recapStr) - { - var decodedRecap = DecodeRecap(recapStr); - return GetActionsFromRecap(decodedRecap); - } - - public static string[] GetActionsFromRecap(ReCap recap) - { - try - { - // Methods are only available for eip155 as per the current implementation - return recap.Att["eip155"] is { } resources - ? resources.Properties.Keys.Select(ability => ability.Split("/")[1]).ToArray() - : Array.Empty(); - } - catch (Exception) - { - return Array.Empty(); - } - } - - public static List GetChainsFromRecap(string recapStr) + public static List GetChainsFromEncodedRecap(string recapStr) { - var decodedRecap = DecodeRecap(recapStr); + var decodedRecap = Decode(recapStr); var recapChains = new List(); @@ -196,36 +240,7 @@ public static List GetChainsFromRecap(string recapStr) return recapChains.Distinct().ToList(); } - public static string EncodeRecap(ReCap recap) - { - ValidateRecap(recap); - - var recapStr = JsonConvert.SerializeObject(recap); - var recapBytes = System.Text.Encoding.UTF8.GetBytes(recapStr); - var recapBase64 = Convert.ToBase64String(recapBytes); - var recapPadded = recapBase64.Replace("=", string.Empty); - - return $"urn:recap:{recapPadded}"; - } - - public static ReCap DecodeRecap(string recapStr) - { - var paddedRecap = recapStr.Replace("urn:recap:", string.Empty); - - paddedRecap = paddedRecap.TrimEnd('='); - var paddingNeeded = (4 - paddedRecap.Length % 4) % 4; - paddedRecap = paddedRecap.PadRight(paddedRecap.Length + paddingNeeded, '='); - - var decodedRecap = Convert.FromBase64String(paddedRecap); - var decodedRecapStr = System.Text.Encoding.UTF8.GetString(decodedRecap); - var recap = JsonConvert.DeserializeObject(decodedRecapStr); - - ValidateRecap(recap); - - return recap; - } - - public static void ValidateRecap(ReCap recap) + public static void Validate(ReCap recap) { if (recap?.Att == null) throw new ArgumentException("No `att` property found"); @@ -248,32 +263,57 @@ public static void ValidateRecap(ReCap recap) if (limits == null || limits.Count == 0) throw new ArgumentException($"Value of ability '{key}' is an empty array; it must contain at least one limit object."); - foreach (var limitToken in limits) + foreach (var limitToken in limits.Where(limitToken => limitToken is not JObject)) { - if (limitToken is not JObject) - throw new ArgumentException($"Each limit for ability '{key}' must be an object. Invalid limit: {limitToken}"); + throw new ArgumentException($"Each limit for ability '{key}' must be an object. Invalid limit: {limitToken}"); } } } } + public static Dictionary[]> AssignAbilityToActions( + string ability, + IEnumerable actions, + Dictionary limits = null) + { + if (string.IsNullOrWhiteSpace(ability)) + throw new ArgumentException("Ability cannot be null or whitespace.", nameof(ability)); + + if (actions == null) + throw new ArgumentNullException(nameof(actions), "Actions list cannot be null."); + + // Sort actions alphabetically + var sortedActions = actions.OrderBy(action => action, StringComparer.Ordinal); + + limits ??= new Dictionary(); + + var abilitiesDictionary = new Dictionary[]>(); + foreach (var action in sortedActions) + { + var key = $"{ability}/{action}"; + abilitiesDictionary[key] = new[] + { + limits + }; + } + + return abilitiesDictionary; + } + public static string MergeEncodedRecaps(string recapStr1, string recapStr2) { - var decoded1 = DecodeRecap(recapStr1); - var decoded2 = DecodeRecap(recapStr2); + var decoded1 = Decode(recapStr1); + var decoded2 = Decode(recapStr2); var mergedRecap = MergeRecaps(decoded1, decoded2); - return EncodeRecap(mergedRecap); + return mergedRecap.Encode(); } public static ReCap MergeRecaps(ReCap recap1, ReCap recap2) { - ValidateRecap(recap1); - ValidateRecap(recap2); + Validate(recap1); + Validate(recap2); - var mergedRecap = new ReCap - { - Att = new Dictionary(StringComparer.Ordinal) - }; + var mergedRecap = new ReCap(new Dictionary(StringComparer.Ordinal)); var allKeys = recap1.Att.Keys.Union(recap2.Att.Keys, StringComparer.Ordinal).OrderBy(k => k); @@ -354,52 +394,8 @@ public static ReCap MergeRecaps(ReCap recap1, ReCap recap2) mergedRecap.Att[key] = mergedValue; } - ValidateRecap(mergedRecap); + Validate(mergedRecap); return mergedRecap; } - - public static string FormatStatementFromRecap(ReCap recap, string statement = "") - { - ValidateRecap(recap); - - const string statementBase = "I further authorize the stated URI to perform the following actions on my behalf: "; - - if (!string.IsNullOrWhiteSpace(statement) && statement.Contains(statementBase)) - return statement; - - var statementForRecap = new List(); - var currentCounter = 0; - foreach (var resource in recap.Att.Keys) - { - var actions = recap.Att[resource].Properties.Keys.Select(ability => new - { - Ability = ability.Split("/")[0], - Action = ability.Split("/")[1] - }); - - actions = actions.OrderBy(action => action.Action); - var uniqueAbilities = new Dictionary>(); - foreach (var action in actions) - { - if (!uniqueAbilities.ContainsKey(action.Ability)) - uniqueAbilities[action.Ability] = new List(); - - uniqueAbilities[action.Ability].Add(action.Action); - } - - var abilities = uniqueAbilities.Keys.Select(ability => - { - currentCounter++; - return $"({currentCounter}) '{ability}': '{string.Join("', '", uniqueAbilities[ability])}' for '{resource}'."; - }); - - statementForRecap.Add(string.Join(", ", abilities).Replace(".,", ".")); - } - - var recapStatement = string.Join(" ", statementForRecap); - recapStatement = $"{statementBase}{recapStatement}"; - - return !string.IsNullOrWhiteSpace(statement) ? $"{statement} {recapStatement}" : recapStatement; - } } } \ No newline at end of file diff --git a/src/Reown.Sign/Runtime/Utils/ReCapUtils.cs.meta b/src/Reown.Sign/Runtime/Models/Cacao/ReCap.cs.meta similarity index 100% rename from src/Reown.Sign/Runtime/Utils/ReCapUtils.cs.meta rename to src/Reown.Sign/Runtime/Models/Cacao/ReCap.cs.meta diff --git a/test/Reown.Sign.Test/RecapTests.cs b/test/Reown.Sign.Test/RecapTests.cs index 29968d2..5be7e53 100644 --- a/test/Reown.Sign.Test/RecapTests.cs +++ b/test/Reown.Sign.Test/RecapTests.cs @@ -43,7 +43,7 @@ public void IsReCap_WithValidResource_ReturnsTrue() { const string resource = "urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9leGFtcGxlLmNvbS9waWN0dXJlcy8iOnsiY3J1ZC9kZWxldGUiOlt7fV0sImNydWQvdXBkYXRlIjpbe31dLCJvdGhlci9hY3Rpb24iOlt7fV19LCJtYWlsdG86dXNlcm5hbWVAZXhhbXBsZS5jb20iOnsibXNnL3JlY2VpdmUiOlt7Im1heF9jb3VudCI6NSwidGVtcGxhdGVzIjpbIm5ld3NsZXR0ZXIiLCJtYXJrZXRpbmciXX1dLCJtc2cvc2VuZCI6W3sidG8iOiJzb21lb25lQGVtYWlsLmNvbSJ9LHsidG8iOiJqb2VAZW1haWwuY29tIn1dfX0sInByZiI6WyJ6ZGo3V2o2Rk5TNHJVVWJzaUp2amp4Y3NOcVpkRENTaVlSOHNLUVhmb1BmcFNadUF3Il19\n"; - Assert.True(ReCapUtils.IsReCap(resource)); + Assert.True(ReCap.IsReCap(resource)); } [Theory] @@ -53,7 +53,7 @@ public void IsReCap_WithValidResource_ReturnsTrue() [InlineData("")] public void IsReCap_WithInvalidResource_ReturnsFalse(string resource) { - Assert.False(ReCapUtils.IsReCap(resource)); + Assert.False(ReCap.IsReCap(resource)); } [Fact] [Trait("Category", "unit")] @@ -61,7 +61,7 @@ public void CreateEncodedRecap_EmptyLimits_ReturnsExpectedResult() { var (resource, ability, actions, _) = GetRecapProperties(); - var encodedRecap = ReCapUtils.CreateEncodedRecap(resource, ability, actions, null); + var encodedRecap = ReCap.CreateEncodedRecap(resource, ability, actions, null); const string expectedEncodedRecap = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7fV0sInJlcXVlc3QvcGVyc29uYWxfc2lnbiI6W3t9XX19fQ"; @@ -73,7 +73,7 @@ public void AssignAbilityToActions_WithValidArgs_ReturnsExpectedResult() { var (_, ability, actions, limits) = GetRecapProperties(); - Dictionary[]> result = ReCapUtils.AssignAbilityToActions(ability, actions, limits); + Dictionary[]> result = ReCap.AssignAbilityToActions(ability, actions, limits); var expected = new Dictionary[]> { @@ -117,7 +117,7 @@ public void CreateRecap_WithValidArgs_ReturnsExpectedResult() { var (resource, ability, actions, limits) = GetRecapProperties(); - var result = ReCapUtils.CreateRecap(resource, ability, actions, limits); + var result = new ReCap(resource, ability, actions, limits); Assert.NotNull(result); @@ -137,14 +137,14 @@ public void CreateEncodedRecap_WithValidArgs_ReturnsExpectedResult() var (resource, ability, actions, limits) = GetRecapProperties(); var chains = limits["chains"] as string[]; - var encodedRecap = ReCapUtils.CreateEncodedRecap(resource, ability, actions, limits); + var encodedRecap = ReCap.CreateEncodedRecap(resource, ability, actions, limits); const string expectedEncodedRecap = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dLCJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dfX19"; Assert.Equal(expectedEncodedRecap, encodedRecap); - var decodedRecap = ReCapUtils.DecodeRecap(encodedRecap); + var decodedRecap = ReCap.Decode(encodedRecap); Assert.NotNull(decodedRecap); @@ -214,8 +214,8 @@ public void ValidateRecap_WithValidRecap_DoesNotThrow() const string validRecap = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dLCJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dfX19"; - var recap = ReCapUtils.DecodeRecap(validRecap); - ReCapUtils.ValidateRecap(recap); // Should not throw + var recap = ReCap.Decode(validRecap); + ReCap.Validate(recap); // Should not throw } [Fact] [Trait("Category", "unit")] @@ -223,7 +223,7 @@ public void ValidateRecap_NullRecap_ThrowsArgumentException() { ReCap recap = null; - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Equal("No `att` property found", exception.Message); } @@ -231,12 +231,9 @@ public void ValidateRecap_NullRecap_ThrowsArgumentException() [Fact] [Trait("Category", "unit")] public void ValidateRecap_NullAtt_ThrowsArgumentException() { - var recap = new ReCap - { - Att = null - }; + var recap = new ReCap(null); - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Equal("No `att` property found", exception.Message); } @@ -244,9 +241,7 @@ public void ValidateRecap_NullAtt_ThrowsArgumentException() [Fact] [Trait("Category", "unit")] public void ValidateRecap_NullProperties_ThrowsArgumentException() { - var recap = new ReCap - { - Att = new Dictionary + var recap = new ReCap(new Dictionary { { "resource1", new AttValue @@ -255,9 +250,9 @@ public void ValidateRecap_NullProperties_ThrowsArgumentException() } } } - }; + ); - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Contains("Resource object is empty or null", exception.Message); } @@ -265,9 +260,7 @@ public void ValidateRecap_NullProperties_ThrowsArgumentException() [Fact] [Trait("Category", "unit")] public void ValidateRecap_EmptyProperties_ThrowsArgumentException() { - var recap = new ReCap - { - Att = new Dictionary + var recap = new ReCap(new Dictionary { { "resource1", new AttValue @@ -276,9 +269,9 @@ public void ValidateRecap_EmptyProperties_ThrowsArgumentException() } } } - }; + ); - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Contains("Resource object is empty or null", exception.Message); } @@ -286,9 +279,7 @@ public void ValidateRecap_EmptyProperties_ThrowsArgumentException() [Fact] [Trait("Category", "unit")] public void ValidateRecap_AbilityNotJArray_ThrowsArgumentException() { - var recap = new ReCap - { - Att = new Dictionary + var recap = new ReCap(new Dictionary { { "resource1", new AttValue @@ -300,9 +291,9 @@ public void ValidateRecap_AbilityNotJArray_ThrowsArgumentException() } } } - }; + ); - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Contains("Ability 'ability1' must be an array.", exception.Message); } @@ -310,9 +301,7 @@ public void ValidateRecap_AbilityNotJArray_ThrowsArgumentException() [Fact] [Trait("Category", "unit")] public void ValidateRecap_EmptyLimitsArray_ThrowsArgumentException() { - var recap = new ReCap - { - Att = new Dictionary + var recap = new ReCap(new Dictionary { { "resource1", new AttValue @@ -324,9 +313,9 @@ public void ValidateRecap_EmptyLimitsArray_ThrowsArgumentException() } } } - }; + ); - var exception = Assert.Throws(() => ReCapUtils.ValidateRecap(recap)); + var exception = Assert.Throws(() => ReCap.Validate(recap)); Assert.Contains("Value of ability 'ability1' is an empty array; it must contain at least one limit object.", exception.Message); } @@ -337,7 +326,7 @@ public void GetMethodsFromRecap_WithValidRecap_ReturnsExpectedMethods() const string recapStr = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dLCJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dfX19"; - var methods = ReCapUtils.GetActionsFromRecap(recapStr); + var methods = ReCap.GetActionsFromEncodedRecap(recapStr); var expectedMethods = new[] { @@ -354,7 +343,7 @@ public void GetChainsFromRecap_WithValidRecap_ReturnsExpectedChains() const string recapStr = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dLCJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dfX19"; - var chains = ReCapUtils.GetChainsFromRecap(recapStr); + var chains = ReCap.GetChainsFromEncodedRecap(recapStr); var expectedChains = new[] { @@ -372,9 +361,9 @@ public void FormatStatementFromRecap_WithValidRecap_ReturnsExpectedStatement() const string recapStr = "urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dLCJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOlt7ImNoYWlucyI6WyJlaXAxNTU6MSIsImVpcDE1NToyIiwiZWlwMTU1OjMiXX1dfX19"; - var recap = ReCapUtils.DecodeRecap(recapStr); + var recap = ReCap.Decode(recapStr); - var formattedStatement = ReCapUtils.FormatStatementFromRecap(recap); + var formattedStatement = recap.FormatStatement(); const string expectedStatement = "I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_signTypedData_v4', 'personal_sign' for 'eip155'."; @@ -405,7 +394,7 @@ public void MergeRecaps_WithOverlappingRecaps_ReturnsMergedRecap() { "chains", chains1 } }; - var recap1 = ReCapUtils.CreateRecap(resource, ability, actions1, limits1); + var recap1 = new ReCap(resource, ability, actions1, limits1); var actions2 = new[] { @@ -426,9 +415,9 @@ public void MergeRecaps_WithOverlappingRecaps_ReturnsMergedRecap() { "chains", chains2 } }; - var recap2 = ReCapUtils.CreateRecap(resource, ability, actions2, limits2); + var recap2 = new ReCap(resource, ability, actions2, limits2); - var mergedRecap = ReCapUtils.MergeRecaps(recap1, recap2); + var mergedRecap = ReCap.MergeRecaps(recap1, recap2); var recapJson = JsonConvert.SerializeObject(mergedRecap); @@ -480,7 +469,7 @@ public void MergeRecaps_WithEmptyLimits_ReturnsMergedRecap() { "chains", chains1 } }; - var recap1 = ReCapUtils.CreateRecap(resource, ability, actions1, limits1); + var recap1 = new ReCap(resource, ability, actions1, limits1); var actions2 = new[] { @@ -491,9 +480,9 @@ public void MergeRecaps_WithEmptyLimits_ReturnsMergedRecap() Dictionary limits2 = null; - var recap2 = ReCapUtils.CreateRecap(resource, ability, actions2, limits2); + var recap2 = new ReCap(resource, ability, actions2, limits2); - var mergedRecap = ReCapUtils.MergeRecaps(recap1, recap2); + var mergedRecap = ReCap.MergeRecaps(recap1, recap2); var recapJson = JsonConvert.SerializeObject(mergedRecap); _testOutputHelper.WriteLine(recapJson); @@ -522,11 +511,11 @@ public void MergeEncodedRecaps_WithValidRecaps_ReturnsMergedRecap() const string encodedRecap1 = "urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9leGFtcGxlMS5jb20iOnsiY3J1ZC9yZWFkIjpbe31dfX19"; const string encodedRecap2 = "urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9leGFtcGxlMS5jb20iOnsiY3J1ZC91cGRhdGUiOlt7Im1heF90aW1lcyI6MX1dfSwiaHR0cHM6Ly9leGFtcGxlMi5jb20iOnsiY3J1ZC9kZWxldGUiOlt7fV19fX0=="; - var mergedRecap = ReCapUtils.MergeEncodedRecaps(encodedRecap1, encodedRecap2); + var mergedRecap = ReCap.MergeEncodedRecaps(encodedRecap1, encodedRecap2); const string expectedMergedRecap = "urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9leGFtcGxlMS5jb20iOnsiY3J1ZC9yZWFkIjpbe31dLCJjcnVkL3VwZGF0ZSI6W3sibWF4X3RpbWVzIjoxfV19LCJodHRwczovL2V4YW1wbGUyLmNvbSI6eyJjcnVkL2RlbGV0ZSI6W3t9XX19fQ"; Assert.Equal(expectedMergedRecap, mergedRecap); - _ = ReCapUtils.DecodeRecap(mergedRecap); + _ = ReCap.Decode(mergedRecap); } } \ No newline at end of file